Skip to content
Open
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
{
"cSpell.words": ["boxel"],
"editor.formatOnSave": true,
"files.associations": {
"*.gts": "glimmer-ts",
"*.gjs": "glimmer-js"
},
"[glimmer-js]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
Expand Down
2 changes: 2 additions & 0 deletions packages/boxel-ui/addon/src/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
cardsInColumn,
columnCount as kanbanColumnCount,
findInsertionFromPointer,
KanbanColumnConfigSidebar,
KanbanDragManager,
KanbanPlane,
resolveInsertion,
Expand Down Expand Up @@ -138,6 +139,7 @@ export {
GridContainer,
Header,
IconButton,
KanbanColumnConfigSidebar,
kanbanColumnCount,
KanbanDragManager,
KanbanPlane,
Expand Down
29 changes: 29 additions & 0 deletions packages/boxel-ui/addon/src/components/field-container/index.gts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface Signature {
icon?: Icon;
iconHeight?: string;
iconWidth?: string;
inline?: boolean;
label: string;
labelFontSize?: BoxelLabelFontSize;
tag?: keyof HTMLElementTagNameMap;
Expand All @@ -33,6 +34,7 @@ const FieldContainer: TemplateOnlyComponent<Signature> = <template>
'boxel-field'
vertical=(or @vertical @centeredDisplay)
horizontal=(not (or @vertical @centeredDisplay))
inline=@inline
small-label=(eq @horizontalLabelSize 'small')
centered-display=@centeredDisplay
with-icon=(bool @icon)
Expand Down Expand Up @@ -111,6 +113,9 @@ const FieldContainer: TemplateOnlyComponent<Signature> = <template>
); /* necessary for our various overlays utilizing box-shadow */
word-break: break-word;
}
.content:not(:has(input)) {
text-box-trim: trim-both;
}

.horizontal {
grid-template-columns:
Expand All @@ -127,6 +132,30 @@ const FieldContainer: TemplateOnlyComponent<Signature> = <template>
align-self: center;
}

.horizontal.inline {
--boxel-field-label-size: auto;
--boxel-field-content-size: auto;
display: inline-grid;
grid-template-columns:
var(--boxel-field-label-size)
var(--boxel-field-content-size);
width: fit-content;
min-height: unset;
gap: var(--boxel-sp-2xs);
}

.horizontal.inline > .label-container {
padding-top: 0;
align-self: start;
display: flex;
align-items: center;
height: var(--boxel-form-control-height);
}

.horizontal.inline > .content {
overflow: clip;
}

.vertical {
grid-template-rows: auto 1fr;
gap: var(--boxel-sp-4xs);
Expand Down
89 changes: 76 additions & 13 deletions packages/boxel-ui/addon/src/components/field-container/usage.gts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,14 @@ export default class FieldUsage extends Component {
@tracked icon = Profile;
@tracked tag?: keyof HTMLElementTagNameMap;

@tracked vertical2 = false;
@tracked horizontalLabelSize2 = 'default';
@tracked icon2: Icon | undefined;
@tracked inline = false;

@tracked inline2 = false;

@tracked inline3 = false;
@tracked vertical3 = false;
@tracked horizontalLabelSize3 = 'default';
@tracked icon3: Icon | undefined;
@cssVariable({ cssClassName: 'boxel-field' })
declare boxelFieldLabelAlign: CSSVariableInfo;
@cssVariable({ cssClassName: 'boxel-field' })
Expand All @@ -42,6 +47,7 @@ export default class FieldUsage extends Component {
@label={{this.label}}
@fieldId={{this.id}}
@vertical={{this.vertical}}
@inline={{this.inline}}
@horizontalLabelSize={{this.horizontalLabelSize}}
@labelFontSize={{this.labelFontSize}}
@centeredDisplay={{this.centeredDisplay}}
Expand Down Expand Up @@ -89,6 +95,13 @@ export default class FieldUsage extends Component {
@onInput={{fn (mut this.vertical)}}
@value={{this.vertical}}
/>
<Args.Bool
@name='inline'
@description='Compact horizontal layout: label column shrinks to content width, min-height removed. Use when embedding the field inside a flex row alongside other controls.'
@defaultValue='false'
@onInput={{fn (mut this.inline)}}
@value={{this.inline}}
/>
<Args.String
@name='horizontalLabelSize'
@description='Width of the label column (only applies to horizontal layout)'
Expand Down Expand Up @@ -135,20 +148,63 @@ export default class FieldUsage extends Component {

<FreestyleUsage @name='Usage with Boxel::Input'>
<:example>
<BoxelFieldContainer @tag='label' @label='Name'>
<BoxelFieldContainer
@tag='label'
@label='Name'
@inline={{this.inline2}}
>
<BoxelInput @id='usage-boxel-input' @value='' />
</BoxelFieldContainer>
</:example>
<:api as |Args|>
<Args.Bool
@name='inline'
@description='Compact horizontal layout: label column shrinks to content width, min-height removed.'
@defaultValue='false'
@onInput={{fn (mut this.inline2)}}
@value={{this.inline2}}
/>
</:api>
</FreestyleUsage>

<FreestyleUsage @name='Inline (compact horizontal)'>
<:description>
Use
<code>@inline</code>
when placing a labeled control inside a flex row alongside other
elements. The label column shrinks to fit its text and
<code>min-height</code>
is removed so the field doesn't impose extra height on the row.
</:description>
<:example>
<div class='inline-example-row'>
<BoxelFieldContainer @tag='label' @label='Max' @inline={{true}}>
<BoxelInput @type='number' @value={{0}} @min={{0}} />
</BoxelFieldContainer>
<BoxelFieldContainer @tag='label' @label='Min' @inline={{true}}>
<BoxelInput @type='number' @value={{0}} @min={{0}} />
</BoxelFieldContainer>
</div>
</:example>
</FreestyleUsage>

<style scoped>
.inline-example-row {
display: flex;
align-items: center;
gap: var(--boxel-sp-sm);
}
</style>

<FreestyleUsage @name='Usage with Boxel::Input (invalid state)'>
<:example>
<BoxelFieldContainer
@tag='label'
@label='Name'
@vertical={{this.vertical2}}
@horizontalLabelSize={{this.horizontalLabelSize2}}
@icon={{this.icon2}}
@inline={{this.inline3}}
@vertical={{this.vertical3}}
@horizontalLabelSize={{this.horizontalLabelSize3}}
@icon={{this.icon3}}
>
<BoxelInput
@id=''
Expand All @@ -163,24 +219,31 @@ export default class FieldUsage extends Component {
<Args.Component
@name='icon'
@description='icon component reference'
@value={{this.icon2}}
@value={{this.icon3}}
@options={{ALL_ICON_COMPONENTS}}
@onChange={{fn (mut this.icon2)}}
@onChange={{fn (mut this.icon3)}}
/>
<Args.Bool
@name='inline'
@description='Compact horizontal layout: label column shrinks to content width, min-height removed.'
@defaultValue='false'
@onInput={{fn (mut this.inline3)}}
@value={{this.inline3}}
/>
<Args.Bool
@name='vertical'
@description='Whether the field should be displayed vertically'
@defaultValue='false'
@onInput={{fn (mut this.vertical2)}}
@value={{this.vertical2}}
@onInput={{fn (mut this.vertical3)}}
@value={{this.vertical3}}
/>
<Args.String
@name='horizontalLabelSize'
@description='Width of the label column (only applies to horizontal layout)'
@options={{array 'small' 'default'}}
@defaultValue='minmax(4rem, 25%)'
@onInput={{fn (mut this.horizontalLabelSize2)}}
@value={{this.horizontalLabelSize2}}
@onInput={{fn (mut this.horizontalLabelSize3)}}
@value={{this.horizontalLabelSize3}}
/>
</:api>
</FreestyleUsage>
Expand Down
56 changes: 49 additions & 7 deletions packages/boxel-ui/addon/src/components/input/index.gts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ export const InputValidationStates = {

export type InputValidationState = Values<typeof InputValidationStates>;

export const InputSizes = {
Default: 'default',
Large: 'large',
} as const;

export const InputBottomTreatments = {
Flat: 'flat',
Rounded: 'rounded',
Expand All @@ -74,7 +79,7 @@ export interface Signature {
placeholder?: string;
readonly?: boolean;
required?: boolean;
size?: 'large' | 'default';
size?: Values<typeof InputSizes>;
state?: InputValidationState;
type?: InputType;
value: string | number | boolean | null | undefined;
Expand Down Expand Up @@ -248,14 +253,16 @@ export default class BoxelInput extends Component<Signature> {
.input-container {
--icon-size: var(--boxel-icon-sm);
--icon-space: var(--boxel-sp-xs);
--icon-full-length: calc(
var(--boxel-icon-sm) + var(--boxel-sp-xs) * 2
--_icon-full-length: var(
--boxel-input-icon-size,
calc(var(--icon-size) + var(--icon-space) * 2)
);

display: grid;
grid-template-columns: var(--icon-full-length) 1fr var(
--icon-full-length
);
grid-template-columns:
var(--_icon-full-length)
1fr
var(--_icon-full-length);
grid-template-areas:
'optional optional optional'
'pre-icon input post-icon'
Expand All @@ -269,7 +276,8 @@ export default class BoxelInput extends Component<Signature> {
grid-row: 2;

box-sizing: border-box;
width: 100%;
width: var(--boxel-input-width, 100%);
min-width: 0;
max-width: 100%;
min-height: var(
--boxel-input-height,
Expand Down Expand Up @@ -299,6 +307,35 @@ export default class BoxelInput extends Component<Signature> {
overflow: auto;
}

.input-container:has([type='color']) {
--boxel-input-icon-size: 0;
}

.input-container:has([type='color']) .validation-icon-container,
.input-container:has([type='color']) .error-message,
.input-container:has([type='color']) .helper-text {
display: none;
}

.boxel-input[type='color'] {
width: 1.5rem;
height: 1.5rem;
min-height: unset;
padding: 0;
background: none;
border-radius: var(--boxel-border-radius-sm);
cursor: pointer;
}

.boxel-input[type='color']::-webkit-color-swatch-wrapper {
padding: 0;
}

.boxel-input[type='color']::-webkit-color-swatch {
border: none;
border-radius: calc(var(--boxel-border-radius-sm) - 1px);
}

.boxel-input:not([type='color']):disabled {
opacity: 0.5;
resize: none; /* do not display resize toggle since it's disabled */
Expand Down Expand Up @@ -392,6 +429,11 @@ export default class BoxelInput extends Component<Signature> {
user-select: none;
}

.is-multiline .validation-icon-container {
align-items: flex-start;
padding-top: var(--boxel-sp-xs);
}

.search ~ .validation-icon-container .validation-icon-loading {
color: var(--primary, var(--boxel-highlight));
--icon-color: currentColor;
Expand Down
9 changes: 6 additions & 3 deletions packages/boxel-ui/addon/src/components/input/usage.gts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ import BoxelInput from './index.gts';
import {
type InputValidationState,
InputBottomTreatments,
InputSizes,
InputTypes,
InputValidationStates,
} from './index.gts';

const validTypes = Object.values(InputTypes);
const validBottomTreatments = Object.values(InputBottomTreatments);
const validStates = Object.values(InputValidationStates);
const validSizes = Object.values(InputSizes);

export default class InputUsage extends Component {
@tracked id = 'sample-input';
Expand All @@ -37,7 +39,8 @@ export default class InputUsage extends Component {
@tracked min = '';
@tracked max = '';
@tracked state: InputValidationState = 'initial';
@tracked size: 'large' | 'default' = 'default';
@tracked size: (typeof InputSizes)[keyof typeof InputSizes] =
InputSizes.Default;

@tracked isChecked = false;

Expand Down Expand Up @@ -196,9 +199,9 @@ export default class InputUsage extends Component {
/>
<Args.String
@name='size'
@description='Optional larger size'
@description='Input size: large increases height, auto sets container width to fit-content'
@onInput={{fn (mut this.size)}}
@options={{Array 'default' 'large'}}
@options={{validSizes}}
@value={{this.size}}
@defaultValue={{this.size}}
/>
Expand Down
Loading
Loading