forty-cdk 0.0.4 → 0.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.
@@ -1,23 +1,23 @@
1
- # forty-cdk/internationalized-date
2
-
3
- Secondary entry point holding the [`@internationalized/date`](https://react-spectrum.adobe.com/internationalized/date/) implementations of the `DateAdapter` contract:
4
-
5
- - `InternationalizedDateAdapter` / `provideInternationalizedDateAdapter()` — day-granular `CalendarDate`.
6
- - `InternationalizedDateTimeAdapter` / `provideInternationalizedDateTimeAdapter()` — time-capable `CalendarDateTime`.
7
-
8
- It exists so the main `forty-cdk` bundle never references `@internationalized/date`: the package is an **optional peer dependency**, required only by consumers who import this entry point. The date/time primitives themselves (`Calendar`, `DateField`, `DatePicker`, `TimeField`) depend only on the abstract `DateAdapter` from the main entry point — `provideNativeDateAdapter()` works with zero extra installs.
9
-
10
- ```bash
11
- npm install @internationalized/date
12
- ```
13
-
14
- ```ts
15
- import { bootstrapApplication } from '@angular/platform-browser';
16
- import { provideInternationalizedDateAdapter } from 'forty-cdk/internationalized-date';
17
-
18
- bootstrapApplication(App, {
19
- providers: [provideInternationalizedDateAdapter()],
20
- });
21
- ```
22
-
23
- See the [Calendar README](../src/lib/calendar/README.md) for the full adapter table and usage examples.
1
+ # forty-cdk/internationalized-date
2
+
3
+ Secondary entry point holding the [`@internationalized/date`](https://react-spectrum.adobe.com/internationalized/date/) implementations of the `DateAdapter` contract:
4
+
5
+ - `InternationalizedDateAdapter` / `provideInternationalizedDateAdapter()` — day-granular `CalendarDate`.
6
+ - `InternationalizedDateTimeAdapter` / `provideInternationalizedDateTimeAdapter()` — time-capable `CalendarDateTime`.
7
+
8
+ It exists so the main `forty-cdk` bundle never references `@internationalized/date`: the package is an **optional peer dependency**, required only by consumers who import this entry point. The date/time primitives themselves (`Calendar`, `DateField`, `DatePicker`, `TimeField`) depend only on the abstract `DateAdapter` from the main entry point — `provideNativeDateAdapter()` works with zero extra installs.
9
+
10
+ ```bash
11
+ npm install @internationalized/date
12
+ ```
13
+
14
+ ```ts
15
+ import { bootstrapApplication } from '@angular/platform-browser';
16
+ import { provideInternationalizedDateAdapter } from 'forty-cdk/internationalized-date';
17
+
18
+ bootstrapApplication(App, {
19
+ providers: [provideInternationalizedDateAdapter()],
20
+ });
21
+ ```
22
+
23
+ See the [Calendar README](../src/lib/calendar/README.md) for the full adapter table and usage examples.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forty-cdk",
3
- "version": "0.0.4",
3
+ "version": "0.2.0",
4
4
  "description": "Headless / styleless UI primitives for Angular with WAI-ARIA accessibility built in.",
5
5
  "keywords": [
6
6
  "angular",
@@ -60,6 +60,10 @@
60
60
  "./internationalized-date": {
61
61
  "types": "./types/forty-cdk-internationalized-date.d.ts",
62
62
  "default": "./fesm2022/forty-cdk-internationalized-date.mjs"
63
+ },
64
+ "./virtualization": {
65
+ "types": "./types/forty-cdk-virtualization.d.ts",
66
+ "default": "./fesm2022/forty-cdk-virtualization.mjs"
63
67
  }
64
68
  },
65
69
  "type": "module"
@@ -5,8 +5,8 @@ import { DateAdapter } from 'forty-cdk';
5
5
 
6
6
  /**
7
7
  * {@link DateAdapter} over `@internationalized/date`'s immutable `CalendarDate`.
8
- * This is the recommended adapter: it is the same date primitive React Aria and
9
- * Ark UI build on, it works in every browser today with no polyfill, and its
8
+ * This is the recommended adapter: it is a widely-used immutable date primitive,
9
+ * it works in every browser today with no polyfill, and its
10
10
  * reference-equality-on-mutation makes it signal-friendly.
11
11
  *
12
12
  * **Gregorian only.** `createDate` always builds a Gregorian `CalendarDate`, so
@@ -0,0 +1,305 @@
1
+ import * as _angular_core from '@angular/core';
2
+ import { Signal, InjectionToken, OnInit } from '@angular/core';
3
+
4
+ /**
5
+ * Configuration for {@link injectVirtualizer}. The consumer owns the data and
6
+ * the DOM; the virtualizer only computes which slice of the data is visible.
7
+ */
8
+ interface VirtualizerOptions {
9
+ /** Reactive total number of items in the list. */
10
+ readonly count: Signal<number>;
11
+ /**
12
+ * Estimated size, in CSS pixels, of the item at `index` along the scroll
13
+ * axis (height when vertical, width when horizontal). Used until an item is
14
+ * measured. Should be a stable function reference.
15
+ */
16
+ readonly estimateSize: (index: number) => number;
17
+ /** Reactive reference to the scroll container element (e.g. a `viewChild`). */
18
+ readonly scrollElement: Signal<HTMLElement | null>;
19
+ /** Scroll axis. Defaults to `'vertical'`. */
20
+ readonly orientation?: 'vertical' | 'horizontal';
21
+ /**
22
+ * Number of items to render beyond the visible window on each side, to
23
+ * reduce blank flashes while scrolling. Defaults to `5`.
24
+ */
25
+ readonly overscan?: number;
26
+ /** Stable key for the item at `index`. Defaults to the index itself. */
27
+ readonly getItemKey?: (index: number) => string | number;
28
+ }
29
+ /** A single item in the currently rendered window. */
30
+ interface VirtualItem {
31
+ /** Index of the item in the full list. */
32
+ readonly index: number;
33
+ /** Stable key (from `getItemKey`, or the index). */
34
+ readonly key: string | number;
35
+ /** Offset of the item from the start of the scroll container, in pixels. */
36
+ readonly start: number;
37
+ /** Size of the item along the scroll axis, in pixels. */
38
+ readonly size: number;
39
+ }
40
+ /** Reactive handle returned by {@link injectVirtualizer}. */
41
+ interface ForVirtualizer {
42
+ /** The items in the currently visible window plus overscan. */
43
+ readonly virtualItems: Signal<readonly VirtualItem[]>;
44
+ /** Total scroll size of all items, in pixels (drives the spacer element). */
45
+ readonly totalSize: Signal<number>;
46
+ /**
47
+ * The inclusive-exclusive `[firstIndex, lastIndex + 1)` index window currently
48
+ * rendered (visible window plus overscan), or `[0, 0]` when nothing is rendered.
49
+ * Plugs straight into a list primitive's `[visibleRange]`-style input (e.g.
50
+ * `[forCombobox][visibleRange]`) so windowing composes without the consumer
51
+ * re-deriving the range from {@link ForVirtualizer.virtualItems}.
52
+ */
53
+ readonly range: Signal<readonly [number, number]>;
54
+ /** Scroll the container so the item at `index` is in view. */
55
+ scrollToIndex(index: number, options?: {
56
+ align?: 'start' | 'center' | 'end' | 'auto';
57
+ }): void;
58
+ /** Scroll the container to an absolute pixel offset. */
59
+ scrollToOffset(offset: number): void;
60
+ /** Record the measured size of a rendered item element (dynamic sizes). */
61
+ measureElement(element: HTMLElement): void;
62
+ }
63
+ /**
64
+ * Headless windowing core: given a reactive item count, a size estimator and a
65
+ * scroll container, returns the slice of items currently visible (plus
66
+ * overscan), the total scroll size, and imperative scroll/measure helpers. The
67
+ * consumer renders the items with their own `@for` and applies the position
68
+ * transform — this primitive owns no DOM.
69
+ *
70
+ * Backed by `@tanstack/virtual-core`. SSR-safe: off-browser it returns an empty
71
+ * window and the estimate-based total without touching `document`/`window`; the
72
+ * first real window is produced after the first browser render.
73
+ *
74
+ * Must be called from an injection context (a component/directive constructor
75
+ * or field initializer).
76
+ *
77
+ * @param options Reactive count, size estimator, scroll element and tuning.
78
+ * @returns A {@link ForVirtualizer} handle of signals + imperative methods.
79
+ */
80
+ declare function injectVirtualizer(options: VirtualizerOptions): ForVirtualizer;
81
+
82
+ /**
83
+ * Configuration for {@link injectInfiniteScroll}. The consumer owns the data and
84
+ * the fetch; this core only decides *when* to ask for more.
85
+ */
86
+ interface InfiniteScrollOptions {
87
+ /**
88
+ * The rendered window, `[firstIndex, lastIndex + 1)` — e.g.
89
+ * `injectVirtualizer(...).range`. An empty `[0, 0]` window never fires.
90
+ */
91
+ readonly range: Signal<readonly [number, number]>;
92
+ /** Reactive total number of currently-loaded items. */
93
+ readonly count: Signal<number>;
94
+ /**
95
+ * Fire when the window's last index comes within this many items of `count`.
96
+ * Defaults to `5` (mirrors the windowing core's default overscan).
97
+ */
98
+ readonly threshold?: number;
99
+ /** When this resolves to `true` the detector never fires. */
100
+ readonly disabled?: Signal<boolean>;
101
+ /**
102
+ * Called once per threshold crossing. If it returns a promise, the next fire
103
+ * is suppressed until that promise settles (`pending` reflects the in-flight
104
+ * state); the detector re-arms when `count` grows.
105
+ */
106
+ readonly onLoadMore: () => void | Promise<unknown>;
107
+ }
108
+ /** Reactive handle returned by {@link injectInfiniteScroll}. */
109
+ interface ForInfiniteScroll {
110
+ /** True while an `onLoadMore` promise is in flight. */
111
+ readonly pending: Signal<boolean>;
112
+ }
113
+ /**
114
+ * Headless infinite-scroll detector: composes on top of any windowed list's
115
+ * `range` + `count` signals and fires `onLoadMore` once per threshold crossing,
116
+ * suppressing re-fire while a returned promise is pending and re-arming when
117
+ * `count` grows (a page was appended). It owns no DOM, adds no scroll listener —
118
+ * the trigger rides the existing reactive recompute — and is SSR-safe by
119
+ * construction: off-browser the window is `[0, 0]`, so it never fires.
120
+ *
121
+ * Must be called from an injection context (a component/directive constructor
122
+ * or field initializer).
123
+ *
124
+ * @param options Reactive `range` + `count`, an optional `threshold`/`disabled`,
125
+ * and the `onLoadMore` callback.
126
+ * @returns A {@link ForInfiniteScroll} handle exposing the `pending` signal.
127
+ */
128
+ declare function injectInfiniteScroll(options: InfiniteScrollOptions): ForInfiniteScroll;
129
+
130
+ /**
131
+ * Coordination surface a {@link ForVirtualViewport} exposes to the
132
+ * `*forVirtualFor` structural directive nested inside it.
133
+ */
134
+ interface ForVirtualViewportContext {
135
+ /** The items in the currently visible window plus overscan. */
136
+ readonly virtualItems: Signal<readonly VirtualItem[]>;
137
+ /** The total number of items in the full (non-windowed) list. */
138
+ readonly count: Signal<number>;
139
+ /** Scroll axis, resolved once when the viewport initializes. */
140
+ readonly orientation: Signal<'vertical' | 'horizontal'>;
141
+ }
142
+ /** DI token carrying the {@link ForVirtualViewportContext}. */
143
+ declare const FOR_VIRTUAL_VIEWPORT_CONTEXT: InjectionToken<ForVirtualViewportContext>;
144
+
145
+ /**
146
+ * Scroll viewport for the ergonomic virtualization layer. Decorate a fixed-size
147
+ * scroll container with `[forVirtualViewport]`, give it `[virtualCount]` and an
148
+ * `[estimateSize]`, and nest a single `*forVirtualFor` inside it — the viewport
149
+ * owns the scroll container, the total-size sizer, and the windowing core, so
150
+ * the consumer writes no manual spacer or position transform.
151
+ *
152
+ * Built on the headless {@link injectVirtualizer} core; for full manual control
153
+ * (custom DOM, dynamic measurement, window/document scroller) use that directly.
154
+ *
155
+ * The viewport forces `overflow: auto` on its host and renders a relatively
156
+ * positioned sizer whose main-axis size tracks `totalSize()`; `*forVirtualFor`
157
+ * projects its rows into that sizer and positions each one absolutely.
158
+ *
159
+ * `orientation` and `overscan` are read once when the viewport initializes;
160
+ * change them before first render, not at runtime.
161
+ */
162
+ declare class ForVirtualViewport implements ForVirtualViewportContext, OnInit {
163
+ #private;
164
+ /** Total number of items in the full list. */
165
+ readonly virtualCount: _angular_core.InputSignal<number>;
166
+ /** Estimated item size in px along the scroll axis: a number or a per-index estimator. */
167
+ readonly estimateSize: _angular_core.InputSignal<number | ((index: number) => number)>;
168
+ /** Scroll axis. Resolved once on init; runtime changes are not tracked by the core. */
169
+ readonly orientation: _angular_core.InputSignal<"vertical" | "horizontal">;
170
+ /** Items rendered beyond the visible window on each side. Resolved once on init. */
171
+ readonly overscan: _angular_core.InputSignal<number>;
172
+ /** Stable key for the item at `index`. Defaults to the index. */
173
+ readonly getItemKey: _angular_core.InputSignal<((index: number) => string | number) | undefined>;
174
+ /**
175
+ * Emits when the rendered window comes within ~`overscan` items of the end of
176
+ * the list, signalling the consumer to load the next page. Built on
177
+ * {@link injectInfiniteScroll}; fires once per threshold crossing and re-arms
178
+ * when the bound count grows. The consumer owns the fetch (e.g. via `resource()`).
179
+ */
180
+ readonly endReached: _angular_core.OutputEmitterRef<void>;
181
+ /**
182
+ * The total number of items in the full (non-windowed) list — the
183
+ * {@link ForVirtualViewportContext.count} the nested `*forVirtualFor` reads.
184
+ * Aliases the `virtualCount` input signal directly (no wrapper node).
185
+ */
186
+ readonly count: _angular_core.InputSignal<number>;
187
+ /** The items in the currently visible window plus overscan. */
188
+ readonly virtualItems: Signal<readonly VirtualItem[]>;
189
+ /** Total scroll size of all items, in pixels (drives the sizer). */
190
+ readonly totalSize: Signal<number>;
191
+ protected readonly sizerWidth: Signal<string>;
192
+ protected readonly sizerHeight: Signal<string>;
193
+ ngOnInit(): void;
194
+ /** Scroll the container so the item at `index` is in view. No-op until initialized. */
195
+ scrollToIndex(index: number, options?: {
196
+ align?: 'start' | 'center' | 'end' | 'auto';
197
+ }): void;
198
+ /** Scroll the container to an absolute pixel offset. No-op until initialized. */
199
+ scrollToOffset(offset: number): void;
200
+ /** Record the measured size of a rendered item element. No-op until initialized. */
201
+ measureElement(element: HTMLElement): void;
202
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<ForVirtualViewport, never>;
203
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<ForVirtualViewport, "for-virtual-viewport, [forVirtualViewport]", ["forVirtualViewport"], { "virtualCount": { "alias": "virtualCount"; "required": true; "isSignal": true; }; "estimateSize": { "alias": "estimateSize"; "required": false; "isSignal": true; }; "orientation": { "alias": "orientation"; "required": false; "isSignal": true; }; "overscan": { "alias": "overscan"; "required": false; "isSignal": true; }; "getItemKey": { "alias": "getItemKey"; "required": false; "isSignal": true; }; }, { "endReached": "endReached"; }, never, ["*"], true, never>;
204
+ }
205
+
206
+ /** Template context exposed to each row rendered by `*forVirtualFor`. */
207
+ interface ForVirtualForContext<T> {
208
+ /** The row data at this index (`let row`). */
209
+ $implicit: T;
210
+ /** The virtual item metadata: index, key, start offset, size (`let item = virtualItem`). */
211
+ virtualItem: VirtualItem;
212
+ /** The item's index in the full list (`let i = index`). */
213
+ index: number;
214
+ /** The total number of items in the full list (`let n = count`). */
215
+ count: number;
216
+ }
217
+ /**
218
+ * Structural directive that renders only the visible window of a list inside a
219
+ * `[forVirtualViewport]`. Pass the full data array; the directive iterates the
220
+ * viewport's `virtualItems()`, exposes each row plus its `virtualItem` to the
221
+ * template, positions it absolutely (so the consumer writes no transform), and
222
+ * binds `aria-setsize` (true total) / `aria-posinset` (`index + 1`) so screen
223
+ * readers announce the real list size.
224
+ *
225
+ * ```html
226
+ * <div forVirtualViewport [virtualCount]="rows().length" [estimateSize]="44">
227
+ * <div *forVirtualFor="let row of rows(); let item = virtualItem">{{ row.label }}</div>
228
+ * </div>
229
+ * ```
230
+ */
231
+ declare class ForVirtualFor<T> {
232
+ #private;
233
+ /** The full list of items to virtualize. */
234
+ readonly forVirtualForOf: _angular_core.InputSignal<readonly T[]>;
235
+ constructor();
236
+ /** Narrows the template context type for `let row of …` strict template checking. */
237
+ static ngTemplateContextGuard<T>(_directive: ForVirtualFor<T>, _context: unknown): _context is ForVirtualForContext<T>;
238
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<ForVirtualFor<any>, never>;
239
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<ForVirtualFor<any>, "[forVirtualFor][forVirtualForOf]", never, { "forVirtualForOf": { "alias": "forVirtualForOf"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>;
240
+ }
241
+
242
+ /**
243
+ * Opt-in row-virtualization companion for `[forTable]` in `<div role>` grid mode. Place it on the
244
+ * same element as `[forTable]`; it builds the windowing core from the table's `[rowCount]` and
245
+ * exposes the visible window for the consumer to render with their own `@for` + position transform.
246
+ *
247
+ * Tree-shakeable: `ForTable` never imports the virtualization core — only consumers that import
248
+ * `ForTableVirtualized` bundle `@tanstack/virtual-core`.
249
+ *
250
+ * The focused row is kept mounted even when scrolled out of the window so the roving-focused
251
+ * `gridcell` is never unmounted. SSR-safe: off-browser the window is empty and
252
+ * `totalSize` is the estimate-based total.
253
+ *
254
+ * Also drives cross-window keyboard navigation: when an Arrow / Page / Ctrl+Home / Ctrl+End grid
255
+ * action resolves a row outside the rendered window, it scrolls that row into view and moves roving
256
+ * focus onto the target cell once it mounts (preserving the current column). `ForTable` stays
257
+ * unaware of virtualization — it delegates the row-crossing move through the table context.
258
+ */
259
+ declare class ForTableVirtualized {
260
+ #private;
261
+ /** Estimated row size in px along the scroll axis (the fixed-size fast path). Read for the size estimate. */
262
+ readonly estimateRowSize: _angular_core.InputSignalWithTransform<number, unknown>;
263
+ /**
264
+ * Scroll container. Defaults to the table root element (the scroll container in `<div>` grid
265
+ * mode). Bind it explicitly when the scroll container is an **ancestor** of the table — e.g. an
266
+ * app-shell viewport that scrolls projected content — since the table cannot inject an ancestor
267
+ * it does not own. A design-system wrapper can re-expose or rename this input through
268
+ * `hostDirectives` input aliasing (`inputs: ['scrollElement: scrollContainer']`) with no bridging
269
+ * effect required.
270
+ */
271
+ readonly scrollElement: _angular_core.InputSignal<HTMLElement | null>;
272
+ constructor();
273
+ /**
274
+ * The rows in the visible window plus overscan, augmented to always include the focused row and
275
+ * the row being reordered even when they are scrolled out of view (so the roving-focused cell
276
+ * stays mounted, and a pointer reorder drag never unmounts the lifted row). Render these with
277
+ * `@for (vrow of v.virtualRows(); track vrow.index)` and position each row absolutely with
278
+ * `transform: translateY(vrow.start + 'px')`. Bind each row's `[virtualIndex]="vrow.index"`.
279
+ */
280
+ readonly virtualRows: _angular_core.Signal<readonly VirtualItem[]>;
281
+ /** Total scroll size of all rows in px. Bind to the body container's height to size the scroll range. */
282
+ readonly totalSize: _angular_core.Signal<number>;
283
+ /**
284
+ * The rendered window as the inclusive-exclusive `[firstIndex, lastIndex + 1)` index range,
285
+ * sourced from the underlying virtualizer (the true window) — not from {@link virtualRows},
286
+ * which is augmented with the focused / reordering rows. So a row retained out of the window
287
+ * never widens this range. Plugs straight into `injectInfiniteScroll({ range, count, onLoadMore })`.
288
+ */
289
+ readonly range: _angular_core.Signal<readonly [number, number]>;
290
+ /**
291
+ * Scroll the container so the row at `index` is in view. Cross-window keyboard
292
+ * navigation calls this internally; consumers may also call it to scroll
293
+ * programmatically.
294
+ */
295
+ scrollToRow(index: number, options?: {
296
+ align?: 'start' | 'center' | 'end' | 'auto';
297
+ }): void;
298
+ /** Record the measured size of a rendered row element (dynamic / measured row heights). */
299
+ measureRow(element: HTMLElement): void;
300
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<ForTableVirtualized, never>;
301
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<ForTableVirtualized, "[forTableVirtualized]", ["forTableVirtualized"], { "estimateRowSize": { "alias": "estimateRowSize"; "required": false; "isSignal": true; }; "scrollElement": { "alias": "scrollElement"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
302
+ }
303
+
304
+ export { FOR_VIRTUAL_VIEWPORT_CONTEXT, ForTableVirtualized, ForVirtualFor, ForVirtualViewport, injectInfiniteScroll, injectVirtualizer };
305
+ export type { ForInfiniteScroll, ForVirtualForContext, ForVirtualViewportContext, ForVirtualizer, InfiniteScrollOptions, VirtualItem, VirtualizerOptions };