@multiplatform.one/frappe-ui 6.4.2 → 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.
- package/dist/cjs/FormSidebar.cjs +2 -2
- package/dist/cjs/FormSidebar.native.js +2 -2
- package/dist/cjs/FormSidebar.native.js.map +1 -1
- package/dist/cjs/FrappeAssignment.cjs +8 -4
- package/dist/cjs/FrappeAssignment.native.js +8 -4
- package/dist/cjs/FrappeAssignment.native.js.map +1 -1
- package/dist/cjs/FrappeCalendar.cjs +2 -8
- package/dist/cjs/FrappeCalendar.native.js +2 -8
- package/dist/cjs/FrappeCalendar.native.js.map +1 -1
- package/dist/cjs/FrappeConnectionIndicator.cjs +1 -0
- package/dist/cjs/FrappeConnectionIndicator.native.js +1 -0
- package/dist/cjs/FrappeConnectionIndicator.native.js.map +1 -1
- package/dist/cjs/FrappeDashboard.cjs +4 -4
- package/dist/cjs/FrappeDashboard.native.js +4 -4
- package/dist/cjs/FrappeDashboard.native.js.map +1 -1
- package/dist/cjs/FrappeGantt.cjs +2 -8
- package/dist/cjs/FrappeGantt.native.js +3 -9
- package/dist/cjs/FrappeGantt.native.js.map +1 -1
- package/dist/cjs/FrappeKanban.cjs +32 -21
- package/dist/cjs/FrappeKanban.native.js +36 -23
- package/dist/cjs/FrappeKanban.native.js.map +1 -1
- package/dist/cjs/FrappeLikedBy.cjs +2 -1
- package/dist/cjs/FrappeLikedBy.native.js +2 -1
- package/dist/cjs/FrappeLikedBy.native.js.map +1 -1
- package/dist/cjs/FrappeList.cjs +0 -1
- package/dist/cjs/FrappeList.native.js +10 -2
- package/dist/cjs/FrappeList.native.js.map +1 -1
- package/dist/cjs/FrappeListView.cjs +10 -8
- package/dist/cjs/FrappeListView.native.js +10 -8
- package/dist/cjs/FrappeListView.native.js.map +1 -1
- package/dist/cjs/FrappeReportBuilder.cjs +1 -1
- package/dist/cjs/FrappeReportBuilder.native.js +1 -1
- package/dist/cjs/FrappeTable.cjs +4 -0
- package/dist/cjs/FrappeTable.native.js +10 -0
- package/dist/cjs/FrappeTable.native.js.map +1 -1
- package/dist/cjs/FrappeTreeView.cjs +1 -1
- package/dist/cjs/FrappeTreeView.native.js +1 -1
- package/dist/cjs/desk/DeskFormPage.cjs +1 -1
- package/dist/cjs/desk/DeskFormPage.native.js +1 -1
- package/dist/cjs/desk/DeskFormPage.native.js.map +1 -1
- package/dist/cjs/desk/DeskListPage.cjs +1 -1
- package/dist/cjs/desk/DeskListPage.native.js +1 -1
- package/dist/cjs/desk/DeskListPage.native.js.map +1 -1
- package/dist/cjs/fieldRegistry.native.js.map +1 -1
- package/dist/cjs/fields/Date/date.native.js +11 -48
- package/dist/cjs/fields/Date/date.native.js.map +1 -1
- package/dist/cjs/fields/Date/index.cjs +10 -3
- package/dist/cjs/fields/Date/index.native.js +15 -3
- package/dist/cjs/fields/Date/index.native.js.map +1 -1
- package/dist/cjs/fields/Date/wire.cjs +49 -0
- package/dist/cjs/fields/Date/wire.native.js +55 -0
- package/dist/cjs/fields/Date/wire.native.js.map +1 -0
- package/dist/cjs/fields/MarkdownEditor/index.native.js.map +1 -1
- package/dist/cjs/fields/ReadOnly/index.cjs +8 -36
- package/dist/cjs/fields/ReadOnly/index.native.js +12 -36
- package/dist/cjs/fields/ReadOnly/index.native.js.map +1 -1
- package/dist/cjs/fields/TableMultiSelect.cjs +7 -15
- package/dist/cjs/fields/TableMultiSelect.native.js +7 -15
- package/dist/cjs/fields/TableMultiSelect.native.js.map +1 -1
- package/dist/cjs/fields/TextEditor/index.cjs +11 -6
- package/dist/cjs/schemaForm/index.cjs +10 -2
- package/dist/cjs/schemaForm/index.native.js +10 -2
- package/dist/cjs/schemaForm/index.native.js.map +1 -1
- package/dist/esm/FormSidebar.mjs +2 -2
- package/dist/esm/FormSidebar.mjs.map +1 -1
- package/dist/esm/FormSidebar.native.js +2 -2
- package/dist/esm/FormSidebar.native.js.map +1 -1
- package/dist/esm/FrappeAssignment.mjs +6 -2
- package/dist/esm/FrappeAssignment.mjs.map +1 -1
- package/dist/esm/FrappeAssignment.native.js +6 -2
- package/dist/esm/FrappeAssignment.native.js.map +1 -1
- package/dist/esm/FrappeCalendar.mjs +3 -9
- package/dist/esm/FrappeCalendar.mjs.map +1 -1
- package/dist/esm/FrappeCalendar.native.js +3 -9
- package/dist/esm/FrappeCalendar.native.js.map +1 -1
- package/dist/esm/FrappeConnectionIndicator.mjs +2 -1
- package/dist/esm/FrappeConnectionIndicator.mjs.map +1 -1
- package/dist/esm/FrappeConnectionIndicator.native.js +2 -1
- package/dist/esm/FrappeConnectionIndicator.native.js.map +1 -1
- package/dist/esm/FrappeDashboard.mjs +5 -5
- package/dist/esm/FrappeDashboard.mjs.map +1 -1
- package/dist/esm/FrappeDashboard.native.js +5 -5
- package/dist/esm/FrappeDashboard.native.js.map +1 -1
- package/dist/esm/FrappeGantt.mjs +3 -9
- package/dist/esm/FrappeGantt.mjs.map +1 -1
- package/dist/esm/FrappeGantt.native.js +4 -10
- package/dist/esm/FrappeGantt.native.js.map +1 -1
- package/dist/esm/FrappeKanban.mjs +33 -22
- package/dist/esm/FrappeKanban.mjs.map +1 -1
- package/dist/esm/FrappeKanban.native.js +37 -24
- package/dist/esm/FrappeKanban.native.js.map +1 -1
- package/dist/esm/FrappeLikedBy.mjs +2 -1
- package/dist/esm/FrappeLikedBy.mjs.map +1 -1
- package/dist/esm/FrappeLikedBy.native.js +2 -1
- package/dist/esm/FrappeLikedBy.native.js.map +1 -1
- package/dist/esm/FrappeList.mjs +0 -1
- package/dist/esm/FrappeList.mjs.map +1 -1
- package/dist/esm/FrappeList.native.js +10 -2
- package/dist/esm/FrappeList.native.js.map +1 -1
- package/dist/esm/FrappeListView.mjs +12 -10
- package/dist/esm/FrappeListView.mjs.map +1 -1
- package/dist/esm/FrappeListView.native.js +12 -10
- package/dist/esm/FrappeListView.native.js.map +1 -1
- package/dist/esm/FrappeReportBuilder.mjs +1 -1
- package/dist/esm/FrappeReportBuilder.mjs.map +1 -1
- package/dist/esm/FrappeReportBuilder.native.js +1 -1
- package/dist/esm/FrappeTable.mjs +4 -0
- package/dist/esm/FrappeTable.mjs.map +1 -1
- package/dist/esm/FrappeTable.native.js +10 -0
- package/dist/esm/FrappeTable.native.js.map +1 -1
- package/dist/esm/FrappeTreeView.mjs +1 -1
- package/dist/esm/FrappeTreeView.mjs.map +1 -1
- package/dist/esm/FrappeTreeView.native.js +1 -1
- package/dist/esm/desk/DeskFormPage.mjs +1 -1
- package/dist/esm/desk/DeskFormPage.mjs.map +1 -1
- package/dist/esm/desk/DeskFormPage.native.js +1 -1
- package/dist/esm/desk/DeskFormPage.native.js.map +1 -1
- package/dist/esm/desk/DeskListPage.mjs +1 -1
- package/dist/esm/desk/DeskListPage.mjs.map +1 -1
- package/dist/esm/desk/DeskListPage.native.js +1 -1
- package/dist/esm/desk/DeskListPage.native.js.map +1 -1
- package/dist/esm/fieldRegistry.mjs.map +1 -1
- package/dist/esm/fields/Date/date.native.js +12 -49
- package/dist/esm/fields/Date/date.native.js.map +1 -1
- package/dist/esm/fields/Date/index.mjs +10 -3
- package/dist/esm/fields/Date/index.mjs.map +1 -1
- package/dist/esm/fields/Date/index.native.js +15 -3
- package/dist/esm/fields/Date/index.native.js.map +1 -1
- package/dist/esm/fields/Date/wire.mjs +23 -0
- package/dist/esm/fields/Date/wire.mjs.map +1 -0
- package/dist/esm/fields/Date/wire.native.js +26 -0
- package/dist/esm/fields/Date/wire.native.js.map +1 -0
- package/dist/esm/fields/MarkdownEditor/index.native.js.map +1 -1
- package/dist/esm/fields/ReadOnly/index.mjs +9 -37
- package/dist/esm/fields/ReadOnly/index.mjs.map +1 -1
- package/dist/esm/fields/ReadOnly/index.native.js +13 -37
- package/dist/esm/fields/ReadOnly/index.native.js.map +1 -1
- package/dist/esm/fields/TableMultiSelect.mjs +4 -12
- package/dist/esm/fields/TableMultiSelect.mjs.map +1 -1
- package/dist/esm/fields/TableMultiSelect.native.js +4 -12
- package/dist/esm/fields/TableMultiSelect.native.js.map +1 -1
- package/dist/esm/fields/TextEditor/index.mjs +11 -6
- package/dist/esm/fields/TextEditor/index.mjs.map +1 -1
- package/dist/esm/resolveControl.mjs.map +1 -1
- package/dist/esm/schemaForm/index.mjs +10 -2
- package/dist/esm/schemaForm/index.mjs.map +1 -1
- package/dist/esm/schemaForm/index.native.js +10 -2
- package/dist/esm/schemaForm/index.native.js.map +1 -1
- package/dist/jsx/FormSidebar.mjs +2 -2
- package/dist/jsx/FormSidebar.mjs.map +1 -1
- package/dist/jsx/FormSidebar.native.js +2 -2
- package/dist/jsx/FormSidebar.native.js.map +1 -1
- package/dist/jsx/FrappeAssignment.mjs +6 -2
- package/dist/jsx/FrappeAssignment.mjs.map +1 -1
- package/dist/jsx/FrappeAssignment.native.js +8 -4
- package/dist/jsx/FrappeAssignment.native.js.map +1 -1
- package/dist/jsx/FrappeCalendar.mjs +3 -9
- package/dist/jsx/FrappeCalendar.mjs.map +1 -1
- package/dist/jsx/FrappeCalendar.native.js +2 -8
- package/dist/jsx/FrappeCalendar.native.js.map +1 -1
- package/dist/jsx/FrappeConnectionIndicator.mjs +2 -1
- package/dist/jsx/FrappeConnectionIndicator.mjs.map +1 -1
- package/dist/jsx/FrappeConnectionIndicator.native.js +1 -0
- package/dist/jsx/FrappeConnectionIndicator.native.js.map +1 -1
- package/dist/jsx/FrappeDashboard.mjs +5 -5
- package/dist/jsx/FrappeDashboard.mjs.map +1 -1
- package/dist/jsx/FrappeDashboard.native.js +4 -4
- package/dist/jsx/FrappeDashboard.native.js.map +1 -1
- package/dist/jsx/FrappeGantt.mjs +3 -9
- package/dist/jsx/FrappeGantt.mjs.map +1 -1
- package/dist/jsx/FrappeGantt.native.js +3 -9
- package/dist/jsx/FrappeGantt.native.js.map +1 -1
- package/dist/jsx/FrappeKanban.mjs +33 -22
- package/dist/jsx/FrappeKanban.mjs.map +1 -1
- package/dist/jsx/FrappeKanban.native.js +36 -23
- package/dist/jsx/FrappeKanban.native.js.map +1 -1
- package/dist/jsx/FrappeLikedBy.mjs +2 -1
- package/dist/jsx/FrappeLikedBy.mjs.map +1 -1
- package/dist/jsx/FrappeLikedBy.native.js +2 -1
- package/dist/jsx/FrappeLikedBy.native.js.map +1 -1
- package/dist/jsx/FrappeList.mjs +0 -1
- package/dist/jsx/FrappeList.mjs.map +1 -1
- package/dist/jsx/FrappeList.native.js +10 -2
- package/dist/jsx/FrappeList.native.js.map +1 -1
- package/dist/jsx/FrappeListView.mjs +12 -10
- package/dist/jsx/FrappeListView.mjs.map +1 -1
- package/dist/jsx/FrappeListView.native.js +10 -8
- package/dist/jsx/FrappeListView.native.js.map +1 -1
- package/dist/jsx/FrappeReportBuilder.mjs +1 -1
- package/dist/jsx/FrappeReportBuilder.mjs.map +1 -1
- package/dist/jsx/FrappeReportBuilder.native.js +1 -1
- package/dist/jsx/FrappeTable.mjs +4 -0
- package/dist/jsx/FrappeTable.mjs.map +1 -1
- package/dist/jsx/FrappeTable.native.js +10 -0
- package/dist/jsx/FrappeTable.native.js.map +1 -1
- package/dist/jsx/FrappeTreeView.mjs +1 -1
- package/dist/jsx/FrappeTreeView.mjs.map +1 -1
- package/dist/jsx/FrappeTreeView.native.js +1 -1
- package/dist/jsx/desk/DeskFormPage.mjs +1 -1
- package/dist/jsx/desk/DeskFormPage.mjs.map +1 -1
- package/dist/jsx/desk/DeskFormPage.native.js +1 -1
- package/dist/jsx/desk/DeskFormPage.native.js.map +1 -1
- package/dist/jsx/desk/DeskListPage.mjs +1 -1
- package/dist/jsx/desk/DeskListPage.mjs.map +1 -1
- package/dist/jsx/desk/DeskListPage.native.js +1 -1
- package/dist/jsx/desk/DeskListPage.native.js.map +1 -1
- package/dist/jsx/fieldRegistry.mjs.map +1 -1
- package/dist/jsx/fields/Date/date.native.js +11 -48
- package/dist/jsx/fields/Date/date.native.js.map +1 -1
- package/dist/jsx/fields/Date/index.mjs +10 -3
- package/dist/jsx/fields/Date/index.mjs.map +1 -1
- package/dist/jsx/fields/Date/index.native.js +15 -3
- package/dist/jsx/fields/Date/index.native.js.map +1 -1
- package/dist/jsx/fields/Date/wire.mjs +23 -0
- package/dist/jsx/fields/Date/wire.mjs.map +1 -0
- package/dist/jsx/fields/Date/wire.native.js +55 -0
- package/dist/jsx/fields/Date/wire.native.js.map +1 -0
- package/dist/jsx/fields/MarkdownEditor/index.native.js.map +1 -1
- package/dist/jsx/fields/ReadOnly/index.mjs +9 -37
- package/dist/jsx/fields/ReadOnly/index.mjs.map +1 -1
- package/dist/jsx/fields/ReadOnly/index.native.js +12 -36
- package/dist/jsx/fields/ReadOnly/index.native.js.map +1 -1
- package/dist/jsx/fields/TableMultiSelect.mjs +4 -12
- package/dist/jsx/fields/TableMultiSelect.mjs.map +1 -1
- package/dist/jsx/fields/TableMultiSelect.native.js +7 -15
- package/dist/jsx/fields/TableMultiSelect.native.js.map +1 -1
- package/dist/jsx/fields/TextEditor/index.mjs +11 -6
- package/dist/jsx/fields/TextEditor/index.mjs.map +1 -1
- package/dist/jsx/resolveControl.mjs.map +1 -1
- package/dist/jsx/schemaForm/index.mjs +10 -2
- package/dist/jsx/schemaForm/index.mjs.map +1 -1
- package/dist/jsx/schemaForm/index.native.js +10 -2
- package/dist/jsx/schemaForm/index.native.js.map +1 -1
- package/package.json +22 -21
- package/src/FormSidebar.stories.tsx +6 -6
- package/src/FormSidebar.tsx +1 -2
- package/src/FrappeAssignment.spec.tsx +97 -0
- package/src/FrappeAssignment.tsx +6 -2
- package/src/FrappeCalendar.stories.tsx +72 -1
- package/src/FrappeCalendar.tsx +3 -13
- package/src/FrappeConnectionIndicator.tsx +8 -2
- package/src/FrappeDashboard.tsx +3 -2
- package/src/FrappeForm.stories.tsx +23 -9
- package/src/FrappeGantt.stories.tsx +6 -1
- package/src/FrappeGantt.tsx +3 -13
- package/src/FrappeKanban.tsx +41 -23
- package/src/FrappeLikedBy.tsx +2 -1
- package/src/FrappeList.tsx +7 -1
- package/src/FrappeListView.tsx +16 -22
- package/src/FrappeTable.stories.tsx +3 -5
- package/src/FrappeTable.tsx +10 -0
- package/src/components/timeline.stories.tsx +7 -2
- package/src/fields/Barcode/Barcode.stories.tsx +1 -1
- package/src/fields/CodeEditor/CodeEditor.stories.tsx +1 -1
- package/src/fields/Color/Color.stories.tsx +1 -1
- package/src/fields/Date/Date.spec.tsx +149 -0
- package/src/fields/Date/Date.stories.tsx +1 -1
- package/src/fields/Date/Datetime.stories.tsx +1 -1
- package/src/fields/Date/date.native.tsx +18 -65
- package/src/fields/Date/index.tsx +1 -25
- package/src/fields/Date/wire.spec.ts +172 -0
- package/src/fields/Date/wire.ts +68 -0
- package/src/fields/DateRange/DateRange.stories.tsx +1 -1
- package/src/fields/Duration/Duration.stories.tsx +1 -1
- package/src/fields/FileUploadField/FileUploadField.stories.tsx +15 -1
- package/src/fields/Geolocation/Geolocation.stories.tsx +12 -1
- package/src/fields/Link.stories.tsx +1 -1
- package/src/fields/LongText/LongText.stories.tsx +1 -1
- package/src/fields/MentionInput/MentionInput.stories.tsx +1 -1
- package/src/fields/OTPInput/OTPInput.stories.tsx +1 -1
- package/src/fields/Password/Password.stories.tsx +1 -1
- package/src/fields/Phone/Phone.stories.tsx +1 -1
- package/src/fields/Progress/Progress.stories.tsx +1 -1
- package/src/fields/RadioGroup/RadioGroup.stories.tsx +1 -1
- package/src/fields/Rating/Rating.stories.tsx +1 -1
- package/src/fields/ReadOnly/index.tsx +23 -33
- package/src/fields/Signature/Signature.stories.tsx +1 -1
- package/src/fields/Slider/Slider.stories.tsx +1 -1
- package/src/fields/SmallText/SmallText.stories.tsx +1 -1
- package/src/fields/Switch/Switch.stories.tsx +1 -1
- package/src/fields/TableMultiSelect.tsx +5 -18
- package/src/fields/TextEditor/TextEditor.stories.tsx +1 -1
- package/src/fields/TextEditor/index.tsx +8 -5
- package/src/fields/TimePicker/TimePicker.stories.tsx +1 -1
- package/src/fields/ToggleGroup/ToggleGroup.stories.tsx +1 -1
- package/src/schemaForm/index.tsx +19 -1
- package/types/FormSidebar.d.ts.map +1 -1
- package/types/FrappeAssignment.d.ts.map +1 -1
- package/types/FrappeCalendar.d.ts.map +1 -1
- package/types/FrappeConnectionIndicator.d.ts.map +1 -1
- package/types/FrappeDashboard.d.ts.map +1 -1
- package/types/FrappeGantt.d.ts.map +1 -1
- package/types/FrappeKanban.d.ts +3 -1
- package/types/FrappeKanban.d.ts.map +1 -1
- package/types/FrappeLikedBy.d.ts.map +1 -1
- package/types/FrappeList.d.ts.map +1 -1
- package/types/FrappeListView.d.ts.map +1 -1
- package/types/FrappeTable.d.ts.map +1 -1
- package/types/fields/Date/date.native.d.ts.map +1 -1
- package/types/fields/Date/index.d.ts.map +1 -1
- package/types/fields/Date/wire.d.ts +41 -0
- package/types/fields/Date/wire.d.ts.map +1 -0
- package/types/fields/ReadOnly/index.d.ts.map +1 -1
- package/types/fields/TableMultiSelect.d.ts.map +1 -1
- package/types/fields/TextEditor/index.d.ts.map +1 -1
- package/types/schemaForm/index.d.ts +9 -1
- package/types/schemaForm/index.d.ts.map +1 -1
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import {
|
|
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
|
-
*
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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 =
|
|
66
|
-
|
|
67
|
-
if (type === "
|
|
68
|
-
|
|
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 ?
|
|
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(
|
|
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(
|
|
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="
|
|
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="
|
|
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
|
|
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="
|
|
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="
|
|
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="
|
|
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
|
);
|
|
@@ -78,7 +78,7 @@ export const States = (args: StoryProps) => (
|
|
|
78
78
|
<Controlled {...args} label="Disabled" disabled />
|
|
79
79
|
<Controlled {...args} label="Disabled + error" disabled error="Broken while disabled" />
|
|
80
80
|
<Controlled {...args} label="Required" required />
|
|
81
|
-
<Controlled {...args} label="Error" error="
|
|
81
|
+
<Controlled {...args} label="Error" error="Value does not match the required format" />
|
|
82
82
|
<Controlled {...args} label="Helper text" helperText="Some helpful context" />
|
|
83
83
|
</YStack>
|
|
84
84
|
);
|
|
@@ -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="
|
|
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
|
);
|
|
@@ -50,7 +50,7 @@ export const States = (args: StoryProps) => (
|
|
|
50
50
|
<Controlled {...args} label="Disabled" disabled />
|
|
51
51
|
<Controlled {...args} label="Disabled + error" disabled error="Broken while disabled" />
|
|
52
52
|
<Controlled {...args} label="Required" required />
|
|
53
|
-
<Controlled {...args} label="Error" error="
|
|
53
|
+
<Controlled {...args} label="Error" error="Value does not match the required format" />
|
|
54
54
|
<Controlled {...args} label="Helper text" helperText="Some helpful context" />
|
|
55
55
|
</YStack>
|
|
56
56
|
);
|
|
@@ -47,7 +47,7 @@ export const States = (args: StoryProps) => (
|
|
|
47
47
|
<Controlled {...args} label="Disabled" disabled />
|
|
48
48
|
<Controlled {...args} label="Disabled + error" disabled error="Broken while disabled" />
|
|
49
49
|
<Controlled {...args} label="Required" required />
|
|
50
|
-
<Controlled {...args} label="Error" error="
|
|
50
|
+
<Controlled {...args} label="Error" error="Value does not match the required format" />
|
|
51
51
|
<Controlled {...args} label="Helper text" helperText="Some helpful context" />
|
|
52
52
|
</YStack>
|
|
53
53
|
);
|
|
@@ -39,7 +39,7 @@ export const States = (args: StoryProps) => (
|
|
|
39
39
|
<Controlled {...args} label="Disabled" disabled />
|
|
40
40
|
<Controlled {...args} label="Disabled + error" disabled error="Broken while disabled" />
|
|
41
41
|
<Controlled {...args} label="Required" required />
|
|
42
|
-
<Controlled {...args} label="Error" error="
|
|
42
|
+
<Controlled {...args} label="Error" error="Value does not match the required format" />
|
|
43
43
|
<Controlled {...args} label="Helper text" helperText="Some helpful context" />
|
|
44
44
|
</YStack>
|
|
45
45
|
);
|
|
@@ -54,7 +54,7 @@ export const States = (args: StoryProps) => (
|
|
|
54
54
|
<Controlled {...args} label="Disabled" disabled />
|
|
55
55
|
<Controlled {...args} label="Disabled + error" disabled error="Broken while disabled" />
|
|
56
56
|
<Controlled {...args} label="Required" required />
|
|
57
|
-
<Controlled {...args} label="Error" error="
|
|
57
|
+
<Controlled {...args} label="Error" error="Value does not match the required format" />
|
|
58
58
|
<Controlled {...args} label="Helper text" helperText="Some helpful context" />
|
|
59
59
|
</YStack>
|
|
60
60
|
);
|
|
@@ -50,7 +50,7 @@ export const States = (args: StoryProps) => (
|
|
|
50
50
|
<Controlled {...args} label="Disabled" disabled />
|
|
51
51
|
<Controlled {...args} label="Disabled + error" disabled error="Broken while disabled" />
|
|
52
52
|
<Controlled {...args} label="Required" required />
|
|
53
|
-
<Controlled {...args} label="Error" error="
|
|
53
|
+
<Controlled {...args} label="Error" error="Value does not match the required format" />
|
|
54
54
|
<Controlled {...args} label="Helper text" helperText="Some helpful context" />
|
|
55
55
|
</YStack>
|
|
56
56
|
);
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { formatDateText, formatNumericText, Skeleton } from "@multiplatform.one/forms";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
formatCurrency,
|
|
4
|
+
formatPercent,
|
|
5
|
+
formatTimeOfDay,
|
|
6
|
+
useResolvedKnobs,
|
|
7
|
+
} from "@multiplatform.one/theme";
|
|
3
8
|
import { useMemo } from "react";
|
|
4
9
|
import { Label, Text, XStack, YStack, type YStackProps } from "tamagui";
|
|
5
10
|
|
|
@@ -38,33 +43,15 @@ export interface ReadOnlyProps extends Omit<YStackProps, "children"> {
|
|
|
38
43
|
compact?: boolean;
|
|
39
44
|
}
|
|
40
45
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"£": "GBP",
|
|
48
|
-
"¥": "JPY",
|
|
49
|
-
"₹": "INR",
|
|
50
|
-
"₩": "KRW",
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
function resolveCurrencyCode(symbol: string): string | undefined {
|
|
54
|
-
if (/^[A-Za-z]{3}$/.test(symbol)) return symbol.toUpperCase();
|
|
55
|
-
return CURRENCY_SYMBOL_TO_CODE[symbol];
|
|
56
|
-
}
|
|
57
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Time cells speak the house time-of-day register (LC-12 render channel) —
|
|
48
|
+
* one voice whether the value arrives as a clock string ("14:30:00") or a
|
|
49
|
+
* datetime, instead of the wire string passing straight through in one
|
|
50
|
+
* branch and an en-GB conversion in the other.
|
|
51
|
+
*/
|
|
58
52
|
function formatTime(value: string, _format?: string): string {
|
|
59
53
|
if (!value) return "";
|
|
60
|
-
|
|
61
|
-
if (/^\d{2}:\d{2}(:\d{2})?$/.test(value)) return value.slice(0, 5);
|
|
62
|
-
const date = new Date(value);
|
|
63
|
-
if (Number.isNaN(date.getTime())) return value;
|
|
64
|
-
return date.toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
|
|
65
|
-
} catch {
|
|
66
|
-
return value;
|
|
67
|
-
}
|
|
54
|
+
return formatTimeOfDay(value);
|
|
68
55
|
}
|
|
69
56
|
|
|
70
57
|
function formatNumeric(
|
|
@@ -79,13 +66,12 @@ function formatNumeric(
|
|
|
79
66
|
return formatNumericText(Math.round(value), { precision: 0 });
|
|
80
67
|
case "Float":
|
|
81
68
|
return formatNumericText(value, { precision });
|
|
82
|
-
case "Currency":
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return
|
|
86
|
-
}
|
|
69
|
+
case "Currency":
|
|
70
|
+
// The money register resolves a symbol to its ISO code itself and
|
|
71
|
+
// prefixes anything it doesn't know.
|
|
72
|
+
return formatCurrency(value, { precision, symbol: currencySymbol });
|
|
87
73
|
case "Percent":
|
|
88
|
-
return
|
|
74
|
+
return formatPercent(value, { precision });
|
|
89
75
|
default:
|
|
90
76
|
return value.toString();
|
|
91
77
|
}
|
|
@@ -225,7 +211,11 @@ export function ReadOnly({
|
|
|
225
211
|
<XStack
|
|
226
212
|
width="$0.75"
|
|
227
213
|
height="$0.75"
|
|
228
|
-
|
|
214
|
+
// A read-only check box is R-SCALE like any other control: the
|
|
215
|
+
// pinned `$1` outlived the knob at `borderRadius:none` (LC-60).
|
|
216
|
+
// Radius-only fragment — the border/fill below are this part's own
|
|
217
|
+
// (LC-55 FRAGMENT-LICENSE).
|
|
218
|
+
{...knobProps.containerRadius}
|
|
229
219
|
borderWidth={1}
|
|
230
220
|
borderColor={value ? "$green10" : "$color8"}
|
|
231
221
|
backgroundColor={value ? "$green5" : "$color3"}
|