Skip to content

Commit 1b062a9

Browse files
authored
feat: v3.0.0 — the killer-feature release (#13)
Brings main from v2.4 to v3.0.0. AI/dev experience docs, layout helpers, file upload, date picker, Wheels resource conventions. See CHANGELOG.md [3.0.0].
2 parents 391a8e6 + 8869da9 commit 1b062a9

19 files changed

Lines changed: 3944 additions & 281 deletions

.ai/ARCHITECTURE.md

Lines changed: 159 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,164 @@
1-
# Basecoat Plugin Architecture
1+
# Architecture (long-form context)
22

3-
See `CLAUDE.md` in the plugin root for the authoritative specification including markup reference, implementation phases, naming conventions, and testing guidance.
3+
For day-to-day reference, read `CLAUDE.md` and `.ai/HELPERS.md`. This file is the design rationale — why the package is shaped the way it is.
44

5-
## Component Categories
5+
## Goal
66

7-
- **Simple** (Phase 1): Button, Badge, Icon, Spinner, Skeleton, Progress, Separator, Tooltip
8-
- **Block** (Phase 2): Alert, Card, Dialog
9-
- **Form** (Phase 3): Field (text, email, textarea, select, checkbox, switch)
10-
- **Complex** (Phase 4): Table, Tabs, Dropdown, Pagination
11-
- **Layout** (Phase 5): Sidebar, Breadcrumb
7+
A Wheels developer should never have to think about basecoat-css class names or basecoat-js component contracts. They write `#uiBoundField(objectName="post", property="title")#` and get a fully styled, model-bound, error-aware, Turbo-friendly form field.
128

13-
## Design Principles
9+
The plugin sits between the application and basecoat:
10+
- it bundles known-good versions of basecoat-css + basecoat-js so apps don't need a build pipeline
11+
- it bridges Wheels' model object idioms (`obj.errorsOn`, `obj.hasErrors`, `obj.allErrors`) into the rendered HTML
12+
- it provides CSP-safe alternatives where basecoat-css ships inline-script patterns
13+
- it ships visual defaults (`wheels-basecoat-extras.min.css`) for components basecoat-css doesn't style
1414

15-
- All helpers return HTML strings for use in views via `#helperName()#`
16-
- Markup matches basecoatui.com v0.3.x patterns exactly
17-
- No JavaScript dependencies for core components (CSS-only where possible)
18-
- Turbo-aware but Hotwire-independent
19-
- Native `<dialog>` element for modals (no JS library)
15+
## File layout
16+
17+
```
18+
wheels-basecoat/
19+
├── Basecoat.cfc # Single CFC; every public method is a helper.
20+
├── package.json # Wheels manifest. provides.mixins=controller.
21+
├── box.json # CommandBox metadata + install hooks.
22+
├── index.cfm # Static doc page rendered at /wheels/packages/wheels-basecoat.
23+
├── CLAUDE.md # AI-tools-read-first. Master index of patterns + rules.
24+
├── README.md # Human docs.
25+
├── CHANGELOG.md
26+
├── .ai/
27+
│ ├── HELPERS.md # Formal signature reference for every helper.
28+
│ ├── EXAMPLES.md # Scenario-driven recipes.
29+
│ ├── SCAFFOLDS.md # Copy-paste page templates.
30+
│ ├── PATTERNS.md # When-to-use decision trees.
31+
│ ├── PITFALLS.md # Footguns + fixes.
32+
│ └── ARCHITECTURE.md # This file.
33+
├── assets/basecoat/
34+
│ ├── basecoat.min.css # Bundled basecoat-css 0.3.11.
35+
│ ├── wheels-basecoat-extras.min.css # Defaults for components basecoat-css doesn't style.
36+
│ └── js/
37+
│ ├── all.min.js # All basecoat-js modules concatenated.
38+
│ ├── basecoat.min.js # Component registration kernel.
39+
│ ├── tabs / dropdown-menu / popover / select / command / sidebar / toast .min.js
40+
│ └── wheels-basecoat-ui.min.js # CSP-safe shim for dialog/theme/sidebar/slider.
41+
├── examples/showcase/ # Mountable live-render showcase
42+
├── scripts/install.cfm # CommandBox post-install hook (publishes assets to public/)
43+
└── tests/Basecoat*Spec.cfc # One spec per major version
44+
```
45+
46+
## Coding conventions
47+
48+
- **CFScript**, no tag-based components. Every public method is a `public string function ...`.
49+
- **Typed parameters with sensible defaults.** Signatures self-document.
50+
- **`var local = {};`** at the top of any helper that uses local scope.
51+
- **`savecontent variable="local.html" { writeOutput(...) }`** for multi-line HTML.
52+
- **Always returns a string.** No helper writes to the response — that lets composition work cleanly and makes testing trivial.
53+
- **Double quotes for HTML attributes.**
54+
- **Unique IDs** via `replace(left(createUUID(), 8), "-", "", "all")` (wrapped in `$uiBuildId()`).
55+
56+
## Naming patterns
57+
58+
| Prefix | Meaning |
59+
|---|---|
60+
| `ui*` | Visible component helper. Returns HTML. |
61+
| `ui*End` | Closing tag for a block component. |
62+
| `uiBound*` | Wheels-bound variant. Auto-resolves model value/error/name. |
63+
| `basecoat*` | Package-level infrastructure (`basecoatIncludes`, etc.). |
64+
| `turbo*` | Turbo Stream helpers. |
65+
| `$uiX` / `$X` | Internal helper. **Still PUBLIC** (PackageLoader only carries public methods) but `$` signals "don't call from app code". |
66+
67+
## The PackageLoader mixin gotcha
68+
69+
Wheels' `PackageLoader.$collectMixins` integrates only PUBLIC methods of the package CFC into the target controller scope. Private methods stay on the CFC and aren't visible from the mixed-in `variables` scope where helpers run when called from views.
70+
71+
Any internal helper called by a public mixed-in helper must itself be `public`. The `$` prefix is the convention for signaling internal-but-still-public:
72+
73+
```cfml
74+
public string function uiButton(...) {
75+
$validateEnum(...); // ← called from mixed-in scope, must be public
76+
}
77+
78+
public void function $validateEnum(...) { /* implementation */ }
79+
```
80+
81+
Past PRs that fixed this: #3 (`$uiBuildId` / `$uiLucideIcon`).
82+
83+
## Argument validation
84+
85+
Every enum-typed argument (`variant`, `size`, `type`, `action`, `side`, `orientation`, `status`) is validated via `$validateEnum`. Bad values throw `WheelsBasecoat.InvalidArgument` with the helper name, the bad value, and the allowed list:
86+
87+
```
88+
WheelsBasecoat.InvalidArgument:
89+
uiButton() received an unsupported variant value: 'primay'.
90+
Allowed values are: primary,secondary,destructive,outline,ghost,link.
91+
```
92+
93+
Goal: typos surface as errors at the call site, not as silent unstyled markup.
94+
95+
## Wheels integration
96+
97+
Three layers:
98+
99+
### Layer 1 — Form helpers
100+
The `uiBound*` family reads from the controller-scoped model:
101+
- `value` from `obj[property]` (with date coercion to ISO format)
102+
- `errorMessage` from `obj.errorsOn(property)[1].message` if `obj.hasErrors(property)`
103+
- `name` constructed as `<objectName>[<property>]`
104+
- `label` humanized from the property name (`firstName``First name`)
105+
106+
For checkbox/switch types: a hidden `value="0"` companion input under the same name solves the "unchecked submits nothing" footgun.
107+
108+
### Layer 2 — Flash messages
109+
`basecoatFlashToasts()` reads `flash()` and renders a toaster + a toast per entry. Standard keys (`success`, `error`, `warning`, `info`, `notice`) map to corresponding variants.
110+
111+
### Layer 3 — Resource conventions (v3.0)
112+
`uiResourceForm(model)` and `uiResourceTable(query)` introspect Wheels models to scaffold full UIs. Read property metadata, `enum()` declarations (rendered as select fields), `validatesPresenceOf` (rendered as `required`).
113+
114+
For polished public-facing pages, hand-author with `uiBound*`. Use the resource family for admin scaffolds and prototypes.
115+
116+
## CSP safety
117+
118+
Inline event handlers (`onclick="..."`) require `unsafe-inline` in CSP. The plugin avoids them via `wheels-basecoat-ui.min.js` — a small delegated handler bundle that listens at `document` for clicks on elements carrying `data-ui-*` attributes:
119+
- `data-ui-dialog-open` / `data-ui-dialog-close` (uiDialog)
120+
- `data-ui-theme-toggle` (uiThemeToggle)
121+
- `data-ui-sidebar-toggle` (uiSidebarToggle)
122+
- `<input type="range">` `input` event (uiSlider live mirror)
123+
124+
The basecoat-js modules themselves attach via delegation too — they're CSP-clean.
125+
126+
## Hotwire / Turbo integration
127+
128+
Three primary patterns:
129+
130+
1. **Frame-scoped form swap on validation failure.** Wrap the form in `<turbo-frame id="post_form">`. On failure, controller does `renderPartial(partial="form", layout=false)`. Turbo finds and swaps the frame.
131+
132+
2. **Turbo Stream remove on delete.** Each row wrapped in `<article id="post_X">`. Delete button form has `data_turbo_stream="true"` so Turbo advertises the stream Accept header. Controller detects, renders `_postRemoved.cfm` emitting `<turbo-stream action="remove" target="post_X">`. Row vanishes; no reload.
133+
134+
3. **Turbo Stream append on create.** New comment form wrapped in `<turbo-frame id="new_comment">`. Comments controller success returns a partial emitting `<turbo-stream action="append" target="comments">` + rendered comment + fresh empty form replacing the frame.
135+
136+
The `turboStream(...)` / `turboStreamEnd()` / `turboStreamHeader()` helpers compose these responses.
137+
138+
## Versioning
139+
140+
| Version | Theme |
141+
|---|---|
142+
| 1.x | Initial helpers (single CFC, basic coverage) |
143+
| 1.1 | Cards/alerts markup aligned with basecoat-css 0.3.x semantic structure |
144+
| 2.0 | Bundled assets, uiBoundField, toasts, popover, dark mode, Turbo helpers, arg validation, CSP-safe dialog |
145+
| 2.1 | Tabs/Dropdown/Sidebar reworked to ARIA roles + basecoat-js contracts |
146+
| 2.2 | Extras CSS (breadcrumb, pagination), uiCommand family, uiSelect |
147+
| 2.3 | uiSlider, uiSteps wizard, uiBoundSelect |
148+
| 2.4 | Bound checkbox/multi-checkbox/radio, uiErrorSummary, uiRating |
149+
| 3.0 | AI/dev experience (CLAUDE.md + .ai/ docs + showcase + install script), uiTagInput, uiAccordion, uiCallout, uiEmptyState, uiCodeBlock, uiTimeline, uiFileUpload, uiBoundFile, uiDatePicker, uiResourceForm, uiResourceTable, uiPaginationFor |
150+
151+
basecoat-css version is pinned in `package.json::basecoatCSSVersion`. When upstream ships a new minor version, run the bundled-asset refresh playbook: download new CSS+JS, run the test suite, update CLAUDE.md if markup contracts shifted, bump.
152+
153+
## Testing
154+
155+
Snapshot-style: every helper has tests asserting canonical output for canonical args. Split by version (`BasecoatSimpleSpec.cfc`, `BasecoatV*Spec.cfc`). Tests run inside a Wheels app context (extends `wheels.WheelsTest`).
156+
157+
The `index.cfm` doc page also serves as a visual regression target — every helper appears there with a version pill.
158+
159+
## Future considerations
160+
161+
- **Type definitions** — a `helpers.d.ts`-style document for IDE/AI parameter completion. `.ai/HELPERS.md` is a step toward this.
162+
- **Component-level i18n.** Most strings are hard-coded English. A `basecoat:translations` configuration map would make localization easy.
163+
- **Per-component configuration defaults.** `set("uiButton.defaults.variant", "outline")` for app-level overrides.
164+
- **Animation primitives.** A `uiTransition(...)` block helper for orchestrated reveals.

0 commit comments

Comments
 (0)