Skip to content

Commit 391a8e6

Browse files
bpamiriPeter Amiriclaude
authored
feat: v2.2 + v2.3 + v2.4 catch-up — extras CSS, command palette, uiSelect, uiSlider, uiSteps, complete form-binding family (#11)
* feat: v2.2.0-rc.1 — extras CSS for breadcrumb/pagination, command palette family, rich select combobox basecoat-css 0.3.x ships no rules for `.breadcrumb` or `.pagination`, so the existing helpers were rendering correct markup with no visual defaults. v2.2 ships a small companion stylesheet (loaded by default via the new `basecoatIncludes(extrasCSS=true)` flag) plus exposes the two remaining basecoat-js components — command palette and rich select combobox — as helpers. Added: * `assets/basecoat/wheels-basecoat-extras.min.css` — visual defaults for `.breadcrumb` (flex `<ol>` with muted color, hover-darken, aria-current emphasis) and `.pagination` (rounded buttons, active state via `.pagination-item-active` or `[aria-current=page]`, disabled state via `[aria-disabled=true]` or the legacy `.opacity-50`). Inherits basecoat color tokens so it tracks light/dark theme automatically. * basecoatIncludes(extrasCssPath=, extrasCSS=true) — new args, loads the extras stylesheet by default. `extrasCSS=false` opts out for apps that ship their own breadcrumb/pagination styling. * Command palette family (driven by basecoat-js's command.js): - uiCommand / uiCommandEnd - uiCommandInput(placeholder=, ariaLabel=) — the search input that command.js wires up - uiCommandList / uiCommandListEnd — opens [role=menu] - uiCommandGroup(label=) / uiCommandGroupEnd - uiCommandItem(text=, href=, keywords=, icon=, kbd=, force=, keepOpen=, disabled=) — emits role=menuitem with the data-* attributes the JS reads (data-keywords, data-force, data-keep-command-open). With href -> <a>, without -> <button>. - uiCommandSeparator() — <hr role="separator"> - uiCommandEmpty(text="No results.") — force-shown placeholder - uiCommandDialog(triggerText=, triggerClass=, id=) / uiCommandDialogEnd — wraps the palette in a <dialog class="dialog command-dialog"> for ⌘K-style modals. Trigger uses CSP-safe data-ui-dialog-open delegation handled by wheels-basecoat-ui.js. * uiSelect(name=, options=, value=, placeholder=, search=, multiselect=, closeOnSelect=, id=, class=) — the basecoat-css 0.3.x rich combobox, distinct from uiField(type="select")'s native <select>. Single-call helper that: - takes the same options="value:Label[:disabled],..." shape - pre-renders the trigger label so there's no FOUC before select.js initializes - emits the four parts the JS queries (trigger button with span, popover with optional search input, [role=listbox] with [role=option] children, hidden input) - multi-select serializes the value as a JSON array in the hidden input; data-placeholder + aria-multiselectable wired automatically - third options segment ":disabled" sets aria-disabled * tests/BasecoatV22Spec.cfc — snapshot-style coverage for the extras CSS opt-out, every command-palette helper (including the data-* attribute pass-through), and uiSelect's four-part structure + pre-rendered label + multi-select serialization. This was the v2.2 list called out as out-of-scope in #8 (built-in CSS for breadcrumb/pagination, uiCommand, richer uiSelect). Nothing breaking; v2.2 is purely additive. Helper API is stable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: v2.3.0-rc.1 — uiSlider, uiSteps, uiBoundSelect, uiBoundSlider Four additions, one CSS extension, one JS extension. All purely additive on top of v2.2. Slider family * uiSlider(name, value, min, max, step, label, showValue, disabled, id, class) — basecoat-styled <input type="range">. Computes the --slider-value CSS variable percentage server-side from the current value, so the filled portion of the track renders correctly on first paint without needing JS. Optional showValue=true renders an <output data-ui-slider-output> mirror. * wheels-basecoat-ui.js extended with an "input" listener that keeps --slider-value in sync as the user drags + mirrors the live value into any matching <output for="slider-id" data-ui-slider-output>. * uiBoundSlider(objectName, property, ...) — Wheels-bound variant. Auto- resolves the value from obj[property], emits name="<obj>[<prop>]", humanizes the property name into the default label. Steps / wizard progress indicator * uiSteps(ariaLabel, class) — opens a labeled <nav><ol class="ui-steps">. * uiStep(text, status="complete|current|upcoming", number, description, href) — emits <li data-status="..."> with the matching ARIA. Auto-numbered via a request-scoped counter when no `number` is passed. Complete steps render a check icon in the marker; current steps emit aria-current="step". Optional href wraps non-current steps in a link. * uiStepsEnd() — closes + clears the counter. * wheels-basecoat-extras.min.css extended with .ui-steps rules (numbered circles connected by a colored progress line, mobile-stacked layout below 640px, hover state on linked steps). Wheels integration for the rich combobox * uiBoundSelect(objectName, property, options, placeholder, search, multiselect, closeOnSelect, id, class) — same options syntax as uiSelect, but tolerates a real array on the model for multi-select (auto-serializes to JSON for the hidden input). Throws WheelsBasecoat.ObjectNotFound when the named object isn't in scope. Fix * Replace `??` with `?:` (CFML's null-coalescing operator) in two new helpers — caught the typo on first reload (Lucee 7 reports the whole component as failing to parse, mixin doesn't load, helpers go undefined). Wheels Tutorial Finding for future reference. Out of scope (potential v2.4+): * Rich popover-based date picker (requires building a calendar grid + JS that basecoat doesn't ship). Native <input type="date"> is correctly styled by basecoat and works for now. * Drag-and-drop file uploader. * Code editor / syntax-highlighted textarea. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: v2.4.0-rc.1 — complete the form-binding round-trip + uiErrorSummary + uiRating With v2.4 every common form input type has a Wheels-bound helper that reads the current value, emits the canonical <obj>[<prop>] name, surfaces validation errors, and humanizes the label. Added: * uiBoundCheckbox(objectName, property, label, switch, ...) — single bound checkbox or switch. Solves the "unchecked checkbox submits nothing" footgun by emitting a hidden value="0" companion input under the same name BEFORE the checkbox, so params.<obj>.<prop> is always defined as 0 or 1. Pass switch=true to render as a basecoat .switch instead. * uiCheckboxGroup(name, options, value, legend, description, inline) — multi-checkbox collection emitting name="<name>[]" so Wheels arrays the values. Tolerates a real CFML array, a JSON-array string, or a CSV string for value. * uiBoundCheckboxGroup(objectName, property, options, ...) — Wheels- bound variant. Auto-resolves the value from the model, humanizes the property into the legend. * uiRadioGroup(name, options, value, legend, description, inline) — radio-group container with role="radiogroup". * uiBoundRadioGroup(objectName, property, options, ...) — Wheels- bound variant. * uiErrorSummary(model, title, description) — drop-in replacement for Wheels' errorMessagesFor(). Renders model.allErrors() as a basecoat destructive alert with a bullet list of field-prefixed messages. Returns "" when no errors so it's safe to call unconditionally at the top of a form. Auto-pluralizes the title ("1 error" vs "3 errors"). * uiRating(value, max, name, ariaLabel, class) — 1-to-N star rating widget. Read-only display by default; pass name= to render as an interactive radio group (CSS-only highlight via the bundled extras stylesheet — no JS required). Renders highest-first internally so the CSS sibling combinator can light earlier stars on hover/check. * One new icon: star. * wheels-basecoat-extras.min.css extended: - .ui-rating rules (display + interactive variants) - .radio rule (basecoat-css 0.3.x ships .checkbox and .switch but no radio styling — the new helpers add a sized circle that matches the checkbox treatment) * tests/BasecoatV24Spec.cfc — snapshot-style coverage: - falsy-companion hidden input + checked-state coercion - JSON / CSV / real-array value resolution for the checkbox group - role=radiogroup + name=plural[] for the multi helpers - uiErrorSummary's empty-string fallback + plural wording - uiRating's read-only fill count + interactive highest-first order The form-binding family now spans: uiBoundField (text, textarea, select, date, etc.) uiBoundSelect (rich combobox, search, multi-select) uiBoundSlider (range) uiBoundCheckbox (single boolean) uiBoundCheckboxGroup (multi-value) uiBoundRadioGroup (single-choice) uiErrorSummary (model-level rollup) Note: this PR also brings v2.2 and v2.3 forward — those PRs (#9, #10) were merged against feat/v2.1's branch instead of main, so main missed them. The first two commits on this branch cherry-pick v2.2 (extras CSS, command palette, uiSelect) and v2.3 (uiSlider, uiSteps, uiBoundSelect, uiBoundSlider) onto current main. v2.4 builds on top. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Peter Amiri <petera@pai.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent cb18eeb commit 391a8e6

9 files changed

Lines changed: 1575 additions & 4 deletions

Basecoat.cfc

Lines changed: 870 additions & 1 deletion
Large diffs are not rendered by default.

CHANGELOG.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,51 @@
22

33
All notable changes to this package will be documented in this file.
44

5+
## [2.4.0-rc.1] — 2026-05-01
6+
7+
### Added
8+
- **`uiBoundCheckbox(objectName, property, label, switch, description, ...)`** — single bound checkbox or switch. Solves the standard "unchecked checkbox submits nothing" footgun by emitting a hidden `value="0"` companion input under the same name BEFORE the checkbox, so `params.<obj>.<prop>` is always defined as `0` or `1`. Pass `switch=true` to render as a basecoat `.switch` instead.
9+
- **`uiCheckboxGroup(name, options, value, legend, description, inline)`** — multi-checkbox collection emitting `name="<name>[]"` so Wheels arrays the values. Same `options="value:Label[:disabled],..."` shape as `uiSelect` / `uiRadioGroup`. Tolerates a real CFML array, a JSON-array string, or a CSV string for `value`.
10+
- **`uiBoundCheckboxGroup(objectName, property, options, ...)`** — Wheels-bound variant. Auto-resolves the array/JSON/CSV value from `obj[property]`, humanizes the property into the legend.
11+
- **`uiRadioGroup(name, options, value, legend, description, inline)`** — radio-group container with `role="radiogroup"`. Same options syntax as the checkbox group.
12+
- **`uiBoundRadioGroup(objectName, property, options, ...)`** — Wheels-bound variant.
13+
- **`uiErrorSummary(model, title, description)`** — drop-in replacement for Wheels' `errorMessagesFor()`. Renders the model's full validation error list as a basecoat destructive alert with a bullet list of field-prefixed messages from `model.allErrors()`. Returns "" when no errors so it's safe to call unconditionally at the top of a form. Auto-pluralizes the title (`"1 error"` vs `"3 errors"`).
14+
- **`uiRating(value, max, name, ariaLabel, class)`** — 1-to-N star rating. Read-only display by default; pass `name=` to render as an interactive radio group (CSS-only highlight via the bundled extras stylesheet — no JS required). Renders highest-first internally so the CSS sibling combinator can light earlier stars on hover/check.
15+
- **One new icon**: `star`.
16+
- **`wheels-basecoat-extras.min.css` extended** with `.ui-rating` rules (display + interactive variants) plus a `.radio` rule (basecoat-css 0.3.x ships `.checkbox` and `.switch` but no radio styling — the new helpers add a sized circle that matches the checkbox treatment).
17+
18+
### Form-binding round-trip is now complete
19+
With v2.4, every common form input type has a Wheels-bound helper that reads the current value, emits the canonical `<obj>[<prop>]` name, surfaces validation errors, and humanizes the label:
20+
21+
| Input | Helper |
22+
|---|---|
23+
| Text / textarea / select / date / etc. | `uiBoundField` |
24+
| Rich combobox (search, multi-select) | `uiBoundSelect` |
25+
| Range slider | `uiBoundSlider` |
26+
| Single boolean | `uiBoundCheckbox` (with hidden companion for unchecked submissions) |
27+
| Multi-checkbox collection | `uiBoundCheckboxGroup` |
28+
| Single-choice radio | `uiBoundRadioGroup` |
29+
30+
Plus `uiErrorSummary(model)` to render model-level validation results without manually iterating `errorMessagesFor()`.
31+
32+
## [2.3.0-rc.1] — 2026-05-01
33+
34+
### Added
35+
- **`uiSlider(name, value, min, max, step, label, showValue, disabled, id, class)`** — basecoat-styled `<input type="range">` wrapper. Computes the `--slider-value` CSS variable percentage server-side from the current value so the filled portion of the track renders correctly on first paint (no JS required for the initial render). Optional `showValue=true` renders an `<output data-ui-slider-output>` mirror that the bundled `wheels-basecoat-ui.js` keeps in sync as the user drags. Emits `aria-valuemin/max/now`. Pairs cleanly with `uiBoundSlider`.
36+
- **`uiBoundSlider(objectName, property, ...)`** — Wheels-bound variant that auto-resolves the value from `obj[property]`, emits `name="<objectName>[<property>]"`, and humanizes the property name into the default label. Mirrors `uiBoundField`'s ergonomics for slider use.
37+
- **`uiSelect`'s Wheels-bound sibling: `uiBoundSelect(objectName, property, options, ...)`** — same options syntax + tolerates a real array on the model for multi-select (auto-serializes to JSON for the hidden input). Throws `WheelsBasecoat.ObjectNotFound` if the named object isn't in scope.
38+
- **Steps / wizard progress indicator**: `uiSteps(ariaLabel, class)` opens a labeled `<nav><ol class="ui-steps">`; `uiStep(text, status="complete|current|upcoming", number, description, href)` renders each `<li data-status="...">`; `uiStepsEnd()` closes. Auto-numbers steps via a request-scoped counter when no `number` is passed. Complete steps render a check icon in the marker; current steps emit `aria-current="step"`. Optional `href` wraps complete/upcoming markers in a link (current never links). Visual defaults shipped in `wheels-basecoat-extras.min.css` — numbered circles connected by a colored progress line, mobile-stacked layout below 640px.
39+
- **`wheels-basecoat-extras.min.css` extended** with `.ui-steps` rules to match.
40+
- **`wheels-basecoat-ui.js` extended** to keep slider `--slider-value` in sync as the user drags + mirror the live value into any matching `<output data-ui-slider-output>`. CSP-safe; no inline event handlers needed.
41+
42+
## [2.2.0-rc.1] — 2026-05-01
43+
44+
### Added
45+
- **Bundled `wheels-basecoat-extras.min.css`** — visual defaults for the components that basecoat-css 0.3.x doesn't ship CSS for (`.breadcrumb`, `.pagination`). Loaded automatically by `basecoatIncludes()` (toggle via the new `extrasCSS` arg, default `true`). Closes the gap that previously left `uiBreadcrumb` and `uiPagination` rendering unstyled — their helpers were correct, the upstream stylesheet just had nothing for them.
46+
- **Command palette family**`uiCommand` / `uiCommandInput` / `uiCommandList` / `uiCommandGroup` / `uiCommandItem` / `uiCommandSeparator` / `uiCommandEmpty` / `uiCommandEnd` plus the modal wrapper `uiCommandDialog` / `uiCommandDialogEnd`. Drives basecoat-js's `command.js` for the live search filter (matches by `data-filter` / textContent / `data-keywords`), arrow-key + Home/End + Enter navigation, and click-to-close behavior when nested inside a `<dialog class="command-dialog">`. Items support `keywords`, `icon`, `kbd`, `force`, `keepOpen`, `disabled`, and either `<a href>` or `<button>` rendering.
47+
- **`uiSelect`** — basecoat-css 0.3.x's rich combobox component (popover, optional search, multi-select). Distinct from `uiField(type="select")` which renders a plain native `<select>`. Single-call helper that takes the same `options="value:Label[:disabled],..."` shape as `uiField`, pre-renders the trigger label so there's no FOUC before `select.js` initializes, and emits the four parts the JS queries (trigger button, popover, listbox, hidden input). Multi-select serializes the value as a JSON array in the hidden input.
48+
- **`basecoatIncludes(extrasCSS=true)`** — the new opt-in that loads the extras CSS. New args: `extrasCssPath`, `extrasCSS`. The default extras-css path mirrors the recommended publish location.
49+
550
## [2.1.0-rc.1] — 2026-05-01
651

752
### Changed (BREAKING)

assets/basecoat/js/wheels-basecoat-ui.min.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,21 @@ document.addEventListener("click",function(e){
1515
var sb=e.target.closest("[data-ui-sidebar-toggle]");
1616
if(sb){var detail={action:sb.getAttribute("data-ui-sidebar-toggle")||"toggle"};var tid=sb.getAttribute("data-ui-sidebar-target");if(tid)detail.id=tid;document.dispatchEvent(new CustomEvent("basecoat:sidebar",{detail:detail}));return;}
1717
});
18+
19+
// Slider (uiSlider) — keep --slider-value in sync as the user drags, and
20+
// mirror the current value into the optional <output data-ui-slider-output>.
21+
function updateSlider(input){
22+
var min=parseFloat(input.min)||0,max=parseFloat(input.max)||100,val=parseFloat(input.value)||0;
23+
var pct=max===min?0:((val-min)/(max-min))*100;
24+
if(pct<0)pct=0;else if(pct>100)pct=100;
25+
input.style.setProperty("--slider-value",pct.toFixed(2)+"%");
26+
input.setAttribute("aria-valuenow",val);
27+
if(input.id){
28+
var out=document.querySelector('output[for="'+input.id+'"][data-ui-slider-output]');
29+
if(out)out.textContent=val;
30+
}
31+
}
32+
document.addEventListener("input",function(e){
33+
if(e.target.tagName==="INPUT"&&e.target.type==="range")updateSlider(e.target);
34+
});
1835
})();
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*! wheels-basecoat-extras v2.3 — visual defaults for components that
2+
basecoat-css 0.3.x does not ship CSS for: .breadcrumb, .pagination,
3+
.ui-steps. Loaded automatically by basecoatIncludes() (extrasCSS=true
4+
default). Inherits the basecoat color tokens (--color-foreground,
5+
--color-muted, etc.). */
6+
7+
.breadcrumb{display:block}
8+
.breadcrumb ol{display:flex;align-items:center;flex-wrap:wrap;gap:.5rem;padding:0;margin:0;list-style:none;font-size:.875rem;color:var(--color-muted-foreground)}
9+
.breadcrumb li{display:inline-flex;align-items:center}
10+
.breadcrumb a{color:inherit;text-decoration:none;transition:color 150ms}
11+
.breadcrumb a:hover{color:var(--color-foreground)}
12+
.breadcrumb [aria-current=page]{color:var(--color-foreground);font-weight:500}
13+
.breadcrumb svg{opacity:.6;width:.875rem;height:.875rem}
14+
15+
.pagination{display:flex;align-items:center;gap:.25rem;padding:0;margin:0;list-style:none;font-size:.875rem}
16+
.pagination-item{display:inline-flex;align-items:center;justify-content:center;min-width:2.25rem;height:2.25rem;padding:0 .625rem;border-radius:var(--radius-md);border:1px solid transparent;color:var(--color-foreground);text-decoration:none;transition:background-color 150ms,color 150ms,border-color 150ms;-webkit-user-select:none;user-select:none}
17+
.pagination-item:hover{background:var(--color-muted)}
18+
.pagination-item-active,.pagination-item[aria-current=page]{background:var(--color-primary);color:var(--color-primary-foreground)}
19+
.pagination-item-active:hover,.pagination-item[aria-current=page]:hover{background:var(--color-primary)}
20+
.pagination-item[aria-disabled=true],.pagination-item.opacity-50{opacity:.5;pointer-events:none}
21+
.pagination-item svg{width:1rem;height:1rem}
22+
.pagination-item:focus-visible{outline:2px solid var(--color-ring);outline-offset:2px}
23+
24+
/* Steps / wizard progress indicator (uiSteps / uiStep). */
25+
.ui-steps{display:flex;align-items:flex-start;gap:0;padding:0;margin:0;list-style:none}
26+
.ui-steps>li{flex:1;display:flex;align-items:flex-start;gap:.625rem;padding:0;font-size:.875rem;position:relative}
27+
.ui-steps>li:not(:last-child){padding-right:.5rem}
28+
.ui-steps>li:not(:last-child)::after{content:"";position:absolute;top:1rem;left:2rem;right:.5rem;height:1px;background:var(--color-border);z-index:0}
29+
.ui-steps .ui-step-marker{display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;border-radius:9999px;border:1px solid var(--color-border);background:var(--color-background);color:var(--color-muted-foreground);font-weight:600;font-size:.8125rem;flex-shrink:0;position:relative;z-index:1}
30+
.ui-steps .ui-step-text{display:flex;flex-direction:column;gap:.125rem;padding-top:.375rem;min-width:0}
31+
.ui-steps .ui-step-label{font-weight:500;color:var(--color-foreground)}
32+
.ui-steps .ui-step-description{color:var(--color-muted-foreground);font-size:.8125rem}
33+
.ui-steps>li[data-status=complete] .ui-step-marker{background:var(--color-primary);border-color:var(--color-primary);color:var(--color-primary-foreground)}
34+
.ui-steps>li[data-status=complete]:not(:last-child)::after{background:var(--color-primary)}
35+
.ui-steps>li[data-status=current] .ui-step-marker{border-color:var(--color-primary);color:var(--color-primary)}
36+
.ui-steps>li[data-status=upcoming] .ui-step-label{color:var(--color-muted-foreground)}
37+
.ui-steps .ui-step-link{color:inherit;text-decoration:none;display:flex;gap:.625rem;align-items:flex-start;width:100%}
38+
.ui-steps .ui-step-link:hover .ui-step-label{color:var(--color-primary)}
39+
@media (max-width: 640px){
40+
.ui-steps{flex-direction:column;gap:.5rem}
41+
.ui-steps>li{padding-right:0}
42+
.ui-steps>li:not(:last-child)::after{display:none}
43+
}
44+
45+
/* Rating (uiRating) — read-only display + interactive (radio-based, CSS-only). */
46+
.ui-rating{display:inline-flex;align-items:center;gap:.125rem;border:0;padding:0;margin:0;color:var(--color-muted-foreground)}
47+
.ui-rating .ui-rating-star{display:inline-flex;align-items:center}
48+
.ui-rating .ui-rating-star.is-filled svg{fill:var(--color-primary);color:var(--color-primary)}
49+
fieldset.ui-rating{flex-direction:row-reverse;justify-content:flex-end}
50+
fieldset.ui-rating input[type=radio]{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap}
51+
fieldset.ui-rating label{cursor:pointer;display:inline-flex;color:var(--color-muted-foreground);transition:color 120ms}
52+
fieldset.ui-rating label:hover,fieldset.ui-rating label:hover ~ label,fieldset.ui-rating input:checked ~ label{color:var(--color-primary)}
53+
fieldset.ui-rating label:hover svg,fieldset.ui-rating label:hover ~ label svg,fieldset.ui-rating input:checked ~ label svg{fill:var(--color-primary)}
54+
fieldset.ui-rating input:focus-visible + label{outline:2px solid var(--color-ring);outline-offset:2px;border-radius:.25rem}
55+
56+
/* Radio button styling — basecoat-css 0.3.x ships .checkbox / .switch but no .radio rule. Mirror the checkbox sizing for the .radio class our group helpers emit. */
57+
.field input[type=radio]:not([role=switch]),.input[type=radio]:not([role=switch]),input[type=radio].radio{appearance:none;-webkit-appearance:none;width:1rem;height:1rem;border:1px solid var(--color-input);border-radius:9999px;background:var(--color-background);transition:box-shadow 120ms;cursor:pointer;flex-shrink:0}
58+
input[type=radio].radio:checked{border-color:var(--color-primary);background:radial-gradient(circle,var(--color-primary) 40%,var(--color-background) 45%)}
59+
input[type=radio].radio:focus-visible{outline:2px solid var(--color-ring);outline-offset:2px}
60+
input[type=radio].radio:disabled{cursor:not-allowed;opacity:.5}

0 commit comments

Comments
 (0)