trek 2.2.0 → 2.3.1

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 (126) hide show
  1. checksums.yaml +4 -4
  2. data/.slim-lint.yml +0 -3
  3. data/AGENTS.md +9 -2
  4. data/CHANGELOG.md +73 -0
  5. data/Gemfile +4 -0
  6. data/Gemfile.lock +23 -3
  7. data/Makefile +11 -9
  8. data/README.md +1 -1
  9. data/app/assets/icons/trek/chevron-down.svg +1 -0
  10. data/app/assets/icons/trek/chevron-up.svg +1 -0
  11. data/app/assets/icons/trek/external.svg +1 -0
  12. data/app/assets/icons/trek/theme-auto.svg +1 -0
  13. data/app/assets/icons/trek/theme-dark.svg +1 -0
  14. data/app/assets/icons/trek/theme-light.svg +1 -0
  15. data/app/assets/stylesheets/trek/base.css +13 -0
  16. data/app/components/trek/button_component/button_component.css +9 -0
  17. data/app/components/trek/button_component.rb +13 -1
  18. data/app/components/trek/dashboard/tile_component/tile_component.html.slim +1 -1
  19. data/app/components/trek/dashboard/tile_component.rb +14 -0
  20. data/app/components/trek/dialog_component/dialog_component.html.slim +6 -2
  21. data/app/components/trek/dialog_component/dialog_component.js +62 -2
  22. data/app/components/trek/dropdown_component/dropdown_component.css +69 -0
  23. data/app/components/trek/dropdown_component/dropdown_component.html.slim +31 -0
  24. data/app/components/trek/dropdown_component/dropdown_component.js +114 -0
  25. data/app/components/trek/dropdown_component.rb +34 -0
  26. data/app/components/trek/form/group_component/group_component.css +34 -0
  27. data/app/components/trek/header_component/header_component.css +9 -4
  28. data/app/components/trek/header_component/header_component.html.slim +17 -10
  29. data/app/components/trek/header_component/header_component.js +29 -5
  30. data/app/components/trek/header_component.rb +24 -3
  31. data/app/components/trek/menu_component/menu_component.css +6 -0
  32. data/app/components/trek/menu_component.rb +29 -4
  33. data/app/components/trek/toaster/toast_component.rb +1 -1
  34. data/app/controllers/concerns/trek/authorization.rb +12 -0
  35. data/app/controllers/concerns/trek/filters.rb +18 -1
  36. data/app/helpers/trek/page_path_helper.rb +4 -3
  37. data/app/helpers/trek/theme_helper.rb +16 -1
  38. data/app/models/concerns/trek/phonable.rb +15 -3
  39. data/app/models/concerns/trek/sectionable.rb +5 -0
  40. data/app/models/concerns/trek/users/validations.rb +15 -6
  41. data/app/uploaders/audio_uploader.rb +11 -0
  42. data/app/uploaders/record_image_uploader.rb +3 -0
  43. data/config/locales/trek.en.yml +3 -2
  44. data/config/locales/trek.fr.yml +1 -0
  45. data/docs/.vitepress/config.mjs +20 -1
  46. data/docs/.vitepress/theme/style.css +36 -8
  47. data/docs/changelog.md +9 -0
  48. data/docs/customize.md +18 -2
  49. data/docs/deploy.md +7 -2
  50. data/docs/getting-started.md +1 -1
  51. data/docs/index.md +8 -8
  52. data/docs/public/apple-touch-icon.png +0 -0
  53. data/docs/public/favicon.ico +0 -0
  54. data/docs/public/favicon.png +0 -0
  55. data/docs/public/trek-mark.webp +0 -0
  56. data/docs/reference/components.md +15 -0
  57. data/docs/reference/concerns/phonable.md +27 -1
  58. data/docs/reference/controllers.md +8 -3
  59. data/docs/reference/environment-variables.md +2 -0
  60. data/docs/reference/forms.md +2 -0
  61. data/docs/reference/generators/install.md +4 -1
  62. data/docs/reference/icons.md +1 -1
  63. data/docs/reference/models/tag.md +1 -1
  64. data/docs/reference/models/user.md +6 -4
  65. data/docs/reference/uploaders.md +31 -6
  66. data/docs/what-is-trek.md +1 -1
  67. data/lib/generators/trek/install/admin/routes_generator.rb +7 -1
  68. data/lib/generators/trek/install/appsignal_generator.rb +102 -0
  69. data/lib/generators/trek/install/model_specs_generator.rb +64 -0
  70. data/lib/generators/trek/install/postmark_generator.rb +3 -0
  71. data/lib/generators/trek/install/ruby_generator.rb +1 -1
  72. data/lib/generators/trek/install/scalingo_generator.rb +1 -1
  73. data/lib/generators/trek/install/uploaders_generator.rb +2 -0
  74. data/lib/generators/trek/install_generator.rb +5 -0
  75. data/lib/generators/trek/taxonomies_generator.rb +7 -0
  76. data/lib/generators/trek/templates/.gitlab-ci.yml +1 -1
  77. data/lib/generators/trek/templates/config/appsignal.rb.tt +25 -0
  78. data/lib/generators/trek/templates/controllers/admin/uploads_controller.rb +4 -0
  79. data/lib/generators/trek/templates/controllers/admin/user_password_resets_controller.rb +7 -0
  80. data/lib/generators/trek/templates/controllers/admin/users_controller.rb +4 -1
  81. data/lib/generators/trek/templates/controllers/concerns/appsignal_context.rb +37 -0
  82. data/lib/generators/trek/templates/db/seeds/setup/fragments.rb +3 -0
  83. data/lib/generators/trek/templates/db/seeds/setup/users.rb +6 -0
  84. data/lib/generators/trek/templates/db/seeds/test/cases/page_tree.rb +5 -0
  85. data/lib/generators/trek/templates/initializers/appsignal_context.rb +5 -0
  86. data/lib/generators/trek/templates/locales/scaffold/model.en.yml.tt +1 -1
  87. data/lib/generators/trek/templates/migrations/fragments.rb.erb +1 -1
  88. data/lib/generators/trek/templates/migrations/menu_nodes.rb.erb +0 -1
  89. data/lib/generators/trek/templates/migrations/tag_categories.rb.erb +1 -1
  90. data/lib/generators/trek/templates/migrations/taggings.rb.erb +1 -1
  91. data/lib/generators/trek/templates/migrations/users.rb.erb +2 -1
  92. data/lib/generators/trek/templates/models/tag.rb +6 -1
  93. data/lib/generators/trek/templates/policies/admin/tag_policy.rb +1 -1
  94. data/lib/generators/trek/templates/policies/external_link_policy.rb +1 -1
  95. data/lib/generators/trek/templates/spec/fixtures/files/image.png +0 -0
  96. data/lib/generators/trek/templates/spec/fixtures/files/sound.mp3 +0 -0
  97. data/lib/generators/trek/templates/spec/models/page_spec.rb +24 -0
  98. data/lib/generators/trek/templates/spec/models/user_spec.rb +109 -0
  99. data/lib/generators/trek/templates/spec/policies/admin/external_link_policy_spec.rb +17 -0
  100. data/lib/generators/trek/templates/spec/policies/admin/fragment_policy_spec.rb +33 -0
  101. data/lib/generators/trek/templates/spec/policies/admin/page_policy_spec.rb +39 -0
  102. data/lib/generators/trek/templates/spec/policies/admin/preview_policy_spec.rb +9 -0
  103. data/lib/generators/trek/templates/spec/policies/admin/tag_category_policy_spec.rb +31 -0
  104. data/lib/generators/trek/templates/spec/policies/admin/tag_policy_spec.rb +31 -0
  105. data/lib/generators/trek/templates/spec/policies/admin/user_policy_spec.rb +58 -0
  106. data/lib/generators/trek/templates/spec/requests/admin/fragments_spec.rb +50 -0
  107. data/lib/generators/trek/templates/spec/requests/admin/pages_spec.rb +47 -0
  108. data/lib/generators/trek/templates/spec/requests/admin/uploads_spec.rb +22 -0
  109. data/lib/generators/trek/templates/spec/requests/admin/user_password_resets_spec.rb +78 -0
  110. data/lib/generators/trek/templates/spec/requests/admin/user_sessions_spec.rb +51 -0
  111. data/lib/generators/trek/templates/spec/requests/admin/users_spec.rb +99 -0
  112. data/lib/generators/trek/templates/views/admin/user_password_resets/edit.html.slim +1 -1
  113. data/lib/generators/trek/templates/views/admin/user_password_resets/new.html.slim +1 -1
  114. data/lib/generators/trek/templates/views/admin/user_sessions/new.html.slim +2 -2
  115. data/lib/generators/trek/templates/views/admin/users/theme.turbo_stream.slim +2 -0
  116. data/lib/generators/trek/upgrade/v2_generator.rb +3 -0
  117. data/lib/tasks/trek/attachments.rake +18 -0
  118. data/lib/trek/cached_attachments.rb +53 -0
  119. data/lib/trek/saved_attachments_reload.rb +44 -0
  120. data/lib/trek/version.rb +1 -1
  121. data/lib/trek.rb +4 -0
  122. data/package.json +1 -1
  123. data/postcss.config.js +36 -0
  124. data/trek.gemspec +6 -0
  125. metadata +89 -3
  126. data/app/assets/icons/trek/theme.svg +0 -11
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 76621e65ede62feb9ed4fd8aa029352bfbdda31c814a7e9679330597a631b2f6
4
- data.tar.gz: a28bce934474a0d57069466281121658c75115eb9b9b1a3056eab9c6ee03ca5f
3
+ metadata.gz: d6ee08de9f89d342f41a9c9139580e2b9bdc3196278a25093c32f4fdb252f2a8
4
+ data.tar.gz: c9117839af6d9cbcec4951468913246ac20af148628d67a7930d0f8b8d74a3be
5
5
  SHA512:
6
- metadata.gz: 5653df741bf84cedce799a58c4f9d8c13d8a13226793f05d1f158b4971b0b08b1806463c82fa818526c8a0454d5b489f65cae207373c581bae074e1e3ae037bd
7
- data.tar.gz: aad5fb5a6e0e9010c5b611ef2ba67e7ddf112d2b983c85684ce698d32083d1da82ac24469404f3377a131d3ef1be56ec369baa81bd9cd7e71b4560a916800e62
6
+ metadata.gz: 79a68fecd5c544d8e3184a9388196e9cc80403b4b788614dc4db7f98ca9734a5a14543a56eeb7cd2b777f96ad3dd57fe29883687ef3771362f6c12f2b7e34123
7
+ data.tar.gz: 66358069ffd43f58794febe08082171cb2d47855e5f701436d5c38a87eb7c929490c7a4ae4837cdc7422765f6c1f57f1ee1214254b779300dcdb499eb01b5c3b
data/.slim-lint.yml CHANGED
@@ -8,9 +8,6 @@
8
8
  skip_frontmatter: false
9
9
 
10
10
  linters:
11
- AllCops:
12
- NewCops: disable
13
-
14
11
  CommentControlStatement:
15
12
  enabled: true
16
13
 
data/AGENTS.md CHANGED
@@ -21,6 +21,7 @@ bin/setup # bundle install + yarn install (dev setup)
21
21
  bin/console # IRB with trek loaded
22
22
  rake # default: spec + rubocop
23
23
  rake spec # run tests (or: bundle exec rspec)
24
+ COVERAGE=1 bundle exec rspec # tests + SimpleCov report in coverage/ (fails under the minimum set in spec/spec_helper.rb)
24
25
  make lint # all linters below
25
26
  make fix # rubocop/eslint/stylelint/prettier autocorrect
26
27
  make release # bump version (version.rb + package.json), stamp CHANGELOG, tag, push gem + npm
@@ -45,7 +46,13 @@ Security: `bundle exec brakeman` · `bundle exec bundle-audit check --update`
45
46
  - `lib/generators/trek/` — the heart of the gem: `install/` (~30 generators), `scaffold_generator.rb`, `admin/`, `templates/`
46
47
  - `lib/trek/` — `engine.rb`, `version.rb`, `i18n_keys_resolver.rb`, `typography_formatter.rb`
47
48
  - `config/locales/` — `trek.en.yml`, `trek.fr.yml`
48
- - `spec/` — RSpec suite · `docs/` — VitePress site
49
+ - `spec/` — RSpec suite, two kinds of spec:
50
+ - **isolated** (`require_relative` the file under test, stub its host): concerns, helpers, simple components
51
+ - **Rails** (`require "rails_helper"`): boots `spec/dummy`, a minimal host app on PostgreSQL (`trek_test`, or `DATABASE_URL`). Its models, migrations, Shrine initializer and the whole admin (controllers, views, routes, policies, mailers) are not committed: `spec/dummy/lib/generated_files.rb` runs Trek's own generator steps into `tmp/dummy` on every boot, and the database is rebuilt from them. A new model or migration template must be added to its `STEPS`, or `spec/dummy/generated_files_spec.rb` fails.
52
+ - **Host-app specs**: model and admin request specs and their [Oaken](https://github.com/kaspth/oaken) seeds are templates (`lib/generators/trek/templates/{spec/models,spec/requests,db/seeds}/`), installed in host apps by `trek:install:model_specs` and run by Trek's suite from `tmp/dummy` (`spec/dummy/generated_specs_spec.rb`); test data comes from `users.create`, `pages.create` and `seed "cases/…"`, not factories. Write specs for Trek's models there, in host-app style (no `frozen_string_literal`, Ruby 3.1-compatible — no bare `it` block parameter), so host apps get them too.
53
+ - **Generators** (`spec/trek/generators_spec.rb`): every generator runs on a copy of a real `rails new` app (`spec/support/generators.rb`, built once per run into `tmp/generator_skeleton`), then again to check it changes nothing. Shell-outs are recorded in `shell_commands`, not run. A new generator is picked up automatically; if it needs arguments, an earlier Trek step or a starting file, add it to the spec's `arguments`, `prerequisites` or `setups`.
54
+ - Both kinds run in one process, so an isolated spec must restore any global it changes (`I18n`, `Shrine` storages…). `spec_helper` loads Rails and Action View before Trek, as a host app does: several dependencies (ActionPolicy, Slim, ViewComponent) configure themselves from what is already loaded.
55
+ - `docs/` — VitePress site
49
56
 
50
57
  ## Conventions
51
58
 
@@ -53,6 +60,6 @@ Security: `bundle exec brakeman` · `bundle exec bundle-audit check --update`
53
60
  - **Slim** linted via slim_lint; **JS/CSS** via ESLint + Stylelint + Prettier 2.8.8
54
61
  - **i18n**: keep English + French locales in sync (`config/locales/trek.*.yml`); DeepL auto-translation via `DEEPL_AUTH_KEY`
55
62
  - **Versioning**: version lives in both `lib/trek/version.rb` and `package.json` — keep in sync via `make release`
56
- - **CHANGELOG.md**: Keep-a-Changelog format. **Every MR must add an `[Unreleased]` entry.** When the change requires action in the host app on upgrade, end the entry with a bold `**Upgrade note:**` label followed by what to do. Use the same label when the change lets host apps remove or simplify existing patches or custom implementations (e.g. "**Upgrade note:** you can now drop your custom …") — never as an unlabelled trailing sentence.
63
+ - **CHANGELOG.md**: Keep-a-Changelog format. **Every MR must add an `[Unreleased]` entry.** Keep entries short: one or two sentences on what changed for host apps, not how it was found or fixed — the MR description holds the details. When the change requires action in the host app on upgrade, end the entry with a bold `**Upgrade note:**` label followed by what to do. Use the same label when the change lets host apps remove or simplify existing patches or custom implementations (e.g. "**Upgrade note:** you can now drop your custom …") — never as an unlabelled trailing sentence.
57
64
  - **Public code**: Trek ships publicly (gem, npm, docs), so nothing committed may mention a client or project name, nor a client's domain models or business logic — not in code, comments, specs, templates, docs, CHANGELOG or commit messages. Use neutral examples (`Page`, `Shop::Product`, an engine's `Blog::Post`). Client context belongs in GitLab issues and MR descriptions only.
58
65
  - **CI**: GitLab CI (`.gitlab-ci.yml`), stages build → lint → security → test → deploy; docs deploy to Netlify; deps via Renovate
data/CHANGELOG.md CHANGED
@@ -1,5 +1,78 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [2.3.1] - 2026-10-02
4
+
5
+ ### Changed
6
+
7
+ - `Trek::Filters` only filters on the columns a controller lists in its new `available_filters` (empty by default), read through strong params (`filter_params`): other keys of `params[:filters]`, and values that aren't a single value, are ignored. Until now any key became a `where` condition, so a request could filter an index on columns it was never meant to expose. **Upgrade note:** in each controller including `Trek::Filters`, define `available_filters` returning the filtered columns (e.g. `def available_filters = %i[country_code]`), or its filters stop applying.
8
+ - `Trek::Users::Validations` stores emails stripped and in lower case (sign-in and password resets now ignore case), checks them with `email_validator`'s strict mode, and refuses a spaces-only password; the password reset form refuses an empty password instead of pretending to change it. **Upgrade note:** resolve users whose emails differ only by case (`User.group("lower(email)").having("count(*) > 1").count`), then downcase the rest (`User.find_each { |user| user.update_column(:email, user.email.strip.downcase) }`); in `Admin::UserPasswordResetsController#update`, redirect back with an error when `params[:password]` is blank (see Trek's template); re-run `rails g trek:install:model_specs` for the new specs.
9
+
10
+ ### Fixed
11
+
12
+ - The header's theme switcher now hovers like the links next to it, and the whole pill is clickable, not just the icon. `Trek::DropdownComponent`'s trigger inherits its context's color, and a new `trigger_classnames:` option styles the trigger button. **Upgrade note:** if you reuse `Trek::DropdownComponent`, move trigger styling (padding, hover) from `classnames:` to `trigger_classnames:`.
13
+
14
+ ## [2.3.0] - 2026-10-01
15
+
16
+ ### Added
17
+
18
+ - More admin request specs from `rails g trek:install:model_specs`, run by Trek's own suite as well: `fragments_spec.rb` (index, new, create, duplicate key, edit, update, destroy), `users_spec.rb` (an admin lists privileged users only, creates a user and sends the invitation, changes a role, deletes a user; an editor is sent back from the users list and other profiles, gets a 403 in JSON, edits their own profile but not their role, switches the admin theme) and `user_password_resets_spec.rb` (the reset request answers known and unknown emails alike and only mails privileged users, reset and invitation links open the form, an invalid link goes back to the request form, a new password signs the user out everywhere, a password under 8 characters is refused). A `db/seeds/setup/fragments.rb` makes `fragments` available to seeds and specs. **Upgrade note:** re-run `rails g trek:install:model_specs` to add them.
19
+ - `Trek::Dashboard::TileComponent` accepts `method:` and `data:`. With `method: :post` (or `:delete`…) the tile issues that request through Turbo, as a `data-turbo-method` link like `Trek::ButtonComponent`'s `to:` links, so a dashboard shortcut can create a record in one click (e.g. a pre-filled draft) rather than only open a form; `data:` adds attributes to the link, such as `turbo_confirm:`. Tiles without them still render a plain GET link. **Upgrade note:** you can now turn a tile pointing to a form into a direct action, and drop buttons kept elsewhere only because a tile couldn't POST.
20
+ - Policy specs for host apps, run by Trek's own suite as well: `rails g trek:install:model_specs` installs `spec/policies/admin/` specs for the page, fragment, user, preview and external link policies (what each role may do, which attributes it may change, which records it sees), and `rails g trek:taxonomies` installs the tag and tag category ones (a tag or category in use can't be deleted). **Upgrade note:** re-run `rails g trek:install:model_specs` (and `rails g trek:taxonomies` if you use tags) to add them.
21
+ - Sound uploads from the admin: `Trek::Form::SoundFieldComponent` (`f.sound_field`) posted to `/admin/upload/audio`, which no generated app routed or handled. `trek:install:uploaders` now installs an `AudioUploader` (MP3, AAC/M4A, Ogg, WAV, WebM, FLAC, up to the field's 200 MB), the generated `Admin::UploadsController` gains an `audio` action, and `trek:install:admin:routes` adds `post "upload/audio", to: "uploads#audio"` to the admin namespace. Attach it with `include AudioUploader::Attachment(:audio)` on a record with an `audio_data` jsonb column. **Upgrade note:** copy `app/uploaders/audio_uploader.rb` from Trek (or re-run `rails g trek:install:uploaders`), add the `audio` action to `app/controllers/admin/uploads_controller.rb` (`set_rack_response AudioUploader.upload_response(:cache, request.env)`), and add `post "upload/audio", to: "uploads#audio"` inside `namespace :admin` in `config/routes.rb`; you can then drop a hand-written audio uploader, action and route that did the same.
22
+ - `rails g trek:install:model_specs` also installs request specs for the admin, run by Trek's own suite as well: `spec/requests/admin/user_sessions_spec.rb` (sign-in form, signing in and out, refusing a wrong password or a user who isn't privileged), `pages_spec.rb` (index, new, create, invalid create, edit, update, destroy as an editor), `uploads_spec.rb` (the image upload endpoint) and the existing `n_plus_one_spec.rb`, with an image fixture in `spec/fixtures/files`. The Oaken users setup gains a `users.password` helper to sign in with. **Upgrade note:** re-run `rails g trek:install:model_specs` to add them, then `bin/rails spec`.
23
+ - `rails g trek:install:model_specs`, a new installer step (also runnable alone) giving host apps specs for the models Trek installs: `spec/models/page_spec.rb` (slugs, full paths through the page tree, path history when a page moves) and `spec/models/user_spec.rb` (validations, roles, `privileged`, `to_s`). Their test data uses [Oaken](https://github.com/kaspth/oaken) seed scripts rather than factories: `db/seeds/setup/users.rb` sets defaults for `users.create`/`users.new` (a unique email, a valid password), and `db/seeds/test/cases/page_tree.rb` lays out a named page tree the Page spec loads with `seed "cases/page_tree"`. The step adds the `oaken` gem (in every group, since `db/seeds.rb` now calls `Oaken.seed`, which also makes these seeds available to `bin/rails db:seed`) and `require "oaken/rspec_setup"` in `spec/rails_helper.rb`, which seeds the database before the suite. The specs check Trek's behaviour inside the app, so a customization that breaks it fails the app's own suite; edit or remove the ones covering behaviour you deliberately changed. Trek's own suite runs the very same files against its dummy app, so they stay green with each Trek release. Oaken needs Ruby 3.2+. **Upgrade note:** run `rails g trek:install:model_specs` to add them (it skips the gem, the `db/seeds.rb` line and the `rails_helper` line when already there, and asks before overwriting an existing file), then `bundle exec rspec spec/models`. `oaken/rspec_setup` truncates every table and runs `db/seeds.rb` before the suite, so make sure your `db/seeds.rb` is fine to run in the test environment.
24
+ - The header's theme button is now a working theme switcher. It used to toggle `light-theme`/`dark-theme` on the page without saving the choice (a `TODO` logged it to the console), so the theme reset on the next page load. It now opens a menu offering **Auto**, **Light** and **Dark**, applies the choice at once and saves it to the user's `theme` through `PATCH /admin/users/theme`. The action answers with a turbo stream that refreshes the header icon. Logged-out pages (login, password reset) now render with `auto-theme` instead of `light-theme`. **Auto** follows the system setting (`prefers-color-scheme`) without JavaScript: a small PostCSS plugin in `postcss.config.js` copies Radix's `.dark-theme` variables into a `@media (prefers-color-scheme: dark)` block scoped to `.auto-theme`. `color-scheme` is now set for each theme too, so native controls and scrollbars follow it. The menu is a new `Trek::DropdownComponent` (a trigger slot plus `with_item` entries, with keyboard navigation), which host apps can reuse. **Upgrade note:** in `app/controllers/admin/users_controller.rb`, replace `head :no_content` in the `theme` action with `respond_to { |format| format.turbo_stream; format.html { head :no_content } }`, and add `app/views/admin/users/theme.turbo_stream.slim` from Trek's generator templates (or re-run `rails g trek:install:admin:users`). Without them the theme is still saved, but the header icon only updates on the next page load. For **Auto** to follow the system setting, copy the `autoDarkTheme` plugin from Trek's `postcss.config.js` into yours and add it to `plugins` right after `postcss-import`; without it, **Auto** renders the light theme.
25
+ - `phonable_attributes` accepts a `strictness:` option — `:possible` (the default, unchanged) or `:valid`. `:possible` only asks Phonelib whether the digits could form a number for the country; `:valid` asks whether they fall in a range the regulator has actually assigned. The gap bites on input errors: with a French default country, `"0012345678"` is possible, so it is accepted and normalized to `"+12345678"` — the stray leading zero read as an international prefix, the number silently rehomed to country code +1, and the original input gone since normalization happens on save. Apps that intend to call or text the numbers they collect want `phonable_attributes :phone, strictness: :valid`.
26
+ - Plain file inputs are now styled: inside a `Trek::Form::GroupComponent` (`f.group`), a standard `f.file_field` gets its native `::file-selector-button` styled like a `Trek::ButtonComponent` with `size: :narrow`, using the accent variables so `data-accent-color` applies as on buttons. The browser's built-in "file chosen" feedback is kept and no JS is involved — useful for one-off uploads (e.g. an .xlsx import) next to Trek's styled fields. The Shrine-backed `image_field`/`sound_field` are unaffected (their file inputs are visually hidden). **Upgrade note:** you can now drop custom `::file-selector-button` styles that mirrored Trek's buttons for plain file inputs.
27
+ - The confirm dialog (`Trek::DialogComponent` wired to `Turbo.config.forms.confirm`) now takes its title, confirm button label and button color per action from the confirming element's data attributes — `data-turbo-confirm-title`, `data-turbo-confirm-button` and `data-turbo-confirm-color` alongside `data-turbo-confirm` — falling back to the current delete wording ("Are you sure?" / "Delete" / tomato) when absent. A non-destructive confirm (a "Validate" flow, a scheduled job trigger) no longer shows a **Delete** button, and one page can host a delete confirm and an action confirm side by side. Works on form submit buttons and on `data-turbo-method` links (the clicked element is remembered, since Turbo only forwards `data-turbo-confirm` to the form it builds for a link). **Upgrade note:** you can now drop `content_for(:dialog)`-style overrides that swapped the whole dialog just to rename the button — pass the data attributes on the confirming button instead.
28
+ - `Trek::MenuComponent` items accept a `target:` option forwarded to the link — e.g. `s.with_item(text: …, href: …, target: "_blank")` for external tools mounted in the admin, like a GoodJob dashboard. A `_blank` item automatically gets `rel="noopener"` and the `trek/external` new-tab icon after its text, as the toast link already did. `Trek::Dashboard::TileComponent` (which already accepted `target:`/`rel:`) now also defaults `rel` to `noopener` when `target` is `_blank`. **Upgrade note:** you can now drop initializer patches that reopened `ItemComponent` to add a `target` option.
29
+ - `Trek::ButtonComponent` accepts a `name:` option, rendered on the plain-button branch (paired with `value:`) so a `type: :submit` button can carry a param — needed when a form has two submit actions with different outcomes (e.g. a "save" button next to a "save then preview" one submitting `then_preview`). **Upgrade note:** you can now drop initializer patches that reopened the component to add a `name` option.
30
+ - `rails g trek:install:appsignal`, a new installer step (also runnable alone) that sets up [AppSignal](https://appsignal.com) error tracking and performance monitoring, until now wired by hand in every app: it adds the `appsignal` gem, injects `APPSIGNAL_PUSH_API_KEY` into the Rails credentials (as the Postmark generator does with its API token — `bundle exec appsignal install` would write the key in plain text and needs a terminal), and writes a `config/appsignal.rb` that reads the key from the credentials, activates the agent in production as soon as a key exists, and ignores the `/healthcheck` action. It also wraps the boot in `config.ru` so a broken initializer or a missing credential is reported, and installs an `AppsignalContext` controller concern that sets the `admin`/`web` namespace and tags transactions with the acting user — included in `ApplicationController` and, through an initializer, in `Trek::BaseController`. `APPSIGNAL_APP_ENV` is added to `.env.example`; the rescued Postmark delivery in `ApplicationMailer` now reports through `Rails.error` rather than only logging, which keeps app code independent of the monitoring vendor. Check an install with `bundle exec appsignal diagnose`. **Upgrade note:** existing apps can run `rails g trek:install:appsignal` — it skips the gem and the credentials when AppSignal is already set up, but review the generated `config/appsignal.rb` against your own (it takes the push API key from the credentials, so drop a plain-text key or an `APPSIGNAL_PUSH_API_KEY` environment variable you no longer need), and remove your hand-written `AppsignalContext` equivalent if you have one.
31
+ - `bin/rails trek:attachments:promote_cached`, a rake task finding the records whose Shrine attachments still point to the temporary `cache` storage — for every model and attachment, whatever the uploader — and promoting those whose cached file still exists (backed by `Trek::CachedAttachments`). It prints one line per attachment with its status: `promoted`, `missing` when the cached file is gone (it has to be uploaded again), or `changed` when the record changed meanwhile; `DRY_RUN=1` only lists them, as `promotable`. Documented in the [uploaders reference](/reference/uploaders).
32
+
33
+ ### Changed
34
+
35
+ - Trek's dummy app now generates and serves the whole admin (authentication, authorization, the admin generator's controllers and layout, routes, the pages, fragments, users, sessions and password-reset panels, mailers), so request and component specs run against the real admin. Line coverage goes from 49% to 73%; the minimum is raised to 72%.
36
+ - Every Trek generator is now run by Trek's own suite (`spec/trek/generators_spec.rb`): on a fresh `rails new` app set up as in the Getting started guide, then a second time to check that re-running it changes nothing (host apps re-run generators when upgrading). Shell-outs (bundle, yarn, migrations, rubocop) are recorded rather than run; the few other gems' generators whose output a later step edits (`rails g model`, `action_policy:install`, `mobility:install`, `rspec:install`) run for real, in-process. `trek:admin:user` has its own spec against the dummy app's database. Line coverage goes from 35% to 49%; the minimum is raised to 49%.
37
+ - `trek:install:ruby` now pins **Ruby 4.0.7** (was 4.0.6), and the CI images (repo + generated `.gitlab-ci.yml` template) use the matching `4.0.7` tag. **Upgrade note:** existing apps can set `4.0.7` in `.ruby-version` and bump the `ci-ruby` image tag in `.gitlab-ci.yml`.
38
+ - Trek's own specs now run in random order (`--seed` replays an order, `--bisect` finds the spec leaking state), and no longer depend on it: `page_path_helper_spec` switched the default locale to French inside `I18n.with_locale`, which left `I18n.locale` set to `:fr` for the specs that ran after it, so `localization_spec` and `i18n_helper_spec` failed under some orders.
39
+ - Trek's own suite can now test code that needs Rails and a database. Specs that `require "rails_helper"` boot `spec/dummy`, a minimal host app on PostgreSQL. Its models, migrations and Shrine initializer are not copied by hand: they are produced on each boot by running Trek's own generator steps (`trek:install:page`, `:user`, `:authentication`, `:fragment`…), so they cannot drift from what `rails g trek:install` gives a real app, and a spec fails when a model or migration template is added without the suite generating it. First `User` and `Page` model specs come with it. Line coverage goes from 9% to 35%, most of it from the app's code now being loaded; the minimum is raised to 34%.
40
+ - Trek's own spec suite runs in CI again, in a `tests` job that had been commented out — so nothing flagged that `spec/helpers/trek/page_path_helper_spec.rb` required a `rails_helper` the repository does not have, which stopped the whole suite from loading. That spec is rewritten in the suite's isolated style (no Rails app, stubbed routes). `COVERAGE=1` turns on SimpleCov, which reports line and branch coverage over every Ruby file in `app/` and `lib/` (generator templates excluded), writes a Cobertura report for GitLab's merge-request diff coverage, and fails the run below a minimum line coverage, currently 9%, to be raised as coverage grows.
41
+ - Trek is now developed and tested against **Shrine 3.10** (was 3.6). The gemspec constraint stays `~> 3.0`, so this changes nothing for host apps by itself — they already resolve 3.10 on Ruby ≥ 3.2 — but Trek's own suite and the generated app in CI no longer run against a version nobody installs. Worth picking up: 3.8 fixes a path traversal outside the storage directory in the `file_system` storage (the development and test storage), 3.7 stops `column` from failing on an empty string, 3.9 fixes `remove_invalid` leaving the attribute set after an invalid file is deassigned and stops the `activerecord` plugin from deleting the original attachment of a duplicated record. Shrine 3.7 dropped Ruby < 3.2, so an app still on Ruby 3.1 keeps resolving 3.6. **Upgrade note:** run `bundle update shrine` (nothing else to change).
42
+
43
+ ### Fixed
44
+
45
+ - A signed-in user opening what their role doesn't allow (an editor on the users panel, or on another user's profile) got a 500 error page: `ActionPolicy::Unauthorized` was raised and handled nowhere. `Trek::Authorization` now rescues it: back to the previous page, or to the dashboard, with a translated alert (`notices.unauthorized`); Turbo Stream and JSON requests get a 403.
46
+ - The new fragment form (and any fragment saved without content) raised `undefined method '[]' for nil`: the generated `fragments` migration created `content` as a jsonb column without the `{}` default its translated `title` has, and Mobility's jsonb backend reads a translation as `column[locale]`. The migration now defaults `content` to `{}`. **Upgrade note:** in apps generated before this version, add a migration with `change_column_default :fragments, :content, from: nil, to: {}` and `up_only { execute "UPDATE fragments SET content = '{}' WHERE content IS NULL" }`.
47
+ - The generated `Admin::ExternalLinkPolicy` scoped external links for non-privileged users with `relation.where(user:)`, a leftover from an example: `external_links` has no `user` column, so applying the scope raised a SQL error. Non-privileged users now get none, as in Trek's other policies. **Upgrade note:** in `app/policies/admin/external_link_policy.rb`, replace `relation.where(user:)` with `relation.none`.
48
+ - With `rails g trek:taxonomies`, asking whether a tag could be deleted (`Admin::TagPolicy#destroy?`) raised `HasManyThroughAssociationPolymorphicSourceError`: it called `Tag#taggables`, a `has_many :taggables, through: :taggings` that Rails can't build over the polymorphic `Tagging#taggable`. The policy now checks `taggings`, and `Tag#taggables` is a method returning the tagged records. **Upgrade note:** in `app/models/tag.rb`, replace `has_many :taggables, through: :taggings` with `def taggables = taggings.includes(:taggable).map(&:taggable)`, and in `app/policies/admin/tag_policy.rb`, replace `record.taggables.empty?` with `record.taggings.empty?`.
49
+ - The docs and README still described [Sorcery](https://github.com/Sorcery/sorcery) as Trek's authentication, and the `User` reference listed its `crypted_password` column. Trek authenticates with Rails' `has_secure_password` and database-backed `UserSession` records; the Getting started guide, the introduction, the home page, the customization guide, the installer and `User` references now say so, and the `User` columns table lists `password_digest` and `last_activity_at`. The README's ClosureTree link, which pointed to Sorcery, now points to ClosureTree.
50
+ - Deleting a page (or a page version) raised `NameError: Missing model class Section for the Page#sections association`: `Trek::Sectionable`, included in the generated `Page` and `PageVersion`, declared `has_many :sections, dependent: :destroy`, but no generator creates a `Section` model. The association is now declared only when the app defines one, so apps with their own sections keep them.
51
+ - Editing a page raised `undefined method 'page_path' for an instance of Trek::HeaderComponent`: the admin header's "See on website" link called `page_path`, which components don't include. The header now includes `Trek::PagePathHelper`, and leaves the link out when the app has no front-office route for the page (a freshly generated app has none), instead of raising.
52
+ - Image uploads from the admin returned 404: `trek:install:admin:routes` wrote `post "upload", to: "uploads#image", namespace: :admin` at the top of the routes (the `namespace:` ended up inside the route string), while image fields post to `/admin/upload/image`. It now writes `post "upload/image", to: "uploads#image"` inside `namespace :admin`. **Upgrade note:** in `config/routes.rb`, replace `post "upload", to: "uploads#image", namespace: :admin` with `post "upload/image", to: "uploads#image"` inside your `namespace :admin do … end` block.
53
+ - Trek's components use `Turbo::FramesHelper`, but the gemspec didn't declare `turbo-rails`: it only worked because `rails new` adds it by default. Trek now depends on `turbo-rails` (>= 1.0) and requires it.
54
+ - `Trek::Users::Validations` only enforced the 8-character password minimum when a user was created: the check for a later change looked at `crypted_password`, a column from Trek's former authentication library that `has_secure_password` users don't have, so an existing user could set a 1-character password (e.g. through the password reset form). It now also applies whenever `password_digest` changes, and still skips saves that leave the password alone. The generated `spec/models/user_spec.rb` covers both cases. **Upgrade note:** nothing to change; to get the new examples in an existing app, re-run `rails g trek:install:model_specs` and accept overwriting `spec/models/user_spec.rb` (or copy the two examples).
55
+ - `rails g trek:upgrade:v2` left two blank lines in `.yarnrc.yml` where it removed the `git.etamin.studio` registry, right before the `npmPreapprovedPackages` block it adds, so running it again changed the file. It now leaves a single one.
56
+ - `rails g trek:scaffold` wrote an invalid `config/locales/models/<model>.en.yml`: the attribute labels ran together on one line (`name: Name description: Description`), so Rails failed to load the translations (`Psych::SyntaxError: mapping values are not allowed in this context`) and every page rendering a translation raised. Each attribute is on its own line again. Trek's CI now generates its test app the way the Getting started guide does (`rails new -j esbuild`) and runs that app's own suite (`bin/rails spec`) after `trek:install` and `trek:scaffold`, which is how this surfaced. **Upgrade note:** in models scaffolded before this version, put each attribute of `config/locales/models/*.en.yml` on its own line.
57
+ - Freshly generated apps got a broken `db/schema.rb`: they resolve `json` 3, whose `JSON.parse` takes its options as keywords only, while ActiveSupport (up to 8.1.3.1, the latest release) passes them positionally. The schema dumper then fails on every jsonb column with a default and writes `# Could not dump table "pages" because of following ArgumentError` in place of `pages`, `page_paths`, `fragments` and `menu_nodes`, so the test database, loaded from `schema.rb`, lacks those tables and the app's specs cannot run. Trek now requires `json` < 3 until Rails supports json 3, and its suite dumps the generated schema to catch a regression. **Upgrade note:** run `bundle update trek` (it moves `json` back to 2.x if you had 3), then `bin/rails db:schema:dump` and check that `db/schema.rb` has no "Could not dump" comment left.
58
+ - Three generated migrations tripped RuboCop in a freshly generated app: `trek_tag_categories` and `trek_taggings` (from `rails g trek:taxonomies`) indented `create_table` by one space, and `trek_menu_nodes` ended with a trailing blank line. The templates are fixed, and Trek's suite now runs RuboCop on every generated migration. **Upgrade note:** apps generated before this version can run `bundle exec rubocop --autocorrect db/migrate` to fix them.
59
+ - `Trek::PagePathHelper#page_path` (and `page_url`) raised `NoMethodError: undefined method 'shop/product_path'` for a page attached to a namespaced pageable: the route name was built from `pageable.class.name.underscore`, which keeps the `/` of the namespace. It now uses `pageable.model_name.singular_route_key`, the key Rails' polymorphic routes use — `shop_product_path` for `Shop::Product`, and `post_path` for an isolated engine's `Blog::Post`. Non-namespaced pageables are unaffected.
60
+ - `Trek::PagePathHelper#page_url` returned a path, not a URL, for a page with a named route (a keyed page such as `about`): that branch always called `#{key}_path` and ignored the `url` suffix the other branches honour. It now calls `about_url`, so mailers, sitemaps and canonical links built with `page_url` get an absolute URL. **Upgrade note:** you can now drop workarounds that made `page_url` absolute for keyed pages (e.g. prefixing `request.base_url`).
61
+ - `trek:install:scalingo` pinned the buildpack with `BUILDPACK_URL=https://github.com/etaminstudio/trek-buildpack#v0.1.0`, and Scalingo resolves a `#ref` suffix as a **branch**: the tag was looked up as `archive/refs/heads/v0.1.0`, which does not exist, and every build failed with `fail to download and extract the custom buildpack: Invalid status code: 404 Not Found`. The generator and the [Deploying to Scalingo](https://trek.etamin.studio/deploy) guide now point at the release archive, `https://github.com/etaminstudio/trek-buildpack/archive/refs/tags/v0.1.0.tar.gz`, which Scalingo downloads as is and which stays pinned to the release. **Upgrade note:** on each Scalingo app, run `scalingo --app <app> env-set BUILDPACK_URL=https://github.com/etaminstudio/trek-buildpack/archive/refs/tags/v0.1.0.tar.gz` — an app still on the `#v0.1.0` form cannot deploy, and nothing surfaces it until the next deploy fails.
62
+ - The English locale gave the content editor's prompt panel its target labels in French ("un nouvel onglet" / "le même onglet"): `admin.content_editor.prompt.targets` had been copied from `trek.fr.yml` rather than translated. An English-locale admin inserting a Turbo-frame prompt now reads "a new tab" / "the same tab", as the link panel right above already did.
63
+ - The generated `trek_users` migration tripped RuboCop's `Rails/NotNullColumn` on `add_column :users, :password_digest, :string, null: false`, so `make lint` (and the `lint` CI job) failed in a freshly generated app — the installer's own closing `make fix` already exited non-zero on it. The column is fine as it stands: the preceding `trek_create_users` migration creates the table empty, so there is nothing to backfill and no default is wanted on a password digest. The line now carries an inline `rubocop:disable Rails/NotNullColumn` with a comment saying why. **Upgrade note:** apps generated before this version can add the same disable comment to `db/migrate/*_trek_users.rb`.
64
+ - The generated `.slim-lint.yml` listed an `AllCops:` key (`NewCops: disable`) among its linters. slim-lint validates linter names since 0.34 ("Add check for invalid keys in config files"), so `make lint` in a freshly generated app died with `No such linter: AllCops. Available: CommentControlStatement, …` before checking a single template; Trek's own lockfile pins 0.33, where the unknown key was ignored, which is why it went unnoticed. The key is gone — it configured nothing (the embedded RuboCop is configured under the `RuboCop` linter's `ignored_cops`). **Upgrade note:** remove the two `AllCops:` / `NewCops: disable` lines from your `.slim-lint.yml` if it still has them.
65
+ - The generated `config/appsignal.rb` now sets `config.log_path = "/tmp"` in production, so the AppSignal agent stops announcing `appsignal: Unable to log to '/app/log'. Logging to '/tmp' instead.` on every boot and every one-off container. Hosts with a read-only filesystem (Scalingo, Heroku and the like) cannot let the agent write its diagnostic log next to the app, and it falls back to `/tmp` by itself — nothing was lost, but the message reads like a misconfiguration. **Upgrade note:** add `config.log_path = "/tmp" if Rails.env.production?` to a `config/appsignal.rb` written before this version, or regenerate it with `rails g trek:install:appsignal`.
66
+ - `Trek::Phonable#normalize_phone` normalized every declared attribute from the value of `phone`: a model declaring `phonable_attributes :phone, :fax` — the concern's own documented example — overwrote the fax with the phone number on every save, and wiped it when the phone was blank. Each attribute is now parsed from its own value. Models declaring a single attribute are unaffected.
67
+ - `Trek::ButtonComponent`'s `disabled` option is now actually rendered — it was declared but ignored by every branch, so a "disabled" button stayed clickable with no visual cue. A disabled button renders `<button disabled>`; a disabled link (`href:` or `to:`) renders an inert anchor — no `href`, no Turbo method — with `aria-disabled="true"`, since `<a>` has no `disabled` attribute. Both get a dimmed, non-interactive style (`opacity: 0.5`, `pointer-events: none`, `cursor: not-allowed`). **Upgrade note:** you can now drop `classnames: "is-disabled"`-style workarounds and their CSS, and pass `disabled:` with a real `href` instead of blanking the href conditionally.
68
+ - The icon of a `Trek::ButtonComponent` no longer shrinks next to a long label: as a flex item it was squeezed (and, with a `min-width`, distorted) when the text ran out of room. It keeps its size, the text wraps or ellipsises as before. **Upgrade note:** you can now drop a `.c-trek--button-icon { flex-shrink: 0 }` override.
69
+ - The login, forgotten-password and password-reset forms labelled their fields with the humanized attribute name ("Email", "Password") whatever the locale. They post to a URL rather than to a record, so `Trek::Form::GroupComponent` had no model to resolve `activerecord.attributes.user.*` against and fell back to the attribute name; the translations shipped in Trek's locales were never looked up. The three generated views now pass `model_name: User.model_name` to `f.group`, which drives the i18n lookup alone and leaves field names (and therefore the params the controllers read) untouched. **Upgrade note:** views generated before this version keep the old labels — add `model_name: User.model_name` to the `f.group` calls in `app/views/admin/user_sessions/new.html.slim` and `app/views/admin/user_password_resets/{new,edit}.html.slim`, or regenerate them.
70
+ - The toast link's new-tab icon rendered as an empty `<svg><use></svg>`: `Trek::Toaster::ToastComponent::LinkComponent` referenced an `external` icon that didn't ship with the gem (it only resolved when the host app provided its own `app/assets/icons/external.svg`). Trek now bundles `trek/external` ([Lucide](https://lucide.dev)'s `external-link`, ISC) and the toast link uses it; the icon is also available to host apps as `Trek::IconComponent.new("trek/external")`. **Upgrade note:** you can now drop an `app/assets/icons/external.svg` that only existed to back the toast link (keep it if your own code renders the bare `external` key).
71
+ - `Trek::MenuComponent` items pointing at a namespaced model doubled the namespace in the path used for the active-state check (`href: [:admin, :missive, Missive::List]` compared against `admin/missive/missive/lists`), so those items never got `is-active`. The namespace segments already covered by the model's collection are now deduplicated, and the item highlights on its own controller. **Upgrade note:** you can now drop `active:` overrides that only worked around this (e.g. `active: "admin/#{model.model_name.collection}"`).
72
+ - `make release` now chains its steps with `&&`, so a failing step halts the run. Previously the steps were separated by `;`: when `rake release` failed (e.g. RubyGems asking for an MFA code), `npm publish` still ran and its output buried the error, leaving the gem unpublished while the tag and npm package were out.
73
+ - `make release` prompts for the RubyGems and npm one-time passwords right before each publish step (passed as `GEM_HOST_OTP_CODE` and `npm publish --otp`), so accounts with MFA no longer fail the push; leave a prompt empty when MFA is off for that registry.
74
+ - A file attached to a record on a numerically ordered closure tree — the generated `Page` and its image — stayed in Shrine's temporary `cache` storage forever when attached on create or together with a parent change. closure_tree reloads the record in its `after_save` callback to reorder siblings, and Shrine's `activerecord` plugin resets the attacher on reload, so the `after_commit` promotion found nothing to promote: the record kept `"storage": "cache"` data, and its derivation URLs broke with "Source file not found" as soon as the cached file was deleted. Updating an existing page's image was unaffected. `Trek::SavedAttachmentsReload`, a Shrine plugin loaded by the generated `RecordImageUploader`, now keeps across a reload the attachers whose file was saved with the record (an unsaved attachment is still discarded), so the file is promoted and the replaced one deleted after commit. **Upgrade note:** add `plugin Trek::SavedAttachmentsReload` under `plugin :activerecord` in `app/uploaders/record_image_uploader.rb` (it covers every model using that uploader, not only Trek's). Records saved before that may still point to cached files — run `bin/rails trek:attachments:promote_cached` to promote those whose file still exists, and have the ones it reports as `missing` uploaded again.
75
+
3
76
  ## [2.2.0] - 2026-08-28
4
77
 
5
78
  ### Added
data/Gemfile CHANGED
@@ -22,6 +22,10 @@ group :development, :test do
22
22
  end
23
23
 
24
24
  group :test do
25
+ gem "bcrypt", "~> 3.1"
25
26
  gem "capybara", "~> 3.38"
27
+ gem "oaken", "~> 1.0"
28
+ gem "pg", "~> 1.5"
26
29
  gem "simplecov", "~> 0.22.0"
30
+ gem "simplecov-cobertura", "~> 3.0"
27
31
  end
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- trek (2.2.0)
4
+ trek (2.3.1)
5
5
  action_policy (~> 0.6)
6
6
  actioncable
7
7
  acts_as_list (~> 1.1)
@@ -10,10 +10,12 @@ PATH
10
10
  closure_tree (~> 9.0)
11
11
  deepl-rb (~> 3.0)
12
12
  dry-initializer (~> 3.1)
13
+ email_validator (~> 2.2)
13
14
  fastimage (~> 2.2)
14
15
  http_accept_language (~> 2.1)
15
16
  i18n-tasks (~> 1.0)
16
17
  image_processing (~> 1.8)
18
+ json (< 3)
17
19
  kaminari
18
20
  listen
19
21
  lookbook (>= 2.3.13)
@@ -28,6 +30,7 @@ PATH
28
30
  shrine-blurhash (~> 0.2)
29
31
  slim-rails (~> 4.0)
30
32
  strip_attributes (~> 2.0)
33
+ turbo-rails (>= 1.0)
31
34
  view_component (< 5.0)
32
35
  view_component-form (~> 0.3)
33
36
  zeitwerk (~> 2.6)
@@ -137,6 +140,7 @@ GEM
137
140
  aws-sigv4 (1.12.1)
138
141
  aws-eventstream (~> 1, >= 1.0.2)
139
142
  base64 (0.3.0)
143
+ bcrypt (3.1.22)
140
144
  bigdecimal (4.1.2)
141
145
  blurhash (0.1.8)
142
146
  brakeman (7.1.2)
@@ -169,8 +173,9 @@ GEM
169
173
  deepl-rb (3.6.1)
170
174
  diff-lcs (1.6.2)
171
175
  docile (1.4.1)
172
- down (5.4.2)
176
+ down (5.6.0)
173
177
  addressable (~> 2.8)
178
+ base64 (~> 0.3)
174
179
  drb (2.2.3)
175
180
  dry-configurable (1.3.0)
176
181
  dry-core (~> 1.1)
@@ -201,6 +206,8 @@ GEM
201
206
  dry-inflector (~> 1.0)
202
207
  dry-logic (~> 1.4)
203
208
  zeitwerk (~> 2.6)
209
+ email_validator (2.2.4)
210
+ activemodel
204
211
  erb (6.0.4)
205
212
  erubi (1.13.1)
206
213
  fastimage (2.4.0)
@@ -302,10 +309,13 @@ GEM
302
309
  racc (~> 1.4)
303
310
  nokogiri (1.19.4-x86_64-darwin)
304
311
  racc (~> 1.4)
312
+ oaken (1.0.0)
305
313
  parallel (1.27.0)
306
314
  parser (3.3.10.0)
307
315
  ast (~> 2.4.1)
308
316
  racc
317
+ pg (1.6.3-arm64-darwin)
318
+ pg (1.6.3-x86_64-darwin)
309
319
  phonelib (0.10.15)
310
320
  postmark (1.25.1)
311
321
  json
@@ -457,7 +467,7 @@ GEM
457
467
  ffi (~> 1.12)
458
468
  logger
459
469
  securerandom (0.4.1)
460
- shrine (3.6.0)
470
+ shrine (3.10.0)
461
471
  content_disposition (~> 1.0)
462
472
  down (~> 5.1)
463
473
  shrine-blurhash (0.2.3)
@@ -467,6 +477,9 @@ GEM
467
477
  docile (~> 1.1)
468
478
  simplecov-html (~> 0.11)
469
479
  simplecov_json_formatter (~> 0.1)
480
+ simplecov-cobertura (3.2.0)
481
+ rexml
482
+ simplecov (~> 0.19)
470
483
  simplecov-html (0.13.2)
471
484
  simplecov_json_formatter (0.1.4)
472
485
  slim (5.2.1)
@@ -491,6 +504,9 @@ GEM
491
504
  tilt (2.6.1)
492
505
  timeout (0.6.1)
493
506
  tsort (0.2.0)
507
+ turbo-rails (2.0.23)
508
+ actionpack (>= 7.1.0)
509
+ railties (>= 7.1.0)
494
510
  tzinfo (2.0.6)
495
511
  concurrent-ruby (~> 1.0)
496
512
  unicode-display_width (3.2.0)
@@ -528,9 +544,12 @@ PLATFORMS
528
544
  x86_64-darwin-24
529
545
 
530
546
  DEPENDENCIES
547
+ bcrypt (~> 3.1)
531
548
  brakeman (~> 7.0)
532
549
  bundler-audit (~> 0.9)
533
550
  capybara (~> 3.38)
551
+ oaken (~> 1.0)
552
+ pg (~> 1.5)
534
553
  rake (~> 13.0)
535
554
  reek (~> 6.1)
536
555
  rspec-html-matchers (~> 0.10)
@@ -541,6 +560,7 @@ DEPENDENCIES
541
560
  rubocop-rails-omakase (~> 1.1)
542
561
  rubocop-rspec (~> 3.5)
543
562
  simplecov (~> 0.22.0)
563
+ simplecov-cobertura (~> 3.0)
544
564
  slim_lint (~> 0.24)
545
565
  trek!
546
566
 
data/Makefile CHANGED
@@ -15,15 +15,17 @@ fix:
15
15
 
16
16
  release:
17
17
  @read -p "New version (current: $$(sed -n 's/.*VERSION = "\(.*\)"/\1/p' lib/trek/version.rb)): " version; \
18
- if [ -z "$$version" ]; then echo "Version cannot be empty"; exit 1; fi; \
19
- sed -i '' "s/VERSION = \".*\"/VERSION = \"$$version\"/" lib/trek/version.rb; \
20
- sed -i '' "s/\"version\": \".*\"/\"version\": \"$$version\"/" package.json; \
21
- awk -v v="$$version" -v d="$$(date +%F)" '!done && /^## \[Unreleased\]/ {print; print ""; print "## [" v "] - " d; done=1; next} {print}' CHANGELOG.md > CHANGELOG.md.tmp && mv CHANGELOG.md.tmp CHANGELOG.md; \
22
- bundle; \
23
- git add -A; \
24
- git commit -m "Version $$version"; \
25
- bundle exec rake release; \
26
- npm publish
18
+ if [ -z "$$version" ]; then echo "Version cannot be empty"; exit 1; fi && \
19
+ sed -i '' "s/VERSION = \".*\"/VERSION = \"$$version\"/" lib/trek/version.rb && \
20
+ sed -i '' "s/\"version\": \".*\"/\"version\": \"$$version\"/" package.json && \
21
+ awk -v v="$$version" -v d="$$(date +%F)" '!done && /^## \[Unreleased\]/ {print; print ""; print "## [" v "] - " d; done=1; next} {print}' CHANGELOG.md > CHANGELOG.md.tmp && mv CHANGELOG.md.tmp CHANGELOG.md && \
22
+ bundle && \
23
+ git add -A && \
24
+ git commit -m "Version $$version" && \
25
+ read -p "RubyGems OTP code (leave empty if MFA is off): " gem_otp && \
26
+ env $${gem_otp:+GEM_HOST_OTP_CODE=$$gem_otp} bundle exec rake release && \
27
+ read -p "npm OTP code (leave empty if MFA is off): " npm_otp && \
28
+ npm publish $${npm_otp:+--otp=$$npm_otp}
27
29
 
28
30
  audit-fix:
29
31
  @out=$$(bundle exec bundle-audit check --update 2>&1); \
data/README.md CHANGED
@@ -16,7 +16,7 @@ Trek is a modern content management system for Ruby on Rails, that is designed t
16
16
  Trek uses these gems under the hood:
17
17
 
18
18
  - [ActionPolicy](https://github.com/palkan/action_policy) for authorization
19
- - [ClosureTree](https://github.com/Sorcery/sorcery) for hierarchies
19
+ - [ClosureTree](https://github.com/ClosureTree/closure_tree) for hierarchies
20
20
  - [Postmark](https://github.com/ActiveCampaign/postmark-rails) for sending emails
21
21
  - [rails-i18n](https://github.com/svenfuchs/rails-i18n), [mobility](https://github.com/shioyama/mobility) and [i18n-tasks](https://github.com/glebm/i18n-tasks) for internationalization
22
22
  - [RSpec](https://github.com/rspec/rspec-rails) for testing
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-down-icon lucide-chevron-down"><path d="m6 9 6 6 6-6"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-up-icon lucide-chevron-up"><path d="m18 15-6-6-6 6"/></svg>
@@ -0,0 +1 @@
1
+ <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M15 3h6v6" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path><path d="M10 14 21 3" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-sun-moon-icon lucide-sun-moon"><path d="M12 2v2"/><path d="M14.837 16.385a6 6 0 1 1-7.223-7.222c.624-.147.97.66.715 1.248a4 4 0 0 0 5.26 5.259c.589-.255 1.396.09 1.248.715"/><path d="M16 12a4 4 0 0 0-4-4"/><path d="m19 5-1.256 1.256"/><path d="M20 12h2"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-moon-icon lucide-moon"><path d="M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-sun-medium-icon lucide-sun-medium"><circle cx="12" cy="12" r="4"/><path d="M12 3v1"/><path d="M12 20v1"/><path d="M3 12h1"/><path d="M20 12h1"/><path d="m18.364 5.636-.707.707"/><path d="m6.343 17.657-.707.707"/><path d="m5.636 5.636.707.707"/><path d="m17.657 17.657.707.707"/></svg>
@@ -7,6 +7,19 @@ html {
7
7
  font-size: 62.5%; /* for easy rem sizing: 1rem = 10px */
8
8
  }
9
9
 
10
+ /* let native UI (form controls, scrollbars) follow the theme */
11
+ .light-theme {
12
+ color-scheme: light;
13
+ }
14
+
15
+ .dark-theme {
16
+ color-scheme: dark;
17
+ }
18
+
19
+ .auto-theme {
20
+ color-scheme: light dark;
21
+ }
22
+
10
23
  /* allow modules to change the box-sizing */
11
24
  *,
12
25
  *::before,
@@ -35,7 +35,16 @@
35
35
  border-color: var(--accent-7);
36
36
  }
37
37
 
38
+ .root:disabled,
39
+ .root[aria-disabled="true"] {
40
+ cursor: not-allowed;
41
+ opacity: 0.5;
42
+ pointer-events: none;
43
+ }
44
+
45
+ /* Keeps its size next to a long label, which would otherwise squeeze it. */
38
46
  .icon {
47
+ flex-shrink: 0;
39
48
  margin-right: 1.2rem;
40
49
  }
41
50
 
@@ -10,6 +10,7 @@ module Trek
10
10
  option :form_class, optional: true
11
11
  option :href, optional: true
12
12
  option :method, optional: true
13
+ option :name, optional: true
13
14
  option :params, optional: true
14
15
  option :position, type: proc(&:to_sym), optional: true
15
16
  option :rel, optional: true
@@ -50,7 +51,16 @@ module Trek
50
51
  end
51
52
 
52
53
  def call
53
- if link?
54
+ if disabled && (link? || to?)
55
+ # <a> has no disabled attribute: render an inert anchor — no href, no
56
+ # turbo method — flagged for assistive technologies.
57
+ content_tag :a,
58
+ rendered_content,
59
+ class: classes,
60
+ data: data_attributes,
61
+ title: title,
62
+ "aria-disabled": true
63
+ elsif link?
54
64
  link_to rendered_content,
55
65
  href,
56
66
  class: classes,
@@ -72,7 +82,9 @@ module Trek
72
82
  rendered_content,
73
83
  class: classes,
74
84
  data: data_attributes,
85
+ disabled:,
75
86
  form:,
87
+ name:,
76
88
  type:,
77
89
  value:
78
90
  end
@@ -1,5 +1,5 @@
1
1
  - if href.present?
2
- = link_to href, class: classes, rel: rel, target: target
2
+ = link_to href, class: classes, data: link_data, rel: link_rel, target: target
3
3
  - if title
4
4
  span(class=class_for("title")) = title
5
5
  - if icon
@@ -3,8 +3,14 @@
3
3
  module Trek
4
4
  class Dashboard::TileComponent < Trek::Component
5
5
  option :color, default: proc { :slate }
6
+ # Extra data attributes for the link, e.g. `turbo_confirm:`.
7
+ option :data, default: proc { {} }
6
8
  option :href, optional: true
7
9
  option :icon, optional: true
10
+ # An HTTP method other than GET (`:post`, `:delete`…), issued through
11
+ # Turbo like Trek::ButtonComponent's `to:` links: a tile can create a
12
+ # record in one click rather than open a form.
13
+ option :method, optional: true
8
14
  option :rel, optional: true
9
15
  option :target, optional: true
10
16
  option :text
@@ -25,6 +31,14 @@ module Trek
25
31
  )
26
32
  end
27
33
 
34
+ def link_data
35
+ data.merge(turbo_method: method).compact
36
+ end
37
+
38
+ def link_rel
39
+ rel || ("noopener" if target == "_blank")
40
+ end
41
+
28
42
  def formatted_text
29
43
  # rubocop:disable Rails/OutputSafety
30
44
  text.gsub("\n", "#{tag.br} ")&.html_safe
@@ -1,5 +1,8 @@
1
1
  dialog(class=root_class data-controller=identifier)
2
- h2(class=class_for("title")) = title || t(".title")
2
+ h2(
3
+ class=class_for("title")
4
+ data=stimulus_target_hash("title")
5
+ ) = title || t(".title")
3
6
  p(
4
7
  class=class_for("text")
5
8
  data=stimulus_target_hash("text")
@@ -22,5 +25,6 @@ dialog(class=root_class data-controller=identifier)
22
25
  text: t("admin.actions.delete"),
23
26
  color: :tomato,
24
27
  value: "confirm",
25
- data: { action: stimulus_action("close") },
28
+ data: { action: stimulus_action("close") }
29
+ .merge(stimulus_target_hash("confirm_button")),
26
30
  )
@@ -3,11 +3,26 @@ import "./dialog_component.css";
3
3
  import { Controller as BaseController } from "@hotwired/stimulus";
4
4
 
5
5
  export class Controller extends BaseController {
6
- static targets = ["text"];
6
+ static targets = ["title", "text", "confirmButton"];
7
7
 
8
8
  connect() {
9
+ this.saveDefaults();
10
+
11
+ // For a link with data-turbo-method, Turbo submits an ephemeral form it
12
+ // only copies data-turbo-confirm onto, and calls confirm() without a
13
+ // submitter — remember the clicked element to read the other
14
+ // data-turbo-confirm-* attributes from.
15
+ this.rememberConfirmSource = (event) => {
16
+ this.confirmSource = event.target.closest?.("[data-turbo-confirm]");
17
+ };
18
+ document.addEventListener("click", this.rememberConfirmSource, {
19
+ capture: true,
20
+ });
21
+
9
22
  // eslint-disable-next-line no-undef
10
- Turbo.config.forms.confirm = (message) => {
23
+ Turbo.config.forms.confirm = (message, _formElement, submitter) => {
24
+ this.customize({ ...this.confirmSource?.dataset, ...submitter?.dataset });
25
+ this.confirmSource = null;
11
26
  this.setText(message);
12
27
  this.open();
13
28
 
@@ -23,6 +38,51 @@ export class Controller extends BaseController {
23
38
  };
24
39
  }
25
40
 
41
+ disconnect() {
42
+ document.removeEventListener("click", this.rememberConfirmSource, {
43
+ capture: true,
44
+ });
45
+ }
46
+
47
+ saveDefaults() {
48
+ if (this.hasTitleTarget) {
49
+ this.defaultTitle = this.titleTarget.textContent;
50
+ }
51
+
52
+ if (this.hasConfirmButtonTarget) {
53
+ this.defaultConfirmLabel = this.confirmLabelElement.textContent;
54
+ this.defaultConfirmColor = this.confirmButtonTarget.dataset.accentColor;
55
+ }
56
+ }
57
+
58
+ customize({ turboConfirmTitle, turboConfirmButton, turboConfirmColor }) {
59
+ if (this.hasTitleTarget) {
60
+ this.titleTarget.textContent = turboConfirmTitle ?? this.defaultTitle;
61
+ }
62
+
63
+ if (this.hasConfirmButtonTarget) {
64
+ this.confirmLabelElement.textContent =
65
+ turboConfirmButton ?? this.defaultConfirmLabel;
66
+ this.setConfirmColor(turboConfirmColor ?? this.defaultConfirmColor);
67
+ }
68
+ }
69
+
70
+ setConfirmColor(color) {
71
+ const button = this.confirmButtonTarget;
72
+
73
+ // Swap the postcss-modules-scoped color-* class along with the accent
74
+ // variables (see generateScopedName in postcss.config.js).
75
+ button.classList.replace(
76
+ `c-trek--button-color-${button.dataset.accentColor}`,
77
+ `c-trek--button-color-${color}`
78
+ );
79
+ button.dataset.accentColor = color;
80
+ }
81
+
82
+ get confirmLabelElement() {
83
+ return this.confirmButtonTarget.querySelector("span");
84
+ }
85
+
26
86
  setText(text) {
27
87
  if (this.hasTextTarget) {
28
88
  this.textTarget.textContent = text;