@astryxdesign/lab 0.6.2-canary.fc74d7e → 0.6.3-canary.039a67e

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,54 @@
1
+ /**
2
+ * @file MobileTokenizer.tsx
3
+ * @input Uses public @astryxdesign/core components only (BottomSheet,
4
+ * BottomSheetSwitcher, Button, Field, Heading, Icon, IconButton, Text,
5
+ * TextInput, Token)
6
+ * @output Exports MobileTokenizer — Lab prototype of the touch Tokenizer
7
+ * flow (stacked manage + add sheets)
8
+ * @position Lab (canary) stack layer 1: validates the design before the
9
+ * Core promotion (Tokenizer presentation="bottom-sheet") stacked after it.
10
+ *
11
+ * API mirrors Core Tokenizer (label/searchSource/value/onChange/change,
12
+ * hasCreate, maxEntries) so graduation is an import swap. Differences from
13
+ * Core are the Lab deltas: the filter is a bordered TextInput (Core's
14
+ * PanelSearchInput is not exported), strings are literals (no i18n keys),
15
+ * and there is no theming/spec contract yet.
16
+ */
17
+ import { type ReactNode } from 'react';
18
+ import type { BaseProps } from '@astryxdesign/core';
19
+ import type { SearchableItem, SearchSource } from '@astryxdesign/core/Typeahead';
20
+ export type MobileTokenizerChange<T extends SearchableItem> = {
21
+ item: T;
22
+ type: 'add';
23
+ } | {
24
+ item: T;
25
+ type: 'create';
26
+ } | {
27
+ item: T;
28
+ type: 'remove';
29
+ } | {
30
+ type: 'reorder';
31
+ };
32
+ export interface MobileTokenizerProps<T extends SearchableItem> extends Omit<BaseProps<HTMLDivElement>, 'onChange'> {
33
+ label: string;
34
+ searchSource: SearchSource<T>;
35
+ value: T[];
36
+ onChange: (items: T[], change: MobileTokenizerChange<T>) => void;
37
+ renderItem?: (item: T) => ReactNode;
38
+ placeholder?: string;
39
+ description?: string;
40
+ isOptional?: boolean;
41
+ isDisabled?: boolean;
42
+ hasCreate?: boolean;
43
+ maxEntries?: number;
44
+ maxMenuItems?: number;
45
+ minQueryLength?: number;
46
+ debounceMs?: number;
47
+ emptySearchResultsText?: string;
48
+ onChangeQuery?: (query: string) => void;
49
+ }
50
+ export declare function MobileTokenizer<T extends SearchableItem>({ label, searchSource, value, onChange, renderItem, placeholder, description, isOptional, isDisabled, hasCreate, maxEntries, maxMenuItems, minQueryLength, debounceMs, emptySearchResultsText, onChangeQuery, xstyle, }: MobileTokenizerProps<T>): import("react").JSX.Element;
51
+ export declare namespace MobileTokenizer {
52
+ var displayName: string;
53
+ }
54
+ //# sourceMappingURL=MobileTokenizer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MobileTokenizer.d.ts","sourceRoot":"","sources":["../../src/MobileTokenizer/MobileTokenizer.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAML,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAEf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,oBAAoB,CAAC;AAWlD,OAAO,KAAK,EACV,cAAc,EACd,YAAY,EACb,MAAM,8BAA8B,CAAC;AAOtC,MAAM,MAAM,qBAAqB,CAAC,CAAC,SAAS,cAAc,IACtD;IAAC,IAAI,EAAE,CAAC,CAAC;IAAC,IAAI,EAAE,KAAK,CAAA;CAAC,GACtB;IAAC,IAAI,EAAE,CAAC,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAA;CAAC,GACzB;IAAC,IAAI,EAAE,CAAC,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAA;CAAC,GACzB;IAAC,IAAI,EAAE,SAAS,CAAA;CAAC,CAAC;AAEtB,MAAM,WAAW,oBAAoB,CAAC,CAAC,SAAS,cAAc,CAAE,SAAQ,IAAI,CAC1E,SAAS,CAAC,cAAc,CAAC,EACzB,UAAU,CACX;IACC,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC;IAC9B,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,qBAAqB,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;IACjE,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,SAAS,CAAC;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC;AA+GD,wBAAgB,eAAe,CAAC,CAAC,SAAS,cAAc,EAAE,EACxD,KAAK,EACL,YAAY,EACZ,KAAK,EACL,QAAQ,EACR,UAAU,EACV,WAAW,EACX,WAAW,EACX,UAAU,EACV,UAAkB,EAClB,SAAiB,EACjB,UAAU,EACV,YAAiB,EACjB,cAAkB,EAClB,UAAgB,EAChB,sBAA2C,EAC3C,aAAa,EACb,MAAM,GACP,EAAE,oBAAoB,CAAC,CAAC,CAAC,+BAyQzB;yBA3Re,eAAe"}
@@ -0,0 +1,386 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file MobileTokenizer.tsx
7
+ * @input Uses public @astryxdesign/core components only (BottomSheet,
8
+ * BottomSheetSwitcher, Button, Field, Heading, Icon, IconButton, Text,
9
+ * TextInput, Token)
10
+ * @output Exports MobileTokenizer — Lab prototype of the touch Tokenizer
11
+ * flow (stacked manage + add sheets)
12
+ * @position Lab (canary) stack layer 1: validates the design before the
13
+ * Core promotion (Tokenizer presentation="bottom-sheet") stacked after it.
14
+ *
15
+ * API mirrors Core Tokenizer (label/searchSource/value/onChange/change,
16
+ * hasCreate, maxEntries) so graduation is an import swap. Differences from
17
+ * Core are the Lab deltas: the filter is a bordered TextInput (Core's
18
+ * PanelSearchInput is not exported), strings are literals (no i18n keys),
19
+ * and there is no theming/spec contract yet.
20
+ */
21
+ import { useEffect, useId, useMemo, useRef, useState } from 'react';
22
+ import * as stylex from '@stylexjs/stylex';
23
+ import { BottomSheet } from '@astryxdesign/core/BottomSheet';
24
+ import { BottomSheetSwitcher } from '@astryxdesign/core/BottomSheet';
25
+ import { Button } from '@astryxdesign/core/Button';
26
+ import { Divider } from '@astryxdesign/core/Divider';
27
+ import { Field } from '@astryxdesign/core/Field';
28
+ import { Icon } from '@astryxdesign/core/Icon';
29
+ import { IconButton } from '@astryxdesign/core/IconButton';
30
+ import { Text } from '@astryxdesign/core/Text';
31
+ import { TextInput } from '@astryxdesign/core/TextInput';
32
+ import { Token } from '@astryxdesign/core/Token';
33
+ import '@astryxdesign/core/theme/tokens.stylex';
34
+ import { colorVars, radiusVars, spacingVars } from '@astryxdesign/core/theme/tokens.stylex';
35
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
36
+ const CREATABLE_ID_PREFIX = '__xds_create__';
37
+ const styles = {
38
+ filterInput: {
39
+ kzQI83: "x1iyjqo2",
40
+ kmuXW: "xs83m0k",
41
+ kCS8Yb: "x1r8uery",
42
+ k7Eaqz: "xeuugli",
43
+ $$css: true
44
+ },
45
+ empty: {
46
+ k8WAf4: "x1na6nto",
47
+ $$css: true
48
+ }
49
+ };
50
+ export function MobileTokenizer({
51
+ label,
52
+ searchSource,
53
+ value,
54
+ onChange,
55
+ renderItem,
56
+ placeholder,
57
+ description,
58
+ isOptional,
59
+ isDisabled = false,
60
+ hasCreate = false,
61
+ maxEntries,
62
+ maxMenuItems = 10,
63
+ minQueryLength = 1,
64
+ debounceMs = 150,
65
+ emptySearchResultsText = 'No results found',
66
+ onChangeQuery,
67
+ xstyle
68
+ }) {
69
+ const triggerId = useId();
70
+ const [activeSheet, setActiveSheet] = useState(null);
71
+ const selectedIds = useMemo(() => new Set(value.map(v => v.id)), [value]);
72
+ const isAtMax = maxEntries != null && value.length >= maxEntries;
73
+ const handleAdd = item => {
74
+ if (isAtMax || selectedIds.has(item.id)) {
75
+ return;
76
+ }
77
+ if (hasCreate && typeof item.id === 'string' && item.id.startsWith(CREATABLE_ID_PREFIX)) {
78
+ const created = item.id.slice(CREATABLE_ID_PREFIX.length);
79
+ if (selectedIds.has(created)) {
80
+ return;
81
+ }
82
+ const real = {
83
+ id: created,
84
+ label: created
85
+ };
86
+ onChange([...value, real], {
87
+ item: real,
88
+ type: 'create'
89
+ });
90
+ return;
91
+ }
92
+ onChange([...value, item], {
93
+ item,
94
+ type: 'add'
95
+ });
96
+ };
97
+ const handleRemove = item => onChange(value.filter(v => v.id !== item.id), {
98
+ item,
99
+ type: 'remove'
100
+ });
101
+ const handleClearAll = () => {
102
+ if (value.length > 0) {
103
+ onChange([], {
104
+ item: value[value.length - 1],
105
+ type: 'remove'
106
+ });
107
+ }
108
+ };
109
+
110
+ // ---- add-sheet search ----
111
+ const [query, setQuery] = useState('');
112
+ const [results, setResults] = useState([]);
113
+ const [isSearching, setIsSearching] = useState(false);
114
+ const seqRef = useRef(0);
115
+ const isAddOpen = activeSheet === 'add';
116
+ useEffect(() => {
117
+ if (!isAddOpen) {
118
+ return;
119
+ }
120
+ const seq = ++seqRef.current;
121
+ const run = async () => {
122
+ setIsSearching(true);
123
+ try {
124
+ const trimmed = query.trim();
125
+ let items;
126
+ if (trimmed === '') {
127
+ items = searchSource.bootstrap ? await searchSource.bootstrap() : await searchSource.search('');
128
+ } else if (trimmed.length < minQueryLength) {
129
+ items = [];
130
+ } else {
131
+ items = await searchSource.search(query);
132
+ }
133
+ if (seqRef.current === seq) {
134
+ setResults(items.slice(0, maxMenuItems));
135
+ }
136
+ } finally {
137
+ if (seqRef.current === seq) {
138
+ setIsSearching(false);
139
+ }
140
+ }
141
+ };
142
+ if (debounceMs > 0) {
143
+ const timer = setTimeout(() => {
144
+ void run();
145
+ }, debounceMs);
146
+ return () => clearTimeout(timer);
147
+ }
148
+ void run();
149
+ }, [isAddOpen, query, searchSource, minQueryLength, maxMenuItems, debounceMs]);
150
+ const createItem = useMemo(() => {
151
+ const trimmed = query.trim();
152
+ if (!hasCreate || trimmed === '' || isAtMax) {
153
+ return null;
154
+ }
155
+ if (selectedIds.has(trimmed) || results.some(r => r.label.toLowerCase() === trimmed.toLowerCase())) {
156
+ return null;
157
+ }
158
+ return {
159
+ id: `${CREATABLE_ID_PREFIX}${trimmed}`,
160
+ label: `Create "${trimmed}"`
161
+ };
162
+ }, [hasCreate, query, results, selectedIds, isAtMax]);
163
+ return /*#__PURE__*/_jsxs(Field, {
164
+ label: label,
165
+ description: description,
166
+ isOptional: isOptional,
167
+ isDisabled: isDisabled,
168
+ inputID: triggerId,
169
+ xstyle: xstyle,
170
+ children: [/*#__PURE__*/_jsxs("button", {
171
+ type: "button",
172
+ id: triggerId,
173
+ "aria-haspopup": "dialog",
174
+ "aria-expanded": activeSheet != null,
175
+ "aria-label": label,
176
+ disabled: isDisabled,
177
+ onClick: () => setActiveSheet('manage'),
178
+ ...{
179
+ className: "x78zum5 x1a02dak x6s0dn4 xzye2dw xh8yej3 x1gg8mnh xu0wf1k xf314gf xmkeg23 x1y0btm7 x14i3s5s xh6dtrn xjbqb8w x1yc453h x1ypdohk x16khyan"
180
+ },
181
+ children: [value.length > 0 ? value.map(item => /*#__PURE__*/_jsx(Token, {
182
+ label: item.label,
183
+ isDisabled: isDisabled
184
+ }, item.id)) : /*#__PURE__*/_jsx("span", {
185
+ ...{
186
+ className: "xv1l7n4"
187
+ },
188
+ children: placeholder ?? ''
189
+ }), /*#__PURE__*/_jsx("span", {
190
+ ...{
191
+ className: "xvc5jky x78zum5 x2lah0s"
192
+ },
193
+ "aria-hidden": "true",
194
+ children: /*#__PURE__*/_jsx(Icon, {
195
+ icon: "chevronDown",
196
+ size: "sm",
197
+ color: "secondary"
198
+ })
199
+ })]
200
+ }), !isDisabled && /*#__PURE__*/_jsxs(BottomSheetSwitcher, {
201
+ activeSheet: activeSheet,
202
+ onActiveSheetChange: id => setActiveSheet(id),
203
+ children: [/*#__PURE__*/_jsx(BottomSheet, {
204
+ sheetId: "manage",
205
+ label: label,
206
+ height: "hug",
207
+ children: /*#__PURE__*/_jsxs("div", {
208
+ ...{
209
+ className: "x78zum5 xdt5ytf x5yr21d x2lwn1j xrrkdod x1vlblms xvmdzux"
210
+ },
211
+ children: [/*#__PURE__*/_jsx("div", {
212
+ ...{
213
+ className: "x78zum5 x13a6bvl x1p37lm5"
214
+ },
215
+ children: /*#__PURE__*/_jsx(IconButton, {
216
+ label: "Close",
217
+ icon: /*#__PURE__*/_jsx(Icon, {
218
+ icon: "close"
219
+ }),
220
+ variant: "ghost",
221
+ size: "sm",
222
+ onClick: () => setActiveSheet(null)
223
+ })
224
+ }), /*#__PURE__*/_jsx("div", {
225
+ ...{
226
+ className: "x78zum5 xdt5ytf x1odjw0f x2lwn1j"
227
+ },
228
+ "data-testid": "mobile-tokenizer-manage-list",
229
+ children: value.length === 0 ? /*#__PURE__*/_jsx(Text, {
230
+ type: "supporting",
231
+ color: "secondary",
232
+ xstyle: styles.empty,
233
+ children: "No items yet"
234
+ }) : value.map((item, i) => /*#__PURE__*/_jsxs("div", {
235
+ children: [i > 0 && /*#__PURE__*/_jsx(Divider, {}), /*#__PURE__*/_jsxs("div", {
236
+ ...{
237
+ className: "x78zum5 x6s0dn4 x1txdalj xh8yej3 x1gg8mnh xce4md1 x7a5moj x1yc453h xjbqb8w xc342km x1tgivj0 x1ypdohk x16khyan"
238
+ },
239
+ children: [/*#__PURE__*/_jsx("span", {
240
+ ...{
241
+ className: "x1iyjqo2 xeuugli"
242
+ },
243
+ children: /*#__PURE__*/_jsx(Text, {
244
+ type: "body",
245
+ children: item.label
246
+ })
247
+ }), /*#__PURE__*/_jsx(IconButton, {
248
+ label: `Remove ${item.label}`,
249
+ icon: /*#__PURE__*/_jsx(Icon, {
250
+ icon: "close"
251
+ }),
252
+ variant: "ghost",
253
+ size: "sm",
254
+ onClick: () => handleRemove(item)
255
+ })]
256
+ })]
257
+ }, item.id))
258
+ }), /*#__PURE__*/_jsxs("div", {
259
+ ...{
260
+ className: "x78zum5 x6s0dn4 x1qughib x1txdalj x1vlblms xr1yuqi"
261
+ },
262
+ children: [/*#__PURE__*/_jsx(Button, {
263
+ label: "Clear all",
264
+ variant: "ghost",
265
+ isDisabled: value.length === 0,
266
+ onClick: handleClearAll
267
+ }), /*#__PURE__*/_jsx(Button, {
268
+ label: "Add item",
269
+ variant: "primary",
270
+ isDisabled: isAtMax,
271
+ onClick: () => setActiveSheet('add')
272
+ })]
273
+ })]
274
+ })
275
+ }), /*#__PURE__*/_jsx(BottomSheet, {
276
+ sheetId: "add",
277
+ label: "Add item",
278
+ height: "tall",
279
+ children: /*#__PURE__*/_jsxs("div", {
280
+ ...{
281
+ className: "x78zum5 xdt5ytf x5yr21d x2lwn1j xrrkdod x1vlblms xvmdzux"
282
+ },
283
+ children: [/*#__PURE__*/_jsxs("div", {
284
+ ...{
285
+ className: "x78zum5 xdt5ytf x1odjw0f x2lwn1j"
286
+ },
287
+ role: "listbox",
288
+ "aria-multiselectable": "true",
289
+ "aria-label": label,
290
+ "data-testid": "mobile-tokenizer-add-list",
291
+ children: [createItem != null && /*#__PURE__*/_jsxs("button", {
292
+ type: "button",
293
+ role: "option",
294
+ "aria-selected": false,
295
+ ...{
296
+ className: "x78zum5 x6s0dn4 x1txdalj xh8yej3 x1gg8mnh xce4md1 x7a5moj x1yc453h xjbqb8w xc342km x1tgivj0 x1ypdohk x16khyan"
297
+ },
298
+ onClick: () => {
299
+ handleAdd(createItem);
300
+ setQuery('');
301
+ },
302
+ children: [/*#__PURE__*/_jsx("span", {
303
+ ...{
304
+ className: "x1iyjqo2 xeuugli"
305
+ },
306
+ children: /*#__PURE__*/_jsx(Text, {
307
+ type: "body",
308
+ children: createItem.label
309
+ })
310
+ }), /*#__PURE__*/_jsx("span", {
311
+ ...{
312
+ className: "x78zum5 x6s0dn4 xl56j7k xgd8bvy x1fgtraw x2lah0s xjspbzw xmkeg23 x1y0btm7 x14i3s5s xosj86m xo5v014"
313
+ },
314
+ "aria-hidden": "true",
315
+ children: "+"
316
+ })]
317
+ }), !isSearching && results.length === 0 && createItem == null ? /*#__PURE__*/_jsx(Text, {
318
+ type: "supporting",
319
+ color: "secondary",
320
+ xstyle: styles.empty,
321
+ children: emptySearchResultsText
322
+ }) : results.map(item => {
323
+ const isSelected = selectedIds.has(item.id);
324
+ const rowDisabled = !isSelected && isAtMax;
325
+ return /*#__PURE__*/_jsxs("button", {
326
+ type: "button",
327
+ role: "option",
328
+ "aria-selected": isSelected,
329
+ "aria-disabled": rowDisabled || undefined,
330
+ ...{
331
+ 0: {
332
+ className: "x78zum5 x6s0dn4 x1txdalj xh8yej3 x1gg8mnh xce4md1 x7a5moj x1yc453h xjbqb8w xc342km x1tgivj0 x1ypdohk x16khyan"
333
+ },
334
+ 1: {
335
+ className: "x78zum5 x6s0dn4 x1txdalj xh8yej3 x1gg8mnh xce4md1 x7a5moj x1yc453h xjbqb8w xc342km x1tgivj0 x1ypdohk x16khyan xbyyjgo"
336
+ }
337
+ }[!!rowDisabled << 0],
338
+ onClick: () => isSelected ? handleRemove(item) : handleAdd(item),
339
+ children: [/*#__PURE__*/_jsx("span", {
340
+ ...{
341
+ className: "x1iyjqo2 xeuugli"
342
+ },
343
+ children: renderItem ? renderItem(item) : /*#__PURE__*/_jsx(Text, {
344
+ type: "body",
345
+ children: item.label
346
+ })
347
+ }), /*#__PURE__*/_jsx("span", {
348
+ ...{
349
+ className: "x78zum5 x6s0dn4 xl56j7k xgd8bvy x1fgtraw x2lah0s xjspbzw xmkeg23 x1y0btm7 x14i3s5s xosj86m xo5v014"
350
+ },
351
+ "aria-hidden": "true",
352
+ children: isSelected ? /*#__PURE__*/_jsx(Icon, {
353
+ icon: "check",
354
+ size: "sm"
355
+ }) : '+'
356
+ })]
357
+ }, item.id);
358
+ })]
359
+ }), /*#__PURE__*/_jsx(Divider, {}), /*#__PURE__*/_jsxs("div", {
360
+ ...{
361
+ className: "x78zum5 xuk3077 x1txdalj x1xye8es xr1yuqi"
362
+ },
363
+ children: [/*#__PURE__*/_jsx(TextInput, {
364
+ label: `Search ${label}`,
365
+ isLabelHidden: true,
366
+ placeholder: "Search...",
367
+ value: query,
368
+ width: "100%",
369
+ hasClear: true,
370
+ onChange: next => {
371
+ setQuery(next);
372
+ onChangeQuery?.(next);
373
+ },
374
+ xstyle: styles.filterInput
375
+ }), /*#__PURE__*/_jsx(Button, {
376
+ label: "Done",
377
+ variant: "primary",
378
+ onClick: () => setActiveSheet('manage')
379
+ })]
380
+ })]
381
+ })
382
+ })]
383
+ })]
384
+ });
385
+ }
386
+ MobileTokenizer.displayName = 'MobileTokenizer';
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @file index.ts
3
+ * @input MobileTokenizer component
4
+ * @output Public MobileTokenizer entry point for @astryxdesign/lab
5
+ * @position Component barrel; re-exported by /packages/lab/src/index.ts
6
+ */
7
+ export { MobileTokenizer } from './MobileTokenizer';
8
+ export type { MobileTokenizerProps, MobileTokenizerChange, } from './MobileTokenizer';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/MobileTokenizer/index.ts"],"names":[],"mappings":"AAIA;;;;;GAKG;AAEH,OAAO,EAAC,eAAe,EAAC,MAAM,mBAAmB,CAAC;AAClD,YAAY,EACV,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,mBAAmB,CAAC"}
@@ -0,0 +1,11 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file index.ts
7
+ * @input MobileTokenizer component
8
+ * @output Public MobileTokenizer entry point for @astryxdesign/lab
9
+ * @position Component barrel; re-exported by /packages/lab/src/index.ts
10
+ */
11
+ export { MobileTokenizer } from "./MobileTokenizer.js";
package/dist/index.d.ts CHANGED
@@ -28,4 +28,5 @@ export { SankeyChart, type SankeyChartProps, SankeyLink, type SankeyLinkProps, S
28
28
  export * from './CircularProgress';
29
29
  export * from './ListInput';
30
30
  export { LogStream, type LogStreamProps, type LogEntry, type LogStreamLevel, } from './LogStream';
31
+ export { MobileTokenizer, type MobileTokenizerProps, type MobileTokenizerChange, } from './MobileTokenizer';
31
32
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -66,6 +66,11 @@ export * from "./ListInput/index.js";
66
66
  // LogStream — experimental streaming log viewer
67
67
  export { LogStream } from "./LogStream/index.js";
68
68
 
69
+ // MobileTokenizer — Lab prototype (stack layer 1) of the touch Tokenizer
70
+ // flow: summary trigger + stacked manage/add BottomSheets. Graduates to
71
+ // Core Tokenizer presentation="bottom-sheet" (stack layer 2).
72
+ export { MobileTokenizer } from "./MobileTokenizer/index.js";
73
+
69
74
  // RichTextEditor (RFC facebook/astryx#3899) has graduated out of @astryxdesign/lab
70
75
  // into its own canary-only package, @astryxdesign/richtext, so it can be canaried
71
76
  // independently (e.g. into EPS/Nest). Import it from there:
package/dist/lab.css CHANGED
@@ -133,6 +133,7 @@
133
133
  .xu0wf1k:not(#\#):not(#\#){padding-block:var(--spacing-1)}
134
134
  .xce4md1:not(#\#):not(#\#){padding-block:var(--spacing-2)}
135
135
  .x8o8v82:not(#\#):not(#\#){padding-block:var(--spacing-3)}
136
+ .x1na6nto:not(#\#):not(#\#){padding-block:var(--spacing-4)}
136
137
  .xnjsko4:not(#\#):not(#\#){padding-inline:0}
137
138
  .x4ezzuc:not(#\#):not(#\#){padding-inline:var(--spacing-1-5)}
138
139
  .x7a5moj:not(#\#):not(#\#){padding-inline:var(--spacing-1)}
@@ -309,6 +310,7 @@
309
310
  .x3nfvp2:not(#\#):not(#\#):not(#\#){display:inline-flex}
310
311
  .x1s85apg:not(#\#):not(#\#):not(#\#){display:none}
311
312
  .xbh8q5q:not(#\#):not(#\#):not(#\#){fill:none}
313
+ .x1r8uery:not(#\#):not(#\#):not(#\#){flex-basis:0}
312
314
  .xdl72j9:not(#\#):not(#\#):not(#\#){flex-basis:auto}
313
315
  .xdt5ytf:not(#\#):not(#\#):not(#\#){flex-direction:column}
314
316
  .x1iyjqo2:not(#\#):not(#\#):not(#\#){flex-grow:1}
@@ -375,6 +377,7 @@
375
377
  .x1lqcxt8:not(#\#):not(#\#):not(#\#){justify-self:start}
376
378
  .x9pfba7:not(#\#):not(#\#):not(#\#){letter-spacing:.08em}
377
379
  .xa7kkou:not(#\#):not(#\#):not(#\#){line-height:1.7}
380
+ .xo5v014:not(#\#):not(#\#):not(#\#){line-height:1}
378
381
  .xklk4pu:not(#\#):not(#\#):not(#\#){line-height:30px}
379
382
  .xw6l6zx:not(#\#):not(#\#):not(#\#){line-height:var(--text-body-leading)}
380
383
  .x17iicif:not(#\#):not(#\#):not(#\#){line-height:var(--text-code-leading)}
@@ -384,6 +387,7 @@
384
387
  .x3ct3a4:not(#\#):not(#\#):not(#\#){list-style-type:none}
385
388
  .x1h1dc9q:not(#\#):not(#\#):not(#\#){margin-inline-end:calc(var(--spacing-0-5) + var(--border-width))}
386
389
  .x1pezmd8:not(#\#):not(#\#):not(#\#){margin-inline-end:var(--spacing-2)}
390
+ .xvc5jky:not(#\#):not(#\#):not(#\#){margin-inline-start:auto}
387
391
  .x1m70ojl:not(#\#):not(#\#):not(#\#){margin-inline-start:calc(-1 * var(--spacing-1-5))}
388
392
  .xhi58n4:not(#\#):not(#\#):not(#\#){margin-inline-start:calc(var(--spacing-1) * -1)}
389
393
  .xladpz9:not(#\#):not(#\#):not(#\#){margin-inline-start:var(--spacing-0-5)}
@@ -426,6 +430,7 @@
426
430
  .x2b8uid:not(#\#):not(#\#):not(#\#){text-align:center}
427
431
  .xp4054r:not(#\#):not(#\#):not(#\#){text-align:end}
428
432
  .xdpxx8g:not(#\#):not(#\#):not(#\#){text-align:left}
433
+ .x1yc453h:not(#\#):not(#\#):not(#\#){text-align:start}
429
434
  .xlyipyv:not(#\#):not(#\#):not(#\#){text-overflow:ellipsis}
430
435
  .xtvhhri:not(#\#):not(#\#):not(#\#){text-transform:uppercase}
431
436
  .x5ve5x3:not(#\#):not(#\#):not(#\#){touch-action:none}
@@ -500,12 +505,14 @@
500
505
  .x11xkdxz:not(#\#):not(#\#):not(#\#):not(#\#){border-top-width:var(--border-width)}
501
506
  .x1ey2m1c:not(#\#):not(#\#):not(#\#):not(#\#){bottom:0}
502
507
  .x1hmuevu:not(#\#):not(#\#):not(#\#):not(#\#){bottom:var(--spacing-3)}
508
+ .x5yr21d:not(#\#):not(#\#):not(#\#):not(#\#){height:100%}
503
509
  .xtdtrs8:not(#\#):not(#\#):not(#\#):not(#\#){height:100dvh}
504
510
  .x170jfvy:not(#\#):not(#\#):not(#\#):not(#\#){height:10px}
505
511
  .x1v9usgg:not(#\#):not(#\#):not(#\#):not(#\#){height:14px}
506
512
  .xlup9mm:not(#\#):not(#\#):not(#\#):not(#\#){height:16px}
507
513
  .xjm9jq1:not(#\#):not(#\#):not(#\#):not(#\#){height:1px}
508
514
  .xxk0z11:not(#\#):not(#\#):not(#\#):not(#\#){height:24px}
515
+ .x1fgtraw:not(#\#):not(#\#):not(#\#):not(#\#){height:28px}
509
516
  .x1gnnpzl:not(#\#):not(#\#):not(#\#):not(#\#){height:30px}
510
517
  .x10w6t97:not(#\#):not(#\#):not(#\#):not(#\#){height:32px}
511
518
  .x1ycjhwn:not(#\#):not(#\#):not(#\#):not(#\#){height:5px}
@@ -516,6 +523,7 @@
516
523
  .x1unci72:not(#\#):not(#\#):not(#\#):not(#\#){left:var(--spacing-4)}
517
524
  .xlstkdb:not(#\#):not(#\#):not(#\#):not(#\#){margin-bottom:var(--spacing-0-5)}
518
525
  .x1p37lm5:not(#\#):not(#\#):not(#\#):not(#\#){margin-bottom:var(--spacing-2)}
526
+ .xr1yuqi:not(#\#):not(#\#):not(#\#):not(#\#){margin-top:auto}
519
527
  .x160xvyo:not(#\#):not(#\#):not(#\#):not(#\#){margin-top:calc(var(--spacing-1) * -1)}
520
528
  .xcsaf9d:not(#\#):not(#\#):not(#\#):not(#\#){margin-top:var(--spacing-1)}
521
529
  .xtbrsbv:not(#\#):not(#\#):not(#\#):not(#\#){margin-top:var(--spacing-2)}
@@ -533,6 +541,7 @@
533
541
  .xisnujt:not(#\#):not(#\#):not(#\#):not(#\#){min-height:20px}
534
542
  .xjwf9q1:not(#\#):not(#\#):not(#\#):not(#\#){min-height:24px}
535
543
  .x1cy360x:not(#\#):not(#\#):not(#\#):not(#\#){min-height:3em}
544
+ .x1gg8mnh:not(#\#):not(#\#):not(#\#):not(#\#){min-height:44px}
536
545
  .x1uogy3y:not(#\#):not(#\#):not(#\#):not(#\#){min-height:var(--size-element-md)}
537
546
  .x1gdqauq:not(#\#):not(#\#):not(#\#):not(#\#){min-height:var(--size-element-sm)}
538
547
  .xu95rz8:not(#\#):not(#\#):not(#\#):not(#\#){min-height:var(--transfer-list-panel-min-block-size)}
@@ -546,9 +555,11 @@
546
555
  .x1odjw0f:not(#\#):not(#\#):not(#\#):not(#\#){overflow-y:auto}
547
556
  .x1a148e8:not(#\#):not(#\#):not(#\#):not(#\#){padding-bottom:env(safe-area-inset-bottom,0px)}
548
557
  .xy143xn:not(#\#):not(#\#):not(#\#):not(#\#){padding-bottom:var(--spacing-1)}
558
+ .xvmdzux:not(#\#):not(#\#):not(#\#):not(#\#){padding-bottom:var(--spacing-3)}
549
559
  .xpgmcrp:not(#\#):not(#\#):not(#\#):not(#\#){padding-top:var(--icon-block-offset)}
550
560
  .xbx876j:not(#\#):not(#\#):not(#\#):not(#\#){padding-top:var(--spacing-0-5)}
551
561
  .x1xye8es:not(#\#):not(#\#):not(#\#):not(#\#){padding-top:var(--spacing-2)}
562
+ .x1vlblms:not(#\#):not(#\#):not(#\#):not(#\#){padding-top:var(--spacing-3)}
552
563
  .x13vifvy:not(#\#):not(#\#):not(#\#):not(#\#){top:0}
553
564
  .xctzyg:not(#\#):not(#\#):not(#\#):not(#\#){top:var(--spacing-2)}
554
565
  .xyx6v2t:not(#\#):not(#\#):not(#\#):not(#\#){top:var(--spacing-3)}
@@ -560,6 +571,7 @@
560
571
  .x1i1rx1s:not(#\#):not(#\#):not(#\#):not(#\#){width:1px}
561
572
  .xvy4d1p:not(#\#):not(#\#):not(#\#):not(#\#){width:24px}
562
573
  .x1dz1jew:not(#\#):not(#\#):not(#\#):not(#\#){width:280px}
574
+ .xgd8bvy:not(#\#):not(#\#):not(#\#):not(#\#){width:28px}
563
575
  .x1849jeq:not(#\#):not(#\#):not(#\#):not(#\#){width:30px}
564
576
  .x1td3qas:not(#\#):not(#\#):not(#\#):not(#\#){width:32px}
565
577
  .x1ftt334:not(#\#):not(#\#):not(#\#):not(#\#){width:5px}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/lab",
3
- "version": "0.6.2-canary.fc74d7e",
3
+ "version": "0.6.3-canary.039a67e",
4
4
  "description": "Experimental Astryx components — published to npm only under the @canary dist-tag for early testing; never released as a stable (latest) version.",
5
5
  "author": "Meta Open Source",
6
6
  "license": "MIT",
@@ -46,7 +46,7 @@
46
46
  },
47
47
  "peerDependencies": {
48
48
  "@stylexjs/stylex": ">=0.10.0",
49
- "@astryxdesign/core": "0.6.2-canary.fc74d7e",
49
+ "@astryxdesign/core": "0.6.3-canary.039a67e",
50
50
  "react": ">=19.0.0",
51
51
  "react-dom": ">=19.0.0"
52
52
  },
@@ -56,8 +56,8 @@
56
56
  "d3-shape": "^3.2.0"
57
57
  },
58
58
  "devDependencies": {
59
- "@astryxdesign/a11y-spec": "0.6.2-canary.fc74d7e",
60
- "@astryxdesign/cli": "0.6.2-canary.fc74d7e",
59
+ "@astryxdesign/a11y-spec": "0.6.3-canary.039a67e",
60
+ "@astryxdesign/cli": "0.6.3-canary.039a67e",
61
61
  "@babel/cli": "^7.29.7",
62
62
  "@babel/core": "^7.29.7",
63
63
  "@babel/preset-react": "^7.29.7",
@@ -0,0 +1,32 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ /** @type {import('@astryxdesign/cli/authoring').ComponentDoc} */
3
+ export const docs = {
4
+ name: 'MobileTokenizer',
5
+ displayName: 'Mobile Tokenizer',
6
+ group: 'MobileTokenizer',
7
+ category: 'Form Controls',
8
+ keywords: ['tokenizer', 'mobile', 'touch', 'bottom sheet', 'tags', 'chips'],
9
+ props: [
10
+ {name: 'label', type: 'string', description: 'Field label and sheet heading.', required: true},
11
+ {name: 'searchSource', type: 'SearchSource<T>', description: 'Core Typeahead search source (search + bootstrap).', required: true},
12
+ {name: 'value', type: 'T[]', description: 'Selected items (Core SearchableItem).', required: true},
13
+ {name: 'onChange', type: '(items: T[], change: MobileTokenizerChange<T>) => void', description: 'Mirrors Core Tokenizer: single-item add/create/remove, or reorder.', required: true},
14
+ {name: 'placeholder', type: 'string', description: 'Trigger text when nothing is selected.'},
15
+ {name: 'hasCreate', type: 'boolean', description: 'Offer Create "<query>" for unmatched free text.', default: 'false'},
16
+ {name: 'maxEntries', type: 'number', description: 'Cap selections; add rows disable at the cap.'},
17
+ {name: 'renderItem', type: '(item: T) => ReactNode', description: 'Custom add-sheet row content.'},
18
+ ],
19
+ usage: {
20
+ description:
21
+ 'Lab prototype for trying the touch Tokenizer flow: tap the field to open the manage sheet (review, remove, Clear all, Add item), tap Add item for the stacked search sheet (results above, filter + Done in the bottom row above the keyboard, tap +/check to toggle immediately).',
22
+ bestPractices: [
23
+ {guidance: true, description: 'Try this in Lab/canary to validate the flow; graduate via Core Tokenizer presentation="adaptive" when it ships.'},
24
+ {guidance: false, description: 'Do not ship stable product on this Lab API; it has no theming, i18n, or spec contract yet.'},
25
+ ],
26
+ anatomy: [
27
+ {name: 'Trigger field', required: true, description: 'Button showing tokens as a summary, with chevron.'},
28
+ {name: 'Manage sheet', required: true, description: 'Selected rows with remove, Clear all footer, Add item.'},
29
+ {name: 'Add sheet', required: true, description: 'Stacked sheet: result rows with +/check toggles, bottom filter + Done row.'},
30
+ ],
31
+ },
32
+ };
@@ -0,0 +1,72 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ /**
3
+ * @file MobileTokenizer.test.tsx
4
+ * @input Uses vitest, @testing-library/react, MobileTokenizer
5
+ * @output Lab tests for the sketch flow: field -> manage -> add (bottom
6
+ * filter) -> Done -> manage
7
+ * @position Lab tests; validates MobileTokenizer.tsx
8
+ */
9
+ import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest';
10
+ import {render, screen, fireEvent, waitFor} from '@testing-library/react';
11
+ import {useState} from 'react';
12
+ import {MobileTokenizer, type MobileTokenizerChange} from './MobileTokenizer';
13
+ import type {SearchSource, SearchableItem} from '@astryxdesign/core/Typeahead';
14
+
15
+ const ITEMS: SearchableItem[] = [
16
+ {id: 'design', label: 'Design'},
17
+ {id: 'eng', label: 'Eng'},
18
+ {id: 'engineer', label: 'Engineer'},
19
+ {id: 'energizer', label: 'Energizer'},
20
+ ];
21
+ const source: SearchSource<SearchableItem> = {
22
+ search: q => ITEMS.filter(i => i.label.toLowerCase().includes(q.toLowerCase())),
23
+ bootstrap: () => ITEMS,
24
+ };
25
+
26
+ function Harness({spy}: {spy?: (i: SearchableItem[], c: MobileTokenizerChange<SearchableItem>) => void}) {
27
+ const [value, setValue] = useState<SearchableItem[]>([ITEMS[0], ITEMS[1]]);
28
+ return (
29
+ <MobileTokenizer label="Tags" searchSource={source} value={value} debounceMs={0} placeholder="Add tags"
30
+ onChange={(items, change) => { setValue(items); spy?.(items, change); }} />
31
+ );
32
+ }
33
+
34
+ beforeEach(() => {
35
+ HTMLDialogElement.prototype.showModal = vi.fn(function (this: HTMLDialogElement) { this.setAttribute('open', ''); });
36
+ HTMLDialogElement.prototype.show = vi.fn(function (this: HTMLDialogElement) { this.setAttribute('open', ''); });
37
+ HTMLDialogElement.prototype.close = vi.fn(function (this: HTMLDialogElement) { this.removeAttribute('open'); });
38
+ if (!Element.prototype.setPointerCapture) { Element.prototype.setPointerCapture = vi.fn(); Element.prototype.releasePointerCapture = vi.fn(); }
39
+ vi.stubGlobal('matchMedia', vi.fn().mockReturnValue({matches: false, media: '', onchange: null, addEventListener: vi.fn(), removeEventListener: vi.fn(), addListener: vi.fn(), removeListener: vi.fn(), dispatchEvent: vi.fn()}));
40
+ vi.stubGlobal('requestAnimationFrame', vi.fn((cb: FrameRequestCallback) => { cb(0); return 1; }));
41
+ });
42
+ afterEach(() => vi.unstubAllGlobals());
43
+
44
+ describe('MobileTokenizer (Lab, sketch flow)', () => {
45
+ it('field -> manage -> add: filter at bottom, tap + adds, Done returns', async () => {
46
+ const spy = vi.fn();
47
+ render(<Harness spy={spy} />);
48
+ const trigger = screen.getByRole('button', {name: /Tags/});
49
+ expect(trigger.getAttribute('aria-haspopup')).toBe('dialog');
50
+ fireEvent.click(trigger);
51
+ expect((await screen.findByTestId('mobile-tokenizer-manage-list')).textContent).toContain('Design');
52
+ fireEvent.click(screen.getByRole('button', {name: 'Add item'}));
53
+ const search = await screen.findByLabelText('Search Tags');
54
+ fireEvent.change(search, {target: {value: 'E'}});
55
+ const engineer = await screen.findByRole('option', {name: /Engineer/});
56
+ fireEvent.click(engineer);
57
+ expect(spy).toHaveBeenCalledWith([ITEMS[0], ITEMS[1], ITEMS[2]], expect.objectContaining({type: 'add'}));
58
+ await waitFor(() => expect(screen.getByRole('option', {name: /Engineer/}).getAttribute('aria-selected')).toBe('true'));
59
+ fireEvent.click(screen.getByRole('button', {name: 'Done'}));
60
+ expect((await screen.findByTestId('mobile-tokenizer-manage-list')).textContent).toContain('Engineer');
61
+ });
62
+ it('manage sheet removes and clears', async () => {
63
+ const spy = vi.fn();
64
+ render(<Harness spy={spy} />);
65
+ fireEvent.click(screen.getByRole('button', {name: /Tags/}));
66
+ await screen.findByTestId('mobile-tokenizer-manage-list');
67
+ fireEvent.click(screen.getByRole('button', {name: 'Remove Eng'}));
68
+ expect(spy).toHaveBeenCalledWith([ITEMS[0]], expect.objectContaining({type: 'remove'}));
69
+ fireEvent.click(screen.getByRole('button', {name: 'Clear all'}));
70
+ expect(spy).toHaveBeenLastCalledWith([], expect.objectContaining({type: 'remove'}));
71
+ });
72
+ });
@@ -0,0 +1,474 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file MobileTokenizer.tsx
7
+ * @input Uses public @astryxdesign/core components only (BottomSheet,
8
+ * BottomSheetSwitcher, Button, Field, Heading, Icon, IconButton, Text,
9
+ * TextInput, Token)
10
+ * @output Exports MobileTokenizer — Lab prototype of the touch Tokenizer
11
+ * flow (stacked manage + add sheets)
12
+ * @position Lab (canary) stack layer 1: validates the design before the
13
+ * Core promotion (Tokenizer presentation="bottom-sheet") stacked after it.
14
+ *
15
+ * API mirrors Core Tokenizer (label/searchSource/value/onChange/change,
16
+ * hasCreate, maxEntries) so graduation is an import swap. Differences from
17
+ * Core are the Lab deltas: the filter is a bordered TextInput (Core's
18
+ * PanelSearchInput is not exported), strings are literals (no i18n keys),
19
+ * and there is no theming/spec contract yet.
20
+ */
21
+
22
+ import {
23
+ useEffect,
24
+ useId,
25
+ useMemo,
26
+ useRef,
27
+ useState,
28
+ type ReactNode,
29
+ } from 'react';
30
+ import * as stylex from '@stylexjs/stylex';
31
+ import type {BaseProps} from '@astryxdesign/core';
32
+ import {BottomSheet} from '@astryxdesign/core/BottomSheet';
33
+ import {BottomSheetSwitcher} from '@astryxdesign/core/BottomSheet';
34
+ import {Button} from '@astryxdesign/core/Button';
35
+ import {Divider} from '@astryxdesign/core/Divider';
36
+ import {Field} from '@astryxdesign/core/Field';
37
+ import {Icon} from '@astryxdesign/core/Icon';
38
+ import {IconButton} from '@astryxdesign/core/IconButton';
39
+ import {Text} from '@astryxdesign/core/Text';
40
+ import {TextInput} from '@astryxdesign/core/TextInput';
41
+ import {Token} from '@astryxdesign/core/Token';
42
+ import type {
43
+ SearchableItem,
44
+ SearchSource,
45
+ } from '@astryxdesign/core/Typeahead';
46
+ import {
47
+ colorVars,
48
+ radiusVars,
49
+ spacingVars,
50
+ } from '@astryxdesign/core/theme/tokens.stylex';
51
+
52
+ export type MobileTokenizerChange<T extends SearchableItem> =
53
+ | {item: T; type: 'add'}
54
+ | {item: T; type: 'create'}
55
+ | {item: T; type: 'remove'}
56
+ | {type: 'reorder'};
57
+
58
+ export interface MobileTokenizerProps<T extends SearchableItem> extends Omit<
59
+ BaseProps<HTMLDivElement>,
60
+ 'onChange'
61
+ > {
62
+ label: string;
63
+ searchSource: SearchSource<T>;
64
+ value: T[];
65
+ onChange: (items: T[], change: MobileTokenizerChange<T>) => void;
66
+ renderItem?: (item: T) => ReactNode;
67
+ placeholder?: string;
68
+ description?: string;
69
+ isOptional?: boolean;
70
+ isDisabled?: boolean;
71
+ hasCreate?: boolean;
72
+ maxEntries?: number;
73
+ maxMenuItems?: number;
74
+ minQueryLength?: number;
75
+ debounceMs?: number;
76
+ emptySearchResultsText?: string;
77
+ onChangeQuery?: (query: string) => void;
78
+ }
79
+
80
+ type SheetId = 'manage' | 'add';
81
+ const CREATABLE_ID_PREFIX = '__xds_create__';
82
+
83
+ const styles = stylex.create({
84
+ trigger: {
85
+ display: 'flex',
86
+ flexWrap: 'wrap',
87
+ alignItems: 'center',
88
+ gap: spacingVars['--spacing-1'],
89
+ width: '100%',
90
+ minHeight: 44,
91
+ paddingBlock: spacingVars['--spacing-1'],
92
+ paddingInline: spacingVars['--spacing-2'],
93
+ borderWidth: 1,
94
+ borderStyle: 'solid',
95
+ borderColor: colorVars['--color-border'],
96
+ borderRadius: radiusVars['--radius-element'],
97
+ backgroundColor: 'transparent',
98
+ textAlign: 'start',
99
+ cursor: {
100
+ default: 'pointer',
101
+ ':is(:disabled,[aria-disabled="true"])': 'default',
102
+ },
103
+ },
104
+ triggerPlaceholder: {
105
+ color: colorVars['--color-text-secondary'],
106
+ },
107
+ chevron: {
108
+ marginInlineStart: 'auto',
109
+ display: 'flex',
110
+ flexShrink: 0,
111
+ },
112
+ sheetBody: {
113
+ display: 'flex',
114
+ flexDirection: 'column',
115
+ height: '100%',
116
+ minHeight: 0,
117
+ paddingInline: spacingVars['--spacing-3'],
118
+ paddingBlockStart: spacingVars['--spacing-3'],
119
+ paddingBlockEnd: spacingVars['--spacing-3'],
120
+ },
121
+ // Sketch has no visible sheet headings — manage is rows + a top-end
122
+ // close affordance only. The sheet's accessible name still comes from
123
+ // BottomSheet's `label`, not a rendered heading.
124
+ closeRow: {
125
+ display: 'flex',
126
+ justifyContent: 'flex-end',
127
+ marginBlockEnd: spacingVars['--spacing-2'],
128
+ },
129
+ list: {
130
+ display: 'flex',
131
+ flexDirection: 'column',
132
+ overflowY: 'auto',
133
+ minHeight: 0,
134
+ },
135
+ row: {
136
+ display: 'flex',
137
+ alignItems: 'center',
138
+ gap: spacingVars['--spacing-2'],
139
+ width: '100%',
140
+ minHeight: 44,
141
+ paddingBlock: spacingVars['--spacing-2'],
142
+ paddingInline: spacingVars['--spacing-1'],
143
+ textAlign: 'start',
144
+ backgroundColor: 'transparent',
145
+ borderWidth: 0,
146
+ color: colorVars['--color-text-primary'],
147
+ cursor: {
148
+ default: 'pointer',
149
+ ':is(:disabled,[aria-disabled="true"])': 'default',
150
+ },
151
+ },
152
+ rowDisabled: {opacity: 0.5},
153
+ rowLabel: {flexGrow: 1, minWidth: 0},
154
+ toggleMark: {
155
+ display: 'flex',
156
+ alignItems: 'center',
157
+ justifyContent: 'center',
158
+ width: 28,
159
+ height: 28,
160
+ flexShrink: 0,
161
+ borderRadius: radiusVars['--radius-full'],
162
+ borderWidth: 1,
163
+ borderStyle: 'solid',
164
+ borderColor: colorVars['--color-border'],
165
+ fontSize: 18,
166
+ lineHeight: 1,
167
+ },
168
+ footer: {
169
+ display: 'flex',
170
+ alignItems: 'center',
171
+ justifyContent: 'space-between',
172
+ gap: spacingVars['--spacing-2'],
173
+ paddingBlockStart: spacingVars['--spacing-3'],
174
+ marginTop: 'auto',
175
+ },
176
+ filterRow: {
177
+ display: 'flex',
178
+ alignItems: 'flex-end',
179
+ gap: spacingVars['--spacing-2'],
180
+ paddingBlockStart: spacingVars['--spacing-2'],
181
+ marginTop: 'auto',
182
+ },
183
+ // TextInput renders Field (block, shrink-to-fit); xstyle lands on that
184
+ // root, so it must flex — the filter takes all row space minus Done+gap.
185
+ filterInput: {flexGrow: 1, flexShrink: 1, flexBasis: 0, minWidth: 0},
186
+ empty: {paddingBlock: spacingVars['--spacing-4']},
187
+ });
188
+
189
+ export function MobileTokenizer<T extends SearchableItem>({
190
+ label,
191
+ searchSource,
192
+ value,
193
+ onChange,
194
+ renderItem,
195
+ placeholder,
196
+ description,
197
+ isOptional,
198
+ isDisabled = false,
199
+ hasCreate = false,
200
+ maxEntries,
201
+ maxMenuItems = 10,
202
+ minQueryLength = 1,
203
+ debounceMs = 150,
204
+ emptySearchResultsText = 'No results found',
205
+ onChangeQuery,
206
+ xstyle,
207
+ }: MobileTokenizerProps<T>) {
208
+ const triggerId = useId();
209
+ const [activeSheet, setActiveSheet] = useState<SheetId | null>(null);
210
+ const selectedIds = useMemo(() => new Set(value.map(v => v.id)), [value]);
211
+ const isAtMax = maxEntries != null && value.length >= maxEntries;
212
+
213
+ const handleAdd = (item: T) => {
214
+ if (isAtMax || selectedIds.has(item.id)) {
215
+ return;
216
+ }
217
+ if (
218
+ hasCreate &&
219
+ typeof item.id === 'string' &&
220
+ item.id.startsWith(CREATABLE_ID_PREFIX)
221
+ ) {
222
+ const created = item.id.slice(CREATABLE_ID_PREFIX.length);
223
+ if (selectedIds.has(created)) {
224
+ return;
225
+ }
226
+ const real = {id: created, label: created} as T;
227
+ onChange([...value, real], {item: real, type: 'create'});
228
+ return;
229
+ }
230
+ onChange([...value, item], {item, type: 'add'});
231
+ };
232
+ const handleRemove = (item: T) =>
233
+ onChange(
234
+ value.filter(v => v.id !== item.id),
235
+ {item, type: 'remove'},
236
+ );
237
+ const handleClearAll = () => {
238
+ if (value.length > 0) {
239
+ onChange([], {item: value[value.length - 1], type: 'remove'});
240
+ }
241
+ };
242
+
243
+ // ---- add-sheet search ----
244
+ const [query, setQuery] = useState('');
245
+ const [results, setResults] = useState<T[]>([]);
246
+ const [isSearching, setIsSearching] = useState(false);
247
+ const seqRef = useRef(0);
248
+ const isAddOpen = activeSheet === 'add';
249
+ useEffect(() => {
250
+ if (!isAddOpen) {
251
+ return;
252
+ }
253
+ const seq = ++seqRef.current;
254
+ const run = async () => {
255
+ setIsSearching(true);
256
+ try {
257
+ const trimmed = query.trim();
258
+ let items: T[];
259
+ if (trimmed === '') {
260
+ items = searchSource.bootstrap
261
+ ? await searchSource.bootstrap()
262
+ : await searchSource.search('');
263
+ } else if (trimmed.length < minQueryLength) {
264
+ items = [];
265
+ } else {
266
+ items = await searchSource.search(query);
267
+ }
268
+ if (seqRef.current === seq) {
269
+ setResults(items.slice(0, maxMenuItems));
270
+ }
271
+ } finally {
272
+ if (seqRef.current === seq) {
273
+ setIsSearching(false);
274
+ }
275
+ }
276
+ };
277
+ if (debounceMs > 0) {
278
+ const timer = setTimeout(() => {
279
+ void run();
280
+ }, debounceMs);
281
+ return () => clearTimeout(timer);
282
+ }
283
+ void run();
284
+ }, [isAddOpen, query, searchSource, minQueryLength, maxMenuItems, debounceMs]);
285
+
286
+ const createItem = useMemo<T | null>(() => {
287
+ const trimmed = query.trim();
288
+ if (!hasCreate || trimmed === '' || isAtMax) {
289
+ return null;
290
+ }
291
+ if (
292
+ selectedIds.has(trimmed) ||
293
+ results.some(r => r.label.toLowerCase() === trimmed.toLowerCase())
294
+ ) {
295
+ return null;
296
+ }
297
+ return {
298
+ id: `${CREATABLE_ID_PREFIX}${trimmed}`,
299
+ label: `Create "${trimmed}"`,
300
+ } as unknown as T;
301
+ }, [hasCreate, query, results, selectedIds, isAtMax]);
302
+
303
+ return (
304
+ <Field
305
+ label={label}
306
+ description={description}
307
+ isOptional={isOptional}
308
+ isDisabled={isDisabled}
309
+ inputID={triggerId}
310
+ xstyle={xstyle}>
311
+ <button
312
+ type="button"
313
+ id={triggerId}
314
+ aria-haspopup="dialog"
315
+ aria-expanded={activeSheet != null}
316
+ aria-label={label}
317
+ disabled={isDisabled}
318
+ onClick={() => setActiveSheet('manage')}
319
+ {...stylex.props(styles.trigger)}>
320
+ {value.length > 0 ? (
321
+ value.map(item => (
322
+ <Token key={item.id} label={item.label} isDisabled={isDisabled} />
323
+ ))
324
+ ) : (
325
+ <span {...stylex.props(styles.triggerPlaceholder)}>
326
+ {placeholder ?? ''}
327
+ </span>
328
+ )}
329
+ <span {...stylex.props(styles.chevron)} aria-hidden="true">
330
+ <Icon icon="chevronDown" size="sm" color="secondary" />
331
+ </span>
332
+ </button>
333
+
334
+ {!isDisabled && (
335
+ <BottomSheetSwitcher
336
+ activeSheet={activeSheet}
337
+ onActiveSheetChange={id => setActiveSheet(id as SheetId | null)}>
338
+ <BottomSheet sheetId="manage" label={label} height="hug">
339
+ <div {...stylex.props(styles.sheetBody)}>
340
+ <div {...stylex.props(styles.closeRow)}>
341
+ <IconButton
342
+ label="Close"
343
+ icon={<Icon icon="close" />}
344
+ variant="ghost"
345
+ size="sm"
346
+ onClick={() => setActiveSheet(null)}
347
+ />
348
+ </div>
349
+ <div {...stylex.props(styles.list)} data-testid="mobile-tokenizer-manage-list">
350
+ {value.length === 0 ? (
351
+ <Text type="supporting" color="secondary" xstyle={styles.empty}>
352
+ No items yet
353
+ </Text>
354
+ ) : (
355
+ value.map((item, i) => (
356
+ <div key={item.id}>
357
+ {i > 0 && <Divider />}
358
+ <div {...stylex.props(styles.row)}>
359
+ <span {...stylex.props(styles.rowLabel)}>
360
+ <Text type="body">{item.label}</Text>
361
+ </span>
362
+ <IconButton
363
+ label={`Remove ${item.label}`}
364
+ icon={<Icon icon="close" />}
365
+ variant="ghost"
366
+ size="sm"
367
+ onClick={() => handleRemove(item)}
368
+ />
369
+ </div>
370
+ </div>
371
+ ))
372
+ )}
373
+ </div>
374
+ <div {...stylex.props(styles.footer)}>
375
+ <Button
376
+ label="Clear all"
377
+ variant="ghost"
378
+ isDisabled={value.length === 0}
379
+ onClick={handleClearAll}
380
+ />
381
+ <Button
382
+ label="Add item"
383
+ variant="primary"
384
+ isDisabled={isAtMax}
385
+ onClick={() => setActiveSheet('add')}
386
+ />
387
+ </div>
388
+ </div>
389
+ </BottomSheet>
390
+
391
+ <BottomSheet sheetId="add" label="Add item" height="tall">
392
+ <div {...stylex.props(styles.sheetBody)}>
393
+ <div
394
+ {...stylex.props(styles.list)}
395
+ role="listbox"
396
+ aria-multiselectable="true"
397
+ aria-label={label}
398
+ data-testid="mobile-tokenizer-add-list">
399
+ {createItem != null && (
400
+ <button
401
+ type="button"
402
+ role="option"
403
+ aria-selected={false}
404
+ {...stylex.props(styles.row)}
405
+ onClick={() => {
406
+ handleAdd(createItem);
407
+ setQuery('');
408
+ }}>
409
+ <span {...stylex.props(styles.rowLabel)}>
410
+ <Text type="body">{createItem.label}</Text>
411
+ </span>
412
+ <span {...stylex.props(styles.toggleMark)} aria-hidden="true">+</span>
413
+ </button>
414
+ )}
415
+ {!isSearching && results.length === 0 && createItem == null ? (
416
+ <Text type="supporting" color="secondary" xstyle={styles.empty}>
417
+ {emptySearchResultsText}
418
+ </Text>
419
+ ) : (
420
+ results.map(item => {
421
+ const isSelected = selectedIds.has(item.id);
422
+ const rowDisabled = !isSelected && isAtMax;
423
+ return (
424
+ <button
425
+ key={item.id}
426
+ type="button"
427
+ role="option"
428
+ aria-selected={isSelected}
429
+ aria-disabled={rowDisabled || undefined}
430
+ {...stylex.props(styles.row, rowDisabled && styles.rowDisabled)}
431
+ onClick={() =>
432
+ isSelected ? handleRemove(item) : handleAdd(item)
433
+ }>
434
+ <span {...stylex.props(styles.rowLabel)}>
435
+ {renderItem ? renderItem(item) : <Text type="body">{item.label}</Text>}
436
+ </span>
437
+ <span {...stylex.props(styles.toggleMark)} aria-hidden="true">
438
+ {isSelected ? <Icon icon="check" size="sm" /> : '+'}
439
+ </span>
440
+ </button>
441
+ );
442
+ })
443
+ )}
444
+ </div>
445
+ <Divider />
446
+ <div {...stylex.props(styles.filterRow)}>
447
+ <TextInput
448
+ label={`Search ${label}`}
449
+ isLabelHidden
450
+ placeholder="Search..."
451
+ value={query}
452
+ width="100%"
453
+ hasClear
454
+ onChange={next => {
455
+ setQuery(next);
456
+ onChangeQuery?.(next);
457
+ }}
458
+ xstyle={styles.filterInput}
459
+ />
460
+ <Button
461
+ label="Done"
462
+ variant="primary"
463
+ onClick={() => setActiveSheet('manage')}
464
+ />
465
+ </div>
466
+ </div>
467
+ </BottomSheet>
468
+ </BottomSheetSwitcher>
469
+ )}
470
+ </Field>
471
+ );
472
+ }
473
+
474
+ MobileTokenizer.displayName = 'MobileTokenizer';
@@ -0,0 +1,16 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file index.ts
7
+ * @input MobileTokenizer component
8
+ * @output Public MobileTokenizer entry point for @astryxdesign/lab
9
+ * @position Component barrel; re-exported by /packages/lab/src/index.ts
10
+ */
11
+
12
+ export {MobileTokenizer} from './MobileTokenizer';
13
+ export type {
14
+ MobileTokenizerProps,
15
+ MobileTokenizerChange,
16
+ } from './MobileTokenizer';
package/src/index.ts CHANGED
@@ -253,6 +253,15 @@ export {
253
253
  type LogStreamLevel,
254
254
  } from './LogStream';
255
255
 
256
+ // MobileTokenizer — Lab prototype (stack layer 1) of the touch Tokenizer
257
+ // flow: summary trigger + stacked manage/add BottomSheets. Graduates to
258
+ // Core Tokenizer presentation="bottom-sheet" (stack layer 2).
259
+ export {
260
+ MobileTokenizer,
261
+ type MobileTokenizerProps,
262
+ type MobileTokenizerChange,
263
+ } from './MobileTokenizer';
264
+
256
265
  // RichTextEditor (RFC facebook/astryx#3899) has graduated out of @astryxdesign/lab
257
266
  // into its own canary-only package, @astryxdesign/richtext, so it can be canaried
258
267
  // independently (e.g. into EPS/Nest). Import it from there: