From aac4b705716dbfd4eb587e36a181de9a4325bd44 Mon Sep 17 00:00:00 2001 From: austincalvelage Date: Wed, 2 Sep 2026 20:03:01 -0600 Subject: [PATCH 1/4] feat(ui): add ghost input variant --- .changeset/quiet-pans-smile.md | 2 ++ packages/swingset/src/lib/registry.ts | 10 ++++++- packages/swingset/src/stories/input.mdx | 9 +++++++ .../swingset/src/stories/input.stories.tsx | 14 ++++++++++ .../ui/src/mosaic/components/input/index.ts | 2 +- .../mosaic/components/input/input.styles.ts | 8 ++++++ .../mosaic/components/input/input.test.tsx | 27 +++++++++++++++++++ .../ui/src/mosaic/components/input/input.tsx | 17 ++++++++++-- packages/ui/src/mosaic/styles/index.ts | 2 +- 9 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 .changeset/quiet-pans-smile.md diff --git a/.changeset/quiet-pans-smile.md b/.changeset/quiet-pans-smile.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/quiet-pans-smile.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/swingset/src/lib/registry.ts b/packages/swingset/src/lib/registry.ts index 8f326162f60..c91fd10fcd4 100644 --- a/packages/swingset/src/lib/registry.ts +++ b/packages/swingset/src/lib/registry.ts @@ -65,6 +65,7 @@ import { import { Default, Disabled as InputDisabled, + Ghost as InputGhost, Invalid, meta as inputMeta, Sizes as InputSizes, @@ -261,7 +262,14 @@ const bannerModule: StoryModule = { const buttonModule: StoryModule = { meta: buttonMeta, Primary, Sizes, Disabled }; -const inputModule: StoryModule = { meta: inputMeta, Default, Sizes: InputSizes, Disabled: InputDisabled, Invalid }; +const inputModule: StoryModule = { + meta: inputMeta, + Default, + Sizes: InputSizes, + Disabled: InputDisabled, + Invalid, + Ghost: InputGhost, +}; const popoverComponentModule: StoryModule = { meta: popoverComponentMeta, diff --git a/packages/swingset/src/stories/input.mdx b/packages/swingset/src/stories/input.mdx index 7726e8e690a..85cb11ca3ee 100644 --- a/packages/swingset/src/stories/input.mdx +++ b/packages/swingset/src/stories/input.mdx @@ -49,3 +49,12 @@ The snippet below reflects the props selected in the table above — change a pr name='Invalid' storyModule={InputStories} /> + +### Ghost + +The ghost variant keeps the input behavior and sizing while allowing a parent composition to provide the field chrome. + + diff --git a/packages/swingset/src/stories/input.stories.tsx b/packages/swingset/src/stories/input.stories.tsx index e1584557c83..2fee5b47912 100644 --- a/packages/swingset/src/stories/input.stories.tsx +++ b/packages/swingset/src/stories/input.stories.tsx @@ -15,9 +15,11 @@ export const meta: StoryMeta = { styles: { _variants: { size: { sm: {}, md: {}, lg: {} }, + variant: { default: {}, ghost: {} }, }, _defaultVariants: { size: 'md', + variant: 'default', }, }, }; @@ -76,3 +78,15 @@ export function Invalid(props: Record) { /> ); } + +export function Ghost(props: Record) { + return ( +
+ +
+ ); +} diff --git a/packages/ui/src/mosaic/components/input/index.ts b/packages/ui/src/mosaic/components/input/index.ts index 188cbaa0d71..8739b78cda1 100644 --- a/packages/ui/src/mosaic/components/input/index.ts +++ b/packages/ui/src/mosaic/components/input/index.ts @@ -1,2 +1,2 @@ export { Input } from './input'; -export type { InputProps } from './input'; +export type { InputProps, InputVariant } from './input'; diff --git a/packages/ui/src/mosaic/components/input/input.styles.ts b/packages/ui/src/mosaic/components/input/input.styles.ts index 935d5003a0a..8a84186ecff 100644 --- a/packages/ui/src/mosaic/components/input/input.styles.ts +++ b/packages/ui/src/mosaic/components/input/input.styles.ts @@ -23,6 +23,14 @@ export const styles = stylex.create({ color: colorVars['--cl-color-input-placeholder'], }, }, + ghost: { + borderRadius: 0, + borderStyle: 'none', + borderWidth: 0, + outline: 'none', + backgroundColor: 'transparent', + boxShadow: 'none', + }, }); export const sizes = stylex.create({ diff --git a/packages/ui/src/mosaic/components/input/input.test.tsx b/packages/ui/src/mosaic/components/input/input.test.tsx index 103361daac4..e9936083a3e 100644 --- a/packages/ui/src/mosaic/components/input/input.test.tsx +++ b/packages/ui/src/mosaic/components/input/input.test.tsx @@ -1,9 +1,20 @@ +import * as stylex from '@stylexjs/stylex'; import { render, screen } from '@testing-library/react'; import React from 'react'; import { describe, expect, it } from 'vitest'; import { Input } from './input'; +const expectedAppearance = stylex.create({ + ghost: { + borderRadius: 0, + borderStyle: 'none', + borderWidth: 0, + backgroundColor: 'transparent', + boxShadow: 'none', + }, +}); + describe('Mosaic Input', () => { it('applies the default size', () => { render(); @@ -24,6 +35,22 @@ describe('Mosaic Input', () => { expect(screen.getByRole('textbox', { name: 'Name' })).toHaveAttribute('data-size', size); }); + it('removes field chrome with the ghost variant', () => { + render( + , + ); + + const input = screen.getByRole('textbox', { name: 'Search' }); + expect(input).toHaveClass('cl-input'); + expect(input).toHaveAttribute('data-variant', 'ghost'); + const appearance = stylex.props(expectedAppearance.ghost).className ?? ''; + expect(input).toHaveClass(...appearance.split(' ').filter(name => name.startsWith('x'))); + expect(input).not.toHaveFocus(); + }); + it('reflects and forwards the disabled state', () => { render( , 'size'> { size?: 'sm' | 'md' | 'lg'; + /** Removes field chrome so a parent composition can provide it. @default 'default' */ + variant?: InputVariant; } export const Input = React.forwardRef(function MosaicInput( { size = 'md', + variant = 'default', disabled: disabledProp, required: requiredProp, render, @@ -52,8 +58,15 @@ export const Input = React.forwardRef(function Mos 'aria-labelledby': fieldProps?.['aria-labelledby'] ?? ariaLabelledBy, 'aria-describedby': fieldProps?.['aria-describedby'] ?? ariaDescribedBy, ...mergeStyleProps( - themeProps('input', { size, disabled }), - stylex.props(reset.base, inputStyles.base, styles.base, sizes[size], disabled && inputStyles.disabled), + themeProps('input', { size, variant, disabled }), + stylex.props( + reset.base, + styles.base, + sizes[size], + variant === 'default' && inputStyles.base, + variant === 'ghost' && styles.ghost, + variant === 'default' && disabled && inputStyles.disabled, + ), className, style, ), diff --git a/packages/ui/src/mosaic/styles/index.ts b/packages/ui/src/mosaic/styles/index.ts index bd0cfde4645..f0829c50674 100644 --- a/packages/ui/src/mosaic/styles/index.ts +++ b/packages/ui/src/mosaic/styles/index.ts @@ -49,7 +49,7 @@ export type { HeadingProps } from '../components/heading'; export { Icon, IconFrame } from '../components/icon'; export type { IconFrameProps, IconProps } from '../components/icon'; export { Input } from '../components/input'; -export type { InputProps } from '../components/input'; +export type { InputProps, InputVariant } from '../components/input'; export { Item } from '../components/item'; export type { ItemProps } from '../components/item'; export { Menu } from '../components/menu'; From 730fc82bca121d77e65316501b11e9b7ae5ec74a Mon Sep 17 00:00:00 2001 From: austincalvelage Date: Wed, 9 Sep 2026 09:51:40 -0600 Subject: [PATCH 2/4] docs(ui): show ghost input without a bordered wrapper --- packages/swingset/src/stories/input.stories.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/swingset/src/stories/input.stories.tsx b/packages/swingset/src/stories/input.stories.tsx index 2fee5b47912..9321349281e 100644 --- a/packages/swingset/src/stories/input.stories.tsx +++ b/packages/swingset/src/stories/input.stories.tsx @@ -81,12 +81,10 @@ export function Invalid(props: Record) { export function Ghost(props: Record) { return ( -
- -
+ ); } From 2a942efdeb905decf37d4cc18ba29adb294c18c6 Mon Sep 17 00:00:00 2001 From: austincalvelage Date: Wed, 9 Sep 2026 12:23:31 -0600 Subject: [PATCH 3/4] docs(swingset): mark input as work in progress --- packages/swingset/src/stories/input.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/swingset/src/stories/input.stories.tsx b/packages/swingset/src/stories/input.stories.tsx index 9321349281e..29fca1a6fd6 100644 --- a/packages/swingset/src/stories/input.stories.tsx +++ b/packages/swingset/src/stories/input.stories.tsx @@ -9,7 +9,7 @@ export { default as __source } from './input.stories?raw'; export const meta: StoryMeta = { group: 'Components', - status: 'stable', + status: 'wip', title: 'Input', source: 'packages/ui/src/mosaic/components/input/input.tsx', styles: { From e693aa84e40ac386c383b4cdb2fdb318f4e4b72d Mon Sep 17 00:00:00 2001 From: austincalvelage Date: Wed, 9 Sep 2026 14:16:19 -0600 Subject: [PATCH 4/4] test(ui): remove ghost input CSS assertions --- .../ui/src/mosaic/components/input/input.test.tsx | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/packages/ui/src/mosaic/components/input/input.test.tsx b/packages/ui/src/mosaic/components/input/input.test.tsx index e9936083a3e..7cc1a522ecb 100644 --- a/packages/ui/src/mosaic/components/input/input.test.tsx +++ b/packages/ui/src/mosaic/components/input/input.test.tsx @@ -1,20 +1,9 @@ -import * as stylex from '@stylexjs/stylex'; import { render, screen } from '@testing-library/react'; import React from 'react'; import { describe, expect, it } from 'vitest'; import { Input } from './input'; -const expectedAppearance = stylex.create({ - ghost: { - borderRadius: 0, - borderStyle: 'none', - borderWidth: 0, - backgroundColor: 'transparent', - boxShadow: 'none', - }, -}); - describe('Mosaic Input', () => { it('applies the default size', () => { render(); @@ -35,7 +24,7 @@ describe('Mosaic Input', () => { expect(screen.getByRole('textbox', { name: 'Name' })).toHaveAttribute('data-size', size); }); - it('removes field chrome with the ghost variant', () => { + it('reflects the ghost variant', () => { render( { const input = screen.getByRole('textbox', { name: 'Search' }); expect(input).toHaveClass('cl-input'); expect(input).toHaveAttribute('data-variant', 'ghost'); - const appearance = stylex.props(expectedAppearance.ghost).className ?? ''; - expect(input).toHaveClass(...appearance.split(' ').filter(name => name.startsWith('x'))); expect(input).not.toHaveFocus(); });