react-native-roster 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE.md +29 -0
- package/README.md +455 -2
- package/dist/src/adapters/rrule/cache.d.ts +15 -0
- package/dist/src/adapters/rrule/cache.d.ts.map +1 -0
- package/dist/src/adapters/rrule/cache.js +53 -0
- package/dist/src/adapters/rrule/cache.js.map +1 -0
- package/dist/src/adapters/rrule/envelope.d.ts +3 -0
- package/dist/src/adapters/rrule/envelope.d.ts.map +1 -0
- package/dist/src/adapters/rrule/envelope.js +9 -0
- package/dist/src/adapters/rrule/envelope.js.map +1 -0
- package/dist/src/adapters/rrule/expand.d.ts +4 -0
- package/dist/src/adapters/rrule/expand.d.ts.map +1 -0
- package/dist/src/adapters/rrule/expand.js +92 -0
- package/dist/src/adapters/rrule/expand.js.map +1 -0
- package/dist/src/adapters/rrule/hash.d.ts +3 -0
- package/dist/src/adapters/rrule/hash.d.ts.map +1 -0
- package/dist/src/adapters/rrule/hash.js +15 -0
- package/dist/src/adapters/rrule/hash.js.map +1 -0
- package/dist/src/adapters/rrule/index.d.ts +5 -0
- package/dist/src/adapters/rrule/index.d.ts.map +1 -0
- package/dist/src/adapters/rrule/index.js +12 -0
- package/dist/src/adapters/rrule/index.js.map +1 -0
- package/dist/src/adapters/rrule/net.d.ts +6 -0
- package/dist/src/adapters/rrule/net.d.ts.map +1 -0
- package/dist/src/adapters/rrule/net.js +31 -0
- package/dist/src/adapters/rrule/net.js.map +1 -0
- package/dist/src/adapters/rrule/occurrences.d.ts +6 -0
- package/dist/src/adapters/rrule/occurrences.d.ts.map +1 -0
- package/dist/src/adapters/rrule/occurrences.js +252 -0
- package/dist/src/adapters/rrule/occurrences.js.map +1 -0
- package/dist/src/adapters/rrule/types.d.ts +62 -0
- package/dist/src/adapters/rrule/types.d.ts.map +1 -0
- package/dist/src/adapters/rrule/types.js +3 -0
- package/dist/src/adapters/rrule/types.js.map +1 -0
- package/dist/src/adapters/rrule/validate.d.ts +6 -0
- package/dist/src/adapters/rrule/validate.d.ts.map +1 -0
- package/dist/src/adapters/rrule/validate.js +64 -0
- package/dist/src/adapters/rrule/validate.js.map +1 -0
- package/dist/src/components/layers/layers.types.d.ts +15 -0
- package/dist/src/components/layers/layers.types.d.ts.map +1 -0
- package/dist/src/components/layers/layers.types.js +3 -0
- package/dist/src/components/layers/layers.types.js.map +1 -0
- package/dist/src/components/layers/parts/gap.d.ts +3 -0
- package/dist/src/components/layers/parts/gap.d.ts.map +1 -0
- package/dist/src/components/layers/parts/gap.js +7 -0
- package/dist/src/components/layers/parts/gap.js.map +1 -0
- package/dist/src/components/layers/parts/interval.d.ts +3 -0
- package/dist/src/components/layers/parts/interval.d.ts.map +1 -0
- package/dist/src/components/layers/parts/interval.js +22 -0
- package/dist/src/components/layers/parts/interval.js.map +1 -0
- package/dist/src/components/layers/utils/styles.d.ts +7 -0
- package/dist/src/components/layers/utils/styles.d.ts.map +1 -0
- package/dist/src/components/layers/utils/styles.js +24 -0
- package/dist/src/components/layers/utils/styles.js.map +1 -0
- package/dist/src/components/primitives/press-point.d.ts +3 -0
- package/dist/src/components/primitives/press-point.d.ts.map +1 -0
- package/dist/src/components/primitives/press-point.js +7 -0
- package/dist/src/components/primitives/press-point.js.map +1 -0
- package/dist/src/components/primitives/press-point.types.d.ts +14 -0
- package/dist/src/components/primitives/press-point.types.d.ts.map +1 -0
- package/dist/src/components/primitives/press-point.types.js +3 -0
- package/dist/src/components/primitives/press-point.types.js.map +1 -0
- package/dist/src/components/primitives/press-point.web.d.ts +3 -0
- package/dist/src/components/primitives/press-point.web.d.ts.map +1 -0
- package/dist/src/components/primitives/press-point.web.js +17 -0
- package/dist/src/components/primitives/press-point.web.js.map +1 -0
- package/dist/src/components/primitives/region-style.d.ts +9 -0
- package/dist/src/components/primitives/region-style.d.ts.map +1 -0
- package/dist/src/components/primitives/region-style.js +20 -0
- package/dist/src/components/primitives/region-style.js.map +1 -0
- package/dist/src/components/roster/body-layout.d.ts +9 -0
- package/dist/src/components/roster/body-layout.d.ts.map +1 -0
- package/dist/src/components/roster/body-layout.js +9 -0
- package/dist/src/components/roster/body-layout.js.map +1 -0
- package/dist/src/components/roster/index.d.ts +3 -0
- package/dist/src/components/roster/index.d.ts.map +1 -0
- package/dist/src/components/roster/index.js +40 -0
- package/dist/src/components/roster/index.js.map +1 -0
- package/dist/src/components/roster/lanes/interval-hover.d.ts +3 -0
- package/dist/src/components/roster/lanes/interval-hover.d.ts.map +1 -0
- package/dist/src/components/roster/lanes/interval-hover.js +7 -0
- package/dist/src/components/roster/lanes/interval-hover.js.map +1 -0
- package/dist/src/components/roster/lanes/interval-hover.types.d.ts +15 -0
- package/dist/src/components/roster/lanes/interval-hover.types.d.ts.map +1 -0
- package/dist/src/components/roster/lanes/interval-hover.types.js +3 -0
- package/dist/src/components/roster/lanes/interval-hover.types.js.map +1 -0
- package/dist/src/components/roster/lanes/interval-hover.web.d.ts +3 -0
- package/dist/src/components/roster/lanes/interval-hover.web.d.ts.map +1 -0
- package/dist/src/components/roster/lanes/interval-hover.web.js +20 -0
- package/dist/src/components/roster/lanes/interval-hover.web.js.map +1 -0
- package/dist/src/components/roster/lanes/lane.d.ts +10 -0
- package/dist/src/components/roster/lanes/lane.d.ts.map +1 -0
- package/dist/src/components/roster/lanes/lane.js +32 -0
- package/dist/src/components/roster/lanes/lane.js.map +1 -0
- package/dist/src/components/roster/lanes/parts/incomplete.d.ts +8 -0
- package/dist/src/components/roster/lanes/parts/incomplete.d.ts.map +1 -0
- package/dist/src/components/roster/lanes/parts/incomplete.js +30 -0
- package/dist/src/components/roster/lanes/parts/incomplete.js.map +1 -0
- package/dist/src/components/roster/lanes/parts/lane-label.d.ts +3 -0
- package/dist/src/components/roster/lanes/parts/lane-label.d.ts.map +1 -0
- package/dist/src/components/roster/lanes/parts/lane-label.js +18 -0
- package/dist/src/components/roster/lanes/parts/lane-label.js.map +1 -0
- package/dist/src/components/roster/layout.d.ts +17 -0
- package/dist/src/components/roster/layout.d.ts.map +1 -0
- package/dist/src/components/roster/layout.js +24 -0
- package/dist/src/components/roster/layout.js.map +1 -0
- package/dist/src/components/roster/parts/body.d.ts +7 -0
- package/dist/src/components/roster/parts/body.d.ts.map +1 -0
- package/dist/src/components/roster/parts/body.js +14 -0
- package/dist/src/components/roster/parts/body.js.map +1 -0
- package/dist/src/components/roster/parts/corner.d.ts +2 -0
- package/dist/src/components/roster/parts/corner.d.ts.map +1 -0
- package/dist/src/components/roster/parts/corner.js +7 -0
- package/dist/src/components/roster/parts/corner.js.map +1 -0
- package/dist/src/components/roster/parts/empty.d.ts +2 -0
- package/dist/src/components/roster/parts/empty.d.ts.map +1 -0
- package/dist/src/components/roster/parts/empty.js +9 -0
- package/dist/src/components/roster/parts/empty.js.map +1 -0
- package/dist/src/components/roster/parts/grid.d.ts +3 -0
- package/dist/src/components/roster/parts/grid.d.ts.map +1 -0
- package/dist/src/components/roster/parts/grid.js +16 -0
- package/dist/src/components/roster/parts/grid.js.map +1 -0
- package/dist/src/components/roster/parts/header-cell.d.ts +3 -0
- package/dist/src/components/roster/parts/header-cell.d.ts.map +1 -0
- package/dist/src/components/roster/parts/header-cell.js +9 -0
- package/dist/src/components/roster/parts/header-cell.js.map +1 -0
- package/dist/src/components/roster/parts/header.d.ts +3 -0
- package/dist/src/components/roster/parts/header.d.ts.map +1 -0
- package/dist/src/components/roster/parts/header.js +13 -0
- package/dist/src/components/roster/parts/header.js.map +1 -0
- package/dist/src/components/roster/parts/lane-label-column.d.ts +3 -0
- package/dist/src/components/roster/parts/lane-label-column.d.ts.map +1 -0
- package/dist/src/components/roster/parts/lane-label-column.js +18 -0
- package/dist/src/components/roster/parts/lane-label-column.js.map +1 -0
- package/dist/src/components/roster/parts/lane-list.d.ts +7 -0
- package/dist/src/components/roster/parts/lane-list.d.ts.map +1 -0
- package/dist/src/components/roster/parts/lane-list.js +25 -0
- package/dist/src/components/roster/parts/lane-list.js.map +1 -0
- package/dist/src/components/roster/roster.types.d.ts +145 -0
- package/dist/src/components/roster/roster.types.d.ts.map +1 -0
- package/dist/src/components/roster/roster.types.js +3 -0
- package/dist/src/components/roster/roster.types.js.map +1 -0
- package/dist/src/components/roster/use-roster.d.ts +3 -0
- package/dist/src/components/roster/use-roster.d.ts.map +1 -0
- package/dist/src/components/roster/use-roster.js +106 -0
- package/dist/src/components/roster/use-roster.js.map +1 -0
- package/dist/src/components/roster/utils/body-content-key.d.ts +3 -0
- package/dist/src/components/roster/utils/body-content-key.d.ts.map +1 -0
- package/dist/src/components/roster/utils/body-content-key.js +29 -0
- package/dist/src/components/roster/utils/body-content-key.js.map +1 -0
- package/dist/src/components/roster/utils/ticks.d.ts +4 -0
- package/dist/src/components/roster/utils/ticks.d.ts.map +1 -0
- package/dist/src/components/roster/utils/ticks.js +60 -0
- package/dist/src/components/roster/utils/ticks.js.map +1 -0
- package/dist/src/components/schedule/days/day.d.ts +20 -0
- package/dist/src/components/schedule/days/day.d.ts.map +1 -0
- package/dist/src/components/schedule/days/day.js +17 -0
- package/dist/src/components/schedule/days/day.js.map +1 -0
- package/dist/src/components/schedule/days/header-layout.d.ts +9 -0
- package/dist/src/components/schedule/days/header-layout.d.ts.map +1 -0
- package/dist/src/components/schedule/days/header-layout.js +9 -0
- package/dist/src/components/schedule/days/header-layout.js.map +1 -0
- package/dist/src/components/schedule/days/layout.d.ts +18 -0
- package/dist/src/components/schedule/days/layout.d.ts.map +1 -0
- package/dist/src/components/schedule/days/layout.js +9 -0
- package/dist/src/components/schedule/days/layout.js.map +1 -0
- package/dist/src/components/schedule/days/parts/column.d.ts +3 -0
- package/dist/src/components/schedule/days/parts/column.d.ts.map +1 -0
- package/dist/src/components/schedule/days/parts/column.js +28 -0
- package/dist/src/components/schedule/days/parts/column.js.map +1 -0
- package/dist/src/components/schedule/days/parts/day-header.d.ts +5 -0
- package/dist/src/components/schedule/days/parts/day-header.d.ts.map +1 -0
- package/dist/src/components/schedule/days/parts/day-header.js +10 -0
- package/dist/src/components/schedule/days/parts/day-header.js.map +1 -0
- package/dist/src/components/schedule/days/parts/grid.d.ts +3 -0
- package/dist/src/components/schedule/days/parts/grid.d.ts.map +1 -0
- package/dist/src/components/schedule/days/parts/grid.js +14 -0
- package/dist/src/components/schedule/days/parts/grid.js.map +1 -0
- package/dist/src/components/schedule/days/parts/now-line.d.ts +5 -0
- package/dist/src/components/schedule/days/parts/now-line.d.ts.map +1 -0
- package/dist/src/components/schedule/days/parts/now-line.js +9 -0
- package/dist/src/components/schedule/days/parts/now-line.js.map +1 -0
- package/dist/src/components/schedule/days/parts/skipped-date.d.ts +4 -0
- package/dist/src/components/schedule/days/parts/skipped-date.d.ts.map +1 -0
- package/dist/src/components/schedule/days/parts/skipped-date.js +17 -0
- package/dist/src/components/schedule/days/parts/skipped-date.js.map +1 -0
- package/dist/src/components/schedule/days/parts/transition.d.ts +3 -0
- package/dist/src/components/schedule/days/parts/transition.d.ts.map +1 -0
- package/dist/src/components/schedule/days/parts/transition.js +42 -0
- package/dist/src/components/schedule/days/parts/transition.js.map +1 -0
- package/dist/src/components/schedule/index.d.ts +3 -0
- package/dist/src/components/schedule/index.d.ts.map +1 -0
- package/dist/src/components/schedule/index.js +44 -0
- package/dist/src/components/schedule/index.js.map +1 -0
- package/dist/src/components/schedule/layout.d.ts +15 -0
- package/dist/src/components/schedule/layout.d.ts.map +1 -0
- package/dist/src/components/schedule/layout.js +10 -0
- package/dist/src/components/schedule/layout.js.map +1 -0
- package/dist/src/components/schedule/parts/gutter.d.ts +3 -0
- package/dist/src/components/schedule/parts/gutter.d.ts.map +1 -0
- package/dist/src/components/schedule/parts/gutter.js +9 -0
- package/dist/src/components/schedule/parts/gutter.js.map +1 -0
- package/dist/src/components/schedule/parts/incomplete.d.ts +6 -0
- package/dist/src/components/schedule/parts/incomplete.d.ts.map +1 -0
- package/dist/src/components/schedule/parts/incomplete.js +9 -0
- package/dist/src/components/schedule/parts/incomplete.js.map +1 -0
- package/dist/src/components/schedule/schedule.types.d.ts +109 -0
- package/dist/src/components/schedule/schedule.types.d.ts.map +1 -0
- package/dist/src/components/schedule/schedule.types.js +3 -0
- package/dist/src/components/schedule/schedule.types.js.map +1 -0
- package/dist/src/components/schedule/use-schedule-viewport.d.ts +7 -0
- package/dist/src/components/schedule/use-schedule-viewport.d.ts.map +1 -0
- package/dist/src/components/schedule/use-schedule-viewport.js +15 -0
- package/dist/src/components/schedule/use-schedule-viewport.js.map +1 -0
- package/dist/src/components/schedule/use-schedule.d.ts +3 -0
- package/dist/src/components/schedule/use-schedule.d.ts.map +1 -0
- package/dist/src/components/schedule/use-schedule.js +71 -0
- package/dist/src/components/schedule/use-schedule.js.map +1 -0
- package/dist/src/components/schedule/utils/days.d.ts +17 -0
- package/dist/src/components/schedule/utils/days.d.ts.map +1 -0
- package/dist/src/components/schedule/utils/days.js +49 -0
- package/dist/src/components/schedule/utils/days.js.map +1 -0
- package/dist/src/core/axis.d.ts +18 -0
- package/dist/src/core/axis.d.ts.map +1 -0
- package/dist/src/core/axis.js +58 -0
- package/dist/src/core/axis.js.map +1 -0
- package/dist/src/core/cache.d.ts +29 -0
- package/dist/src/core/cache.d.ts.map +1 -0
- package/dist/src/core/cache.js +33 -0
- package/dist/src/core/cache.js.map +1 -0
- package/dist/src/core/columns.d.ts +3 -0
- package/dist/src/core/columns.d.ts.map +1 -0
- package/dist/src/core/columns.js +45 -0
- package/dist/src/core/columns.js.map +1 -0
- package/dist/src/core/coverage.d.ts +3 -0
- package/dist/src/core/coverage.d.ts.map +1 -0
- package/dist/src/core/coverage.js +60 -0
- package/dist/src/core/coverage.js.map +1 -0
- package/dist/src/core/flag.d.ts +3 -0
- package/dist/src/core/flag.d.ts.map +1 -0
- package/dist/src/core/flag.js +17 -0
- package/dist/src/core/flag.js.map +1 -0
- package/dist/src/core/hash.d.ts +5 -0
- package/dist/src/core/hash.d.ts.map +1 -0
- package/dist/src/core/hash.js +29 -0
- package/dist/src/core/hash.js.map +1 -0
- package/dist/src/core/hit-test.d.ts +6 -0
- package/dist/src/core/hit-test.d.ts.map +1 -0
- package/dist/src/core/hit-test.js +23 -0
- package/dist/src/core/hit-test.js.map +1 -0
- package/dist/src/core/index.d.ts +12 -0
- package/dist/src/core/index.d.ts.map +1 -0
- package/dist/src/core/index.js +30 -0
- package/dist/src/core/index.js.map +1 -0
- package/dist/src/core/layout.d.ts +3 -0
- package/dist/src/core/layout.d.ts.map +1 -0
- package/dist/src/core/layout.js +96 -0
- package/dist/src/core/layout.js.map +1 -0
- package/dist/src/core/order.d.ts +7 -0
- package/dist/src/core/order.d.ts.map +1 -0
- package/dist/src/core/order.js +12 -0
- package/dist/src/core/order.js.map +1 -0
- package/dist/src/core/scale.d.ts +9 -0
- package/dist/src/core/scale.d.ts.map +1 -0
- package/dist/src/core/scale.js +41 -0
- package/dist/src/core/scale.js.map +1 -0
- package/dist/src/core/snap.d.ts +10 -0
- package/dist/src/core/snap.d.ts.map +1 -0
- package/dist/src/core/snap.js +43 -0
- package/dist/src/core/snap.js.map +1 -0
- package/dist/src/core/spans.d.ts +3 -0
- package/dist/src/core/spans.d.ts.map +1 -0
- package/dist/src/core/spans.js +60 -0
- package/dist/src/core/spans.js.map +1 -0
- package/dist/src/core/types.d.ts +104 -0
- package/dist/src/core/types.d.ts.map +1 -0
- package/dist/src/core/types.js +3 -0
- package/dist/src/core/types.js.map +1 -0
- package/dist/src/core/zone.d.ts +10 -0
- package/dist/src/core/zone.d.ts.map +1 -0
- package/dist/src/core/zone.js +103 -0
- package/dist/src/core/zone.js.map +1 -0
- package/dist/src/index.d.ts +29 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +67 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/nativewind/index.d.ts +59 -0
- package/dist/src/nativewind/index.d.ts.map +1 -0
- package/dist/src/nativewind/index.js +24 -0
- package/dist/src/nativewind/index.js.map +1 -0
- package/docs/migrations.md +61 -0
- package/llms.txt +498 -0
- package/package.json +155 -8
- package/src/adapters/rrule/README.md +41 -0
- package/src/adapters/rrule/cache.ts +50 -0
- package/src/adapters/rrule/envelope.ts +7 -0
- package/src/adapters/rrule/expand.ts +105 -0
- package/src/adapters/rrule/hash.ts +13 -0
- package/src/adapters/rrule/index.ts +11 -0
- package/src/adapters/rrule/net.ts +30 -0
- package/src/adapters/rrule/occurrences.ts +288 -0
- package/src/adapters/rrule/types.ts +52 -0
- package/src/adapters/rrule/validate.ts +78 -0
- package/src/components/layers/README.md +15 -0
- package/src/components/layers/layers.types.ts +6 -0
- package/src/components/layers/parts/gap.tsx +5 -0
- package/src/components/layers/parts/interval.tsx +24 -0
- package/src/components/layers/utils/styles.ts +24 -0
- package/src/components/primitives/README.md +12 -0
- package/src/components/primitives/press-point.tsx +5 -0
- package/src/components/primitives/press-point.types.ts +5 -0
- package/src/components/primitives/press-point.web.tsx +19 -0
- package/src/components/primitives/region-style.ts +21 -0
- package/src/components/roster/README.md +29 -0
- package/src/components/roster/body-layout.tsx +32 -0
- package/src/components/roster/index.tsx +108 -0
- package/src/components/roster/lanes/interval-hover.tsx +5 -0
- package/src/components/roster/lanes/interval-hover.types.ts +11 -0
- package/src/components/roster/lanes/interval-hover.web.tsx +19 -0
- package/src/components/roster/lanes/lane.tsx +93 -0
- package/src/components/roster/lanes/parts/incomplete.tsx +44 -0
- package/src/components/roster/lanes/parts/lane-label.tsx +41 -0
- package/src/components/roster/layout.tsx +68 -0
- package/src/components/roster/parts/body.tsx +24 -0
- package/src/components/roster/parts/corner.tsx +3 -0
- package/src/components/roster/parts/empty.tsx +5 -0
- package/src/components/roster/parts/grid.tsx +26 -0
- package/src/components/roster/parts/header-cell.tsx +10 -0
- package/src/components/roster/parts/header.tsx +23 -0
- package/src/components/roster/parts/lane-label-column.tsx +31 -0
- package/src/components/roster/parts/lane-list.tsx +74 -0
- package/src/components/roster/roster.types.ts +156 -0
- package/src/components/roster/use-roster.ts +133 -0
- package/src/components/roster/utils/body-content-key.ts +46 -0
- package/src/components/roster/utils/ticks.ts +62 -0
- package/src/components/schedule/README.md +23 -0
- package/src/components/schedule/days/day.tsx +89 -0
- package/src/components/schedule/days/header-layout.tsx +12 -0
- package/src/components/schedule/days/layout.tsx +38 -0
- package/src/components/schedule/days/parts/column.tsx +59 -0
- package/src/components/schedule/days/parts/day-header.tsx +18 -0
- package/src/components/schedule/days/parts/grid.tsx +21 -0
- package/src/components/schedule/days/parts/now-line.tsx +11 -0
- package/src/components/schedule/days/parts/skipped-date.tsx +22 -0
- package/src/components/schedule/days/parts/transition.tsx +70 -0
- package/src/components/schedule/index.tsx +108 -0
- package/src/components/schedule/layout.tsx +42 -0
- package/src/components/schedule/parts/gutter.tsx +16 -0
- package/src/components/schedule/parts/incomplete.tsx +13 -0
- package/src/components/schedule/schedule.types.ts +103 -0
- package/src/components/schedule/use-schedule-viewport.ts +13 -0
- package/src/components/schedule/use-schedule.ts +75 -0
- package/src/components/schedule/utils/days.ts +54 -0
- package/src/core/README.md +25 -0
- package/src/core/axis.ts +58 -0
- package/src/core/cache.ts +38 -0
- package/src/core/columns.ts +49 -0
- package/src/core/coverage.ts +56 -0
- package/src/core/flag.ts +14 -0
- package/src/core/hash.ts +28 -0
- package/src/core/hit-test.ts +27 -0
- package/src/core/index.ts +34 -0
- package/src/core/layout.ts +101 -0
- package/src/core/order.ts +18 -0
- package/src/core/scale.ts +45 -0
- package/src/core/snap.ts +52 -0
- package/src/core/spans.ts +58 -0
- package/src/core/types.ts +92 -0
- package/src/core/zone.ts +100 -0
- package/src/index.ts +52 -0
- package/src/nativewind/README.md +14 -0
- package/src/nativewind/index.ts +30 -0
- package/index.js +0 -4
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { DayColumn, Window } from './types';
|
|
2
|
+
import {
|
|
3
|
+
dateEpoch,
|
|
4
|
+
dateString,
|
|
5
|
+
dayMilliseconds,
|
|
6
|
+
localDateAt,
|
|
7
|
+
startOfDate,
|
|
8
|
+
transitionsBetween,
|
|
9
|
+
} from './zone';
|
|
10
|
+
|
|
11
|
+
// Unbounded, like the date-start memo: zone data is stable for this runtime,
|
|
12
|
+
// and retaining visited days avoids probing again on pointer and navigation revisits.
|
|
13
|
+
const columns = new Map<string, DayColumn | null>();
|
|
14
|
+
|
|
15
|
+
export function dayColumnsFor(window: Window, timezone: string): DayColumn[] {
|
|
16
|
+
const days: DayColumn[] = [];
|
|
17
|
+
if (window.start >= window.end) return days;
|
|
18
|
+
let localDate = localDateAt(window.start, timezone);
|
|
19
|
+
let start = startOfDate(localDate, timezone);
|
|
20
|
+
// After a rollback across midnight, the wall date can lag the column whose
|
|
21
|
+
// first instant has already passed. Walk until the next absolute date start.
|
|
22
|
+
while (start < window.end) {
|
|
23
|
+
const nextDate = dateString(dateEpoch(localDate) + dayMilliseconds);
|
|
24
|
+
const end = startOfDate(nextDate, timezone);
|
|
25
|
+
const key = JSON.stringify([localDate, timezone]);
|
|
26
|
+
let day = columns.get(key);
|
|
27
|
+
if (day === undefined) {
|
|
28
|
+
day =
|
|
29
|
+
start < end
|
|
30
|
+
? {
|
|
31
|
+
start,
|
|
32
|
+
end,
|
|
33
|
+
localDate,
|
|
34
|
+
label: new Intl.DateTimeFormat('en-US', {
|
|
35
|
+
timeZone: timezone,
|
|
36
|
+
weekday: 'short',
|
|
37
|
+
}).format(start),
|
|
38
|
+
// Boundary transitions describe wall bands; absolute bounds stay exclusive.
|
|
39
|
+
transitions: transitionsBetween(start - 1, end + 1, timezone),
|
|
40
|
+
}
|
|
41
|
+
: null;
|
|
42
|
+
columns.set(key, day);
|
|
43
|
+
}
|
|
44
|
+
if (day && day.end > window.start) days.push(day);
|
|
45
|
+
localDate = nextDate;
|
|
46
|
+
start = end;
|
|
47
|
+
}
|
|
48
|
+
return days;
|
|
49
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { counts, coverageCache } from './cache';
|
|
2
|
+
import { laneKey } from './hash';
|
|
3
|
+
import type { Coverage, Lane, LayerRole, Window } from './types';
|
|
4
|
+
|
|
5
|
+
export function coverageFor(lane: Lane, window: Window): Coverage {
|
|
6
|
+
const key = laneKey(lane, window);
|
|
7
|
+
const cached = coverageCache.get(key);
|
|
8
|
+
if (cached) {
|
|
9
|
+
counts.coverage.cacheHits++;
|
|
10
|
+
return cached;
|
|
11
|
+
}
|
|
12
|
+
counts.coverage.runs++;
|
|
13
|
+
const available = union(lane, window, 'availability');
|
|
14
|
+
const booked = union(lane, window, 'booking');
|
|
15
|
+
let intersection = 0;
|
|
16
|
+
let a = 0;
|
|
17
|
+
let b = 0;
|
|
18
|
+
while (a < available.length && b < booked.length) {
|
|
19
|
+
const left = available[a] as Window;
|
|
20
|
+
const right = booked[b] as Window;
|
|
21
|
+
intersection += Math.max(0, Math.min(left.end, right.end) - Math.max(left.start, right.start));
|
|
22
|
+
if (left.end < right.end) a++;
|
|
23
|
+
else b++;
|
|
24
|
+
}
|
|
25
|
+
const availabilityMinutes = duration(available) / 60_000;
|
|
26
|
+
const result = {
|
|
27
|
+
availabilityMinutes,
|
|
28
|
+
bookingMinutes: duration(booked) / 60_000,
|
|
29
|
+
availabilityMinusBookingMinutes: availabilityMinutes - intersection / 60_000,
|
|
30
|
+
};
|
|
31
|
+
coverageCache.set(key, result);
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function union(lane: Lane, window: Window, role: LayerRole): Window[] {
|
|
36
|
+
const spans = lane.layers
|
|
37
|
+
.filter((layer) => layer.role === role)
|
|
38
|
+
.flatMap((layer) => layer.intervals)
|
|
39
|
+
.map(({ start, end }) => ({
|
|
40
|
+
start: Math.max(start, window.start),
|
|
41
|
+
end: Math.min(end, window.end),
|
|
42
|
+
}))
|
|
43
|
+
.filter(({ start, end }) => start < end)
|
|
44
|
+
.sort((a, b) => a.start - b.start);
|
|
45
|
+
const result: Window[] = [];
|
|
46
|
+
for (const span of spans) {
|
|
47
|
+
const last = result.at(-1);
|
|
48
|
+
if (last && span.start <= last.end) last.end = Math.max(last.end, span.end);
|
|
49
|
+
else result.push(span);
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function duration(spans: Window[]): number {
|
|
55
|
+
return spans.reduce((sum, span) => sum + span.end - span.start, 0);
|
|
56
|
+
}
|
package/src/core/flag.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Lane, LaneFlag, Window } from './types';
|
|
2
|
+
|
|
3
|
+
export function flagFor(lane: Lane, window: Window): LaneFlag {
|
|
4
|
+
if (lane.flag !== undefined) return lane.flag;
|
|
5
|
+
let hasIntervals = false;
|
|
6
|
+
for (const layer of lane.layers) {
|
|
7
|
+
for (const interval of layer.intervals) {
|
|
8
|
+
hasIntervals = true;
|
|
9
|
+
if (Math.max(interval.start, window.start) < Math.min(interval.end, window.end))
|
|
10
|
+
return 'none';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return hasIntervals ? 'empty-in-window' : 'none';
|
|
14
|
+
}
|
package/src/core/hash.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Canonical structural keys retain the full encoding to avoid hash collisions.
|
|
2
|
+
// Only layers participate without a version; lane display metadata never does.
|
|
3
|
+
import type { Lane, Projection, Window } from './types';
|
|
4
|
+
|
|
5
|
+
export function structuralKey(value: unknown): string {
|
|
6
|
+
if (Array.isArray(value)) return `[${value.map(structuralKey).join(',')}]`;
|
|
7
|
+
if (value !== null && typeof value === 'object') {
|
|
8
|
+
return `{${Object.entries(value)
|
|
9
|
+
.filter(([, item]) => item !== undefined)
|
|
10
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
11
|
+
.map(([key, item]) => `${JSON.stringify(key)}:${structuralKey(item)}`)
|
|
12
|
+
.join(',')}}`;
|
|
13
|
+
}
|
|
14
|
+
return JSON.stringify(value) as string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function laneKey(lane: Lane, window: Window): string {
|
|
18
|
+
return JSON.stringify([
|
|
19
|
+
lane.id,
|
|
20
|
+
lane.version === undefined ? ['layers', structuralKey(lane.layers)] : ['version', lane.version],
|
|
21
|
+
window.start,
|
|
22
|
+
window.end,
|
|
23
|
+
]);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function projectionKey(projection: Projection): string {
|
|
27
|
+
return structuralKey(projection);
|
|
28
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Lane, LaneGeometry, Rect } from './types';
|
|
2
|
+
|
|
3
|
+
export function hitTest(
|
|
4
|
+
lane: Lane,
|
|
5
|
+
geometry: LaneGeometry,
|
|
6
|
+
x: number,
|
|
7
|
+
y: number,
|
|
8
|
+
): { kind: 'interval' | 'gap'; rect: Rect } | undefined {
|
|
9
|
+
for (const kind of ['interval', 'gap'] as const) {
|
|
10
|
+
const rects = kind === 'interval' ? geometry.rects : geometry.gapRects;
|
|
11
|
+
let hit: Rect | undefined;
|
|
12
|
+
for (const rect of rects) {
|
|
13
|
+
if (x < rect.x || x >= rect.x + rect.width || y < rect.y || y >= rect.y + rect.height)
|
|
14
|
+
continue;
|
|
15
|
+
const previous = hit;
|
|
16
|
+
if (
|
|
17
|
+
!previous ||
|
|
18
|
+
rect.z > previous.z ||
|
|
19
|
+
(rect.z === previous.z &&
|
|
20
|
+
lane.layers.findIndex((layer) => layer.id === rect.layerId) >
|
|
21
|
+
lane.layers.findIndex((layer) => layer.id === previous.layerId))
|
|
22
|
+
)
|
|
23
|
+
hit = rect;
|
|
24
|
+
}
|
|
25
|
+
if (hit) return { kind, rect: hit };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type { Span, WindowSpec } from './axis';
|
|
2
|
+
export { next, prev, today, windowFor } from './axis';
|
|
3
|
+
export {
|
|
4
|
+
clearCoverageCache,
|
|
5
|
+
clearLayoutCache,
|
|
6
|
+
coverageStats,
|
|
7
|
+
layoutStats,
|
|
8
|
+
resetStats,
|
|
9
|
+
} from './cache';
|
|
10
|
+
export { dayColumnsFor } from './columns';
|
|
11
|
+
export { coverageFor } from './coverage';
|
|
12
|
+
export { flagFor } from './flag';
|
|
13
|
+
export { layoutLane } from './layout';
|
|
14
|
+
export type { LaneComparator } from './order';
|
|
15
|
+
export { byCoverage, byLabel } from './order';
|
|
16
|
+
export { snapToStep, timeAtX, timeAtY } from './snap';
|
|
17
|
+
export type {
|
|
18
|
+
Coverage,
|
|
19
|
+
DayColumn,
|
|
20
|
+
Gap,
|
|
21
|
+
Interval,
|
|
22
|
+
Lane,
|
|
23
|
+
LaneFlag,
|
|
24
|
+
LaneGeometry,
|
|
25
|
+
Layer,
|
|
26
|
+
LayerRole,
|
|
27
|
+
LayerStyle,
|
|
28
|
+
Projection,
|
|
29
|
+
Rect,
|
|
30
|
+
Source,
|
|
31
|
+
Transition,
|
|
32
|
+
Weekday,
|
|
33
|
+
Window,
|
|
34
|
+
} from './types';
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { counts, layoutCache } from './cache';
|
|
2
|
+
import { coverageFor } from './coverage';
|
|
3
|
+
import { flagFor } from './flag';
|
|
4
|
+
import { laneKey, projectionKey } from './hash';
|
|
5
|
+
import { type ScalePiece, scalePieces } from './scale';
|
|
6
|
+
import { sourceSpans } from './spans';
|
|
7
|
+
import type { Interval, Lane, LaneGeometry, Layer, Projection, Rect, Window } from './types';
|
|
8
|
+
|
|
9
|
+
export function layoutLane(lane: Lane, window: Window, projection: Projection): LaneGeometry {
|
|
10
|
+
const key = `${laneKey(lane, window)}|${projectionKey(projection)}`;
|
|
11
|
+
let cached = layoutCache.get(key);
|
|
12
|
+
const coverage = coverageFor(lane, window);
|
|
13
|
+
const flag = flagFor(lane, window);
|
|
14
|
+
if (cached) {
|
|
15
|
+
counts.layout.cacheHits++;
|
|
16
|
+
} else {
|
|
17
|
+
counts.layout.runs++;
|
|
18
|
+
const pieces =
|
|
19
|
+
projection.orientation === 'columns'
|
|
20
|
+
? projection.days.map((day) => scalePieces(day, projection.viewTimezone))
|
|
21
|
+
: [];
|
|
22
|
+
const rects: Rect[] = [];
|
|
23
|
+
const gapRects: Rect[] = [];
|
|
24
|
+
for (const layer of lane.layers) {
|
|
25
|
+
project(sourceSpans(layer.intervals, window), layer, window, projection, pieces, rects);
|
|
26
|
+
project(sourceSpans(layer.gaps ?? [], window), layer, window, projection, pieces, gapRects);
|
|
27
|
+
}
|
|
28
|
+
cached = { rects, gapRects, assemblies: new WeakMap() };
|
|
29
|
+
layoutCache.set(key, cached);
|
|
30
|
+
}
|
|
31
|
+
// Read the current flag and coverage above, then retain each complete assembly
|
|
32
|
+
// by reference, including when a consumer revisits an earlier explicit flag.
|
|
33
|
+
let assemblies = cached.assemblies.get(coverage);
|
|
34
|
+
if (!assemblies) {
|
|
35
|
+
assemblies = new Map();
|
|
36
|
+
cached.assemblies.set(coverage, assemblies);
|
|
37
|
+
}
|
|
38
|
+
const previous = assemblies.get(flag);
|
|
39
|
+
if (previous) return previous;
|
|
40
|
+
const result = {
|
|
41
|
+
laneId: lane.id,
|
|
42
|
+
rects: cached.rects,
|
|
43
|
+
gapRects: cached.gapRects,
|
|
44
|
+
coverage,
|
|
45
|
+
flag,
|
|
46
|
+
};
|
|
47
|
+
assemblies.set(flag, result);
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function project(
|
|
52
|
+
spans: Interval[],
|
|
53
|
+
layer: Layer,
|
|
54
|
+
window: Window,
|
|
55
|
+
projection: Projection,
|
|
56
|
+
columns: ScalePiece[][],
|
|
57
|
+
rects: Rect[],
|
|
58
|
+
): void {
|
|
59
|
+
const inset = layer.style.inset ?? 0;
|
|
60
|
+
if (projection.orientation === 'horizontal') {
|
|
61
|
+
for (const span of spans) {
|
|
62
|
+
rects.push({
|
|
63
|
+
x: ((span.start - window.start) / 60_000) * projection.pxPerMinute,
|
|
64
|
+
y: inset,
|
|
65
|
+
width: ((span.end - span.start) / 60_000) * projection.pxPerMinute,
|
|
66
|
+
height: Math.max(0, projection.rowHeight - 2 * inset),
|
|
67
|
+
layerId: layer.id,
|
|
68
|
+
z: layer.z,
|
|
69
|
+
sources: span.sources,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
// Both lists are ordered in absolute time; advance once through the columns.
|
|
75
|
+
let firstSpan = 0;
|
|
76
|
+
for (const [column, pieces] of columns.entries()) {
|
|
77
|
+
for (const piece of pieces) {
|
|
78
|
+
while (firstSpan < spans.length && (spans[firstSpan] as Interval).end <= piece.start)
|
|
79
|
+
firstSpan++;
|
|
80
|
+
for (let i = firstSpan; i < spans.length; i++) {
|
|
81
|
+
const span = spans[i] as Interval;
|
|
82
|
+
if (span.start >= piece.end) break;
|
|
83
|
+
const start = Math.max(span.start, piece.start);
|
|
84
|
+
const end = Math.min(span.end, piece.end);
|
|
85
|
+
rects.push({
|
|
86
|
+
x: inset,
|
|
87
|
+
y:
|
|
88
|
+
((piece.minute + ((start - piece.start) / 60_000) * piece.scale) *
|
|
89
|
+
projection.pxPerHour) /
|
|
90
|
+
60,
|
|
91
|
+
width: Math.max(0, projection.columnWidth - 2 * inset),
|
|
92
|
+
height: (((end - start) / 60_000) * piece.scale * projection.pxPerHour) / 60,
|
|
93
|
+
layerId: layer.id,
|
|
94
|
+
z: layer.z,
|
|
95
|
+
sources: span.sources,
|
|
96
|
+
column,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Coverage, Lane } from './types';
|
|
2
|
+
|
|
3
|
+
export type LaneComparator = (a: Lane, b: Lane, coverage: ReadonlyMap<string, Coverage>) => number;
|
|
4
|
+
|
|
5
|
+
export function byLabel(a: Lane, b: Lane): number {
|
|
6
|
+
return a.label.localeCompare(b.label);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function byCoverage({
|
|
10
|
+
measure,
|
|
11
|
+
}: {
|
|
12
|
+
measure: 'availability' | 'availabilityMinusBooking';
|
|
13
|
+
}): LaneComparator {
|
|
14
|
+
const field =
|
|
15
|
+
measure === 'availability' ? 'availabilityMinutes' : 'availabilityMinusBookingMinutes';
|
|
16
|
+
return (a, b, coverage) =>
|
|
17
|
+
(coverage.get(b.id)?.[field] ?? 0) - (coverage.get(a.id)?.[field] ?? 0) || byLabel(a, b);
|
|
18
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// A rect must never cross a scale boundary. Repeats have three boundaries:
|
|
2
|
+
// T - |delta|, T, and T + |delta|. Pieces serve layout and pointer inversion
|
|
3
|
+
// together so that both halves select the same absolute occurrence.
|
|
4
|
+
import type { DayColumn } from './types';
|
|
5
|
+
import { dateEpoch, wallTime } from './zone';
|
|
6
|
+
|
|
7
|
+
export type ScalePiece = { start: number; end: number; minute: number; scale: number };
|
|
8
|
+
|
|
9
|
+
export function scalePieces(day: DayColumn, timezone: string): ScalePiece[] {
|
|
10
|
+
const boundaries = new Set([day.start, day.end]);
|
|
11
|
+
const addBoundary = (time: number) =>
|
|
12
|
+
boundaries.add(Math.max(day.start, Math.min(day.end, time)));
|
|
13
|
+
for (const { at, deltaMinutes } of day.transitions) {
|
|
14
|
+
addBoundary(at);
|
|
15
|
+
if (deltaMinutes < 0) {
|
|
16
|
+
const delta = -deltaMinutes * 60_000;
|
|
17
|
+
addBoundary(at - delta);
|
|
18
|
+
addBoundary(at + delta);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const ordered = [...boundaries].sort((a, b) => a - b);
|
|
22
|
+
const pieces: ScalePiece[] = [];
|
|
23
|
+
const midnight = dateEpoch(day.localDate);
|
|
24
|
+
for (let i = 0; i < ordered.length - 1; i++) {
|
|
25
|
+
const start = ordered[i] as number;
|
|
26
|
+
const end = ordered[i + 1] as number;
|
|
27
|
+
let minute = (wallTime(start, timezone) - midnight) / 60_000;
|
|
28
|
+
let scale = 1;
|
|
29
|
+
for (const { at, deltaMinutes } of day.transitions) {
|
|
30
|
+
const delta = -deltaMinutes * 60_000;
|
|
31
|
+
if (delta > 0 && start >= at - delta && start < at + delta) {
|
|
32
|
+
const repeatedStart = (wallTime(at, timezone) - midnight) / 60_000;
|
|
33
|
+
// Clamp a repeat crossing midnight to the top edge. Compress its
|
|
34
|
+
// surviving absolute span into the remaining wall-clock region so
|
|
35
|
+
// layout and inversion agree where ordinary wall time resumes.
|
|
36
|
+
const clipped = Math.max(0, -repeatedStart);
|
|
37
|
+
const repeatStart = at - delta + clipped * 60_000;
|
|
38
|
+
scale = (deltaMinutes + clipped) / (2 * deltaMinutes + clipped);
|
|
39
|
+
minute = repeatedStart + clipped + ((start - repeatStart) / 60_000) * scale;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
pieces.push({ start, end, minute, scale });
|
|
43
|
+
}
|
|
44
|
+
return pieces;
|
|
45
|
+
}
|
package/src/core/snap.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { dayColumnsFor } from './columns';
|
|
2
|
+
import { scalePieces } from './scale';
|
|
3
|
+
import type { Projection, Window } from './types';
|
|
4
|
+
import { wallTime } from './zone';
|
|
5
|
+
|
|
6
|
+
export function snapToStep(time: number, minuteStep: number, timezone: string): number {
|
|
7
|
+
if (!Number.isInteger(minuteStep) || minuteStep <= 0 || 60 % minuteStep !== 0) {
|
|
8
|
+
throw new RangeError('minuteStep must be a positive divisor of 60');
|
|
9
|
+
}
|
|
10
|
+
const step = minuteStep * 60_000;
|
|
11
|
+
const wall = wallTime(time, timezone);
|
|
12
|
+
let result = time - (wall - Math.floor(wall / step) * step);
|
|
13
|
+
const days = dayColumnsFor({ start: time, end: time + 1 }, timezone);
|
|
14
|
+
for (const day of days) {
|
|
15
|
+
for (const { at } of day.transitions) {
|
|
16
|
+
if (at <= time) result = Math.max(result, at);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function timeAtY(
|
|
23
|
+
projection: Extract<Projection, { orientation: 'columns' }>,
|
|
24
|
+
columnIndex: number,
|
|
25
|
+
y: number,
|
|
26
|
+
): number | null {
|
|
27
|
+
const day = projection.days[columnIndex];
|
|
28
|
+
if (!day || y < 0 || y >= 24 * projection.pxPerHour) return null;
|
|
29
|
+
const minute = (y * 60) / projection.pxPerHour;
|
|
30
|
+
for (const piece of scalePieces(day, projection.viewTimezone)) {
|
|
31
|
+
if (
|
|
32
|
+
minute >= piece.minute &&
|
|
33
|
+
minute < piece.minute + ((piece.end - piece.start) / 60_000) * piece.scale
|
|
34
|
+
) {
|
|
35
|
+
// Epoch addition can round an interior pointer to the exclusive boundary.
|
|
36
|
+
return Math.min(
|
|
37
|
+
piece.end - 1,
|
|
38
|
+
piece.start + ((minute - piece.minute) * 60_000) / piece.scale,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Absolute time at true elapsed length; the window supplies the projection's origin. */
|
|
46
|
+
export function timeAtX(
|
|
47
|
+
projection: Extract<Projection, { orientation: 'horizontal' }>,
|
|
48
|
+
window: Window,
|
|
49
|
+
x: number,
|
|
50
|
+
): number {
|
|
51
|
+
return window.start + (x * 60_000) / projection.pxPerMinute;
|
|
52
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Sweep boundaries once; reference counts preserve provenance through nested
|
|
2
|
+
// intervals and equal-time ends/starts. Labels never affect source identity.
|
|
3
|
+
import type { Interval, Source, Window } from './types';
|
|
4
|
+
|
|
5
|
+
type Boundary = { at: number; delta: number; sources: Source[] };
|
|
6
|
+
|
|
7
|
+
export function sourceSpans(intervals: Interval[], window: Window): Interval[] {
|
|
8
|
+
const boundaries: Boundary[] = [];
|
|
9
|
+
for (const interval of intervals) {
|
|
10
|
+
const start = Math.max(interval.start, window.start);
|
|
11
|
+
const end = Math.min(interval.end, window.end);
|
|
12
|
+
if (start >= end) continue;
|
|
13
|
+
boundaries.push({ at: start, delta: 1, sources: interval.sources });
|
|
14
|
+
boundaries.push({ at: end, delta: -1, sources: interval.sources });
|
|
15
|
+
}
|
|
16
|
+
boundaries.sort((a, b) => a.at - b.at);
|
|
17
|
+
const active = new Map<string, { count: number; source: Source }>();
|
|
18
|
+
const spans: Interval[] = [];
|
|
19
|
+
let depth = 0;
|
|
20
|
+
let previous = window.start;
|
|
21
|
+
let lastKeys = new Set<string>();
|
|
22
|
+
let sourceDifference = 0;
|
|
23
|
+
for (let i = 0; i < boundaries.length; ) {
|
|
24
|
+
const at = (boundaries[i] as Boundary).at;
|
|
25
|
+
if (depth > 0 && previous < at) {
|
|
26
|
+
const last = spans.at(-1);
|
|
27
|
+
if (last && last.end === previous && sourceDifference === 0) {
|
|
28
|
+
last.end = at;
|
|
29
|
+
} else {
|
|
30
|
+
spans.push({
|
|
31
|
+
start: previous,
|
|
32
|
+
end: at,
|
|
33
|
+
sources: [...active.values()].map((item) => item.source),
|
|
34
|
+
});
|
|
35
|
+
lastKeys = new Set(active.keys());
|
|
36
|
+
sourceDifference = 0;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
while (i < boundaries.length && (boundaries[i] as Boundary).at === at) {
|
|
40
|
+
const boundary = boundaries[i++] as Boundary;
|
|
41
|
+
depth += boundary.delta;
|
|
42
|
+
for (const source of boundary.sources) {
|
|
43
|
+
const key = JSON.stringify([source.kind, source.id]);
|
|
44
|
+
const item = active.get(key);
|
|
45
|
+
const count = (item?.count ?? 0) + boundary.delta;
|
|
46
|
+
// Track set differences incrementally so redundant overlap boundaries
|
|
47
|
+
// do not copy or compare the full source set on an unchanged span.
|
|
48
|
+
if ((count === 0) !== (item === undefined)) {
|
|
49
|
+
sourceDifference += lastKeys.has(key) ? -boundary.delta : boundary.delta;
|
|
50
|
+
}
|
|
51
|
+
if (count === 0) active.delete(key);
|
|
52
|
+
else active.set(key, { count, source: item?.source ?? source });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
previous = at;
|
|
56
|
+
}
|
|
57
|
+
return spans;
|
|
58
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/** Identity is (kind, id). `label` is display metadata and never participates in equality, merging, or highlighting. */
|
|
2
|
+
export type Source = { kind: string; id: string; label?: string };
|
|
3
|
+
|
|
4
|
+
/** Covered time. `sources` lists the contributors that cover this exact span (for the rrule adapter: the includes). */
|
|
5
|
+
export type Interval = { start: number; end: number; sources: Source[] }; // epoch ms, end-exclusive
|
|
6
|
+
|
|
7
|
+
/** Removed time. `sources` lists what removed it (for the rrule adapter: the excludes). Survives complete subtraction. */
|
|
8
|
+
export type Gap = { start: number; end: number; sources: Source[] };
|
|
9
|
+
|
|
10
|
+
export type LayerRole = 'availability' | 'booking' | 'custom';
|
|
11
|
+
/** `inset` is a cross-axis distance in px (vertical in the horizontal projection, horizontal in columns), applied by the engine. */
|
|
12
|
+
export type LayerStyle = {
|
|
13
|
+
color: string;
|
|
14
|
+
highlightColor?: string;
|
|
15
|
+
inset?: number;
|
|
16
|
+
opacity?: number;
|
|
17
|
+
};
|
|
18
|
+
export type Layer = {
|
|
19
|
+
id: string;
|
|
20
|
+
role: LayerRole;
|
|
21
|
+
z: number;
|
|
22
|
+
style: LayerStyle;
|
|
23
|
+
intervals: Interval[];
|
|
24
|
+
gaps?: Gap[];
|
|
25
|
+
label?: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type LaneFlag = 'none' | 'never-set' | 'empty-in-window';
|
|
29
|
+
export type Lane = {
|
|
30
|
+
id: string;
|
|
31
|
+
/** Bumped by the consumer when layer content changes; the engine hashes layer content when absent. */
|
|
32
|
+
version?: string | number;
|
|
33
|
+
label: string;
|
|
34
|
+
timezone?: string;
|
|
35
|
+
flag?: LaneFlag;
|
|
36
|
+
/** False when the lane's intervals are known to be incomplete (an adapter cap bit, #4). Default true. The hooks (#5, #11) show the incomplete label (#8) when false; the engine never infers it. */
|
|
37
|
+
complete?: boolean;
|
|
38
|
+
layers: Layer[];
|
|
39
|
+
meta?: unknown;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** A span of absolute time. No span kind, no resolution; those are axis and component concerns (#6). */
|
|
43
|
+
export type Window = { start: number; end: number };
|
|
44
|
+
|
|
45
|
+
/** RFC 5545 weekday numbering: 0 = Monday through 6 = Sunday. Used by `wkst`, `byweekday`, and `windowFor`. */
|
|
46
|
+
export type Weekday = 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
|
47
|
+
|
|
48
|
+
/** A UTC-offset change inside a column, from `dayColumnsFor` (#6). Any size, any count. */
|
|
49
|
+
export type Transition = { at: number; deltaMinutes: number }; // positive = clocks moved forward (time skipped)
|
|
50
|
+
|
|
51
|
+
/** One day column in the view zone. Always projected as 24 equal wall-clock hour bands. `localDate` is the column's calendar date in the view zone ('YYYY-MM-DD'); a local date with no instants (`Pacific/Apia`, 2011-12-30) produces no column at all (#6). */
|
|
52
|
+
export type DayColumn = {
|
|
53
|
+
start: number;
|
|
54
|
+
end: number;
|
|
55
|
+
localDate: string;
|
|
56
|
+
label: string;
|
|
57
|
+
transitions: Transition[];
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export type Projection =
|
|
61
|
+
| { orientation: 'horizontal'; viewTimezone: string; pxPerMinute: number; rowHeight: number }
|
|
62
|
+
| {
|
|
63
|
+
orientation: 'columns';
|
|
64
|
+
viewTimezone: string;
|
|
65
|
+
pxPerHour: number;
|
|
66
|
+
columnWidth: number;
|
|
67
|
+
days: DayColumn[];
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** Final visible bounds, inset already applied; also the hit-test bounds (#8). */
|
|
71
|
+
export type Rect = {
|
|
72
|
+
x: number;
|
|
73
|
+
y: number;
|
|
74
|
+
width: number;
|
|
75
|
+
height: number;
|
|
76
|
+
layerId: string;
|
|
77
|
+
z: number;
|
|
78
|
+
sources: Source[];
|
|
79
|
+
column?: number;
|
|
80
|
+
};
|
|
81
|
+
export type Coverage = {
|
|
82
|
+
availabilityMinutes: number;
|
|
83
|
+
bookingMinutes: number;
|
|
84
|
+
availabilityMinusBookingMinutes: number;
|
|
85
|
+
};
|
|
86
|
+
export type LaneGeometry = {
|
|
87
|
+
laneId: string;
|
|
88
|
+
rects: Rect[];
|
|
89
|
+
gapRects: Rect[];
|
|
90
|
+
coverage: Coverage;
|
|
91
|
+
flag: LaneFlag;
|
|
92
|
+
};
|
package/src/core/zone.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Intl supplies offsets; UTC Date arithmetic supplies local-date arithmetic.
|
|
2
|
+
// Hourly probes bracket IANA transitions, then binary search keeps exact epoch
|
|
3
|
+
// boundaries, including historical second offsets. No host-local Date methods.
|
|
4
|
+
import type { Transition } from './types';
|
|
5
|
+
|
|
6
|
+
const formatters = new Map<string, Intl.DateTimeFormat>();
|
|
7
|
+
const starts = new Map<string, number>();
|
|
8
|
+
const minute = 60_000;
|
|
9
|
+
export const dayMilliseconds = 86_400_000;
|
|
10
|
+
|
|
11
|
+
export function wallTime(time: number, timezone: string): number {
|
|
12
|
+
let formatter = formatters.get(timezone);
|
|
13
|
+
if (!formatter) {
|
|
14
|
+
formatter = new Intl.DateTimeFormat('en-US-u-ca-gregory-nu-latn', {
|
|
15
|
+
timeZone: timezone,
|
|
16
|
+
era: 'short',
|
|
17
|
+
year: 'numeric',
|
|
18
|
+
month: '2-digit',
|
|
19
|
+
day: '2-digit',
|
|
20
|
+
hour: '2-digit',
|
|
21
|
+
minute: '2-digit',
|
|
22
|
+
second: '2-digit',
|
|
23
|
+
hourCycle: 'h23',
|
|
24
|
+
});
|
|
25
|
+
formatters.set(timezone, formatter);
|
|
26
|
+
}
|
|
27
|
+
const parts: Record<string, string> = {};
|
|
28
|
+
for (const part of formatter.formatToParts(time)) parts[part.type] = part.value;
|
|
29
|
+
const date = new Date(0);
|
|
30
|
+
const year = parts.era === 'BC' ? 1 - Number(parts.year) : Number(parts.year);
|
|
31
|
+
date.setUTCFullYear(year, Number(parts.month) - 1, Number(parts.day));
|
|
32
|
+
date.setUTCHours(Number(parts.hour) % 24, Number(parts.minute), Number(parts.second), 0);
|
|
33
|
+
return date.getTime() + (((time % 1000) + 1000) % 1000);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function offsetAt(time: number, timezone: string): number {
|
|
37
|
+
return wallTime(time, timezone) - time;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function localDateAt(time: number, timezone: string): string {
|
|
41
|
+
return dateString(wallTime(time, timezone));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function dateString(time: number): string {
|
|
45
|
+
return new Date(time).toISOString().slice(0, 10);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function dateEpoch(localDate: string): number {
|
|
49
|
+
const time = Date.parse(`${localDate}T00:00:00.000Z`);
|
|
50
|
+
if (
|
|
51
|
+
!/^\d{4}-\d{2}-\d{2}$/.test(localDate) ||
|
|
52
|
+
!Number.isFinite(time) ||
|
|
53
|
+
dateString(time) !== localDate
|
|
54
|
+
) {
|
|
55
|
+
throw new RangeError('Expected a valid YYYY-MM-DD local date');
|
|
56
|
+
}
|
|
57
|
+
return time;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function transitionsBetween(start: number, end: number, timezone: string): Transition[] {
|
|
61
|
+
const transitions: Transition[] = [];
|
|
62
|
+
let cursor = start;
|
|
63
|
+
let offset = offsetAt(cursor, timezone);
|
|
64
|
+
while (cursor < end) {
|
|
65
|
+
const probe = Math.min(cursor + 60 * minute, end);
|
|
66
|
+
const nextOffset = offsetAt(probe, timezone);
|
|
67
|
+
if (nextOffset !== offset) {
|
|
68
|
+
let low = cursor;
|
|
69
|
+
let high = probe;
|
|
70
|
+
while (high - low > 1) {
|
|
71
|
+
const middle = Math.floor((low + high) / 2);
|
|
72
|
+
if (offsetAt(middle, timezone) === offset) low = middle;
|
|
73
|
+
else high = middle;
|
|
74
|
+
}
|
|
75
|
+
if (high < end) transitions.push({ at: high, deltaMinutes: (nextOffset - offset) / minute });
|
|
76
|
+
}
|
|
77
|
+
cursor = probe;
|
|
78
|
+
offset = nextOffset;
|
|
79
|
+
}
|
|
80
|
+
return transitions;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function startOfDate(localDate: string, timezone: string): number {
|
|
84
|
+
const key = JSON.stringify([localDate, timezone]);
|
|
85
|
+
const cached = starts.get(key);
|
|
86
|
+
if (cached !== undefined) return cached;
|
|
87
|
+
const target = dateEpoch(localDate);
|
|
88
|
+
const start = target - 2 * dayMilliseconds;
|
|
89
|
+
const end = target + 2 * dayMilliseconds;
|
|
90
|
+
const boundaries = [start, ...transitionsBetween(start, end, timezone).map(({ at }) => at), end];
|
|
91
|
+
let result = Infinity;
|
|
92
|
+
for (let i = 0; i < boundaries.length - 1; i++) {
|
|
93
|
+
const left = boundaries[i] as number;
|
|
94
|
+
const right = boundaries[i + 1] as number;
|
|
95
|
+
const candidate = Math.max(left, target - offsetAt(left, timezone));
|
|
96
|
+
if (candidate < right) result = Math.min(result, candidate);
|
|
97
|
+
}
|
|
98
|
+
starts.set(key, result);
|
|
99
|
+
return result;
|
|
100
|
+
}
|