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
@@ -9,6 +9,11 @@ module Trek
9
9
  end
10
10
 
11
11
  included do
12
+ # Sections are the host app's own `Section` model, which Trek doesn't generate: without one, a
13
+ # record has no sections, and declaring the association anyway would make `destroy` raise
14
+ # (`dependent: :destroy`). `const_defined?` also sees a model Zeitwerk hasn't loaded yet.
15
+ next unless Object.const_defined?(:Section)
16
+
12
17
  has_many :sections,
13
18
  -> { ordered },
14
19
  as: :sectionable,
@@ -4,14 +4,23 @@ module Trek
4
4
  extend ActiveSupport::Concern
5
5
 
6
6
  included do
7
- validates :password,
8
- length: { minimum: 8 },
9
- if: -> { new_record? || changes[:crypted_password] },
10
- allow_blank: true
7
+ # Also applies to lookups (`find_by(email:)`, `authenticate_by`), so signing in and asking
8
+ # for a password reset work whatever the case typed.
9
+ normalizes :email, with: ->(email) { email.strip.downcase }
10
+
11
11
  validates :email,
12
12
  presence: true,
13
- uniqueness: true,
14
- format: { with: URI::MailTo::EMAIL_REGEXP }
13
+ uniqueness: { case_sensitive: false },
14
+ email: { mode: :strict }
15
+ # Only when a password was assigned: `password` is nil otherwise.
16
+ validates :password, length: { minimum: 8 }, allow_nil: true
17
+ validate :password_not_only_spaces
18
+ end
19
+
20
+ private
21
+
22
+ def password_not_only_spaces
23
+ errors.add(:password, :blank) if password&.strip&.empty?
15
24
  end
16
25
  end
17
26
  end
@@ -0,0 +1,11 @@
1
+ class AudioUploader < Shrine
2
+ plugin :determine_mime_type
3
+ plugin :validation_helpers
4
+ # The admin's sound field accepts files up to 200 MB (`Trek::Form::SoundFieldComponent`), above
5
+ # the 20 MB the Shrine initializer allows images.
6
+ plugin :upload_endpoint, url: true, max_size: 200.megabytes
7
+
8
+ Attacher.validate do
9
+ validate_mime_type %w[audio/mpeg audio/mp4 audio/aac audio/ogg audio/wav audio/x-wav audio/webm audio/flac]
10
+ end
11
+ end
@@ -1,3 +1,6 @@
1
1
  class RecordImageUploader < ImageUploader
2
2
  plugin :activerecord
3
+ # Keeps a file attached during a save that reloads the record (closure_tree reorders siblings
4
+ # that way) pending, so it is still promoted out of the cache storage on commit.
5
+ plugin Trek::SavedAttachmentsReload
3
6
  end
@@ -69,8 +69,8 @@ en:
69
69
  self: the same tab
70
70
  prompt:
71
71
  targets:
72
- blank: un nouvel onglet
73
- self: le même onglet
72
+ blank: a new tab
73
+ self: the same tab
74
74
  contents: Contents
75
75
  dashboard: Dashboard
76
76
  dashboards:
@@ -264,3 +264,4 @@ en:
264
264
  notices:
265
265
  authentication_needed: Please log in
266
266
  rate_limited: Too many attempts, please try again later
267
+ unauthorized: You are not allowed to do that
@@ -273,3 +273,4 @@ fr:
273
273
  notices:
274
274
  authentication_needed: Veuillez vous connecter
275
275
  rate_limited: Trop de tentatives, veuillez réessayer plus tard
276
+ unauthorized: Vous n'êtes pas autorisé·e à faire cela
@@ -1,5 +1,9 @@
1
1
  import { defineConfig } from 'vitepress'
2
2
  import llmstxt from 'vitepress-plugin-llms'
3
+ import { readFileSync } from 'node:fs'
4
+
5
+ // The released version: `make release` keeps package.json in sync with lib/trek/version.rb.
6
+ const { version } = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'))
3
7
 
4
8
 
5
9
  // https://vitepress.dev/reference/site-config
@@ -11,6 +15,13 @@ export default defineConfig({
11
15
  description: "A modern content management & back-office system for Ruby on Rails",
12
16
  lang: 'en-US',
13
17
  lastUpdated: true,
18
+ // The header logo doubles as the favicon (PNG versions for older browsers and iOS home screens,
19
+ // favicon.ico for clients that only request /favicon.ico).
20
+ head: [
21
+ ['link', { rel: 'icon', type: 'image/svg+xml', href: '/logo.svg' }],
22
+ ['link', { rel: 'icon', type: 'image/png', sizes: '32x32', href: '/favicon.png' }],
23
+ ['link', { rel: 'apple-touch-icon', href: '/apple-touch-icon.png' }],
24
+ ],
14
25
  themeConfig: {
15
26
  logo: { src: '/logo.svg', width: 24, height: 24 },
16
27
 
@@ -18,7 +29,15 @@ export default defineConfig({
18
29
  nav: [
19
30
  { text: 'Home', link: '/' },
20
31
  { text: 'Guide', link: '/what-is-trek' },
21
- { text: 'Reference', link: '/reference/' }
32
+ { text: 'Reference', link: '/reference/' },
33
+ {
34
+ text: `v${version}`,
35
+ items: [
36
+ { text: 'Changelog', link: '/changelog' },
37
+ { text: 'RubyGems', link: `https://rubygems.org/gems/trek/versions/${version}` },
38
+ { text: 'npm', link: `https://www.npmjs.com/package/trek/v/${version}` }
39
+ ]
40
+ }
22
41
  ],
23
42
 
24
43
  sidebar: {
@@ -75,15 +75,18 @@
75
75
  * -------------------------------------------------------------------------- */
76
76
 
77
77
  :root {
78
+ /* The violet end of the hero name gradient, a step darker on hover and when pressed */
79
+ --trek-button-brand: #7a4df0;
80
+
78
81
  --vp-button-brand-border: transparent;
79
82
  --vp-button-brand-text: var(--vp-c-white);
80
- --vp-button-brand-bg: var(--vp-c-brand-3);
83
+ --vp-button-brand-bg: var(--trek-button-brand);
81
84
  --vp-button-brand-hover-border: transparent;
82
85
  --vp-button-brand-hover-text: var(--vp-c-white);
83
- --vp-button-brand-hover-bg: var(--vp-c-brand-2);
86
+ --vp-button-brand-hover-bg: color-mix(in srgb, var(--trek-button-brand) 88%, black);
84
87
  --vp-button-brand-active-border: transparent;
85
88
  --vp-button-brand-active-text: var(--vp-c-white);
86
- --vp-button-brand-active-bg: var(--vp-c-brand-1);
89
+ --vp-button-brand-active-bg: color-mix(in srgb, var(--trek-button-brand) 76%, black);
87
90
  }
88
91
 
89
92
  /**
@@ -91,19 +94,44 @@
91
94
  * -------------------------------------------------------------------------- */
92
95
 
93
96
  :root {
97
+ --trek-green: #63e0a6;
98
+ --trek-purple: #9b7bff;
99
+
94
100
  --vp-home-hero-name-color: transparent;
95
- --vp-home-hero-name-background: -webkit-linear-gradient(
101
+ /* Darker stops on the light background, the brand colors themselves on the dark one */
102
+ --vp-home-hero-name-background: linear-gradient(
96
103
  120deg,
97
- #bd34fe 30%,
98
- #41d1ff
104
+ #1fa971 15%,
105
+ #7a4df0 100%
99
106
  );
100
107
 
101
108
  --vp-home-hero-image-background-image: linear-gradient(
102
109
  -45deg,
103
- #bd34fe 50%,
104
- #47caff 50%
110
+ var(--trek-purple) 50%,
111
+ var(--trek-green) 50%
105
112
  );
106
113
  --vp-home-hero-image-filter: blur(44px);
114
+
115
+ /* Feature icons (docs/index.md): the ink follows the text color, light or dark */
116
+ --trek-icon-ink: currentColor;
117
+ --trek-icon-accent: var(--trek-green);
118
+ }
119
+
120
+ /* Text selection in the brand violet rather than the browser's default blue */
121
+ ::selection {
122
+ background-color: color-mix(in srgb, var(--trek-purple) 30%, transparent);
123
+ }
124
+
125
+ .dark ::selection {
126
+ background-color: color-mix(in srgb, var(--trek-purple) 45%, transparent);
127
+ }
128
+
129
+ .dark {
130
+ --vp-home-hero-name-background: linear-gradient(
131
+ 120deg,
132
+ var(--trek-green) 15%,
133
+ var(--trek-purple) 100%
134
+ );
107
135
  }
108
136
 
109
137
  @media (min-width: 640px) {
data/docs/changelog.md ADDED
@@ -0,0 +1,9 @@
1
+ ---
2
+ outline: 2
3
+ ---
4
+
5
+ # Changelog
6
+
7
+ What changed in each release of Trek, newest first, with upgrade notes when an app needs to act. **Unreleased** lists what's merged but not yet published.
8
+
9
+ <!--@include: ../CHANGELOG.md-->
data/docs/customize.md CHANGED
@@ -27,7 +27,21 @@ Replace Trek's favicon with your own:
27
27
  Trek's UI components accept options where it matters:
28
28
 
29
29
  - **`Trek::Form::ActionsComponent`** — pick an `aspect` and add your own `classnames` to restyle form action bars.
30
- - **`Trek::DialogComponent`** — set the `title` and the buttons of modal dialogs.
30
+ - **`Trek::DialogComponent`** — set the `title` and the buttons of modal dialogs. The confirm dialog wired to `data-turbo-confirm` can also be customized per action from the confirming element's data attributes, with the delete wording as fallback:
31
+
32
+ ```slim
33
+ = render Trek::ButtonComponent.new(
34
+ text: "Run the prize draw",
35
+ to: admin_event_draw_path(event), method: :post,
36
+ data: {
37
+ turbo_confirm: "Run the prize draw now? Winners will be picked and emails sent.",
38
+ turbo_confirm_title: "Prize draw",
39
+ turbo_confirm_button: "Run the draw",
40
+ turbo_confirm_color: "grass",
41
+ },
42
+ )
43
+ ```
44
+
31
45
  - **`Trek::Form::ContentEditorComponent`** — toggle the `nodes`, `blocks` and `floating` options to slim down the editor where full rich text isn't needed:
32
46
 
33
47
  ```ruby
@@ -62,9 +76,11 @@ Each scaffolded resource gets its own policy in `app/policies/admin`. Adjust the
62
76
 
63
77
  Trek's admin supports light, dark and auto themes, selectable per user. The design tokens are based on [Radix colors](https://www.radix-ui.com/colors); override the CSS custom properties in your own stylesheet to adapt the palette.
64
78
 
79
+ The auto theme follows the system setting (`prefers-color-scheme`). The `autoDarkTheme` plugin in the generated `postcss.config.js` copies every rule targeting `.dark-theme` into a `@media (prefers-color-scheme: dark)` block, with `.auto-theme` in its place, so dark overrides in your own stylesheets (`.dark-theme { --accent-9: … }`, `.dark-theme .card { … }`) apply to the auto theme too.
80
+
65
81
  ## Dependencies configuration
66
82
 
67
- The installer configures Shrine, Postmark, Sorcery, ActionPolicy, Mobility and friends with sensible defaults — through initializers generated **in your app**. You can edit them at any time:
83
+ The installer configures Shrine, Postmark, ActionPolicy, Mobility and friends with sensible defaults — through initializers generated **in your app**. You can edit them at any time:
68
84
 
69
85
  - `config/initializers/shrine.rb` — storage backends, upload limits
70
86
  - `config/credentials.yml.enc` — Postmark API token (`EDITOR="code --wait" bin/rails credentials:edit`)
data/docs/deploy.md CHANGED
@@ -20,14 +20,19 @@ Generate the Scalingo files (an `Aptfile` and a `Procfile` with the web process
20
20
  rails g trek:install:scalingo
21
21
  ```
22
22
 
23
- Then configure the Scalingo app's environment, pinning the buildpack to a release tag:
23
+ Then configure the Scalingo app's environment, pinning the buildpack to a release through its archive:
24
24
 
25
25
  ```sh
26
- scalingo --app <app> env-set BUILDPACK_URL=https://github.com/etaminstudio/trek-buildpack#v0.1.0
26
+ scalingo --app <app> env-set BUILDPACK_URL=https://github.com/etaminstudio/trek-buildpack/archive/refs/tags/v0.1.0.tar.gz
27
27
  scalingo --app <app> env-set 'DATABASE_URL=$SCALINGO_POSTGRESQL_URL'
28
28
  scalingo --app <app> env-set RAILS_MASTER_KEY=$(cat config/master.key)
29
29
  ```
30
30
 
31
+ > Pin through the archive, not a `#tag` suffix: Scalingo reads `#ref` on `BUILDPACK_URL` as a **branch**, so
32
+ > `…/trek-buildpack#v0.1.0` asks for a branch named `v0.1.0`, which does not exist, and the build fails with
33
+ > `fail to download and extract the custom buildpack: Invalid status code: 404 Not Found`. The `.tar.gz` form above is
34
+ > downloaded as is and stays pinned to the release.
35
+
31
36
  Make sure `config/database.yml` reads `DATABASE_URL` in production — the section `rails new` generates targets a local socket with a password variable instead:
32
37
 
33
38
  ```yml
@@ -68,7 +68,7 @@ The installer will:
68
68
  - create the `Fragment` model and include Trek's concerns
69
69
  - create the relevant policies
70
70
  - generate and run the relevant migrations
71
- - set up authentication (Sorcery), authorization (ActionPolicy), uploads (Shrine), emails (Postmark), i18n, linting, CI and more
71
+ - set up authentication (`has_secure_password` with database-backed sessions), authorization (ActionPolicy), uploads (Shrine), emails (Postmark), i18n, linting, CI and more
72
72
 
73
73
  ### Configure Postmark (optional)
74
74
 
data/docs/index.md CHANGED
@@ -7,7 +7,7 @@ hero:
7
7
  text: "Take the easy path"
8
8
  tagline: "A modern content management & back-office system for Ruby on Rails"
9
9
  image:
10
- src: /logo.svg
10
+ src: /trek-mark.webp
11
11
  alt: Trek
12
12
  actions:
13
13
  - theme: brand
@@ -18,22 +18,22 @@ hero:
18
18
  link: /getting-started
19
19
 
20
20
  features:
21
- - icon: 🚀
21
+ - icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="32" height="32" aria-hidden="true"><path fill-rule="evenodd" fill="var(--trek-icon-ink, currentColor)" d="M16 14A8 8 0 0 1 32 14V38H16ZM21 18A3 3 0 1 0 27 18A3 3 0 1 0 21 18ZM16 26V38H4A12 12 0 0 1 16 26ZM32 26A12 12 0 0 1 44 38H32Z"/><path fill="var(--trek-icon-accent, #63E0A6)" d="M18 40H30A6 6 0 0 1 18 40Z"/></svg>'
22
22
  title: Ready out of the box
23
23
  details: One installer sets up authentication, authorization, file uploads, emails, i18n and a polished admin UI — with sensible conventions you can override later.
24
- - icon: 📝
24
+ - icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="32" height="32" aria-hidden="true"><path fill-rule="evenodd" fill="var(--trek-icon-ink, currentColor)" d="M6 6H30V30A12 12 0 0 1 18 42H6ZM12 14H24V18H12ZM12 22H20V26H12Z"/><path fill="var(--trek-icon-accent, #63E0A6)" d="M34 6H42V32A4 4 0 0 1 34 32Z"/></svg>'
25
25
  title: Modern content editing
26
26
  details: A Tiptap-based rich text editor, hierarchical pages with versioning and SEO-friendly paths, plus reusable content fragments.
27
- - icon: 🛠
27
+ - icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="32" height="32" aria-hidden="true"><path fill-rule="evenodd" fill="var(--trek-icon-ink, currentColor)" d="M6 18H30V30H42V42H6Z"/><path fill="var(--trek-icon-accent, #63E0A6)" d="M6 18V6A12 12 0 0 1 18 18Z"/></svg>'
28
28
  title: Scaffold-driven development
29
29
  details: One command generates the model, admin panel, views, policy, routes and locales for any resource — just like Rails scaffolding, but for your back-office.
30
- - icon: 🌍
30
+ - icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="32" height="32" aria-hidden="true"><path fill-rule="evenodd" fill="var(--trek-icon-ink, currentColor)" d="M22 22H5A17 17 0 0 1 22 5ZM22 26V43A17 17 0 0 1 5 26ZM26 26H43A17 17 0 0 1 26 43Z"/><path fill="var(--trek-icon-accent, #63E0A6)" d="M26 22V5A17 17 0 0 1 43 22Z"/></svg>'
31
31
  title: Multilingual by default
32
32
  details: Content and admin UI translations powered by Mobility and i18n-tasks, with optional automatic translation via DeepL.
33
- - icon: 🔐
33
+ - icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="32" height="32" aria-hidden="true"><path fill-rule="evenodd" fill="var(--trek-icon-ink, currentColor)" d="M14 22V16A10 10 0 0 1 34 16V22H28V16A4 4 0 0 0 20 16V22ZM8 22H24V42H16A8 8 0 0 1 8 34Z"/><path fill="var(--trek-icon-accent, #63E0A6)" d="M24 22H40V34A8 8 0 0 1 32 42H24Z"/></svg>'
34
34
  title: Roles & policies built in
35
- details: Sorcery authentication and ActionPolicy authorization with admin, editor, user, reader and guest roles, enforced across every panel.
36
- - icon: 🧩
35
+ details: Authentication on Rails' own has_secure_password and ActionPolicy authorization with admin, editor, user, reader and guest roles, enforced across every panel.
36
+ - icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="32" height="32" aria-hidden="true"><path fill-rule="evenodd" fill="var(--trek-icon-ink, currentColor)" d="M6 6H22V22H6ZM26 22V6A16 16 0 0 1 42 22ZM6 26H22V42A16 16 0 0 1 6 26Z"/><path fill="var(--trek-icon-accent, #63E0A6)" d="M26 26H42V42H26Z"/></svg>'
37
37
  title: Hotwire-native UI
38
38
  details: ViewComponents, Stimulus and Turbo under the hood, with light & dark themes, Radix colors and a customizable design system.
39
39
  ---
Binary file
Binary file
Binary file
Binary file
@@ -22,6 +22,21 @@ The main building blocks, all in the `Trek::` namespace:
22
22
  | `HeadingComponent` | headings |
23
23
  | `GateComponent` | authorization-based visibility gate |
24
24
  | `IconComponent` | SVG [icon](/reference/icons) renderer |
25
+ | `Dashboard::SectionComponent` / `Dashboard::TileComponent` | dashboard sections of shortcut tiles |
26
+
27
+ ## Dashboard tiles
28
+
29
+ A tile is a link by default. With `method:`, it issues another HTTP method through Turbo (as a `data-turbo-method` link), so a tile can create a record in one click rather than open a form; `data:` adds attributes to the link, such as a confirmation:
30
+
31
+ ```ruby
32
+ s.with_tile(
33
+ text: "New weekly draft",
34
+ icon: "trek/add",
35
+ href: [:draft_current_week, :admin, :newsletters],
36
+ method: :post,
37
+ data: { turbo_confirm: "Create this week's draft?" }
38
+ )
39
+ ```
25
40
 
26
41
  ## Form components
27
42
 
@@ -13,11 +13,37 @@ end
13
13
 
14
14
  For each declared attribute:
15
15
 
16
- - a validation (`phone: { possible: true, allow_blank: true }`)
16
+ - a validation (`phone: { possible: true, allow_blank: true }`, see [Strictness](#strictness))
17
17
  - normalization to E.164 international format on save
18
18
  - `formatted_{attr}` — national format (`"01 23 45 67 89"`)
19
19
  - `{attr}_with_country_code` — E.164 (`"+33123456789"`)
20
20
 
21
+ ## Strictness
22
+
23
+ `strictness:` picks how hard Phonelib looks at a number. It applies to every attribute of the call.
24
+
25
+ | value | what is accepted |
26
+ | --- | --- |
27
+ | `:possible` (default) | the digits **could** form a number for the country — right length and shape |
28
+ | `:valid` | the digits fall in a range the regulator has actually **assigned** |
29
+
30
+ ```ruby
31
+ class Organization < ApplicationRecord
32
+ include Trek::Phonable
33
+ phonable_attributes :phone, strictness: :valid
34
+ end
35
+ ```
36
+
37
+ The gap between the two is worth knowing before leaving the default in place. With
38
+ `Phonelib.default_country = "FR"`, `"0012345678"` is *possible*: under `:possible` it is accepted and
39
+ normalized to `"+12345678"` — the stray leading zero read as an international prefix, the number
40
+ silently rehomed to country code +1, and the original input gone, since normalization happens on
41
+ save. Under `:valid` the same input is rejected and the person is asked to correct it.
42
+
43
+ `:possible` is the right default for a number stored as a way to reach someone, where a false
44
+ rejection is worse than a number that never gets dialled. Choose `:valid` when the application
45
+ intends to actually call or text what it collects.
46
+
21
47
  ## Usage
22
48
 
23
49
  ```ruby
@@ -26,9 +26,10 @@ end
26
26
 
27
27
  ### Filters
28
28
 
29
- `Trek::Filters` lets users narrow an index by attribute values. Call `apply_filters!` in your `index` action — it
30
- rewrites `@objects` from `params[:filters]`, applying a `where(column => value)` per non-blank filter (a value of
31
- `"nil"` matches `NULL`):
29
+ `Trek::Filters` lets users narrow an index by attribute values. Declare the columns users may filter on via
30
+ `available_filters`, then call `apply_filters!` in your `index` action — it rewrites `@objects` from
31
+ `params[:filters]`, applying a `where(column => value)` per non-blank filter (a value of `"nil"` matches `NULL`). The filters are read
32
+ through strong params (`filter_params`): any other key, and any value that isn't a single value, is ignored:
32
33
 
33
34
  ```ruby
34
35
  module Admin
@@ -44,6 +45,10 @@ module Admin
44
45
 
45
46
  private
46
47
 
48
+ def available_filters
49
+ %i[country_code]
50
+ end
51
+
47
52
  def countries
48
53
  City.distinct.pluck(:country_code).index_with { |code| I18n.t("countries.#{code}") }
49
54
  end
@@ -4,6 +4,8 @@
4
4
  | --- | --- |
5
5
  | `ADMIN_EMAIL` / `ADMIN_PASSWORD` | credentials used by [`trek:admin:user`](/reference/generators/admin-user) |
6
6
  | `POSTMARK_API_TOKEN` | Postmark email delivery (auto-injected into credentials by the [installer](/reference/generators/install)) |
7
+ | `APPSIGNAL_PUSH_API_KEY` | AppSignal error tracking and performance monitoring (auto-injected into credentials by the [installer](/reference/generators/install)) |
8
+ | `APPSIGNAL_APP_ENV` | environment name reported to AppSignal (optional, defaults to `RAILS_ENV`) |
7
9
  | `DEEPL_AUTH_KEY` | automatic locale translation in the [scaffold generator](/reference/generators/scaffold) |
8
10
  | `FEATURE_STORAGE` | `local` (development) or `s3` (production) |
9
11
  | `S3_BUCKET`, `S3_REGION`, `S3_ENDPOINT`, `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY`, `S3_BASE_URL` | S3 storage configuration for [uploads](/reference/uploaders) |
@@ -13,6 +13,8 @@ Trek ships `Trek::FormBuilder`, an extension of [`ViewComponent::Form`](https://
13
13
 
14
14
  Each helper renders the matching component from the [`Trek::Form` namespace](/reference/components#form-components).
15
15
 
16
+ Standard helpers are styled too: a plain `f.file_field` inside an `f.group` gets its native `::file-selector-button` styled like a narrow `Trek::ButtonComponent` (following the current `data-accent-color`), keeping the browser's built-in "file chosen" feedback with no JS — handy for one-off uploads like a spreadsheet import.
17
+
16
18
  ```slim
17
19
  = form_with model: [:admin, @book] do |f|
18
20
  = f.group :title
@@ -21,7 +21,7 @@ The installer orchestrates dedicated sub-generators, all under the `trek:install
21
21
 
22
22
  | Area | What gets set up |
23
23
  | --- | --- |
24
- | Authentication | Sorcery, the `UserSession` form model, login/logout & password reset panels |
24
+ | Authentication | bcrypt and `has_secure_password` on `User`, the `UserSession` model (one per signed-in browser, found through a signed `session_id` cookie), the `Trek::Authentication` controller concern, login/logout & password reset panels |
25
25
  | Authorization | ActionPolicy and the base policies |
26
26
  | Models | `User`, `Page`, `PagePath`, `PageVersion`, `Fragment`, `MenuNode`, `ExternalLink`, `Current` — with migrations |
27
27
  | Attachments | Shrine configuration and the uploaders |
@@ -32,6 +32,8 @@ The installer orchestrates dedicated sub-generators, all under the `trek:install
32
32
  | Tooling | esbuild, PostCSS, EditorConfig, Procfiles, Makefile, health check endpoint |
33
33
  | Linting | Standard, ESLint, Stylelint, Prettier, Slim Lint, Lefthook git hooks |
34
34
  | Security | Brakeman, bundler-audit, ruby_audit |
35
+ | Monitoring | AppSignal error tracking and performance monitoring — `config/appsignal.rb`, the push API key in the credentials, and the `AppsignalContext` concern tagging transactions per area and user |
36
+ | Specs | RSpec, and specs for the models Trek installs (`trek:install:model_specs`): `spec/models/page_spec.rb` and `spec/models/user_spec.rb`, with their test data as [Oaken](https://github.com/kaspth/oaken) seed scripts — defaults in `db/seeds/setup/`, scenarios in `db/seeds/test/cases/` loaded with `seed "cases/…"`. They check Trek's behaviour in your app — slugs and paths, user validations and roles — so a customization that breaks it fails your suite; edit or remove the ones you deliberately change. Trek's own suite runs the same files |
35
37
  | N+1 detection | Prosopite (development/test), scanning every request — see [Controllers › N+1 detection](/reference/controllers#n-1-detection) |
36
38
  | CI | GitLab CI configuration |
37
39
  | Deployment | Scalingo files (`trek:install:scalingo`, run on demand) — see [Deploying to Scalingo](/deploy) |
@@ -41,4 +43,5 @@ The installer orchestrates dedicated sub-generators, all under the `trek:install
41
43
  Define these **before** running the installer to have them injected automatically:
42
44
 
43
45
  - `POSTMARK_API_TOKEN` — injected into the Rails credentials for email delivery
46
+ - `APPSIGNAL_PUSH_API_KEY` — injected into the Rails credentials for error tracking and performance monitoring
44
47
  - `DEEPL_AUTH_KEY` — enables automatic locale translation in the [scaffold generator](/reference/generators/scaffold)
@@ -6,6 +6,6 @@ Trek bundles ~40 SVG icons, rendered with `Trek::IconComponent`:
6
6
  render Trek::IconComponent.new(:search)
7
7
  ```
8
8
 
9
- Available keys include: `add`, `arrow-bar-left`, `arrow-bar-right`, `bold`, `check`, `chevron-left`, `chevron-right`, `chevron-double`, `close`, `cursor`, `danger`, `download`, `eraser`, `eye`, `grid`, `hr`, `italic`, `layout`, `link`, `list`, `logout`, `paragraph`, `pic`, `profile`, `quotation`, `rating`, `redo`, `undo`, `search`…
9
+ Available keys include: `add`, `arrow-bar-left`, `arrow-bar-right`, `bold`, `check`, `chevron-left`, `chevron-right`, `chevron-double`, `close`, `cursor`, `danger`, `download`, `eraser`, `external`, `eye`, `grid`, `hr`, `italic`, `layout`, `link`, `list`, `logout`, `paragraph`, `pic`, `profile`, `quotation`, `rating`, `redo`, `undo`, `search`…
10
10
 
11
11
  [MenuNode](/reference/models/menu-node) records can reference an icon via their `icon_key` to display it in the admin sidebar.
@@ -12,7 +12,7 @@ The taxonomy models, installed by the [taxonomies generator](/reference/generato
12
12
  | `position` | integer | ordering within the category |
13
13
  | `taggings_count` | integer | counter cache |
14
14
 
15
- - `belongs_to :category`, `has_many :taggings`, `has_many :taggables, through: :taggings`
15
+ - `belongs_to :category`, `has_many :taggings`, and `taggables`, the tagged records of any model
16
16
  - Scopes: `ordered`, `in_category(key)`, `in_categories(keys)`
17
17
 
18
18
  ## TagCategory
@@ -1,6 +1,6 @@
1
1
  # User
2
2
 
3
- The host application's `User` model, into which Trek injects authentication (Sorcery), roles, theme & locale preferences and the invitation workflow.
3
+ The host application's `User` model, into which Trek injects authentication (`has_secure_password` and database-backed sessions), roles, theme & locale preferences and the invitation workflow.
4
4
 
5
5
  ## Columns added by Trek
6
6
 
@@ -10,8 +10,10 @@ The host application's `User` model, into which Trek injects authentication (Sor
10
10
  | `role` | integer | enum, default `guest` |
11
11
  | `theme` | integer | enum, default `auto` |
12
12
  | `locale` | string | admin UI language |
13
+ | `password_digest` | string | `has_secure_password` (bcrypt), required |
14
+ | `last_activity_at` | datetime | updated after each signed-in admin request |
13
15
 
14
- Plus the Sorcery authentication columns (`email`, `crypted_password`, reset tokens…).
16
+ When the app has no `User` model yet, Trek creates the `users` table with its `email` column. Signed-in browsers are `UserSession` records (`user_agent`, `ip_address`) in a `user_sessions` table, found through a signed `session_id` cookie. Password reset (15 minutes) and invitation (1 week) links carry tokens from `generates_token_for`, so no column stores them.
15
17
 
16
18
  ## Roles
17
19
 
@@ -50,8 +52,8 @@ User.create(email: "new@example.com", role: :editor, send_invite: true)
50
52
 
51
53
  `Trek::Users::Validations` enforces:
52
54
 
53
- - `email` — presence, uniqueness, valid format
54
- - `password` — minimum 8 characters (on creation or password change)
55
+ - `email` — stored stripped and in lower case (lookups too, so sign-in and password resets ignore case), present, unique whatever its case, and well-formed by [`email_validator`](https://github.com/K-and-R/email_validator)'s strict mode (no consecutive or leading/trailing dots, a domain with a TLD)
56
+ - `password` — when one is assigned: at least 8 characters, and not spaces only. A blank password is ignored by `has_secure_password` (the current one is kept); the password reset form refuses it
55
57
 
56
58
  ## Scopes
57
59
 
@@ -5,15 +5,40 @@ File uploads are handled by [Shrine](https://shrinerb.com), with Uppy on the fro
5
5
  - `ImageUploader` — base image uploader
6
6
  - `RecordImageUploader` — attach an image to any record: `include RecordImageUploader::Attachment(:image)`
7
7
  - `ModelImageUploader` — model-level image uploader (used by content editor images)
8
+ - `AudioUploader` — sounds for `f.sound_field` (MP3, AAC/M4A, Ogg, WAV, WebM, FLAC): `include AudioUploader::Attachment(:audio)` on a record with an `audio_data` jsonb column
8
9
 
9
10
  ## Storage
10
11
 
11
12
  Configured per environment in the generated Shrine initializer (`config/initializers/shrine.rb`):
12
13
 
13
- | Environment | Storage |
14
- | --- | --- |
15
- | production | S3 (via the [`S3_*` ENV variables](/reference/environment-variables)) |
16
- | development | local filesystem (`public/uploads/`) |
17
- | test | in-memory |
14
+ | Environment | Storage |
15
+ | ----------- | --------------------------------------------------------------------- |
16
+ | production | S3 (via the [`S3_*` ENV variables](/reference/environment-variables)) |
17
+ | development | local filesystem (`public/uploads/`) |
18
+ | test | in-memory |
18
19
 
19
- Image processing uses libvips, blurhash placeholders are generated for progressive loading, and direct uploads go through the Shrine `upload_endpoint` (20 MB max).
20
+ Image processing uses libvips, blurhash placeholders are generated for progressive loading, and direct uploads go through the Shrine `upload_endpoint`: `POST /admin/upload/image` (20 MB max) and `POST /admin/upload/audio` (200 MB max), handled by the generated `Admin::UploadsController`. The endpoint only checks the size; each uploader validates the file type when it is attached to a record.
21
+
22
+ ## Attachments and records reloaded mid-save
23
+
24
+ `RecordImageUploader` loads `Trek::SavedAttachmentsReload` on top of Shrine's `activerecord` plugin:
25
+
26
+ ```ruby
27
+ class RecordImageUploader < ImageUploader
28
+ plugin :activerecord
29
+ plugin Trek::SavedAttachmentsReload
30
+ end
31
+ ```
32
+
33
+ Shrine resets every attacher when a record is reloaded, which is right for an explicit reload but loses a pending attachment when something reloads the record _during_ the save — closure_tree does exactly that in its `after_save` callback when it reorders the siblings of a numerically ordered tree, as the generated `Page` is. The file would then never be promoted out of the `cache` storage. The plugin keeps, across such a reload, the attachers whose file was saved with the record; an attachment assigned but not saved is still discarded.
34
+
35
+ ## Files left in the cache storage
36
+
37
+ An uploaded file first lands in the temporary `cache` storage, and is promoted to the permanent `store` after the record is committed. A record whose promotion was skipped keeps pointing to the cache, and its file breaks as soon as the cache is cleaned up. To find those records, for every model and attachment, and promote the ones whose file still exists:
38
+
39
+ ```bash
40
+ DRY_RUN=1 bin/rails trek:attachments:promote_cached # only lists them
41
+ bin/rails trek:attachments:promote_cached
42
+ ```
43
+
44
+ Each line gives the status (`promoted`, `promotable` on a dry run, `missing` when the cached file is gone, `changed` when the record changed meanwhile), the record and attachment, and the file id. Missing files have to be uploaded again.
data/docs/what-is-trek.md CHANGED
@@ -10,7 +10,7 @@ Trek ships as a Rails engine (a gem plus an NPM package) that integrates into yo
10
10
  - **Pages** — hierarchical, translatable pages with version history, SEO metadata and stable, redirect-aware URL paths.
11
11
  - **Fragments** — reusable chunks of content (a footer text, a hero tagline…) editable from the admin and usable anywhere in your views.
12
12
  - **A rich text editor** — based on [Tiptap](https://tiptap.dev), with image uploads (Uppy), link insertion and content stored as structured ProseMirror JSON.
13
- - **Authentication & authorization** — [Sorcery](https://github.com/Sorcery/sorcery)-based sign in, password resets, invitations, and [ActionPolicy](https://actionpolicy.evilmartians.io) policies with `admin`, `editor`, `user`, `reader` and `guest` roles.
13
+ - **Authentication & authorization** — sign in built on Rails' [`has_secure_password`](https://api.rubyonrails.org/classes/ActiveModel/SecurePassword/ClassMethods.html) with database-backed sessions, password resets, invitations, and [ActionPolicy](https://actionpolicy.evilmartians.io) policies with `admin`, `editor`, `user`, `reader` and `guest` roles.
14
14
  - **Internationalization** — content translations with [Mobility](https://github.com/shioyama/mobility), locale management with [i18n-tasks](https://github.com/glebm/i18n-tasks), and optional automatic translation through DeepL.
15
15
  - **File uploads** — [Shrine](https://shrinerb.com)-backed attachments with S3 or local storage, image processing via libvips, and blurhash placeholders.
16
16
  - **Generators** — an installer that sets up everything above, and a scaffold generator that creates a full admin panel for any model in one command.
@@ -14,7 +14,13 @@ module Trek
14
14
  end
15
15
 
16
16
  def upload_image
17
- route 'post "upload", to: "uploads#image", namespace: :admin'
17
+ # The endpoint `Trek::Form::ImageFieldComponent` uploads to: /admin/upload/image
18
+ route 'post "upload/image", to: "uploads#image"', namespace: :admin
19
+ end
20
+
21
+ def upload_audio
22
+ # The endpoint `Trek::Form::SoundFieldComponent` uploads to: /admin/upload/audio
23
+ route 'post "upload/audio", to: "uploads#audio"', namespace: :admin
18
24
  end
19
25
 
20
26
  def lookbook