@sprucelabs/spruce-heartwood-utils 38.17.2 → 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.
Files changed (2) hide show
  1. package/README.md +90 -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'
273
283
 
284
+ // Field declaration — works with both AutoLogoutPlugin and SpyAutoLogoutPlugin:
285
+ private autoLogoutPlugin: AutoLogoutViewPlugin
286
+ ```
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 {
@@ -834,6 +871,13 @@ enqueue(() => this.handleReady())
834
871
 
835
872
  Cancels a pending hide and queues a show for elements toggled back before their hide ran.
836
873
 
874
+ ```ts
875
+ function clearPendingHideAndQueueShow(
876
+ refOrNode: React.RefObject<HTMLElement> | HTMLElement,
877
+ delay?: number // default: 40ms
878
+ ): void
879
+ ```
880
+
837
881
  > Prefer `showRightAway()` for most toggle cases.
838
882
 
839
883
  ---
@@ -842,6 +886,13 @@ Cancels a pending hide and queues a show for elements toggled back before their
842
886
 
843
887
  Cancels a pending show and queues a hide.
844
888
 
889
+ ```ts
890
+ function clearPendingShowAndQueueHide(
891
+ refOrNode: React.RefObject<HTMLElement> | HTMLElement,
892
+ delay?: number // default: 40ms
893
+ ): void
894
+ ```
895
+
845
896
  > Prefer `hideRightAway()` for most toggle cases.
846
897
 
847
898
  ---
@@ -874,12 +925,19 @@ return <div ref={ref} className="your-panel hidden">...</div>
874
925
 
875
926
  #### `useShowNow(ref, delay?)` — React Hook
876
927
 
877
- 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.
878
929
 
879
930
  ```ts
880
931
  function useShowNow(ref: React.RefObject<HTMLElement>, delay?: number): void
881
932
  ```
882
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
+
883
941
  ---
884
942
 
885
943
  ### Sizer — Animated Height Container
@@ -906,6 +964,11 @@ interface SizerProps {
906
964
  // overflow is always visible (adds force-show-overflow class).
907
965
  shouldHideOverflow?: boolean
908
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
+
909
972
  // Event emitter. Without one, Sizer only resizes on React re-renders,
910
973
  // not in response to external layout events.
911
974
  emitter?: AnimationEmitter
@@ -916,6 +979,8 @@ interface SizerProps {
916
979
 
917
980
  ```ts
918
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
919
984
  ```
920
985
 
921
986
  #### Emitter Events
@@ -1014,28 +1079,34 @@ delayedPlacer.current?.placeRightAway(): void // re-measure and re-place immedi
1014
1079
  #### Usage
1015
1080
 
1016
1081
  ```tsx
1017
- const placerRef = React.createRef<DelayedPlacer>()
1082
+ import React, { useRef, useMemo } from 'react'
1083
+ import { DelayedPlacer, SimpleEmitter } from '@sprucelabs/spruce-heartwood-utils'
1018
1084
 
1019
- // Re-place after content changes:
1020
- private onContentChange() {
1021
- setTimeout(() => { placerRef.current?.placeRightAway() }, 50)
1022
- }
1085
+ function YourPanel({ isFocused }: { isFocused: () => boolean }) {
1086
+ const placerRef = useRef<React.ElementRef<typeof DelayedPlacer>>(null)
1087
+ const emitter = useMemo(() => new SimpleEmitter(), [])
1023
1088
 
1024
- render() {
1025
1089
  return (
1026
1090
  <DelayedPlacer
1027
- className="placer__card"
1028
- isEnabled={this.isPlacementEnabled}
1029
- emitter={this.emitter}
1091
+ className="placer__panel"
1092
+ isEnabled={true}
1093
+ emitter={emitter}
1030
1094
  ref={placerRef}
1031
- isFocused={() => true}
1095
+ isFocused={isFocused}
1032
1096
  >
1033
- {yourCard}
1097
+ {yourContent}
1034
1098
  </DelayedPlacer>
1035
1099
  )
1036
1100
  }
1037
1101
  ```
1038
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
+
1039
1110
  > `isEnabled`, `className`, and `isFocused` are all required. The child must be `position: absolute` in CSS for placement to have visual effect.
1040
1111
 
1041
1112
  ---
@@ -1098,6 +1169,7 @@ sizeUtil.bodyWidth = () => 1200
1098
1169
  A complete example showing all three systems working together in a card component:
1099
1170
 
1100
1171
  ```tsx
1172
+ import React, { useMemo } from 'react'
1101
1173
  import {
1102
1174
  Sizer, DelayedPlacer, queueShow, Settings, SimpleEmitter,
1103
1175
  } from '@sprucelabs/spruce-heartwood-utils'
@@ -1139,6 +1211,9 @@ queueShow removes .hidden from each element with a 40ms stagger
1139
1211
  **Test setup for animation components:**
1140
1212
 
1141
1213
  ```ts
1214
+ import { Settings, stopQueue } from '@sprucelabs/spruce-heartwood-utils'
1215
+ import { skillViewState } from '@sprucelabs/spruce-heartwood-utils/build/components/skillViews/skillViewState'
1216
+
1142
1217
  protected async beforeEach() {
1143
1218
  Settings.disableAnimations()
1144
1219
  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.3",
5
5
  "skill": {
6
6
  "namespace": "heartwood"
7
7
  },