@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.
Files changed (2) hide show
  1. package/README.md +118 -20
  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,
@@ -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 the following rules yourself.
610
+ **Outside Heartwood (standalone use):** supply every rule below yourself. Missing any one of them causes that animation to silently skip.
582
611
 
583
- The `queueShow` system toggles a `hidden` CSS class. Elements must start with `hidden` in their `className` and define their own transition:
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 goes on the element, not .hidden */
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
- `Sizer` uses `.sizer` and `.sizer__inner`:
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
- `DelayedPlacer` uses `.placer`. Its child must be `position: absolute`:
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. 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:
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
- const placerRef = React.createRef<DelayedPlacer>()
1082
+ import React, { useRef, useMemo } from 'react'
1083
+ import { DelayedPlacer, SimpleEmitter } from '@sprucelabs/spruce-heartwood-utils'
995
1084
 
996
- // Re-place after content changes:
997
- private onContentChange() {
998
- setTimeout(() => { placerRef.current?.placeRightAway() }, 50)
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="placer__card"
1005
- isEnabled={this.isPlacementEnabled}
1006
- emitter={this.emitter}
1091
+ className="placer__panel"
1092
+ isEnabled={true}
1093
+ emitter={emitter}
1007
1094
  ref={placerRef}
1008
- isFocused={() => true}
1095
+ isFocused={isFocused}
1009
1096
  >
1010
- {yourCard}
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
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.1",
4
+ "version": "38.17.3",
5
5
  "skill": {
6
6
  "namespace": "heartwood"
7
7
  },