Skip to content
Closed
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
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: 'ComplexSelector',
name: 'Complex Selector — Async Assignment',
displayName: 'Complex Selector — Async Assignment',
description:
'A reviewer picker using changeAction, so the trigger shows the new value optimistically with a busy spinner while the save settles into a validation status.',
isReady: true,
aspectRatio: 4 / 3,
componentsUsed: ['ComplexSelector', 'RadioList'],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.

'use client';

import {useState} from 'react';
import {
ComplexSelector,
type ComplexSelectorStatus,
} from '@astryxdesign/core/ComplexSelector';
import {RadioList, RadioListItem} from '@astryxdesign/core/RadioList';

const REVIEWERS = [
{
value: 'sarah-chen',
label: 'Sarah Chen',
description: 'Design systems · 2 open reviews',
},
{
value: 'marcus-webb',
label: 'Marcus Webb',
description: 'Accessibility · 5 open reviews',
},
{
value: 'priya-raman',
label: 'Priya Raman',
description: 'Platform · 1 open review',
},
];

export default function ComplexSelectorAsyncAssign() {
const [reviewer, setReviewer] = useState('');
const [isSaved, setIsSaved] = useState(false);
const selected = REVIEWERS.find(option => option.value === reviewer);

let status: ComplexSelectorStatus | undefined;
if (selected == null) {
status = {type: 'error', message: 'Every draft needs a reviewer'};
} else if (isSaved) {
status = {type: 'success', message: `Assigned to ${selected.label}`};
}

return (
// A radio group commits on arrow navigation, so closing the popup on
// change would dismiss it mid-traversal; the user dismisses it instead.
<ComplexSelector
label="Reviewer"
description="Assignments save as soon as you pick someone"
width={320}
isRequired
value={reviewer}
onChange={value => {
setReviewer(value);
setIsSaved(false);
}}
changeAction={async () => {
await new Promise(resolve => {
setTimeout(resolve, 900);
});
setIsSaved(true);
}}
placeholder="Unassigned"
triggerLabel={selected?.label}
status={status}>
{(value, onChange) => (
<RadioList
label="Reviewer"
isLabelHidden
value={value}
onChange={onChange}>
{REVIEWERS.map(option => (
<RadioListItem
key={option.value}
label={option.label}
value={option.value}
description={option.description}
/>
))}
</RadioList>
)}
</ComplexSelector>
);
}
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: 'ComplexSelector',
name: 'Complex Selector — Folder Tree',
displayName: 'Complex Selector — Folder Tree',
description:
'A destination picker whose popup content is a TreeList, so the hierarchy keeps its own tree keyboard navigation.',
isReady: true,
aspectRatio: 4 / 3,
componentsUsed: ['ComplexSelector', 'TreeList'],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.

'use client';

import {useState} from 'react';
import {ComplexSelector} from '@astryxdesign/core/ComplexSelector';
import {TreeList} from '@astryxdesign/core/TreeList';

interface Destination {
id: string;
path: string;
}

const FOLDERS = [
{
id: 'design-systems',
label: 'Design systems',
children: [
{id: 'components', label: 'Components'},
{id: 'accessibility', label: 'Accessibility'},
{id: 'tokens', label: 'Tokens'},
],
},
{
id: 'research',
label: 'Research',
children: [
{id: 'interviews', label: 'Interviews'},
{id: 'field-notes', label: 'Field notes'},
],
},
];

export default function ComplexSelectorFolderTree() {
const [destination, setDestination] = useState<Destination>({
id: 'components',
path: 'Design systems / Components',
});

return (
<ComplexSelector<Destination>
label="Move to folder"
description="Where this document should live"
width={320}
value={destination}
onChange={setDestination}
triggerLabel={destination.path}>
{(value, onChange, close) => (
<TreeList
items={FOLDERS.map(folder => ({
id: folder.id,
label: folder.label,
isExpanded: true,
children: folder.children.map(child => ({
id: child.id,
label: child.label,
isSelected: child.id === value.id,
onClick: () => {
onChange({
id: child.id,
path: `${folder.label} / ${child.label}`,
});
close();
},
})),
}))}
/>
)}
</ComplexSelector>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.

/** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */
export const doc = {
type: 'block',
exampleFor: 'ComplexSelector',
name: 'Complex Selector',
displayName: 'Complex Selector',
description:
'A reporting-period picker: quick presets beside a range Calendar, edited as a draft and committed on Apply, in a single field.',
isReady: true,
aspectRatio: 4 / 3,
isShowcase: true,
componentsUsed: [
'ComplexSelector',
'Calendar',
'List',
'Button',
'Divider',
'Text',
'HStack',
'VStack',
],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.

'use client';

import {useState} from 'react';
import {Button} from '@astryxdesign/core/Button';
import {
Calendar,
type DateRange,
type ISODateString,
} from '@astryxdesign/core/Calendar';
import {ComplexSelector} from '@astryxdesign/core/ComplexSelector';
import {Divider} from '@astryxdesign/core/Divider';
import {HStack, VStack} from '@astryxdesign/core/Layout';
import {List, ListItem} from '@astryxdesign/core/List';
import {Text} from '@astryxdesign/core/Text';

const PRESETS: Array<{
label: string;
start: ISODateString;
end: ISODateString;
}> = [
{label: 'Last 7 days', start: '2026-03-02', end: '2026-03-08'},
{label: 'Last 30 days', start: '2026-02-07', end: '2026-03-08'},
{label: 'This quarter', start: '2026-01-01', end: '2026-03-08'},
{label: 'Previous quarter', start: '2025-10-01', end: '2025-12-31'},
];

function formatDay(date: ISODateString, hasYear = false) {
return new Date(`${date}T00:00:00`).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: hasYear ? 'numeric' : undefined,
});
}

function formatRange(range: DateRange) {
if (range.start == null || range.end == null) {
return undefined;
}
const isSameYear = range.start.slice(0, 4) === range.end.slice(0, 4);
return `${formatDay(range.start, !isSameYear)} – ${formatDay(range.end, true)}`;
}

function DateRangeContent({
value,
onChange,
close,
}: {
value: DateRange;
onChange: (value: DateRange) => void;
close: () => void;
}) {
const [draft, setDraft] = useState(value);
const isComplete = draft.start != null && draft.end != null;

return (
<VStack gap={3}>
<HStack gap={4} wrap="wrap">
<List>
{PRESETS.map(preset => (
<ListItem
key={preset.label}
label={preset.label}
isSelected={
draft.start === preset.start && draft.end === preset.end
}
onClick={() => setDraft({start: preset.start, end: preset.end})}
/>
))}
</List>
<Calendar
mode="range"
value={draft}
onChange={setDraft}
focusDate={draft.start ?? '2026-03-01'}
/>
</HStack>
<Divider />
<HStack gap={2} vAlign="center" hAlign="end">
<Text type="supporting" color="secondary">
{formatRange(draft) ?? 'Pick a start and end date'}
</Text>
<Button label="Cancel" variant="ghost" onClick={close} />
<Button
label="Apply"
variant="primary"
isDisabled={!isComplete}
onClick={() => {
onChange(draft);
close();
}}
/>
</HStack>
</VStack>
);
}

export default function ComplexSelectorShowcase() {
const [range, setRange] = useState<DateRange>({
start: '2026-03-02',
end: '2026-03-08',
});

return (
<ComplexSelector<DateRange>
label="Reporting period"
description="Pick a preset or draw a custom range"
width={320}
value={range}
onChange={setRange}
placeholder="All time"
triggerLabel={formatRange(range)}>
{(value, onChange, close) => (
<DateRangeContent value={value} onChange={onChange} close={close} />
)}
</ComplexSelector>
);
}
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: 'ComplexSelector',
name: 'Complex Selector — Multi-select Filter',
displayName: 'Complex Selector — Multi-select Filter',
description:
'A stage filter that keeps the popup open while several checkboxes are toggled and closes only when the apply button calls close().',
isReady: true,
aspectRatio: 4 / 3,
componentsUsed: ['ComplexSelector', 'CheckboxList', 'Button', 'VStack'],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.

'use client';

import {useState} from 'react';
import {Button} from '@astryxdesign/core/Button';
import {CheckboxList, CheckboxListItem} from '@astryxdesign/core/CheckboxList';
import {ComplexSelector} from '@astryxdesign/core/ComplexSelector';
import {VStack} from '@astryxdesign/core/Layout';

const STAGES = [
{value: 'discovery', label: 'Discovery'},
{value: 'in-review', label: 'In review'},
{value: 'approved', label: 'Approved'},
{value: 'shipped', label: 'Shipped'},
];

export default function ComplexSelectorStageFilter() {
const [stages, setStages] = useState<string[]>(['in-review', 'approved']);

return (
<ComplexSelector<string[]>
label="Pipeline stages"
description="Filter the roadmap by delivery stage"
width={320}
value={stages}
onChange={setStages}
placeholder="All stages"
triggerLabel={
stages.length > 0
? `${stages.length} of ${STAGES.length} stages`
: undefined
}>
{(value, onChange, close) => (
<VStack gap={3}>
<CheckboxList
label="Pipeline stages"
isLabelHidden
value={value}
onChange={onChange}>
{STAGES.map(stage => (
<CheckboxListItem
key={stage.value}
label={stage.label}
value={stage.value}
/>
))}
</CheckboxList>
<Button label="Apply filters" variant="primary" onClick={close} />
</VStack>
)}
</ComplexSelector>
);
}
Loading
Loading