@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.
- package/CHANGELOG.md +23 -0
- package/README.md +45 -43
- package/api/discover/_adapter.d.mts +5 -3
- package/api/discover/_adapter.mjs +6 -4
- package/api/template/cdn/cdn.d.mts +23 -0
- package/api/template/cdn/cdn.mjs +86 -0
- package/api/template/cdn/cdn.test.mjs +108 -0
- package/api/template/template.d.mts +2 -0
- package/api/template/template.doc.mjs +21 -3
- package/api/template/template.mjs +16 -2
- package/api/template/template.type.d.mts +19 -0
- package/api/template/template.type.mjs +12 -0
- package/api/theme/build/build.font-warning.test.mjs +23 -14
- package/api/theme/build/build.icons-specifier.test.mjs +149 -0
- package/api/theme/build/build.mjs +88 -16
- package/api/theme/build/build.test.mjs +185 -14
- package/api/theme/theme.type.d.mts +22 -0
- package/api/theme/theme.type.mjs +15 -1
- package/assets/cdn.template.html +124 -0
- package/assets/docs/theme.doc.dense.mjs +1 -1
- package/assets/docs/theme.doc.mjs +11 -5
- package/assets/docs/theme.doc.zh.mjs +1 -1
- package/assets/templates/blocks/components/BottomSheet/BottomSheetHeights.doc.mjs +21 -0
- package/assets/templates/blocks/components/BottomSheet/BottomSheetHeights.tsx +46 -0
- package/assets/templates/blocks/components/BottomSheet/BottomSheetMobileKeyboard.doc.mjs +23 -0
- package/assets/templates/blocks/components/BottomSheet/BottomSheetMobileKeyboard.tsx +100 -0
- package/assets/templates/blocks/components/BottomSheet/BottomSheetNoScrim.doc.mjs +22 -0
- package/assets/templates/blocks/components/BottomSheet/BottomSheetNoScrim.tsx +47 -0
- package/assets/templates/blocks/components/BottomSheet/BottomSheetShowcase.doc.mjs +22 -0
- package/assets/templates/blocks/components/BottomSheet/BottomSheetShowcase.tsx +51 -0
- package/assets/templates/blocks/components/BottomSheet/BottomSheetSwitcherShowcase.doc.mjs +26 -0
- package/assets/templates/blocks/components/BottomSheet/BottomSheetSwitcherShowcase.tsx +221 -0
- package/assets/templates/blocks/components/ComplexSelector/ComplexSelectorDeadlinePicker.doc.mjs +20 -0
- package/assets/templates/blocks/components/ComplexSelector/ComplexSelectorDeadlinePicker.tsx +87 -0
- package/assets/templates/blocks/components/ComplexSelector/ComplexSelectorShowcase.doc.mjs +15 -0
- package/assets/templates/blocks/components/ComplexSelector/ComplexSelectorShowcase.tsx +199 -0
- package/assets/templates/blocks/components/ComplexSelector/ComplexSelectorTreeSearch.doc.mjs +14 -0
- package/assets/templates/blocks/components/ComplexSelector/ComplexSelectorTreeSearch.tsx +188 -0
- package/assets/templates/themes/neutral/neutralTheme.ts +6 -4
- package/assets/theme.template.ts +4 -3
- package/authoring/doctypes/base/type.ts +9 -0
- package/clients/cli/commands/build-theme.font-warning.test.mjs +9 -6
- package/clients/cli/commands/build-theme.icons-specifier.test.mjs +132 -7
- package/clients/cli/commands/build-theme.mjs +154 -65
- package/clients/cli/commands/build-theme.multi.test.mjs +148 -0
- package/clients/cli/commands/build-theme.watch.test.mjs +67 -0
- package/clients/cli/commands/discover.broken-integration.test.mjs +112 -0
- package/clients/cli/commands/discover.mjs +12 -0
- package/clients/cli/commands/search.mjs +9 -0
- package/clients/cli/commands/template-cdn.behavior.test.mjs +113 -0
- package/clients/cli/commands/template.doc.mjs +12 -2
- package/clients/cli/commands/template.mjs +24 -3
- package/clients/cli/commands/theme-build.doc.mjs +11 -5
- package/clients/cli/lib/json-shim.test.mjs +1 -1
- package/clients/cli/lib/manifest.mjs +3 -2
- package/clients/cli/lib/manifest.test.mjs +4 -2
- package/foundation/fs/path-safety.mjs +2 -2
- package/foundation/fs/path-safety.test.mjs +7 -0
- package/foundation/integrations/integration-warnings.test.mjs +17 -0
- package/foundation/integrations/validate-contributions.mjs +7 -0
- package/foundation/response/response-types.doc.mjs +11 -0
- package/foundation/text/copyright-header.mjs +11 -4
- package/package.json +12 -9
- package/assets/templates/pages/table-page-chart/page.tsx +0 -577
- package/assets/templates/pages/table-page-chart/template.doc.mjs +0 -13
- package/assets/templates/pages/table-page-heatmap-status/page.tsx +0 -467
- package/assets/templates/pages/table-page-heatmap-status/template.doc.mjs +0 -13
- package/assets/templates/pages/table-page-shoe-store-heatmap/page.tsx +0 -931
- 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
|
+
}
|
package/assets/templates/blocks/components/ComplexSelector/ComplexSelectorDeadlinePicker.doc.mjs
ADDED
|
@@ -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
|
+
};
|