railspress-engine 0.1.2 → 1.2.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 (140) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +20 -0
  3. data/README.md +195 -25
  4. data/app/assets/javascripts/railspress/admin.js +39 -0
  5. data/app/assets/javascripts/railspress/markdown_mode.js +343 -0
  6. data/app/assets/stylesheets/application.css +0 -0
  7. data/app/assets/stylesheets/railspress/admin/badges.css +70 -0
  8. data/app/assets/stylesheets/railspress/admin/base.css +25 -0
  9. data/app/assets/stylesheets/railspress/admin/buttons.css +140 -0
  10. data/app/assets/stylesheets/railspress/admin/cards.css +52 -0
  11. data/app/assets/stylesheets/railspress/admin/components/exports.css +55 -0
  12. data/app/assets/stylesheets/railspress/admin/components/focal_point.css +801 -0
  13. data/app/assets/stylesheets/railspress/admin/components/imports.css +144 -0
  14. data/app/assets/stylesheets/railspress/admin/components/lexxy.css +156 -0
  15. data/app/assets/stylesheets/railspress/admin/filters.css +73 -0
  16. data/app/assets/stylesheets/railspress/admin/flash.css +26 -0
  17. data/app/assets/stylesheets/railspress/admin/forms.css +459 -0
  18. data/app/assets/stylesheets/railspress/admin/layout.css +256 -0
  19. data/app/assets/stylesheets/railspress/admin/lists.css +24 -0
  20. data/app/assets/stylesheets/railspress/admin/page.css +111 -0
  21. data/app/assets/stylesheets/railspress/admin/responsive.css +174 -0
  22. data/app/assets/stylesheets/railspress/admin/stats.css +43 -0
  23. data/app/assets/stylesheets/railspress/admin/tables.css +163 -0
  24. data/app/assets/stylesheets/railspress/admin/utilities.css +202 -0
  25. data/app/assets/stylesheets/railspress/admin/variables.css +58 -0
  26. data/app/assets/stylesheets/railspress/application.css +44 -13
  27. data/app/controllers/railspress/admin/base_controller.rb +6 -3
  28. data/app/controllers/railspress/admin/categories_controller.rb +1 -1
  29. data/app/controllers/railspress/admin/cms_transfers_controller.rb +49 -0
  30. data/app/controllers/railspress/admin/content_element_versions_controller.rb +12 -0
  31. data/app/controllers/railspress/admin/content_elements_controller.rb +143 -0
  32. data/app/controllers/railspress/admin/content_groups_controller.rb +69 -0
  33. data/app/controllers/railspress/admin/dashboard_controller.rb +6 -0
  34. data/app/controllers/railspress/admin/entities_controller.rb +157 -0
  35. data/app/controllers/railspress/admin/exports_controller.rb +55 -0
  36. data/app/controllers/railspress/admin/focal_points_controller.rb +100 -0
  37. data/app/controllers/railspress/admin/imports_controller.rb +63 -0
  38. data/app/controllers/railspress/admin/posts_controller.rb +58 -4
  39. data/app/controllers/railspress/admin/prototypes_controller.rb +30 -0
  40. data/app/controllers/railspress/admin/tags_controller.rb +1 -1
  41. data/app/controllers/railspress/application_controller.rb +1 -0
  42. data/app/helpers/railspress/admin_helper.rb +733 -0
  43. data/app/helpers/railspress/application_helper.rb +23 -0
  44. data/app/helpers/railspress/cms_helper.rb +319 -0
  45. data/app/javascript/railspress/controllers/cms_inline_editor_controller.js +147 -0
  46. data/app/javascript/railspress/controllers/content_element_form_controller.js +15 -0
  47. data/app/javascript/railspress/controllers/crop_controller.js +224 -0
  48. data/app/javascript/railspress/controllers/dropzone_controller.js +261 -0
  49. data/app/javascript/railspress/controllers/focal_point_controller.js +124 -0
  50. data/app/javascript/railspress/controllers/image_section_controller.js +94 -0
  51. data/app/javascript/railspress/controllers/index.js +37 -0
  52. data/app/javascript/railspress/index.js +62 -0
  53. data/app/jobs/railspress/export_posts_job.rb +16 -0
  54. data/app/jobs/railspress/import_posts_job.rb +44 -0
  55. data/app/models/concerns/railspress/has_focal_point.rb +242 -0
  56. data/app/models/concerns/railspress/soft_deletable.rb +23 -0
  57. data/app/models/concerns/railspress/taggable.rb +23 -0
  58. data/app/models/railspress/content_element.rb +103 -0
  59. data/app/models/railspress/content_element_version.rb +32 -0
  60. data/app/models/railspress/content_group.rb +39 -0
  61. data/app/models/railspress/export.rb +67 -0
  62. data/app/models/railspress/focal_point.rb +70 -0
  63. data/app/models/railspress/import.rb +65 -0
  64. data/app/models/railspress/post.rb +102 -15
  65. data/app/models/railspress/post_export_processor.rb +162 -0
  66. data/app/models/railspress/post_import_processor.rb +382 -0
  67. data/app/models/railspress/tag.rb +10 -3
  68. data/app/models/railspress/tagging.rb +11 -0
  69. data/app/services/railspress/content_export_service.rb +122 -0
  70. data/app/services/railspress/content_import_service.rb +228 -0
  71. data/app/views/action_text/attachables/_remote_image.html.erb +8 -0
  72. data/app/views/active_storage/blobs/_blob.html.erb +1 -1
  73. data/app/views/layouts/railspress/admin.html.erb +3 -1
  74. data/app/views/railspress/admin/categories/index.html.erb +11 -15
  75. data/app/views/railspress/admin/cms_transfers/show.html.erb +167 -0
  76. data/app/views/railspress/admin/content_element_versions/show.html.erb +42 -0
  77. data/app/views/railspress/admin/content_elements/_form.html.erb +71 -0
  78. data/app/views/railspress/admin/content_elements/_inline_form.html.erb +32 -0
  79. data/app/views/railspress/admin/content_elements/_inline_form_frame.html.erb +6 -0
  80. data/app/views/railspress/admin/content_elements/edit.html.erb +6 -0
  81. data/app/views/railspress/admin/content_elements/index.html.erb +74 -0
  82. data/app/views/railspress/admin/content_elements/new.html.erb +6 -0
  83. data/app/views/railspress/admin/content_elements/show.html.erb +124 -0
  84. data/app/views/railspress/admin/content_groups/_form.html.erb +9 -0
  85. data/app/views/railspress/admin/content_groups/edit.html.erb +6 -0
  86. data/app/views/railspress/admin/content_groups/index.html.erb +42 -0
  87. data/app/views/railspress/admin/content_groups/new.html.erb +6 -0
  88. data/app/views/railspress/admin/content_groups/show.html.erb +92 -0
  89. data/app/views/railspress/admin/dashboard/index.html.erb +36 -1
  90. data/app/views/railspress/admin/entities/_form.html.erb +53 -0
  91. data/app/views/railspress/admin/entities/edit.html.erb +4 -0
  92. data/app/views/railspress/admin/entities/index.html.erb +74 -0
  93. data/app/views/railspress/admin/entities/new.html.erb +4 -0
  94. data/app/views/railspress/admin/entities/show.html.erb +117 -0
  95. data/app/views/railspress/admin/exports/show.html.erb +62 -0
  96. data/app/views/railspress/admin/imports/_instructions.html.erb +56 -0
  97. data/app/views/railspress/admin/imports/show.html.erb +137 -0
  98. data/app/views/railspress/admin/posts/_form.html.erb +102 -28
  99. data/app/views/railspress/admin/posts/_post_row.html.erb +40 -0
  100. data/app/views/railspress/admin/posts/index.html.erb +47 -36
  101. data/app/views/railspress/admin/posts/show.html.erb +55 -19
  102. data/app/views/railspress/admin/prototypes/image_section.html.erb +42 -0
  103. data/app/views/railspress/admin/shared/_dropzone.html.erb +84 -0
  104. data/app/views/railspress/admin/shared/_focal_point_editor.html.erb +102 -0
  105. data/app/views/railspress/admin/shared/_image_section.html.erb +159 -0
  106. data/app/views/railspress/admin/shared/_image_section_compact.html.erb +90 -0
  107. data/app/views/railspress/admin/shared/_image_section_editor.html.erb +171 -0
  108. data/app/views/railspress/admin/shared/_image_section_v2.html.erb +205 -0
  109. data/app/views/railspress/admin/shared/_sidebar.html.erb +73 -5
  110. data/app/views/railspress/admin/tags/index.html.erb +12 -16
  111. data/config/brakeman.ignore +18 -0
  112. data/config/importmap.rb +23 -0
  113. data/config/routes.rb +62 -1
  114. data/db/migrate/20241218000004_create_railspress_post_tags.rb +1 -1
  115. data/db/migrate/20241218000005_create_railspress_imports.rb +21 -0
  116. data/db/migrate/20241218000006_create_railspress_exports.rb +20 -0
  117. data/db/migrate/20241218000007_create_railspress_taggings.rb +20 -0
  118. data/db/migrate/20241218000008_drop_railspress_post_tags.rb +14 -0
  119. data/db/migrate/20241218000010_add_reading_time_to_railspress_posts.rb +5 -0
  120. data/db/migrate/20250105000002_create_railspress_focal_points.rb +20 -0
  121. data/db/migrate/20260206000001_create_railspress_content_groups.rb +18 -0
  122. data/db/migrate/20260206000002_create_railspress_content_elements.rb +21 -0
  123. data/db/migrate/20260206000003_create_railspress_content_element_versions.rb +20 -0
  124. data/db/migrate/20260207000001_add_unique_index_to_content_elements.rb +11 -0
  125. data/db/migrate/20260211112812_add_image_hint_to_railspress_content_elements.rb +7 -0
  126. data/db/migrate/20260211154040_add_required_to_railspress_content_elements.rb +5 -0
  127. data/lib/generators/railspress/entity/entity_generator.rb +89 -0
  128. data/lib/generators/railspress/entity/templates/migration.rb.tt +13 -0
  129. data/lib/generators/railspress/entity/templates/model.rb.tt +21 -0
  130. data/lib/generators/railspress/install/install_generator.rb +51 -40
  131. data/lib/generators/railspress/install/templates/initializer.rb +29 -0
  132. data/lib/railspress/engine.rb +38 -0
  133. data/lib/railspress/entity.rb +239 -0
  134. data/lib/railspress/version.rb +1 -1
  135. data/lib/railspress.rb +198 -8
  136. data/lib/tasks/railspress_tasks.rake +49 -4
  137. metadata +215 -21
  138. data/MIT-LICENSE +0 -20
  139. data/app/assets/stylesheets/railspress/admin.css +0 -1207
  140. data/app/models/railspress/post_tag.rb +0 -8
@@ -0,0 +1,343 @@
1
+ /**
2
+ * RailsPress Markdown Mode
3
+ * Toggle between rich text (Lexxy) and raw markdown editing
4
+ */
5
+
6
+ (function() {
7
+ 'use strict';
8
+
9
+ // ============================================
10
+ // HTML to Markdown Converter (Simple)
11
+ // ============================================
12
+
13
+ function htmlToMarkdown(html) {
14
+ if (!html || html.trim() === '' || html === '<p></p>' || html === '<p><br></p>') {
15
+ return '';
16
+ }
17
+
18
+ let md = html;
19
+
20
+ // Normalize whitespace
21
+ md = md.replace(/\r\n/g, '\n');
22
+
23
+ // Convert block elements first
24
+
25
+ // Headings
26
+ md = md.replace(/<h1[^>]*>(.*?)<\/h1>/gi, '# $1\n\n');
27
+ md = md.replace(/<h2[^>]*>(.*?)<\/h2>/gi, '## $1\n\n');
28
+ md = md.replace(/<h3[^>]*>(.*?)<\/h3>/gi, '### $1\n\n');
29
+ md = md.replace(/<h4[^>]*>(.*?)<\/h4>/gi, '#### $1\n\n');
30
+ md = md.replace(/<h5[^>]*>(.*?)<\/h5>/gi, '##### $1\n\n');
31
+ md = md.replace(/<h6[^>]*>(.*?)<\/h6>/gi, '###### $1\n\n');
32
+
33
+ // Code blocks (before inline code)
34
+ md = md.replace(/<pre[^>]*><code[^>]*class="[^"]*language-(\w+)[^"]*"[^>]*>([\s\S]*?)<\/code><\/pre>/gi, function(match, lang, code) {
35
+ return '```' + lang + '\n' + decodeHtmlEntities(code.trim()) + '\n```\n\n';
36
+ });
37
+ md = md.replace(/<pre[^>]*><code[^>]*>([\s\S]*?)<\/code><\/pre>/gi, function(match, code) {
38
+ return '```\n' + decodeHtmlEntities(code.trim()) + '\n```\n\n';
39
+ });
40
+ md = md.replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gi, function(match, code) {
41
+ return '```\n' + decodeHtmlEntities(code.trim()) + '\n```\n\n';
42
+ });
43
+
44
+ // Blockquotes
45
+ md = md.replace(/<blockquote[^>]*>([\s\S]*?)<\/blockquote>/gi, function(match, content) {
46
+ const lines = content.replace(/<\/?p[^>]*>/gi, '\n').trim().split('\n');
47
+ return lines.map(line => '> ' + line.trim()).join('\n') + '\n\n';
48
+ });
49
+
50
+ // Horizontal rules
51
+ md = md.replace(/<hr[^>]*>/gi, '---\n\n');
52
+
53
+ // Lists
54
+ md = md.replace(/<ul[^>]*>([\s\S]*?)<\/ul>/gi, function(match, content) {
55
+ return content.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, '- $1\n') + '\n';
56
+ });
57
+ md = md.replace(/<ol[^>]*>([\s\S]*?)<\/ol>/gi, function(match, content) {
58
+ let index = 1;
59
+ return content.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, function(m, item) {
60
+ return (index++) + '. ' + item.trim() + '\n';
61
+ }) + '\n';
62
+ });
63
+
64
+ // Paragraphs
65
+ md = md.replace(/<p[^>]*>([\s\S]*?)<\/p>/gi, '$1\n\n');
66
+
67
+ // Line breaks
68
+ md = md.replace(/<br\s*\/?>/gi, ' \n');
69
+
70
+ // Now inline elements
71
+
72
+ // Images (before links to avoid nested issues)
73
+ md = md.replace(/<img[^>]*src="([^"]*)"[^>]*alt="([^"]*)"[^>]*>/gi, '![$2]($1)');
74
+ md = md.replace(/<img[^>]*alt="([^"]*)"[^>]*src="([^"]*)"[^>]*>/gi, '![$1]($2)');
75
+ md = md.replace(/<img[^>]*src="([^"]*)"[^>]*>/gi, '![]($1)');
76
+
77
+ // Links
78
+ md = md.replace(/<a[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi, '[$2]($1)');
79
+
80
+ // Bold and italic (order matters)
81
+ md = md.replace(/<(strong|b)[^>]*>([\s\S]*?)<\/\1>/gi, '**$2**');
82
+ md = md.replace(/<(em|i)[^>]*>([\s\S]*?)<\/\1>/gi, '*$2*');
83
+
84
+ // Inline code
85
+ md = md.replace(/<code[^>]*>(.*?)<\/code>/gi, '`$1`');
86
+
87
+ // Strikethrough
88
+ md = md.replace(/<(del|s|strike)[^>]*>([\s\S]*?)<\/\1>/gi, '~~$2~~');
89
+
90
+ // Remove remaining tags
91
+ md = md.replace(/<[^>]+>/g, '');
92
+
93
+ // Decode HTML entities
94
+ md = decodeHtmlEntities(md);
95
+
96
+ // Clean up whitespace
97
+ md = md.replace(/\n{3,}/g, '\n\n');
98
+ md = md.trim();
99
+
100
+ return md;
101
+ }
102
+
103
+ // ============================================
104
+ // Markdown to HTML Converter (Simple)
105
+ // ============================================
106
+
107
+ function markdownToHtml(md) {
108
+ if (!md || md.trim() === '') {
109
+ return '<p></p>';
110
+ }
111
+
112
+ let html = md;
113
+
114
+ // Escape HTML entities in the source
115
+ html = html.replace(/&/g, '&amp;');
116
+ html = html.replace(/</g, '&lt;');
117
+ html = html.replace(/>/g, '&gt;');
118
+
119
+ // Code blocks first (to protect their contents)
120
+ const codeBlocks = [];
121
+ html = html.replace(/```(\w*)\n([\s\S]*?)```/g, function(match, lang, code) {
122
+ const index = codeBlocks.length;
123
+ const langClass = lang ? ` class="language-${lang}"` : '';
124
+ codeBlocks.push(`<pre><code${langClass}>${code.trim()}</code></pre>`);
125
+ return `%%CODEBLOCK${index}%%`;
126
+ });
127
+
128
+ // Inline code (protect before other processing)
129
+ const inlineCodes = [];
130
+ html = html.replace(/`([^`]+)`/g, function(match, code) {
131
+ const index = inlineCodes.length;
132
+ inlineCodes.push(`<code>${code}</code>`);
133
+ return `%%INLINECODE${index}%%`;
134
+ });
135
+
136
+ // Headings
137
+ html = html.replace(/^###### (.*)$/gm, '<h6>$1</h6>');
138
+ html = html.replace(/^##### (.*)$/gm, '<h5>$1</h5>');
139
+ html = html.replace(/^#### (.*)$/gm, '<h4>$1</h4>');
140
+ html = html.replace(/^### (.*)$/gm, '<h3>$1</h3>');
141
+ html = html.replace(/^## (.*)$/gm, '<h2>$1</h2>');
142
+ html = html.replace(/^# (.*)$/gm, '<h1>$1</h1>');
143
+
144
+ // Horizontal rules
145
+ html = html.replace(/^---$/gm, '<hr>');
146
+ html = html.replace(/^\*\*\*$/gm, '<hr>');
147
+ html = html.replace(/^___$/gm, '<hr>');
148
+
149
+ // Blockquotes
150
+ html = html.replace(/^> (.*)$/gm, '<blockquote>$1</blockquote>');
151
+ // Merge consecutive blockquotes
152
+ html = html.replace(/<\/blockquote>\n<blockquote>/g, '\n');
153
+
154
+ // Unordered lists
155
+ html = html.replace(/^[\*\-] (.*)$/gm, '<li>$1</li>');
156
+ html = html.replace(/(<li>.*<\/li>\n?)+/g, function(match) {
157
+ return '<ul>\n' + match + '</ul>\n';
158
+ });
159
+
160
+ // Ordered lists
161
+ html = html.replace(/^\d+\. (.*)$/gm, '<oli>$1</oli>');
162
+ html = html.replace(/(<oli>.*<\/oli>\n?)+/g, function(match) {
163
+ return '<ol>\n' + match.replace(/oli>/g, 'li>') + '</ol>\n';
164
+ });
165
+
166
+ // Images
167
+ html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1">');
168
+
169
+ // Links
170
+ html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
171
+
172
+ // Bold and italic
173
+ html = html.replace(/\*\*\*([^*]+)\*\*\*/g, '<strong><em>$1</em></strong>');
174
+ html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
175
+ html = html.replace(/\*([^*]+)\*/g, '<em>$1</em>');
176
+ html = html.replace(/__([^_]+)__/g, '<strong>$1</strong>');
177
+ html = html.replace(/_([^_]+)_/g, '<em>$1</em>');
178
+
179
+ // Strikethrough
180
+ html = html.replace(/~~([^~]+)~~/g, '<del>$1</del>');
181
+
182
+ // Line breaks (two spaces at end of line)
183
+ html = html.replace(/ \n/g, '<br>\n');
184
+
185
+ // Paragraphs - wrap remaining text blocks
186
+ html = html.split(/\n\n+/).map(function(block) {
187
+ block = block.trim();
188
+ if (!block) return '';
189
+ // Don't wrap if already a block element
190
+ if (/^<(h[1-6]|ul|ol|blockquote|pre|hr|div|p)/i.test(block)) {
191
+ return block;
192
+ }
193
+ // Don't wrap code block placeholders
194
+ if (/^%%CODEBLOCK\d+%%$/.test(block)) {
195
+ return block;
196
+ }
197
+ return '<p>' + block.replace(/\n/g, '<br>') + '</p>';
198
+ }).join('\n');
199
+
200
+ // Restore code blocks
201
+ codeBlocks.forEach(function(code, index) {
202
+ html = html.replace(`%%CODEBLOCK${index}%%`, code);
203
+ });
204
+
205
+ // Restore inline code
206
+ inlineCodes.forEach(function(code, index) {
207
+ html = html.replace(`%%INLINECODE${index}%%`, code);
208
+ });
209
+
210
+ return html;
211
+ }
212
+
213
+ // ============================================
214
+ // Helper Functions
215
+ // ============================================
216
+
217
+ function decodeHtmlEntities(text) {
218
+ const textarea = document.createElement('textarea');
219
+ textarea.innerHTML = text;
220
+ return textarea.value;
221
+ }
222
+
223
+ // ============================================
224
+ // Markdown Mode Controller
225
+ // ============================================
226
+
227
+ function initMarkdownMode() {
228
+ console.log('[MarkdownMode] Initializing...');
229
+ const containers = document.querySelectorAll('[data-markdown-mode]');
230
+ console.log('[MarkdownMode] Found containers:', containers.length);
231
+
232
+ containers.forEach(function(container, index) {
233
+ console.log('[MarkdownMode] Container', index, ':', container);
234
+ const editor = container.querySelector('lexxy-editor');
235
+ const textarea = container.querySelector('[data-markdown-textarea]');
236
+ const toggleBtn = container.querySelector('[data-markdown-toggle]');
237
+ const richLabel = container.querySelector('[data-mode-label="rich"]');
238
+ const mdLabel = container.querySelector('[data-mode-label="markdown"]');
239
+
240
+ console.log('[MarkdownMode] Container', index, 'elements:', {
241
+ editor: !!editor,
242
+ textarea: !!textarea,
243
+ toggleBtn: !!toggleBtn
244
+ });
245
+
246
+ if (!editor || !textarea || !toggleBtn) {
247
+ console.log('[MarkdownMode] Container', index, 'missing required elements, skipping');
248
+ return;
249
+ }
250
+ console.log('[MarkdownMode] Container', index, 'initialized successfully');
251
+
252
+ let isMarkdownMode = false;
253
+
254
+ function updateLabels() {
255
+ if (richLabel) richLabel.hidden = isMarkdownMode;
256
+ if (mdLabel) mdLabel.hidden = !isMarkdownMode;
257
+ }
258
+
259
+ function switchToMarkdown() {
260
+ // Get HTML from Lexxy
261
+ const html = editor.value || '';
262
+
263
+ // Convert to markdown
264
+ const markdown = htmlToMarkdown(html);
265
+
266
+ // Show textarea, hide editor
267
+ textarea.value = markdown;
268
+ textarea.hidden = false;
269
+ editor.style.display = 'none';
270
+
271
+ isMarkdownMode = true;
272
+ toggleBtn.setAttribute('aria-pressed', 'true');
273
+ updateLabels();
274
+ }
275
+
276
+ function switchToRichText() {
277
+ // Get markdown from textarea
278
+ const markdown = textarea.value || '';
279
+
280
+ // Convert to HTML
281
+ const html = markdownToHtml(markdown);
282
+
283
+ // Show editor, hide textarea
284
+ editor.style.display = '';
285
+ textarea.hidden = true;
286
+
287
+ // Load HTML into Lexxy
288
+ editor.value = html;
289
+
290
+ isMarkdownMode = false;
291
+ toggleBtn.setAttribute('aria-pressed', 'false');
292
+ updateLabels();
293
+ }
294
+
295
+ toggleBtn.addEventListener('click', function() {
296
+ console.log('[MarkdownMode] Toggle clicked, current mode:', isMarkdownMode ? 'markdown' : 'rich');
297
+ if (isMarkdownMode) {
298
+ switchToRichText();
299
+ } else {
300
+ switchToMarkdown();
301
+ }
302
+ });
303
+
304
+ // Sync textarea changes back to hidden input on form submit
305
+ const form = container.closest('form');
306
+ if (form) {
307
+ form.addEventListener('submit', function() {
308
+ if (isMarkdownMode) {
309
+ // Convert markdown to HTML before submit
310
+ const markdown = textarea.value || '';
311
+ const html = markdownToHtml(markdown);
312
+ editor.value = html;
313
+ }
314
+ });
315
+ }
316
+
317
+ // Initialize labels
318
+ updateLabels();
319
+ });
320
+ }
321
+
322
+ // ============================================
323
+ // Export for testing/external use
324
+ // ============================================
325
+
326
+ window.RailsPress = window.RailsPress || {};
327
+ window.RailsPress.MarkdownMode = {
328
+ htmlToMarkdown: htmlToMarkdown,
329
+ markdownToHtml: markdownToHtml,
330
+ init: initMarkdownMode
331
+ };
332
+
333
+ // ============================================
334
+ // Initialize
335
+ // ============================================
336
+
337
+ if (document.readyState === 'loading') {
338
+ document.addEventListener('DOMContentLoaded', initMarkdownMode);
339
+ } else {
340
+ initMarkdownMode();
341
+ }
342
+
343
+ })();
File without changes
@@ -0,0 +1,70 @@
1
+ /*
2
+ * RailsPress Admin - Badges & Tags
3
+ * Status badges and tag components
4
+ */
5
+
6
+ /* Badges */
7
+ .rp-badge {
8
+ display: inline-flex;
9
+ align-items: center;
10
+ padding: 2px 10px;
11
+ font-size: 0.6875rem;
12
+ font-weight: 600;
13
+ text-transform: uppercase;
14
+ letter-spacing: 0.03em;
15
+ border-radius: 999px;
16
+ }
17
+
18
+ .rp-badge--draft {
19
+ background: var(--rp-bg);
20
+ color: var(--rp-text-muted);
21
+ border: 1px solid var(--rp-border);
22
+ }
23
+
24
+ .rp-badge--published {
25
+ background: var(--rp-success-light);
26
+ color: var(--rp-success);
27
+ }
28
+
29
+ .rp-badge--scheduled {
30
+ background: var(--rp-info-light, #e8f4fd);
31
+ color: var(--rp-primary, #1e88e5);
32
+ }
33
+
34
+ .rp-badge--pending {
35
+ background: var(--rp-info-light);
36
+ color: var(--rp-primary);
37
+ }
38
+
39
+ .rp-badge--processing {
40
+ background: #fff8e6;
41
+ color: #b8860b;
42
+ }
43
+
44
+ .rp-badge--completed {
45
+ background: var(--rp-success-light);
46
+ color: var(--rp-success);
47
+ }
48
+
49
+ .rp-badge--failed {
50
+ background: var(--rp-danger-light);
51
+ color: var(--rp-danger);
52
+ }
53
+
54
+ /* Tags */
55
+ .rp-tag {
56
+ display: inline-flex;
57
+ padding: 2px 8px;
58
+ font-size: 0.75rem;
59
+ font-weight: 500;
60
+ background: var(--rp-primary-light);
61
+ color: var(--rp-primary);
62
+ border-radius: 4px;
63
+ }
64
+
65
+ .rp-tag-list {
66
+ display: flex;
67
+ flex-wrap: wrap;
68
+ gap: var(--rp-space-xs);
69
+ margin-top: var(--rp-space-xs);
70
+ }
@@ -0,0 +1,25 @@
1
+ /*
2
+ * RailsPress Admin - Base Styles
3
+ * Reset and typography foundation
4
+ */
5
+
6
+ @import url('https://fonts.googleapis.com/css2?family=Source+Serif+4:wght@600;700&display=swap');
7
+
8
+ *, *::before, *::after {
9
+ box-sizing: border-box;
10
+ margin: 0;
11
+ padding: 0;
12
+ }
13
+
14
+ html {
15
+ font-size: 16px;
16
+ -webkit-font-smoothing: antialiased;
17
+ }
18
+
19
+ body {
20
+ font-family: var(--rp-font-body);
21
+ font-size: 0.9375rem;
22
+ line-height: 1.5;
23
+ color: var(--rp-text);
24
+ background: var(--rp-bg);
25
+ }
@@ -0,0 +1,140 @@
1
+ /*
2
+ * RailsPress Admin - Buttons & Links
3
+ * Button components and link styles
4
+ */
5
+
6
+ /* Buttons */
7
+ .rp-btn {
8
+ display: inline-flex;
9
+ align-items: center;
10
+ justify-content: center;
11
+ gap: var(--rp-space-sm);
12
+ padding: 10px 20px;
13
+ font-family: var(--rp-font-body);
14
+ font-size: 0.875rem;
15
+ font-weight: 600;
16
+ text-decoration: none;
17
+ border-radius: var(--rp-radius);
18
+ border: 1px solid transparent;
19
+ cursor: pointer;
20
+ transition: all 0.15s;
21
+ white-space: nowrap;
22
+ }
23
+
24
+ .rp-btn:focus {
25
+ outline: none;
26
+ box-shadow: 0 0 0 3px var(--rp-primary-light);
27
+ }
28
+
29
+ .rp-btn--primary {
30
+ background: var(--rp-primary);
31
+ color: white;
32
+ box-shadow: 0 2px 6px rgba(61, 90, 128, 0.25);
33
+ transition: all 0.15s ease;
34
+ }
35
+
36
+ .rp-btn--primary:hover {
37
+ background: var(--rp-primary-hover);
38
+ box-shadow: 0 4px 10px rgba(61, 90, 128, 0.35);
39
+ transform: translateY(-1px);
40
+ }
41
+
42
+ .rp-btn--secondary {
43
+ background: var(--rp-bg-elevated);
44
+ color: var(--rp-text);
45
+ border-color: var(--rp-border);
46
+ }
47
+
48
+ .rp-btn--secondary:hover {
49
+ background: var(--rp-bg);
50
+ border-color: var(--rp-text-muted);
51
+ }
52
+
53
+ .rp-btn--danger {
54
+ background: var(--rp-danger);
55
+ color: white;
56
+ }
57
+
58
+ .rp-btn--danger:hover {
59
+ background: #8f3436;
60
+ }
61
+
62
+ .rp-btn--text {
63
+ background: none;
64
+ border: none;
65
+ color: var(--rp-text-muted);
66
+ padding: 8px 12px;
67
+ }
68
+
69
+ .rp-btn--text:hover {
70
+ color: var(--rp-text);
71
+ text-decoration: underline;
72
+ }
73
+
74
+ .rp-btn--sm {
75
+ padding: 6px 12px;
76
+ font-size: 0.8125rem;
77
+ }
78
+
79
+ .rp-btn--lg {
80
+ padding: 14px 28px;
81
+ font-size: 1rem;
82
+ }
83
+
84
+ /* Links */
85
+ .rp-link {
86
+ color: var(--rp-primary);
87
+ text-decoration: none;
88
+ font-weight: 500;
89
+ }
90
+
91
+ .rp-link:hover {
92
+ text-decoration: underline;
93
+ }
94
+
95
+ .rp-link--danger {
96
+ color: var(--rp-danger);
97
+ background: none;
98
+ border: none;
99
+ padding: 0;
100
+ font: inherit;
101
+ cursor: pointer;
102
+ }
103
+
104
+ /* Icon Buttons */
105
+ .rp-icon-btn {
106
+ display: inline-flex;
107
+ align-items: center;
108
+ justify-content: center;
109
+ width: 32px;
110
+ height: 32px;
111
+ padding: 0;
112
+ background: none;
113
+ border: 1px solid transparent;
114
+ border-radius: var(--rp-radius);
115
+ color: var(--rp-text-muted);
116
+ cursor: pointer;
117
+ transition: all 0.15s;
118
+ }
119
+
120
+ .rp-icon-btn svg {
121
+ width: 16px;
122
+ height: 16px;
123
+ }
124
+
125
+ .rp-icon-btn:hover {
126
+ background: var(--rp-bg);
127
+ color: var(--rp-primary);
128
+ border-color: var(--rp-border);
129
+ }
130
+
131
+ .rp-icon-btn--danger:hover {
132
+ background: var(--rp-danger-light);
133
+ color: var(--rp-danger);
134
+ border-color: var(--rp-danger-light);
135
+ }
136
+
137
+ .rp-icon-btn--disabled {
138
+ opacity: 0.35;
139
+ cursor: not-allowed;
140
+ }
@@ -0,0 +1,52 @@
1
+ /*
2
+ * RailsPress Admin - Cards
3
+ * Card components
4
+ */
5
+
6
+ .rp-card {
7
+ background: var(--rp-bg-elevated);
8
+ border-radius: var(--rp-radius);
9
+ box-shadow: var(--rp-shadow-md);
10
+ border: none;
11
+ }
12
+
13
+ .rp-card--padded {
14
+ padding: var(--rp-space-xl);
15
+ }
16
+
17
+ /* Detail list for show views */
18
+ .rp-detail-list {
19
+ padding: var(--rp-space-lg);
20
+ }
21
+
22
+ .rp-detail-item {
23
+ padding: var(--rp-space-md) 0;
24
+ border-bottom: 1px solid var(--rp-border);
25
+ }
26
+
27
+ .rp-detail-item:last-child {
28
+ border-bottom: none;
29
+ }
30
+
31
+ .rp-detail-label {
32
+ font-size: var(--rp-font-sm);
33
+ color: var(--rp-text-secondary);
34
+ font-weight: 500;
35
+ margin-bottom: var(--rp-space-xs);
36
+ text-transform: uppercase;
37
+ letter-spacing: 0.05em;
38
+ }
39
+
40
+ .rp-detail-value {
41
+ color: var(--rp-text-primary);
42
+ line-height: 1.5;
43
+ }
44
+
45
+ .rp-rich-text-content {
46
+ max-width: 65ch;
47
+ }
48
+
49
+ .rp-text-content {
50
+ max-width: 65ch;
51
+ white-space: pre-wrap;
52
+ }
@@ -0,0 +1,55 @@
1
+ /*
2
+ * RailsPress Admin - Exports
3
+ * Export page styles
4
+ */
5
+
6
+ .rp-export-format-card {
7
+ margin-bottom: var(--rp-space-lg);
8
+ }
9
+
10
+ .rp-export-format-content {
11
+ display: flex;
12
+ align-items: center;
13
+ justify-content: space-between;
14
+ gap: var(--rp-space-lg);
15
+ }
16
+
17
+ .rp-export-format-info {
18
+ display: flex;
19
+ flex-direction: column;
20
+ gap: var(--rp-space-xs);
21
+ }
22
+
23
+ .rp-export-format-info strong {
24
+ font-size: 1rem;
25
+ color: var(--rp-text);
26
+ }
27
+
28
+ .rp-export-format-info span {
29
+ color: var(--rp-text-muted);
30
+ font-size: 0.9rem;
31
+ line-height: 1.5;
32
+ }
33
+
34
+ .rp-export-format-info code {
35
+ background: var(--rp-bg);
36
+ padding: 0.1em 0.4em;
37
+ border-radius: var(--rp-radius-sm);
38
+ font-size: 0.85em;
39
+ }
40
+
41
+ .rp-export-format-action {
42
+ flex-shrink: 0;
43
+ }
44
+
45
+ @media (max-width: 767px) {
46
+ .rp-export-format-content {
47
+ flex-direction: column;
48
+ align-items: stretch;
49
+ text-align: center;
50
+ }
51
+
52
+ .rp-export-format-action {
53
+ margin-top: var(--rp-space-md);
54
+ }
55
+ }