panda-cms 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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