Skip to content

Commit 1ed7e8e

Browse files
committed
Put Pret UI's other field controls on Input's field style
1 parent 7c427c8 commit 1ed7e8e

22 files changed

Lines changed: 265 additions & 172 deletions

‎packages/boxel-ui/src/components/input/index.gts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,8 @@ export default class BoxelInput extends Component<Signature> {
407407
}
408408
409409
.search-icon {
410+
width: var(--boxel-input-search-icon-size, var(--icon-size));
411+
height: var(--boxel-input-search-icon-size, var(--icon-size));
410412
--icon-color: var(
411413
--boxel-input-search-icon-color,
412414
var(--boxel-highlight)

‎packages/boxel-ui/src/components/input/usage.gts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,11 @@ export default class InputUsage extends Component {
247247
@type='color'
248248
@description='Search icon svg color'
249249
/>
250+
<Css.Basic
251+
@name='--boxel-input-search-icon-size'
252+
@type='dimension'
253+
@description='Search icon size (defaults to --boxel-icon-sm)'
254+
/>
250255
</:cssVars>
251256
</FreestyleUsage>
252257

‎packages/pretui/components/combobox.gts‎

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -150,18 +150,18 @@ export class Combobox extends Component<ComboboxSignature> {
150150
position: relative;
151151
display: block;
152152
min-width: 0;
153-
--boxel-form-control-border-radius: var(--radius);
154-
--boxel-border-radius-sm: var(--radius);
153+
--boxel-form-control-border-radius: calc(var(--radius) - 2px);
154+
--boxel-border-radius-sm: calc(var(--radius) - 2px);
155155
--boxel-select-trigger-padding: 0;
156156
--boxel-dropdown-background-color: var(--popover);
157157
--boxel-dropdown-text-color: var(--foreground);
158-
--boxel-dropdown-hover-color: var(--hover, var(--boxel-100));
159-
--boxel-dropdown-highlight-color: var(--hover, var(--boxel-100));
158+
--boxel-dropdown-hover-color: var(--hover);
159+
--boxel-dropdown-highlight-color: var(--hover);
160160
}
161161
.pretui-cbx-row {
162162
display: flex;
163163
align-items: baseline;
164-
gap: var(--space-3, 8px);
164+
gap: 0.5rem;
165165
min-width: 0;
166166
}
167167
.pretui-cbx-label {
@@ -181,25 +181,35 @@ export class Combobox extends Component<ComboboxSignature> {
181181
.pretui-combobox :deep(.pretui-pickertrigger.ember-power-select-trigger) {
182182
display: flex;
183183
align-items: center;
184-
min-height: var(--control-h, 28px);
185-
padding: 2px 8px 2px 4px;
184+
min-height: var(--boxel-form-control-height-sm);
185+
padding-block: 0.125rem;
186+
padding-inline: 0.25rem 0.5rem;
186187
border: 0;
187-
border-radius: var(--radius);
188-
background: var(--field, var(--boxel-light));
188+
border-radius: calc(var(--radius) - 2px);
189+
background-color: var(--field);
189190
box-shadow: 0 0 0 1px var(--input);
190191
color: var(--foreground);
192+
font-size: var(--boxel-font-size-xs);
193+
letter-spacing: var(--boxel-lsp-xs);
194+
/* power-select's 1.75 line-height outgrows the height at 16px */
195+
line-height: var(--boxel-form-control-line-height-sm);
191196
width: 100%;
192197
}
198+
/* iOS zooms into inputs under 16px on focus */
199+
@media (pointer: coarse) {
200+
.pretui-combobox :deep(.pretui-pickertrigger.ember-power-select-trigger) {
201+
font-size: var(--boxel-form-control-touch-font-size);
202+
}
203+
}
193204
/* Transparent outline doubles the ring for forced-colors, where a
194205
box-shadow is not painted. */
195206
.pretui-combobox
196207
:deep(.pretui-pickertrigger.ember-power-select-trigger[aria-expanded='true']),
197208
.pretui-combobox
198209
:deep(.pretui-pickertrigger.ember-power-select-trigger:focus-within) {
199-
outline: 2px solid transparent;
210+
outline: 1.5px solid transparent;
200211
outline-offset: 1px;
201-
box-shadow: 0 0 0 2px var(--primary),
202-
var(--pretui-shadow-inset, inset 0 1px 2px rgb(0 0 0 / 0.16));
212+
box-shadow: 0 0 0 1.5px var(--ring);
203213
}
204214
.pretui-combobox[data-invalid]
205215
:deep(.pretui-pickertrigger.ember-power-select-trigger) {
@@ -265,8 +275,7 @@ export class Combobox extends Component<ComboboxSignature> {
265275
border: 0;
266276
background: transparent;
267277
font: inherit;
268-
font-size: var(--text-ui-md, 12.5px);
269-
letter-spacing: var(--track-ui, 0.01em);
278+
letter-spacing: inherit;
270279
color: var(--foreground);
271280
}
272281
.pretui-combobox
@@ -275,7 +284,7 @@ export class Combobox extends Component<ComboboxSignature> {
275284
}
276285
.pretui-combobox
277286
:deep(input.ember-power-select-trigger-multiple-input::placeholder) {
278-
color: var(--ink-3, var(--boxel-400));
287+
color: var(--muted-foreground);
279288
}
280289
.pretui-combobox
281290
:deep(.pretui-picker-dropdown.ember-power-select-dropdown) {

‎packages/pretui/components/combobox.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,20 @@ The difference from **Autocomplete** is what counts as a value. A combobox's val
3232

3333
Where Pretui is better: the explicit split from Autocomplete. Most kits ship one component with a `freeSolo` or `allowsCustomValue` flag, which means the value's _type_ changes with a boolean — a caller has to handle both shapes everywhere. Two components with two contracts is the clearer answer.
3434

35-
Where it is thinner: no async loading, no grouped or sectioned options, no multi-select, and no virtualisation — a very long option list renders in full. There is also no "create this" affordance, which is what a `freeSolo` flag usually buys.
35+
Where it is thinner: no async loading, no grouped or sectioned options, no multi-select, and no virtualization — a very long option list renders in full. There is also no "create this" affordance, which is what a `freeSolo` flag usually buys.
3636

3737
## Accessibility
3838

3939
- **The input is the combobox** and carries `aria-invalid` when `@invalid`, so the error state is announced rather than only dressed.
40-
- **`@label` names the input.** Without a wrapper supplying a label, an unnamed combobox is announced as an unlabelled text field with a popup.
40+
- **`@label` names the input.** Without a wrapper supplying a label, an unnamed combobox is announced as an unlabeled text field with a popup.
4141
- **`<:option>` is rendered into a row whose accessible name is computed**, so custom markup in the block cannot corrupt what the option announces.
4242
- **Open and close are reported through `@onOpenChange`**, which a caller needs if it is coordinating with anything else on screen — two popovers open at once is a focus problem.
4343
- **Local filtering means the list is complete and stable**, so a reader arrowing through options is not racing a fetch.
4444

4545
## Theming
4646

47-
`--pretui-combobox-max-height` bounds the dropdown, over the kit's shared overlay tokens — `--pretui-shadow-overlay` for the layer, `--pretui-shadow-control` and `--pretui-shadow-inset` for the field, `--pretui-destructive-ink` for the invalid state, and `--pretui-dur-snap` / `--pretui-ease-snap` for the open transition.
47+
The trigger has Input's field style: a `--field` face with `--foreground` text, an `--input` edge, a `--muted-foreground` placeholder, Button's corner (`calc(var(--radius) - 2px)`), a 1.75rem height (`--boxel-form-control-height-sm`) and `--boxel-font-size-xs` text. On touch screens (`pointer: coarse`) the text and placeholder are at least 16px (`--boxel-form-control-touch-font-size`), so iOS doesn't zoom into the field on focus.
48+
49+
Focus is a 1.5px `--ring`, as on Select; invalid is a `--destructive` edge. `--pretui-combobox-max-height` bounds the dropdown, and `--pretui-shadow-overlay` draws its layer.
4850

4951
The rules for the combobox's own elements sit in `@layer PretComponent`, so a caller's unlayered CSS overrides them without a more specific selector. The `:deep()` rules that restyle BoxelMultiSelect's power-select markup stay unlayered, because its own rules are unlayered and would beat them from inside a layer.

‎packages/pretui/components/email-input.gts‎

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { EmailInput as BoxelEmailInput } from '@cardstack/boxel-ui/components';
44
import type { EmailFormatValidationError } from '@cardstack/boxel-ui/helpers';
55
import { emit, firstDefined } from '../pretui-primitives';
66
import type { ControlAliasArgs } from '../pretui-primitives';
7+
import { TYPED_FONT } from '../internal/extras';
78

89
// boxel-ui's validation engine (format checks, debounced feedback,
910
// blur-gated error surfacing) is the value; only the cloth changes. The
@@ -55,27 +56,31 @@ export class EmailInput extends Component<EmailInputSignature> {
5556
@disabled={{this.disabled}}
5657
@required={{this.required}}
5758
id={{@controlId}}
59+
style={{TYPED_FONT}}
5860
/>
5961
</div>
6062
<style scoped>
6163
@layer PretComponent {
62-
/* Pretui control dress fed into boxel-ui's Input via its token
63-
channel: semantic vars (--background/--border/--ring read the
64-
Pretui field tokens) plus the --boxel-* dimension knobs. The two
65-
--boxel-sp-* overrides tune boxel's inner padding so the control
66-
lands on the h28 / 0-9px Pretui input geometry. */
64+
/* Input's field style, fed into boxel-ui's Input through its token
65+
channel: --background and --border plus the --boxel-* dimension
66+
knobs */
6767
.pretui-boxelwrap {
6868
width: 100%;
69-
font-size: var(--text-ui-md, 12.5px);
70-
letter-spacing: var(--track-ui, 0.01em);
71-
--background: var(--field, var(--boxel-light));
69+
font-size: var(--boxel-font-size-xs);
70+
letter-spacing: var(--boxel-lsp-xs);
71+
--background: var(--field);
7272
--border: var(--input);
73-
--ring: var(--primary);
74-
--boxel-form-control-height: var(--control-h, 28px);
75-
--boxel-input-height: var(--control-h, 28px);
76-
--boxel-form-control-border-radius: var(--radius);
77-
--boxel-sp-xs: 5px;
78-
--boxel-sp-sm: 9px;
73+
--boxel-form-control-height: var(--boxel-form-control-height-sm);
74+
--boxel-input-height: var(--boxel-form-control-height-sm);
75+
--boxel-form-control-border-radius: calc(var(--radius) - 2px);
76+
--boxel-sp-xs: var(--boxel-sp-3xs);
77+
--boxel-sp-sm: calc(0.625rem - 1px);
78+
}
79+
/* iOS zooms into inputs under 16px on focus */
80+
@media (pointer: coarse) {
81+
.pretui-boxelwrap {
82+
font-size: var(--boxel-form-control-touch-font-size);
83+
}
7984
}
8085
}
8186
</style>

‎packages/pretui/components/email-input.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ This is a **thin runtime wrap of boxel-ui's email input**, per the kit's standin
2020

2121
Against the field: **Web Awesome** has no email input — `wa-input type="email"` plus native constraint validation, with `label`/`hint` props and slots. **React Spectrum** likewise has no dedicated email field; `TextField type="email"` with a `validate` function. **shadcn** has none.
2222

23-
So the honest comparison is against native `type="email"` plus the browser's validation, and the difference is **when the error appears**. Native constraint validation fires on submit and shows a UA bubble that cannot be styled, positioned or announced consistently. The boxel engine's blur-gated, debounced feedback is the behaviour every serious form eventually implements by hand — showing an error while someone is still typing "chris@" is the single most common form-UX mistake.
23+
So the honest comparison is against native `type="email"` plus the browser's validation, and the difference is **when the error appears**. Native constraint validation fires on submit and shows a UA bubble that cannot be styled, positioned or announced consistently. The boxel engine's blur-gated, debounced feedback is the behavior every serious form eventually implements by hand — showing an error while someone is still typing "chris@" is the single most common form-UX mistake.
2424

2525
Where Pretui adds value over boxel-ui: the callback split (above) and API consistency with the rest of the typed-input family — **PhoneInput**, **UrlInput**, **NumberInput** and **PasswordInput** all take the same shape, so they are interchangeable at a call site.
2626

@@ -43,9 +43,9 @@ Gaps:
4343

4444
## Theming
4545

46-
Consumed: `--field` (control face), `--input` (hairline), `--primary` (focus ring, via `--ring`), `--ink-3` (placeholder ink), `--control-h`, `--radius`, `--text-ui-md`, `--text-ui-sm`, `--text-ui-xs`, `--track-ui`.
46+
Input's field style: a `--field` face with `--foreground` text, an `--input` edge, a `--muted-foreground` placeholder, Button's corner (`calc(var(--radius) - 2px)`), a 1.75rem height (`--boxel-form-control-height-sm`) and `--boxel-font-size-xs` text. On touch screens (`pointer: coarse`) the text and placeholder are at least 16px (`--boxel-form-control-touch-font-size`), so iOS doesn't zoom into the field on focus.
4747

48-
Forwarded into boxel-ui through the `--boxel-*` channel: the form-control height, radius, font sizes and spacing knobs, exactly as **Input** does — the two are dressed to be pixel-identical, so an email field and a text field in the same form align.
48+
Forwarded into boxel-ui through the `--boxel-*` channel: the form-control height, radius, font sizes and spacing knobs, as **Input** does, so an email field and a text field in the same form line up.
4949

5050
An inline `style` carries the font inheritance, because the UA font on native controls is one of the two places the custom-property channel cannot reach. The invalid dress arrives through the token channel from an enclosing **Field** (`--border` → `--destructive`), so it works here without the component knowing about it.
5151

‎packages/pretui/components/input-group.gts‎

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -238,22 +238,35 @@ export class InputGroup extends Component<InputGroupSignature> {
238238
</div>
239239
<style scoped>
240240
@layer PretComponent {
241-
/* Pretui group dress via the token channel: field face, hairline,
242-
h28, r(--radius), 12.5px type; the ring token powers boxel's
243-
focus-within outline. */
241+
/* Input's field style via the token channel; the text accessories
242+
and the helper row take the step below the field's text */
244243
.pretui-inputgroup {
245244
width: 100%;
246-
font-size: var(--text-ui-md, 12.5px);
247-
letter-spacing: var(--track-ui, 0.01em);
248-
--background: var(--field, var(--boxel-light));
245+
font-size: var(--boxel-font-size-xs);
246+
letter-spacing: var(--boxel-lsp-xs);
247+
/* the group's input inherits this line-height, and its padding-block
248+
is --boxel-sp-xxs: the line-height token assumes 0.25rem */
249+
line-height: var(--boxel-form-control-line-height-sm);
250+
--boxel-sp-xxs: 0.25rem;
251+
--background: var(--field);
249252
--border: var(--input);
250-
--ring: var(--primary);
251-
--boxel-form-control-height: var(--control-h, 28px);
252-
--boxel-input-height: var(--control-h, 28px);
253-
--boxel-form-control-border-radius: var(--radius);
254-
--boxel-font-size-sm: var(--text-ui-sm, 11.5px);
255-
--boxel-sp-xs: 5px;
256-
--boxel-sp-sm: 9px;
253+
--boxel-form-control-height: var(--boxel-form-control-height-sm);
254+
/* the group's controls inherit its min-height inside its 1px
255+
border, so the knob is the height less both borders */
256+
--boxel-input-height: calc(var(--boxel-form-control-height-sm) - 2px);
257+
--boxel-form-control-border-radius: calc(var(--radius) - 2px);
258+
--boxel-font-size-sm: var(--boxel-font-size-2xs);
259+
/* a Button add-on's own 14px text overflows the height */
260+
--boxel-button-font: 700 var(--boxel-font-size-2xs) /
261+
var(--boxel-form-control-line-height-sm) var(--boxel-font-family);
262+
--boxel-sp-xs: var(--boxel-sp-3xs);
263+
--boxel-sp-sm: calc(0.625rem - 1px);
264+
}
265+
/* iOS zooms into inputs under 16px on focus */
266+
@media (pointer: coarse) {
267+
.pretui-inputgroup {
268+
font-size: var(--boxel-form-control-touch-font-size);
269+
}
257270
}
258271
.pretui-inputgroup[data-disabled] {
259272
opacity: 0.75;

‎packages/pretui/components/input-group.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Where it is behind Bootstrap's version: no size axis, and the two-mode API is mo
3232

3333
## Accessibility
3434

35-
No APG pattern; it is a labelled control plus decorations, governed by WCAG **1.3.1**, **3.3.2** and **4.1.2**.
35+
No APG pattern; it is a labeled control plus decorations, governed by WCAG **1.3.1**, **3.3.2** and **4.1.2**.
3636

3737
Gaps, and the affix one is the interesting one:
3838

@@ -46,7 +46,7 @@ Gaps, and the affix one is the interesting one:
4646

4747
## Theming
4848

49-
Consumed: `--field`, `--input`, `--primary` (focus ring via `--ring`), `--ink-3`, `--control-h`, `--radius`, `--text-ui-md`, `--track-ui`, plus **Button**/**IconButton**/**Select** tokens for whatever accessories you place.
49+
Input's field style: a `--field` face with `--foreground` text, an `--input` edge, Button's corner (`calc(var(--radius) - 2px)`), a 1.75rem height (`--boxel-form-control-height-sm`) and `--boxel-font-size-xs` text. On touch screens (`pointer: coarse`) the text and placeholder are at least 16px (`--boxel-form-control-touch-font-size`), so iOS doesn't zoom into the field on focus. BoxelInputGroup has no placeholder setting, so the placeholder keeps the browser's color. Text accessories and the helper row are `--boxel-font-size-2xs`, a step below the field's text; **Button**/**IconButton**/**Select** accessories bring their own tokens.
5050

5151
Forwarded into boxel-ui through the `--boxel-*` channel. `data-state` (the validation enum) and `data-disabled` are reflected on the wrapper, so a stylesheet can dress both without reaching inside.
5252

‎packages/pretui/components/input.gts‎

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,17 @@ export interface InputSignature {
4848
// line-height; the inline constant can. Borders, padding-block and
4949
// --boxel-form-control-line-height-sm add up to
5050
// --boxel-form-control-height-sm (1.75rem); the line-height token assumes
51-
// this padding-block, so change them together. BoxelInput pads only the physical left, so the
52-
// inline style sets Select's inset on both inline sides, which also holds
53-
// in right-to-left text. A search input keeps BoxelInput's start padding,
54-
// which clears its icon. A color input keeps BoxelInput's `padding: 0`, or
55-
// its swatch shrinks inside the fixed box.
51+
// this padding-block, so change them together. BoxelInput pads only the
52+
// physical left, so the inline style sets Select's inset on both inline
53+
// sides, which also holds in right-to-left text. A search input matches
54+
// SearchInput: its corner and the start padding that clears the magnifier go
55+
// inline too. A color input keeps BoxelInput's `padding: 0`, or its swatch
56+
// shrinks inside the fixed box.
5657
const INPUT_FONT = htmlSafe(
5758
'font: inherit; letter-spacing: inherit; line-height: var(--boxel-form-control-line-height-sm); padding-block: 0.25rem; padding-inline: calc(0.625rem - 1px);',
5859
);
5960
const SEARCH_INPUT_FONT = htmlSafe(
60-
'font: inherit; letter-spacing: inherit; line-height: var(--boxel-form-control-line-height-sm); padding-block: 0.25rem; padding-inline-end: calc(0.625rem - 1px);',
61+
'font: inherit; letter-spacing: inherit; line-height: var(--boxel-form-control-line-height-sm); padding-block: 0.25rem; border-radius: calc(var(--radius) - 2px); padding-inline-start: 1.875rem; padding-inline-end: calc(0.625rem - 1px);',
6162
);
6263
const COLOR_INPUT_FONT = htmlSafe('font: inherit; letter-spacing: inherit;');
6364

@@ -104,6 +105,7 @@ export class Input extends Component<InputSignature> {
104105
<div
105106
class='pretui-inputwrap'
106107
data-invalid={{if this.invalid 'true'}}
108+
data-type={{@type}}
107109
data-test-pretui-input
108110
>
109111
<BoxelInput
@@ -147,6 +149,14 @@ export class Input extends Component<InputSignature> {
147149
/* Select's 0.625rem text inset, less this edge's 1px border */
148150
--boxel-sp-sm: calc(0.625rem - 1px);
149151
}
152+
/* SearchInput's search dress */
153+
.pretui-inputwrap[data-type='search'] {
154+
--boxel-input-search-background-color: var(--field);
155+
--boxel-input-search-color: var(--foreground);
156+
--boxel-input-search-icon-color: var(--muted-foreground);
157+
--boxel-input-search-icon-size: 0.875rem;
158+
--boxel-input-icon-size: 2.125rem;
159+
}
150160
/* iOS zooms into inputs under 16px on focus */
151161
@media (pointer: coarse) {
152162
.pretui-inputwrap {

0 commit comments

Comments
 (0)