@astryxdesign/cli 0.4.2-canary.bb07062 → 0.4.2-canary.e60974e

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */
4
+ export const doc = {
5
+ type: 'block',
6
+ exampleFor: 'ComplexSelector',
7
+ name: 'ComplexSelector — Deadline Picker',
8
+ displayName: 'Complex Selector — Deadline Picker',
9
+ description:
10
+ 'A multi-step deadline field: pick a preset like Today or Next week, or switch to a custom date and time before applying. The popup stays open until the user commits, so the content owns the Apply action.',
11
+ isReady: true,
12
+ aspectRatio: 4 / 3,
13
+ componentsUsed: [
14
+ 'ComplexSelector',
15
+ 'RadioList',
16
+ 'DateInput',
17
+ 'TimeInput',
18
+ 'Button',
19
+ ],
20
+ };
@@ -0,0 +1,87 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ import {useState} from 'react';
6
+ import {ComplexSelector} from '@astryxdesign/core/ComplexSelector';
7
+ import {RadioList, RadioListItem} from '@astryxdesign/core/RadioList';
8
+ import {DateInput} from '@astryxdesign/core/DateInput';
9
+ import {TimeInput} from '@astryxdesign/core/TimeInput';
10
+ import {Button} from '@astryxdesign/core/Button';
11
+ import {VStack} from '@astryxdesign/core/Layout';
12
+
13
+ type ISODate =
14
+ `${number}${number}${number}${number}-${number}${number}-${number}${number}`;
15
+ type ISOTime = string & {readonly __brand: 'ISOTimeString'};
16
+
17
+ interface Deadline {
18
+ preset: 'today' | 'next-week' | 'custom';
19
+ date: ISODate;
20
+ time: ISOTime;
21
+ }
22
+
23
+ const presetLabels: Record<Deadline['preset'], string> = {
24
+ today: 'Today',
25
+ 'next-week': 'Next week',
26
+ custom: 'Custom date',
27
+ };
28
+
29
+ function formatDeadline(value: Deadline) {
30
+ if (value.preset === 'custom') {
31
+ return `${value.date} at ${value.time}`;
32
+ }
33
+ return presetLabels[value.preset];
34
+ }
35
+
36
+ export default function ComplexSelectorDeadlinePicker() {
37
+ const [value, setValue] = useState<Deadline>({
38
+ preset: 'today',
39
+ date: '2026-04-06' as ISODate,
40
+ time: '17:00' as ISOTime,
41
+ });
42
+
43
+ return (
44
+ <ComplexSelector<Deadline>
45
+ label="Deadline"
46
+ description="Choose a preset or set a custom date and time."
47
+ value={value}
48
+ onChange={setValue}
49
+ triggerLabel={formatDeadline(value)}
50
+ style={{width: 320}}>
51
+ {(selectedValue, onChange, close) => {
52
+ const set = (patch: Partial<Deadline>) =>
53
+ onChange({...selectedValue, ...patch});
54
+
55
+ return (
56
+ <VStack gap={4} style={{width: 320}}>
57
+ <RadioList
58
+ label="When is it due?"
59
+ value={selectedValue.preset}
60
+ onChange={preset => set({preset: preset as Deadline['preset']})}>
61
+ <RadioListItem label="Today" value="today" />
62
+ <RadioListItem label="Next week" value="next-week" />
63
+ <RadioListItem label="Custom date" value="custom" />
64
+ </RadioList>
65
+
66
+ {selectedValue.preset === 'custom' && (
67
+ <VStack gap={3}>
68
+ <DateInput
69
+ label="Date"
70
+ value={selectedValue.date}
71
+ onChange={date => date && set({date})}
72
+ />
73
+ <TimeInput
74
+ label="Time"
75
+ value={selectedValue.time}
76
+ onChange={time => time && set({time})}
77
+ />
78
+ </VStack>
79
+ )}
80
+
81
+ <Button label="Apply" variant="primary" onClick={close} />
82
+ </VStack>
83
+ );
84
+ }}
85
+ </ComplexSelector>
86
+ );
87
+ }
@@ -0,0 +1,15 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */
4
+ export const doc = {
5
+ type: 'block',
6
+ exampleFor: 'ComplexSelector',
7
+ name: 'ComplexSelector',
8
+ displayName: 'Complex Selector',
9
+ description:
10
+ 'A two-axis picker: choose a fruit and a ripeness level from one control. ComplexSelector owns the trigger, popover, and focus restore while the custom grid owns its keyboard semantics.',
11
+ isReady: true,
12
+ aspectRatio: 1,
13
+ isShowcase: true,
14
+ componentsUsed: ['ComplexSelector', 'Text'],
15
+ };
@@ -0,0 +1,199 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ import {useEffect, useState} from 'react';
6
+ import * as stylex from '@stylexjs/stylex';
7
+ import {ComplexSelector} from '@astryxdesign/core/ComplexSelector';
8
+ import {Text} from '@astryxdesign/core/Text';
9
+ import {useGridFocus} from '@astryxdesign/core/hooks';
10
+ import {
11
+ borderVars,
12
+ colorVars,
13
+ radiusVars,
14
+ spacingVars,
15
+ } from '@astryxdesign/core/theme/tokens.stylex';
16
+
17
+ type Fruit = 'Apple' | 'Pear' | 'Peach' | 'Plum';
18
+ type Ripeness = 'Crisp' | 'Tender' | 'Juicy' | 'Peak';
19
+
20
+ interface FruitValue {
21
+ fruit: Fruit;
22
+ ripeness: Ripeness;
23
+ }
24
+
25
+ const fruits: Array<{id: Fruit; emoji: string; description: string}> = [
26
+ {id: 'Apple', emoji: '🍎', description: 'Bright and balanced'},
27
+ {id: 'Pear', emoji: '🍐', description: 'Soft floral sweetness'},
28
+ {id: 'Peach', emoji: '🍑', description: 'Round summer flavor'},
29
+ {id: 'Plum', emoji: '🟣', description: 'Jammy and tart'},
30
+ ];
31
+
32
+ const ripenessLevels: Array<{
33
+ id: Ripeness;
34
+ shortLabel: string;
35
+ description: string;
36
+ }> = [
37
+ {id: 'Crisp', shortLabel: 'C', description: 'Snappy bite'},
38
+ {id: 'Tender', shortLabel: 'T', description: 'Easy bite'},
39
+ {id: 'Juicy', shortLabel: 'J', description: 'Full juice'},
40
+ {id: 'Peak', shortLabel: 'P', description: 'Most intense'},
41
+ ];
42
+
43
+ const GRID_CELL_SELECTOR = '[role="gridcell"]';
44
+
45
+ const styles = stylex.create({
46
+ grid: {
47
+ display: 'flex',
48
+ flexDirection: 'column',
49
+ gap: spacingVars['--spacing-1'],
50
+ minWidth: 280,
51
+ },
52
+ row: {
53
+ display: 'grid',
54
+ gridTemplateColumns: 'minmax(150px, 1fr) repeat(4, 44px)',
55
+ alignItems: 'center',
56
+ columnGap: spacingVars['--spacing-1'],
57
+ },
58
+ rowHeader: {
59
+ display: 'flex',
60
+ alignItems: 'center',
61
+ gap: spacingVars['--spacing-2'],
62
+ textAlign: 'start',
63
+ minWidth: 0,
64
+ },
65
+ emoji: {
66
+ fontSize: 18,
67
+ flexShrink: 0,
68
+ },
69
+ fruitText: {
70
+ display: 'flex',
71
+ flexDirection: 'column',
72
+ minWidth: 0,
73
+ },
74
+ cell: {
75
+ display: 'flex',
76
+ alignItems: 'center',
77
+ justifyContent: 'center',
78
+ height: 36,
79
+ borderWidth: borderVars['--border-width'],
80
+ borderStyle: 'solid',
81
+ borderColor: colorVars['--color-border'],
82
+ borderRadius: radiusVars['--radius-container'],
83
+ backgroundColor: colorVars['--color-background-card'],
84
+ color: colorVars['--color-text-secondary'],
85
+ fontFamily: 'inherit',
86
+ cursor: 'pointer',
87
+ ':hover': {
88
+ '@media (hover: hover)': {
89
+ borderColor: colorVars['--color-border-emphasized'],
90
+ color: colorVars['--color-text-primary'],
91
+ },
92
+ },
93
+ },
94
+ cellSelected: {
95
+ borderColor: colorVars['--color-accent'],
96
+ backgroundColor: colorVars['--color-accent'],
97
+ color: colorVars['--color-on-accent'],
98
+ },
99
+ });
100
+
101
+ function FruitRipenessGrid({
102
+ value,
103
+ onChange,
104
+ }: {
105
+ value: FruitValue;
106
+ onChange: (value: FruitValue) => void;
107
+ }) {
108
+ const {gridRef, handleKeyDown, handleFocus, focusCell} =
109
+ useGridFocus<HTMLDivElement>({
110
+ columns: ripenessLevels.length,
111
+ cellSelector: GRID_CELL_SELECTOR,
112
+ hasRovingTabIndex: true,
113
+ });
114
+
115
+ useEffect(() => {
116
+ const rowIndex = fruits.findIndex(f => f.id === value.fruit);
117
+ const columnIndex = ripenessLevels.findIndex(l => l.id === value.ripeness);
118
+ requestAnimationFrame(() => {
119
+ focusCell(
120
+ rowIndex >= 0 && columnIndex >= 0
121
+ ? rowIndex * ripenessLevels.length + columnIndex
122
+ : 0,
123
+ );
124
+ });
125
+ }, [focusCell, value]);
126
+
127
+ return (
128
+ <div
129
+ ref={gridRef}
130
+ role="grid"
131
+ aria-label="Fruit ripeness choices"
132
+ onKeyDown={handleKeyDown}
133
+ onFocus={handleFocus}
134
+ {...stylex.props(styles.grid)}>
135
+ {fruits.map(fruit => (
136
+ <div key={fruit.id} role="row" {...stylex.props(styles.row)}>
137
+ <div role="rowheader" {...stylex.props(styles.rowHeader)}>
138
+ <span aria-hidden="true" {...stylex.props(styles.emoji)}>
139
+ {fruit.emoji}
140
+ </span>
141
+ <span {...stylex.props(styles.fruitText)}>
142
+ <Text type="body">{fruit.id}</Text>
143
+ <Text type="supporting" color="secondary">
144
+ {fruit.description}
145
+ </Text>
146
+ </span>
147
+ </div>
148
+ {ripenessLevels.map(level => {
149
+ const isSelected =
150
+ value.fruit === fruit.id && value.ripeness === level.id;
151
+ return (
152
+ <button
153
+ key={`${fruit.id}-${level.id}`}
154
+ type="button"
155
+ role="gridcell"
156
+ aria-label={`${fruit.id}, ${level.id}: ${level.description}`}
157
+ aria-selected={isSelected || undefined}
158
+ tabIndex={isSelected ? 0 : -1}
159
+ onClick={() => onChange({fruit: fruit.id, ripeness: level.id})}
160
+ {...stylex.props(
161
+ styles.cell,
162
+ isSelected && styles.cellSelected,
163
+ )}>
164
+ {level.shortLabel}
165
+ </button>
166
+ );
167
+ })}
168
+ </div>
169
+ ))}
170
+ </div>
171
+ );
172
+ }
173
+
174
+ export default function ComplexSelectorShowcase() {
175
+ const [value, setValue] = useState<FruitValue>({
176
+ fruit: 'Apple',
177
+ ripeness: 'Juicy',
178
+ });
179
+
180
+ return (
181
+ <ComplexSelector<FruitValue>
182
+ label="Fruit blend"
183
+ description="Choose a fruit and ripeness in one control. Arrow keys move across the grid."
184
+ value={value}
185
+ onChange={setValue}
186
+ triggerLabel={`${value.fruit} · ${value.ripeness}`}
187
+ style={{width: 280}}>
188
+ {(selectedValue, onChange, close) => (
189
+ <FruitRipenessGrid
190
+ value={selectedValue}
191
+ onChange={nextValue => {
192
+ onChange(nextValue);
193
+ close();
194
+ }}
195
+ />
196
+ )}
197
+ </ComplexSelector>
198
+ );
199
+ }
@@ -0,0 +1,14 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */
4
+ export const doc = {
5
+ type: 'block',
6
+ exampleFor: 'ComplexSelector',
7
+ name: 'ComplexSelector — Tree Search',
8
+ displayName: 'Complex Selector — Tree Search',
9
+ description:
10
+ 'A destination picker that combines a search field with a TreeList hierarchy. TreeList owns tree keyboard navigation; ComplexSelector owns the trigger, popover, and focus restore. Selecting a folder closes the popup.',
11
+ isReady: true,
12
+ aspectRatio: 4 / 3,
13
+ componentsUsed: ['ComplexSelector', 'TextInput', 'TreeList'],
14
+ };
@@ -0,0 +1,188 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ import {useMemo, useState} from 'react';
6
+ import {ComplexSelector} from '@astryxdesign/core/ComplexSelector';
7
+ import {TextInput} from '@astryxdesign/core/TextInput';
8
+ import {TreeList, type TreeListItemData} from '@astryxdesign/core/TreeList';
9
+ import {Text} from '@astryxdesign/core/Text';
10
+ import {VStack} from '@astryxdesign/core/Layout';
11
+
12
+ interface DestinationNode {
13
+ id: string;
14
+ label: string;
15
+ path: string;
16
+ children?: DestinationNode[];
17
+ }
18
+
19
+ interface Destination {
20
+ id: string;
21
+ label: string;
22
+ path: string;
23
+ }
24
+
25
+ const destinationTree: DestinationNode[] = [
26
+ {
27
+ id: 'workspace',
28
+ label: 'Workspace',
29
+ path: '/Workspace',
30
+ children: [
31
+ {
32
+ id: 'research',
33
+ label: 'Research',
34
+ path: '/Workspace/Research',
35
+ children: [
36
+ {
37
+ id: 'field-notes',
38
+ label: 'Field notes',
39
+ path: '/Workspace/Research/Field notes',
40
+ },
41
+ {
42
+ id: 'interviews',
43
+ label: 'Interviews',
44
+ path: '/Workspace/Research/Interviews',
45
+ },
46
+ ],
47
+ },
48
+ {
49
+ id: 'roadmap',
50
+ label: 'Roadmap',
51
+ path: '/Workspace/Roadmap',
52
+ },
53
+ ],
54
+ },
55
+ {
56
+ id: 'teams',
57
+ label: 'Teams',
58
+ path: '/Teams',
59
+ children: [
60
+ {
61
+ id: 'design-systems',
62
+ label: 'Design systems',
63
+ path: '/Teams/Design systems',
64
+ children: [
65
+ {
66
+ id: 'accessibility',
67
+ label: 'Accessibility',
68
+ path: '/Teams/Design systems/Accessibility',
69
+ },
70
+ ],
71
+ },
72
+ ],
73
+ },
74
+ ];
75
+
76
+ function matches(node: DestinationNode, query: string): boolean {
77
+ if (node.label.toLowerCase().includes(query)) {
78
+ return true;
79
+ }
80
+ return (node.children ?? []).some(child => matches(child, query));
81
+ }
82
+
83
+ function filterTree(
84
+ nodes: DestinationNode[],
85
+ query: string,
86
+ ): DestinationNode[] {
87
+ if (!query) {
88
+ return nodes;
89
+ }
90
+ return nodes
91
+ .filter(node => matches(node, query))
92
+ .map(node => ({
93
+ ...node,
94
+ children: node.children ? filterTree(node.children, query) : undefined,
95
+ }));
96
+ }
97
+
98
+ function toItems(
99
+ nodes: DestinationNode[],
100
+ selectedId: string,
101
+ onSelect: (value: Destination) => void,
102
+ ): TreeListItemData[] {
103
+ return nodes.map(node => {
104
+ const hasChildren = (node.children ?? []).length > 0;
105
+ return {
106
+ id: node.id,
107
+ label: node.label,
108
+ isSelected: node.id === selectedId,
109
+ isExpanded: true,
110
+ onClick: hasChildren
111
+ ? undefined
112
+ : () => onSelect({id: node.id, label: node.label, path: node.path}),
113
+ children: hasChildren
114
+ ? toItems(node.children ?? [], selectedId, onSelect)
115
+ : undefined,
116
+ };
117
+ });
118
+ }
119
+
120
+ function DestinationSearch({
121
+ value,
122
+ onChange,
123
+ close,
124
+ }: {
125
+ value: Destination;
126
+ onChange: (value: Destination) => void;
127
+ close: () => void;
128
+ }) {
129
+ const [query, setQuery] = useState('');
130
+ const items = useMemo(
131
+ () =>
132
+ toItems(
133
+ filterTree(destinationTree, query.toLowerCase()),
134
+ value.id,
135
+ next => {
136
+ onChange(next);
137
+ close();
138
+ },
139
+ ),
140
+ [query, value.id, onChange, close],
141
+ );
142
+
143
+ return (
144
+ <VStack gap={3} style={{width: 360}}>
145
+ <TextInput
146
+ label="Search destinations"
147
+ isLabelHidden
148
+ value={query}
149
+ onChange={setQuery}
150
+ hasClear
151
+ placeholder="Search folders or teams"
152
+ />
153
+ {items.length > 0 ? (
154
+ <TreeList items={items} density="compact" />
155
+ ) : (
156
+ <Text type="supporting" color="secondary">
157
+ No matching destinations.
158
+ </Text>
159
+ )}
160
+ </VStack>
161
+ );
162
+ }
163
+
164
+ export default function ComplexSelectorTreeSearch() {
165
+ const [value, setValue] = useState<Destination>({
166
+ id: 'accessibility',
167
+ label: 'Accessibility',
168
+ path: '/Teams/Design systems/Accessibility',
169
+ });
170
+
171
+ return (
172
+ <ComplexSelector<Destination>
173
+ label="Destination"
174
+ description="Search and browse nested folders."
175
+ value={value}
176
+ onChange={setValue}
177
+ triggerLabel={value.path}
178
+ style={{width: 360}}>
179
+ {(selectedValue, onChange, close) => (
180
+ <DestinationSearch
181
+ value={selectedValue}
182
+ onChange={onChange}
183
+ close={close}
184
+ />
185
+ )}
186
+ </ComplexSelector>
187
+ );
188
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/cli",
3
- "version": "0.4.2-canary.bb07062",
3
+ "version": "0.4.2-canary.e60974e",
4
4
  "displayName": "CLI",
5
5
  "description": "Scaffold projects, browse templates, generate themes, and get agent-ready docs from the command line.",
6
6
  "author": "Meta Open Source",
@@ -84,10 +84,10 @@
84
84
  "zod": "^4.4.3"
85
85
  },
86
86
  "peerDependencies": {
87
- "@astryxdesign/charts": "0.4.2-canary.bb07062",
88
- "@astryxdesign/core": "0.4.2-canary.bb07062",
89
- "@astryxdesign/lab": "0.4.2-canary.bb07062",
90
- "@astryxdesign/theme-neutral": "0.4.2-canary.bb07062",
87
+ "@astryxdesign/charts": "0.4.2-canary.e60974e",
88
+ "@astryxdesign/core": "0.4.2-canary.e60974e",
89
+ "@astryxdesign/lab": "0.4.2-canary.e60974e",
90
+ "@astryxdesign/theme-neutral": "0.4.2-canary.e60974e",
91
91
  "gpt-tokenizer": "^3.4.0"
92
92
  },
93
93
  "peerDependenciesMeta": {
@@ -105,10 +105,10 @@
105
105
  }
106
106
  },
107
107
  "devDependencies": {
108
- "@astryxdesign/charts": "0.4.2-canary.bb07062",
109
- "@astryxdesign/core": "0.4.2-canary.bb07062",
110
- "@astryxdesign/lab": "0.4.2-canary.bb07062",
111
- "@astryxdesign/theme-neutral": "0.4.2-canary.bb07062",
108
+ "@astryxdesign/charts": "0.4.2-canary.e60974e",
109
+ "@astryxdesign/core": "0.4.2-canary.e60974e",
110
+ "@astryxdesign/lab": "0.4.2-canary.e60974e",
111
+ "@astryxdesign/theme-neutral": "0.4.2-canary.e60974e",
112
112
  "gpt-tokenizer": "^3.4.0"
113
113
  },
114
114
  "scripts": {