@sprucelabs/spruce-heartwood-utils 38.17.0 → 38.17.2

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 CHANGED
@@ -576,26 +576,43 @@ import {
576
576
 
577
577
  ### Required CSS
578
578
 
579
+ > **⚠ 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.
580
+
579
581
  **Inside a Heartwood skill view:** all required CSS is already provided by Heartwood's stylesheet. No extra setup needed.
580
582
 
581
- **Outside Heartwood (standalone use):** supply the following rules yourself.
583
+ **Outside Heartwood (standalone use):** supply every rule below yourself. Missing any one of them causes that animation to silently skip.
584
+
585
+ ---
586
+
587
+ #### `queueShow` / `queueHide` — fade + slide transitions
582
588
 
583
- The `queueShow` system toggles a `hidden` CSS class. Elements must start with `hidden` in their `className` and define their own transition:
589
+ `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
590
 
585
591
  ```css
592
+ /* Hidden state — sets opacity, nudge, and pointer-events */
586
593
  .hidden {
587
594
  opacity: 0;
588
595
  transform: translateY(4px);
589
596
  pointer-events: none;
590
597
  }
591
598
 
592
- /* Transition goes on the element, not .hidden */
599
+ /* Transition on the element this is what the browser animates */
593
600
  .your-element {
594
601
  transition: opacity 200ms ease, transform 200ms ease;
595
602
  }
596
603
  ```
597
604
 
598
- `Sizer` uses `.sizer` and `.sizer__inner`:
605
+ Elements must also start with `hidden` in their `className` so they are invisible until `queueShow` fires:
606
+
607
+ ```tsx
608
+ <div className="your-element hidden" ref={(ref) => { ref && queueShow(ref) }} />
609
+ ```
610
+
611
+ ---
612
+
613
+ #### `Sizer` — animated height
614
+
615
+ `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
616
 
600
617
  ```css
601
618
  .sizer {
@@ -604,7 +621,11 @@ The `queueShow` system toggles a `hidden` CSS class. Elements must start with `h
604
621
  }
605
622
  ```
606
623
 
607
- `DelayedPlacer` uses `.placer`. Its child must be `position: absolute`:
624
+ ---
625
+
626
+ #### `DelayedPlacer` — absolute positioning
627
+
628
+ `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
629
 
609
630
  ```css
610
631
  .placer {
@@ -615,6 +636,8 @@ The `queueShow` system toggles a `hidden` CSS class. Elements must start with `h
615
636
  }
616
637
  ```
617
638
 
639
+ No transition is needed on `.placer` itself — placement jumps immediately to the measured position.
640
+
618
641
  ---
619
642
 
620
643
  ### System Architecture
@@ -659,28 +682,6 @@ interface AnimationEmitter {
659
682
  | `did-change-orientation` | DelayedPlacer | your code | Portrait ↔ landscape switch |
660
683
  | `did-place-cards` | your listeners | DelayedPlacer | Placement calculation finished |
661
684
 
662
- **Minimal implementation:**
663
-
664
- ```ts
665
- class SimpleEmitter implements AnimationEmitter {
666
- private listeners: Record<string, Array<() => void>> = {}
667
-
668
- on(event: string, handler: () => void): void {
669
- ;(this.listeners[event] ??= []).push(handler)
670
- }
671
-
672
- off(event: string, handler: () => void): void {
673
- this.listeners[event] = (this.listeners[event] ?? []).filter(
674
- (h) => h !== handler
675
- )
676
- }
677
-
678
- emit(event: string): void {
679
- for (const h of this.listeners[event] ?? []) h()
680
- }
681
- }
682
- ```
683
-
684
685
  > Pass the **same function reference** to `on` and `off`. Inline arrow functions cannot be removed with `off`.
685
686
 
686
687
  ---
@@ -928,6 +929,8 @@ sizer.current?.resize(): boolean // measure and apply new height; returns tru
928
929
  #### Usage
929
930
 
930
931
  ```tsx
932
+ import { Sizer, SimpleEmitter } from '@sprucelabs/spruce-heartwood-utils'
933
+
931
934
  // Simplest — just clip overflow during height transition:
932
935
  <Sizer shouldHideOverflow>
933
936
  {isExpanded && <YourExpandableContent />}
@@ -1096,18 +1099,9 @@ A complete example showing all three systems working together in a card componen
1096
1099
 
1097
1100
  ```tsx
1098
1101
  import {
1099
- Sizer, DelayedPlacer, queueShow, Settings, AnimationEmitter,
1102
+ Sizer, DelayedPlacer, queueShow, Settings, SimpleEmitter,
1100
1103
  } from '@sprucelabs/spruce-heartwood-utils'
1101
1104
 
1102
- class SimpleEmitter implements AnimationEmitter {
1103
- private listeners: Record<string, Array<() => void>> = {}
1104
- on(event: string, handler: () => void) { (this.listeners[event] ??= []).push(handler) }
1105
- off(event: string, handler: () => void) {
1106
- this.listeners[event] = (this.listeners[event] ?? []).filter(h => h !== handler)
1107
- }
1108
- emit(event: string) { for (const h of this.listeners[event] ?? []) h() }
1109
- }
1110
-
1111
1105
  function YourCard({ isPlaced, isFocused }: { isPlaced: boolean; isFocused: () => boolean }) {
1112
1106
  const emitter = useMemo(() => new SimpleEmitter(), [])
1113
1107
 
@@ -0,0 +1,7 @@
1
+ import { AnimationEmitter } from './types';
2
+ export default class SimpleEmitter implements AnimationEmitter {
3
+ private listeners;
4
+ on(event: string, handler: () => void): void;
5
+ off(event: string, handler: () => void): void;
6
+ emit(event: string): void;
7
+ }
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ class SimpleEmitter {
4
+ constructor() {
5
+ this.listeners = {};
6
+ }
7
+ on(event, handler) {
8
+ var _a;
9
+ ;
10
+ ((_a = this.listeners)[event] ?? (_a[event] = [])).push(handler);
11
+ }
12
+ off(event, handler) {
13
+ this.listeners[event] = (this.listeners[event] ?? []).filter((h) => h !== handler);
14
+ }
15
+ emit(event) {
16
+ for (const h of this.listeners[event] ?? []) {
17
+ h();
18
+ }
19
+ }
20
+ }
21
+ exports.default = SimpleEmitter;
22
+ //# sourceMappingURL=SimpleEmitter.js.map
@@ -6,3 +6,4 @@ export type { DelayedPlacerProps } from './DelayedPlacerExport';
6
6
  export { default as sizeUtil } from '../calendars/utils/size.utility';
7
7
  export { default as Settings } from '../Settings';
8
8
  export type { AnimationEmitter } from './types';
9
+ export { default as SimpleEmitter } from './SimpleEmitter';
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.Settings = exports.sizeUtil = exports.DelayedPlacer = exports.Sizer = exports.stopQueue = exports.clearPendingShowAndQueueHide = exports.clearPendingHideAndQueueShow = exports.callbackImmediately = exports.queueCallback = exports.queueHide = exports.queueShow = exports.hideRightAway = exports.showRightAway = exports.useShowNow = exports.useQueueShow = void 0;
6
+ exports.SimpleEmitter = exports.Settings = exports.sizeUtil = exports.DelayedPlacer = exports.Sizer = exports.stopQueue = exports.clearPendingShowAndQueueHide = exports.clearPendingHideAndQueueShow = exports.callbackImmediately = exports.queueCallback = exports.queueHide = exports.queueShow = exports.hideRightAway = exports.showRightAway = exports.useShowNow = exports.useQueueShow = void 0;
7
7
  // Queue system — all functions and hooks
8
8
  var queueShow_1 = require("../../hooks/queueShow");
9
9
  Object.defineProperty(exports, "useQueueShow", { enumerable: true, get: function () { return queueShow_1.useQueueShow; } });
@@ -27,4 +27,6 @@ var size_utility_1 = require("../calendars/utils/size.utility");
27
27
  Object.defineProperty(exports, "sizeUtil", { enumerable: true, get: function () { return __importDefault(size_utility_1).default; } });
28
28
  var Settings_1 = require("../Settings");
29
29
  Object.defineProperty(exports, "Settings", { enumerable: true, get: function () { return __importDefault(Settings_1).default; } });
30
+ var SimpleEmitter_1 = require("./SimpleEmitter");
31
+ Object.defineProperty(exports, "SimpleEmitter", { enumerable: true, get: function () { return __importDefault(SimpleEmitter_1).default; } });
30
32
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ import { AnimationEmitter } from './types';
2
+ export default class SimpleEmitter implements AnimationEmitter {
3
+ private listeners;
4
+ on(event: string, handler: () => void): void;
5
+ off(event: string, handler: () => void): void;
6
+ emit(event: string): void;
7
+ }
@@ -0,0 +1,21 @@
1
+ export default class SimpleEmitter {
2
+ constructor() {
3
+ this.listeners = {};
4
+ }
5
+ on(event, handler) {
6
+ var _a;
7
+ var _b;
8
+ ;
9
+ ((_a = (_b = this.listeners)[event]) !== null && _a !== void 0 ? _a : (_b[event] = [])).push(handler);
10
+ }
11
+ off(event, handler) {
12
+ var _a;
13
+ this.listeners[event] = ((_a = this.listeners[event]) !== null && _a !== void 0 ? _a : []).filter((h) => h !== handler);
14
+ }
15
+ emit(event) {
16
+ var _a;
17
+ for (const h of (_a = this.listeners[event]) !== null && _a !== void 0 ? _a : []) {
18
+ h();
19
+ }
20
+ }
21
+ }
@@ -6,3 +6,4 @@ export type { DelayedPlacerProps } from './DelayedPlacerExport';
6
6
  export { default as sizeUtil } from '../calendars/utils/size.utility';
7
7
  export { default as Settings } from '../Settings';
8
8
  export type { AnimationEmitter } from './types';
9
+ export { default as SimpleEmitter } from './SimpleEmitter';
@@ -6,3 +6,4 @@ export { default as DelayedPlacer } from './DelayedPlacerExport.js';
6
6
  // Utilities (useful for consumers who want to measure DOM nodes)
7
7
  export { default as sizeUtil } from '../calendars/utils/size.utility.js';
8
8
  export { default as Settings } from '../Settings.js';
9
+ export { default as SimpleEmitter } from './SimpleEmitter.js';
@@ -29,33 +29,18 @@ All three methods accept a plain string event name and return either `void` or `
29
29
  | `did-change-orientation` | DelayedPlacer | Device orientation handler | Portrait ↔ landscape switch |
30
30
  | `did-place-cards` | (external listeners) | DelayedPlacer | Placement calculation complete |
31
31
 
32
- ## Implementing a Compatible Emitter
32
+ ## Using SimpleEmitter
33
33
 
34
- The interface is intentionally simple. Any Node-style EventEmitter or custom implementation works if it matches the signature:
34
+ `SimpleEmitter` is exported from the package import it directly instead of building your own:
35
35
 
36
36
  ```ts
37
- // Minimal implementation for standalone use:
38
- class SimpleEmitter implements AnimationEmitter {
39
- private listeners: Record<string, Array<() => void>> = {}
40
-
41
- on(event: string, handler: () => void): void {
42
- ;(this.listeners[event] ??= []).push(handler)
43
- }
44
-
45
- off(event: string, handler: () => void): void {
46
- this.listeners[event] = (this.listeners[event] ?? []).filter(
47
- (h) => h !== handler
48
- )
49
- }
50
-
51
- emit(event: string): void {
52
- for (const h of this.listeners[event] ?? []) {
53
- h()
54
- }
55
- }
56
- }
37
+ import { SimpleEmitter } from '@sprucelabs/spruce-heartwood-utils'
38
+
39
+ const emitter = new SimpleEmitter()
57
40
  ```
58
41
 
42
+ It implements `AnimationEmitter` with a plain in-memory listener map. Use it for standalone components, isolated tests, or any context where the full `SkillViewEmitter` is unnecessary.
43
+
59
44
  ## Noop Emitter
60
45
 
61
46
  `SizerExport.tsx` defines and uses a noop emitter as the default when no emitter is passed:
@@ -104,7 +104,9 @@ handleSizing() (debounced at sizerDelayMs, default 250ms)
104
104
  ### With emitter — responds to external layout events
105
105
 
106
106
  ```tsx
107
- const [emitter] = useState(() => createMyEmitter())
107
+ import { SimpleEmitter } from '@sprucelabs/spruce-heartwood-utils'
108
+
109
+ const [emitter] = useState(() => new SimpleEmitter())
108
110
 
109
111
  <Sizer emitter={emitter} shouldStartAtZero>
110
112
  <MyContent />
@@ -114,6 +116,8 @@ const [emitter] = useState(() => createMyEmitter())
114
116
  ### With ref — manually drive resize on content change
115
117
 
116
118
  ```tsx
119
+ import { SimpleEmitter } from '@sprucelabs/spruce-heartwood-utils'
120
+
117
121
  const emitter = useMemo(() => new SimpleEmitter(), [])
118
122
  const sizer = useRef<React.ElementRef<typeof Sizer>>(null)
119
123
 
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.0",
4
+ "version": "38.17.2",
5
5
  "skill": {
6
6
  "namespace": "heartwood"
7
7
  },
@@ -86,6 +86,10 @@
86
86
  "build/components/animation/types.d.ts",
87
87
  "build/esm/components/animation/types.js",
88
88
  "build/esm/components/animation/types.d.ts",
89
+ "build/components/animation/SimpleEmitter.js",
90
+ "build/components/animation/SimpleEmitter.d.ts",
91
+ "build/esm/components/animation/SimpleEmitter.js",
92
+ "build/esm/components/animation/SimpleEmitter.d.ts",
89
93
  "build/hooks/queueShow.js",
90
94
  "build/hooks/queueShow.d.ts",
91
95
  "build/esm/hooks/queueShow.js",