diff --git a/packages/cli/assets/templates/blocks/components/ComplexSelector/ComplexSelectorAsyncAssign.doc.mjs b/packages/cli/assets/templates/blocks/components/ComplexSelector/ComplexSelectorAsyncAssign.doc.mjs new file mode 100644 index 000000000000..19dc0ac61f93 --- /dev/null +++ b/packages/cli/assets/templates/blocks/components/ComplexSelector/ComplexSelectorAsyncAssign.doc.mjs @@ -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'], +}; diff --git a/packages/cli/assets/templates/blocks/components/ComplexSelector/ComplexSelectorAsyncAssign.tsx b/packages/cli/assets/templates/blocks/components/ComplexSelector/ComplexSelectorAsyncAssign.tsx new file mode 100644 index 000000000000..67a9196148a9 --- /dev/null +++ b/packages/cli/assets/templates/blocks/components/ComplexSelector/ComplexSelectorAsyncAssign.tsx @@ -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. + { + setReviewer(value); + setIsSaved(false); + }} + changeAction={async () => { + await new Promise(resolve => { + setTimeout(resolve, 900); + }); + setIsSaved(true); + }} + placeholder="Unassigned" + triggerLabel={selected?.label} + status={status}> + {(value, onChange) => ( + + {REVIEWERS.map(option => ( + + ))} + + )} + + ); +} diff --git a/packages/cli/assets/templates/blocks/components/ComplexSelector/ComplexSelectorFolderTree.doc.mjs b/packages/cli/assets/templates/blocks/components/ComplexSelector/ComplexSelectorFolderTree.doc.mjs new file mode 100644 index 000000000000..4cd738dbc824 --- /dev/null +++ b/packages/cli/assets/templates/blocks/components/ComplexSelector/ComplexSelectorFolderTree.doc.mjs @@ -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'], +}; diff --git a/packages/cli/assets/templates/blocks/components/ComplexSelector/ComplexSelectorFolderTree.tsx b/packages/cli/assets/templates/blocks/components/ComplexSelector/ComplexSelectorFolderTree.tsx new file mode 100644 index 000000000000..d175a591e9ea --- /dev/null +++ b/packages/cli/assets/templates/blocks/components/ComplexSelector/ComplexSelectorFolderTree.tsx @@ -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({ + id: 'components', + path: 'Design systems / Components', + }); + + return ( + + label="Move to folder" + description="Where this document should live" + width={320} + value={destination} + onChange={setDestination} + triggerLabel={destination.path}> + {(value, onChange, close) => ( + ({ + 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(); + }, + })), + }))} + /> + )} + + ); +} diff --git a/packages/cli/assets/templates/blocks/components/ComplexSelector/ComplexSelectorShowcase.doc.mjs b/packages/cli/assets/templates/blocks/components/ComplexSelector/ComplexSelectorShowcase.doc.mjs new file mode 100644 index 000000000000..c17433436736 --- /dev/null +++ b/packages/cli/assets/templates/blocks/components/ComplexSelector/ComplexSelectorShowcase.doc.mjs @@ -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', + ], +}; diff --git a/packages/cli/assets/templates/blocks/components/ComplexSelector/ComplexSelectorShowcase.tsx b/packages/cli/assets/templates/blocks/components/ComplexSelector/ComplexSelectorShowcase.tsx new file mode 100644 index 000000000000..54aea3d3d000 --- /dev/null +++ b/packages/cli/assets/templates/blocks/components/ComplexSelector/ComplexSelectorShowcase.tsx @@ -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 ( + + + + {PRESETS.map(preset => ( + setDraft({start: preset.start, end: preset.end})} + /> + ))} + + + + + + + {formatRange(draft) ?? 'Pick a start and end date'} + +