@astryxdesign/cli 0.4.2 → 0.4.3-canary.00bed29

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.
Files changed (69) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +45 -43
  3. package/api/discover/_adapter.d.mts +5 -3
  4. package/api/discover/_adapter.mjs +6 -4
  5. package/api/template/cdn/cdn.d.mts +23 -0
  6. package/api/template/cdn/cdn.mjs +86 -0
  7. package/api/template/cdn/cdn.test.mjs +108 -0
  8. package/api/template/template.d.mts +2 -0
  9. package/api/template/template.doc.mjs +21 -3
  10. package/api/template/template.mjs +16 -2
  11. package/api/template/template.type.d.mts +19 -0
  12. package/api/template/template.type.mjs +12 -0
  13. package/api/theme/build/build.font-warning.test.mjs +23 -14
  14. package/api/theme/build/build.icons-specifier.test.mjs +149 -0
  15. package/api/theme/build/build.mjs +88 -16
  16. package/api/theme/build/build.test.mjs +185 -14
  17. package/api/theme/theme.type.d.mts +22 -0
  18. package/api/theme/theme.type.mjs +15 -1
  19. package/assets/cdn.template.html +124 -0
  20. package/assets/docs/theme.doc.dense.mjs +1 -1
  21. package/assets/docs/theme.doc.mjs +11 -5
  22. package/assets/docs/theme.doc.zh.mjs +1 -1
  23. package/assets/templates/blocks/components/BottomSheet/BottomSheetHeights.doc.mjs +21 -0
  24. package/assets/templates/blocks/components/BottomSheet/BottomSheetHeights.tsx +46 -0
  25. package/assets/templates/blocks/components/BottomSheet/BottomSheetMobileKeyboard.doc.mjs +23 -0
  26. package/assets/templates/blocks/components/BottomSheet/BottomSheetMobileKeyboard.tsx +100 -0
  27. package/assets/templates/blocks/components/BottomSheet/BottomSheetNoScrim.doc.mjs +22 -0
  28. package/assets/templates/blocks/components/BottomSheet/BottomSheetNoScrim.tsx +47 -0
  29. package/assets/templates/blocks/components/BottomSheet/BottomSheetShowcase.doc.mjs +22 -0
  30. package/assets/templates/blocks/components/BottomSheet/BottomSheetShowcase.tsx +51 -0
  31. package/assets/templates/blocks/components/BottomSheet/BottomSheetSwitcherShowcase.doc.mjs +26 -0
  32. package/assets/templates/blocks/components/BottomSheet/BottomSheetSwitcherShowcase.tsx +221 -0
  33. package/assets/templates/blocks/components/ComplexSelector/ComplexSelectorDeadlinePicker.doc.mjs +20 -0
  34. package/assets/templates/blocks/components/ComplexSelector/ComplexSelectorDeadlinePicker.tsx +87 -0
  35. package/assets/templates/blocks/components/ComplexSelector/ComplexSelectorShowcase.doc.mjs +15 -0
  36. package/assets/templates/blocks/components/ComplexSelector/ComplexSelectorShowcase.tsx +199 -0
  37. package/assets/templates/blocks/components/ComplexSelector/ComplexSelectorTreeSearch.doc.mjs +14 -0
  38. package/assets/templates/blocks/components/ComplexSelector/ComplexSelectorTreeSearch.tsx +188 -0
  39. package/assets/templates/themes/neutral/neutralTheme.ts +6 -4
  40. package/assets/theme.template.ts +4 -3
  41. package/authoring/doctypes/base/type.ts +9 -0
  42. package/clients/cli/commands/build-theme.font-warning.test.mjs +9 -6
  43. package/clients/cli/commands/build-theme.icons-specifier.test.mjs +132 -7
  44. package/clients/cli/commands/build-theme.mjs +154 -65
  45. package/clients/cli/commands/build-theme.multi.test.mjs +148 -0
  46. package/clients/cli/commands/build-theme.watch.test.mjs +67 -0
  47. package/clients/cli/commands/discover.broken-integration.test.mjs +112 -0
  48. package/clients/cli/commands/discover.mjs +12 -0
  49. package/clients/cli/commands/search.mjs +9 -0
  50. package/clients/cli/commands/template-cdn.behavior.test.mjs +113 -0
  51. package/clients/cli/commands/template.doc.mjs +12 -2
  52. package/clients/cli/commands/template.mjs +24 -3
  53. package/clients/cli/commands/theme-build.doc.mjs +11 -5
  54. package/clients/cli/lib/json-shim.test.mjs +1 -1
  55. package/clients/cli/lib/manifest.mjs +3 -2
  56. package/clients/cli/lib/manifest.test.mjs +4 -2
  57. package/foundation/fs/path-safety.mjs +2 -2
  58. package/foundation/fs/path-safety.test.mjs +7 -0
  59. package/foundation/integrations/integration-warnings.test.mjs +17 -0
  60. package/foundation/integrations/validate-contributions.mjs +7 -0
  61. package/foundation/response/response-types.doc.mjs +11 -0
  62. package/foundation/text/copyright-header.mjs +11 -4
  63. package/package.json +12 -9
  64. package/assets/templates/pages/table-page-chart/page.tsx +0 -577
  65. package/assets/templates/pages/table-page-chart/template.doc.mjs +0 -13
  66. package/assets/templates/pages/table-page-heatmap-status/page.tsx +0 -467
  67. package/assets/templates/pages/table-page-heatmap-status/template.doc.mjs +0 -13
  68. package/assets/templates/pages/table-page-shoe-store-heatmap/page.tsx +0 -931
  69. package/assets/templates/pages/table-page-shoe-store-heatmap/template.doc.mjs +0 -13
@@ -0,0 +1,221 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ import {useState} from 'react';
6
+ import {BottomSheet, BottomSheetSwitcher} from '@astryxdesign/core/BottomSheet';
7
+ import {Button} from '@astryxdesign/core/Button';
8
+ import {CheckboxInput} from '@astryxdesign/core/CheckboxInput';
9
+ import {Divider} from '@astryxdesign/core/Divider';
10
+ import {Heading} from '@astryxdesign/core/Heading';
11
+ import {Section} from '@astryxdesign/core/Section';
12
+ import {HStack, VStack} from '@astryxdesign/core/Stack';
13
+ import {Text} from '@astryxdesign/core/Text';
14
+ import {RadioList, RadioListItem} from '@astryxdesign/core/RadioList';
15
+
16
+ type NotificationSheetHeight = 'hug' | 'capped';
17
+
18
+ interface NotificationOverviewSheetProps {
19
+ height: NotificationSheetHeight;
20
+ onCancel: () => void;
21
+ onContinue: () => void;
22
+ }
23
+
24
+ function NotificationOverviewSheet({
25
+ height,
26
+ onCancel,
27
+ onContinue,
28
+ }: NotificationOverviewSheetProps) {
29
+ return (
30
+ <BottomSheet
31
+ sheetId="overview"
32
+ label="Set up notifications"
33
+ height={height}>
34
+ <Section padding={4}>
35
+ <VStack gap={4}>
36
+ <VStack gap={1}>
37
+ <Heading level={3}>Set up notifications</Heading>
38
+ <Text type="supporting" color="secondary">
39
+ Step 1 of 3
40
+ </Text>
41
+ </VStack>
42
+ <Divider />
43
+ <Text type="supporting" color="secondary">
44
+ Stay informed about activity that matters without checking back
45
+ throughout the day.
46
+ </Text>
47
+ <VStack gap={3}>
48
+ <VStack gap={1}>
49
+ <Text type="label">Important activity</Text>
50
+ <Text type="supporting" color="secondary">
51
+ Know when someone mentions you or needs your attention.
52
+ </Text>
53
+ </VStack>
54
+ <VStack gap={1}>
55
+ <Text type="label">Timely reminders</Text>
56
+ <Text type="supporting" color="secondary">
57
+ Get a reminder before work reaches its due date.
58
+ </Text>
59
+ </VStack>
60
+ <VStack gap={1}>
61
+ <Text type="label">Useful summaries</Text>
62
+ <Text type="supporting" color="secondary">
63
+ Catch up on anything you may have missed.
64
+ </Text>
65
+ </VStack>
66
+ </VStack>
67
+ <HStack gap={2} hAlign="end">
68
+ <Button label="Cancel" variant="secondary" onClick={onCancel} />
69
+ <Button label="Continue" onClick={onContinue} />
70
+ </HStack>
71
+ </VStack>
72
+ </Section>
73
+ </BottomSheet>
74
+ );
75
+ }
76
+
77
+ interface NotificationFrequencySheetProps {
78
+ height: NotificationSheetHeight;
79
+ onBack: () => void;
80
+ onContinue: () => void;
81
+ }
82
+
83
+ function NotificationFrequencySheet({
84
+ height,
85
+ onBack,
86
+ onContinue,
87
+ }: NotificationFrequencySheetProps) {
88
+ const [frequency, setFrequency] = useState('daily');
89
+
90
+ return (
91
+ <BottomSheet
92
+ sheetId="frequency"
93
+ label="Notification frequency"
94
+ height={height}>
95
+ <Section padding={4}>
96
+ <VStack gap={4}>
97
+ <VStack gap={1}>
98
+ <Heading level={3}>How often?</Heading>
99
+ <Text type="supporting" color="secondary">
100
+ Step 2 of 3
101
+ </Text>
102
+ </VStack>
103
+ <Divider />
104
+ <RadioList
105
+ label="Notification frequency"
106
+ isLabelHidden
107
+ value={frequency}
108
+ onChange={setFrequency}>
109
+ <RadioListItem label="Immediately" value="immediately" />
110
+ <RadioListItem label="Daily" value="daily" />
111
+ <RadioListItem label="Weekly" value="weekly" />
112
+ </RadioList>
113
+ <HStack gap={2} hAlign="end">
114
+ <Button label="Back" variant="secondary" onClick={onBack} />
115
+ <Button label="Continue" onClick={onContinue} />
116
+ </HStack>
117
+ </VStack>
118
+ </Section>
119
+ </BottomSheet>
120
+ );
121
+ }
122
+
123
+ interface NotificationChannelsSheetProps {
124
+ height: NotificationSheetHeight;
125
+ onBack: () => void;
126
+ onFinish: () => void;
127
+ }
128
+
129
+ function NotificationChannelsSheet({
130
+ height,
131
+ onBack,
132
+ onFinish,
133
+ }: NotificationChannelsSheetProps) {
134
+ const [email, setEmail] = useState(true);
135
+ const [pushNotifications, setPushNotifications] = useState(true);
136
+ const [textMessages, setTextMessages] = useState(false);
137
+
138
+ return (
139
+ <BottomSheet
140
+ sheetId="channels"
141
+ label="Notification channels"
142
+ height={height}>
143
+ <Section padding={4}>
144
+ <VStack gap={4}>
145
+ <VStack gap={1}>
146
+ <Heading level={3}>Where should we notify you?</Heading>
147
+ <Text type="supporting" color="secondary">
148
+ Step 3 of 3
149
+ </Text>
150
+ </VStack>
151
+ <Divider />
152
+ <Text type="supporting" color="secondary">
153
+ Choose any combination. You can change these preferences later.
154
+ </Text>
155
+ <VStack gap={2}>
156
+ <CheckboxInput label="Email" value={email} onChange={setEmail} />
157
+ <CheckboxInput
158
+ label="Push notifications"
159
+ value={pushNotifications}
160
+ onChange={setPushNotifications}
161
+ />
162
+ <CheckboxInput
163
+ label="Text messages"
164
+ value={textMessages}
165
+ onChange={setTextMessages}
166
+ />
167
+ </VStack>
168
+ <HStack gap={2} hAlign="end">
169
+ <Button label="Back" variant="secondary" onClick={onBack} />
170
+ <Button label="Finish" onClick={onFinish} />
171
+ </HStack>
172
+ </VStack>
173
+ </Section>
174
+ </BottomSheet>
175
+ );
176
+ }
177
+
178
+ interface MultiStepSwitcherExampleProps {
179
+ height: NotificationSheetHeight;
180
+ hasScrim?: boolean;
181
+ }
182
+
183
+ function MultiStepSwitcherExample({
184
+ height,
185
+ hasScrim = true,
186
+ }: MultiStepSwitcherExampleProps) {
187
+ const [activeSheet, setActiveSheet] = useState<string | null>(null);
188
+
189
+ return (
190
+ <>
191
+ <Button
192
+ label="Set up notifications"
193
+ onClick={() => setActiveSheet('overview')}
194
+ />
195
+ <BottomSheetSwitcher
196
+ activeSheet={activeSheet}
197
+ onActiveSheetChange={setActiveSheet}
198
+ hasScrim={hasScrim}>
199
+ <NotificationOverviewSheet
200
+ height={height}
201
+ onCancel={() => setActiveSheet(null)}
202
+ onContinue={() => setActiveSheet('frequency')}
203
+ />
204
+ <NotificationFrequencySheet
205
+ height={height}
206
+ onBack={() => setActiveSheet('overview')}
207
+ onContinue={() => setActiveSheet('channels')}
208
+ />
209
+ <NotificationChannelsSheet
210
+ height={height}
211
+ onBack={() => setActiveSheet('frequency')}
212
+ onFinish={() => setActiveSheet(null)}
213
+ />
214
+ </BottomSheetSwitcher>
215
+ </>
216
+ );
217
+ }
218
+
219
+ export default function BottomSheetSwitcherShowcase() {
220
+ return <MultiStepSwitcherExample height="hug" />;
221
+ }
@@ -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
+ };