react-native-roster 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +73 -3
  2. package/dist/src/components/roster/body-layout.d.ts +3 -1
  3. package/dist/src/components/roster/body-layout.d.ts.map +1 -1
  4. package/dist/src/components/roster/body-layout.js +6 -2
  5. package/dist/src/components/roster/body-layout.js.map +1 -1
  6. package/dist/src/components/roster/index.d.ts.map +1 -1
  7. package/dist/src/components/roster/index.js +4 -3
  8. package/dist/src/components/roster/index.js.map +1 -1
  9. package/dist/src/components/roster/parts/body.d.ts.map +1 -1
  10. package/dist/src/components/roster/parts/body.js +2 -2
  11. package/dist/src/components/roster/parts/body.js.map +1 -1
  12. package/dist/src/components/roster/parts/lane-list.d.ts +2 -2
  13. package/dist/src/components/roster/parts/lane-list.d.ts.map +1 -1
  14. package/dist/src/components/roster/parts/now-line.d.ts +3 -0
  15. package/dist/src/components/roster/parts/now-line.d.ts.map +1 -0
  16. package/dist/src/components/roster/parts/now-line.js +16 -0
  17. package/dist/src/components/roster/parts/now-line.js.map +1 -0
  18. package/dist/src/components/roster/roster.types.d.ts +17 -1
  19. package/dist/src/components/roster/roster.types.d.ts.map +1 -1
  20. package/dist/src/components/roster/use-roster.d.ts.map +1 -1
  21. package/dist/src/components/roster/use-roster.js +6 -1
  22. package/dist/src/components/roster/use-roster.js.map +1 -1
  23. package/dist/src/core/index.d.ts +2 -1
  24. package/dist/src/core/index.d.ts.map +1 -1
  25. package/dist/src/core/index.js +6 -1
  26. package/dist/src/core/index.js.map +1 -1
  27. package/dist/src/core/layout.d.ts.map +1 -1
  28. package/dist/src/core/layout.js +2 -1
  29. package/dist/src/core/layout.js.map +1 -1
  30. package/dist/src/core/snap.d.ts +4 -0
  31. package/dist/src/core/snap.d.ts.map +1 -1
  32. package/dist/src/core/snap.js +5 -0
  33. package/dist/src/core/snap.js.map +1 -1
  34. package/dist/src/core/spans.d.ts +19 -0
  35. package/dist/src/core/spans.d.ts.map +1 -1
  36. package/dist/src/core/spans.js +64 -0
  37. package/dist/src/core/spans.js.map +1 -1
  38. package/dist/src/index.d.ts +2 -1
  39. package/dist/src/index.d.ts.map +1 -1
  40. package/dist/src/index.js +5 -3
  41. package/dist/src/index.js.map +1 -1
  42. package/dist/src/nativewind/index.d.ts +1 -0
  43. package/dist/src/nativewind/index.d.ts.map +1 -1
  44. package/llms.txt +51 -7
  45. package/package.json +2 -2
  46. package/src/components/roster/README.md +16 -5
  47. package/src/components/roster/body-layout.tsx +15 -0
  48. package/src/components/roster/index.tsx +5 -0
  49. package/src/components/roster/parts/body.tsx +39 -2
  50. package/src/components/roster/parts/lane-list.tsx +2 -2
  51. package/src/components/roster/parts/now-line.tsx +19 -0
  52. package/src/components/roster/roster.types.ts +15 -9
  53. package/src/components/roster/use-roster.ts +8 -1
  54. package/src/core/README.md +15 -2
  55. package/src/core/index.ts +2 -1
  56. package/src/core/layout.ts +2 -1
  57. package/src/core/snap.ts +9 -0
  58. package/src/core/spans.ts +66 -2
  59. package/src/index.ts +3 -0
@@ -2,7 +2,7 @@ import { useRef, useState } from 'react';
2
2
  import type { ScrollView } from 'react-native';
3
3
  import { makeMutable, useAnimatedStyle } from 'react-native-reanimated';
4
4
  import type { Lane, LaneGeometry, Rect, Window } from '../../core';
5
- import { byLabel, coverageFor, flagFor, layoutLane, timeAtX, windowFor } from '../../core';
5
+ import { byLabel, coverageFor, flagFor, layoutLane, timeAtX, windowFor, xAtTime } from '../../core';
6
6
  import type { RosterInput, RosterModel, RosterProjection } from './roster.types';
7
7
  import { type SelectedInterval, useRosterPress } from './use-roster-press';
8
8
  import { ticksFor } from './utils/ticks';
@@ -11,6 +11,7 @@ export function useRoster(input: RosterInput): RosterModel {
11
11
  const {
12
12
  lanes,
13
13
  windowSpec,
14
+ now = null,
14
15
  minuteStep = 60,
15
16
  sortLanes = byLabel,
16
17
  rowHeight = 48,
@@ -44,6 +45,10 @@ export function useRoster(input: RosterInput): RosterModel {
44
45
  rowHeight,
45
46
  pxPerMinute: Math.max(pxPerMinute, duration === 0 ? 0 : viewport.width / duration),
46
47
  };
48
+ const nowLine =
49
+ now !== null && now >= window.start && now < window.end
50
+ ? { x: xAtTime(projection, window, now), now }
51
+ : null;
47
52
  const contentWidth = duration * projection.pxPerMinute;
48
53
  const coverage = new Map(lanes.map((lane) => [lane.id, coverageFor(lane, window)]));
49
54
  const orderedLanes = [...lanes].sort((a, b) => sortLanes(a, b, coverage));
@@ -78,6 +83,8 @@ export function useRoster(input: RosterInput): RosterModel {
78
83
  press,
79
84
  status: lanes.length === 0 ? 'empty' : 'ready',
80
85
  ticks: ticksFor(window, windowSpec, projection, minuteStep),
86
+ now,
87
+ nowLine,
81
88
  contentWidth,
82
89
  viewport,
83
90
  navigate: (next) => onNavigate?.(next),
@@ -6,6 +6,9 @@ no React Native, no dependencies.
6
6
  - Public subpath: `react-native-roster/core`; the root entry re-exports it
7
7
  - Authoritative types: `types.ts` (`Lane`, `Layer`, `Interval`, `Gap`,
8
8
  `Source`, `Window`, `Projection`, `Rect`, `Coverage`, `LaneGeometry`)
9
+ - Interval helpers: `extentOf(segments: readonly Window[]): Window | null`,
10
+ `intersectionOf(spans: readonly Window[]): Window | null`, and
11
+ `unionOf(spans: readonly Window[]): Window[]`
9
12
  - Barrel: `index.ts` only
10
13
 
11
14
  | File | Role |
@@ -14,14 +17,24 @@ no React Native, no dependencies.
14
17
  | `layout.ts` | rect geometry for one lane in either projection |
15
18
  | `coverage.ts` | covered and removed minutes per lane |
16
19
  | `source.ts` | `hasSource(sources, source)` matches kind and id, ignoring labels |
17
- | `spans.ts` | the provenance sweep: exact sources per merged span |
20
+ | `spans.ts` | `extentOf`, `intersectionOf`, `unionOf`, and the provenance sweep with exact sources per merged span |
18
21
  | `columns.ts` | day columns, transitions, and skipped dates for the columns projection |
19
22
  | `zone.ts` | Intl-only timezone math (offsets, first instants, transitions) |
20
- | `scale.ts`, `snap.ts` | pixel and time conversion, pointer snapping |
23
+ | `scale.ts`, `snap.ts` | pixel and time conversion (`xAtTime`, `timeAtX`, `timeAtY`), pointer snapping |
21
24
  | `hit-test.ts` | the geometry walk both hooks use to resolve a press |
22
25
  | `order.ts`, `flag.ts` | lane comparators and lane flags |
23
26
  | `cache.ts`, `hash.ts` | layout and coverage caches keyed by lane content |
24
27
 
28
+ All interval helpers reuse the bare `{ start, end }` `Window` type with
29
+ end-exclusive epoch millisecond bounds. `extentOf` bridges disjoint segments;
30
+ `intersectionOf` returns only their shared span. Both return `null` for empty
31
+ input; an empty intersection, including touching bounds, also returns `null`.
32
+ `unionOf` merges overlapping or touching windows, sorts by start, and keeps
33
+ disjoint windows separate. It returns `[]` for empty input and new window objects.
34
+ Every input must have finite bounds and `end > start`; otherwise the helpers throw `RangeError`.
35
+ Inputs may be unsorted, overlapping, nested, or duplicated; inputs are not mutated.
36
+ The existing `Span` type names axis choices only.
37
+
25
38
  Reference: [design](../../docs/design.md), [timezones](../../docs/timezones.md),
26
39
  [caches](../../docs/caches.md). Tests: `test/core`.
27
40
 
package/src/core/index.ts CHANGED
@@ -13,8 +13,9 @@ export { flagFor } from './flag';
13
13
  export { layoutLane } from './layout';
14
14
  export type { LaneComparator } from './order';
15
15
  export { byCoverage, byLabel } from './order';
16
- export { snapToStep, timeAtX, timeAtY } from './snap';
16
+ export { snapToStep, timeAtX, timeAtY, xAtTime } from './snap';
17
17
  export { hasSource } from './source';
18
+ export { extentOf, intersectionOf, unionOf } from './spans';
18
19
  export type {
19
20
  Coverage,
20
21
  DayColumn,
@@ -3,6 +3,7 @@ import { coverageFor } from './coverage';
3
3
  import { flagFor } from './flag';
4
4
  import { laneKey, projectionKey } from './hash';
5
5
  import { type ScalePiece, scalePieces } from './scale';
6
+ import { xAtTime } from './snap';
6
7
  import { sourceSpans } from './spans';
7
8
  import type { Interval, Lane, LaneGeometry, Layer, Projection, Rect, Window } from './types';
8
9
 
@@ -60,7 +61,7 @@ function project(
60
61
  if (projection.orientation === 'horizontal') {
61
62
  for (const span of spans) {
62
63
  rects.push({
63
- x: ((span.start - window.start) / 60_000) * projection.pxPerMinute,
64
+ x: xAtTime(projection, window, span.start),
64
65
  y: inset,
65
66
  width: ((span.end - span.start) / 60_000) * projection.pxPerMinute,
66
67
  height: Math.max(0, projection.rowHeight - 2 * inset),
package/src/core/snap.ts CHANGED
@@ -50,3 +50,12 @@ export function timeAtX(
50
50
  ): number {
51
51
  return window.start + (x * 60_000) / projection.pxPerMinute;
52
52
  }
53
+
54
+ /** Horizontal position at true elapsed length; the window supplies the projection's origin. */
55
+ export function xAtTime(
56
+ projection: Extract<Projection, { orientation: 'horizontal' }>,
57
+ window: Window,
58
+ time: number,
59
+ ): number {
60
+ return ((time - window.start) / 60_000) * projection.pxPerMinute;
61
+ }
package/src/core/spans.ts CHANGED
@@ -1,5 +1,6 @@
1
- // Sweep boundaries once; reference counts preserve provenance through nested
2
- // intervals and equal-time ends/starts. Labels never affect source identity.
1
+ // Provenance sweep logic and the extentOf, intersectionOf, and unionOf interval helpers.
2
+ // The sweep uses reference counts to preserve provenance through nested intervals
3
+ // and equal-time ends/starts. Labels never affect source identity.
3
4
  import type { Interval, Source, Window } from './types';
4
5
 
5
6
  type Boundary = { at: number; delta: number; sources: Source[] };
@@ -56,3 +57,66 @@ export function sourceSpans(intervals: Interval[], window: Window): Interval[] {
56
57
  }
57
58
  return spans;
58
59
  }
60
+
61
+ /**
62
+ * Earliest start through latest end in epoch milliseconds, end-exclusive; empty input returns null.
63
+ * Throws RangeError for non-finite bounds or end <= start.
64
+ * Inputs may be unsorted, overlapping, nested, or duplicated; inputs are not mutated.
65
+ */
66
+ export function extentOf(segments: readonly Window[]): Window | null {
67
+ let start = Infinity;
68
+ let end = -Infinity;
69
+ for (const segment of segments) {
70
+ validateSpan(segment);
71
+ start = Math.min(start, segment.start);
72
+ end = Math.max(end, segment.end);
73
+ }
74
+ return start < end ? { start, end } : null;
75
+ }
76
+
77
+ /**
78
+ * Shared epoch milliseconds, end-exclusive; empty input or an empty intersection returns null.
79
+ * Throws RangeError for non-finite bounds or end <= start.
80
+ * Inputs may be unsorted, overlapping, nested, or duplicated; inputs are not mutated.
81
+ */
82
+ export function intersectionOf(spans: readonly Window[]): Window | null {
83
+ if (spans.length === 0) return null;
84
+ let start = -Infinity;
85
+ let end = Infinity;
86
+ for (const span of spans) {
87
+ validateSpan(span);
88
+ start = Math.max(start, span.start);
89
+ end = Math.min(end, span.end);
90
+ }
91
+ return start < end ? { start, end } : null;
92
+ }
93
+
94
+ /**
95
+ * Merged end-exclusive epoch windows sorted by start; empty input returns an empty array.
96
+ * Overlapping or touching spans merge; disjoint spans remain separate.
97
+ * Throws RangeError for non-finite bounds or end <= start.
98
+ * Returns new objects without mutating inputs.
99
+ */
100
+ export function unionOf(spans: readonly Window[]): Window[] {
101
+ const sorted = spans.map((span) => {
102
+ validateSpan(span);
103
+ return { start: span.start, end: span.end };
104
+ });
105
+ sorted.sort((a, b) => a.start - b.start);
106
+ const merged: Window[] = [];
107
+ for (const span of sorted) {
108
+ const last = merged.at(-1);
109
+ if (last && span.start <= last.end) {
110
+ last.end = Math.max(last.end, span.end);
111
+ } else {
112
+ merged.push(span);
113
+ }
114
+ }
115
+ return merged;
116
+ }
117
+
118
+ function validateSpan(span: Window): void {
119
+ if (!Number.isFinite(span.start) || !Number.isFinite(span.end) || span.end <= span.start) {
120
+ throw new RangeError('Span bounds must be finite with end greater than start');
121
+ }
122
+ }
package/src/index.ts CHANGED
@@ -13,6 +13,7 @@ export { RosterHeader } from './components/roster/parts/header';
13
13
  export { RosterHeaderCell } from './components/roster/parts/header-cell';
14
14
  export { RosterLaneLabelColumn } from './components/roster/parts/lane-label-column';
15
15
  export { RosterLaneList } from './components/roster/parts/lane-list';
16
+ export { RosterNowLine } from './components/roster/parts/now-line';
16
17
  export type {
17
18
  BodyInput,
18
19
  GridInput,
@@ -21,8 +22,10 @@ export type {
21
22
  IntervalDetailInput,
22
23
  LabelColumnInput,
23
24
  LaneLabelInput,
25
+ LaneListInput,
24
26
  RosterInput,
25
27
  RosterModel,
28
+ RosterNowLineInput,
26
29
  RosterProjection,
27
30
  RosterProps,
28
31
  RosterScroll,