@sprucelabs/spruce-heartwood-utils 38.16.2 → 38.17.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.
- package/README.md +3 -32
- package/build/components/animation/SimpleEmitter.d.ts +7 -0
- package/build/components/animation/SimpleEmitter.js +22 -0
- package/build/components/animation/index.d.ts +1 -0
- package/build/components/animation/index.js +3 -1
- package/build/esm/components/animation/SimpleEmitter.d.ts +7 -0
- package/build/esm/components/animation/SimpleEmitter.js +21 -0
- package/build/esm/components/animation/index.d.ts +1 -0
- package/build/esm/components/animation/index.js +1 -0
- package/build/esm/index-module.d.ts +2 -0
- package/build/esm/index-module.js +1 -0
- package/build/index-module.d.ts +2 -0
- package/build/index-module.js +3 -1
- package/docs/animation/README.md +64 -106
- package/docs/animation/animation-emitter.md +7 -22
- package/docs/animation/sizer.md +5 -1
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -659,28 +659,6 @@ interface AnimationEmitter {
|
|
|
659
659
|
| `did-change-orientation` | DelayedPlacer | your code | Portrait ↔ landscape switch |
|
|
660
660
|
| `did-place-cards` | your listeners | DelayedPlacer | Placement calculation finished |
|
|
661
661
|
|
|
662
|
-
**Minimal implementation:**
|
|
663
|
-
|
|
664
|
-
```ts
|
|
665
|
-
class SimpleEmitter implements AnimationEmitter {
|
|
666
|
-
private listeners: Record<string, Array<() => void>> = {}
|
|
667
|
-
|
|
668
|
-
on(event: string, handler: () => void): void {
|
|
669
|
-
;(this.listeners[event] ??= []).push(handler)
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
off(event: string, handler: () => void): void {
|
|
673
|
-
this.listeners[event] = (this.listeners[event] ?? []).filter(
|
|
674
|
-
(h) => h !== handler
|
|
675
|
-
)
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
emit(event: string): void {
|
|
679
|
-
for (const h of this.listeners[event] ?? []) h()
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
```
|
|
683
|
-
|
|
684
662
|
> Pass the **same function reference** to `on` and `off`. Inline arrow functions cannot be removed with `off`.
|
|
685
663
|
|
|
686
664
|
---
|
|
@@ -928,6 +906,8 @@ sizer.current?.resize(): boolean // measure and apply new height; returns tru
|
|
|
928
906
|
#### Usage
|
|
929
907
|
|
|
930
908
|
```tsx
|
|
909
|
+
import { Sizer, SimpleEmitter } from '@sprucelabs/spruce-heartwood-utils'
|
|
910
|
+
|
|
931
911
|
// Simplest — just clip overflow during height transition:
|
|
932
912
|
<Sizer shouldHideOverflow>
|
|
933
913
|
{isExpanded && <YourExpandableContent />}
|
|
@@ -1096,18 +1076,9 @@ A complete example showing all three systems working together in a card componen
|
|
|
1096
1076
|
|
|
1097
1077
|
```tsx
|
|
1098
1078
|
import {
|
|
1099
|
-
Sizer, DelayedPlacer, queueShow, Settings,
|
|
1079
|
+
Sizer, DelayedPlacer, queueShow, Settings, SimpleEmitter,
|
|
1100
1080
|
} from '@sprucelabs/spruce-heartwood-utils'
|
|
1101
1081
|
|
|
1102
|
-
class SimpleEmitter implements AnimationEmitter {
|
|
1103
|
-
private listeners: Record<string, Array<() => void>> = {}
|
|
1104
|
-
on(event: string, handler: () => void) { (this.listeners[event] ??= []).push(handler) }
|
|
1105
|
-
off(event: string, handler: () => void) {
|
|
1106
|
-
this.listeners[event] = (this.listeners[event] ?? []).filter(h => h !== handler)
|
|
1107
|
-
}
|
|
1108
|
-
emit(event: string) { for (const h of this.listeners[event] ?? []) h() }
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
1082
|
function YourCard({ isPlaced, isFocused }: { isPlaced: boolean; isFocused: () => boolean }) {
|
|
1112
1083
|
const emitter = useMemo(() => new SimpleEmitter(), [])
|
|
1113
1084
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
class SimpleEmitter {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.listeners = {};
|
|
6
|
+
}
|
|
7
|
+
on(event, handler) {
|
|
8
|
+
var _a;
|
|
9
|
+
;
|
|
10
|
+
((_a = this.listeners)[event] ?? (_a[event] = [])).push(handler);
|
|
11
|
+
}
|
|
12
|
+
off(event, handler) {
|
|
13
|
+
this.listeners[event] = (this.listeners[event] ?? []).filter((h) => h !== handler);
|
|
14
|
+
}
|
|
15
|
+
emit(event) {
|
|
16
|
+
for (const h of this.listeners[event] ?? []) {
|
|
17
|
+
h();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
exports.default = SimpleEmitter;
|
|
22
|
+
//# sourceMappingURL=SimpleEmitter.js.map
|
|
@@ -6,3 +6,4 @@ export type { DelayedPlacerProps } from './DelayedPlacerExport';
|
|
|
6
6
|
export { default as sizeUtil } from '../calendars/utils/size.utility';
|
|
7
7
|
export { default as Settings } from '../Settings';
|
|
8
8
|
export type { AnimationEmitter } from './types';
|
|
9
|
+
export { default as SimpleEmitter } from './SimpleEmitter';
|
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.Settings = exports.sizeUtil = exports.DelayedPlacer = exports.Sizer = exports.stopQueue = exports.clearPendingShowAndQueueHide = exports.clearPendingHideAndQueueShow = exports.callbackImmediately = exports.queueCallback = exports.queueHide = exports.queueShow = exports.hideRightAway = exports.showRightAway = exports.useShowNow = exports.useQueueShow = void 0;
|
|
6
|
+
exports.SimpleEmitter = exports.Settings = exports.sizeUtil = exports.DelayedPlacer = exports.Sizer = exports.stopQueue = exports.clearPendingShowAndQueueHide = exports.clearPendingHideAndQueueShow = exports.callbackImmediately = exports.queueCallback = exports.queueHide = exports.queueShow = exports.hideRightAway = exports.showRightAway = exports.useShowNow = exports.useQueueShow = void 0;
|
|
7
7
|
// Queue system — all functions and hooks
|
|
8
8
|
var queueShow_1 = require("../../hooks/queueShow");
|
|
9
9
|
Object.defineProperty(exports, "useQueueShow", { enumerable: true, get: function () { return queueShow_1.useQueueShow; } });
|
|
@@ -27,4 +27,6 @@ var size_utility_1 = require("../calendars/utils/size.utility");
|
|
|
27
27
|
Object.defineProperty(exports, "sizeUtil", { enumerable: true, get: function () { return __importDefault(size_utility_1).default; } });
|
|
28
28
|
var Settings_1 = require("../Settings");
|
|
29
29
|
Object.defineProperty(exports, "Settings", { enumerable: true, get: function () { return __importDefault(Settings_1).default; } });
|
|
30
|
+
var SimpleEmitter_1 = require("./SimpleEmitter");
|
|
31
|
+
Object.defineProperty(exports, "SimpleEmitter", { enumerable: true, get: function () { return __importDefault(SimpleEmitter_1).default; } });
|
|
30
32
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export default class SimpleEmitter {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.listeners = {};
|
|
4
|
+
}
|
|
5
|
+
on(event, handler) {
|
|
6
|
+
var _a;
|
|
7
|
+
var _b;
|
|
8
|
+
;
|
|
9
|
+
((_a = (_b = this.listeners)[event]) !== null && _a !== void 0 ? _a : (_b[event] = [])).push(handler);
|
|
10
|
+
}
|
|
11
|
+
off(event, handler) {
|
|
12
|
+
var _a;
|
|
13
|
+
this.listeners[event] = ((_a = this.listeners[event]) !== null && _a !== void 0 ? _a : []).filter((h) => h !== handler);
|
|
14
|
+
}
|
|
15
|
+
emit(event) {
|
|
16
|
+
var _a;
|
|
17
|
+
for (const h of (_a = this.listeners[event]) !== null && _a !== void 0 ? _a : []) {
|
|
18
|
+
h();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -6,3 +6,4 @@ export type { DelayedPlacerProps } from './DelayedPlacerExport';
|
|
|
6
6
|
export { default as sizeUtil } from '../calendars/utils/size.utility';
|
|
7
7
|
export { default as Settings } from '../Settings';
|
|
8
8
|
export type { AnimationEmitter } from './types';
|
|
9
|
+
export { default as SimpleEmitter } from './SimpleEmitter';
|
|
@@ -6,3 +6,4 @@ export { default as DelayedPlacer } from './DelayedPlacerExport.js';
|
|
|
6
6
|
// Utilities (useful for consumers who want to measure DOM nodes)
|
|
7
7
|
export { default as sizeUtil } from '../calendars/utils/size.utility.js';
|
|
8
8
|
export { default as Settings } from '../Settings.js';
|
|
9
|
+
export { default as SimpleEmitter } from './SimpleEmitter.js';
|
|
@@ -13,3 +13,5 @@ export { default as AutoLogoutPlugin } from './plugins/AutoLogoutViewPlugin';
|
|
|
13
13
|
export type { AutoLogoutViewPlugin } from './plugins/AutoLogoutViewPlugin';
|
|
14
14
|
export { default as SpyAutoLogoutPlugin } from './plugins/SpyAutoLogoutViewPlugin';
|
|
15
15
|
export * from './components/animation';
|
|
16
|
+
export { default as SkillViewEmitter } from './app/SkillViewEmitter';
|
|
17
|
+
export type { SkillViewEventContract, SkillViewEvents, SkillViewEmitPayloads, SkillViewEmitter as SkillViewEmitterType, } from './app/SkillViewEmitter';
|
|
@@ -9,3 +9,4 @@ export { default as loadActiveThemeForOrg } from './theming/loadActiveThemeForOr
|
|
|
9
9
|
export { default as AutoLogoutPlugin } from './plugins/AutoLogoutViewPlugin.js';
|
|
10
10
|
export { default as SpyAutoLogoutPlugin } from './plugins/SpyAutoLogoutViewPlugin.js';
|
|
11
11
|
export * from './components/animation/index.js';
|
|
12
|
+
export { default as SkillViewEmitter } from './app/SkillViewEmitter.js';
|
package/build/index-module.d.ts
CHANGED
|
@@ -13,3 +13,5 @@ export { default as AutoLogoutPlugin } from './plugins/AutoLogoutViewPlugin';
|
|
|
13
13
|
export type { AutoLogoutViewPlugin } from './plugins/AutoLogoutViewPlugin';
|
|
14
14
|
export { default as SpyAutoLogoutPlugin } from './plugins/SpyAutoLogoutViewPlugin';
|
|
15
15
|
export * from './components/animation';
|
|
16
|
+
export { default as SkillViewEmitter } from './app/SkillViewEmitter';
|
|
17
|
+
export type { SkillViewEventContract, SkillViewEvents, SkillViewEmitPayloads, SkillViewEmitter as SkillViewEmitterType, } from './app/SkillViewEmitter';
|
package/build/index-module.js
CHANGED
|
@@ -17,7 +17,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
17
17
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
18
18
|
};
|
|
19
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
-
exports.SpyAutoLogoutPlugin = exports.AutoLogoutPlugin = exports.loadActiveThemeForOrg = exports.MockRemoteViewControllerFactory = exports.fakeGetViews = exports.remoteVcAssert = exports.CardRegistrar = exports.RemoteViewControllerFactoryImpl = void 0;
|
|
20
|
+
exports.SkillViewEmitter = exports.SpyAutoLogoutPlugin = exports.AutoLogoutPlugin = exports.loadActiveThemeForOrg = exports.MockRemoteViewControllerFactory = exports.fakeGetViews = exports.remoteVcAssert = exports.CardRegistrar = exports.RemoteViewControllerFactoryImpl = void 0;
|
|
21
21
|
var RemoteViewControllerFactory_1 = require("./views/RemoteViewControllerFactory");
|
|
22
22
|
Object.defineProperty(exports, "RemoteViewControllerFactoryImpl", { enumerable: true, get: function () { return __importDefault(RemoteViewControllerFactory_1).default; } });
|
|
23
23
|
__exportStar(require("./views/RemoteViewControllerFactory"), exports);
|
|
@@ -37,4 +37,6 @@ Object.defineProperty(exports, "AutoLogoutPlugin", { enumerable: true, get: func
|
|
|
37
37
|
var SpyAutoLogoutViewPlugin_1 = require("./plugins/SpyAutoLogoutViewPlugin");
|
|
38
38
|
Object.defineProperty(exports, "SpyAutoLogoutPlugin", { enumerable: true, get: function () { return __importDefault(SpyAutoLogoutViewPlugin_1).default; } });
|
|
39
39
|
__exportStar(require("./components/animation"), exports);
|
|
40
|
+
var SkillViewEmitter_1 = require("./app/SkillViewEmitter");
|
|
41
|
+
Object.defineProperty(exports, "SkillViewEmitter", { enumerable: true, get: function () { return __importDefault(SkillViewEmitter_1).default; } });
|
|
40
42
|
//# sourceMappingURL=index-module.js.map
|
package/docs/animation/README.md
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# Animation Utilities — Complete Reference
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
These utilities let third-party consumers use Heartwood's animation and layout primitives without depending on internal Spruce types (`AppEngine`, `SkillViewEmitter`).
|
|
3
|
+
These utilities let third-party consumers use Heartwood's animation and layout primitives. Both `SkillViewEmitter` and `SimpleEmitter` are exported from the package — no need to build your own event bus.
|
|
6
4
|
|
|
7
5
|
---
|
|
8
6
|
|
|
@@ -34,6 +32,8 @@ import {
|
|
|
34
32
|
stopQueue,
|
|
35
33
|
// Components
|
|
36
34
|
Sizer, DelayedPlacer,
|
|
35
|
+
// Emitters — pick one (see AnimationEmitter section)
|
|
36
|
+
SkillViewEmitter, SimpleEmitter,
|
|
37
37
|
// Utilities
|
|
38
38
|
sizeUtil, Settings,
|
|
39
39
|
// Types
|
|
@@ -45,7 +45,37 @@ import {
|
|
|
45
45
|
|
|
46
46
|
## Required CSS
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
You must include the rules below in your stylesheet. They are **not bundled with the package** — they come from Heartwood's `public/stylesheets/main.css`, which is not shipped as part of this module.
|
|
49
|
+
|
|
50
|
+
### Copy-paste starter block
|
|
51
|
+
|
|
52
|
+
```css
|
|
53
|
+
/* Hidden state — required for queueShow / showRightAway / hideRightAway */
|
|
54
|
+
.hidden {
|
|
55
|
+
opacity: 0 !important;
|
|
56
|
+
transform: translate(0, 10px);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* Transition on your animated elements — required for smooth animation */
|
|
60
|
+
.my-animated-element {
|
|
61
|
+
transition: opacity 0.5s ease, transform 0.5s ease;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* Sizer height transitions — required for the Sizer component */
|
|
65
|
+
.sizer {
|
|
66
|
+
transition: all 0.5s;
|
|
67
|
+
}
|
|
68
|
+
body.portrait .sizer {
|
|
69
|
+
transition: all 1s;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* Absolute positioning for DelayedPlacer children — required */
|
|
73
|
+
.being_placed {
|
|
74
|
+
position: absolute;
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Replace `.my-animated-element` with your own class name(s). The remaining rules must appear verbatim.
|
|
49
79
|
|
|
50
80
|
---
|
|
51
81
|
|
|
@@ -53,13 +83,12 @@ import {
|
|
|
53
83
|
|
|
54
84
|
Every element you pass to `queueShow`, `showRightAway`, or `hideRightAway` must start with `class="... hidden"`. The queue works by toggling this class on and off.
|
|
55
85
|
|
|
56
|
-
**
|
|
86
|
+
**Rule from Heartwood (`public/stylesheets/main.css`):**
|
|
57
87
|
|
|
58
88
|
```css
|
|
59
89
|
.hidden {
|
|
60
90
|
opacity: 0 !important;
|
|
61
91
|
transform: translate(0, 10px);
|
|
62
|
-
transition: all 0.5s
|
|
63
92
|
}
|
|
64
93
|
```
|
|
65
94
|
|
|
@@ -274,35 +303,6 @@ The `.message.hide` state is separate:
|
|
|
274
303
|
|
|
275
304
|
---
|
|
276
305
|
|
|
277
|
-
### Minimal CSS for standalone use
|
|
278
|
-
|
|
279
|
-
If you are using these utilities completely outside of a Heartwood skill view, here is the minimum CSS needed:
|
|
280
|
-
|
|
281
|
-
```css
|
|
282
|
-
/* 1. Hidden state — required for queueShow to work */
|
|
283
|
-
.hidden {
|
|
284
|
-
opacity: 0 !important;
|
|
285
|
-
transform: translate(0, 10px);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/* 2. Transition on your elements — required for animation (not just snap) */
|
|
289
|
-
.my-animated-element {
|
|
290
|
-
transition: opacity 0.5s ease, transform 0.5s ease;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/* 3. Sizer height transitions — required for Sizer component */
|
|
294
|
-
.sizer {
|
|
295
|
-
transition: all 0.5s;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/* 4. Absolute positioning for DelayedPlacer children — required */
|
|
299
|
-
.being_placed {
|
|
300
|
-
position: absolute;
|
|
301
|
-
}
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
---
|
|
305
|
-
|
|
306
306
|
## System Architecture
|
|
307
307
|
|
|
308
308
|
Three independent systems coordinate through a shared `AnimationEmitter`:
|
|
@@ -355,28 +355,28 @@ interface AnimationEmitter {
|
|
|
355
355
|
| `did-change-orientation` | your app | DelayedPlacer | Portrait ↔ landscape switch |
|
|
356
356
|
| `did-place-cards` | DelayedPlacer | your app (optional) | Placement complete |
|
|
357
357
|
|
|
358
|
-
###
|
|
358
|
+
### Getting an emitter instance
|
|
359
|
+
|
|
360
|
+
Two emitters are exported from the package — choose based on your needs:
|
|
361
|
+
|
|
362
|
+
**`SkillViewEmitter`** — full-featured mercury-style emitter, recommended when integrating with a Heartwood skill view:
|
|
359
363
|
|
|
360
364
|
```ts
|
|
361
|
-
|
|
362
|
-
private listeners: Record<string, Array<() => void>> = {}
|
|
365
|
+
import { SkillViewEmitter } from '@sprucelabs/spruce-heartwood-utils'
|
|
363
366
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
}
|
|
367
|
+
const emitter = SkillViewEmitter.getInstance()
|
|
368
|
+
```
|
|
367
369
|
|
|
368
|
-
|
|
369
|
-
this.listeners[event] = (this.listeners[event] ?? []).filter(h => h !== handler)
|
|
370
|
-
}
|
|
370
|
+
**`SimpleEmitter`** — lightweight in-memory emitter, suitable for standalone components, isolated tests, or any context that doesn't need the full mercury stack:
|
|
371
371
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
}
|
|
375
|
-
}
|
|
372
|
+
```ts
|
|
373
|
+
import { SimpleEmitter } from '@sprucelabs/spruce-heartwood-utils'
|
|
376
374
|
|
|
377
375
|
const emitter = new SimpleEmitter()
|
|
378
376
|
```
|
|
379
377
|
|
|
378
|
+
Either way, use a single instance shared across all components that need to coordinate (Sizer, DelayedPlacer, your resize/render wiring).
|
|
379
|
+
|
|
380
380
|
### Wiring the emitter to window resize
|
|
381
381
|
|
|
382
382
|
```ts
|
|
@@ -416,7 +416,7 @@ emitter.off('did-resize', () => this.doSomething()) // does nothing
|
|
|
416
416
|
```tsx
|
|
417
417
|
// They MUST share an emitter instance to coordinate.
|
|
418
418
|
// Sizer emits 'did-resize-content' → DelayedPlacer re-places.
|
|
419
|
-
const
|
|
419
|
+
const emitter = SkillViewEmitter.getInstance()
|
|
420
420
|
|
|
421
421
|
<Sizer emitter={emitter}>
|
|
422
422
|
<DelayedPlacer emitter={emitter} isEnabled={true} className="placer__card" isFocused={() => true}>
|
|
@@ -437,15 +437,13 @@ Controls whether animations run and what their duration is. **Always call `Setti
|
|
|
437
437
|
Settings.disableAnimations() // sets animationDuration to 0, queue runs synchronously
|
|
438
438
|
Settings.getIsAnimationEnabled() // → boolean
|
|
439
439
|
Settings.animationDuration // → 0 | 500 | 1000 (landscape / portrait)
|
|
440
|
-
Settings.shouldExportSwiper // → boolean (reads process.env.EXPORT_SWIPER)
|
|
441
440
|
```
|
|
442
441
|
|
|
443
442
|
### Disabling animations in tests
|
|
444
443
|
|
|
445
|
-
|
|
444
|
+
Call this in your test setup. Without it, tests rely on real timers and become flaky.
|
|
446
445
|
|
|
447
446
|
```ts
|
|
448
|
-
// AbstractHeartwoodTest.ts — base class for all Heartwood tests
|
|
449
447
|
protected async beforeEach() {
|
|
450
448
|
await super.beforeEach()
|
|
451
449
|
Settings.disableAnimations()
|
|
@@ -458,22 +456,6 @@ protected async afterEach() {
|
|
|
458
456
|
}
|
|
459
457
|
```
|
|
460
458
|
|
|
461
|
-
```ts
|
|
462
|
-
// ButtonGroup.test.tsx — standalone test file
|
|
463
|
-
protected async beforeEach() {
|
|
464
|
-
await super.beforeEach()
|
|
465
|
-
Settings.disableAnimations()
|
|
466
|
-
this.engine = AppEngine.getInstance()
|
|
467
|
-
await this.engine.reset()
|
|
468
|
-
// ...
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
protected async afterEach() {
|
|
472
|
-
await super.afterEach()
|
|
473
|
-
stopQueue()
|
|
474
|
-
}
|
|
475
|
-
```
|
|
476
|
-
|
|
477
459
|
### Effect on the queue
|
|
478
460
|
|
|
479
461
|
```ts
|
|
@@ -507,6 +489,8 @@ queueShow(someRef) // fires instantly, no setTimeout
|
|
|
507
489
|
|
|
508
490
|
A singleton FIFO queue. All consumers share one queue. Items are dequeued every 40ms (default) by toggling the `hidden` CSS class on DOM elements.
|
|
509
491
|
|
|
492
|
+
> **Elements need a CSS `transition` to animate.** The queue only toggles the `hidden` class — the browser's CSS engine drives the visual animation. Without a `transition` on the element, show and hide operations still work but snap instantly with no fade or movement. See the [Required CSS](#required-css) section for the rules to include.
|
|
493
|
+
|
|
510
494
|
### When to use which function
|
|
511
495
|
|
|
512
496
|
| Situation | Use |
|
|
@@ -1055,15 +1039,6 @@ hideRightAway(this.panelRef)
|
|
|
1055
1039
|
Clears the interval and marks the queue as stopped. Does **not** drain the queue — pending items are abandoned. Always call this in test teardown.
|
|
1056
1040
|
|
|
1057
1041
|
```ts
|
|
1058
|
-
// AbstractHeartwoodTest.ts — every Heartwood test calls this in afterEach
|
|
1059
|
-
protected async afterEach() {
|
|
1060
|
-
await super.afterEach()
|
|
1061
|
-
await this.components.afterEach()
|
|
1062
|
-
await AppEngine.reset()
|
|
1063
|
-
stopQueue()
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
// ButtonGroup.test.tsx — standalone test teardown
|
|
1067
1042
|
protected async afterEach() {
|
|
1068
1043
|
await super.afterEach()
|
|
1069
1044
|
stopQueue()
|
|
@@ -1282,7 +1257,7 @@ async function handleSizeChange(props: any) {
|
|
|
1282
1257
|
|
|
1283
1258
|
```tsx
|
|
1284
1259
|
// When the parent tells Sizer the layout changed, Sizer re-measures:
|
|
1285
|
-
const
|
|
1260
|
+
const emitter = SkillViewEmitter.getInstance()
|
|
1286
1261
|
|
|
1287
1262
|
useEffect(() => {
|
|
1288
1263
|
// After every React render, notify Sizer to re-check its height
|
|
@@ -1393,39 +1368,32 @@ placeRightAway()
|
|
|
1393
1368
|
6. setTimeout(() => emitter.emit('did-place-cards'), 100)
|
|
1394
1369
|
```
|
|
1395
1370
|
|
|
1396
|
-
### Pattern 1 —
|
|
1397
|
-
|
|
1398
|
-
The only usage in Heartwood. `isDelayedPlacerEnabled` comes from the card's view controller.
|
|
1371
|
+
### Pattern 1 — Component with optional absolute positioning
|
|
1399
1372
|
|
|
1400
1373
|
```tsx
|
|
1401
|
-
// Card.tsx — wraps every card; isEnabled defaults to false
|
|
1402
1374
|
private delayedPlacerRef = React.createRef<React.ElementRef<typeof DelayedPlacer>>()
|
|
1375
|
+
private emitter = SkillViewEmitter.getInstance()
|
|
1403
1376
|
|
|
1404
1377
|
private handleRender() {
|
|
1405
|
-
this.setRenderingClass(true)
|
|
1406
|
-
|
|
1407
1378
|
clearTimeout(this.renderTimeout)
|
|
1408
1379
|
this.renderTimeout = setTimeout(() => {
|
|
1409
1380
|
// Force re-placement after content changes
|
|
1410
1381
|
this.delayedPlacerRef.current?.placeRightAway()
|
|
1411
1382
|
this.props.onRender?.()
|
|
1412
|
-
this.renderingClassTimeout = setTimeout(() => {
|
|
1413
|
-
this.setRenderingClass(false)
|
|
1414
|
-
this.setupClassNames()
|
|
1415
|
-
}, Settings.animationDuration)
|
|
1416
1383
|
}, 50)
|
|
1417
1384
|
}
|
|
1418
1385
|
|
|
1419
1386
|
render() {
|
|
1387
|
+
const { isFloating } = this.props
|
|
1420
1388
|
return (
|
|
1421
1389
|
<DelayedPlacer
|
|
1422
1390
|
className="placer__card"
|
|
1423
|
-
isEnabled={
|
|
1424
|
-
emitter={this.emitter}
|
|
1391
|
+
isEnabled={isFloating ?? false}
|
|
1392
|
+
emitter={this.emitter}
|
|
1425
1393
|
ref={this.delayedPlacerRef}
|
|
1426
|
-
isFocused={this.props.
|
|
1394
|
+
isFocused={this.props.isFocused ?? (() => true)}
|
|
1427
1395
|
>
|
|
1428
|
-
{
|
|
1396
|
+
{this.props.children}
|
|
1429
1397
|
</DelayedPlacer>
|
|
1430
1398
|
)
|
|
1431
1399
|
}
|
|
@@ -1594,11 +1562,8 @@ private sizeMyselfToCorrectHeight() {
|
|
|
1594
1562
|
Since `sizeUtil` is a plain object, methods can be replaced directly:
|
|
1595
1563
|
|
|
1596
1564
|
```ts
|
|
1597
|
-
//
|
|
1565
|
+
// Stub viewport width for layout-dependent logic:
|
|
1598
1566
|
sizeUtil.bodyWidth = () => 1200
|
|
1599
|
-
this.setApp(AppRenderingToolBelt)
|
|
1600
|
-
await this.pushSvcWithNoToolBeltDeclared()
|
|
1601
|
-
this.assertRenderingTool(activeApp.toolId)
|
|
1602
1567
|
```
|
|
1603
1568
|
|
|
1604
1569
|
Restore after test if needed (or rely on test module isolation).
|
|
@@ -1670,21 +1635,14 @@ DelayedPlacer.sizeEverything() fires
|
|
|
1670
1635
|
### Setting up all three systems together
|
|
1671
1636
|
|
|
1672
1637
|
```tsx
|
|
1673
|
-
import React, { useEffect, useRef
|
|
1638
|
+
import React, { useEffect, useRef } from 'react'
|
|
1674
1639
|
import {
|
|
1675
|
-
Sizer, DelayedPlacer,
|
|
1640
|
+
Sizer, DelayedPlacer, SkillViewEmitter,
|
|
1676
1641
|
queueShow, stopQueue, Settings
|
|
1677
1642
|
} from '@sprucelabs/spruce-heartwood-utils'
|
|
1678
1643
|
|
|
1679
|
-
class SimpleEmitter implements AnimationEmitter {
|
|
1680
|
-
private listeners: Record<string, Array<() => void>> = {}
|
|
1681
|
-
on(event: string, h: () => void) { (this.listeners[event] ??= []).push(h) }
|
|
1682
|
-
off(event: string, h: () => void) { this.listeners[event] = (this.listeners[event] ?? []).filter(x => x !== h) }
|
|
1683
|
-
emit(event: string) { for (const h of this.listeners[event] ?? []) h() }
|
|
1684
|
-
}
|
|
1685
|
-
|
|
1686
1644
|
function MyPanel({ isFloating }: { isFloating: boolean }) {
|
|
1687
|
-
const
|
|
1645
|
+
const emitter = SkillViewEmitter.getInstance()
|
|
1688
1646
|
const delayedPlacerRef = useRef<any>(null)
|
|
1689
1647
|
|
|
1690
1648
|
// Wire React renders → emitter
|
|
@@ -29,33 +29,18 @@ All three methods accept a plain string event name and return either `void` or `
|
|
|
29
29
|
| `did-change-orientation` | DelayedPlacer | Device orientation handler | Portrait ↔ landscape switch |
|
|
30
30
|
| `did-place-cards` | (external listeners) | DelayedPlacer | Placement calculation complete |
|
|
31
31
|
|
|
32
|
-
##
|
|
32
|
+
## Using SimpleEmitter
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
`SimpleEmitter` is exported from the package — import it directly instead of building your own:
|
|
35
35
|
|
|
36
36
|
```ts
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
on(event: string, handler: () => void): void {
|
|
42
|
-
;(this.listeners[event] ??= []).push(handler)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
off(event: string, handler: () => void): void {
|
|
46
|
-
this.listeners[event] = (this.listeners[event] ?? []).filter(
|
|
47
|
-
(h) => h !== handler
|
|
48
|
-
)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
emit(event: string): void {
|
|
52
|
-
for (const h of this.listeners[event] ?? []) {
|
|
53
|
-
h()
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
37
|
+
import { SimpleEmitter } from '@sprucelabs/spruce-heartwood-utils'
|
|
38
|
+
|
|
39
|
+
const emitter = new SimpleEmitter()
|
|
57
40
|
```
|
|
58
41
|
|
|
42
|
+
It implements `AnimationEmitter` with a plain in-memory listener map. Use it for standalone components, isolated tests, or any context where the full `SkillViewEmitter` is unnecessary.
|
|
43
|
+
|
|
59
44
|
## Noop Emitter
|
|
60
45
|
|
|
61
46
|
`SizerExport.tsx` defines and uses a noop emitter as the default when no emitter is passed:
|
package/docs/animation/sizer.md
CHANGED
|
@@ -104,7 +104,9 @@ handleSizing() (debounced at sizerDelayMs, default 250ms)
|
|
|
104
104
|
### With emitter — responds to external layout events
|
|
105
105
|
|
|
106
106
|
```tsx
|
|
107
|
-
|
|
107
|
+
import { SimpleEmitter } from '@sprucelabs/spruce-heartwood-utils'
|
|
108
|
+
|
|
109
|
+
const [emitter] = useState(() => new SimpleEmitter())
|
|
108
110
|
|
|
109
111
|
<Sizer emitter={emitter} shouldStartAtZero>
|
|
110
112
|
<MyContent />
|
|
@@ -114,6 +116,8 @@ const [emitter] = useState(() => createMyEmitter())
|
|
|
114
116
|
### With ref — manually drive resize on content change
|
|
115
117
|
|
|
116
118
|
```tsx
|
|
119
|
+
import { SimpleEmitter } from '@sprucelabs/spruce-heartwood-utils'
|
|
120
|
+
|
|
117
121
|
const emitter = useMemo(() => new SimpleEmitter(), [])
|
|
118
122
|
const sizer = useRef<React.ElementRef<typeof Sizer>>(null)
|
|
119
123
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sprucelabs/spruce-heartwood-utils",
|
|
3
3
|
"description": "Heartwood Utilities",
|
|
4
|
-
"version": "38.
|
|
4
|
+
"version": "38.17.1",
|
|
5
5
|
"skill": {
|
|
6
6
|
"namespace": "heartwood"
|
|
7
7
|
},
|
|
@@ -86,6 +86,10 @@
|
|
|
86
86
|
"build/components/animation/types.d.ts",
|
|
87
87
|
"build/esm/components/animation/types.js",
|
|
88
88
|
"build/esm/components/animation/types.d.ts",
|
|
89
|
+
"build/components/animation/SimpleEmitter.js",
|
|
90
|
+
"build/components/animation/SimpleEmitter.d.ts",
|
|
91
|
+
"build/esm/components/animation/SimpleEmitter.js",
|
|
92
|
+
"build/esm/components/animation/SimpleEmitter.d.ts",
|
|
89
93
|
"build/hooks/queueShow.js",
|
|
90
94
|
"build/hooks/queueShow.d.ts",
|
|
91
95
|
"build/esm/hooks/queueShow.js",
|