Skip to content
Merged
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
7 changes: 7 additions & 0 deletions .changeset/ghost-selector-variants.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[feat] Add a ghost trigger variant for Selector and MultiSelector for toolbar-style controls, with ghost status messages detached by default.

@cixzhang
47 changes: 47 additions & 0 deletions apps/storybook/stories/MultiSelector.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import type {Meta, StoryObj} from '@storybook/react';
import {useState} from 'react';
import {Button} from '@astryxdesign/core/Button';
import {MultiSelector} from '@astryxdesign/core/MultiSelector';
import {Theme, defineTheme} from '@astryxdesign/core/theme';

Expand All @@ -25,6 +26,7 @@ const meta: Meta<typeof MultiSelector> = {
description: {control: 'text'},
placeholder: {control: 'text'},
size: {control: 'radio', options: ['sm', 'md', 'lg']},
variant: {control: 'radio', options: ['input', 'ghost']},
triggerDisplay: {
control: 'radio',
options: ['count', 'labels', 'badges'],
Expand Down Expand Up @@ -269,6 +271,51 @@ export const DisabledWithMessage: Story = {
decorators: [Story => <Story />],
};

// Ghost variant for toolbar composition
export const GhostVariant: Story = {
render: () => {
const [columns, setColumns] = useState<string[]>(['Name', 'Email']);
const [filters, setFilters] = useState<string[]>(['Active']);
return (
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
width: 'max-content',
}}>
<Button label="Refresh" variant="ghost" />
<MultiSelector
label="Columns"
isLabelHidden
variant="ghost"
size="md"
options={['Name', 'Email', 'Role', 'Status', 'Created']}
value={columns}
onChange={setColumns}
triggerDisplay="labels"
placeholder="Columns"
/>
<MultiSelector
label="Status"
isLabelHidden
variant="ghost"
size="md"
options={['Active', 'Inactive', 'Pending', 'Archived']}
value={filters}
onChange={setFilters}
triggerDisplay="labels"
placeholder="Status"
status={{type: 'warning', message: 'Some filters hide archived rows'}}
statusVariant="tooltip"
/>
<Button label="Export" variant="ghost" />
</div>
);
},
decorators: [Story => <Story />],
};

// Status variants
export const Status: Story = {
render: () => {
Expand Down
55 changes: 55 additions & 0 deletions apps/storybook/stories/Selector.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import type {Meta, StoryObj} from '@storybook/react';
import {useState} from 'react';
import {Button} from '@astryxdesign/core/Button';
import {Selector, SelectorOption} from '@astryxdesign/core/Selector';
import {Theme, defineTheme} from '@astryxdesign/core/theme';
import {UserIcon, CogIcon, BellIcon} from '@heroicons/react/24/outline';
Expand Down Expand Up @@ -51,6 +52,11 @@ const meta: Meta<typeof Selector> = {
options: ['sm', 'md', 'lg'],
description: 'Size variant of the selector',
},
variant: {
control: 'radio',
options: ['input', 'ghost'],
description: 'Visual trigger style',
},
placement: {
control: 'select',
options: ['above', 'below', 'start', 'end'],
Expand Down Expand Up @@ -435,6 +441,55 @@ export const SizeVariants: Story = {
decorators: [Story => <Story />],
};

// Ghost variant for toolbar composition
export const GhostVariant: Story = {
render: () => {
const [view, setView] = useState<string | undefined>('week');
const [density, setDensity] = useState<string | undefined>('comfortable');
return (
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
width: 'max-content',
}}>
<Button label="Today" variant="ghost" />
<Selector
label="View"
isLabelHidden
variant="ghost"
size="md"
options={[
{value: 'day', label: 'Day'},
{value: 'week', label: 'Week'},
{value: 'month', label: 'Month'},
]}
value={view}
onChange={setView}
/>
<Selector
label="Density"
isLabelHidden
variant="ghost"
size="md"
options={[
{value: 'compact', label: 'Compact'},
{value: 'comfortable', label: 'Comfortable'},
{value: 'spacious', label: 'Spacious'},
]}
value={density}
onChange={setDensity}
status={{type: 'warning', message: 'This setting affects all users'}}
statusVariant="tooltip"
/>
<Button label="Export" variant="ghost" />
</div>
);
},
decorators: [Story => <Story />],
};

// With status
export const WithStatus: Story = {
render: () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.

/** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */
export const doc = {
type: 'block',
exampleFor: 'MultiSelector',
name: 'MultiSelector — Ghost Toolbar',
displayName: 'MultiSelector — Ghost Toolbar',
description:
'Borderless MultiSelector variant composed with ghost buttons in a toolbar.',
isReady: true,
aspectRatio: 16 / 9,
componentsUsed: ['MultiSelector', 'Button', 'HStack'],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.

'use client';

import {useState} from 'react';
import {Button} from '@astryxdesign/core/Button';
import {HStack} from '@astryxdesign/core/Layout';
import {MultiSelector} from '@astryxdesign/core/MultiSelector';

export default function MultiSelectorGhostToolbar() {
const [columns, setColumns] = useState<string[]>(['Name', 'Email']);
const [filters, setFilters] = useState<string[]>(['Active']);

return (
<HStack align="center" gap={2}>
<Button label="Refresh" variant="ghost" />
<MultiSelector
label="Columns"
isLabelHidden
variant="ghost"
options={['Name', 'Email', 'Role', 'Status', 'Created']}
value={columns}
onChange={setColumns}
triggerDisplay="labels"
placeholder="Columns"
/>
<MultiSelector
label="Status"
isLabelHidden
variant="ghost"
options={['Active', 'Inactive', 'Pending', 'Archived']}
value={filters}
onChange={setFilters}
triggerDisplay="labels"
placeholder="Status"
status={{type: 'warning', message: 'Some filters hide archived rows'}}
statusVariant="tooltip"
/>
<Button label="Export" variant="ghost" />
</HStack>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.

/** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */
export const doc = {
type: 'block',
exampleFor: 'Selector',
name: 'Selector — Ghost Toolbar',
displayName: 'Selector — Ghost Toolbar',
description:
'Borderless Selector variant composed with ghost buttons in a toolbar.',
isReady: true,
aspectRatio: 16 / 9,
componentsUsed: ['Selector', 'Button', 'HStack'],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.

'use client';

import {useState} from 'react';
import {Button} from '@astryxdesign/core/Button';
import {HStack} from '@astryxdesign/core/Layout';
import {Selector} from '@astryxdesign/core/Selector';

export default function SelectorGhostToolbar() {
const [view, setView] = useState<string | undefined>('week');
const [density, setDensity] = useState<string | undefined>('comfortable');

return (
<HStack align="center" gap={2}>
<Button label="Today" variant="ghost" />
<Selector
label="Calendar view"
isLabelHidden
variant="ghost"
options={[
{value: 'day', label: 'Day'},
{value: 'week', label: 'Week'},
{value: 'month', label: 'Month'},
]}
value={view}
onChange={setView}
/>
<Selector
label="Density"
isLabelHidden
variant="ghost"
options={[
{value: 'compact', label: 'Compact'},
{value: 'comfortable', label: 'Comfortable'},
{value: 'spacious', label: 'Spacious'},
]}
value={density}
onChange={setDensity}
status={{type: 'warning', message: 'This setting affects all users'}}
statusVariant="tooltip"
/>
<Button label="Export" variant="ghost" />
</HStack>
);
}
37 changes: 31 additions & 6 deletions packages/core/src/MultiSelector/MultiSelector.doc.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ export const docs = {
],
theming: {
targets: [
{className: 'astryx-multi-selector', visualProps: ['size', 'status']},
{
className: 'astryx-multi-selector',
visualProps: ['variant', 'size', 'status'],
},
{className: 'astryx-multi-selector-clear-icon'},
{
className: 'astryx-multi-selector-indicator-icon',
Expand Down Expand Up @@ -77,6 +80,13 @@ export const docs = {
description: 'Size variant for the selector.',
default: "'md'",
},
{
name: 'variant',
type: "'input' | 'ghost'",
description:
'Visual trigger style. input is the bordered input treatment for forms; ghost is borderless and matches ghost buttons for toolbar usage.',
default: "'input'",
},
{
name: 'triggerDisplay',
type: "'count' | 'labels' | 'badges'",
Expand Down Expand Up @@ -162,10 +172,11 @@ export const docs = {
},
{
name: 'statusVariant',
type: "'attached' | 'detached'",
type: "'attached' | 'detached' | 'tooltip'",
description:
'How the status message is placed relative to the input. attached overlaps directly below the input (bordered treatment); detached floats below as a separate element with spacing.',
default: "'attached'",
'How the status message is placed relative to the input. attached overlaps directly below the bordered input and is only valid for the input variant; ghost selectors detach attached status messages by default. Use tooltip for compact toolbar controls.',
default:
"'attached' for input selectors; 'detached' for ghost selectors",
},
{
name: 'renderOption',
Expand Down Expand Up @@ -217,6 +228,11 @@ export const docs = {
description:
'Use inside InputGroup only when the control needs a short prefix or suffix addon as part of one decorated input surface; prefer count or labels trigger display so the group stays single-line.',
},
{
guidance: true,
description:
'Use variant="ghost" when a multi-selector sits in a toolbar with ghost buttons. If validation status is needed there, prefer statusVariant="tooltip" so the toolbar height stays compact.',
},
{
guidance: false,
description: 'Use for single-value selection; use Selector instead.',
Expand Down Expand Up @@ -268,7 +284,8 @@ export const docsZh = {
isRequired: '将字段标记为必填。',
isLoading: '在触发器中显示加载旋转器。',
status: '带可选消息的验证状态。',
statusVariant: '状态消息的放置方式:attached 直接叠加在输入框下方;detached 作为独立元素浮于下方并留有间距。',
statusVariant:
'状态消息的放置方式:attached 直接叠加在输入框下方;detached 作为独立元素浮于下方并留有间距。',
renderOption:
'每个可选选项的自定义渲染函数。不会用于分隔线、分组或全选行。',
xstyle: '布局自定义的 StyleX 样式,必须是 stylex.create() 值。',
Expand Down Expand Up @@ -349,6 +366,11 @@ export const docsDense = {
description:
'Use inside InputGroup only for a short prefix or suffix addon; prefer count or labels trigger display so the group stays single-line.',
},
{
guidance: true,
description:
'Use variant="ghost" in toolbars with ghost buttons; prefer statusVariant="tooltip" for compact validation status.',
},
{
guidance: false,
description: 'Use for single-value selection; use Selector instead.',
Expand Down Expand Up @@ -378,6 +400,8 @@ export const docsDense = {
changeAction: 'async; fires after onChange',
placeholder: 'text when nothing selected',
size: 'size variant',
variant:
'visual trigger style: input bordered control or ghost toolbar control',
triggerDisplay: 'how to show selected in trigger',
maxBadges: 'max badges before "+N"; badges mode only',
hasSelectAll: 'show select-all checkbox',
Expand All @@ -394,7 +418,8 @@ export const docsDense = {
isRequired: 'marks required',
isLoading: 'spinner in trigger',
status: 'validation status w/ optional message',
statusVariant: 'How status message is placed: attached overlaps below input; detached floats below w/ spacing.',
statusVariant:
'status message placement; ghost detaches attached by default; use tooltip for compact toolbars.',
renderOption:
'custom render fn per selectable option; not dividers/sections/select-all',
xstyle: 'StyleX layout styles; stylex.create() only',
Expand Down
Loading
Loading