@multiplatform.one/forms 7.5.0 → 7.6.1
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/dist/cjs/Button/index.cjs +6 -10
- package/dist/cjs/Button/index.native.js +6 -10
- package/dist/cjs/Button/index.native.js.map +1 -1
- package/dist/cjs/ContextualSaveBar/UnsavedChangesDialog.cjs +0 -1
- package/dist/cjs/ContextualSaveBar/UnsavedChangesDialog.native.js +0 -1
- package/dist/cjs/ContextualSaveBar/UnsavedChangesDialog.native.js.map +1 -1
- package/dist/cjs/FieldDisplay/index.cjs +2 -1
- package/dist/cjs/FieldDisplay/index.native.js +9 -2
- package/dist/cjs/FieldDisplay/index.native.js.map +1 -1
- package/dist/cjs/FloatingPanel/index.cjs +8 -1
- package/dist/cjs/InputParts/index.cjs +64 -2
- package/dist/cjs/InputParts/index.native.js +70 -4
- package/dist/cjs/InputParts/index.native.js.map +1 -1
- package/dist/cjs/fields/Barcode/BarcodePreview.cjs +15 -9
- package/dist/cjs/fields/Barcode/BarcodePreview.native.js +15 -9
- package/dist/cjs/fields/Barcode/BarcodePreview.native.js.map +1 -1
- package/dist/cjs/fields/Combobox/index.cjs +14 -11
- package/dist/cjs/fields/Combobox/index.native.js +16 -11
- package/dist/cjs/fields/Combobox/index.native.js.map +1 -1
- package/dist/cjs/fields/FileUpload/index.cjs +66 -5
- package/dist/cjs/fields/FileUpload/index.native.js +85 -5
- package/dist/cjs/fields/FileUpload/index.native.js.map +1 -1
- package/dist/cjs/fields/Geolocation/geoValue.cjs +122 -0
- package/dist/cjs/fields/Geolocation/geoValue.native.js +151 -0
- package/dist/cjs/fields/Geolocation/geoValue.native.js.map +1 -0
- package/dist/cjs/fields/Geolocation/index.cjs +16 -29
- package/dist/cjs/fields/Geolocation/index.native.js +16 -29
- package/dist/cjs/fields/Geolocation/index.native.js.map +1 -1
- package/dist/cjs/fields/Icon/index.cjs +1 -0
- package/dist/cjs/fields/Icon/index.native.js +1 -0
- package/dist/cjs/fields/Icon/index.native.js.map +1 -1
- package/dist/cjs/fields/Input/index.cjs +175 -95
- package/dist/cjs/fields/Input/index.native.js +83 -4
- package/dist/cjs/fields/Input/index.native.js.map +1 -1
- package/dist/cjs/fields/MentionInput/index.cjs +6 -1
- package/dist/cjs/fields/MentionInput/index.native.js +6 -1
- package/dist/cjs/fields/MentionInput/index.native.js.map +1 -1
- package/dist/cjs/fields/OTPInput/index.cjs +7 -2
- package/dist/cjs/fields/OTPInput/index.native.js +7 -2
- package/dist/cjs/fields/OTPInput/index.native.js.map +1 -1
- package/dist/cjs/fields/PhoneInput/index.native.js +1 -0
- package/dist/cjs/fields/PhoneInput/index.native.js.map +1 -1
- package/dist/cjs/fields/SearchInput/index.native.js.map +1 -1
- package/dist/cjs/shared/utils.cjs +4 -14
- package/dist/cjs/shared/utils.native.js +6 -14
- package/dist/cjs/shared/utils.native.js.map +1 -1
- package/dist/esm/Button/index.mjs +6 -9
- package/dist/esm/Button/index.mjs.map +1 -1
- package/dist/esm/Button/index.native.js +6 -9
- package/dist/esm/Button/index.native.js.map +1 -1
- package/dist/esm/ContextualSaveBar/UnsavedChangesDialog.mjs +0 -1
- package/dist/esm/ContextualSaveBar/UnsavedChangesDialog.mjs.map +1 -1
- package/dist/esm/ContextualSaveBar/UnsavedChangesDialog.native.js +0 -1
- package/dist/esm/ContextualSaveBar/UnsavedChangesDialog.native.js.map +1 -1
- package/dist/esm/FieldDisplay/index.mjs +2 -1
- package/dist/esm/FieldDisplay/index.mjs.map +1 -1
- package/dist/esm/FieldDisplay/index.native.js +9 -2
- package/dist/esm/FieldDisplay/index.native.js.map +1 -1
- package/dist/esm/FloatingPanel/index.mjs +8 -1
- package/dist/esm/FloatingPanel/index.mjs.map +1 -1
- package/dist/esm/InputParts/index.mjs +64 -2
- package/dist/esm/InputParts/index.mjs.map +1 -1
- package/dist/esm/InputParts/index.native.js +70 -4
- package/dist/esm/InputParts/index.native.js.map +1 -1
- package/dist/esm/fields/Barcode/BarcodePreview.mjs +15 -9
- package/dist/esm/fields/Barcode/BarcodePreview.mjs.map +1 -1
- package/dist/esm/fields/Barcode/BarcodePreview.native.js +15 -9
- package/dist/esm/fields/Barcode/BarcodePreview.native.js.map +1 -1
- package/dist/esm/fields/Combobox/index.mjs +14 -11
- package/dist/esm/fields/Combobox/index.mjs.map +1 -1
- package/dist/esm/fields/Combobox/index.native.js +16 -11
- package/dist/esm/fields/Combobox/index.native.js.map +1 -1
- package/dist/esm/fields/FileUpload/index.mjs +66 -5
- package/dist/esm/fields/FileUpload/index.mjs.map +1 -1
- package/dist/esm/fields/FileUpload/index.native.js +85 -5
- package/dist/esm/fields/FileUpload/index.native.js.map +1 -1
- package/dist/esm/fields/Geolocation/geoValue.mjs +95 -0
- package/dist/esm/fields/Geolocation/geoValue.mjs.map +1 -0
- package/dist/esm/fields/Geolocation/geoValue.native.js +121 -0
- package/dist/esm/fields/Geolocation/geoValue.native.js.map +1 -0
- package/dist/esm/fields/Geolocation/index.mjs +15 -28
- package/dist/esm/fields/Geolocation/index.mjs.map +1 -1
- package/dist/esm/fields/Geolocation/index.native.js +15 -28
- package/dist/esm/fields/Geolocation/index.native.js.map +1 -1
- package/dist/esm/fields/Icon/index.mjs +1 -0
- package/dist/esm/fields/Icon/index.mjs.map +1 -1
- package/dist/esm/fields/Icon/index.native.js +1 -0
- package/dist/esm/fields/Icon/index.native.js.map +1 -1
- package/dist/esm/fields/Input/index.mjs +177 -98
- package/dist/esm/fields/Input/index.mjs.map +1 -1
- package/dist/esm/fields/Input/index.native.js +85 -7
- package/dist/esm/fields/Input/index.native.js.map +1 -1
- package/dist/esm/fields/MentionInput/index.mjs +6 -1
- package/dist/esm/fields/MentionInput/index.mjs.map +1 -1
- package/dist/esm/fields/MentionInput/index.native.js +6 -1
- package/dist/esm/fields/MentionInput/index.native.js.map +1 -1
- package/dist/esm/fields/OTPInput/index.mjs +7 -3
- package/dist/esm/fields/OTPInput/index.mjs.map +1 -1
- package/dist/esm/fields/OTPInput/index.native.js +7 -3
- package/dist/esm/fields/OTPInput/index.native.js.map +1 -1
- package/dist/esm/fields/PhoneInput/index.mjs.map +1 -1
- package/dist/esm/fields/PhoneInput/index.native.js +1 -0
- package/dist/esm/fields/PhoneInput/index.native.js.map +1 -1
- package/dist/esm/fields/SearchInput/index.mjs.map +1 -1
- package/dist/esm/fields/SearchInput/index.native.js.map +1 -1
- package/dist/esm/shared/utils.mjs +4 -14
- package/dist/esm/shared/utils.mjs.map +1 -1
- package/dist/esm/shared/utils.native.js +6 -14
- package/dist/esm/shared/utils.native.js.map +1 -1
- package/dist/jsx/Button/index.mjs +6 -9
- package/dist/jsx/Button/index.mjs.map +1 -1
- package/dist/jsx/Button/index.native.js +6 -10
- package/dist/jsx/Button/index.native.js.map +1 -1
- package/dist/jsx/ContextualSaveBar/UnsavedChangesDialog.mjs +0 -1
- package/dist/jsx/ContextualSaveBar/UnsavedChangesDialog.mjs.map +1 -1
- package/dist/jsx/ContextualSaveBar/UnsavedChangesDialog.native.js +0 -1
- package/dist/jsx/ContextualSaveBar/UnsavedChangesDialog.native.js.map +1 -1
- package/dist/jsx/FieldDisplay/index.mjs +2 -1
- package/dist/jsx/FieldDisplay/index.mjs.map +1 -1
- package/dist/jsx/FieldDisplay/index.native.js +9 -2
- package/dist/jsx/FieldDisplay/index.native.js.map +1 -1
- package/dist/jsx/FloatingPanel/index.mjs +8 -1
- package/dist/jsx/FloatingPanel/index.mjs.map +1 -1
- package/dist/jsx/InputParts/index.mjs +64 -2
- package/dist/jsx/InputParts/index.mjs.map +1 -1
- package/dist/jsx/InputParts/index.native.js +70 -4
- package/dist/jsx/InputParts/index.native.js.map +1 -1
- package/dist/jsx/fields/Barcode/BarcodePreview.mjs +15 -9
- package/dist/jsx/fields/Barcode/BarcodePreview.mjs.map +1 -1
- package/dist/jsx/fields/Barcode/BarcodePreview.native.js +15 -9
- package/dist/jsx/fields/Barcode/BarcodePreview.native.js.map +1 -1
- package/dist/jsx/fields/Combobox/index.mjs +14 -11
- package/dist/jsx/fields/Combobox/index.mjs.map +1 -1
- package/dist/jsx/fields/Combobox/index.native.js +16 -11
- package/dist/jsx/fields/Combobox/index.native.js.map +1 -1
- package/dist/jsx/fields/FileUpload/index.mjs +66 -5
- package/dist/jsx/fields/FileUpload/index.mjs.map +1 -1
- package/dist/jsx/fields/FileUpload/index.native.js +85 -5
- package/dist/jsx/fields/FileUpload/index.native.js.map +1 -1
- package/dist/jsx/fields/Geolocation/geoValue.mjs +95 -0
- package/dist/jsx/fields/Geolocation/geoValue.mjs.map +1 -0
- package/dist/jsx/fields/Geolocation/geoValue.native.js +151 -0
- package/dist/jsx/fields/Geolocation/geoValue.native.js.map +1 -0
- package/dist/jsx/fields/Geolocation/index.mjs +15 -28
- package/dist/jsx/fields/Geolocation/index.mjs.map +1 -1
- package/dist/jsx/fields/Geolocation/index.native.js +16 -29
- package/dist/jsx/fields/Geolocation/index.native.js.map +1 -1
- package/dist/jsx/fields/Icon/index.mjs +1 -0
- package/dist/jsx/fields/Icon/index.mjs.map +1 -1
- package/dist/jsx/fields/Icon/index.native.js +1 -0
- package/dist/jsx/fields/Icon/index.native.js.map +1 -1
- package/dist/jsx/fields/Input/index.mjs +177 -98
- package/dist/jsx/fields/Input/index.mjs.map +1 -1
- package/dist/jsx/fields/Input/index.native.js +83 -4
- package/dist/jsx/fields/Input/index.native.js.map +1 -1
- package/dist/jsx/fields/MentionInput/index.mjs +6 -1
- package/dist/jsx/fields/MentionInput/index.mjs.map +1 -1
- package/dist/jsx/fields/MentionInput/index.native.js +6 -1
- package/dist/jsx/fields/MentionInput/index.native.js.map +1 -1
- package/dist/jsx/fields/OTPInput/index.mjs +7 -3
- package/dist/jsx/fields/OTPInput/index.mjs.map +1 -1
- package/dist/jsx/fields/OTPInput/index.native.js +7 -2
- package/dist/jsx/fields/OTPInput/index.native.js.map +1 -1
- package/dist/jsx/fields/PhoneInput/index.mjs.map +1 -1
- package/dist/jsx/fields/PhoneInput/index.native.js +1 -0
- package/dist/jsx/fields/PhoneInput/index.native.js.map +1 -1
- package/dist/jsx/fields/SearchInput/index.mjs.map +1 -1
- package/dist/jsx/fields/SearchInput/index.native.js.map +1 -1
- package/dist/jsx/shared/utils.mjs +4 -14
- package/dist/jsx/shared/utils.mjs.map +1 -1
- package/dist/jsx/shared/utils.native.js +6 -14
- package/dist/jsx/shared/utils.native.js.map +1 -1
- package/package.json +7 -7
- package/src/Button/Button.density.spec.tsx +118 -32
- package/src/Button/Button.labelInk.spec.tsx +72 -19
- package/src/Button/Button.lc71.spec.tsx +10 -3
- package/src/Button/Button.stories.tsx +97 -7
- package/src/Button/index.tsx +22 -12
- package/src/ContextualSaveBar/UnsavedChangesDialog.tsx +0 -1
- package/src/FieldDisplay/FieldDisplay.spec.tsx +11 -2
- package/src/FieldDisplay/index.tsx +5 -0
- package/src/FloatingPanel/index.tsx +22 -1
- package/src/FloatingPanel/panelWidthContract.spec.ts +1 -0
- package/src/InputParts/index.tsx +100 -3
- package/src/fields/Barcode/BarcodePreview.spec.tsx +153 -5
- package/src/fields/Barcode/BarcodePreview.stories.tsx +104 -0
- package/src/fields/Barcode/BarcodePreview.tsx +47 -15
- package/src/fields/ColorPicker/PresetSwatches.spec.tsx +71 -0
- package/src/fields/ColorPicker/PresetSwatches.stories.tsx +94 -0
- package/src/fields/Combobox/Combobox.spec.tsx +64 -1
- package/src/fields/Combobox/index.tsx +22 -11
- package/src/fields/FileUpload/FileUpload.spec.tsx +71 -0
- package/src/fields/FileUpload/index.tsx +100 -3
- package/src/fields/Geolocation/Geolocation.spec.tsx +126 -0
- package/src/fields/Geolocation/geoValue.spec.ts +169 -0
- package/src/fields/Geolocation/geoValue.ts +149 -0
- package/src/fields/Geolocation/index.tsx +15 -24
- package/src/fields/Icon/Icon.stories.tsx +1 -1
- package/src/fields/Icon/index.tsx +1 -0
- package/src/fields/Input/index.tsx +180 -81
- package/src/fields/MentionInput/index.tsx +9 -1
- package/src/fields/OTPInput/OtpInput.spec.tsx +36 -4
- package/src/fields/OTPInput/index.tsx +18 -3
- package/src/fields/OTPInput/otpCellHeightTouch.spec.ts +44 -0
- package/src/fields/PhoneInput/index.tsx +1 -0
- package/src/fields/SearchInput/index.tsx +5 -3
- package/src/shared/utils.spec.ts +46 -0
- package/src/shared/utils.ts +31 -16
- package/types/Button/index.d.ts +8 -3
- package/types/Button/index.d.ts.map +1 -1
- package/types/ContextualSaveBar/UnsavedChangesDialog.d.ts.map +1 -1
- package/types/FieldDisplay/index.d.ts.map +1 -1
- package/types/InputParts/index.d.ts +9 -5
- package/types/InputParts/index.d.ts.map +1 -1
- package/types/fields/Barcode/BarcodePreview.d.ts +20 -9
- package/types/fields/Barcode/BarcodePreview.d.ts.map +1 -1
- package/types/fields/Checkbox/index.d.ts +3 -3
- package/types/fields/Combobox/index.d.ts.map +1 -1
- package/types/fields/FileUpload/index.d.ts.map +1 -1
- package/types/fields/Geolocation/geoValue.d.ts +45 -0
- package/types/fields/Geolocation/geoValue.d.ts.map +1 -0
- package/types/fields/Geolocation/index.d.ts.map +1 -1
- package/types/fields/Icon/index.d.ts.map +1 -1
- package/types/fields/Input/index.d.ts +18 -6
- package/types/fields/Input/index.d.ts.map +1 -1
- package/types/fields/MentionInput/index.d.ts.map +1 -1
- package/types/fields/OTPInput/index.d.ts +12 -0
- package/types/fields/OTPInput/index.d.ts.map +1 -1
- package/types/fields/PhoneInput/index.d.ts.map +1 -1
- package/types/fields/RadioGroup/index.d.ts +2 -2
- package/types/fields/SearchInput/index.d.ts.map +1 -1
- package/types/fields/ToggleGroup/index.d.ts +1 -1
- package/types/shared/utils.d.ts.map +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ComponentProps, ReactElement, ReactNode } from "react";
|
|
2
|
-
import { useRef } from "react";
|
|
3
|
-
import { WarningCircleIcon } from "@phosphor-icons/react";
|
|
2
|
+
import { useCallback, useRef } from "react";
|
|
3
|
+
import { ClipboardIcon, WarningCircleIcon } from "@phosphor-icons/react";
|
|
4
4
|
import { MIN_PRESS_TARGET, pressTargetHitSlop } from "@multiplatform.one/theme";
|
|
5
5
|
import type {
|
|
6
6
|
InputKeyboardType,
|
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
SizeTokens,
|
|
10
10
|
TamaguiElement,
|
|
11
11
|
} from "tamagui";
|
|
12
|
-
import { Text } from "tamagui";
|
|
12
|
+
import { isWeb, Text } from "tamagui";
|
|
13
13
|
import { Skeleton } from "../../Skeleton";
|
|
14
14
|
import { resolveFieldInputProps } from "../../fieldDefaults";
|
|
15
15
|
import { Field, FieldLayout } from "../../fieldLayout";
|
|
@@ -27,6 +27,23 @@ import { LinkCell } from "../../utils/LinkCell";
|
|
|
27
27
|
|
|
28
28
|
type InputAreaProps = ComponentProps<typeof InputParts.Area>;
|
|
29
29
|
|
|
30
|
+
/** F1 / reference §10: standalone Input rings at offset 0, not the inset −2. */
|
|
31
|
+
const INPUT_FIELD_RING_STYLE_ID = "mp-input-field-ring-offset";
|
|
32
|
+
|
|
33
|
+
export function ensureInputFieldRingOffset(): void {
|
|
34
|
+
if (typeof document === "undefined") return;
|
|
35
|
+
if (document.getElementById(INPUT_FIELD_RING_STYLE_ID)) return;
|
|
36
|
+
const tag = document.createElement("style");
|
|
37
|
+
tag.id = INPUT_FIELD_RING_STYLE_ID;
|
|
38
|
+
tag.textContent = `[data-mp-input-field][data-mp-ring="inset"]:has(input:focus),
|
|
39
|
+
[data-mp-input-field][data-mp-ring="inset"]:has(textarea:focus),
|
|
40
|
+
[data-mp-input-field][data-mp-ring="inset"]:has([data-mp-input-area]:focus),
|
|
41
|
+
[data-mp-input-field][data-mp-ring="inset"]:focus-visible {
|
|
42
|
+
outline-offset: 0px !important;
|
|
43
|
+
}`;
|
|
44
|
+
document.head.appendChild(tag);
|
|
45
|
+
}
|
|
46
|
+
|
|
30
47
|
export interface InputProps {
|
|
31
48
|
name?: string;
|
|
32
49
|
form?: AnyFormApi;
|
|
@@ -91,6 +108,25 @@ export interface InputProps {
|
|
|
91
108
|
compact?: boolean;
|
|
92
109
|
/** When true, renders without borders/background (for toolbar integration) */
|
|
93
110
|
chromeless?: boolean;
|
|
111
|
+
/**
|
|
112
|
+
* Declared copy end-cap (CopyField / LC-16 second consumer of Input.Button).
|
|
113
|
+
* Empty omits the cap. Disabled keeps geometry and drops the tab stop.
|
|
114
|
+
*/
|
|
115
|
+
copy?: boolean;
|
|
116
|
+
/** Called after a copy attempt. CopyField routes this through notify(). */
|
|
117
|
+
onCopyResult?: (ok: boolean) => void;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const COPY_LABEL = "Copy to clipboard";
|
|
121
|
+
|
|
122
|
+
async function writeClipboard(text: string): Promise<boolean> {
|
|
123
|
+
try {
|
|
124
|
+
if (typeof navigator === "undefined" || !navigator.clipboard?.writeText) return false;
|
|
125
|
+
await navigator.clipboard.writeText(text);
|
|
126
|
+
return true;
|
|
127
|
+
} catch {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
94
130
|
}
|
|
95
131
|
|
|
96
132
|
export function Input({
|
|
@@ -130,6 +166,8 @@ export function Input({
|
|
|
130
166
|
skeleton,
|
|
131
167
|
compact,
|
|
132
168
|
chromeless,
|
|
169
|
+
copy,
|
|
170
|
+
onCopyResult,
|
|
133
171
|
}: InputProps) {
|
|
134
172
|
const { resolvedForm, knobProps, control, id } = useFormField({
|
|
135
173
|
form: formProp,
|
|
@@ -138,6 +176,7 @@ export function Input({
|
|
|
138
176
|
});
|
|
139
177
|
const inputRef = useRef<TamaguiElement>(null);
|
|
140
178
|
const focusTrigger = useForwardFocus(inputRef);
|
|
179
|
+
if (isWeb) ensureInputFieldRingOffset();
|
|
141
180
|
|
|
142
181
|
const resolvedValidators = useResolvedValidators(required, validators, label, name);
|
|
143
182
|
|
|
@@ -161,6 +200,10 @@ export function Input({
|
|
|
161
200
|
"data-visual-height": String(visualHeight),
|
|
162
201
|
"data-press-floor": belowFloor ? "slop" : "box",
|
|
163
202
|
"data-ring-target": "box",
|
|
203
|
+
"data-radius-class": "DEFAULT",
|
|
204
|
+
"data-radius-part": "Input",
|
|
205
|
+
"data-ring-offset": "0",
|
|
206
|
+
"data-mp-input-field": "true",
|
|
164
207
|
...(belowFloor ? { hitSlop: pressTargetHitSlop(visualHeight) } : undefined),
|
|
165
208
|
} as const;
|
|
166
209
|
|
|
@@ -186,7 +229,8 @@ export function Input({
|
|
|
186
229
|
const layoutBind = {
|
|
187
230
|
id,
|
|
188
231
|
label: visibleLabel,
|
|
189
|
-
labelProps
|
|
232
|
+
// R7 / §6.1: label is weight 400. Consumer labelProps still eject last.
|
|
233
|
+
labelProps: { fontWeight: "400" as const, ...labelProps },
|
|
190
234
|
helperText,
|
|
191
235
|
required,
|
|
192
236
|
disabled,
|
|
@@ -201,6 +245,23 @@ export function Input({
|
|
|
201
245
|
const areaName =
|
|
202
246
|
visibleLabel || !resolvedAriaLabel ? undefined : { "aria-label": resolvedAriaLabel };
|
|
203
247
|
|
|
248
|
+
const handleCopy = useCallback(
|
|
249
|
+
async (text: string) => {
|
|
250
|
+
const ok = await writeClipboard(text);
|
|
251
|
+
onCopyResult?.(ok);
|
|
252
|
+
},
|
|
253
|
+
[onCopyResult],
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
// Identifier values (tokens, URLs) ride the mono register (LC-12).
|
|
257
|
+
const identifierProps = copy
|
|
258
|
+
? ({
|
|
259
|
+
fontFamily: "$mono",
|
|
260
|
+
letterSpacing: 0,
|
|
261
|
+
"data-register": "identifier",
|
|
262
|
+
} as const)
|
|
263
|
+
: undefined;
|
|
264
|
+
|
|
204
265
|
// Render skeleton placeholder — FieldLayout keeps label/helper anatomy
|
|
205
266
|
// (LC-20); only the control chrome is the skeleton.
|
|
206
267
|
if (skeleton) {
|
|
@@ -226,96 +287,134 @@ export function Input({
|
|
|
226
287
|
handleChange?: (text: string) => void,
|
|
227
288
|
handleBlur?: (...args: any[]) => void,
|
|
228
289
|
hasError?: boolean,
|
|
229
|
-
) =>
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
290
|
+
) => {
|
|
291
|
+
const copyText = String(inputValue ?? "");
|
|
292
|
+
// Empty omits the cap so the field stays one tab stop (LC-49).
|
|
293
|
+
const showCopy = Boolean(copy && copyText);
|
|
294
|
+
const activateCopy = () => {
|
|
295
|
+
if (disabled) return;
|
|
296
|
+
void handleCopy(copyText);
|
|
297
|
+
};
|
|
298
|
+
return (
|
|
299
|
+
<InputParts.Box
|
|
300
|
+
{...boxContract}
|
|
301
|
+
{...(handleBlur ? { onBlur: handleBlur } : undefined)}
|
|
302
|
+
// LC-40: the Box owns the disabled chrome treatment (read-only renders
|
|
303
|
+
// its own branch below and never dims).
|
|
304
|
+
disabled={disabled}
|
|
305
|
+
{...(showCopy ? { paddingInlineEnd: 0 } : undefined)}
|
|
306
|
+
{...chromelessProps}
|
|
307
|
+
>
|
|
308
|
+
{leftIcon && (
|
|
309
|
+
<InputParts.Icon adornment="leading" {...focusTrigger}>
|
|
310
|
+
{leftIcon}
|
|
311
|
+
</InputParts.Icon>
|
|
312
|
+
)}
|
|
313
|
+
{readOnly ? (
|
|
314
|
+
linkResolver ? (
|
|
315
|
+
<LinkCell value={inputValue} linkResolver={linkResolver}>
|
|
316
|
+
<Text
|
|
317
|
+
id={id}
|
|
318
|
+
aria-readonly="true"
|
|
319
|
+
aria-required={required || undefined}
|
|
320
|
+
aria-invalid={hasError || undefined}
|
|
321
|
+
{...areaName}
|
|
322
|
+
{...identifierProps}
|
|
323
|
+
color="$color"
|
|
324
|
+
>
|
|
325
|
+
{inputValue ?? ""}
|
|
326
|
+
</Text>
|
|
327
|
+
</LinkCell>
|
|
328
|
+
) : (
|
|
329
|
+
<InputParts.Area
|
|
330
|
+
ref={inputRef}
|
|
331
|
+
value={inputValue ?? ""}
|
|
332
|
+
readOnly
|
|
247
333
|
id={id}
|
|
248
334
|
aria-readonly="true"
|
|
249
335
|
aria-required={required || undefined}
|
|
250
336
|
aria-invalid={hasError || undefined}
|
|
337
|
+
{...identifierProps}
|
|
251
338
|
{...areaName}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
{inputValue ?? ""}
|
|
255
|
-
</Text>
|
|
256
|
-
</LinkCell>
|
|
339
|
+
/>
|
|
340
|
+
)
|
|
257
341
|
) : (
|
|
258
342
|
<InputParts.Area
|
|
259
343
|
ref={inputRef}
|
|
260
|
-
|
|
261
|
-
|
|
344
|
+
{...(purposeInputProps.inputMode !== undefined
|
|
345
|
+
? { inputMode: purposeInputProps.inputMode }
|
|
346
|
+
: undefined)}
|
|
347
|
+
{...(purposeInputProps.keyboardType !== undefined
|
|
348
|
+
? { keyboardType: purposeInputProps.keyboardType }
|
|
349
|
+
: undefined)}
|
|
350
|
+
{...(purposeInputProps.autoComplete !== undefined
|
|
351
|
+
? { autoComplete: purposeInputProps.autoComplete }
|
|
352
|
+
: undefined)}
|
|
353
|
+
{...inputProps}
|
|
354
|
+
{...identifierProps}
|
|
355
|
+
{...(multiline !== undefined ? { multiline } : undefined)}
|
|
356
|
+
{...(maxLength !== undefined ? { maxLength } : undefined)}
|
|
357
|
+
{...(keyboardType !== undefined ? { keyboardType } : undefined)}
|
|
358
|
+
{...(textContentType !== undefined ? { textContentType } : undefined)}
|
|
359
|
+
{...(autoComplete !== undefined ? { autoComplete } : undefined)}
|
|
360
|
+
{...(secureTextEntry !== undefined ? { secureTextEntry } : undefined)}
|
|
361
|
+
{...(placeholder !== undefined ? { placeholder } : undefined)}
|
|
362
|
+
disabled={disabled}
|
|
262
363
|
id={id}
|
|
263
|
-
|
|
364
|
+
{...(handleChange ? { value: inputValue ?? "" } : { defaultValue: inputValue })}
|
|
264
365
|
aria-required={required || undefined}
|
|
265
366
|
aria-invalid={hasError || undefined}
|
|
266
367
|
{...areaName}
|
|
368
|
+
onChange={onChange}
|
|
369
|
+
onChangeText={(text) => {
|
|
370
|
+
if (disabled) return;
|
|
371
|
+
handleChange?.(text);
|
|
372
|
+
onChangeText?.(text);
|
|
373
|
+
}}
|
|
267
374
|
/>
|
|
268
|
-
)
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
: undefined
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
// M3 / Polaris / Spectrum: in-field error glyph (color is not the
|
|
312
|
-
// only channel). Omitted when the consumer owns the trailing slot.
|
|
313
|
-
<InputParts.Icon adornment="trailing" color={formCommonColors.error}>
|
|
314
|
-
<WarningCircleIcon aria-hidden />
|
|
315
|
-
</InputParts.Icon>
|
|
316
|
-
) : null}
|
|
317
|
-
</InputParts.Box>
|
|
318
|
-
);
|
|
375
|
+
)}
|
|
376
|
+
{showCopy ? (
|
|
377
|
+
<InputParts.Button
|
|
378
|
+
glyphRing
|
|
379
|
+
size={resolvedSize}
|
|
380
|
+
type="button"
|
|
381
|
+
disabled={disabled}
|
|
382
|
+
tabIndex={disabled ? -1 : 0}
|
|
383
|
+
aria-label={COPY_LABEL}
|
|
384
|
+
hitSlop={belowFloor ? pressTargetHitSlop(visualHeight) : undefined}
|
|
385
|
+
onPress={disabled ? undefined : activateCopy}
|
|
386
|
+
onKeyDown={
|
|
387
|
+
((e: KeyboardEvent) => {
|
|
388
|
+
if (disabled) return;
|
|
389
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
390
|
+
e.preventDefault();
|
|
391
|
+
e.stopPropagation();
|
|
392
|
+
activateCopy();
|
|
393
|
+
}
|
|
394
|
+
}) as unknown as () => void
|
|
395
|
+
}
|
|
396
|
+
>
|
|
397
|
+
<InputParts.Icon aria-hidden>
|
|
398
|
+
<ClipboardIcon />
|
|
399
|
+
</InputParts.Icon>
|
|
400
|
+
</InputParts.Button>
|
|
401
|
+
) : rightIcon ? (
|
|
402
|
+
<InputParts.Icon
|
|
403
|
+
adornment="trailing"
|
|
404
|
+
{...(rightIconInteractive ? undefined : focusTrigger)}
|
|
405
|
+
>
|
|
406
|
+
{rightIcon}
|
|
407
|
+
</InputParts.Icon>
|
|
408
|
+
) : hasError ? (
|
|
409
|
+
// M3 / Polaris / Spectrum: in-field error glyph (color is not the
|
|
410
|
+
// only channel). Omitted when the consumer owns the trailing slot.
|
|
411
|
+
<InputParts.Icon adornment="trailing" color={formCommonColors.error}>
|
|
412
|
+
<WarningCircleIcon aria-hidden />
|
|
413
|
+
</InputParts.Icon>
|
|
414
|
+
) : null}
|
|
415
|
+
</InputParts.Box>
|
|
416
|
+
);
|
|
417
|
+
};
|
|
319
418
|
|
|
320
419
|
if (!resolvedForm || !name) {
|
|
321
420
|
return (
|
|
@@ -790,11 +790,19 @@ export function MentionInput<
|
|
|
790
790
|
|
|
791
791
|
// ── Floating dropdown ──────────────────────────────────────
|
|
792
792
|
|
|
793
|
+
// Same clobber the FloatingPanel web branch carried (MPO-38): the complete
|
|
794
|
+
// `elevatedSurface` fragment ends in a web-only `style: { boxShadow }`, and a
|
|
795
|
+
// second `style` prop on one element replaces rather than merges — so
|
|
796
|
+
// spreading it after the positioning style dropped `position: fixed`. Fold
|
|
797
|
+
// its style in instead of letting the two compete.
|
|
798
|
+
const { style: elevatedSurfaceStyle, ...elevatedSurfaceProps } = panel.knobProps.elevatedSurface;
|
|
799
|
+
|
|
793
800
|
const floatingDropdown = panel.mounted ? (
|
|
794
801
|
<YStack
|
|
795
802
|
{...getFloatingProps({
|
|
796
803
|
ref: panel.refs.setFloating,
|
|
797
804
|
style: {
|
|
805
|
+
...elevatedSurfaceStyle,
|
|
798
806
|
position: panel.strategy,
|
|
799
807
|
top: panel.y ?? 0,
|
|
800
808
|
left: panel.x ?? 0,
|
|
@@ -810,7 +818,7 @@ export function MentionInput<
|
|
|
810
818
|
})}
|
|
811
819
|
outlineWidth={0}
|
|
812
820
|
pointerEvents={panel.visible ? "auto" : "none"}
|
|
813
|
-
{...
|
|
821
|
+
{...elevatedSurfaceProps}
|
|
814
822
|
borderRadius={panel.dropdownRadius}
|
|
815
823
|
padding={0}
|
|
816
824
|
maxHeight={suggestionsMaxHeight}
|
|
@@ -2,11 +2,17 @@ import { useForm } from "@tanstack/react-form";
|
|
|
2
2
|
import { act, fireEvent, waitFor } from "@testing-library/react";
|
|
3
3
|
import { describe, expect, it, vi } from "vitest";
|
|
4
4
|
import { TestProviders, renderWithProviders } from "@multiplatform.one/test-utils";
|
|
5
|
-
import {
|
|
5
|
+
import { getSize } from "@tamagui/get-token";
|
|
6
|
+
import { getVariableValue, type SizeTokens } from "tamagui";
|
|
7
|
+
import {
|
|
8
|
+
Preset,
|
|
9
|
+
resolveRadiusClass,
|
|
10
|
+
sizeRecipeForToken,
|
|
11
|
+
type BorderRadius,
|
|
12
|
+
} from "@multiplatform.one/theme";
|
|
6
13
|
import { Form } from "../../Form";
|
|
7
14
|
import { Button } from "../../Button";
|
|
8
|
-
import {
|
|
9
|
-
import { OTPInput, extractOtpCode } from "./index";
|
|
15
|
+
import { OTPInput, extractOtpCode, otpCellHeightPx } from "./index";
|
|
10
16
|
|
|
11
17
|
describe("OTPInput", () => {
|
|
12
18
|
describe("basic rendering", () => {
|
|
@@ -708,7 +714,10 @@ describe("OTPInput", () => {
|
|
|
708
714
|
}
|
|
709
715
|
|
|
710
716
|
it("cell measures height/2 at full and 0 at none, riding the token scale between", () => {
|
|
711
|
-
|
|
717
|
+
// the cell's own painted edge — `height={cellSize}` is the `$size` token,
|
|
718
|
+
// so the circle is that token halved. Reading the recipe here instead would
|
|
719
|
+
// pass on a desktop run and lie on a touch one (recipe $4 = 48 vs token 44).
|
|
720
|
+
const heightPx = getVariableValue(getSize("$4" as SizeTokens)) as number;
|
|
712
721
|
for (const stop of RADIUS_STOPS) {
|
|
713
722
|
const result = renderWithProviders(
|
|
714
723
|
<Preset overrides={{ borderRadius: stop }}>
|
|
@@ -727,5 +736,28 @@ describe("OTPInput", () => {
|
|
|
727
736
|
expect(paintedRadiusPx(cell as HTMLElement), `cell @ ${stop}`).toBe(expected);
|
|
728
737
|
}
|
|
729
738
|
});
|
|
739
|
+
|
|
740
|
+
it("halves the cell's painted token, not the touch-floored recipe height", () => {
|
|
741
|
+
for (const token of ["$2", "$3", "$4", "$6"] as const) {
|
|
742
|
+
const painted = getVariableValue(getSize(token as SizeTokens)) as number;
|
|
743
|
+
expect(otpCellHeightPx(token), `painted height @ ${token}`).toBe(painted);
|
|
744
|
+
// The recipe's touch column is a different number at EVERY step (44/44/48/68
|
|
745
|
+
// against 28/36/44/64). Halving it puts the radius past the cell's own edge,
|
|
746
|
+
// and the engine clamps it back — the browser clamp MPO-56 exists to kill.
|
|
747
|
+
const touchHeight = sizeRecipeForToken(token, { touch: true }).height;
|
|
748
|
+
expect(touchHeight, `touch recipe @ ${token}`).not.toBe(painted);
|
|
749
|
+
expect(
|
|
750
|
+
resolveRadiusClass("CIRCULAR-AT-FULL", "full", { heightPx: otpCellHeightPx(token) }),
|
|
751
|
+
`full @ ${token}`,
|
|
752
|
+
).toBe(painted / 2);
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
it("an explicit numeric cell size is its own painted height", () => {
|
|
757
|
+
expect(otpCellHeightPx(52)).toBe(52);
|
|
758
|
+
expect(
|
|
759
|
+
resolveRadiusClass("CIRCULAR-AT-FULL", "full", { heightPx: otpCellHeightPx(52) }),
|
|
760
|
+
).toBe(26);
|
|
761
|
+
});
|
|
730
762
|
});
|
|
731
763
|
});
|
|
@@ -15,7 +15,8 @@ import {
|
|
|
15
15
|
useState,
|
|
16
16
|
} from "react";
|
|
17
17
|
import { useProps, type SizeTokens, type TamaguiElement } from "tamagui";
|
|
18
|
-
import { Input, Text, XStack, isWeb, useDidFinishSSR } from "tamagui";
|
|
18
|
+
import { Input, Text, XStack, getVariableValue, isWeb, useDidFinishSSR } from "tamagui";
|
|
19
|
+
import { getSize } from "@tamagui/get-token";
|
|
19
20
|
import { Button } from "../../Button";
|
|
20
21
|
import { Field, FieldLayout } from "../../fieldLayout";
|
|
21
22
|
import {
|
|
@@ -132,9 +133,23 @@ function digitFontSize(cellSize: string | number | undefined): string {
|
|
|
132
133
|
return "$6";
|
|
133
134
|
}
|
|
134
135
|
|
|
135
|
-
|
|
136
|
+
/**
|
|
137
|
+
* The height the cell PAINTS, which is what CIRCULAR-AT-FULL halves (DG-RAD-02).
|
|
138
|
+
*
|
|
139
|
+
* The cell sets `height={cellSize}`, so its painted edge is the `$size` token —
|
|
140
|
+
* NOT the touch-floored recipe height. On a touch surface those disagree at every
|
|
141
|
+
* step (`sizeRecipeForToken("$4", { touch: true }).height` is 48 against a 44pt
|
|
142
|
+
* token, `$2` is 44 against 28), and a resolver fed the recipe height resolves 24
|
|
143
|
+
* on a 44pt cell: over the edge, so the engine clamps it back to 22. That clamp is
|
|
144
|
+
* exactly the defect MPO-56 killed, so the radius rides the painted token instead.
|
|
145
|
+
* Same source as the avatar square face (`avatarTokenPx`, components/Avatar).
|
|
146
|
+
*/
|
|
147
|
+
export function otpCellHeightPx(cellSize: string | number | undefined): number {
|
|
136
148
|
if (typeof cellSize === "number" && Number.isFinite(cellSize) && cellSize > 0) return cellSize;
|
|
137
|
-
|
|
149
|
+
const token = (cellSize ?? "$4") as SizeTokens;
|
|
150
|
+
const painted = getVariableValue(getSize(token));
|
|
151
|
+
if (typeof painted === "number" && Number.isFinite(painted) && painted > 0) return painted;
|
|
152
|
+
return getFieldHeight(token);
|
|
138
153
|
}
|
|
139
154
|
|
|
140
155
|
/**
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The touch arm of DG-RAD-02 for the OTP cell, in its own file because the
|
|
5
|
+
* surface probe is module-level state: `getFieldHeight` reads `isTouchSurface()`
|
|
6
|
+
* on every call, and a desktop run cannot tell the painted token from the recipe
|
|
7
|
+
* height (they are the same number there). Only on a touch surface do the two
|
|
8
|
+
* ladders separate — 28/36/44/64 painted against 44/44/48/68 recipe — which is
|
|
9
|
+
* where a resolver fed the wrong one hands the circle back to the engine clamp.
|
|
10
|
+
*/
|
|
11
|
+
vi.mock("@multiplatform.one/theme", async () => {
|
|
12
|
+
const actual = await vi.importActual<typeof import("@multiplatform.one/theme")>(
|
|
13
|
+
"@multiplatform.one/theme",
|
|
14
|
+
);
|
|
15
|
+
return { ...actual, isTouchSurface: () => true };
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
import { getSize } from "@tamagui/get-token";
|
|
19
|
+
import { resolveRadiusClass } from "@multiplatform.one/theme";
|
|
20
|
+
import { getVariableValue, type SizeTokens } from "tamagui";
|
|
21
|
+
import { getFieldHeight } from "../../shared/utils";
|
|
22
|
+
import { otpCellHeightPx } from "./index";
|
|
23
|
+
|
|
24
|
+
const tokens = ["$2", "$3", "$4", "$6"] as const;
|
|
25
|
+
|
|
26
|
+
describe("OTP cell geometry on a touch surface (DG-RAD-02)", () => {
|
|
27
|
+
it("the two ladders really do separate, or this file proves nothing", () => {
|
|
28
|
+
for (const token of tokens) {
|
|
29
|
+
const painted = getVariableValue(getSize(token as SizeTokens)) as number;
|
|
30
|
+
expect(getFieldHeight(token as SizeTokens), `recipe height @ ${token}`).not.toBe(painted);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("the cell resolves against the height it paints, not the recipe height", () => {
|
|
35
|
+
for (const token of tokens) {
|
|
36
|
+
const painted = getVariableValue(getSize(token as SizeTokens)) as number;
|
|
37
|
+
expect(otpCellHeightPx(token), `painted height @ ${token}`).toBe(painted);
|
|
38
|
+
expect(
|
|
39
|
+
resolveRadiusClass("CIRCULAR-AT-FULL", "full", { heightPx: otpCellHeightPx(token) }),
|
|
40
|
+
`full @ ${token}`,
|
|
41
|
+
).toBe(painted / 2);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -347,6 +347,7 @@ export function PhoneInput<
|
|
|
347
347
|
<XStack
|
|
348
348
|
alignItems="center"
|
|
349
349
|
gap="$1.5"
|
|
350
|
+
// size-recipe-escape: zero-reset — the country trigger rides inside Input.Box, which owns the inset (LC-71)
|
|
350
351
|
paddingHorizontal={0}
|
|
351
352
|
height="100%"
|
|
352
353
|
cursor={disabled ? "not-allowed" : "pointer"}
|
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
* second focusable and rings its own glyph (chip-dismiss, LC-16).
|
|
8
8
|
*
|
|
9
9
|
* Clear is a View sized from the size-recipe square (same geometry as
|
|
10
|
-
* Input.Button).
|
|
11
|
-
* chip-dismiss ring classes
|
|
12
|
-
*
|
|
10
|
+
* Input.Button). It was hand-rolled because TButton swallows className, so
|
|
11
|
+
* the chip-dismiss ring classes could not ride Input.Button. `glyphRing`
|
|
12
|
+
* now exists (InputParts/index.tsx, MPO-52) and renders this exact pair, so
|
|
13
|
+
* this block can collapse onto `<Input.Button glyphRing>` — in SearchInput's
|
|
14
|
+
* own lane, not this one.
|
|
13
15
|
*/
|
|
14
16
|
import { MagnifyingGlassIcon, XIcon } from "@phosphor-icons/react";
|
|
15
17
|
import {
|
package/src/shared/utils.spec.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { BorderRadius } from "@multiplatform.one/theme";
|
|
3
|
+
import { borderRadiusMap, resolveRadiusClass } from "@multiplatform.one/theme";
|
|
2
4
|
import {
|
|
3
5
|
clampRadiusForControl,
|
|
4
6
|
clampRadiusForLargeComponent,
|
|
@@ -180,6 +182,50 @@ describe("clampRadiusInStateProps", () => {
|
|
|
180
182
|
expect(result?.[key]?.borderRadius).toBe(THUMB_H / 2);
|
|
181
183
|
}
|
|
182
184
|
});
|
|
185
|
+
|
|
186
|
+
// LC-16 ONE-IMPLEMENTATION. The BINARY row is doctrine-GENERATED
|
|
187
|
+
// (radiusClassTable.generated.ts, from docs/design-guidelines.md §13).
|
|
188
|
+
// A state bag must resolve through THAT table, not through a copy of it
|
|
189
|
+
// living in forms — otherwise a DG-RAD edit reaches the base radius and
|
|
190
|
+
// silently misses every hover/press/focus bag. Driven off borderRadiusMap
|
|
191
|
+
// so a stop added to the doctrine is covered without touching this test.
|
|
192
|
+
const STOPS = Object.keys(borderRadiusMap) as BorderRadius[];
|
|
193
|
+
|
|
194
|
+
it.each(STOPS)("agrees with resolveRadiusClass('BINARY') at the %s stop", (stop) => {
|
|
195
|
+
for (const heightPx of [THUMB_H, TRACK_H, 21]) {
|
|
196
|
+
const expected = resolveRadiusClass("BINARY", stop, { heightPx });
|
|
197
|
+
for (const spelling of [stop, borderRadiusMap[stop]]) {
|
|
198
|
+
const result = clampRadiusInStateProps(
|
|
199
|
+
{ borderRadius: spelling },
|
|
200
|
+
{ radiusClass: "BINARY", heightPx },
|
|
201
|
+
);
|
|
202
|
+
expect(result?.borderRadius).toBe(expected);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("reads the token/stop mapping off borderRadiusMap rather than a hand-kept copy", () => {
|
|
208
|
+
for (const stop of STOPS) {
|
|
209
|
+
const byName = clampRadiusInStateProps(
|
|
210
|
+
{ borderRadius: stop },
|
|
211
|
+
{ radiusClass: "BINARY", heightPx: THUMB_H },
|
|
212
|
+
);
|
|
213
|
+
const byToken = clampRadiusInStateProps(
|
|
214
|
+
{ borderRadius: borderRadiusMap[stop] },
|
|
215
|
+
{ radiusClass: "BINARY", heightPx: THUMB_H },
|
|
216
|
+
);
|
|
217
|
+
expect(byName?.borderRadius).toBe(byToken?.borderRadius);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("refuses a BINARY bag with no height instead of guessing one", () => {
|
|
222
|
+
expect(() =>
|
|
223
|
+
clampRadiusInStateProps({ borderRadius: "$12" }, { radiusClass: "BINARY" }),
|
|
224
|
+
).toThrow(/heightPx/);
|
|
225
|
+
expect(() =>
|
|
226
|
+
clampRadiusInStateProps({ borderRadius: "$0" }, { radiusClass: "BINARY", heightPx: 0 }),
|
|
227
|
+
).toThrow(/heightPx/);
|
|
228
|
+
});
|
|
183
229
|
});
|
|
184
230
|
});
|
|
185
231
|
|
package/src/shared/utils.ts
CHANGED
|
@@ -229,20 +229,25 @@ export interface ClampRadiusInStatePropsOptions {
|
|
|
229
229
|
|
|
230
230
|
const STATE_STYLE_KEYS = ["hoverStyle", "pressStyle", "focusStyle", "focusVisibleStyle"] as const;
|
|
231
231
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
};
|
|
232
|
+
/**
|
|
233
|
+
* Token/stop-name lookup DERIVED from `borderRadiusMap` — the same single
|
|
234
|
+
* source `otpRadiusStop` reads a few lines up. A hand-kept second copy of the
|
|
235
|
+
* token scale is how this file would end up with two answers for one stop, so
|
|
236
|
+
* there is only one (LC-16 ONE-IMPLEMENTATION).
|
|
237
|
+
*/
|
|
238
|
+
const RADIUS_TOKEN_TO_STOP = Object.fromEntries(
|
|
239
|
+
(Object.keys(borderRadiusMap) as BorderRadius[]).flatMap((stop) => [
|
|
240
|
+
[stop, stop],
|
|
241
|
+
[borderRadiusMap[stop], stop],
|
|
242
|
+
]),
|
|
243
|
+
) as Record<string, BorderRadius>;
|
|
245
244
|
|
|
245
|
+
/**
|
|
246
|
+
* A state bag carries whatever the recipe put in it — a stop name, a radius
|
|
247
|
+
* token, or a raw px value. An unrecognised value resolves to `full`, which
|
|
248
|
+
* for BINARY is h/2: the safe direction, because the class has no
|
|
249
|
+
* intermediate value to fall back to.
|
|
250
|
+
*/
|
|
246
251
|
function radiusValueToStop(value: unknown): BorderRadius {
|
|
247
252
|
if (value == null || value === 0) return "none";
|
|
248
253
|
const key = String(value).trim();
|
|
@@ -251,8 +256,18 @@ function radiusValueToStop(value: unknown): BorderRadius {
|
|
|
251
256
|
}
|
|
252
257
|
|
|
253
258
|
/**
|
|
254
|
-
* BINARY class contract (DG-RAD-03
|
|
255
|
-
*
|
|
259
|
+
* BINARY class contract (DG-RAD-03): none is square, every other stop is h/2,
|
|
260
|
+
* and tracks square with their thumbs (§R2).
|
|
261
|
+
*
|
|
262
|
+
* The table is NOT restated here. It is read from the doctrine-GENERATED
|
|
263
|
+
* resolver in `@multiplatform.one/theme`, so a DG-RAD edit reaches per-state
|
|
264
|
+
* override bags the same way it reaches the base radius — a local copy of the
|
|
265
|
+
* BINARY row would keep answering with the old doctrine (LC-16).
|
|
266
|
+
*
|
|
267
|
+
* The `heightPx` guard is the CALLER's contract rather than the table's: the
|
|
268
|
+
* doctrine only needs a height for the h/2 stops, but a part that declares
|
|
269
|
+
* BINARY without its height would resolve at `none` and throw at every other
|
|
270
|
+
* stop. Refusing up front beats working in one state and throwing in the next.
|
|
256
271
|
*/
|
|
257
272
|
function resolveBinaryStateRadius(value: unknown, heightPx: number | undefined): number {
|
|
258
273
|
if (heightPx === undefined || heightPx <= 0) {
|
|
@@ -260,7 +275,7 @@ function resolveBinaryStateRadius(value: unknown, heightPx: number | undefined):
|
|
|
260
275
|
"clampRadiusInStateProps: BINARY resolves to 0 or h/2 and needs heightPx — the part's own painted height (shorter edge)",
|
|
261
276
|
);
|
|
262
277
|
}
|
|
263
|
-
return radiusValueToStop(value)
|
|
278
|
+
return resolveRadiusClass("BINARY", radiusValueToStop(value), { heightPx });
|
|
264
279
|
}
|
|
265
280
|
|
|
266
281
|
function clampOneStateRadius(
|