lean_cms 0.2.12
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +235 -0
- data/LICENSE +21 -0
- data/README.md +107 -0
- data/app/assets/images/lean_cms/sloth-404.png +0 -0
- data/app/assets/images/lean_cms/sloth-500.png +0 -0
- data/app/assets/images/lean_cms/sloth-favicon-16.png +0 -0
- data/app/assets/images/lean_cms/sloth-favicon-32.png +0 -0
- data/app/assets/images/lean_cms/sloth-favicon-64.png +0 -0
- data/app/assets/images/lean_cms/sloth-logo.png +0 -0
- data/app/assets/lean_cms/actiontext.css +440 -0
- data/app/assets/lean_cms/cms_edit_controls.css +548 -0
- data/app/assets/tailwind/lean_cms/engine.css +14 -0
- data/app/components/lean_cms/base_component.rb +61 -0
- data/app/components/lean_cms/bullets_section_component.html.erb +23 -0
- data/app/components/lean_cms/bullets_section_component.rb +54 -0
- data/app/components/lean_cms/cards_section_component.html.erb +237 -0
- data/app/components/lean_cms/cards_section_component.rb +71 -0
- data/app/components/lean_cms/editable_content_component.html.erb +15 -0
- data/app/components/lean_cms/editable_content_component.rb +53 -0
- data/app/components/lean_cms/section_component.html.erb +18 -0
- data/app/components/lean_cms/section_component.rb +35 -0
- data/app/controllers/concerns/lean_cms/authentication.rb +60 -0
- data/app/controllers/concerns/lean_cms/authorization.rb +60 -0
- data/app/controllers/lean_cms/activity_controller.rb +16 -0
- data/app/controllers/lean_cms/application_controller.rb +48 -0
- data/app/controllers/lean_cms/dashboard_controller.rb +13 -0
- data/app/controllers/lean_cms/form_submissions_controller.rb +37 -0
- data/app/controllers/lean_cms/notification_settings_controller.rb +145 -0
- data/app/controllers/lean_cms/notifications_controller.rb +26 -0
- data/app/controllers/lean_cms/page_contents_controller.rb +403 -0
- data/app/controllers/lean_cms/password_setup_controller.rb +65 -0
- data/app/controllers/lean_cms/passwords_controller.rb +42 -0
- data/app/controllers/lean_cms/posts_controller.rb +78 -0
- data/app/controllers/lean_cms/sessions_controller.rb +50 -0
- data/app/controllers/lean_cms/settings_controller.rb +124 -0
- data/app/controllers/lean_cms/users_controller.rb +113 -0
- data/app/helpers/lean_cms/activity_helper.rb +190 -0
- data/app/helpers/lean_cms/application_helper.rb +43 -0
- data/app/helpers/lean_cms/content_helper.rb +34 -0
- data/app/helpers/lean_cms/page_content_helper.rb +359 -0
- data/app/javascript/controllers/cards_editor_controller.js +317 -0
- data/app/javascript/controllers/cms_sticky_overlay_controller.js +59 -0
- data/app/javascript/controllers/field_editor_form_controller.js +68 -0
- data/app/javascript/controllers/field_editor_modal_controller.js +79 -0
- data/app/javascript/controllers/inline_edit_controller.js +414 -0
- data/app/javascript/controllers/inline_edit_toggle_controller.js +81 -0
- data/app/javascript/controllers/notifications_controller.js +19 -0
- data/app/javascript/controllers/settings_inline_edit_sync_controller.js +38 -0
- data/app/javascript/controllers/settings_override_controller.js +45 -0
- data/app/mailers/lean_cms/application_mailer.rb +6 -0
- data/app/mailers/lean_cms/passwords_mailer.rb +8 -0
- data/app/mailers/lean_cms/users_mailer.rb +39 -0
- data/app/models/lean_cms/current.rb +6 -0
- data/app/models/lean_cms/form_submission.rb +45 -0
- data/app/models/lean_cms/magic_link.rb +76 -0
- data/app/models/lean_cms/meta_tag.rb +30 -0
- data/app/models/lean_cms/notification_setting.rb +69 -0
- data/app/models/lean_cms/page.rb +23 -0
- data/app/models/lean_cms/page_content.rb +245 -0
- data/app/models/lean_cms/post.rb +65 -0
- data/app/models/lean_cms/session.rb +7 -0
- data/app/models/lean_cms/setting.rb +156 -0
- data/app/policies/lean_cms/application_policy.rb +35 -0
- data/app/policies/lean_cms/page_content_policy.rb +31 -0
- data/app/policies/lean_cms/post_policy.rb +37 -0
- data/app/policies/lean_cms/setting_policy.rb +17 -0
- data/app/views/layouts/lean_cms/application.html.erb +114 -0
- data/app/views/layouts/lean_cms/auth.html.erb +200 -0
- data/app/views/lean_cms/activity/index.html.erb +79 -0
- data/app/views/lean_cms/dashboard/index.html.erb +180 -0
- data/app/views/lean_cms/form_submissions/index.html.erb +104 -0
- data/app/views/lean_cms/form_submissions/show.html.erb +157 -0
- data/app/views/lean_cms/notification_settings/edit.html.erb +192 -0
- data/app/views/lean_cms/notifications/index.html.erb +72 -0
- data/app/views/lean_cms/notifications/show.html.erb +39 -0
- data/app/views/lean_cms/page_contents/_field_editor.html.erb +174 -0
- data/app/views/lean_cms/page_contents/edit.html.erb +428 -0
- data/app/views/lean_cms/page_contents/index.html.erb +113 -0
- data/app/views/lean_cms/password_setup/show.html.erb +35 -0
- data/app/views/lean_cms/passwords/edit.html.erb +26 -0
- data/app/views/lean_cms/passwords/new.html.erb +21 -0
- data/app/views/lean_cms/passwords_mailer/reset.html.erb +6 -0
- data/app/views/lean_cms/passwords_mailer/reset.text.erb +4 -0
- data/app/views/lean_cms/posts/_form.html.erb +118 -0
- data/app/views/lean_cms/posts/edit.html.erb +31 -0
- data/app/views/lean_cms/posts/index.html.erb +100 -0
- data/app/views/lean_cms/posts/new.html.erb +16 -0
- data/app/views/lean_cms/sessions/new.html.erb +28 -0
- data/app/views/lean_cms/settings/edit.html.erb +384 -0
- data/app/views/lean_cms/shared/_admin_bar.html.erb +85 -0
- data/app/views/lean_cms/shared/_header.html.erb +86 -0
- data/app/views/lean_cms/shared/_notifications_bell.html.erb +84 -0
- data/app/views/lean_cms/shared/_sidebar.html.erb +102 -0
- data/app/views/lean_cms/users/_form.html.erb +105 -0
- data/app/views/lean_cms/users/edit.html.erb +8 -0
- data/app/views/lean_cms/users/index.html.erb +99 -0
- data/app/views/lean_cms/users/new.html.erb +8 -0
- data/app/views/lean_cms/users_mailer/admin_triggered_password_reset.html.erb +13 -0
- data/app/views/lean_cms/users_mailer/admin_triggered_password_reset.text.erb +11 -0
- data/app/views/lean_cms/users_mailer/invitation.html.erb +13 -0
- data/app/views/lean_cms/users_mailer/invitation.text.erb +11 -0
- data/app/views/lean_cms/users_mailer/reactivation.html.erb +13 -0
- data/app/views/lean_cms/users_mailer/reactivation.text.erb +11 -0
- data/config/importmap.rb +8 -0
- data/config/routes.rb +78 -0
- data/db/migrate/20251112034030_create_lean_cms_tables.rb +131 -0
- data/db/migrate/20260513000001_create_lean_cms_auth_tables.rb +31 -0
- data/db/migrate/20260514000001_create_paper_trail_versions.rb +16 -0
- data/db/migrate/20260514000002_create_action_text_tables.rb +18 -0
- data/db/migrate/20260514000003_create_active_storage_tables.rb +45 -0
- data/db/migrate/20260514000004_create_noticed_tables.rb +27 -0
- data/lib/generators/lean_cms/demo/demo_generator.rb +54 -0
- data/lib/generators/lean_cms/demo/templates/lean_cms_structure.yml +129 -0
- data/lib/generators/lean_cms/demo/templates/pages_controller.rb +30 -0
- data/lib/generators/lean_cms/demo/templates/views/pages/about.html.erb +40 -0
- data/lib/generators/lean_cms/demo/templates/views/pages/contact.html.erb +55 -0
- data/lib/generators/lean_cms/demo/templates/views/pages/home.html.erb +31 -0
- data/lib/generators/lean_cms/install/install_generator.rb +317 -0
- data/lib/generators/lean_cms/install/templates/add_lean_cms_columns_to_users.rb.tt +7 -0
- data/lib/generators/lean_cms/install/templates/lean_cms.rb +11 -0
- data/lib/generators/lean_cms/install/templates/lean_cms_structure.yml +29 -0
- data/lib/lean_cms/configuration.rb +32 -0
- data/lib/lean_cms/engine.rb +93 -0
- data/lib/lean_cms/loader.rb +217 -0
- data/lib/lean_cms/sync_helper.rb +182 -0
- data/lib/lean_cms/version.rb +3 -0
- data/lib/lean_cms.rb +26 -0
- data/lib/tasks/lean_cms.rake +390 -0
- metadata +313 -0
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
/* CMS Edit Controls - In-context editing for logged-in CMS users */
|
|
2
|
+
|
|
3
|
+
/* All interactive elements rendered by the CMS — admin bar buttons, modal
|
|
4
|
+
editor controls, the close-X — should show a pointer cursor on hover.
|
|
5
|
+
Tailwind v4's preflight no longer sets cursor:pointer on <button>, so we
|
|
6
|
+
restore it here scoped to the CMS UI surfaces. */
|
|
7
|
+
.cms-modal a,
|
|
8
|
+
.cms-modal button,
|
|
9
|
+
.cms-modal [role="button"],
|
|
10
|
+
.cms-modal input[type="submit"],
|
|
11
|
+
.cms-modal input[type="button"],
|
|
12
|
+
.cms-modal label[for],
|
|
13
|
+
[data-controller~="inline-edit-toggle"] a,
|
|
14
|
+
[data-controller~="inline-edit-toggle"] button,
|
|
15
|
+
[data-controller~="inline-edit-toggle"] [role="button"],
|
|
16
|
+
[data-controller~="inline-edit-toggle"] label[for] {
|
|
17
|
+
cursor: pointer;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.cms-editable-section {
|
|
21
|
+
position: relative !important;
|
|
22
|
+
border: 2px dashed transparent !important;
|
|
23
|
+
transition: border-color 0.2s ease;
|
|
24
|
+
isolation: isolate;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.cms-editable-section:hover {
|
|
28
|
+
border-color: rgba(37, 99, 235, 0.6) !important;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* Disable section borders when inline editing is turned off */
|
|
32
|
+
.cms-editable-section.cms-inline-editing-disabled:hover {
|
|
33
|
+
border-color: transparent !important;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.cms-editable-section.cms-inline-editing-disabled .cms-edit-overlay {
|
|
37
|
+
display: none !important;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.cms-edit-overlay {
|
|
41
|
+
position: absolute !important;
|
|
42
|
+
top: 12px !important;
|
|
43
|
+
right: 12px !important;
|
|
44
|
+
opacity: 0;
|
|
45
|
+
transition: opacity 0.2s ease;
|
|
46
|
+
pointer-events: none;
|
|
47
|
+
z-index: 9999 !important;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* Only the very first editable section on the page needs extra space for the header */
|
|
51
|
+
main > .cms-editable-section:first-of-type .cms-edit-overlay {
|
|
52
|
+
top: 120px !important;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.cms-editable-section:hover .cms-edit-overlay {
|
|
56
|
+
opacity: 1;
|
|
57
|
+
pointer-events: auto;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.cms-edit-controls {
|
|
61
|
+
display: flex !important;
|
|
62
|
+
align-items: center !important;
|
|
63
|
+
gap: 12px !important;
|
|
64
|
+
background: rgba(37, 99, 235, 0.95) !important;
|
|
65
|
+
color: white !important;
|
|
66
|
+
padding: 10px 14px !important;
|
|
67
|
+
border-radius: 8px !important;
|
|
68
|
+
font-size: 13px !important;
|
|
69
|
+
font-weight: 500 !important;
|
|
70
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15), 0 2px 4px rgba(0, 0, 0, 0.1) !important;
|
|
71
|
+
backdrop-filter: blur(8px) !important;
|
|
72
|
+
white-space: nowrap !important;
|
|
73
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.cms-section-title {
|
|
77
|
+
font-weight: 600 !important;
|
|
78
|
+
letter-spacing: 0.01em !important;
|
|
79
|
+
padding-right: 8px !important;
|
|
80
|
+
border-right: 1px solid rgba(255, 255, 255, 0.3) !important;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.cms-edit-button {
|
|
84
|
+
display: inline-flex !important;
|
|
85
|
+
align-items: center !important;
|
|
86
|
+
gap: 6px !important;
|
|
87
|
+
padding: 6px 12px !important;
|
|
88
|
+
background: white !important;
|
|
89
|
+
color: rgb(37, 99, 235) !important;
|
|
90
|
+
border-radius: 6px !important;
|
|
91
|
+
font-size: 12px !important;
|
|
92
|
+
font-weight: 600 !important;
|
|
93
|
+
text-decoration: none !important;
|
|
94
|
+
transition: all 0.15s ease !important;
|
|
95
|
+
line-height: 1 !important;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.cms-edit-button:hover {
|
|
99
|
+
background: rgb(243, 244, 246) !important;
|
|
100
|
+
transform: translateY(-1px) !important;
|
|
101
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important;
|
|
102
|
+
color: rgb(37, 99, 235) !important;
|
|
103
|
+
text-decoration: none !important;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.cms-edit-button::after {
|
|
107
|
+
content: '→' !important;
|
|
108
|
+
font-size: 14px !important;
|
|
109
|
+
margin-left: 2px !important;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* Sticky overlay state - needs to clear the fixed header + admin bar (~128px total) */
|
|
113
|
+
.cms-edit-overlay.cms-overlay-stuck {
|
|
114
|
+
position: fixed !important;
|
|
115
|
+
top: 140px !important;
|
|
116
|
+
right: auto !important; /* Clear right so left positioning works correctly */
|
|
117
|
+
z-index: 9999 !important;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* Make sure edit controls work on dark backgrounds */
|
|
121
|
+
.cms-editable-section.dark-section:hover {
|
|
122
|
+
outline-color: rgba(255, 255, 255, 0.5);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* Ensure proper stacking for sections with their own positioning */
|
|
126
|
+
.cms-editable-section > section,
|
|
127
|
+
.cms-editable-section > div {
|
|
128
|
+
position: relative;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* Inline editing field wrapper */
|
|
132
|
+
.cms-inline-field {
|
|
133
|
+
position: relative;
|
|
134
|
+
display: inline-block;
|
|
135
|
+
min-width: 2em;
|
|
136
|
+
transition: all 0.2s ease;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.cms-inline-field:hover {
|
|
140
|
+
background-color: rgba(59, 130, 246, 0.05);
|
|
141
|
+
outline: 2px dashed rgba(59, 130, 246, 0.3);
|
|
142
|
+
outline-offset: 2px;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/* Disable inline field hover effects when editing is off */
|
|
146
|
+
body.cms-inline-editing-disabled .cms-inline-field:hover {
|
|
147
|
+
background-color: transparent;
|
|
148
|
+
outline: none;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* Edit icon container */
|
|
152
|
+
.cms-inline-edit-icons {
|
|
153
|
+
position: absolute;
|
|
154
|
+
top: -8px;
|
|
155
|
+
right: -8px;
|
|
156
|
+
display: none;
|
|
157
|
+
gap: 4px;
|
|
158
|
+
z-index: 10;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.cms-inline-field:hover .cms-inline-edit-icons {
|
|
162
|
+
display: flex;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* Hide inline edit icons when editing is disabled */
|
|
166
|
+
body.cms-inline-editing-disabled .cms-inline-field:hover .cms-inline-edit-icons {
|
|
167
|
+
display: none !important;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/* Individual edit icons */
|
|
171
|
+
.cms-inline-edit-icon {
|
|
172
|
+
width: 24px;
|
|
173
|
+
height: 24px;
|
|
174
|
+
color: white;
|
|
175
|
+
border-radius: 50%;
|
|
176
|
+
display: flex;
|
|
177
|
+
align-items: center;
|
|
178
|
+
justify-content: center;
|
|
179
|
+
cursor: pointer;
|
|
180
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
181
|
+
transition: all 0.2s ease;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.cms-edit-icon-edit {
|
|
185
|
+
background: rgb(59, 130, 246);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.cms-edit-icon-edit:hover {
|
|
189
|
+
background: rgb(37, 99, 235);
|
|
190
|
+
transform: scale(1.1);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.cms-edit-icon-undo {
|
|
194
|
+
background: rgb(251, 146, 60);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.cms-edit-icon-undo:hover {
|
|
198
|
+
background: rgb(249, 115, 22);
|
|
199
|
+
transform: scale(1.1);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* Inline input */
|
|
203
|
+
.cms-inline-input {
|
|
204
|
+
width: 100%;
|
|
205
|
+
min-width: 200px;
|
|
206
|
+
padding: 8px 12px;
|
|
207
|
+
border: 2px solid rgb(59, 130, 246);
|
|
208
|
+
border-radius: 4px;
|
|
209
|
+
font: inherit;
|
|
210
|
+
font-size: inherit;
|
|
211
|
+
color: #1f2937 !important;
|
|
212
|
+
background: white;
|
|
213
|
+
outline: none;
|
|
214
|
+
box-sizing: border-box;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/* Inline textarea (for multi-line content) */
|
|
218
|
+
.cms-inline-textarea {
|
|
219
|
+
resize: vertical;
|
|
220
|
+
line-height: inherit;
|
|
221
|
+
font-family: inherit;
|
|
222
|
+
text-align: inherit;
|
|
223
|
+
color: #1f2937 !important;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/* Feedback message */
|
|
227
|
+
.cms-inline-feedback {
|
|
228
|
+
position: absolute;
|
|
229
|
+
bottom: -24px;
|
|
230
|
+
right: 0;
|
|
231
|
+
padding: 4px 8px;
|
|
232
|
+
border-radius: 4px;
|
|
233
|
+
font-size: 12px;
|
|
234
|
+
font-weight: 500;
|
|
235
|
+
white-space: nowrap;
|
|
236
|
+
z-index: 100;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.cms-inline-feedback-success {
|
|
240
|
+
background: rgb(34, 197, 94);
|
|
241
|
+
color: white;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.cms-inline-feedback-error {
|
|
245
|
+
background: rgb(239, 68, 68);
|
|
246
|
+
color: white;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/* Inline editing buttons */
|
|
250
|
+
.cms-inline-buttons {
|
|
251
|
+
display: flex;
|
|
252
|
+
gap: 8px;
|
|
253
|
+
margin-top: 8px;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.cms-inline-button {
|
|
257
|
+
display: inline-flex;
|
|
258
|
+
align-items: center;
|
|
259
|
+
gap: 6px;
|
|
260
|
+
padding: 6px 12px;
|
|
261
|
+
border: none;
|
|
262
|
+
border-radius: 6px;
|
|
263
|
+
font-size: 13px;
|
|
264
|
+
font-weight: 600;
|
|
265
|
+
cursor: pointer;
|
|
266
|
+
transition: all 0.15s ease;
|
|
267
|
+
white-space: nowrap;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.cms-inline-button svg {
|
|
271
|
+
flex-shrink: 0;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.cms-inline-button-save {
|
|
275
|
+
background: rgb(34, 197, 94);
|
|
276
|
+
color: white;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.cms-inline-button-save:hover {
|
|
280
|
+
background: rgb(22, 163, 74);
|
|
281
|
+
transform: translateY(-1px);
|
|
282
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.cms-inline-button-cancel {
|
|
286
|
+
background: rgb(107, 114, 128);
|
|
287
|
+
color: white;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.cms-inline-button-cancel:hover {
|
|
291
|
+
background: rgb(75, 85, 99);
|
|
292
|
+
transform: translateY(-1px);
|
|
293
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/* Modal styles */
|
|
297
|
+
.cms-modal {
|
|
298
|
+
backdrop-filter: blur(4px);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.cms-modal.hidden {
|
|
302
|
+
display: none !important;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/* Inline text edit dialog (replaces in-place input approach) */
|
|
306
|
+
.cms-text-edit-overlay {
|
|
307
|
+
position: fixed;
|
|
308
|
+
inset: 0;
|
|
309
|
+
z-index: 10001;
|
|
310
|
+
display: flex;
|
|
311
|
+
align-items: center;
|
|
312
|
+
justify-content: center;
|
|
313
|
+
background: rgba(0, 0, 0, 0.45);
|
|
314
|
+
backdrop-filter: blur(2px);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.cms-text-edit-dialog {
|
|
318
|
+
background: #fff;
|
|
319
|
+
border-radius: 12px;
|
|
320
|
+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.25), 0 4px 16px rgba(0, 0, 0, 0.1);
|
|
321
|
+
width: 480px;
|
|
322
|
+
max-width: calc(100vw - 32px);
|
|
323
|
+
overflow: hidden;
|
|
324
|
+
animation: cms-dialog-in 0.15s ease;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
@keyframes cms-dialog-in {
|
|
328
|
+
from { opacity: 0; transform: scale(0.95) translateY(-8px); }
|
|
329
|
+
to { opacity: 1; transform: scale(1) translateY(0); }
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.cms-text-edit-header {
|
|
333
|
+
display: flex;
|
|
334
|
+
align-items: center;
|
|
335
|
+
justify-content: space-between;
|
|
336
|
+
padding: 14px 18px;
|
|
337
|
+
border-bottom: 1px solid #e5e7eb;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.cms-text-edit-label {
|
|
341
|
+
font-size: 13px;
|
|
342
|
+
font-weight: 600;
|
|
343
|
+
color: #374151;
|
|
344
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.cms-text-edit-close {
|
|
348
|
+
display: flex;
|
|
349
|
+
align-items: center;
|
|
350
|
+
justify-content: center;
|
|
351
|
+
width: 28px;
|
|
352
|
+
height: 28px;
|
|
353
|
+
border: none;
|
|
354
|
+
background: transparent;
|
|
355
|
+
color: #9ca3af;
|
|
356
|
+
border-radius: 6px;
|
|
357
|
+
cursor: pointer;
|
|
358
|
+
transition: background 0.15s, color 0.15s;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.cms-text-edit-close:hover {
|
|
362
|
+
background: #f3f4f6;
|
|
363
|
+
color: #374151;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
.cms-text-edit-input {
|
|
367
|
+
display: block;
|
|
368
|
+
width: 100%;
|
|
369
|
+
padding: 14px 18px;
|
|
370
|
+
border: none;
|
|
371
|
+
border-bottom: 1px solid #e5e7eb;
|
|
372
|
+
font-size: 15px;
|
|
373
|
+
font-family: inherit;
|
|
374
|
+
color: #111827;
|
|
375
|
+
background: #f9fafb;
|
|
376
|
+
outline: none;
|
|
377
|
+
resize: vertical;
|
|
378
|
+
box-sizing: border-box;
|
|
379
|
+
transition: background 0.15s;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.cms-text-edit-input:focus {
|
|
383
|
+
background: #fff;
|
|
384
|
+
border-bottom-color: #3b82f6;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.cms-text-edit-footer {
|
|
388
|
+
display: flex;
|
|
389
|
+
justify-content: flex-end;
|
|
390
|
+
gap: 8px;
|
|
391
|
+
padding: 12px 18px;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.cms-text-edit-btn {
|
|
395
|
+
padding: 7px 18px;
|
|
396
|
+
border: none;
|
|
397
|
+
border-radius: 7px;
|
|
398
|
+
font-size: 13px;
|
|
399
|
+
font-weight: 600;
|
|
400
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
401
|
+
cursor: pointer;
|
|
402
|
+
transition: all 0.15s ease;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
.cms-text-edit-btn-cancel {
|
|
406
|
+
background: #f3f4f6;
|
|
407
|
+
color: #374151;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
.cms-text-edit-btn-cancel:hover {
|
|
411
|
+
background: #e5e7eb;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.cms-text-edit-btn-save {
|
|
415
|
+
background: #2563eb;
|
|
416
|
+
color: #fff;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
.cms-text-edit-btn-save:hover {
|
|
420
|
+
background: #1d4ed8;
|
|
421
|
+
transform: translateY(-1px);
|
|
422
|
+
box-shadow: 0 2px 6px rgba(37, 99, 235, 0.35);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
.cms-text-edit-btn-save:disabled {
|
|
426
|
+
opacity: 0.6;
|
|
427
|
+
cursor: not-allowed;
|
|
428
|
+
transform: none;
|
|
429
|
+
box-shadow: none;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
.cms-text-edit-btn-danger {
|
|
433
|
+
background: #dc2626;
|
|
434
|
+
color: #fff;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
.cms-text-edit-btn-danger:hover {
|
|
438
|
+
background: #b91c1c;
|
|
439
|
+
transform: translateY(-1px);
|
|
440
|
+
box-shadow: 0 2px 6px rgba(220, 38, 38, 0.35);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/* Confirm dialog — slightly wider to accommodate diff */
|
|
444
|
+
.cms-confirm-dialog {
|
|
445
|
+
width: 480px;
|
|
446
|
+
max-width: 92vw;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
.cms-confirm-message {
|
|
450
|
+
padding: 18px 18px 8px;
|
|
451
|
+
font-size: 14px;
|
|
452
|
+
color: #4b5563;
|
|
453
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
454
|
+
line-height: 1.5;
|
|
455
|
+
margin: 0;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/* Undo diff block */
|
|
459
|
+
.cms-diff-block {
|
|
460
|
+
margin: 14px 18px 6px;
|
|
461
|
+
border-radius: 8px;
|
|
462
|
+
overflow: hidden;
|
|
463
|
+
border: 1px solid #e5e7eb;
|
|
464
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
465
|
+
font-size: 13px;
|
|
466
|
+
line-height: 1.55;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
.cms-diff-row {
|
|
470
|
+
display: flex;
|
|
471
|
+
gap: 0;
|
|
472
|
+
align-items: baseline;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
.cms-diff-row-current {
|
|
476
|
+
background: #fef2f2;
|
|
477
|
+
border-bottom: 1px solid #fecaca;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
.cms-diff-row-previous {
|
|
481
|
+
background: #f0fdf4;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
.cms-diff-label {
|
|
485
|
+
flex: 0 0 96px;
|
|
486
|
+
padding: 9px 10px 9px 12px;
|
|
487
|
+
font-size: 11px;
|
|
488
|
+
font-weight: 600;
|
|
489
|
+
text-transform: uppercase;
|
|
490
|
+
letter-spacing: 0.04em;
|
|
491
|
+
white-space: nowrap;
|
|
492
|
+
text-align: right;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
.cms-diff-row-current .cms-diff-label { color: #b91c1c; }
|
|
496
|
+
.cms-diff-row-previous .cms-diff-label { color: #15803d; }
|
|
497
|
+
|
|
498
|
+
.cms-diff-value {
|
|
499
|
+
flex: 1;
|
|
500
|
+
padding: 9px 12px 9px 0;
|
|
501
|
+
color: #111827;
|
|
502
|
+
word-break: break-word;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/* Highlighted changed words */
|
|
506
|
+
.cms-diff-value del {
|
|
507
|
+
text-decoration: none;
|
|
508
|
+
background: #fca5a5;
|
|
509
|
+
border-radius: 3px;
|
|
510
|
+
padding: 1px 2px;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
.cms-diff-value ins {
|
|
514
|
+
text-decoration: none;
|
|
515
|
+
background: #86efac;
|
|
516
|
+
border-radius: 3px;
|
|
517
|
+
padding: 1px 2px;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/* Cards editor — color swatch button */
|
|
521
|
+
.cards-color-swatch {
|
|
522
|
+
flex: 0 0 36px;
|
|
523
|
+
width: 36px;
|
|
524
|
+
height: 36px;
|
|
525
|
+
padding: 2px;
|
|
526
|
+
border: 1px solid #d1d5db;
|
|
527
|
+
border-radius: 8px;
|
|
528
|
+
cursor: pointer;
|
|
529
|
+
background: none;
|
|
530
|
+
appearance: none;
|
|
531
|
+
-webkit-appearance: none;
|
|
532
|
+
overflow: hidden;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
.cards-color-swatch::-webkit-color-swatch-wrapper {
|
|
536
|
+
padding: 0;
|
|
537
|
+
border-radius: 6px;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
.cards-color-swatch::-webkit-color-swatch {
|
|
541
|
+
border: none;
|
|
542
|
+
border-radius: 6px;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
.cards-color-swatch:hover {
|
|
546
|
+
border-color: #6b7280;
|
|
547
|
+
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2);
|
|
548
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/* LeanCMS engine Tailwind sources.
|
|
2
|
+
tailwindcss-rails auto-imports this via app/assets/builds/tailwind/lean_cms.css.
|
|
3
|
+
Paths are relative to this file's location inside the gem. */
|
|
4
|
+
|
|
5
|
+
@source "../../../../app/views/**/*.erb";
|
|
6
|
+
@source "../../../../app/javascript/controllers/**/*.js";
|
|
7
|
+
|
|
8
|
+
/* Safelist for class names that the gem's view components interpolate at
|
|
9
|
+
runtime (e.g. `gap-<%= gap %>` in CardsSectionComponent). Tailwind's
|
|
10
|
+
@source scanner reads literal strings only — it can't follow ERB — so
|
|
11
|
+
those utilities never make it into the host's compiled CSS unless they
|
|
12
|
+
appear elsewhere or are safelisted here. */
|
|
13
|
+
@source inline("md:grid-cols-{1,2,3,4,5,6}");
|
|
14
|
+
@source inline("gap-{2,3,4,5,6,8,10,12}");
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module LeanCms
|
|
2
|
+
class BaseComponent < ViewComponent::Base
|
|
3
|
+
attr_reader :page
|
|
4
|
+
|
|
5
|
+
def initialize(page: nil, **options)
|
|
6
|
+
@page = page || @view_context.instance_variable_get(:@page)
|
|
7
|
+
super(**options)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
# Check if current user can edit CMS content
|
|
13
|
+
def can_edit_cms?
|
|
14
|
+
return false unless @view_context.respond_to?(:authenticated?) && @view_context.authenticated?
|
|
15
|
+
return false unless @view_context.current_user&.has_any_cms_permission?
|
|
16
|
+
LeanCms::Setting.get('in_context_editing', 'true') == 'true'
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Generate cache key for this component.
|
|
20
|
+
# Pre-normalization era: `page` may be a raw slug String. In that case we
|
|
21
|
+
# don't have a Page record to read updated_at off of, so fall back to the
|
|
22
|
+
# max(updated_at) over PageContent rows for that slug — which is touched
|
|
23
|
+
# whenever an editor changes any content on the page.
|
|
24
|
+
def cache_key(identifier)
|
|
25
|
+
page_updated_at = if page.is_a?(LeanCms::Page)
|
|
26
|
+
page.updated_at
|
|
27
|
+
else
|
|
28
|
+
LeanCms::PageContent.where(page: page.to_s).maximum(:updated_at)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
["lean_cms", page_slug, identifier, page_updated_at&.to_i, can_edit_cms?]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Get page slug (string)
|
|
35
|
+
def page_slug
|
|
36
|
+
page.is_a?(LeanCms::Page) ? page.slug : page.to_s
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Find a PageContent field using preloaded data if available
|
|
40
|
+
def find_field(section, key)
|
|
41
|
+
if page.is_a?(LeanCms::Page) && page.page_contents.loaded?
|
|
42
|
+
page.page_contents.find { |pc| pc.section == section.to_s && pc.key == key.to_s }
|
|
43
|
+
elsif page.is_a?(LeanCms::Page)
|
|
44
|
+
LeanCms::PageContent.find_by(page_id: page.id, section: section, key: key)
|
|
45
|
+
else
|
|
46
|
+
LeanCms::PageContent.find_by("page = ? AND section = ? AND key = ?", page.to_s, section.to_s, key.to_s)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Get field value using preloaded data if available
|
|
51
|
+
def field_value(section, key, default: nil)
|
|
52
|
+
field = find_field(section, key)
|
|
53
|
+
field&.display_value || default
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Expose helpers to components
|
|
57
|
+
def helpers
|
|
58
|
+
@view_context
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<% cache cache_key, expires_in: 1.hour do %>
|
|
2
|
+
<% bullet_items = bullets %>
|
|
3
|
+
<% if bullet_items.any? %>
|
|
4
|
+
<% list_html = capture do %>
|
|
5
|
+
<ul class="space-y-3">
|
|
6
|
+
<% bullet_items.each do |bullet| %>
|
|
7
|
+
<li class="flex items-start text-gray-700">
|
|
8
|
+
<svg class="w-5 h-5 text-[#2563eb] mt-1 mr-3 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
|
9
|
+
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
|
|
10
|
+
</svg>
|
|
11
|
+
<span><%= bullet %></span>
|
|
12
|
+
</li>
|
|
13
|
+
<% end %>
|
|
14
|
+
</ul>
|
|
15
|
+
<% end %>
|
|
16
|
+
|
|
17
|
+
<% if can_edit_cms? && field %>
|
|
18
|
+
<%= content_tag(:div, list_html, class: 'cms-inline-field relative', data: data_attributes) %>
|
|
19
|
+
<% else %>
|
|
20
|
+
<%= list_html %>
|
|
21
|
+
<% end %>
|
|
22
|
+
<% end %>
|
|
23
|
+
<% end %>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module LeanCms
|
|
2
|
+
class BulletsSectionComponent < BaseComponent
|
|
3
|
+
attr_reader :section
|
|
4
|
+
|
|
5
|
+
def initialize(section:, page: nil, **options)
|
|
6
|
+
super(page: page)
|
|
7
|
+
@section = section
|
|
8
|
+
@options = options
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def cache_key
|
|
14
|
+
super("#{section}_bullets")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def bullets
|
|
18
|
+
if page.is_a?(LeanCms::Page) && page.page_contents.loaded?
|
|
19
|
+
content_record = page.page_contents.find { |pc| pc.section == section.to_s && pc.key == 'bullets' }
|
|
20
|
+
return [] unless content_record&.bullets?
|
|
21
|
+
content_record.display_value
|
|
22
|
+
else
|
|
23
|
+
page_key = page.is_a?(LeanCms::Page) ? page.slug : page.to_s
|
|
24
|
+
Rails.cache.fetch("page_bullets/#{page_key}/#{section}", expires_in: 1.hour) do
|
|
25
|
+
content_record = if page.is_a?(LeanCms::Page)
|
|
26
|
+
LeanCms::PageContent.find_by(page_id: page.id, section: section, key: 'bullets')
|
|
27
|
+
else
|
|
28
|
+
LeanCms::PageContent.find_by("page = ? AND section = ? AND key = ?", page.to_s, section.to_s, 'bullets')
|
|
29
|
+
end
|
|
30
|
+
return [] unless content_record&.bullets?
|
|
31
|
+
content_record.display_value
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def field
|
|
37
|
+
@field ||= find_field(section, 'bullets')
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def data_attributes
|
|
41
|
+
return {} unless can_edit_cms? && field
|
|
42
|
+
|
|
43
|
+
{
|
|
44
|
+
controller: 'inline-edit',
|
|
45
|
+
inline_edit_field_id_value: field.id,
|
|
46
|
+
inline_edit_type_value: 'bullets',
|
|
47
|
+
inline_edit_inline_value: false,
|
|
48
|
+
inline_edit_page_value: page_slug,
|
|
49
|
+
inline_edit_section_value: section,
|
|
50
|
+
inline_edit_key_value: 'bullets'
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|