@astryxdesign/core 0.7.7-canary.fc68123 → 0.7.7
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 +104 -0
- package/dist/AlertDialog/AlertDialog.d.ts +1 -4
- package/dist/AlertDialog/AlertDialog.d.ts.map +1 -1
- package/dist/AlertDialog/AlertDialog.js +6 -36
- package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetPanel.js +4 -23
- package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -1
- package/dist/BottomSheet/useSheetGestures.js +1 -3
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +1 -17
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +2 -13
- package/dist/ComplexSelector/ComplexSelector.d.ts +1 -4
- package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
- package/dist/ComplexSelector/ComplexSelector.js +4 -14
- package/dist/ContextMenu/ContextMenu.js +2 -2
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +31 -7
- package/dist/Field/Field.js +2 -2
- package/dist/HoverCard/HoverCard.d.ts +3 -4
- package/dist/HoverCard/HoverCard.d.ts.map +1 -1
- package/dist/HoverCard/HoverCard.js +2 -27
- package/dist/HoverCard/useHoverCard.d.ts +4 -5
- package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
- package/dist/HoverCard/useHoverCard.js +31 -17
- package/dist/Icon/Icon.d.ts +1 -5
- package/dist/Icon/Icon.d.ts.map +1 -1
- package/dist/Icon/Icon.js +2 -5
- package/dist/Layer/usePressPin.d.ts +5 -17
- package/dist/Layer/usePressPin.d.ts.map +1 -1
- package/dist/Layer/usePressPin.js +12 -62
- package/dist/Lightbox/Lightbox.d.ts.map +1 -1
- package/dist/Lightbox/Lightbox.js +0 -18
- package/dist/PowerSearch/PowerSearch.d.ts +3 -36
- package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearch.js +127 -433
- package/dist/PowerSearch/PowerSearchEditPopover.d.ts +1 -32
- package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearchEditPopover.js +6 -17
- package/dist/PowerSearch/PowerSearchValueEditor.d.ts +3 -38
- package/dist/PowerSearch/PowerSearchValueEditor.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearchValueEditor.js +61 -170
- package/dist/PowerSearch/formatFilterValue.d.ts +0 -7
- package/dist/PowerSearch/formatFilterValue.d.ts.map +1 -1
- package/dist/PowerSearch/formatFilterValue.js +1 -11
- package/dist/RadioList/RadioList.d.ts.map +1 -1
- package/dist/RadioList/RadioList.js +2 -3
- package/dist/RadioList/RadioListItem.d.ts +1 -1
- package/dist/RadioList/RadioListItem.d.ts.map +1 -1
- package/dist/RadioList/RadioListItem.js +3 -5
- package/dist/Resizable/ResizeHandle.d.ts +1 -2
- package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
- package/dist/Resizable/ResizeHandle.js +77 -99
- package/dist/SegmentedControl/useSegmentedSelectionMotion.d.ts.map +1 -1
- package/dist/SegmentedControl/useSegmentedSelectionMotion.js +3 -3
- package/dist/Slider/sliderTouchController.js +2 -2
- package/dist/Spinner/Spinner.d.ts +1 -1
- package/dist/Spinner/Spinner.d.ts.map +1 -1
- package/dist/Spinner/Spinner.js +58 -60
- package/dist/Switch/useSwitchTouch.js +4 -4
- package/dist/Table/BaseTable.d.ts.map +1 -1
- package/dist/Table/BaseTable.js +8 -29
- package/dist/TextInput/TextInput.d.ts +2 -2
- package/dist/TextInput/TextInput.d.ts.map +1 -1
- package/dist/TextInput/TextInput.js +7 -10
- package/dist/Toast/Toast.d.ts +1 -11
- package/dist/Toast/Toast.d.ts.map +1 -1
- package/dist/Toast/Toast.js +15 -25
- package/dist/Toast/ToastViewport.d.ts.map +1 -1
- package/dist/Toast/ToastViewport.js +34 -94
- package/dist/Toast/types.d.ts +3 -9
- package/dist/Toast/types.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.d.ts +1 -19
- package/dist/Tooltip/useTooltip.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.js +53 -34
- package/dist/Typeahead/TypeaheadItem.d.ts +1 -2
- package/dist/Typeahead/TypeaheadItem.d.ts.map +1 -1
- package/dist/astryx.css +6 -35
- package/dist/gestures/PressTracker.d.ts.map +1 -1
- package/dist/gestures/PressTracker.js +3 -4
- package/dist/hooks/actionGroupTracker.d.ts +1 -4
- package/dist/hooks/actionGroupTracker.d.ts.map +1 -1
- package/dist/hooks/actionGroupTracker.js +3 -13
- package/dist/hooks/index.d.ts +0 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +0 -1
- package/dist/hooks/useAttachment.d.ts +1 -1
- package/dist/hooks/useAttachment.js +1 -1
- package/dist/hooks/useInputStatusIcon.d.ts +1 -1
- package/dist/hooks/useInputStatusIcon.d.ts.map +1 -1
- package/dist/hooks/useInputStatusIcon.js +2 -2
- package/dist/hooks/useLongPress.d.ts +2 -6
- package/dist/hooks/useLongPress.d.ts.map +1 -1
- package/dist/hooks/useLongPress.js +15 -95
- package/dist/i18n/generated-english/PowerSearch/PowerSearchValueEditor.generated.d.ts.map +1 -1
- package/dist/i18n/generated-english/PowerSearch/PowerSearchValueEditor.generated.js +0 -6
- package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/en.generated.js +0 -30
- package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/pseudo.generated.js +0 -30
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -4
- package/dist/motion/animatedLayout.d.ts.map +1 -1
- package/dist/motion/animatedLayout.js +6 -5
- package/dist/motion/animator.d.ts +14 -14
- package/dist/motion/animator.d.ts.map +1 -1
- package/dist/motion/animator.js +18 -41
- package/dist/utils/dateParser.d.ts.map +1 -1
- package/dist/utils/dateParser.js +0 -12
- package/dist/utils/focusReturn.d.ts +0 -15
- package/dist/utils/focusReturn.d.ts.map +1 -1
- package/dist/utils/focusReturn.js +0 -28
- package/dist/utils/interactionOverlay.stylex.d.ts +2 -3
- package/dist/utils/interactionOverlay.stylex.d.ts.map +1 -1
- package/dist/utils/interactionOverlay.stylex.js +2 -3
- package/dist/utils/pressFeedback.d.ts.map +1 -1
- package/dist/utils/pressFeedback.js +6 -19
- package/locales/en.json +0 -120
- package/locales/pseudo.json +0 -90
- package/package.json +5 -10
- package/src/AlertDialog/AlertDialog.doc.mjs +2 -2
- package/src/AlertDialog/AlertDialog.tsx +5 -37
- package/src/BottomSheet/BottomSheetPanel.tsx +3 -37
- package/src/BottomSheet/useSheetGestures.ts +1 -3
- package/src/Button/Button.tsx +4 -26
- package/src/CheckboxInput/CheckboxInput.doc.mjs +0 -18
- package/src/CheckboxInput/CheckboxInput.tsx +9 -36
- package/src/ComplexSelector/ComplexSelector.tsx +5 -20
- package/src/ContextMenu/ContextMenu.tsx +2 -2
- package/src/Dialog/Dialog.tsx +31 -10
- package/src/Field/Field.tsx +2 -2
- package/src/HoverCard/HoverCard.doc.mjs +2 -4
- package/src/HoverCard/HoverCard.tsx +4 -35
- package/src/HoverCard/useHoverCard.doc.mjs +36 -178
- package/src/HoverCard/useHoverCard.tsx +52 -46
- package/src/Icon/Icon.doc.mjs +8 -49
- package/src/Icon/Icon.tsx +7 -15
- package/src/Layer/usePressPin.ts +15 -81
- package/src/Lightbox/Lightbox.tsx +0 -18
- package/src/PowerSearch/PowerSearch.doc.mjs +5 -54
- package/src/PowerSearch/PowerSearch.tsx +155 -609
- package/src/PowerSearch/PowerSearchEditPopover.tsx +6 -28
- package/src/PowerSearch/PowerSearchValueEditor.tsx +38 -203
- package/src/PowerSearch/formatFilterValue.ts +1 -18
- package/src/RadioList/RadioList.tsx +2 -3
- package/src/RadioList/RadioListItem.tsx +3 -5
- package/src/Resizable/Resizable.doc.mjs +4 -5
- package/src/Resizable/ResizeHandle.tsx +109 -167
- package/src/SegmentedControl/useSegmentedSelectionMotion.ts +3 -6
- package/src/Slider/sliderTouchController.ts +2 -2
- package/src/Spinner/Spinner.tsx +63 -77
- package/src/Switch/useSwitchTouch.ts +4 -4
- package/src/Table/BaseTable.tsx +12 -46
- package/src/TextInput/TextInput.tsx +10 -11
- package/src/Toast/Toast.doc.mjs +3 -4
- package/src/Toast/Toast.tsx +32 -52
- package/src/Toast/ToastViewport.tsx +35 -121
- package/src/Toast/types.ts +3 -9
- package/src/Tooltip/useTooltip.doc.mjs +0 -24
- package/src/Tooltip/useTooltip.tsx +56 -61
- package/src/Typeahead/TypeaheadItem.doc.mjs +3 -6
- package/src/Typeahead/TypeaheadItem.tsx +1 -2
- package/src/gestures/PressTracker.ts +3 -4
- package/src/gestures/README.md +1 -7
- package/src/hooks/actionGroupTracker.ts +5 -20
- package/src/hooks/index.ts +0 -2
- package/src/hooks/useAttachment.ts +1 -1
- package/src/hooks/useInputStatusIcon.tsx +2 -3
- package/src/hooks/useLongPress.doc.mjs +2 -2
- package/src/hooks/useLongPress.ts +15 -136
- package/src/i18n/generated-english/PowerSearch/PowerSearchValueEditor.generated.ts +0 -6
- package/src/i18n/generated-locales/en.generated.ts +0 -41
- package/src/i18n/generated-locales/pseudo.generated.ts +0 -42
- package/src/index.ts +0 -13
- package/src/motion/README.md +5 -9
- package/src/motion/animatedLayout.ts +4 -5
- package/src/motion/animator.ts +29 -61
- package/src/utils/dateParser.ts +0 -16
- package/src/utils/focusReturn.ts +0 -30
- package/src/utils/interactionOverlay.stylex.ts +2 -3
- package/src/utils/pressFeedback.ts +6 -19
- package/dist/PowerSearch/PowerSearchBottomSheet.d.ts +0 -100
- package/dist/PowerSearch/PowerSearchBottomSheet.d.ts.map +0 -1
- package/dist/PowerSearch/PowerSearchBottomSheet.js +0 -1913
- package/dist/PowerSearch/formatTokenValue.d.ts +0 -28
- package/dist/PowerSearch/formatTokenValue.d.ts.map +0 -1
- package/dist/PowerSearch/formatTokenValue.js +0 -160
- package/dist/PowerSearch/powerSearchSheetUtils.d.ts +0 -65
- package/dist/PowerSearch/powerSearchSheetUtils.d.ts.map +0 -1
- package/dist/PowerSearch/powerSearchSheetUtils.js +0 -168
- package/dist/animator/createAnimator.d.ts +0 -98
- package/dist/animator/createAnimator.d.ts.map +0 -1
- package/dist/animator/createAnimator.js +0 -259
- package/dist/hooks/pressGroupContext.d.ts +0 -6
- package/dist/hooks/pressGroupContext.d.ts.map +0 -1
- package/dist/hooks/pressGroupContext.js +0 -20
- package/dist/hooks/useActionGroupTracking.d.ts +0 -53
- package/dist/hooks/useActionGroupTracking.d.ts.map +0 -1
- package/dist/hooks/useActionGroupTracking.js +0 -174
- package/dist/hooks/useAnimator.d.ts +0 -41
- package/dist/hooks/useAnimator.d.ts.map +0 -1
- package/dist/hooks/useAnimator.js +0 -70
- package/dist/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.d.ts +0 -8
- package/dist/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.d.ts.map +0 -1
- package/dist/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.js +0 -42
- package/dist/i18n/generated-english/PowerSearch/formatTokenValue.generated.d.ts +0 -8
- package/dist/i18n/generated-english/PowerSearch/formatTokenValue.generated.d.ts.map +0 -1
- package/dist/i18n/generated-english/PowerSearch/formatTokenValue.generated.js +0 -14
- package/dist/internal/indicatorPress.stylex.d.ts +0 -20
- package/dist/internal/indicatorPress.stylex.d.ts.map +0 -1
- package/dist/internal/indicatorPress.stylex.js +0 -26
- package/dist/motion/progress.d.ts +0 -88
- package/dist/motion/progress.d.ts.map +0 -1
- package/dist/motion/progress.js +0 -128
- package/dist/motion/uikit/alert.d.ts +0 -41
- package/dist/motion/uikit/alert.d.ts.map +0 -1
- package/dist/motion/uikit/alert.js +0 -43
- package/dist/privateAttributes/pressGroupItem.stylex.d.ts +0 -15
- package/dist/privateAttributes/pressGroupItem.stylex.d.ts.map +0 -1
- package/dist/privateAttributes/pressGroupItem.stylex.js +0 -23
- package/dist/utils/triggerFocus.d.ts +0 -36
- package/dist/utils/triggerFocus.d.ts.map +0 -1
- package/dist/utils/triggerFocus.js +0 -65
- package/src/PowerSearch/PowerSearchBottomSheet.tsx +0 -2515
- package/src/PowerSearch/formatTokenValue.ts +0 -215
- package/src/PowerSearch/powerSearchSheetUtils.ts +0 -224
- package/src/animator/createAnimator.doc.mjs +0 -153
- package/src/animator/createAnimator.ts +0 -324
- package/src/hooks/pressGroupContext.ts +0 -21
- package/src/hooks/useActionGroupTracking.ts +0 -224
- package/src/hooks/useAnimator.doc.mjs +0 -109
- package/src/hooks/useAnimator.ts +0 -67
- package/src/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.ts +0 -43
- package/src/i18n/generated-english/PowerSearch/formatTokenValue.generated.ts +0 -15
- package/src/internal/indicatorPress.stylex.ts +0 -24
- package/src/motion/progress.ts +0 -155
- package/src/motion/uikit/alert.ts +0 -43
- package/src/privateAttributes/pressGroupItem.stylex.ts +0 -23
- package/src/utils/triggerFocus.ts +0 -80
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @fileoverview The text a PowerSearch filter token shows for a value.
|
|
5
|
-
*
|
|
6
|
-
* @input `OperatorValue`, `FilterValue`, a width budget, and the locale.
|
|
7
|
-
* @output `formatTokenValueText` — one string, or null for an empty value.
|
|
8
|
-
* @position Shared by the token itself (`PowerSearch.tsx`) and the sheet
|
|
9
|
-
* presentation's Filters rows and removal announcement
|
|
10
|
-
* (`PowerSearchBottomSheet.tsx`), so every reading of a value on the field
|
|
11
|
-
* uses the same words and the same zone handling.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import type {OperatorValue, FilterValue, EnumItem} from './types';
|
|
15
|
-
import type {Locale, TranslatorFn} from '../i18n';
|
|
16
|
-
import {truncateCharacters} from '../utils/characters';
|
|
17
|
-
import {
|
|
18
|
-
formatDateAbsoluteCompact,
|
|
19
|
-
formatSharedNumber,
|
|
20
|
-
} from './formatFilterValue';
|
|
21
|
-
import {registerEnglishFallback} from '../i18n/englishFallback';
|
|
22
|
-
import english from '../i18n/generated-english/PowerSearch/formatTokenValue.generated';
|
|
23
|
-
|
|
24
|
-
registerEnglishFallback(english);
|
|
25
|
-
|
|
26
|
-
type ListFormatFn = (items: ReadonlyArray<string>) => string;
|
|
27
|
-
|
|
28
|
-
function truncateString(value: string, limit: number): string {
|
|
29
|
-
// Same semantics as before — strings within limit + 3 pass through, longer
|
|
30
|
-
// ones cut to limit + '...' — but counted in characters, not code units.
|
|
31
|
-
return truncateCharacters(value, limit + 3, '...');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function getEnumLabel(values: ReadonlyArray<EnumItem>, value: string): string {
|
|
35
|
-
return values.find(v => v.value === value)?.label ?? value;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Intl's maximum fraction cap: no number the row formats is rounded.
|
|
39
|
-
const MAX_FRACTION_DIGITS = 100;
|
|
40
|
-
|
|
41
|
-
// Whether a locale writes a number with the token's own numerals: Latin
|
|
42
|
-
// digits and a `.` decimal mark, so `String(value)` is already the locale's
|
|
43
|
-
// reading. Probed once per locale through the shared number helper.
|
|
44
|
-
const tokenNumeralLocales = new Map<string, boolean>();
|
|
45
|
-
function usesTokenNumerals(locale: Locale): boolean {
|
|
46
|
-
let uses = tokenNumeralLocales.get(locale);
|
|
47
|
-
if (uses == null) {
|
|
48
|
-
uses =
|
|
49
|
-
formatSharedNumber(1234567.5, locale, {
|
|
50
|
-
useGrouping: false,
|
|
51
|
-
maximumFractionDigits: MAX_FRACTION_DIGITS,
|
|
52
|
-
}) === '1234567.5';
|
|
53
|
-
tokenNumeralLocales.set(locale, uses);
|
|
54
|
-
}
|
|
55
|
-
return uses;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// A number as the token reads it. Where the locale writes Latin digits and a
|
|
59
|
-
// `.` decimal mark, use the token's own text, `String(value)`. Elsewhere, use
|
|
60
|
-
// the locale's digits and decimal mark, with no grouping and no rounding: the
|
|
61
|
-
// fraction cap is Intl's maximum, so `1e-21` keeps its digits.
|
|
62
|
-
function formatNumberAsToken(value: number, locale: Locale): string {
|
|
63
|
-
return usesTokenNumerals(locale)
|
|
64
|
-
? String(value)
|
|
65
|
-
: formatSharedNumber(value, locale, {
|
|
66
|
-
useGrouping: false,
|
|
67
|
-
maximumFractionDigits: MAX_FRACTION_DIGITS,
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// The summaries, lists, and numbers the token writes as English literals, and
|
|
72
|
-
// the locale-aware readings of the same: with a translator (the sheet
|
|
73
|
-
// presentation's rows and announcements) the summaries resolve through the
|
|
74
|
-
// catalog, lists through the provider locale's list format, and numbers
|
|
75
|
-
// through its number format; without one they stay the token's own literals,
|
|
76
|
-
// the released popover token's text. In English the two read identically.
|
|
77
|
-
function itemsSummary(count: number, t: TranslatorFn | undefined): string {
|
|
78
|
-
return t
|
|
79
|
-
? t('@astryx.powersearch.valueEditor.itemsCount', {count})
|
|
80
|
-
: // eslint-disable-next-line @astryx/no-hardcoded-i18n-string -- the popover token's released English text; translating it is a separate stable change
|
|
81
|
-
`${count} items`;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function formatListText(
|
|
85
|
-
labels: ReadonlyArray<string>,
|
|
86
|
-
maxLength: number,
|
|
87
|
-
t: TranslatorFn | undefined,
|
|
88
|
-
formatList: ListFormatFn | undefined,
|
|
89
|
-
): string | null {
|
|
90
|
-
if (labels.length === 0) {
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
if (labels.length === 1) {
|
|
94
|
-
return truncateString(labels[0], maxLength);
|
|
95
|
-
}
|
|
96
|
-
const totalLength = labels.reduce((sum, s) => sum + s.length, 0);
|
|
97
|
-
if (totalLength > maxLength) {
|
|
98
|
-
return itemsSummary(labels.length, t);
|
|
99
|
-
}
|
|
100
|
-
return formatList ? formatList(labels) : labels.join(', ');
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* The text a filter token shows for a value: the one place that decides how
|
|
105
|
-
* a value reads on the field, so every other reading of the same value —
|
|
106
|
-
* the sheet presentation's Filters rows and its removal announcement — can
|
|
107
|
-
* use the same words and the same zone handling. `maxLength` is the token's
|
|
108
|
-
* width budget; pass `Number.MAX_SAFE_INTEGER` for the untruncated text.
|
|
109
|
-
* With `t`, the list, date-range and nested summaries resolve through the
|
|
110
|
-
* catalog (`@astryx.powersearch.valueEditor.*`); without it they are the
|
|
111
|
-
* token's own literals. With `formatList` (the provider's list formatter),
|
|
112
|
-
* several labels join as a list in the locale; without it they join with
|
|
113
|
-
* the token's literal ", ".
|
|
114
|
-
*/
|
|
115
|
-
export function formatTokenValueText(
|
|
116
|
-
operatorValue: OperatorValue,
|
|
117
|
-
filterValue: FilterValue,
|
|
118
|
-
maxLength: number,
|
|
119
|
-
locale: Locale,
|
|
120
|
-
t?: TranslatorFn,
|
|
121
|
-
formatList?: ListFormatFn,
|
|
122
|
-
): string | null {
|
|
123
|
-
switch (filterValue.type) {
|
|
124
|
-
case 'empty':
|
|
125
|
-
return null;
|
|
126
|
-
|
|
127
|
-
case 'string':
|
|
128
|
-
return truncateString(filterValue.value, maxLength);
|
|
129
|
-
|
|
130
|
-
case 'integer':
|
|
131
|
-
case 'float':
|
|
132
|
-
return t
|
|
133
|
-
? formatNumberAsToken(filterValue.value, locale)
|
|
134
|
-
: String(filterValue.value);
|
|
135
|
-
|
|
136
|
-
case 'enum':
|
|
137
|
-
if (operatorValue.type === 'enum') {
|
|
138
|
-
return truncateString(
|
|
139
|
-
getEnumLabel(operatorValue.values, filterValue.value),
|
|
140
|
-
maxLength,
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
return truncateString(filterValue.value, maxLength);
|
|
144
|
-
|
|
145
|
-
case 'string_list':
|
|
146
|
-
return formatListText(filterValue.value, maxLength, t, formatList);
|
|
147
|
-
|
|
148
|
-
case 'enum_list': {
|
|
149
|
-
const items = filterValue.value;
|
|
150
|
-
if (items.length === 0) {
|
|
151
|
-
return null;
|
|
152
|
-
}
|
|
153
|
-
if (operatorValue.type === 'enum_list') {
|
|
154
|
-
return formatListText(
|
|
155
|
-
items.map(v => getEnumLabel(operatorValue.values, v)),
|
|
156
|
-
maxLength,
|
|
157
|
-
t,
|
|
158
|
-
formatList,
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
if (items.length === 1) {
|
|
162
|
-
return truncateString(items[0], maxLength);
|
|
163
|
-
}
|
|
164
|
-
return itemsSummary(items.length, t);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
case 'entity_list':
|
|
168
|
-
return formatListText(
|
|
169
|
-
filterValue.value.map(e => e.label),
|
|
170
|
-
maxLength,
|
|
171
|
-
t,
|
|
172
|
-
formatList,
|
|
173
|
-
);
|
|
174
|
-
|
|
175
|
-
case 'time':
|
|
176
|
-
return filterValue.value;
|
|
177
|
-
|
|
178
|
-
case 'date_absolute':
|
|
179
|
-
return truncateString(
|
|
180
|
-
formatDateAbsoluteCompact(filterValue.unixSeconds, locale),
|
|
181
|
-
maxLength,
|
|
182
|
-
);
|
|
183
|
-
|
|
184
|
-
case 'date_relative':
|
|
185
|
-
return truncateString(filterValue.value, maxLength);
|
|
186
|
-
|
|
187
|
-
case 'date_range':
|
|
188
|
-
if (t) {
|
|
189
|
-
return t('@astryx.powersearch.valueEditor.dateRange');
|
|
190
|
-
}
|
|
191
|
-
// eslint-disable-next-line @astryx/no-hardcoded-i18n-string -- the popover token's released English text; translating it is a separate stable change
|
|
192
|
-
return 'date range';
|
|
193
|
-
|
|
194
|
-
case 'custom':
|
|
195
|
-
if (operatorValue.type === 'custom') {
|
|
196
|
-
return truncateString(
|
|
197
|
-
operatorValue.getString(filterValue.value),
|
|
198
|
-
maxLength,
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
return filterValue.value;
|
|
202
|
-
|
|
203
|
-
case 'nested': {
|
|
204
|
-
const count = filterValue.value.length;
|
|
205
|
-
if (t) {
|
|
206
|
-
return t('@astryx.powersearch.valueEditor.filtersCount', {count});
|
|
207
|
-
}
|
|
208
|
-
// eslint-disable-next-line @astryx/no-hardcoded-i18n-string -- the popover token's released English text; translating it is a separate stable change
|
|
209
|
-
return count === 1 ? '1 filter' : `${count} filters`;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
default:
|
|
213
|
-
return null;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @file powerSearchSheetUtils.ts
|
|
5
|
-
* @input PowerSearch config/filter types and the field typeahead source
|
|
6
|
-
* @output Pure helpers for the sheet presentation: which operators the
|
|
7
|
-
* sheet's own controls edit, field matching, filter identity tracking
|
|
8
|
-
* across controlled re-renders, the focus target after a removal, and the
|
|
9
|
-
* content search's suggestion source over the field typeahead
|
|
10
|
-
* @position Helper module; consumed by PowerSearchBottomSheet.tsx and its
|
|
11
|
-
* tests
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import type {SearchSource} from '../Typeahead/types';
|
|
15
|
-
import type {InternalConfig} from './useInternalConfig';
|
|
16
|
-
import type {
|
|
17
|
-
PowerSearchField,
|
|
18
|
-
PowerSearchFilter,
|
|
19
|
-
PowerSearchItem,
|
|
20
|
-
PowerSearchOperator,
|
|
21
|
-
} from './types';
|
|
22
|
-
|
|
23
|
-
// =============================================================================
|
|
24
|
-
// Operators and fields
|
|
25
|
-
// =============================================================================
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Whether the value step edits an operator's value with a sheet-native
|
|
29
|
-
* control (choice rows, or the popover's editor in its sheet presentation).
|
|
30
|
-
* A `nested` value is the exception: it is edited by the popover's built-in
|
|
31
|
-
* NestedEditor, rendered in the step's body,
|
|
32
|
-
* so every operator opens an editable step and no field is left out.
|
|
33
|
-
*/
|
|
34
|
-
export function isSupportedOperator(operator: PowerSearchOperator): boolean {
|
|
35
|
-
return operator.value.type !== 'nested';
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function fieldMatchesQuery(
|
|
39
|
-
field: PowerSearchField,
|
|
40
|
-
normalizedQuery: string,
|
|
41
|
-
): boolean {
|
|
42
|
-
if (normalizedQuery === '') {
|
|
43
|
-
return true;
|
|
44
|
-
}
|
|
45
|
-
if (field.label.toLowerCase().includes(normalizedQuery)) {
|
|
46
|
-
return true;
|
|
47
|
-
}
|
|
48
|
-
return (
|
|
49
|
-
field.typeaheadAliases?.some(alias =>
|
|
50
|
-
alias.toLowerCase().includes(normalizedQuery),
|
|
51
|
-
) ?? false
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** Synchronous suggestion source; the sheet renders results inline. */
|
|
56
|
-
export interface SheetSuggestionSource {
|
|
57
|
-
search(query: string): PowerSearchItem[];
|
|
58
|
-
bootstrap(): PowerSearchItem[];
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* The field typeahead source as the content search's rows: the popover
|
|
63
|
-
* presentation's own suggestions, minus any whose field the config no longer
|
|
64
|
-
* knows. That source resolves synchronously from the config; a promise (none
|
|
65
|
-
* today) yields no rows rather than a stale list.
|
|
66
|
-
*/
|
|
67
|
-
export function createSheetSuggestionSource(
|
|
68
|
-
config: InternalConfig,
|
|
69
|
-
source: SearchSource<PowerSearchItem>,
|
|
70
|
-
): SheetSuggestionSource {
|
|
71
|
-
// Every suggestion whose field the config knows is offered: the value step
|
|
72
|
-
// edits every operator, `nested` included.
|
|
73
|
-
const isEditable = (item: PowerSearchItem): boolean => {
|
|
74
|
-
const aux = item.auxiliaryData;
|
|
75
|
-
return aux != null && config.getField(aux.fieldKey) != null;
|
|
76
|
-
};
|
|
77
|
-
const settle = (
|
|
78
|
-
results: PowerSearchItem[] | Promise<PowerSearchItem[]>,
|
|
79
|
-
): PowerSearchItem[] =>
|
|
80
|
-
Array.isArray(results) ? results.filter(isEditable) : [];
|
|
81
|
-
return {
|
|
82
|
-
search: query => settle(source.search(query)),
|
|
83
|
-
bootstrap: () => settle(source.bootstrap()),
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// =============================================================================
|
|
88
|
-
// Filter identity across controlled updates
|
|
89
|
-
// =============================================================================
|
|
90
|
-
|
|
91
|
-
function stableSerialize(value: unknown): string {
|
|
92
|
-
if (Array.isArray(value)) {
|
|
93
|
-
return `[${value.map(stableSerialize).join(',')}]`;
|
|
94
|
-
}
|
|
95
|
-
if (value != null && typeof value === 'object') {
|
|
96
|
-
return `{${Object.entries(value)
|
|
97
|
-
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
98
|
-
.map(([key, entry]) => `${JSON.stringify(key)}:${stableSerialize(entry)}`)
|
|
99
|
-
.join(',')}}`;
|
|
100
|
-
}
|
|
101
|
-
return JSON.stringify(value) ?? 'undefined';
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Stable identities for the Filters rows across controlled updates, so a row
|
|
106
|
-
* keeps its element — and its focus — when the parent reorders the filters or
|
|
107
|
-
* removes one above it. A filter object seen before keeps its
|
|
108
|
-
* id; a new object takes the id of a filter from the previous update with the
|
|
109
|
-
* same signature that no present object claims (a parent that clones its
|
|
110
|
-
* values on every change); anything else mints a new id. Two equal filters
|
|
111
|
-
* are told apart by order. Call `next` once per render with the current list.
|
|
112
|
-
*/
|
|
113
|
-
export function createFilterIdentities(): {
|
|
114
|
-
next: (filters: ReadonlyArray<PowerSearchFilter>) => number[];
|
|
115
|
-
} {
|
|
116
|
-
const byObject = new WeakMap<PowerSearchFilter, number>();
|
|
117
|
-
let previousBySignature = new Map<string, number[]>();
|
|
118
|
-
let nextId = 0;
|
|
119
|
-
return {
|
|
120
|
-
next(filters) {
|
|
121
|
-
const used = new Set<number>();
|
|
122
|
-
const ids: (number | null)[] = filters.map(filter => {
|
|
123
|
-
const known = byObject.get(filter);
|
|
124
|
-
if (known == null || used.has(known)) {
|
|
125
|
-
return null;
|
|
126
|
-
}
|
|
127
|
-
used.add(known);
|
|
128
|
-
return known;
|
|
129
|
-
});
|
|
130
|
-
const bySignature = new Map<string, number[]>();
|
|
131
|
-
filters.forEach((filter, index) => {
|
|
132
|
-
const signature = filterSignature(filter);
|
|
133
|
-
let id = ids[index];
|
|
134
|
-
if (id == null) {
|
|
135
|
-
id =
|
|
136
|
-
previousBySignature
|
|
137
|
-
.get(signature)
|
|
138
|
-
?.find(candidate => !used.has(candidate)) ?? nextId++;
|
|
139
|
-
used.add(id);
|
|
140
|
-
byObject.set(filter, id);
|
|
141
|
-
ids[index] = id;
|
|
142
|
-
}
|
|
143
|
-
bySignature.set(signature, [...(bySignature.get(signature) ?? []), id]);
|
|
144
|
-
});
|
|
145
|
-
previousBySignature = bySignature;
|
|
146
|
-
return ids as number[];
|
|
147
|
-
},
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/** Structural snapshot of a filter, for parents that clone their values. */
|
|
152
|
-
export function filterSignature(filter: PowerSearchFilter): string {
|
|
153
|
-
return stableSerialize({
|
|
154
|
-
field: filter.field,
|
|
155
|
-
operator: filter.operator,
|
|
156
|
-
value: filter.value,
|
|
157
|
-
isReadOnly: filter.isReadOnly ?? false,
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Finds the filter a step was opened on after the controlled parent may have
|
|
163
|
-
* reordered, cloned, or removed it: object identity first, then the original
|
|
164
|
-
* slot when it still holds a structurally equal filter, then a unique
|
|
165
|
-
* structural match anywhere. Returns null when the target is gone or
|
|
166
|
-
* ambiguous, so an edit never lands on a different filter.
|
|
167
|
-
*/
|
|
168
|
-
export function resolveFilterIdentityIndex(
|
|
169
|
-
filters: ReadonlyArray<PowerSearchFilter>,
|
|
170
|
-
sourceFilter: PowerSearchFilter,
|
|
171
|
-
sourceSignature: string,
|
|
172
|
-
preferredIndex: number,
|
|
173
|
-
): number | null {
|
|
174
|
-
const identityIndex = filters.indexOf(sourceFilter);
|
|
175
|
-
if (identityIndex >= 0) {
|
|
176
|
-
return identityIndex;
|
|
177
|
-
}
|
|
178
|
-
if (
|
|
179
|
-
filters[preferredIndex] != null &&
|
|
180
|
-
filterSignature(filters[preferredIndex]) === sourceSignature
|
|
181
|
-
) {
|
|
182
|
-
return preferredIndex;
|
|
183
|
-
}
|
|
184
|
-
const matchingIndices = filters.flatMap((filter, index) =>
|
|
185
|
-
filterSignature(filter) === sourceSignature ? [index] : [],
|
|
186
|
-
);
|
|
187
|
-
return matchingIndices.length === 1 ? matchingIndices[0] : null;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* After removing the filter at `removedIndex`, focus should land on the
|
|
192
|
-
* nearest remaining row that is a button, preferring the one after it.
|
|
193
|
-
* `isRowButton` is the Filters sheet's own rule for which rows are buttons:
|
|
194
|
-
* the field interactive and the filter not read-only (a `nested` or stale
|
|
195
|
-
* filter's row is a button too).
|
|
196
|
-
*/
|
|
197
|
-
export function nearestEditableIndex(
|
|
198
|
-
nextFilters: ReadonlyArray<PowerSearchFilter>,
|
|
199
|
-
removedIndex: number,
|
|
200
|
-
isRowButton: (filter: PowerSearchFilter) => boolean = filter =>
|
|
201
|
-
!filter.isReadOnly,
|
|
202
|
-
): number | undefined {
|
|
203
|
-
const editableIndices = nextFilters.flatMap((candidate, candidateIndex) =>
|
|
204
|
-
isRowButton(candidate) ? [candidateIndex] : [],
|
|
205
|
-
);
|
|
206
|
-
return editableIndices.reduce<number | undefined>((nearest, candidate) => {
|
|
207
|
-
if (nearest == null) {
|
|
208
|
-
return candidate;
|
|
209
|
-
}
|
|
210
|
-
const candidateDistance = Math.abs(candidate - removedIndex);
|
|
211
|
-
const nearestDistance = Math.abs(nearest - removedIndex);
|
|
212
|
-
if (candidateDistance < nearestDistance) {
|
|
213
|
-
return candidate;
|
|
214
|
-
}
|
|
215
|
-
if (
|
|
216
|
-
candidateDistance === nearestDistance &&
|
|
217
|
-
candidate >= removedIndex &&
|
|
218
|
-
nearest < removedIndex
|
|
219
|
-
) {
|
|
220
|
-
return candidate;
|
|
221
|
-
}
|
|
222
|
-
return nearest;
|
|
223
|
-
}, undefined);
|
|
224
|
-
}
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
-
|
|
3
|
-
/** @type {import('@astryxdesign/cli/authoring').HookDoc} */
|
|
4
|
-
export const docs = {
|
|
5
|
-
name: 'createAnimator',
|
|
6
|
-
displayName: 'createAnimator',
|
|
7
|
-
// A factory documented as a hook: listed with the utilities, beside its
|
|
8
|
-
// hook, not as a tile in the components gallery.
|
|
9
|
-
group: 'Utilities',
|
|
10
|
-
keywords: [
|
|
11
|
-
'animate',
|
|
12
|
-
'animation',
|
|
13
|
-
'animator',
|
|
14
|
-
'spring',
|
|
15
|
-
'decay',
|
|
16
|
-
'momentum',
|
|
17
|
-
'fling',
|
|
18
|
-
'motion',
|
|
19
|
-
'transform',
|
|
20
|
-
'opacity',
|
|
21
|
-
'vanilla',
|
|
22
|
-
'factory',
|
|
23
|
-
],
|
|
24
|
-
params: [
|
|
25
|
-
{
|
|
26
|
-
name: 'element',
|
|
27
|
-
type: 'HTMLElement',
|
|
28
|
-
description:
|
|
29
|
-
'The element to animate. The animator writes its `translate`, `scale`, `rotate` and `opacity`, and while it moves an inline `will-change` for them, merged with any it already has and restored 500 ms after its last motion; nothing else, so lay it out and style it as usual. Use one animator per element: two on one element overwrite each other.',
|
|
30
|
-
required: true,
|
|
31
|
-
},
|
|
32
|
-
],
|
|
33
|
-
returns: [
|
|
34
|
-
{
|
|
35
|
-
name: 'animate',
|
|
36
|
-
type: '(target: {x?, y?, scale?, rotate?, opacity?}, options?: {velocity?}) => Promise<void>',
|
|
37
|
-
description:
|
|
38
|
-
"Springs the given channels to `target` from wherever they are on screen, on the state-change motion Astryx's components use, and resolves when they arrive (or as soon as a later call takes them over). `x` and `y` are px, `scale` is 1 at the element's size, `rotate` is degrees and `opacity` runs from 0 to 1. Pass `velocity` (units per second, such as a drag's release velocity) to start with it; without it a channel keeps the velocity it has on screen, so interrupting a motion never jumps.",
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
name: 'decay',
|
|
42
|
-
type: '(velocity: {x?, y?, scale?, rotate?, opacity?}) => Promise<void>',
|
|
43
|
-
description:
|
|
44
|
-
"Coasts the given channels from a release velocity in their units per second, slowing as a scroll view's momentum does, and resolves when they stop (or as soon as a later call takes any of them over); opacity stops at 0 or 1. The distance follows from the velocity: use it to fling something away, then remove or reset it.",
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
name: 'set',
|
|
48
|
-
type: '(values: {x?, y?, scale?, rotate?, opacity?}) => void',
|
|
49
|
-
description:
|
|
50
|
-
'Puts the given channels at these values at once, ending their motion: follow a finger with it, frame by frame.',
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
name: 'get',
|
|
54
|
-
type: '() => {x, y, scale, rotate, opacity}',
|
|
55
|
-
description: "Every channel's value on screen now.",
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
name: 'getVelocity',
|
|
59
|
-
type: '() => {x, y, scale, rotate, opacity}',
|
|
60
|
-
description:
|
|
61
|
-
"Every channel's velocity on screen now, in units per second.",
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
name: 'stop',
|
|
65
|
-
type: '() => void',
|
|
66
|
-
description: 'Halts every channel where it is on screen.',
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
name: 'destroy',
|
|
70
|
-
type: '() => void',
|
|
71
|
-
description:
|
|
72
|
-
'Stops every motion and lets the element go; the element keeps the values it shows. Call it when you remove the element or no longer animate it.',
|
|
73
|
-
},
|
|
74
|
-
],
|
|
75
|
-
usage: {
|
|
76
|
-
description:
|
|
77
|
-
"Animates one element with the motion Astryx's own components use, without React. `animate` springs an element to new values on the state-change motion, `decay` flings it with the momentum of a released scroll view, and `set` moves it directly, so a card can follow a finger, snap back and be thrown away. Use the `useAnimator` hook in React. To animate an area's layout as its items change, use `useAnimatedLayout`; for what a component already animates, such as a sheet's detents or a carousel's snapping, use the component's own props.",
|
|
78
|
-
bestPractices: [
|
|
79
|
-
{
|
|
80
|
-
guidance: true,
|
|
81
|
-
description:
|
|
82
|
-
'Start every `animate` that follows a drag with the release velocity, so the element carries on as it was thrown.',
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
guidance: true,
|
|
86
|
-
description:
|
|
87
|
-
'Reach every state an animation shows without the animation too: a key, a button or a menu item that calls the same code.',
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
guidance: true,
|
|
91
|
-
description:
|
|
92
|
-
'`await` the promise before removing an element you animate out, and call `destroy` once it is gone.',
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
guidance: false,
|
|
96
|
-
description:
|
|
97
|
-
'Read the element\u2019s position from layout while it moves: `get` gives the value on screen, wherever the motion runs.',
|
|
98
|
-
},
|
|
99
|
-
],
|
|
100
|
-
accessibility: [
|
|
101
|
-
{
|
|
102
|
-
name: 'Keyboard and assistive technology',
|
|
103
|
-
category: 'Keyboard',
|
|
104
|
-
description:
|
|
105
|
-
'An animator moves an element; it adds no way to act. Give every action an animation stands for a control a keyboard and a screen reader can reach, such as a button that discards a card, and have it call `animate`, which Reduce Motion turns into a fade in place. Keep `decay` for a throw the person made: Reduce Motion keeps the momentum of a coast, as it continues that throw. Keep focus on an element that stays on screen: move it before you fling its element away.',
|
|
106
|
-
},
|
|
107
|
-
{
|
|
108
|
-
name: 'Reduce Motion',
|
|
109
|
-
category: 'Content',
|
|
110
|
-
criterion: '2.3.3 Animation from Interactions',
|
|
111
|
-
description:
|
|
112
|
-
'When the person asks for reduced motion, every call is planned for them. On a touch screen a move or a scale becomes a short opacity cross-fade: the element fades out where it was and fades back in where it lands. With a mouse it lands at once. A decay of `x` and `y` keeps its momentum, as it continues the throw the person made; a decay of `scale`, `rotate` or `opacity` is planned like a move. No option turns this off.',
|
|
113
|
-
},
|
|
114
|
-
],
|
|
115
|
-
},
|
|
116
|
-
relatedHooks: ['useAnimator', 'useAnimatedLayout'],
|
|
117
|
-
importPath: '@astryxdesign/core',
|
|
118
|
-
category: 'interaction',
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
/** @type {import('@astryxdesign/cli/authoring').HookTranslationDoc} */
|
|
122
|
-
export const docsDense = {
|
|
123
|
-
description:
|
|
124
|
-
"Animates one element (x, y, scale, rotate, opacity) with Astryx's motion, without React.",
|
|
125
|
-
paramDescriptions: {
|
|
126
|
-
element: 'The element to animate; one animator per element.',
|
|
127
|
-
},
|
|
128
|
-
returnDescriptions: {
|
|
129
|
-
animate:
|
|
130
|
-
'(target, {velocity?}) => Promise<void>; springs on the state-change motion.',
|
|
131
|
-
decay:
|
|
132
|
-
'({x?, y?, scale?, rotate?, opacity?}) => Promise<void>; coasts from a release velocity.',
|
|
133
|
-
set: '(values) => void; jumps, ending motion.',
|
|
134
|
-
get: '() => every value on screen.',
|
|
135
|
-
getVelocity: '() => every velocity on screen.',
|
|
136
|
-
stop: '() => void; halts where it is.',
|
|
137
|
-
destroy: '() => void; lets the element go.',
|
|
138
|
-
},
|
|
139
|
-
usage: {
|
|
140
|
-
description:
|
|
141
|
-
'Spring with animate, fling with decay, follow a finger with set. Reduce Motion: a cross-fade on touch, at once with a mouse; decay keeps its momentum.',
|
|
142
|
-
bestPractices: [
|
|
143
|
-
{
|
|
144
|
-
guidance: true,
|
|
145
|
-
description: 'Pass the release velocity to animate after a drag.',
|
|
146
|
-
},
|
|
147
|
-
{
|
|
148
|
-
guidance: false,
|
|
149
|
-
description: 'Read layout for the position mid-motion; use get.',
|
|
150
|
-
},
|
|
151
|
-
],
|
|
152
|
-
},
|
|
153
|
-
};
|