@sprucelabs/spruce-heartwood-utils 38.17.2 → 38.17.4
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 +200 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,6 +4,16 @@ Tools for building Spruce skills that integrate with Heartwood — remote card l
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @sprucelabs/spruce-heartwood-utils
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
This package is designed for use inside a Spruce skill. It expects `@sprucelabs/heartwood-view-controllers`, `@sprucelabs/mercury-client`, and React to already be present in your project — they are pulled in transitively when you scaffold a Heartwood skill.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
7
17
|
## Table of Contents
|
|
8
18
|
|
|
9
19
|
1. [Remote View Controllers](#remote-view-controllers)
|
|
@@ -269,8 +279,15 @@ plugin.disableAutoLogout() // cancel a pending auto-logout
|
|
|
269
279
|
Use this type when you want to reference the plugin without coupling to the concrete class — for example, when declaring a field that holds either the real plugin or a spy:
|
|
270
280
|
|
|
271
281
|
```ts
|
|
272
|
-
import { AutoLogoutViewPlugin } from '@sprucelabs/spruce-heartwood-utils'
|
|
282
|
+
import type { AutoLogoutViewPlugin } from '@sprucelabs/spruce-heartwood-utils'
|
|
283
|
+
|
|
284
|
+
// Field declaration — works with both AutoLogoutPlugin and SpyAutoLogoutPlugin:
|
|
285
|
+
private autoLogoutPlugin: AutoLogoutViewPlugin
|
|
286
|
+
```
|
|
273
287
|
|
|
288
|
+
The interface shape:
|
|
289
|
+
|
|
290
|
+
```ts
|
|
274
291
|
interface AutoLogoutViewPlugin extends ViewControllerPlugin {
|
|
275
292
|
enableAutoLogout(durationSec: number): void
|
|
276
293
|
disableAutoLogout(): void
|
|
@@ -397,9 +414,17 @@ import { fakeGetViews } from '@sprucelabs/spruce-heartwood-utils'
|
|
|
397
414
|
|
|
398
415
|
// Stub two remote views:
|
|
399
416
|
await fakeGetViews.fakeGetViews(['your-skill.card-a', 'your-skill.card-b'])
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
The optional second argument is the name of a method to instrument on each stub VC. When provided, each stub controller gets a function at that name that records whether it was called and what arguments it received:
|
|
400
420
|
|
|
401
|
-
|
|
421
|
+
```ts
|
|
402
422
|
await fakeGetViews.fakeGetViews(['your-skill.card'], 'load')
|
|
423
|
+
|
|
424
|
+
// After your code triggers the stub:
|
|
425
|
+
const stub = /* get rendered card controller from your vc */
|
|
426
|
+
assert.isTrue(stub.wasHit)
|
|
427
|
+
assert.isEqualDeep(stub.passedParams, [expectedArgs])
|
|
403
428
|
```
|
|
404
429
|
|
|
405
430
|
> **Note:** The export is the `heartwoodEventFaker` object. Call it as `fakeGetViews.fakeGetViews(...)`.
|
|
@@ -565,6 +590,8 @@ import {
|
|
|
565
590
|
Sizer, DelayedPlacer,
|
|
566
591
|
// Utilities
|
|
567
592
|
sizeUtil, Settings,
|
|
593
|
+
// Emitter — pass to Sizer and DelayedPlacer to coordinate layout events
|
|
594
|
+
SimpleEmitter,
|
|
568
595
|
// Types
|
|
569
596
|
AnimationEmitter,
|
|
570
597
|
SizerProps,
|
|
@@ -662,7 +689,17 @@ Pass the **same emitter instance** to `Sizer` and `DelayedPlacer` when they are
|
|
|
662
689
|
|
|
663
690
|
### AnimationEmitter
|
|
664
691
|
|
|
665
|
-
The interface `Sizer` and `DelayedPlacer` use to communicate layout-change events.
|
|
692
|
+
The interface `Sizer` and `DelayedPlacer` use to communicate layout-change events. Pass an instance as the `emitter` prop to coordinate components. The package exports `SimpleEmitter` — a lightweight in-memory implementation you can use directly:
|
|
693
|
+
|
|
694
|
+
```ts
|
|
695
|
+
import { SimpleEmitter } from '@sprucelabs/spruce-heartwood-utils'
|
|
696
|
+
|
|
697
|
+
const emitter = new SimpleEmitter()
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
`SimpleEmitter` implements all three methods (`on`, `off`, `emit`) with a plain listener map. Use it for standalone components, isolated tests, or any context where a full Heartwood emitter is not available.
|
|
701
|
+
|
|
702
|
+
The `AnimationEmitter` interface, for reference:
|
|
666
703
|
|
|
667
704
|
```ts
|
|
668
705
|
interface AnimationEmitter {
|
|
@@ -672,6 +709,32 @@ interface AnimationEmitter {
|
|
|
672
709
|
}
|
|
673
710
|
```
|
|
674
711
|
|
|
712
|
+
#### Quick Setup
|
|
713
|
+
|
|
714
|
+
1. **Install**
|
|
715
|
+
|
|
716
|
+
```sh
|
|
717
|
+
npm install @sprucelabs/spruce-heartwood-utils
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
2. **Create an emitter** — one per component tree, stable across renders:
|
|
721
|
+
|
|
722
|
+
```ts
|
|
723
|
+
import { SimpleEmitter } from '@sprucelabs/spruce-heartwood-utils'
|
|
724
|
+
|
|
725
|
+
const emitter = useMemo(() => new SimpleEmitter(), [])
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
3. **Wire to components and emit events** — pass the emitter to `Sizer` and/or `DelayedPlacer`, then signal layout changes:
|
|
729
|
+
|
|
730
|
+
```ts
|
|
731
|
+
// After a React render cycle completes:
|
|
732
|
+
await emitter.emit('did-render')
|
|
733
|
+
|
|
734
|
+
// After a viewport resize:
|
|
735
|
+
await emitter.emit('did-resize')
|
|
736
|
+
```
|
|
737
|
+
|
|
675
738
|
**Events:**
|
|
676
739
|
|
|
677
740
|
| Event | Who listens | Who emits | Meaning |
|
|
@@ -834,6 +897,13 @@ enqueue(() => this.handleReady())
|
|
|
834
897
|
|
|
835
898
|
Cancels a pending hide and queues a show for elements toggled back before their hide ran.
|
|
836
899
|
|
|
900
|
+
```ts
|
|
901
|
+
function clearPendingHideAndQueueShow(
|
|
902
|
+
refOrNode: React.RefObject<HTMLElement> | HTMLElement,
|
|
903
|
+
delay?: number // default: 40ms
|
|
904
|
+
): void
|
|
905
|
+
```
|
|
906
|
+
|
|
837
907
|
> Prefer `showRightAway()` for most toggle cases.
|
|
838
908
|
|
|
839
909
|
---
|
|
@@ -842,6 +912,13 @@ Cancels a pending hide and queues a show for elements toggled back before their
|
|
|
842
912
|
|
|
843
913
|
Cancels a pending show and queues a hide.
|
|
844
914
|
|
|
915
|
+
```ts
|
|
916
|
+
function clearPendingShowAndQueueHide(
|
|
917
|
+
refOrNode: React.RefObject<HTMLElement> | HTMLElement,
|
|
918
|
+
delay?: number // default: 40ms
|
|
919
|
+
): void
|
|
920
|
+
```
|
|
921
|
+
|
|
845
922
|
> Prefer `hideRightAway()` for most toggle cases.
|
|
846
923
|
|
|
847
924
|
---
|
|
@@ -874,18 +951,63 @@ return <div ref={ref} className="your-panel hidden">...</div>
|
|
|
874
951
|
|
|
875
952
|
#### `useShowNow(ref, delay?)` — React Hook
|
|
876
953
|
|
|
877
|
-
Like `useQueueShow` but places itself at the front of the queue on every render.
|
|
954
|
+
Like `useQueueShow` but places itself at the front of the queue on every render. Use for elements that must appear before any other queued items — such as a primary heading that should always be visible before supporting content fades in.
|
|
878
955
|
|
|
879
956
|
```ts
|
|
880
957
|
function useShowNow(ref: React.RefObject<HTMLElement>, delay?: number): void
|
|
881
958
|
```
|
|
882
959
|
|
|
960
|
+
```tsx
|
|
961
|
+
const ref = useRef<HTMLHeadingElement>(null)
|
|
962
|
+
useShowNow(ref)
|
|
963
|
+
|
|
964
|
+
return <h1 ref={ref} className="your-heading hidden">Title</h1>
|
|
965
|
+
```
|
|
966
|
+
|
|
883
967
|
---
|
|
884
968
|
|
|
885
969
|
### Sizer — Animated Height Container
|
|
886
970
|
|
|
887
971
|
Wraps children in a div whose `height` is set via inline style to match the measured height of its content. Listens to `did-render` and `did-resize` events so it resizes whenever layout changes — enabling smooth CSS height transitions on content that would otherwise be `height: auto`. Emits `did-resize-content` so `DelayedPlacer` can re-place after a height change.
|
|
888
972
|
|
|
973
|
+
#### Quick Setup
|
|
974
|
+
|
|
975
|
+
1. **Install and add the required CSS**
|
|
976
|
+
|
|
977
|
+
```sh
|
|
978
|
+
npm install @sprucelabs/spruce-heartwood-utils
|
|
979
|
+
```
|
|
980
|
+
|
|
981
|
+
```css
|
|
982
|
+
.sizer {
|
|
983
|
+
transition: height 500ms ease;
|
|
984
|
+
overflow: hidden;
|
|
985
|
+
}
|
|
986
|
+
```
|
|
987
|
+
|
|
988
|
+
> Inside a Heartwood skill view this CSS is already provided — skip this step.
|
|
989
|
+
|
|
990
|
+
2. **Wrap your content** — `shouldHideOverflow` clips children during the height transition:
|
|
991
|
+
|
|
992
|
+
```tsx
|
|
993
|
+
import { Sizer, SimpleEmitter } from '@sprucelabs/spruce-heartwood-utils'
|
|
994
|
+
|
|
995
|
+
const emitter = useMemo(() => new SimpleEmitter(), [])
|
|
996
|
+
|
|
997
|
+
<Sizer emitter={emitter} shouldHideOverflow>
|
|
998
|
+
<YourContent />
|
|
999
|
+
</Sizer>
|
|
1000
|
+
```
|
|
1001
|
+
|
|
1002
|
+
3. **Signal layout changes** — emit after renders and on viewport resize; Sizer measures content and updates `style.height` automatically:
|
|
1003
|
+
|
|
1004
|
+
```ts
|
|
1005
|
+
await emitter.emit('did-render')
|
|
1006
|
+
await emitter.emit('did-resize')
|
|
1007
|
+
```
|
|
1008
|
+
|
|
1009
|
+
In tests, call `Settings.disableAnimations()` in `beforeEach` so height changes apply synchronously.
|
|
1010
|
+
|
|
889
1011
|
#### Props (`SizerProps`)
|
|
890
1012
|
|
|
891
1013
|
```ts
|
|
@@ -906,6 +1028,11 @@ interface SizerProps {
|
|
|
906
1028
|
// overflow is always visible (adds force-show-overflow class).
|
|
907
1029
|
shouldHideOverflow?: boolean
|
|
908
1030
|
|
|
1031
|
+
// Debounce delay in ms before each resize measurement fires. Default: 250ms.
|
|
1032
|
+
// Lower values make Sizer react faster to content changes; useful when
|
|
1033
|
+
// content updates quickly and you want tighter animation timing.
|
|
1034
|
+
sizerDelayMs?: number
|
|
1035
|
+
|
|
909
1036
|
// Event emitter. Without one, Sizer only resizes on React re-renders,
|
|
910
1037
|
// not in response to external layout events.
|
|
911
1038
|
emitter?: AnimationEmitter
|
|
@@ -916,6 +1043,8 @@ interface SizerProps {
|
|
|
916
1043
|
|
|
917
1044
|
```ts
|
|
918
1045
|
sizer.current?.resize(): boolean // measure and apply new height; returns true if it changed
|
|
1046
|
+
sizer.current?.showOverflow(): void // temporarily allow overflow (e.g. while a dropdown inside is open)
|
|
1047
|
+
sizer.current?.hideOverflow(): void // re-apply overflow: hidden
|
|
919
1048
|
```
|
|
920
1049
|
|
|
921
1050
|
#### Emitter Events
|
|
@@ -973,6 +1102,52 @@ Positions a child element to match the location of its in-flow placeholder. Wrap
|
|
|
973
1102
|
|
|
974
1103
|
When `isEnabled={false}`, children render inline with no wrapper.
|
|
975
1104
|
|
|
1105
|
+
#### Quick Setup
|
|
1106
|
+
|
|
1107
|
+
1. **Install and add the required CSS**
|
|
1108
|
+
|
|
1109
|
+
```sh
|
|
1110
|
+
npm install @sprucelabs/spruce-heartwood-utils
|
|
1111
|
+
```
|
|
1112
|
+
|
|
1113
|
+
```css
|
|
1114
|
+
.placer { position: relative; }
|
|
1115
|
+
.placer > * { position: absolute; }
|
|
1116
|
+
```
|
|
1117
|
+
|
|
1118
|
+
> Inside a Heartwood skill view this CSS is already provided — skip this step.
|
|
1119
|
+
|
|
1120
|
+
2. **Wrap your content** — `isEnabled`, `className`, and `isFocused` are all required:
|
|
1121
|
+
|
|
1122
|
+
```tsx
|
|
1123
|
+
import { DelayedPlacer, SimpleEmitter } from '@sprucelabs/spruce-heartwood-utils'
|
|
1124
|
+
|
|
1125
|
+
const emitter = useMemo(() => new SimpleEmitter(), [])
|
|
1126
|
+
|
|
1127
|
+
<DelayedPlacer
|
|
1128
|
+
className="placer__card"
|
|
1129
|
+
isEnabled={true}
|
|
1130
|
+
emitter={emitter}
|
|
1131
|
+
isFocused={() => true}
|
|
1132
|
+
>
|
|
1133
|
+
<YourCard />
|
|
1134
|
+
</DelayedPlacer>
|
|
1135
|
+
```
|
|
1136
|
+
|
|
1137
|
+
Pass `() => true` for `isFocused` when using outside Heartwood — placement is skipped when this returns false.
|
|
1138
|
+
|
|
1139
|
+
3. **Share the emitter with `Sizer`** when they are siblings so height changes automatically trigger re-placement:
|
|
1140
|
+
|
|
1141
|
+
```tsx
|
|
1142
|
+
<DelayedPlacer className="placer__card" isEnabled emitter={emitter} isFocused={isFocused}>
|
|
1143
|
+
<Sizer emitter={emitter} shouldHideOverflow>
|
|
1144
|
+
<YourContent />
|
|
1145
|
+
</Sizer>
|
|
1146
|
+
</DelayedPlacer>
|
|
1147
|
+
```
|
|
1148
|
+
|
|
1149
|
+
`DelayedPlacer` emits `did-place-cards` when placement is complete. Listen for it if you need to react after cards settle.
|
|
1150
|
+
|
|
976
1151
|
#### Props (`DelayedPlacerProps`)
|
|
977
1152
|
|
|
978
1153
|
```ts
|
|
@@ -1014,28 +1189,34 @@ delayedPlacer.current?.placeRightAway(): void // re-measure and re-place immedi
|
|
|
1014
1189
|
#### Usage
|
|
1015
1190
|
|
|
1016
1191
|
```tsx
|
|
1017
|
-
|
|
1192
|
+
import React, { useRef, useMemo } from 'react'
|
|
1193
|
+
import { DelayedPlacer, SimpleEmitter } from '@sprucelabs/spruce-heartwood-utils'
|
|
1018
1194
|
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
}
|
|
1195
|
+
function YourPanel({ isFocused }: { isFocused: () => boolean }) {
|
|
1196
|
+
const placerRef = useRef<React.ElementRef<typeof DelayedPlacer>>(null)
|
|
1197
|
+
const emitter = useMemo(() => new SimpleEmitter(), [])
|
|
1023
1198
|
|
|
1024
|
-
render() {
|
|
1025
1199
|
return (
|
|
1026
1200
|
<DelayedPlacer
|
|
1027
|
-
className="
|
|
1028
|
-
isEnabled={
|
|
1029
|
-
emitter={
|
|
1201
|
+
className="placer__panel"
|
|
1202
|
+
isEnabled={true}
|
|
1203
|
+
emitter={emitter}
|
|
1030
1204
|
ref={placerRef}
|
|
1031
|
-
isFocused={
|
|
1205
|
+
isFocused={isFocused}
|
|
1032
1206
|
>
|
|
1033
|
-
{
|
|
1207
|
+
{yourContent}
|
|
1034
1208
|
</DelayedPlacer>
|
|
1035
1209
|
)
|
|
1036
1210
|
}
|
|
1037
1211
|
```
|
|
1038
1212
|
|
|
1213
|
+
When you need to re-place after a content change that happens outside React state:
|
|
1214
|
+
|
|
1215
|
+
```ts
|
|
1216
|
+
// Trigger placement immediately, bypassing the debounce:
|
|
1217
|
+
placerRef.current?.placeRightAway()
|
|
1218
|
+
```
|
|
1219
|
+
|
|
1039
1220
|
> `isEnabled`, `className`, and `isFocused` are all required. The child must be `position: absolute` in CSS for placement to have visual effect.
|
|
1040
1221
|
|
|
1041
1222
|
---
|
|
@@ -1098,6 +1279,7 @@ sizeUtil.bodyWidth = () => 1200
|
|
|
1098
1279
|
A complete example showing all three systems working together in a card component:
|
|
1099
1280
|
|
|
1100
1281
|
```tsx
|
|
1282
|
+
import React, { useMemo } from 'react'
|
|
1101
1283
|
import {
|
|
1102
1284
|
Sizer, DelayedPlacer, queueShow, Settings, SimpleEmitter,
|
|
1103
1285
|
} from '@sprucelabs/spruce-heartwood-utils'
|
|
@@ -1139,6 +1321,9 @@ queueShow removes .hidden from each element with a 40ms stagger
|
|
|
1139
1321
|
**Test setup for animation components:**
|
|
1140
1322
|
|
|
1141
1323
|
```ts
|
|
1324
|
+
import { Settings, stopQueue } from '@sprucelabs/spruce-heartwood-utils'
|
|
1325
|
+
import { skillViewState } from '@sprucelabs/spruce-heartwood-utils/build/components/skillViews/skillViewState'
|
|
1326
|
+
|
|
1142
1327
|
protected async beforeEach() {
|
|
1143
1328
|
Settings.disableAnimations()
|
|
1144
1329
|
skillViewState.isFullScreen = false
|