@multiplatform.one/forms 6.4.1 → 6.4.2
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/package.json +7 -7
- package/src/Wheel/index.tsx +1 -1
- package/src/fields/DatePicker/DatetimePicker.tsx +26 -20
- package/src/fields/DatePicker/fieldValueSync.tsx +34 -0
- package/src/fields/DatePicker/index.tsx +2 -43
- package/src/fields/RadioGroup/index.tsx +5 -2
- package/src/fields/Rating/index.tsx +10 -2
- package/src/shared/colorRamps.ts +16 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@multiplatform.one/forms",
|
|
3
|
-
"version": "6.4.
|
|
3
|
+
"version": "6.4.2",
|
|
4
4
|
"description": "Cross-platform form components with URL state management",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cross-platform",
|
|
@@ -69,9 +69,9 @@
|
|
|
69
69
|
"perfect-freehand": "^1.2.3",
|
|
70
70
|
"react-native-svg": "15.15.4",
|
|
71
71
|
"tamagui": "2.0.0-rc.41",
|
|
72
|
-
"@multiplatform.one/router": "6.4.
|
|
73
|
-
"@multiplatform.one/
|
|
74
|
-
"@multiplatform.one/
|
|
72
|
+
"@multiplatform.one/router": "6.4.2",
|
|
73
|
+
"@multiplatform.one/theme": "6.4.2",
|
|
74
|
+
"@multiplatform.one/table-primitives": "6.4.2"
|
|
75
75
|
},
|
|
76
76
|
"devDependencies": {
|
|
77
77
|
"@tamagui/build": "2.0.0-rc.41",
|
|
@@ -92,9 +92,9 @@
|
|
|
92
92
|
"react-i18next": "^16.6.6",
|
|
93
93
|
"react-native-web": "^0.21.2",
|
|
94
94
|
"vitest": "^4.1.5",
|
|
95
|
-
"@multiplatform.one/
|
|
96
|
-
"@multiplatform.one/
|
|
97
|
-
"@multiplatform.one/
|
|
95
|
+
"@multiplatform.one/storybook": "6.4.2",
|
|
96
|
+
"@multiplatform.one/test-utils": "6.4.2",
|
|
97
|
+
"@multiplatform.one/config": "6.4.2"
|
|
98
98
|
},
|
|
99
99
|
"peerDependencies": {
|
|
100
100
|
"expo-maps": "*",
|
package/src/Wheel/index.tsx
CHANGED
|
@@ -596,7 +596,7 @@ export function Wheel<T = string | number>({
|
|
|
596
596
|
<WheelContainer
|
|
597
597
|
height={containerHeight}
|
|
598
598
|
opacity={disabled ? 0.5 : 1}
|
|
599
|
-
{...knobProps.
|
|
599
|
+
{...knobProps.containerRadius}
|
|
600
600
|
data-testid="wheel"
|
|
601
601
|
>
|
|
602
602
|
{/* Selection indicator */}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from "@multiplatform.one/theme";
|
|
8
8
|
import { type ComponentProps, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
9
9
|
import { coerceToDate } from "./utils";
|
|
10
|
+
import { DatePickerFieldValueSync } from "./fieldValueSync";
|
|
10
11
|
import { Calendar, X } from "@tamagui/lucide-icons-2";
|
|
11
12
|
import { AnimatePresence, Text, View, useProps, type Popover } from "tamagui";
|
|
12
13
|
|
|
@@ -794,26 +795,31 @@ export function DatetimePicker<
|
|
|
794
795
|
const resolvedError = getFieldError(field, errorProp);
|
|
795
796
|
|
|
796
797
|
return (
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
798
|
+
<>
|
|
799
|
+
{/* Form branch displays from `selectedDatetime` — keep it synced
|
|
800
|
+
to the live field value (late seeds / programmatic updates). */}
|
|
801
|
+
<DatePickerFieldValueSync value={field.state.value} onSync={setSelectedDatetime} />
|
|
802
|
+
<FieldLayout
|
|
803
|
+
id={id}
|
|
804
|
+
label={label}
|
|
805
|
+
labelProps={labelProps}
|
|
806
|
+
error={resolvedError}
|
|
807
|
+
helperText={helperText}
|
|
808
|
+
required={required}
|
|
809
|
+
size={size}
|
|
810
|
+
knobProps={knobProps}
|
|
811
|
+
// LC-40: see standalone branch — FieldLayout carries dimWhole.
|
|
812
|
+
disabled={disabled}
|
|
813
|
+
>
|
|
814
|
+
<InputParts size={size || knobProps.sizeToken}>
|
|
815
|
+
{renderPopover(
|
|
816
|
+
!!resolvedError,
|
|
817
|
+
mergeFieldHandler(field, "handleBlur", _onBlur),
|
|
818
|
+
mergeFieldHandler(field, "handleChange"),
|
|
819
|
+
)}
|
|
820
|
+
</InputParts>
|
|
821
|
+
</FieldLayout>
|
|
822
|
+
</>
|
|
817
823
|
);
|
|
818
824
|
}}
|
|
819
825
|
</Field>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Dispatch, SetStateAction } from "react";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
import { coerceToDate } from "./utils";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Mirrors the TanStack field value into a picker's display state
|
|
7
|
+
* (`selectedDate` / `selectedDatetime`). Without it the form branch only
|
|
8
|
+
* ever showed the mount-time value, so a value applied AFTER first paint
|
|
9
|
+
* (create-mode engine seeding, form.setFieldValue, live server merge) never
|
|
10
|
+
* rendered. Display-only: never calls onChange/onValueChange (LC-14 — a
|
|
11
|
+
* programmatic value application is not a user edit). Rendered inside the
|
|
12
|
+
* Field render prop, which re-renders on field state changes (hooks cannot
|
|
13
|
+
* live in the render prop itself — same extraction as Stepper/PhoneInput
|
|
14
|
+
* renderers).
|
|
15
|
+
*/
|
|
16
|
+
export function DatePickerFieldValueSync({
|
|
17
|
+
value,
|
|
18
|
+
onSync,
|
|
19
|
+
}: {
|
|
20
|
+
value: unknown;
|
|
21
|
+
onSync: Dispatch<SetStateAction<Date | null>>;
|
|
22
|
+
}) {
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const next = coerceToDate(value);
|
|
25
|
+
// Keep the previous Date identity when the instant is unchanged so the
|
|
26
|
+
// sync never churns downstream effects (close-on-select watches state).
|
|
27
|
+
onSync((prev) =>
|
|
28
|
+
prev === next || (prev !== null && next !== null && prev.getTime() === next.getTime())
|
|
29
|
+
? prev
|
|
30
|
+
: next,
|
|
31
|
+
);
|
|
32
|
+
}, [value, onSync]);
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
import type { DeepKeys, DeepValue } from "@tanstack/form-core";
|
|
2
2
|
import { useAccentOnSurface, useResolvedKnobs } from "@multiplatform.one/theme";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
type Dispatch,
|
|
6
|
-
type SetStateAction,
|
|
7
|
-
useCallback,
|
|
8
|
-
useEffect,
|
|
9
|
-
useRef,
|
|
10
|
-
useState,
|
|
11
|
-
} from "react";
|
|
3
|
+
import { type ComponentProps, useCallback, useEffect, useRef, useState } from "react";
|
|
4
|
+
import { DatePickerFieldValueSync } from "./fieldValueSync";
|
|
12
5
|
import { coerceToDate } from "./utils";
|
|
13
6
|
import { AnimatePresence, type Popover, View, useProps } from "tamagui";
|
|
14
7
|
|
|
@@ -254,40 +247,6 @@ function CalendarBody({
|
|
|
254
247
|
);
|
|
255
248
|
}
|
|
256
249
|
|
|
257
|
-
// ---------------------------------------------------------------------------
|
|
258
|
-
// FieldValueSync — form-integrated display sync
|
|
259
|
-
// ---------------------------------------------------------------------------
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* Mirrors the TanStack field value into the picker's display state
|
|
263
|
-
* (`selectedDate`). Without it the form branch only ever showed the
|
|
264
|
-
* mount-time value, so a value applied AFTER first paint (create-mode
|
|
265
|
-
* engine seeding, form.setFieldValue, live server merge) never rendered.
|
|
266
|
-
* Display-only: never calls onChange/onValueChange (LC-14 — a programmatic
|
|
267
|
-
* value application is not a user edit). Rendered inside the Field render
|
|
268
|
-
* prop, which re-renders on field state changes (hooks cannot live in the
|
|
269
|
-
* render prop itself — same extraction as Stepper/PhoneInput renderers).
|
|
270
|
-
*/
|
|
271
|
-
function DatePickerFieldValueSync({
|
|
272
|
-
value,
|
|
273
|
-
onSync,
|
|
274
|
-
}: {
|
|
275
|
-
value: unknown;
|
|
276
|
-
onSync: Dispatch<SetStateAction<Date | null>>;
|
|
277
|
-
}) {
|
|
278
|
-
useEffect(() => {
|
|
279
|
-
const next = coerceToDate(value);
|
|
280
|
-
// Keep the previous Date identity when the instant is unchanged so the
|
|
281
|
-
// sync never churns downstream effects (close-on-select watches state).
|
|
282
|
-
onSync((prev) =>
|
|
283
|
-
prev === next || (prev !== null && next !== null && prev.getTime() === next.getTime())
|
|
284
|
-
? prev
|
|
285
|
-
: next,
|
|
286
|
-
);
|
|
287
|
-
}, [value, onSync]);
|
|
288
|
-
return null;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
250
|
// ---------------------------------------------------------------------------
|
|
292
251
|
// DatePicker — main exported component
|
|
293
252
|
// ---------------------------------------------------------------------------
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
import { getSize } from "@tamagui/get-token";
|
|
17
17
|
import { Skeleton } from "../../Skeleton";
|
|
18
18
|
import { Field, FieldLayout } from "../../fieldLayout";
|
|
19
|
-
import { formControlColors } from "../../shared/colorRamps";
|
|
19
|
+
import { formControlColors, formSelectedColors } from "../../shared/colorRamps";
|
|
20
20
|
import {
|
|
21
21
|
getElevationWrapperProps,
|
|
22
22
|
getFieldError,
|
|
@@ -284,7 +284,10 @@ export function RadioGroup({
|
|
|
284
284
|
>
|
|
285
285
|
<RadioIndicatorFrame
|
|
286
286
|
borderRadius={indicatorRadius}
|
|
287
|
-
|
|
287
|
+
// LC-05: the dot is the selection mark, so it paints the active
|
|
288
|
+
// accent — `textAccentColor` is the LABEL ink tier ($color/$color11)
|
|
289
|
+
// and made the checked radio read as ink, not as chosen (D-01).
|
|
290
|
+
backgroundColor={formSelectedColors.mark}
|
|
288
291
|
{...(knobProps.transition ? { animation: knobProps.transition } : undefined)}
|
|
289
292
|
/>
|
|
290
293
|
</RadioGlyph>
|
|
@@ -8,6 +8,7 @@ import { getVariable, isWeb, styled, useTheme, View, XStack } from "tamagui";
|
|
|
8
8
|
import Svg, { Path } from "react-native-svg";
|
|
9
9
|
import { Skeleton } from "../../Skeleton";
|
|
10
10
|
import { Field, FieldLayout } from "../../fieldLayout";
|
|
11
|
+
import { formSelectedColors } from "../../shared/colorRamps";
|
|
11
12
|
import { t } from "../../shared/t";
|
|
12
13
|
import {
|
|
13
14
|
getFieldError,
|
|
@@ -304,9 +305,16 @@ export function Rating({
|
|
|
304
305
|
const resolvedValidators = useResolvedValidators(required, validators, label, name);
|
|
305
306
|
const filledStars = useMemo(() => Math.round(value * maxStars), [value, maxStars]);
|
|
306
307
|
|
|
307
|
-
// Resolve color tokens to actual CSS values for Phosphor icons
|
|
308
|
+
// Resolve color tokens to actual CSS values for Phosphor icons.
|
|
309
|
+
// LC-05: the FILLED star is the selection mark, so it resolves the shared
|
|
310
|
+
// accent mark token — it painted `$color10`, a muted neutral that made a
|
|
311
|
+
// 3-star rating read as disabled chrome rather than as a chosen value
|
|
312
|
+
// (D-01). The empty star stays neutral: it marks nothing.
|
|
308
313
|
const theme = useTheme();
|
|
309
|
-
const
|
|
314
|
+
const markToken = formSelectedColors.mark.slice(1);
|
|
315
|
+
const filledColor = getVariable(
|
|
316
|
+
(theme as any)[markToken]?.get("web") ?? theme.color10?.get("web") ?? theme.color?.get("web"),
|
|
317
|
+
);
|
|
310
318
|
const emptyColor = getVariable(theme.color7?.get("web") ?? theme.color?.get("web"));
|
|
311
319
|
|
|
312
320
|
const handleStarPress = (
|
package/src/shared/colorRamps.ts
CHANGED
|
@@ -51,13 +51,28 @@ export const formControlColors = {
|
|
|
51
51
|
},
|
|
52
52
|
} as const;
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Selected/checked MARK channel (LC-05 ONE-EMPHASIS).
|
|
56
|
+
*
|
|
57
|
+
* Every choice control in the field family marks its selection with the ACTIVE
|
|
58
|
+
* ACCENT — the radio dot, the filled rating star and the checked Switch track
|
|
59
|
+
* all resolve here — so a user learns one meaning for the accent instead of
|
|
60
|
+
* decoding a per-control dialect. Ink marks selection nowhere (D-01).
|
|
61
|
+
* Marks are non-text chrome, so the floor they must clear is the 3:1
|
|
62
|
+
* graphical-object tier, not the 4.5:1 text tier; text placed ON this fill
|
|
63
|
+
* takes `useReadableTextOn` rather than an assumed anchor.
|
|
64
|
+
*/
|
|
65
|
+
export const formSelectedColors = {
|
|
66
|
+
mark: "$accentBackground",
|
|
67
|
+
} as const;
|
|
68
|
+
|
|
54
69
|
// Bento switch polarity: accent track when on, light thumb. Separate from
|
|
55
70
|
// formControlColors.thumb, which Slider still uses with inverted polarity.
|
|
56
71
|
// Checked state uses the accent solid so on/off reads at a glance and
|
|
57
72
|
// matches the accent CTAs on the same screen (SB-D-19).
|
|
58
73
|
export const formSwitchColors = {
|
|
59
74
|
track: {
|
|
60
|
-
on:
|
|
75
|
+
on: formSelectedColors.mark,
|
|
61
76
|
off: "$color5",
|
|
62
77
|
},
|
|
63
78
|
thumb: "$color1",
|