@ankhorage/zora 25.9.1 → 25.10.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 +13 -0
- package/README.md +1 -1
- package/dist/capabilities/index.js +12 -3
- package/dist/capabilities/index.js.map +6 -5
- package/dist/features/calendar/public.d.ts +2 -0
- package/dist/features/calendar/public.d.ts.map +1 -0
- package/dist/features/calendar/public.js +2 -0
- package/dist/features/calendar/public.js.map +1 -0
- package/dist/features/list/adapters/inbound/FlatList.d.ts.map +1 -1
- package/dist/features/list/adapters/inbound/FlatList.js +2 -1
- package/dist/features/list/adapters/inbound/FlatList.js.map +1 -1
- package/dist/features/list/adapters/inbound/SectionList.d.ts.map +1 -1
- package/dist/features/list/adapters/inbound/SectionList.js +2 -1
- package/dist/features/list/adapters/inbound/SectionList.js.map +1 -1
- package/dist/features/list/constants/virtualization.d.ts +6 -0
- package/dist/features/list/constants/virtualization.d.ts.map +1 -0
- package/dist/features/list/constants/virtualization.js +6 -0
- package/dist/features/list/constants/virtualization.js.map +1 -0
- package/dist/features/list/flatListMeta.d.ts +4 -0
- package/dist/features/list/flatListMeta.d.ts.map +1 -1
- package/dist/features/list/flatListMeta.js +2 -1
- package/dist/features/list/flatListMeta.js.map +1 -1
- package/dist/features/list/sectionListMeta.d.ts +2 -0
- package/dist/features/list/sectionListMeta.d.ts.map +1 -1
- package/dist/features/list/sectionListMeta.js +4 -1
- package/dist/features/list/sectionListMeta.js.map +1 -1
- package/dist/readerFree.d.ts +1 -0
- package/dist/readerFree.d.ts.map +1 -1
- package/dist/readerFree.js.map +1 -1
- package/dist/types/temporal.d.ts +147 -0
- package/dist/types/temporal.d.ts.map +1 -0
- package/dist/types/temporal.js +2 -0
- package/dist/types/temporal.js.map +1 -0
- package/package.json +2 -2
- package/src/features/calendar/public.ts +22 -0
- package/src/features/list/adapters/inbound/FlatList.tsx +3 -2
- package/src/features/list/adapters/inbound/SectionList.tsx +3 -2
- package/src/features/list/adapters/inbound/virtualizedLists.test.tsx +90 -0
- package/src/features/list/constants/virtualization.ts +5 -0
- package/src/features/list/flatListMeta.ts +2 -1
- package/src/features/list/sectionListMeta.ts +4 -1
- package/src/features/list/virtualizationDefaults.test.ts +24 -0
- package/src/readerFree.ts +22 -0
- package/src/types/temporal.test.ts +88 -0
- package/src/types/temporal.ts +184 -0
- package/web-dist/chunks/{Accordion-wf60fekp.js → Accordion-rkg1y7x3.js} +10 -4
- package/web-dist/chunks/{Accordion-nsamv0p0.js → Accordion-xt8vafkn.js} +1 -1
- package/web-dist/components/data-table/DataTable.js +2 -2
- package/web-dist/components/flat-list/FlatList.js +1 -1
- package/web-dist/components/list/List.js +1 -1
- package/web-dist/components/list-item/ListItem.js +1 -1
- package/web-dist/components/list-section/ListSection.js +1 -1
- package/web-dist/components/section-list/SectionList.js +1 -1
- package/web-dist/components/skeleton/Skeleton.js +2 -2
- package/web-dist/components/skeleton-card/SkeletonCard.js +2 -2
- package/web-dist/components/skeleton-list/SkeletonList.js +2 -2
- package/web-dist/components/skeleton-text/SkeletonText.js +2 -2
- package/web-dist/components/tree-item/TreeItem.js +1 -1
- package/web-dist/manifest.json +16 -16
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"temporal.js","sourceRoot":"","sources":["../../src/types/temporal.ts"],"names":[],"mappings":"","sourcesContent":["/** A calendar date without a time or timezone, represented as `YYYY-MM-DD`. */\nexport type TemporalDate = `${number}-${number}-${number}`;\n\n/** A UTC ISO-8601 instant, represented as an RFC 3339 string ending in `Z`. */\nexport type TemporalInstant = `${string}Z`;\n\n/** An IANA timezone identifier such as `Europe/Zurich`. */\nexport type TemporalTimeZone = string;\n\n/** An ISO-8601 duration string such as `PT30M` or `-PT15M`. */\nexport type TemporalDuration = string;\n\n/**\n * This contract describes serializable shape, not runtime validation. Consumers must validate\n * RFC 3339 instants, ISO calendar dates and durations, IANA zone identifiers, positive non-zero\n * intervals, ordered ranges, and finite JSON numbers at their input boundary.\n */\nexport type TemporalDataValue =\n | boolean\n | null\n | number\n | string\n | readonly TemporalDataValue[]\n | { readonly [key: string]: TemporalDataValue };\n\n/**\n * A timed interval with exactly one exclusive end. Recurring timed events retain the local\n * wall-clock time derived from `start` in `timeZone` across DST transitions; a recurrence engine\n * materializes each occurrence as its corresponding UTC instant.\n */\nexport type TemporalTimedRange =\n | {\n readonly allDay: false;\n readonly start: TemporalInstant;\n readonly end: TemporalInstant;\n readonly duration?: never;\n readonly timeZone: TemporalTimeZone;\n }\n | {\n readonly allDay: false;\n readonly start: TemporalInstant;\n readonly end?: never;\n readonly duration: TemporalDuration;\n readonly timeZone: TemporalTimeZone;\n };\n\n/**\n * A timezone-free all-day calendar-date interval with exactly one exclusive end. `end` is the\n * first day not included, so a one-day event from `2026-10-24` ends on `2026-10-25`.\n */\nexport type TemporalAllDayRange =\n | {\n readonly allDay: true;\n readonly start: TemporalDate;\n readonly end: TemporalDate;\n readonly duration?: never;\n }\n | {\n readonly allDay: true;\n readonly start: TemporalDate;\n readonly end?: never;\n readonly duration: TemporalDuration;\n };\n\n/** A timed or all-day interval with exactly one way to define its exclusive end. */\nexport type TemporalRange = TemporalAllDayRange | TemporalTimedRange;\n\n/** The availability an event contributes without disclosing its details. */\nexport type TemporalAvailability = 'free' | 'busy' | 'tentative' | 'unavailable';\n\n/** A provider-neutral lifecycle state for a scheduled event. */\nexport type TemporalEventStatus = 'cancelled' | 'confirmed' | 'draft' | 'tentative';\n\n/** A semantic theme treatment that later ZORA renderers resolve through their active theme. */\nexport type TemporalTone = 'accent' | 'critical' | 'info' | 'neutral' | 'positive' | 'warning';\n\n/** A person or organization participating in an event without provider-specific identity objects. */\nexport interface TemporalParticipant {\n readonly id?: string;\n readonly name?: string;\n readonly email?: string;\n readonly role?: 'optional' | 'organizer' | 'required' | 'resource';\n readonly response?: 'accepted' | 'declined' | 'delegated' | 'needs-action' | 'tentative';\n}\n\n/**\n * A reminder relative to the event start or exclusive end. A negative ISO duration fires before\n * that reference point; a positive duration fires after it.\n */\nexport interface TemporalReminder {\n readonly id: string;\n readonly offset: TemporalDuration;\n readonly reference: 'end' | 'start';\n readonly channel?: 'display' | 'email' | 'push';\n}\n\n/**\n * One normalized recurrence rule. `until` has the same value type as DTSTART: a date for all-day\n * series and a UTC instant for timed series. Adapter layers map this to and from RFC 5545 RRULE.\n */\nexport interface TemporalRecurrenceRule<TValue extends TemporalDate | TemporalInstant> {\n readonly frequency: 'daily' | 'monthly' | 'weekly' | 'yearly';\n readonly interval?: number;\n readonly count?: number;\n readonly until?: TValue;\n readonly weekdays?: readonly (\n 'friday' | 'monday' | 'saturday' | 'sunday' | 'thursday' | 'tuesday' | 'wednesday'\n )[];\n readonly monthDays?: readonly number[];\n readonly months?: readonly number[];\n}\n\n/**\n * Recurrence data retained independently from provider-specific series representations. `dates`\n * and `excludedDates` correspond to RFC 5545 RDATE and EXDATE and use the DTSTART value type.\n */\nexport interface TemporalRecurrence<TValue extends TemporalDate | TemporalInstant> {\n readonly rules: readonly TemporalRecurrenceRule<TValue>[];\n readonly dates?: readonly TValue[];\n readonly excludedDates?: readonly TValue[];\n}\n\n/**\n * A detached occurrence that replaces or cancels one generated recurrence instance. `recurrenceId`\n * is the original scheduled DTSTART value: for timed DST series, the UTC instant materialized from\n * that occurrence's wall-clock time in the series timezone; for all-day series, its calendar date.\n */\nexport interface TemporalRecurrenceException<\n TValue extends TemporalDate | TemporalInstant,\n TRange extends TemporalRange,\n> {\n readonly recurrenceId: TValue;\n readonly status: 'cancelled' | 'replacement';\n readonly range?: TRange;\n readonly title?: string;\n}\n\n/** Lossless serialized data that an interoperability adapter must round-trip without exposing its runtime. */\nexport interface TemporalInteroperabilityData {\n readonly format: 'icalendar' | 'jcal' | 'provider';\n readonly value: TemporalDataValue;\n}\n\ninterface TemporalEventBase {\n readonly id: string;\n readonly title: string;\n readonly description?: string;\n readonly location?: string;\n readonly conferenceUrl?: string;\n readonly calendarId?: string;\n readonly tone?: TemporalTone;\n readonly availability?: TemporalAvailability;\n readonly status?: TemporalEventStatus;\n readonly resourceIds?: readonly string[];\n readonly organizer?: TemporalParticipant;\n readonly attendees?: readonly TemporalParticipant[];\n readonly reminders?: readonly TemporalReminder[];\n readonly editable?: boolean;\n readonly metadata?: Readonly<Record<string, TemporalDataValue>>;\n readonly interoperability?: readonly TemporalInteroperabilityData[];\n}\n\n/** A provider-neutral timed event with instant-based recurrence values. */\nexport interface TemporalTimedEvent extends TemporalEventBase {\n readonly range: TemporalTimedRange;\n readonly recurrence?: TemporalRecurrence<TemporalInstant>;\n readonly recurrenceExceptions?: readonly TemporalRecurrenceException<\n TemporalInstant,\n TemporalTimedRange\n >[];\n}\n\n/** A provider-neutral all-day event with date-based recurrence values. */\nexport interface TemporalAllDayEvent extends TemporalEventBase {\n readonly range: TemporalAllDayRange;\n readonly recurrence?: TemporalRecurrence<TemporalDate>;\n readonly recurrenceExceptions?: readonly TemporalRecurrenceException<\n TemporalDate,\n TemporalAllDayRange\n >[];\n}\n\n/** Provider-neutral, serializable event data shared by future Calendar and Scheduler presentations. */\nexport type TemporalEvent = TemporalAllDayEvent | TemporalTimedEvent;\n"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ankhorage/zora",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "25.
|
|
4
|
+
"version": "25.10.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": {
|
|
@@ -155,7 +155,7 @@
|
|
|
155
155
|
"format:check": "ankhorage-prettier --check .",
|
|
156
156
|
"prepack": "bun run build",
|
|
157
157
|
"test": "bun run build && bun test src examples/grid-workspaces/time examples/grid-workspaces/plugins --isolate",
|
|
158
|
-
"typecheck": "bun x tsc --noEmit -p tsconfig.json && bun x tsc --noEmit -p tsconfig.scripts.json",
|
|
158
|
+
"typecheck": "bun x tsc --noEmit -p tsconfig.json && bun x tsc --noEmit -p tsconfig.scripts.json && bun x tsc -p tsconfig.typecheck.json",
|
|
159
159
|
"version-packages": "ankhorage-changeset version"
|
|
160
160
|
},
|
|
161
161
|
"devDependencies": {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
TemporalAllDayEvent,
|
|
3
|
+
TemporalAllDayRange,
|
|
4
|
+
TemporalAvailability,
|
|
5
|
+
TemporalDataValue,
|
|
6
|
+
TemporalDate,
|
|
7
|
+
TemporalDuration,
|
|
8
|
+
TemporalEvent,
|
|
9
|
+
TemporalEventStatus,
|
|
10
|
+
TemporalInstant,
|
|
11
|
+
TemporalInteroperabilityData,
|
|
12
|
+
TemporalParticipant,
|
|
13
|
+
TemporalRange,
|
|
14
|
+
TemporalRecurrence,
|
|
15
|
+
TemporalRecurrenceException,
|
|
16
|
+
TemporalRecurrenceRule,
|
|
17
|
+
TemporalReminder,
|
|
18
|
+
TemporalTimedEvent,
|
|
19
|
+
TemporalTimedRange,
|
|
20
|
+
TemporalTimeZone,
|
|
21
|
+
TemporalTone,
|
|
22
|
+
} from '../../types/temporal';
|
|
@@ -4,6 +4,7 @@ import { FlatList as NativeFlatList } from 'react-native';
|
|
|
4
4
|
import type { ManifestListProps } from '../../../../types/manifest-list';
|
|
5
5
|
import { withZoraThemeScope } from '../../../theme/adapters/inbound/withZoraThemeScope';
|
|
6
6
|
import { Text } from '../../../typography/public';
|
|
7
|
+
import { LIST_VIRTUALIZATION_DEFAULTS } from '../../constants/virtualization';
|
|
7
8
|
import { HorizontalListSeparator } from './HorizontalListSeparator';
|
|
8
9
|
import { useListVisibility } from './useListVisibility';
|
|
9
10
|
import { VerticalListSeparator } from './VerticalListSeparator';
|
|
@@ -19,9 +20,9 @@ function FlatListInner({
|
|
|
19
20
|
horizontal,
|
|
20
21
|
refreshing = false,
|
|
21
22
|
initialNumToRender,
|
|
22
|
-
maxToRenderPerBatch,
|
|
23
|
+
maxToRenderPerBatch = LIST_VIRTUALIZATION_DEFAULTS.maxToRenderPerBatch,
|
|
23
24
|
windowSize,
|
|
24
|
-
onEndReachedThreshold,
|
|
25
|
+
onEndReachedThreshold = LIST_VIRTUALIZATION_DEFAULTS.onEndReachedThreshold,
|
|
25
26
|
showsScrollIndicator = true,
|
|
26
27
|
bounces,
|
|
27
28
|
showSeparators,
|
|
@@ -5,6 +5,7 @@ import type { ManifestSectionListProps } from '../../../../types/manifest-list';
|
|
|
5
5
|
import { withZoraThemeScope } from '../../../theme/adapters/inbound/withZoraThemeScope';
|
|
6
6
|
import { Text } from '../../../typography/public';
|
|
7
7
|
import { partitionListSections } from '../../application/use-cases/partitionListSections';
|
|
8
|
+
import { LIST_VIRTUALIZATION_DEFAULTS } from '../../constants/virtualization';
|
|
8
9
|
import { HorizontalListSeparator } from './HorizontalListSeparator';
|
|
9
10
|
import { useListVisibility } from './useListVisibility';
|
|
10
11
|
import { VerticalListSeparator } from './VerticalListSeparator';
|
|
@@ -21,9 +22,9 @@ function SectionListInner({
|
|
|
21
22
|
horizontal,
|
|
22
23
|
refreshing = false,
|
|
23
24
|
initialNumToRender,
|
|
24
|
-
maxToRenderPerBatch,
|
|
25
|
+
maxToRenderPerBatch = LIST_VIRTUALIZATION_DEFAULTS.maxToRenderPerBatch,
|
|
25
26
|
windowSize,
|
|
26
|
-
onEndReachedThreshold,
|
|
27
|
+
onEndReachedThreshold = LIST_VIRTUALIZATION_DEFAULTS.onEndReachedThreshold,
|
|
27
28
|
showsScrollIndicator = true,
|
|
28
29
|
bounces,
|
|
29
30
|
showSeparators,
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { expect, mock, test } from 'bun:test';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { renderToStaticMarkup } from 'react-dom/server';
|
|
4
|
+
|
|
5
|
+
type EndReachedEvent = Readonly<{ distanceFromEnd: number }>;
|
|
6
|
+
|
|
7
|
+
type NativeListProps = Readonly<{
|
|
8
|
+
readonly children?: React.ReactNode;
|
|
9
|
+
readonly maxToRenderPerBatch?: number;
|
|
10
|
+
readonly onEndReached?: (event: EndReachedEvent) => void;
|
|
11
|
+
readonly onEndReachedThreshold?: number;
|
|
12
|
+
readonly [key: string]: unknown;
|
|
13
|
+
}>;
|
|
14
|
+
|
|
15
|
+
const flatListProps: NativeListProps[] = [];
|
|
16
|
+
const sectionListProps: NativeListProps[] = [];
|
|
17
|
+
|
|
18
|
+
/*** Captures the props ZORA delivers to the React Native FlatList boundary. */
|
|
19
|
+
function NativeFlatList(props: NativeListProps) {
|
|
20
|
+
flatListProps.push(props);
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/*** Captures the props ZORA delivers to the React Native SectionList boundary. */
|
|
25
|
+
function NativeSectionList(props: NativeListProps) {
|
|
26
|
+
sectionListProps.push(props);
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
await mock.module('react-native', () => ({
|
|
31
|
+
FlatList: NativeFlatList,
|
|
32
|
+
Platform: { OS: 'web' },
|
|
33
|
+
SectionList: NativeSectionList,
|
|
34
|
+
Text: () => null,
|
|
35
|
+
}));
|
|
36
|
+
await mock.module('@ankhorage/surface', () => ({
|
|
37
|
+
ThemeScope: ({ children }: { readonly children: React.ReactNode }) => children,
|
|
38
|
+
resolveResponsive: <T,>(value: T) => value,
|
|
39
|
+
useResponsiveRuntime: () => ({ breakpoint: undefined }),
|
|
40
|
+
useTheme: () => ({ theme: {} }),
|
|
41
|
+
}));
|
|
42
|
+
await mock.module('../../../layout/public', () => ({ Divider: () => null }));
|
|
43
|
+
|
|
44
|
+
const { FlatList } = await import('./FlatList');
|
|
45
|
+
const { SectionList } = await import('./SectionList');
|
|
46
|
+
|
|
47
|
+
test.each([
|
|
48
|
+
{
|
|
49
|
+
name: 'FlatList',
|
|
50
|
+
props: flatListProps,
|
|
51
|
+
render: (properties: {
|
|
52
|
+
readonly interactionPolicy?: 'passive';
|
|
53
|
+
readonly onEndReached?: (event: EndReachedEvent) => void;
|
|
54
|
+
readonly maxToRenderPerBatch?: number;
|
|
55
|
+
readonly onEndReachedThreshold?: number;
|
|
56
|
+
}) => <FlatList {...properties} />,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: 'SectionList',
|
|
60
|
+
props: sectionListProps,
|
|
61
|
+
render: (properties: {
|
|
62
|
+
readonly interactionPolicy?: 'passive';
|
|
63
|
+
readonly onEndReached?: (event: EndReachedEvent) => void;
|
|
64
|
+
readonly maxToRenderPerBatch?: number;
|
|
65
|
+
readonly onEndReachedThreshold?: number;
|
|
66
|
+
}) => <SectionList sections={[]} {...properties} />,
|
|
67
|
+
},
|
|
68
|
+
])(
|
|
69
|
+
'$name forwards virtualization and pagination defaults, overrides, and callbacks',
|
|
70
|
+
({ props, render }) => {
|
|
71
|
+
const onEndReached = mock();
|
|
72
|
+
renderToStaticMarkup(render({ onEndReached }));
|
|
73
|
+
|
|
74
|
+
const defaults = props.at(-1);
|
|
75
|
+
expect(defaults?.maxToRenderPerBatch).toBe(10);
|
|
76
|
+
expect(defaults?.onEndReachedThreshold).toBe(2);
|
|
77
|
+
defaults?.onEndReached?.({ distanceFromEnd: 24 });
|
|
78
|
+
expect(onEndReached).toHaveBeenCalledWith({ distanceFromEnd: 24 });
|
|
79
|
+
|
|
80
|
+
renderToStaticMarkup(
|
|
81
|
+
render({ maxToRenderPerBatch: 4, onEndReachedThreshold: 0.25, onEndReached }),
|
|
82
|
+
);
|
|
83
|
+
const overrides = props.at(-1);
|
|
84
|
+
expect(overrides?.maxToRenderPerBatch).toBe(4);
|
|
85
|
+
expect(overrides?.onEndReachedThreshold).toBe(0.25);
|
|
86
|
+
|
|
87
|
+
renderToStaticMarkup(render({ interactionPolicy: 'passive', onEndReached }));
|
|
88
|
+
expect(props.at(-1)?.onEndReached).toBeUndefined();
|
|
89
|
+
},
|
|
90
|
+
);
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { CONTAINER_ALLOWED_CHILDREN } from '../../constants/authoring';
|
|
2
2
|
import type { ZoraComponentMeta } from '../../types/authoring';
|
|
3
|
+
import { LIST_VIRTUALIZATION_DEFAULTS } from './constants/virtualization';
|
|
3
4
|
|
|
4
5
|
export const flatListMeta = {
|
|
5
6
|
name: 'FlatList',
|
|
6
7
|
category: 'layout',
|
|
7
8
|
directManifestNode: true,
|
|
8
|
-
blueprint: { label: 'Flat list' },
|
|
9
|
+
blueprint: { label: 'Flat list', defaultProps: LIST_VIRTUALIZATION_DEFAULTS },
|
|
9
10
|
allowedChildren: [...CONTAINER_ALLOWED_CHILDREN],
|
|
10
11
|
description:
|
|
11
12
|
'Virtualized children. Put node.repeat on this list for keyed data and item bindings; bind selection on the child Button/Card press event. Bound the list viewport and avoid nesting in a same-direction ScrollView.',
|
|
@@ -4,7 +4,10 @@ import { flatListMeta } from './flatListMeta';
|
|
|
4
4
|
export const sectionListMeta = {
|
|
5
5
|
...flatListMeta,
|
|
6
6
|
name: 'SectionList',
|
|
7
|
-
blueprint: {
|
|
7
|
+
blueprint: {
|
|
8
|
+
label: 'Section list',
|
|
9
|
+
defaultProps: { ...flatListMeta.blueprint.defaultProps, sections: [] },
|
|
10
|
+
},
|
|
8
11
|
description:
|
|
9
12
|
'Virtualized sections of ordered children. Section counts must cover every child exactly once.',
|
|
10
13
|
events: {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { LIST_VIRTUALIZATION_DEFAULTS } from './constants/virtualization';
|
|
4
|
+
import { flatListMeta } from './flatListMeta';
|
|
5
|
+
import { sectionListMeta } from './sectionListMeta';
|
|
6
|
+
|
|
7
|
+
test('publishes aligned list virtualization and pagination defaults in authoring metadata', () => {
|
|
8
|
+
expect(LIST_VIRTUALIZATION_DEFAULTS).toEqual({
|
|
9
|
+
maxToRenderPerBatch: 10,
|
|
10
|
+
onEndReachedThreshold: 2,
|
|
11
|
+
});
|
|
12
|
+
expect(flatListMeta.blueprint.defaultProps).toEqual(LIST_VIRTUALIZATION_DEFAULTS);
|
|
13
|
+
expect(sectionListMeta.blueprint.defaultProps).toEqual({
|
|
14
|
+
...LIST_VIRTUALIZATION_DEFAULTS,
|
|
15
|
+
sections: [],
|
|
16
|
+
});
|
|
17
|
+
for (const metadata of [flatListMeta, sectionListMeta]) {
|
|
18
|
+
expect(metadata.events.endReached.payloadFields).toEqual([
|
|
19
|
+
{ path: 'distanceFromEnd', type: 'number' },
|
|
20
|
+
]);
|
|
21
|
+
expect(metadata.props.maxToRenderPerBatch.type).toBe('number');
|
|
22
|
+
expect(metadata.props.onEndReachedThreshold.type).toBe('number');
|
|
23
|
+
}
|
|
24
|
+
});
|
package/src/readerFree.ts
CHANGED
|
@@ -70,6 +70,28 @@ export type { IconButtonProps } from './features/button/public';
|
|
|
70
70
|
export { Button } from './features/button/public';
|
|
71
71
|
export { ButtonGroup } from './features/button/public';
|
|
72
72
|
export { IconButton } from './features/button/public';
|
|
73
|
+
export type {
|
|
74
|
+
TemporalAllDayEvent,
|
|
75
|
+
TemporalAllDayRange,
|
|
76
|
+
TemporalAvailability,
|
|
77
|
+
TemporalDataValue,
|
|
78
|
+
TemporalDate,
|
|
79
|
+
TemporalDuration,
|
|
80
|
+
TemporalEvent,
|
|
81
|
+
TemporalEventStatus,
|
|
82
|
+
TemporalInstant,
|
|
83
|
+
TemporalInteroperabilityData,
|
|
84
|
+
TemporalParticipant,
|
|
85
|
+
TemporalRange,
|
|
86
|
+
TemporalRecurrence,
|
|
87
|
+
TemporalRecurrenceException,
|
|
88
|
+
TemporalRecurrenceRule,
|
|
89
|
+
TemporalReminder,
|
|
90
|
+
TemporalTimedEvent,
|
|
91
|
+
TemporalTimedRange,
|
|
92
|
+
TemporalTimeZone,
|
|
93
|
+
TemporalTone,
|
|
94
|
+
} from './features/calendar/public';
|
|
73
95
|
export type { CardProps } from './features/card/public';
|
|
74
96
|
export type { MediaCardImageProps, MediaCardProps } from './features/card/public';
|
|
75
97
|
export type { MetricCardProps } from './features/card/public';
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import type { TemporalEvent, TemporalRange } from './temporal';
|
|
4
|
+
|
|
5
|
+
const timedRange = {
|
|
6
|
+
allDay: false,
|
|
7
|
+
end: '2026-10-25T09:30:00Z',
|
|
8
|
+
start: '2026-10-25T09:00:00Z',
|
|
9
|
+
timeZone: 'Europe/Zurich',
|
|
10
|
+
} as const satisfies TemporalRange;
|
|
11
|
+
|
|
12
|
+
const allDayRange = {
|
|
13
|
+
allDay: true,
|
|
14
|
+
end: '2026-10-25',
|
|
15
|
+
start: '2026-10-24',
|
|
16
|
+
} as const satisfies TemporalRange;
|
|
17
|
+
|
|
18
|
+
test('models calendar, scheduler, planner and Gantt inputs as provider-neutral serialized data', () => {
|
|
19
|
+
const calendarEvent = {
|
|
20
|
+
id: 'review',
|
|
21
|
+
range: timedRange,
|
|
22
|
+
title: 'Design review',
|
|
23
|
+
} as const satisfies TemporalEvent;
|
|
24
|
+
const schedulerEvent = {
|
|
25
|
+
availability: 'busy',
|
|
26
|
+
id: 'booking',
|
|
27
|
+
range: timedRange,
|
|
28
|
+
resourceIds: ['room-1'],
|
|
29
|
+
title: 'Room booking',
|
|
30
|
+
} as const satisfies TemporalEvent;
|
|
31
|
+
const plannerItem = {
|
|
32
|
+
editable: true,
|
|
33
|
+
id: 'plan',
|
|
34
|
+
range: allDayRange,
|
|
35
|
+
status: 'draft',
|
|
36
|
+
title: 'Plan release',
|
|
37
|
+
} as const satisfies TemporalEvent;
|
|
38
|
+
const ganttItem = {
|
|
39
|
+
id: 'task',
|
|
40
|
+
metadata: { progress: 0.5 },
|
|
41
|
+
range: timedRange,
|
|
42
|
+
title: 'Implement contract',
|
|
43
|
+
} as const satisfies TemporalEvent;
|
|
44
|
+
|
|
45
|
+
expect([calendarEvent, schedulerEvent, plannerItem, ganttItem]).toHaveLength(4);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('keeps Europe/Zurich DST, all-day and recurrence edge fixtures serializable', () => {
|
|
49
|
+
const event = {
|
|
50
|
+
id: 'series',
|
|
51
|
+
interoperability: [
|
|
52
|
+
{
|
|
53
|
+
format: 'icalendar',
|
|
54
|
+
value: { uid: 'series@example.test', xProperties: ['X-OWNER:calendar'] },
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
range: timedRange,
|
|
58
|
+
recurrence: {
|
|
59
|
+
dates: ['2026-11-01T09:00:00Z'],
|
|
60
|
+
excludedDates: ['2026-11-08T09:00:00Z'],
|
|
61
|
+
rules: [
|
|
62
|
+
{
|
|
63
|
+
frequency: 'weekly',
|
|
64
|
+
interval: 2,
|
|
65
|
+
until: '2026-11-29T09:00:00Z',
|
|
66
|
+
weekdays: ['sunday'],
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
recurrenceExceptions: [{ recurrenceId: '2026-11-22T09:00:00Z', status: 'cancelled' }],
|
|
71
|
+
reminders: [{ id: 'before-start', offset: '-PT15M', reference: 'start' }],
|
|
72
|
+
title: 'Biweekly review',
|
|
73
|
+
} as const satisfies TemporalEvent;
|
|
74
|
+
const allDayEvent = {
|
|
75
|
+
id: 'all-day-series',
|
|
76
|
+
range: allDayRange,
|
|
77
|
+
recurrence: {
|
|
78
|
+
dates: ['2026-10-31'],
|
|
79
|
+
excludedDates: ['2026-11-07'],
|
|
80
|
+
rules: [{ frequency: 'weekly', until: '2026-11-28', weekdays: ['saturday'] }],
|
|
81
|
+
},
|
|
82
|
+
recurrenceExceptions: [{ recurrenceId: '2026-11-14', status: 'cancelled' }],
|
|
83
|
+
reminders: [{ id: 'after-end', offset: 'PT15M', reference: 'end' }],
|
|
84
|
+
title: 'All-day Saturday series',
|
|
85
|
+
} as const satisfies TemporalEvent;
|
|
86
|
+
|
|
87
|
+
expect(JSON.parse(JSON.stringify([allDayEvent, event]))).toEqual([allDayEvent, event]);
|
|
88
|
+
});
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/** A calendar date without a time or timezone, represented as `YYYY-MM-DD`. */
|
|
2
|
+
export type TemporalDate = `${number}-${number}-${number}`;
|
|
3
|
+
|
|
4
|
+
/** A UTC ISO-8601 instant, represented as an RFC 3339 string ending in `Z`. */
|
|
5
|
+
export type TemporalInstant = `${string}Z`;
|
|
6
|
+
|
|
7
|
+
/** An IANA timezone identifier such as `Europe/Zurich`. */
|
|
8
|
+
export type TemporalTimeZone = string;
|
|
9
|
+
|
|
10
|
+
/** An ISO-8601 duration string such as `PT30M` or `-PT15M`. */
|
|
11
|
+
export type TemporalDuration = string;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* This contract describes serializable shape, not runtime validation. Consumers must validate
|
|
15
|
+
* RFC 3339 instants, ISO calendar dates and durations, IANA zone identifiers, positive non-zero
|
|
16
|
+
* intervals, ordered ranges, and finite JSON numbers at their input boundary.
|
|
17
|
+
*/
|
|
18
|
+
export type TemporalDataValue =
|
|
19
|
+
| boolean
|
|
20
|
+
| null
|
|
21
|
+
| number
|
|
22
|
+
| string
|
|
23
|
+
| readonly TemporalDataValue[]
|
|
24
|
+
| { readonly [key: string]: TemporalDataValue };
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A timed interval with exactly one exclusive end. Recurring timed events retain the local
|
|
28
|
+
* wall-clock time derived from `start` in `timeZone` across DST transitions; a recurrence engine
|
|
29
|
+
* materializes each occurrence as its corresponding UTC instant.
|
|
30
|
+
*/
|
|
31
|
+
export type TemporalTimedRange =
|
|
32
|
+
| {
|
|
33
|
+
readonly allDay: false;
|
|
34
|
+
readonly start: TemporalInstant;
|
|
35
|
+
readonly end: TemporalInstant;
|
|
36
|
+
readonly duration?: never;
|
|
37
|
+
readonly timeZone: TemporalTimeZone;
|
|
38
|
+
}
|
|
39
|
+
| {
|
|
40
|
+
readonly allDay: false;
|
|
41
|
+
readonly start: TemporalInstant;
|
|
42
|
+
readonly end?: never;
|
|
43
|
+
readonly duration: TemporalDuration;
|
|
44
|
+
readonly timeZone: TemporalTimeZone;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A timezone-free all-day calendar-date interval with exactly one exclusive end. `end` is the
|
|
49
|
+
* first day not included, so a one-day event from `2026-10-24` ends on `2026-10-25`.
|
|
50
|
+
*/
|
|
51
|
+
export type TemporalAllDayRange =
|
|
52
|
+
| {
|
|
53
|
+
readonly allDay: true;
|
|
54
|
+
readonly start: TemporalDate;
|
|
55
|
+
readonly end: TemporalDate;
|
|
56
|
+
readonly duration?: never;
|
|
57
|
+
}
|
|
58
|
+
| {
|
|
59
|
+
readonly allDay: true;
|
|
60
|
+
readonly start: TemporalDate;
|
|
61
|
+
readonly end?: never;
|
|
62
|
+
readonly duration: TemporalDuration;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/** A timed or all-day interval with exactly one way to define its exclusive end. */
|
|
66
|
+
export type TemporalRange = TemporalAllDayRange | TemporalTimedRange;
|
|
67
|
+
|
|
68
|
+
/** The availability an event contributes without disclosing its details. */
|
|
69
|
+
export type TemporalAvailability = 'free' | 'busy' | 'tentative' | 'unavailable';
|
|
70
|
+
|
|
71
|
+
/** A provider-neutral lifecycle state for a scheduled event. */
|
|
72
|
+
export type TemporalEventStatus = 'cancelled' | 'confirmed' | 'draft' | 'tentative';
|
|
73
|
+
|
|
74
|
+
/** A semantic theme treatment that later ZORA renderers resolve through their active theme. */
|
|
75
|
+
export type TemporalTone = 'accent' | 'critical' | 'info' | 'neutral' | 'positive' | 'warning';
|
|
76
|
+
|
|
77
|
+
/** A person or organization participating in an event without provider-specific identity objects. */
|
|
78
|
+
export interface TemporalParticipant {
|
|
79
|
+
readonly id?: string;
|
|
80
|
+
readonly name?: string;
|
|
81
|
+
readonly email?: string;
|
|
82
|
+
readonly role?: 'optional' | 'organizer' | 'required' | 'resource';
|
|
83
|
+
readonly response?: 'accepted' | 'declined' | 'delegated' | 'needs-action' | 'tentative';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* A reminder relative to the event start or exclusive end. A negative ISO duration fires before
|
|
88
|
+
* that reference point; a positive duration fires after it.
|
|
89
|
+
*/
|
|
90
|
+
export interface TemporalReminder {
|
|
91
|
+
readonly id: string;
|
|
92
|
+
readonly offset: TemporalDuration;
|
|
93
|
+
readonly reference: 'end' | 'start';
|
|
94
|
+
readonly channel?: 'display' | 'email' | 'push';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* One normalized recurrence rule. `until` has the same value type as DTSTART: a date for all-day
|
|
99
|
+
* series and a UTC instant for timed series. Adapter layers map this to and from RFC 5545 RRULE.
|
|
100
|
+
*/
|
|
101
|
+
export interface TemporalRecurrenceRule<TValue extends TemporalDate | TemporalInstant> {
|
|
102
|
+
readonly frequency: 'daily' | 'monthly' | 'weekly' | 'yearly';
|
|
103
|
+
readonly interval?: number;
|
|
104
|
+
readonly count?: number;
|
|
105
|
+
readonly until?: TValue;
|
|
106
|
+
readonly weekdays?: readonly (
|
|
107
|
+
'friday' | 'monday' | 'saturday' | 'sunday' | 'thursday' | 'tuesday' | 'wednesday'
|
|
108
|
+
)[];
|
|
109
|
+
readonly monthDays?: readonly number[];
|
|
110
|
+
readonly months?: readonly number[];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Recurrence data retained independently from provider-specific series representations. `dates`
|
|
115
|
+
* and `excludedDates` correspond to RFC 5545 RDATE and EXDATE and use the DTSTART value type.
|
|
116
|
+
*/
|
|
117
|
+
export interface TemporalRecurrence<TValue extends TemporalDate | TemporalInstant> {
|
|
118
|
+
readonly rules: readonly TemporalRecurrenceRule<TValue>[];
|
|
119
|
+
readonly dates?: readonly TValue[];
|
|
120
|
+
readonly excludedDates?: readonly TValue[];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* A detached occurrence that replaces or cancels one generated recurrence instance. `recurrenceId`
|
|
125
|
+
* is the original scheduled DTSTART value: for timed DST series, the UTC instant materialized from
|
|
126
|
+
* that occurrence's wall-clock time in the series timezone; for all-day series, its calendar date.
|
|
127
|
+
*/
|
|
128
|
+
export interface TemporalRecurrenceException<
|
|
129
|
+
TValue extends TemporalDate | TemporalInstant,
|
|
130
|
+
TRange extends TemporalRange,
|
|
131
|
+
> {
|
|
132
|
+
readonly recurrenceId: TValue;
|
|
133
|
+
readonly status: 'cancelled' | 'replacement';
|
|
134
|
+
readonly range?: TRange;
|
|
135
|
+
readonly title?: string;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Lossless serialized data that an interoperability adapter must round-trip without exposing its runtime. */
|
|
139
|
+
export interface TemporalInteroperabilityData {
|
|
140
|
+
readonly format: 'icalendar' | 'jcal' | 'provider';
|
|
141
|
+
readonly value: TemporalDataValue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
interface TemporalEventBase {
|
|
145
|
+
readonly id: string;
|
|
146
|
+
readonly title: string;
|
|
147
|
+
readonly description?: string;
|
|
148
|
+
readonly location?: string;
|
|
149
|
+
readonly conferenceUrl?: string;
|
|
150
|
+
readonly calendarId?: string;
|
|
151
|
+
readonly tone?: TemporalTone;
|
|
152
|
+
readonly availability?: TemporalAvailability;
|
|
153
|
+
readonly status?: TemporalEventStatus;
|
|
154
|
+
readonly resourceIds?: readonly string[];
|
|
155
|
+
readonly organizer?: TemporalParticipant;
|
|
156
|
+
readonly attendees?: readonly TemporalParticipant[];
|
|
157
|
+
readonly reminders?: readonly TemporalReminder[];
|
|
158
|
+
readonly editable?: boolean;
|
|
159
|
+
readonly metadata?: Readonly<Record<string, TemporalDataValue>>;
|
|
160
|
+
readonly interoperability?: readonly TemporalInteroperabilityData[];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** A provider-neutral timed event with instant-based recurrence values. */
|
|
164
|
+
export interface TemporalTimedEvent extends TemporalEventBase {
|
|
165
|
+
readonly range: TemporalTimedRange;
|
|
166
|
+
readonly recurrence?: TemporalRecurrence<TemporalInstant>;
|
|
167
|
+
readonly recurrenceExceptions?: readonly TemporalRecurrenceException<
|
|
168
|
+
TemporalInstant,
|
|
169
|
+
TemporalTimedRange
|
|
170
|
+
>[];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** A provider-neutral all-day event with date-based recurrence values. */
|
|
174
|
+
export interface TemporalAllDayEvent extends TemporalEventBase {
|
|
175
|
+
readonly range: TemporalAllDayRange;
|
|
176
|
+
readonly recurrence?: TemporalRecurrence<TemporalDate>;
|
|
177
|
+
readonly recurrenceExceptions?: readonly TemporalRecurrenceException<
|
|
178
|
+
TemporalDate,
|
|
179
|
+
TemporalAllDayRange
|
|
180
|
+
>[];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Provider-neutral, serializable event data shared by future Calendar and Scheduler presentations. */
|
|
184
|
+
export type TemporalEvent = TemporalAllDayEvent | TemporalTimedEvent;
|
|
@@ -38,6 +38,12 @@ import {
|
|
|
38
38
|
var import_react_native = __toESM(require_cjs(), 1);
|
|
39
39
|
import React2 from "react";
|
|
40
40
|
|
|
41
|
+
// src/features/list/constants/virtualization.ts
|
|
42
|
+
var LIST_VIRTUALIZATION_DEFAULTS = {
|
|
43
|
+
maxToRenderPerBatch: 10,
|
|
44
|
+
onEndReachedThreshold: 2
|
|
45
|
+
};
|
|
46
|
+
|
|
41
47
|
// src/features/list/adapters/inbound/HorizontalListSeparator.tsx
|
|
42
48
|
import { jsx } from "react/jsx-runtime";
|
|
43
49
|
function HorizontalListSeparator() {
|
|
@@ -81,9 +87,9 @@ function FlatListInner({
|
|
|
81
87
|
horizontal,
|
|
82
88
|
refreshing = false,
|
|
83
89
|
initialNumToRender,
|
|
84
|
-
maxToRenderPerBatch,
|
|
90
|
+
maxToRenderPerBatch = LIST_VIRTUALIZATION_DEFAULTS.maxToRenderPerBatch,
|
|
85
91
|
windowSize,
|
|
86
|
-
onEndReachedThreshold,
|
|
92
|
+
onEndReachedThreshold = LIST_VIRTUALIZATION_DEFAULTS.onEndReachedThreshold,
|
|
87
93
|
showsScrollIndicator = true,
|
|
88
94
|
bounces,
|
|
89
95
|
showSeparators,
|
|
@@ -464,9 +470,9 @@ function SectionListInner({
|
|
|
464
470
|
horizontal,
|
|
465
471
|
refreshing = false,
|
|
466
472
|
initialNumToRender,
|
|
467
|
-
maxToRenderPerBatch,
|
|
473
|
+
maxToRenderPerBatch = LIST_VIRTUALIZATION_DEFAULTS.maxToRenderPerBatch,
|
|
468
474
|
windowSize,
|
|
469
|
-
onEndReachedThreshold,
|
|
475
|
+
onEndReachedThreshold = LIST_VIRTUALIZATION_DEFAULTS.onEndReachedThreshold,
|
|
470
476
|
showsScrollIndicator = true,
|
|
471
477
|
bounces,
|
|
472
478
|
showSeparators,
|
|
@@ -43,8 +43,8 @@ import {
|
|
|
43
43
|
} from "../../chunks/Accordion-nj7k0m66.js";
|
|
44
44
|
import {
|
|
45
45
|
SkeletonList2
|
|
46
|
-
} from "../../chunks/Accordion-
|
|
47
|
-
import"../../chunks/Accordion-
|
|
46
|
+
} from "../../chunks/Accordion-xt8vafkn.js";
|
|
47
|
+
import"../../chunks/Accordion-rkg1y7x3.js";
|
|
48
48
|
import"../../chunks/Accordion-ng4f4k3b.js";
|
|
49
49
|
import"../../chunks/Accordion-eqvdv7tt.js";
|
|
50
50
|
import"../../chunks/Accordion-61e19g3e.js";
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
List2,
|
|
13
13
|
ListSection2,
|
|
14
14
|
SectionList2
|
|
15
|
-
} from "../../chunks/Accordion-
|
|
15
|
+
} from "../../chunks/Accordion-rkg1y7x3.js";
|
|
16
16
|
import"../../chunks/Accordion-xstk85jq.js";
|
|
17
17
|
import"../../chunks/Accordion-qr5qn652.js";
|
|
18
18
|
import"../../chunks/Accordion-ng4f4k3b.js";
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
List2,
|
|
13
13
|
ListSection2,
|
|
14
14
|
SectionList2
|
|
15
|
-
} from "../../chunks/Accordion-
|
|
15
|
+
} from "../../chunks/Accordion-rkg1y7x3.js";
|
|
16
16
|
import"../../chunks/Accordion-xstk85jq.js";
|
|
17
17
|
import"../../chunks/Accordion-qr5qn652.js";
|
|
18
18
|
import"../../chunks/Accordion-ng4f4k3b.js";
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
List2,
|
|
13
13
|
ListSection2,
|
|
14
14
|
SectionList2
|
|
15
|
-
} from "../../chunks/Accordion-
|
|
15
|
+
} from "../../chunks/Accordion-rkg1y7x3.js";
|
|
16
16
|
import"../../chunks/Accordion-xstk85jq.js";
|
|
17
17
|
import"../../chunks/Accordion-qr5qn652.js";
|
|
18
18
|
import"../../chunks/Accordion-ng4f4k3b.js";
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
List2,
|
|
13
13
|
ListSection2,
|
|
14
14
|
SectionList2
|
|
15
|
-
} from "../../chunks/Accordion-
|
|
15
|
+
} from "../../chunks/Accordion-rkg1y7x3.js";
|
|
16
16
|
import"../../chunks/Accordion-xstk85jq.js";
|
|
17
17
|
import"../../chunks/Accordion-qr5qn652.js";
|
|
18
18
|
import"../../chunks/Accordion-ng4f4k3b.js";
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
List2,
|
|
13
13
|
ListSection2,
|
|
14
14
|
SectionList2
|
|
15
|
-
} from "../../chunks/Accordion-
|
|
15
|
+
} from "../../chunks/Accordion-rkg1y7x3.js";
|
|
16
16
|
import"../../chunks/Accordion-xstk85jq.js";
|
|
17
17
|
import"../../chunks/Accordion-qr5qn652.js";
|
|
18
18
|
import"../../chunks/Accordion-ng4f4k3b.js";
|