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.
Files changed (130) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +235 -0
  3. data/LICENSE +21 -0
  4. data/README.md +107 -0
  5. data/app/assets/images/lean_cms/sloth-404.png +0 -0
  6. data/app/assets/images/lean_cms/sloth-500.png +0 -0
  7. data/app/assets/images/lean_cms/sloth-favicon-16.png +0 -0
  8. data/app/assets/images/lean_cms/sloth-favicon-32.png +0 -0
  9. data/app/assets/images/lean_cms/sloth-favicon-64.png +0 -0
  10. data/app/assets/images/lean_cms/sloth-logo.png +0 -0
  11. data/app/assets/lean_cms/actiontext.css +440 -0
  12. data/app/assets/lean_cms/cms_edit_controls.css +548 -0
  13. data/app/assets/tailwind/lean_cms/engine.css +14 -0
  14. data/app/components/lean_cms/base_component.rb +61 -0
  15. data/app/components/lean_cms/bullets_section_component.html.erb +23 -0
  16. data/app/components/lean_cms/bullets_section_component.rb +54 -0
  17. data/app/components/lean_cms/cards_section_component.html.erb +237 -0
  18. data/app/components/lean_cms/cards_section_component.rb +71 -0
  19. data/app/components/lean_cms/editable_content_component.html.erb +15 -0
  20. data/app/components/lean_cms/editable_content_component.rb +53 -0
  21. data/app/components/lean_cms/section_component.html.erb +18 -0
  22. data/app/components/lean_cms/section_component.rb +35 -0
  23. data/app/controllers/concerns/lean_cms/authentication.rb +60 -0
  24. data/app/controllers/concerns/lean_cms/authorization.rb +60 -0
  25. data/app/controllers/lean_cms/activity_controller.rb +16 -0
  26. data/app/controllers/lean_cms/application_controller.rb +48 -0
  27. data/app/controllers/lean_cms/dashboard_controller.rb +13 -0
  28. data/app/controllers/lean_cms/form_submissions_controller.rb +37 -0
  29. data/app/controllers/lean_cms/notification_settings_controller.rb +145 -0
  30. data/app/controllers/lean_cms/notifications_controller.rb +26 -0
  31. data/app/controllers/lean_cms/page_contents_controller.rb +403 -0
  32. data/app/controllers/lean_cms/password_setup_controller.rb +65 -0
  33. data/app/controllers/lean_cms/passwords_controller.rb +42 -0
  34. data/app/controllers/lean_cms/posts_controller.rb +78 -0
  35. data/app/controllers/lean_cms/sessions_controller.rb +50 -0
  36. data/app/controllers/lean_cms/settings_controller.rb +124 -0
  37. data/app/controllers/lean_cms/users_controller.rb +113 -0
  38. data/app/helpers/lean_cms/activity_helper.rb +190 -0
  39. data/app/helpers/lean_cms/application_helper.rb +43 -0
  40. data/app/helpers/lean_cms/content_helper.rb +34 -0
  41. data/app/helpers/lean_cms/page_content_helper.rb +359 -0
  42. data/app/javascript/controllers/cards_editor_controller.js +317 -0
  43. data/app/javascript/controllers/cms_sticky_overlay_controller.js +59 -0
  44. data/app/javascript/controllers/field_editor_form_controller.js +68 -0
  45. data/app/javascript/controllers/field_editor_modal_controller.js +79 -0
  46. data/app/javascript/controllers/inline_edit_controller.js +414 -0
  47. data/app/javascript/controllers/inline_edit_toggle_controller.js +81 -0
  48. data/app/javascript/controllers/notifications_controller.js +19 -0
  49. data/app/javascript/controllers/settings_inline_edit_sync_controller.js +38 -0
  50. data/app/javascript/controllers/settings_override_controller.js +45 -0
  51. data/app/mailers/lean_cms/application_mailer.rb +6 -0
  52. data/app/mailers/lean_cms/passwords_mailer.rb +8 -0
  53. data/app/mailers/lean_cms/users_mailer.rb +39 -0
  54. data/app/models/lean_cms/current.rb +6 -0
  55. data/app/models/lean_cms/form_submission.rb +45 -0
  56. data/app/models/lean_cms/magic_link.rb +76 -0
  57. data/app/models/lean_cms/meta_tag.rb +30 -0
  58. data/app/models/lean_cms/notification_setting.rb +69 -0
  59. data/app/models/lean_cms/page.rb +23 -0
  60. data/app/models/lean_cms/page_content.rb +245 -0
  61. data/app/models/lean_cms/post.rb +65 -0
  62. data/app/models/lean_cms/session.rb +7 -0
  63. data/app/models/lean_cms/setting.rb +156 -0
  64. data/app/policies/lean_cms/application_policy.rb +35 -0
  65. data/app/policies/lean_cms/page_content_policy.rb +31 -0
  66. data/app/policies/lean_cms/post_policy.rb +37 -0
  67. data/app/policies/lean_cms/setting_policy.rb +17 -0
  68. data/app/views/layouts/lean_cms/application.html.erb +114 -0
  69. data/app/views/layouts/lean_cms/auth.html.erb +200 -0
  70. data/app/views/lean_cms/activity/index.html.erb +79 -0
  71. data/app/views/lean_cms/dashboard/index.html.erb +180 -0
  72. data/app/views/lean_cms/form_submissions/index.html.erb +104 -0
  73. data/app/views/lean_cms/form_submissions/show.html.erb +157 -0
  74. data/app/views/lean_cms/notification_settings/edit.html.erb +192 -0
  75. data/app/views/lean_cms/notifications/index.html.erb +72 -0
  76. data/app/views/lean_cms/notifications/show.html.erb +39 -0
  77. data/app/views/lean_cms/page_contents/_field_editor.html.erb +174 -0
  78. data/app/views/lean_cms/page_contents/edit.html.erb +428 -0
  79. data/app/views/lean_cms/page_contents/index.html.erb +113 -0
  80. data/app/views/lean_cms/password_setup/show.html.erb +35 -0
  81. data/app/views/lean_cms/passwords/edit.html.erb +26 -0
  82. data/app/views/lean_cms/passwords/new.html.erb +21 -0
  83. data/app/views/lean_cms/passwords_mailer/reset.html.erb +6 -0
  84. data/app/views/lean_cms/passwords_mailer/reset.text.erb +4 -0
  85. data/app/views/lean_cms/posts/_form.html.erb +118 -0
  86. data/app/views/lean_cms/posts/edit.html.erb +31 -0
  87. data/app/views/lean_cms/posts/index.html.erb +100 -0
  88. data/app/views/lean_cms/posts/new.html.erb +16 -0
  89. data/app/views/lean_cms/sessions/new.html.erb +28 -0
  90. data/app/views/lean_cms/settings/edit.html.erb +384 -0
  91. data/app/views/lean_cms/shared/_admin_bar.html.erb +85 -0
  92. data/app/views/lean_cms/shared/_header.html.erb +86 -0
  93. data/app/views/lean_cms/shared/_notifications_bell.html.erb +84 -0
  94. data/app/views/lean_cms/shared/_sidebar.html.erb +102 -0
  95. data/app/views/lean_cms/users/_form.html.erb +105 -0
  96. data/app/views/lean_cms/users/edit.html.erb +8 -0
  97. data/app/views/lean_cms/users/index.html.erb +99 -0
  98. data/app/views/lean_cms/users/new.html.erb +8 -0
  99. data/app/views/lean_cms/users_mailer/admin_triggered_password_reset.html.erb +13 -0
  100. data/app/views/lean_cms/users_mailer/admin_triggered_password_reset.text.erb +11 -0
  101. data/app/views/lean_cms/users_mailer/invitation.html.erb +13 -0
  102. data/app/views/lean_cms/users_mailer/invitation.text.erb +11 -0
  103. data/app/views/lean_cms/users_mailer/reactivation.html.erb +13 -0
  104. data/app/views/lean_cms/users_mailer/reactivation.text.erb +11 -0
  105. data/config/importmap.rb +8 -0
  106. data/config/routes.rb +78 -0
  107. data/db/migrate/20251112034030_create_lean_cms_tables.rb +131 -0
  108. data/db/migrate/20260513000001_create_lean_cms_auth_tables.rb +31 -0
  109. data/db/migrate/20260514000001_create_paper_trail_versions.rb +16 -0
  110. data/db/migrate/20260514000002_create_action_text_tables.rb +18 -0
  111. data/db/migrate/20260514000003_create_active_storage_tables.rb +45 -0
  112. data/db/migrate/20260514000004_create_noticed_tables.rb +27 -0
  113. data/lib/generators/lean_cms/demo/demo_generator.rb +54 -0
  114. data/lib/generators/lean_cms/demo/templates/lean_cms_structure.yml +129 -0
  115. data/lib/generators/lean_cms/demo/templates/pages_controller.rb +30 -0
  116. data/lib/generators/lean_cms/demo/templates/views/pages/about.html.erb +40 -0
  117. data/lib/generators/lean_cms/demo/templates/views/pages/contact.html.erb +55 -0
  118. data/lib/generators/lean_cms/demo/templates/views/pages/home.html.erb +31 -0
  119. data/lib/generators/lean_cms/install/install_generator.rb +317 -0
  120. data/lib/generators/lean_cms/install/templates/add_lean_cms_columns_to_users.rb.tt +7 -0
  121. data/lib/generators/lean_cms/install/templates/lean_cms.rb +11 -0
  122. data/lib/generators/lean_cms/install/templates/lean_cms_structure.yml +29 -0
  123. data/lib/lean_cms/configuration.rb +32 -0
  124. data/lib/lean_cms/engine.rb +93 -0
  125. data/lib/lean_cms/loader.rb +217 -0
  126. data/lib/lean_cms/sync_helper.rb +182 -0
  127. data/lib/lean_cms/version.rb +3 -0
  128. data/lib/lean_cms.rb +26 -0
  129. data/lib/tasks/lean_cms.rake +390 -0
  130. metadata +313 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: cf29b9988643df801321311e1154d6f553ad2919ecede28dec23027a8ef57a96
4
+ data.tar.gz: fea9f113f47e67371e5f97a97bbe80342ed0206bd5512befc629e12067079f2f
5
+ SHA512:
6
+ metadata.gz: 54d383a8799e2391a5f7ac84d1a3aa07ac332798a7394866ce98b5cab46c11614d7fc5be719aa9cba5e33e18fdcb04693e0ea73aba9d3a36f555c2e03e2fe0f7
7
+ data.tar.gz: 230440bd456f36ab053e8855968ef6f189eec98b04b0cd75aab11aeec902b8c4c710c064a3f937063ea7637ea1593738deaac2ba41cf87997d1b3eed9faa978d
data/CHANGELOG.md ADDED
@@ -0,0 +1,235 @@
1
+ # Changelog
2
+
3
+ All notable changes to Lean CMS will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.2.12] — 2026-05-15
11
+
12
+ ### Changed
13
+ - **`lean_cms:sync:{pull,push,stage,start,finish}` now abort with a clear message on non-SQLite adapters.** These tasks `cp` SQLite database files directly and have always been SQLite-specific; the new guard prevents accidental no-ops or confusing errors on Postgres/MySQL hosts. `lean_cms:sync:{lock,unlock,status,workflow}` are unaffected — they only toggle a `LeanCms::Setting` and work on any adapter. Task descriptions updated to reflect the SQLite-only scope.
14
+
15
+ ### Verified
16
+ - **Postgres compatibility POC.** Fresh Rails 8 + `--database=postgresql` host, install generator, all 7 gem-shipped migrations (`lean_cms_*`, PaperTrail, Action Text, Active Storage, Noticed), `lean_cms:load_structure`, and the rich_text association all worked end-to-end with zero gem changes. The gem's schema is Rails-DSL-only — happy path remains SQLite, but Postgres (and likely MySQL) installs work out of the box for hosts that prefer them. Docs at leancms.dev to be updated separately.
17
+
18
+ ## [0.2.11] — 2026-05-14
19
+
20
+ ### Added
21
+ - **`cms_google_analytics_tag` helper.** Renders a GA4 gtag.js snippet using the measurement ID stored in `LeanCms::Setting.get("google_analytics_id")`. Returns an empty string when the setting is blank — safe to call unconditionally from your layout's `<head>`. Admins set the ID via `/lean-cms/settings` without touching code. Example value: `G-XXXXXXXXXX`.
22
+
23
+ ```erb
24
+ <head>
25
+ <!-- ... -->
26
+ <%= cms_google_analytics_tag %>
27
+ </head>
28
+ ```
29
+
30
+ ## [0.2.10] — 2026-05-14
31
+
32
+ Install-flow polish — plugs three remaining "fresh install doesn't fully work" gaps surfaced bootstrapping the demo site, all related to gem-side expectations of host-side wiring.
33
+
34
+ ### Added
35
+ - **Gem now ships the Noticed table migrations.** `noticed_events` + `noticed_notifications` were the last unbundled tables the gem's admin notifications bell needed; hosts previously had to run `bin/rails noticed:install:migrations` separately. New `db/migrate/20260514000004_create_noticed_tables.rb` uses `create_table … if_not_exists: true` so installs that ran `noticed:install:migrations` independently are unaffected.
36
+ - **Engine initializer drops an `actiontext.css` stub** into the host's `app/assets/stylesheets/` on boot (mirrors the existing `lean_cms.edit_controls_css` initializer). The gem's admin layout calls `stylesheet_link_tag "actiontext"` from day one, so the file has to exist before `bin/rails action_text:install` would normally run. Only written once — hosts can edit / replace freely.
37
+
38
+ ### Changed
39
+ - **Gem partials now degrade gracefully when host User model lacks optional methods.** `_notifications_bell.html.erb` short-circuits via `current_user.respond_to?(:notifications)` — hosts that don't want Noticed integration can opt out by just leaving the association off. `_header.html.erb` falls back to `email_address.split("@").first` when `current_user.display_name` is undefined, and to `email_address` when `permissions_summary` is undefined.
40
+ - **Install generator's "Wire up your User model" instructions now show the optional `display_name`, `permissions_summary`, and `has_many :notifications` snippets** alongside the required permission predicates, with a comment noting they're optional but the admin uses them nicely when present.
41
+
42
+ ## [0.2.9] — 2026-05-14
43
+
44
+ Extracts the body of `lean_cms:load_structure` out of the Rake task and into a callable service so background jobs and scripts can reseed content without `require 'rake'` / `Rails.application.load_tasks` / `task.reenable; task.invoke` gymnastics.
45
+
46
+ ### Added
47
+ - **`LeanCms::Loader` service class** at `lib/lean_cms/loader.rb`. Same behavior as `lean_cms:load_structure` but callable from anywhere:
48
+ ```ruby
49
+ result = LeanCms::Loader.new.load!
50
+ result.created # => 28
51
+ result.updated # => 0
52
+ ```
53
+ Optional kwargs: `yaml_path:` (default `Rails.root.join("config/lean_cms_structure.yml")`), `system_user:` (default: first super-admin or first user), `logger:` (default: silent — pass `Rails.logger` from a job or `Logger.new($stdout)` from a script). Raises `LeanCms::Loader::StructureFileMissing` or `LeanCms::Loader::NoUsersFound` instead of `exit 1` so callers can rescue meaningfully.
54
+
55
+ ### Changed
56
+ - **`lean_cms:load_structure` Rake task is now a thin wrapper** that delegates to `LeanCms::Loader`. Same stdout output, same `exit 1` on the two error cases, so existing CI / deploy scripts that shell out to `rake` are unaffected. ~230 lines down to ~20.
57
+
58
+ ## [0.2.8] — 2026-05-14
59
+
60
+ Small ergonomic follow-up to v0.2.7.
61
+
62
+ ### Added
63
+ - **`cms_admin_bar` helper.** Wrapper around `render "lean_cms/shared/admin_bar"` so hosts don't have to remember the partial path. Available everywhere `LeanCms::PageContentHelper` is included.
64
+ - **Install generator output now documents the admin bar wiring.** New step 5 in the post-install instructions shows the exact `<body>` snippet hosts should paste into their public layout to surface the bar.
65
+
66
+ ## [0.2.7] — 2026-05-14
67
+
68
+ Polish pass after putting the demo live at `demo.leancms.dev`. One bug fix, two ergonomic additions hosts can lean on instead of rebuilding themselves.
69
+
70
+ ### Fixed
71
+ - **`LeanCms::PageContent.find_or_initialize_content` actually finds existing records now.** The lookup used `where(page: page, section: section, key: key).first`, but `where(page: ...)` resolves to the `belongs_to :page` association — emitting `WHERE page_id = NULL` and missing every existing record. Re-running `lean_cms:load_structure` (e.g. to pick up new YAML fields) was crashing on the SQLite unique index for every row. Now uses an explicit string-column comparison via `where("page = ? AND section = ? AND key = ?", …)`. Same shape of fix as the validation patches in v0.2.5.
72
+
73
+ ### Added
74
+ - **`lean_cms/shared/_admin_bar` partial.** The fixed-top admin strip with the Inline Editing toggle, Help, Admin Dashboard, and Sign Out — previously every Lean CMS host had to copy/paste ~40 lines of ERB into their own public layout. Now hosts just `<%= render "lean_cms/shared/admin_bar" %>` from their layout and get the whole widget. The Admin Dashboard button reads its color from `LeanCms.primary_color`. Push the body's top padding down ~40px (e.g. Tailwind `pt-10`) when `current_user&.has_any_cms_permission?` is true so it doesn't overlap your header.
75
+ - **`LeanCms.docs_url` configuration option** (default `https://leancms.dev/docs/`). Drives the new Help icon in the admin bar and the admin-side `_header.html.erb`. Override in `config/initializers/lean_cms.rb` to point at internal docs instead.
76
+
77
+ ## [0.2.6] — 2026-05-14
78
+
79
+ Two more demo-bootstrap fixes on top of v0.2.5 — `cards_section` was crashing with a `String#updated_at` error, and `bullets_section` was rendering silently empty even when bullet data existed.
80
+
81
+ ### Fixed
82
+ - **`LeanCms::BaseComponent#cache_key` handles the String-slug `page` form.** The cache key called `page&.updated_at&.to_i` assuming `page` was always a `LeanCms::Page` record. In the standard helper-driven usage `cards_section("offerings")`, `page` is the slug String (`"trips"`) and `String#updated_at` blew up. Now branches: uses `page.updated_at` when it's a Page record, falls back to `LeanCms::PageContent.where(page: slug).maximum(:updated_at)` when it's a slug — preserves `touch: true` invalidation through the legacy path.
83
+ - **`LeanCms::BulletsSectionComponent` template rewritten.** The previous template split a `content_tag :div do %>` block across two `if can_edit_cms?` guards (open in one, close in another) plus an `<% return %>` inside the cache block. The result rendered as completely empty markup whenever bullets data was present — every bullets section on the public site showed only its heading with a large blank gap below. New template captures the `<ul>` once and wraps it in the edit-controls div only when an editor's signed in. Same behavior for editors, fixes the blank-render bug for public visitors.
84
+
85
+ ## [0.2.5] — 2026-05-14
86
+
87
+ Two more bugs surfaced bootstrapping the demo site on top of v0.2.4. Both are pre-existing, both block a fresh install from completing `lean_cms:load_structure`.
88
+
89
+ ### Fixed
90
+ - **`lean_cms:load_structure` no longer fails with "Page can't be blank" after the v0.2.4 fix.** With the slug now correctly written to the string column via `record[:page] = page`, validation still failed because `validates :page, presence: true` was checking the `belongs_to :page` association (which is `optional: true` and has a nil `page_id` on fresh installs). Replaced with `validate :page_slug_present` reading `read_attribute(:page)` so presence is enforced on the slug column. Added a `page_slug` reader as the public accessor for the slug, alongside the association.
91
+ - **Gem now ships the PaperTrail `versions` table migration.** Four gem models (`PageContent`, `Setting`, `Post`, `FormSubmission`) call `has_paper_trail`, but the install never created the underlying table. The first write on a fresh install crashed with `Could not find table 'versions'`. Added `db/migrate/20260514000001_create_paper_trail_versions.rb`; uses `create_table :versions, if_not_exists: true` plus a guarded `add_index`, so existing installs that ran `paper_trail:install` separately are unaffected.
92
+ - **Gem now ships the Action Text + Active Storage migrations.** `LeanCms::PageContent` calls `has_rich_text :rich_content`, `has_one_attached :image_file`, and `has_many_attached :card_images`; `LeanCms::Setting` calls `has_one_attached :file`. Without these tables, `lean_cms:load_structure` fails the moment it hits the first `rich_text` field. Added `db/migrate/20260514000002_create_action_text_tables.rb` and `db/migrate/20260514000003_create_active_storage_tables.rb`, both fully idempotent — hosts that ran `action_text:install` / `active_storage:install` separately get no-ops.
93
+ - **`(page, section, key)` uniqueness now scopes on the slug column, not the association FK.** The built-in `validates :key, uniqueness: { scope: [:page, :section] }` resolved `:page` to `page_id` (always NULL on fresh installs until the slug → `LeanCms::Page` normalization completes), so the scope collapsed to just `:section`. Records with the same section + key on different pages (e.g. `home/hero/heading` and `trips/hero/heading`) all blocked each other with "Key has already been taken". Replaced with a custom `validate :key_unique_within_page_section` that scopes on the slug via `read_attribute(:page)`.
94
+
95
+ ## [0.2.4] — 2026-05-14
96
+
97
+ Surfaced while bootstrapping a fresh demo site from `lean_cms_structure.yml`.
98
+
99
+ ### Fixed
100
+ - **`lean_cms:load_structure` no longer crashes with `ActiveRecord::AssociationTypeMismatch: LeanCms::Page expected, got "home"`.** `LeanCms::PageContent` has both a string column `page` (the slug) and a `belongs_to :page, class_name: 'LeanCms::Page'` association (FK on `page_id`). The association shadows the column for mass assignment, so `find_or_initialize_by(page: page_key, …)` was trying to coerce the slug string into a `LeanCms::Page` instance. Introduced `LeanCms::PageContent.find_or_initialize_content(page:, section:, key:)` which bypasses the association setter and writes the slug directly to the string column; the three call sites in the rake task (regular fields, cards, bullets) now route through it. Existing installs continue to work — the helper is purely additive.
101
+
102
+ ## [0.2.3] — 2026-05-14
103
+
104
+ Doc-accuracy pass surfaced a couple of small gaps in the gem itself; rolled into this release.
105
+
106
+ ### Added
107
+ - **`lean_cms:export_structure` rake task.** Dumps current `LeanCms::PageContent` records back into a YAML file shaped like `lean_cms_structure.yml`, with each field's current value emitted as its `default`. Useful for bootstrapping a second environment from a live database, or for documenting an existing install. Writes to `config/lean_cms_structure_export.yml` by default; override with `OUTPUT=path/to/file.yml`. Image attachments are not included — re-attach via the CMS UI or by copying ActiveStorage blobs.
108
+ - **Install template now generates `posts_per_page` and `portfolio_enabled` config lines.** Both options have existed in `LeanCms::Configuration` since v0.1.0 but were missing from the generated initializer, so most installs didn't know they could be tuned.
109
+
110
+ ## [0.2.2] — 2026-05-13
111
+
112
+ The v0.2.1 Tailwind fix was incomplete — it stopped the RoutingError but didn't actually plug the gem's CSS into Tailwind's compile pipeline, so utilities for gem views weren't being emitted. This release adopts `tailwindcss-rails`' native engine support instead.
113
+
114
+ ### Fixed
115
+ - **Engine name aligned with our Tailwind directory.** Set `engine_name "lean_cms"` explicitly. Rails' default derivation for `LeanCms::Engine` was `lean_cms_engine`, so `Tailwindcss::Engines.bundle` (the built-in `tailwindcss-rails` engine walker) was looking for `app/assets/tailwind/lean_cms_engine/engine.css` — never matched our `app/assets/tailwind/lean_cms/engine.css`. Now `tailwindcss-rails` finds and bundles us natively. Safe to change because we don't use `isolate_namespace` (no route helper prefixing).
116
+ - **Removed our `lean_cms.tailwind_css` initializer entirely.** It was duplicating `Tailwindcss::Engines.bundle` (which `tailwindcss-rails 4.x` runs as a prereq of every `tailwindcss:build` and `tailwindcss:watch`) — except buggily. The v0.2.1 attempt to fix it by inlining engine.css contents broke the relative `@source` paths. Letting tailwindcss-rails do its own thing is correct.
117
+
118
+ ### Added
119
+ - **Install generator now wires up Tailwind.** New `wire_tailwind` step appends `@import "../builds/tailwind/lean_cms.css";` to the host's `app/assets/tailwind/application.css` so Tailwind actually scans the gem's views during compile. If no Tailwind input file is found, prints the line the user needs to paste themselves.
120
+
121
+ ## [0.2.1] — 2026-05-13
122
+
123
+ Polish pass on the install + demo flow surfaced by an end-to-end test drive of v0.2.0 on a fresh Rails 8 app. No public API changes.
124
+
125
+ ### Fixed
126
+ - **Install generator runs cleanly on fresh apps.** `CreateLeanCmsTables` migration crashed with `Unknown key: :foreign_key` because `t.integer :page_id, foreign_key: { to_table: ... }` is invalid syntax (only `t.references` accepts `foreign_key:`). Removed; the FK was already added at the bottom of the migration via `add_foreign_key`.
127
+ - **Install generator `destination_root.join` crash.** `destination_root` returns a String, not a Pathname. Switched to `File.join`.
128
+ - **Migrations are now portable across host user-table names.** Removed inline `foreign_key: { to_table: :users }` from every `t.references` pointing at the host user table (`lean_cms_posts.author`, `lean_cms_posts.last_edited_by`, `lean_cms_page_contents.last_edited_by`, `lean_cms_sessions.user`, `lean_cms_magic_links.user`). SQLite revalidates *all* FKs in a schema when ALTER triggers a table rebuild — without the host's users table (or with a non-`users` name like Devise's `admins`), this blew up `db:migrate` partway through. Models keep `belongs_to ..., class_name: "User"` for app-level integrity.
129
+ - **Lean CMS no longer requires User-side `has_many` associations.** Was conflicting with Rails 8's built-in `bin/rails generate authentication` which already adds `has_many :sessions`. Gem now uses `LeanCms::Session.create!(user: ...)` / `LeanCms::Session.where(user:).destroy_all` / `LeanCms::MagicLink.where(user:).for_purpose(...)` directly. Host User stays clean.
130
+ - **`lib/lean_cms.rb` now requires its runtime deps eagerly** — `paper_trail`, `view_component`, `kaminari`, `pundit`, `noticed`, `image_processing`, `meta-tags`, `rack-attack`. Hosts that `bundle add lean_cms` were hitting `NameError: uninitialized constant Pundit` when following install instructions, because Bundler doesn't auto-require transitive gemspec deps.
131
+ - **`lean_cms:load_structure` rake task.** Replaced dead `User.cms_admin.first` (pre-0.2 enum scope) with `LeanCms.user_class.constantize.where(is_super_admin: true).first || .first`.
132
+ - **Demo generator crashes.** `directory "views/shared"` referenced a non-existent template directory. Demo's `PagesController` template inherited `LeanCms::Authentication`'s default-protect and locked out public visitors; added `allow_unauthenticated_access`. Demo contact view used non-existent `submit_contact_path` helper; switched to `contact_path`.
133
+ - **Tailwind engine.css 404 on fresh installs.** The `lean_cms.tailwind_css` initializer was writing `@import "/Users/.../lean-cms/app/assets/tailwind/lean_cms/engine.css"` into the host's `app/assets/builds/tailwind/lean_cms.css`. That absolute filesystem path is fine for the Tailwind CLI at compile-time but leaks to the browser as a real HTTP request → Rails `RoutingError`. Initializer now inlines the engine's CSS contents instead of `@import`-ing them, and is guarded with `defined?(Tailwindcss::Engine)` so non-Tailwind hosts don't get the file at all.
134
+ - **Engine initializers no longer scribble into themselves when host == engine.** The Tailwind and edit-controls CSS initializers now `next if app.root == root` (mirroring the migrations initializer), preventing artifacts during in-repo test runs from polluting the gem's working tree.
135
+
136
+ ### Added
137
+ - **Minitest test suite** under `test/` with a self-contained dummy Rails 8 app (`test/dummy/`) and in-memory SQLite. Runs via `bundle exec rake test`; CI now invokes it on every push and PR. Initial coverage: engine boot + table names + configuration, `LeanCms::Setting` get/set/JSON/lock helpers, `LeanCms::MagicLink` invitation / password-reset / expiration / invalidation.
138
+ - **`check_user_model` pre-flight in install generator.** Aborts with a clear message if the host's user table doesn't exist or is missing `email_address` / `password_digest` columns. Points consumers at `bin/rails generate authentication` (Rails 8 built-in), Devise, or `rails generate model`. Re-runnable.
139
+ - **`--user=ClassName` flag on the install generator** for hosts whose auth model isn't `User` (e.g. Devise's `Admin`). Drives both the pre-flight check (looks for the `admins` table) and the value written to `LeanCms.user_class` in the generated initializer.
140
+ - **`add_missing_user_columns` step in the install generator.** Diffs the host's user table against the nine Lean CMS-required columns (`name`, `active`, `must_change_password`, `last_login_at`, plus the five permission flags) and generates `db/migrate/add_lean_cms_columns_to_<table>.rb` with only the missing columns. The migration runs as part of the same install — clean install ends with a fully provisioned user table, zero schema hand-editing.
141
+ - **Rails 8 auth conflict detection.** If the host's `ApplicationController` includes `Authentication` (the concern Rails 8's `bin/rails generate authentication` ships), the install generator prints a prominent warning explaining the collision (both concerns define `before_action :require_authentication`; last-included wins; without cleanup `/lean-cms` redirects to `/session/new`). Lists the exact line to remove, files to delete, and routes to drop.
142
+ - **Self-contained auth-page styling.** Login, password reset, password setup pages now ship complete inline CSS keyed off `LeanCms.primary_color` / `secondary_color`. No Tailwind required for the auth flow — drops cleanly into any Rails 8 app with zero CSS framework setup. (Admin / post-login layout is still Tailwind-dependent — separate decision.)
143
+
144
+ ### Changed
145
+ - **Pundit is now a gem-internal concern.** `LeanCms::ApplicationController` includes `Pundit::Authorization` itself. Hosts only need `include Pundit::Authorization` in their own AC if they want `authorize` / `policy_scope` available in their own non-CMS controllers — no longer a Lean CMS install step.
146
+ - Install generator's "Wire up your User model" instructions are shorter (no `has_many :sessions` line) and gain a note explaining that the gem uses `LeanCms::Session` directly to stay compatible with Rails 8 auth.
147
+
148
+ ## [0.2.0] — 2026-05-13
149
+
150
+ ### Added
151
+ - **Sloth mascot assets** under `app/assets/images/lean_cms/` — favicons (16/32/64), full logo, and 404/500 error illustrations. The gem's admin layouts (`lean_cms/application` and `lean_cms/auth`) now wire in the sloth favicon by default.
152
+ - `LeanCms::Setting.site_favicon_url` — returns the ActiveStorage URL of an uploaded favicon override, or `nil` if none is set. Host apps use this with a fallback to the gem's sloth PNGs for the public site's `<link rel="icon">`.
153
+ - `LeanCms::Setting.update_site_favicon!(io)` and `remove_site_favicon!` — programmatic helpers for the favicon attachment.
154
+ - Favicon upload UI in the Settings page (`/lean-cms/settings`) — admins can upload a PNG/ICO/SVG to override the default sloth on the public site, or remove it to fall back to the default.
155
+ - `has_one_attached :file` on `LeanCms::Setting` — supports per-setting file attachments (currently used only for `site_favicon`).
156
+
157
+ ### Fixed
158
+ - `LeanCms::Setting.set` now references `LeanCms::Current.user` (was top-level `Current.user`, which would raise `NameError` after the auth-into-gem migration if any setting was saved from a controller).
159
+
160
+ ### Added (continued)
161
+ - **Authentication owned by the gem.** Login, password reset, and magic-link password setup now live under `/lean-cms/login`, `/lean-cms/reset-password`, and `/lean-cms/setup-password/:token`. New gem-owned pieces:
162
+ - `LeanCms::Authentication` controller concern — host's `ApplicationController` includes this to expose `current_user`, `authenticated?`, `start_new_session_for`, and `terminate_session`.
163
+ - `LeanCms::Current` (replaces host `Current`) — `ActiveSupport::CurrentAttributes` with `session` and delegated `user`.
164
+ - `LeanCms::Session` (table `lean_cms_sessions`) and `LeanCms::MagicLink` (table `lean_cms_magic_links`).
165
+ - `LeanCms::SessionsController`, `PasswordsController`, `PasswordSetupController` with branded views rendered via the new `lean_cms/auth` layout.
166
+ - `LeanCms::PasswordsMailer` (`reset`) and `LeanCms::UsersMailer` (`invitation`, `reactivation`, `admin_triggered_password_reset`).
167
+ - Migration `CreateLeanCmsAuthTables` — idempotent (skips if tables already exist).
168
+ - `LeanCms.mailer_from` config — `From:` address for gem-sent emails (default: `noreply@example.com`).
169
+ - `lean_cms:optimize_images` rake task — reads originals from `app/assets/images/source/` and emits resized WebP + same-format fallback variants (default widths: 640/1280/1920) into `app/assets/images/`. Overridable via `WIDTHS`, `WEBP_QUALITY`, `JPEG_QUALITY` env vars. Idempotent (skips outputs newer than source). Powered by libvips via `image_processing`.
170
+ - `lean_cms_picture_tag(name, alt:, widths:, format:, sizes:, **img_options)` helper — renders a `<picture>` with a WebP `<source>` and a JPG/PNG fallback `<img>`, both with proper `srcset` for the configured widths. Defaults to lazy loading and async decoding. Use for static layout images optimized via `lean_cms:optimize_images`.
171
+
172
+ ### Changed
173
+ - **BREAKING:** `LeanCms::Authorization` references `LeanCms::Current.user` (was `Current.user`). Hosts that previously relied on a top-level `Current` constant must either include the gem's auth (and remove their own `Current`) or alias `Current = LeanCms::Current`.
174
+ - **BREAKING:** auth URL helpers are namespaced: `lean_cms_new_session_path` (was `new_session_path`), `lean_cms_new_password_path`, `lean_cms_password_setup_path`, etc.
175
+
176
+ ### Host migration notes
177
+ Hosts moving from in-app auth to gem auth should:
178
+ 1. `bundle update lean_cms`, then `bin/rails db:migrate` to create `lean_cms_sessions` and `lean_cms_magic_links`.
179
+ 2. Add `include LeanCms::Authentication` to `ApplicationController` (replacing any local `Authentication` concern).
180
+ 3. Update User: `has_many :sessions, class_name: "LeanCms::Session", dependent: :destroy` and the same for `:magic_links`.
181
+ 4. Delete local `SessionsController`, `PasswordsController`, `PasswordSetupController`, `Session`, `Current`, `MagicLink`, `PasswordsMailer`, `UsersMailer`, related views, and remove their routes.
182
+ 5. Update any references to the unprefixed URL helpers to the `lean_cms_*` namespaced versions.
183
+ 6. Optionally drop the old `sessions` and `magic_links` tables in a follow-up migration.
184
+ - `app/assets/tailwind/lean_cms/engine.css` — hooks into `tailwindcss-rails` v4 engine support so the gem's views and Stimulus controllers are scanned when compiling Tailwind CSS in the host app, fixing missing utility classes in production
185
+ - Content Sync card in Settings page showing live lock status with lock/unlock buttons and reason field
186
+ - Lock status banner in the CMS admin layout — displayed prominently across all pages when content is locked, with an inline Unlock button
187
+
188
+ ### Fixed
189
+ - Register `app/javascript` with Propshaft's asset load path so Stimulus controllers are served correctly in production
190
+ - Content lock enforcement now also covers `update_field` and `undo_field` inline editing actions (previously only `update` was blocked)
191
+
192
+ ## [0.1.0] - 2026-02-21
193
+
194
+ ### Added
195
+ - Initial gem extraction from the CAS application
196
+ - Rails Engine with `isolate_namespace LeanCms`
197
+ - **9 content types**: `text`, `rich_text`, `image`, `boolean`, `url`, `color`, `dropdown`, `cards`, `bullets`
198
+ - In-context editing with hover-activated section overlays (`cms_editable_section`)
199
+ - Inline field editing with undo (`editable_content`)
200
+ - `LeanCms::CardsSectionComponent` — renders card grids with drag-to-reorder editor
201
+ - `LeanCms::BulletsSectionComponent` — renders bullet lists with inline editor
202
+ - `LeanCms::EditableContentComponent` — wraps fields with inline edit controls
203
+ - `LeanCms::SectionComponent` — section wrapper with caching and edit overlay
204
+ - Helper API: `page_content`, `page_content_html`, `page_content_image_url`, `page_content?`, `page_section`, `page_structure`, `page_cards`, `page_bullets`
205
+ - `cms_editable_section` and `cms_settings_section` helpers
206
+ - `cards_section` and `bullets_section` component helpers
207
+ - CMS admin: dashboard, page contents editor, posts (blog + portfolio), settings, users, form submissions, notifications, activity log
208
+ - `LeanCms::Setting` — key-value store with caching; convenience methods for site phone, email, address, business hours
209
+ - Content locking for safe database sync workflow
210
+ - `lean_cms:sync` rake task suite — pull, push, stage, start, finish, lock, unlock
211
+ - `rails generate lean_cms:install_kamal_hooks` — installs pre/post deploy hooks
212
+ - Stimulus controllers: `cms-sticky-overlay`, `inline-edit`, `inline-edit-toggle`, `cards-editor`, `settings-inline-edit-sync`, `settings-override`
213
+ - `cms_edit_controls.css` — styles for in-context editing overlays
214
+ - Pundit policies for `PageContent`, `Post`, `Setting`
215
+ - Role-based authorization via `LeanCms::Authorization` concern
216
+ - PaperTrail integration on `PageContent`, `Post`, `Setting`
217
+ - `lean_cms:load_structure` rake task — seeds content from `config/lean_cms_structure.yml`
218
+ - `lean_cms:stats` rake task — prints content field counts by page
219
+ - `LeanCms::SyncHelper` — SQLite database sync between local and production
220
+
221
+ [Unreleased]: https://github.com/Ovrland-Services/lean-cms/compare/v0.2.12...HEAD
222
+ [0.2.12]: https://github.com/Ovrland-Services/lean-cms/compare/v0.2.11...v0.2.12
223
+ [0.2.11]: https://github.com/Ovrland-Services/lean-cms/compare/v0.2.10...v0.2.11
224
+ [0.2.10]: https://github.com/Ovrland-Services/lean-cms/compare/v0.2.9...v0.2.10
225
+ [0.2.9]: https://github.com/Ovrland-Services/lean-cms/compare/v0.2.8...v0.2.9
226
+ [0.2.8]: https://github.com/Ovrland-Services/lean-cms/compare/v0.2.7...v0.2.8
227
+ [0.2.7]: https://github.com/Ovrland-Services/lean-cms/compare/v0.2.6...v0.2.7
228
+ [0.2.6]: https://github.com/Ovrland-Services/lean-cms/compare/v0.2.5...v0.2.6
229
+ [0.2.5]: https://github.com/Ovrland-Services/lean-cms/compare/v0.2.4...v0.2.5
230
+ [0.2.4]: https://github.com/Ovrland-Services/lean-cms/compare/v0.2.3...v0.2.4
231
+ [0.2.3]: https://github.com/Ovrland-Services/lean-cms/compare/v0.2.2...v0.2.3
232
+ [0.2.2]: https://github.com/Ovrland-Services/lean-cms/compare/v0.2.1...v0.2.2
233
+ [0.2.1]: https://github.com/Ovrland-Services/lean-cms/compare/v0.2.0...v0.2.1
234
+ [0.2.0]: https://github.com/Ovrland-Services/lean-cms/compare/v0.1.0...v0.2.0
235
+ [0.1.0]: https://github.com/Ovrland-Services/lean-cms/releases/tag/v0.1.0
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ovrland Services LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,107 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/Ovrland-Services/lean-cms/main/app/assets/images/lean_cms/sloth-logo.png" alt="Lean CMS sloth mascot — a sloth reclining on a beanbag chair with sunglasses and a Ruby-stickered laptop" width="240">
3
+ </p>
4
+
5
+ <h1 align="center">Lean CMS</h1>
6
+
7
+ <p align="center">
8
+ A lightweight Rails CMS with in-context editing for marketing sites.
9
+ <br>
10
+ <a href="https://leancms.dev"><strong>leancms.dev</strong></a>
11
+ </p>
12
+
13
+ ---
14
+
15
+ Lean CMS is a Rails Engine that adds in-context content editing, page content
16
+ management, blog & portfolio, settings, role-based authentication, and
17
+ notifications to any Rails 8 application. It's designed for marketing sites:
18
+ server-rendered, no separate CMS service to host, and one rake task to
19
+ seed your whole site structure from YAML.
20
+
21
+ ## Why Lean CMS
22
+
23
+ - **In-context editing** — content editors hover over any page section to
24
+ reveal an Edit overlay; no separate "admin" interface for day-to-day edits.
25
+ - **You own your design** — the gem never touches your layouts or styles;
26
+ helpers pull content into your own ERB templates.
27
+ - **YAML-defined structure** — every page, section, and field is declared in
28
+ `config/lean_cms_structure.yml` and seeded with one rake task.
29
+ - **Built-in auth** — login, password reset, and magic-link invitations all
30
+ shipped by the gem and namespaced under `/lean-cms`.
31
+ - **9 content types** — `text`, `rich_text`, `image`, `boolean`, `url`,
32
+ `color`, `dropdown`, `cards`, `bullets`.
33
+ - **Version history** — every content edit tracked via PaperTrail; one-click
34
+ undo.
35
+
36
+ ## Installation
37
+
38
+ ```ruby
39
+ # Gemfile
40
+ gem "lean_cms"
41
+ ```
42
+
43
+ ```bash
44
+ bundle install
45
+ rails generate lean_cms:install
46
+ rails db:migrate
47
+ ```
48
+
49
+ Then in `app/controllers/application_controller.rb`:
50
+
51
+ ```ruby
52
+ include LeanCms::Authentication
53
+ ```
54
+
55
+ And in `app/helpers/application_helper.rb`:
56
+
57
+ ```ruby
58
+ include LeanCms::PageContentHelper
59
+ ```
60
+
61
+ See the [Getting Started guide](https://leancms.dev/docs/getting-started/installation/) for the full setup including User-model requirements and YAML structure.
62
+
63
+ > **Heads up — authentication.** Lean CMS ships its own auth (login at `/lean-cms/login`, sessions, magic-link invites). It coexists cleanly with Rails 8's built-in `bin/rails generate authentication` (the install generator detects + warns). **If you're already using Devise or another auth gem**, the gem currently still adds a second login screen — first-class host-auth integration is the v0.3 milestone. See [issue tracker](https://github.com/Ovrland-Services/lean-cms/issues) for status.
64
+
65
+ ## A taste of the helper API
66
+
67
+ ```erb
68
+ <%# A whole section, hover-editable for admins %>
69
+ <%= cms_editable_section(page: "home", section: "hero", display_title: "Hero") do %>
70
+ <section class="hero">
71
+ <h1><%= editable_content("hero", "heading") %></h1>
72
+ <p><%= editable_content("hero", "subheading") %></p>
73
+
74
+ <% bg = page_content_image_url(@page, "hero", "background") %>
75
+ <% if bg %><%= image_tag bg, class: "absolute inset-0" %><% end %>
76
+ </section>
77
+ <% end %>
78
+
79
+ <%# Responsive optimized <picture> for static layout images %>
80
+ <%= lean_cms_picture_tag "wire-panel",
81
+ alt: "Wiring",
82
+ widths: [640, 1280],
83
+ sizes: "(min-width: 768px) 448px, 100vw",
84
+ class: "rounded-2xl shadow-lg" %>
85
+ ```
86
+
87
+ ## Requirements
88
+
89
+ - Ruby ≥ 3.2
90
+ - Rails ≥ 8.0
91
+ - SQLite, PostgreSQL, or MySQL (SQLite is the happy path; see [Database support](https://leancms.dev/docs/deployment/database-support/) for the compatibility matrix)
92
+
93
+ ## Roadmap
94
+
95
+ Lean CMS is `0.2.x` — pre-1.0, API may shift between minors. Public roadmap:
96
+
97
+ - **v0.2.x** _(current)_: feature-complete CMS surface, in-context editing, hourly-resettable demo. Ships its own auth.
98
+ - **v0.3**: host-auth adapter pattern — first-class integration with Devise / Rodauth / custom-auth hosts. Sibling [`lean-cms-devise-example`](https://github.com/Ovrland-Services/lean-cms) repo as the proving ground + public reference.
99
+ - **v0.4** _(idea stage)_: AI-powered `lean_cms-scraper` companion gem — `bin/rails generate lean_cms:scrape URL=...` writes a starter `lean_cms_structure.yml` from an existing live website.
100
+
101
+ ## Demo
102
+
103
+ Live at [demo.leancms.dev](https://demo.leancms.dev) — sign in with `demo@leancms.dev` / `demo123` and edit anything. Content resets every hour at `:00 UTC`. Source: [`Ovrland-Services/lean-cms-demo`](https://github.com/Ovrland-Services/lean-cms-demo).
104
+
105
+ ## License
106
+
107
+ MIT. See [LICENSE](LICENSE).