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.
Files changed (81) hide show
  1. package/README.md +108 -31
  2. package/dist/src/components/roster/body-layout.d.ts +9 -0
  3. package/dist/src/components/roster/body-layout.d.ts.map +1 -0
  4. package/dist/src/components/roster/body-layout.js +9 -0
  5. package/dist/src/components/roster/body-layout.js.map +1 -0
  6. package/dist/src/components/roster/index.d.ts +1 -1
  7. package/dist/src/components/roster/index.d.ts.map +1 -1
  8. package/dist/src/components/roster/index.js +10 -26
  9. package/dist/src/components/roster/index.js.map +1 -1
  10. package/dist/src/components/roster/lanes/lane.d.ts +2 -2
  11. package/dist/src/components/roster/lanes/lane.d.ts.map +1 -1
  12. package/dist/src/components/roster/lanes/lane.js +3 -8
  13. package/dist/src/components/roster/lanes/lane.js.map +1 -1
  14. package/dist/src/components/roster/parts/body.d.ts +3 -3
  15. package/dist/src/components/roster/parts/body.d.ts.map +1 -1
  16. package/dist/src/components/roster/parts/body.js +5 -20
  17. package/dist/src/components/roster/parts/body.js.map +1 -1
  18. package/dist/src/components/roster/parts/header-cell.d.ts +2 -4
  19. package/dist/src/components/roster/parts/header-cell.d.ts.map +1 -1
  20. package/dist/src/components/roster/parts/header-cell.js.map +1 -1
  21. package/dist/src/components/roster/parts/header.d.ts +1 -1
  22. package/dist/src/components/roster/parts/header.d.ts.map +1 -1
  23. package/dist/src/components/roster/parts/header.js +2 -2
  24. package/dist/src/components/roster/parts/header.js.map +1 -1
  25. package/dist/src/components/roster/parts/lane-label-column.d.ts +1 -1
  26. package/dist/src/components/roster/parts/lane-label-column.d.ts.map +1 -1
  27. package/dist/src/components/roster/parts/lane-label-column.js +2 -2
  28. package/dist/src/components/roster/parts/lane-label-column.js.map +1 -1
  29. package/dist/src/components/roster/parts/lane-list.d.ts +7 -0
  30. package/dist/src/components/roster/parts/lane-list.d.ts.map +1 -0
  31. package/dist/src/components/roster/parts/lane-list.js +25 -0
  32. package/dist/src/components/roster/parts/lane-list.js.map +1 -0
  33. package/dist/src/components/roster/roster.types.d.ts +19 -20
  34. package/dist/src/components/roster/roster.types.d.ts.map +1 -1
  35. package/dist/src/components/roster/utils/body-content-key.d.ts +1 -1
  36. package/dist/src/components/roster/utils/body-content-key.d.ts.map +1 -1
  37. package/dist/src/components/roster/utils/body-content-key.js +10 -10
  38. package/dist/src/components/roster/utils/body-content-key.js.map +1 -1
  39. package/dist/src/components/schedule/days/day.d.ts +2 -2
  40. package/dist/src/components/schedule/days/day.d.ts.map +1 -1
  41. package/dist/src/components/schedule/days/day.js +2 -11
  42. package/dist/src/components/schedule/days/day.js.map +1 -1
  43. package/dist/src/components/schedule/days/header-layout.d.ts +1 -1
  44. package/dist/src/components/schedule/days/header-layout.d.ts.map +1 -1
  45. package/dist/src/components/schedule/days/parts/column.d.ts +1 -1
  46. package/dist/src/components/schedule/days/parts/column.d.ts.map +1 -1
  47. package/dist/src/components/schedule/days/parts/column.js +3 -8
  48. package/dist/src/components/schedule/days/parts/column.js.map +1 -1
  49. package/dist/src/components/schedule/index.d.ts.map +1 -1
  50. package/dist/src/components/schedule/index.js +7 -8
  51. package/dist/src/components/schedule/index.js.map +1 -1
  52. package/dist/src/components/schedule/schedule.types.d.ts +17 -17
  53. package/dist/src/components/schedule/schedule.types.d.ts.map +1 -1
  54. package/dist/src/index.d.ts +3 -1
  55. package/dist/src/index.d.ts.map +1 -1
  56. package/dist/src/index.js +5 -1
  57. package/dist/src/index.js.map +1 -1
  58. package/dist/src/nativewind/index.d.ts +24 -26
  59. package/dist/src/nativewind/index.d.ts.map +1 -1
  60. package/docs/migrations.md +61 -0
  61. package/llms.txt +46 -29
  62. package/package.json +3 -2
  63. package/src/components/layers/README.md +7 -3
  64. package/src/components/roster/README.md +15 -4
  65. package/src/components/roster/body-layout.tsx +32 -0
  66. package/src/components/roster/index.tsx +63 -45
  67. package/src/components/roster/lanes/lane.tsx +16 -11
  68. package/src/components/roster/parts/body.tsx +17 -84
  69. package/src/components/roster/parts/header-cell.tsx +2 -2
  70. package/src/components/roster/parts/header.tsx +7 -2
  71. package/src/components/roster/parts/lane-label-column.tsx +2 -2
  72. package/src/components/roster/parts/lane-list.tsx +74 -0
  73. package/src/components/roster/roster.types.ts +17 -16
  74. package/src/components/roster/utils/body-content-key.ts +13 -13
  75. package/src/components/schedule/README.md +8 -3
  76. package/src/components/schedule/days/day.tsx +31 -18
  77. package/src/components/schedule/days/header-layout.tsx +1 -1
  78. package/src/components/schedule/days/parts/column.tsx +10 -10
  79. package/src/components/schedule/index.tsx +20 -21
  80. package/src/components/schedule/schedule.types.ts +13 -13
  81. package/src/index.ts +3 -0
@@ -1,91 +1,24 @@
1
- import { LegendList } from '@legendapp/list';
2
- import { Profiler, type ProfilerOnRenderCallback } from 'react';
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 { bodyContentKey } from '../utils/body-content-key';
4
+ import { RosterLaneList } from './lane-list';
7
5
 
8
- export function RosterBody({
9
- lanes,
10
- geometryFor,
11
- projection,
12
- scroll,
13
- press,
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
- <ScrollView
33
- testID="roster-horizontal-scroll"
34
- ref={scroll.bodyRef}
35
- horizontal
36
- onScroll={scroll.onBodyScroll}
37
- scrollEventThrottle={16}
38
- style={{ flex: 1 }}
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 { RosterTick } from '../roster.types';
2
+ import type { HeaderCellInput } from '../roster.types';
3
3
 
4
- export function RosterHeaderCell({ tick }: { tick: RosterTick }) {
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({ ticks, contentWidth, scroll, headerCellZone }: HeaderInput) {
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
- {headerCellZone({ tick })}
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
- laneLabelZone,
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
- {laneLabelZone(label)}
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
- headerCellZone: (input: { tick: RosterTick }) => ReactNode;
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
- laneLabelZone: (input: LaneLabelInput) => ReactNode;
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
- intervalZone: (input: IntervalInput) => ReactNode;
105
+ intervalComponent: ComponentType<IntervalInput>;
105
106
  /** Removed rect filler, placed inside a pressable by LaneRow. */
106
- gapZone: (input: GapInput) => ReactNode;
107
+ gapComponent: ComponentType<GapInput>;
107
108
  /** Noninteractive tick lines behind every lane, sized to the content width. */
108
- gridZone: (input: GridInput) => ReactNode;
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
- laneLabelZone?: (input: LaneLabelInput) => ReactNode;
137
+ laneLabelComponent?: ComponentType<LaneLabelInput>;
137
138
  /** Time label within one positioned header tick. */
138
- headerCellZone?: (input: { tick: RosterTick }) => ReactNode;
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
- intervalZone?: (input: IntervalInput) => ReactNode;
141
+ intervalComponent?: ComponentType<IntervalInput>;
141
142
  /** Removed rect content; LaneRow supplies the invisible pressable. */
142
- gapZone?: (input: GapInput) => ReactNode;
143
+ gapComponent?: ComponentType<GapInput>;
143
144
  /** Tick lines behind the lanes; RosterGrid draws one hairline per tick. */
144
- gridZone?: (input: GridInput) => ReactNode;
145
+ gridComponent?: ComponentType<GridInput>;
145
146
  /** Empty roster content, conventionally the text No lanes. */
146
- emptyZone?: () => ReactNode;
147
+ emptyZone?: ReactNode;
147
148
  /** Top-left cell above the lane labels; RosterCorner renders nothing. */
148
- cornerZone?: () => ReactNode;
149
+ cornerZone?: ReactNode;
149
150
  /** Frozen time header, supplied with ticks, scale, and shared scrolling. */
150
- headerZone?: (input: HeaderInput) => ReactNode;
151
+ headerComponent?: ComponentType<HeaderInput>;
151
152
  /** Frozen labels, supplied with ordered lanes and per-lane flag and completeness. */
152
- laneLabelColumnZone?: (input: LabelColumnInput) => ReactNode;
153
+ laneLabelColumnComponent?: ComponentType<LabelColumnInput>;
153
154
  /** Virtualized lane body, supplied with geometry, projection, scrolling, and press resolution. */
154
- bodyZone?: (input: BodyInput) => ReactNode;
155
+ bodyComponent?: ComponentType<BodyInput>;
155
156
  };
@@ -1,13 +1,13 @@
1
1
  import type { BodyInput } from '../roster.types';
2
2
 
3
- const zoneIds = new WeakMap<object, number>();
4
- let nextZoneId = 0;
3
+ const identityIds = new WeakMap<object, number>();
4
+ let nextIdentityId = 0;
5
5
 
6
- function zoneId(zone: object): number {
7
- let id = zoneIds.get(zone);
6
+ function identityId(identity: object): number {
7
+ let id = identityIds.get(identity);
8
8
  if (id === undefined) {
9
- id = nextZoneId++;
10
- zoneIds.set(zone, id);
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
- intervalZone,
20
- gapZone,
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
- | 'intervalZone'
29
- | 'gapZone'
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
- zoneId(intervalZone),
42
- zoneId(gapZone),
43
- onIntervalHover && zoneId(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` zone type from `schedule.types.ts`
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 zone input type
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 fillers live in `../layers`; press geometry and `regionStyle`
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<ScheduleProps, 'gridZone' | 'columnZone' | 'transitionZone' | 'intervalZone' | 'gapZone'>
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
- gridZone,
37
- columnZone,
38
- transitionZone,
39
- intervalZone,
40
- gapZone,
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={gridZone({ hours, pxPerHour: projection.pxPerHour })}
57
- columnZone={columnZone({
58
- day,
59
- lane,
60
- rects: geometry.rects.filter((rect) => rect.column === column),
61
- gapRects: geometry.gapRects.filter((rect) => rect.column === column),
62
- highlightSource,
63
- intervalZone,
64
- gapZone,
65
- press,
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
- {transitionZone({ day, transition, ...transitionBounds(day, transition, projection) })}
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 dayHeaderZone, aligned with its column. */
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
- intervalZone,
12
- gapZone,
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
- {intervalZone({
24
- rect,
25
- layer,
26
- lane,
27
- highlighted: rect.sources.some(
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
- {gapZone({ rect, layer, lane })}
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
- gutterZone = ScheduleGutter,
54
- dayHeaderZone = ScheduleDayHeader,
55
- skippedDateZone = ScheduleSkippedDate,
56
- columnZone = ScheduleColumn,
57
- gridZone = ScheduleGrid,
58
- transitionZone = ScheduleTransition,
59
- nowLineZone = ScheduleNowLine,
60
- intervalZone = RosterInterval,
61
- gapZone = RosterGap,
62
- incompleteZone = ScheduleIncomplete,
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 ? incompleteZone({ lane, label: incompleteLabel }) : null;
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={gutterZone({ hours, pxPerHour: projection.pxPerHour })}
73
+ gutterZone={<GutterComponent hours={hours} pxPerHour={projection.pxPerHour} />}
75
74
  dayHeaderZone={headerDates(windowSpec, days).map(({ localDate, day }) => {
76
- if (!day) return <Fragment key={localDate}>{skippedDateZone({ localDate })}</Fragment>;
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={dayHeaderZone({ day })}
80
+ headerZone={<DayHeaderComponent day={day} />}
82
81
  />
83
82
  );
84
83
  })}
85
84
  daysZone={days.map((day, column) => {
86
- const nowLine = position?.column === column ? nowLineZone(position) : null;
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
- gridZone={gridZone}
99
- columnZone={columnZone}
100
- transitionZone={transitionZone}
101
- intervalZone={intervalZone}
102
- gapZone={gapZone}
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 { ReactNode } from 'react';
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
- intervalZone: (input: IntervalInput) => ReactNode;
56
+ intervalComponent: ComponentType<IntervalInput>;
57
57
  /** Removed rect content shared with Roster; the column supplies its pressable bounds. */
58
- gapZone: (input: GapInput) => ReactNode;
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
- gutterZone?: (input: ScheduleHoursInput) => ReactNode;
84
+ gutterComponent?: ComponentType<ScheduleHoursInput>;
85
85
  /** Hour bands behind each day's rects; ScheduleGrid draws 24 bordered bands. */
86
- gridZone?: (input: ScheduleHoursInput) => ReactNode;
86
+ gridComponent?: ComponentType<ScheduleHoursInput>;
87
87
  /** Frozen day heading; ScheduleDayHeader shows weekday, localDate, and a transition badge. */
88
- dayHeaderZone?: (input: { day: DayColumn }) => ReactNode;
88
+ dayHeaderComponent?: ComponentType<{ day: DayColumn }>;
89
89
  /** Wholly skipped local date marker; ScheduleSkippedDate labels its zero-width header gap. */
90
- skippedDateZone?: (input: { localDate: string }) => ReactNode;
90
+ skippedDateComponent?: ComponentType<{ localDate: string }>;
91
91
  /** The day's covered and removed rects; ScheduleColumn draws final bounds in layer order. */
92
- columnZone?: (input: ScheduleColumnInput) => ReactNode;
92
+ columnComponent?: ComponentType<ScheduleColumnInput>;
93
93
  /** Transition chrome only; ScheduleTransition hatches skips and divides repeats with again. */
94
- transitionZone?: (input: ScheduleTransitionInput) => ReactNode;
94
+ transitionComponent?: ComponentType<ScheduleTransitionInput>;
95
95
  /** Current day's line at y; ScheduleNowLine spans the containing column. */
96
- nowLineZone?: (input: { y: number; column: number }) => ReactNode;
96
+ nowLineComponent?: ComponentType<{ y: number; column: number }>;
97
97
  /** Covered rect filler shared with Roster; position using rect bounds and use pointerEvents="none". */
98
- intervalZone?: (input: IntervalInput) => ReactNode;
98
+ intervalComponent?: ComponentType<IntervalInput>;
99
99
  /** Removed rect content shared with Roster; ScheduleColumn supplies the invisible pressable. */
100
- gapZone?: (input: GapInput) => ReactNode;
100
+ gapComponent?: ComponentType<GapInput>;
101
101
  /** Completeness notice in reserved empty space above the day grid; receives the lane and localized label. */
102
- incompleteZone?: (input: { lane: Lane; label: string }) => ReactNode;
102
+ incompleteComponent?: ComponentType<{ lane: Lane; label: string }>;
103
103
  };