@multiplatform.one/frappe-ui 6.4.3 → 6.6.0

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.
Files changed (260) hide show
  1. package/dist/cjs/FormSidebar.cjs +2 -2
  2. package/dist/cjs/FormSidebar.native.js +2 -2
  3. package/dist/cjs/FormSidebar.native.js.map +1 -1
  4. package/dist/cjs/FrappeAssignment.cjs +8 -4
  5. package/dist/cjs/FrappeAssignment.native.js +8 -4
  6. package/dist/cjs/FrappeAssignment.native.js.map +1 -1
  7. package/dist/cjs/FrappeCalendar.cjs +2 -8
  8. package/dist/cjs/FrappeCalendar.native.js +2 -8
  9. package/dist/cjs/FrappeCalendar.native.js.map +1 -1
  10. package/dist/cjs/FrappeConnectionIndicator.cjs +1 -0
  11. package/dist/cjs/FrappeConnectionIndicator.native.js +1 -0
  12. package/dist/cjs/FrappeConnectionIndicator.native.js.map +1 -1
  13. package/dist/cjs/FrappeDashboard.cjs +2 -2
  14. package/dist/cjs/FrappeDashboard.native.js +2 -2
  15. package/dist/cjs/FrappeDashboard.native.js.map +1 -1
  16. package/dist/cjs/FrappeGantt.cjs +2 -8
  17. package/dist/cjs/FrappeGantt.native.js +3 -9
  18. package/dist/cjs/FrappeGantt.native.js.map +1 -1
  19. package/dist/cjs/FrappeKanban.cjs +32 -21
  20. package/dist/cjs/FrappeKanban.native.js +36 -23
  21. package/dist/cjs/FrappeKanban.native.js.map +1 -1
  22. package/dist/cjs/FrappeLikedBy.cjs +2 -1
  23. package/dist/cjs/FrappeLikedBy.native.js +2 -1
  24. package/dist/cjs/FrappeLikedBy.native.js.map +1 -1
  25. package/dist/cjs/FrappeList.cjs +0 -1
  26. package/dist/cjs/FrappeList.native.js +10 -2
  27. package/dist/cjs/FrappeList.native.js.map +1 -1
  28. package/dist/cjs/FrappeListView.cjs +9 -7
  29. package/dist/cjs/FrappeListView.native.js +9 -7
  30. package/dist/cjs/FrappeListView.native.js.map +1 -1
  31. package/dist/cjs/FrappeTable.cjs +4 -0
  32. package/dist/cjs/FrappeTable.native.js +10 -0
  33. package/dist/cjs/FrappeTable.native.js.map +1 -1
  34. package/dist/cjs/fields/Date/date.native.js +11 -48
  35. package/dist/cjs/fields/Date/date.native.js.map +1 -1
  36. package/dist/cjs/fields/Date/index.cjs +2 -13
  37. package/dist/cjs/fields/Date/index.native.js +2 -15
  38. package/dist/cjs/fields/Date/index.native.js.map +1 -1
  39. package/dist/cjs/fields/Date/wire.cjs +49 -0
  40. package/dist/cjs/fields/Date/wire.native.js +55 -0
  41. package/dist/cjs/fields/Date/wire.native.js.map +1 -0
  42. package/dist/cjs/fields/ReadOnly/index.cjs +8 -36
  43. package/dist/cjs/fields/ReadOnly/index.native.js +12 -36
  44. package/dist/cjs/fields/ReadOnly/index.native.js.map +1 -1
  45. package/dist/cjs/fields/TableMultiSelect.cjs +7 -15
  46. package/dist/cjs/fields/TableMultiSelect.native.js +7 -15
  47. package/dist/cjs/fields/TableMultiSelect.native.js.map +1 -1
  48. package/dist/cjs/fields/TextEditor/index.cjs +11 -6
  49. package/dist/cjs/schemaForm/index.cjs +10 -2
  50. package/dist/cjs/schemaForm/index.native.js +10 -2
  51. package/dist/cjs/schemaForm/index.native.js.map +1 -1
  52. package/dist/esm/FormSidebar.mjs +2 -2
  53. package/dist/esm/FormSidebar.mjs.map +1 -1
  54. package/dist/esm/FormSidebar.native.js +2 -2
  55. package/dist/esm/FormSidebar.native.js.map +1 -1
  56. package/dist/esm/FrappeAssignment.mjs +6 -2
  57. package/dist/esm/FrappeAssignment.mjs.map +1 -1
  58. package/dist/esm/FrappeAssignment.native.js +6 -2
  59. package/dist/esm/FrappeAssignment.native.js.map +1 -1
  60. package/dist/esm/FrappeCalendar.mjs +3 -9
  61. package/dist/esm/FrappeCalendar.mjs.map +1 -1
  62. package/dist/esm/FrappeCalendar.native.js +3 -9
  63. package/dist/esm/FrappeCalendar.native.js.map +1 -1
  64. package/dist/esm/FrappeConnectionIndicator.mjs +2 -1
  65. package/dist/esm/FrappeConnectionIndicator.mjs.map +1 -1
  66. package/dist/esm/FrappeConnectionIndicator.native.js +2 -1
  67. package/dist/esm/FrappeConnectionIndicator.native.js.map +1 -1
  68. package/dist/esm/FrappeDashboard.mjs +3 -3
  69. package/dist/esm/FrappeDashboard.mjs.map +1 -1
  70. package/dist/esm/FrappeDashboard.native.js +3 -3
  71. package/dist/esm/FrappeDashboard.native.js.map +1 -1
  72. package/dist/esm/FrappeGantt.mjs +3 -9
  73. package/dist/esm/FrappeGantt.mjs.map +1 -1
  74. package/dist/esm/FrappeGantt.native.js +4 -10
  75. package/dist/esm/FrappeGantt.native.js.map +1 -1
  76. package/dist/esm/FrappeKanban.mjs +33 -22
  77. package/dist/esm/FrappeKanban.mjs.map +1 -1
  78. package/dist/esm/FrappeKanban.native.js +37 -24
  79. package/dist/esm/FrappeKanban.native.js.map +1 -1
  80. package/dist/esm/FrappeLikedBy.mjs +2 -1
  81. package/dist/esm/FrappeLikedBy.mjs.map +1 -1
  82. package/dist/esm/FrappeLikedBy.native.js +2 -1
  83. package/dist/esm/FrappeLikedBy.native.js.map +1 -1
  84. package/dist/esm/FrappeList.mjs +0 -1
  85. package/dist/esm/FrappeList.mjs.map +1 -1
  86. package/dist/esm/FrappeList.native.js +10 -2
  87. package/dist/esm/FrappeList.native.js.map +1 -1
  88. package/dist/esm/FrappeListView.mjs +11 -9
  89. package/dist/esm/FrappeListView.mjs.map +1 -1
  90. package/dist/esm/FrappeListView.native.js +11 -9
  91. package/dist/esm/FrappeListView.native.js.map +1 -1
  92. package/dist/esm/FrappeTable.mjs +4 -0
  93. package/dist/esm/FrappeTable.mjs.map +1 -1
  94. package/dist/esm/FrappeTable.native.js +10 -0
  95. package/dist/esm/FrappeTable.native.js.map +1 -1
  96. package/dist/esm/fields/Date/date.native.js +12 -49
  97. package/dist/esm/fields/Date/date.native.js.map +1 -1
  98. package/dist/esm/fields/Date/index.mjs +1 -12
  99. package/dist/esm/fields/Date/index.mjs.map +1 -1
  100. package/dist/esm/fields/Date/index.native.js +1 -14
  101. package/dist/esm/fields/Date/index.native.js.map +1 -1
  102. package/dist/esm/fields/Date/wire.mjs +23 -0
  103. package/dist/esm/fields/Date/wire.mjs.map +1 -0
  104. package/dist/esm/fields/Date/wire.native.js +26 -0
  105. package/dist/esm/fields/Date/wire.native.js.map +1 -0
  106. package/dist/esm/fields/ReadOnly/index.mjs +9 -37
  107. package/dist/esm/fields/ReadOnly/index.mjs.map +1 -1
  108. package/dist/esm/fields/ReadOnly/index.native.js +13 -37
  109. package/dist/esm/fields/ReadOnly/index.native.js.map +1 -1
  110. package/dist/esm/fields/TableMultiSelect.mjs +4 -12
  111. package/dist/esm/fields/TableMultiSelect.mjs.map +1 -1
  112. package/dist/esm/fields/TableMultiSelect.native.js +4 -12
  113. package/dist/esm/fields/TableMultiSelect.native.js.map +1 -1
  114. package/dist/esm/fields/TextEditor/index.mjs +11 -6
  115. package/dist/esm/fields/TextEditor/index.mjs.map +1 -1
  116. package/dist/esm/schemaForm/index.mjs +10 -2
  117. package/dist/esm/schemaForm/index.mjs.map +1 -1
  118. package/dist/esm/schemaForm/index.native.js +10 -2
  119. package/dist/esm/schemaForm/index.native.js.map +1 -1
  120. package/dist/jsx/FormSidebar.mjs +2 -2
  121. package/dist/jsx/FormSidebar.mjs.map +1 -1
  122. package/dist/jsx/FormSidebar.native.js +2 -2
  123. package/dist/jsx/FormSidebar.native.js.map +1 -1
  124. package/dist/jsx/FrappeAssignment.mjs +6 -2
  125. package/dist/jsx/FrappeAssignment.mjs.map +1 -1
  126. package/dist/jsx/FrappeAssignment.native.js +8 -4
  127. package/dist/jsx/FrappeAssignment.native.js.map +1 -1
  128. package/dist/jsx/FrappeCalendar.mjs +3 -9
  129. package/dist/jsx/FrappeCalendar.mjs.map +1 -1
  130. package/dist/jsx/FrappeCalendar.native.js +2 -8
  131. package/dist/jsx/FrappeCalendar.native.js.map +1 -1
  132. package/dist/jsx/FrappeConnectionIndicator.mjs +2 -1
  133. package/dist/jsx/FrappeConnectionIndicator.mjs.map +1 -1
  134. package/dist/jsx/FrappeConnectionIndicator.native.js +1 -0
  135. package/dist/jsx/FrappeConnectionIndicator.native.js.map +1 -1
  136. package/dist/jsx/FrappeDashboard.mjs +3 -3
  137. package/dist/jsx/FrappeDashboard.mjs.map +1 -1
  138. package/dist/jsx/FrappeDashboard.native.js +2 -2
  139. package/dist/jsx/FrappeDashboard.native.js.map +1 -1
  140. package/dist/jsx/FrappeGantt.mjs +3 -9
  141. package/dist/jsx/FrappeGantt.mjs.map +1 -1
  142. package/dist/jsx/FrappeGantt.native.js +3 -9
  143. package/dist/jsx/FrappeGantt.native.js.map +1 -1
  144. package/dist/jsx/FrappeKanban.mjs +33 -22
  145. package/dist/jsx/FrappeKanban.mjs.map +1 -1
  146. package/dist/jsx/FrappeKanban.native.js +36 -23
  147. package/dist/jsx/FrappeKanban.native.js.map +1 -1
  148. package/dist/jsx/FrappeLikedBy.mjs +2 -1
  149. package/dist/jsx/FrappeLikedBy.mjs.map +1 -1
  150. package/dist/jsx/FrappeLikedBy.native.js +2 -1
  151. package/dist/jsx/FrappeLikedBy.native.js.map +1 -1
  152. package/dist/jsx/FrappeList.mjs +0 -1
  153. package/dist/jsx/FrappeList.mjs.map +1 -1
  154. package/dist/jsx/FrappeList.native.js +10 -2
  155. package/dist/jsx/FrappeList.native.js.map +1 -1
  156. package/dist/jsx/FrappeListView.mjs +11 -9
  157. package/dist/jsx/FrappeListView.mjs.map +1 -1
  158. package/dist/jsx/FrappeListView.native.js +9 -7
  159. package/dist/jsx/FrappeListView.native.js.map +1 -1
  160. package/dist/jsx/FrappeTable.mjs +4 -0
  161. package/dist/jsx/FrappeTable.mjs.map +1 -1
  162. package/dist/jsx/FrappeTable.native.js +10 -0
  163. package/dist/jsx/FrappeTable.native.js.map +1 -1
  164. package/dist/jsx/fields/Date/date.native.js +11 -48
  165. package/dist/jsx/fields/Date/date.native.js.map +1 -1
  166. package/dist/jsx/fields/Date/index.mjs +1 -12
  167. package/dist/jsx/fields/Date/index.mjs.map +1 -1
  168. package/dist/jsx/fields/Date/index.native.js +2 -15
  169. package/dist/jsx/fields/Date/index.native.js.map +1 -1
  170. package/dist/jsx/fields/Date/wire.mjs +23 -0
  171. package/dist/jsx/fields/Date/wire.mjs.map +1 -0
  172. package/dist/jsx/fields/Date/wire.native.js +55 -0
  173. package/dist/jsx/fields/Date/wire.native.js.map +1 -0
  174. package/dist/jsx/fields/ReadOnly/index.mjs +9 -37
  175. package/dist/jsx/fields/ReadOnly/index.mjs.map +1 -1
  176. package/dist/jsx/fields/ReadOnly/index.native.js +12 -36
  177. package/dist/jsx/fields/ReadOnly/index.native.js.map +1 -1
  178. package/dist/jsx/fields/TableMultiSelect.mjs +4 -12
  179. package/dist/jsx/fields/TableMultiSelect.mjs.map +1 -1
  180. package/dist/jsx/fields/TableMultiSelect.native.js +7 -15
  181. package/dist/jsx/fields/TableMultiSelect.native.js.map +1 -1
  182. package/dist/jsx/fields/TextEditor/index.mjs +11 -6
  183. package/dist/jsx/fields/TextEditor/index.mjs.map +1 -1
  184. package/dist/jsx/schemaForm/index.mjs +10 -2
  185. package/dist/jsx/schemaForm/index.mjs.map +1 -1
  186. package/dist/jsx/schemaForm/index.native.js +10 -2
  187. package/dist/jsx/schemaForm/index.native.js.map +1 -1
  188. package/package.json +22 -22
  189. package/src/FormSidebar.stories.tsx +6 -6
  190. package/src/FormSidebar.tsx +1 -2
  191. package/src/FrappeAssignment.spec.tsx +97 -0
  192. package/src/FrappeAssignment.tsx +6 -2
  193. package/src/FrappeCalendar.stories.tsx +72 -1
  194. package/src/FrappeCalendar.tsx +3 -13
  195. package/src/FrappeConnectionIndicator.tsx +8 -2
  196. package/src/FrappeDashboard.tsx +3 -2
  197. package/src/FrappeForm.stories.tsx +23 -9
  198. package/src/FrappeGantt.stories.tsx +6 -1
  199. package/src/FrappeGantt.tsx +3 -13
  200. package/src/FrappeKanban.tsx +41 -23
  201. package/src/FrappeLikedBy.tsx +2 -1
  202. package/src/FrappeList.tsx +7 -1
  203. package/src/FrappeListView.tsx +16 -22
  204. package/src/FrappeTable.tsx +10 -0
  205. package/src/components/timeline.stories.tsx +7 -2
  206. package/src/fields/Barcode/Barcode.stories.tsx +1 -1
  207. package/src/fields/CodeEditor/CodeEditor.stories.tsx +1 -1
  208. package/src/fields/Color/Color.stories.tsx +1 -1
  209. package/src/fields/Date/Date.spec.tsx +149 -0
  210. package/src/fields/Date/Date.stories.tsx +1 -1
  211. package/src/fields/Date/Datetime.stories.tsx +1 -1
  212. package/src/fields/Date/date.native.tsx +18 -65
  213. package/src/fields/Date/index.tsx +1 -25
  214. package/src/fields/Date/wire.spec.ts +172 -0
  215. package/src/fields/Date/wire.ts +68 -0
  216. package/src/fields/DateRange/DateRange.stories.tsx +1 -1
  217. package/src/fields/Duration/Duration.stories.tsx +1 -1
  218. package/src/fields/FileUploadField/FileUploadField.stories.tsx +15 -1
  219. package/src/fields/Geolocation/Geolocation.stories.tsx +12 -1
  220. package/src/fields/Link.stories.tsx +1 -1
  221. package/src/fields/LongText/LongText.stories.tsx +1 -1
  222. package/src/fields/MentionInput/MentionInput.stories.tsx +1 -1
  223. package/src/fields/OTPInput/OTPInput.stories.tsx +1 -1
  224. package/src/fields/Password/Password.stories.tsx +1 -1
  225. package/src/fields/Phone/Phone.stories.tsx +1 -1
  226. package/src/fields/Progress/Progress.stories.tsx +1 -1
  227. package/src/fields/RadioGroup/RadioGroup.stories.tsx +1 -1
  228. package/src/fields/Rating/Rating.stories.tsx +1 -1
  229. package/src/fields/ReadOnly/index.tsx +23 -33
  230. package/src/fields/Signature/Signature.stories.tsx +1 -1
  231. package/src/fields/Slider/Slider.stories.tsx +1 -1
  232. package/src/fields/SmallText/SmallText.stories.tsx +1 -1
  233. package/src/fields/Switch/Switch.stories.tsx +1 -1
  234. package/src/fields/TableMultiSelect.tsx +5 -18
  235. package/src/fields/TextEditor/TextEditor.stories.tsx +1 -1
  236. package/src/fields/TextEditor/index.tsx +8 -5
  237. package/src/fields/TimePicker/TimePicker.stories.tsx +1 -1
  238. package/src/fields/ToggleGroup/ToggleGroup.stories.tsx +1 -1
  239. package/src/schemaForm/index.tsx +19 -1
  240. package/types/FormSidebar.d.ts.map +1 -1
  241. package/types/FrappeAssignment.d.ts.map +1 -1
  242. package/types/FrappeCalendar.d.ts.map +1 -1
  243. package/types/FrappeConnectionIndicator.d.ts.map +1 -1
  244. package/types/FrappeDashboard.d.ts.map +1 -1
  245. package/types/FrappeGantt.d.ts.map +1 -1
  246. package/types/FrappeKanban.d.ts +3 -1
  247. package/types/FrappeKanban.d.ts.map +1 -1
  248. package/types/FrappeLikedBy.d.ts.map +1 -1
  249. package/types/FrappeList.d.ts.map +1 -1
  250. package/types/FrappeListView.d.ts.map +1 -1
  251. package/types/FrappeTable.d.ts.map +1 -1
  252. package/types/fields/Date/date.native.d.ts.map +1 -1
  253. package/types/fields/Date/index.d.ts.map +1 -1
  254. package/types/fields/Date/wire.d.ts +41 -0
  255. package/types/fields/Date/wire.d.ts.map +1 -0
  256. package/types/fields/ReadOnly/index.d.ts.map +1 -1
  257. package/types/fields/TableMultiSelect.d.ts.map +1 -1
  258. package/types/fields/TextEditor/index.d.ts.map +1 -1
  259. package/types/schemaForm/index.d.ts +9 -1
  260. package/types/schemaForm/index.d.ts.map +1 -1
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Date wrapper value contract (late-value display fix):
3
+ *
4
+ * The wrapper used to fold the controlled `value` into the forms DatePicker's
5
+ * mount-only `defaultValue` and DROP `onChange` — so a doc value arriving
6
+ * after first paint (create-mode engine seeding on /backoffice/todo/new)
7
+ * never displayed, and picks never reached the consumer doc. It now passes
8
+ * `value` through controlled and converts picked Dates back to the Frappe
9
+ * wire shape. Programmatic value application must not emit onChange (LC-14:
10
+ * a seeded value is not a user edit).
11
+ */
12
+
13
+ import { describe, it, expect, vi } from "vitest";
14
+
15
+ // The first field-module import in a cold fork transforms the whole fields
16
+ // graph (same budget as tests/fieldRenderers.spec.tsx).
17
+ vi.setConfig({ testTimeout: 240_000 });
18
+
19
+ // Mock react-native-reanimated to prevent ESM module resolution failures
20
+ // (same shim as tests/fieldRenderers.spec.tsx).
21
+ vi.mock("react-native-reanimated", () => ({
22
+ default: {
23
+ useSharedValue: (init: any) => ({ value: init }),
24
+ useAnimatedStyle: () => ({}),
25
+ withTiming: (val: any) => val,
26
+ withSpring: (val: any) => val,
27
+ createAnimatedComponent: (component: any) => component,
28
+ runOnJS: (fn: any) => fn,
29
+ runOnUI: (fn: any) => fn,
30
+ useDerivedValue: (fn: () => any) => ({ value: fn() }),
31
+ useAnimatedRef: () => ({ current: null }),
32
+ Easing: { linear: (v: any) => v, ease: (v: any) => v, bezier: () => (v: any) => v },
33
+ FadeIn: { duration: () => ({}) },
34
+ FadeOut: { duration: () => ({}) },
35
+ Layout: { duration: () => ({}) },
36
+ Extrapolation: { CLAMP: "clamp" },
37
+ measure: () => null,
38
+ scrollTo: () => {},
39
+ makeMutable: (init: any) => ({ value: init }),
40
+ },
41
+ useSharedValue: (init: any) => ({ value: init }),
42
+ useAnimatedStyle: () => ({}),
43
+ withTiming: (val: any) => val,
44
+ withSpring: (val: any) => val,
45
+ createAnimatedComponent: (component: any) => component,
46
+ runOnJS: (fn: any) => fn,
47
+ runOnUI: (fn: any) => fn,
48
+ useDerivedValue: (fn: () => any) => ({ value: fn() }),
49
+ useAnimatedRef: () => ({ current: null }),
50
+ Easing: { linear: (v: any) => v, ease: (v: any) => v, bezier: () => (v: any) => v },
51
+ FadeIn: { duration: () => ({}) },
52
+ FadeOut: { duration: () => ({}) },
53
+ Layout: { duration: () => ({}) },
54
+ }));
55
+
56
+ import { renderWithProviders } from "@multiplatform.one/test-utils";
57
+ import { act, fireEvent, waitFor } from "@testing-library/react";
58
+ import React from "react";
59
+
60
+ async function loadDateField() {
61
+ return (await import(".")).Date;
62
+ }
63
+
64
+ const getDisplayInput = (container: ParentNode) =>
65
+ container.querySelector("input") as HTMLInputElement | null;
66
+
67
+ /** Enabled day-cell buttons inside the (portaled) calendar popover. */
68
+ function getDayButton(root: ParentNode, day: number): HTMLButtonElement | undefined {
69
+ const allButtons = Array.from(
70
+ root.querySelectorAll('button, [role="button"]'),
71
+ ) as HTMLButtonElement[];
72
+ return allButtons.find((btn) => {
73
+ const text = btn.textContent?.trim() ?? "";
74
+ const disabled =
75
+ btn.hasAttribute("disabled") ||
76
+ btn.getAttribute("aria-disabled") === "true" ||
77
+ btn.hasAttribute("data-disabled");
78
+ return text === String(day) && !disabled;
79
+ });
80
+ }
81
+
82
+ describe("Date wrapper — late-arriving value display", () => {
83
+ it("a value arriving after mount displays, without onChange", async () => {
84
+ const DateField = await loadDateField();
85
+ const onChange = vi.fn();
86
+ const result = renderWithProviders(
87
+ <DateField label="Due Date" value="" onChange={onChange} type="date" />,
88
+ );
89
+ expect(getDisplayInput(result.container)?.value ?? "").toBe("");
90
+
91
+ // Late seed: the engine applies create defaults once meta loads.
92
+ result.rerender(
93
+ <DateField label="Due Date" value="2026-03-05" onChange={onChange} type="date" />,
94
+ );
95
+ await waitFor(() => {
96
+ expect(getDisplayInput(result.container)?.value).toBe("Mar 5, 2026");
97
+ });
98
+ expect(onChange).not.toHaveBeenCalled();
99
+ });
100
+ });
101
+
102
+ describe("Date wrapper — pick emits the Frappe wire string", () => {
103
+ it("emits YYYY-MM-DD once for a Date field pick", async () => {
104
+ const DateField = await loadDateField();
105
+ const onChange = vi.fn();
106
+ const result = renderWithProviders(
107
+ <DateField label="Due Date" value="2026-03-05" onChange={onChange} type="date" />,
108
+ );
109
+ const trigger = getDisplayInput(result.container);
110
+ expect(trigger).toBeTruthy();
111
+ await act(async () => {
112
+ if (trigger) fireEvent.click(trigger);
113
+ });
114
+ await waitFor(() => {
115
+ expect(getDayButton(document.body, 15)).toBeTruthy();
116
+ });
117
+ await act(async () => {
118
+ fireEvent.click(getDayButton(document.body, 15)!);
119
+ });
120
+ expect(onChange).toHaveBeenCalledTimes(1);
121
+ expect(onChange).toHaveBeenCalledWith("2026-03-15");
122
+ });
123
+
124
+ it("preserves the existing time part for a Datetime field pick", async () => {
125
+ const DateField = await loadDateField();
126
+ const onChange = vi.fn();
127
+ const result = renderWithProviders(
128
+ <DateField
129
+ label="Starts On"
130
+ value="2026-03-05 14:30:00"
131
+ onChange={onChange}
132
+ type="datetime"
133
+ />,
134
+ );
135
+ const trigger = getDisplayInput(result.container);
136
+ expect(trigger).toBeTruthy();
137
+ await act(async () => {
138
+ if (trigger) fireEvent.click(trigger);
139
+ });
140
+ await waitFor(() => {
141
+ expect(getDayButton(document.body, 20)).toBeTruthy();
142
+ });
143
+ await act(async () => {
144
+ fireEvent.click(getDayButton(document.body, 20)!);
145
+ });
146
+ expect(onChange).toHaveBeenCalledTimes(1);
147
+ expect(onChange).toHaveBeenCalledWith("2026-03-20 14:30:00");
148
+ });
149
+ });
@@ -126,7 +126,7 @@ export const AllStates = () => (
126
126
  <ControlledDate label="Default (Empty)" type="date" />
127
127
  <ControlledDate label="With Value" type="date" value="2026-06-15" />
128
128
  <ControlledDate label="Required" type="date" required />
129
- <ControlledDate label="With Error" type="date" error="Please select a valid date" />
129
+ <ControlledDate label="With Error" type="date" error="A valid date is required" />
130
130
  <ControlledDate label="Disabled" type="date" value="2026-01-01" disabled />
131
131
  </YStack>
132
132
  );
@@ -52,7 +52,7 @@ export const States = (args: StoryProps) => (
52
52
  <Controlled {...args} label="Disabled" disabled />
53
53
  <Controlled {...args} label="Disabled + error" disabled error="Broken while disabled" />
54
54
  <Controlled {...args} label="Required" required />
55
- <Controlled {...args} label="Error" error="This field has an error" />
55
+ <Controlled {...args} label="Error" error="Value does not match the required format" />
56
56
  <Controlled {...args} label="Helper text" helperText="Some helpful context" />
57
57
  </YStack>
58
58
  );
@@ -1,9 +1,15 @@
1
- import { useResolvedKnobs } from "@multiplatform.one/theme";
1
+ import {
2
+ formatAbsoluteDate,
3
+ formatAbsoluteDateTime,
4
+ formatTimeOfDay,
5
+ useResolvedKnobs,
6
+ } from "@multiplatform.one/theme";
2
7
  import React, { useState, useCallback, useMemo } from "react";
3
8
  import { Modal, Pressable, StyleSheet, View } from "react-native";
4
9
  import { platform } from "@multiplatform.one/platform";
5
10
  import DateTimePicker, { type DateTimePickerEvent } from "@react-native-community/datetimepicker";
6
11
  import { Button, Label, Text, XStack, YStack, type YStackProps } from "tamagui";
12
+ import { fromFrappeDateValue, toFrappeDateValue } from "./wire";
7
13
 
8
14
  export interface DateProps extends Omit<YStackProps, "children" | "onChange"> {
9
15
  label?: string;
@@ -19,72 +25,19 @@ export interface DateProps extends Omit<YStackProps, "children" | "onChange"> {
19
25
  }
20
26
 
21
27
  /**
22
- * Format date to ISO string format for the given type
23
- */
24
- function formatValue(date: Date, type: "date" | "datetime" | "time"): string {
25
- if (type === "time") {
26
- const hours = date.getHours().toString().padStart(2, "0");
27
- const minutes = date.getMinutes().toString().padStart(2, "0");
28
- return `${hours}:${minutes}`;
29
- }
30
- if (type === "datetime") {
31
- return date.toISOString().slice(0, 16).replace("T", " ");
32
- }
33
- // date
34
- return date.toISOString().slice(0, 10);
35
- }
36
-
37
- /**
38
- * Parse value string to Date object
39
- */
40
- function parseValue(value: string, type: "date" | "datetime" | "time"): Date {
41
- if (!value) return new Date();
42
-
43
- if (type === "time") {
44
- const [hours, minutes] = value.split(":").map(Number);
45
- const date = new Date();
46
- date.setHours(hours || 0, minutes || 0, 0, 0);
47
- return date;
48
- }
49
- if (type === "datetime") {
50
- // Handle both "YYYY-MM-DD HH:MM" and "YYYY-MM-DDTHH:MM" formats
51
- const normalized = value.replace(" ", "T");
52
- return new Date(normalized);
53
- }
54
- // date - YYYY-MM-DD
55
- return new Date(value + "T00:00:00");
56
- }
57
-
58
- /**
59
- * Format display value for the button
28
+ * Display value for the button the house registers (LC-12 render channel),
29
+ * so the native picker reads the same voice as the web picker, table date
30
+ * cells and the timeline. A picker is an editing surface, so the year is
31
+ * always shown.
60
32
  */
61
33
  function formatDisplayValue(value: string, type: "date" | "datetime" | "time"): string {
62
34
  if (!value) return "";
63
35
 
64
36
  try {
65
- const date = parseValue(value, type);
66
-
67
- if (type === "time") {
68
- return date.toLocaleTimeString(undefined, {
69
- hour: "2-digit",
70
- minute: "2-digit",
71
- });
72
- }
73
- if (type === "datetime") {
74
- return date.toLocaleString(undefined, {
75
- year: "numeric",
76
- month: "short",
77
- day: "numeric",
78
- hour: "2-digit",
79
- minute: "2-digit",
80
- });
81
- }
82
- // date
83
- return date.toLocaleDateString(undefined, {
84
- year: "numeric",
85
- month: "short",
86
- day: "numeric",
87
- });
37
+ const date = fromFrappeDateValue(value, type);
38
+ if (type === "time") return formatTimeOfDay(date);
39
+ if (type === "datetime") return formatAbsoluteDateTime(date, { year: "always" });
40
+ return formatAbsoluteDate(date, { year: "always" });
88
41
  } catch {
89
42
  return value;
90
43
  }
@@ -114,7 +67,7 @@ export function DateNative({
114
67
 
115
68
  // Parse current value to Date
116
69
  const currentDate = useMemo(() => {
117
- return value ? parseValue(value, type) : new Date();
70
+ return value ? fromFrappeDateValue(value, type) : new Date();
118
71
  }, [value, type]);
119
72
 
120
73
  // Determine picker mode
@@ -172,7 +125,7 @@ export function DateNative({
172
125
  )
173
126
  : selectedDate;
174
127
 
175
- onChange(formatValue(finalDate, type));
128
+ onChange(toFrappeDateValue(finalDate, type));
176
129
  setShowPicker(false);
177
130
  setShowTimePicker(false);
178
131
  setTempDate(null);
@@ -183,7 +136,7 @@ export function DateNative({
183
136
 
184
137
  const handleIOSConfirm = useCallback(() => {
185
138
  if (tempDate) {
186
- onChange(formatValue(tempDate, type));
139
+ onChange(toFrappeDateValue(tempDate, type));
187
140
  }
188
141
  setShowPicker(false);
189
142
  setShowTimePicker(false);
@@ -2,6 +2,7 @@ import type { FormFieldProps, FieldComponentProps } from "@multiplatform.one/for
2
2
  import { DatePicker as FormsDatePicker } from "@multiplatform.one/forms";
3
3
  import type { DeepKeys } from "@tanstack/form-core";
4
4
  import { useProps } from "tamagui";
5
+ import { toFrappeDateValue } from "./wire";
5
6
 
6
7
  export type DateProps<
7
8
  TData extends Record<string, unknown> = Record<string, unknown>,
@@ -40,31 +41,6 @@ export type DateProps<
40
41
  inputProps?: Record<string, unknown>;
41
42
  };
42
43
 
43
- const pad2 = (n: number) => String(n).padStart(2, "0");
44
-
45
- /**
46
- * Format a picked Date back into the Frappe wire shape for the fieldtype:
47
- * `YYYY-MM-DD` (Date), `YYYY-MM-DD HH:mm:ss` (Datetime — the picker edits
48
- * only the date part, so an existing time part on the previous value is
49
- * preserved), `HH:mm:ss` (Time). Local date parts — Frappe date strings are
50
- * timezone-naive (same contract as FrappeCalendar's toFrappeFieldValue).
51
- */
52
- function toFrappeDateValue(
53
- date: globalThis.Date,
54
- type: "date" | "datetime" | "time",
55
- previousValue: string | undefined,
56
- ): string {
57
- const datePart = `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`;
58
- if (type === "time") {
59
- return `${pad2(date.getHours())}:${pad2(date.getMinutes())}:${pad2(date.getSeconds())}`;
60
- }
61
- if (type === "datetime") {
62
- const previousTime = previousValue?.trim().split(/[ T]/)[1];
63
- return `${datePart} ${previousTime ?? "00:00:00"}`;
64
- }
65
- return datePart;
66
- }
67
-
68
44
  export function Date<
69
45
  TData extends Record<string, unknown> = Record<string, unknown>,
70
46
  TName extends string = string,
@@ -0,0 +1,172 @@
1
+ /**
2
+ * The Date field's Frappe wire contract, proved in both directions across
3
+ * timezones — the WRITE counterpart to `parseCalendarDate`'s matrix in
4
+ * `@multiplatform.one/theme` (tests/dateParsing.spec.tsx covers the read side
5
+ * through the two Frappe views).
6
+ *
7
+ * The field used to serialize picks with `toISOString()`, which renders the
8
+ * UTC instant rather than the timezone-naive local value Frappe stores. Each
9
+ * assertion below is paired with a negative control running that old
10
+ * formulation through the same expectation, so a matrix that stopped
11
+ * discriminating — because the host stopped honouring `TZ`, say — fails
12
+ * loudly instead of passing vacuously.
13
+ */
14
+
15
+ import { describe, expect, it } from "vitest";
16
+ import { fromFrappeDateValue, toFrappeDateValue } from "./wire";
17
+
18
+ // Spans UTC, two negative offsets and the far positive extreme (UTC+14), so a
19
+ // serializer that leans on the host zone fails somewhere in this list.
20
+ const ZONES = ["UTC", "America/Los_Angeles", "America/New_York", "Pacific/Kiritimati"] as const;
21
+
22
+ function inZone<T>(timeZone: string, read: () => T): T {
23
+ const previous = process.env.TZ;
24
+ process.env.TZ = timeZone;
25
+ try {
26
+ return read();
27
+ } finally {
28
+ if (previous === undefined) delete process.env.TZ;
29
+ else process.env.TZ = previous;
30
+ }
31
+ }
32
+
33
+ const pad = (n: number) => String(n).padStart(2, "0");
34
+ const localDay = (date: Date) =>
35
+ `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
36
+
37
+ /** The formulation this fix replaced, kept as the negative control. */
38
+ function isoWireValue(date: Date, type: "date" | "datetime"): string {
39
+ return type === "datetime"
40
+ ? date.toISOString().slice(0, 16).replace("T", " ")
41
+ : date.toISOString().slice(0, 10);
42
+ }
43
+
44
+ /**
45
+ * The two shapes of Date the picker hands back, because which one it is
46
+ * decides the DIRECTION of the old defect:
47
+ * - `midnight` — the field held a value, so the picker was seeded from it;
48
+ * - `evening` — the field was empty, so the picker was seeded with
49
+ * `new Date()` and the pick carries the current wall clock.
50
+ * Both name Aug 15 2026 on the user's calendar.
51
+ */
52
+ const DATE_PICKS = {
53
+ midnight: () => new Date(2026, 7, 15),
54
+ evening: () => new Date(2026, 7, 15, 18, 30),
55
+ } as const;
56
+
57
+ describe("toFrappeDateValue — a date-only pick round-trips as a calendar date", () => {
58
+ for (const pick of ["midnight", "evening"] as const) {
59
+ it(`persists the day the user picked in every timezone (picker held ${pick})`, () => {
60
+ for (const timeZone of ZONES) {
61
+ const result = inZone(timeZone, () => {
62
+ const wire = toFrappeDateValue(DATE_PICKS[pick](), "date");
63
+ // Read it straight back: write and read must agree, or a no-op
64
+ // edit silently rewrites the document (the drag-move defect the
65
+ // read-path fix found on the Gantt).
66
+ return { timeZone, wire, readsBackAs: localDay(fromFrappeDateValue(wire, "date")) };
67
+ });
68
+ expect(result).toEqual({
69
+ timeZone,
70
+ wire: "2026-08-15",
71
+ readsBackAs: "2026-08-15",
72
+ });
73
+ }
74
+ });
75
+ }
76
+
77
+ it("negative control: the ISO formulation writes a different day, both ways", () => {
78
+ // UTC+14 before local noon: the UTC day is still yesterday.
79
+ expect(inZone("Pacific/Kiritimati", () => isoWireValue(DATE_PICKS.midnight(), "date"))).toBe(
80
+ "2026-08-14",
81
+ );
82
+ // UTC−7 after local 17:00: the UTC day has already rolled over.
83
+ expect(inZone("America/Los_Angeles", () => isoWireValue(DATE_PICKS.evening(), "date"))).toBe(
84
+ "2026-08-16",
85
+ );
86
+ // ...and it agrees with the fixed body exactly where the offset cannot
87
+ // reach across the date boundary, which is why this shipped for so long.
88
+ expect(inZone("America/New_York", () => isoWireValue(DATE_PICKS.evening(), "date"))).toBe(
89
+ "2026-08-15",
90
+ );
91
+ });
92
+ });
93
+
94
+ describe("toFrappeDateValue — a datetime pick preserves the instant", () => {
95
+ it("round-trips to the same instant and the same wall clock in every timezone", () => {
96
+ for (const timeZone of ZONES) {
97
+ const result = inZone(timeZone, () => {
98
+ const picked = new Date(2026, 7, 15, 15, 0, 0);
99
+ const wire = toFrappeDateValue(picked, "datetime");
100
+ const readBack = fromFrappeDateValue(wire, "datetime");
101
+ return {
102
+ timeZone,
103
+ wire,
104
+ // The instant itself — not merely the digits.
105
+ instantPreserved: readBack.getTime() === picked.getTime(),
106
+ readsBackAtHour: readBack.getHours(),
107
+ };
108
+ });
109
+ expect(result).toEqual({
110
+ timeZone,
111
+ wire: "2026-08-15 15:00:00",
112
+ instantPreserved: true,
113
+ readsBackAtHour: 15,
114
+ });
115
+ }
116
+ });
117
+
118
+ it("negative control: the ISO formulation shifts the instant by the whole offset", () => {
119
+ const shifted = inZone("America/Los_Angeles", () => {
120
+ const picked = new Date(2026, 7, 15, 15, 0, 0);
121
+ const wire = isoWireValue(picked, "datetime");
122
+ const readBack = fromFrappeDateValue(wire, "datetime");
123
+ return {
124
+ wire,
125
+ instantPreserved: readBack.getTime() === picked.getTime(),
126
+ driftHours: (readBack.getTime() - picked.getTime()) / 3_600_000,
127
+ };
128
+ });
129
+ // 15:00 picked in Los Angeles was persisted as 22:00 and read back as
130
+ // 22:00 LOCAL — seven hours later than the user chose, every save.
131
+ expect(shifted).toEqual({ wire: "2026-08-15 22:00", instantPreserved: false, driftHours: 7 });
132
+ });
133
+
134
+ it("keeps an existing time part when only the day was edited (web date-only picker)", () => {
135
+ for (const separator of [" ", "T"]) {
136
+ expect(
137
+ inZone("Pacific/Kiritimati", () =>
138
+ toFrappeDateValue(new Date(2026, 7, 15), "datetime", `2026-03-05${separator}14:30:00`),
139
+ ),
140
+ ).toBe("2026-08-15 14:30:00");
141
+ }
142
+ });
143
+ });
144
+
145
+ describe("toFrappeDateValue — a time pick is a local clock", () => {
146
+ it("emits local HH:mm:ss, never the UTC clock", () => {
147
+ for (const timeZone of ZONES) {
148
+ expect({
149
+ timeZone,
150
+ wire: inZone(timeZone, () => toFrappeDateValue(new Date(2026, 7, 15, 15, 45, 30), "time")),
151
+ }).toEqual({ timeZone, wire: "15:45:30" });
152
+ }
153
+ });
154
+ });
155
+
156
+ describe("fromFrappeDateValue — the read side is local too", () => {
157
+ it("opens a bare YYYY-MM-DD on local midnight in every timezone", () => {
158
+ for (const timeZone of ZONES) {
159
+ expect(
160
+ inZone(timeZone, () => {
161
+ const date = fromFrappeDateValue("2026-08-15", "date");
162
+ return { timeZone, day: localDay(date), hour: date.getHours() };
163
+ }),
164
+ ).toEqual({ timeZone, day: "2026-08-15", hour: 0 });
165
+ }
166
+ });
167
+
168
+ it("negative control: the bare ECMA-262 parse still shifts west of UTC", () => {
169
+ // If this ever stops shifting, the test above has stopped proving anything.
170
+ expect(inZone("America/Los_Angeles", () => new Date("2026-08-15").getDate())).toBe(14);
171
+ });
172
+ });
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The Date field's Frappe wire contract, in both directions — one body shared
3
+ * by the web half (`index.tsx`) and the native half (`date.native.tsx`), so
4
+ * the same pick persists the same string on either platform.
5
+ *
6
+ * Frappe date fields are timezone-NAIVE: `Date` is a bare calendar date
7
+ * (`YYYY-MM-DD`), `Datetime` is a bare local wall clock
8
+ * (`YYYY-MM-DD HH:mm:ss`), `Time` is a bare clock (`HH:mm:ss`). None carries an
9
+ * offset, and every other writer in the client already builds them from LOCAL
10
+ * parts — `FrappeCalendar` / `FrappeGantt` `toFrappeFieldValue`, backoffice
11
+ * `CalendarView.toFrappeDateValue`. So does the read side here, and so does
12
+ * `parseCalendarDate` in `@multiplatform.one/theme`.
13
+ *
14
+ * Serializing through `toISOString()` instead renders the UTC instant, which
15
+ * breaks both halves of that contract: it names the wrong calendar day whenever
16
+ * the picked local time of day sits across the UTC date boundary (picking
17
+ * Aug 15 at UTC+14 wrote `2026-08-14`), and it shifts every datetime by the
18
+ * whole UTC offset, because the value is then read back as local wall clock.
19
+ */
20
+
21
+ const pad2 = (n: number) => String(n).padStart(2, "0");
22
+
23
+ export type FrappeDateFieldType = "date" | "datetime" | "time";
24
+
25
+ /**
26
+ * Serialize a picked `Date` into the wire shape for the fieldtype.
27
+ *
28
+ * `previousValue` is for pickers that edit only the day: the web half wraps a
29
+ * date-only calendar, so an existing time part survives the edit. The native
30
+ * half picks the time too and passes nothing, in which case the datetime
31
+ * branch uses the picked Date's own local time.
32
+ */
33
+ export function toFrappeDateValue(
34
+ date: Date,
35
+ type: FrappeDateFieldType,
36
+ previousValue?: string,
37
+ ): string {
38
+ const datePart = `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`;
39
+ const timePart = `${pad2(date.getHours())}:${pad2(date.getMinutes())}:${pad2(date.getSeconds())}`;
40
+ if (type === "time") return timePart;
41
+ if (type === "datetime") {
42
+ return `${datePart} ${previousValue?.trim().split(/[ T]/)[1] ?? timePart}`;
43
+ }
44
+ return datePart;
45
+ }
46
+
47
+ /**
48
+ * Read a wire value back into the `Date` the picker opens on. Empty input
49
+ * seeds on today — this is a picker's initial value, not a parse of stored
50
+ * data, so it has no null branch.
51
+ *
52
+ * A bare `YYYY-MM-DD` gets an explicit local midnight, because
53
+ * `new Date("YYYY-MM-DD")` means UTC midnight per ECMA-262 and would open the
54
+ * picker on the previous day west of UTC. A wall clock with no offset is
55
+ * already read as local per ES2016+.
56
+ */
57
+ export function fromFrappeDateValue(value: string, type: FrappeDateFieldType): Date {
58
+ if (!value) return new Date();
59
+ if (type === "time") {
60
+ const [hours, minutes] = value.split(":").map(Number);
61
+ const date = new Date();
62
+ date.setHours(hours || 0, minutes || 0, 0, 0);
63
+ return date;
64
+ }
65
+ // Accepts both "YYYY-MM-DD HH:MM" and "YYYY-MM-DDTHH:MM".
66
+ if (type === "datetime") return new Date(value.replace(" ", "T"));
67
+ return new Date(`${value}T00:00:00`);
68
+ }
@@ -48,7 +48,7 @@ export const States = (args: StoryProps) => (
48
48
  <Controlled {...args} label="Disabled" disabled />
49
49
  <Controlled {...args} label="Disabled + error" disabled error="Broken while disabled" />
50
50
  <Controlled {...args} label="Required" required />
51
- <Controlled {...args} label="Error" error="This field has an error" />
51
+ <Controlled {...args} label="Error" error="Value does not match the required format" />
52
52
  <Controlled {...args} label="Helper text" helperText="Some helpful context" />
53
53
  </YStack>
54
54
  );
@@ -51,7 +51,7 @@ export const States = (args: StoryProps) => (
51
51
  <Controlled {...args} label="Disabled" disabled />
52
52
  <Controlled {...args} label="Disabled + error" disabled error="Broken while disabled" />
53
53
  <Controlled {...args} label="Required" required />
54
- <Controlled {...args} label="Error" error="This field has an error" />
54
+ <Controlled {...args} label="Error" error="Value does not match the required format" />
55
55
  <Controlled {...args} label="Helper text" helperText="Some helpful context" />
56
56
  </YStack>
57
57
  );
@@ -56,11 +56,25 @@ export const States = (args: StoryProps) => (
56
56
  error="Broken while disabled"
57
57
  />
58
58
  <FileUploadField {...(args as any)} label="Required" required />
59
- <FileUploadField {...(args as any)} label="Error" error="This field has an error" />
59
+ <FileUploadField
60
+ {...(args as any)}
61
+ label="Error"
62
+ error="Value does not match the required format"
63
+ />
60
64
  <FileUploadField {...(args as any)} label="Helper text" helperText="Some helpful context" />
61
65
  </YStack>
62
66
  );
63
67
  States.args = {} as Partial<StoryProps>;
68
+ // LC-57 allowlist: the Disabled rows demo the disabled dial, and the field's
69
+ // internal upload Button has no disabledReason plumbing yet (component-contract
70
+ // follow-up). The warn still fires; the harvester tolerates exactly this code.
71
+ States.parameters = {
72
+ guardrailSpecimen: {
73
+ warns: ["bare-disabled"],
74
+ reason:
75
+ "Disabled-dial rows: FileUpload's internal upload Button has no disabledReason plumbing yet.",
76
+ },
77
+ };
64
78
 
65
79
  /** Thin-adapter contract probe: readOnly / skeleton / compact forwarding. */
66
80
  export const ContractProbe = (args: StoryProps) => (
@@ -52,7 +52,7 @@ export const States = (args: StoryProps) => (
52
52
  <Controlled {...args} label="Disabled" disabled />
53
53
  <Controlled {...args} label="Disabled + error" disabled error="Broken while disabled" />
54
54
  <Controlled {...args} label="Required" required />
55
- <Controlled {...args} label="Error" error="This field has an error" />
55
+ <Controlled {...args} label="Error" error="Value does not match the required format" />
56
56
  <Controlled {...args} label="Helper text" helperText="Some helpful context" />
57
57
  </YStack>
58
58
  );
@@ -60,6 +60,17 @@ States.args = {
60
60
  label: "Location",
61
61
  value: '{"lat": 30.2672, "lng": -97.7431}',
62
62
  } as Partial<StoryProps>;
63
+ // LC-57 allowlist: the Disabled rows demo the disabled dial, and Geolocation's
64
+ // internal Locate/Coordinates Buttons have no disabledReason plumbing yet
65
+ // (component-contract follow-up). The warn still fires; the harvester tolerates
66
+ // exactly this code.
67
+ States.parameters = {
68
+ guardrailSpecimen: {
69
+ warns: ["bare-disabled"],
70
+ reason:
71
+ "Disabled-dial rows: Geolocation's internal action Buttons have no disabledReason plumbing yet.",
72
+ },
73
+ };
63
74
 
64
75
  /**
65
76
  * Thin-adapter contract probe: forwards readOnly / skeleton / compact through
@@ -72,7 +72,7 @@ export const States = (args: StoryProps) =>
72
72
  <Controlled {...args} label="Disabled" disabled />
73
73
  <Controlled {...args} label="Disabled + error" disabled error="Broken while disabled" />
74
74
  <Controlled {...args} label="Required" required />
75
- <Controlled {...args} label="Error" error="This field has an error" />
75
+ <Controlled {...args} label="Error" error="Value does not match the required format" />
76
76
  <Controlled {...args} label="Allow create" allowCreate onCreateNew={action("onCreateNew")} />
77
77
  </YStack>,
78
78
  );
@@ -51,7 +51,7 @@ export const States = (args: StoryProps) => (
51
51
  <Controlled {...args} label="Disabled" disabled />
52
52
  <Controlled {...args} label="Disabled + error" disabled error="Broken while disabled" />
53
53
  <Controlled {...args} label="Required" required />
54
- <Controlled {...args} label="Error" error="This field has an error" />
54
+ <Controlled {...args} label="Error" error="Value does not match the required format" />
55
55
  <Controlled {...args} label="Helper text" helperText="Some helpful context" />
56
56
  </YStack>
57
57
  );