@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.
Files changed (2) hide show
  1. package/README.md +200 -15
  2. 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
- // Stub a view with a tracked method — stub exposes wasHit and passedParams:
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. You implement this and pass it as the `emitter` prop to coordinate components.
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
- const placerRef = React.createRef<DelayedPlacer>()
1192
+ import React, { useRef, useMemo } from 'react'
1193
+ import { DelayedPlacer, SimpleEmitter } from '@sprucelabs/spruce-heartwood-utils'
1018
1194
 
1019
- // Re-place after content changes:
1020
- private onContentChange() {
1021
- setTimeout(() => { placerRef.current?.placeRightAway() }, 50)
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="placer__card"
1028
- isEnabled={this.isPlacementEnabled}
1029
- emitter={this.emitter}
1201
+ className="placer__panel"
1202
+ isEnabled={true}
1203
+ emitter={emitter}
1030
1204
  ref={placerRef}
1031
- isFocused={() => true}
1205
+ isFocused={isFocused}
1032
1206
  >
1033
- {yourCard}
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
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.17.2",
4
+ "version": "38.17.4",
5
5
  "skill": {
6
6
  "namespace": "heartwood"
7
7
  },