From 8f5c3dde941e359626f51110fe9b432bef0bfd33 Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Thu, 5 Mar 2026 15:35:09 +0700 Subject: [PATCH 1/6] Update boxel ui input type checkbox --- .../addon/src/components/input/index.gts | 91 ++++++++++++++++++- .../addon/src/components/input/usage.gts | 90 ++++++++++++++++++ .../components/card-search/section-header.gts | 15 ++- .../operator-mode-card-catalog-test.gts | 3 +- 4 files changed, 189 insertions(+), 10 deletions(-) diff --git a/packages/boxel-ui/addon/src/components/input/index.gts b/packages/boxel-ui/addon/src/components/input/index.gts index afd28a2fe43..8440182a790 100644 --- a/packages/boxel-ui/addon/src/components/input/index.gts +++ b/packages/boxel-ui/addon/src/components/input/index.gts @@ -75,7 +75,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; } @@ -95,6 +95,10 @@ export default class BoxelInput extends Component { return this.args.type === 'search'; } + private get isCheckbox() { + return this.args.type === 'checkbox'; + } + private get type() { let type = this.args.type; @@ -141,6 +145,7 @@ export default class BoxelInput extends Component { 'input-container' has-validation=this.hasValidation is-multiline=this.isMultiline + is-checkbox=this.isCheckbox }} > {{#if (and (not @required) @optional)}} @@ -432,6 +437,90 @@ export default class BoxelInput extends Component { animation: var(--boxel-infinite-spin-animation); } } + + /* Checkbox type */ + .input-container.is-checkbox { + display: inline-flex; + align-items: center; + width: auto; + } + + .boxel-input[type='checkbox'] { + --checkbox-size: var(--boxel-checkbox-size, 18px); + --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, + #333 + ); + + appearance: none; + /* stylelint-disable-next-line property-no-vendor-prefix */ + -webkit-appearance: none; + position: relative; + 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']:checked::after { + content: ''; + position: absolute; + left: 5px; + top: 1px; + width: 5px; + height: 9px; + border: solid var(--checkbox-checkmark-color); + border-width: 0 2px 2px 0; + transform: rotate(45deg); + } + + .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']:disabled { + opacity: 0.5; + cursor: default; + } } diff --git a/packages/boxel-ui/addon/src/components/input/usage.gts b/packages/boxel-ui/addon/src/components/input/usage.gts index 2158a6279ea..e6f45a6f877 100644 --- a/packages/boxel-ui/addon/src/components/input/usage.gts +++ b/packages/boxel-ui/addon/src/components/input/usage.gts @@ -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; @@ -58,6 +60,10 @@ export default class InputUsage extends Component { 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) { @@ -223,6 +229,83 @@ export default class InputUsage extends Component { + + <:description> + Use + @type='checkbox' + with + @value + to render a styled checkbox. Use + @onChange + to handle state changes. + + <:example> + + + <:api as |Args|> + + + + + <:cssVars as |Css|> + + + + + + + + + + <:example> @@ -351,6 +434,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-xxs); + cursor: pointer; + font: var(--boxel-font-sm); + } :deep(.FreestyleUsageCssVar input) { display: none; } diff --git a/packages/host/app/components/card-search/section-header.gts b/packages/host/app/components/card-search/section-header.gts index fdf2afb5b3f..c8ff9f9ff65 100644 --- a/packages/host/app/components/card-search/section-header.gts +++ b/packages/host/app/components/card-search/section-header.gts @@ -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'; @@ -61,14 +60,14 @@ export default class SearchSheetSectionHeader extends Component { {{/unless}} {{#if @showOnlyLabel}} {{/if}} diff --git a/packages/host/tests/integration/components/operator-mode-card-catalog-test.gts b/packages/host/tests/integration/components/operator-mode-card-catalog-test.gts index ed6aef77aec..419a274a099 100644 --- a/packages/host/tests/integration/components/operator-mode-card-catalog-test.gts +++ b/packages/host/tests/integration/components/operator-mode-card-catalog-test.gts @@ -20,7 +20,7 @@ import { import OperatorMode from '@cardstack/host/components/operator-mode/container'; -import { testRealmURL } from '../../helpers'; +import { percySnapshot, testRealmURL } from '../../helpers'; import { renderComponent } from '../../helpers/render-component'; import { setupOperatorModeTests } from './operator-mode/setup'; @@ -965,6 +965,7 @@ module('Integration | operator-mode | card catalog', function (hooks) { await click(`[data-test-open-search-field]`); await fillIn(`[data-test-search-field]`, 'ma'); await waitFor('[data-test-search-sheet-show-only]'); + await percySnapshot(assert); await click('[data-test-search-sheet-show-only]'); const collapsedBlocks = document.querySelectorAll( '.search-result-block--collapsed', From 87f56067a689a547f302d293f8fcf1a3eb7f2233 Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Tue, 10 Mar 2026 12:22:46 +0700 Subject: [PATCH 2/6] Ensure theme works in checkbox input --- packages/base/structured-theme-variables.gts | 4 + .../addon/src/components/input/index.gts | 36 +- .../addon/src/components/input/usage.gts | 2 +- packages/experiments-realm/themed-invoice.gts | 43 +- .../host/tests/acceptance/theme-card-test.gts | 414 +++++++++++++++++- 5 files changed, 487 insertions(+), 12 deletions(-) diff --git a/packages/base/structured-theme-variables.gts b/packages/base/structured-theme-variables.gts index a97fdb82bea..c0887b0c716 100644 --- a/packages/base/structured-theme-variables.gts +++ b/packages/base/structured-theme-variables.gts @@ -450,6 +450,10 @@ export default class ThemeVarField extends FieldDef { @field trackingNormal = contains(CSSValueField, { description: 'Specifies letter-spacing base value.', }); + @field boxelBodyFontSize = contains(CSSValueField, { + description: + 'Base body font size. Also controls checkbox and radio button dimensions.', + }); // box-shadow variables @field shadow2xs = contains(CSSValueField, { diff --git a/packages/boxel-ui/addon/src/components/input/index.gts b/packages/boxel-ui/addon/src/components/input/index.gts index 8440182a790..f799357ffc9 100644 --- a/packages/boxel-ui/addon/src/components/input/index.gts +++ b/packages/boxel-ui/addon/src/components/input/index.gts @@ -440,13 +440,22 @@ export default class BoxelInput extends Component { /* Checkbox type */ .input-container.is-checkbox { - display: inline-flex; - align-items: center; + display: inline-grid; + grid-template-columns: auto; + grid-template-areas: + 'optional' + 'input' + 'error' + 'helper'; width: auto; + align-items: center; } .boxel-input[type='checkbox'] { - --checkbox-size: var(--boxel-checkbox-size, 18px); + --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, @@ -466,9 +475,14 @@ export default class BoxelInput extends Component { ); --checkbox-checkmark-color: var( --boxel-checkbox-checkmark-color, - #333 + var(--primary-foreground, #333) + ); + --checkbox-padding: var( + --boxel-checkbox-padding, + var(--spacing, 2px) ); + grid-area: input; appearance: none; /* stylelint-disable-next-line property-no-vendor-prefix */ -webkit-appearance: none; @@ -498,13 +512,17 @@ export default class BoxelInput extends Component { .boxel-input[type='checkbox']:checked::after { content: ''; position: absolute; - left: 5px; - top: 1px; - width: 5px; - height: 9px; + left: 50%; + top: 50%; + width: calc( + (var(--checkbox-size) - 2 * var(--checkbox-padding)) * 0.35 + ); + height: calc( + (var(--checkbox-size) - 2 * var(--checkbox-padding)) * 0.65 + ); border: solid var(--checkbox-checkmark-color); border-width: 0 2px 2px 0; - transform: rotate(45deg); + transform: translate(-50%, -60%) rotate(45deg); } .boxel-input[type='checkbox']:focus-visible { diff --git a/packages/boxel-ui/addon/src/components/input/usage.gts b/packages/boxel-ui/addon/src/components/input/usage.gts index e6f45a6f877..cf85e4e62e3 100644 --- a/packages/boxel-ui/addon/src/components/input/usage.gts +++ b/packages/boxel-ui/addon/src/components/input/usage.gts @@ -437,7 +437,7 @@ export default class InputUsage extends Component { .checkbox-example-label { display: flex; align-items: center; - gap: var(--boxel-sp-xxs); + gap: var(--boxel-sp-2xs); cursor: pointer; font: var(--boxel-font-sm); } diff --git a/packages/experiments-realm/themed-invoice.gts b/packages/experiments-realm/themed-invoice.gts index d9e41aa36ef..ff6d7981023 100644 --- a/packages/experiments-realm/themed-invoice.gts +++ b/packages/experiments-realm/themed-invoice.gts @@ -10,7 +10,9 @@ import NumberField from 'https://cardstack.com/base/number'; import DateField from 'https://cardstack.com/base/date'; import { dayjsFormat } from '@cardstack/boxel-ui/helpers'; -import { CardContainer } from '@cardstack/boxel-ui/components'; +import { CardContainer, BoxelInput } from '@cardstack/boxel-ui/components'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; import { BrandTheme as Theme, sanitize } from './theme'; @@ -38,6 +40,12 @@ export class ThemedInvoice extends CardDef { @field companyAddress = contains(StringField); static isolated = class Isolated extends Component { + @tracked isPaymentConfirmed = false; + + @action togglePaymentConfirmed() { + this.isPaymentConfirmed = !this.isPaymentConfirmed; + } + }; diff --git a/packages/host/tests/acceptance/theme-card-test.gts b/packages/host/tests/acceptance/theme-card-test.gts index 849c10cc672..1e6f3354423 100644 --- a/packages/host/tests/acceptance/theme-card-test.gts +++ b/packages/host/tests/acceptance/theme-card-test.gts @@ -1,11 +1,14 @@ import { click, fillIn } from '@ember/test-helpers'; +import { getService } from '@universal-ember/test-support'; + import window from 'ember-window-mock'; import { module, test } from 'qunit'; +import { BoxelInput } from '@cardstack/boxel-ui/components'; import { dasherize } from '@cardstack/boxel-ui/helpers'; -import { Deferred } from '@cardstack/runtime-common'; +import { baseRealm, Deferred } from '@cardstack/runtime-common'; import { percySnapshot, @@ -93,6 +96,40 @@ const ROOT_STYLE_ATTRS = Object.entries(ROOT_CSS_VARS) .map(([key, val]) => [`--${dasherize(key)}`, val].join(': ')) .join('; '); +const DARK_GOLD_THEME_VARS = { + primary: '#ffd700', + primaryForeground: '#0a0f23', + border: '#3a4073', + background: '#1a1f3a', + spacing: '0.3rem', + boxelBodyFontSize: '18px', +}; + +const OCEAN_BLUE_THEME_VARS = { + primary: '#0058A3', + primaryForeground: '#FFFFFF', + border: '#003B6F', + background: '#E3F2FD', + spacing: '0.25rem', + boxelBodyFontSize: '14px', +}; + +const FOREST_GREEN_THEME_VARS = { + primary: '#2e7d32', + primaryForeground: '#FFFFFF', + border: '#1b5e20', + background: '#E8F5E9', + spacing: '0.25rem', + boxelBodyFontSize: '16px', +}; + +function hexToRgb(hex: string): string { + const r = parseInt(hex.slice(1, 3), 16); + const g = parseInt(hex.slice(3, 5), 16); + const b = parseInt(hex.slice(5, 7), 16); + return `rgb(${r}, ${g}, ${b})`; +} + const SOFT_POP_VARS = `:root { --background: oklch(0.9789 0.0082 121.6272); --foreground: oklch(0 0 0); @@ -172,11 +209,42 @@ module('Acceptance | theme-card-test', function (hooks) { setupUserSubscription(); setupAuthEndpoints(); + let loader = getService('loader-service').loader; + let cardApi: typeof import('https://cardstack.com/base/card-api'); + let booleanMod: typeof import('https://cardstack.com/base/boolean'); + cardApi = await loader.import(`${baseRealm.url}card-api`); + booleanMod = await loader.import(`${baseRealm.url}boolean`); + + let { field, contains, CardDef, Component } = cardApi; + let { default: BooleanField } = booleanMod; + + class CheckboxCard extends CardDef { + static displayName = 'Checkbox Card'; + @field isChecked = contains(BooleanField); + + static isolated = class Isolated extends Component { + + }; + } + await withCachedRealmSetup(async () => { await setupAcceptanceTestRealm({ mockMatrixUtils, contents: { ...SYSTEM_CARD_FIXTURE_CONTENTS, + 'checkbox-card.gts': { CheckboxCard }, '.realm.json': { name: 'Theme Playground', }, @@ -270,6 +338,129 @@ module('Acceptance | theme-card-test', function (hooks) { }, }, }, + 'dark-gold-theme.json': { + data: { + meta: { + adoptsFrom: { + name: 'default', + module: 'https://cardstack.com/base/structured-theme', + }, + }, + type: 'card', + attributes: { + cardInfo: { + name: 'Dark Gold', + }, + rootVariables: DARK_GOLD_THEME_VARS, + }, + }, + }, + 'ocean-blue-theme.json': { + data: { + meta: { + adoptsFrom: { + name: 'default', + module: 'https://cardstack.com/base/structured-theme', + }, + }, + type: 'card', + attributes: { + cardInfo: { + name: 'Ocean Blue', + }, + rootVariables: OCEAN_BLUE_THEME_VARS, + }, + }, + }, + 'forest-green-theme.json': { + data: { + meta: { + adoptsFrom: { + name: 'default', + module: 'https://cardstack.com/base/structured-theme', + }, + }, + type: 'card', + attributes: { + cardInfo: { + name: 'Forest Green', + }, + rootVariables: FOREST_GREEN_THEME_VARS, + }, + }, + }, + 'checkbox-dark-gold.json': { + data: { + meta: { + adoptsFrom: { + name: 'CheckboxCard', + module: `${testRealmURL}checkbox-card`, + }, + }, + type: 'card', + attributes: { + isChecked: true, + cardInfo: { + name: 'Dark Gold Checkbox', + }, + }, + relationships: { + 'cardInfo.theme': { + links: { + self: `${testRealmURL}dark-gold-theme`, + }, + }, + }, + }, + }, + 'checkbox-ocean-blue.json': { + data: { + meta: { + adoptsFrom: { + name: 'CheckboxCard', + module: `${testRealmURL}checkbox-card`, + }, + }, + type: 'card', + attributes: { + isChecked: true, + cardInfo: { + name: 'Ocean Blue Checkbox', + }, + }, + relationships: { + 'cardInfo.theme': { + links: { + self: `${testRealmURL}ocean-blue-theme`, + }, + }, + }, + }, + }, + 'checkbox-forest-green.json': { + data: { + meta: { + adoptsFrom: { + name: 'CheckboxCard', + module: `${testRealmURL}checkbox-card`, + }, + }, + type: 'card', + attributes: { + isChecked: true, + cardInfo: { + name: 'Forest Green Checkbox', + }, + }, + relationships: { + 'cardInfo.theme': { + links: { + self: `${testRealmURL}forest-green-theme`, + }, + }, + }, + }, + }, }, }); }); @@ -588,4 +779,225 @@ module('Acceptance | theme-card-test', function (hooks) { assert.dom('[data-test-css-field]').containsText('/* No CSS defined */'); }); }); + + module('themed-checkbox', () => { + test('dark gold theme applies correct checkbox styles', async function (assert) { + let cardId = `${testRealmURL}checkbox-dark-gold`; + await visitOperatorMode({ + stacks: [[{ id: cardId, format: 'isolated' }]], + }); + + assert + .dom(`[data-test-card="${cardId}"]`) + .hasClass('boxel-card-container--themed'); + + let container = document.querySelector( + `[data-test-card="${cardId}"]`, + ); + assert.ok(container, 'themed card container is present'); + + let styleAttr = container?.getAttribute('style') ?? ''; + assert.ok( + styleAttr.includes('--primary: #ffd700'), + 'inline style includes --primary', + ); + assert.ok( + styleAttr.includes('--border: #3a4073'), + 'inline style includes --border', + ); + assert.ok( + styleAttr.includes('--background: #1a1f3a'), + 'inline style includes --background', + ); + assert.ok( + styleAttr.includes('--boxel-body-font-size: 18px'), + 'inline style includes --boxel-body-font-size', + ); + + assert + .dom('[data-test-checkbox-checked]') + .exists('checked checkbox renders'); + assert + .dom('[data-test-checkbox-unchecked]') + .exists('unchecked checkbox renders'); + + let checkedEl = document.querySelector( + '[data-test-checkbox-checked]', + ); + let uncheckedEl = document.querySelector( + '[data-test-checkbox-unchecked]', + ); + + assert.strictEqual( + window + .getComputedStyle(checkedEl!) + .getPropertyValue('background-color'), + hexToRgb('#ffd700'), + 'checked checkbox background-color matches --primary', + ); + assert.strictEqual( + window.getComputedStyle(uncheckedEl!).getPropertyValue('border-color'), + hexToRgb('#3a4073'), + 'unchecked checkbox border-color matches --border', + ); + assert.strictEqual( + window + .getComputedStyle(uncheckedEl!) + .getPropertyValue('background-color'), + hexToRgb('#1a1f3a'), + 'unchecked checkbox background-color matches --background', + ); + assert.strictEqual( + window.getComputedStyle(checkedEl!).getPropertyValue('width'), + '18px', + 'checkbox size matches --boxel-body-font-size', + ); + }); + + test('ocean blue theme applies correct checkbox styles', async function (assert) { + let cardId = `${testRealmURL}checkbox-ocean-blue`; + await visitOperatorMode({ + stacks: [[{ id: cardId, format: 'isolated' }]], + }); + + assert + .dom(`[data-test-card="${cardId}"]`) + .hasClass('boxel-card-container--themed'); + + let container = document.querySelector( + `[data-test-card="${cardId}"]`, + ); + assert.ok(container, 'themed card container is present'); + + let styleAttr = container?.getAttribute('style') ?? ''; + assert.ok( + styleAttr.includes('--primary: #0058A3'), + 'inline style includes --primary', + ); + assert.ok( + styleAttr.includes('--border: #003B6F'), + 'inline style includes --border', + ); + assert.ok( + styleAttr.includes('--background: #E3F2FD'), + 'inline style includes --background', + ); + assert.ok( + styleAttr.includes('--boxel-body-font-size: 14px'), + 'inline style includes --boxel-body-font-size', + ); + + assert + .dom('[data-test-checkbox-checked]') + .exists('checked checkbox renders'); + assert + .dom('[data-test-checkbox-unchecked]') + .exists('unchecked checkbox renders'); + + let checkedEl = document.querySelector( + '[data-test-checkbox-checked]', + ); + let uncheckedEl = document.querySelector( + '[data-test-checkbox-unchecked]', + ); + + assert.strictEqual( + window + .getComputedStyle(checkedEl!) + .getPropertyValue('background-color'), + hexToRgb('#0058A3'), + 'checked checkbox background-color matches --primary', + ); + assert.strictEqual( + window.getComputedStyle(uncheckedEl!).getPropertyValue('border-color'), + hexToRgb('#003B6F'), + 'unchecked checkbox border-color matches --border', + ); + assert.strictEqual( + window + .getComputedStyle(uncheckedEl!) + .getPropertyValue('background-color'), + hexToRgb('#E3F2FD'), + 'unchecked checkbox background-color matches --background', + ); + assert.strictEqual( + window.getComputedStyle(checkedEl!).getPropertyValue('width'), + '14px', + 'checkbox size matches --boxel-body-font-size', + ); + }); + + test('forest green theme applies correct checkbox styles', async function (assert) { + let cardId = `${testRealmURL}checkbox-forest-green`; + await visitOperatorMode({ + stacks: [[{ id: cardId, format: 'isolated' }]], + }); + + assert + .dom(`[data-test-card="${cardId}"]`) + .hasClass('boxel-card-container--themed'); + + let container = document.querySelector( + `[data-test-card="${cardId}"]`, + ); + assert.ok(container, 'themed card container is present'); + + let styleAttr = container?.getAttribute('style') ?? ''; + assert.ok( + styleAttr.includes('--primary: #2e7d32'), + 'inline style includes --primary', + ); + assert.ok( + styleAttr.includes('--border: #1b5e20'), + 'inline style includes --border', + ); + assert.ok( + styleAttr.includes('--background: #E8F5E9'), + 'inline style includes --background', + ); + assert.ok( + styleAttr.includes('--boxel-body-font-size: 16px'), + 'inline style includes --boxel-body-font-size', + ); + + assert + .dom('[data-test-checkbox-checked]') + .exists('checked checkbox renders'); + assert + .dom('[data-test-checkbox-unchecked]') + .exists('unchecked checkbox renders'); + + let checkedEl = document.querySelector( + '[data-test-checkbox-checked]', + ); + let uncheckedEl = document.querySelector( + '[data-test-checkbox-unchecked]', + ); + + assert.strictEqual( + window + .getComputedStyle(checkedEl!) + .getPropertyValue('background-color'), + hexToRgb('#2e7d32'), + 'checked checkbox background-color matches --primary', + ); + assert.strictEqual( + window.getComputedStyle(uncheckedEl!).getPropertyValue('border-color'), + hexToRgb('#1b5e20'), + 'unchecked checkbox border-color matches --border', + ); + assert.strictEqual( + window + .getComputedStyle(uncheckedEl!) + .getPropertyValue('background-color'), + hexToRgb('#E8F5E9'), + 'unchecked checkbox background-color matches --background', + ); + assert.strictEqual( + window.getComputedStyle(checkedEl!).getPropertyValue('width'), + '16px', + 'checkbox size matches --boxel-body-font-size', + ); + }); + }); }); From 00c4b364b4cd2ccb84e57a37830fa447ab170e32 Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Tue, 10 Mar 2026 13:09:26 +0700 Subject: [PATCH 3/6] Address feedback --- .../boxel-ui/addon/src/components/input/index.gts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/boxel-ui/addon/src/components/input/index.gts b/packages/boxel-ui/addon/src/components/input/index.gts index f799357ffc9..025407012da 100644 --- a/packages/boxel-ui/addon/src/components/input/index.gts +++ b/packages/boxel-ui/addon/src/components/input/index.gts @@ -66,7 +66,7 @@ export interface Signature { onBlur?: (ev: Event) => void; onChange?: (ev: Event) => void; onFocus?: (ev: Event) => void; - onInput?: (val: string) => void; + onInput?: (val: string | boolean) => void; onKeyPress?: (ev: KeyboardEvent) => Promise | void; optional?: boolean; placeholder?: string; @@ -99,6 +99,10 @@ export default class BoxelInput extends Component { return this.args.type === 'checkbox'; } + private get onInputPath() { + return this.isCheckbox ? 'target.checked' : 'target.value'; + } + private get type() { let type = this.args.type; @@ -164,7 +168,7 @@ export default class BoxelInput extends Component { }} 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}} @@ -187,7 +191,7 @@ export default class BoxelInput extends Component { 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)}} @@ -535,6 +539,10 @@ export default class BoxelInput extends Component { 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; From 544df96f4beae7397d574c60c1841fd1d023fd65 Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Tue, 10 Mar 2026 13:10:51 +0700 Subject: [PATCH 4/6] Revert unnecessary changes --- packages/experiments-realm/themed-invoice.gts | 43 +------------------ 1 file changed, 1 insertion(+), 42 deletions(-) diff --git a/packages/experiments-realm/themed-invoice.gts b/packages/experiments-realm/themed-invoice.gts index ff6d7981023..d9e41aa36ef 100644 --- a/packages/experiments-realm/themed-invoice.gts +++ b/packages/experiments-realm/themed-invoice.gts @@ -10,9 +10,7 @@ import NumberField from 'https://cardstack.com/base/number'; import DateField from 'https://cardstack.com/base/date'; import { dayjsFormat } from '@cardstack/boxel-ui/helpers'; -import { CardContainer, BoxelInput } from '@cardstack/boxel-ui/components'; -import { tracked } from '@glimmer/tracking'; -import { action } from '@ember/object'; +import { CardContainer } from '@cardstack/boxel-ui/components'; import { BrandTheme as Theme, sanitize } from './theme'; @@ -40,12 +38,6 @@ export class ThemedInvoice extends CardDef { @field companyAddress = contains(StringField); static isolated = class Isolated extends Component { - @tracked isPaymentConfirmed = false; - - @action togglePaymentConfirmed() { - this.isPaymentConfirmed = !this.isPaymentConfirmed; - } - }; From 0238546a44bf816d1661fd5371a427dfe874769d Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Tue, 10 Mar 2026 13:24:52 +0700 Subject: [PATCH 5/6] lint fix --- packages/boxel-ui/addon/src/components/input/index.gts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/boxel-ui/addon/src/components/input/index.gts b/packages/boxel-ui/addon/src/components/input/index.gts index 025407012da..646d3a987f6 100644 --- a/packages/boxel-ui/addon/src/components/input/index.gts +++ b/packages/boxel-ui/addon/src/components/input/index.gts @@ -66,7 +66,7 @@ export interface Signature { onBlur?: (ev: Event) => void; onChange?: (ev: Event) => void; onFocus?: (ev: Event) => void; - onInput?: (val: string | boolean) => void; + onInput?: (val: string) => void; onKeyPress?: (ev: KeyboardEvent) => Promise | void; optional?: boolean; placeholder?: string; From ce7a56bff067f2d1a18ced5e7fe2e8e0ca8df13f Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Thu, 12 Mar 2026 14:15:43 +0700 Subject: [PATCH 6/6] Update checkmark size --- packages/base/structured-theme-variables.gts | 5 -- .../addon/src/components/input/index.gts | 59 ++++++++++--------- .../addon/src/components/input/usage.gts | 11 +++- .../host/tests/acceptance/theme-card-test.gts | 30 ++++++---- 4 files changed, 56 insertions(+), 49 deletions(-) diff --git a/packages/base/structured-theme-variables.gts b/packages/base/structured-theme-variables.gts index c0887b0c716..cbbb2bfefda 100644 --- a/packages/base/structured-theme-variables.gts +++ b/packages/base/structured-theme-variables.gts @@ -450,11 +450,6 @@ export default class ThemeVarField extends FieldDef { @field trackingNormal = contains(CSSValueField, { description: 'Specifies letter-spacing base value.', }); - @field boxelBodyFontSize = contains(CSSValueField, { - description: - 'Base body font size. Also controls checkbox and radio button dimensions.', - }); - // box-shadow variables @field shadow2xs = contains(CSSValueField, { description: 'Smallest shadow depth.', diff --git a/packages/boxel-ui/addon/src/components/input/index.gts b/packages/boxel-ui/addon/src/components/input/index.gts index 646d3a987f6..010a7dde5d2 100644 --- a/packages/boxel-ui/addon/src/components/input/index.gts +++ b/packages/boxel-ui/addon/src/components/input/index.gts @@ -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'; @@ -198,6 +199,9 @@ export default class BoxelInput extends Component { {{on 'change' (optional @onChange)}} ...attributes /> + {{#if (and this.isCheckbox (bool @value))}} + + {{/if}} {{#if this.isSearch}}
{ /* Checkbox type */ .input-container.is-checkbox { - display: inline-grid; - grid-template-columns: auto; - grid-template-areas: - 'optional' - 'input' - 'error' - 'helper'; - width: auto; - align-items: center; - } - - .boxel-input[type='checkbox'] { --checkbox-size: var( --boxel-checkbox-size, var(--boxel-body-font-size) @@ -486,11 +478,36 @@ export default class BoxelInput extends Component { 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; - position: relative; box-sizing: border-box; width: var(--checkbox-size); height: var(--checkbox-size); @@ -513,22 +530,6 @@ export default class BoxelInput extends Component { border-color: var(--checkbox-checked-border-color); } - .boxel-input[type='checkbox']:checked::after { - content: ''; - position: absolute; - left: 50%; - top: 50%; - width: calc( - (var(--checkbox-size) - 2 * var(--checkbox-padding)) * 0.35 - ); - height: calc( - (var(--checkbox-size) - 2 * var(--checkbox-padding)) * 0.65 - ); - border: solid var(--checkbox-checkmark-color); - border-width: 0 2px 2px 0; - transform: translate(-50%, -60%) rotate(45deg); - } - .boxel-input[type='checkbox']:focus-visible { outline: 2px solid var(--ring, var(--boxel-highlight)); outline-offset: 2px; diff --git a/packages/boxel-ui/addon/src/components/input/usage.gts b/packages/boxel-ui/addon/src/components/input/usage.gts index cf85e4e62e3..517b3aae216 100644 --- a/packages/boxel-ui/addon/src/components/input/usage.gts +++ b/packages/boxel-ui/addon/src/components/input/usage.gts @@ -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; @@ -52,8 +52,13 @@ 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 { diff --git a/packages/host/tests/acceptance/theme-card-test.gts b/packages/host/tests/acceptance/theme-card-test.gts index 1e6f3354423..e28fe026250 100644 --- a/packages/host/tests/acceptance/theme-card-test.gts +++ b/packages/host/tests/acceptance/theme-card-test.gts @@ -102,7 +102,6 @@ const DARK_GOLD_THEME_VARS = { border: '#3a4073', background: '#1a1f3a', spacing: '0.3rem', - boxelBodyFontSize: '18px', }; const OCEAN_BLUE_THEME_VARS = { @@ -111,7 +110,6 @@ const OCEAN_BLUE_THEME_VARS = { border: '#003B6F', background: '#E3F2FD', spacing: '0.25rem', - boxelBodyFontSize: '14px', }; const FOREST_GREEN_THEME_VARS = { @@ -120,7 +118,6 @@ const FOREST_GREEN_THEME_VARS = { border: '#1b5e20', background: '#E8F5E9', spacing: '0.25rem', - boxelBodyFontSize: '16px', }; function hexToRgb(hex: string): string { @@ -352,6 +349,9 @@ module('Acceptance | theme-card-test', function (hooks) { name: 'Dark Gold', }, rootVariables: DARK_GOLD_THEME_VARS, + typography: { + body: { fontSize: '18px' }, + }, }, }, }, @@ -369,6 +369,9 @@ module('Acceptance | theme-card-test', function (hooks) { name: 'Ocean Blue', }, rootVariables: OCEAN_BLUE_THEME_VARS, + typography: { + body: { fontSize: '14px' }, + }, }, }, }, @@ -386,6 +389,9 @@ module('Acceptance | theme-card-test', function (hooks) { name: 'Forest Green', }, rootVariables: FOREST_GREEN_THEME_VARS, + typography: { + body: { fontSize: '16px' }, + }, }, }, }, @@ -810,8 +816,8 @@ module('Acceptance | theme-card-test', function (hooks) { 'inline style includes --background', ); assert.ok( - styleAttr.includes('--boxel-body-font-size: 18px'), - 'inline style includes --boxel-body-font-size', + styleAttr.includes('--theme-body-font-size: 18px'), + 'inline style includes --theme-body-font-size', ); assert @@ -850,7 +856,7 @@ module('Acceptance | theme-card-test', function (hooks) { assert.strictEqual( window.getComputedStyle(checkedEl!).getPropertyValue('width'), '18px', - 'checkbox size matches --boxel-body-font-size', + 'checkbox size matches --theme-body-font-size', ); }); @@ -883,8 +889,8 @@ module('Acceptance | theme-card-test', function (hooks) { 'inline style includes --background', ); assert.ok( - styleAttr.includes('--boxel-body-font-size: 14px'), - 'inline style includes --boxel-body-font-size', + styleAttr.includes('--theme-body-font-size: 14px'), + 'inline style includes --theme-body-font-size', ); assert @@ -923,7 +929,7 @@ module('Acceptance | theme-card-test', function (hooks) { assert.strictEqual( window.getComputedStyle(checkedEl!).getPropertyValue('width'), '14px', - 'checkbox size matches --boxel-body-font-size', + 'checkbox size matches --theme-body-font-size', ); }); @@ -956,8 +962,8 @@ module('Acceptance | theme-card-test', function (hooks) { 'inline style includes --background', ); assert.ok( - styleAttr.includes('--boxel-body-font-size: 16px'), - 'inline style includes --boxel-body-font-size', + styleAttr.includes('--theme-body-font-size: 16px'), + 'inline style includes --theme-body-font-size', ); assert @@ -996,7 +1002,7 @@ module('Acceptance | theme-card-test', function (hooks) { assert.strictEqual( window.getComputedStyle(checkedEl!).getPropertyValue('width'), '16px', - 'checkbox size matches --boxel-body-font-size', + 'checkbox size matches --theme-body-font-size', ); }); });