Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion packages/base/structured-theme-variables.gts
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,6 @@ export default class ThemeVarField extends FieldDef {
@field trackingNormal = contains(CSSValueField, {
description: 'Specifies letter-spacing base value.',
});

// box-shadow variables
@field shadow2xs = contains(CSSValueField, {
description: 'Smallest shadow depth.',
Expand Down
122 changes: 119 additions & 3 deletions packages/boxel-ui/addon/src/components/input/index.gts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import element from '../../helpers/element.ts';
import optional from '../../helpers/optional.ts';
import pick from '../../helpers/pick.ts';
import { and, bool, eq, not } from '../../helpers/truth-helpers.ts';
import CheckMark from '../../icons/check-mark.gts';
import FailureBordered from '../../icons/failure-bordered.gts';
import IconSearch from '../../icons/icon-search.gts';
import LoadingIndicator from '../../icons/loading-indicator.gts';
Expand Down Expand Up @@ -75,7 +76,7 @@ export interface Signature {
size?: 'large' | 'default';
state?: InputValidationState;
type?: InputType;
value: string | number | null | undefined;
value: string | number | boolean | null | undefined;
};
Element: HTMLInputElement | HTMLTextAreaElement | HTMLDivElement;
}
Expand All @@ -95,6 +96,14 @@ export default class BoxelInput extends Component<Signature> {
return this.args.type === 'search';
}

private get isCheckbox() {
return this.args.type === 'checkbox';
}

private get onInputPath() {
return this.isCheckbox ? 'target.checked' : 'target.value';
}

private get type() {
let type = this.args.type;

Expand Down Expand Up @@ -141,6 +150,7 @@ export default class BoxelInput extends Component<Signature> {
'input-container'
has-validation=this.hasValidation
is-multiline=this.isMultiline
is-checkbox=this.isCheckbox
}}
>
{{#if (and (not @required) @optional)}}
Expand All @@ -159,7 +169,7 @@ export default class BoxelInput extends Component<Signature> {
}}
id={{this.id}}
type={{this.type}}
value={{@value}}
value={{unless this.isCheckbox @value}}
checked={{if (and (eq @type 'checkbox') (bool @value)) @value}}
placeholder={{@placeholder}}
min={{@min}}
Expand All @@ -182,13 +192,16 @@ export default class BoxelInput extends Component<Signature> {
data-test-boxel-input
data-test-boxel-input-id={{@id}}
data-test-boxel-input-validation-state={{if @disabled false @state}}
{{on 'input' (pick 'target.value' (optional @onInput))}}
{{on 'input' (pick this.onInputPath (optional @onInput))}}
{{on 'blur' (optional @onBlur)}}
{{on 'keypress' (optional @onKeyPress)}}
{{on 'focus' (optional @onFocus)}}
{{on 'change' (optional @onChange)}}
...attributes
/>
{{#if (and this.isCheckbox (bool @value))}}
<CheckMark class='checkbox-checkmark-icon' />
{{/if}}
{{#if this.isSearch}}
<div
class={{cn
Expand Down Expand Up @@ -432,6 +445,109 @@ export default class BoxelInput extends Component<Signature> {
animation: var(--boxel-infinite-spin-animation);
}
}

/* Checkbox type */
.input-container.is-checkbox {
--checkbox-size: var(
--boxel-checkbox-size,
var(--boxel-body-font-size)
);
--checkbox-border-radius: var(--boxel-checkbox-border-radius, 3px);
--checkbox-border-color: var(
--boxel-checkbox-border-color,
var(--border, var(--boxel-400))
);
--checkbox-background: var(
--boxel-checkbox-background-color,
var(--background, transparent)
);
--checkbox-checked-background: var(
--boxel-checkbox-checked-background-color,
var(--primary, var(--boxel-highlight))
);
--checkbox-checked-border-color: var(
--boxel-checkbox-checked-border-color,
var(--primary, var(--boxel-dark))
);
--checkbox-checkmark-color: var(
--boxel-checkbox-checkmark-color,
var(--primary-foreground, #333)
);
--checkbox-padding: var(
--boxel-checkbox-padding,
var(--spacing, 2px)
);

display: inline-grid;
grid-template-columns: auto;
grid-template-areas: 'input';
width: auto;
align-items: center;
justify-items: center;
position: relative;
}

.input-container.is-checkbox .optional,
.input-container.is-checkbox .error-message,
.input-container.is-checkbox .helper-text,
.input-container.is-checkbox .search-icon-container,
.input-container.is-checkbox .validation-icon-container {
display: none;
}

.checkbox-checkmark-icon {
grid-area: input;
pointer-events: none;
width: calc(var(--checkbox-size) * 0.8);
height: calc(var(--checkbox-size) * 0.8);
--icon-color: var(--checkbox-checkmark-color);
}

.boxel-input[type='checkbox'] {
grid-area: input;
appearance: none;
/* stylelint-disable-next-line property-no-vendor-prefix */
-webkit-appearance: none;
box-sizing: border-box;
width: var(--checkbox-size);
height: var(--checkbox-size);
min-height: unset;
padding: 0;
margin: 0;
border: 1px solid var(--checkbox-border-color);
border-radius: var(--checkbox-border-radius);
background-color: var(--checkbox-background);
box-shadow: none;
cursor: pointer;
transition:
background-color var(--boxel-transition),
border-color var(--boxel-transition);
flex-shrink: 0;
}

.boxel-input[type='checkbox']:checked {
background-color: var(--checkbox-checked-background);
border-color: var(--checkbox-checked-border-color);
}

.boxel-input[type='checkbox']:focus-visible {
outline: 2px solid var(--ring, var(--boxel-highlight));
outline-offset: 2px;
border-color: var(--checkbox-border-color);
}

.boxel-input[type='checkbox']:hover:not(:disabled):not(:checked) {
border-color: var(--boxel-dark);
}

.boxel-input[type='checkbox']:hover:not(:disabled):checked {
border-color: var(--checkbox-checked-border-color);
}

.boxel-input[type='checkbox']:disabled {
opacity: 0.5;
cursor: default;
}
}
</style>
</template>
Expand Down
101 changes: 98 additions & 3 deletions packages/boxel-ui/addon/src/components/input/usage.gts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const validStates = Object.values(InputValidationStates);

export default class InputUsage extends Component {
@tracked id = 'sample-input';
@tracked value = '';
@tracked value: string | boolean = '';
@tracked disabled = false;
@tracked readonly = false;
@tracked required = false;
Expand All @@ -39,6 +39,8 @@ export default class InputUsage extends Component {
@tracked state: InputValidationState = 'initial';
@tracked size: 'large' | 'default' = 'default';

@tracked isChecked = false;

defaultType = InputTypes.Text;
@tracked type = this.defaultType;

Expand All @@ -50,14 +52,23 @@ export default class InputUsage extends Component {

@action set(ev: Event): void {
let target = ev.target as HTMLInputElement;
this.value = target?.value;
this.validate(ev);
if (target.type === 'checkbox') {
this.isChecked = target.checked;
this.value = target.checked;
} else {
this.value = target?.value;
this.validate(ev);
}
}

@action logValue(value: any): void {
console.log(value);
}

@action toggleChecked(ev: Event): void {
this.isChecked = (ev.target as HTMLInputElement).checked;
}

@action validate(ev: Event): void {
let target = ev.target as HTMLInputElement;
if (!target.validity?.valid) {
Expand Down Expand Up @@ -223,6 +234,83 @@ export default class InputUsage extends Component {
</:cssVars>
</FreestyleUsage>

<FreestyleUsage @name='Checkbox'>
<:description>
Use
<code>@type='checkbox'</code>
with
<code>@value</code>
to render a styled checkbox. Use
<code>@onChange</code>
to handle state changes.
</:description>
<:example>
<label class='checkbox-example-label'>
<BoxelInput
@type='checkbox'
@value={{this.isChecked}}
@disabled={{this.disabled}}
@onChange={{this.toggleChecked}}
/>
<span>Example checkbox label</span>
</label>
</:example>
<:api as |Args|>
<Args.Bool
@name='value (checked)'
@value={{this.isChecked}}
@onInput={{fn (mut this.isChecked)}}
@description='Whether the checkbox is checked'
/>
<Args.Bool
@name='disabled'
@value={{this.disabled}}
@onInput={{fn (mut this.disabled)}}
/>
<Args.Action
@name='onChange'
@description='Receives the change Event'
/>
</:api>
<:cssVars as |Css|>
<Css.Basic
@name='--boxel-checkbox-size'
@type='dimension'
@description='Checkbox width and height (default: 18px)'
/>
<Css.Basic
@name='--boxel-checkbox-border-radius'
@type='dimension'
@description='Border radius (default: 3px)'
/>
<Css.Basic
@name='--boxel-checkbox-border-color'
@type='color'
@description='Unchecked border color'
/>
<Css.Basic
@name='--boxel-checkbox-background-color'
@type='color'
@description='Unchecked background color'
/>
<Css.Basic
@name='--boxel-checkbox-checked-background-color'
@type='color'
@description='Checked background color'
/>
<Css.Basic
@name='--boxel-checkbox-checked-border-color'
@type='color'
@description='Checked border color'
/>
<Css.Basic
@name='--boxel-checkbox-checkmark-color'
@type='color'
@description='Checkmark stroke color (default: #333)'
/>
</:cssVars>
</FreestyleUsage>

<FreestyleUsage class='remove-in-percy' @name='Usage on nested card'>
<:example>
<CardContainer @displayBoundaries={{true}}>
Expand Down Expand Up @@ -351,6 +439,13 @@ export default class InputUsage extends Component {
background-color: var(--sidebar);
color: var(--sidebar-foreground);
}
.checkbox-example-label {
display: flex;
align-items: center;
gap: var(--boxel-sp-2xs);
cursor: pointer;
font: var(--boxel-font-sm);
}
:deep(.FreestyleUsageCssVar input) {
display: none;
}
Expand Down
15 changes: 7 additions & 8 deletions packages/host/app/components/card-search/section-header.gts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { on } from '@ember/modifier';
import { action } from '@ember/object';
import Component from '@glimmer/component';

import { RealmIcon } from '@cardstack/boxel-ui/components';
import { BoxelInput, RealmIcon } from '@cardstack/boxel-ui/components';
import { eq } from '@cardstack/boxel-ui/helpers';

import type { RealmSectionInfo } from './search-content';
Expand Down Expand Up @@ -61,14 +60,14 @@ export default class SearchSheetSectionHeader extends Component<Signature> {
{{/unless}}
{{#if @showOnlyLabel}}
<label class='show-only'>
<input
type='checkbox'
checked={{@showOnlyChecked}}
{{on 'change' this.handleShowOnlyChange}}
data-test-search-sheet-show-only
/>
<span class='show-only-label'>Show only
<strong>{{@showOnlyLabel}}</strong></span>
<BoxelInput
@type='checkbox'
@value={{@showOnlyChecked}}
@onChange={{this.handleShowOnlyChange}}
data-test-search-sheet-show-only
/>
</label>
{{/if}}
</header>
Expand Down
Loading
Loading