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,322 @@
|
|
|
1
|
+
# Design System
|
|
2
|
+
|
|
3
|
+
> **This is a Bootstrap-default starting point.** Customise tokens, typography, and component patterns to match your brand. The structure below shows what to document so future contributors (humans and AI) know the rules.
|
|
4
|
+
|
|
5
|
+
The shipped install wires `bootstrap` 5.3 + `dartsass-rails` and pre-compiles `app/assets/stylesheets/application.bootstrap.scss` → `app/assets/builds/application.css`. All tokens below come from Bootstrap's CSS custom properties unless your app overrides them.
|
|
6
|
+
|
|
7
|
+
If you change SCSS in `app/assets/stylesheets/` or add a new visual primitive under `app/concepts/base/component/`, update this file in the same PR.
|
|
8
|
+
|
|
9
|
+
## Principles
|
|
10
|
+
|
|
11
|
+
1. **Tokens before values.** Use `--bs-primary` / `$primary`, never hex literals.
|
|
12
|
+
2. **Type over colour.** Hierarchy comes from spacing and type weight first, colour second.
|
|
13
|
+
3. **One destructive cue.** `--bs-danger` (or your override) is reserved for destructive / single-CTA contexts. Don't sprinkle it.
|
|
14
|
+
4. **Components beat CSS.** New visual rules belong in `Base::Component::*` (or a sibling), not in inline class soup.
|
|
15
|
+
5. **One stylesheet entrypoint.** Everything imports from `application.bootstrap.scss`. No per-feature global CSS files.
|
|
16
|
+
|
|
17
|
+
## Tokens
|
|
18
|
+
|
|
19
|
+
### Palette — light mode (Bootstrap defaults)
|
|
20
|
+
|
|
21
|
+
```scss
|
|
22
|
+
$primary: #0d6efd; // --bs-primary
|
|
23
|
+
$secondary: #6c757d; // --bs-secondary
|
|
24
|
+
$success: #198754; // --bs-success
|
|
25
|
+
$info: #0dcaf0; // --bs-info
|
|
26
|
+
$warning: #ffc107; // --bs-warning
|
|
27
|
+
$danger: #dc3545; // --bs-danger
|
|
28
|
+
$light: #f8f9fa; // --bs-light
|
|
29
|
+
$dark: #212529; // --bs-dark
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Surface tokens (page background, body text, hairline borders):
|
|
33
|
+
|
|
34
|
+
```scss
|
|
35
|
+
$body-bg: #fff;
|
|
36
|
+
$body-color: #212529;
|
|
37
|
+
$border-color: #dee2e6;
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
To rebrand: override the SCSS variables **above** the `@import "bootstrap"` line in `application.bootstrap.scss`. Bootstrap's `_variables.scss` uses `!default`, so your override wins.
|
|
41
|
+
|
|
42
|
+
```scss
|
|
43
|
+
// application.bootstrap.scss
|
|
44
|
+
$primary: #1F3D2C; // your brand primary
|
|
45
|
+
$body-bg: #F4EFE6; // your page background
|
|
46
|
+
@import "bootstrap";
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Palette — dark mode
|
|
50
|
+
|
|
51
|
+
Bootstrap 5.3 ships `data-bs-theme="dark"` switching via attribute. Toggle via a Stimulus controller (see `stimulus-controllers.md` — there's a `theme_controller.js` template that mirrors `data-bs-theme` on `<body>` from a `localStorage` toggle).
|
|
52
|
+
|
|
53
|
+
To customise dark-mode tokens, override `[data-bs-theme="dark"]` after the import:
|
|
54
|
+
|
|
55
|
+
```scss
|
|
56
|
+
@import "bootstrap";
|
|
57
|
+
|
|
58
|
+
[data-bs-theme="dark"] {
|
|
59
|
+
--bs-body-bg: #1a1a1a;
|
|
60
|
+
--bs-body-color: #e9ecef;
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Typography
|
|
65
|
+
|
|
66
|
+
Bootstrap default: system font stack (`-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, ...`). Sizes:
|
|
67
|
+
|
|
68
|
+
| Token | Default | Use |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| `$h1-font-size` | 2.5rem | Page hero headings |
|
|
71
|
+
| `$h2-font-size` | 2rem | Section headings |
|
|
72
|
+
| `$h3-font-size` | 1.75rem | Sub-section headings |
|
|
73
|
+
| `$h4-font-size` | 1.5rem | Card / dashboard tile headings |
|
|
74
|
+
| `$h5-font-size` | 1.25rem | Form section headings |
|
|
75
|
+
| `$h6-font-size` | 1rem | Inline labels (caps + tracking) |
|
|
76
|
+
| `$font-size-base` | 1rem | Body |
|
|
77
|
+
| `$font-size-sm` | 0.875rem | Helper text, table headers |
|
|
78
|
+
|
|
79
|
+
To swap fonts (e.g. a Google Font for headings, Inter for body):
|
|
80
|
+
|
|
81
|
+
```scss
|
|
82
|
+
$font-family-sans-serif: "Inter", system-ui, -apple-system, sans-serif;
|
|
83
|
+
$headings-font-family: "Playfair Display", Georgia, serif; // optional
|
|
84
|
+
@import "bootstrap";
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Then add `<link>` tags or `@import url(...)` in your stylesheet. Document the exact fonts here once chosen.
|
|
88
|
+
|
|
89
|
+
### Geometry
|
|
90
|
+
|
|
91
|
+
```scss
|
|
92
|
+
$border-radius: 0.375rem; // default cards, inputs, buttons
|
|
93
|
+
$border-radius-sm: 0.25rem; // badges, small chips
|
|
94
|
+
$border-radius-lg: 0.5rem; // modals, alerts
|
|
95
|
+
$border-radius-xl: 1rem; // hero-section panels (rare)
|
|
96
|
+
$border-radius-pill: 50rem; // pills (avoid for primary CTAs)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Spacing scale — Bootstrap's `0` through `5`:
|
|
100
|
+
|
|
101
|
+
| Token | Value | Common use |
|
|
102
|
+
|---|---|---|
|
|
103
|
+
| `$spacer` | 1rem | Base unit |
|
|
104
|
+
| `0` | 0 | Reset |
|
|
105
|
+
| `1` | 0.25rem | Inline gap |
|
|
106
|
+
| `2` | 0.5rem | Component-internal spacing |
|
|
107
|
+
| `3` | 1rem | Card body padding |
|
|
108
|
+
| `4` | 1.5rem | Section spacing |
|
|
109
|
+
| `5` | 3rem | Page-level rhythm |
|
|
110
|
+
|
|
111
|
+
Use the utility classes (`.p-3`, `.mb-4`, `.gap-2`) — don't hand-roll margins in component SCSS.
|
|
112
|
+
|
|
113
|
+
### Motion
|
|
114
|
+
|
|
115
|
+
Bootstrap ships `--bs-transition` and component-specific durations. Defaults work for most cases; customise via:
|
|
116
|
+
|
|
117
|
+
```scss
|
|
118
|
+
$transition-base: all 0.2s ease-in-out;
|
|
119
|
+
$transition-fade: opacity 0.15s linear;
|
|
120
|
+
$transition-collapse: height 0.35s ease;
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Reduce-motion: respect `@media (prefers-reduced-motion: reduce)` in custom animations — Bootstrap already does this for built-ins.
|
|
124
|
+
|
|
125
|
+
## Component catalog
|
|
126
|
+
|
|
127
|
+
### Buttons (`Base::Component::Btn`)
|
|
128
|
+
|
|
129
|
+
Wraps Bootstrap `<button>` / `<a>` with a typed API. See [`ui-components.md`](ui-components.md#buttons-always-use-basecomponentbtn) for full props. Visual rules:
|
|
130
|
+
|
|
131
|
+
- One `.btn-primary` per page (primary action).
|
|
132
|
+
- Use `.btn-outline-secondary` for secondary actions.
|
|
133
|
+
- Reserve `.btn-danger` for destructive operations.
|
|
134
|
+
- Avoid pill-shaped buttons (`$border-radius-pill`) for primary CTAs.
|
|
135
|
+
|
|
136
|
+
Sizes via `$btn-padding-y-{sm,lg}` / `$btn-font-size-{sm,lg}`. Stick to the `XS` / `SM` / `M` / `L` mapping in `Base::Component::Btn`.
|
|
137
|
+
|
|
138
|
+
### Stat cards (dashboard tiles)
|
|
139
|
+
|
|
140
|
+
```scss
|
|
141
|
+
.stat-card {
|
|
142
|
+
background: var(--bs-body-bg);
|
|
143
|
+
border: 1px solid var(--bs-border-color);
|
|
144
|
+
border-radius: var(--bs-border-radius);
|
|
145
|
+
padding: 1.5rem;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.stat-card--emphasis {
|
|
149
|
+
background: var(--bs-primary);
|
|
150
|
+
color: var(--bs-white);
|
|
151
|
+
border-color: var(--bs-primary);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.stat-card-label {
|
|
155
|
+
text-transform: uppercase;
|
|
156
|
+
letter-spacing: 0.18em;
|
|
157
|
+
font-size: 0.75rem;
|
|
158
|
+
color: var(--bs-secondary-color);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.stat-card-figure {
|
|
162
|
+
font-size: 3rem;
|
|
163
|
+
font-weight: 600;
|
|
164
|
+
display: block;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.stat-card-description {
|
|
168
|
+
font-size: 0.875rem;
|
|
169
|
+
color: var(--bs-secondary-color);
|
|
170
|
+
margin: 0;
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Use one `.stat-card--emphasis` per dashboard for the primary metric; `.stat-card` for everything else.
|
|
175
|
+
|
|
176
|
+
### Information card (`Base::Component::InformationCard`)
|
|
177
|
+
|
|
178
|
+
Detail card pattern used on user-show / property-show pages. Composition:
|
|
179
|
+
|
|
180
|
+
- Header strip — `padding: 1rem 1.5rem; background: var(--bs-light);`
|
|
181
|
+
- Monogram tile — square 48px hairline-bordered, no rounded avatar
|
|
182
|
+
- `.section-title` rendered as small uppercase label + hairline divider
|
|
183
|
+
|
|
184
|
+
### Cards (general)
|
|
185
|
+
|
|
186
|
+
Bootstrap's `.card` defaults are fine. Augment with `.card.shadow-sm` for elevated cards (e.g. the gem's Home example). Don't reach for `box-shadow: 0 4px 12px ...` per-component — use the shadow tokens.
|
|
187
|
+
|
|
188
|
+
```scss
|
|
189
|
+
$box-shadow-sm: 0 .125rem .25rem rgba($black, .075);
|
|
190
|
+
$box-shadow: 0 .5rem 1rem rgba($black, .15);
|
|
191
|
+
$box-shadow-lg: 0 1rem 3rem rgba($black, .175);
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Tables (`Base::Component::Table::Table`)
|
|
195
|
+
|
|
196
|
+
Hairline tables, no zebra striping by default. Action buttons inside cells use `.table-action-btn` (32px square hairline). Wrap inside `.table-responsive` for mobile collapse.
|
|
197
|
+
|
|
198
|
+
### Forms
|
|
199
|
+
|
|
200
|
+
Two visual variants:
|
|
201
|
+
|
|
202
|
+
- `.form-control` — Bootstrap default. Boxed input, hairline border, primary focus halo.
|
|
203
|
+
- `.form-control--underline` — auth-page variant: no box, just a bottom hairline that thickens on focus.
|
|
204
|
+
|
|
205
|
+
```scss
|
|
206
|
+
.form-control--underline {
|
|
207
|
+
border: 0;
|
|
208
|
+
border-bottom: 1px solid var(--bs-border-color);
|
|
209
|
+
border-radius: 0;
|
|
210
|
+
padding-left: 0;
|
|
211
|
+
padding-right: 0;
|
|
212
|
+
|
|
213
|
+
&:focus {
|
|
214
|
+
box-shadow: none;
|
|
215
|
+
border-bottom-width: 2px;
|
|
216
|
+
border-bottom-color: var(--bs-primary);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Apply the underline variant via a wrapping `.auth-form` selector — see `auth.scss`.
|
|
222
|
+
|
|
223
|
+
### Sidebar (`Shared::Sidebar::Component`)
|
|
224
|
+
|
|
225
|
+
Full-height left rail, hairline divider on the right edge. Nav links use `display: flex; width: 100%;` so the active/hover bar spans the full width — don't override.
|
|
226
|
+
|
|
227
|
+
### Navbar
|
|
228
|
+
|
|
229
|
+
Top bar, hairline divider on the bottom edge. The brand-lockup composition (logo + wordmark) lives in `Shared::Navbar::*`.
|
|
230
|
+
|
|
231
|
+
### Auth (sign-in / sign-up / edit profile)
|
|
232
|
+
|
|
233
|
+
Centred card, no sidebar/navbar. Uses `.form-control--underline` inputs and a single `.btn-primary` submit. Sign-up form fields belong in `app/views/devise/registrations/new.html.slim`.
|
|
234
|
+
|
|
235
|
+
## Utility classes
|
|
236
|
+
|
|
237
|
+
### Typography
|
|
238
|
+
|
|
239
|
+
- `.section-label` — small uppercase tracked label (use for stat-card labels, navigational micro-headers).
|
|
240
|
+
- `.caps` — looser uppercase tracking (0.12em).
|
|
241
|
+
- `.figure` / `.figure-lg` — large display numbers.
|
|
242
|
+
- `.display-1` through `.display-6` — Bootstrap's display headings.
|
|
243
|
+
|
|
244
|
+
### Color
|
|
245
|
+
|
|
246
|
+
Use Bootstrap's `.text-{primary,secondary,muted,success,warning,danger,info,light,dark}` and matching `.bg-*`. Don't introduce new colour utilities for one-offs — reach for a token override instead.
|
|
247
|
+
|
|
248
|
+
### Layout
|
|
249
|
+
|
|
250
|
+
- `.container` / `.container-fluid` — Bootstrap's responsive containers.
|
|
251
|
+
- `.row.g-{0..5}` — flex grid with gap.
|
|
252
|
+
- `.col-{xs,sm,md,lg,xl}-{1..12}` — column spans.
|
|
253
|
+
- `.d-flex` / `.gap-{1..5}` / `.justify-content-*` / `.align-items-*` — flex utilities.
|
|
254
|
+
|
|
255
|
+
## I18n integration
|
|
256
|
+
|
|
257
|
+
Brand-/UI-only strings live under a dedicated `ui:` namespace (see `i18n.md`):
|
|
258
|
+
|
|
259
|
+
```yaml
|
|
260
|
+
en:
|
|
261
|
+
ui:
|
|
262
|
+
sections:
|
|
263
|
+
workspace: "Workspace"
|
|
264
|
+
account: "Account"
|
|
265
|
+
administration: "Administration"
|
|
266
|
+
brand:
|
|
267
|
+
tagline: "..."
|
|
268
|
+
year: "2026"
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Reference via `I18n.t('ui.sections.workspace')`. Keep typographic constants identical across locales (`year: "2026"` is the same in every locale).
|
|
272
|
+
|
|
273
|
+
## Antipatterns
|
|
274
|
+
|
|
275
|
+
### Color
|
|
276
|
+
|
|
277
|
+
- ❌ Hex literals scattered through component CSS → ✅ tokens (`var(--bs-primary)`, `$primary`).
|
|
278
|
+
- ❌ Coloured Bootstrap card backgrounds (`.card.bg-primary` for stats) → ✅ `.stat-card` / `.stat-card--emphasis`.
|
|
279
|
+
- ❌ Using `--bs-danger` for non-destructive UI just because it stands out → ✅ reserve danger for destructive/CTA only.
|
|
280
|
+
|
|
281
|
+
### Geometry
|
|
282
|
+
|
|
283
|
+
- ❌ `border-radius: 9999px` (pill shape) on primary action buttons → ✅ `$border-radius` (default).
|
|
284
|
+
- ❌ Custom `box-shadow: 0 4px 12px ...` per component → ✅ `$box-shadow-sm` / `$box-shadow`.
|
|
285
|
+
|
|
286
|
+
### Typography
|
|
287
|
+
|
|
288
|
+
- ❌ Inline `font-family` overrides on individual elements → ✅ set once in `$font-family-sans-serif` / `$headings-font-family`.
|
|
289
|
+
- ❌ Hardcoded `text-uppercase` + inline `letter-spacing` → ✅ `.section-label` / `.caps` utility class.
|
|
290
|
+
- ❌ Inline font-size literals (`font-size: 14px`) → ✅ `$font-size-sm` token.
|
|
291
|
+
|
|
292
|
+
### Components
|
|
293
|
+
|
|
294
|
+
- ❌ Hand-rolled buttons / cards / modals → ✅ use `Base::Component::*` or Bootstrap directly.
|
|
295
|
+
- ❌ Class soup (`class="bg-primary text-white text-center fw-bold p-3 rounded shadow"`) → ✅ extract a small SCSS utility class or component partial.
|
|
296
|
+
- ❌ `Base::Component::Btn` calls without leading `::` from inside another component → ✅ `::Base::Component::Btn.new(...)` (constant lookup).
|
|
297
|
+
|
|
298
|
+
### Layout / interaction
|
|
299
|
+
|
|
300
|
+
- ❌ Per-page custom margins → ✅ Bootstrap spacing utilities (`.mb-4`, `.gap-3`).
|
|
301
|
+
- ❌ Sidebar `.nav-link` without `display: flex; width: 100%` → ✅ active bar must span full sidebar width.
|
|
302
|
+
- ❌ `active_nav_class('/crm')` for a dashboard link (matches every CRM page) → ✅ `active_nav_class('/crm', '/crm/dashboard', exact: true)`.
|
|
303
|
+
|
|
304
|
+
## Adding new tokens or components
|
|
305
|
+
|
|
306
|
+
1. **Add the token to `application.bootstrap.scss`** above the `@import "bootstrap"` line.
|
|
307
|
+
2. **Document it in this file** under the appropriate section.
|
|
308
|
+
3. **If it's component-shaped**, create a sibling under `Base::Component::*` and document the API in [`ui-components.md`](ui-components.md).
|
|
309
|
+
4. **Run `bin/rails dartsass:build`** locally to verify the SCSS compiles without warnings.
|
|
310
|
+
5. **Update the antipatterns** if the new token replaces an existing pattern (so future contributors don't reach for the old one).
|
|
311
|
+
|
|
312
|
+
## File map
|
|
313
|
+
|
|
314
|
+
```
|
|
315
|
+
app/assets/stylesheets/
|
|
316
|
+
application.bootstrap.scss # entry point — your overrides + @import "bootstrap"
|
|
317
|
+
components/ # per-component SCSS partials (stat-card, info-card, ...)
|
|
318
|
+
utilities/ # custom utility classes (.section-label, .caps, ...)
|
|
319
|
+
layouts/ # per-layout SCSS (sidebar, navbar, auth, ...)
|
|
320
|
+
|
|
321
|
+
app/concepts/base/component/ # Ruby ViewComponents (Btn, Table, TitleRow, InformationCard, ...)
|
|
322
|
+
```
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Documentation Standards
|
|
2
|
+
|
|
3
|
+
## Directory structure
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
docs/
|
|
7
|
+
├── <domain>/ # Feature domain directory (admin, crm, screener, ...)
|
|
8
|
+
│ ├── overview.md # Domain-level overview (entry points, key concepts)
|
|
9
|
+
│ ├── operations.md # Operations and their flows
|
|
10
|
+
│ ├── components.md # ViewComponents in this domain
|
|
11
|
+
│ └── <specific_feature>.md # Specific feature docs
|
|
12
|
+
└── <standalone_topic>.md # Cross-domain infrastructure topics
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### Existing domains
|
|
16
|
+
|
|
17
|
+
`admin`, `crm`, `screener`, `shared`, `base`. New documentation should slot into one of these or sit at the root if cross-cutting.
|
|
18
|
+
|
|
19
|
+
### Placement rules
|
|
20
|
+
|
|
21
|
+
- Feature belongs to an existing domain → put in that domain's directory
|
|
22
|
+
- Feature crosses multiple domains → primary domain, reference from others
|
|
23
|
+
- Standalone infrastructure → root of `docs/`
|
|
24
|
+
|
|
25
|
+
## When to create documentation
|
|
26
|
+
|
|
27
|
+
- New feature with non-obvious flow (multi-file, jobs, API calls)
|
|
28
|
+
- Bug fix that reveals non-obvious behavior worth remembering
|
|
29
|
+
- Flow that involves race conditions, caching, or async processing
|
|
30
|
+
- Integration with an external service
|
|
31
|
+
|
|
32
|
+
## When NOT to create documentation
|
|
33
|
+
|
|
34
|
+
- Simple CRUD operations that follow the Concepts Pattern
|
|
35
|
+
- Trivial UI changes
|
|
36
|
+
- Something already well-documented in code
|
|
37
|
+
|
|
38
|
+
## Document template
|
|
39
|
+
|
|
40
|
+
```markdown
|
|
41
|
+
# <Feature Name>
|
|
42
|
+
|
|
43
|
+
> Last updated: YYYY-MM-DD
|
|
44
|
+
|
|
45
|
+
<1–2 sentence summary.>
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Overview
|
|
50
|
+
|
|
51
|
+
<What this feature does, why it exists, who uses it.>
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Flow
|
|
56
|
+
|
|
57
|
+
<ASCII diagram or numbered steps from trigger to result.>
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
Trigger (UI / Job / API)
|
|
61
|
+
└─ Controller#action
|
|
62
|
+
└─ Operation#perform!
|
|
63
|
+
└─ Service / Job / etc.
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Key Files
|
|
69
|
+
|
|
70
|
+
| File | Role |
|
|
71
|
+
|------|------|
|
|
72
|
+
| `app/path/to/file.rb` | What it does |
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Potential Issues
|
|
77
|
+
|
|
78
|
+
| # | Location | Description | Severity |
|
|
79
|
+
|---|----------|-------------|----------|
|
|
80
|
+
| 1 | `Class#method` | Description | Low/Medium/High |
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Writing rules
|
|
84
|
+
|
|
85
|
+
- **Audience**: developers on this team, not end users.
|
|
86
|
+
- **Be specific**: use file paths, method names, class names.
|
|
87
|
+
- **Include the full flow**: don't skip steps.
|
|
88
|
+
- **Document behavior, not implementation**: focus on what happens, not line-by-line code.
|
|
89
|
+
- **Language**: English.
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Forms — when to introduce a Form object
|
|
2
|
+
|
|
3
|
+
The default flow for simple CRUD is to permit params directly inside the
|
|
4
|
+
operation:
|
|
5
|
+
|
|
6
|
+
```ruby
|
|
7
|
+
class Crm::Property::Operation::Create < ::Base::Operation::Base
|
|
8
|
+
def perform!(params:, current_user:)
|
|
9
|
+
property = Crm::Property.new
|
|
10
|
+
authorize! property, :create?
|
|
11
|
+
|
|
12
|
+
property.assign_attributes(property_params(params))
|
|
13
|
+
property.save!
|
|
14
|
+
|
|
15
|
+
self.model = property
|
|
16
|
+
self.redirect_path = crm_property_path(property)
|
|
17
|
+
notice(I18n.t("crm.property.create.success"))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def property_params(params)
|
|
23
|
+
params.require(:property).permit(:name, :description)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
This is fine when the form maps 1:1 to ActiveRecord columns. Once the form
|
|
29
|
+
gets non-trivial, **promote `_params` into a dedicated `<Concept>::Form`
|
|
30
|
+
class** instead of growing the operation.
|
|
31
|
+
|
|
32
|
+
## When to promote to a Form object
|
|
33
|
+
|
|
34
|
+
Introduce a `Form` class as soon as **any** of these is true:
|
|
35
|
+
|
|
36
|
+
- The form has **virtual attributes** that aren't AR columns (e.g. a
|
|
37
|
+
settlement reference, an external API code, raw address pieces that resolve
|
|
38
|
+
to a city_id).
|
|
39
|
+
- The form needs **pre-assignment cleanup** (deep symbolize, reject blanks,
|
|
40
|
+
coerce arrays, normalize booleans).
|
|
41
|
+
- The form **calls a sub-operation mid-assignment** (e.g. resolving a
|
|
42
|
+
`settlement_ref` to a `city_id` before the AR record is saved).
|
|
43
|
+
- The form aggregates **multiple AR records** under one submit (e.g.
|
|
44
|
+
parent + nested has_many with custom permit logic).
|
|
45
|
+
- Strong params alone can't express the rule (conditional permits, branches
|
|
46
|
+
by user role, attributes coming from multiple top-level keys).
|
|
47
|
+
|
|
48
|
+
If none of those hold, keep the inline `_params` method. **Do not introduce
|
|
49
|
+
a Form object preemptively** — simple CRUD does not need the indirection.
|
|
50
|
+
|
|
51
|
+
## The pattern
|
|
52
|
+
|
|
53
|
+
Layout:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
app/concepts/<feature>/
|
|
57
|
+
├── form.rb <- the Form class
|
|
58
|
+
├── operation/
|
|
59
|
+
│ ├── create.rb <- uses the Form
|
|
60
|
+
│ ├── update.rb <- uses the Form
|
|
61
|
+
│ └── …
|
|
62
|
+
└── component/…
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Class shape:
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
class <Concept>::Form
|
|
69
|
+
ASSIGNABLE_TO_AR = %i[name description …].freeze
|
|
70
|
+
ASSIGNABLE_NESTED = %i[photos_attributes working_hours_attributes].freeze
|
|
71
|
+
VIRTUAL_ATTRIBUTES = %i[external_ref settlement_name source_code].freeze
|
|
72
|
+
|
|
73
|
+
attr_reader :record, *VIRTUAL_ATTRIBUTES
|
|
74
|
+
|
|
75
|
+
delegate :valid?, :invalid?, :errors, :save, :save!, to: :record
|
|
76
|
+
|
|
77
|
+
def initialize(record)
|
|
78
|
+
@record = record
|
|
79
|
+
VIRTUAL_ATTRIBUTES.each { |a| instance_variable_set("@#{a}", nil) }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def assign(raw_params)
|
|
83
|
+
return self if raw_params.blank?
|
|
84
|
+
|
|
85
|
+
permitted = sanitize(raw_params)
|
|
86
|
+
extract_virtual_attributes!(permitted)
|
|
87
|
+
@record.assign_attributes(permitted)
|
|
88
|
+
self
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def sanitize(raw_params)
|
|
94
|
+
raw = raw_params.respond_to?(:to_unsafe_h) ? raw_params.to_unsafe_h : raw_params.to_h
|
|
95
|
+
sym = raw.deep_symbolize_keys
|
|
96
|
+
|
|
97
|
+
permitted = {}
|
|
98
|
+
ASSIGNABLE_TO_AR.each { |k| permitted[k] = sym[k] if sym.key?(k) }
|
|
99
|
+
ASSIGNABLE_NESTED.each { |k| permitted[k] = sym[k] if sym.key?(k) }
|
|
100
|
+
VIRTUAL_ATTRIBUTES.each { |k| permitted[k] = sym[k] if sym.key?(k) }
|
|
101
|
+
permitted
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def extract_virtual_attributes!(permitted)
|
|
105
|
+
VIRTUAL_ATTRIBUTES.each do |a|
|
|
106
|
+
next unless permitted.key?(a)
|
|
107
|
+
|
|
108
|
+
instance_variable_set("@#{a}", permitted.delete(a))
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Usage from the operation:
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
def perform!(params:, current_user:)
|
|
118
|
+
record = <Resource>.new
|
|
119
|
+
authorize! record, :create?
|
|
120
|
+
|
|
121
|
+
form = <Concept>::Form.new(record).assign(params[:<concept>])
|
|
122
|
+
apply_external_resolution!(form, current_user) if form.external_ref.present?
|
|
123
|
+
|
|
124
|
+
record.save!
|
|
125
|
+
|
|
126
|
+
self.model = record
|
|
127
|
+
self.redirect_path = "/<plural>/#{record.id}"
|
|
128
|
+
notice(I18n.t("<concept>.create.success"))
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
def apply_external_resolution!(form, current_user)
|
|
134
|
+
result = <Concept>::Operation::ResolveSomething.call(
|
|
135
|
+
params: { ref: form.external_ref },
|
|
136
|
+
current_user: current_user
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if result.failure?
|
|
140
|
+
form.record.errors.add(:base, I18n.t("<concept>.create.errors.unresolved"))
|
|
141
|
+
raise ActiveRecord::RecordInvalid.new(form.record)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
form.assign_resolved!(result.model)
|
|
145
|
+
end
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## What NOT to do
|
|
149
|
+
|
|
150
|
+
- **Do not put complex param logic inside the operation's `_params` method.**
|
|
151
|
+
Once you need virtual attributes, conditional permits, or sub-operations,
|
|
152
|
+
promote to a Form object.
|
|
153
|
+
- **Do not use Reform, dry-validation, or other DSL gems.** This stack is
|
|
154
|
+
intentionally plain Ruby — Form objects are POROs that delegate validations
|
|
155
|
+
to the AR record.
|
|
156
|
+
- **Do not skip `authorize!`** because the Form is involved. The operation
|
|
157
|
+
still authorizes the AR record before calling `form.assign(...)`.
|
|
158
|
+
- **Do not generate a Form via the concept generator.** The
|
|
159
|
+
`tsykvas_rails_template:concept` scaffold intentionally emits the inline
|
|
160
|
+
`_params` shape — it covers the simple case. Promote by hand when the
|
|
161
|
+
form actually needs it.
|
|
162
|
+
|
|
163
|
+
## Checklist when promoting
|
|
164
|
+
|
|
165
|
+
1. Move strong-params logic out of the operation into `app/concepts/<feature>/form.rb`.
|
|
166
|
+
2. Update `Operation::Create` and `Operation::Update` to call
|
|
167
|
+
`Form.new(record).assign(params[:concept])` instead of
|
|
168
|
+
`record.assign_attributes(_params(params))`.
|
|
169
|
+
3. Add a spec for the Form: `spec/concepts/<feature>/form_spec.rb`. Test that
|
|
170
|
+
permitted attributes are kept, virtual attributes are extracted into reader
|
|
171
|
+
methods, and unknown keys are rejected.
|
|
172
|
+
4. If virtual attributes drive a sub-operation, write a request spec
|
|
173
|
+
exercising the failure path (e.g. unresolvable settlement) so the
|
|
174
|
+
`add_error` + `raise ActiveRecord::RecordInvalid` flow is covered.
|