react-native-roster 0.4.3 → 0.4.5

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,301 +1,295 @@
1
- # react-native-roster
1
+ <p align="center">
2
+ <a href="https://simiancraft.github.io/react-native-roster/showcase">
3
+ <img src="https://raw.githubusercontent.com/simiancraft/react-native-roster/main/docs/assets/hero.png" alt="Twelve people as lanes on a shared day axis with a red now line, beside one person's week as a schedule" width="860" />
4
+ </a>
5
+ </p>
6
+
7
+ <p align="center">
8
+ <a href="https://simiancraft.github.io/react-native-roster/showcase">
9
+ <img src="https://img.shields.io/badge/▶%20Live%20demo-press%20a%20gap%2C%20see%20why-4f46e5?style=for-the-badge" alt="Live demo" />
10
+ </a>
11
+ </p>
2
12
 
3
- **Resource timeline and schedule components for React Native and web, with layered intervals, coverage, provenance, and explicit daylight-saving handling.**
13
+ # react-native-roster
4
14
 
5
15
  [![npm version](https://img.shields.io/npm/v/react-native-roster?color=cb3837&logo=npm)](https://www.npmjs.com/package/react-native-roster)
6
16
  [![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)
7
17
  [![Coverage](https://img.shields.io/codecov/c/github/simiancraft/react-native-roster?logo=codecov)](https://codecov.io/github/simiancraft/react-native-roster)
8
- [![Types: included](https://img.shields.io/badge/types-included-3178c6?logo=typescript)](#contract)
9
- [![Core under 15 kB](https://img.shields.io/badge/core-%3C15%20kB-0f766e)](./.size-limit.json)
10
- [![Expo SDK 54](https://img.shields.io/badge/Expo-SDK%2054-000020?logo=expo)](./demo/package.json)
11
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
18
+ [![Types: included](https://img.shields.io/npm/types/react-native-roster?color=3178c6&logo=typescript)](https://www.npmjs.com/package/react-native-roster)
12
19
  [![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)
20
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/simiancraft/react-native-roster/blob/main/LICENSE)
13
21
 
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).**
22
+ **"Why is nobody on Wednesday?"** Rosters and schedules for React Native and web. Press the empty Wednesday and it names the dentist appointment that emptied it.
16
23
 
17
- <!-- HERO GIF PLACEHOLDER: insert a recorded gallery or showcase tour here when available. -->
24
+ ```tsx
25
+ <Roster
26
+ lanes={lanes}
27
+ windowSpec={windowSpec}
28
+ onGapPress={(rect) => console.log(rect.sources)} // [{ kind: 'date', id: 'dentist' }]
29
+ />
30
+ ```
18
31
 
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.
32
+ A staffing screen gets asked who is on right now, who is free at 3, and why
33
+ nobody is on Wednesday. The third question needs the rule behind the rectangle,
34
+ so every interval and gap keeps its sources. Press an interval to get the sources
35
+ that produced it; press a gap to get the sources that removed it.
22
36
 
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.
37
+ Give each person or resource a lane. `Roster` draws every lane on one time axis;
38
+ `Schedule` draws any one of them as a week, days across and hours down. The same
39
+ lane feeds both.
29
40
 
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
+ `Roster` draws elapsed time, so the week the clocks change is 167 or 169 hours
42
+ wide; `Schedule` hatches the hour they skipped.
33
43
 
34
- ## Install
44
+ It does not create, drag, or resize anything, and it has no month grid; if you
45
+ need those, reach for [react-native-calendar-kit](https://github.com/howljs/react-native-calendar-kit)
46
+ or [react-native-big-calendar](https://github.com/acro5piano/react-native-big-calendar).
35
47
 
36
- ```sh
37
- bun add react-native-roster @legendapp/list
38
- bunx expo install react-native-reanimated --bun
39
- ```
48
+ [Browse every example](https://simiancraft.github.io/react-native-roster/) in the gallery.
49
+
50
+ ## Quickstart
51
+
52
+ ### Install
40
53
 
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.
54
+ These steps assume an Expo app.
48
55
 
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.
56
+ 1. Add the package and LegendList:
55
57
 
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
+ ```sh
59
+ bun add react-native-roster @legendapp/list
60
+ ```
60
61
 
61
- ## Quick start: static intervals
62
+ 2. Add Reanimated for your Expo SDK:
62
63
 
63
- Give the component a bounded height. No adapter is needed for absolute intervals.
64
+ ```sh
65
+ bunx expo install react-native-reanimated --bun
66
+ ```
67
+
68
+ 3. For web, add React DOM, React Native Web, and Radix Popover; every web build needs all three:
69
+
70
+ ```sh
71
+ bunx expo install react-dom react-native-web --bun
72
+ bun add '@radix-ui/react-popover@^1.1.23'
73
+ ```
74
+
75
+ Without Expo, follow the [Reanimated install guide](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/getting-started/)
76
+ for 3.19 or newer. On React Native older than 0.79, turn on
77
+ `resolver.unstable_enablePackageExports` in Metro.
78
+
79
+ ### Roster
80
+
81
+ The smallest useful roster: Alex works 09:00 to 17:00 UTC on Monday, and a table
82
+ row says so. `Roster` fills its parent by default; here it gets a fixed height.
64
83
 
65
84
  ```tsx
66
85
  import { Roster } from 'react-native-roster';
67
86
  import type { Lane } from 'react-native-roster/core';
68
87
 
69
- const lanes: Lane[] = [{
70
- id: 'one', label: 'Lane one', layers: [{
71
- id: 'open', role: 'availability', z: 0,
72
- style: { color: '#4f9478' },
73
- intervals: [{
74
- start: Date.UTC(2024, 0, 1, 9), end: Date.UTC(2024, 0, 1, 17),
75
- sources: [{ kind: 'table', id: 'row-one' }],
76
- }],
77
- }],
78
- }];
79
-
80
- export function Example() {
81
- return <Roster lanes={lanes}
82
- style={{ height: 480, flex: undefined }}
83
- windowSpec={{ span: 'week', anchorDate: '2024-01-01', timezone: 'UTC' }}
84
- onIntervalPress={(rect, lane) => console.log(lane.id, rect.sources)} />;
88
+ const lane: Lane = {
89
+ id: 'alex',
90
+ label: 'Alex',
91
+ layers: [
92
+ {
93
+ id: 'working-hours',
94
+ role: 'availability',
95
+ z: 0,
96
+ style: { color: '#4f9478' },
97
+ intervals: [
98
+ {
99
+ start: Date.UTC(2026, 8, 21, 9),
100
+ end: Date.UTC(2026, 8, 21, 17),
101
+ sources: [{ kind: 'table', id: 'alex-monday' }],
102
+ },
103
+ ],
104
+ },
105
+ ],
106
+ };
107
+
108
+ export function RosterExample() {
109
+ return (
110
+ <Roster
111
+ lanes={[lane]}
112
+ style={{ height: 480, flex: undefined }}
113
+ windowSpec={{ span: 'day', anchorDate: '2026-09-21', timezone: 'UTC' }}
114
+ onIntervalPress={(rect, pressedLane) => {
115
+ console.log(pressedLane.label, rect.sources); // Alex [{ kind: 'table', id: 'alex-monday' }]
116
+ }}
117
+ />
118
+ );
85
119
  }
86
120
  ```
87
121
 
88
- Press the interval to receive its `table / row-one` source. `minuteStep`
89
- defaults to 60 and accepts any positive divisor of 60; it changes ticks and
90
- empty-space snapping, never interval boundaries. `rowHeight` defaults to 48 and
91
- `pxPerMinute` to 0.5, growing to fit wider viewports. Both scale and row height
92
- must be positive and finite.
93
-
94
- ## Quick start: recurring local hours
122
+ Add lanes for more people. Add layers and they stack by `z`.
95
123
 
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.
124
+ ### Schedule
102
125
 
103
- ## Contract
126
+ Hand `Schedule` that same `lane` and it draws days across and hours down. It
127
+ takes day and week windows.
104
128
 
105
- | Term | Meaning |
106
- | --- | --- |
107
- | roster | Shared time axis, lanes, and layers. |
108
- | lane | One person or resource row; `id`, `label`, `layers`, and optional display metadata. |
109
- | layer | Ordered intervals and gaps with `id`, `role`, `z`, and `style`. |
110
- | interval | Covered `[start, end)` in epoch milliseconds, with `sources: Source[]`. |
111
- | gap | Removed `[start, end)` with exclusion sources; survives complete subtraction. |
112
- | source | `{ kind, id, label? }`; identity is `(kind, id)`, never the label. |
113
- | window | Absolute `{ start, end }` only; span and resolution belong to the axis. |
114
- | projection | Horizontal elapsed time, or day columns with wall-clock hours. |
115
- | adapter | Consumer-side conversion from upstream data to lanes and layers. |
116
- | schedule | One lane projected into days across and hours down. |
117
-
118
- `LayerRole` is `'availability' | 'booking' | 'custom'`. The first two feed
119
- coverage math (`availabilityMinutes`, `bookingMinutes`, and
120
- `availabilityMinusBookingMinutes`); use `custom` for anything else. `LayerStyle`
121
- is `{ color, highlightColor?, inset?, opacity? }`; the engine applies inset on
122
- the cross axis and returns final bounds for drawing and hit testing.
123
-
124
- `Lane` also accepts optional data-quality fields: `version` (bump when layers
125
- change), `timezone` (a badge), `flag: 'never-set'` for lanes you know were never
126
- configured, `complete: false` when an adapter cap truncated the result, and
127
- `meta` for your own display data. The notices default to "No availability set"
128
- and "Availability may be incomplete"; both are localizable props.
129
-
130
- ## Entry points
131
-
132
- | Import | Shipped surface |
133
- | --- | --- |
134
- | `react-native-roster` | `Roster`, `Schedule`, hooks, slot components, `RosterSelectionPopover`, `PortalHost`, `Portal`, and all core exports. |
135
- | `react-native-roster/core` | Types, `layoutLane`, `coverageFor`, `flagFor`, `extentOf`, `intersectionOf`, `unionOf`, axis helpers, comparators, and counters. Standard JavaScript and `Intl` only. |
136
- | `react-native-roster/rrule` | `expandRuleSet`, `envelopeFor`, types, and expansion counters and caches. Uses pinned `rrule-temporal` and `@js-temporal/polyfill`. |
137
- | `react-native-roster/nativewind` | Registers `Roster` and `Schedule` with NativeWind so their `className` props resolve; re-exports the registered components. |
138
-
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.
129
+ ```tsx
130
+ import { Schedule } from 'react-native-roster';
131
+
132
+ export function ScheduleExample() {
133
+ return (
134
+ <Schedule
135
+ lane={lane}
136
+ style={{ height: 480, flex: undefined }}
137
+ windowSpec={{ span: 'week', anchorDate: '2026-09-21', timezone: 'UTC' }}
138
+ />
139
+ );
140
+ }
141
+ ```
143
142
 
144
- ## Caches
143
+ ### Recurrence from rrule
145
144
 
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.
145
+ The `/rrule` adapter turns daily, weekly, and monthly rules into intervals and
146
+ gaps. Here is the Wednesday from the top of this page: weekdays 09:00 to 17:00
147
+ in New York, and a dentist appointment on the 23rd. Press the empty day and it
148
+ names `dentist`.
151
149
 
152
- ## Core
150
+ ```tsx
151
+ import { Roster } from 'react-native-roster';
152
+ import { windowFor } from 'react-native-roster/core';
153
+ import type { Lane, WindowSpec } from 'react-native-roster/core';
154
+ import { expandRuleSet } from 'react-native-roster/rrule';
155
+ import type { RuleSet } from 'react-native-roster/rrule';
156
+
157
+ const windowSpec: WindowSpec = {
158
+ span: 'week',
159
+ anchorDate: '2026-09-21',
160
+ timezone: 'America/New_York',
161
+ };
162
+
163
+ const ruleSet: RuleSet = {
164
+ rules: [
165
+ {
166
+ id: 'weekday-hours',
167
+ kind: 'include',
168
+ frequency: 'WEEKLY',
169
+ dtstart: '2026-09-21',
170
+ byweekday: [0, 1, 2, 3, 4],
171
+ hourstart: 9,
172
+ hourend: 17,
173
+ timezone: 'America/New_York',
174
+ },
175
+ ],
176
+ dates: [
177
+ { id: 'dentist', kind: 'exclude', date: '2026-09-23', timezone: 'America/New_York' },
178
+ ],
179
+ };
180
+
181
+ const result = expandRuleSet(ruleSet, windowFor(windowSpec)); // 4 intervals, 1 gap
182
+
183
+ const recurringLane: Lane = {
184
+ id: 'alex',
185
+ label: 'Alex',
186
+ timezone: 'America/New_York',
187
+ complete: result.complete,
188
+ layers: [
189
+ {
190
+ id: 'working-hours',
191
+ role: 'availability',
192
+ z: 0,
193
+ style: { color: '#4f9478' },
194
+ intervals: result.intervals,
195
+ gaps: result.gaps,
196
+ },
197
+ ],
198
+ };
199
+
200
+ export function RecurringRosterExample() {
201
+ return (
202
+ <Roster
203
+ lanes={[recurringLane]}
204
+ windowSpec={windowSpec}
205
+ style={{ height: 480, flex: undefined }}
206
+ onGapPress={(rect) => console.log(rect.sources)} // [{ kind: 'date', id: 'dentist' }]
207
+ />
208
+ );
209
+ }
210
+ ```
153
211
 
154
- ### Interval helpers
212
+ When the window can change, expand for the selected window in your screen's
213
+ hook. The [adapter guide](https://github.com/simiancraft/react-native-roster/blob/main/docs/adapters.md)
214
+ covers live data; the [recurrence reference](https://github.com/simiancraft/react-native-roster/blob/main/docs/recurrence.md)
215
+ lists the supported rules.
155
216
 
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).
217
+ ### NativeWind
159
218
 
160
- ## Navigation and interaction
219
+ react-native-roster supports [NativeWind](https://www.nativewind.dev/).
161
220
 
162
- A `WindowSpec` is `{ span: 'day' | 'week' | 'month', anchorDate, timezone, wkst? }`
163
- or `{ span: 'custom', window, timezone }`. `anchorDate` is a local `YYYY-MM-DD`.
164
- `windowFor`, `prev`, `next`, and `today` handle bounds and navigation; weeks
165
- start Monday (weekday 0) unless `wkst` says otherwise. Components accept
166
- `onNavigate` but ship no toolbar; your controls update the controlled spec.
221
+ 1. Install NativeWind 4.1 or newer and finish its
222
+ [setup](https://www.nativewind.dev/docs/getting-started/installation):
167
223
 
168
- Three zones take part in a render: the rule timezone interprets local hours in
169
- the adapter, the lane timezone is a badge, and `windowSpec.timezone` drives the
170
- projection. Changing the view zone preserves the local anchor and usually reuses
171
- expanded occurrences. Column spans, rollbacks across midnight, and pointer
172
- inversion are documented in [timezones](./docs/timezones.md).
224
+ ```sh
225
+ bun add 'nativewind@^4.1'
226
+ ```
173
227
 
174
- Presses resolve one result: intervals before gaps, highest z first, later
175
- layers winning equal-z ties. `onIntervalPress(rect, lane)` and
176
- `onGapPress(rect, lane)` return the contributing sources; `onCellPress(lane,
177
- time)` returns snapped absolute time for empty space. `onIntervalHover` is
178
- Roster's web-only pointer callback. `highlightSource={{ kind, id }}` applies
179
- `highlightColor` to every matching rect without new geometry. Sorting defaults
180
- to `byLabel`; `byCoverage({ measure })` sorts by coverage descending.
228
+ 2. Register the components once, in your app's root module:
181
229
 
182
- ## Now line
230
+ ```tsx
231
+ import 'react-native-roster/nativewind';
232
+ ```
183
233
 
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.
234
+ 3. Style with class props:
187
235
 
188
- ## Roster zones
236
+ ```tsx
237
+ <Roster
238
+ className="rounded-xl bg-white"
239
+ headerClassName="bg-slate-100"
240
+ laneLabelColumnClassName="bg-slate-100"
241
+ {...rest}
242
+ />
243
+ ```
189
244
 
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.
245
+ Every style prop has a `className` twin; the
246
+ [NativeWind page](https://github.com/simiancraft/react-native-roster/blob/main/src/nativewind/README.md)
247
+ lists them.
194
248
 
195
- ### Selection
249
+ ## Customization
196
250
 
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.
251
+ You can replace every region: header cells, lane labels, intervals, gaps, the
252
+ grid, the now line, and the detail popover. Props ending in `Component` take a
253
+ component type; props ending in `Zone` take a node. If you want your own layout
254
+ entirely, `useRoster` and `useSchedule` return the same models the components
255
+ render from. See [customization](https://github.com/simiancraft/react-native-roster/blob/main/docs/customization.md).
203
256
 
204
- ### Consumer data in slot components
257
+ ## Size and support
205
258
 
206
- Use context with stable slot types; see the [complete recipe](./docs/customization.md#consumer-data-in-slot-components).
259
+ Minified, with peers external: `/core` is 13.6 kB and the root entry is 51.2 kB.
260
+ `/rrule` adds 12.9 kB of its own code plus its two dependencies, `rrule-temporal`
261
+ and `@js-temporal/polyfill`, which install with the package. Web runs in CI on
262
+ every ready pull request. iOS and Android are implemented but have not been
263
+ verified on devices yet. Tested against Expo SDK 54, React Native 0.81, and
264
+ Reanimated 3.19.
207
265
 
208
- ## Schedule and its zones
266
+ ## Reference
209
267
 
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).
268
+ | Import | What you get |
269
+ | --- | --- |
270
+ | `react-native-roster` | `Roster`, `Schedule`, `useRoster`, `useSchedule`, the default slot components, and everything in `/core`. |
271
+ | `react-native-roster/core` | Types, layout, coverage, interval helpers, and window navigation. Standard JavaScript and `Intl` only. |
272
+ | `react-native-roster/rrule` | `expandRuleSet`, `envelopeFor`, and their caches. The only entry that imports `rrule-temporal` and `@js-temporal/polyfill`. |
273
+ | `react-native-roster/nativewind` | Registers the components with NativeWind. |
215
274
 
216
- ## NativeWind
275
+ - [Customization](https://github.com/simiancraft/react-native-roster/blob/main/docs/customization.md): slots, selection, and passing your data to slot components.
276
+ - [Adapters](https://github.com/simiancraft/react-native-roster/blob/main/docs/adapters.md) and [recurrence](https://github.com/simiancraft/react-native-roster/blob/main/docs/recurrence.md): getting upstream data into lanes.
277
+ - [Timezones](https://github.com/simiancraft/react-native-roster/blob/main/docs/timezones.md): rule, lane, and view timezones; skipped and repeated hours.
278
+ - [Caches](https://github.com/simiancraft/react-native-roster/blob/main/docs/caches.md): keys, versions, and clearing.
279
+ - [Design note](https://github.com/simiancraft/react-native-roster/blob/main/docs/design.md): why intervals, and why geometry is computed before render.
280
+ - [Migrations](https://github.com/simiancraft/react-native-roster/blob/main/docs/migrations.md): prop renames by version.
281
+ - [llms.txt](https://github.com/simiancraft/react-native-roster/blob/main/llms.txt): the full contract, defaults, and edge cases in one file, written for coding agents.
217
282
 
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).
283
+ ## Contributing
224
284
 
225
- ## Gallery and platform support
285
+ [CONTRIBUTING.md](https://github.com/simiancraft/react-native-roster/blob/main/CONTRIBUTING.md)
286
+ has setup and the commit rules; merging to `main` releases. Report security
287
+ issues through [SECURITY.md](https://github.com/simiancraft/react-native-roster/blob/main/SECURITY.md).
288
+ Release history is in the [changelog](https://github.com/simiancraft/react-native-roster/blob/main/CHANGELOG.md).
226
289
 
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).
290
+ ## License
232
291
 
233
- | Platform | Support and evidence |
234
- | --- | --- |
235
- | Web via React Native Web | Static export and Chromium gallery checks; first-class target. |
236
- | iOS | React Native implementation; device rendering and Hermes behavior not yet verified here. |
237
- | Android | React Native implementation; device rendering and release performance not yet captured. |
238
- | Node and Bun | Built core and adapter work without a renderer; root components require native peers or a web bundler. |
239
-
240
- ## Performance
241
-
242
- Workload W is 200 lanes, one week, two layers, a 24-lane viewport, and
243
- 15-minute ticks: 63 interval rects and 7 gap rects per lane. The
244
- [committed CI baseline](./test/performance/baseline.json) on GitHub Actions
245
- ubuntu-latest records target-cold layout of 24 lanes at 2.070 ms and coverage of
246
- all 200 lanes at 1.063 ms, gated below 16 ms everywhere and within 1.5 times the
247
- baseline in CI. Minified, uncompressed size gates with peers external hold core under 15 kB and root under 52 kB. Device
248
- captures and the paired comparison against other libraries are described, not
249
- yet recorded, in the [performance guide](./docs/performance.md); CI timings are
250
- not phone frame rates.
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
-
266
- ## What this isn't
267
-
268
- This is a read surface. Consumers own creation, dragging, resizing,
269
- persistence, and permission checks through their own interactions. It is not a
270
- month grid or an arbitrary event list, and it does not expand rules inside the
271
- core. The short [design note](./docs/design.md) explains the reasoning.
272
-
273
- ## Development and reference
274
-
275
- ```sh
276
- bunx playwright install chromium
277
- bun run check
278
- ```
292
+ MIT, copyright 2026 Jesse Harlin (the-simian). See [LICENSE](https://github.com/simiancraft/react-native-roster/blob/main/LICENSE)
293
+ and [NOTICE.md](https://github.com/simiancraft/react-native-roster/blob/main/NOTICE.md).
279
294
 
280
- `check` runs Biome, library, test, and demo typechecks, React Compiler safety,
281
- the library build, the static web export, tests with coverage, knip, strict
282
- publint, size-limit, and Playwright.
283
-
284
- - [Migrations](https://github.com/simiancraft/react-native-roster/blob/main/docs/migrations.md) lists prop renames by version with before-and-after examples.
285
- - [Adapter guide](./docs/adapters.md), [recurrence semantics](./docs/recurrence.md),
286
- [timezones](./docs/timezones.md), [caches](./docs/caches.md), and the
287
- [performance guide](./docs/performance.md).
288
- - [Design note](./docs/design.md): precompute, then render geometry.
289
- - Area landing pages: [core](./src/core/README.md), [Roster](./src/components/roster/README.md),
290
- [Schedule](./src/components/schedule/README.md), [layers](./src/components/layers/README.md),
291
- [primitives](./src/components/primitives/README.md), [rrule adapter](./src/adapters/rrule/README.md),
292
- [nativewind](./src/nativewind/README.md), the [gallery](./demo/components/gallery/README.md), the
293
- [showcase](./demo/components/team-roster/README.md), and the [theme](./demo/components/theme/README.md);
294
- [adding an adapter](./docs/adding-an-adapter.md)
295
- is the contributor recipe.
296
- - [llms.txt](./llms.txt): integration instructions for agents.
297
- - [CONTRIBUTING.md](./CONTRIBUTING.md) and [AGENTS.md](./AGENTS.md): contributor workflow.
298
- - [CHANGELOG.md](./CHANGELOG.md): release history, maintained by semantic-release.
299
- - [SECURITY.md](./SECURITY.md), [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md), and [NOTICE.md](./NOTICE.md).
300
-
301
- MIT, copyright 2026 Jesse Harlin (the-simian). See [LICENSE](./LICENSE).
295
+ <p align="center"><sub>Crafted with care by <a href="https://simiancraft.com">Simiancraft</a>.</sub></p>
@@ -0,0 +1,4 @@
1
+ export declare const focusSafeClip: {
2
+ overflow: "hidden";
3
+ };
4
+ //# sourceMappingURL=focus-safe-clip.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"focus-safe-clip.d.ts","sourceRoot":"","sources":["../../../../src/components/roster/focus-safe-clip.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,aAAa;;CAA6C,CAAC"}
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.focusSafeClip = void 0;
4
+ exports.focusSafeClip = { overflow: 'hidden' };
5
+ //# sourceMappingURL=focus-safe-clip.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"focus-safe-clip.js","sourceRoot":"","sources":["../../../../src/components/roster/focus-safe-clip.ts"],"names":[],"mappings":";;;AAEa,QAAA,aAAa,GAAG,EAAE,QAAQ,EAAE,QAAQ,EAAsB,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { ViewStyle } from 'react-native';
2
+ export declare const focusSafeClip: ViewStyle;
3
+ //# sourceMappingURL=focus-safe-clip.web.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"focus-safe-clip.web.d.ts","sourceRoot":"","sources":["../../../../src/components/roster/focus-safe-clip.web.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9C,eAAO,MAAM,aAAa,EAAsC,SAAS,CAAC"}
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.focusSafeClip = void 0;
4
+ exports.focusSafeClip = { overflow: 'clip' };
5
+ //# sourceMappingURL=focus-safe-clip.web.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"focus-safe-clip.web.js","sourceRoot":"","sources":["../../../../src/components/roster/focus-safe-clip.web.ts"],"names":[],"mappings":";;;AAEa,QAAA,aAAa,GAAG,EAAE,QAAQ,EAAE,MAAM,EAA0B,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../../../../src/components/roster/layout.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAGtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAEvD,KAAK,iBAAiB,GAAG,IAAI,CAAC,gBAAgB,EAAE,GAAG,MAAM,WAAW,GAAG,WAAW,CAAC,GAAG;IACpF,qDAAqD;IACrD,UAAU,EAAE,SAAS,CAAC;IACtB,mEAAmE;IACnE,UAAU,EAAE,SAAS,CAAC;IACtB,+FAA+F;IAC/F,mBAAmB,EAAE,SAAS,CAAC;IAC/B,kFAAkF;IAClF,QAAQ,EAAE,SAAS,CAAC;IACpB,QAAQ,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;CAC9C,CAAC;AAEF,wBAAgB,YAAY,CAAC,EAC3B,UAAU,EACV,UAAU,EACV,mBAAmB,EACnB,QAAQ,EACR,QAAQ,EACR,KAAK,EACL,WAAW,EACX,oBAAoB,EACpB,SAAS,EACT,cAAoB,GACrB,EAAE,iBAAiB,2CAuBnB"}
1
+ {"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../../../../src/components/roster/layout.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAItD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAEvD,KAAK,iBAAiB,GAAG,IAAI,CAAC,gBAAgB,EAAE,GAAG,MAAM,WAAW,GAAG,WAAW,CAAC,GAAG;IACpF,qDAAqD;IACrD,UAAU,EAAE,SAAS,CAAC;IACtB,mEAAmE;IACnE,UAAU,EAAE,SAAS,CAAC;IACtB,+FAA+F;IAC/F,mBAAmB,EAAE,SAAS,CAAC;IAC/B,kFAAkF;IAClF,QAAQ,EAAE,SAAS,CAAC;IACpB,QAAQ,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;CAC9C,CAAC;AAEF,wBAAgB,YAAY,CAAC,EAC3B,UAAU,EACV,UAAU,EACV,mBAAmB,EACnB,QAAQ,EACR,QAAQ,EACR,KAAK,EACL,WAAW,EACX,oBAAoB,EACpB,SAAS,EACT,cAAoB,GACrB,EAAE,iBAAiB,2CAuBnB"}
@@ -4,15 +4,16 @@ exports.RosterLayout = RosterLayout;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const react_native_1 = require("react-native");
6
6
  const region_style_1 = require("../primitives/region-style");
7
+ const focus_safe_clip_1 = require("./focus-safe-clip");
7
8
  function RosterLayout({ cornerZone, headerZone, laneLabelColumnZone, bodyZone, onLayout, style, headerStyle, laneLabelColumnStyle, bodyStyle, laneLabelWidth = 180, }) {
8
9
  return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: (0, region_style_1.regionStyle)(structure.root, paint.root, style), children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { style: (0, region_style_1.regionStyle)(structure.header, paint.header, headerStyle), children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: { width: laneLabelWidth }, children: cornerZone }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: structure.headerStrip, children: headerZone })] }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: structure.content, children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: (0, region_style_1.regionStyle)({ ...structure.labels, width: laneLabelWidth }, paint.labels, laneLabelColumnStyle), children: laneLabelColumnZone }), (0, jsx_runtime_1.jsx)(react_native_1.View, { onLayout: onLayout, style: (0, region_style_1.regionStyle)(structure.body, {}, bodyStyle), children: bodyZone })] })] }));
9
10
  }
10
11
  const structure = {
11
12
  root: { flex: 1, minHeight: 0, overflow: 'hidden' },
12
13
  header: { flexDirection: 'row', height: 40, overflow: 'hidden' },
13
- headerStrip: { flex: 1, minWidth: 0, overflow: 'hidden' },
14
+ headerStrip: { flex: 1, minWidth: 0, ...focus_safe_clip_1.focusSafeClip },
14
15
  content: { flex: 1, minHeight: 0, flexDirection: 'row' },
15
- labels: { overflow: 'hidden' },
16
+ labels: focus_safe_clip_1.focusSafeClip,
16
17
  body: { flex: 1, minWidth: 0, overflow: 'hidden' },
17
18
  };
18
19
  // Paint is what a class replaces; see regionStyle.
@@ -1 +1 @@
1
- {"version":3,"file":"layout.js","sourceRoot":"","sources":["../../../../src/components/roster/layout.tsx"],"names":[],"mappings":";;;;AAEA,+CAAoC;AACpC,6DAAyD;AAezD,sBAA6B,EAC3B,UAAU,EACV,UAAU,EACV,mBAAmB,EACnB,QAAQ,EACR,QAAQ,EACR,KAAK,EACL,WAAW,EACX,oBAAoB,EACpB,SAAS,EACT,cAAc,GAAG,GAAG,GACF;IAClB,OAAO,CACL,wBAAC,mBAAI,IAAC,KAAK,EAAE,IAAA,0BAAW,EAAC,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,aACzD,wBAAC,mBAAI,IAAC,KAAK,EAAE,IAAA,0BAAW,EAAC,SAAS,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,WAAW,CAAC,aACnE,uBAAC,mBAAI,IAAC,KAAK,EAAE,EAAE,KAAK,EAAE,cAAc,EAAE,YAAG,UAAU,GAAQ,EAC3D,uBAAC,mBAAI,IAAC,KAAK,EAAE,SAAS,CAAC,WAAW,YAAG,UAAU,GAAQ,IAClD,EACP,wBAAC,mBAAI,IAAC,KAAK,EAAE,SAAS,CAAC,OAAO,aAC5B,uBAAC,mBAAI,IACH,KAAK,EAAE,IAAA,0BAAW,EAChB,EAAE,GAAG,SAAS,CAAC,MAAM,EAAE,KAAK,EAAE,cAAc,EAAE,EAC9C,KAAK,CAAC,MAAM,EACZ,oBAAoB,CACrB,YAEA,mBAAmB,GACf,EACP,uBAAC,mBAAI,IAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAA,0BAAW,EAAC,SAAS,CAAC,IAAI,EAAE,EAAE,EAAE,SAAS,CAAC,YACxE,QAAQ,GACJ,IACF,IACF,CACR,CAAC;AACJ,CAAC;AAED,MAAM,SAAS,GAAG;IAChB,IAAI,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE;IACnD,MAAM,EAAE,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE;IAChE,WAAW,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE;IACzD,OAAO,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,aAAa,EAAE,KAAK,EAAE;IACxD,MAAM,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE;IAC9B,IAAI,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE;CAC1C,CAAC;AACX,mDAAmD;AACnD,MAAM,KAAK,GAAG;IACZ,IAAI,EAAE,EAAE,eAAe,EAAE,MAAM,EAAE;IACjC,MAAM,EAAE,EAAE,iBAAiB,EAAE,CAAC,EAAE,iBAAiB,EAAE,SAAS,EAAE;IAC9D,MAAM,EAAE,EAAE,gBAAgB,EAAE,CAAC,EAAE,gBAAgB,EAAE,SAAS,EAAE;CACpD,CAAC"}
1
+ {"version":3,"file":"layout.js","sourceRoot":"","sources":["../../../../src/components/roster/layout.tsx"],"names":[],"mappings":";;;;AAEA,+CAAoC;AACpC,6DAAyD;AACzD,uDAAkD;AAelD,sBAA6B,EAC3B,UAAU,EACV,UAAU,EACV,mBAAmB,EACnB,QAAQ,EACR,QAAQ,EACR,KAAK,EACL,WAAW,EACX,oBAAoB,EACpB,SAAS,EACT,cAAc,GAAG,GAAG,GACF;IAClB,OAAO,CACL,wBAAC,mBAAI,IAAC,KAAK,EAAE,IAAA,0BAAW,EAAC,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,aACzD,wBAAC,mBAAI,IAAC,KAAK,EAAE,IAAA,0BAAW,EAAC,SAAS,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,WAAW,CAAC,aACnE,uBAAC,mBAAI,IAAC,KAAK,EAAE,EAAE,KAAK,EAAE,cAAc,EAAE,YAAG,UAAU,GAAQ,EAC3D,uBAAC,mBAAI,IAAC,KAAK,EAAE,SAAS,CAAC,WAAW,YAAG,UAAU,GAAQ,IAClD,EACP,wBAAC,mBAAI,IAAC,KAAK,EAAE,SAAS,CAAC,OAAO,aAC5B,uBAAC,mBAAI,IACH,KAAK,EAAE,IAAA,0BAAW,EAChB,EAAE,GAAG,SAAS,CAAC,MAAM,EAAE,KAAK,EAAE,cAAc,EAAE,EAC9C,KAAK,CAAC,MAAM,EACZ,oBAAoB,CACrB,YAEA,mBAAmB,GACf,EACP,uBAAC,mBAAI,IAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAA,0BAAW,EAAC,SAAS,CAAC,IAAI,EAAE,EAAE,EAAE,SAAS,CAAC,YACxE,QAAQ,GACJ,IACF,IACF,CACR,CAAC;AACJ,CAAC;AAED,MAAM,SAAS,GAAG;IAChB,IAAI,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE;IACnD,MAAM,EAAE,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE;IAChE,WAAW,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,GAAG,+BAAa,EAAE;IACvD,OAAO,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,aAAa,EAAE,KAAK,EAAE;IACxD,MAAM,EAAE,+BAAa;IACrB,IAAI,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE;CAC1C,CAAC;AACX,mDAAmD;AACnD,MAAM,KAAK,GAAG;IACZ,IAAI,EAAE,EAAE,eAAe,EAAE,MAAM,EAAE;IACjC,MAAM,EAAE,EAAE,iBAAiB,EAAE,CAAC,EAAE,iBAAiB,EAAE,SAAS,EAAE;IAC9D,MAAM,EAAE,EAAE,gBAAgB,EAAE,CAAC,EAAE,gBAAgB,EAAE,SAAS,EAAE;CACpD,CAAC"}
package/llms.txt CHANGED
@@ -200,6 +200,8 @@ Native Web renders object styles inline where they would beat any class. Labels
200
200
  be incomplete". Supply a bounded height; Roster waits for viewport measurement
201
201
  before mounting LegendList. Reanimated keeps horizontal and vertical offsets
202
202
  outside React state.
203
+ On web, translated header and lane-label regions use non-scrolling clips, so browser
204
+ focus scrolling cannot move custom content independently of the roster body.
203
205
 
204
206
  useRoster returns window, projection, orderedLanes, coverage, laneState,
205
207
  geometryFor(lane), scroll, press(lane,x,y), ticks, contentWidth, viewport,
@@ -590,6 +592,26 @@ The package does not create, drag, resize, persist, or authorize intervals; it
590
592
  is not a general calendar. Consumers implement their own interactions. A rule
591
593
  editor in the gallery is an integration example, not a rule-authoring product.
592
594
 
595
+ ## Alternatives, evidence, and sizes
596
+
597
+ Choose react-native-calendar-kit for creation and editing gestures and pinch to
598
+ zoom; react-native-big-calendar for day, week, and month views of titled events;
599
+ FullCalendar's resource timeline for a web timeline with dragging and resizing.
600
+ This package adds layer-role coverage math, exact source sets on covered and
601
+ removed spans, one lane contract for a many-lane roster and a single-lane
602
+ schedule, and geometry for skipped and repeated wall time. It has no creation,
603
+ dragging, resizing, or month grid. No relative performance comparison exists.
604
+
605
+ Measured on GitHub Actions ubuntu-latest (test/performance/baseline.json):
606
+ target-cold layout of 24 lanes of a 200-lane week at 15-minute ticks takes
607
+ 2.070 ms; coverage of all 200 lanes takes 1.063 ms; both are gated below 16 ms.
608
+ Minified, uncompressed, peers external: /core 13.6 kB (limit 15), /rrule 12.9 kB
609
+ (limit 15), root 51.2 kB (limit 52), /nativewind 47.1 kB (limit 48). Web is
610
+ checked in Chromium on every pull request; iOS and Android device rendering,
611
+ Hermes behavior, and phone frame rates are not yet recorded. The demo pins Expo
612
+ SDK 54, React 19.1, React Native 0.81.5, LegendList 2.0.19, and Reanimated
613
+ 3.19.5; those versions do not establish every peer-range combination.
614
+
593
615
  ## Working from a checkout
594
616
 
595
617
  Use Bun 1.4.2 and Node 22 for repository development. At the repository root:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-roster",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
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",
@@ -41,6 +41,7 @@
41
41
  "types": "./dist/src/index.d.ts",
42
42
  "browser": {
43
43
  "./dist/src/components/primitives/press-point.js": "./dist/src/components/primitives/press-point.web.js",
44
+ "./dist/src/components/roster/focus-safe-clip.js": "./dist/src/components/roster/focus-safe-clip.web.js",
44
45
  "./dist/src/components/roster/lanes/interval-hover.js": "./dist/src/components/roster/lanes/interval-hover.web.js",
45
46
  "./dist/src/components/roster/selection/selection-layout.js": "./dist/src/components/roster/selection/selection-layout.web.js"
46
47
  },
@@ -30,6 +30,9 @@ gallery, not a supported customization point. Production Profiler callbacks are
30
30
  Input-bearing slots accept component types and mount in the data-owning parts.
31
31
  The chassis binds defaults once; emptyZone and cornerZone accept nodes. The body
32
32
  content key tracks interval and gap component identity, including class components.
33
+ The layout uses non-scrolling web clips around translated header and lane-label
34
+ content, preventing focus from offsetting either region independently. Native keeps
35
+ overflow clipping on the same regions.
33
36
 
34
37
  `now` is a controlled epoch millisecond value, default null. `useRoster` derives
35
38
  `nowLine` (`{ x, now }`) during render via `xAtTime` using the fitted horizontal
@@ -0,0 +1,3 @@
1
+ import type { ViewStyle } from 'react-native';
2
+
3
+ export const focusSafeClip = { overflow: 'hidden' } satisfies ViewStyle;
@@ -0,0 +1,3 @@
1
+ import type { ViewStyle } from 'react-native';
2
+
3
+ export const focusSafeClip = { overflow: 'clip' } as unknown as ViewStyle;
@@ -2,6 +2,7 @@ import type { ReactNode } from 'react';
2
2
  import type { LayoutChangeEvent } from 'react-native';
3
3
  import { View } from 'react-native';
4
4
  import { regionStyle } from '../primitives/region-style';
5
+ import { focusSafeClip } from './focus-safe-clip';
5
6
  import type { RosterStyleProps } from './roster.types';
6
7
 
7
8
  type RosterLayoutProps = Omit<RosterStyleProps, `${string}ClassName` | 'className'> & {
@@ -55,9 +56,9 @@ export function RosterLayout({
55
56
  const structure = {
56
57
  root: { flex: 1, minHeight: 0, overflow: 'hidden' },
57
58
  header: { flexDirection: 'row', height: 40, overflow: 'hidden' },
58
- headerStrip: { flex: 1, minWidth: 0, overflow: 'hidden' },
59
+ headerStrip: { flex: 1, minWidth: 0, ...focusSafeClip },
59
60
  content: { flex: 1, minHeight: 0, flexDirection: 'row' },
60
- labels: { overflow: 'hidden' },
61
+ labels: focusSafeClip,
61
62
  body: { flex: 1, minWidth: 0, overflow: 'hidden' },
62
63
  } as const;
63
64
  // Paint is what a class replaces; see regionStyle.