postnhost 0.0.0 → 0.1.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 (500) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +33 -0
  3. data/{LICENSE.txt → LICENSE} +6 -6
  4. data/README.md +343 -15
  5. data/Rakefile +66 -6
  6. data/app/assets/builds/postnhost/application.css +2 -0
  7. data/app/assets/builds/postnhost/application.js +50190 -0
  8. data/app/assets/config/postnhost_manifest.js +2 -0
  9. data/app/assets/images/postnhost/demo.webp +0 -0
  10. data/app/assets/images/postnhost/icons/alert-circle.svg +3 -0
  11. data/app/assets/images/postnhost/icons/arrow-left-long.svg +3 -0
  12. data/app/assets/images/postnhost/icons/arrow-left.svg +3 -0
  13. data/app/assets/images/postnhost/icons/arrow-right-end-on-rectangle.svg +3 -0
  14. data/app/assets/images/postnhost/icons/arrow-right-long.svg +3 -0
  15. data/app/assets/images/postnhost/icons/arrow-right.svg +3 -0
  16. data/app/assets/images/postnhost/icons/bars.svg +3 -0
  17. data/app/assets/images/postnhost/icons/bluesky.svg +3 -0
  18. data/app/assets/images/postnhost/icons/check-circle.svg +3 -0
  19. data/app/assets/images/postnhost/icons/chevron-down-small.svg +3 -0
  20. data/app/assets/images/postnhost/icons/chevron-down.svg +3 -0
  21. data/app/assets/images/postnhost/icons/chevron-left.svg +3 -0
  22. data/app/assets/images/postnhost/icons/chevron-right.svg +3 -0
  23. data/app/assets/images/postnhost/icons/clock.svg +3 -0
  24. data/app/assets/images/postnhost/icons/close.svg +3 -0
  25. data/app/assets/images/postnhost/icons/code-bracket.svg +3 -0
  26. data/app/assets/images/postnhost/icons/document-text.svg +3 -0
  27. data/app/assets/images/postnhost/icons/drag-handle.svg +8 -0
  28. data/app/assets/images/postnhost/icons/edit.svg +3 -0
  29. data/app/assets/images/postnhost/icons/external-link.svg +3 -0
  30. data/app/assets/images/postnhost/icons/eye.svg +4 -0
  31. data/app/assets/images/postnhost/icons/facebook.svg +3 -0
  32. data/app/assets/images/postnhost/icons/folder.svg +3 -0
  33. data/app/assets/images/postnhost/icons/image.svg +3 -0
  34. data/app/assets/images/postnhost/icons/info-circle.svg +3 -0
  35. data/app/assets/images/postnhost/icons/instagram.svg +3 -0
  36. data/app/assets/images/postnhost/icons/language.svg +3 -0
  37. data/app/assets/images/postnhost/icons/layers.svg +3 -0
  38. data/app/assets/images/postnhost/icons/lightning.svg +3 -0
  39. data/app/assets/images/postnhost/icons/linkedin.svg +3 -0
  40. data/app/assets/images/postnhost/icons/logout.svg +3 -0
  41. data/app/assets/images/postnhost/icons/magnifying-glass.svg +3 -0
  42. data/app/assets/images/postnhost/icons/mastodon.svg +3 -0
  43. data/app/assets/images/postnhost/icons/menu.svg +3 -0
  44. data/app/assets/images/postnhost/icons/plus.svg +3 -0
  45. data/app/assets/images/postnhost/icons/schedule.svg +3 -0
  46. data/app/assets/images/postnhost/icons/settings.svg +4 -0
  47. data/app/assets/images/postnhost/icons/sparkles.svg +3 -0
  48. data/app/assets/images/postnhost/icons/spinner.svg +4 -0
  49. data/app/assets/images/postnhost/icons/threads.svg +3 -0
  50. data/app/assets/images/postnhost/icons/tiktok.svg +3 -0
  51. data/app/assets/images/postnhost/icons/tiptap/align-center.svg +3 -0
  52. data/app/assets/images/postnhost/icons/tiptap/align-left.svg +3 -0
  53. data/app/assets/images/postnhost/icons/tiptap/align-right.svg +3 -0
  54. data/app/assets/images/postnhost/icons/tiptap/apply-link.svg +3 -0
  55. data/app/assets/images/postnhost/icons/tiptap/blockquote.svg +3 -0
  56. data/app/assets/images/postnhost/icons/tiptap/bold.svg +3 -0
  57. data/app/assets/images/postnhost/icons/tiptap/bullet-list.svg +3 -0
  58. data/app/assets/images/postnhost/icons/tiptap/code-block.svg +3 -0
  59. data/app/assets/images/postnhost/icons/tiptap/code.svg +3 -0
  60. data/app/assets/images/postnhost/icons/tiptap/heading-2.svg +3 -0
  61. data/app/assets/images/postnhost/icons/tiptap/heading-3.svg +3 -0
  62. data/app/assets/images/postnhost/icons/tiptap/image-add.svg +3 -0
  63. data/app/assets/images/postnhost/icons/tiptap/italic.svg +3 -0
  64. data/app/assets/images/postnhost/icons/tiptap/link.svg +3 -0
  65. data/app/assets/images/postnhost/icons/tiptap/ordered-list.svg +3 -0
  66. data/app/assets/images/postnhost/icons/tiptap/redo.svg +3 -0
  67. data/app/assets/images/postnhost/icons/tiptap/remove-link.svg +3 -0
  68. data/app/assets/images/postnhost/icons/tiptap/strike.svg +3 -0
  69. data/app/assets/images/postnhost/icons/tiptap/table.svg +3 -0
  70. data/app/assets/images/postnhost/icons/tiptap/underline.svg +3 -0
  71. data/app/assets/images/postnhost/icons/tiptap/undo.svg +3 -0
  72. data/app/assets/images/postnhost/icons/tiptap/youtube.svg +4 -0
  73. data/app/assets/images/postnhost/icons/translate.svg +3 -0
  74. data/app/assets/images/postnhost/icons/trash.svg +3 -0
  75. data/app/assets/images/postnhost/icons/user.svg +4 -0
  76. data/app/assets/images/postnhost/icons/users.svg +3 -0
  77. data/app/assets/images/postnhost/icons/warning.svg +3 -0
  78. data/app/assets/images/postnhost/icons/x.svg +3 -0
  79. data/app/assets/images/postnhost/icons/youtube.svg +3 -0
  80. data/app/assets/images/postnhost/logo.webp +0 -0
  81. data/app/assets/images/postnhost/og-image.webp +0 -0
  82. data/app/assets/images/postnhost/sample-docs/08-how-to-control-suggested-articles-and-top-picks-01-discovery-controls.webp +0 -0
  83. data/app/assets/images/postnhost/sample-docs/09-all-about-authors-in-postnhost-01-author-profile.webp +0 -0
  84. data/app/assets/images/postnhost/sample-docs/09-all-about-authors-in-postnhost-02-multiple-authors.webp +0 -0
  85. data/app/assets/images/postnhost/sample-docs/11-how-to-schedule-an-article-for-publication-01-timezone.webp +0 -0
  86. data/app/assets/images/postnhost/sample-docs/11-how-to-schedule-an-article-for-publication-02-scheduled-article.webp +0 -0
  87. data/app/assets/images/postnhost/sample-docs/12-how-article-version-history-and-restores-work-01-version-history.webp +0 -0
  88. data/app/assets/images/postnhost/sample-docs/13-how-to-add-and-manage-languages-01-language-management.webp +0 -0
  89. data/app/assets/images/postnhost/sample-docs/14-how-to-translate-articles-pages-and-categories-01-translation-options.webp +0 -0
  90. data/app/assets/images/postnhost/sample-docs/14-how-to-translate-articles-pages-and-categories-02-ai-language-selection.webp +0 -0
  91. data/app/assets/images/postnhost/sample-docs/16-how-to-add-navigation-links-01-navigation-builder.webp +0 -0
  92. data/app/assets/images/postnhost/sample-docs/16-how-to-add-navigation-links-02-external-link-settings.webp +0 -0
  93. data/app/assets/images/postnhost/sample-docs/17-how-to-add-a-logo-and-default-og-image-01-assets-settings.webp +0 -0
  94. data/app/assets/images/postnhost/sample-docs/19-how-to-customize-public-text-for-each-language-01-i18n-overrides.webp +0 -0
  95. data/app/assets/images/postnhost/sample-docs/20-how-to-configure-structured-data-and-sitemap-seo-01-site-schema.webp +0 -0
  96. data/app/assets/images/postnhost/sample-docs/20-how-to-configure-structured-data-and-sitemap-seo-02-author-schema-defaults.webp +0 -0
  97. data/app/assets/images/postnhost/sample-docs/20-how-to-configure-structured-data-and-sitemap-seo-03-author-schema-advanced-fields.webp +0 -0
  98. data/app/assets/images/postnhost/sample-docs/covers/08-suggested-articles-and-top-picks.webp +0 -0
  99. data/app/assets/images/postnhost/sample-docs/covers/09-authors.webp +0 -0
  100. data/app/assets/images/postnhost/sample-docs/covers/11-scheduling.webp +0 -0
  101. data/app/assets/images/postnhost/sample-docs/covers/12-version-history.webp +0 -0
  102. data/app/assets/images/postnhost/sample-docs/covers/13-languages.webp +0 -0
  103. data/app/assets/images/postnhost/sample-docs/covers/14-translations.webp +0 -0
  104. data/app/assets/images/postnhost/sample-docs/covers/16-navigation-links.webp +0 -0
  105. data/app/assets/images/postnhost/sample-docs/covers/17-logo-and-default-og-image.webp +0 -0
  106. data/app/assets/images/postnhost/sample-docs/covers/19-public-text-for-each-language.webp +0 -0
  107. data/app/assets/images/postnhost/sample-docs/covers/20-structured-data-and-sitemap-seo.webp +0 -0
  108. data/app/assets/stylesheets/application.tailwind.css +347 -0
  109. data/app/assets/stylesheets/postnhost/application.css +15 -0
  110. data/app/assets/xsl/sitemap.xsl +64 -0
  111. data/app/controllers/concerns/postnhost/authentication.rb +59 -0
  112. data/app/controllers/concerns/postnhost/canonical_first_page_redirect.rb +14 -0
  113. data/app/controllers/concerns/postnhost/language_switcher.rb +42 -0
  114. data/app/controllers/concerns/postnhost/onboarding/access_control.rb +57 -0
  115. data/app/controllers/concerns/postnhost/onboarding/step_assignment.rb +55 -0
  116. data/app/controllers/concerns/postnhost/public_footer_data.rb +103 -0
  117. data/app/controllers/concerns/postnhost/public_http_caching.rb +35 -0
  118. data/app/controllers/concerns/postnhost/public_page_size.rb +17 -0
  119. data/app/controllers/concerns/postnhost/public_template_resolvable.rb +44 -0
  120. data/app/controllers/concerns/postnhost/settings/i18n_manageable.rb +61 -0
  121. data/app/controllers/concerns/postnhost/settings/navigation_manageable.rb +118 -0
  122. data/app/controllers/concerns/postnhost/settings/schema_manageable.rb +140 -0
  123. data/app/controllers/concerns/postnhost/users/schema_manageable.rb +119 -0
  124. data/app/controllers/postnhost/application_controller.rb +37 -0
  125. data/app/controllers/postnhost/article_variants_controller.rb +138 -0
  126. data/app/controllers/postnhost/articles/cover_images_controller.rb +35 -0
  127. data/app/controllers/postnhost/articles/images_controller.rb +28 -0
  128. data/app/controllers/postnhost/articles/translations_controller.rb +26 -0
  129. data/app/controllers/postnhost/articles_controller.rb +130 -0
  130. data/app/controllers/postnhost/categories/translations_controller.rb +24 -0
  131. data/app/controllers/postnhost/categories_controller.rb +59 -0
  132. data/app/controllers/postnhost/category_variants_controller.rb +69 -0
  133. data/app/controllers/postnhost/languages_controller.rb +66 -0
  134. data/app/controllers/postnhost/onboarding_controller.rb +70 -0
  135. data/app/controllers/postnhost/page_variants_controller.rb +136 -0
  136. data/app/controllers/postnhost/pages/images_controller.rb +28 -0
  137. data/app/controllers/postnhost/pages/translations_controller.rb +26 -0
  138. data/app/controllers/postnhost/pages_controller.rb +95 -0
  139. data/app/controllers/postnhost/public/articles_controller.rb +135 -0
  140. data/app/controllers/postnhost/public/authors_controller.rb +57 -0
  141. data/app/controllers/postnhost/public/categories_controller.rb +61 -0
  142. data/app/controllers/postnhost/public/static_pages_controller.rb +77 -0
  143. data/app/controllers/postnhost/public_controller.rb +32 -0
  144. data/app/controllers/postnhost/sessions_controller.rb +37 -0
  145. data/app/controllers/postnhost/settings/site_scripts_controller.rb +61 -0
  146. data/app/controllers/postnhost/settings_controller.rb +63 -0
  147. data/app/controllers/postnhost/sitemap_controller.rb +17 -0
  148. data/app/controllers/postnhost/templates_controller.rb +26 -0
  149. data/app/controllers/postnhost/users_controller.rb +112 -0
  150. data/app/helpers/postnhost/application_helper.rb +236 -0
  151. data/app/helpers/postnhost/article_helper.rb +41 -0
  152. data/app/helpers/postnhost/category_helper.rb +52 -0
  153. data/app/helpers/postnhost/common_css_helper.rb +97 -0
  154. data/app/helpers/postnhost/navigation_helper.rb +148 -0
  155. data/app/helpers/postnhost/pagy_helper.rb +103 -0
  156. data/app/helpers/postnhost/public_pages_helper.rb +287 -0
  157. data/app/helpers/postnhost/schema_helper.rb +102 -0
  158. data/app/helpers/postnhost/seo_helper.rb +428 -0
  159. data/app/helpers/postnhost/settings_helper.rb +79 -0
  160. data/app/javascript/application.js +3 -0
  161. data/app/javascript/controllers/ai_translation_controller.js +42 -0
  162. data/app/javascript/controllers/application.js +9 -0
  163. data/app/javascript/controllers/author_manager_controller.js +96 -0
  164. data/app/javascript/controllers/auto_save_controller.js +244 -0
  165. data/app/javascript/controllers/bulk_selection_controller.js +74 -0
  166. data/app/javascript/controllers/burger_controller.js +20 -0
  167. data/app/javascript/controllers/category_manager_controller.js +99 -0
  168. data/app/javascript/controllers/collapsible_sidebar_controller.js +87 -0
  169. data/app/javascript/controllers/cover_image_upload_controller.js +164 -0
  170. data/app/javascript/controllers/dropdown_controller.js +38 -0
  171. data/app/javascript/controllers/editor_mobile_sidebars_controller.js +36 -0
  172. data/app/javascript/controllers/file_auto_submit_controller.js +23 -0
  173. data/app/javascript/controllers/flash_message_controller.js +40 -0
  174. data/app/javascript/controllers/index.js +71 -0
  175. data/app/javascript/controllers/navigation_builder_controller.js +463 -0
  176. data/app/javascript/controllers/public_mobile_nav_controller.js +26 -0
  177. data/app/javascript/controllers/publish_guard_controller.js +47 -0
  178. data/app/javascript/controllers/reading_progress_controller.js +38 -0
  179. data/app/javascript/controllers/schedule_manager_controller.js +87 -0
  180. data/app/javascript/controllers/suggestion_manager_controller.js +99 -0
  181. data/app/javascript/controllers/template_preview_controller.js +34 -0
  182. data/app/javascript/controllers/tiptap_editor_controller.js +812 -0
  183. data/app/javascript/controllers/title_field_controller.js +17 -0
  184. data/app/javascript/helpers/auto_save_state.js +47 -0
  185. data/app/javascript/helpers/code_highlighting.js +48 -0
  186. data/app/javascript/helpers/dom_helpers.js +109 -0
  187. data/app/javascript/helpers/style_helpers.js +168 -0
  188. data/app/jobs/postnhost/application_job.rb +9 -0
  189. data/app/jobs/postnhost/scheduled_article_job.rb +13 -0
  190. data/app/jobs/postnhost/translation/article_variant_job.rb +17 -0
  191. data/app/jobs/postnhost/translation/category_variant_job.rb +17 -0
  192. data/app/jobs/postnhost/translation/page_variant_job.rb +17 -0
  193. data/app/models/concerns/postnhost/article/image_uploadable.rb +11 -0
  194. data/app/models/concerns/postnhost/excerpt_optimizable.rb +57 -0
  195. data/app/models/concerns/postnhost/inline_image_uploadable.rb +40 -0
  196. data/app/models/concerns/postnhost/page/image_uploadable.rb +11 -0
  197. data/app/models/concerns/postnhost/primary_language_guard.rb +18 -0
  198. data/app/models/concerns/postnhost/public_revision_touch.rb +15 -0
  199. data/app/models/concerns/postnhost/schema_localizable.rb +28 -0
  200. data/app/models/postnhost/application_record.rb +5 -0
  201. data/app/models/postnhost/article.rb +142 -0
  202. data/app/models/postnhost/article_author.rb +20 -0
  203. data/app/models/postnhost/article_category.rb +8 -0
  204. data/app/models/postnhost/article_suggestion.rb +19 -0
  205. data/app/models/postnhost/article_variant.rb +28 -0
  206. data/app/models/postnhost/category.rb +34 -0
  207. data/app/models/postnhost/category_variant.rb +24 -0
  208. data/app/models/postnhost/language.rb +38 -0
  209. data/app/models/postnhost/navigation.rb +76 -0
  210. data/app/models/postnhost/navigation_item.rb +100 -0
  211. data/app/models/postnhost/page.rb +63 -0
  212. data/app/models/postnhost/page_variant.rb +26 -0
  213. data/app/models/postnhost/public_site_revision.rb +24 -0
  214. data/app/models/postnhost/published_cover_image.rb +28 -0
  215. data/app/models/postnhost/setting.rb +116 -0
  216. data/app/models/postnhost/site_script.rb +11 -0
  217. data/app/models/postnhost/snapshot/article.rb +142 -0
  218. data/app/models/postnhost/snapshot/article_author.rb +11 -0
  219. data/app/models/postnhost/snapshot/article_category.rb +10 -0
  220. data/app/models/postnhost/snapshot/article_suggestion.rb +11 -0
  221. data/app/models/postnhost/snapshot/article_variant.rb +60 -0
  222. data/app/models/postnhost/snapshot/page.rb +33 -0
  223. data/app/models/postnhost/snapshot/page_variant.rb +41 -0
  224. data/app/models/postnhost/template.rb +20 -0
  225. data/app/models/postnhost/user.rb +63 -0
  226. data/app/models/postnhost/version.rb +7 -0
  227. data/app/services/postnhost/base_service.rb +17 -0
  228. data/app/services/postnhost/constraints/public_locale_constraint.rb +9 -0
  229. data/app/services/postnhost/constraints/public_route_constraint.rb +19 -0
  230. data/app/services/postnhost/onboarding/step_processor.rb +27 -0
  231. data/app/services/postnhost/onboarding/steps/base.rb +52 -0
  232. data/app/services/postnhost/onboarding/steps/choose_languages.rb +66 -0
  233. data/app/services/postnhost/onboarding/steps/complete_post_install_checklist.rb +12 -0
  234. data/app/services/postnhost/onboarding/steps/create_admin_account.rb +28 -0
  235. data/app/services/postnhost/onboarding/steps/finish_setup.rb +27 -0
  236. data/app/services/postnhost/onboarding/steps/set_site_metadata.rb +70 -0
  237. data/app/services/postnhost/public_navigation_targets.rb +67 -0
  238. data/app/services/postnhost/public_request_context.rb +101 -0
  239. data/app/services/postnhost/publishing/article_variants/publish.rb +26 -0
  240. data/app/services/postnhost/publishing/article_variants/publish_many.rb +23 -0
  241. data/app/services/postnhost/publishing/article_variants/snapshot_writer.rb +44 -0
  242. data/app/services/postnhost/publishing/article_variants/unpublish.rb +27 -0
  243. data/app/services/postnhost/publishing/article_variants/unpublish_many.rb +23 -0
  244. data/app/services/postnhost/publishing/articles/publish.rb +25 -0
  245. data/app/services/postnhost/publishing/articles/publish_scheduled.rb +42 -0
  246. data/app/services/postnhost/publishing/articles/snapshot_writer.rb +84 -0
  247. data/app/services/postnhost/publishing/articles/unpublish.rb +26 -0
  248. data/app/services/postnhost/publishing/bulk_transition.rb +63 -0
  249. data/app/services/postnhost/publishing/error.rb +12 -0
  250. data/app/services/postnhost/publishing/page_variants/publish.rb +26 -0
  251. data/app/services/postnhost/publishing/page_variants/publish_many.rb +23 -0
  252. data/app/services/postnhost/publishing/page_variants/snapshot_writer.rb +41 -0
  253. data/app/services/postnhost/publishing/page_variants/unpublish.rb +27 -0
  254. data/app/services/postnhost/publishing/page_variants/unpublish_many.rb +23 -0
  255. data/app/services/postnhost/publishing/pages/publish.rb +25 -0
  256. data/app/services/postnhost/publishing/pages/snapshot_writer.rb +38 -0
  257. data/app/services/postnhost/publishing/pages/unpublish.rb +26 -0
  258. data/app/services/postnhost/publishing/revision.rb +37 -0
  259. data/app/services/postnhost/publishing/route_validator.rb +41 -0
  260. data/app/services/postnhost/publishing/versions/restore_draft.rb +64 -0
  261. data/app/services/postnhost/service_result.rb +7 -0
  262. data/app/services/postnhost/sitemap_builder.rb +395 -0
  263. data/app/services/postnhost/translation/article_variant_translator.rb +102 -0
  264. data/app/services/postnhost/translation/category_variant_translator.rb +92 -0
  265. data/app/services/postnhost/translation/content_translator.rb +119 -0
  266. data/app/services/postnhost/translation/openai_llm_client.rb +30 -0
  267. data/app/services/postnhost/translation/page_variant_translator.rb +55 -0
  268. data/app/uploaders/base_uploader.rb +26 -0
  269. data/app/uploaders/cover_image_uploader.rb +31 -0
  270. data/app/uploaders/postnhost/avatar_uploader.rb +17 -0
  271. data/app/uploaders/postnhost/inline_article_image_uploader.rb +38 -0
  272. data/app/uploaders/postnhost/inline_page_image_uploader.rb +38 -0
  273. data/app/uploaders/postnhost/setting_asset_uploader.rb +18 -0
  274. data/app/views/layouts/postnhost/_favicon.html.erb +6 -0
  275. data/app/views/layouts/postnhost/_footer.html.erb +67 -0
  276. data/app/views/layouts/postnhost/_header.html.erb +89 -0
  277. data/app/views/layouts/postnhost/_sidebar.html.erb +72 -0
  278. data/app/views/layouts/postnhost/application.html.erb +23 -0
  279. data/app/views/layouts/postnhost/articles.html.erb +34 -0
  280. data/app/views/layouts/postnhost/product.html.erb +36 -0
  281. data/app/views/layouts/postnhost/public/templates/default.html.erb +110 -0
  282. data/app/views/layouts/postnhost/public/templates/swiss-editorial.html.erb +213 -0
  283. data/app/views/layouts/postnhost/public/templates/workspace-journal.html.erb +231 -0
  284. data/app/views/layouts/postnhost/public_header/_dashboard_sign_in.html.erb +14 -0
  285. data/app/views/layouts/postnhost/public_header/_language_switcher.html.erb +43 -0
  286. data/app/views/layouts/postnhost/public_header/_logo.html.erb +9 -0
  287. data/app/views/postnhost/article_variants/_variants_list.html.erb +82 -0
  288. data/app/views/postnhost/article_variants/edit.html.erb +48 -0
  289. data/app/views/postnhost/article_variants/index.html.erb +128 -0
  290. data/app/views/postnhost/article_variants/publish.turbo_stream.erb +3 -0
  291. data/app/views/postnhost/article_variants/unpublish.turbo_stream.erb +3 -0
  292. data/app/views/postnhost/article_variants/update.turbo_stream.erb +3 -0
  293. data/app/views/postnhost/articles/_author_manager.html.erb +38 -0
  294. data/app/views/postnhost/articles/_category_manager.html.erb +42 -0
  295. data/app/views/postnhost/articles/_cover_image_upload.html.erb +82 -0
  296. data/app/views/postnhost/articles/_suggestion_manager.html.erb +43 -0
  297. data/app/views/postnhost/articles/_version_snapshot.html.erb +10 -0
  298. data/app/views/postnhost/articles/edit.html.erb +51 -0
  299. data/app/views/postnhost/articles/index.html.erb +132 -0
  300. data/app/views/postnhost/articles/publish.turbo_stream.erb +3 -0
  301. data/app/views/postnhost/articles/unpublish.turbo_stream.erb +3 -0
  302. data/app/views/postnhost/articles/update.turbo_stream.erb +3 -0
  303. data/app/views/postnhost/articles/version_preview.html.erb +17 -0
  304. data/app/views/postnhost/articles/versions.html.erb +68 -0
  305. data/app/views/postnhost/categories/edit.html.erb +77 -0
  306. data/app/views/postnhost/categories/index.html.erb +80 -0
  307. data/app/views/postnhost/categories/new.html.erb +50 -0
  308. data/app/views/postnhost/categories/show.html.erb +99 -0
  309. data/app/views/postnhost/category_variants/_category_variants_list.html.erb +67 -0
  310. data/app/views/postnhost/category_variants/_form.html.erb +29 -0
  311. data/app/views/postnhost/category_variants/edit.html.erb +21 -0
  312. data/app/views/postnhost/category_variants/index.html.erb +82 -0
  313. data/app/views/postnhost/category_variants/new.html.erb +21 -0
  314. data/app/views/postnhost/languages/edit.html.erb +45 -0
  315. data/app/views/postnhost/languages/index.html.erb +86 -0
  316. data/app/views/postnhost/languages/new.html.erb +45 -0
  317. data/app/views/postnhost/languages/show.html.erb +156 -0
  318. data/app/views/postnhost/onboarding/_post_install_checklist.html.erb +40 -0
  319. data/app/views/postnhost/onboarding/_step_1.html.erb +42 -0
  320. data/app/views/postnhost/onboarding/_step_2.html.erb +38 -0
  321. data/app/views/postnhost/onboarding/_step_3.html.erb +62 -0
  322. data/app/views/postnhost/onboarding/_step_4.html.erb +26 -0
  323. data/app/views/postnhost/onboarding/show.html.erb +51 -0
  324. data/app/views/postnhost/page_variants/_variants_list.html.erb +82 -0
  325. data/app/views/postnhost/page_variants/edit.html.erb +50 -0
  326. data/app/views/postnhost/page_variants/index.html.erb +128 -0
  327. data/app/views/postnhost/page_variants/publish.turbo_stream.erb +3 -0
  328. data/app/views/postnhost/page_variants/unpublish.turbo_stream.erb +3 -0
  329. data/app/views/postnhost/page_variants/update.turbo_stream.erb +3 -0
  330. data/app/views/postnhost/pages/edit.html.erb +50 -0
  331. data/app/views/postnhost/pages/index.html.erb +110 -0
  332. data/app/views/postnhost/pages/publish.turbo_stream.erb +3 -0
  333. data/app/views/postnhost/pages/unpublish.turbo_stream.erb +3 -0
  334. data/app/views/postnhost/public/articles/content_for/_index.html.erb +30 -0
  335. data/app/views/postnhost/public/articles/content_for/_preview.html.erb +9 -0
  336. data/app/views/postnhost/public/articles/content_for/_show.html.erb +24 -0
  337. data/app/views/postnhost/public/authors/content_for/_show.html.erb +24 -0
  338. data/app/views/postnhost/public/categories/content_for/_index.html.erb +29 -0
  339. data/app/views/postnhost/public/static_pages/content_for/_show.html.erb +19 -0
  340. data/app/views/postnhost/public/templates/default/articles/_article_card.html.erb +37 -0
  341. data/app/views/postnhost/public/templates/default/articles/_search_form.html.erb +12 -0
  342. data/app/views/postnhost/public/templates/default/articles/_suggestions.html.erb +44 -0
  343. data/app/views/postnhost/public/templates/default/articles/_top_picks.html.erb +42 -0
  344. data/app/views/postnhost/public/templates/default/articles/index.html.erb +53 -0
  345. data/app/views/postnhost/public/templates/default/articles/preview.html.erb +102 -0
  346. data/app/views/postnhost/public/templates/default/articles/show.html.erb +111 -0
  347. data/app/views/postnhost/public/templates/default/authors/_article_card.html.erb +28 -0
  348. data/app/views/postnhost/public/templates/default/authors/_profile_block.html.erb +29 -0
  349. data/app/views/postnhost/public/templates/default/authors/show.html.erb +36 -0
  350. data/app/views/postnhost/public/templates/default/categories/_article_card.html.erb +34 -0
  351. data/app/views/postnhost/public/templates/default/categories/index.html.erb +48 -0
  352. data/app/views/postnhost/public/templates/default/shared/_article_authors.html.erb +24 -0
  353. data/app/views/postnhost/public/templates/default/shared/_author_avatar.html.erb +9 -0
  354. data/app/views/postnhost/public/templates/default/shared/_hero_cover_image.html.erb +11 -0
  355. data/app/views/postnhost/public/templates/default/shared/_pagination.html.erb +35 -0
  356. data/app/views/postnhost/public/templates/default/shared/_thumbnail_cover_image.html.erb +9 -0
  357. data/app/views/postnhost/public/templates/default/static_pages/show.html.erb +22 -0
  358. data/app/views/postnhost/public/templates/swiss-editorial/articles/_article_card.html.erb +28 -0
  359. data/app/views/postnhost/public/templates/swiss-editorial/articles/_search_form.html.erb +19 -0
  360. data/app/views/postnhost/public/templates/swiss-editorial/articles/_suggestions.html.erb +42 -0
  361. data/app/views/postnhost/public/templates/swiss-editorial/articles/_top_picks.html.erb +38 -0
  362. data/app/views/postnhost/public/templates/swiss-editorial/articles/index.html.erb +133 -0
  363. data/app/views/postnhost/public/templates/swiss-editorial/articles/preview.html.erb +42 -0
  364. data/app/views/postnhost/public/templates/swiss-editorial/articles/show.html.erb +100 -0
  365. data/app/views/postnhost/public/templates/swiss-editorial/authors/_article_card.html.erb +27 -0
  366. data/app/views/postnhost/public/templates/swiss-editorial/authors/_profile_block.html.erb +32 -0
  367. data/app/views/postnhost/public/templates/swiss-editorial/authors/show.html.erb +31 -0
  368. data/app/views/postnhost/public/templates/swiss-editorial/categories/_article_card.html.erb +28 -0
  369. data/app/views/postnhost/public/templates/swiss-editorial/categories/index.html.erb +88 -0
  370. data/app/views/postnhost/public/templates/swiss-editorial/shared/_article_authors.html.erb +26 -0
  371. data/app/views/postnhost/public/templates/swiss-editorial/shared/_author_avatar.html.erb +9 -0
  372. data/app/views/postnhost/public/templates/swiss-editorial/shared/_hero_cover_image.html.erb +11 -0
  373. data/app/views/postnhost/public/templates/swiss-editorial/shared/_pagination.html.erb +35 -0
  374. data/app/views/postnhost/public/templates/swiss-editorial/shared/_thumbnail_cover_image.html.erb +9 -0
  375. data/app/views/postnhost/public/templates/swiss-editorial/static_pages/show.html.erb +17 -0
  376. data/app/views/postnhost/public/templates/workspace-journal/articles/_article_card.html.erb +44 -0
  377. data/app/views/postnhost/public/templates/workspace-journal/articles/_search_form.html.erb +19 -0
  378. data/app/views/postnhost/public/templates/workspace-journal/articles/_suggestions.html.erb +49 -0
  379. data/app/views/postnhost/public/templates/workspace-journal/articles/_top_picks.html.erb +34 -0
  380. data/app/views/postnhost/public/templates/workspace-journal/articles/index.html.erb +69 -0
  381. data/app/views/postnhost/public/templates/workspace-journal/articles/preview.html.erb +113 -0
  382. data/app/views/postnhost/public/templates/workspace-journal/articles/show.html.erb +120 -0
  383. data/app/views/postnhost/public/templates/workspace-journal/authors/_article_card.html.erb +1 -0
  384. data/app/views/postnhost/public/templates/workspace-journal/authors/_profile_block.html.erb +32 -0
  385. data/app/views/postnhost/public/templates/workspace-journal/authors/show.html.erb +33 -0
  386. data/app/views/postnhost/public/templates/workspace-journal/categories/_article_card.html.erb +1 -0
  387. data/app/views/postnhost/public/templates/workspace-journal/categories/index.html.erb +58 -0
  388. data/app/views/postnhost/public/templates/workspace-journal/shared/_article_authors.html.erb +27 -0
  389. data/app/views/postnhost/public/templates/workspace-journal/shared/_article_card.html.erb +44 -0
  390. data/app/views/postnhost/public/templates/workspace-journal/shared/_author_avatar.html.erb +9 -0
  391. data/app/views/postnhost/public/templates/workspace-journal/shared/_category_navigation.html.erb +10 -0
  392. data/app/views/postnhost/public/templates/workspace-journal/shared/_hero_cover_image.html.erb +11 -0
  393. data/app/views/postnhost/public/templates/workspace-journal/shared/_pagination.html.erb +35 -0
  394. data/app/views/postnhost/public/templates/workspace-journal/shared/_thumbnail_cover_image.html.erb +9 -0
  395. data/app/views/postnhost/public/templates/workspace-journal/static_pages/show.html.erb +17 -0
  396. data/app/views/postnhost/sessions/new.html.erb +59 -0
  397. data/app/views/postnhost/settings/edit.html.erb +68 -0
  398. data/app/views/postnhost/settings/sections/_assets.html.erb +40 -0
  399. data/app/views/postnhost/settings/sections/_feature_flags.html.erb +43 -0
  400. data/app/views/postnhost/settings/sections/_i18n.html.erb +57 -0
  401. data/app/views/postnhost/settings/sections/_navigation.html.erb +98 -0
  402. data/app/views/postnhost/settings/sections/_schema.html.erb +253 -0
  403. data/app/views/postnhost/settings/sections/_scripts.html.erb +79 -0
  404. data/app/views/postnhost/settings/sections/_site.html.erb +41 -0
  405. data/app/views/postnhost/settings/sections/_timezone.html.erb +19 -0
  406. data/app/views/postnhost/shared/_ai_translation_modal.html.erb +85 -0
  407. data/app/views/postnhost/shared/_flash_messages.html.erb +27 -0
  408. data/app/views/postnhost/shared/_form_errors.html.erb +22 -0
  409. data/app/views/postnhost/shared/_seo_meta_tags.html.erb +70 -0
  410. data/app/views/postnhost/shared/_structured_data.html.erb +5 -0
  411. data/app/views/postnhost/shared/articles/_action_buttons.html.erb +87 -0
  412. data/app/views/postnhost/shared/articles/_cover_image_preview.html.erb +30 -0
  413. data/app/views/postnhost/shared/articles/_editor_column.html.erb +15 -0
  414. data/app/views/postnhost/shared/articles/_editor_column_preview.html.erb +33 -0
  415. data/app/views/postnhost/shared/articles/_error_messages.html.erb +23 -0
  416. data/app/views/postnhost/shared/articles/_meta_sidebar.html.erb +322 -0
  417. data/app/views/postnhost/shared/articles/_meta_sidebar_preview.html.erb +29 -0
  418. data/app/views/postnhost/shared/articles/_post_actions.html.erb +105 -0
  419. data/app/views/postnhost/shared/articles/_schedule_actions.html.erb +51 -0
  420. data/app/views/postnhost/shared/articles/_tiptap_editor.html.erb +225 -0
  421. data/app/views/postnhost/shared/articles/_title_field.html.erb +9 -0
  422. data/app/views/postnhost/shared/articles/_version_action_buttons.html.erb +35 -0
  423. data/app/views/postnhost/shared/elements/_button.html.erb +20 -0
  424. data/app/views/postnhost/shared/elements/_button_link.html.erb +24 -0
  425. data/app/views/postnhost/shared/elements/_empty_state.html.erb +22 -0
  426. data/app/views/postnhost/shared/elements/_error_messages.html.erb +28 -0
  427. data/app/views/postnhost/shared/elements/_form_card.html.erb +10 -0
  428. data/app/views/postnhost/shared/elements/_form_page_header.html.erb +21 -0
  429. data/app/views/postnhost/shared/elements/_icon_button.html.erb +17 -0
  430. data/app/views/postnhost/shared/elements/_page_header.html.erb +40 -0
  431. data/app/views/postnhost/shared/elements/_table_container.html.erb +23 -0
  432. data/app/views/postnhost/shared/pages/_action_buttons.html.erb +84 -0
  433. data/app/views/postnhost/shared/pages/_meta_sidebar.html.erb +132 -0
  434. data/app/views/postnhost/shared/pages/_post_actions.html.erb +69 -0
  435. data/app/views/postnhost/templates/edit.html.erb +83 -0
  436. data/app/views/postnhost/users/_avatar.html.erb +9 -0
  437. data/app/views/postnhost/users/_form.html.erb +160 -0
  438. data/app/views/postnhost/users/edit.html.erb +26 -0
  439. data/app/views/postnhost/users/index.html.erb +91 -0
  440. data/app/views/postnhost/users/new.html.erb +9 -0
  441. data/app/views/postnhost/users/schema_edit.html.erb +175 -0
  442. data/config/locales/de.yml +55 -0
  443. data/config/locales/en.yml +88 -0
  444. data/config/locales/es.yml +58 -0
  445. data/config/locales/fr.yml +55 -0
  446. data/config/locales/ja.yml +55 -0
  447. data/config/locales/ko.yml +55 -0
  448. data/config/locales/pl.yml +55 -0
  449. data/config/locales/pt.yml +55 -0
  450. data/config/locales/ru.yml +55 -0
  451. data/config/routes.rb +112 -0
  452. data/db/migrate/20250822213820_create_users.rb +29 -0
  453. data/db/migrate/20250822214046_create_categories.rb +15 -0
  454. data/db/migrate/20250822214106_create_languages.rb +16 -0
  455. data/db/migrate/20250822214148_create_articles.rb +34 -0
  456. data/db/migrate/20250822214217_create_article_variants.rb +24 -0
  457. data/db/migrate/20250929091137_create_article_categories.rb +12 -0
  458. data/db/migrate/20251013204200_create_versions.rb +40 -0
  459. data/db/migrate/20260106074532_create_article_suggestions.rb +13 -0
  460. data/db/migrate/20260108110506_create_category_variants.rb +15 -0
  461. data/db/migrate/20260308130000_create_settings.rb +23 -0
  462. data/db/migrate/20260323113000_create_pages.rb +19 -0
  463. data/db/migrate/20260414100000_create_article_authors.rb +14 -0
  464. data/db/migrate/20260512100000_create_templates.rb +9 -0
  465. data/db/migrate/20260520110001_create_navigations.rb +9 -0
  466. data/db/migrate/20260520110002_create_navigation_items.rb +22 -0
  467. data/db/migrate/20260528000002_create_page_variants.rb +20 -0
  468. data/db/migrate/20260709090000_create_snapshots.rb +154 -0
  469. data/db/migrate/20260726130000_create_site_scripts.rb +11 -0
  470. data/lib/generators/postnhost/install/install_generator.rb +55 -0
  471. data/lib/generators/postnhost/install/templates/carrierwave.rb +17 -0
  472. data/lib/generators/postnhost/install/templates/postnhost.rb +36 -0
  473. data/lib/generators/postnhost/locale/locale_generator.rb +36 -0
  474. data/lib/generators/postnhost/static_pages/static_pages_generator.rb +47 -0
  475. data/lib/generators/postnhost/tailwindcss/install_generator.rb +100 -0
  476. data/lib/generators/postnhost/tailwindcss/templates/host.tailwind.css +4 -0
  477. data/lib/generators/postnhost/user/user_generator.rb +36 -0
  478. data/lib/generators/postnhost/views/views_generator.rb +140 -0
  479. data/lib/postnhost/configuration.rb +64 -0
  480. data/lib/postnhost/css_build.rb +68 -0
  481. data/lib/postnhost/css_scope.rb +77 -0
  482. data/lib/postnhost/engine.rb +57 -0
  483. data/lib/postnhost/sample_data.rb +865 -0
  484. data/lib/postnhost/settings/i18n_overrides.rb +137 -0
  485. data/lib/postnhost/settings/i18n_patch.rb +18 -0
  486. data/lib/postnhost/tailwind_compiler.rb +75 -0
  487. data/lib/postnhost/tailwind_input.rb +32 -0
  488. data/lib/postnhost/version.rb +1 -3
  489. data/lib/postnhost.rb +26 -3
  490. data/lib/tasks/postnhost_snapshots.rake +77 -0
  491. data/lib/tasks/postnhost_tasks.rake +71 -0
  492. metadata +679 -21
  493. data/.github/workflows/main.yml +0 -27
  494. data/.gitignore +0 -11
  495. data/.rspec +0 -3
  496. data/.rubocop.yml +0 -8
  497. data/Gemfile +0 -13
  498. data/bin/console +0 -11
  499. data/bin/setup +0 -8
  500. data/postnhost.gemspec +0 -31
@@ -0,0 +1,225 @@
1
+ <div data-controller="tiptap-editor"
2
+ data-tiptap-editor-content-value="<%= content %>"
3
+ data-tiptap-editor-resource-type-value="<%= resource_type %>"
4
+ data-tiptap-editor-resource-id-value="<%= resource_id %>"
5
+ data-tiptap-editor-mount-path-value="<%= postnhost.root_path %>"
6
+ class="relative">
7
+ <!-- Toolbar -->
8
+ <div class="sticky top-0 z-20 bg-white border-b border-gray-100 px-6 py-1">
9
+ <div class="flex flex-wrap gap-1 items-center justify-between">
10
+ <div class="flex flex-wrap gap-1 items-center">
11
+ <!-- Undo/Redo -->
12
+ <button type="button" data-action="undo" class="p-2 text-gray-500 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors duration-200">
13
+ <%= icon("undo", css_class: "w-5 h-5", subfolder: "tiptap") %>
14
+ </button>
15
+ <button type="button" data-action="redo" class="p-2 text-gray-500 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors duration-200">
16
+ <%= icon("redo", css_class: "w-5 h-5", subfolder: "tiptap") %>
17
+ </button>
18
+
19
+ <!-- Headings -->
20
+ <button type="button" data-action="heading-2" class="p-2 text-gray-500 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors duration-200">
21
+ <%= icon("heading-2", css_class: "w-5 h-5", subfolder: "tiptap") %>
22
+ </button>
23
+
24
+ <button type="button" data-action="heading-3" class="p-2 text-gray-500 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors duration-200">
25
+ <%= icon("heading-3", css_class: "w-5 h-5", subfolder: "tiptap") %>
26
+ </button>
27
+
28
+ <!-- Lists -->
29
+ <button type="button" data-action="bullet-list" class="p-2 text-gray-500 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors duration-200">
30
+ <%= icon("bullet-list", css_class: "w-5 h-5", subfolder: "tiptap") %>
31
+ </button>
32
+
33
+ <button type="button" data-action="ordered-list" class="p-2 text-gray-500 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors duration-200">
34
+ <%= icon("ordered-list", css_class: "w-5 h-5", subfolder: "tiptap") %>
35
+ </button>
36
+
37
+ <!-- Blockquote -->
38
+ <button type="button" data-action="blockquote" class="p-2 text-gray-500 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors duration-200">
39
+ <%= icon("blockquote", css_class: "w-5 h-5", subfolder: "tiptap") %>
40
+ </button>
41
+
42
+ <!-- Text Formatting -->
43
+ <button type="button" data-action="bold" class="p-2 text-gray-500 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors duration-200">
44
+ <%= icon("bold", css_class: "w-5 h-5", subfolder: "tiptap") %>
45
+ </button>
46
+
47
+ <button type="button" data-action="italic" class="p-2 text-gray-500 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors duration-200">
48
+ <%= icon("italic", css_class: "w-5 h-5", subfolder: "tiptap") %>
49
+ </button>
50
+
51
+ <button type="button" data-action="strike" class="p-2 text-gray-500 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors duration-200">
52
+ <%= icon("strike", css_class: "w-5 h-5", subfolder: "tiptap") %>
53
+ </button>
54
+
55
+ <!-- Code Block -->
56
+ <button type="button" data-action="code-block" class="p-2 text-gray-500 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors duration-200" title="Code block" aria-label="Code block">
57
+ <%= icon("code-block", css_class: "w-5 h-5", subfolder: "tiptap") %>
58
+ </button>
59
+
60
+ <button type="button" data-action="code" class="p-2 text-gray-500 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors duration-200">
61
+ <%= icon("code", css_class: "w-5 h-5", subfolder: "tiptap") %>
62
+ </button>
63
+
64
+ <button type="button" data-action="underline" class="p-2 text-gray-500 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors duration-200">
65
+ <%= icon("underline", css_class: "w-5 h-5", subfolder: "tiptap") %>
66
+ </button>
67
+
68
+ <button type="button" data-action="link" class="p-2 text-gray-500 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors duration-200">
69
+ <%= icon("link", css_class: "w-5 h-5", subfolder: "tiptap") %>
70
+ </button>
71
+
72
+ <button type="button" data-action="image" class="p-2 text-gray-500 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors duration-200" title="Upload image">
73
+ <%= icon("image-add", css_class: "w-5 h-5", subfolder: "tiptap") %>
74
+ </button>
75
+
76
+ <button type="button" data-action="youtube" class="p-2 text-gray-500 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors duration-200" title="Embed YouTube video">
77
+ <%= icon("youtube", css_class: "w-5 h-5", subfolder: "tiptap") %>
78
+ </button>
79
+
80
+ <!-- Table Dropdown -->
81
+ <div class="relative" data-controller="dropdown">
82
+ <button type="button" data-action="click->dropdown#toggle table" class="p-2 text-gray-500 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors duration-200" title="Table">
83
+ <%= icon("table", css_class: "w-5 h-5", subfolder: "tiptap") %>
84
+ </button>
85
+
86
+ <div data-dropdown-target="menu" class="hidden absolute left-0 mt-2 w-48 rounded-lg shadow-lg bg-white border border-gray-200 z-50">
87
+ <div class="py-1" role="none">
88
+ <button type="button" class="text-gray-700 block w-full text-left px-4 py-2 text-sm hover:bg-gray-100" data-action="click->tiptap-editor#insertTable click->dropdown#close" role="menuitem">Insert Table</button>
89
+ <button type="button" class="text-gray-700 block w-full text-left px-4 py-2 text-sm hover:bg-gray-100" data-action="click->tiptap-editor#deleteTable click->dropdown#close" role="menuitem">Delete Table</button>
90
+ <button type="button" class="text-gray-700 block w-full text-left px-4 py-2 text-sm hover:bg-gray-100" data-action="click->tiptap-editor#addColumnAfter click->dropdown#close" role="menuitem">Add Column</button>
91
+ <button type="button" class="text-gray-700 block w-full text-left px-4 py-2 text-sm hover:bg-gray-100" data-action="click->tiptap-editor#deleteColumn click->dropdown#close" role="menuitem">Delete Column</button>
92
+ <button type="button" class="text-gray-700 block w-full text-left px-4 py-2 text-sm hover:bg-gray-100" data-action="click->tiptap-editor#addRowAfter click->dropdown#close" role="menuitem">Add Row</button>
93
+ <button type="button" class="text-gray-700 block w-full text-left px-4 py-2 text-sm hover:bg-gray-100" data-action="click->tiptap-editor#deleteRow click->dropdown#close" role="menuitem">Delete Row</button>
94
+ </div>
95
+ </div>
96
+ </div>
97
+
98
+ <!-- Text Alignment -->
99
+ <button type="button" data-action="align-left" class="p-2 text-gray-500 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors duration-200">
100
+ <%= icon("align-left", css_class: "w-5 h-5", subfolder: "tiptap") %>
101
+ </button>
102
+
103
+ <button type="button" data-action="align-center" class="p-2 text-gray-500 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors duration-200">
104
+ <%= icon("align-center", css_class: "w-5 h-5", subfolder: "tiptap") %>
105
+ </button>
106
+
107
+ <button type="button" data-action="align-right" class="p-2 text-gray-500 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors duration-200">
108
+ <%= icon("align-right", css_class: "w-5 h-5", subfolder: "tiptap") %>
109
+ </button>
110
+ </div>
111
+
112
+ </div>
113
+ </div>
114
+
115
+ <!-- Editor -->
116
+ <div data-tiptap-editor-target="editor" class="min-h-[400px] p-4"></div>
117
+
118
+ <!-- Contextual code language selector -->
119
+ <select
120
+ data-tiptap-editor-target="codeLanguage"
121
+ data-action="change->tiptap-editor#changeCodeLanguage"
122
+ class="absolute z-10 hidden h-8 w-32 cursor-pointer rounded-md border border-slate-600 bg-slate-800 px-2 text-xs font-medium text-slate-200 focus:border-blue-400 focus:outline-none focus:ring-2 focus:ring-blue-400"
123
+ aria-label="Code language"
124
+ title="Code language">
125
+ <option value="plaintext">Plain text</option>
126
+ <option value="bash">Bash</option>
127
+ <option value="c">C</option>
128
+ <option value="cpp">C++</option>
129
+ <option value="csharp">C#</option>
130
+ <option value="css">CSS</option>
131
+ <option value="diff">Diff</option>
132
+ <option value="go">Go</option>
133
+ <option value="html">HTML</option>
134
+ <option value="java">Java</option>
135
+ <option value="javascript">JavaScript</option>
136
+ <option value="json">JSON</option>
137
+ <option value="kotlin">Kotlin</option>
138
+ <option value="markdown">Markdown</option>
139
+ <option value="php">PHP</option>
140
+ <option value="python">Python</option>
141
+ <option value="ruby">Ruby</option>
142
+ <option value="rust">Rust</option>
143
+ <option value="sql">SQL</option>
144
+ <option value="swift">Swift</option>
145
+ <option value="typescript">TypeScript</option>
146
+ <option value="xml">XML</option>
147
+ <option value="yaml">YAML</option>
148
+ </select>
149
+
150
+ <!-- Link Dropdown -->
151
+ <div data-tiptap-editor-target="linkDropdown" class="absolute z-10 bg-white border border-gray-200 rounded-lg shadow-lg p-2 hidden" style="min-width: 300px;">
152
+ <div class="space-y-3">
153
+ <div class="flex items-center space-x-2">
154
+ <input
155
+ data-tiptap-editor-target="linkInput"
156
+ data-action="keydown->tiptap-editor#handleLinkInputKeydown"
157
+ type="text"
158
+ placeholder="Paste a link..."
159
+ class="flex-1 px-3 py-2 border border-gray-200 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
160
+ autocomplete="off"
161
+ autocorrect="off"
162
+ autocapitalize="off"
163
+ tabindex="-1"
164
+ />
165
+ <button
166
+ data-action="click->tiptap-editor#applyLink"
167
+ type="button"
168
+ title="Apply link"
169
+ class="p-2 text-gray-500 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors duration-200">
170
+ <%= icon("apply-link", css_class: "w-4 h-4", subfolder: "tiptap") %>
171
+ </button>
172
+
173
+ <button
174
+ data-action="click->tiptap-editor#removeLink"
175
+ type="button"
176
+ title="Remove link"
177
+ class="p-2 text-gray-500 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors duration-200">
178
+ <%= icon("remove-link", css_class: "w-4 h-4", subfolder: "tiptap") %>
179
+ </button>
180
+ </div>
181
+ </div>
182
+ </div>
183
+
184
+ <!-- Alt Text Editor -->
185
+ <div data-tiptap-editor-target="altDropdown" class="absolute z-10 hidden text-gray-600">
186
+ <input
187
+ data-tiptap-editor-target="altInput"
188
+ data-action="input->tiptap-editor#handleAltInputChange keydown->tiptap-editor#handleAltInputKeydown blur->tiptap-editor#applyAlt"
189
+ type="text"
190
+ placeholder="Add alt text..."
191
+ class="w-full px-2 py-1 text-sm text-gray-600 focus:outline-none bg-transparent"
192
+ autocomplete="off"
193
+ tabindex="-1"
194
+ />
195
+ </div>
196
+
197
+ <!-- YouTube Dropdown -->
198
+ <div data-tiptap-editor-target="youtubeDropdown" class="absolute z-10 bg-white border border-gray-200 rounded-lg shadow-lg p-2 hidden" style="min-width: 300px;">
199
+ <div class="space-y-3">
200
+ <div class="flex items-center space-x-2">
201
+ <input
202
+ data-tiptap-editor-target="youtubeInput"
203
+ data-action="keydown->tiptap-editor#handleYoutubeInputKeydown"
204
+ type="text"
205
+ placeholder="Paste YouTube URL..."
206
+ class="flex-1 px-3 py-2 border border-gray-200 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
207
+ autocomplete="off"
208
+ autocorrect="off"
209
+ autocapitalize="off"
210
+ tabindex="-1"
211
+ />
212
+ <button
213
+ data-action="click->tiptap-editor#applyYoutube"
214
+ type="button"
215
+ title="Embed video"
216
+ class="p-2 text-gray-500 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors duration-200">
217
+ <%= icon("apply-link", css_class: "w-4 h-4", subfolder: "tiptap") %>
218
+ </button>
219
+ </div>
220
+ </div>
221
+ </div>
222
+
223
+ <!-- Hidden field for form submission -->
224
+ <%= form.hidden_field :content, data: { tiptap_editor_target: "content" } %>
225
+ </div>
@@ -0,0 +1,9 @@
1
+ <div class="px-6 py-6 border-b border-gray-100" data-controller="title-field">
2
+ <%= form.text_area :title,
3
+ class: "w-full text-3xl font-bold text-gray-900 placeholder-gray-400 border-none outline-none focus:ring-0 p-0 m-0 bg-transparent resize-none overflow-hidden leading-snug block",
4
+ placeholder: placeholder,
5
+ rows: 1,
6
+ style: "field-sizing: content;",
7
+ data: { auto_save_target: "titleField", publish_guard_target: "titleField", action: "keydown->title-field#preventNewLine input->publish-guard#sync" },
8
+ autocomplete: "off" %>
9
+ </div>
@@ -0,0 +1,35 @@
1
+ <div class="w-1/5 bg-white border border-gray-200 p-6 hidden md:block border-t-0 sticky top-0 self-start min-h-screen">
2
+ <div class="space-y-4">
3
+ <%= link_to versions_article_path(article), class: "w-full bg-white border border-gray-300 text-gray-700 py-2.5 px-4 rounded-xl hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-900 focus:ring-offset-2 transition duration-200 text-sm font-medium text-center block cursor-pointer" do %>
4
+ <div class="flex items-center justify-center space-x-2">
5
+ <%= icon("arrow-left", css_class: "w-4 h-4") %>
6
+ <span>Back to Versions</span>
7
+ </div>
8
+ <% end %>
9
+
10
+ <%= link_to rollback_article_path(article, version_id: version.id),
11
+ method: :post,
12
+ data: {
13
+ turbo_method: :post,
14
+ turbo_confirm: "Are you sure you want to restore this version? Your current changes will be overwritten."
15
+ },
16
+ class: "w-full bg-gray-900 text-white py-2.5 px-4 rounded-xl hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-gray-900 focus:ring-offset-2 transition duration-200 text-sm font-medium text-center block cursor-pointer" do %>
17
+ <div class="flex items-center justify-center space-x-2">
18
+ <%= icon("clock", css_class: "w-4 h-4") %>
19
+ <span>Restore This Version</span>
20
+ </div>
21
+ <% end %>
22
+
23
+ <div class="pt-4 border-t border-gray-100">
24
+ <h4 class="text-sm font-semibold text-gray-900 mb-3">Version Information</h4>
25
+ <div class="text-sm text-gray-600 space-y-1">
26
+ <p><strong>Created:</strong> <%= admin_datetime(version.created_at) %></p>
27
+ <% if version.id == article.article_snapshot&.paper_trail_version_id %>
28
+ <span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
29
+ Published Version
30
+ </span>
31
+ <% end %>
32
+ </div>
33
+ </div>
34
+ </div>
35
+ </div>
@@ -0,0 +1,20 @@
1
+ <%
2
+ # Button partial - renders an actual button element
3
+ #
4
+ # Parameters:
5
+ # text: (required) Button text
6
+ # variant: :primary (default), :secondary, :danger
7
+ # icon: Icon name (optional)
8
+ # disabled: Boolean (optional)
9
+ # data: Data attributes (optional)
10
+ # extra_class: Additional CSS classes (optional)
11
+ %>
12
+
13
+ <button type="button"
14
+ class="<%= button_classes(variant: local_assigns[:variant] || :primary, disabled: local_assigns[:disabled] || false) %> <%= local_assigns[:extra_class] %>"
15
+ <%= 'disabled' if local_assigns[:disabled] %>>
16
+ <% if local_assigns[:icon] %>
17
+ <%= icon(icon, css_class: "w-4 h-4 mr-2") %>
18
+ <% end %>
19
+ <%= text %>
20
+ </button>
@@ -0,0 +1,24 @@
1
+ <%
2
+ # Button link partial - renders a link styled as a button
3
+ #
4
+ # Parameters:
5
+ # text: (required) Button text
6
+ # url: (required) Button URL
7
+ # variant: :primary (default), :secondary, :danger
8
+ # icon: Icon name (optional)
9
+ # method: HTTP method (optional)
10
+ # data: Data attributes (optional)
11
+ # extra_class: Additional CSS classes (optional)
12
+ # target: Link target (optional, e.g. '_blank')
13
+ %>
14
+
15
+ <%= link_to url,
16
+ class: "#{button_classes(variant: local_assigns[:variant] || :primary)} #{local_assigns[:extra_class]}",
17
+ method: local_assigns[:method],
18
+ target: local_assigns[:target],
19
+ data: local_assigns[:data] do %>
20
+ <% if local_assigns[:icon] %>
21
+ <%= icon(icon, css_class: "w-4 h-4 mr-2") %>
22
+ <% end %>
23
+ <%= text %>
24
+ <% end %>
@@ -0,0 +1,22 @@
1
+ <%
2
+ # Empty state partial for "no items" states
3
+ #
4
+ # Parameters:
5
+ # icon: (required) Icon name
6
+ # title: (required) Title text
7
+ # description: (required) Description text
8
+ # actions block: Optional action buttons
9
+ %>
10
+
11
+ <div class="text-center py-12">
12
+ <div class="mx-auto h-12 w-12 text-gray-400">
13
+ <%= icon(icon, css_class: "w-full h-full") %>
14
+ </div>
15
+ <h3 class="mt-2 text-sm font-medium text-gray-900"><%= title %></h3>
16
+ <p class="mt-1 text-sm text-gray-500"><%= description %></p>
17
+ <% if block_given? %>
18
+ <div class="mt-6">
19
+ <%= yield %>
20
+ </div>
21
+ <% end %>
22
+ </div>
@@ -0,0 +1,28 @@
1
+ <%
2
+ # Error messages partial for form validation errors
3
+ #
4
+ # Parameters:
5
+ # resource: (required) ActiveRecord model instance with errors
6
+ %>
7
+
8
+ <% if resource.errors.any? %>
9
+ <div class="<%= error_container_classes %>">
10
+ <div class="flex">
11
+ <div class="flex-shrink-0">
12
+ <%= icon("alert-circle", css_class: "h-5 w-5 text-red-400") %>
13
+ </div>
14
+ <div class="ml-3">
15
+ <h3 class="text-sm font-medium text-red-800">
16
+ <%= pluralize(resource.errors.count, "error") %> prohibited this <%= resource.model_name.human.downcase %> from being saved:
17
+ </h3>
18
+ <div class="mt-2 text-sm text-red-700">
19
+ <ul class="list-disc pl-5 space-y-1">
20
+ <% resource.errors.full_messages.each do |message| %>
21
+ <li><%= message %></li>
22
+ <% end %>
23
+ </ul>
24
+ </div>
25
+ </div>
26
+ </div>
27
+ </div>
28
+ <% end %>
@@ -0,0 +1,10 @@
1
+ <%
2
+ # Form card partial for form field containers
3
+ #
4
+ # Parameters:
5
+ # content block: Form fields
6
+ %>
7
+
8
+ <div class="<%= form_card_classes %>">
9
+ <%= yield %>
10
+ </div>
@@ -0,0 +1,21 @@
1
+ <%
2
+ # Form page header partial for new/edit pages
3
+ #
4
+ # Parameters:
5
+ # title: (required) Page title
6
+ # description: (required) Page description
7
+ # actions block: Action buttons (optional)
8
+ %>
9
+
10
+ <div class="mb-8 flex flex-col md:flex-row md:items-start md:justify-between gap-4">
11
+ <div>
12
+ <h1 class="text-3xl font-bold text-gray-900"><%= title %></h1>
13
+ <p class="mt-2 text-gray-600"><%= description %></p>
14
+ </div>
15
+
16
+ <% if block_given? %>
17
+ <div class="flex flex-col space-y-3 md:flex-row md:items-center md:space-y-0 md:space-x-3 w-full md:w-auto shrink-0">
18
+ <%= yield %>
19
+ </div>
20
+ <% end %>
21
+ </div>
@@ -0,0 +1,17 @@
1
+ <%
2
+ # Icon button partial for action icons (edit, view, delete)
3
+ #
4
+ # Parameters:
5
+ # icon: (required) Icon name
6
+ # url: (required) Button URL
7
+ # variant: :default, :danger
8
+ # method: HTTP method for link_to
9
+ # data: Data attributes hash
10
+ %>
11
+
12
+ <%= link_to url,
13
+ class: icon_button_classes(variant: local_assigns[:variant] || :default),
14
+ method: local_assigns[:method],
15
+ data: local_assigns[:data] do %>
16
+ <%= icon(icon, css_class: "w-4 h-4") %>
17
+ <% end %>
@@ -0,0 +1,40 @@
1
+ <%
2
+ # Page header partial for consistent page titles
3
+ #
4
+ # Parameters:
5
+ # title: (required) Page title
6
+ # description: Subtitle/description text
7
+ # stats: Array of stats hashes [{text: "...", class: "..."}]
8
+ # actions block: Action buttons
9
+ %>
10
+
11
+ <div class="<%= page_header_classes %>">
12
+ <div>
13
+ <h1 class="text-3xl font-bold text-gray-900"><%= title %></h1>
14
+ <% if defined?(description) && description %>
15
+ <p class="mt-2 text-gray-600 max-w-lg"><%= description %></p>
16
+ <% end %>
17
+ <% if defined?(stats) && stats.any? %>
18
+ <div class="mt-2 flex items-center space-x-4 text-sm text-gray-500">
19
+ <% stats.each do |stat| %>
20
+ <% if stat[:url] %>
21
+ <%= link_to stat[:url], class: "#{stat[:class]} hover:text-gray-700 inline-flex items-center cursor-pointer", target: stat[:target] do %>
22
+ <%= stat[:text] %>
23
+ <% if stat[:icon] %>
24
+ <%= icon(stat[:icon], css_class: "w-3.5 h-3.5 ml-1") %>
25
+ <% end %>
26
+ <% end %>
27
+ <% else %>
28
+ <span class="<%= stat[:class] %>"><%= stat[:text] %></span>
29
+ <% end %>
30
+ <% end %>
31
+ </div>
32
+ <% end %>
33
+ </div>
34
+
35
+ <% if block_given? %>
36
+ <div class="flex flex-col space-y-3 md:flex-row md:items-center md:space-y-0 md:space-x-3 w-full md:w-auto">
37
+ <%= yield %>
38
+ </div>
39
+ <% end %>
40
+ </div>
@@ -0,0 +1,23 @@
1
+ <%
2
+ # Table container partial for consistent table layouts
3
+ #
4
+ # Parameters:
5
+ # headers: Array of header hashes [{text: "...", class: "..."}]
6
+ # header_grid_class: Optional grid classes for the header row (default: 12-column grid)
7
+ # content block: Table body content
8
+ header_grid_class = local_assigns.fetch(:header_grid_class, 'grid grid-cols-12 gap-4')
9
+ %>
10
+
11
+ <div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
12
+ <div class="px-6 py-4 border-b border-gray-200 bg-gray-50">
13
+ <div class="<%= header_grid_class %> text-xs font-medium text-gray-500 uppercase tracking-wider">
14
+ <% headers.each do |header| %>
15
+ <div class="<%= header[:class] %>"><%= header[:text] %></div>
16
+ <% end %>
17
+ </div>
18
+ </div>
19
+
20
+ <div class="divide-y divide-gray-200">
21
+ <%= yield %>
22
+ </div>
23
+ </div>
@@ -0,0 +1,84 @@
1
+ <div class="order-2 md:order-3 hidden md:block w-full md:w-1/5 bg-white border border-gray-200 p-4 md:p-6 border-t-0 md:sticky md:top-0 md:self-start md:min-h-screen"
2
+ data-editor-mobile-sidebars-target="<%= local_assigns[:mobile_target] %>">
3
+ <div class="md:hidden flex items-center justify-between mb-4">
4
+ <h3 class="text-sm font-semibold text-gray-900">Actions</h3>
5
+ <button type="button"
6
+ class="text-gray-500 hover:text-gray-700 cursor-pointer"
7
+ data-action="click->editor-mobile-sidebars#closeActions">
8
+ <%= icon("close", css_class: "w-4 h-4") %>
9
+ </button>
10
+ </div>
11
+ <div class="space-y-4">
12
+ <button
13
+ type="button"
14
+ data-auto-save-target="saveButton"
15
+ data-action="click->auto-save#save"
16
+ disabled
17
+ class="w-full bg-white border border-gray-300 text-gray-700 py-2.5 px-4 rounded-xl hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-900 focus:ring-offset-2 transition duration-200 text-sm font-medium cursor-pointer opacity-50">
18
+ Saved
19
+ </button>
20
+
21
+ <% if is_variant %>
22
+ <%= link_to edit_page_path(page), class: "w-full bg-white border border-gray-300 text-gray-700 py-2.5 px-4 rounded-xl hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-900 focus:ring-offset-2 transition duration-200 text-sm font-medium text-center block cursor-pointer" do %>
23
+ <div class="flex items-center justify-center space-x-2">
24
+ <%= icon("arrow-left", css_class: "w-4 h-4") %>
25
+ <span>Back to Original</span>
26
+ </div>
27
+ <% end %>
28
+
29
+ <div class="border-b border-gray-100 pb-4">
30
+ <%= link_to page_variants_path(page), data: { turbo_prefetch: false }, class: "w-full bg-white border border-gray-300 text-gray-700 py-2.5 px-4 rounded-xl hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-900 focus:ring-offset-2 transition duration-200 text-sm font-medium text-center block cursor-pointer" do %>
31
+ <div class="flex items-center justify-center space-x-2">
32
+ <%= icon("language", css_class: "w-4 h-4") %>
33
+ <span>All Translations</span>
34
+ </div>
35
+ <% end %>
36
+ </div>
37
+ <% else %>
38
+ <div class="border-b border-gray-100 pb-4">
39
+ <%= link_to page_variants_path(page), data: { turbo_prefetch: false }, class: "w-full bg-white border border-gray-300 text-gray-700 py-2.5 px-4 rounded-xl hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-900 focus:ring-offset-2 transition duration-200 text-sm font-medium text-center block" do %>
40
+ <div class="flex items-center justify-center space-x-2">
41
+ <%= icon("language", css_class: "w-4 h-4") %>
42
+ <span>Translations</span>
43
+ <% if page.page_variants.any? %>
44
+ <span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-200 text-gray-800">
45
+ <%= page.page_variants_count %>
46
+ </span>
47
+ <% end %>
48
+ </div>
49
+ <% end %>
50
+ </div>
51
+ <% end %>
52
+
53
+ <%= render "postnhost/shared/pages/post_actions",
54
+ resource: resource,
55
+ resource_type: (is_variant ? "variant" : "page"),
56
+ page: page %>
57
+
58
+ <div class="pt-4">
59
+ <% if is_variant %>
60
+ <%= render "postnhost/shared/elements/button_link",
61
+ text: "Delete",
62
+ url: page_variant_path(page, resource),
63
+ variant: :danger,
64
+ icon: "trash",
65
+ extra_class: "w-full",
66
+ data: {
67
+ turbo_method: :delete,
68
+ turbo_confirm: "Are you sure you want to delete this variant? This action cannot be undone."
69
+ } %>
70
+ <% else %>
71
+ <%= render "postnhost/shared/elements/button_link",
72
+ text: "Delete",
73
+ url: resource,
74
+ variant: :danger,
75
+ icon: "trash",
76
+ extra_class: "w-full",
77
+ data: {
78
+ turbo_method: :delete,
79
+ turbo_confirm: "Are you sure you want to delete this page? This action cannot be undone."
80
+ } %>
81
+ <% end %>
82
+ </div>
83
+ </div>
84
+ </div>