@spatika/react 1.1.0 → 1.2.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/AGENTS.md +71 -0
- package/README.md +4 -3
- package/dist/composites/Card.d.ts +7 -1
- package/dist/composites/Card.d.ts.map +1 -1
- package/dist/composites/Card.js +10 -0
- package/dist/composites/ChartDataGrid.d.ts +33 -0
- package/dist/composites/ChartDataGrid.d.ts.map +1 -0
- package/dist/composites/ChartDataGrid.js +77 -0
- package/dist/composites/CoverHero.d.ts +1 -1
- package/dist/composites/CoverHero.d.ts.map +1 -1
- package/dist/composites/CoverHero.js +2 -2
- package/dist/composites/EntityMediaCard.d.ts +1 -1
- package/dist/composites/EntityMediaCard.d.ts.map +1 -1
- package/dist/composites/EntityMediaCard.js +12 -4
- package/dist/composites/EventCalendar.d.ts +35 -0
- package/dist/composites/EventCalendar.d.ts.map +1 -0
- package/dist/composites/EventCalendar.js +339 -0
- package/dist/composites/EventEditor.d.ts +22 -0
- package/dist/composites/EventEditor.d.ts.map +1 -0
- package/dist/composites/EventEditor.js +86 -0
- package/dist/composites/EventTimeline.d.ts +24 -0
- package/dist/composites/EventTimeline.d.ts.map +1 -0
- package/dist/composites/EventTimeline.js +184 -0
- package/dist/composites/MapChart.d.ts +24 -0
- package/dist/composites/MapChart.d.ts.map +1 -0
- package/dist/composites/MapChart.js +58 -0
- package/dist/composites/ProfileHero.d.ts +6 -4
- package/dist/composites/ProfileHero.d.ts.map +1 -1
- package/dist/composites/ProfileHero.js +8 -8
- package/dist/composites/ScatterWebGL.d.ts +22 -0
- package/dist/composites/ScatterWebGL.d.ts.map +1 -0
- package/dist/composites/ScatterWebGL.js +45 -0
- package/dist/composites/SiteFooter.d.ts +1 -1
- package/dist/composites/SiteFooter.js +1 -1
- package/dist/composites/SiteNav.d.ts +7 -2
- package/dist/composites/SiteNav.d.ts.map +1 -1
- package/dist/composites/SiteNav.js +9 -5
- package/dist/composites/StatCard.d.ts +2 -1
- package/dist/composites/StatCard.d.ts.map +1 -1
- package/dist/composites/StatCard.js +3 -3
- package/dist/composites/StatusDot.d.ts +1 -1
- package/dist/composites/Wordmark.d.ts +2 -2
- package/dist/composites/Wordmark.d.ts.map +1 -1
- package/dist/composites/cartesian-charts.d.ts +138 -0
- package/dist/composites/cartesian-charts.d.ts.map +1 -0
- package/dist/composites/cartesian-charts.js +401 -0
- package/dist/composites/chart-composition.d.ts +139 -0
- package/dist/composites/chart-composition.d.ts.map +1 -0
- package/dist/composites/chart-composition.js +486 -0
- package/dist/composites/chart-ui.d.ts +83 -0
- package/dist/composites/chart-ui.d.ts.map +1 -0
- package/dist/composites/chart-ui.js +108 -0
- package/dist/composites/charts-3d.d.ts +37 -0
- package/dist/composites/charts-3d.d.ts.map +1 -0
- package/dist/composites/charts-3d.js +121 -0
- package/dist/composites/flow-charts.d.ts +89 -0
- package/dist/composites/flow-charts.d.ts.map +1 -0
- package/dist/composites/flow-charts.js +186 -0
- package/dist/composites/radial-charts.d.ts +109 -0
- package/dist/composites/radial-charts.d.ts.map +1 -0
- package/dist/composites/radial-charts.js +176 -0
- package/dist/composites/scheduler-ui.d.ts +43 -0
- package/dist/composites/scheduler-ui.d.ts.map +1 -0
- package/dist/composites/scheduler-ui.js +71 -0
- package/dist/index.d.ts +67 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +57 -7
- package/dist/lib/chart-export.d.ts +10 -0
- package/dist/lib/chart-export.d.ts.map +1 -0
- package/dist/lib/chart-export.js +102 -0
- package/dist/lib/charts.d.ts +235 -0
- package/dist/lib/charts.d.ts.map +1 -0
- package/dist/lib/charts.js +744 -0
- package/dist/lib/geo.d.ts +39 -0
- package/dist/lib/geo.d.ts.map +1 -0
- package/dist/lib/geo.js +101 -0
- package/dist/lib/iso.d.ts +21 -0
- package/dist/lib/iso.d.ts.map +1 -0
- package/dist/lib/iso.js +56 -0
- package/dist/lib/rrule.d.ts +20 -0
- package/dist/lib/rrule.d.ts.map +1 -0
- package/dist/lib/rrule.js +236 -0
- package/dist/lib/scheduler.d.ts +152 -0
- package/dist/lib/scheduler.d.ts.map +1 -0
- package/dist/lib/scheduler.js +501 -0
- package/dist/lib/webgl-scatter.d.ts +44 -0
- package/dist/lib/webgl-scatter.d.ts.map +1 -0
- package/dist/lib/webgl-scatter.js +185 -0
- package/dist/primitives/Accordion.d.ts +44 -20
- package/dist/primitives/Accordion.d.ts.map +1 -1
- package/dist/primitives/Accordion.js +82 -22
- package/dist/primitives/AppBar.d.ts +13 -0
- package/dist/primitives/AppBar.d.ts.map +1 -0
- package/dist/primitives/AppBar.js +27 -0
- package/dist/primitives/Autocomplete.d.ts +28 -0
- package/dist/primitives/Autocomplete.d.ts.map +1 -0
- package/dist/primitives/Autocomplete.js +118 -0
- package/dist/primitives/Avatar.d.ts +13 -1
- package/dist/primitives/Avatar.d.ts.map +1 -1
- package/dist/primitives/Avatar.js +17 -2
- package/dist/primitives/Backdrop.d.ts +17 -0
- package/dist/primitives/Backdrop.d.ts.map +1 -0
- package/dist/primitives/Backdrop.js +17 -0
- package/dist/primitives/BottomNavigation.d.ts +23 -0
- package/dist/primitives/BottomNavigation.d.ts.map +1 -0
- package/dist/primitives/BottomNavigation.js +30 -0
- package/dist/primitives/ButtonBase.d.ts +14 -0
- package/dist/primitives/ButtonBase.d.ts.map +1 -0
- package/dist/primitives/ButtonBase.js +13 -0
- package/dist/primitives/ButtonGroup.d.ts +25 -0
- package/dist/primitives/ButtonGroup.d.ts.map +1 -0
- package/dist/primitives/ButtonGroup.js +44 -0
- package/dist/primitives/CircularProgress.d.ts +16 -0
- package/dist/primitives/CircularProgress.d.ts.map +1 -0
- package/dist/primitives/CircularProgress.js +17 -0
- package/dist/primitives/ClickAwayListener.d.ts +16 -0
- package/dist/primitives/ClickAwayListener.d.ts.map +1 -0
- package/dist/primitives/ClickAwayListener.js +33 -0
- package/dist/primitives/Collapse.d.ts +19 -0
- package/dist/primitives/Collapse.d.ts.map +1 -0
- package/dist/primitives/Collapse.js +14 -0
- package/dist/primitives/Container.d.ts +13 -0
- package/dist/primitives/Container.d.ts.map +1 -0
- package/dist/primitives/Container.js +26 -0
- package/dist/primitives/Fab.d.ts +22 -0
- package/dist/primitives/Fab.d.ts.map +1 -0
- package/dist/primitives/Fab.js +42 -0
- package/dist/primitives/FormControl.d.ts +27 -0
- package/dist/primitives/FormControl.d.ts.map +1 -0
- package/dist/primitives/FormControl.js +24 -0
- package/dist/primitives/FormControlLabel.d.ts +18 -0
- package/dist/primitives/FormControlLabel.d.ts.map +1 -0
- package/dist/primitives/FormControlLabel.js +19 -0
- package/dist/primitives/Grid.d.ts +32 -0
- package/dist/primitives/Grid.d.ts.map +1 -0
- package/dist/primitives/Grid.js +50 -0
- package/dist/primitives/IconButton.d.ts +22 -0
- package/dist/primitives/IconButton.d.ts.map +1 -0
- package/dist/primitives/IconButton.js +39 -0
- package/dist/primitives/ImageList.d.ts +31 -0
- package/dist/primitives/ImageList.d.ts.map +1 -0
- package/dist/primitives/ImageList.js +33 -0
- package/dist/primitives/InputAdornment.d.ts +28 -0
- package/dist/primitives/InputAdornment.d.ts.map +1 -0
- package/dist/primitives/InputAdornment.js +13 -0
- package/dist/primitives/Link.d.ts +17 -0
- package/dist/primitives/Link.d.ts.map +1 -0
- package/dist/primitives/Link.js +29 -0
- package/dist/primitives/List.d.ts +41 -0
- package/dist/primitives/List.d.ts.map +1 -0
- package/dist/primitives/List.js +22 -0
- package/dist/primitives/Masonry.d.ts +14 -0
- package/dist/primitives/Masonry.d.ts.map +1 -0
- package/dist/primitives/Masonry.js +15 -0
- package/dist/primitives/Menu.d.ts +28 -0
- package/dist/primitives/Menu.d.ts.map +1 -0
- package/dist/primitives/Menu.js +54 -0
- package/dist/primitives/MobileStepper.d.ts +15 -0
- package/dist/primitives/MobileStepper.d.ts.map +1 -0
- package/dist/primitives/MobileStepper.js +8 -0
- package/dist/primitives/Modal.d.ts +18 -0
- package/dist/primitives/Modal.d.ts.map +1 -0
- package/dist/primitives/Modal.js +29 -0
- package/dist/primitives/NoSsr.d.ts +15 -0
- package/dist/primitives/NoSsr.d.ts.map +1 -0
- package/dist/primitives/NoSsr.js +19 -0
- package/dist/primitives/Paper.d.ts +22 -0
- package/dist/primitives/Paper.d.ts.map +1 -0
- package/dist/primitives/Paper.js +33 -0
- package/dist/primitives/Progress.d.ts +6 -2
- package/dist/primitives/Progress.d.ts.map +1 -1
- package/dist/primitives/Progress.js +8 -3
- package/dist/primitives/Snackbar.d.ts +23 -0
- package/dist/primitives/Snackbar.d.ts.map +1 -0
- package/dist/primitives/Snackbar.js +33 -0
- package/dist/primitives/SpeedDial.d.ts +26 -0
- package/dist/primitives/SpeedDial.d.ts.map +1 -0
- package/dist/primitives/SpeedDial.js +26 -0
- package/dist/primitives/Stack.d.ts +15 -0
- package/dist/primitives/Stack.d.ts.map +1 -0
- package/dist/primitives/Stack.js +45 -0
- package/dist/primitives/StarRating.d.ts +19 -1
- package/dist/primitives/StarRating.d.ts.map +1 -1
- package/dist/primitives/StarRating.js +36 -6
- package/dist/primitives/Stepper.d.ts +36 -0
- package/dist/primitives/Stepper.d.ts.map +1 -0
- package/dist/primitives/Stepper.js +53 -0
- package/dist/primitives/TablePagination.d.ts +16 -0
- package/dist/primitives/TablePagination.d.ts.map +1 -0
- package/dist/primitives/TablePagination.js +12 -0
- package/dist/primitives/TextField.d.ts +24 -0
- package/dist/primitives/TextField.d.ts.map +1 -0
- package/dist/primitives/TextField.js +19 -0
- package/dist/primitives/TextareaAutosize.d.ts +11 -0
- package/dist/primitives/TextareaAutosize.d.ts.map +1 -0
- package/dist/primitives/TextareaAutosize.js +26 -0
- package/dist/primitives/ToggleButton.d.ts +28 -0
- package/dist/primitives/ToggleButton.d.ts.map +1 -0
- package/dist/primitives/ToggleButton.js +56 -0
- package/dist/primitives/TransferList.d.ts +21 -0
- package/dist/primitives/TransferList.d.ts.map +1 -0
- package/dist/primitives/TransferList.js +40 -0
- package/dist/primitives/Typography.d.ts +23 -0
- package/dist/primitives/Typography.d.ts.map +1 -0
- package/dist/primitives/Typography.js +69 -0
- package/dist/primitives/transitions.d.ts +29 -0
- package/dist/primitives/transitions.d.ts.map +1 -0
- package/dist/primitives/transitions.js +52 -0
- package/package.json +28 -4
- package/skills/spatika-ui/SKILL.md +96 -0
- package/src/composites/Card.tsx +51 -1
- package/src/composites/ChartDataGrid.test.tsx +57 -0
- package/src/composites/ChartDataGrid.tsx +196 -0
- package/src/composites/CoverHero.test.tsx +38 -0
- package/src/composites/CoverHero.tsx +29 -22
- package/src/composites/EntityMediaCard.test.tsx +58 -0
- package/src/composites/EntityMediaCard.tsx +31 -13
- package/src/composites/EventCalendar.tsx +928 -0
- package/src/composites/EventEditor.tsx +268 -0
- package/src/composites/EventTimeline.tsx +357 -0
- package/src/composites/MapChart.test.tsx +39 -0
- package/src/composites/MapChart.tsx +130 -0
- package/src/composites/ProfileHero.test.tsx +33 -0
- package/src/composites/ProfileHero.tsx +96 -79
- package/src/composites/ScatterWebGL.test.tsx +25 -0
- package/src/composites/ScatterWebGL.tsx +79 -0
- package/src/composites/SiteFooter.test.tsx +37 -0
- package/src/composites/SiteFooter.tsx +1 -1
- package/src/composites/SiteNav.test.tsx +46 -0
- package/src/composites/SiteNav.tsx +14 -4
- package/src/composites/StatCard.test.tsx +30 -0
- package/src/composites/StatCard.tsx +4 -0
- package/src/composites/Wordmark.test.tsx +23 -0
- package/src/composites/Wordmark.tsx +2 -2
- package/src/composites/cartesian-charts.test.tsx +52 -0
- package/src/composites/cartesian-charts.tsx +1287 -0
- package/src/composites/chart-composition.test.tsx +53 -0
- package/src/composites/chart-composition.tsx +898 -0
- package/src/composites/chart-ui.tsx +328 -0
- package/src/composites/charts-3d.test.tsx +25 -0
- package/src/composites/charts-3d.tsx +318 -0
- package/src/composites/charts.test.tsx +254 -0
- package/src/composites/chrome.test.tsx +29 -0
- package/src/composites/flow-charts.test.tsx +79 -0
- package/src/composites/flow-charts.tsx +607 -0
- package/src/composites/marketing.test.tsx +38 -34
- package/src/composites/radial-charts.test.tsx +74 -0
- package/src/composites/radial-charts.tsx +617 -0
- package/src/composites/scheduler-ui.tsx +304 -0
- package/src/composites/scheduler.test.tsx +194 -0
- package/src/index.test.ts +22 -0
- package/src/index.ts +277 -5
- package/src/lib/chart-export.test.ts +20 -0
- package/src/lib/chart-export.ts +99 -0
- package/src/lib/charts-platform.test.ts +93 -0
- package/src/lib/charts.test.ts +132 -0
- package/src/lib/charts.ts +1021 -0
- package/src/lib/geo.test.ts +42 -0
- package/src/lib/geo.ts +138 -0
- package/src/lib/iso.test.ts +22 -0
- package/src/lib/iso.ts +102 -0
- package/src/lib/rrule.test.ts +53 -0
- package/src/lib/rrule.ts +281 -0
- package/src/lib/scheduler.test.ts +186 -0
- package/src/lib/scheduler.ts +688 -0
- package/src/lib/tokens.test.ts +2 -2
- package/src/lib/webgl-scatter.test.ts +36 -0
- package/src/lib/webgl-scatter.ts +230 -0
- package/src/primitives/Accordion.tsx +218 -63
- package/src/primitives/AppBar.tsx +39 -0
- package/src/primitives/Autocomplete.tsx +236 -0
- package/src/primitives/Avatar.tsx +52 -1
- package/src/primitives/Backdrop.tsx +48 -0
- package/src/primitives/BottomNavigation.tsx +102 -0
- package/src/primitives/ButtonBase.tsx +34 -0
- package/src/primitives/ButtonGroup.tsx +90 -0
- package/src/primitives/CircularProgress.tsx +104 -0
- package/src/primitives/ClickAwayListener.tsx +46 -0
- package/src/primitives/Collapse.tsx +39 -0
- package/src/primitives/Container.tsx +40 -0
- package/src/primitives/Fab.tsx +62 -0
- package/src/primitives/FormControl.tsx +90 -0
- package/src/primitives/FormControlLabel.tsx +51 -0
- package/src/primitives/Grid.tsx +101 -0
- package/src/primitives/IconButton.tsx +59 -0
- package/src/primitives/ImageList.test.tsx +48 -0
- package/src/primitives/ImageList.tsx +112 -0
- package/src/primitives/InputAdornment.tsx +86 -0
- package/src/primitives/Link.tsx +48 -0
- package/src/primitives/List.tsx +121 -0
- package/src/primitives/Masonry.tsx +32 -0
- package/src/primitives/Menu.tsx +112 -0
- package/src/primitives/MobileStepper.tsx +64 -0
- package/src/primitives/Modal.tsx +70 -0
- package/src/primitives/NoSsr.tsx +28 -0
- package/src/primitives/Paper.tsx +55 -0
- package/src/primitives/Progress.tsx +25 -7
- package/src/primitives/Snackbar.tsx +83 -0
- package/src/primitives/SpeedDial.tsx +110 -0
- package/src/primitives/Stack.tsx +88 -0
- package/src/primitives/StarRating.tsx +87 -28
- package/src/primitives/Stepper.tsx +184 -0
- package/src/primitives/TablePagination.tsx +58 -0
- package/src/primitives/TextField.tsx +105 -0
- package/src/primitives/TextareaAutosize.tsx +49 -0
- package/src/primitives/ToggleButton.tsx +148 -0
- package/src/primitives/TransferList.tsx +110 -0
- package/src/primitives/Typography.tsx +90 -0
- package/src/primitives/mui-more.test.tsx +76 -0
- package/src/primitives/mui.test.tsx +134 -0
- package/src/primitives/transitions.tsx +120 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { featureId, fitFeatures, geoPath, mercator } from "./geo";
|
|
3
|
+
|
|
4
|
+
describe("geo", () => {
|
|
5
|
+
it("projects the equator to the mercator midpoint", () => {
|
|
6
|
+
const [x, y] = mercator([0, 0]);
|
|
7
|
+
expect(x).toBeCloseTo(0.5);
|
|
8
|
+
expect(y).toBeCloseTo(0.5);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("builds a closed polygon path fitted to a viewport", () => {
|
|
12
|
+
const features = [
|
|
13
|
+
{
|
|
14
|
+
type: "Feature" as const,
|
|
15
|
+
id: "box",
|
|
16
|
+
properties: { name: "Box" },
|
|
17
|
+
geometry: {
|
|
18
|
+
type: "Polygon" as const,
|
|
19
|
+
coordinates: [
|
|
20
|
+
[
|
|
21
|
+
[0, 0],
|
|
22
|
+
[10, 0],
|
|
23
|
+
[10, 10],
|
|
24
|
+
[0, 10],
|
|
25
|
+
[0, 0],
|
|
26
|
+
],
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
const project = fitFeatures(features, 200, 100, 10);
|
|
32
|
+
const d = geoPath(features[0]!.geometry, project);
|
|
33
|
+
expect(d.startsWith("M")).toBe(true);
|
|
34
|
+
expect(d.includes("Z")).toBe(true);
|
|
35
|
+
expect(featureId(features[0]!)).toBe("box");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("projects a point to a move-to command", () => {
|
|
39
|
+
const d = geoPath({ type: "Point", coordinates: [0, 0] }, mercator);
|
|
40
|
+
expect(d.startsWith("M")).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
});
|
package/src/lib/geo.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny GeoJSON helpers — Mercator / equirectangular, no d3-geo.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type GeoPosition = [number, number];
|
|
6
|
+
|
|
7
|
+
export type GeoJsonGeometry =
|
|
8
|
+
| { type: "Polygon"; coordinates: GeoPosition[][] }
|
|
9
|
+
| { type: "MultiPolygon"; coordinates: GeoPosition[][][] }
|
|
10
|
+
| { type: "Point"; coordinates: GeoPosition }
|
|
11
|
+
| { type: "LineString"; coordinates: GeoPosition[] }
|
|
12
|
+
| { type: "MultiLineString"; coordinates: GeoPosition[][] };
|
|
13
|
+
|
|
14
|
+
export type GeoJsonFeature = {
|
|
15
|
+
type: "Feature";
|
|
16
|
+
id?: string | number;
|
|
17
|
+
properties?: Record<string, unknown> | null;
|
|
18
|
+
geometry: GeoJsonGeometry | null;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type GeoJsonFeatureCollection = {
|
|
22
|
+
type: "FeatureCollection";
|
|
23
|
+
features: GeoJsonFeature[];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type GeoProjectionKind = "mercator" | "equirectangular";
|
|
27
|
+
|
|
28
|
+
export function mercator([lon, lat]: GeoPosition): [number, number] {
|
|
29
|
+
const x = (lon + 180) / 360;
|
|
30
|
+
const clamped = Math.min(85.051129, Math.max(-85.051129, lat));
|
|
31
|
+
const sin = Math.sin((clamped * Math.PI) / 180);
|
|
32
|
+
const y = 0.5 - Math.log((1 + sin) / (1 - sin)) / (4 * Math.PI);
|
|
33
|
+
return [x, y];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function equirectangular([lon, lat]: GeoPosition): [number, number] {
|
|
37
|
+
return [(lon + 180) / 360, (90 - lat) / 180];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function projectLonLat(pos: GeoPosition, kind: GeoProjectionKind): [number, number] {
|
|
41
|
+
return kind === "equirectangular" ? equirectangular(pos) : mercator(pos);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function featureId(feature: GeoJsonFeature, idProperty = "id"): string {
|
|
45
|
+
if (feature.id != null) return String(feature.id);
|
|
46
|
+
const value = feature.properties?.[idProperty] ?? feature.properties?.name;
|
|
47
|
+
return value != null ? String(value) : "";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function walkGeometry(geometry: GeoJsonGeometry, visit: (pos: GeoPosition) => void) {
|
|
51
|
+
if (geometry.type === "Point") {
|
|
52
|
+
visit(geometry.coordinates);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (geometry.type === "LineString") {
|
|
56
|
+
geometry.coordinates.forEach(visit);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (geometry.type === "MultiLineString") {
|
|
60
|
+
geometry.coordinates.forEach((line) => line.forEach(visit));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (geometry.type === "Polygon") {
|
|
64
|
+
geometry.coordinates.forEach((ring) => ring.forEach(visit));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
geometry.coordinates.forEach((poly) => poly.forEach((ring) => ring.forEach(visit)));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function walkFeatures(features: GeoJsonFeature[], visit: (pos: GeoPosition) => void) {
|
|
71
|
+
for (const feature of features) {
|
|
72
|
+
if (feature.geometry) walkGeometry(feature.geometry, visit);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function ringPath(ring: GeoPosition[], project: (pos: GeoPosition) => [number, number]): string {
|
|
77
|
+
if (!ring.length) return "";
|
|
78
|
+
const pts = ring.map(project);
|
|
79
|
+
return `M${pts.map(([x, y]) => `${x} ${y}`).join("L")}Z`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function linePath(line: GeoPosition[], project: (pos: GeoPosition) => [number, number]): string {
|
|
83
|
+
if (!line.length) return "";
|
|
84
|
+
const pts = line.map(project);
|
|
85
|
+
return `M${pts.map(([x, y]) => `${x} ${y}`).join("L")}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function geoPath(
|
|
89
|
+
geometry: GeoJsonGeometry,
|
|
90
|
+
project: (pos: GeoPosition) => [number, number],
|
|
91
|
+
): string {
|
|
92
|
+
if (geometry.type === "Polygon") {
|
|
93
|
+
return geometry.coordinates.map((ring) => ringPath(ring, project)).join(" ");
|
|
94
|
+
}
|
|
95
|
+
if (geometry.type === "MultiPolygon") {
|
|
96
|
+
return geometry.coordinates
|
|
97
|
+
.map((poly) => poly.map((ring) => ringPath(ring, project)).join(" "))
|
|
98
|
+
.join(" ");
|
|
99
|
+
}
|
|
100
|
+
if (geometry.type === "LineString") return linePath(geometry.coordinates, project);
|
|
101
|
+
if (geometry.type === "MultiLineString") {
|
|
102
|
+
return geometry.coordinates.map((line) => linePath(line, project)).join(" ");
|
|
103
|
+
}
|
|
104
|
+
const [x, y] = project(geometry.coordinates);
|
|
105
|
+
return `M${x} ${y}m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function fitFeatures(
|
|
109
|
+
features: GeoJsonFeature[],
|
|
110
|
+
width: number,
|
|
111
|
+
height: number,
|
|
112
|
+
padding: number,
|
|
113
|
+
kind: GeoProjectionKind = "mercator",
|
|
114
|
+
): (pos: GeoPosition) => [number, number] {
|
|
115
|
+
const raw = (pos: GeoPosition) => projectLonLat(pos, kind);
|
|
116
|
+
let minX = Infinity;
|
|
117
|
+
let minY = Infinity;
|
|
118
|
+
let maxX = -Infinity;
|
|
119
|
+
let maxY = -Infinity;
|
|
120
|
+
walkFeatures(features, (pos) => {
|
|
121
|
+
const [x, y] = raw(pos);
|
|
122
|
+
minX = Math.min(minX, x);
|
|
123
|
+
minY = Math.min(minY, y);
|
|
124
|
+
maxX = Math.max(maxX, x);
|
|
125
|
+
maxY = Math.max(maxY, y);
|
|
126
|
+
});
|
|
127
|
+
const bw = Math.max(1e-6, maxX - minX);
|
|
128
|
+
const bh = Math.max(1e-6, maxY - minY);
|
|
129
|
+
const innerW = Math.max(1, width - padding * 2);
|
|
130
|
+
const innerH = Math.max(1, height - padding * 2);
|
|
131
|
+
const scale = Math.min(innerW / bw, innerH / bh);
|
|
132
|
+
const tx = padding + (innerW - bw * scale) / 2;
|
|
133
|
+
const ty = padding + (innerH - bh * scale) / 2;
|
|
134
|
+
return (pos) => {
|
|
135
|
+
const [x, y] = raw(pos);
|
|
136
|
+
return [tx + (x - minX) * scale, ty + (y - minY) * scale];
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { ellipseSlicePath, isoBoxFaces, isoPoint } from "./iso";
|
|
3
|
+
|
|
4
|
+
describe("iso", () => {
|
|
5
|
+
it("projects isometric points with y up", () => {
|
|
6
|
+
const origin = isoPoint(0, 0, 0, 40, 80);
|
|
7
|
+
const raised = isoPoint(0, 10, 0, 40, 80);
|
|
8
|
+
expect(raised.y).toBeLessThan(origin.y);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("emits three faces for a box", () => {
|
|
12
|
+
const faces = isoBoxFaces(0, 0, 0, 10, 20, 8, 40, 80);
|
|
13
|
+
expect(faces.front.startsWith("M")).toBe(true);
|
|
14
|
+
expect(faces.top).not.toEqual(faces.front);
|
|
15
|
+
expect(faces.side).not.toEqual(faces.top);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("draws an elliptical pie slice", () => {
|
|
19
|
+
const d = ellipseSlicePath(50, 40, 30, 12, 0, 90);
|
|
20
|
+
expect(d.includes("A30 12")).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
});
|
package/src/lib/iso.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Isometric 2.5D helpers for SVG bar and pie charts.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const COS = Math.cos(Math.PI / 6);
|
|
6
|
+
const SIN = Math.sin(Math.PI / 6);
|
|
7
|
+
|
|
8
|
+
export function isoPoint(
|
|
9
|
+
x: number,
|
|
10
|
+
y: number,
|
|
11
|
+
z: number,
|
|
12
|
+
originX: number,
|
|
13
|
+
originY: number,
|
|
14
|
+
): { x: number; y: number } {
|
|
15
|
+
return {
|
|
16
|
+
x: originX + (x - z) * COS,
|
|
17
|
+
y: originY - y + (x + z) * SIN,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function poly(points: Array<{ x: number; y: number }>): string {
|
|
22
|
+
if (!points.length) return "";
|
|
23
|
+
return `M${points.map((p) => `${p.x} ${p.y}`).join("L")}Z`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type IsoBoxFaces = {
|
|
27
|
+
front: string;
|
|
28
|
+
top: string;
|
|
29
|
+
side: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** Axis-aligned box: x/z on the ground plane, y up. */
|
|
33
|
+
export function isoBoxFaces(
|
|
34
|
+
x: number,
|
|
35
|
+
y0: number,
|
|
36
|
+
z: number,
|
|
37
|
+
w: number,
|
|
38
|
+
h: number,
|
|
39
|
+
d: number,
|
|
40
|
+
originX: number,
|
|
41
|
+
originY: number,
|
|
42
|
+
): IsoBoxFaces {
|
|
43
|
+
const p = (px: number, py: number, pz: number) => isoPoint(px, py, pz, originX, originY);
|
|
44
|
+
const a = p(x, y0, z);
|
|
45
|
+
const b = p(x + w, y0, z);
|
|
46
|
+
const c = p(x + w, y0 + h, z);
|
|
47
|
+
const e = p(x, y0 + h, z);
|
|
48
|
+
const f = p(x + w, y0, z + d);
|
|
49
|
+
const g = p(x + w, y0 + h, z + d);
|
|
50
|
+
const k = p(x, y0 + h, z + d);
|
|
51
|
+
return {
|
|
52
|
+
front: poly([a, b, c, e]),
|
|
53
|
+
top: poly([e, c, g, k]),
|
|
54
|
+
side: poly([b, f, g, c]),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function ellipsePoint(
|
|
59
|
+
cx: number,
|
|
60
|
+
cy: number,
|
|
61
|
+
rx: number,
|
|
62
|
+
ry: number,
|
|
63
|
+
angleDeg: number,
|
|
64
|
+
): { x: number; y: number } {
|
|
65
|
+
const rad = ((angleDeg - 90) * Math.PI) / 180;
|
|
66
|
+
return { x: cx + rx * Math.cos(rad), y: cy + ry * Math.sin(rad) };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function ellipseSlicePath(
|
|
70
|
+
cx: number,
|
|
71
|
+
cy: number,
|
|
72
|
+
rx: number,
|
|
73
|
+
ry: number,
|
|
74
|
+
startAngle: number,
|
|
75
|
+
endAngle: number,
|
|
76
|
+
): string {
|
|
77
|
+
const start = ellipsePoint(cx, cy, rx, ry, startAngle);
|
|
78
|
+
const end = ellipsePoint(cx, cy, rx, ry, endAngle);
|
|
79
|
+
const sweep = ((endAngle - startAngle) % 360 + 360) % 360;
|
|
80
|
+
if (sweep <= 0) return "";
|
|
81
|
+
const large = sweep > 180 ? 1 : 0;
|
|
82
|
+
return `M${cx} ${cy} L${start.x} ${start.y} A${rx} ${ry} 0 ${large} 1 ${end.x} ${end.y} Z`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function ellipseWallPath(
|
|
86
|
+
cx: number,
|
|
87
|
+
cy: number,
|
|
88
|
+
rx: number,
|
|
89
|
+
ry: number,
|
|
90
|
+
startAngle: number,
|
|
91
|
+
endAngle: number,
|
|
92
|
+
thickness: number,
|
|
93
|
+
): string {
|
|
94
|
+
const top0 = ellipsePoint(cx, cy, rx, ry, startAngle);
|
|
95
|
+
const top1 = ellipsePoint(cx, cy, rx, ry, endAngle);
|
|
96
|
+
const bot0 = { x: top0.x, y: top0.y + thickness };
|
|
97
|
+
const bot1 = { x: top1.x, y: top1.y + thickness };
|
|
98
|
+
const sweep = ((endAngle - startAngle) % 360 + 360) % 360;
|
|
99
|
+
if (sweep <= 0) return "";
|
|
100
|
+
const large = sweep > 180 ? 1 : 0;
|
|
101
|
+
return `M${bot0.x} ${bot0.y} L${top0.x} ${top0.y} A${rx} ${ry} 0 ${large} 1 ${top1.x} ${top1.y} L${bot1.x} ${bot1.y} A${rx} ${ry} 0 ${large} 0 ${bot0.x} ${bot0.y} Z`;
|
|
102
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { expandEventOccurrences, parseRRule } from "./rrule";
|
|
3
|
+
import { normalizeEvents } from "./scheduler";
|
|
4
|
+
|
|
5
|
+
describe("rrule", () => {
|
|
6
|
+
it("parses FREQ, INTERVAL, BYDAY, and COUNT", () => {
|
|
7
|
+
const rule = parseRRule("FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,WE;COUNT=6");
|
|
8
|
+
expect(rule).toMatchObject({
|
|
9
|
+
freq: "WEEKLY",
|
|
10
|
+
interval: 2,
|
|
11
|
+
count: 6,
|
|
12
|
+
});
|
|
13
|
+
expect(rule?.byDay?.map((item) => item.weekday)).toEqual([1, 3]);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("expands weekly BYDAY into the visible range", () => {
|
|
17
|
+
const [event] = normalizeEvents([
|
|
18
|
+
{
|
|
19
|
+
id: "standup",
|
|
20
|
+
title: "Standup",
|
|
21
|
+
start: "2026-08-03T10:00:00",
|
|
22
|
+
end: "2026-08-03T10:15:00",
|
|
23
|
+
rrule: "FREQ=WEEKLY;BYDAY=MO,WE,FR",
|
|
24
|
+
},
|
|
25
|
+
]);
|
|
26
|
+
const occurrences = expandEventOccurrences(
|
|
27
|
+
event,
|
|
28
|
+
new Date(2026, 7, 3),
|
|
29
|
+
new Date(2026, 7, 8),
|
|
30
|
+
);
|
|
31
|
+
expect(occurrences.map((item) => item.start.getDay())).toEqual([1, 3, 5]);
|
|
32
|
+
expect(occurrences.every((item) => item.id === "standup")).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("skips exception dates", () => {
|
|
36
|
+
const [event] = normalizeEvents([
|
|
37
|
+
{
|
|
38
|
+
id: "daily",
|
|
39
|
+
title: "Daily",
|
|
40
|
+
start: "2026-08-03T09:00:00",
|
|
41
|
+
end: "2026-08-03T09:30:00",
|
|
42
|
+
rrule: "FREQ=DAILY;COUNT=5",
|
|
43
|
+
exceptionDates: ["2026-08-04"],
|
|
44
|
+
},
|
|
45
|
+
]);
|
|
46
|
+
const occurrences = expandEventOccurrences(
|
|
47
|
+
event,
|
|
48
|
+
new Date(2026, 7, 3),
|
|
49
|
+
new Date(2026, 7, 10),
|
|
50
|
+
);
|
|
51
|
+
expect(occurrences.map((item) => item.start.getDate())).toEqual([3, 5, 6, 7]);
|
|
52
|
+
});
|
|
53
|
+
});
|
package/src/lib/rrule.ts
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import type { NormalizedEvent, SchedulerEvent } from "./scheduler";
|
|
2
|
+
|
|
3
|
+
function toDate(value: Date | string): Date {
|
|
4
|
+
if (value instanceof Date) return new Date(value.getTime());
|
|
5
|
+
const dateOnly = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
|
6
|
+
if (dateOnly) {
|
|
7
|
+
return new Date(Number(dateOnly[1]), Number(dateOnly[2]) - 1, Number(dateOnly[3]));
|
|
8
|
+
}
|
|
9
|
+
return new Date(value);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function startOfDay(date: Date): Date {
|
|
13
|
+
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function addDays(date: Date, amount: number): Date {
|
|
17
|
+
const next = new Date(date.getTime());
|
|
18
|
+
next.setDate(next.getDate() + amount);
|
|
19
|
+
return next;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function addMonths(date: Date, amount: number): Date {
|
|
23
|
+
const next = new Date(date.getTime());
|
|
24
|
+
next.setMonth(next.getMonth() + amount);
|
|
25
|
+
return next;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function addMs(date: Date, ms: number): Date {
|
|
29
|
+
return new Date(date.getTime() + ms);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const WEEKDAY: Record<string, number> = {
|
|
33
|
+
SU: 0,
|
|
34
|
+
MO: 1,
|
|
35
|
+
TU: 2,
|
|
36
|
+
WE: 3,
|
|
37
|
+
TH: 4,
|
|
38
|
+
FR: 5,
|
|
39
|
+
SA: 6,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type ParsedRRule = {
|
|
43
|
+
freq: "DAILY" | "WEEKLY" | "MONTHLY" | "YEARLY";
|
|
44
|
+
interval: number;
|
|
45
|
+
count?: number;
|
|
46
|
+
until?: Date;
|
|
47
|
+
byDay?: { weekday: number; nth?: number }[];
|
|
48
|
+
byMonthDay?: number[];
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export function parseRRule(rrule: string): ParsedRRule | null {
|
|
52
|
+
const parts = rrule
|
|
53
|
+
.replace(/^RRULE:/i, "")
|
|
54
|
+
.split(";")
|
|
55
|
+
.map((part) => part.trim())
|
|
56
|
+
.filter(Boolean);
|
|
57
|
+
const map = new Map<string, string>();
|
|
58
|
+
for (const part of parts) {
|
|
59
|
+
const [key, value] = part.split("=");
|
|
60
|
+
if (key && value) map.set(key.toUpperCase(), value);
|
|
61
|
+
}
|
|
62
|
+
const freq = map.get("FREQ") as ParsedRRule["freq"] | undefined;
|
|
63
|
+
if (!freq || !["DAILY", "WEEKLY", "MONTHLY", "YEARLY"].includes(freq)) return null;
|
|
64
|
+
|
|
65
|
+
const untilRaw = map.get("UNTIL");
|
|
66
|
+
let until: Date | undefined;
|
|
67
|
+
if (untilRaw) {
|
|
68
|
+
const compact = /^(\d{4})(\d{2})(\d{2})(?:T(\d{2})(\d{2})(\d{2})Z?)?$/.exec(untilRaw);
|
|
69
|
+
until = compact
|
|
70
|
+
? new Date(
|
|
71
|
+
Date.UTC(
|
|
72
|
+
Number(compact[1]),
|
|
73
|
+
Number(compact[2]) - 1,
|
|
74
|
+
Number(compact[3]),
|
|
75
|
+
Number(compact[4] ?? 23),
|
|
76
|
+
Number(compact[5] ?? 59),
|
|
77
|
+
Number(compact[6] ?? 59),
|
|
78
|
+
),
|
|
79
|
+
)
|
|
80
|
+
: toDate(untilRaw);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const byDay = map.get("BYDAY")
|
|
84
|
+
?.split(",")
|
|
85
|
+
.flatMap((token) => {
|
|
86
|
+
const match = /^(-?\d+)?(SU|MO|TU|WE|TH|FR|SA)$/.exec(token.trim().toUpperCase());
|
|
87
|
+
if (!match) return [];
|
|
88
|
+
return [{ weekday: WEEKDAY[match[2]], nth: match[1] ? Number(match[1]) : undefined }];
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const byMonthDay = map
|
|
92
|
+
.get("BYMONTHDAY")
|
|
93
|
+
?.split(",")
|
|
94
|
+
.map((value) => Number(value))
|
|
95
|
+
.filter((value) => Number.isFinite(value));
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
freq,
|
|
99
|
+
interval: Math.max(1, Number(map.get("INTERVAL") ?? 1) || 1),
|
|
100
|
+
count: map.get("COUNT") ? Math.max(1, Number(map.get("COUNT"))) : undefined,
|
|
101
|
+
until,
|
|
102
|
+
byDay,
|
|
103
|
+
byMonthDay,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function nthWeekdayOfMonth(year: number, month: number, weekday: number, nth: number): Date | null {
|
|
108
|
+
if (nth > 0) {
|
|
109
|
+
const first = new Date(year, month, 1);
|
|
110
|
+
const shift = (weekday - first.getDay() + 7) % 7;
|
|
111
|
+
const day = 1 + shift + (nth - 1) * 7;
|
|
112
|
+
const date = new Date(year, month, day);
|
|
113
|
+
return date.getMonth() === month ? date : null;
|
|
114
|
+
}
|
|
115
|
+
const last = new Date(year, month + 1, 0);
|
|
116
|
+
const shift = (last.getDay() - weekday + 7) % 7;
|
|
117
|
+
const day = last.getDate() - shift + (nth + 1) * 7;
|
|
118
|
+
const date = new Date(year, month, day);
|
|
119
|
+
return date.getMonth() === month ? date : null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function matchesByDay(date: Date, rule: ParsedRRule): boolean {
|
|
123
|
+
if (!rule.byDay?.length) return true;
|
|
124
|
+
return rule.byDay.some((item) => {
|
|
125
|
+
if (item.weekday !== date.getDay()) return false;
|
|
126
|
+
if (item.nth == null) return true;
|
|
127
|
+
const found = nthWeekdayOfMonth(date.getFullYear(), date.getMonth(), item.weekday, item.nth);
|
|
128
|
+
return found ? found.getDate() === date.getDate() : false;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function exceptionSet(dates: Array<Date | string> | undefined): Set<string> {
|
|
133
|
+
const set = new Set<string>();
|
|
134
|
+
for (const value of dates ?? []) {
|
|
135
|
+
const date = startOfDay(toDate(value));
|
|
136
|
+
set.add(`${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`);
|
|
137
|
+
}
|
|
138
|
+
return set;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function dayKey(date: Date): string {
|
|
142
|
+
return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Expand a recurring event into occurrences overlapping [rangeStart, rangeEnd).
|
|
147
|
+
* Non-recurring events are returned as a single occurrence.
|
|
148
|
+
*/
|
|
149
|
+
export function expandEventOccurrences(
|
|
150
|
+
event: NormalizedEvent,
|
|
151
|
+
rangeStart: Date,
|
|
152
|
+
rangeEnd: Date,
|
|
153
|
+
source?: SchedulerEvent,
|
|
154
|
+
): NormalizedEvent[] {
|
|
155
|
+
if (!event.rrule) return [event];
|
|
156
|
+
const rule = parseRRule(event.rrule);
|
|
157
|
+
if (!rule) return [event];
|
|
158
|
+
|
|
159
|
+
const duration = Math.max(0, event.end.getTime() - event.start.getTime());
|
|
160
|
+
const exceptions = exceptionSet(source?.exceptionDates ?? event.exceptionDates);
|
|
161
|
+
const occurrences: NormalizedEvent[] = [];
|
|
162
|
+
const seed = new Date(event.start.getTime());
|
|
163
|
+
let emitted = 0;
|
|
164
|
+
const hardCap = rule.count ?? 400;
|
|
165
|
+
|
|
166
|
+
const pushIfVisible = (start: Date) => {
|
|
167
|
+
if (start.getTime() < seed.getTime() - 60_000) return;
|
|
168
|
+
if (exceptions.has(dayKey(start))) return;
|
|
169
|
+
if (rule.until && start.getTime() > rule.until.getTime()) return;
|
|
170
|
+
const end = addMs(start, duration || 3_600_000);
|
|
171
|
+
if (end.getTime() <= rangeStart.getTime() || start.getTime() >= rangeEnd.getTime()) return;
|
|
172
|
+
occurrences.push({
|
|
173
|
+
...event,
|
|
174
|
+
start,
|
|
175
|
+
end,
|
|
176
|
+
occurrenceStart: start,
|
|
177
|
+
});
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
if (rule.freq === "DAILY") {
|
|
181
|
+
let cursor = new Date(seed.getTime());
|
|
182
|
+
while (emitted < hardCap && cursor.getTime() < rangeEnd.getTime() + duration) {
|
|
183
|
+
if (!rule.until || cursor.getTime() <= rule.until.getTime()) {
|
|
184
|
+
pushIfVisible(new Date(cursor.getTime()));
|
|
185
|
+
emitted += 1;
|
|
186
|
+
}
|
|
187
|
+
cursor = addDays(cursor, rule.interval);
|
|
188
|
+
if (rule.count && emitted >= rule.count) break;
|
|
189
|
+
}
|
|
190
|
+
return occurrences;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (rule.freq === "WEEKLY") {
|
|
194
|
+
const days = rule.byDay?.length
|
|
195
|
+
? [...new Set(rule.byDay.map((item) => item.weekday))].sort()
|
|
196
|
+
: [seed.getDay()];
|
|
197
|
+
let weekStart = addDays(startOfDay(seed), -seed.getDay());
|
|
198
|
+
while (emitted < hardCap && weekStart.getTime() < rangeEnd.getTime()) {
|
|
199
|
+
for (const weekday of days) {
|
|
200
|
+
const start = new Date(
|
|
201
|
+
weekStart.getFullYear(),
|
|
202
|
+
weekStart.getMonth(),
|
|
203
|
+
weekStart.getDate() + weekday,
|
|
204
|
+
seed.getHours(),
|
|
205
|
+
seed.getMinutes(),
|
|
206
|
+
seed.getSeconds(),
|
|
207
|
+
);
|
|
208
|
+
if (start.getTime() < seed.getTime() - 60_000) continue;
|
|
209
|
+
if (rule.until && start.getTime() > rule.until.getTime()) continue;
|
|
210
|
+
pushIfVisible(start);
|
|
211
|
+
emitted += 1;
|
|
212
|
+
if (rule.count && emitted >= rule.count) return occurrences;
|
|
213
|
+
}
|
|
214
|
+
weekStart = addDays(weekStart, 7 * rule.interval);
|
|
215
|
+
}
|
|
216
|
+
return occurrences;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (rule.freq === "MONTHLY") {
|
|
220
|
+
let year = seed.getFullYear();
|
|
221
|
+
let month = seed.getMonth();
|
|
222
|
+
while (emitted < hardCap) {
|
|
223
|
+
const monthStart = new Date(year, month, 1);
|
|
224
|
+
if (monthStart.getTime() > rangeEnd.getTime()) break;
|
|
225
|
+
const candidates: Date[] = [];
|
|
226
|
+
if (rule.byDay?.length) {
|
|
227
|
+
for (const item of rule.byDay) {
|
|
228
|
+
const found = nthWeekdayOfMonth(year, month, item.weekday, item.nth ?? 1);
|
|
229
|
+
if (found) {
|
|
230
|
+
candidates.push(
|
|
231
|
+
new Date(year, month, found.getDate(), seed.getHours(), seed.getMinutes(), seed.getSeconds()),
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
const days = rule.byMonthDay?.length ? rule.byMonthDay : [seed.getDate()];
|
|
237
|
+
for (const day of days) {
|
|
238
|
+
const date = new Date(year, month, day, seed.getHours(), seed.getMinutes(), seed.getSeconds());
|
|
239
|
+
if (date.getMonth() === month) candidates.push(date);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
for (const start of candidates.sort((a, b) => a.getTime() - b.getTime())) {
|
|
243
|
+
if (rule.until && start.getTime() > rule.until.getTime()) continue;
|
|
244
|
+
pushIfVisible(start);
|
|
245
|
+
emitted += 1;
|
|
246
|
+
if (rule.count && emitted >= rule.count) return occurrences;
|
|
247
|
+
}
|
|
248
|
+
const next = addMonths(new Date(year, month, 1), rule.interval);
|
|
249
|
+
year = next.getFullYear();
|
|
250
|
+
month = next.getMonth();
|
|
251
|
+
}
|
|
252
|
+
return occurrences;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
let cursor = new Date(seed.getTime());
|
|
256
|
+
while (emitted < hardCap && cursor.getTime() < rangeEnd.getTime() + duration) {
|
|
257
|
+
if (matchesByDay(cursor, rule) && (!rule.until || cursor.getTime() <= rule.until.getTime())) {
|
|
258
|
+
pushIfVisible(new Date(cursor.getTime()));
|
|
259
|
+
emitted += 1;
|
|
260
|
+
}
|
|
261
|
+
cursor = addMonths(cursor, 12 * rule.interval);
|
|
262
|
+
if (rule.count && emitted >= rule.count) break;
|
|
263
|
+
}
|
|
264
|
+
return occurrences;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function expandVisibleEvents(
|
|
268
|
+
events: NormalizedEvent[],
|
|
269
|
+
rangeStart: Date,
|
|
270
|
+
rangeEnd: Date,
|
|
271
|
+
sources: SchedulerEvent[] = [],
|
|
272
|
+
): NormalizedEvent[] {
|
|
273
|
+
return events.flatMap((event) =>
|
|
274
|
+
expandEventOccurrences(
|
|
275
|
+
event,
|
|
276
|
+
rangeStart,
|
|
277
|
+
rangeEnd,
|
|
278
|
+
sources.find((item) => item.id === event.id),
|
|
279
|
+
),
|
|
280
|
+
);
|
|
281
|
+
}
|