|
1 | | -# Basecoat Plugin Architecture |
| 1 | +# Architecture (long-form context) |
2 | 2 |
|
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. |
4 | 4 |
|
5 | | -## Component Categories |
| 5 | +## Goal |
6 | 6 |
|
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. |
12 | 8 |
|
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 |
14 | 14 |
|
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