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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +200 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +589 -0
- data/Rakefile +17 -0
- data/lib/generators/tsykvas_rails_template/companions/companions_generator.rb +273 -0
- data/lib/generators/tsykvas_rails_template/concept/concept_generator.rb +145 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/component/edit.html.slim.tt +5 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/component/edit.rb.tt +11 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/component/index.html.slim.tt +5 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/component/index.rb.tt +11 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/component/new.html.slim.tt +5 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/component/new.rb.tt +11 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/component/show.html.slim.tt +4 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/component/show.rb.tt +11 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/controller.rb.tt +45 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/operation/create.rb.tt +31 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/operation/destroy.rb.tt +13 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/operation/edit.rb.tt +10 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/operation/index.rb.tt +9 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/operation/new.rb.tt +10 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/operation/show.rb.tt +10 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/operation/update.rb.tt +31 -0
- data/lib/generators/tsykvas_rails_template/install/bootstrap_installer.rb +225 -0
- data/lib/generators/tsykvas_rails_template/install/install_generator.rb +298 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/agents/buddy.md +157 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/agents/code-reviewer.md +117 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/agents/security-reviewer.md +113 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/agents/tech-lead.md +150 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/check.md +51 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/code-review.md +60 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/docs-create.md +102 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/pr-review.md +81 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/pushit.md +160 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/refactor.md +132 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/task-sum.md +47 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/tests.md +67 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/tsykvas-claude.md +262 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/update-docs.md +78 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/update-rules.md +102 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/update-tests.md +135 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/architecture.md +315 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/authentication.md +96 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/background-jobs.md +135 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/code-style.md +101 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/commands.md +34 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/companions.md +128 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/concepts-refactoring.md +194 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/database.md +135 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/deployment.md +138 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/design-system.md +322 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/documentation.md +89 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/forms.md +174 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/i18n.md +165 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/routing-and-namespaces.md +114 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/security.md +122 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/stimulus-controllers.md +166 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/testing-examples.md +180 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/testing.md +117 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/tsykvas_rails_template.md +280 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/ui-components.md +196 -0
- data/lib/generators/tsykvas_rails_template/install/templates/CLAUDE.md.tt +81 -0
- data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/base/component/base.rb +6 -0
- data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/base/operation/base.rb +124 -0
- data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/base/operation/result.rb +56 -0
- data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/home/component/index.html.slim +49 -0
- data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/home/component/index.rb +11 -0
- data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/home/operation/index.rb +17 -0
- data/lib/generators/tsykvas_rails_template/install/templates/app/controllers/concerns/operations_methods.rb +148 -0
- data/lib/generators/tsykvas_rails_template/install/templates/app/controllers/home_controller.rb +10 -0
- data/lib/generators/tsykvas_rails_template/install/templates/app/policies/application_policy.rb +33 -0
- data/lib/generators/tsykvas_rails_template/install/templates/app/policies/home_policy.rb +8 -0
- data/lib/tasks/tsykvas.rake +11 -0
- data/lib/tsykvas_rails_template/probe.rb +236 -0
- data/lib/tsykvas_rails_template/railtie.rb +13 -0
- data/lib/tsykvas_rails_template/version.rb +5 -0
- data/lib/tsykvas_rails_template.rb +18 -0
- 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.
|
data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/base/operation/base.rb
ADDED
|
@@ -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
|
data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/base/operation/result.rb
ADDED
|
@@ -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
|
data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/home/operation/index.rb
ADDED
|
@@ -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
|