@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@multiplatform.one/forms",
3
- "version": "6.4.1",
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.1",
73
- "@multiplatform.one/table-primitives": "6.4.1",
74
- "@multiplatform.one/theme": "6.4.1"
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/config": "6.4.1",
96
- "@multiplatform.one/storybook": "6.4.1",
97
- "@multiplatform.one/test-utils": "6.4.1"
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": "*",
@@ -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.borderRadius}
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
- <FieldLayout
798
- id={id}
799
- label={label}
800
- labelProps={labelProps}
801
- error={resolvedError}
802
- helperText={helperText}
803
- required={required}
804
- size={size}
805
- knobProps={knobProps}
806
- // LC-40: see standalone branch — FieldLayout carries dimWhole.
807
- disabled={disabled}
808
- >
809
- <InputParts size={size || knobProps.sizeToken}>
810
- {renderPopover(
811
- !!resolvedError,
812
- mergeFieldHandler(field, "handleBlur", _onBlur),
813
- mergeFieldHandler(field, "handleChange"),
814
- )}
815
- </InputParts>
816
- </FieldLayout>
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
- type ComponentProps,
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
- backgroundColor={knobProps.textAccentColor}
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 filledColor = getVariable(theme.color10?.get("web") ?? theme.color?.get("web"));
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 = (
@@ -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: "$accentBackground",
75
+ on: formSelectedColors.mark,
61
76
  off: "$color5",
62
77
  },
63
78
  thumb: "$color1",