focomy 0.1.115__tar.gz → 0.1.117__tar.gz

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 (211) hide show
  1. {focomy-0.1.115 → focomy-0.1.117}/PKG-INFO +1 -1
  2. {focomy-0.1.115 → focomy-0.1.117}/core/admin/routes.py +32 -0
  3. {focomy-0.1.115 → focomy-0.1.117}/core/engine/routes.py +53 -0
  4. {focomy-0.1.115 → focomy-0.1.117}/core/services/theme.py +100 -12
  5. {focomy-0.1.115 → focomy-0.1.117}/core/templates/admin/customize.html +189 -9
  6. {focomy-0.1.115 → focomy-0.1.117}/core/templates/admin/entity_form.html +29 -0
  7. {focomy-0.1.115 → focomy-0.1.117}/pyproject.toml +1 -1
  8. focomy-0.1.117/themes/default/customizations.json +22 -0
  9. {focomy-0.1.115 → focomy-0.1.117}/.gitignore +0 -0
  10. {focomy-0.1.115 → focomy-0.1.117}/LICENSE +0 -0
  11. {focomy-0.1.115 → focomy-0.1.117}/README.md +0 -0
  12. {focomy-0.1.115 → focomy-0.1.117}/core/__init__.py +0 -0
  13. {focomy-0.1.115 → focomy-0.1.117}/core/admin/__init__.py +0 -0
  14. {focomy-0.1.115 → focomy-0.1.117}/core/admin/url.py +0 -0
  15. {focomy-0.1.115 → focomy-0.1.117}/core/api/__init__.py +0 -0
  16. {focomy-0.1.115 → focomy-0.1.117}/core/api/auth.py +0 -0
  17. {focomy-0.1.115 → focomy-0.1.117}/core/api/comments.py +0 -0
  18. {focomy-0.1.115 → focomy-0.1.117}/core/api/entities.py +0 -0
  19. {focomy-0.1.115 → focomy-0.1.117}/core/api/forms.py +0 -0
  20. {focomy-0.1.115 → focomy-0.1.117}/core/api/media.py +0 -0
  21. {focomy-0.1.115 → focomy-0.1.117}/core/api/relations.py +0 -0
  22. {focomy-0.1.115 → focomy-0.1.117}/core/api/revisions.py +0 -0
  23. {focomy-0.1.115 → focomy-0.1.117}/core/api/schema.py +0 -0
  24. {focomy-0.1.115 → focomy-0.1.117}/core/api/search.py +0 -0
  25. {focomy-0.1.115 → focomy-0.1.117}/core/api/seo.py +0 -0
  26. {focomy-0.1.115 → focomy-0.1.117}/core/cli.py +0 -0
  27. {focomy-0.1.115 → focomy-0.1.117}/core/config.py +0 -0
  28. {focomy-0.1.115 → focomy-0.1.117}/core/content_types/__init__.py +0 -0
  29. {focomy-0.1.115 → focomy-0.1.117}/core/content_types/audit_log.yaml +0 -0
  30. {focomy-0.1.115 → focomy-0.1.117}/core/content_types/category.yaml +0 -0
  31. {focomy-0.1.115 → focomy-0.1.117}/core/content_types/channel.yaml +0 -0
  32. {focomy-0.1.115 → focomy-0.1.117}/core/content_types/comment.yaml +0 -0
  33. {focomy-0.1.115 → focomy-0.1.117}/core/content_types/edit_lock.yaml +0 -0
  34. {focomy-0.1.115 → focomy-0.1.117}/core/content_types/form.yaml +0 -0
  35. {focomy-0.1.115 → focomy-0.1.117}/core/content_types/form_submission.yaml +0 -0
  36. {focomy-0.1.115 → focomy-0.1.117}/core/content_types/menu_item.yaml +0 -0
  37. {focomy-0.1.115 → focomy-0.1.117}/core/content_types/news.yaml +0 -0
  38. {focomy-0.1.115 → focomy-0.1.117}/core/content_types/page.yaml +0 -0
  39. {focomy-0.1.115 → focomy-0.1.117}/core/content_types/plugin.yaml +0 -0
  40. {focomy-0.1.115 → focomy-0.1.117}/core/content_types/plugin_developer.yaml +0 -0
  41. {focomy-0.1.115 → focomy-0.1.117}/core/content_types/plugin_review.yaml +0 -0
  42. {focomy-0.1.115 → focomy-0.1.117}/core/content_types/post.yaml +0 -0
  43. {focomy-0.1.115 → focomy-0.1.117}/core/content_types/redirect.yaml +0 -0
  44. {focomy-0.1.115 → focomy-0.1.117}/core/content_types/scheduled_action.yaml +0 -0
  45. {focomy-0.1.115 → focomy-0.1.117}/core/content_types/series.yaml +0 -0
  46. {focomy-0.1.115 → focomy-0.1.117}/core/content_types/site_setting.yaml +0 -0
  47. {focomy-0.1.115 → focomy-0.1.117}/core/content_types/tag.yaml +0 -0
  48. {focomy-0.1.115 → focomy-0.1.117}/core/content_types/user.yaml +0 -0
  49. {focomy-0.1.115 → focomy-0.1.117}/core/content_types/widget.yaml +0 -0
  50. {focomy-0.1.115 → focomy-0.1.117}/core/content_types/workflow_history.yaml +0 -0
  51. {focomy-0.1.115 → focomy-0.1.117}/core/database.py +0 -0
  52. {focomy-0.1.115 → focomy-0.1.117}/core/engine/__init__.py +0 -0
  53. {focomy-0.1.115 → focomy-0.1.117}/core/main.py +0 -0
  54. {focomy-0.1.115 → focomy-0.1.117}/core/migrations/env.py +0 -0
  55. {focomy-0.1.115 → focomy-0.1.117}/core/migrations/script.py.mako +0 -0
  56. {focomy-0.1.115 → focomy-0.1.117}/core/migrations/versions/2038bdf6693b_add_import_jobs_table.py +0 -0
  57. {focomy-0.1.115 → focomy-0.1.117}/core/migrations/versions/3a1b2c3d4e5f_add_file_hash_to_media.py +0 -0
  58. {focomy-0.1.115 → focomy-0.1.117}/core/models/__init__.py +0 -0
  59. {focomy-0.1.115 → focomy-0.1.117}/core/models/auth.py +0 -0
  60. {focomy-0.1.115 → focomy-0.1.117}/core/models/entity.py +0 -0
  61. {focomy-0.1.115 → focomy-0.1.117}/core/models/import_job.py +0 -0
  62. {focomy-0.1.115 → focomy-0.1.117}/core/models/media.py +0 -0
  63. {focomy-0.1.115 → focomy-0.1.117}/core/models/relation.py +0 -0
  64. {focomy-0.1.115 → focomy-0.1.117}/core/models/revision.py +0 -0
  65. {focomy-0.1.115 → focomy-0.1.117}/core/plugins/__init__.py +0 -0
  66. {focomy-0.1.115 → focomy-0.1.117}/core/plugins/base.py +0 -0
  67. {focomy-0.1.115 → focomy-0.1.117}/core/plugins/hooks.py +0 -0
  68. {focomy-0.1.115 → focomy-0.1.117}/core/plugins/loader.py +0 -0
  69. {focomy-0.1.115 → focomy-0.1.117}/core/plugins/manager.py +0 -0
  70. {focomy-0.1.115 → focomy-0.1.117}/core/rate_limit.py +0 -0
  71. {focomy-0.1.115 → focomy-0.1.117}/core/relations.yaml +0 -0
  72. {focomy-0.1.115 → focomy-0.1.117}/core/scaffold/.env.template +0 -0
  73. {focomy-0.1.115 → focomy-0.1.117}/core/scaffold/.gitignore.template +0 -0
  74. {focomy-0.1.115 → focomy-0.1.117}/core/scaffold/__init__.py +0 -0
  75. {focomy-0.1.115 → focomy-0.1.117}/core/scaffold/config.yaml.template +0 -0
  76. {focomy-0.1.115 → focomy-0.1.117}/core/scaffold/themes/default/templates/archive.html +0 -0
  77. {focomy-0.1.115 → focomy-0.1.117}/core/scaffold/themes/default/templates/base.html +0 -0
  78. {focomy-0.1.115 → focomy-0.1.117}/core/scaffold/themes/default/templates/category.html +0 -0
  79. {focomy-0.1.115 → focomy-0.1.117}/core/scaffold/themes/default/templates/home.html +0 -0
  80. {focomy-0.1.115 → focomy-0.1.117}/core/scaffold/themes/default/templates/post.html +0 -0
  81. {focomy-0.1.115 → focomy-0.1.117}/core/scaffold/themes/default/templates/search.html +0 -0
  82. {focomy-0.1.115 → focomy-0.1.117}/core/scaffold/themes/default/theme.yaml +0 -0
  83. {focomy-0.1.115 → focomy-0.1.117}/core/schemas/__init__.py +0 -0
  84. {focomy-0.1.115 → focomy-0.1.117}/core/schemas/import_schema.py +0 -0
  85. {focomy-0.1.115 → focomy-0.1.117}/core/seo/__init__.py +0 -0
  86. {focomy-0.1.115 → focomy-0.1.117}/core/services/__init__.py +0 -0
  87. {focomy-0.1.115 → focomy-0.1.117}/core/services/api_auth.py +0 -0
  88. {focomy-0.1.115 → focomy-0.1.117}/core/services/assets.py +0 -0
  89. {focomy-0.1.115 → focomy-0.1.117}/core/services/audit.py +0 -0
  90. {focomy-0.1.115 → focomy-0.1.117}/core/services/auth.py +0 -0
  91. {focomy-0.1.115 → focomy-0.1.117}/core/services/block_converter.py +0 -0
  92. {focomy-0.1.115 → focomy-0.1.117}/core/services/bulk.py +0 -0
  93. {focomy-0.1.115 → focomy-0.1.117}/core/services/cache.py +0 -0
  94. {focomy-0.1.115 → focomy-0.1.117}/core/services/channel.py +0 -0
  95. {focomy-0.1.115 → focomy-0.1.117}/core/services/cleanup.py +0 -0
  96. {focomy-0.1.115 → focomy-0.1.117}/core/services/comment.py +0 -0
  97. {focomy-0.1.115 → focomy-0.1.117}/core/services/config_priority.py +0 -0
  98. {focomy-0.1.115 → focomy-0.1.117}/core/services/deployment.py +0 -0
  99. {focomy-0.1.115 → focomy-0.1.117}/core/services/edit_lock.py +0 -0
  100. {focomy-0.1.115 → focomy-0.1.117}/core/services/entity.py +0 -0
  101. {focomy-0.1.115 → focomy-0.1.117}/core/services/export.py +0 -0
  102. {focomy-0.1.115 → focomy-0.1.117}/core/services/field.py +0 -0
  103. {focomy-0.1.115 → focomy-0.1.117}/core/services/formula.py +0 -0
  104. {focomy-0.1.115 → focomy-0.1.117}/core/services/i18n.py +0 -0
  105. {focomy-0.1.115 → focomy-0.1.117}/core/services/index.py +0 -0
  106. {focomy-0.1.115 → focomy-0.1.117}/core/services/invite.py +0 -0
  107. {focomy-0.1.115 → focomy-0.1.117}/core/services/link_validator.py +0 -0
  108. {focomy-0.1.115 → focomy-0.1.117}/core/services/logging.py +0 -0
  109. {focomy-0.1.115 → focomy-0.1.117}/core/services/mail.py +0 -0
  110. {focomy-0.1.115 → focomy-0.1.117}/core/services/marketplace.py +0 -0
  111. {focomy-0.1.115 → focomy-0.1.117}/core/services/marketplace_verify.py +0 -0
  112. {focomy-0.1.115 → focomy-0.1.117}/core/services/media.py +0 -0
  113. {focomy-0.1.115 → focomy-0.1.117}/core/services/media_cleanup.py +0 -0
  114. {focomy-0.1.115 → focomy-0.1.117}/core/services/menu.py +0 -0
  115. {focomy-0.1.115 → focomy-0.1.117}/core/services/migration_helpers.py +0 -0
  116. {focomy-0.1.115 → focomy-0.1.117}/core/services/oauth.py +0 -0
  117. {focomy-0.1.115 → focomy-0.1.117}/core/services/pagination.py +0 -0
  118. {focomy-0.1.115 → focomy-0.1.117}/core/services/plugin_resolver.py +0 -0
  119. {focomy-0.1.115 → focomy-0.1.117}/core/services/plugin_sandbox.py +0 -0
  120. {focomy-0.1.115 → focomy-0.1.117}/core/services/preview.py +0 -0
  121. {focomy-0.1.115 → focomy-0.1.117}/core/services/query_optimizer.py +0 -0
  122. {focomy-0.1.115 → focomy-0.1.117}/core/services/rbac.py +0 -0
  123. {focomy-0.1.115 → focomy-0.1.117}/core/services/redirect.py +0 -0
  124. {focomy-0.1.115 → focomy-0.1.117}/core/services/relation.py +0 -0
  125. {focomy-0.1.115 → focomy-0.1.117}/core/services/revision.py +0 -0
  126. {focomy-0.1.115 → focomy-0.1.117}/core/services/routing.py +0 -0
  127. {focomy-0.1.115 → focomy-0.1.117}/core/services/sanitizer.py +0 -0
  128. {focomy-0.1.115 → focomy-0.1.117}/core/services/schedule.py +0 -0
  129. {focomy-0.1.115 → focomy-0.1.117}/core/services/search.py +0 -0
  130. {focomy-0.1.115 → focomy-0.1.117}/core/services/sentry.py +0 -0
  131. {focomy-0.1.115 → focomy-0.1.117}/core/services/seo.py +0 -0
  132. {focomy-0.1.115 → focomy-0.1.117}/core/services/settings.py +0 -0
  133. {focomy-0.1.115 → focomy-0.1.117}/core/services/spam_filter.py +0 -0
  134. {focomy-0.1.115 → focomy-0.1.117}/core/services/storage.py +0 -0
  135. {focomy-0.1.115 → focomy-0.1.117}/core/services/theme_inheritance.py +0 -0
  136. {focomy-0.1.115 → focomy-0.1.117}/core/services/thumbnail.py +0 -0
  137. {focomy-0.1.115 → focomy-0.1.117}/core/services/update.py +0 -0
  138. {focomy-0.1.115 → focomy-0.1.117}/core/services/widget.py +0 -0
  139. {focomy-0.1.115 → focomy-0.1.117}/core/services/wordpress_import/__init__.py +0 -0
  140. {focomy-0.1.115 → focomy-0.1.117}/core/services/wordpress_import/acf.py +0 -0
  141. {focomy-0.1.115 → focomy-0.1.117}/core/services/wordpress_import/analyzer.py +0 -0
  142. {focomy-0.1.115 → focomy-0.1.117}/core/services/wordpress_import/constants.py +0 -0
  143. {focomy-0.1.115 → focomy-0.1.117}/core/services/wordpress_import/content_sanitizer.py +0 -0
  144. {focomy-0.1.115 → focomy-0.1.117}/core/services/wordpress_import/diff_detector.py +0 -0
  145. {focomy-0.1.115 → focomy-0.1.117}/core/services/wordpress_import/dry_run.py +0 -0
  146. {focomy-0.1.115 → focomy-0.1.117}/core/services/wordpress_import/error_collector.py +0 -0
  147. {focomy-0.1.115 → focomy-0.1.117}/core/services/wordpress_import/id_resolver.py +0 -0
  148. {focomy-0.1.115 → focomy-0.1.117}/core/services/wordpress_import/import_service.py +0 -0
  149. {focomy-0.1.115 → focomy-0.1.117}/core/services/wordpress_import/importer.py +0 -0
  150. {focomy-0.1.115 → focomy-0.1.117}/core/services/wordpress_import/link_fixer.py +0 -0
  151. {focomy-0.1.115 → focomy-0.1.117}/core/services/wordpress_import/media.py +0 -0
  152. {focomy-0.1.115 → focomy-0.1.117}/core/services/wordpress_import/preview.py +0 -0
  153. {focomy-0.1.115 → focomy-0.1.117}/core/services/wordpress_import/redirects.py +0 -0
  154. {focomy-0.1.115 → focomy-0.1.117}/core/services/wordpress_import/rest_client.py +0 -0
  155. {focomy-0.1.115 → focomy-0.1.117}/core/services/wordpress_import/rollback.py +0 -0
  156. {focomy-0.1.115 → focomy-0.1.117}/core/services/wordpress_import/verification.py +0 -0
  157. {focomy-0.1.115 → focomy-0.1.117}/core/services/wordpress_import/wxr_parser.py +0 -0
  158. {focomy-0.1.115 → focomy-0.1.117}/core/services/workflow.py +0 -0
  159. {focomy-0.1.115 → focomy-0.1.117}/core/templates/admin/backup.html +0 -0
  160. {focomy-0.1.115 → focomy-0.1.117}/core/templates/admin/base.html +0 -0
  161. {focomy-0.1.115 → focomy-0.1.117}/core/templates/admin/comments.html +0 -0
  162. {focomy-0.1.115 → focomy-0.1.117}/core/templates/admin/components/editor.html +0 -0
  163. {focomy-0.1.115 → focomy-0.1.117}/core/templates/admin/dashboard.html +0 -0
  164. {focomy-0.1.115 → focomy-0.1.117}/core/templates/admin/entity_list.html +0 -0
  165. {focomy-0.1.115 → focomy-0.1.117}/core/templates/admin/forgot_password.html +0 -0
  166. {focomy-0.1.115 → focomy-0.1.117}/core/templates/admin/import.html +0 -0
  167. {focomy-0.1.115 → focomy-0.1.117}/core/templates/admin/link_validator.html +0 -0
  168. {focomy-0.1.115 → focomy-0.1.117}/core/templates/admin/login.html +0 -0
  169. {focomy-0.1.115 → focomy-0.1.117}/core/templates/admin/media.html +0 -0
  170. {focomy-0.1.115 → focomy-0.1.117}/core/templates/admin/menus.html +0 -0
  171. {focomy-0.1.115 → focomy-0.1.117}/core/templates/admin/plugins.html +0 -0
  172. {focomy-0.1.115 → focomy-0.1.117}/core/templates/admin/redirects.html +0 -0
  173. {focomy-0.1.115 → focomy-0.1.117}/core/templates/admin/reset_password.html +0 -0
  174. {focomy-0.1.115 → focomy-0.1.117}/core/templates/admin/settings.html +0 -0
  175. {focomy-0.1.115 → focomy-0.1.117}/core/templates/admin/sitemap.html +0 -0
  176. {focomy-0.1.115 → focomy-0.1.117}/core/templates/admin/system.html +0 -0
  177. {focomy-0.1.115 → focomy-0.1.117}/core/templates/admin/themes.html +0 -0
  178. {focomy-0.1.115 → focomy-0.1.117}/core/templates/admin/widgets.html +0 -0
  179. {focomy-0.1.115 → focomy-0.1.117}/core/themes/__init__.py +0 -0
  180. {focomy-0.1.115 → focomy-0.1.117}/core/themes/customizer.py +0 -0
  181. {focomy-0.1.115 → focomy-0.1.117}/core/themes/manager.py +0 -0
  182. {focomy-0.1.115 → focomy-0.1.117}/core/themes/marketplace.py +0 -0
  183. {focomy-0.1.115 → focomy-0.1.117}/core/utils.py +0 -0
  184. {focomy-0.1.115 → focomy-0.1.117}/static/favicon.svg +0 -0
  185. {focomy-0.1.115 → focomy-0.1.117}/themes/blog/templates/base.html +0 -0
  186. {focomy-0.1.115 → focomy-0.1.117}/themes/blog/templates/home.html +0 -0
  187. {focomy-0.1.115 → focomy-0.1.117}/themes/blog/templates/post.html +0 -0
  188. {focomy-0.1.115 → focomy-0.1.117}/themes/blog/theme.yaml +0 -0
  189. {focomy-0.1.115 → focomy-0.1.117}/themes/corporate/templates/base.html +0 -0
  190. {focomy-0.1.115 → focomy-0.1.117}/themes/corporate/templates/home.html +0 -0
  191. {focomy-0.1.115 → focomy-0.1.117}/themes/corporate/templates/page.html +0 -0
  192. {focomy-0.1.115 → focomy-0.1.117}/themes/corporate/theme.yaml +0 -0
  193. {focomy-0.1.115 → focomy-0.1.117}/themes/default/templates/404.html +0 -0
  194. {focomy-0.1.115 → focomy-0.1.117}/themes/default/templates/500.html +0 -0
  195. {focomy-0.1.115 → focomy-0.1.117}/themes/default/templates/archive.html +0 -0
  196. {focomy-0.1.115 → focomy-0.1.117}/themes/default/templates/base.html +0 -0
  197. {focomy-0.1.115 → focomy-0.1.117}/themes/default/templates/category.html +0 -0
  198. {focomy-0.1.115 → focomy-0.1.117}/themes/default/templates/channel.html +0 -0
  199. {focomy-0.1.115 → focomy-0.1.117}/themes/default/templates/form.html +0 -0
  200. {focomy-0.1.115 → focomy-0.1.117}/themes/default/templates/form_success.html +0 -0
  201. {focomy-0.1.115 → focomy-0.1.117}/themes/default/templates/home.html +0 -0
  202. {focomy-0.1.115 → focomy-0.1.117}/themes/default/templates/page.html +0 -0
  203. {focomy-0.1.115 → focomy-0.1.117}/themes/default/templates/post.html +0 -0
  204. {focomy-0.1.115 → focomy-0.1.117}/themes/default/templates/search.html +0 -0
  205. {focomy-0.1.115 → focomy-0.1.117}/themes/default/templates/series.html +0 -0
  206. {focomy-0.1.115 → focomy-0.1.117}/themes/default/theme.yaml +0 -0
  207. {focomy-0.1.115 → focomy-0.1.117}/themes/minimal/templates/base.html +0 -0
  208. {focomy-0.1.115 → focomy-0.1.117}/themes/minimal/templates/home.html +0 -0
  209. {focomy-0.1.115 → focomy-0.1.117}/themes/minimal/templates/page.html +0 -0
  210. {focomy-0.1.115 → focomy-0.1.117}/themes/minimal/templates/post.html +0 -0
  211. {focomy-0.1.115 → focomy-0.1.117}/themes/minimal/theme.yaml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: focomy
3
- Version: 0.1.115
3
+ Version: 0.1.117
4
4
  Summary: The Most Beautiful CMS - A metadata-driven, zero-duplicate-code content management system
5
5
  Project-URL: Homepage, https://github.com/focomy/focomy
6
6
  Project-URL: Documentation, https://focomy.dev/docs
@@ -2020,6 +2020,38 @@ async def preview_render(
2020
2020
  return {"html": html}
2021
2021
 
2022
2022
 
2023
+ @router.post("/api/preview/token")
2024
+ async def create_preview_token(
2025
+ request: Request,
2026
+ db: AsyncSession = Depends(get_db),
2027
+ current_user: Entity = Depends(require_admin_api),
2028
+ ):
2029
+ """Create a preview token for an entity."""
2030
+ from ..services.preview import get_preview_service
2031
+
2032
+ try:
2033
+ body = await request.json()
2034
+ except Exception:
2035
+ raise HTTPException(status_code=400, detail="Invalid JSON")
2036
+
2037
+ entity_id = body.get("entity_id")
2038
+ if not entity_id:
2039
+ raise HTTPException(status_code=400, detail="entity_id required")
2040
+
2041
+ # Verify entity exists
2042
+ entity_svc = EntityService(db)
2043
+ entity = await entity_svc.get(entity_id)
2044
+ if not entity:
2045
+ raise HTTPException(status_code=404, detail="Entity not found")
2046
+
2047
+ # Create preview token
2048
+ preview_svc = get_preview_service(db)
2049
+ token = await preview_svc.create_token(entity_id, current_user.id)
2050
+ preview_url = preview_svc.get_preview_url(token)
2051
+
2052
+ return {"token": token, "url": preview_url}
2053
+
2054
+
2023
2055
  @router.get("/system", response_class=HTMLResponse)
2024
2056
  async def system_info(
2025
2057
  request: Request,
@@ -261,6 +261,59 @@ async def sitemap_xml(
261
261
  return Response(content=xml_content, media_type="application/xml")
262
262
 
263
263
 
264
+ # === Preview Routes ===
265
+
266
+
267
+ @router.get("/preview/{token}", response_class=HTMLResponse)
268
+ async def preview_entity(
269
+ token: str,
270
+ request: Request,
271
+ db: AsyncSession = Depends(get_db),
272
+ ):
273
+ """Preview an entity with a valid token (shows draft/unpublished content)."""
274
+ from ..services.preview import get_preview_service
275
+
276
+ preview_svc = get_preview_service(db)
277
+ entity = await preview_svc.get_preview_entity(token)
278
+
279
+ if not entity:
280
+ raise HTTPException(status_code=404, detail="Preview not found or expired")
281
+
282
+ entity_svc = EntityService(db)
283
+ entity_data = entity_svc.serialize(entity)
284
+
285
+ # Get content type info
286
+ content_type = entity.type
287
+ ct = field_service.get_content_type(content_type)
288
+
289
+ # Get site URL and contexts
290
+ site_url = str(request.base_url).rstrip("/")
291
+ menus_ctx = await get_menus_context(db)
292
+ widgets_ctx = await get_widgets_context(db)
293
+ seo_ctx = await get_seo_settings(db, site_url)
294
+
295
+ # Add preview flag to context
296
+ context = {
297
+ "post": entity_data,
298
+ "entity": entity_data,
299
+ "content": entity_data,
300
+ "is_preview": True,
301
+ "content_type": ct,
302
+ **menus_ctx,
303
+ **widgets_ctx,
304
+ **seo_ctx,
305
+ }
306
+
307
+ # Determine template
308
+ template = "post.html"
309
+ if ct and ct.template:
310
+ template = ct.template
311
+
312
+ html = await render_theme(db, template, context, request=request)
313
+
314
+ return HTMLResponse(content=html)
315
+
316
+
264
317
  async def get_menus_context(db: AsyncSession) -> dict:
265
318
  """Get menus context for templates."""
266
319
  menu_svc = MenuService(db)
@@ -388,25 +388,39 @@ class ThemeService:
388
388
  return css.strip()
389
389
 
390
390
  def get_css_variables(self, theme_name: str = None, minify: bool = True) -> str:
391
- """Generate CSS variables from theme config."""
391
+ """Generate CSS variables from theme config with customizations applied."""
392
392
  theme = self.get_theme(theme_name)
393
393
  if not theme:
394
394
  return ""
395
395
 
396
+ # Get user customizations
397
+ customizations = self.get_customizations(theme_name)
398
+
396
399
  lines = [":root {"]
397
400
 
398
- # Colors
399
- for name, value in theme.colors.items():
401
+ # Colors (with customization override)
402
+ for name, default_value in theme.colors.items():
403
+ value = customizations.get(f"color_{name}", default_value)
400
404
  lines.append(f" --color-{name}: {value};")
401
405
 
402
- # Fonts
403
- for name, value in theme.fonts.items():
406
+ # Fonts (with customization override)
407
+ for name, default_value in theme.fonts.items():
408
+ value = customizations.get(f"font_{name}", default_value)
404
409
  lines.append(f" --font-{name}: {value};")
405
410
 
406
- # Spacing
407
- for name, value in theme.spacing.items():
411
+ # Spacing (with customization override)
412
+ for name, default_value in theme.spacing.items():
413
+ value = customizations.get(f"space_{name}", default_value)
408
414
  lines.append(f" --space-{name}: {value};")
409
415
 
416
+ # Header/Background images
417
+ header_image = customizations.get("header_image", "")
418
+ background_image = customizations.get("background_image", "")
419
+ if header_image:
420
+ lines.append(f" --header-image: url({header_image});")
421
+ if background_image:
422
+ lines.append(f" --background-image: url({background_image});")
423
+
410
424
  lines.append("}")
411
425
 
412
426
  # Add base styles
@@ -416,7 +430,10 @@ class ThemeService:
416
430
 
417
431
  body {
418
432
  font-family: var(--font-sans);
419
- background: var(--color-background);
433
+ background: var(--background-image, var(--color-background));
434
+ background-size: cover;
435
+ background-position: center;
436
+ background-attachment: fixed;
420
437
  color: var(--color-text);
421
438
  line-height: 1.6;
422
439
  }
@@ -428,7 +445,9 @@ body {
428
445
  }
429
446
 
430
447
  .site-header {
431
- background: var(--color-surface);
448
+ background: var(--header-image, var(--color-surface));
449
+ background-size: cover;
450
+ background-position: center;
432
451
  border-bottom: 1px solid var(--color-border);
433
452
  padding: var(--space-md) 0;
434
453
  }
@@ -645,9 +664,12 @@ body {
645
664
  """
646
665
  )
647
666
 
648
- # Add custom CSS
649
- if theme.custom_css:
650
- lines.append(theme.custom_css)
667
+ # Add custom CSS (from customizations or theme default)
668
+ custom_css = customizations.get("custom_css", theme.custom_css or "")
669
+ if custom_css:
670
+ lines.append("")
671
+ lines.append("/* Custom CSS */")
672
+ lines.append(custom_css)
651
673
 
652
674
  css = "\n".join(lines)
653
675
  return self._minify_css(css) if minify else css
@@ -1240,6 +1262,46 @@ body {
1240
1262
  customizations = self.get_customizations(theme_name)
1241
1263
  settings = []
1242
1264
 
1265
+ # Site Identity (logo, favicon)
1266
+ settings.append({
1267
+ "id": "site_logo",
1268
+ "type": "image",
1269
+ "label": "サイトロゴ",
1270
+ "category": "site_identity",
1271
+ "default": "",
1272
+ "value": customizations.get("site_logo", ""),
1273
+ "description": "ヘッダーに表示されるロゴ画像(推奨: 高さ60px以下)",
1274
+ })
1275
+ settings.append({
1276
+ "id": "site_icon",
1277
+ "type": "image",
1278
+ "label": "サイトアイコン",
1279
+ "category": "site_identity",
1280
+ "default": "",
1281
+ "value": customizations.get("site_icon", ""),
1282
+ "description": "ファビコン・アプリアイコン(推奨: 512x512px)",
1283
+ })
1284
+
1285
+ # Header Images
1286
+ settings.append({
1287
+ "id": "header_image",
1288
+ "type": "image",
1289
+ "label": "ヘッダー画像",
1290
+ "category": "header",
1291
+ "default": "",
1292
+ "value": customizations.get("header_image", ""),
1293
+ "description": "ヘッダー背景画像(推奨: 1920x400px)",
1294
+ })
1295
+ settings.append({
1296
+ "id": "background_image",
1297
+ "type": "image",
1298
+ "label": "背景画像",
1299
+ "category": "header",
1300
+ "default": "",
1301
+ "value": customizations.get("background_image", ""),
1302
+ "description": "サイト全体の背景画像",
1303
+ })
1304
+
1243
1305
  # Colors
1244
1306
  for name, default_value in theme.colors.items():
1245
1307
  settings.append({
@@ -1273,6 +1335,17 @@ body {
1273
1335
  "value": customizations.get(f"space_{name}", default_value),
1274
1336
  })
1275
1337
 
1338
+ # Custom CSS
1339
+ settings.append({
1340
+ "id": "custom_css",
1341
+ "type": "code",
1342
+ "label": "カスタムCSS",
1343
+ "category": "custom_css",
1344
+ "default": theme.custom_css or "",
1345
+ "value": customizations.get("custom_css", theme.custom_css or ""),
1346
+ "description": "独自のCSSを追加できます",
1347
+ })
1348
+
1276
1349
  return settings
1277
1350
 
1278
1351
  def generate_preview_css(self, preview_values: dict, theme_name: str = None) -> str:
@@ -1311,8 +1384,23 @@ body {
1311
1384
  value = values.get(f"space_{name}", default_value)
1312
1385
  lines.append(f" --space-{name}: {value};")
1313
1386
 
1387
+ # Header/Background images
1388
+ header_image = values.get("header_image", "")
1389
+ background_image = values.get("background_image", "")
1390
+ if header_image:
1391
+ lines.append(f" --header-image: url({header_image});")
1392
+ if background_image:
1393
+ lines.append(f" --background-image: url({background_image});")
1394
+
1314
1395
  lines.append("}")
1315
1396
 
1397
+ # Custom CSS
1398
+ custom_css = values.get("custom_css", "")
1399
+ if custom_css:
1400
+ lines.append("")
1401
+ lines.append("/* Custom CSS */")
1402
+ lines.append(custom_css)
1403
+
1316
1404
  return "\n".join(lines)
1317
1405
 
1318
1406
 
@@ -22,6 +22,74 @@
22
22
 
23
23
  <div class="customize-layout">
24
24
  <div class="customize-sidebar">
25
+ <!-- Site Identity -->
26
+ {% if grouped_settings.site_identity %}
27
+ <div class="customize-section">
28
+ <h3>サイトID</h3>
29
+ {% for setting in grouped_settings.site_identity %}
30
+ <div class="customize-field">
31
+ <label for="{{ setting.id }}">{{ setting.label }}</label>
32
+ {% if setting.description %}
33
+ <p class="field-description">{{ setting.description }}</p>
34
+ {% endif %}
35
+ <div class="image-input-wrapper">
36
+ <div class="image-preview" id="{{ setting.id }}_preview">
37
+ {% if setting.value %}
38
+ <img src="{{ setting.value }}" alt="{{ setting.label }}">
39
+ {% else %}
40
+ <span class="no-image">画像未設定</span>
41
+ {% endif %}
42
+ </div>
43
+ <input type="url"
44
+ id="{{ setting.id }}"
45
+ name="{{ setting.id }}"
46
+ value="{{ setting.value }}"
47
+ data-default="{{ setting.default }}"
48
+ class="customize-input image-url-input"
49
+ placeholder="https://example.com/image.png">
50
+ <div class="image-actions">
51
+ <button type="button" class="btn btn-sm btn-outline" onclick="clearImageInput('{{ setting.id }}')">クリア</button>
52
+ </div>
53
+ </div>
54
+ </div>
55
+ {% endfor %}
56
+ </div>
57
+ {% endif %}
58
+
59
+ <!-- Header Images -->
60
+ {% if grouped_settings.header %}
61
+ <div class="customize-section">
62
+ <h3>ヘッダー・背景</h3>
63
+ {% for setting in grouped_settings.header %}
64
+ <div class="customize-field">
65
+ <label for="{{ setting.id }}">{{ setting.label }}</label>
66
+ {% if setting.description %}
67
+ <p class="field-description">{{ setting.description }}</p>
68
+ {% endif %}
69
+ <div class="image-input-wrapper">
70
+ <div class="image-preview" id="{{ setting.id }}_preview">
71
+ {% if setting.value %}
72
+ <img src="{{ setting.value }}" alt="{{ setting.label }}">
73
+ {% else %}
74
+ <span class="no-image">画像未設定</span>
75
+ {% endif %}
76
+ </div>
77
+ <input type="url"
78
+ id="{{ setting.id }}"
79
+ name="{{ setting.id }}"
80
+ value="{{ setting.value }}"
81
+ data-default="{{ setting.default }}"
82
+ class="customize-input image-url-input"
83
+ placeholder="https://example.com/image.png">
84
+ <div class="image-actions">
85
+ <button type="button" class="btn btn-sm btn-outline" onclick="clearImageInput('{{ setting.id }}')">クリア</button>
86
+ </div>
87
+ </div>
88
+ </div>
89
+ {% endfor %}
90
+ </div>
91
+ {% endif %}
92
+
25
93
  <!-- Colors -->
26
94
  {% if grouped_settings.colors %}
27
95
  <div class="customize-section">
@@ -82,6 +150,27 @@
82
150
  {% endfor %}
83
151
  </div>
84
152
  {% endif %}
153
+
154
+ <!-- Custom CSS -->
155
+ {% if grouped_settings.custom_css %}
156
+ <div class="customize-section">
157
+ <h3>カスタムCSS</h3>
158
+ {% for setting in grouped_settings.custom_css %}
159
+ <div class="customize-field">
160
+ <label for="{{ setting.id }}">{{ setting.label }}</label>
161
+ {% if setting.description %}
162
+ <p class="field-description">{{ setting.description }}</p>
163
+ {% endif %}
164
+ <textarea id="{{ setting.id }}"
165
+ name="{{ setting.id }}"
166
+ data-default="{{ setting.default }}"
167
+ class="customize-input code-input"
168
+ rows="10"
169
+ placeholder="/* CSSを入力 */&#10;.my-class {&#10; color: red;&#10;}">{{ setting.value }}</textarea>
170
+ </div>
171
+ {% endfor %}
172
+ </div>
173
+ {% endif %}
85
174
  </div>
86
175
 
87
176
  <div class="customize-preview">
@@ -191,6 +280,66 @@
191
280
  font-family: monospace;
192
281
  }
193
282
 
283
+ .field-description {
284
+ font-size: 0.75rem;
285
+ color: var(--text-muted);
286
+ margin-bottom: 0.5rem;
287
+ }
288
+
289
+ .code-input {
290
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
291
+ font-size: 0.8125rem;
292
+ line-height: 1.5;
293
+ resize: vertical;
294
+ min-height: 150px;
295
+ background: #1e1e1e;
296
+ color: #d4d4d4;
297
+ padding: 0.75rem;
298
+ }
299
+
300
+ .image-input-wrapper {
301
+ display: flex;
302
+ flex-direction: column;
303
+ gap: 0.5rem;
304
+ }
305
+
306
+ .image-preview {
307
+ width: 100%;
308
+ height: 80px;
309
+ border: 2px dashed var(--border);
310
+ border-radius: 0.375rem;
311
+ display: flex;
312
+ align-items: center;
313
+ justify-content: center;
314
+ background: var(--bg);
315
+ overflow: hidden;
316
+ }
317
+
318
+ .image-preview img {
319
+ max-width: 100%;
320
+ max-height: 100%;
321
+ object-fit: contain;
322
+ }
323
+
324
+ .image-preview .no-image {
325
+ color: var(--text-muted);
326
+ font-size: 0.75rem;
327
+ }
328
+
329
+ .image-url-input {
330
+ font-size: 0.8125rem;
331
+ }
332
+
333
+ .image-actions {
334
+ display: flex;
335
+ gap: 0.5rem;
336
+ }
337
+
338
+ .btn-sm {
339
+ padding: 0.25rem 0.5rem;
340
+ font-size: 0.75rem;
341
+ }
342
+
194
343
  .customize-preview {
195
344
  background: var(--card);
196
345
  border: 1px solid var(--border);
@@ -360,8 +509,36 @@
360
509
  if (!input.classList.contains('color-input')) {
361
510
  input.addEventListener('input', debouncedUpdate);
362
511
  }
512
+ // Add image preview update for image-url-input
513
+ if (input.classList.contains('image-url-input')) {
514
+ input.addEventListener('input', () => {
515
+ updateImagePreview(input.id, input.value);
516
+ });
517
+ }
363
518
  });
364
519
 
520
+ // Update image preview
521
+ function updateImagePreview(inputId, url) {
522
+ const previewEl = document.getElementById(inputId + '_preview');
523
+ if (!previewEl) return;
524
+
525
+ if (url && url.trim()) {
526
+ previewEl.innerHTML = `<img src="${url}" alt="Preview" onerror="this.parentElement.innerHTML='<span class=\\'no-image\\'>読み込みエラー</span>'">`;
527
+ } else {
528
+ previewEl.innerHTML = '<span class="no-image">画像未設定</span>';
529
+ }
530
+ }
531
+
532
+ // Clear image input (global function for onclick)
533
+ window.clearImageInput = function(inputId) {
534
+ const input = document.getElementById(inputId);
535
+ if (input) {
536
+ input.value = '';
537
+ updateImagePreview(inputId, '');
538
+ debouncedUpdate();
539
+ }
540
+ };
541
+
365
542
  // Save button
366
543
  saveBtn.addEventListener('click', async () => {
367
544
  const values = getValues();
@@ -402,15 +579,18 @@
402
579
  if (!confirm('カスタマイズをデフォルトに戻しますか?')) return;
403
580
 
404
581
  inputs.forEach(input => {
405
- const defaultValue = input.dataset.default;
406
- if (defaultValue) {
407
- input.value = defaultValue;
408
-
409
- // Sync color text input
410
- if (input.classList.contains('color-input')) {
411
- const textInput = document.getElementById(input.id + '_text');
412
- if (textInput) textInput.value = defaultValue;
413
- }
582
+ const defaultValue = input.dataset.default || '';
583
+ input.value = defaultValue;
584
+
585
+ // Sync color text input
586
+ if (input.classList.contains('color-input')) {
587
+ const textInput = document.getElementById(input.id + '_text');
588
+ if (textInput) textInput.value = defaultValue;
589
+ }
590
+
591
+ // Update image preview
592
+ if (input.classList.contains('image-url-input')) {
593
+ updateImagePreview(input.id, defaultValue);
414
594
  }
415
595
  });
416
596
 
@@ -388,6 +388,7 @@
388
388
  <a href="{{ cancel_url }}" class="btn btn-secondary">Cancel</a>
389
389
  {% if entity %}
390
390
  <button type="button" class="btn btn-secondary" onclick="toggleRevisions()">History</button>
391
+ <button type="button" class="btn btn-secondary" onclick="openPreview()">Preview</button>
391
392
  {% endif %}
392
393
  <button type="submit" class="btn btn-primary">
393
394
  {% if entity %}Update{% else %}Create{% endif %}
@@ -1239,6 +1240,34 @@ function toggleRevisions() {
1239
1240
  }
1240
1241
  }
1241
1242
 
1243
+ // Preview in new tab
1244
+ async function openPreview() {
1245
+ const entityId = '{{ entity.id if entity else "" }}';
1246
+ if (!entityId) {
1247
+ alert('Please save the content first before previewing.');
1248
+ return;
1249
+ }
1250
+
1251
+ try {
1252
+ const response = await fetch('/admin/api/preview/token', {
1253
+ method: 'POST',
1254
+ headers: { 'Content-Type': 'application/json' },
1255
+ body: JSON.stringify({ entity_id: entityId })
1256
+ });
1257
+
1258
+ if (!response.ok) {
1259
+ const error = await response.json();
1260
+ alert('Preview error: ' + (error.detail || 'Unknown error'));
1261
+ return;
1262
+ }
1263
+
1264
+ const data = await response.json();
1265
+ window.open(data.url, '_blank');
1266
+ } catch (e) {
1267
+ alert('Preview error: ' + e.message);
1268
+ }
1269
+ }
1270
+
1242
1271
  async function loadRevisions() {
1243
1272
  const entityId = '{{ entity.id }}';
1244
1273
  const list = document.getElementById('revisions-list');
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "focomy"
7
- version = "0.1.115"
7
+ version = "0.1.117"
8
8
  description = "The Most Beautiful CMS - A metadata-driven, zero-duplicate-code content management system"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -0,0 +1,22 @@
1
+ {
2
+ "color_primary": "#2563eb",
3
+ "color_primary-hover": "#1d4ed8",
4
+ "color_background": "#ffffff",
5
+ "color_surface": "#f8fafc",
6
+ "color_text": "#1e293b",
7
+ "color_text-muted": "#64748b",
8
+ "color_border": "#e2e8f0",
9
+ "color_success": "#22c55e",
10
+ "color_error": "#ef4444",
11
+ "color_warning": "#f59e0b",
12
+ "font_sans": "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
13
+ "font_serif": "Georgia, 'Times New Roman', serif",
14
+ "font_mono": "ui-monospace, SFMono-Regular, Menlo, monospace",
15
+ "space_xs": "0.25rem",
16
+ "space_sm": "0.5rem",
17
+ "space_md": "1rem",
18
+ "space_lg": "1.5rem",
19
+ "space_xl": "2rem",
20
+ "space_2xl": "3rem",
21
+ "custom_css": "/* Test CSS */\n.test-class { color: red; }"
22
+ }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes