@ankhorage/zora 5.0.0 → 6.0.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/CHANGELOG.md +9 -0
- package/README.md +12 -9
- package/dist/components/button/resolveButtonThemeRecipe.d.ts +1 -1
- package/dist/components/date-picker/DatePicker.d.ts.map +1 -1
- package/dist/components/date-picker/DatePicker.js +92 -86
- package/dist/components/date-picker/DatePicker.js.map +1 -1
- package/dist/components/date-picker/meta.d.ts +1 -1
- package/dist/components/date-picker/meta.js +1 -1
- package/dist/components/date-picker/meta.js.map +1 -1
- package/dist/components/heading/resolveHeadingThemeRecipe.d.ts +1 -1
- package/dist/components/text/resolveTextThemeRecipe.d.ts +1 -1
- package/dist/components/time-picker/TimePicker.d.ts +1 -3
- package/dist/components/time-picker/TimePicker.d.ts.map +1 -1
- package/dist/components/time-picker/TimePicker.js +55 -32
- package/dist/components/time-picker/TimePicker.js.map +1 -1
- package/dist/components/time-picker/meta.d.ts +1 -1
- package/dist/components/time-picker/meta.js +1 -1
- package/dist/components/time-picker/meta.js.map +1 -1
- package/dist/features/bottom-sheet/public.d.ts +3 -0
- package/dist/features/bottom-sheet/public.d.ts.map +1 -0
- package/dist/features/bottom-sheet/public.js +2 -0
- package/dist/features/bottom-sheet/public.js.map +1 -0
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/metadata/componentMeta.d.ts.map +1 -1
- package/dist/metadata/componentMeta.js +0 -3
- package/dist/metadata/componentMeta.js.map +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +0 -3
- package/dist/registry.js.map +1 -1
- package/dist/theme/ZoraProvider.d.ts +1 -1
- package/dist/theme/ZoraProvider.d.ts.map +1 -1
- package/dist/theme/ZoraProvider.js +3 -2
- package/dist/theme/ZoraProvider.js.map +1 -1
- package/dist/theme/useZoraTheme.d.ts +1 -1
- package/package.json +16 -3
- package/src/components/date-picker/DatePicker.tsx +197 -129
- package/src/components/date-picker/meta.ts +1 -1
- package/src/components/time-picker/TimePicker.tsx +147 -60
- package/src/components/time-picker/meta.ts +1 -1
- package/src/expo57Migration.test.ts +56 -0
- package/src/features/bottom-sheet/public.test.ts +39 -0
- package/src/features/bottom-sheet/public.ts +5 -0
- package/src/index.ts +0 -2
- package/src/metadata/componentMeta.ts +0 -3
- package/src/platformAcceptance.test.ts +1 -1
- package/src/registry.ts +0 -3
- package/src/theme/ZoraProvider.tsx +3 -2
- package/dist/components/action-sheet/ActionSheet.d.ts +0 -7
- package/dist/components/action-sheet/ActionSheet.d.ts.map +0 -1
- package/dist/components/action-sheet/ActionSheet.js +0 -11
- package/dist/components/action-sheet/ActionSheet.js.map +0 -1
- package/dist/components/action-sheet/ActionSheetItem.d.ts +0 -7
- package/dist/components/action-sheet/ActionSheetItem.d.ts.map +0 -1
- package/dist/components/action-sheet/ActionSheetItem.js +0 -17
- package/dist/components/action-sheet/ActionSheetItem.js.map +0 -1
- package/dist/components/action-sheet/index.d.ts +0 -4
- package/dist/components/action-sheet/index.d.ts.map +0 -1
- package/dist/components/action-sheet/index.js +0 -3
- package/dist/components/action-sheet/index.js.map +0 -1
- package/dist/components/action-sheet/meta.d.ts +0 -17
- package/dist/components/action-sheet/meta.d.ts.map +0 -1
- package/dist/components/action-sheet/meta.js +0 -17
- package/dist/components/action-sheet/meta.js.map +0 -1
- package/dist/components/action-sheet/types.d.ts +0 -13
- package/dist/components/action-sheet/types.d.ts.map +0 -1
- package/dist/components/action-sheet/types.js +0 -2
- package/dist/components/action-sheet/types.js.map +0 -1
- package/src/components/action-sheet/ActionSheet.tsx +0 -14
- package/src/components/action-sheet/ActionSheetItem.tsx +0 -30
- package/src/components/action-sheet/index.ts +0 -3
- package/src/components/action-sheet/meta.ts +0 -19
- package/src/components/action-sheet/types.ts +0 -23
|
@@ -7,6 +7,6 @@ export interface ZoraProviderProps {
|
|
|
7
7
|
themeConfig?: ThemeConfig;
|
|
8
8
|
initialMode?: ZoraThemeMode;
|
|
9
9
|
}
|
|
10
|
-
/** Installs the ZORA theme runtime and
|
|
10
|
+
/** Installs the ZORA theme runtime, Surface theme, and shared bottom-sheet controller. */
|
|
11
11
|
export declare function ZoraProvider({ children, theme, themeConfig, initialMode, }: ZoraProviderProps): React.JSX.Element;
|
|
12
12
|
//# sourceMappingURL=ZoraProvider.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ZoraProvider.d.ts","sourceRoot":"","sources":["../../src/theme/ZoraProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"ZoraProvider.d.ts","sourceRoot":"","sources":["../../src/theme/ZoraProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAGxD,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAIxD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,WAAW,CAAC,EAAE,aAAa,CAAC;CAC7B;AAED,0FAA0F;AAC1F,wBAAgB,YAAY,CAAC,EAC3B,QAAQ,EACR,KAAwB,EACxB,WAAW,EACX,WAAqB,GACtB,EAAE,iBAAiB,qBAcnB"}
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { ThemeProvider } from '@ankhorage/surface';
|
|
2
|
+
import { BottomSheetProvider } from '@ankhorage/surface/bottom-sheet';
|
|
2
3
|
import React from 'react';
|
|
3
4
|
import { createZoraThemeConfig } from './createZoraThemeConfig';
|
|
4
5
|
import { zoraDefaultTheme } from './zoraDefaultTheme';
|
|
5
6
|
import { ZoraThemeRuntimeContext } from './ZoraThemeRuntimeContext';
|
|
6
|
-
/** Installs the ZORA theme runtime and
|
|
7
|
+
/** Installs the ZORA theme runtime, Surface theme, and shared bottom-sheet controller. */
|
|
7
8
|
export function ZoraProvider({ children, theme = zoraDefaultTheme, themeConfig, initialMode = 'light', }) {
|
|
8
9
|
const resolvedConfig = React.useMemo(() => themeConfig ?? createZoraThemeConfig(theme), [theme, themeConfig]);
|
|
9
10
|
const runtimeValue = React.useMemo(() => ({ themeId: resolvedConfig.id }), [resolvedConfig.id]);
|
|
10
11
|
return (<ZoraThemeRuntimeContext.Provider value={runtimeValue}>
|
|
11
12
|
<ThemeProvider initialConfig={resolvedConfig} initialMode={initialMode}>
|
|
12
|
-
{children}
|
|
13
|
+
<BottomSheetProvider>{children}</BottomSheetProvider>
|
|
13
14
|
</ThemeProvider>
|
|
14
15
|
</ZoraThemeRuntimeContext.Provider>);
|
|
15
16
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ZoraProvider.js","sourceRoot":"","sources":["../../src/theme/ZoraProvider.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAEhE,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AASpE,
|
|
1
|
+
{"version":3,"file":"ZoraProvider.js","sourceRoot":"","sources":["../../src/theme/ZoraProvider.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AACtE,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAEhE,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AASpE,0FAA0F;AAC1F,MAAM,UAAU,YAAY,CAAC,EAC3B,QAAQ,EACR,KAAK,GAAG,gBAAgB,EACxB,WAAW,EACX,WAAW,GAAG,OAAO,GACH;IAClB,MAAM,cAAc,GAAG,KAAK,CAAC,OAAO,CAClC,GAAG,EAAE,CAAC,WAAW,IAAI,qBAAqB,CAAC,KAAK,CAAC,EACjD,CAAC,KAAK,EAAE,WAAW,CAAC,CACrB,CAAC;IACF,MAAM,YAAY,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,cAAc,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC,CAAC;IAEhG,OAAO,CACL,CAAC,uBAAuB,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,YAAY,CAAC,CACpD;MAAA,CAAC,aAAa,CAAC,aAAa,CAAC,CAAC,cAAc,CAAC,CAAC,WAAW,CAAC,CAAC,WAAW,CAAC,CACrE;QAAA,CAAC,mBAAmB,CAAC,CAAC,QAAQ,CAAC,EAAE,mBAAmB,CACtD;MAAA,EAAE,aAAa,CACjB;IAAA,EAAE,uBAAuB,CAAC,QAAQ,CAAC,CACpC,CAAC;AACJ,CAAC","sourcesContent":["import type { ThemeConfig } from '@ankhorage/contracts';\nimport { ThemeProvider } from '@ankhorage/surface';\nimport { BottomSheetProvider } from '@ankhorage/surface/bottom-sheet';\nimport React from 'react';\n\nimport { createZoraThemeConfig } from './createZoraThemeConfig';\nimport type { ZoraTheme, ZoraThemeMode } from './types';\nimport { zoraDefaultTheme } from './zoraDefaultTheme';\nimport { ZoraThemeRuntimeContext } from './ZoraThemeRuntimeContext';\n\nexport interface ZoraProviderProps {\n children: React.ReactNode;\n theme?: ZoraTheme;\n themeConfig?: ThemeConfig;\n initialMode?: ZoraThemeMode;\n}\n\n/** Installs the ZORA theme runtime, Surface theme, and shared bottom-sheet controller. */\nexport function ZoraProvider({\n children,\n theme = zoraDefaultTheme,\n themeConfig,\n initialMode = 'light',\n}: ZoraProviderProps) {\n const resolvedConfig = React.useMemo(\n () => themeConfig ?? createZoraThemeConfig(theme),\n [theme, themeConfig],\n );\n const runtimeValue = React.useMemo(() => ({ themeId: resolvedConfig.id }), [resolvedConfig.id]);\n\n return (\n <ZoraThemeRuntimeContext.Provider value={runtimeValue}>\n <ThemeProvider initialConfig={resolvedConfig} initialMode={initialMode}>\n <BottomSheetProvider>{children}</BottomSheetProvider>\n </ThemeProvider>\n </ZoraThemeRuntimeContext.Provider>\n );\n}\n"]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export declare function useZoraTheme(): {
|
|
2
2
|
theme: import("@ankhorage/surface").SurfaceTheme;
|
|
3
3
|
mode: "light" | "dark";
|
|
4
|
-
setThemeConfig: (config: Partial<import("@ankhorage/
|
|
4
|
+
setThemeConfig: (config: Partial<import("@ankhorage/contracts").ThemeConfig>) => void;
|
|
5
5
|
setMode: (mode: "light" | "dark") => void;
|
|
6
6
|
_hasProvider?: boolean;
|
|
7
7
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ankhorage/zora",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "6.0.0",
|
|
5
5
|
"description": "Opinionated React Native and React Native Web UI kit built on @ankhorage/surface.",
|
|
6
6
|
"homepage": "https://github.com/ankhorage/zora#readme",
|
|
7
7
|
"bugs": {
|
|
@@ -32,14 +32,17 @@
|
|
|
32
32
|
"@react-native-vector-icons/ionicons": "^13.1.3",
|
|
33
33
|
"react": "19.2.3",
|
|
34
34
|
"react-native": "0.86.x",
|
|
35
|
+
"react-native-gesture-handler": "~2.32.0",
|
|
36
|
+
"react-native-reanimated": "4.5.1",
|
|
35
37
|
"react-native-safe-area-context": "~5.7.0",
|
|
36
38
|
"react-native-svg": "15.15.4",
|
|
37
|
-
"react-native-web": "~0.21.0"
|
|
39
|
+
"react-native-web": "~0.21.0",
|
|
40
|
+
"react-native-worklets": "0.10.1"
|
|
38
41
|
},
|
|
39
42
|
"dependencies": {
|
|
40
43
|
"@ankhorage/color-theory": "^0.3.0",
|
|
41
44
|
"@ankhorage/contracts": "^12.0.1",
|
|
42
|
-
"@ankhorage/surface": "^
|
|
45
|
+
"@ankhorage/surface": "^4.1.0"
|
|
43
46
|
},
|
|
44
47
|
"files": [
|
|
45
48
|
"dist",
|
|
@@ -57,6 +60,13 @@
|
|
|
57
60
|
"import": "./dist/index.js",
|
|
58
61
|
"default": "./dist/index.js"
|
|
59
62
|
},
|
|
63
|
+
"./bottom-sheet": {
|
|
64
|
+
"react-native": "./src/features/bottom-sheet/public.ts",
|
|
65
|
+
"browser": "./src/features/bottom-sheet/public.ts",
|
|
66
|
+
"types": "./dist/features/bottom-sheet/public.d.ts",
|
|
67
|
+
"import": "./dist/features/bottom-sheet/public.js",
|
|
68
|
+
"default": "./dist/features/bottom-sheet/public.js"
|
|
69
|
+
},
|
|
60
70
|
"./metadata": {
|
|
61
71
|
"react-native": "./src/metadata/index.ts",
|
|
62
72
|
"browser": "./src/metadata/index.ts",
|
|
@@ -111,9 +121,12 @@
|
|
|
111
121
|
"react": "19.2.3",
|
|
112
122
|
"react-dom": "19.2.3",
|
|
113
123
|
"react-native": "0.86.3",
|
|
124
|
+
"react-native-gesture-handler": "~2.32.0",
|
|
125
|
+
"react-native-reanimated": "4.5.1",
|
|
114
126
|
"react-native-safe-area-context": "~5.7.0",
|
|
115
127
|
"react-native-svg": "15.15.4",
|
|
116
128
|
"react-native-web": "~0.21.0",
|
|
129
|
+
"react-native-worklets": "0.10.1",
|
|
117
130
|
"typescript": "~6.0.3"
|
|
118
131
|
},
|
|
119
132
|
"packageManager": "bun@1.4.2"
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Field } from '@ankhorage/surface';
|
|
2
2
|
import React from 'react';
|
|
3
3
|
|
|
4
|
+
import { useBottomSheet } from '../../features/bottom-sheet/public';
|
|
4
5
|
import { Box, Stack } from '../../foundation';
|
|
5
6
|
import { withZoraThemeScope } from '../../theme/withZoraThemeScope';
|
|
6
|
-
import { ActionSheet } from '../action-sheet';
|
|
7
7
|
import { Button } from '../button';
|
|
8
8
|
import { Text } from '../text';
|
|
9
9
|
import type { DatePickerProps } from './types';
|
|
@@ -11,6 +11,7 @@ import type { DatePickerProps } from './types';
|
|
|
11
11
|
const WEEKDAY_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] as const;
|
|
12
12
|
const DAY_CELL_WIDTH = 42;
|
|
13
13
|
|
|
14
|
+
/*** Render the field label and optional supporting description. */
|
|
14
15
|
function renderLabel(label: React.ReactNode, description: React.ReactNode | undefined) {
|
|
15
16
|
return (
|
|
16
17
|
<Stack gap="xs">
|
|
@@ -26,75 +27,70 @@ function renderLabel(label: React.ReactNode, description: React.ReactNode | unde
|
|
|
26
27
|
);
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
/*** Normalize a date to the start of its local calendar day. */
|
|
29
31
|
function startOfLocalDay(value: Date): Date {
|
|
30
32
|
return new Date(value.getFullYear(), value.getMonth(), value.getDate());
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
/*** Compare two values by local calendar day. */
|
|
33
36
|
function isSameLocalDay(left: Date | null, right: Date): boolean {
|
|
34
|
-
if (!left)
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
|
-
|
|
37
|
+
if (!left) return false;
|
|
38
38
|
return startOfLocalDay(left).getTime() === startOfLocalDay(right).getTime();
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/*** Report whether one local calendar day precedes another. */
|
|
41
42
|
function isBeforeLocalDay(left: Date, right: Date): boolean {
|
|
42
43
|
return startOfLocalDay(left).getTime() < startOfLocalDay(right).getTime();
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
/*** Report whether one local calendar day follows another. */
|
|
45
47
|
function isAfterLocalDay(left: Date, right: Date): boolean {
|
|
46
48
|
return startOfLocalDay(left).getTime() > startOfLocalDay(right).getTime();
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
/*** Resolve whether a date falls outside the configured picker range. */
|
|
49
52
|
function isDateDisabled(
|
|
50
53
|
value: Date,
|
|
51
54
|
minDate: Date | undefined,
|
|
52
55
|
maxDate: Date | undefined,
|
|
53
56
|
): boolean {
|
|
54
|
-
if (minDate && isBeforeLocalDay(value, minDate))
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (maxDate && isAfterLocalDay(value, maxDate)) {
|
|
59
|
-
return true;
|
|
60
|
-
}
|
|
61
|
-
|
|
57
|
+
if (minDate && isBeforeLocalDay(value, minDate)) return true;
|
|
58
|
+
if (maxDate && isAfterLocalDay(value, maxDate)) return true;
|
|
62
59
|
return false;
|
|
63
60
|
}
|
|
64
61
|
|
|
62
|
+
/*** Format the current calendar month for the picker heading. */
|
|
65
63
|
function formatMonthLabel(value: Date): string {
|
|
66
64
|
return value.toLocaleDateString(undefined, { month: 'long', year: 'numeric' });
|
|
67
65
|
}
|
|
68
66
|
|
|
67
|
+
/*** Format a selected date with the platform locale defaults. */
|
|
69
68
|
function formatDefaultDate(value: Date): string {
|
|
70
69
|
return value.toLocaleDateString();
|
|
71
70
|
}
|
|
72
71
|
|
|
72
|
+
/*** Build the padded calendar grid for one month. */
|
|
73
73
|
function createMonthDays(month: Date): (Date | null)[] {
|
|
74
74
|
const firstDay = new Date(month.getFullYear(), month.getMonth(), 1);
|
|
75
75
|
const dayCount = new Date(month.getFullYear(), month.getMonth() + 1, 0).getDate();
|
|
76
76
|
const days: (Date | null)[] = [];
|
|
77
77
|
|
|
78
|
-
for (let offset = 0; offset < firstDay.getDay(); offset += 1)
|
|
79
|
-
days.push(null);
|
|
80
|
-
}
|
|
81
|
-
|
|
78
|
+
for (let offset = 0; offset < firstDay.getDay(); offset += 1) days.push(null);
|
|
82
79
|
for (let day = 1; day <= dayCount; day += 1) {
|
|
83
80
|
days.push(new Date(month.getFullYear(), month.getMonth(), day));
|
|
84
81
|
}
|
|
85
|
-
|
|
86
|
-
while (days.length % 7 !== 0) {
|
|
87
|
-
days.push(null);
|
|
88
|
-
}
|
|
82
|
+
while (days.length % 7 !== 0) days.push(null);
|
|
89
83
|
|
|
90
84
|
return days;
|
|
91
85
|
}
|
|
92
86
|
|
|
87
|
+
/*** Resolve the month initially shown by the calendar sheet. */
|
|
93
88
|
function resolveInitialMonth(value: Date | null, minDate: Date | undefined): Date {
|
|
94
89
|
const base = value ?? minDate ?? new Date();
|
|
95
90
|
return new Date(base.getFullYear(), base.getMonth(), 1);
|
|
96
91
|
}
|
|
97
92
|
|
|
93
|
+
/*** Report whether the configured range intersects a candidate month. */
|
|
98
94
|
function canNavigateToMonth(
|
|
99
95
|
month: Date,
|
|
100
96
|
minDate: Date | undefined,
|
|
@@ -103,14 +99,8 @@ function canNavigateToMonth(
|
|
|
103
99
|
const firstDay = new Date(month.getFullYear(), month.getMonth(), 1);
|
|
104
100
|
const lastDay = new Date(month.getFullYear(), month.getMonth() + 1, 0);
|
|
105
101
|
|
|
106
|
-
if (maxDate && isAfterLocalDay(firstDay, maxDate))
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (minDate && isBeforeLocalDay(lastDay, minDate)) {
|
|
111
|
-
return false;
|
|
112
|
-
}
|
|
113
|
-
|
|
102
|
+
if (maxDate && isAfterLocalDay(firstDay, maxDate)) return false;
|
|
103
|
+
if (minDate && isBeforeLocalDay(lastDay, minDate)) return false;
|
|
114
104
|
return true;
|
|
115
105
|
}
|
|
116
106
|
|
|
@@ -131,35 +121,35 @@ function DatePickerInner({
|
|
|
131
121
|
testID,
|
|
132
122
|
interactionPolicy,
|
|
133
123
|
}: DatePickerProps) {
|
|
124
|
+
const { dismiss, present } = useBottomSheet();
|
|
134
125
|
const passive = interactionPolicy === 'passive';
|
|
135
|
-
const [visible, setVisible] = React.useState(false);
|
|
136
|
-
const [fallbackMonth] = React.useState(() => resolveInitialMonth(null, undefined));
|
|
137
|
-
const initialMonth = value || minDate ? resolveInitialMonth(value, minDate) : fallbackMonth;
|
|
138
|
-
const initialMonthKey = initialMonth.getTime();
|
|
139
|
-
const [displayMonthState, setDisplayMonthState] = React.useState(() => ({
|
|
140
|
-
sourceMonthKey: initialMonthKey,
|
|
141
|
-
month: initialMonth,
|
|
142
|
-
}));
|
|
143
|
-
const displayMonth =
|
|
144
|
-
displayMonthState.sourceMonthKey === initialMonthKey ? displayMonthState.month : initialMonth;
|
|
145
|
-
const monthDays = React.useMemo(() => createMonthDays(displayMonth), [displayMonth]);
|
|
146
|
-
const previousMonth = new Date(displayMonth.getFullYear(), displayMonth.getMonth() - 1, 1);
|
|
147
|
-
const nextMonth = new Date(displayMonth.getFullYear(), displayMonth.getMonth() + 1, 1);
|
|
148
126
|
const displayValue = value
|
|
149
127
|
? formatDate
|
|
150
128
|
? formatDate(value)
|
|
151
129
|
: formatDefaultDate(value)
|
|
152
130
|
: placeholder;
|
|
153
131
|
|
|
154
|
-
|
|
155
|
-
setDisplayMonthState({ sourceMonthKey: initialMonthKey, month });
|
|
156
|
-
}
|
|
157
|
-
|
|
132
|
+
/*** Present the calendar content through the shared BottomSheet controller. */
|
|
158
133
|
function openPicker() {
|
|
159
134
|
if (passive) return;
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
135
|
+
present({
|
|
136
|
+
content: (
|
|
137
|
+
<DatePickerSheet
|
|
138
|
+
description={description}
|
|
139
|
+
interactionPolicy={interactionPolicy}
|
|
140
|
+
label={label}
|
|
141
|
+
maxDate={maxDate}
|
|
142
|
+
minDate={minDate}
|
|
143
|
+
onDismiss={dismiss}
|
|
144
|
+
onSelect={(date) => {
|
|
145
|
+
onValueChange?.(startOfLocalDay(date));
|
|
146
|
+
dismiss();
|
|
147
|
+
}}
|
|
148
|
+
testID={testID}
|
|
149
|
+
value={value}
|
|
150
|
+
/>
|
|
151
|
+
),
|
|
152
|
+
});
|
|
163
153
|
}
|
|
164
154
|
|
|
165
155
|
return (
|
|
@@ -181,95 +171,173 @@ function DatePickerInner({
|
|
|
181
171
|
>
|
|
182
172
|
{displayValue}
|
|
183
173
|
</Button>
|
|
184
|
-
|
|
185
|
-
|
|
174
|
+
</Field>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
interface DatePickerSheetProps extends Pick<
|
|
179
|
+
DatePickerProps,
|
|
180
|
+
'description' | 'interactionPolicy' | 'label' | 'maxDate' | 'minDate' | 'testID' | 'value'
|
|
181
|
+
> {
|
|
182
|
+
onDismiss: () => void;
|
|
183
|
+
onSelect: (value: Date) => void;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/*** Render the calendar-specific content inside the shared BottomSheet host. */
|
|
187
|
+
function DatePickerSheet({
|
|
188
|
+
description,
|
|
189
|
+
interactionPolicy,
|
|
190
|
+
label,
|
|
191
|
+
maxDate,
|
|
192
|
+
minDate,
|
|
193
|
+
onDismiss,
|
|
194
|
+
onSelect,
|
|
195
|
+
testID,
|
|
196
|
+
value,
|
|
197
|
+
}: DatePickerSheetProps) {
|
|
198
|
+
const [displayMonth, setDisplayMonth] = React.useState(() => resolveInitialMonth(value, minDate));
|
|
199
|
+
const monthDays = React.useMemo(() => createMonthDays(displayMonth), [displayMonth]);
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<Stack gap="m" p="m" testID={testID ? `${testID}-sheet` : undefined}>
|
|
203
|
+
<PickerSheetHeader description={description} title={label ?? 'Choose date'} />
|
|
204
|
+
<DatePickerMonthNavigation
|
|
205
|
+
displayMonth={displayMonth}
|
|
186
206
|
interactionPolicy={interactionPolicy}
|
|
187
|
-
|
|
188
|
-
|
|
207
|
+
maxDate={maxDate}
|
|
208
|
+
minDate={minDate}
|
|
209
|
+
onMonthChange={setDisplayMonth}
|
|
210
|
+
/>
|
|
211
|
+
<DatePickerWeekdays />
|
|
212
|
+
<DatePickerCalendar
|
|
213
|
+
interactionPolicy={interactionPolicy}
|
|
214
|
+
maxDate={maxDate}
|
|
215
|
+
minDate={minDate}
|
|
216
|
+
monthDays={monthDays}
|
|
217
|
+
onSelect={onSelect}
|
|
218
|
+
testID={testID}
|
|
219
|
+
value={value}
|
|
220
|
+
/>
|
|
221
|
+
<Button fullWidth interactionPolicy={interactionPolicy} onPress={onDismiss} variant="ghost">
|
|
222
|
+
Cancel
|
|
223
|
+
</Button>
|
|
224
|
+
</Stack>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/*** Render the shared title block used by the calendar sheet. */
|
|
229
|
+
function PickerSheetHeader({
|
|
230
|
+
description,
|
|
231
|
+
title,
|
|
232
|
+
}: {
|
|
233
|
+
description: React.ReactNode | undefined;
|
|
234
|
+
title: React.ReactNode;
|
|
235
|
+
}) {
|
|
236
|
+
return (
|
|
237
|
+
<Stack gap="xxs">
|
|
238
|
+
<Text align="center" variant="label" weight="semiBold">
|
|
239
|
+
{title}
|
|
240
|
+
</Text>
|
|
241
|
+
{description ? (
|
|
242
|
+
<Text align="center" emphasis="muted" variant="bodySmall">
|
|
243
|
+
{description}
|
|
244
|
+
</Text>
|
|
245
|
+
) : null}
|
|
246
|
+
</Stack>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/*** Render previous/next month controls for the calendar sheet. */
|
|
251
|
+
function DatePickerMonthNavigation({
|
|
252
|
+
displayMonth,
|
|
253
|
+
interactionPolicy,
|
|
254
|
+
maxDate,
|
|
255
|
+
minDate,
|
|
256
|
+
onMonthChange,
|
|
257
|
+
}: Pick<DatePickerSheetProps, 'interactionPolicy' | 'maxDate' | 'minDate'> & {
|
|
258
|
+
displayMonth: Date;
|
|
259
|
+
onMonthChange: (month: Date) => void;
|
|
260
|
+
}) {
|
|
261
|
+
const previousMonth = new Date(displayMonth.getFullYear(), displayMonth.getMonth() - 1, 1);
|
|
262
|
+
const nextMonth = new Date(displayMonth.getFullYear(), displayMonth.getMonth() + 1, 1);
|
|
189
263
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
264
|
+
return (
|
|
265
|
+
<Stack align="center" direction="row" justify="space-between">
|
|
266
|
+
<Button
|
|
267
|
+
disabled={!canNavigateToMonth(previousMonth, minDate, maxDate)}
|
|
268
|
+
interactionPolicy={interactionPolicy}
|
|
269
|
+
onPress={() => onMonthChange(previousMonth)}
|
|
270
|
+
size="s"
|
|
271
|
+
variant="ghost"
|
|
195
272
|
>
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
273
|
+
Previous
|
|
274
|
+
</Button>
|
|
275
|
+
<Text align="center" variant="label" weight="semiBold">
|
|
276
|
+
{formatMonthLabel(displayMonth)}
|
|
277
|
+
</Text>
|
|
278
|
+
<Button
|
|
279
|
+
disabled={!canNavigateToMonth(nextMonth, minDate, maxDate)}
|
|
280
|
+
interactionPolicy={interactionPolicy}
|
|
281
|
+
onPress={() => onMonthChange(nextMonth)}
|
|
282
|
+
size="s"
|
|
283
|
+
variant="ghost"
|
|
284
|
+
>
|
|
285
|
+
Next
|
|
286
|
+
</Button>
|
|
287
|
+
</Stack>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
203
290
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
291
|
+
/*** Render weekday headings for the calendar grid. */
|
|
292
|
+
function DatePickerWeekdays() {
|
|
293
|
+
return (
|
|
294
|
+
<Stack direction="row" justify="space-between">
|
|
295
|
+
{WEEKDAY_LABELS.map((weekday) => (
|
|
296
|
+
<Box key={weekday} width={DAY_CELL_WIDTH}>
|
|
297
|
+
<Text align="center" emphasis="muted" variant="caption" weight="semiBold">
|
|
298
|
+
{weekday}
|
|
299
|
+
</Text>
|
|
300
|
+
</Box>
|
|
301
|
+
))}
|
|
302
|
+
</Stack>
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/*** Render selectable day cells for the current calendar month. */
|
|
307
|
+
function DatePickerCalendar({
|
|
308
|
+
interactionPolicy,
|
|
309
|
+
maxDate,
|
|
310
|
+
minDate,
|
|
311
|
+
monthDays,
|
|
312
|
+
onSelect,
|
|
313
|
+
testID,
|
|
314
|
+
value,
|
|
315
|
+
}: Pick<DatePickerSheetProps, 'interactionPolicy' | 'maxDate' | 'minDate' | 'testID' | 'value'> & {
|
|
316
|
+
monthDays: readonly (Date | null)[];
|
|
317
|
+
onSelect: (value: Date) => void;
|
|
318
|
+
}) {
|
|
319
|
+
return (
|
|
320
|
+
<Stack direction="row" gap="xs" wrap="wrap">
|
|
321
|
+
{monthDays.map((day, index) => {
|
|
322
|
+
if (!day) return <Box key={`empty-${index}`} width={DAY_CELL_WIDTH} />;
|
|
323
|
+
const selected = isSameLocalDay(value, day);
|
|
324
|
+
return (
|
|
325
|
+
<Box key={day.toISOString()} width={DAY_CELL_WIDTH}>
|
|
214
326
|
<Button
|
|
215
|
-
|
|
327
|
+
color={selected ? 'primary' : 'neutral'}
|
|
328
|
+
disabled={isDateDisabled(day, minDate, maxDate)}
|
|
216
329
|
interactionPolicy={interactionPolicy}
|
|
217
|
-
onPress={() =>
|
|
218
|
-
if (passive) return;
|
|
219
|
-
|
|
220
|
-
setDisplayMonth(nextMonth);
|
|
221
|
-
}}
|
|
330
|
+
onPress={() => onSelect(day)}
|
|
222
331
|
size="s"
|
|
223
|
-
|
|
332
|
+
testID={testID ? `${testID}-day-${day.getDate()}` : undefined}
|
|
333
|
+
variant={selected ? 'solid' : 'ghost'}
|
|
224
334
|
>
|
|
225
|
-
|
|
335
|
+
{day.getDate()}
|
|
226
336
|
</Button>
|
|
227
|
-
</
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
<Box key={weekday} style={{ width: DAY_CELL_WIDTH }}>
|
|
232
|
-
<Text align="center" emphasis="muted" variant="caption" weight="semiBold">
|
|
233
|
-
{weekday}
|
|
234
|
-
</Text>
|
|
235
|
-
</Box>
|
|
236
|
-
))}
|
|
237
|
-
</Stack>
|
|
238
|
-
|
|
239
|
-
<Stack direction="row" gap="xs" style={{ flexWrap: 'wrap' }}>
|
|
240
|
-
{monthDays.map((day, index) => {
|
|
241
|
-
if (!day) {
|
|
242
|
-
return <Box key={`empty-${index}`} style={{ width: DAY_CELL_WIDTH }} />;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const selected = isSameLocalDay(value, day);
|
|
246
|
-
const dayDisabled = isDateDisabled(day, minDate, maxDate);
|
|
247
|
-
|
|
248
|
-
return (
|
|
249
|
-
<Box key={day.toISOString()} style={{ width: DAY_CELL_WIDTH }}>
|
|
250
|
-
<Button
|
|
251
|
-
color={selected ? 'primary' : 'neutral'}
|
|
252
|
-
disabled={dayDisabled}
|
|
253
|
-
interactionPolicy={interactionPolicy}
|
|
254
|
-
onPress={() => {
|
|
255
|
-
if (passive) return;
|
|
256
|
-
|
|
257
|
-
onValueChange?.(startOfLocalDay(day));
|
|
258
|
-
setVisible(false);
|
|
259
|
-
}}
|
|
260
|
-
size="s"
|
|
261
|
-
testID={testID ? `${testID}-day-${day.getDate()}` : undefined}
|
|
262
|
-
variant={selected ? 'solid' : 'ghost'}
|
|
263
|
-
>
|
|
264
|
-
{day.getDate()}
|
|
265
|
-
</Button>
|
|
266
|
-
</Box>
|
|
267
|
-
);
|
|
268
|
-
})}
|
|
269
|
-
</Stack>
|
|
270
|
-
</Stack>
|
|
271
|
-
</ActionSheet>
|
|
272
|
-
</Field>
|
|
337
|
+
</Box>
|
|
338
|
+
);
|
|
339
|
+
})}
|
|
340
|
+
</Stack>
|
|
273
341
|
);
|
|
274
342
|
}
|
|
275
343
|
|
|
@@ -5,6 +5,6 @@ export const datePickerMeta = {
|
|
|
5
5
|
category: 'component',
|
|
6
6
|
directManifestNode: false,
|
|
7
7
|
allowedChildren: [],
|
|
8
|
-
note: 'Code-facing
|
|
8
|
+
note: 'Code-facing BottomSheet-backed date picker; not represented as a manifest node in v1.',
|
|
9
9
|
props: {},
|
|
10
10
|
} as const satisfies ZoraComponentMeta;
|