@1771technologies/lytenyte-pro 2.1.3 → 2.2.1

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 (34) hide show
  1. package/README.md +99 -58
  2. package/dist/__play__/annotations.play.d.ts +4 -0
  3. package/dist/__play__/annotations.play.js +79 -0
  4. package/dist/__play__/renderers/arrow.d.ts +7 -0
  5. package/dist/__play__/renderers/arrow.js +19 -0
  6. package/dist/__play__/renderers/border.d.ts +6 -0
  7. package/dist/__play__/renderers/border.js +11 -0
  8. package/dist/__play__/renderers/comment.d.ts +6 -0
  9. package/dist/__play__/renderers/comment.js +14 -0
  10. package/dist/components/annotations/annotation-header-overlay.d.ts +4 -0
  11. package/dist/components/annotations/annotation-header-overlay.js +27 -0
  12. package/dist/components/annotations/annotation-header-rect.d.ts +6 -0
  13. package/dist/components/annotations/annotation-header-rect.js +40 -0
  14. package/dist/components/annotations/annotation-point-rect.d.ts +5 -0
  15. package/dist/components/annotations/annotation-point-rect.js +30 -0
  16. package/dist/components/annotations/annotation-rect.d.ts +6 -0
  17. package/dist/components/annotations/annotation-rect.js +98 -0
  18. package/dist/components/annotations/annotation-row-sections.d.ts +10 -0
  19. package/dist/components/annotations/annotation-row-sections.js +53 -0
  20. package/dist/components/annotations/resolve-anchor.d.ts +5 -0
  21. package/dist/components/annotations/resolve-anchor.js +18 -0
  22. package/dist/components/annotations/types.d.ts +29 -0
  23. package/dist/components/annotations/types.js +1 -0
  24. package/dist/components/headless/dialog/container.js +16 -0
  25. package/dist/components/pill-manager/item.js +1 -1
  26. package/dist/components/pill-manager/root.js +85 -2
  27. package/dist/data-source-server/source/use-on-view-change.d.ts +1 -1
  28. package/dist/data-source-server/source/use-on-view-change.js +30 -6
  29. package/dist/data-source-server/use-server-data-source.js +2 -1
  30. package/dist/index.d.ts +1 -0
  31. package/dist/root/root.d.ts +4 -1
  32. package/dist/root/root.js +18 -3
  33. package/dist/types.d.ts +1 -0
  34. package/package.json +7 -7
package/README.md CHANGED
@@ -1,31 +1,22 @@
1
- <div align="center">
2
- <br><br>
3
- <picture>
4
- <source media="(prefers-color-scheme: dark)" srcset="https://xwhoymbxqtnizjadugph.supabase.co/storage/v1/object/public/images/github-lytenyte-dark.svg"/>
5
- <source media="(prefers-color-scheme: light)" srcset="https://xwhoymbxqtnizjadugph.supabase.co/storage/v1/object/public/images/github-lytenyte-light.svg"/>
6
- <img width="360" alt="Logo of LyteNyte Grid" src="https://xwhoymbxqtnizjadugph.supabase.co/storage/v1/object/public/images/github-lytenyte-light.svg"/>
7
- </picture>
8
- <br><br>
9
-
10
- <h3>
11
- The Fastest React Data Grid On The Modern Web
12
- </h3>
1
+ ![Banner](https://xwhoymbxqtnizjadugph.supabase.co/storage/v1/object/public/images/github-banner.png)
13
2
 
14
- </div>
15
- <div align="center">
3
+ <!-- <div align="center">
16
4
 
17
- LyteNyte Grid is a **~40 KB, zero-dependency React data grid** engineered for
18
- high performance and enterprise-scale datasets. It can handle **10,000 updates/sec**
19
- and render **millions of rows**. No wrappers. No heavy abstractions.
5
+ <picture>
20
6
 
21
- Choose between **headless** components or a **pre-styled** grid out of
22
- the box, and ship faster with a **declarative API** that
23
- is purpose-built for React.
7
+ <source media="(prefers-color-scheme: dark)" srcset="https://xwhoymbxqtnizjadugph.supabase.co/storage/v1/object/public/images/github-banner.png"/>
8
+ <source media="(prefers-color-scheme: light)" srcset="https://xwhoymbxqtnizjadugph.supabase.co/storage/v1/object/public/images/github-banner.png"/>
9
+ <img width="360" alt="Logo of LyteNyte Grid" src="https://xwhoymbxqtnizjadugph.supabase.co/storage/v1/object/public/images/github-banner.png"/>
10
+ </picture>
11
+ </div> -->
24
12
 
25
- Start with **LyteNyte Grid Core** (free and open source). Upgrade
26
- to **LyteNyte Grid PRO** for advanced enterprise features and support.
13
+ <h3>Drop The Fastest React Data Grid Into Your App In Seconds</h3>
27
14
 
28
- [Compare Features](https://1771technologies.com/pricing)
15
+ Building a feature-rich data grid that remains highly performant for end users takes a long time.
16
+ Time that could be spent focusing on your application.
17
+
18
+ We created LyteNyte Grid to solve that. Designed with an obsessive focus on DX, it's ultra-fast,
19
+ easy to customize, and drastically reduces the engineering effort required for data grids.
29
20
 
30
21
  </div>
31
22
 
@@ -45,23 +36,60 @@ to **LyteNyte Grid PRO** for advanced enterprise features and support.
45
36
 
46
37
  <div align="center">
47
38
 
48
- <a href="https://1771technologies.com">Website</a> &nbsp;&nbsp;—&nbsp;&nbsp; <a href="https://1771technologies.com/docs/intro-getting-started">Documentation</a> &nbsp;&nbsp;—&nbsp;&nbsp; <a href="https://1771technologies.com/demo">Demo</a> &nbsp;&nbsp;—&nbsp;&nbsp; <a href="https://1771technologies.com/pricing">Pricing</a> &nbsp;&nbsp;—&nbsp;&nbsp; <a href="https://1771technologies.com/eula">EULA</a>
39
+ <a href="https://1771technologies.com">Website</a> &nbsp;&nbsp;—&nbsp;&nbsp; <a href="https://1771technologies.com/docs/intro-getting-started">Documentation</a> &nbsp;&nbsp;—&nbsp;&nbsp; <a href="https://1771technologies.com/demo">Demo</a> &nbsp;&nbsp;—&nbsp;&nbsp; <a href="https://1771technologies.com/pricing">Pricing</a> &nbsp;&nbsp;—&nbsp;&nbsp; <a href="https://www.1771technologies.com/blog">Blog</a>
40
+ &nbsp;&nbsp;—&nbsp;&nbsp; <a href="https://www.1771technologies.com/support">Support</a>
49
41
 
50
42
  </div>
51
43
 
52
44
  ![LyteNyte Logo](https://xwhoymbxqtnizjadugph.supabase.co/storage/v1/object/public/images//github-readme-image.png)
53
45
 
54
- ## LyteNyte Editions
46
+ ## Why Choose LyteNyte Grid?
55
47
 
56
- LyteNyte Grid is available in two editions:
48
+ ⚡Up to **7x** [faster performance](https://www.1771technologies.com/blog/performance-benchmarks)
49
+ than competing grids. At just **40 KB gzipped**, it renders millions of rows and handles up to **10,000 updates/sec**.
57
50
 
58
- 1. **LyteNyte Grid Core:** Free, **open-source edition** for building high-performance data tables. Includes sorting,
59
- filtering, editing, row selection, detail views, and more. Also includes advanced features such
60
- as aggregation, row grouping, and cell range selection, often locked behind paywalls in other grids.
51
+ 🧰 Get **150+ built-in** [grid features](https://www.1771technologies.com/pricing?showFeatures=true#lytenyte-features),
52
+ including server-side data loading, infinite scrolling, pivoting, tree views, filtering, row grouping, and cell editing.
61
53
 
62
- 2. **LyteNyte Grid PRO: Commercial edition** that extends Core with powerful enterprise features,
63
- including server-side data loading, pivoting, tree data, component managers, and advanced
64
- filtering. Build custom menus, popovers, expressions, and workflow-driven UIs with ease.
54
+ 🎯 Better DX with a [declarative API](https://www.1771technologies.com/docs/grid-api-extensions)
55
+ that lets you extend the API and column definitions with your own custom props and methods. **100% type-safe**. **No wrappers or dependencies**.
56
+
57
+ 🎨 No [styling tradeoffs](https://www.1771technologies.com/docs/grid-theming)
58
+ by forcing a choice between a **headless table and a pre-built table**. You can run it headless for ultimate customization
59
+ or with pre-built logic and themes if you need to ship in a rush.
60
+
61
+ 🔄Driven by [your state](https://www.1771technologies.com/docs/grid-reactivity). It’s **fully stateless and prop-driven**. Configure it declaratively from
62
+ URL params, server state, or external state for simpler code and zero sync headaches.
63
+
64
+ 🤖 Turbocharges [AI workflows](https://www.1771technologies.com/docs/ai-skills-overview). The grid's
65
+ declarative and type-safe nature allows **AI to statically verify code**, preventing hidden bugs
66
+ and excess token usage caused by wrapping layers and imperative logic found in other grids.
67
+
68
+ ## Installation
69
+
70
+ LyteNyte Grid can be installed via NPM:
71
+
72
+ **PRO:**
73
+
74
+ ```sh
75
+ npm install --save @1771technologies/lytenyte-pro
76
+ ```
77
+
78
+ > [!NOTE]
79
+ > You can install and use LyteNyte Grid PRO
80
+ > without a license, but a watermark will be displayed.
81
+
82
+ **Core:**
83
+
84
+ ```sh
85
+ npm install --save @1771technologies/lytenyte-core
86
+ ```
87
+
88
+ ## Quick Start
89
+
90
+ - Begin with our comprehensive [getting started guide](https://www.1771technologies.com/docs/intro-getting-started).
91
+ - Explore our flexible [pricing options](https://www.1771technologies.com/pricing) to find the ideal fit for your project.
92
+ - Follow our step-by-step [installation guide](https://www.1771technologies.com/docs/intro-installation) to seamlessly integrate LyteNyte Grid into your application.
65
93
 
66
94
  ## AI-Enhanced Workflows
67
95
 
@@ -124,10 +152,24 @@ See our [AI Skills Overview guide](https://1771technologies.com/docs/ai-skills-o
124
152
  <summary><b>🚀&nbsp; Virtualization</b></summary>
125
153
  <br>
126
154
 
127
- | Feature | Core | PRO |
128
- | ---------------------------------------------------------------------------------- | :--: | :-: |
129
- | [Row Virtualization](https://www.1771technologies.com/docs/grid-virtualization) | ✅ | ✅ |
130
- | [Column Virtualization](https://www.1771technologies.com/docs/grid-virtualization) | ✅ | ✅ |
155
+ | Feature | Core | PRO |
156
+ | ------------------------------------------------------------------------------------------------------------------- | :--: | :-: |
157
+ | [Row Virtualization](https://www.1771technologies.com/docs/grid-virtualization) | ✅ | ✅ |
158
+ | [Column Virtualization](https://www.1771technologies.com/docs/grid-virtualization) | ✅ | ✅ |
159
+ | [Scroll Flash Suppression](https://www.1771technologies.com/docs/grid-virtualization#scroll-flash-suppression) | ✅ | ✅ |
160
+ | [Initial Viewport Dimensions](https://www.1771technologies.com/docs/grid-virtualization#initial-viewport-rendering) | ✅ | ✅ |
161
+
162
+ </details>
163
+
164
+ <details>
165
+ <summary><b>🎬&nbsp; Animations</b></summary>
166
+ <br>
167
+
168
+ | Feature | Core | PRO |
169
+ | ----------------------------------------------------------------------------------------------------- | :--: | :-: |
170
+ | [Row Animations](https://www.1771technologies.com/docs/grid-animations) | ✅ | ✅ |
171
+ | [Column Animations](https://www.1771technologies.com/docs/grid-animations) | ✅ | ✅ |
172
+ | [Custom Animation Keyframes](https://www.1771technologies.com/docs/grid-animations#custom-animations) | ✅ | ✅ |
131
173
 
132
174
  </details>
133
175
 
@@ -155,6 +197,7 @@ See our [AI Skills Overview guide](https://1771technologies.com/docs/ai-skills-o
155
197
  | **Basic Row Operations** | ✅ | ✅ |
156
198
  | &nbsp;&nbsp;&nbsp;↳ [Row Pinning](https://www.1771technologies.com/docs/row-pinning) | ✅ | ✅ |
157
199
  | &nbsp;&nbsp;&nbsp;↳ [Row Spanning](https://www.1771technologies.com/docs/row-spanning) | ✅ | ✅ |
200
+ | &nbsp;&nbsp;&nbsp;↳ [Row Banding](https://www.1771technologies.com/docs/row-banding) | ✅ | ✅ |
158
201
  | **Row Height** | ✅ | ✅ |
159
202
  | &nbsp;&nbsp;&nbsp;↳ [Variable Row Height](https://www.1771technologies.com/docs/row-height#variable-row-height) | ✅ | ✅ |
160
203
  | &nbsp;&nbsp;&nbsp;↳ [Fixed Row Height](https://www.1771technologies.com/docs/row-height#number-row-heights) | ✅ | ✅ |
@@ -204,6 +247,18 @@ See our [AI Skills Overview guide](https://1771technologies.com/docs/ai-skills-o
204
247
 
205
248
  </details>
206
249
 
250
+ <details>
251
+ <summary><b>📌&nbsp; Annotations</b> &nbsp;</summary>
252
+ <br>
253
+
254
+ | Feature | Core | PRO |
255
+ | ------------------------------------------------------------------------------------- | :--: | :-: |
256
+ | [Grid Annotations](https://www.1771technologies.com/docs/grid-annotations) | — | ✅ |
257
+ | [Cell Notes](https://www.1771technologies.com/docs/grid-annotations#cell-notes) | — | ✅ |
258
+ | [Marching Ants](https://www.1771technologies.com/docs/grid-annotations#marching-ants) | — | ✅ |
259
+
260
+ </details>
261
+
207
262
  <details>
208
263
  <summary><b>🔀&nbsp; Sorting</b></summary>
209
264
  <br>
@@ -408,31 +463,17 @@ See our [AI Skills Overview guide](https://1771technologies.com/docs/ai-skills-o
408
463
  implements one-way data binding and declarative rendering to
409
464
  maintain predictable state management.
410
465
 
411
- ## Installation
412
-
413
- LyteNyte Grid can be installed via NPM:
414
-
415
- **PRO:**
416
-
417
- ```sh
418
- npm install --save @1771technologies/lytenyte-pro
419
- ```
420
-
421
- > [!NOTE]
422
- > You can install and use LyteNyte Grid PRO
423
- > without a license, but a watermark will be displayed.
424
-
425
- **Core:**
466
+ ## LyteNyte Editions
426
467
 
427
- ```sh
428
- npm install --save @1771technologies/lytenyte-core
429
- ```
468
+ LyteNyte Grid is available in two editions:
430
469
 
431
- ## Quick Start
470
+ 1. **LyteNyte Grid Core:** Free, **open-source edition** for building high-performance data tables. Includes sorting,
471
+ filtering, editing, row selection, detail views, and more. Also includes advanced features such
472
+ as aggregation, row grouping, and cell range selection, often locked behind paywalls in other grids.
432
473
 
433
- - Begin with our comprehensive [getting started guide](https://www.1771technologies.com/docs/intro-getting-started).
434
- - Explore our flexible [pricing options](https://www.1771technologies.com/pricing) to find the ideal fit for your project.
435
- - Follow our step-by-step [installation guide](https://www.1771technologies.com/docs/intro-installation) to seamlessly integrate LyteNyte Grid into your application.
474
+ 2. **LyteNyte Grid PRO: Commercial edition** that extends Core with powerful enterprise features,
475
+ including server-side data loading, pivoting, tree data, component managers, and advanced
476
+ filtering. Build custom menus, popovers, expressions, and workflow-driven UIs with ease.
436
477
 
437
478
  ## Licensing
438
479
 
@@ -0,0 +1,4 @@
1
+ import "@1771technologies/lytenyte-design/shadcn-vars.css";
2
+ import "@1771technologies/lytenyte-design/fonts.css";
3
+ import "../../css/grid-full.css";
4
+ export default function AnnotationsPlay(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,79 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import "@1771technologies/lytenyte-design/shadcn-vars.css";
3
+ import "@1771technologies/lytenyte-design/fonts.css";
4
+ import "../../css/grid-full.css";
5
+ import { useState } from "react";
6
+ import { bankDataSmall } from "@1771technologies/grid-sample-data/bank-data-smaller";
7
+ import { useClientDataSource } from "../data-source-client/use-client-data-source.js";
8
+ import { Grid } from "../index.js";
9
+ import { annotationBorder } from "./renderers/border.js";
10
+ import { annotationArrow } from "./renderers/arrow.js";
11
+ import { annotationComment } from "./renderers/comment.js";
12
+ const columns = [
13
+ { id: "age", pin: "start", width: 90 },
14
+ { id: "job", pin: "start", width: 130 },
15
+ { id: "balance", width: 110 },
16
+ { id: "marital", width: 110 },
17
+ { id: "education", width: 110 },
18
+ { id: "default", width: 90 },
19
+ { id: "housing", width: 90 },
20
+ { id: "loan", width: 90 },
21
+ { id: "contact", width: 110 },
22
+ { id: "day", width: 90 },
23
+ { id: "month", width: 90 },
24
+ { id: "duration", width: 110 },
25
+ { id: "campaign", pin: "end", width: 110 },
26
+ { id: "y", pin: "end", width: 90 },
27
+ ];
28
+ export default function AnnotationsPlay() {
29
+ const [sync, setSync] = useState(false);
30
+ const ds = useClientDataSource({
31
+ data: bankDataSmall,
32
+ topData: bankDataSmall.slice(0, 2),
33
+ botData: bankDataSmall.slice(0, 2),
34
+ });
35
+ const annotations = [
36
+ {
37
+ id: "range-corner",
38
+ anchor: { kind: "range", rowStart: 0, rowEnd: 2, colStart: 0, colEnd: 2 },
39
+ render: annotationBorder({ color: "#ef4444", width: 3 }),
40
+ },
41
+ {
42
+ id: "range-center",
43
+ anchor: { kind: "range", rowStart: 10, rowEnd: 13, colStart: 3, colEnd: 6 },
44
+ render: annotationBorder({ color: "#22c55e" }),
45
+ },
46
+ {
47
+ id: "point-comment",
48
+ anchor: { kind: "point", x: 700, y: 800 },
49
+ render: annotationComment({ text: "This scrolls with the data" }),
50
+ },
51
+ {
52
+ id: "header-arrow-end",
53
+ anchor: { kind: "header", colStart: 12, colEnd: 13 },
54
+ render: annotationArrow({ direction: "down", color: "#3b82f6" }),
55
+ },
56
+ {
57
+ id: "header-arrow-center",
58
+ anchor: { kind: "header", colStart: 5, colEnd: 6 },
59
+ render: annotationArrow({ direction: "down", color: "#f59e0b" }),
60
+ },
61
+ {
62
+ id: "range-by-id",
63
+ anchor: {
64
+ kind: "range",
65
+ rowStart: "leaf-15",
66
+ rowEnd: "leaf-17",
67
+ colStart: "contact",
68
+ colEnd: "duration",
69
+ },
70
+ render: annotationBorder({ color: "#a855f7", width: 3 }),
71
+ },
72
+ {
73
+ id: "range-bad-id",
74
+ anchor: { kind: "range", rowStart: "does-not-exist", rowEnd: "leaf-17", colStart: 0, colEnd: 2 },
75
+ render: annotationBorder({ color: "#000000" }),
76
+ },
77
+ ];
78
+ return (_jsxs(_Fragment, { children: [_jsx("button", { onClick: () => setSync((s) => !s), children: "Toggle suppressScrollFlash (sync mode)" }), _jsx("div", { style: { width: "100%", height: "90vh" }, className: "ln-grid ln-shadcn", children: _jsx(Grid, { columns: columns, columnBase: { resizable: true }, rowSource: ds, suppressScrollFlash: sync, annotations: annotations }) })] }));
79
+ }
@@ -0,0 +1,7 @@
1
+ import type { Annotation } from "../../types.js";
2
+ export interface AnnotationArrowOptions {
3
+ readonly direction?: "up" | "down" | "left" | "right";
4
+ readonly color?: string;
5
+ readonly size?: number;
6
+ }
7
+ export declare function annotationArrow(options?: AnnotationArrowOptions): Annotation<any>["render"];
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ const ROTATION = {
3
+ up: 0,
4
+ right: 90,
5
+ down: 180,
6
+ left: 270,
7
+ };
8
+ export function annotationArrow(options) {
9
+ const direction = options?.direction ?? "up";
10
+ const color = options?.color ?? "#3b82f6";
11
+ const size = options?.size ?? 24;
12
+ return () => (_jsx("div", { style: {
13
+ position: "absolute",
14
+ inset: 0,
15
+ display: "flex",
16
+ alignItems: "center",
17
+ justifyContent: "center",
18
+ }, children: _jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", style: { transform: `rotate(${ROTATION[direction]}deg)` }, children: _jsx("path", { d: "M12 2 L22 20 L12 15 L2 20 Z", fill: color }) }) }));
19
+ }
@@ -0,0 +1,6 @@
1
+ import type { Annotation } from "../../types.js";
2
+ export interface AnnotationBorderOptions {
3
+ readonly color?: string;
4
+ readonly width?: number;
5
+ }
6
+ export declare function annotationBorder(options?: AnnotationBorderOptions): Annotation<any>["render"];
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ export function annotationBorder(options) {
3
+ const color = options?.color ?? "#3b82f6";
4
+ const width = options?.width ?? 2;
5
+ return () => (_jsx("div", { style: {
6
+ position: "absolute",
7
+ inset: 0,
8
+ boxSizing: "border-box",
9
+ border: `${width}px solid ${color}`,
10
+ } }));
11
+ }
@@ -0,0 +1,6 @@
1
+ import type { Annotation } from "../../types.js";
2
+ export interface AnnotationCommentOptions {
3
+ readonly text: string;
4
+ readonly color?: string;
5
+ }
6
+ export declare function annotationComment(options: AnnotationCommentOptions): Annotation<any>["render"];
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ export function annotationComment(options) {
3
+ const color = options.color ?? "#f59e0b";
4
+ return () => (_jsx("div", { title: options.text, style: {
5
+ position: "absolute",
6
+ top: 0,
7
+ insetInlineEnd: 0,
8
+ width: 0,
9
+ height: 0,
10
+ borderTop: `8px solid ${color}`,
11
+ borderInlineStart: "8px solid transparent",
12
+ pointerEvents: "auto",
13
+ } }));
14
+ }
@@ -0,0 +1,4 @@
1
+ import type { Annotation } from "./types.js";
2
+ export declare function AnnotationHeaderOverlay({ annotations, }: {
3
+ readonly annotations: readonly Annotation<any>[];
4
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,27 @@
1
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useMemo } from "react";
3
+ import { splitRect } from "@1771technologies/lytenyte-shared";
4
+ import { useCutoffContext, useColumnsContext } from "@1771technologies/lytenyte-core/internal";
5
+ import { AnnotationHeaderRect } from "./annotation-header-rect.js";
6
+ import { resolveColumnIndex, useColumnIdToIndex } from "./resolve-anchor.js";
7
+ export function AnnotationHeaderOverlay({ annotations, }) {
8
+ const { startCutoff, endCutoff } = useCutoffContext();
9
+ const { view } = useColumnsContext();
10
+ const colIdToIndex = useColumnIdToIndex(view.visibleColumns);
11
+ const split = useMemo(() => {
12
+ const result = [];
13
+ for (const annotation of annotations) {
14
+ if (annotation.anchor.kind !== "header")
15
+ continue;
16
+ const colStart = resolveColumnIndex(annotation.anchor.colStart, colIdToIndex);
17
+ const colEnd = resolveColumnIndex(annotation.anchor.colEnd, colIdToIndex);
18
+ if (colStart == null || colEnd == null)
19
+ continue;
20
+ const rects = splitRect({ rowStart: 0, rowEnd: 1, columnStart: colStart, columnEnd: colEnd }, startCutoff, endCutoff, 0, 1);
21
+ for (const rect of rects)
22
+ result.push({ rect, annotation });
23
+ }
24
+ return result;
25
+ }, [annotations, startCutoff, endCutoff, colIdToIndex]);
26
+ return (_jsx(_Fragment, { children: split.map((s, i) => (_jsx(AnnotationHeaderRect, { rect: s.rect, annotation: s.annotation }, `${s.annotation.id}-${i}`))) }));
27
+ }
@@ -0,0 +1,6 @@
1
+ import { type SectionedRect } from "@1771technologies/lytenyte-shared";
2
+ import type { Annotation } from "./types.js";
3
+ export declare function AnnotationHeaderRect({ rect, annotation, }: {
4
+ readonly rect: SectionedRect;
5
+ readonly annotation: Annotation<any>;
6
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,40 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { startSection, endSection } from "@1771technologies/lytenyte-shared";
3
+ import { useMemo } from "react";
4
+ import { sizeFromCoord } from "@1771technologies/js-utils";
5
+ import { useXCoordinates, useRtlContext, useSuppressScrollFlashContext, useDimensionContext, useAPI, } from "@1771technologies/lytenyte-core/internal";
6
+ export function AnnotationHeaderRect({ rect, annotation, }) {
7
+ const rtl = useRtlContext();
8
+ const xPositions = useXCoordinates();
9
+ const sync = useSuppressScrollFlashContext();
10
+ const dimensions = useDimensionContext();
11
+ const api = useAPI();
12
+ const vpWidth = dimensions.innerWidth;
13
+ const style = useMemo(() => {
14
+ const isStart = startSection[rect.section];
15
+ const isEnd = endSection[rect.section];
16
+ const width = sizeFromCoord(rect.columnStart, xPositions, rect.columnEnd - rect.columnStart);
17
+ const factor = rtl ? -1 : 1;
18
+ const x = isEnd
19
+ ? xPositions[rect.columnStart] - xPositions.at(-1) + vpWidth
20
+ : xPositions[rect.columnStart] * factor;
21
+ const style = {
22
+ height: "100%",
23
+ width,
24
+ position: "absolute",
25
+ pointerEvents: "none",
26
+ top: 0,
27
+ transform: sync && !(isStart || isEnd)
28
+ ? `var(--ln-x-transform) translate3d(${x}px, 0px, 0px)`
29
+ : `translate3d(${x}px, 0px, 0px)`,
30
+ zIndex: 1,
31
+ };
32
+ if (isStart || isEnd) {
33
+ style.position = "sticky";
34
+ style.insetInlineStart = "0px";
35
+ style.zIndex = 12;
36
+ }
37
+ return style;
38
+ }, [rect.columnStart, rect.columnEnd, rect.section, xPositions, rtl, sync, vpWidth]);
39
+ return (_jsx("div", { style: style, "data-ln-annotation-rect": true, "data-ln-annotation-id": annotation.id, children: annotation.render({ api }) }));
40
+ }
@@ -0,0 +1,5 @@
1
+ import type { Annotation, AnnotationAnchorPoint } from "./types.js";
2
+ export declare function AnnotationPointRect({ anchor, annotation, }: {
3
+ readonly anchor: AnnotationAnchorPoint;
4
+ readonly annotation: Annotation<any>;
5
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useMemo } from "react";
3
+ import { useYCoordinates, useRowCountsContext, useRtlContext, useSuppressScrollFlashContext, useAPI, } from "@1771technologies/lytenyte-core/internal";
4
+ export function AnnotationPointRect({ anchor, annotation, }) {
5
+ const rtl = useRtlContext();
6
+ const yPositions = useYCoordinates();
7
+ const { topCount: rowTopCount } = useRowCountsContext();
8
+ const isSync = useSuppressScrollFlashContext();
9
+ const api = useAPI();
10
+ const style = useMemo(() => {
11
+ const factor = rtl ? -1 : 1;
12
+ const x = isSync ? `calc(${anchor.x * factor}px - var(--ln-x-sync-offset,0))` : anchor.x;
13
+ const y = isSync
14
+ ? anchor.y - yPositions[rowTopCount]
15
+ : `calc(${anchor.y - yPositions[rowTopCount]}px - var(--ln-y-offset, 0px))`;
16
+ const transform = `translate3d(${typeof x === "string" ? x : `${x}px`}, ${typeof y === "string" ? y : `${y}px`}, 0px)`;
17
+ return {
18
+ transform,
19
+ position: "absolute",
20
+ pointerEvents: "none",
21
+ top: 0,
22
+ zIndex: 1,
23
+ gridRowStart: "1",
24
+ gridRowEnd: "2",
25
+ gridColumnStart: "1",
26
+ gridColumnEnd: "2",
27
+ };
28
+ }, [anchor.x, anchor.y, rtl, isSync, yPositions, rowTopCount]);
29
+ return (_jsx("div", { style: style, "data-ln-annotation-rect": true, "data-ln-annotation-id": annotation.id, children: annotation.render({ api }) }));
30
+ }
@@ -0,0 +1,6 @@
1
+ import { type SectionedRect } from "@1771technologies/lytenyte-shared";
2
+ import type { Annotation } from "./types.js";
3
+ export declare function AnnotationRect({ rect, annotation, }: {
4
+ readonly rect: SectionedRect;
5
+ readonly annotation: Annotation<any>;
6
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,98 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { bottomSection, endSection, startSection, topSection, } from "@1771technologies/lytenyte-shared";
3
+ import { useMemo } from "react";
4
+ import { sizeFromCoord } from "@1771technologies/js-utils";
5
+ import { useXCoordinates, useYCoordinates, useRowCountsContext, useDimensionContext, useRtlContext, useSuppressScrollFlashContext, useAPI, } from "@1771technologies/lytenyte-core/internal";
6
+ export function AnnotationRect({ rect, annotation, }) {
7
+ const rtl = useRtlContext();
8
+ const dimensions = useDimensionContext();
9
+ const xPositions = useXCoordinates();
10
+ const yPositions = useYCoordinates();
11
+ const { rowCount, topCount: rowTopCount, bottomCount: rowBotCount } = useRowCountsContext();
12
+ const isSync = useSuppressScrollFlashContext();
13
+ const api = useAPI();
14
+ const vpWidth = dimensions.innerWidth;
15
+ const style = useMemo(() => {
16
+ const columnStart = rect.columnStart;
17
+ const columnEnd = rect.columnEnd;
18
+ const isStart = startSection[rect.section];
19
+ const isEnd = endSection[rect.section];
20
+ const isTop = topSection[rect.section];
21
+ const isBot = bottomSection[rect.section];
22
+ const columnIndex = columnStart;
23
+ const rowIndex = rect.rowStart;
24
+ const width = sizeFromCoord(columnStart, xPositions, columnEnd - columnStart);
25
+ const height = sizeFromCoord(rect.rowStart, yPositions, rect.rowEnd - rect.rowStart);
26
+ const firstBotIndex = rowCount - rowBotCount;
27
+ let x;
28
+ const factor = rtl ? -1 : 1;
29
+ if (isSync) {
30
+ x = isEnd
31
+ ? (xPositions[columnIndex] - xPositions.at(-1) + vpWidth) * factor
32
+ : isStart
33
+ ? xPositions[columnIndex] * factor
34
+ : `calc(${xPositions[columnIndex] * factor}px - var(--ln-x-sync-offset,0))`;
35
+ }
36
+ else {
37
+ x = isEnd ? xPositions[columnIndex] - xPositions.at(-1) + vpWidth : xPositions[columnIndex];
38
+ }
39
+ let y;
40
+ if (isSync) {
41
+ if (isTop)
42
+ y = yPositions[rowIndex];
43
+ else
44
+ y = yPositions[rowIndex] - yPositions[rowTopCount];
45
+ }
46
+ else {
47
+ if (isBot)
48
+ y = yPositions[rowIndex] - yPositions[firstBotIndex];
49
+ else if (isTop)
50
+ y = yPositions[rowIndex];
51
+ else {
52
+ y = `calc(${yPositions[rowIndex] - yPositions[rowTopCount]}px - var(--ln-y-offset, 0px))`;
53
+ }
54
+ }
55
+ const transform = `translate3d(${typeof x == "string" ? x : `${x}px`}, ${typeof y === "string" ? y : `${y}px`}, 0px)`;
56
+ const pinnedRow = isTop || isBot;
57
+ const pinnedCell = isStart || isEnd;
58
+ const style = {
59
+ height,
60
+ width,
61
+ transform,
62
+ position: "absolute",
63
+ pointerEvents: "none",
64
+ top: 0,
65
+ zIndex: annotation.zIndex ?? 1,
66
+ gridRowStart: "1",
67
+ gridRowEnd: "2",
68
+ gridColumnStart: "1",
69
+ gridColumnEnd: "2",
70
+ };
71
+ if (pinnedCell) {
72
+ style.insetInlineStart = "0px";
73
+ style.position = "sticky";
74
+ style.zIndex = annotation.zIndex ?? (pinnedRow ? 6 : 3);
75
+ }
76
+ if (pinnedRow) {
77
+ style.position = "sticky";
78
+ style.top = "0px";
79
+ style.zIndex = annotation.zIndex ?? (pinnedCell ? 6 : 3);
80
+ }
81
+ return style;
82
+ }, [
83
+ rect.columnStart,
84
+ rect.columnEnd,
85
+ rect.section,
86
+ rect.rowStart,
87
+ rect.rowEnd,
88
+ xPositions,
89
+ yPositions,
90
+ rowCount,
91
+ rowBotCount,
92
+ rtl,
93
+ isSync,
94
+ vpWidth,
95
+ rowTopCount,
96
+ ]);
97
+ return (_jsx("div", { style: style, "data-ln-annotation-rect": true, "data-ln-annotation-id": annotation.id, children: annotation.render({ api }) }));
98
+ }
@@ -0,0 +1,10 @@
1
+ import type { Annotation } from "./types.js";
2
+ export declare function AnnotationRowSectionTop({ annotations, }: {
3
+ readonly annotations: readonly Annotation<any>[];
4
+ }): import("react/jsx-runtime").JSX.Element;
5
+ export declare function AnnotationRowSectionBottom({ annotations, }: {
6
+ readonly annotations: readonly Annotation<any>[];
7
+ }): import("react/jsx-runtime").JSX.Element;
8
+ export declare function AnnotationRowSectionCenter({ annotations, }: {
9
+ readonly annotations: readonly Annotation<any>[];
10
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,53 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo } from "react";
3
+ import { splitRect, topSection, centerSection, bottomSection, } from "@1771technologies/lytenyte-shared";
4
+ import { useCutoffContext, useRowSourceContext, useColumnsContext, } from "@1771technologies/lytenyte-core/internal";
5
+ import { AnnotationRect } from "./annotation-rect.js";
6
+ import { AnnotationPointRect } from "./annotation-point-rect.js";
7
+ import { resolveRowIndex, resolveColumnIndex, useColumnIdToIndex } from "./resolve-anchor.js";
8
+ const wrapperStyle = {
9
+ width: "100%",
10
+ height: 0,
11
+ display: "grid",
12
+ gridTemplateRows: "0px",
13
+ gridTemplateColumns: "0px",
14
+ };
15
+ function useSplitRangeAnnotations(annotations) {
16
+ const { startCutoff, endCutoff, topCutoff, bottomCutoff } = useCutoffContext();
17
+ const rowSource = useRowSourceContext();
18
+ const { view } = useColumnsContext();
19
+ const colIdToIndex = useColumnIdToIndex(view.visibleColumns);
20
+ return useMemo(() => {
21
+ const result = [];
22
+ for (const annotation of annotations) {
23
+ if (annotation.anchor.kind !== "range")
24
+ continue;
25
+ const rowStart = resolveRowIndex(annotation.anchor.rowStart, rowSource);
26
+ const rowEnd = resolveRowIndex(annotation.anchor.rowEnd, rowSource);
27
+ const colStart = resolveColumnIndex(annotation.anchor.colStart, colIdToIndex);
28
+ const colEnd = resolveColumnIndex(annotation.anchor.colEnd, colIdToIndex);
29
+ if (rowStart == null || rowEnd == null || colStart == null || colEnd == null)
30
+ continue;
31
+ const rects = splitRect({ rowStart, rowEnd, columnStart: colStart, columnEnd: colEnd }, startCutoff, endCutoff, topCutoff, bottomCutoff);
32
+ for (const rect of rects)
33
+ result.push({ rect, annotation });
34
+ }
35
+ return result;
36
+ }, [annotations, startCutoff, endCutoff, topCutoff, bottomCutoff, rowSource, colIdToIndex]);
37
+ }
38
+ export function AnnotationRowSectionTop({ annotations, }) {
39
+ const split = useSplitRangeAnnotations(annotations);
40
+ const topSplit = useMemo(() => split.filter((s) => topSection[s.rect.section]), [split]);
41
+ return (_jsx("div", { role: "presentation", style: wrapperStyle, children: topSplit.map((s, i) => (_jsx(AnnotationRect, { rect: s.rect, annotation: s.annotation }, `${s.annotation.id}-${i}`))) }));
42
+ }
43
+ export function AnnotationRowSectionBottom({ annotations, }) {
44
+ const split = useSplitRangeAnnotations(annotations);
45
+ const bottomSplit = useMemo(() => split.filter((s) => bottomSection[s.rect.section]), [split]);
46
+ return (_jsx("div", { role: "presentation", style: wrapperStyle, children: bottomSplit.map((s, i) => (_jsx(AnnotationRect, { rect: s.rect, annotation: s.annotation }, `${s.annotation.id}-${i}`))) }));
47
+ }
48
+ export function AnnotationRowSectionCenter({ annotations, }) {
49
+ const split = useSplitRangeAnnotations(annotations);
50
+ const centerSplit = useMemo(() => split.filter((s) => centerSection[s.rect.section]), [split]);
51
+ const points = useMemo(() => annotations.filter((a) => a.anchor.kind === "point"), [annotations]);
52
+ return (_jsxs("div", { role: "presentation", style: wrapperStyle, children: [centerSplit.map((s, i) => (_jsx(AnnotationRect, { rect: s.rect, annotation: s.annotation }, `${s.annotation.id}-${i}`))), points.map((a) => (_jsx(AnnotationPointRect, { anchor: a.anchor, annotation: a }, a.id)))] }));
53
+ }
@@ -0,0 +1,5 @@
1
+ import type { ColumnAbstract, RowSource } from "@1771technologies/lytenyte-shared";
2
+ import type { AnnotationIndexOrId } from "./types.js";
3
+ export declare function resolveRowIndex(value: AnnotationIndexOrId, rowSource: RowSource): number | null;
4
+ export declare function resolveColumnIndex(value: AnnotationIndexOrId, colIdToIndex: ReadonlyMap<string, number>): number | null;
5
+ export declare function useColumnIdToIndex(visibleColumns: readonly ColumnAbstract[]): ReadonlyMap<string, number>;
@@ -0,0 +1,18 @@
1
+ import { useMemo } from "react";
2
+ export function resolveRowIndex(value, rowSource) {
3
+ if (typeof value === "number")
4
+ return value;
5
+ return rowSource.rowIdToRowIndex(value) ?? null;
6
+ }
7
+ export function resolveColumnIndex(value, colIdToIndex) {
8
+ if (typeof value === "number")
9
+ return value;
10
+ return colIdToIndex.get(value) ?? null;
11
+ }
12
+ export function useColumnIdToIndex(visibleColumns) {
13
+ return useMemo(() => {
14
+ const map = new Map();
15
+ visibleColumns.forEach((c, i) => map.set(c.id, i));
16
+ return map;
17
+ }, [visibleColumns]);
18
+ }
@@ -0,0 +1,29 @@
1
+ import type { ReactNode } from "react";
2
+ import type { Root } from "../../root/root.js";
3
+ export type AnnotationIndexOrId = number | string;
4
+ export type AnnotationAnchorRange = {
5
+ readonly kind: "range";
6
+ readonly rowStart: AnnotationIndexOrId;
7
+ readonly rowEnd: AnnotationIndexOrId;
8
+ readonly colStart: AnnotationIndexOrId;
9
+ readonly colEnd: AnnotationIndexOrId;
10
+ };
11
+ export type AnnotationAnchorPoint = {
12
+ readonly kind: "point";
13
+ readonly x: number;
14
+ readonly y: number;
15
+ };
16
+ export type AnnotationAnchorHeader = {
17
+ readonly kind: "header";
18
+ readonly colStart: AnnotationIndexOrId;
19
+ readonly colEnd: AnnotationIndexOrId;
20
+ };
21
+ export type AnnotationAnchor = AnnotationAnchorRange | AnnotationAnchorPoint | AnnotationAnchorHeader;
22
+ export type Annotation<Spec extends Root.GridSpec = Root.GridSpec> = {
23
+ readonly id: string;
24
+ readonly anchor: AnnotationAnchor;
25
+ readonly render: (params: {
26
+ readonly api: Root.API<Spec>;
27
+ }) => ReactNode;
28
+ readonly zIndex?: number;
29
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -148,6 +148,22 @@ function DialogContainerBase(props, ref) {
148
148
  ev.clientX > bb.right ||
149
149
  ev.clientY < bb.top ||
150
150
  ev.clientY > bb.bottom) {
151
+ // If the click lands inside a sibling overlay (e.g. a menu that opened on top
152
+ // of this popover), let the event reach it and close this dialog quietly
153
+ // instead of swallowing the click with stopImmediatePropagation.
154
+ const targetEl = ev.target;
155
+ const insideOtherDismissable = targetEl.closest?.("[data-ln-light-dismiss='true']");
156
+ if (insideOtherDismissable && insideOtherDismissable !== dialog) {
157
+ setTimeout(() => onOpenChange(false));
158
+ return;
159
+ }
160
+ // If the click lands inside another open <dialog> (e.g. a modal edit popover),
161
+ // let the event reach it — don't swallow clicks meant for its buttons.
162
+ const insideOtherDialog = targetEl.closest?.("dialog[open]");
163
+ if (insideOtherDialog && insideOtherDialog !== dialog) {
164
+ setTimeout(() => onOpenChange(false));
165
+ return;
166
+ }
151
167
  ev.stopPropagation();
152
168
  ev.stopImmediatePropagation();
153
169
  if (typeof lightDismiss === "function") {
@@ -185,7 +185,7 @@ function PillItemBase({ item, elementEnd, ...props }, ref) {
185
185
  row,
186
186
  },
187
187
  });
188
- return (_jsxs("div", { "data-ln-pill-type": row.type, "data-ln-pill-item-container": true, "data-ln-pill-active": item.active, "data-ln-draggable": item.movable, "data-ln-drag-active": isDragActive, onDragLeave: (e) => {
188
+ return (_jsxs("div", { "data-ln-pill-type": row.type, "data-ln-pill-item-container": true, "data-ln-pill-id": item.id, "data-ln-pill-row-id": row.id, "data-ln-pill-active": item.active, "data-ln-draggable": item.movable, "data-ln-drag-active": isDragActive, onDragLeave: (e) => {
189
189
  e.stopPropagation();
190
190
  e.preventDefault();
191
191
  }, onDragEnter: handleDragEnter, children: [slot, placeholder] }));
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { forwardRef, Fragment, memo, useMemo, useRef, useState } from "react";
2
+ import { forwardRef, Fragment, memo, useLayoutEffect, useMemo, useRef, useState, } from "react";
3
+ import { useCombinedRefs } from "@1771technologies/lytenyte-core/internal";
3
4
  import { PillRootProvider } from "./root.context.js";
4
5
  import { PillRowDefault } from "./row-default.js";
5
6
  function PillRootImpl({ children = PillRowDefault, rows, orientation, onPillRowChange, onPillItemActiveChange, onPillItemThrown, ...p }, ref) {
@@ -8,6 +9,9 @@ function PillRootImpl({ children = PillRowDefault, rows, orientation, onPillRowC
8
9
  const prevSwapId = useRef(null);
9
10
  const prevRowId = useRef(null);
10
11
  const movedRef = useRef(null);
12
+ const rootElRef = useRef(null);
13
+ const snapshotRef = useRef(new Map());
14
+ const animationsRef = useRef(new Map());
11
15
  const value = useMemo(() => {
12
16
  return {
13
17
  orientation: orientation ?? "horizontal",
@@ -28,6 +32,85 @@ function PillRootImpl({ children = PillRowDefault, rows, orientation, onPillRowC
28
32
  const r = cloned ?? rows;
29
33
  return r.map((x, i) => _jsx(Fragment, { children: children(x, value) }, i));
30
34
  }, [children, cloned, rows, value]);
31
- return (_jsx(PillRootProvider, { value: value, children: _jsx("div", { ...p, ref: ref, "data-ln-pill-root": true, "data-ln-orientation": orientation ?? "horizontal", children: rendered }) }));
35
+ // FLIP "First": capture pill positions before this render's DOM commit.
36
+ // useMemo runs during render, before React's mutation phase, so rootElRef.current
37
+ // still reflects the previous layout — exactly the "before" positions we need.
38
+ // Only snapshot when cloned is non-null (drag is active and a reorder just occurred).
39
+ useMemo(() => {
40
+ const el = rootElRef.current;
41
+ if (!el || cloned == null)
42
+ return;
43
+ const snap = new Map();
44
+ el.querySelectorAll("[data-ln-pill-id]").forEach((pillEl) => {
45
+ // Key by "rowId:pillId" — pill IDs are only unique within a row, not across rows.
46
+ const key = `${pillEl.getAttribute("data-ln-pill-row-id")}:${pillEl.getAttribute("data-ln-pill-id")}`;
47
+ snap.set(key, pillEl.getBoundingClientRect());
48
+ });
49
+ snapshotRef.current = snap;
50
+ }, [cloned]);
51
+ // FLIP "Last + Invert + Play": after React commits the new DOM order, measure new
52
+ // positions, compute the delta from the snapshot, and animate from delta → none.
53
+ useLayoutEffect(() => {
54
+ const el = rootElRef.current;
55
+ if (!el || cloned == null)
56
+ return;
57
+ el.querySelectorAll("[data-ln-pill-id]").forEach((pillEl) => {
58
+ const pillId = pillEl.getAttribute("data-ln-pill-id");
59
+ const rowId = pillEl.getAttribute("data-ln-pill-row-id");
60
+ const key = `${rowId}:${pillId}`;
61
+ const oldRect = snapshotRef.current.get(key);
62
+ if (!oldRect)
63
+ return;
64
+ const newRect = pillEl.getBoundingClientRect();
65
+ const existing = animationsRef.current.get(key);
66
+ let dx;
67
+ let dy;
68
+ if (existing) {
69
+ try {
70
+ existing.commitStyles();
71
+ const xm = /translateX\(([-\d.]+)px\)/.exec(pillEl.style.transform);
72
+ const ym = /translateY\(([-\d.]+)px\)/.exec(pillEl.style.transform);
73
+ dx = (xm ? parseFloat(xm[1]) : 0) + (oldRect.left - newRect.left);
74
+ dy = (ym ? parseFloat(ym[1]) : 0) + (oldRect.top - newRect.top);
75
+ }
76
+ catch {
77
+ dx = oldRect.left - newRect.left;
78
+ dy = oldRect.top - newRect.top;
79
+ }
80
+ }
81
+ else {
82
+ dx = oldRect.left - newRect.left;
83
+ dy = oldRect.top - newRect.top;
84
+ }
85
+ if (Math.abs(dx) < 0.5 && Math.abs(dy) < 0.5)
86
+ return;
87
+ pillEl.style.transform = "none";
88
+ const from = `translateX(${dx}px) translateY(${dy}px)`;
89
+ if (existing) {
90
+ existing.effect?.setKeyframes([
91
+ { transform: from },
92
+ { transform: "none" },
93
+ ]);
94
+ existing.currentTime = 0;
95
+ existing.play();
96
+ return;
97
+ }
98
+ const anim = pillEl.animate([{ transform: from }, { transform: "none" }], {
99
+ duration: 200,
100
+ easing: "ease-out",
101
+ });
102
+ animationsRef.current.set(key, anim);
103
+ anim.finished
104
+ .catch(() => { })
105
+ .finally(() => {
106
+ if (animationsRef.current.get(key) === anim) {
107
+ animationsRef.current.delete(key);
108
+ pillEl.style.transform = "";
109
+ }
110
+ });
111
+ });
112
+ }, [cloned]);
113
+ const combined = useCombinedRefs(ref, rootElRef);
114
+ return (_jsx(PillRootProvider, { value: value, children: _jsx("div", { ...p, ref: combined, "data-ln-pill-root": true, "data-ln-orientation": orientation ?? "horizontal", children: rendered }) }));
32
115
  }
33
116
  export const PillManager = memo(forwardRef(PillRootImpl));
@@ -1,3 +1,3 @@
1
1
  import type { ServerData } from "../server-data";
2
2
  import type { DataRequest } from "../types";
3
- export declare function useOnViewChange<T>(source: ServerData, requestsForView: DataRequest[], setRequestsForView: (v: DataRequest[]) => void): (view: import("@1771technologies/lytenyte-shared").SpanLayout) => void;
3
+ export declare function useOnViewChange<T>(source: ServerData, requestsForView: DataRequest[], setRequestsForView: (v: DataRequest[]) => void, debounceDuration: number): (view: import("@1771technologies/lytenyte-shared").SpanLayout) => void;
@@ -1,13 +1,37 @@
1
+ import { useRef } from "react";
1
2
  import { useEvent } from "@1771technologies/lytenyte-core/internal";
2
3
  import { equal } from "@1771technologies/js-utils";
3
- export function useOnViewChange(source, requestsForView, setRequestsForView) {
4
- const onViewChange = useEvent((bounds) => {
5
- // This will result in the server sending the requests for the current view.
6
- source.rowViewBounds = [bounds.rowCenterStart, bounds.rowCenterEnd];
4
+ export function useOnViewChange(source, requestsForView, setRequestsForView, debounceDuration) {
5
+ const timerRef = useRef(null);
6
+ const pendingBoundsRef = useRef(null);
7
+ // Reads latest requestsForView/setRequestsForView at call time (via useEvent), so the
8
+ // timer always flushes against the current state even if renders occurred while pending.
9
+ const flush = useEvent(() => {
10
+ const bounds = pendingBoundsRef.current;
11
+ if (!bounds)
12
+ return;
13
+ pendingBoundsRef.current = null;
14
+ source.rowViewBounds = bounds;
7
15
  const requests = source.requestsForView();
8
- if (equal(requests, requestsForView))
16
+ if (!equal(requests, requestsForView))
17
+ setRequestsForView(requests);
18
+ });
19
+ const onViewChange = useEvent((bounds) => {
20
+ if (debounceDuration <= 0) {
21
+ source.rowViewBounds = [bounds.rowCenterStart, bounds.rowCenterEnd];
22
+ const requests = source.requestsForView();
23
+ if (!equal(requests, requestsForView))
24
+ setRequestsForView(requests);
9
25
  return;
10
- setRequestsForView(requests);
26
+ }
27
+ // Store the latest bounds so the timer always flushes with the final scroll position, not an intermediate one.
28
+ pendingBoundsRef.current = [bounds.rowCenterStart, bounds.rowCenterEnd];
29
+ if (timerRef.current !== null)
30
+ clearTimeout(timerRef.current);
31
+ timerRef.current = setTimeout(() => {
32
+ timerRef.current = null;
33
+ flush();
34
+ }, debounceDuration);
11
35
  });
12
36
  return onViewChange;
13
37
  }
@@ -34,7 +34,8 @@ export function useServerDataSource(props) {
34
34
  const rowParents = useRowParents(source);
35
35
  const rowsBetween = useRowsBetween(source);
36
36
  const rowChildren = useRowChildren(source);
37
- const onViewChange = useOnViewChange(source, state.requestsForView, state.setRequestsForView);
37
+ // TODO @Lee expose a scroll debounce prop.
38
+ const onViewChange = useOnViewChange(source, state.requestsForView, state.setRequestsForView, 200);
38
39
  const idSpec = useEvent((id) => {
39
40
  const node = source.tree.rowIdToNode.get(id);
40
41
  if (!node || node.kind === "leaf")
package/dist/index.d.ts CHANGED
@@ -32,6 +32,7 @@ export declare namespace Grid {
32
32
  type RowGroupColumn<Spec extends GridSpec = GridSpec> = Required<Root.Props<Spec>>["rowGroupColumn"];
33
33
  type Events<Spec extends GridSpec = GridSpec> = Required<Root.Props<Spec>>["events"];
34
34
  type Style = LnTypes.GridStyle;
35
+ type Annotation<Spec extends GridSpec = GridSpec> = LnTypes.Annotation<Spec>;
35
36
  namespace Components {
36
37
  type Header = GridCore.Components.Header;
37
38
  type HeaderRow = GridCore.Components.HeaderRow;
@@ -2,12 +2,15 @@ import type * as Core from "@1771technologies/lytenyte-core/types";
2
2
  import { type PropsWithChildren, type ReactNode } from "react";
3
3
  import type { RowSource } from "@1771technologies/lytenyte-shared";
4
4
  import type { Grid } from "@1771technologies/lytenyte-core";
5
+ import type { Annotation } from "../components/annotations/types.js";
5
6
  export declare const Root: <Spec extends Root.GridSpec = Root.GridSpec>(props: PropsWithChildren<Root.Props<Spec> & (undefined extends Spec["api"] ? object : {
6
7
  apiExtension: Spec["api"];
7
8
  })>) => ReactNode;
8
9
  export declare namespace Root {
9
10
  type GridSpec<T = unknown, C extends Record<string, any> = object, S extends RowSource<T> = RowSource, E extends Record<string, any> = object> = Grid.GridSpec<T, C, S, E>;
10
- type Props<Spec extends GridSpec = GridSpec> = Grid.Props<Spec>;
11
+ type Props<Spec extends GridSpec = GridSpec> = Grid.Props<Spec> & {
12
+ readonly annotations?: Annotation<Spec>[];
13
+ };
11
14
  type API<Spec extends GridSpec = GridSpec> = Core.API<Spec>;
12
15
  type Column<Spec extends GridSpec = GridSpec> = Core.Column<Spec>;
13
16
  }
package/dist/root/root.js CHANGED
@@ -1,8 +1,23 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { Root as RootCore } from "@1771technologies/lytenyte-core/internal";
3
- import { forwardRef, memo, useEffect } from "react";
2
+ import { Root as RootCore, OverlaySlotsProvider } from "@1771technologies/lytenyte-core/internal";
3
+ import { forwardRef, memo, useEffect, useMemo } from "react";
4
4
  import { hasAValidLicense, licenseState } from "../license.js";
5
+ import { AnnotationRowSectionTop } from "../components/annotations/annotation-row-sections.js";
6
+ import { AnnotationRowSectionCenter } from "../components/annotations/annotation-row-sections.js";
7
+ import { AnnotationRowSectionBottom } from "../components/annotations/annotation-row-sections.js";
8
+ import { AnnotationHeaderOverlay } from "../components/annotations/annotation-header-overlay.js";
5
9
  const RootWrapper = (props, forwarded) => {
10
+ const { annotations, ...rest } = props;
11
+ const overlaySlots = useMemo(() => {
12
+ if (!annotations || annotations.length === 0)
13
+ return {};
14
+ return {
15
+ rowsTop: _jsx(AnnotationRowSectionTop, { annotations: annotations }),
16
+ rowsCenter: _jsx(AnnotationRowSectionCenter, { annotations: annotations }),
17
+ rowsBottom: _jsx(AnnotationRowSectionBottom, { annotations: annotations }),
18
+ header: _jsx(AnnotationHeaderOverlay, { annotations: annotations }),
19
+ };
20
+ }, [annotations]);
6
21
  useEffect(() => {
7
22
  if (hasAValidLicense)
8
23
  return;
@@ -29,6 +44,6 @@ const RootWrapper = (props, forwarded) => {
29
44
  document.body.appendChild(invalidLicenseWatermark);
30
45
  return () => invalidLicenseWatermark.remove();
31
46
  }, []);
32
- return _jsx(RootCore, { ref: forwarded, ...props });
47
+ return (_jsx(OverlaySlotsProvider, { value: overlaySlots, children: _jsx(RootCore, { ref: forwarded, ...rest }) }));
33
48
  };
34
49
  export const Root = memo(forwardRef(RootWrapper));
package/dist/types.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export type * from "@1771technologies/lytenyte-core/types";
2
2
  export type { HavingFilterFn, PivotField, PivotModel, LabelFilter, } from "./data-source-client/use-client-data-source.js";
3
3
  export type { PivotState } from "./data-source-client/hooks/use-pivot/use-pivot-columns.js";
4
+ export type { Annotation, AnnotationAnchor, AnnotationAnchorRange, AnnotationAnchorPoint, AnnotationAnchorHeader, } from "./components/annotations/types.js";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@1771technologies/lytenyte-pro",
3
3
  "description": "Blazingly fast headless React data grid with 100s of features.",
4
- "version": "2.1.3",
4
+ "version": "2.2.1",
5
5
  "type": "module",
6
6
  "license": "COMMERCIAL",
7
7
  "files": [
@@ -85,14 +85,14 @@
85
85
  "access": "public"
86
86
  },
87
87
  "dependencies": {
88
- "@1771technologies/js-utils": "2.1.3",
89
- "@1771technologies/lytenyte-core": "2.1.3",
90
- "@1771technologies/lytenyte-design": "2.1.3",
91
- "@1771technologies/dom-utils": "2.1.3",
92
- "@1771technologies/lytenyte-shared": "2.1.3"
88
+ "@1771technologies/lytenyte-shared": "2.2.1",
89
+ "@1771technologies/dom-utils": "2.2.1",
90
+ "@1771technologies/lytenyte-core": "2.2.1",
91
+ "@1771technologies/js-utils": "2.2.1",
92
+ "@1771technologies/lytenyte-design": "2.2.1"
93
93
  },
94
94
  "devDependencies": {
95
- "@1771technologies/grid-sample-data": "2.1.3"
95
+ "@1771technologies/grid-sample-data": "2.2.1"
96
96
  },
97
97
  "peerDependencies": {
98
98
  "react": "18.0.0 || ^19.0.0",