react-native-roster 0.4.1 → 0.4.2

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 CHANGED
@@ -1,9 +1,8 @@
1
1
  # react-native-roster
2
2
 
3
- **Lanes, layers, and sources on one time axis.**
4
-
5
- [![Gallery](https://img.shields.io/badge/▶%20Gallery-lanes%2C%20layers%2C%20and%20sources-4f46e5?style=for-the-badge)](https://simiancraft.github.io/react-native-roster/)
3
+ **Resource timeline and schedule components for React Native and web, with layered intervals, coverage, provenance, and explicit daylight-saving handling.**
6
4
 
5
+ [![npm version](https://img.shields.io/npm/v/react-native-roster?color=cb3837&logo=npm)](https://www.npmjs.com/package/react-native-roster)
7
6
  [![CI](https://github.com/simiancraft/react-native-roster/actions/workflows/ci.yml/badge.svg)](https://github.com/simiancraft/react-native-roster/actions/workflows/ci.yml)
8
7
  [![Coverage](https://img.shields.io/codecov/c/github/simiancraft/react-native-roster?logo=codecov)](https://codecov.io/github/simiancraft/react-native-roster)
9
8
  [![Types: included](https://img.shields.io/badge/types-included-3178c6?logo=typescript)](#contract)
@@ -12,32 +11,25 @@
12
11
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
13
12
  [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/simiancraft/react-native-roster/badge)](https://securityscorecards.dev/viewer/?uri=github.com/simiancraft/react-native-roster)
14
13
 
15
- A read surface for React Native and web that renders who is on, when, and why.
16
- Feed it absolute intervals with provenance; press any filled or removed span and
17
- get back the exact rules, rows, or exclusions that produced it. Daylight-saving
18
- transitions are drawn honestly: skipped hours are hatched, repeated hours appear
19
- twice. `Roster` stacks many lanes horizontally; `Schedule` opens one lane as a
20
- week of wall-clock columns. The core is standard JavaScript plus `Intl`, under
21
- 15 kB, and recurrence expansion is an optional subpath.
22
-
23
- It is not a calendar, a booking system, or a drag-to-create editor; you own
24
- mutations and persistence. If you want an event grid with gestures, an event
25
- calendar library is the better fit. If you need to explain coverage across two
26
- hundred people without lying about time zones, this is the one.
27
-
28
- - **Provenance on every span.** Intervals and gaps carry `sources`; presses
29
- return the winning span's `(kind, id)` list, including the exclusion that
30
- removed time.
31
- - **Honest time.** Elapsed-time roster weeks are 167 or 169 hours wide across a
32
- transition; schedule columns hatch skipped hours and draw repeated hours twice.
33
- - **Layers with roles.** `availability`, `booking`, and `custom` layers stack by
34
- z-order; coverage math unions availability, subtracts bookings, and sorts.
35
- - **Recurrence adapter.** Weekly, daily, and monthly rules with exclusions,
36
- caps, and content-keyed caches, kept out of the core.
37
- - **Zones everywhere.** Input-bearing regions accept component types, singletons accept nodes, and
38
- every chrome region takes `style` plus a NativeWind `className` twin.
39
- - **Virtualized and measured.** Geometry runs only for mounted lanes; CI gates
40
- layout, coverage, bundle size, and browser action budgets.
14
+ **Live demo: [explore the gallery](https://simiancraft.github.io/react-native-roster/)
15
+ or [try the showcase](https://simiancraft.github.io/react-native-roster/showcase).**
16
+
17
+ <!-- HERO GIF PLACEHOLDER: insert a recorded gallery or showcase tour here when available. -->
18
+
19
+ Give it lanes of absolute intervals and their sources. Get a resource timeline
20
+ across many lanes, or a day-column schedule for one lane, with coverage totals
21
+ and press callbacks that explain exactly which sources produced each span.
22
+
23
+ **It renders who is on, when, and why.** Press any covered or removed span and
24
+ get back the rules, rows, or exclusions behind it; removed time keeps its
25
+ exclusion sources. Daylight-saving transitions are drawn honestly: skipped hours
26
+ are hatched, repeated hours have separate geometry, and a week containing a
27
+ transition is 167 or 169 hours wide. `Roster` and `Schedule` project the same
28
+ lane data; `useRoster` and `useSchedule` expose their models for custom layouts.
29
+
30
+ Consumers own creation, dragging, resizing, and persistence. It ships
31
+ virtualized roster lanes, replaceable components, NativeWind chrome, and a small
32
+ standard JavaScript and `Intl` core with a [15 kB size gate](./.size-limit.json).
41
33
 
42
34
  ## Install
43
35
 
@@ -46,19 +38,29 @@ bun add react-native-roster @legendapp/list
46
38
  bunx expo install react-native-reanimated --bun
47
39
  ```
48
40
 
49
- To try an unreleased checkout instead, pack it with `bun pm pack --destination
50
- .cache` and `bun add` the printed tarball path. Keep React, React Native, and
51
- Expo aligned with your SDK; the
52
- tested demo uses Expo SDK 54, React 19.1, React Native 0.81.5, LegendList
53
- 2.0.19, and Reanimated 3.19.5. Web also needs React DOM and React Native Web.
54
- Peer ranges are React 18.2+, React Native 0.74+, and LegendList 2+; Reanimated
55
- is optional for core-only use and required by `Roster`. On Metro before React
56
- Native 0.79, enable `resolver.unstable_enablePackageExports` so the `/core`,
57
- `/rrule`, and `/nativewind` subpaths resolve.
41
+ For web, also install React DOM and React Native Web versions compatible with
42
+ your React/Expo setup, plus `bun add @radix-ui/react-popover@^1.1.23`.
43
+ **Radix is required whenever a web bundler resolves the root entry or
44
+ `/nativewind` to the web selection layout, even with selection disabled or only
45
+ `Schedule` imported.** That layout imports Radix unconditionally; the published
46
+ browser remap and Metro's web source resolution both reach it. Native and
47
+ `/core`-only or `/rrule`-only imports do not require Radix.
48
+
49
+ Peer ranges: React >=18.2, React Native >=0.74, LegendList >=2, Reanimated >=3.19,
50
+ Expo >=51 (optional), NativeWind >=4.1 (optional), and Radix ^1.1.23 (web).
51
+ Reanimated is required by `Roster` and optional for core-only use. Keep React,
52
+ React Native, and Expo aligned with your SDK. The demo uses Expo SDK 54,
53
+ React 19.1, React Native 0.81.5, LegendList 2.0.19, and Reanimated 3.19.5;
54
+ these versions do not establish support for every peer-range combination.
55
+
56
+ On Metro before React Native 0.79, enable `resolver.unstable_enablePackageExports`
57
+ so the subpaths resolve. Keep one React copy in workspaces.
58
+ `rrule-temporal` and `@js-temporal/polyfill` install as ordinary dependencies;
59
+ only `/rrule` imports them. Import isolation does not reduce installation size.
58
60
 
59
61
  ## Quick start: static intervals
60
62
 
61
- Twenty lines, no adapter. Give the component a bounded height.
63
+ Give the component a bounded height. No adapter is needed for absolute intervals.
62
64
 
63
65
  ```tsx
64
66
  import { Roster } from 'react-native-roster';
@@ -86,55 +88,17 @@ export function Example() {
86
88
  Press the interval to receive its `table / row-one` source. `minuteStep`
87
89
  defaults to 60 and accepts any positive divisor of 60; it changes ticks and
88
90
  empty-space snapping, never interval boundaries. `rowHeight` defaults to 48 and
89
- `pxPerMinute` to a minimum of 0.5, growing to fit wider viewports.
91
+ `pxPerMinute` to 0.5, growing to fit wider viewports. Both scale and row height
92
+ must be positive and finite.
90
93
 
91
94
  ## Quick start: recurring local hours
92
95
 
93
- ```tsx
94
- import { Roster } from 'react-native-roster';
95
- import type { Lane, WindowSpec } from 'react-native-roster/core';
96
- import { windowFor } from 'react-native-roster/core';
97
- import { expandRuleSet } from 'react-native-roster/rrule';
98
- import type { RuleSet } from 'react-native-roster/rrule';
99
-
100
- const spec: WindowSpec = {
101
- span: 'week', anchorDate: '2024-01-01', timezone: 'America/Chicago',
102
- };
103
- const set: RuleSet = {
104
- rules: [{
105
- id: 'weekday-hours', kind: 'include', frequency: 'WEEKLY',
106
- dtstart: '2024-01-01', byweekday: [0, 1, 2, 3, 4],
107
- hourstart: 9, hourend: 17, timezone: 'America/Chicago',
108
- }],
109
- dates: [{
110
- id: 'closed', kind: 'exclude', date: '2024-01-02',
111
- timezone: 'America/Chicago', note: 'Closed all day',
112
- }],
113
- };
114
- const result = expandRuleSet(set, windowFor(spec));
115
- const lane: Lane = {
116
- id: 'one', label: 'Lane one', timezone: 'America/Chicago',
117
- complete: result.complete,
118
- layers: [{
119
- id: 'open', role: 'availability', z: 0, style: { color: '#4f9478' },
120
- intervals: result.intervals, gaps: result.gaps,
121
- }],
122
- };
123
-
124
- export function RecurringExample() {
125
- return <Roster lanes={[lane]} windowSpec={spec}
126
- style={{ height: 480, flex: undefined }}
127
- onGapPress={(rect) => console.log(rect.sources)} />;
128
- }
129
- ```
130
-
131
- Tuesday is a gap carrying the dated exclusion; the other weekdays carry the
132
- include rule. Expand once per `windowFor(spec)` in your screen's hook, never
133
- inside a slot component; content-keyed caches reuse unchanged occurrences. The
134
- [adapter guide](./docs/adapters.md) covers validation, caps, cache lifetime,
135
- and mapping tables or feeds without a recurrence dependency;
136
- [recurrence semantics](./docs/recurrence.md) records the exact anchor, UNTIL,
137
- COUNT, and BYSETPOS rules.
96
+ Import `expandRuleSet` from `/rrule` to turn daily, weekly, or monthly local
97
+ hours and dated exclusions into intervals and gaps. Expand for `windowFor(spec)`
98
+ in your screen's hook, and carry `result.complete` into `Lane.complete`.
99
+ The [runnable recurring example](./docs/adapters.md#quick-start-recurring-local-hours)
100
+ shows the complete mapping; [recurrence semantics](./docs/recurrence.md) covers
101
+ validation, COUNT, UNTIL, and BYSETPOS. Never expand inside a slot component.
138
102
 
139
103
  ## Contract
140
104
 
@@ -172,58 +136,26 @@ and "Availability may be incomplete"; both are localizable props.
172
136
  | `react-native-roster/rrule` | `expandRuleSet`, `envelopeFor`, types, and expansion counters and caches. Uses pinned `rrule-temporal` and `@js-temporal/polyfill`. |
173
137
  | `react-native-roster/nativewind` | Registers `Roster` and `Schedule` with NativeWind so their `className` props resolve; re-exports the registered components. |
174
138
 
175
- Root and core never import recurrence dependencies. Public entry aliases retain the
176
- implementation function identities while keeping CommonJS export overhead within the size gates. Metro selects TypeScript
177
- source through the `react-native` export condition; other bundlers select
178
- emitted CommonJS with declarations in `dist/src`. The emit is CommonJS, so
179
- importing one function from the root costs the whole root bundle; import from
180
- `/core` for a core-only bundle. Cache keys, counters, and clearing are in
181
- [caches](./docs/caches.md).
139
+ Root and core never import recurrence dependencies. Metro selects TypeScript
140
+ source; the default condition selects CommonJS, and `types` selects declarations
141
+ in `dist/src`.
142
+ The CommonJS root pulls in the root bundle; use `/core` for core-only consumers.
143
+
144
+ ## Caches
145
+
146
+ Bump a supplied `lane.version` whenever layers change; otherwise structural layer
147
+ content supplies the key. Treat returned references as read-only. Consumers own
148
+ cache lifetime: clear layout, coverage, and expansion caches when discarding old
149
+ windows. Coverage runs for all lanes; geometry runs for mounted lanes.
150
+ [Cache keys, counters, and clearing](./docs/caches.md) explain target-warm reuse.
182
151
 
183
152
  ## Core
184
153
 
185
154
  ### Interval helpers
186
155
 
187
- `unionOf(spans: readonly Window[]): Window[]` merges overlapping or touching
188
- windows and returns them sorted by start, keeping disjoint windows separate.
189
- Empty input returns `[]`. Every returned window is a new object; the input array
190
- and its windows remain untouched. Like `extentOf` and `intersectionOf`, it uses
191
- end-exclusive epoch milliseconds and throws `RangeError` for non-finite bounds
192
- or `end <= start`.
193
-
194
- Collapse each person's segments with `extentOf`, then use `intersectionOf` to
195
- find the shared time across those windows. Each extent bridges gaps between
196
- segments, so the result describes the collapsed windows.
197
-
198
- ```ts
199
- import type { Window } from 'react-native-roster/core';
200
- import { extentOf, intersectionOf } from 'react-native-roster/core';
201
-
202
- const people: { name: string; segments: Window[] }[] = [
203
- {
204
- name: 'Alex',
205
- segments: [
206
- { start: 1_000, end: 3_000 },
207
- { start: 4_000, end: 8_000 },
208
- ],
209
- },
210
- {
211
- name: 'Sam',
212
- segments: [
213
- { start: 2_000, end: 5_000 },
214
- { start: 6_000, end: 9_000 },
215
- ],
216
- },
217
- ];
218
-
219
- const windows = people.map((person): Window => {
220
- const extent = extentOf(person.segments);
221
- if (extent === null) throw new Error(`${person.name} has no segments`);
222
- return extent;
223
- });
224
- const sharedTime = intersectionOf(windows); // { start: 2_000, end: 8_000 }
225
- // A null result from intersectionOf means there is no shared time.
226
- ```
156
+ `extentOf`, `intersectionOf`, and `unionOf` operate on end-exclusive absolute
157
+ windows. Extents bridge gaps; unions preserve disjoint spans. See the
158
+ [helper walkthrough](./src/core/README.md#interval-helper-walkthrough).
227
159
 
228
160
  ## Navigation and interaction
229
161
 
@@ -249,327 +181,54 @@ to `byLabel`; `byCoverage({ measure })` sorts by coverage descending.
249
181
 
250
182
  ## Now line
251
183
 
252
- The optional `now` prop accepts epoch milliseconds or `null` (the default, which
253
- draws nothing). Pass `now={timestamp}` to draw a vertical 2 px red line across the
254
- body at the projected instant. Only `window.start <= now < window.end` is visible.
255
- The caller owns clock updates; Roster starts no timer. `useRoster` exposes `now` and `nowLine`.
256
- `now` retains the raw instant. `nowLine` (`{ x, now }`) uses the fitted
257
- `pxPerMinute` scale and is null when `now` is null or outside the window.
258
- Replace `nowLineComponent` to customize the line using `RosterNowLineInput`
259
- (`{ x, now }`). The line follows horizontal scrolling; changing `now` does not
260
- invalidate mounted lanes. The every-zone gallery fixture can toggle a fixed
261
- instant at the window midpoint.
184
+ Pass controlled `now={timestamp}` to Roster; `null` (the default) draws nothing.
185
+ The caller owns clock updates. Only instants inside the end-exclusive window are visible. Replace `nowLineComponent` to style it; the
186
+ [Roster reference](./src/components/roster/README.md) covers hook and scroll behavior.
262
187
 
263
188
  ## Roster zones
264
189
 
265
- Props ending in `Component` accept `ComponentType<Input>` and are mounted by React.
266
- Props ending in `Zone` accept `ReactNode`. Define components at module scope so
267
- state survives rerenders; hooks and class components are supported. Compose the
268
- exported default components inside replacements to retain scrolling, geometry,
269
- or press behavior. Pass `null` to a node slot to suppress its default.
270
-
271
- | Slot | Component inputs or node | Default and behavior |
272
- | --- | --- | --- |
273
- | `emptyZone` | `ReactNode` | `RosterEmpty`: No lanes. |
274
- | `cornerZone` | `ReactNode` | `RosterCorner`: nothing; the cell above the labels, `laneLabelWidth` wide. |
275
- | `laneLabelComponent` | `lane`, `flag`, `complete`, `viewTimezone`, localized labels | `RosterLaneLabel`: label, differing IANA zone badge, and notices. |
276
- | `headerCellComponent` | `HeaderCellInput` (`tick`) | `RosterHeaderCell`: tick label. |
277
- | `intervalComponent` | `rect`, `layer`, `lane`, `highlighted` | `RosterInterval`: positioned colored rect, with final inset bounds. |
278
- | `intervalDetailComponent` | `rect`, `layer`, `lane`, `highlighted`, absolute `start` and `end`, `viewTimezone` | Absent by default; enables selection and fills its details. |
279
- | `selectionLayout` | `SelectionLayoutProps`: nodes, targetBounds, open, dismissal, host, and shared scroll | `RosterSelectionPopover`: native portal or Radix web popover; replace at runtime. |
280
- | `gapComponent` | `rect`, `layer`, `lane` | `RosterGap`: no visible content; the row supplies pressable bounds. |
281
- | `nowLineComponent` | `RosterNowLineInput` (`x`, `now`) | `RosterNowLine`: noninteractive vertical red line across the body. |
282
- | `gridComponent` | `ticks`, `contentWidth` | `RosterGrid`: one hairline per tick behind every lane. |
283
- | `headerComponent` | `ticks`, `projection`, `scroll`, `contentWidth`, `headerCellComponent` | `RosterHeader`: frozen header following horizontal offset. |
284
- | `laneLabelColumnComponent` | `labels`, `projection`, `scroll`, `laneLabelComponent` | `RosterLaneLabelColumn`: frozen labels following vertical offset. |
285
- | `bodyComponent` | Ordered `lanes`, `window`, `geometryFor`, `projection`, `scroll`, `press`, `ticks`, `viewport`, `contentWidth`, `nowLine`, `nowLineComponent`, highlight and hover, incomplete label, and rect components | `RosterBody`: virtualized lanes. |
286
-
287
- `RosterBody` composes `RosterBodyLayout`, which arranges `gridZone`, `listZone`, and optional `overlayZone`
288
- nodes with scroll wiring, and `RosterLaneList`, which owns LegendList and its
289
- per-lane callback. The body waits for viewport measurement before mounting the list.
290
-
291
- ```tsx
292
- import type { LaneLabelInput } from 'react-native-roster';
293
- import { Text } from 'react-native';
294
-
295
- function LaneLabel({ lane }: LaneLabelInput) {
296
- return <Text>{lane.label}</Text>;
297
- }
298
-
299
- <Roster lanes={lanes} windowSpec={windowSpec}
300
- laneLabelComponent={LaneLabel} cornerZone={<Text>People</Text>} />
301
- ```
302
-
303
- A custom interval component positions at `rect.x/y`, uses `rect.width/height/z`,
304
- and sets `pointerEvents="none"` so the parent hit-test walk owns presses. Gap
305
- fillers are already inside positioned pressables. `useRoster` exposes ordered
306
- lanes, coverage, lane state, ticks, `geometryFor`, shared scrolling, `press`,
307
- viewport measurement, navigation, and status for fully custom layouts.
308
-
309
- Chrome regions take style props: `style`, `headerStyle` (the 40 px header row
310
- holding the corner and ticks), `laneLabelColumnStyle`, and `bodyStyle`.
311
- `laneLabelWidth` sizes the corner and label column, default 180. Each style
312
- prop has a `className` twin; see [NativeWind](#nativewind).
190
+ Props ending in `Component` accept component types; props ending in `Zone`
191
+ accept React nodes. Define slot components at module scope to preserve state.
192
+ Use exported defaults inside replacements to retain geometry and interaction.
193
+ The [slot tables and examples](./docs/customization.md#roster-zones) cover every region.
313
194
 
314
195
  ### Selection
315
196
 
316
- Add `intervalDetailComponent` to enable pressed-interval details. The press still
317
- fires `onIntervalPress`; without the component, presses retain no selection.
318
- `useRoster` owns `selection` and `dismissSelection`.
319
- Pressing the selected interval again dismisses it; pressing another interval
320
- switches selection. Cell and gap presses dismiss selection while still firing
321
- `onCellPress` and `onGapPress`. These rules live in the hook and apply on native
322
- and web; web outside press and Escape still dismiss.
323
- Removing the selected lane, layer, or interval bounds clears selection. The private
324
- reconcileSelection helper matches source identity sets and chooses nearest bounds with
325
- a total difference of at most 1 ms for projection roundoff.
326
-
327
- Native has no intercepting dismissal overlay; body presses reach the hook, and the detail
328
- card captures its own presses. Web excludes the body wrapper from Radix outside
329
- dismissal, leaving body presses to the hook while preserving true outside presses and Escape.
330
- The hook toggles the selected interval closed, switches to another interval, and dismisses
331
- on cell or gap presses while preserving their callbacks. The body restores horizontal
332
- and vertical offsets from shared values on remount when selectionLayout changes.
333
- A mount effect calls the horizontal ScrollView ref's scrollTo without animation;
334
- native also retains contentOffset. LegendList restores initialScrollOffset through
335
- its own web mount effect and native initial offset.
336
-
337
- Current data and geometry replace old references, including after resizing or sorting. Reconciliation matches the layer id and
338
- order-insensitive source identities, then chooses the nearest absolute bounds. The sum
339
- of bound differences must be at most 1 ms. Stored bounds remain the display values.
340
-
341
- `selectable?: boolean` belongs only to the hook input, `RosterInput`, and defaults to false.
342
- It is excluded from `RosterProps`. Hook consumers
343
- pass `selectable: true` to `useRoster` to retain selection. `intervalDetailComponent`,
344
- `selectionLayout`, and `portalHost` live on `RosterProps` because the chassis mounts
345
- the content and layout. The chassis enables selection with
346
- `selectable: Boolean(intervalDetailComponent)`. Custom chassis mount their layout
347
- with the returned selection, dismissal, and scroll inputs.
348
-
349
- ```tsx
350
- import { Text, View } from 'react-native';
351
- import { Roster } from 'react-native-roster';
352
- import type { IntervalDetailInput, Lane } from 'react-native-roster';
353
-
354
- function IntervalDetails({ lane, layer, rect, start, end, viewTimezone }: IntervalDetailInput) {
355
- const format = new Intl.DateTimeFormat('en-US', { timeZone: viewTimezone, timeStyle: 'short' });
356
- return <View style={{ padding: 16, backgroundColor: 'white' }}>
357
- <Text>{lane.label}: {layer.label ?? layer.id}</Text>
358
- <Text>{format.format(start)} to {format.format(end)}</Text>
359
- {rect.sources.map((source) =>
360
- <Text key={JSON.stringify([source.kind, source.id])}>
361
- {source.label ?? source.id}
362
- </Text>)}
363
- </View>;
364
- }
365
-
366
- export function SelectionExample({ lanes }: { lanes: Lane[] }) {
367
- return <Roster lanes={lanes}
368
- windowSpec={{ span: 'day', anchorDate: '2024-01-01', timezone: 'UTC' }}
369
- style={{ height: 480, flex: undefined }}
370
- intervalDetailComponent={IntervalDetails} />;
371
- }
372
- ```
373
-
374
- Web consumers install `bun add @radix-ui/react-popover`. It is an optional peer
375
- for native and core-only consumers, and required by the web root entry. Consumers
376
- resolving the library from source with a web bundler (Vite, Storybook, or Metro web)
377
- must install it even without enabling selection because `selection-layout.web.tsx`
378
- is in the module graph. Native and core-only consumers do not need it.
379
- `RosterSelectionPopover` selects native or web through platform resolution.
380
- Native mounts a local `PortalHost` and registers `Portal` content; web uses Radix
381
- for portal placement, outside press, Escape, and collisions. The layout returns focus
382
- to the previously focused element only after Escape.
383
- The overlay tracks both scroll offsets with Reanimated shared values, without
384
- React scroll state. Native clamps details horizontally to the measured viewport,
385
- flips above when that fits, and uses the top edge when neither vertical placement
386
- fits. The default native popover waits for viewport measurement and scrolls oversized
387
- content on both axes within a maximum size of the viewport minus 8 px on each axis.
388
- Consumers wanting a different presentation supply `selectionLayout`.
389
- Native also dismisses on hardware back.
390
-
391
- `selectionLayout?: ComponentType<SelectionLayoutProps>` is the layout strategy
392
- naming exception to the `Component` suffix. The chassis mounts it with `anchorZone`
393
- (the body), `contentZone` (details or null), `targetBounds` (body-content bounds), `open`,
394
- `onDismiss`, `portalHost`, and `scroll`. The target bounds include the lane offset and
395
- interval inset. Layout scroll inputs are limited to `x`, `y`, `headerStyle`, and
396
- `labelStyle`. Render each node once. A consumer inspector
397
- can arrange the nodes in columns and call `onDismiss` from its close button.
398
- The [interval-detail gallery route](./demo/app/gallery/interval-detail.tsx)
399
- switches presentations at runtime.
400
-
401
- `portalHost` overrides the native destination name, whose default uses a unique
402
- per-roster `useId`. Give separate rosters separate override names. The default
403
- layout owns its host; do not also mount that name at an ancestor. Custom layouts
404
- can instead target their own ancestor host, using the exported `PortalHost` and
405
- `Portal`. A store-based portal does not preserve context from the registration
406
- site; place required providers above the host or re-provide them in the content.
407
- Selection never enters the lane list's body content key.
408
-
409
- Schedule does not yet support selection; it is a later change.
197
+ Add `intervalDetailComponent` for pressed-interval details. Pressing the same
198
+ interval dismisses it; pressing another switches selection. Cell and gap presses
199
+ also dismiss, preserving their callbacks. Web outside press and Escape dismiss.
200
+ Schedule does not support selection. Custom `selectionLayout` implementations
201
+ must follow the [layout and portal contract](./docs/customization.md#selection),
202
+ including unique native host names and provider placement above the host.
410
203
 
411
204
  ### Consumer data in slot components
412
205
 
413
- For consumer data beyond a slot's Input, mount a React context provider above
414
- `Roster` and read it with `useContext` in a module-scope slot component. This
415
- example supplies density and locale while keeping the header cell type stable:
416
-
417
- ```tsx
418
- import { createContext, useContext } from 'react';
419
- import { Text } from 'react-native';
420
- import type { HeaderCellInput, RosterProps } from 'react-native-roster';
421
- import { Roster } from 'react-native-roster';
422
-
423
- type SlotPreferences = {
424
- density: 'compact' | 'comfortable';
425
- locale: string;
426
- };
427
-
428
- const SlotPreferencesContext = createContext<SlotPreferences>({
429
- density: 'comfortable',
430
- locale: 'en-US',
431
- });
432
-
433
- function LocalizedHeaderCell({ tick }: HeaderCellInput) {
434
- const { density, locale } = useContext(SlotPreferencesContext);
435
- const label = new Intl.DateTimeFormat(locale, {
436
- timeZone: 'UTC',
437
- ...(tick.kind === 'day'
438
- ? { month: 'short', day: 'numeric' }
439
- : { hour: 'numeric', minute: '2-digit' }),
440
- }).format(tick.time);
441
-
442
- return (
443
- <Text numberOfLines={1} style={{ padding: density === 'compact' ? 2 : 6 }}>
444
- {label}
445
- </Text>
446
- );
447
- }
448
-
449
- export function ConsumerRoster({ lanes }: Pick<RosterProps, 'lanes'>) {
450
- return (
451
- <SlotPreferencesContext.Provider value={{ density: 'compact', locale: 'en-GB' }}>
452
- <Roster
453
- lanes={lanes}
454
- windowSpec={{ span: 'week', anchorDate: '2024-01-01', timezone: 'UTC' }}
455
- style={{ height: 480, flex: undefined }}
456
- headerCellComponent={LocalizedHeaderCell}
457
- />
458
- </SlotPreferencesContext.Provider>
459
- );
460
- }
461
- ```
462
-
463
- An inline closure or a memoized factory that returns a new component per render
464
- is not the recommended path. A new interval component type remounts every interval
465
- and defeats the body's content key. Context supplies changing consumer data while
466
- preserving the module-scope component type.
206
+ Use context with stable slot types; see the [complete recipe](./docs/customization.md#consumer-data-in-slot-components).
467
207
 
468
208
  ## Schedule and its zones
469
209
 
470
- ```tsx
471
- import { Schedule } from 'react-native-roster';
472
- import type { Lane } from 'react-native-roster/core';
473
-
474
- export function Week({ lane }: { lane: Lane }) {
475
- return <Schedule lane={lane} minuteStep={60}
476
- style={{ height: 600, flex: undefined }}
477
- windowSpec={{ span: 'week', anchorDate: '2024-01-01', timezone: 'UTC' }}
478
- onIntervalPress={(rect) => console.log(rect.sources)} />;
479
- }
480
- ```
481
-
482
- Only day and week specs are accepted. The component measures its width,
483
- reserves a 48 px gutter, and fits the day columns without horizontal scrolling.
484
- `pxPerHour` defaults to 48. Chrome takes `style`, `headerStyle`, `gutterStyle`,
485
- and `daysStyle`, each with a `className` twin.
486
-
487
- | Slot | Component inputs or node | Default and behavior |
488
- | --- | --- | --- |
489
- | `gutterComponent` | `hours`, `pxPerHour` | `ScheduleGutter`: 24 frozen hour labels. |
490
- | `gridComponent` | `hours`, `pxPerHour` | `ScheduleGrid`: 24 bordered hour bands behind each day's rects. |
491
- | `dayHeaderComponent` | `day` | `ScheduleDayHeader`: weekday, localDate, and transition badge. |
492
- | `skippedDateComponent` | `localDate` | `ScheduleSkippedDate`: zero-width header marker for a wholly skipped date. |
493
- | `columnComponent` | `day`, `rects`, `gapRects`, `lane`, `highlightSource`, `press`, interval and gap components | `ScheduleColumn`: final rect bounds in layer order. |
494
- | `transitionComponent` | `day`, `transition`, `y`, `dividerY`, `height`, `width` | `ScheduleTransition`: skipped-time hatch, or repeat divider and again label. |
495
- | `nowLineComponent` | `y`, `column` | `ScheduleNowLine`: line in the current day's column, updated each minute. |
496
- | `intervalComponent`, `gapComponent` | Same inputs as Roster | Shared `RosterInterval` and `RosterGap`. |
497
- | `incompleteComponent` | `lane`, `label` | `ScheduleIncomplete`: notice above the grid when the lane is incomplete. |
210
+ Pass the same lane to `<Schedule lane={lane} windowSpec={spec} />` with a bounded
211
+ height. Schedule accepts day and week specs, fits columns to measured width,
212
+ and reserves a 48 px gutter. `pxPerHour` defaults to 48 and must be positive and
213
+ finite. Its automatic now line updates each minute. See the
214
+ [example and slot table](./docs/customization.md#schedule-zones).
498
215
 
499
216
  ## NativeWind
500
217
 
501
- `className` is a first-class prop on `Roster` and `Schedule`, alongside `style`.
502
- Every chrome style prop has a class twin: `className`, `headerClassName`,
503
- `laneLabelColumnClassName`, and `bodyClassName` on Roster; `className`,
504
- `headerClassName`, `gutterClassName`, and `daysClassName` on Schedule. A class
505
- on a region replaces that region's default paint (background and border colors)
506
- while its structure (size, flex, overflow) stays. This is deliberate: React
507
- Native Web renders object styles inline, and inline paint would otherwise beat
508
- any class.
509
-
510
- Set up NativeWind 4 as usual (the demo's [babel.config.js](./demo/babel.config.js),
511
- [metro.config.js](./demo/metro.config.js), [tailwind.config.js](./demo/tailwind.config.js),
512
- and [global.css](./demo/global.css) are a working reference), add the optional
513
- peer with `bun add nativewind`, and register the components once at your app
514
- root:
515
-
516
- ```tsx
517
- // app/_layout.tsx
518
- import '../global.css';
519
- import 'react-native-roster/nativewind';
520
- ```
521
-
522
- ```tsx
523
- import { Roster } from 'react-native-roster';
524
-
525
- <Roster
526
- lanes={lanes}
527
- windowSpec={windowSpec}
528
- className="flex-1 rounded-xl bg-background"
529
- headerClassName="border-b border-border bg-card"
530
- laneLabelColumnClassName="border-r border-border bg-card"
531
- laneLabelWidth={220}
532
- />
533
- ```
534
-
535
- Slot components are ordinary React Native views, so a custom `laneLabelComponent` or
536
- `intervalComponent` uses `className` on `View` and `Text` directly. Without the
537
- entry point, class props are ignored and style props still work. The entry
538
- point is the package's only module with side effects and is listed in
539
- `sideEffects`. The [showcase route](./demo/app/showcase.tsx) styles every region
540
- and zone this way in light and dark palettes.
218
+ Install NativeWind >=4.1 and configure it for your app, then import
219
+ `react-native-roster/nativewind` once at the root. Every chrome style prop has a
220
+ `className` twin; classes replace default paint while preserving structure.
221
+ Slot components use `className` on ordinary views. Without registration, class
222
+ props are ignored. [Setup, examples, and demo configuration](./src/nativewind/README.md#setup-and-styling)
223
+ show all regions, including the [showcase route](./demo/app/showcase.tsx).
541
224
 
542
225
  ## Gallery and platform support
543
226
 
544
- The [Expo gallery](https://simiancraft.github.io/react-native-roster/) is the
545
- demo; there is no Storybook. Run it locally with `bun install --frozen-lockfile`,
546
- `bun run build`, and `bun run demo:web` (add `EXPO_OFFLINE=1` in a restricted
547
- network). The home page lists every fixture route:
548
-
549
- - Empty, one, 20, and 200 lanes; inset layers; default and replaced zones.
550
- - Day, week, and month routes at 15, 30, and 60-minute steps.
551
- - Full-day gaps, lane flags, equal-z precedence, highlight, coverage sorting,
552
- and incomplete expansion.
553
- - Chicago DST weeks, mixed rule, lane, and view zones, and editable adapter routes.
554
- - Schedule empty, layered, excluded, spring, fall, Lord Howe, Apia, incomplete,
555
- midnight, side-by-side projections, and every zone.
556
- - The showcase at `/showcase`: twelve generated people across seven zones,
557
- weekly hours and exclusions from the recurrence adapter, booked events, day
558
- and week spans, sorting, filtering, and a per-person Schedule, styled with
559
- NativeWind class props and slot components, with a sun and moon theme toggle.
560
- Names, titles, and the organization come from `@faker-js/faker` with a fixed
561
- seed; any resemblance to real people is coincidental.
562
- Each event compares scheduled time with seeded attendee arrivals and departures.
563
- A fixed seeded now separates past, live, and future events; attendees are expected,
564
- pending, present, attended, or absent. Present spans extend through now without
565
- exposing future departures, including after scheduled end until actual departure.
566
- A bottom strip draws the union of attendances over the lighter scheduled block,
567
- preserving gaps and overhang. The event detail popover shares a shaded scheduled
568
- band across attendance rows. Its footer status line defaults to the attendance
569
- legend and scheduled-band explanation; web hover or visible keyboard focus shows
570
- row detail, and native taps toggle detail or switch rows. The inspector retains
571
- member selection. This feature lives in
572
- [demo/components/team-roster](./demo/components/team-roster/README.md).
227
+ The gallery covers lane counts, layer precedence, coverage sorting, provenance,
228
+ DST transitions, editable recurrence, and replaceable components. The showcase
229
+ combines filtering, per-lane schedules, and attendance details. See the
230
+ [fixture catalog and local setup](./demo/components/gallery/README.md#run-the-gallery-and-explore-fixtures)
231
+ and [showcase tour](./demo/components/team-roster/README.md#showcase-tour).
573
232
 
574
233
  | Platform | Support and evidence |
575
234
  | --- | --- |
@@ -585,11 +244,25 @@ Workload W is 200 lanes, one week, two layers, a 24-lane viewport, and
585
244
  [committed CI baseline](./test/performance/baseline.json) on GitHub Actions
586
245
  ubuntu-latest records target-cold layout of 24 lanes at 2.070 ms and coverage of
587
246
  all 200 lanes at 1.063 ms, gated below 16 ms everywhere and within 1.5 times the
588
- baseline in CI. Size gates hold core under 15 kB and root under 44 kB. Device
247
+ baseline in CI. Minified, uncompressed size gates with peers external hold core under 15 kB and root under 52 kB. Device
589
248
  captures and the paired comparison against other libraries are described, not
590
249
  yet recorded, in the [performance guide](./docs/performance.md); CI timings are
591
250
  not phone frame rates.
592
251
 
252
+ ## Compared to
253
+
254
+ | Alternative | Choose it for | What react-native-roster adds to that model |
255
+ | --- | --- | --- |
256
+ | [react-native-calendar-kit](https://howljs.github.io/react-native-calendar-kit/) | Creation/editing gestures and pinch-to-zoom in a React Native calendar. | Layer-role coverage math and exact source sets on covered and removed spans. |
257
+ | [react-native-big-calendar](https://github.com/acro5piano/react-native-big-calendar) | Day, week, and month views of events. | A many-resource timeline and a single-lane schedule over the same layered interval contract. |
258
+ | [FullCalendar's resource timeline](https://fullcalendar.io/docs/timeline-view) | A web resource timeline with [dragging and resizing](https://fullcalendar.io/docs/event-dragging-resizing). | React Native components plus a renderer-free coverage/provenance core and explicit skipped/repeated wall-time geometry. |
259
+
260
+ These are differences in the supplied models, not claims that an alternative
261
+ cannot be extended. Their linked documentation describes their features; this
262
+ package's [contract](./src/core/types.ts), [coverage](./src/core/coverage.ts), and
263
+ [geometry](./docs/timezones.md) document its side. No relative performance
264
+ comparison has been recorded; see [performance evidence](./docs/performance.md).
265
+
593
266
  ## What this isn't
594
267
 
595
268
  This is a read surface. Consumers own creation, dragging, resizing,
@@ -616,7 +289,7 @@ publint, size-limit, and Playwright.
616
289
  - Area landing pages: [core](./src/core/README.md), [Roster](./src/components/roster/README.md),
617
290
  [Schedule](./src/components/schedule/README.md), [layers](./src/components/layers/README.md),
618
291
  [primitives](./src/components/primitives/README.md), [rrule adapter](./src/adapters/rrule/README.md),
619
- [nativewind](./src/nativewind/README.md), the [gallery](./demo/components/gallery/README.md), and the
292
+ [nativewind](./src/nativewind/README.md), the [gallery](./demo/components/gallery/README.md), the
620
293
  [showcase](./demo/components/team-roster/README.md), and the [theme](./demo/components/theme/README.md);
621
294
  [adding an adapter](./docs/adding-an-adapter.md)
622
295
  is the contributor recipe.
package/llms.txt CHANGED
@@ -1,10 +1,9 @@
1
1
  # react-native-roster
2
2
 
3
- > React Native read surfaces over layered absolute intervals with provenance.
3
+ > Resource timeline and schedule components for React Native and web, with layered intervals, coverage, provenance, and explicit daylight-saving handling.
4
4
  > Roster projects many lanes horizontally; Schedule projects one lane into day
5
5
  > columns. The core owns geometry, coverage, axis math, and caches. Adapters own
6
- > upstream conversion and recurrence. The gallery and integration guides describe
7
- > the implemented surface; package distribution status is in README.md.
6
+ > upstream conversion and recurrence.
8
7
 
9
8
  This file is for an agent integrating the package into a consumer app. Follow the
10
9
  setup, contract, and cache rules below. You do not need the repository's issues
@@ -13,32 +12,32 @@ this repository. The demo is the working integration example, not a dependency.
13
12
 
14
13
  ## Setup and first render
15
14
 
16
- Use Bun 1.4.2 and Node 22 for this checkout. Run at repository root:
17
-
18
15
  ```sh
19
- bun install --ignore-scripts --frozen-lockfile
20
- bun run build
21
- bun run demo:web
16
+ bun add react-native-roster @legendapp/list
17
+ bunx expo install react-native-reanimated --bun
22
18
  ```
23
19
 
24
- The root install also installs demo dependencies. Use `EXPO_OFFLINE=1 bun run
25
- demo:web` if Expo cannot write its online dependency-validation cache. To consume from another app,
26
- `bun add react-native-roster`; for an unreleased checkout, run
27
- `bun pm pack --destination .cache` here, then `bun add` the actual absolute
28
- tarball path in your app. Add `@legendapp/list` and install the SDK's
29
- compatible Reanimated with `bunx expo install react-native-reanimated --bun`.
30
- Keep React, React Native, and Expo aligned with the app's SDK. The demo uses Expo
31
- 54, React 19.1, RN 0.81.5, LegendList 2.0.19, and Reanimated 3.19.5. NativeWind is
32
- an optional peer (>=4.1); import react-native-roster/nativewind once at the app
33
- root to enable className props on Roster and Schedule. Web needs compatible
34
- React DOM and React Native Web. See demo/babel.config.js for workspace presets.
35
-
36
- Peer ranges: React >=18.2, React Native >=0.74, Expo >=51, LegendList >=2.
37
- Reanimated >=3.19 is optional for core-only installs and required for Roster.
38
- Tested dependency versions are not proof of every combination in those ranges.
39
-
40
- Save this as a screen component, or as demo/app/gallery/readme-example.tsx with
41
- `export default function Example` for Expo Router, and visit /gallery/readme-example:
20
+ For web, also install React DOM and React Native Web versions compatible with
21
+ your React/Expo setup, plus `bun add @radix-ui/react-popover@^1.1.23`.
22
+ Radix is required whenever a web bundler resolves the root entry or
23
+ `/nativewind` to the web selection layout, even with selection disabled or only
24
+ `Schedule` imported. That layout imports Radix unconditionally; the published
25
+ browser remap and Metro's web source resolution both reach it. Native and
26
+ `/core`-only or `/rrule`-only imports do not require Radix.
27
+
28
+ Peer ranges: React >=18.2, React Native >=0.74, LegendList >=2, Reanimated >=3.19,
29
+ Expo >=51 (optional), NativeWind >=4.1 (optional), and Radix ^1.1.23 (web).
30
+ Reanimated is required by `Roster` and optional for core-only use. Keep React,
31
+ React Native, and Expo aligned with your SDK. The demo uses Expo SDK 54,
32
+ React 19.1, React Native 0.81.5, LegendList 2.0.19, and Reanimated 3.19.5;
33
+ these versions do not establish support for every peer-range combination.
34
+
35
+ On Metro before React Native 0.79, enable `resolver.unstable_enablePackageExports`
36
+ so the subpaths resolve. Keep one React copy in workspaces.
37
+ `rrule-temporal` and `@js-temporal/polyfill` install as ordinary dependencies;
38
+ only `/rrule` imports them. Import isolation does not reduce installation size.
39
+
40
+ Save this as a screen component in your app:
42
41
 
43
42
  ```tsx
44
43
  import { Roster } from 'react-native-roster';
@@ -61,8 +60,8 @@ export function Example() {
61
60
  ```
62
61
 
63
62
  Expect a green 09:00 to 17:00 interval on January 1, 2024. Scroll horizontally to
64
- see the time range. A bounded viewport is required. Remove the scratch route
65
- when finished. In a controlled app, own WindowSpec in your screen's hook; update
63
+ see the time range. A bounded viewport is required. In a controlled app, own
64
+ WindowSpec in your screen's hook; update
66
65
  it in navigation handlers, and prepare adapter output for windowFor(spec).
67
66
 
68
67
  ## Vocabulary
@@ -189,7 +188,7 @@ Component props are ComponentType<Input>.
189
188
 
190
189
  Roster required props: lanes: Lane[], windowSpec: WindowSpec.
191
190
  Optional: now (epoch ms or null, default null), minuteStep (60), rowHeight (48),
192
- pxPerMinute (minimum 0.5, fits viewport),
191
+ pxPerMinute (default 0.5, positive finite, grows to fit viewport),
193
192
  sortLanes (byLabel), highlightSource, onNavigate, onIntervalPress, onGapPress,
194
193
  onCellPress, onIntervalHover (web only), style, headerStyle, laneLabelColumnStyle,
195
194
  bodyStyle, laneLabelWidth (180), neverSetLabel, incompleteLabel, and zones below.
@@ -583,7 +582,7 @@ W: seed 1318, 200 lanes, week, two layers, 24-lane layout batch, 15-minute ticks
583
582
  Bun 1.4.0, 2026-09-08T05:18:00Z, commit
584
583
  1c4ac0e8580b1d46c043f946aa7ebc1ee316d0fa. Median of 11 target-cold samples after
585
584
  five runtime warmups. Each timing must stay below 16 ms; CI also enforces <=1.5x
586
- its baseline. Size gates: core <15 kB, root <44 kB minified/uncompressed, peers
585
+ its baseline. Size gates: core <15 kB, root <52 kB minified/uncompressed, peers
587
586
  external. Phone fps and cold-layout numbers are not yet captured; the paired
588
587
  react-big-scheduler comparison is not yet measured. Do not claim relative speed.
589
588
 
@@ -591,9 +590,25 @@ The package does not create, drag, resize, persist, or authorize intervals; it
591
590
  is not a general calendar. Consumers implement their own interactions. A rule
592
591
  editor in the gallery is an integration example, not a rule-authoring product.
593
592
 
593
+ ## Working from a checkout
594
+
595
+ Use Bun 1.4.2 and Node 22 for repository development. At the repository root:
596
+
597
+ ```sh
598
+ bun install --ignore-scripts --frozen-lockfile
599
+ bun run build
600
+ bun run demo:web
601
+ ```
602
+
603
+ The workspace install includes demo dependencies. Use `EXPO_OFFLINE=1 bun run
604
+ demo:web` if Expo cannot write its online dependency-validation cache. To consume
605
+ an unreleased checkout, run `bun pm pack --destination .cache`, then `bun add`
606
+ the printed absolute tarball path in your app.
607
+
594
608
  ## Reference
595
609
 
596
- - [README](./README.md): runnable quick starts, setup, zones, and measured evidence.
610
+ - [README](./README.md): published-package setup, quick start, positioning, and evidence.
611
+ - [Customization](./docs/customization.md): slot tables, selection layouts, and context recipe.
597
612
  - [Adapter guide](./docs/adapters.md): table/feed conversion and memoization contract.
598
613
  - Area landing pages: [core](./src/core/README.md), [Roster](./src/components/roster/README.md),
599
614
  [Schedule](./src/components/schedule/README.md), [layers](./src/components/layers/README.md),
package/package.json CHANGED
@@ -1,14 +1,24 @@
1
1
  {
2
2
  "name": "react-native-roster",
3
- "version": "0.4.1",
4
- "description": "Layered intervals with provenance across lanes and a shared time axis for React Native.",
3
+ "version": "0.4.2",
4
+ "description": "Resource timeline and schedule components for React Native and web, with layered intervals, coverage, provenance, and explicit daylight-saving handling.",
5
5
  "keywords": [
6
6
  "react-native",
7
+ "react-native-web",
7
8
  "expo",
8
9
  "roster",
9
- "lanes",
10
+ "timeline",
11
+ "resource-timeline",
12
+ "schedule",
13
+ "resource-scheduling",
14
+ "coverage",
10
15
  "intervals",
16
+ "timezone",
17
+ "dst",
18
+ "recurrence",
19
+ "rrule",
11
20
  "provenance",
21
+ "typescript",
12
22
  "nativewind"
13
23
  ],
14
24
  "author": "Jesse Harlin (the-simian) <info@simiancraft.com>",
@@ -8,8 +8,9 @@ provenance per span.
8
8
  - Exports: `expandRuleSet`, `envelopeFor`, `clearExpandCache`, `expandStats`,
9
9
  `resetExpandStats`, and the `RosterRule`, `RosterDate`, `RuleSet`,
10
10
  `ExpandOptions`, `ExpandResult`, and `ExpandStats` types
11
- - Imports: `../../core` only; never components or another adapter
12
- - Owns: `rrule-temporal` and the Temporal polyfill; root and core never load them
11
+ - Internal imports: `../../core` only; never components or another adapter
12
+ - Owns: `rrule-temporal` and the Temporal polyfill; ordinary installed dependencies
13
+ imported only by this subpath, never by root or core
13
14
 
14
15
  ## Supported input
15
16
 
@@ -26,7 +27,7 @@ unbounded strings are rejected by validation.
26
27
  | `index.ts` | the entry point; the only barrel |
27
28
  | `types.ts` | input and result types |
28
29
  | `validate.ts` | input rejection before any engine work |
29
- | `envelope.ts` | widens a window to whole local days per rule zone |
30
+ | `envelope.ts` | pads both absolute window bounds by 48 hours, shared across rule zones |
30
31
  | `occurrences.ts` | drives the engine and applies every calendar correction |
31
32
  | `net.ts` | nets includes against excludes into intervals and gaps |
32
33
  | `cache.ts`, `hash.ts` | retained occurrences and envelopes keyed by content |
@@ -8,7 +8,7 @@ projections draw the same components, so they live above Roster and Schedule.
8
8
  - `parts/interval.tsx`, `parts/gap.tsx`: the default components
9
9
  - `utils/styles.ts`: content-keyed style objects per layer
10
10
 
11
- Imports core only. Tests: `test/components/layers`.
11
+ Internal imports stay within layers and core; the components also import React Native. Tests: `test/components/layers`.
12
12
 
13
13
  Both projections accept intervalComponent and gapComponent as ComponentType inputs.
14
14
  Their data-owning lane or column mounts the component with the final rect, layer,
@@ -17,8 +17,8 @@ This feature is about a roster; its children are lanes.
17
17
  - `utils/`: ticks and the body content key
18
18
 
19
19
  Interval and gap components live in `../layers`; press geometry and `regionStyle`
20
- in `../primitives`. Zone contracts are documented in the README's Roster zones
21
- section and in `llms.txt`. Tests: `test/components/roster`.
20
+ in `../primitives`. See the [slot tables, selection contract, and context recipe](../../../docs/customization.md#roster-zones)
21
+ and `llms.txt` for integration details. Tests: `test/components/roster`.
22
22
 
23
23
  This feature is about a roster body; its children are lanes. `RosterBody` gates
24
24
  measurement and composes the body layout and lane list. `BodyInput` extends the
@@ -14,8 +14,8 @@ This feature is about a schedule; its children are days.
14
14
  - `utils/days.ts`: header dates, now position, and transition bounds
15
15
 
16
16
  Interval and gap components live in `../layers`; press geometry and `regionStyle`
17
- in `../primitives`. Zone contracts are documented in the README's Schedule
18
- section and in `llms.txt`. Tests: `test/components/schedule`.
17
+ in `../primitives`. Zone contracts and a complete example live in the
18
+ [customization guide](../../../docs/customization.md#schedule-zones) and `llms.txt`. Tests: `test/components/schedule`.
19
19
 
20
20
  The chassis binds default component types once; ScheduleDay mounts components
21
21
  with day data and passes nodes into ScheduleDayLayout. There is no black-box day
@@ -40,3 +40,46 @@ Reference: [design](../../docs/design.md), [timezones](../../docs/timezones.md),
40
40
 
41
41
  The public entry binds immutable function aliases directly to preserve implementation
42
42
  identity while avoiding repeated CommonJS getter wrappers in consumer bundles.
43
+
44
+ ## Interval helper walkthrough
45
+
46
+ `unionOf(spans: readonly Window[]): Window[]` merges overlapping or touching
47
+ windows and returns them sorted by start, keeping disjoint windows separate.
48
+ Empty input returns `[]`. Every returned window is a new object; the input array
49
+ and its windows remain untouched. Like `extentOf` and `intersectionOf`, it uses
50
+ end-exclusive epoch milliseconds and throws `RangeError` for non-finite bounds
51
+ or `end <= start`.
52
+
53
+ Collapse each person's segments with `extentOf`, then use `intersectionOf` to
54
+ find the shared time across those windows. Each extent bridges gaps between
55
+ segments, so the result describes the collapsed windows.
56
+
57
+ ```ts
58
+ import type { Window } from 'react-native-roster/core';
59
+ import { extentOf, intersectionOf } from 'react-native-roster/core';
60
+
61
+ const people: { name: string; segments: Window[] }[] = [
62
+ {
63
+ name: 'Alex',
64
+ segments: [
65
+ { start: 1_000, end: 3_000 },
66
+ { start: 4_000, end: 8_000 },
67
+ ],
68
+ },
69
+ {
70
+ name: 'Sam',
71
+ segments: [
72
+ { start: 2_000, end: 5_000 },
73
+ { start: 6_000, end: 9_000 },
74
+ ],
75
+ },
76
+ ];
77
+
78
+ const windows = people.map((person): Window => {
79
+ const extent = extentOf(person.segments);
80
+ if (extent === null) throw new Error(`${person.name} has no segments`);
81
+ return extent;
82
+ });
83
+ const sharedTime = intersectionOf(windows); // { start: 2_000, end: 8_000 }
84
+ // A null result from intersectionOf means there is no shared time.
85
+ ```
@@ -10,5 +10,49 @@ Registers `Roster` and `Schedule` with NativeWind's `cssInterop` so each
10
10
 
11
11
  Class props win over default paint because layouts compose regions with
12
12
  `regionStyle`, which drops the default when the override carries a class entry.
13
- Fine detail is styled through zones. See the README's NativeWind section.
13
+ Fine detail is styled through zones. See [setup and styling](#setup-and-styling).
14
+ Web imports also require Radix through Roster; see [Install](../../README.md#install).
14
15
  Tests: `test/package/nativewind-entry.test.ts`.
16
+
17
+ ## Setup and styling
18
+
19
+ `className` is a first-class prop on `Roster` and `Schedule`, alongside `style`.
20
+ Every chrome style prop has a class twin: `className`, `headerClassName`,
21
+ `laneLabelColumnClassName`, and `bodyClassName` on Roster; `className`,
22
+ `headerClassName`, `gutterClassName`, and `daysClassName` on Schedule. A class
23
+ on a region replaces that region's default paint (background and border colors)
24
+ while its structure (size, flex, overflow) stays. This is deliberate: React
25
+ Native Web renders object styles inline, and inline paint would otherwise beat
26
+ any class.
27
+
28
+ Set up NativeWind 4 as usual (the demo's [babel.config.js](../../demo/babel.config.js),
29
+ [metro.config.js](../../demo/metro.config.js), [tailwind.config.js](../../demo/tailwind.config.js),
30
+ and [global.css](../../demo/global.css) are a working reference), add the optional
31
+ peer with `bun add nativewind`, and register the components once at your app
32
+ root:
33
+
34
+ ```tsx
35
+ // app/_layout.tsx
36
+ import '../global.css';
37
+ import 'react-native-roster/nativewind';
38
+ ```
39
+
40
+ ```tsx
41
+ import { Roster } from 'react-native-roster';
42
+
43
+ <Roster
44
+ lanes={lanes}
45
+ windowSpec={windowSpec}
46
+ className="flex-1 rounded-xl bg-background"
47
+ headerClassName="border-b border-border bg-card"
48
+ laneLabelColumnClassName="border-r border-border bg-card"
49
+ laneLabelWidth={220}
50
+ />
51
+ ```
52
+
53
+ Slot components are ordinary React Native views, so a custom `laneLabelComponent` or
54
+ `intervalComponent` uses `className` on `View` and `Text` directly. Without the
55
+ entry point, class props are ignored and style props still work. The entry
56
+ point is the package's only module with side effects and is listed in
57
+ `sideEffects`. The [showcase route](../../demo/app/showcase.tsx) styles every region
58
+ and zone this way in light and dark palettes.