@sprucelabs/spruce-heartwood-utils 38.17.1 → 38.17.3
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 +118 -20
- 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,
|
|
@@ -576,26 +603,43 @@ import {
|
|
|
576
603
|
|
|
577
604
|
### Required CSS
|
|
578
605
|
|
|
606
|
+
> **⚠ Silent failure warning:** If CSS transitions are missing, animations will not play and no error will be thrown. Elements will simply snap instantly between states with no visual feedback. Every class listed below requires its own `transition` declaration — the system provides none.
|
|
607
|
+
|
|
579
608
|
**Inside a Heartwood skill view:** all required CSS is already provided by Heartwood's stylesheet. No extra setup needed.
|
|
580
609
|
|
|
581
|
-
**Outside Heartwood (standalone use):** supply
|
|
610
|
+
**Outside Heartwood (standalone use):** supply every rule below yourself. Missing any one of them causes that animation to silently skip.
|
|
582
611
|
|
|
583
|
-
|
|
612
|
+
---
|
|
613
|
+
|
|
614
|
+
#### `queueShow` / `queueHide` — fade + slide transitions
|
|
615
|
+
|
|
616
|
+
`queueShow` removes the `hidden` class; `queueHide` adds it back. The `hidden` class sets the hidden state (opacity, position offset, pointer-events). **The transition must be declared on the element itself, not on `.hidden`** — if the transition is on `.hidden` it is removed at the same moment the class is removed and the browser never interpolates.
|
|
584
617
|
|
|
585
618
|
```css
|
|
619
|
+
/* Hidden state — sets opacity, nudge, and pointer-events */
|
|
586
620
|
.hidden {
|
|
587
621
|
opacity: 0;
|
|
588
622
|
transform: translateY(4px);
|
|
589
623
|
pointer-events: none;
|
|
590
624
|
}
|
|
591
625
|
|
|
592
|
-
/* Transition
|
|
626
|
+
/* Transition on the element — this is what the browser animates */
|
|
593
627
|
.your-element {
|
|
594
628
|
transition: opacity 200ms ease, transform 200ms ease;
|
|
595
629
|
}
|
|
596
630
|
```
|
|
597
631
|
|
|
598
|
-
`
|
|
632
|
+
Elements must also start with `hidden` in their `className` so they are invisible until `queueShow` fires:
|
|
633
|
+
|
|
634
|
+
```tsx
|
|
635
|
+
<div className="your-element hidden" ref={(ref) => { ref && queueShow(ref) }} />
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
---
|
|
639
|
+
|
|
640
|
+
#### `Sizer` — animated height
|
|
641
|
+
|
|
642
|
+
`Sizer` measures its content and writes `style.height` directly. The CSS `transition` on `.sizer` is what makes the height change animate smoothly. Without it the height jumps instantly.
|
|
599
643
|
|
|
600
644
|
```css
|
|
601
645
|
.sizer {
|
|
@@ -604,7 +648,11 @@ The `queueShow` system toggles a `hidden` CSS class. Elements must start with `h
|
|
|
604
648
|
}
|
|
605
649
|
```
|
|
606
650
|
|
|
607
|
-
|
|
651
|
+
---
|
|
652
|
+
|
|
653
|
+
#### `DelayedPlacer` — absolute positioning
|
|
654
|
+
|
|
655
|
+
`DelayedPlacer` writes `style.left` / `style.top` on the child. The `.placer` wrapper must be `position: relative` so the child's absolute coordinates are relative to it.
|
|
608
656
|
|
|
609
657
|
```css
|
|
610
658
|
.placer {
|
|
@@ -615,6 +663,8 @@ The `queueShow` system toggles a `hidden` CSS class. Elements must start with `h
|
|
|
615
663
|
}
|
|
616
664
|
```
|
|
617
665
|
|
|
666
|
+
No transition is needed on `.placer` itself — placement jumps immediately to the measured position.
|
|
667
|
+
|
|
618
668
|
---
|
|
619
669
|
|
|
620
670
|
### System Architecture
|
|
@@ -639,7 +689,17 @@ Pass the **same emitter instance** to `Sizer` and `DelayedPlacer` when they are
|
|
|
639
689
|
|
|
640
690
|
### AnimationEmitter
|
|
641
691
|
|
|
642
|
-
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:
|
|
643
703
|
|
|
644
704
|
```ts
|
|
645
705
|
interface AnimationEmitter {
|
|
@@ -811,6 +871,13 @@ enqueue(() => this.handleReady())
|
|
|
811
871
|
|
|
812
872
|
Cancels a pending hide and queues a show for elements toggled back before their hide ran.
|
|
813
873
|
|
|
874
|
+
```ts
|
|
875
|
+
function clearPendingHideAndQueueShow(
|
|
876
|
+
refOrNode: React.RefObject<HTMLElement> | HTMLElement,
|
|
877
|
+
delay?: number // default: 40ms
|
|
878
|
+
): void
|
|
879
|
+
```
|
|
880
|
+
|
|
814
881
|
> Prefer `showRightAway()` for most toggle cases.
|
|
815
882
|
|
|
816
883
|
---
|
|
@@ -819,6 +886,13 @@ Cancels a pending hide and queues a show for elements toggled back before their
|
|
|
819
886
|
|
|
820
887
|
Cancels a pending show and queues a hide.
|
|
821
888
|
|
|
889
|
+
```ts
|
|
890
|
+
function clearPendingShowAndQueueHide(
|
|
891
|
+
refOrNode: React.RefObject<HTMLElement> | HTMLElement,
|
|
892
|
+
delay?: number // default: 40ms
|
|
893
|
+
): void
|
|
894
|
+
```
|
|
895
|
+
|
|
822
896
|
> Prefer `hideRightAway()` for most toggle cases.
|
|
823
897
|
|
|
824
898
|
---
|
|
@@ -851,12 +925,19 @@ return <div ref={ref} className="your-panel hidden">...</div>
|
|
|
851
925
|
|
|
852
926
|
#### `useShowNow(ref, delay?)` — React Hook
|
|
853
927
|
|
|
854
|
-
Like `useQueueShow` but places itself at the front of the queue on every render.
|
|
928
|
+
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.
|
|
855
929
|
|
|
856
930
|
```ts
|
|
857
931
|
function useShowNow(ref: React.RefObject<HTMLElement>, delay?: number): void
|
|
858
932
|
```
|
|
859
933
|
|
|
934
|
+
```tsx
|
|
935
|
+
const ref = useRef<HTMLHeadingElement>(null)
|
|
936
|
+
useShowNow(ref)
|
|
937
|
+
|
|
938
|
+
return <h1 ref={ref} className="your-heading hidden">Title</h1>
|
|
939
|
+
```
|
|
940
|
+
|
|
860
941
|
---
|
|
861
942
|
|
|
862
943
|
### Sizer — Animated Height Container
|
|
@@ -883,6 +964,11 @@ interface SizerProps {
|
|
|
883
964
|
// overflow is always visible (adds force-show-overflow class).
|
|
884
965
|
shouldHideOverflow?: boolean
|
|
885
966
|
|
|
967
|
+
// Debounce delay in ms before each resize measurement fires. Default: 250ms.
|
|
968
|
+
// Lower values make Sizer react faster to content changes; useful when
|
|
969
|
+
// content updates quickly and you want tighter animation timing.
|
|
970
|
+
sizerDelayMs?: number
|
|
971
|
+
|
|
886
972
|
// Event emitter. Without one, Sizer only resizes on React re-renders,
|
|
887
973
|
// not in response to external layout events.
|
|
888
974
|
emitter?: AnimationEmitter
|
|
@@ -893,6 +979,8 @@ interface SizerProps {
|
|
|
893
979
|
|
|
894
980
|
```ts
|
|
895
981
|
sizer.current?.resize(): boolean // measure and apply new height; returns true if it changed
|
|
982
|
+
sizer.current?.showOverflow(): void // temporarily allow overflow (e.g. while a dropdown inside is open)
|
|
983
|
+
sizer.current?.hideOverflow(): void // re-apply overflow: hidden
|
|
896
984
|
```
|
|
897
985
|
|
|
898
986
|
#### Emitter Events
|
|
@@ -991,28 +1079,34 @@ delayedPlacer.current?.placeRightAway(): void // re-measure and re-place immedi
|
|
|
991
1079
|
#### Usage
|
|
992
1080
|
|
|
993
1081
|
```tsx
|
|
994
|
-
|
|
1082
|
+
import React, { useRef, useMemo } from 'react'
|
|
1083
|
+
import { DelayedPlacer, SimpleEmitter } from '@sprucelabs/spruce-heartwood-utils'
|
|
995
1084
|
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
}
|
|
1085
|
+
function YourPanel({ isFocused }: { isFocused: () => boolean }) {
|
|
1086
|
+
const placerRef = useRef<React.ElementRef<typeof DelayedPlacer>>(null)
|
|
1087
|
+
const emitter = useMemo(() => new SimpleEmitter(), [])
|
|
1000
1088
|
|
|
1001
|
-
render() {
|
|
1002
1089
|
return (
|
|
1003
1090
|
<DelayedPlacer
|
|
1004
|
-
className="
|
|
1005
|
-
isEnabled={
|
|
1006
|
-
emitter={
|
|
1091
|
+
className="placer__panel"
|
|
1092
|
+
isEnabled={true}
|
|
1093
|
+
emitter={emitter}
|
|
1007
1094
|
ref={placerRef}
|
|
1008
|
-
isFocused={
|
|
1095
|
+
isFocused={isFocused}
|
|
1009
1096
|
>
|
|
1010
|
-
{
|
|
1097
|
+
{yourContent}
|
|
1011
1098
|
</DelayedPlacer>
|
|
1012
1099
|
)
|
|
1013
1100
|
}
|
|
1014
1101
|
```
|
|
1015
1102
|
|
|
1103
|
+
When you need to re-place after a content change that happens outside React state:
|
|
1104
|
+
|
|
1105
|
+
```ts
|
|
1106
|
+
// Trigger placement immediately, bypassing the debounce:
|
|
1107
|
+
placerRef.current?.placeRightAway()
|
|
1108
|
+
```
|
|
1109
|
+
|
|
1016
1110
|
> `isEnabled`, `className`, and `isFocused` are all required. The child must be `position: absolute` in CSS for placement to have visual effect.
|
|
1017
1111
|
|
|
1018
1112
|
---
|
|
@@ -1075,6 +1169,7 @@ sizeUtil.bodyWidth = () => 1200
|
|
|
1075
1169
|
A complete example showing all three systems working together in a card component:
|
|
1076
1170
|
|
|
1077
1171
|
```tsx
|
|
1172
|
+
import React, { useMemo } from 'react'
|
|
1078
1173
|
import {
|
|
1079
1174
|
Sizer, DelayedPlacer, queueShow, Settings, SimpleEmitter,
|
|
1080
1175
|
} from '@sprucelabs/spruce-heartwood-utils'
|
|
@@ -1116,6 +1211,9 @@ queueShow removes .hidden from each element with a 40ms stagger
|
|
|
1116
1211
|
**Test setup for animation components:**
|
|
1117
1212
|
|
|
1118
1213
|
```ts
|
|
1214
|
+
import { Settings, stopQueue } from '@sprucelabs/spruce-heartwood-utils'
|
|
1215
|
+
import { skillViewState } from '@sprucelabs/spruce-heartwood-utils/build/components/skillViews/skillViewState'
|
|
1216
|
+
|
|
1119
1217
|
protected async beforeEach() {
|
|
1120
1218
|
Settings.disableAnimations()
|
|
1121
1219
|
skillViewState.isFullScreen = false
|