react-native-roster 0.0.1 → 0.1.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/README.md +108 -31
- package/dist/src/components/roster/body-layout.d.ts +9 -0
- package/dist/src/components/roster/body-layout.d.ts.map +1 -0
- package/dist/src/components/roster/body-layout.js +9 -0
- package/dist/src/components/roster/body-layout.js.map +1 -0
- package/dist/src/components/roster/index.d.ts +1 -1
- package/dist/src/components/roster/index.d.ts.map +1 -1
- package/dist/src/components/roster/index.js +10 -26
- package/dist/src/components/roster/index.js.map +1 -1
- package/dist/src/components/roster/lanes/lane.d.ts +2 -2
- package/dist/src/components/roster/lanes/lane.d.ts.map +1 -1
- package/dist/src/components/roster/lanes/lane.js +3 -8
- package/dist/src/components/roster/lanes/lane.js.map +1 -1
- package/dist/src/components/roster/parts/body.d.ts +3 -3
- package/dist/src/components/roster/parts/body.d.ts.map +1 -1
- package/dist/src/components/roster/parts/body.js +5 -20
- package/dist/src/components/roster/parts/body.js.map +1 -1
- package/dist/src/components/roster/parts/header-cell.d.ts +2 -4
- package/dist/src/components/roster/parts/header-cell.d.ts.map +1 -1
- package/dist/src/components/roster/parts/header-cell.js.map +1 -1
- package/dist/src/components/roster/parts/header.d.ts +1 -1
- package/dist/src/components/roster/parts/header.d.ts.map +1 -1
- package/dist/src/components/roster/parts/header.js +2 -2
- package/dist/src/components/roster/parts/header.js.map +1 -1
- package/dist/src/components/roster/parts/lane-label-column.d.ts +1 -1
- package/dist/src/components/roster/parts/lane-label-column.d.ts.map +1 -1
- package/dist/src/components/roster/parts/lane-label-column.js +2 -2
- package/dist/src/components/roster/parts/lane-label-column.js.map +1 -1
- package/dist/src/components/roster/parts/lane-list.d.ts +7 -0
- package/dist/src/components/roster/parts/lane-list.d.ts.map +1 -0
- package/dist/src/components/roster/parts/lane-list.js +25 -0
- package/dist/src/components/roster/parts/lane-list.js.map +1 -0
- package/dist/src/components/roster/roster.types.d.ts +19 -20
- package/dist/src/components/roster/roster.types.d.ts.map +1 -1
- package/dist/src/components/roster/utils/body-content-key.d.ts +1 -1
- package/dist/src/components/roster/utils/body-content-key.d.ts.map +1 -1
- package/dist/src/components/roster/utils/body-content-key.js +10 -10
- package/dist/src/components/roster/utils/body-content-key.js.map +1 -1
- package/dist/src/components/schedule/days/day.d.ts +2 -2
- package/dist/src/components/schedule/days/day.d.ts.map +1 -1
- package/dist/src/components/schedule/days/day.js +2 -11
- package/dist/src/components/schedule/days/day.js.map +1 -1
- package/dist/src/components/schedule/days/header-layout.d.ts +1 -1
- package/dist/src/components/schedule/days/header-layout.d.ts.map +1 -1
- package/dist/src/components/schedule/days/parts/column.d.ts +1 -1
- package/dist/src/components/schedule/days/parts/column.d.ts.map +1 -1
- package/dist/src/components/schedule/days/parts/column.js +3 -8
- package/dist/src/components/schedule/days/parts/column.js.map +1 -1
- package/dist/src/components/schedule/index.d.ts.map +1 -1
- package/dist/src/components/schedule/index.js +7 -8
- package/dist/src/components/schedule/index.js.map +1 -1
- package/dist/src/components/schedule/schedule.types.d.ts +17 -17
- package/dist/src/components/schedule/schedule.types.d.ts.map +1 -1
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +5 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/nativewind/index.d.ts +24 -26
- package/dist/src/nativewind/index.d.ts.map +1 -1
- package/docs/migrations.md +61 -0
- package/llms.txt +46 -29
- package/package.json +3 -2
- package/src/components/layers/README.md +7 -3
- package/src/components/roster/README.md +15 -4
- package/src/components/roster/body-layout.tsx +32 -0
- package/src/components/roster/index.tsx +63 -45
- package/src/components/roster/lanes/lane.tsx +16 -11
- package/src/components/roster/parts/body.tsx +17 -84
- package/src/components/roster/parts/header-cell.tsx +2 -2
- package/src/components/roster/parts/header.tsx +7 -2
- package/src/components/roster/parts/lane-label-column.tsx +2 -2
- package/src/components/roster/parts/lane-list.tsx +74 -0
- package/src/components/roster/roster.types.ts +17 -16
- package/src/components/roster/utils/body-content-key.ts +13 -13
- package/src/components/schedule/README.md +8 -3
- package/src/components/schedule/days/day.tsx +31 -18
- package/src/components/schedule/days/header-layout.tsx +1 -1
- package/src/components/schedule/days/parts/column.tsx +10 -10
- package/src/components/schedule/index.tsx +20 -21
- package/src/components/schedule/schedule.types.ts +13 -13
- package/src/index.ts +3 -0
|
@@ -1,91 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { ScrollView, View } from 'react-native';
|
|
4
|
-
import { LaneRow } from '../lanes/lane';
|
|
1
|
+
import type { ProfilerOnRenderCallback } from 'react';
|
|
2
|
+
import { RosterBodyLayout } from '../body-layout';
|
|
5
3
|
import type { BodyInput } from '../roster.types';
|
|
6
|
-
import {
|
|
4
|
+
import { RosterLaneList } from './lane-list';
|
|
7
5
|
|
|
8
|
-
export function RosterBody(
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
ticks,
|
|
15
|
-
contentWidth,
|
|
16
|
-
viewport,
|
|
17
|
-
window,
|
|
18
|
-
intervalZone,
|
|
19
|
-
gapZone,
|
|
20
|
-
gridZone,
|
|
21
|
-
highlightSource,
|
|
22
|
-
onIntervalHover,
|
|
23
|
-
incompleteLabel,
|
|
24
|
-
onRowRender,
|
|
25
|
-
}: BodyInput & {
|
|
26
|
-
/** Optional per-lane commit observer; used by the development gallery's Profiler gate. */
|
|
27
|
-
onRowRender?: ProfilerOnRenderCallback;
|
|
28
|
-
}) {
|
|
6
|
+
export function RosterBody(
|
|
7
|
+
props: BodyInput & {
|
|
8
|
+
/** Development-only Profiler hook used by the gallery; not a supported customization point. */
|
|
9
|
+
onRowRender?: ProfilerOnRenderCallback;
|
|
10
|
+
},
|
|
11
|
+
) {
|
|
12
|
+
const { viewport, scroll, contentWidth, ticks, gridComponent: GridComponent } = props;
|
|
29
13
|
// LegendList requires a measured viewport and does not support static rendering.
|
|
30
14
|
if (viewport.width <= 0 || viewport.height <= 0) return null;
|
|
31
15
|
return (
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
>
|
|
40
|
-
<View style={{ width: contentWidth, height: viewport.height }}>
|
|
41
|
-
{gridZone({ ticks, contentWidth })}
|
|
42
|
-
<LegendList
|
|
43
|
-
testID="roster-vertical-scroll"
|
|
44
|
-
data={lanes}
|
|
45
|
-
extraData={bodyContentKey({
|
|
46
|
-
window,
|
|
47
|
-
projection,
|
|
48
|
-
highlightSource,
|
|
49
|
-
intervalZone,
|
|
50
|
-
gapZone,
|
|
51
|
-
onIntervalHover,
|
|
52
|
-
incompleteLabel,
|
|
53
|
-
})}
|
|
54
|
-
keyExtractor={(lane) => lane.id}
|
|
55
|
-
estimatedListSize={{ width: contentWidth, height: viewport.height }}
|
|
56
|
-
estimatedItemSize={projection.rowHeight}
|
|
57
|
-
getFixedItemSize={() => projection.rowHeight}
|
|
58
|
-
drawDistance={0}
|
|
59
|
-
recycleItems={false}
|
|
60
|
-
maintainVisibleContentPosition={false}
|
|
61
|
-
onScroll={scroll.onVerticalScroll}
|
|
62
|
-
scrollEventThrottle={16}
|
|
63
|
-
style={{ flex: 1 }}
|
|
64
|
-
renderItem={({ item }) => {
|
|
65
|
-
const row = (
|
|
66
|
-
<LaneRow
|
|
67
|
-
lane={item}
|
|
68
|
-
geometry={geometryFor(item)}
|
|
69
|
-
width={contentWidth}
|
|
70
|
-
rowHeight={projection.rowHeight}
|
|
71
|
-
press={press}
|
|
72
|
-
intervalZone={intervalZone}
|
|
73
|
-
gapZone={gapZone}
|
|
74
|
-
highlightSource={highlightSource}
|
|
75
|
-
onIntervalHover={onIntervalHover}
|
|
76
|
-
incompleteLabel={incompleteLabel}
|
|
77
|
-
/>
|
|
78
|
-
);
|
|
79
|
-
if (onRowRender)
|
|
80
|
-
return (
|
|
81
|
-
<Profiler id={item.id} onRender={onRowRender}>
|
|
82
|
-
{row}
|
|
83
|
-
</Profiler>
|
|
84
|
-
);
|
|
85
|
-
return row;
|
|
86
|
-
}}
|
|
87
|
-
/>
|
|
88
|
-
</View>
|
|
89
|
-
</ScrollView>
|
|
16
|
+
<RosterBodyLayout
|
|
17
|
+
scroll={scroll}
|
|
18
|
+
contentWidth={contentWidth}
|
|
19
|
+
viewport={viewport}
|
|
20
|
+
gridZone={<GridComponent ticks={ticks} contentWidth={contentWidth} />}
|
|
21
|
+
listZone={<RosterLaneList {...props} />}
|
|
22
|
+
/>
|
|
90
23
|
);
|
|
91
24
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Text } from 'react-native';
|
|
2
|
-
import type {
|
|
2
|
+
import type { HeaderCellInput } from '../roster.types';
|
|
3
3
|
|
|
4
|
-
export function RosterHeaderCell({ tick }:
|
|
4
|
+
export function RosterHeaderCell({ tick }: HeaderCellInput) {
|
|
5
5
|
return (
|
|
6
6
|
<Text numberOfLines={1} style={{ color: '#475569', fontSize: 11, padding: 4 }}>
|
|
7
7
|
{tick.label}
|
|
@@ -2,7 +2,12 @@ import { View } from 'react-native';
|
|
|
2
2
|
import Animated from 'react-native-reanimated';
|
|
3
3
|
import type { HeaderInput } from '../roster.types';
|
|
4
4
|
|
|
5
|
-
export function RosterHeader({
|
|
5
|
+
export function RosterHeader({
|
|
6
|
+
ticks,
|
|
7
|
+
contentWidth,
|
|
8
|
+
scroll,
|
|
9
|
+
headerCellComponent: HeaderCellComponent,
|
|
10
|
+
}: HeaderInput) {
|
|
6
11
|
return (
|
|
7
12
|
<Animated.View
|
|
8
13
|
testID="roster-header"
|
|
@@ -10,7 +15,7 @@ export function RosterHeader({ ticks, contentWidth, scroll, headerCellZone }: He
|
|
|
10
15
|
>
|
|
11
16
|
{ticks.map((tick) => (
|
|
12
17
|
<View key={tick.time} style={{ position: 'absolute', left: tick.x, top: 0 }}>
|
|
13
|
-
|
|
18
|
+
<HeaderCellComponent tick={tick} />
|
|
14
19
|
</View>
|
|
15
20
|
))}
|
|
16
21
|
</Animated.View>
|
|
@@ -6,7 +6,7 @@ export function RosterLaneLabelColumn({
|
|
|
6
6
|
labels,
|
|
7
7
|
projection,
|
|
8
8
|
scroll,
|
|
9
|
-
|
|
9
|
+
laneLabelComponent: LaneLabelComponent,
|
|
10
10
|
}: LabelColumnInput) {
|
|
11
11
|
return (
|
|
12
12
|
<Animated.View
|
|
@@ -23,7 +23,7 @@ export function RosterLaneLabelColumn({
|
|
|
23
23
|
width: '100%',
|
|
24
24
|
}}
|
|
25
25
|
>
|
|
26
|
-
{
|
|
26
|
+
<LaneLabelComponent {...label} />
|
|
27
27
|
</View>
|
|
28
28
|
))}
|
|
29
29
|
</Animated.View>
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { LegendList } from '@legendapp/list';
|
|
2
|
+
import { Profiler, type ProfilerOnRenderCallback } from 'react';
|
|
3
|
+
import { LaneRow } from '../lanes/lane';
|
|
4
|
+
import type { BodyInput } from '../roster.types';
|
|
5
|
+
import { bodyContentKey } from '../utils/body-content-key';
|
|
6
|
+
|
|
7
|
+
export function RosterLaneList({
|
|
8
|
+
lanes,
|
|
9
|
+
geometryFor,
|
|
10
|
+
projection,
|
|
11
|
+
scroll,
|
|
12
|
+
press,
|
|
13
|
+
contentWidth,
|
|
14
|
+
viewport,
|
|
15
|
+
window,
|
|
16
|
+
intervalComponent,
|
|
17
|
+
gapComponent,
|
|
18
|
+
highlightSource,
|
|
19
|
+
onIntervalHover,
|
|
20
|
+
incompleteLabel,
|
|
21
|
+
onRowRender,
|
|
22
|
+
}: Omit<BodyInput, 'ticks' | 'gridComponent'> & {
|
|
23
|
+
/** Development-only Profiler hook used by the gallery; not a supported customization point. */
|
|
24
|
+
onRowRender?: ProfilerOnRenderCallback;
|
|
25
|
+
}) {
|
|
26
|
+
return (
|
|
27
|
+
<LegendList
|
|
28
|
+
testID="roster-vertical-scroll"
|
|
29
|
+
data={lanes}
|
|
30
|
+
extraData={bodyContentKey({
|
|
31
|
+
window,
|
|
32
|
+
projection,
|
|
33
|
+
highlightSource,
|
|
34
|
+
intervalComponent,
|
|
35
|
+
gapComponent,
|
|
36
|
+
onIntervalHover,
|
|
37
|
+
incompleteLabel,
|
|
38
|
+
})}
|
|
39
|
+
keyExtractor={(lane) => lane.id}
|
|
40
|
+
estimatedListSize={{ width: contentWidth, height: viewport.height }}
|
|
41
|
+
estimatedItemSize={projection.rowHeight}
|
|
42
|
+
getFixedItemSize={() => projection.rowHeight}
|
|
43
|
+
drawDistance={0}
|
|
44
|
+
recycleItems={false}
|
|
45
|
+
maintainVisibleContentPosition={false}
|
|
46
|
+
onScroll={scroll.onVerticalScroll}
|
|
47
|
+
scrollEventThrottle={16}
|
|
48
|
+
style={{ flex: 1 }}
|
|
49
|
+
renderItem={({ item }) => {
|
|
50
|
+
const row = (
|
|
51
|
+
<LaneRow
|
|
52
|
+
lane={item}
|
|
53
|
+
geometry={geometryFor(item)}
|
|
54
|
+
width={contentWidth}
|
|
55
|
+
rowHeight={projection.rowHeight}
|
|
56
|
+
press={press}
|
|
57
|
+
intervalComponent={intervalComponent}
|
|
58
|
+
gapComponent={gapComponent}
|
|
59
|
+
highlightSource={highlightSource}
|
|
60
|
+
onIntervalHover={onIntervalHover}
|
|
61
|
+
incompleteLabel={incompleteLabel}
|
|
62
|
+
/>
|
|
63
|
+
);
|
|
64
|
+
if (onRowRender)
|
|
65
|
+
return (
|
|
66
|
+
<Profiler id={item.id} onRender={onRowRender}>
|
|
67
|
+
{row}
|
|
68
|
+
</Profiler>
|
|
69
|
+
);
|
|
70
|
+
return row;
|
|
71
|
+
}}
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ReactNode, RefObject } from 'react';
|
|
1
|
+
import type { ComponentType, ReactNode, RefObject } from 'react';
|
|
2
2
|
import type {
|
|
3
3
|
LayoutChangeEvent,
|
|
4
4
|
NativeScrollEvent,
|
|
@@ -75,15 +75,16 @@ export type LaneLabelInput = {
|
|
|
75
75
|
incompleteLabel: string;
|
|
76
76
|
neverSetLabel: string;
|
|
77
77
|
};
|
|
78
|
+
export type HeaderCellInput = { tick: RosterTick };
|
|
78
79
|
export type GridInput = { ticks: RosterTick[]; contentWidth: number };
|
|
79
80
|
export type HeaderInput = Pick<RosterModel, 'ticks' | 'projection' | 'scroll' | 'contentWidth'> & {
|
|
80
81
|
/** Time label filler, positioned by the header. */
|
|
81
|
-
|
|
82
|
+
headerCellComponent: ComponentType<HeaderCellInput>;
|
|
82
83
|
};
|
|
83
84
|
export type LabelColumnInput = Pick<RosterModel, 'projection' | 'scroll'> & {
|
|
84
85
|
labels: LaneLabelInput[];
|
|
85
86
|
/** Lane label filler, positioned at the fixed row height. */
|
|
86
|
-
|
|
87
|
+
laneLabelComponent: ComponentType<LaneLabelInput>;
|
|
87
88
|
};
|
|
88
89
|
export type BodyInput = Pick<
|
|
89
90
|
RosterModel,
|
|
@@ -101,11 +102,11 @@ export type BodyInput = Pick<
|
|
|
101
102
|
onIntervalHover?: (rect: Rect, lane: Lane) => void;
|
|
102
103
|
incompleteLabel?: string;
|
|
103
104
|
/** Covered rect view; position it using the final rect bounds, as RosterInterval does. */
|
|
104
|
-
|
|
105
|
+
intervalComponent: ComponentType<IntervalInput>;
|
|
105
106
|
/** Removed rect filler, placed inside a pressable by LaneRow. */
|
|
106
|
-
|
|
107
|
+
gapComponent: ComponentType<GapInput>;
|
|
107
108
|
/** Noninteractive tick lines behind every lane, sized to the content width. */
|
|
108
|
-
|
|
109
|
+
gridComponent: ComponentType<GridInput>;
|
|
109
110
|
};
|
|
110
111
|
/**
|
|
111
112
|
* Chrome style props and their NativeWind class twins. Class props resolve only
|
|
@@ -133,23 +134,23 @@ export type RosterProps = RosterInput &
|
|
|
133
134
|
incompleteLabel?: string;
|
|
134
135
|
neverSetLabel?: string;
|
|
135
136
|
/** Label, differing lane zone, effective flag, and completeness notice. */
|
|
136
|
-
|
|
137
|
+
laneLabelComponent?: ComponentType<LaneLabelInput>;
|
|
137
138
|
/** Time label within one positioned header tick. */
|
|
138
|
-
|
|
139
|
+
headerCellComponent?: ComponentType<HeaderCellInput>;
|
|
139
140
|
/** Covered rect view; use rect bounds for absolute position, rect.z for stacking, and pointerEvents="none" for row hit testing. */
|
|
140
|
-
|
|
141
|
+
intervalComponent?: ComponentType<IntervalInput>;
|
|
141
142
|
/** Removed rect content; LaneRow supplies the invisible pressable. */
|
|
142
|
-
|
|
143
|
+
gapComponent?: ComponentType<GapInput>;
|
|
143
144
|
/** Tick lines behind the lanes; RosterGrid draws one hairline per tick. */
|
|
144
|
-
|
|
145
|
+
gridComponent?: ComponentType<GridInput>;
|
|
145
146
|
/** Empty roster content, conventionally the text No lanes. */
|
|
146
|
-
emptyZone?:
|
|
147
|
+
emptyZone?: ReactNode;
|
|
147
148
|
/** Top-left cell above the lane labels; RosterCorner renders nothing. */
|
|
148
|
-
cornerZone?:
|
|
149
|
+
cornerZone?: ReactNode;
|
|
149
150
|
/** Frozen time header, supplied with ticks, scale, and shared scrolling. */
|
|
150
|
-
|
|
151
|
+
headerComponent?: ComponentType<HeaderInput>;
|
|
151
152
|
/** Frozen labels, supplied with ordered lanes and per-lane flag and completeness. */
|
|
152
|
-
|
|
153
|
+
laneLabelColumnComponent?: ComponentType<LabelColumnInput>;
|
|
153
154
|
/** Virtualized lane body, supplied with geometry, projection, scrolling, and press resolution. */
|
|
154
|
-
|
|
155
|
+
bodyComponent?: ComponentType<BodyInput>;
|
|
155
156
|
};
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { BodyInput } from '../roster.types';
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
let
|
|
3
|
+
const identityIds = new WeakMap<object, number>();
|
|
4
|
+
let nextIdentityId = 0;
|
|
5
5
|
|
|
6
|
-
function
|
|
7
|
-
let id =
|
|
6
|
+
function identityId(identity: object): number {
|
|
7
|
+
let id = identityIds.get(identity);
|
|
8
8
|
if (id === undefined) {
|
|
9
|
-
id =
|
|
10
|
-
|
|
9
|
+
id = nextIdentityId++;
|
|
10
|
+
identityIds.set(identity, id);
|
|
11
11
|
}
|
|
12
12
|
return id;
|
|
13
13
|
}
|
|
@@ -16,8 +16,8 @@ export function bodyContentKey({
|
|
|
16
16
|
window,
|
|
17
17
|
projection,
|
|
18
18
|
highlightSource,
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
intervalComponent,
|
|
20
|
+
gapComponent,
|
|
21
21
|
onIntervalHover,
|
|
22
22
|
incompleteLabel,
|
|
23
23
|
}: Pick<
|
|
@@ -25,8 +25,8 @@ export function bodyContentKey({
|
|
|
25
25
|
| 'window'
|
|
26
26
|
| 'projection'
|
|
27
27
|
| 'highlightSource'
|
|
28
|
-
| '
|
|
29
|
-
| '
|
|
28
|
+
| 'intervalComponent'
|
|
29
|
+
| 'gapComponent'
|
|
30
30
|
| 'onIntervalHover'
|
|
31
31
|
| 'incompleteLabel'
|
|
32
32
|
>): string {
|
|
@@ -38,9 +38,9 @@ export function bodyContentKey({
|
|
|
38
38
|
projection.pxPerMinute,
|
|
39
39
|
highlightSource?.kind,
|
|
40
40
|
highlightSource?.id,
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
onIntervalHover &&
|
|
41
|
+
identityId(intervalComponent),
|
|
42
|
+
identityId(gapComponent),
|
|
43
|
+
onIntervalHover && identityId(onIntervalHover),
|
|
44
44
|
incompleteLabel,
|
|
45
45
|
]);
|
|
46
46
|
}
|
|
@@ -3,16 +3,21 @@
|
|
|
3
3
|
This feature is about a schedule; its children are days.
|
|
4
4
|
|
|
5
5
|
- Root entry exports: `Schedule`, `useSchedule`, the `ScheduleXxx` parts, and
|
|
6
|
-
every `ScheduleXxxInput`
|
|
6
|
+
every `ScheduleXxxInput` slot input type from `schedule.types.ts`
|
|
7
7
|
|
|
8
8
|
- `index.tsx`: the chassis; measures the viewport, calls `useSchedule`, composes zones
|
|
9
9
|
- `use-schedule.ts`, `use-schedule-viewport.ts`: the hook and the width context
|
|
10
10
|
- `layout.tsx`: arranges header, gutter, and days regions only
|
|
11
|
-
- `schedule.types.ts`: props, model, and every
|
|
11
|
+
- `schedule.types.ts`: props, model, and every slot input type
|
|
12
12
|
- `parts/`: collection-level parts (gutter, incomplete)
|
|
13
13
|
- `days/`: `ScheduleDay`, the day and day-header layouts, and day-local parts
|
|
14
14
|
- `utils/days.ts`: header dates, now position, and transition bounds
|
|
15
15
|
|
|
16
|
-
Interval and gap
|
|
16
|
+
Interval and gap components live in `../layers`; press geometry and `regionStyle`
|
|
17
17
|
in `../primitives`. Zone contracts are documented in the README's Schedule
|
|
18
18
|
section and in `llms.txt`. Tests: `test/components/schedule`.
|
|
19
|
+
|
|
20
|
+
The chassis binds default component types once; ScheduleDay mounts components
|
|
21
|
+
with day data and passes nodes into ScheduleDayLayout. There is no black-box day
|
|
22
|
+
list to split: days, transitions, and rects already map in their owning parts.
|
|
23
|
+
Schedule has ten input-bearing component slots and no zero-argument singleton slots.
|
|
@@ -7,7 +7,14 @@ import { transitionBounds } from '../utils/days';
|
|
|
7
7
|
import { ScheduleDayLayout } from './layout';
|
|
8
8
|
|
|
9
9
|
type ScheduleDayZones = Required<
|
|
10
|
-
Pick<
|
|
10
|
+
Pick<
|
|
11
|
+
ScheduleProps,
|
|
12
|
+
| 'gridComponent'
|
|
13
|
+
| 'columnComponent'
|
|
14
|
+
| 'transitionComponent'
|
|
15
|
+
| 'intervalComponent'
|
|
16
|
+
| 'gapComponent'
|
|
17
|
+
>
|
|
11
18
|
>;
|
|
12
19
|
|
|
13
20
|
type ScheduleDayProps = ScheduleDayZones & {
|
|
@@ -33,11 +40,11 @@ export function ScheduleDay({
|
|
|
33
40
|
highlightSource,
|
|
34
41
|
hours,
|
|
35
42
|
press,
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
gridComponent: GridComponent,
|
|
44
|
+
columnComponent: ColumnComponent,
|
|
45
|
+
transitionComponent: TransitionComponent,
|
|
46
|
+
intervalComponent,
|
|
47
|
+
gapComponent,
|
|
41
48
|
nowLineZone,
|
|
42
49
|
}: ScheduleDayProps) {
|
|
43
50
|
return (
|
|
@@ -53,20 +60,26 @@ export function ScheduleDay({
|
|
|
53
60
|
width={projection.columnWidth}
|
|
54
61
|
height={24 * projection.pxPerHour}
|
|
55
62
|
chromeZ={Math.max(0, ...lane.layers.map((layer) => layer.z)) + 1}
|
|
56
|
-
gridZone={
|
|
57
|
-
columnZone={
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
gridZone={<GridComponent hours={hours} pxPerHour={projection.pxPerHour} />}
|
|
64
|
+
columnZone={
|
|
65
|
+
<ColumnComponent
|
|
66
|
+
day={day}
|
|
67
|
+
lane={lane}
|
|
68
|
+
rects={geometry.rects.filter((rect) => rect.column === column)}
|
|
69
|
+
gapRects={geometry.gapRects.filter((rect) => rect.column === column)}
|
|
70
|
+
highlightSource={highlightSource}
|
|
71
|
+
intervalComponent={intervalComponent}
|
|
72
|
+
gapComponent={gapComponent}
|
|
73
|
+
press={press}
|
|
74
|
+
/>
|
|
75
|
+
}
|
|
67
76
|
transitionZone={day.transitions.map((transition) => (
|
|
68
77
|
<Fragment key={transition.at}>
|
|
69
|
-
|
|
78
|
+
<TransitionComponent
|
|
79
|
+
day={day}
|
|
80
|
+
transition={transition}
|
|
81
|
+
{...transitionBounds(day, transition, projection)}
|
|
82
|
+
/>
|
|
70
83
|
</Fragment>
|
|
71
84
|
))}
|
|
72
85
|
nowLineZone={nowLineZone}
|
|
@@ -3,7 +3,7 @@ import { View } from 'react-native';
|
|
|
3
3
|
|
|
4
4
|
type ScheduleDayHeaderLayoutProps = {
|
|
5
5
|
width: number;
|
|
6
|
-
/** Day heading supplied by
|
|
6
|
+
/** Day heading supplied by dayHeaderComponent, aligned with its column. */
|
|
7
7
|
headerZone: ReactNode;
|
|
8
8
|
};
|
|
9
9
|
|
|
@@ -8,8 +8,8 @@ export function ScheduleColumn({
|
|
|
8
8
|
rects,
|
|
9
9
|
gapRects,
|
|
10
10
|
press,
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
intervalComponent: IntervalComponent,
|
|
12
|
+
gapComponent: GapComponent,
|
|
13
13
|
highlightSource,
|
|
14
14
|
}: ScheduleColumnInput) {
|
|
15
15
|
return [...lane.layers]
|
|
@@ -20,15 +20,15 @@ export function ScheduleColumn({
|
|
|
20
20
|
.filter((rect) => rect.layerId === layer.id)
|
|
21
21
|
.map((rect) => (
|
|
22
22
|
<Fragment key={`interval-${rect.y}:${rect.height}`}>
|
|
23
|
-
|
|
24
|
-
rect
|
|
25
|
-
layer
|
|
26
|
-
lane
|
|
27
|
-
highlighted
|
|
23
|
+
<IntervalComponent
|
|
24
|
+
rect={rect}
|
|
25
|
+
layer={layer}
|
|
26
|
+
lane={lane}
|
|
27
|
+
highlighted={rect.sources.some(
|
|
28
28
|
(source) =>
|
|
29
29
|
source.kind === highlightSource?.kind && source.id === highlightSource.id,
|
|
30
|
-
)
|
|
31
|
-
|
|
30
|
+
)}
|
|
31
|
+
/>
|
|
32
32
|
</Fragment>
|
|
33
33
|
))}
|
|
34
34
|
{gapRects
|
|
@@ -51,7 +51,7 @@ export function ScheduleColumn({
|
|
|
51
51
|
zIndex: rect.z,
|
|
52
52
|
}}
|
|
53
53
|
>
|
|
54
|
-
|
|
54
|
+
<GapComponent rect={rect} layer={layer} lane={lane} />
|
|
55
55
|
</Pressable>
|
|
56
56
|
))}
|
|
57
57
|
</Fragment>
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { Fragment } from 'react';
|
|
2
1
|
import { View } from 'react-native';
|
|
3
2
|
import { RosterGap } from '../layers/parts/gap';
|
|
4
3
|
import { RosterInterval } from '../layers/parts/interval';
|
|
@@ -50,40 +49,40 @@ function ScheduleContent(props: ScheduleProps) {
|
|
|
50
49
|
lane,
|
|
51
50
|
windowSpec,
|
|
52
51
|
highlightSource,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
52
|
+
gutterComponent: GutterComponent = ScheduleGutter,
|
|
53
|
+
dayHeaderComponent: DayHeaderComponent = ScheduleDayHeader,
|
|
54
|
+
skippedDateComponent: SkippedDateComponent = ScheduleSkippedDate,
|
|
55
|
+
columnComponent = ScheduleColumn,
|
|
56
|
+
gridComponent = ScheduleGrid,
|
|
57
|
+
transitionComponent = ScheduleTransition,
|
|
58
|
+
nowLineComponent: NowLineComponent = ScheduleNowLine,
|
|
59
|
+
intervalComponent = RosterInterval,
|
|
60
|
+
gapComponent = RosterGap,
|
|
61
|
+
incompleteComponent: IncompleteComponent = ScheduleIncomplete,
|
|
63
62
|
incompleteLabel = 'Availability may be incomplete',
|
|
64
63
|
} = props;
|
|
65
64
|
const position = nowPosition(projection, now);
|
|
66
65
|
const incomplete =
|
|
67
|
-
lane.complete === false ?
|
|
66
|
+
lane.complete === false ? <IncompleteComponent lane={lane} label={incompleteLabel} /> : null;
|
|
68
67
|
return (
|
|
69
68
|
<ScheduleLayout
|
|
70
69
|
headerStyle={props.headerStyle}
|
|
71
70
|
gutterStyle={props.gutterStyle}
|
|
72
71
|
daysStyle={props.daysStyle}
|
|
73
72
|
incompleteZone={incomplete}
|
|
74
|
-
gutterZone={
|
|
73
|
+
gutterZone={<GutterComponent hours={hours} pxPerHour={projection.pxPerHour} />}
|
|
75
74
|
dayHeaderZone={headerDates(windowSpec, days).map(({ localDate, day }) => {
|
|
76
|
-
if (!day) return <
|
|
75
|
+
if (!day) return <SkippedDateComponent key={localDate} localDate={localDate} />;
|
|
77
76
|
return (
|
|
78
77
|
<ScheduleDayHeaderLayout
|
|
79
78
|
key={localDate}
|
|
80
79
|
width={projection.columnWidth}
|
|
81
|
-
headerZone={
|
|
80
|
+
headerZone={<DayHeaderComponent day={day} />}
|
|
82
81
|
/>
|
|
83
82
|
);
|
|
84
83
|
})}
|
|
85
84
|
daysZone={days.map((day, column) => {
|
|
86
|
-
const nowLine = position?.column === column ?
|
|
85
|
+
const nowLine = position?.column === column ? <NowLineComponent {...position} /> : null;
|
|
87
86
|
return (
|
|
88
87
|
<ScheduleDay
|
|
89
88
|
key={day.localDate}
|
|
@@ -95,11 +94,11 @@ function ScheduleContent(props: ScheduleProps) {
|
|
|
95
94
|
highlightSource={highlightSource}
|
|
96
95
|
hours={hours}
|
|
97
96
|
press={(x, y) => press(column, x, y)}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
97
|
+
gridComponent={gridComponent}
|
|
98
|
+
columnComponent={columnComponent}
|
|
99
|
+
transitionComponent={transitionComponent}
|
|
100
|
+
intervalComponent={intervalComponent}
|
|
101
|
+
gapComponent={gapComponent}
|
|
103
102
|
nowLineZone={nowLine}
|
|
104
103
|
/>
|
|
105
104
|
);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ComponentType } from 'react';
|
|
2
2
|
import type { StyleProp, ViewStyle } from 'react-native';
|
|
3
3
|
import type {
|
|
4
4
|
DayColumn,
|
|
@@ -53,9 +53,9 @@ export type ScheduleColumnInput = {
|
|
|
53
53
|
highlightSource?: Source;
|
|
54
54
|
press: (x: number, y: number) => void;
|
|
55
55
|
/** Covered rect filler shared with Roster; final bounds and stacking belong to the rect. */
|
|
56
|
-
|
|
56
|
+
intervalComponent: ComponentType<IntervalInput>;
|
|
57
57
|
/** Removed rect content shared with Roster; the column supplies its pressable bounds. */
|
|
58
|
-
|
|
58
|
+
gapComponent: ComponentType<GapInput>;
|
|
59
59
|
};
|
|
60
60
|
export type ScheduleHoursInput = { hours: number[]; pxPerHour: number };
|
|
61
61
|
/**
|
|
@@ -81,23 +81,23 @@ export type ScheduleProps = ScheduleInput &
|
|
|
81
81
|
ScheduleStyleProps & {
|
|
82
82
|
incompleteLabel?: string;
|
|
83
83
|
/** Frozen left hour labels; ScheduleGutter receives hours 0 through 23 and their scale. */
|
|
84
|
-
|
|
84
|
+
gutterComponent?: ComponentType<ScheduleHoursInput>;
|
|
85
85
|
/** Hour bands behind each day's rects; ScheduleGrid draws 24 bordered bands. */
|
|
86
|
-
|
|
86
|
+
gridComponent?: ComponentType<ScheduleHoursInput>;
|
|
87
87
|
/** Frozen day heading; ScheduleDayHeader shows weekday, localDate, and a transition badge. */
|
|
88
|
-
|
|
88
|
+
dayHeaderComponent?: ComponentType<{ day: DayColumn }>;
|
|
89
89
|
/** Wholly skipped local date marker; ScheduleSkippedDate labels its zero-width header gap. */
|
|
90
|
-
|
|
90
|
+
skippedDateComponent?: ComponentType<{ localDate: string }>;
|
|
91
91
|
/** The day's covered and removed rects; ScheduleColumn draws final bounds in layer order. */
|
|
92
|
-
|
|
92
|
+
columnComponent?: ComponentType<ScheduleColumnInput>;
|
|
93
93
|
/** Transition chrome only; ScheduleTransition hatches skips and divides repeats with again. */
|
|
94
|
-
|
|
94
|
+
transitionComponent?: ComponentType<ScheduleTransitionInput>;
|
|
95
95
|
/** Current day's line at y; ScheduleNowLine spans the containing column. */
|
|
96
|
-
|
|
96
|
+
nowLineComponent?: ComponentType<{ y: number; column: number }>;
|
|
97
97
|
/** Covered rect filler shared with Roster; position using rect bounds and use pointerEvents="none". */
|
|
98
|
-
|
|
98
|
+
intervalComponent?: ComponentType<IntervalInput>;
|
|
99
99
|
/** Removed rect content shared with Roster; ScheduleColumn supplies the invisible pressable. */
|
|
100
|
-
|
|
100
|
+
gapComponent?: ComponentType<GapInput>;
|
|
101
101
|
/** Completeness notice in reserved empty space above the day grid; receives the lane and localized label. */
|
|
102
|
-
|
|
102
|
+
incompleteComponent?: ComponentType<{ lane: Lane; label: string }>;
|
|
103
103
|
};
|