panda-cms 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +73 -0
  3. data/Rakefile +7 -0
  4. data/app/assets/builds/panda.cms.css +2808 -0
  5. data/app/assets/config/panda_cms_manifest.js +4 -0
  6. data/app/assets/stylesheets/panda/cms/application.tailwind.css +162 -0
  7. data/app/assets/stylesheets/panda/cms/editor.css +120 -0
  8. data/app/builders/panda/cms/form_builder.rb +234 -0
  9. data/app/components/panda/cms/admin/button_component.rb +70 -0
  10. data/app/components/panda/cms/admin/container_component.html.erb +13 -0
  11. data/app/components/panda/cms/admin/container_component.rb +13 -0
  12. data/app/components/panda/cms/admin/flash_message_component.html.erb +31 -0
  13. data/app/components/panda/cms/admin/flash_message_component.rb +47 -0
  14. data/app/components/panda/cms/admin/heading_component.rb +45 -0
  15. data/app/components/panda/cms/admin/panel_component.html.erb +7 -0
  16. data/app/components/panda/cms/admin/panel_component.rb +13 -0
  17. data/app/components/panda/cms/admin/slideover_component.html.erb +9 -0
  18. data/app/components/panda/cms/admin/slideover_component.rb +15 -0
  19. data/app/components/panda/cms/admin/statistics_component.html.erb +4 -0
  20. data/app/components/panda/cms/admin/statistics_component.rb +17 -0
  21. data/app/components/panda/cms/admin/tab_bar_component.html.erb +35 -0
  22. data/app/components/panda/cms/admin/tab_bar_component.rb +15 -0
  23. data/app/components/panda/cms/admin/table_component.html.erb +29 -0
  24. data/app/components/panda/cms/admin/table_component.rb +46 -0
  25. data/app/components/panda/cms/admin/tag_component.rb +35 -0
  26. data/app/components/panda/cms/admin/user_activity_component.html.erb +5 -0
  27. data/app/components/panda/cms/admin/user_activity_component.rb +33 -0
  28. data/app/components/panda/cms/admin/user_display_component.html.erb +17 -0
  29. data/app/components/panda/cms/admin/user_display_component.rb +21 -0
  30. data/app/components/panda/cms/code_component.rb +64 -0
  31. data/app/components/panda/cms/grid_component.html.erb +6 -0
  32. data/app/components/panda/cms/grid_component.rb +15 -0
  33. data/app/components/panda/cms/menu_component.html.erb +6 -0
  34. data/app/components/panda/cms/menu_component.rb +58 -0
  35. data/app/components/panda/cms/page_menu_component.html.erb +21 -0
  36. data/app/components/panda/cms/page_menu_component.rb +38 -0
  37. data/app/components/panda/cms/rich_text_component.html.erb +6 -0
  38. data/app/components/panda/cms/rich_text_component.rb +84 -0
  39. data/app/components/panda/cms/text_component.rb +72 -0
  40. data/app/constraints/panda/cms/admin_constraint.rb +18 -0
  41. data/app/controllers/panda/cms/admin/block_contents_controller.rb +52 -0
  42. data/app/controllers/panda/cms/admin/dashboard_controller.rb +20 -0
  43. data/app/controllers/panda/cms/admin/files_controller.rb +21 -0
  44. data/app/controllers/panda/cms/admin/forms_controller.rb +53 -0
  45. data/app/controllers/panda/cms/admin/menus_controller.rb +30 -0
  46. data/app/controllers/panda/cms/admin/pages_controller.rb +91 -0
  47. data/app/controllers/panda/cms/admin/posts_controller.rb +146 -0
  48. data/app/controllers/panda/cms/admin/sessions_controller.rb +94 -0
  49. data/app/controllers/panda/cms/admin/settings/bulk_editor_controller.rb +37 -0
  50. data/app/controllers/panda/cms/admin/settings_controller.rb +20 -0
  51. data/app/controllers/panda/cms/application_controller.rb +57 -0
  52. data/app/controllers/panda/cms/errors_controller.rb +33 -0
  53. data/app/controllers/panda/cms/form_submissions_controller.rb +23 -0
  54. data/app/controllers/panda/cms/pages_controller.rb +72 -0
  55. data/app/controllers/panda/cms/posts_controller.rb +13 -0
  56. data/app/helpers/panda/cms/admin/files_helper.rb +6 -0
  57. data/app/helpers/panda/cms/admin/pages_helper.rb +6 -0
  58. data/app/helpers/panda/cms/admin/posts_helper.rb +48 -0
  59. data/app/helpers/panda/cms/application_helper.rb +120 -0
  60. data/app/helpers/panda/cms/pages_helper.rb +6 -0
  61. data/app/helpers/panda/cms/theme_helper.rb +18 -0
  62. data/app/javascript/panda/cms/@editorjs--editorjs.js +2577 -0
  63. data/app/javascript/panda/cms/@hotwired--stimulus.js +4 -0
  64. data/app/javascript/panda/cms/@hotwired--turbo.js +160 -0
  65. data/app/javascript/panda/cms/@rails--actioncable--src.js +4 -0
  66. data/app/javascript/panda/cms/application_panda_cms.js +39 -0
  67. data/app/javascript/panda/cms/controllers/dashboard_controller.js +7 -0
  68. data/app/javascript/panda/cms/controllers/editor_form_controller.js +77 -0
  69. data/app/javascript/panda/cms/controllers/editor_iframe_controller.js +320 -0
  70. data/app/javascript/panda/cms/controllers/index.js +48 -0
  71. data/app/javascript/panda/cms/controllers/slug_controller.js +87 -0
  72. data/app/javascript/panda/cms/editor/css_extractor.js +80 -0
  73. data/app/javascript/panda/cms/editor/editor_js_config.js +177 -0
  74. data/app/javascript/panda/cms/editor/editor_js_initializer.js +285 -0
  75. data/app/javascript/panda/cms/editor/plain_text_editor.js +110 -0
  76. data/app/javascript/panda/cms/editor/resource_loader.js +115 -0
  77. data/app/javascript/panda/cms/tailwindcss-stimulus-components.js +4 -0
  78. data/app/jobs/panda/cms/application_job.rb +6 -0
  79. data/app/jobs/panda/cms/record_visit_job.rb +31 -0
  80. data/app/mailers/panda/cms/application_mailer.rb +8 -0
  81. data/app/mailers/panda/cms/form_mailer.rb +21 -0
  82. data/app/models/action_text/rich_text_version.rb +6 -0
  83. data/app/models/panda/cms/application_record.rb +7 -0
  84. data/app/models/panda/cms/block.rb +34 -0
  85. data/app/models/panda/cms/block_content.rb +18 -0
  86. data/app/models/panda/cms/block_content_version.rb +8 -0
  87. data/app/models/panda/cms/breadcrumb.rb +12 -0
  88. data/app/models/panda/cms/current.rb +17 -0
  89. data/app/models/panda/cms/form.rb +9 -0
  90. data/app/models/panda/cms/form_submission.rb +7 -0
  91. data/app/models/panda/cms/menu.rb +52 -0
  92. data/app/models/panda/cms/menu_item.rb +58 -0
  93. data/app/models/panda/cms/page.rb +96 -0
  94. data/app/models/panda/cms/page_version.rb +8 -0
  95. data/app/models/panda/cms/post.rb +60 -0
  96. data/app/models/panda/cms/post_version.rb +8 -0
  97. data/app/models/panda/cms/redirect.rb +11 -0
  98. data/app/models/panda/cms/template.rb +124 -0
  99. data/app/models/panda/cms/template_version.rb +8 -0
  100. data/app/models/panda/cms/user.rb +31 -0
  101. data/app/models/panda/cms/version.rb +8 -0
  102. data/app/models/panda/cms/visit.rb +9 -0
  103. data/app/services/panda/cms/html_to_editor_js_converter.rb +200 -0
  104. data/app/views/active_storage/blobs/blobs/_blob.html.erb +14 -0
  105. data/app/views/layouts/action_text/contents/_content.html.erb +3 -0
  106. data/app/views/layouts/panda/cms/application.html.erb +41 -0
  107. data/app/views/layouts/panda/cms/public.html.erb +3 -0
  108. data/app/views/panda/cms/admin/dashboard/show.html.erb +12 -0
  109. data/app/views/panda/cms/admin/files/index.html.erb +124 -0
  110. data/app/views/panda/cms/admin/files/show.html.erb +2 -0
  111. data/app/views/panda/cms/admin/forms/edit.html.erb +0 -0
  112. data/app/views/panda/cms/admin/forms/index.html.erb +13 -0
  113. data/app/views/panda/cms/admin/forms/new.html.erb +15 -0
  114. data/app/views/panda/cms/admin/forms/show.html.erb +35 -0
  115. data/app/views/panda/cms/admin/menus/index.html.erb +8 -0
  116. data/app/views/panda/cms/admin/pages/edit.html.erb +36 -0
  117. data/app/views/panda/cms/admin/pages/index.html.erb +22 -0
  118. data/app/views/panda/cms/admin/pages/new.html.erb +15 -0
  119. data/app/views/panda/cms/admin/pages/show.html.erb +1 -0
  120. data/app/views/panda/cms/admin/posts/_form.html.erb +29 -0
  121. data/app/views/panda/cms/admin/posts/edit.html.erb +6 -0
  122. data/app/views/panda/cms/admin/posts/index.html.erb +18 -0
  123. data/app/views/panda/cms/admin/posts/new.html.erb +6 -0
  124. data/app/views/panda/cms/admin/sessions/new.html.erb +17 -0
  125. data/app/views/panda/cms/admin/settings/bulk_editor/new.html.erb +68 -0
  126. data/app/views/panda/cms/admin/settings/index.html.erb +21 -0
  127. data/app/views/panda/cms/admin/settings/insta.html +4 -0
  128. data/app/views/panda/cms/admin/shared/_breadcrumbs.html.erb +28 -0
  129. data/app/views/panda/cms/admin/shared/_flash.html.erb +5 -0
  130. data/app/views/panda/cms/admin/shared/_sidebar.html.erb +41 -0
  131. data/app/views/panda/cms/form_mailer/notification_email.html.erb +11 -0
  132. data/app/views/panda/cms/shared/_editor.html.erb +0 -0
  133. data/app/views/panda/cms/shared/_favicons.html.erb +9 -0
  134. data/app/views/panda/cms/shared/_footer.html.erb +2 -0
  135. data/app/views/panda/cms/shared/_header.html.erb +15 -0
  136. data/app/views/panda/cms/shared/_importmap.html.erb +33 -0
  137. data/config/importmap.rb +13 -0
  138. data/config/initializers/inflections.rb +3 -0
  139. data/config/initializers/panda/cms/form_errors.rb +38 -0
  140. data/config/initializers/panda/cms/healthcheck_log_silencer.rb +11 -0
  141. data/config/initializers/panda/cms/paper_trail.rb +7 -0
  142. data/config/initializers/panda/cms.rb +10 -0
  143. data/config/initializers/zeitwork.rb +3 -0
  144. data/config/locales/en.yml +49 -0
  145. data/config/puma/test.rb +9 -0
  146. data/config/routes.rb +48 -0
  147. data/config/tailwind.config.js +37 -0
  148. data/db/migrate/20240205223709_create_panda_cms_pages.rb +9 -0
  149. data/db/migrate/20240219213327_create_panda_cms_page_versions.rb +14 -0
  150. data/db/migrate/20240303002805_create_panda_cms_templates.rb +11 -0
  151. data/db/migrate/20240303003434_create_panda_cms_template_versions.rb +14 -0
  152. data/db/migrate/20240303022441_create_panda_cms_blocks.rb +13 -0
  153. data/db/migrate/20240303024256_create_panda_cms_block_contents.rb +10 -0
  154. data/db/migrate/20240303024746_create_panda_cms_block_content_versions.rb +14 -0
  155. data/db/migrate/20240303233238_add_panda_cms_menu_table.rb +10 -0
  156. data/db/migrate/20240303234724_add_panda_cms_menu_item_table.rb +12 -0
  157. data/db/migrate/20240304134343_add_parent_id_to_panda_cms_pages.rb +5 -0
  158. data/db/migrate/20240305000000_convert_html_content_to_editor_js.rb +82 -0
  159. data/db/migrate/20240315125411_add_status_to_panda_cms_pages.rb +9 -0
  160. data/db/migrate/20240315125421_add_nested_sets_to_panda_cms_pages.rb +16 -0
  161. data/db/migrate/20240316212822_add_kind_to_panda_cms_menus.rb +6 -0
  162. data/db/migrate/20240316221425_add_start_page_to_panda_cms_menus.rb +5 -0
  163. data/db/migrate/20240316230706_add_nested_to_panda_cms_menu_items.rb +24 -0
  164. data/db/migrate/20240317010532_create_panda_cms_users.rb +12 -0
  165. data/db/migrate/20240317161534_add_max_uses_to_panda_cms_template.rb +7 -0
  166. data/db/migrate/20240317163053_reset_counter_cache_on_panda_cms_template.rb +5 -0
  167. data/db/migrate/20240317214827_create_panda_cms_redirects.rb +14 -0
  168. data/db/migrate/20240317230622_create_panda_cms_visits.rb +13 -0
  169. data/db/migrate/20240324205703_create_active_storage_tables.active_storage.rb +58 -0
  170. data/db/migrate/20240408084718_default_panda_cms_users_admin_to_false.rb +5 -0
  171. data/db/migrate/20240701225422_add_service_name_to_active_storage_blobs.active_storage.rb +22 -0
  172. data/db/migrate/20240701225423_create_active_storage_variant_records.active_storage.rb +28 -0
  173. data/db/migrate/20240701225424_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb +8 -0
  174. data/db/migrate/20240804235210_create_panda_cms_forms.rb +11 -0
  175. data/db/migrate/20240805013612_create_panda_cms_form_submissions.rb +9 -0
  176. data/db/migrate/20240805121123_create_panda_cms_posts.rb +27 -0
  177. data/db/migrate/20240805123104_create_panda_cms_post_versions.rb +14 -0
  178. data/db/migrate/20240806112735_fix_panda_cms_visits_column_names.rb +13 -0
  179. data/db/migrate/20240806204412_add_completion_path_to_panda_cms_forms.rb +5 -0
  180. data/db/migrate/20240820081917_change_form_submissions_to_submission_count.rb +5 -0
  181. data/db/migrate/20240904200605_create_action_text_tables.action_text.rb +24 -0
  182. data/db/migrate/20240923234535_add_depth_to_panda_cms_menus.rb +11 -0
  183. data/db/migrate/20241031205109_add_cached_content_to_panda_cms_block_contents.rb +5 -0
  184. data/db/migrate/20241119214548_convert_post_content_to_editor_js.rb +35 -0
  185. data/db/migrate/20241119214549_remove_action_text_from_posts.rb +9 -0
  186. data/db/migrate/20241120000419_remove_post_tag_references.rb +19 -0
  187. data/db/migrate/20241120110943_add_editor_js_to_posts.rb +27 -0
  188. data/db/migrate/20241120113859_add_cached_content_to_panda_cms_posts.rb +5 -0
  189. data/db/migrate/20241123234140_remove_post_tag_id_from_posts.rb +5 -0
  190. data/db/migrate/migrate +1 -0
  191. data/db/seeds.rb +5 -0
  192. data/lib/generators/panda/cms/install_generator.rb +29 -0
  193. data/lib/panda/cms/bulk_editor.rb +171 -0
  194. data/lib/panda/cms/demo_site_generator.rb +67 -0
  195. data/lib/panda/cms/editor_js/blocks/alert.rb +34 -0
  196. data/lib/panda/cms/editor_js/blocks/base.rb +33 -0
  197. data/lib/panda/cms/editor_js/blocks/header.rb +15 -0
  198. data/lib/panda/cms/editor_js/blocks/image.rb +36 -0
  199. data/lib/panda/cms/editor_js/blocks/list.rb +32 -0
  200. data/lib/panda/cms/editor_js/blocks/paragraph.rb +15 -0
  201. data/lib/panda/cms/editor_js/blocks/quote.rb +41 -0
  202. data/lib/panda/cms/editor_js/blocks/table.rb +50 -0
  203. data/lib/panda/cms/editor_js/renderer.rb +124 -0
  204. data/lib/panda/cms/editor_js.rb +16 -0
  205. data/lib/panda/cms/editor_js_content.rb +21 -0
  206. data/lib/panda/cms/engine.rb +257 -0
  207. data/lib/panda/cms/exceptions_app.rb +26 -0
  208. data/lib/panda/cms/railtie.rb +11 -0
  209. data/lib/panda/cms/slug.rb +24 -0
  210. data/lib/panda/cms.rb +0 -0
  211. data/lib/panda-cms/version.rb +5 -0
  212. data/lib/panda-cms.rb +81 -0
  213. data/lib/tasks/panda_cms.rake +54 -0
  214. data/lib/templates/erb/scaffold/_form.html.erb.tt +43 -0
  215. data/lib/templates/erb/scaffold/edit.html.erb.tt +8 -0
  216. data/lib/templates/erb/scaffold/index.html.erb.tt +14 -0
  217. data/lib/templates/erb/scaffold/new.html.erb.tt +7 -0
  218. data/lib/templates/erb/scaffold/partial.html.erb.tt +22 -0
  219. data/lib/templates/erb/scaffold/show.html.erb.tt +15 -0
  220. data/public/panda-cms-assets/favicons/android-chrome-192x192.png +0 -0
  221. data/public/panda-cms-assets/favicons/android-chrome-512x512.png +0 -0
  222. data/public/panda-cms-assets/favicons/apple-touch-icon.png +0 -0
  223. data/public/panda-cms-assets/favicons/browserconfig.xml +9 -0
  224. data/public/panda-cms-assets/favicons/favicon-16x16.png +0 -0
  225. data/public/panda-cms-assets/favicons/favicon-32x32.png +0 -0
  226. data/public/panda-cms-assets/favicons/favicon.ico +0 -0
  227. data/public/panda-cms-assets/favicons/mstile-150x150.png +0 -0
  228. data/public/panda-cms-assets/favicons/safari-pinned-tab.svg +61 -0
  229. data/public/panda-cms-assets/favicons/site.webmanifest +14 -0
  230. data/public/panda-cms-assets/panda-logo-screenprint.png +0 -0
  231. data/public/panda-cms-assets/panda-nav.png +0 -0
  232. data/public/panda-cms-assets/rich_text_editor.css +568 -0
  233. metadata +654 -0
@@ -0,0 +1,4 @@
1
+ //= link_tree ../builds/ .css
2
+ //= link_directory ../../javascript/panda/cms .js
3
+ //= link_directory ../../javascript/panda/cms/controllers .js
4
+ //= link_directory ../../javascript/panda/cms/editor .js
@@ -0,0 +1,162 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ @layer base {
6
+ html[data-theme="default"] {
7
+ --color-white: 249 249 249; /* #F9F9F9 */
8
+ --color-black: 26 22 29; /* #1A161D */
9
+
10
+ --color-light: 238 206 230; /* #EECEE6 */
11
+ --color-mid: 141 94 183; /* #8D5EB7 */
12
+ --color-dark: 33 29 73; /* #211D49 */
13
+
14
+ --color-highlight: 208 64 20; /* #D04014 */
15
+
16
+ --color-active: 0 135 85; /* #008755 */
17
+ --color-warning: 250 207 142; /* #FACF8E */
18
+ --color-inactive: 216 247 245; /* #d6e4f7 */
19
+ --color-error: 245 129 129; /* #F58181 */
20
+ }
21
+
22
+ html[data-theme="sky"] {
23
+ --color-white: 249 249 249; /* #F9F9F9 */
24
+ --color-black: 26 22 29; /* #1A161D */
25
+ --color-light: 204 238 242; /* #CCEEF2 */
26
+ --color-mid: 42 102 159; /* #2A669F */
27
+ --color-dark: 20 32 74; /* #14204A */
28
+ --color-highlight: 208 64 20; /* #D04014 */
29
+
30
+ --color-active: 166 211 129; /* #A6D381 */
31
+ --color-warning: 244 190 102; /* #F4BE66 */
32
+ --color-inactive: 216 247 245; /* #d6e4f7 */
33
+ --color-error: 208 64 20; /* #D04014 */
34
+ }
35
+
36
+ a.block-link:after {
37
+ position: absolute;
38
+ content: "";
39
+ inset: 0;
40
+ }
41
+ }
42
+
43
+ /* Default editor styles */
44
+ @layer components {
45
+ .codex-editor__redactor .ce-block .ce-block__content {
46
+ @apply text-base font-normal font-sans text-dark leading-[1.6] space-y-[1.6rem];
47
+
48
+ h1.ce-header {
49
+ @apply text-3xl md:text-4xl font-semibold font-sans leading-[1.2];
50
+ }
51
+
52
+ h2.ce-header {
53
+ @apply text-2xl font-medium font-sans leading-[1.3] mb-4 mt-8;
54
+ }
55
+
56
+ h3.ce-header {
57
+ @apply text-xl font-normal font-sans leading-[1.3] mb-4 mt-6;
58
+ }
59
+
60
+ p,
61
+ li {
62
+ @apply leading-[1.6] tracking-wide max-w-[85ch];
63
+
64
+ a {
65
+ @apply text-[#1A9597] underline underline-offset-2 hover:text-[#158486] focus:outline-2 focus:outline-offset-2 focus:outline-[#1A9597];
66
+ }
67
+
68
+ strong,
69
+ b {
70
+ @apply font-semibold;
71
+ }
72
+ }
73
+
74
+ p {
75
+ @apply mb-4;
76
+ }
77
+
78
+ .cdx-quote {
79
+ @apply bg-[#eef0f3] border-l-inactive border-l-8 p-6 mb-4;
80
+
81
+ .cdx-quote__caption {
82
+ @apply block ml-6 mt-2 text-sm text-dark;
83
+ }
84
+
85
+ .cdx-quote__text {
86
+ quotes: "\201C""\201D""\2018""\2019";
87
+ @apply pl-6;
88
+
89
+ &:before {
90
+ @apply -ml-8 mr-2 text-dark text-6xl leading-4 align-text-bottom font-serif;
91
+ content: open-quote;
92
+ }
93
+
94
+ p {
95
+ @apply inline italic text-lg;
96
+ }
97
+ }
98
+ }
99
+
100
+ .cdx-list {
101
+ @apply mb-4 pl-6;
102
+
103
+ &--ordered {
104
+ @apply list-decimal;
105
+ }
106
+
107
+ &--unordered {
108
+ @apply list-disc;
109
+ }
110
+
111
+ .cdx-list {
112
+ @apply mt-2 mb-0;
113
+ }
114
+
115
+ .cdx-list__item {
116
+ @apply mb-2 pl-2;
117
+ }
118
+ }
119
+
120
+ .cdx-nested-list {
121
+ @apply mb-4 pl-6;
122
+
123
+ &--ordered {
124
+ @apply list-decimal;
125
+ }
126
+
127
+ &--unordered {
128
+ @apply list-disc;
129
+ }
130
+
131
+ .cdx-nested-list {
132
+ @apply mt-2 mb-0;
133
+ }
134
+
135
+ .cdx-nested-list__item {
136
+ @apply mb-2 pl-2;
137
+ }
138
+ }
139
+
140
+ .cdx-table {
141
+ @apply w-full border-collapse border-2 border-dark my-6;
142
+
143
+ &__head {
144
+ @apply font-semibold border-dark border-r-2 p-3 bg-light;
145
+ }
146
+
147
+ &__row {
148
+ @apply border-dark border-b-2;
149
+ }
150
+
151
+ &__cell {
152
+ @apply border-dark border-r-2 p-3;
153
+ }
154
+ }
155
+
156
+ .cdx-embed {
157
+ iframe {
158
+ @apply w-full border-none;
159
+ }
160
+ }
161
+ }
162
+ }
@@ -0,0 +1,120 @@
1
+ /* Base content styles, where .codex-editor applies them to the Panda editor too */
2
+ @layer components {
3
+ .codex-editor__redactor .ce-block .ce-block__content {
4
+ @apply text-base font-normal font-sans text-dark leading-[1.6] space-y-[1.6rem];
5
+
6
+ h1.ce-header {
7
+ @apply text-3xl md:text-4xl font-semibold font-sans text-[#104071] leading-[1.2] max-w-[85ch];
8
+ }
9
+
10
+ h2.ce-header {
11
+ @apply text-2xl font-medium font-sans text-[#104071] leading-[1.3] mb-4 mt-8 max-w-[85ch];
12
+ }
13
+
14
+ h3.ce-header {
15
+ @apply text-xl font-normal font-sans text-[#104071] leading-[1.3] mb-4 mt-6 max-w-[85ch];
16
+ }
17
+
18
+ p,
19
+ li {
20
+ @apply leading-[1.6] tracking-wide max-w-[85ch];
21
+
22
+ a {
23
+ @apply text-[#1A9597] underline underline-offset-2 hover:text-[#158486] focus:outline-2 focus:outline-offset-2 focus:outline-[#1A9597];
24
+ }
25
+
26
+ strong,
27
+ b {
28
+ @apply font-semibold;
29
+ }
30
+ }
31
+
32
+ p {
33
+ @apply mb-4;
34
+ }
35
+
36
+ .cdx-quote {
37
+ @apply bg-[#eef0f3] border-l-inactive border-l-8 p-6 mb-4;
38
+
39
+ .cdx-quote__caption {
40
+ @apply block ml-6 mt-2 text-sm text-dark;
41
+ }
42
+
43
+ .cdx-quote__text {
44
+ quotes: "\201C" "\201D" "\2018" "\2019";
45
+ @apply pl-6;
46
+
47
+ &:before {
48
+ @apply -ml-8 mr-2 text-dark text-6xl leading-4 align-text-bottom font-serif;
49
+ content: open-quote;
50
+ }
51
+
52
+ p {
53
+ @apply inline italic text-lg;
54
+ }
55
+ }
56
+ }
57
+
58
+ .cdx-list {
59
+ @apply mb-4 pl-6;
60
+
61
+ &--ordered {
62
+ @apply list-decimal;
63
+ }
64
+
65
+ &--unordered {
66
+ @apply list-disc;
67
+ }
68
+
69
+ .cdx-list {
70
+ @apply mt-2 mb-0;
71
+ }
72
+
73
+ .cdx-list__item {
74
+ @apply mb-2 pl-2;
75
+ }
76
+ }
77
+
78
+ .cdx-nested-list {
79
+ @apply mb-4 pl-6;
80
+
81
+ &--ordered {
82
+ @apply list-decimal;
83
+ }
84
+
85
+ &--unordered {
86
+ @apply list-disc;
87
+ }
88
+
89
+ .cdx-nested-list {
90
+ @apply mt-2 mb-0;
91
+ }
92
+
93
+ .cdx-nested-list__item {
94
+ @apply mb-2 pl-2;
95
+ }
96
+ }
97
+
98
+ .cdx-table {
99
+ @apply w-full border-collapse border-2 border-dark my-6;
100
+
101
+ &__head {
102
+ @apply font-semibold border-dark border-r-2 p-3 bg-light;
103
+ }
104
+
105
+ &__row {
106
+ @apply border-dark border-b-2;
107
+ }
108
+
109
+ &__cell {
110
+ @apply border-dark border-r-2 p-3;
111
+ }
112
+ }
113
+
114
+ .cdx-embed {
115
+ iframe {
116
+ @apply w-full border-none;
117
+ }
118
+ }
119
+ }
120
+ }
@@ -0,0 +1,234 @@
1
+ require "ostruct"
2
+
3
+ module Panda
4
+ module CMS
5
+ class FormBuilder < ActionView::Helpers::FormBuilder
6
+ include ActionView::Helpers::TagHelper
7
+ include ActionView::Helpers::FormTagHelper
8
+
9
+ def label(attribute, text = nil, options = {}, &block)
10
+ super(attribute, text, options.reverse_merge(class: label_styles))
11
+ end
12
+
13
+ def text_field(attribute, options = {})
14
+ if options.dig(:data, :prefix)
15
+ content_tag :div, class: container_styles do
16
+ label(attribute) + meta_text(options) +
17
+ content_tag(:div, class: "flex flex-grow") do
18
+ content_tag(:span, class: "inline-flex items-center px-3 text-base border border-r-none rounded-s-md whitespace-nowrap break-keep") { options.dig(:data, :prefix) } +
19
+ super(attribute, options.reverse_merge(class: input_styles_prefix + " input-prefix rounded-l-none border-l-none"))
20
+ end
21
+ end
22
+ else
23
+ content_tag :div, class: container_styles do
24
+ label(attribute) + meta_text(options) + super(attribute, options.reverse_merge(class: input_styles))
25
+ end
26
+ end
27
+ end
28
+
29
+ def email_field(method, options = {})
30
+ content_tag :div, class: container_styles do
31
+ label(method) + meta_text(options) + super(method, options.reverse_merge(class: input_styles))
32
+ end
33
+ end
34
+
35
+ def datetime_field(method, options = {})
36
+ content_tag :div, class: container_styles do
37
+ label(method) + meta_text(options) + super(method, options.reverse_merge(class: input_styles))
38
+ end
39
+ end
40
+
41
+ def text_area(method, options = {})
42
+ content_tag :div, class: container_styles do
43
+ label(method) + meta_text(options) + super(method, options.reverse_merge(class: input_styles))
44
+ end
45
+ end
46
+
47
+ def password_field(attribute, options = {})
48
+ content_tag :div, class: container_styles do
49
+ label(attribute) + meta_text(options) + super(attribute, options.reverse_merge(class: input_styles))
50
+ end
51
+ end
52
+
53
+ def select(method, choices = nil, options = {}, html_options = {}, &block)
54
+ content_tag :div, class: container_styles do
55
+ label(method) + meta_text(options) + super(method, choices, options, html_options.reverse_merge(class: select_styles)) + select_svg
56
+ end
57
+ end
58
+
59
+ def collection_select(method, collection, value_method, text_method, options = {}, html_options = {})
60
+ content_tag :div, class: container_styles do
61
+ label(method) + meta_text(options) + super(method, collection, value_method, text_method, options, html_options.reverse_merge(class: input_styles))
62
+ end
63
+ end
64
+
65
+ def time_zone_select(method, priority_zones = nil, options = {}, html_options = {})
66
+ wrap_field(method, options) do
67
+ super(
68
+ method,
69
+ priority_zones,
70
+ options,
71
+ html_options.reverse_merge(class: select_styles)
72
+ )
73
+ end
74
+ end
75
+
76
+ def file_field(method, options = {})
77
+ content_tag :div, class: container_styles do
78
+ label(method) + meta_text(options) + super(method, options.reverse_merge(class: "file:rounded file:border-0 file:text-sm file:bg-white file:text-gray-500 hover:file:bg-gray-50 bg-white px-2.5 hover:bg-gray-50".concat(input_styles)))
79
+ end
80
+ end
81
+
82
+ def button(value = nil, options = {}, &block)
83
+ value ||= submit_default_value
84
+ options = options.dup
85
+
86
+ # Handle formmethod specially
87
+ if options[:formmethod] == "delete"
88
+ options[:name] = "_method"
89
+ options[:value] = "delete"
90
+ end
91
+
92
+ base_classes = [
93
+ "inline-flex items-center rounded-md",
94
+ "px-3 py-2",
95
+ "text-base font-semibold",
96
+ "shadow-sm"
97
+ ]
98
+
99
+ # Only add fa-circle-check for non-block buttons
100
+ base_classes << "fa-circle-check" unless block_given?
101
+
102
+ options[:class] = [
103
+ *base_classes,
104
+ options[:class]
105
+ ].compact.join(" ")
106
+
107
+ if block_given?
108
+ @template.button_tag(options, &block)
109
+ else
110
+ @template.button_tag(value, options)
111
+ end
112
+ end
113
+
114
+ def submit(value = nil, options = {})
115
+ value ||= submit_default_value
116
+
117
+ # Use the same style logic as ButtonComponent
118
+ action = object.persisted? ? :save : :create
119
+ button_classes = case action
120
+ when :save, :create
121
+ "text-white bg-active"
122
+ when :save_inactive
123
+ "text-white bg-inactive"
124
+ when :secondary
125
+ "text-dark border-2 border-mid bg-transparent hover:bg-light transition-all"
126
+ else
127
+ "text-dark border-2 border-mid bg-transparent hover:bg-light transition-all"
128
+ end
129
+
130
+ # Combine with common button classes
131
+ classes = "inline-flex items-center rounded-md font-medium shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 px-3 py-2 #{button_classes}"
132
+
133
+ options[:class] = options[:class] ? "#{options[:class]} #{classes}" : classes
134
+
135
+ super
136
+ end
137
+
138
+ def check_box(method, options = {}, checked_value = "1", unchecked_value = "0")
139
+ content_tag :div, class: container_styles do
140
+ label(method) + meta_text(options) + super(method, options.reverse_merge(class: "border-gray-300 ml-2"), checked_value, unchecked_value)
141
+ end
142
+ end
143
+
144
+ def date_field(method, options = {})
145
+ content_tag :div, class: container_styles do
146
+ label(method) + meta_text(options) + super(method, options.reverse_merge(class: input_styles))
147
+ end
148
+ end
149
+
150
+ def rich_text_area(method, options = {})
151
+ content_tag :div, class: container_styles do
152
+ label(method) + meta_text(options) + super(method, options.reverse_merge(class: textarea_styles))
153
+ end
154
+ end
155
+
156
+ def rich_text_field(method, options = {})
157
+ wrap_field(method, options) do
158
+ if defined?(ActionText)
159
+ # For test environment
160
+ if Rails.env.test?
161
+ # Just render a textarea for testing
162
+ text_area(method, options.reverse_merge(class: textarea_styles))
163
+ else
164
+ rich_text_area(method, options.reverse_merge(class: textarea_styles))
165
+ end
166
+ else
167
+ text_area(method, options.reverse_merge(class: textarea_styles))
168
+ end
169
+ end
170
+ end
171
+
172
+ def meta_text(options)
173
+ return unless options[:meta]
174
+ @template.content_tag(:p, options[:meta], class: "block text-black/60 text-sm mb-2")
175
+ end
176
+
177
+ private
178
+
179
+ def label_styles
180
+ "font-light inline-block mb-1 text-base leading-6"
181
+ end
182
+
183
+ def base_input_styles
184
+ "bg-white block w-full rounded-md border border-mid focus:border-mid p-2 text-dark outline-0 focus:outline-0 ring-0 focus:ring-0 focus:ring-mid ring-offset-0 focus:ring-offset-0 shadow-none focus:shadow-none border-mid"
185
+ end
186
+
187
+ def input_styles
188
+ base_input_styles
189
+ end
190
+
191
+ def input_styles_prefix
192
+ input_styles.concat(" prefix")
193
+ end
194
+
195
+ def select_styles
196
+ "col-start-1 row-start-1 w-full appearance-none rounded-md bg-white py-1.5 pl-3 pr-8 text-dark text-base outline-0 outline-dark focus:outline focus:-outline-offset-2 focus:outline-dark"
197
+ end
198
+
199
+ def select_svg
200
+ @template.content_tag(:svg, class: "pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-gray-400", aria_hidden: true) do
201
+ @template.content_tag(:path, d: "M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z")
202
+ end
203
+ end
204
+
205
+ def button_styles
206
+ "inline-flex items-center rounded-md font-medium shadow-sm focus-visible:outline focus-visible:outline-0 focus-visible:outline-offset-none text-dark border-2 border-mid bg-transparent hover:bg-light transition-all gap-x-1.5 px-3 py-2 text-base gap-x-1.5 px-2.5 py-1.5 mt-2 "
207
+ end
208
+
209
+ def container_styles
210
+ "panda-cms-field-container mb-4"
211
+ end
212
+
213
+ def textarea_styles
214
+ input_styles.concat(" min-h-32")
215
+ end
216
+
217
+ def submit_default_value
218
+ object.persisted? ? "Update #{object.class.name.demodulize}" : "Create #{object.class.name.demodulize}"
219
+ end
220
+
221
+ def wrap_field(method, options = {}, &block)
222
+ @template.content_tag(:div, class: "panda-cms-field-container") do
223
+ label(method, class: "font-light inline-block mb-1 text-base leading-6") +
224
+ meta_text(options) +
225
+ @template.content_tag(:div, class: field_wrapper_styles, &block)
226
+ end
227
+ end
228
+
229
+ def field_wrapper_styles
230
+ "mt-1"
231
+ end
232
+ end
233
+ end
234
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module CMS
5
+ module Admin
6
+ class ButtonComponent < ViewComponent::Base
7
+ attr_accessor :text, :action, :link, :icon, :size, :data
8
+
9
+ def initialize(text: "Button", action: nil, data: {}, link: "#", icon: nil, size: :regular, id: nil)
10
+ @text = text
11
+ @action = action
12
+ @data = data
13
+ @link = link
14
+ @icon = icon
15
+ @size = size
16
+ @id = id
17
+ end
18
+
19
+ def call
20
+ @icon = set_icon_from_action(@action) if @action && @icon.nil?
21
+ icon = content_tag(:i, "", class: "mr-2 fa-regular fa-#{@icon}") if @icon
22
+ @text = "#{icon} #{@text.titleize}".html_safe
23
+
24
+ classes = "inline-flex items-center rounded-md font-medium shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
25
+
26
+ case @size
27
+ when :small, :sm
28
+ classes += "gap-x-1.5 px-2.5 py-1.5 text-sm "
29
+ when :medium, :regular, :md
30
+ classes += "gap-x-1.5 px-3 py-2 text-base "
31
+ when :large, :lg
32
+ classes += "gap-x-2 px-3.5 py-2.5 text-lg "
33
+ end
34
+
35
+ classes += case @action
36
+ when :save, :create
37
+ "text-white bg-active"
38
+ when :save_inactive
39
+ "text-white bg-inactive"
40
+ when :secondary
41
+ "text-dark border-2 border-dark bg-transparent hover:bg-light transition-all "
42
+ when :delete, :destroy, :danger
43
+ "text-error border border-error bg-red-100 hover:bg-red-200 hover:text-error focus-visible:outline-red-300 "
44
+ else
45
+ "text-dark border-2 border-dark bg-transparent hover:bg-light transition-all "
46
+ end
47
+
48
+ content_tag :a, href: @link, class: classes, data: @data, id: @id do
49
+ @text
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def set_icon_from_action(action)
56
+ case action
57
+ when :add, :new, :create
58
+ "plus"
59
+ when :save
60
+ "check"
61
+ when :edit, :update
62
+ "pencil"
63
+ when :delete, :destroy
64
+ "trash"
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,13 @@
1
+ <main class="overflow-auto flex-1 h-full min-h-full max-h-full">
2
+ <div class="overflow-auto px-2 pt-4 mx-auto sm:px-6 lg:px-6">
3
+ <%= heading %>
4
+ <%= tab_bar %>
5
+ <%# I mean, you can edit this CSS if you want, but I hope you want to lose 2 hours to iFrame joy? %>
6
+ <section class="flex-auto h-[calc(100vh-10rem)]">
7
+ <div class="flex-1 mt-4 w-full h-full">
8
+ <%= content %>
9
+ </div>
10
+ <%= slideover %>
11
+ </section>
12
+ </div>
13
+ </main>
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module CMS
5
+ module Admin
6
+ class ContainerComponent < ViewComponent::Base
7
+ renders_one :heading, "Panda::CMS::Admin::HeadingComponent"
8
+ renders_one :tab_bar, "Panda::CMS::Admin::TabBarComponent"
9
+ renders_one :slideover, "Panda::CMS::Admin::SlideoverComponent"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,31 @@
1
+ <div class="fixed top-2 right-2 z-50 p-2 space-y-4 w-full max-w-sm pointer-events-none sm:items-end"
2
+ data-controller="alert"
3
+ <% if @temporary %> data-alert-dismiss-after-value="3000"<% end %>
4
+ data-transition-enter="ease-in-out duration-500"
5
+ data-transition-enter-from="translate-x-full opacity-0"
6
+ data-transition-enter-to="translate-x-0 opacity-100"
7
+ data-transition-leave="ease-in-out duration-500"
8
+ data-transition-leave-from="translate-x-0 opacity-100"
9
+ data-transition-leave-to="translate-x-full opacity-0">
10
+ <div class="overflow-hidden w-full max-w-sm bg-white rounded-lg ring-1 ring-black ring-opacity-5 shadow-lg pointer-events-auto">
11
+ <div class="p-4">
12
+ <div class="flex items-start">
13
+ <div class="flex-shrink-0">
14
+ <i class="fa-regular text-xl <%= icon_css %> <%= text_colour_css %>"></i>
15
+ </div>
16
+ <div class="flex-1 pt-0.5 ml-3 w-0">
17
+ <p class="mb-1 text-sm font-medium flash-message-title <%= text_colour_css %>"><%= kind.to_s.titleize %></p>
18
+ <p class="mt-1 mb-0 text-sm text-gray-500 flash-message-text"><%= message %></p>
19
+ </div>
20
+ <div class="flex flex-shrink-0 ml-4">
21
+ <button data-action="alert#close" type="button" class="inline-flex text-gray-400 bg-white rounded-md transition duration-150 ease-in-out hover:text-gray-500 focus:ring-2 focus:ring-offset-2 focus:outline-none focus:ring-sky-500">
22
+ <span class="sr-only">Close</span>
23
+ <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
24
+ <path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" />
25
+ </svg>
26
+ </button>
27
+ </div>
28
+ </div>
29
+ </div>
30
+ </div>
31
+ </div>
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module CMS
5
+ module Admin
6
+ class FlashMessageComponent < ::ViewComponent::Base
7
+ attr_reader :kind, :message
8
+
9
+ def initialize(message:, kind:, temporary: true)
10
+ @kind = kind.to_sym
11
+ @message = message
12
+ @temporary = temporary
13
+ end
14
+
15
+ def text_colour_css
16
+ case kind
17
+ when :success
18
+ "text-active"
19
+ when :alert, :error
20
+ "text-error"
21
+ when :warning
22
+ "text-warning"
23
+ when :info, :notice
24
+ "text-active"
25
+ else
26
+ "text-mid"
27
+ end
28
+ end
29
+
30
+ def icon_css
31
+ case kind
32
+ when :success
33
+ "fa-circle-check"
34
+ when :alert
35
+ "fa-circle-xmark"
36
+ when :warning
37
+ "fa-triangle-exclamation"
38
+ when :info, :notice
39
+ "fa-circle-info"
40
+ else
41
+ "fa-circle-info"
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end