@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
@@ -32,7 +32,12 @@ interface CalEvent {
32
32
  }
33
33
 
34
34
  const today = new Date();
35
- const formatDate = (date: Date) => date.toISOString().split("T")[0];
35
+ // LOCAL parts, matching FrappeCalendar's own `datePart`: a `toISOString()` date
36
+ // is UTC, so east of Greenwich the fixture names yesterday for the hour after
37
+ // local midnight and no event lands on the view's "today".
38
+ const pad2 = (n: number) => String(n).padStart(2, "0");
39
+ const formatDate = (date: Date) =>
40
+ `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`;
36
41
 
37
42
  // Fixture doc colors come from the sanctioned categorical palette (chart
38
43
  // palette precedent) instead of raw brand hexes — Frappe docs store one color
@@ -287,6 +292,72 @@ Live.parameters = {
287
292
  },
288
293
  };
289
294
 
295
+ // ---------------------------------------------------------------------------
296
+ // Frappe `Date` fields — date-only wire values, no time component
297
+ // ---------------------------------------------------------------------------
298
+
299
+ interface LeaveRequest {
300
+ name: string;
301
+ doctype: string;
302
+ subject: string;
303
+ from_date: string;
304
+ to_date: string;
305
+ color: string;
306
+ }
307
+
308
+ const dateOnlyFixtures: LeaveRequest[] = [
309
+ {
310
+ name: "LEAVE-001",
311
+ doctype: "LeaveApplication",
312
+ subject: "Annual leave",
313
+ from_date: formatDate(today),
314
+ to_date: formatDate(new Date(today.getTime() + 2 * 86400000)),
315
+ color: green,
316
+ },
317
+ {
318
+ name: "LEAVE-002",
319
+ doctype: "LeaveApplication",
320
+ subject: "Conference travel",
321
+ from_date: formatDate(new Date(today.getTime() + 4 * 86400000)),
322
+ to_date: formatDate(new Date(today.getTime() + 5 * 86400000)),
323
+ color: orange,
324
+ },
325
+ ];
326
+
327
+ /**
328
+ * Frappe `Date` fields arrive as bare "YYYY-MM-DD" — a calendar date carrying
329
+ * no timezone, the shape every other fixture story here omits by always
330
+ * sending a time. They parse to LOCAL midnight (`parseCalendarDate`); read as
331
+ * UTC they render a day early in every negative-offset zone, so this is the
332
+ * story that makes that class of regression visible.
333
+ */
334
+ export const DateOnlyFields = () => (
335
+ <FrappeProvider
336
+ baseURL="https://demo.frappe.cloud"
337
+ fixtures={new InMemoryFixtureProvider({ LeaveApplication: dateOnlyFixtures })}
338
+ >
339
+ <FrappeCalendar<LeaveRequest>
340
+ doctype="LeaveApplication"
341
+ dateField="from_date"
342
+ endDateField="to_date"
343
+ titleField="subject"
344
+ colorField="color"
345
+ defaultView="month"
346
+ onEventClick={action("onEventClick")}
347
+ onDateClick={action("onDateClick")}
348
+ height={600}
349
+ />
350
+ </FrappeProvider>
351
+ );
352
+ DateOnlyFields.parameters = {
353
+ docs: {
354
+ description: {
355
+ story:
356
+ "Date-only Frappe `Date` fields (no time component). The leave block starts on today's cell in every timezone — read as UTC midnight it would sit on yesterday west of Greenwich.",
357
+ },
358
+ },
359
+ };
360
+
290
361
  // ---------------------------------------------------------------------------
291
362
  // Named Frappe colors (P-15): palette references resolve to sanctioned ramps,
292
363
  // literals pass through, unknown names fall back to the identity slot
@@ -9,7 +9,7 @@ import { Spinner } from "@multiplatform.one/forms";
9
9
  import React, { useMemo, useCallback, useState, type ReactNode } from "react";
10
10
  import { Text, YStack, type YStackProps } from "tamagui";
11
11
  import { useTranslation } from "react-i18next";
12
- import { useChartPalette, useResolvedKnobs } from "@multiplatform.one/theme";
12
+ import { parseCalendarDate, useChartPalette, useResolvedKnobs } from "@multiplatform.one/theme";
13
13
  import { FrappeConnectionIndicator } from "./FrappeConnectionIndicator";
14
14
  import { useFrappeConfig } from "./FrappeProvider";
15
15
  import { describeSyncError } from "./syncError";
@@ -57,16 +57,6 @@ export interface FrappeCalendarProps<TDoc extends { name: string; doctype: strin
57
57
  firstDayOfWeek?: 0 | 1;
58
58
  }
59
59
 
60
- /**
61
- * Parse a date string from Frappe (YYYY-MM-DD or YYYY-MM-DD HH:MM:SS).
62
- */
63
- function parseDate(dateStr: string | Date | undefined): Date | null {
64
- if (!dateStr) return null;
65
- if (dateStr instanceof Date) return dateStr;
66
- const parsed = new Date(dateStr.replace(" ", "T"));
67
- return isNaN(parsed.getTime()) ? null : parsed;
68
- }
69
-
70
60
  /**
71
61
  * Format a moved date back into the same shape the document field used:
72
62
  * Date stays Date, "YYYY-MM-DD HH:MM:SS" keeps its time part, plain
@@ -195,9 +185,9 @@ export function FrappeCalendar<TDoc extends { name: string; doctype: string }>({
195
185
  const events = useMemo((): CalendarEvent[] => {
196
186
  return docs
197
187
  .map((doc) => {
198
- const startDate = parseDate((doc as any)[dateField]);
188
+ const startDate = parseCalendarDate((doc as any)[dateField]);
199
189
  if (!startDate) return null;
200
- const endDate = endDateField ? parseDate((doc as any)[endDateField]) : undefined;
190
+ const endDate = endDateField ? parseCalendarDate((doc as any)[endDateField]) : undefined;
201
191
  const title = titleField ? String((doc as any)[titleField] || doc.name) : doc.name;
202
192
  const color = colorField
203
193
  ? resolveFrappeEventColor((doc as any)[colorField], chartPalette)
@@ -2,7 +2,7 @@ import { useFrappeConnection, type UseFrappeCollectionConfig } from "@multiplatf
2
2
  import React from "react";
3
3
  import { useTranslation } from "react-i18next";
4
4
  import { Text, XStack, YStack } from "tamagui";
5
- import { useResolvedKnobs } from "@multiplatform.one/theme";
5
+ import { radiusClassProps, useResolvedKnobs } from "@multiplatform.one/theme";
6
6
 
7
7
  export interface FrappeConnectionIndicatorProps {
8
8
  /** The same connection config the sibling collection hooks use. */
@@ -37,7 +37,13 @@ export function FrappeConnectionIndicator({ config }: FrappeConnectionIndicatorP
37
37
  borderColor="$yellow7"
38
38
  borderWidth={1}
39
39
  >
40
- <YStack width={8} height={8} borderRadius={4} backgroundColor="$yellow10" />
40
+ <YStack
41
+ {...radiusClassProps("R-PILL", "connection status dot")}
42
+ width={8}
43
+ height={8}
44
+ borderRadius={4}
45
+ backgroundColor="$yellow10"
46
+ />
41
47
  <Text fontSize="$2" color="$yellow11">
42
48
  {t("Reconnecting…")}
43
49
  </Text>
@@ -16,6 +16,7 @@ import { useTranslation } from "react-i18next";
16
16
  import { Spinner } from "@multiplatform.one/forms";
17
17
  import { Button, Text, XStack, YStack, type YStackProps, H3 } from "tamagui";
18
18
  import {
19
+ formatTimeOfDay,
19
20
  MIN_PRESS_TARGET,
20
21
  pressTargetHitSlop,
21
22
  pressTargetStyle,
@@ -217,7 +218,7 @@ function LiveDashboardContent<TDoc extends { name: string; doctype: string }>({
217
218
  {isLiveStale && <Spinner size="small" aria-label={t("Updating")} />}
218
219
  <Text fontSize="$1" color="$color10">
219
220
  {t("Last updated: {{time}}", {
220
- time: (dataUpdatedAt ?? lastRefresh).toLocaleTimeString(),
221
+ time: formatTimeOfDay(dataUpdatedAt ?? lastRefresh),
221
222
  })}
222
223
  </Text>
223
224
  <Button
@@ -328,7 +329,7 @@ export function FrappeDashboard<TDoc extends { name: string; doctype: string }>(
328
329
  <XStack {...knobProps.gap} alignItems="center">
329
330
  {autoRefresh && (
330
331
  <Text fontSize="$1" color="$color10">
331
- {t("Last updated: {{time}}", { time: lastRefresh.toLocaleTimeString() })}
332
+ {t("Last updated: {{time}}", { time: formatTimeOfDay(lastRefresh) })}
332
333
  </Text>
333
334
  )}
334
335
  <Button
@@ -2,6 +2,7 @@ import { InMemoryFixtureProvider } from "@multiplatform.one/frappe";
2
2
  import { action } from "@multiplatform.one/storybook";
3
3
  import { YStack } from "tamagui";
4
4
  import { Data } from "./fields/Data";
5
+ import { RadioGroupField } from "./fields/RadioGroup";
5
6
  import { Select } from "./fields/Select";
6
7
  import { Check } from "./fields/Check";
7
8
  import { Date } from "./fields/Date";
@@ -51,7 +52,8 @@ function contactMeta() {
51
52
  fieldname: "status",
52
53
  fieldtype: "Select",
53
54
  label: "Status",
54
- options: "Active\nLead\nInactive",
55
+ // >5 options: shorter single-select lists trip `select-too-few-options` (DG-CTRL-01).
56
+ options: "Active\nLead\nProspect\nCustomer\nInactive\nArchived",
55
57
  },
56
58
  ],
57
59
  permissions: [{ role: "All", read: 1, write: 1, create: 1 }],
@@ -72,7 +74,8 @@ function salesOrderMeta() {
72
74
  fieldname: "status",
73
75
  fieldtype: "Select",
74
76
  label: "Status",
75
- options: "Draft\nSubmitted\nCompleted\nCancelled",
77
+ // >5 options: shorter single-select lists trip `select-too-few-options` (DG-CTRL-01).
78
+ options: "Draft\nSubmitted\nIn Progress\nOn Hold\nCompleted\nCancelled",
76
79
  },
77
80
  { fieldname: "is_paid", fieldtype: "Check", label: "Payment Received" },
78
81
  { fieldname: "notes", fieldtype: "Small Text", label: "Notes" },
@@ -172,7 +175,7 @@ export const CreateForm = () => (
172
175
  label="Status"
173
176
  value={doc.status || ""}
174
177
  onChange={(v) => setValue("status", v)}
175
- options={["Active", "Lead", "Inactive"]}
178
+ options={["Active", "Lead", "Prospect", "Customer", "Inactive", "Archived"]}
176
179
  />
177
180
  </YStack>
178
181
  )}
@@ -227,7 +230,7 @@ export const EditForm = () => (
227
230
  label="Status"
228
231
  value={doc.status || ""}
229
232
  onChange={(v) => setValue("status", v)}
230
- options={["Active", "Lead", "Inactive"]}
233
+ options={["Active", "Lead", "Prospect", "Customer", "Inactive", "Archived"]}
231
234
  />
232
235
  </YStack>
233
236
  )}
@@ -359,7 +362,7 @@ export const ComplexForm = () => (
359
362
  label="Status"
360
363
  value={doc.status || ""}
361
364
  onChange={(v) => setValue("status", v)}
362
- options={["Draft", "Submitted", "Completed", "Cancelled"]}
365
+ options={["Draft", "Submitted", "In Progress", "On Hold", "Completed", "Cancelled"]}
363
366
  />
364
367
  <Check
365
368
  label="Payment Received"
@@ -438,18 +441,29 @@ export const Live = (args: { name?: string }) => (
438
441
  error={errors.description}
439
442
  disabled={disabled}
440
443
  />
441
- <Select
444
+ {/* The real ToDo doctype pins 3-option enums; DG-CTRL-01 routes short
445
+ single choices to RadioGroup, so the live arm composes the honest
446
+ control instead of tripping `select-too-few-options` at rest. */}
447
+ <RadioGroupField
442
448
  label="Status"
443
449
  value={doc.status || ""}
444
450
  onChange={(v) => setValue("status", v)}
445
- options={["Open", "Closed", "Cancelled"]}
451
+ options={[
452
+ { label: "Open", value: "Open" },
453
+ { label: "Closed", value: "Closed" },
454
+ { label: "Cancelled", value: "Cancelled" },
455
+ ]}
446
456
  disabled={disabled}
447
457
  />
448
- <Select
458
+ <RadioGroupField
449
459
  label="Priority"
450
460
  value={doc.priority || ""}
451
461
  onChange={(v) => setValue("priority", v)}
452
- options={["Low", "Medium", "High"]}
462
+ options={[
463
+ { label: "Low", value: "Low" },
464
+ { label: "Medium", value: "Medium" },
465
+ { label: "High", value: "High" },
466
+ ]}
453
467
  disabled={disabled}
454
468
  />
455
469
  </YStack>
@@ -31,7 +31,12 @@ interface ProjectTask {
31
31
  }
32
32
 
33
33
  const today = new Date();
34
- const formatDate = (date: Date) => date.toISOString().split("T")[0];
34
+ // LOCAL parts, matching FrappeGantt's own `datePart`: a `toISOString()` date is
35
+ // UTC, so east of Greenwich the fixture names yesterday for the hour after
36
+ // local midnight and every bar sits a day off the view's "today" marker.
37
+ const pad2 = (n: number) => String(n).padStart(2, "0");
38
+ const formatDate = (date: Date) =>
39
+ `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`;
35
40
  const dayOffset = (days: number) => formatDate(new Date(today.getTime() + days * 86400000));
36
41
 
37
42
  // Fixture doc colors come from the sanctioned categorical palette (LC-27
@@ -8,7 +8,7 @@ import {
8
8
  import React, { useCallback, useMemo, useState, type ReactNode } from "react";
9
9
  import { Text, YStack, type YStackProps } from "tamagui";
10
10
  import { useTranslation } from "react-i18next";
11
- import { useChartPalette, useResolvedKnobs } from "@multiplatform.one/theme";
11
+ import { parseCalendarDate, useChartPalette, useResolvedKnobs } from "@multiplatform.one/theme";
12
12
  import { FrappeConnectionIndicator } from "./FrappeConnectionIndicator";
13
13
  import { useFrappeConfig } from "./FrappeProvider";
14
14
  import { describeSyncError } from "./syncError";
@@ -56,16 +56,6 @@ export interface FrappeGanttProps<TDoc extends { name: string; doctype: string }
56
56
  initialDate?: Date;
57
57
  }
58
58
 
59
- /**
60
- * Parse a date string from Frappe (YYYY-MM-DD or YYYY-MM-DD HH:MM:SS).
61
- */
62
- function parseDate(dateStr: string | Date | undefined): Date | null {
63
- if (!dateStr) return null;
64
- if (dateStr instanceof Date) return dateStr;
65
- const parsed = new Date(dateStr.replace(" ", "T"));
66
- return isNaN(parsed.getTime()) ? null : parsed;
67
- }
68
-
69
59
  /**
70
60
  * Format a moved date back into the same shape the document field used:
71
61
  * Date stays Date, "YYYY-MM-DD HH:MM:SS" keeps its time part, plain
@@ -195,9 +185,9 @@ export function FrappeGantt<TDoc extends { name: string; doctype: string }>({
195
185
  const tasks = useMemo((): GanttTask[] => {
196
186
  return docs
197
187
  .map((doc) => {
198
- const start = parseDate((doc as any)[startField]);
188
+ const start = parseCalendarDate((doc as any)[startField]);
199
189
  if (!start) return null;
200
- const end = parseDate((doc as any)[endField]) ?? start;
190
+ const end = parseCalendarDate((doc as any)[endField]) ?? start;
201
191
  const title = String((doc as any)[titleField] || doc.name);
202
192
  const color = colorField
203
193
  ? resolveFrappeEventColor((doc as any)[colorField], chartPalette)
@@ -4,10 +4,11 @@ import {
4
4
  type KanbanColumn as GenericKanbanColumn,
5
5
  type KanbanProps,
6
6
  } from "@multiplatform.one/components";
7
+ import { Button } from "@multiplatform.one/forms";
7
8
  import { PlusIcon } from "@phosphor-icons/react";
8
9
  import React, { useMemo, useCallback, useState } from "react";
9
10
  import { useTranslation } from "react-i18next";
10
- import { Button, Text, XStack, YStack } from "tamagui";
11
+ import { Text, XStack, YStack } from "tamagui";
11
12
 
12
13
  import { useResolvedKnobs } from "@multiplatform.one/theme";
13
14
  import { FrappeConnectionIndicator } from "./FrappeConnectionIndicator";
@@ -48,6 +49,8 @@ export interface FrappeKanbanProps<TDoc extends { name: string; doctype: string
48
49
  renderCard?: (item: Partial<TDoc>) => React.ReactNode;
49
50
  onCardClick?: (card: Partial<TDoc>) => void;
50
51
  onCardMove?: (cardName: string, fromStatus: string, toStatus: string) => void | Promise<void>;
52
+ /** Disable card dragging (the board stays a read-only visualization). */
53
+ readOnly?: boolean;
51
54
  enableCardCreation?: boolean;
52
55
  onCreateCard?: (status: string) => void;
53
56
  loading?: React.ReactNode;
@@ -75,10 +78,12 @@ export function FrappeKanban<TDoc extends { name: string; doctype: string }>({
75
78
  renderCard,
76
79
  onCardClick,
77
80
  onCardMove,
81
+ readOnly = false,
78
82
  enableCardCreation = false,
79
83
  onCreateCard,
80
84
  loading,
81
85
  error,
86
+ renderColumnActions,
82
87
  ...kanbanProps
83
88
  }: FrappeKanbanProps<TDoc>) {
84
89
  const config = useFrappeConfig();
@@ -234,6 +239,37 @@ export function FrappeKanban<TDoc extends { name: string; doctype: string }>({
234
239
  [onCardClick],
235
240
  );
236
241
 
242
+ // Desk parity: the quick-add "+" is anchored IN its column header (never a
243
+ // detached button strip) and names its column in the accessible label. The
244
+ // synthetic "Unassigned" bucket has no field value to prefill, so it gets
245
+ // no add affordance. Consumer-provided column actions compose after it.
246
+ const effectiveRenderColumnActions = useMemo(() => {
247
+ const canQuickAdd = enableCardCreation && Boolean(onCreateCard);
248
+ if (!canQuickAdd && !renderColumnActions) return undefined;
249
+ return (column: GenericKanbanColumn<Partial<TDoc>>) => (
250
+ <XStack alignItems="center" gap="$1">
251
+ {canQuickAdd && column.id !== "__unassigned__" && (
252
+ <Button
253
+ size="$2"
254
+ circular
255
+ chromeless
256
+ icon={<PlusIcon size={14} />}
257
+ aria-label={t("Add card to {{column}}", { column: column.title })}
258
+ onPress={() => {
259
+ setActionError(null);
260
+ try {
261
+ onCreateCard?.(column.id);
262
+ } catch (err) {
263
+ setActionError(err instanceof Error ? err : new Error(String(err)));
264
+ }
265
+ }}
266
+ />
267
+ )}
268
+ {renderColumnActions?.(column)}
269
+ </XStack>
270
+ );
271
+ }, [enableCardCreation, onCreateCard, renderColumnActions, t]);
272
+
237
273
  // Custom loading slot only — default path keeps empty columns visible so
238
274
  // fixture-less demos still render the board (Kanban owns isLoading chrome).
239
275
  if (isLoading && cards.length === 0 && loading) {
@@ -271,33 +307,15 @@ export function FrappeKanban<TDoc extends { name: string; doctype: string }>({
271
307
  </Text>
272
308
  </YStack>
273
309
  )}
274
- {enableCardCreation && onCreateCard && (
275
- <XStack {...knobProps.gap} flexWrap="wrap" paddingHorizontal="$2">
276
- {columns.map((col) => (
277
- <Button
278
- key={col.value}
279
- size="$2"
280
- chromeless
281
- icon={PlusIcon}
282
- onPress={() => {
283
- setActionError(null);
284
- try {
285
- onCreateCard(col.value);
286
- } catch (err) {
287
- setActionError(err instanceof Error ? err : new Error(String(err)));
288
- }
289
- }}
290
- aria-label={t("Add card to {{column}}", { column: col.title })}
291
- />
292
- ))}
293
- </XStack>
294
- )}
295
310
  <Kanban
296
311
  {...knobProps.gap}
297
312
  columns={genericColumns}
298
313
  renderCard={effectiveRenderCard}
299
- onCardMove={handleCardMove}
314
+ // The generic Kanban enables drag when a move handler exists —
315
+ // readOnly withholds it so the board stays a visualization.
316
+ onCardMove={readOnly ? undefined : handleCardMove}
300
317
  onCardSelect={handleCardSelect}
318
+ renderColumnActions={effectiveRenderColumnActions}
301
319
  isLoading={isLoading}
302
320
  error={boardError}
303
321
  getItemKey={(item, index) => item.name ?? index}
@@ -13,11 +13,12 @@
13
13
  */
14
14
  import { AsyncBoundary, Avatar } from "@multiplatform.one/components";
15
15
  import { HttpClient, useFrappeCollection, useLiveQuery } from "@multiplatform.one/frappe";
16
+ import { Button } from "@multiplatform.one/forms";
16
17
  import { useResolvedKnobs } from "@multiplatform.one/theme";
17
18
  import { HeartIcon } from "@phosphor-icons/react";
18
19
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
19
20
  import { useTranslation } from "react-i18next";
20
- import { Button, Text, XStack, YStack, isWeb, useTheme, type YStackProps } from "tamagui";
21
+ import { Text, XStack, YStack, isWeb, useTheme, type YStackProps } from "tamagui";
21
22
  import { useFrappeConfig } from "./FrappeProvider";
22
23
  import { FrappeReconnectIndicator } from "./FrappeReconnectIndicator";
23
24
  import { useFrappeUsers } from "./frappeUsers";
@@ -94,8 +94,14 @@ export function FrappeList<TDoc extends { name: string; doctype: string }>({
94
94
  return (
95
95
  <YStack {...knobProps.gap} flex={1}>
96
96
  <FrappeReconnectIndicator config={connectionConfig} />
97
+ {/*
98
+ No chrome fragment rides this passthrough (LC-55 FRAGMENT-LICENSE):
99
+ List forwards its stack props straight to AsyncBoundary, a state-switch
100
+ wrapper that declares no surface class and paints no fill, border, or
101
+ clip. Rows and the error/empty panels own their own chrome; the error
102
+ branch above renders a bare AsyncBoundary, so both paths now match.
103
+ */}
97
104
  <List
98
- {...knobProps.borderRadius}
99
105
  items={items}
100
106
  isLoading={isLoading}
101
107
  renderItem={renderItem}
@@ -4,19 +4,9 @@ import { ArrowsClockwiseIcon, PlusIcon } from "@phosphor-icons/react";
4
4
  import React, { useState, useMemo, useCallback, useEffect } from "react";
5
5
  import { useTranslation } from "react-i18next";
6
6
  import { SearchInput, Spinner } from "@multiplatform.one/forms";
7
- import {
8
- Button,
9
- Image,
10
- Text,
11
- View,
12
- XStack,
13
- YStack,
14
- isWeb,
15
- useTheme,
16
- type YStackProps,
17
- } from "tamagui";
7
+ import { Button, Text, View, XStack, YStack, isWeb, useTheme, type YStackProps } from "tamagui";
18
8
  import { SavedFilterDropdown, useSavedFilters, type ParsedFilter } from "@multiplatform.one/table";
19
- import { ViewSwitcher } from "@multiplatform.one/components";
9
+ import { Avatar, ViewSwitcher } from "@multiplatform.one/components";
20
10
  import {
21
11
  MIN_PRESS_TARGET,
22
12
  pressTargetHitSlop,
@@ -311,15 +301,13 @@ export function FrappeListView<TDoc extends { name: string; doctype: string }>({
311
301
  return (
312
302
  <XStack alignItems="center" gap="$3" paddingVertical="$2" paddingHorizontal="$3">
313
303
  {imageUrl ? (
314
- <Image
315
- src={imageUrl}
316
- alt={typeof title === "string" ? title : ""}
317
- width={40}
318
- height={40}
319
- borderRadius={1000}
320
- backgroundColor="$color3"
321
- objectFit="cover"
322
- />
304
+ <Avatar circular size={40} backgroundColor="$color3">
305
+ <Avatar.Image
306
+ src={imageUrl}
307
+ alt={typeof title === "string" ? title : ""}
308
+ objectFit="cover"
309
+ />
310
+ </Avatar>
323
311
  ) : null}
324
312
  <YStack flex={1} gap="$0.5">
325
313
  <Text fontSize="$4" fontWeight="500" numberOfLines={1}>
@@ -503,7 +491,13 @@ export function FrappeListView<TDoc extends { name: string; doctype: string }>({
503
491
  {...refreshPressFloorProps}
504
492
  />
505
493
  {onAdd && (
506
- <Button size="$3" theme="accent" icon={PlusIcon} onPress={onAdd}>
494
+ <Button
495
+ size="$3"
496
+ theme="accent"
497
+ icon={PlusIcon}
498
+ onPress={onAdd}
499
+ {...pressTargetStyle()}
500
+ >
507
501
  {t("Add {{doctype}}", { doctype })}
508
502
  </Button>
509
503
  )}
@@ -556,6 +556,16 @@ export function FrappeTable<TDoc extends { name: string; doctype: string }>({
556
556
  onPress={() => onRowClick(props.row.original)}
557
557
  cursor="pointer"
558
558
  hoverStyle={{ backgroundColor: "$color2" }}
559
+ // Cell containment (LC-06/LC-10): a bare View defaults to
560
+ // flexShrink 0 and hugs its content, so long cell content
561
+ // (e.g. a ToDo description) painted PAST the column bounds and
562
+ // under the neighbor cells (the td itself doesn't clip).
563
+ // Shrink with the cell, cap at its box, clip the excess — the
564
+ // inner one-line Texts then ellipsize in-column.
565
+ minWidth={0}
566
+ maxWidth="100%"
567
+ flexShrink={1}
568
+ overflow="hidden"
559
569
  >
560
570
  {/* raw strings crash native (text must be inside <Text>) */}
561
571
  {typeof content === "string" || typeof content === "number" ? (
@@ -1,5 +1,6 @@
1
1
  import { InMemoryFixtureProvider } from "@multiplatform.one/frappe";
2
2
  import { action } from "@multiplatform.one/storybook";
3
+ import { formatAbsoluteDateTime } from "@multiplatform.one/theme";
3
4
  import { FrappeProvider } from "../FrappeProvider";
4
5
  import { Timeline, type FrappeComment, type FrappeVersion } from "./timeline";
5
6
 
@@ -138,14 +139,18 @@ export const ReadOnly = () => (
138
139
  </FrappeProvider>
139
140
  );
140
141
 
141
- /** Absolute timestamps via a custom formatTimestamp override the relative default. */
142
+ /**
143
+ * The formatTimestamp eject overrides the per-class register defaults —
144
+ * demoed with the house absolute register pinned to always show the year
145
+ * (LC-12: even an ejected formatter speaks a human register, never raw ISO).
146
+ */
142
147
  export const AbsoluteTimestamps = () => (
143
148
  <FrappeProvider baseURL="https://demo.frappe.cloud" fixtures={makeFixtures()}>
144
149
  <Timeline
145
150
  doctype="Task"
146
151
  docname="TASK-001"
147
152
  label="Activity"
148
- formatTimestamp={(ts) => new Date(ts).toISOString()}
153
+ formatTimestamp={(ts) => formatAbsoluteDateTime(ts, { year: "always" })}
149
154
  />
150
155
  </FrappeProvider>
151
156
  );
@@ -46,7 +46,7 @@ export const States = (args: StoryProps) => (
46
46
  <Controlled {...args} label="Disabled" disabled />
47
47
  <Controlled {...args} label="Disabled + error" disabled error="Broken while disabled" />
48
48
  <Controlled {...args} label="Required" required />
49
- <Controlled {...args} label="Error" error="This field has an error" />
49
+ <Controlled {...args} label="Error" error="Value does not match the required format" />
50
50
  <Controlled {...args} label="Helper text" helperText="Some helpful context" />
51
51
  </YStack>
52
52
  );
@@ -118,7 +118,7 @@ ReadOnly.args = {
118
118
 
119
119
  export const States = (args: StoryProps) => (
120
120
  <YStack gap="$5">
121
- <Controlled {...args} label="Error" error="Script has a syntax error" />
121
+ <Controlled {...args} label="Error" error="Script fails to parse at line 3" />
122
122
  <Controlled {...args} label="Disabled" disabled />
123
123
  <Controlled {...args} label="Compact" compact />
124
124
  <CodeEditorField label="Skeleton" skeleton onChange={() => {}} />
@@ -52,7 +52,7 @@ export const AllStates = () => (
52
52
  <ControlledColor label="Default" />
53
53
  <ControlledColor label="With Value" value="#10B981" />
54
54
  <ControlledColor label="Required" required />
55
- <ControlledColor label="With Error" value="invalid" error="Invalid color" />
55
+ <ControlledColor label="With Error" value="invalid" error="Color must be a recognized value" />
56
56
  <ControlledColor label="Disabled" value="#F59E0B" disabled />
57
57
  <ControlledColor label="No Presets" showPresets={false} />
58
58
  <ControlledColor