tsykvas_rails_template 0.1.0

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 (79) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +200 -0
  3. data/CODE_OF_CONDUCT.md +10 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +589 -0
  6. data/Rakefile +17 -0
  7. data/lib/generators/tsykvas_rails_template/companions/companions_generator.rb +273 -0
  8. data/lib/generators/tsykvas_rails_template/concept/concept_generator.rb +145 -0
  9. data/lib/generators/tsykvas_rails_template/concept/templates/component/edit.html.slim.tt +5 -0
  10. data/lib/generators/tsykvas_rails_template/concept/templates/component/edit.rb.tt +11 -0
  11. data/lib/generators/tsykvas_rails_template/concept/templates/component/index.html.slim.tt +5 -0
  12. data/lib/generators/tsykvas_rails_template/concept/templates/component/index.rb.tt +11 -0
  13. data/lib/generators/tsykvas_rails_template/concept/templates/component/new.html.slim.tt +5 -0
  14. data/lib/generators/tsykvas_rails_template/concept/templates/component/new.rb.tt +11 -0
  15. data/lib/generators/tsykvas_rails_template/concept/templates/component/show.html.slim.tt +4 -0
  16. data/lib/generators/tsykvas_rails_template/concept/templates/component/show.rb.tt +11 -0
  17. data/lib/generators/tsykvas_rails_template/concept/templates/controller.rb.tt +45 -0
  18. data/lib/generators/tsykvas_rails_template/concept/templates/operation/create.rb.tt +31 -0
  19. data/lib/generators/tsykvas_rails_template/concept/templates/operation/destroy.rb.tt +13 -0
  20. data/lib/generators/tsykvas_rails_template/concept/templates/operation/edit.rb.tt +10 -0
  21. data/lib/generators/tsykvas_rails_template/concept/templates/operation/index.rb.tt +9 -0
  22. data/lib/generators/tsykvas_rails_template/concept/templates/operation/new.rb.tt +10 -0
  23. data/lib/generators/tsykvas_rails_template/concept/templates/operation/show.rb.tt +10 -0
  24. data/lib/generators/tsykvas_rails_template/concept/templates/operation/update.rb.tt +31 -0
  25. data/lib/generators/tsykvas_rails_template/install/bootstrap_installer.rb +225 -0
  26. data/lib/generators/tsykvas_rails_template/install/install_generator.rb +298 -0
  27. data/lib/generators/tsykvas_rails_template/install/templates/.claude/agents/buddy.md +157 -0
  28. data/lib/generators/tsykvas_rails_template/install/templates/.claude/agents/code-reviewer.md +117 -0
  29. data/lib/generators/tsykvas_rails_template/install/templates/.claude/agents/security-reviewer.md +113 -0
  30. data/lib/generators/tsykvas_rails_template/install/templates/.claude/agents/tech-lead.md +150 -0
  31. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/check.md +51 -0
  32. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/code-review.md +60 -0
  33. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/docs-create.md +102 -0
  34. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/pr-review.md +81 -0
  35. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/pushit.md +160 -0
  36. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/refactor.md +132 -0
  37. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/task-sum.md +47 -0
  38. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/tests.md +67 -0
  39. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/tsykvas-claude.md +262 -0
  40. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/update-docs.md +78 -0
  41. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/update-rules.md +102 -0
  42. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/update-tests.md +135 -0
  43. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/architecture.md +315 -0
  44. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/authentication.md +96 -0
  45. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/background-jobs.md +135 -0
  46. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/code-style.md +101 -0
  47. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/commands.md +34 -0
  48. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/companions.md +128 -0
  49. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/concepts-refactoring.md +194 -0
  50. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/database.md +135 -0
  51. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/deployment.md +138 -0
  52. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/design-system.md +322 -0
  53. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/documentation.md +89 -0
  54. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/forms.md +174 -0
  55. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/i18n.md +165 -0
  56. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/routing-and-namespaces.md +114 -0
  57. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/security.md +122 -0
  58. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/stimulus-controllers.md +166 -0
  59. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/testing-examples.md +180 -0
  60. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/testing.md +117 -0
  61. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/tsykvas_rails_template.md +280 -0
  62. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/ui-components.md +196 -0
  63. data/lib/generators/tsykvas_rails_template/install/templates/CLAUDE.md.tt +81 -0
  64. data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/base/component/base.rb +6 -0
  65. data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/base/operation/base.rb +124 -0
  66. data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/base/operation/result.rb +56 -0
  67. data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/home/component/index.html.slim +49 -0
  68. data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/home/component/index.rb +11 -0
  69. data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/home/operation/index.rb +17 -0
  70. data/lib/generators/tsykvas_rails_template/install/templates/app/controllers/concerns/operations_methods.rb +148 -0
  71. data/lib/generators/tsykvas_rails_template/install/templates/app/controllers/home_controller.rb +10 -0
  72. data/lib/generators/tsykvas_rails_template/install/templates/app/policies/application_policy.rb +33 -0
  73. data/lib/generators/tsykvas_rails_template/install/templates/app/policies/home_policy.rb +8 -0
  74. data/lib/tasks/tsykvas.rake +11 -0
  75. data/lib/tsykvas_rails_template/probe.rb +236 -0
  76. data/lib/tsykvas_rails_template/railtie.rb +13 -0
  77. data/lib/tsykvas_rails_template/version.rb +5 -0
  78. data/lib/tsykvas_rails_template.rb +18 -0
  79. metadata +183 -0
@@ -0,0 +1,196 @@
1
+ # UI Components
2
+
3
+ For visual tokens (palette, typography, geometry, motion) see [`design-system.md`](design-system.md). This page documents the component APIs that ship with the template.
4
+
5
+ ## Buttons — always use `Base::Component::Btn`
6
+
7
+ ```ruby
8
+ render ::Base::Component::Btn.new(type: 'add', text: t('admin.users.new'), path: new_admin_user_path)
9
+ render ::Base::Component::Btn.new(type: 'save', text: t('forms.save'), submit: true)
10
+ render ::Base::Component::Btn.new(type: 'remove', text: t('admin.users.delete'), modal_target: "delete_modal_#{user.id}")
11
+ ```
12
+
13
+ Valid types: `add`, `cancel`, `check`, `edit`, `next`, `save`, `search`, `show`, `remove` (constants in `Base::Component::Btn::VALID_TYPES`). Each maps to a Bootstrap Icon and a CSS variant:
14
+
15
+ | `type` | Variant | Use |
16
+ |---|---|---|
17
+ | `add`, `save`, `next`, `search`, `cancel` | `.btn-primary` | Primary action |
18
+ | `show`, `edit` | `.btn-outline-secondary` | Secondary / informational action |
19
+ | `check` | `.btn-outline-success` | Confirm / positive action |
20
+ | `remove` | `.btn-danger` | Destructive action only |
21
+
22
+ Sizes: `XS`, `SM` / `S` (default), `M`, `L`. Use `S` for table action buttons and `L` for full-width form submits / page-header CTAs.
23
+
24
+ Other parameters: `disabled:` (Boolean), `path:` (renders `<a>`), `submit:` (renders `<button type="submit">`), `method:` (HTTP method), `data:` (hash of data attributes), `target: '_blank'`, `prefetch:` (Turbo prefetch override), `formaction:`, `modal_target:` (Bootstrap modal id without `#`).
25
+
26
+ When you need multiple buttons in a single table cell, wrap them in a flex container:
27
+
28
+ ```ruby
29
+ safe_join([
30
+ render(::Base::Component::Btn.new(type: 'show', text: t('view'), path: admin_user_path(user))),
31
+ render(::Base::Component::Btn.new(type: 'remove', text: t('delete'), path: admin_user_path(user), method: :delete))
32
+ ])
33
+ ```
34
+
35
+ See [`design-system.md`](design-system.md#buttons) for the full variant catalog (including manual classes like `.btn-secondary`, `.btn-outline-light`, `.btn-ghost` not surfaced via `Btn`).
36
+
37
+ ---
38
+
39
+ ## Tables — always use `Base::Component::Table::Table`
40
+
41
+ Extract the table itself into a dedicated ViewComponent (e.g. `Admin::User::Component::UsersTable`). Inside, build the table imperatively:
42
+
43
+ ```ruby
44
+ def call
45
+ table = Base::Component::Table::Table.new(rows: @users)
46
+
47
+ table.add_column(
48
+ header: I18n.t('admin.users.index.table.id'),
49
+ sort_field: :id,
50
+ sort_path: @sorting_path,
51
+ sort_data_type: 'number'
52
+ ) { |user| user.id.to_s }
53
+
54
+ table.add_column(
55
+ header: I18n.t('admin.users.index.table.role'),
56
+ sort_field: :role,
57
+ sort_path: @sorting_path,
58
+ sort_data_type: 'string',
59
+ stack: { to: :mobile, prefix: :header, smaller_than: :lg },
60
+ hide: { smaller_than: :md }
61
+ ) { |user| role_badge(user.role) }
62
+
63
+ table.add_column(header: I18n.t('admin.users.index.table.actions'), type: :button) do |user|
64
+ action_buttons(user)
65
+ end
66
+
67
+ render table
68
+ end
69
+ ```
70
+
71
+ Column options: `header:`, `align:`, `type:` (`:regular` or `:button`), `stack:` (collapse columns on small screens — `{ to:, prefix:, smaller_than: }`), `hide:` (`{ smaller_than: }`), `sort_field:`, `sort_path:`, `sort_data_type:` (`'number'` / `'string'`), and a `&block` returning the cell content.
72
+
73
+ Sortable columns require a `sort_path` matching the controller's index path; the operation must use `Base::Operation::Sortable#apply_sorting` with the same allowlist.
74
+
75
+ `Table#format_date(date)` is a helper that returns a locale-formatted date string (configured in `config/locales/<locale>.yml` under `date.formats.default`).
76
+
77
+ Action buttons inside cells use `.table-action-btn` (square hairline 32px) — see [`design-system.md`](design-system.md#tables).
78
+
79
+ ---
80
+
81
+ ## Title row — `Base::Component::TitleRow`
82
+
83
+ ```ruby
84
+ render ::Base::Component::TitleRow.new(
85
+ title: I18n.t('admin.users.index.title'),
86
+ back_path: admin_root_path,
87
+ back_text: I18n.t('common.back'),
88
+ divider: true
89
+ )
90
+ ```
91
+
92
+ Renders a header with optional back-link and `<hr>` divider. Configure via `Base::Component::TitleRowConfig` if you need to share configuration across pages. The `<h1>` it renders inherits typography from your design-system stylesheet.
93
+
94
+ ---
95
+
96
+ ## Stat cards (dashboard tiles)
97
+
98
+ Replacement for the colored Bootstrap `.card.bg-primary/.bg-success/.bg-warning` tiles. Two visual variants — both use neutral surfaces and rely on type/spacing for hierarchy rather than colour.
99
+
100
+ ```slim
101
+ .stat-card.stat-card--emphasis
102
+ span.stat-card-label = I18n.t('admin.dashboard.users.title')
103
+ span.stat-card-figure = users_count
104
+ p.stat-card-description = I18n.t('admin.dashboard.users.description')
105
+
106
+ .stat-card
107
+ span.stat-card-label = I18n.t('admin.dashboard.properties.title')
108
+ span.stat-card-figure = properties_count
109
+ p.stat-card-description = I18n.t('admin.dashboard.properties.description')
110
+ ```
111
+
112
+ - **`.stat-card--emphasis`** — primary-coloured surface, inverse text. Use for ONE primary metric per dashboard.
113
+ - **`.stat-card`** — neutral hairline-bordered card. Use for everything else.
114
+
115
+ Structure parts:
116
+ - `.stat-card-label` — uppercase small label.
117
+ - `.stat-card-figure` — large display number.
118
+ - `.stat-card-description` — small body line.
119
+
120
+ Lay them out inside `.row.g-4` with `.col-md-4` (Bootstrap grid). Don't put more than ~3 in a row.
121
+
122
+ ---
123
+
124
+ ## Forms
125
+
126
+ ViewComponent does not auto-mix Rails form helpers. Inside Slim templates of components, call `simple_form_for` via `helpers.`:
127
+
128
+ ```slim
129
+ = helpers.simple_form_for @property, url: crm_edit_property_path, method: :patch do |f|
130
+ = f.input :name, label: t('crm.property.edit.name'), placeholder: t('crm.property.edit.name_placeholder')
131
+ = render ::Base::Component::Btn.new(type: 'save', text: t('crm.property.edit.submit'), submit: true)
132
+ ```
133
+
134
+ For new/edit pages, follow the **Form / New / Edit** triad:
135
+
136
+ - `form.rb` + `form.html.slim` — shared form fields
137
+ - `new.rb` — subclass that sets title/breadcrumbs, embeds `Form`
138
+ - `edit.rb` — same, for edit
139
+
140
+ Two visual variants:
141
+
142
+ | Class | Use |
143
+ |---|---|
144
+ | `.form-control` | Standard forms (CRM, admin) — boxed input, hairline border, primary focus halo |
145
+ | `.form-control--underline` | Auth pages — no box, just bottom hairline that thickens on focus |
146
+
147
+ `auth.scss` automatically applies underline-style inputs to anything inside `.auth-form`. For non-auth forms use the standard `.form-control`.
148
+
149
+ See [`forms.md`](forms.md) for the full form pattern.
150
+
151
+ ---
152
+
153
+ ## Section / overline text
154
+
155
+ Utility classes for small uppercase labels (used as section titles, stat-card labels):
156
+
157
+ ```slim
158
+ span.section-label = I18n.t('ui.sections.workspace')
159
+ ```
160
+
161
+ Pair with a display heading below for editorial dashboard headers:
162
+
163
+ ```slim
164
+ .dashboard-header
165
+ span.section-label = I18n.t('ui.sections.administration')
166
+ h1.mb-0.mt-2 = I18n.t('admin.navbar.dashboard')
167
+ hr.divider
168
+ ```
169
+
170
+ Define `.section-label` as a small uppercase tracked label in your design system; `.divider` as a hairline border divider.
171
+
172
+ ---
173
+
174
+ ## Layouts
175
+
176
+ Three top-level layouts under `app/views/layouts/` (one per top-level concept namespace):
177
+
178
+ - `admin.slim` — used by `Admin::BaseController` (renders the admin sidebar)
179
+ - `crm.slim` — used by CRM controllers
180
+ - `public.slim` — public-facing layout for unauthenticated visitors
181
+ - `application.html.erb` — default Devise wrapper (sign-in / sign-up)
182
+
183
+ All layouts load the same stylesheet entry (`application.bootstrap.scss` by default — see `design-system.md`).
184
+
185
+ `Shared::Sidebar::*` and `Shared::Navbar::*` live under `app/concepts/shared/`.
186
+
187
+ ---
188
+
189
+ ## Other base components
190
+
191
+ - `Base::Component::Btn` / `Base::Component::BtnConfig` — buttons (above)
192
+ - `Base::Component::Table::Table` / `TableRow` — tables (above)
193
+ - `Base::Component::TitleRow` / `TitleRowConfig` — page headers (above)
194
+ - `Base::Component::InformationCard` (`+ InformationCardConfig`) — detail card on user-show / property-show pages. Header strip + hairline divider + monogram tile (square, no rounded avatar).
195
+ - `Shared::Navbar::Component::*` — top navigation per domain
196
+ - `Shared::Sidebar::Component::*` — sidebar per domain
@@ -0,0 +1,81 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ > **Keep this file ≤ 100 lines.** Loaded into every Claude session — brevity = token economy. Push detail into `.claude/docs/` and link from the routing table below. Content between `<!-- tsykvas-template:start ... -->` markers is gem-owned; `/tsykvas-claude` rewrites only inside the fences.
6
+
7
+ <!-- tsykvas-template:start v=0.1.0 section=routing -->
8
+ ## Read these first
9
+
10
+ For anything beyond a one-line edit, read the relevant docs in `.claude/docs/` — they hold the architecture, conventions, and review checklists. After install only `tsykvas_rails_template.md`, `forms.md`, and `companions.md` exist; the rest are generated by `/tsykvas-claude` from probe data.
11
+
12
+ | Topic | File |
13
+ |---|---|
14
+ | **Big-picture: how the gem works** | [.claude/docs/tsykvas_rails_template.md](.claude/docs/tsykvas_rails_template.md) |
15
+ | **Architecture** — Concepts Pattern, `endpoint`, `Base::Operation::Base`, sub-operations, Pundit | [.claude/docs/architecture.md](.claude/docs/architecture.md) |
16
+ | **Concepts walkthrough** — operations, components, refactor checklist | [.claude/docs/concepts-refactoring.md](.claude/docs/concepts-refactoring.md) |
17
+ | **Routing & namespaces** — domain setup, base controllers, redirect dispatch | [.claude/docs/routing-and-namespaces.md](.claude/docs/routing-and-namespaces.md) |
18
+ | **Authentication** — Devise sign-up, custom controllers, permitted params | [.claude/docs/authentication.md](.claude/docs/authentication.md) |
19
+ | **Design system** — colour tokens, typography, component catalog | [.claude/docs/design-system.md](.claude/docs/design-system.md) |
20
+ | **UI components** — `Base::Component::Btn`, table, title row, layouts | [.claude/docs/ui-components.md](.claude/docs/ui-components.md) |
21
+ | **Forms** — `simple_form_for`, Form/New/Edit triad, when to promote `<Concept>::Form` | [.claude/docs/forms.md](.claude/docs/forms.md) |
22
+ | **Stimulus controllers** — naming, `disconnect()` cleanup, Turbo events | [.claude/docs/stimulus-controllers.md](.claude/docs/stimulus-controllers.md) |
23
+ | **I18n** — full-key rule, locale parity, simple_form labels, time zone | [.claude/docs/i18n.md](.claude/docs/i18n.md) |
24
+ | **Database & migrations** — multi-DB, enum patterns, schema, seeds | [.claude/docs/database.md](.claude/docs/database.md) |
25
+ | **Background jobs** — SolidQueue placement, recurring, calling from operations | [.claude/docs/background-jobs.md](.claude/docs/background-jobs.md) |
26
+ | **Code style + anti-patterns** — frozen_string_literal, full I18n keys, git workflow | [.claude/docs/code-style.md](.claude/docs/code-style.md) |
27
+ | **Security** — Pundit enforcement, parameter filtering, CSP, Brakeman | [.claude/docs/security.md](.claude/docs/security.md) |
28
+ | **Testing** — RSpec setup, factories, mocks | [.claude/docs/testing.md](.claude/docs/testing.md) |
29
+ | **Testing examples** — operation / component / model / policy / controller | [.claude/docs/testing-examples.md](.claude/docs/testing-examples.md) |
30
+ | **Commands** — dev, tests, lint, security, deploy | [.claude/docs/commands.md](.claude/docs/commands.md) |
31
+ | **Deployment (Kamal)** — `config/deploy.yml`, secrets, hooks, scaling | [.claude/docs/deployment.md](.claude/docs/deployment.md) |
32
+ | **Companion gems** (`:companions` generator) | [.claude/docs/companions.md](.claude/docs/companions.md) |
33
+ | **Documentation standards** — when and how to add to `docs/` | [.claude/docs/documentation.md](.claude/docs/documentation.md) |
34
+ <!-- tsykvas-template:end -->
35
+
36
+ <!-- tsykvas-template:start v=0.1.0 section=tldr -->
37
+ ## TL;DR
38
+
39
+ - **Stack**: Rails 8, Ruby 3.x, PostgreSQL, Slim + ViewComponent, Hotwire (Turbo + Stimulus), Devise + Pundit, simple_form, solid_queue/cache/cable, Kamal.
40
+ - **Concepts Pattern**: every feature lives in `app/concepts/<namespace>/<feature>/{operation,component}/`. Controllers are one-liner `endpoint OperationClass, ComponentClass` calls.
41
+ - **Operation contract** (`Base::Operation::Base`): override `perform!(params:, current_user:)`; set `self.model`, `self.redirect_path`, `notice(...)`; **always call** `authorize!` / `policy_scope` / `authorize_and_save!` / `skip_authorize` (enforced by `OperationsMethods#check_authorization_is_called`).
42
+ - **Component contract** (`Base::Component::Base`): pure presentation, constructor kwargs use **specific data names** — `initialize(events:)`, never `initialize(model:)`. Pair each `.rb` with adjacent `.html.slim`.
43
+ - **Forms**: keep `_params` inline for simple CRUD; promote to `<Concept>::Form` when the form has virtual attributes, pre-assignment cleanup, sub-operation calls, or aggregates multiple AR records.
44
+ - **Quick commands**: `bin/dev`, `bundle exec rspec`, `bin/rubocop`, `bin/brakeman --no-pager`, `bin/rails zeitwerk:check`.
45
+ <!-- tsykvas-template:end -->
46
+
47
+ <!-- tsykvas-template:start v=0.1.0 section=tree -->
48
+ ## What lives where
49
+
50
+ ```
51
+ app/
52
+ ├── concepts/ # Feature code (operations + components + slim)
53
+ │ ├── <namespace>/<feature>/ # e.g. crm/property/, admin/user/
54
+ │ ├── shared/ # Cross-domain UI (navbar, sidebar, etc.)
55
+ │ └── base/ # Base::Operation::Base, Base::Component::Base, Btn, Table, ...
56
+ ├── controllers/
57
+ │ ├── application_controller.rb # Pundit + OperationsMethods + redirect dispatch
58
+ │ ├── concerns/operations_methods.rb # The `endpoint` helper
59
+ │ ├── home_controller.rb # Landing page scaffolded by install
60
+ │ └── <namespace>/base_controller.rb
61
+ ├── models/ # AR models — validations + associations only
62
+ ├── policies/ # Pundit; default-deny ApplicationPolicy
63
+ ├── jobs/ # SolidQueue Active Jobs
64
+ ├── javascript/controllers/ # Stimulus
65
+ ├── views/layouts/ # Per-domain layout files (admin / crm / ...)
66
+ └── views/devise/ # Devise overrides (when added)
67
+ config/
68
+ ├── routes.rb # `root "home#index"` after install
69
+ ├── deploy.yml # Kamal
70
+ ├── queue.yml + recurring.yml # SolidQueue
71
+ ├── cache.yml + cable.yml # SolidCache + SolidCable
72
+ └── locales/*.yml
73
+ db/
74
+ ├── schema.rb # Primary
75
+ └── {cache,queue,cable}_schema.rb # Solid* secondary DBs
76
+ spec/ # Mirrors app/ structure
77
+ .claude/{docs,commands,agents}/ # This guide + commands + subagents
78
+ ```
79
+ <!-- tsykvas-template:end -->
80
+
81
+ If you change anything under `app/concepts/base/`, `app/controllers/concerns/operations_methods.rb`, `app/controllers/application_controller.rb`, or any `app/policies/**/base_policy.rb`, run `/update-rules` so the docs above stay in sync.
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "view_component"
4
+
5
+ class Base::Component::Base < ViewComponent::Base
6
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pundit"
4
+
5
+ class Base::Operation::Base
6
+ attr_accessor :result
7
+
8
+ def self.call(**args)
9
+ ops = new(**args).tap(&:call)
10
+ ops.result
11
+ end
12
+
13
+ def initialize(**attrs)
14
+ @attrs = attrs
15
+ @result = ::Base::Operation::Result.new
16
+ end
17
+
18
+ def call
19
+ perform!(**@attrs)
20
+ copy_errors_from_result_to_model
21
+ @result
22
+ rescue ActiveRecord::RecordInvalid => e
23
+ add_errors e.record&.errors
24
+ copy_errors_from_result_to_model
25
+ @result
26
+ end
27
+
28
+ private
29
+
30
+ def copy_errors_from_result_to_model
31
+ return if model.nil? || !model.respond_to?(:errors)
32
+
33
+ result.errors[:base].each do |message|
34
+ model.errors.add(:base, message) unless model.errors[:base].include?(message)
35
+ end
36
+ end
37
+
38
+ def notice(text, level: :notice)
39
+ @result[:notice] = {
40
+ text:,
41
+ level:
42
+ }
43
+ end
44
+
45
+ def redirect_path=(path)
46
+ @result[:redirect_path] = path
47
+ end
48
+
49
+ def redirect_path
50
+ @result[:redirect_path]
51
+ end
52
+
53
+ def model=(model)
54
+ @result[:model] = model
55
+ end
56
+
57
+ def model
58
+ @result[:model]
59
+ end
60
+
61
+ def add_error(key, message)
62
+ @result.errors.add :base, key, message:
63
+ end
64
+
65
+ def add_errors(from)
66
+ return if from.nil?
67
+
68
+ from.each do |error|
69
+ from[error.attribute].each do |error_msg|
70
+ @result.errors.add(error.attribute, error_msg)
71
+ end
72
+ end
73
+ end
74
+
75
+ def invalid!
76
+ @result.invalid!
77
+ end
78
+
79
+ ### Run sub operations ###
80
+
81
+ def run_operation(operation_class, parameters)
82
+ manually_handle_errors = parameters[:manually_handle_errors].present?
83
+ parameters.except!(:manually_handle_errors)
84
+ run_result = operation_class.new(**parameters).tap(&:call).result
85
+ result.sub_results << run_result
86
+ if !manually_handle_errors && run_result.present? && run_result.failure?
87
+ add_errors run_result.errors
88
+ raise ActiveRecord::RecordInvalid
89
+ end
90
+ run_result
91
+ end
92
+
93
+ ### Authorization methods ###
94
+
95
+ def authorize!(record, query, policy: Pundit.policy!(@attrs[:current_user], record), fail_message: nil)
96
+ if policy.public_send(query)
97
+ @result[:pundit] = true
98
+ return
99
+ end
100
+
101
+ raise Pundit::NotAuthorizedError, fail_message if fail_message.present?
102
+
103
+ raise Pundit::NotAuthorizedError, query:, record:, policy:
104
+ end
105
+
106
+ def policy_scope(scope)
107
+ @result[:pundit_scope] = true
108
+ Pundit.policy_scope!(@attrs[:current_user], scope)
109
+ end
110
+
111
+ def skip_authorize
112
+ @result[:pundit] = true
113
+ end
114
+
115
+ def skip_policy_scope
116
+ @result[:pundit_scope] = true
117
+ end
118
+
119
+ def authorize_and_save!(auth_method = nil)
120
+ auth_method ||= model.new_record? ? :create? : :update?
121
+ authorize! model, auth_method
122
+ model.save!
123
+ end
124
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Base::Operation::Result
4
+ include ActiveModel::Validations
5
+
6
+ def initialize
7
+ @attrs = {}
8
+ @forced_invalid = false
9
+ end
10
+
11
+ delegate :[], :[]=, :fetch, to: :@attrs
12
+
13
+ def model
14
+ @attrs[:model]
15
+ end
16
+
17
+ def redirect_path
18
+ @attrs[:redirect_path]
19
+ end
20
+
21
+ def sub_results
22
+ @attrs[:sub_results] ||= []
23
+ end
24
+
25
+ def success?
26
+ return false unless !@forced_invalid && errors.empty?
27
+ return false unless model.nil? || !model.respond_to?(:errors) ? true : model.errors.empty?
28
+
29
+ sub_results.all?(&:success?)
30
+ end
31
+
32
+ def failure?
33
+ !success?
34
+ end
35
+
36
+ def invalid!
37
+ @forced_invalid = true
38
+ end
39
+
40
+ def error_message
41
+ errors[:base].join(" ")
42
+ end
43
+
44
+ def all_error_messages
45
+ errors.map(&:message)
46
+ end
47
+
48
+ # Translated message for use in flash notices and alerts.
49
+ def message
50
+ @attrs.dig(:notice, :text)
51
+ end
52
+
53
+ def message_level
54
+ @attrs.dig(:notice, :level)
55
+ end
56
+ end
@@ -0,0 +1,49 @@
1
+ .container.py-5
2
+ .row.justify-content-center
3
+ .col-lg-8
4
+ .card.shadow-sm
5
+ .card-body.p-4
6
+ h1.card-title.h2.mb-3= message
7
+
8
+ .alert.alert-success.d-flex.align-items-center.mb-4 role="alert"
9
+ span.badge.bg-success.me-2 OK
10
+ span Bootstrap 5.3 is installed and configured (this card, the alert, the modal button — all Bootstrap).
11
+
12
+ h2.h5.text-muted Next steps
13
+
14
+ ul.list-group.list-group-flush.mb-4
15
+ li.list-group-item
16
+ | Run
17
+ =< " "
18
+ code bin/rails g tsykvas_rails_template:companions
19
+ =< " "
20
+ | to add the recommended dev/test gems.
21
+ li.list-group-item
22
+ | Run
23
+ =< " "
24
+ code bin/rails g tsykvas_rails_template:concept Foo
25
+ =< " "
26
+ | to scaffold a new concept.
27
+ li.list-group-item
28
+ | Open Claude Code and run <code>/tsykvas-claude</code> to tailor docs to your stack.
29
+
30
+ .d-flex.flex-wrap.gap-2
31
+ a.btn.btn-primary href="https://github.com/tsykvas/tsykvas_rails_template" target="_blank" rel="noopener"
32
+ | Documentation
33
+ button.btn.btn-outline-secondary type="button" data-bs-toggle="modal" data-bs-target="#tsykvasWelcomeModal"
34
+ | Open a Bootstrap modal
35
+
36
+ / Bootstrap modal — opening it proves the JS bundle (window.bootstrap) loaded.
37
+ #tsykvasWelcomeModal.modal.fade tabindex="-1" aria-labelledby="tsykvasWelcomeModalLabel" aria-hidden="true"
38
+ .modal-dialog.modal-dialog-centered
39
+ .modal-content
40
+ .modal-header
41
+ h5#tsykvasWelcomeModalLabel.modal-title Bootstrap modal demo
42
+ .modal-body
43
+ p
44
+ | If this opened, the Bootstrap JS bundle is wired through
45
+ =< " "
46
+ code app/javascript/application.js
47
+ | .
48
+ .modal-footer
49
+ button.btn.btn-secondary type="button" data-bs-dismiss="modal" Close
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Home::Component::Index < ::Base::Component::Base
4
+ def initialize(message:)
5
+ @message = message
6
+ end
7
+
8
+ private
9
+
10
+ attr_reader :message
11
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ostruct"
4
+
5
+ # Canonical example operation. Replace with real logic when the home page
6
+ # becomes more than a placeholder. Demonstrates the three required calls:
7
+ # `authorize!`, `skip_policy_scope`, and assigning `self.model`.
8
+ class Home::Operation::Index < ::Base::Operation::Base
9
+ def perform!(params:, current_user:)
10
+ authorize! :home, :index?
11
+ skip_policy_scope
12
+
13
+ self.model = OpenStruct.new(
14
+ message: "Welcome — your app is wired up with tsykvas_rails_template."
15
+ )
16
+ end
17
+ end