@sprucelabs/spruce-heartwood-utils 38.16.2 → 38.17.0

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.
@@ -13,3 +13,5 @@ export { default as AutoLogoutPlugin } from './plugins/AutoLogoutViewPlugin';
13
13
  export type { AutoLogoutViewPlugin } from './plugins/AutoLogoutViewPlugin';
14
14
  export { default as SpyAutoLogoutPlugin } from './plugins/SpyAutoLogoutViewPlugin';
15
15
  export * from './components/animation';
16
+ export { default as SkillViewEmitter } from './app/SkillViewEmitter';
17
+ export type { SkillViewEventContract, SkillViewEvents, SkillViewEmitPayloads, SkillViewEmitter as SkillViewEmitterType, } from './app/SkillViewEmitter';
@@ -9,3 +9,4 @@ export { default as loadActiveThemeForOrg } from './theming/loadActiveThemeForOr
9
9
  export { default as AutoLogoutPlugin } from './plugins/AutoLogoutViewPlugin.js';
10
10
  export { default as SpyAutoLogoutPlugin } from './plugins/SpyAutoLogoutViewPlugin.js';
11
11
  export * from './components/animation/index.js';
12
+ export { default as SkillViewEmitter } from './app/SkillViewEmitter.js';
@@ -13,3 +13,5 @@ export { default as AutoLogoutPlugin } from './plugins/AutoLogoutViewPlugin';
13
13
  export type { AutoLogoutViewPlugin } from './plugins/AutoLogoutViewPlugin';
14
14
  export { default as SpyAutoLogoutPlugin } from './plugins/SpyAutoLogoutViewPlugin';
15
15
  export * from './components/animation';
16
+ export { default as SkillViewEmitter } from './app/SkillViewEmitter';
17
+ export type { SkillViewEventContract, SkillViewEvents, SkillViewEmitPayloads, SkillViewEmitter as SkillViewEmitterType, } from './app/SkillViewEmitter';
@@ -17,7 +17,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
17
17
  return (mod && mod.__esModule) ? mod : { "default": mod };
18
18
  };
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
- exports.SpyAutoLogoutPlugin = exports.AutoLogoutPlugin = exports.loadActiveThemeForOrg = exports.MockRemoteViewControllerFactory = exports.fakeGetViews = exports.remoteVcAssert = exports.CardRegistrar = exports.RemoteViewControllerFactoryImpl = void 0;
20
+ exports.SkillViewEmitter = exports.SpyAutoLogoutPlugin = exports.AutoLogoutPlugin = exports.loadActiveThemeForOrg = exports.MockRemoteViewControllerFactory = exports.fakeGetViews = exports.remoteVcAssert = exports.CardRegistrar = exports.RemoteViewControllerFactoryImpl = void 0;
21
21
  var RemoteViewControllerFactory_1 = require("./views/RemoteViewControllerFactory");
22
22
  Object.defineProperty(exports, "RemoteViewControllerFactoryImpl", { enumerable: true, get: function () { return __importDefault(RemoteViewControllerFactory_1).default; } });
23
23
  __exportStar(require("./views/RemoteViewControllerFactory"), exports);
@@ -37,4 +37,6 @@ Object.defineProperty(exports, "AutoLogoutPlugin", { enumerable: true, get: func
37
37
  var SpyAutoLogoutViewPlugin_1 = require("./plugins/SpyAutoLogoutViewPlugin");
38
38
  Object.defineProperty(exports, "SpyAutoLogoutPlugin", { enumerable: true, get: function () { return __importDefault(SpyAutoLogoutViewPlugin_1).default; } });
39
39
  __exportStar(require("./components/animation"), exports);
40
+ var SkillViewEmitter_1 = require("./app/SkillViewEmitter");
41
+ Object.defineProperty(exports, "SkillViewEmitter", { enumerable: true, get: function () { return __importDefault(SkillViewEmitter_1).default; } });
40
42
  //# sourceMappingURL=index-module.js.map
@@ -1,8 +1,6 @@
1
1
  # Animation Utilities — Complete Reference
2
2
 
3
- Exported from `@sprucelabs/spruce-heartwood-utils` via `src/index-module.ts src/components/animation/index.ts`.
4
-
5
- These utilities let third-party consumers use Heartwood's animation and layout primitives without depending on internal Spruce types (`AppEngine`, `SkillViewEmitter`).
3
+ These utilities let third-party consumers use Heartwood's animation and layout primitives. Both `SkillViewEmitter` and `SimpleEmitter` are exported from the package — no need to build your own event bus.
6
4
 
7
5
  ---
8
6
 
@@ -34,6 +32,8 @@ import {
34
32
  stopQueue,
35
33
  // Components
36
34
  Sizer, DelayedPlacer,
35
+ // Emitters — pick one (see AnimationEmitter section)
36
+ SkillViewEmitter, SimpleEmitter,
37
37
  // Utilities
38
38
  sizeUtil, Settings,
39
39
  // Types
@@ -45,7 +45,37 @@ import {
45
45
 
46
46
  ## Required CSS
47
47
 
48
- **All of this CSS is provided by Heartwood's stylesheet** (`public/stylesheets/main.css`). If you are consuming these utilities inside a Heartwood skill view, no extra CSS setup is needed. If you are using them in a standalone project, you must supply the rules below yourself.
48
+ You must include the rules below in your stylesheet. They are **not bundled with the package** they come from Heartwood's `public/stylesheets/main.css`, which is not shipped as part of this module.
49
+
50
+ ### Copy-paste starter block
51
+
52
+ ```css
53
+ /* Hidden state — required for queueShow / showRightAway / hideRightAway */
54
+ .hidden {
55
+ opacity: 0 !important;
56
+ transform: translate(0, 10px);
57
+ }
58
+
59
+ /* Transition on your animated elements — required for smooth animation */
60
+ .my-animated-element {
61
+ transition: opacity 0.5s ease, transform 0.5s ease;
62
+ }
63
+
64
+ /* Sizer height transitions — required for the Sizer component */
65
+ .sizer {
66
+ transition: all 0.5s;
67
+ }
68
+ body.portrait .sizer {
69
+ transition: all 1s;
70
+ }
71
+
72
+ /* Absolute positioning for DelayedPlacer children — required */
73
+ .being_placed {
74
+ position: absolute;
75
+ }
76
+ ```
77
+
78
+ Replace `.my-animated-element` with your own class name(s). The remaining rules must appear verbatim.
49
79
 
50
80
  ---
51
81
 
@@ -53,13 +83,12 @@ import {
53
83
 
54
84
  Every element you pass to `queueShow`, `showRightAway`, or `hideRightAway` must start with `class="... hidden"`. The queue works by toggling this class on and off.
55
85
 
56
- **Compiled rule from Heartwood:**
86
+ **Rule from Heartwood (`public/stylesheets/main.css`):**
57
87
 
58
88
  ```css
59
89
  .hidden {
60
90
  opacity: 0 !important;
61
91
  transform: translate(0, 10px);
62
- transition: all 0.5s
63
92
  }
64
93
  ```
65
94
 
@@ -274,35 +303,6 @@ The `.message.hide` state is separate:
274
303
 
275
304
  ---
276
305
 
277
- ### Minimal CSS for standalone use
278
-
279
- If you are using these utilities completely outside of a Heartwood skill view, here is the minimum CSS needed:
280
-
281
- ```css
282
- /* 1. Hidden state — required for queueShow to work */
283
- .hidden {
284
- opacity: 0 !important;
285
- transform: translate(0, 10px);
286
- }
287
-
288
- /* 2. Transition on your elements — required for animation (not just snap) */
289
- .my-animated-element {
290
- transition: opacity 0.5s ease, transform 0.5s ease;
291
- }
292
-
293
- /* 3. Sizer height transitions — required for Sizer component */
294
- .sizer {
295
- transition: all 0.5s;
296
- }
297
-
298
- /* 4. Absolute positioning for DelayedPlacer children — required */
299
- .being_placed {
300
- position: absolute;
301
- }
302
- ```
303
-
304
- ---
305
-
306
306
  ## System Architecture
307
307
 
308
308
  Three independent systems coordinate through a shared `AnimationEmitter`:
@@ -355,28 +355,28 @@ interface AnimationEmitter {
355
355
  | `did-change-orientation` | your app | DelayedPlacer | Portrait ↔ landscape switch |
356
356
  | `did-place-cards` | DelayedPlacer | your app (optional) | Placement complete |
357
357
 
358
- ### Minimal implementation
358
+ ### Getting an emitter instance
359
+
360
+ Two emitters are exported from the package — choose based on your needs:
361
+
362
+ **`SkillViewEmitter`** — full-featured mercury-style emitter, recommended when integrating with a Heartwood skill view:
359
363
 
360
364
  ```ts
361
- class SimpleEmitter implements AnimationEmitter {
362
- private listeners: Record<string, Array<() => void>> = {}
365
+ import { SkillViewEmitter } from '@sprucelabs/spruce-heartwood-utils'
363
366
 
364
- on(event: string, handler: () => void): void {
365
- (this.listeners[event] ??= []).push(handler)
366
- }
367
+ const emitter = SkillViewEmitter.getInstance()
368
+ ```
367
369
 
368
- off(event: string, handler: () => void): void {
369
- this.listeners[event] = (this.listeners[event] ?? []).filter(h => h !== handler)
370
- }
370
+ **`SimpleEmitter`** — lightweight in-memory emitter, suitable for standalone components, isolated tests, or any context that doesn't need the full mercury stack:
371
371
 
372
- emit(event: string): void {
373
- for (const h of this.listeners[event] ?? []) h()
374
- }
375
- }
372
+ ```ts
373
+ import { SimpleEmitter } from '@sprucelabs/spruce-heartwood-utils'
376
374
 
377
375
  const emitter = new SimpleEmitter()
378
376
  ```
379
377
 
378
+ Either way, use a single instance shared across all components that need to coordinate (Sizer, DelayedPlacer, your resize/render wiring).
379
+
380
380
  ### Wiring the emitter to window resize
381
381
 
382
382
  ```ts
@@ -416,7 +416,7 @@ emitter.off('did-resize', () => this.doSomething()) // does nothing
416
416
  ```tsx
417
417
  // They MUST share an emitter instance to coordinate.
418
418
  // Sizer emits 'did-resize-content' → DelayedPlacer re-places.
419
- const [emitter] = useState(() => new SimpleEmitter())
419
+ const emitter = SkillViewEmitter.getInstance()
420
420
 
421
421
  <Sizer emitter={emitter}>
422
422
  <DelayedPlacer emitter={emitter} isEnabled={true} className="placer__card" isFocused={() => true}>
@@ -437,15 +437,13 @@ Controls whether animations run and what their duration is. **Always call `Setti
437
437
  Settings.disableAnimations() // sets animationDuration to 0, queue runs synchronously
438
438
  Settings.getIsAnimationEnabled() // → boolean
439
439
  Settings.animationDuration // → 0 | 500 | 1000 (landscape / portrait)
440
- Settings.shouldExportSwiper // → boolean (reads process.env.EXPORT_SWIPER)
441
440
  ```
442
441
 
443
442
  ### Disabling animations in tests
444
443
 
445
- Both Heartwood test base classes do this. Without it, tests rely on real timers and become flaky.
444
+ Call this in your test setup. Without it, tests rely on real timers and become flaky.
446
445
 
447
446
  ```ts
448
- // AbstractHeartwoodTest.ts — base class for all Heartwood tests
449
447
  protected async beforeEach() {
450
448
  await super.beforeEach()
451
449
  Settings.disableAnimations()
@@ -458,22 +456,6 @@ protected async afterEach() {
458
456
  }
459
457
  ```
460
458
 
461
- ```ts
462
- // ButtonGroup.test.tsx — standalone test file
463
- protected async beforeEach() {
464
- await super.beforeEach()
465
- Settings.disableAnimations()
466
- this.engine = AppEngine.getInstance()
467
- await this.engine.reset()
468
- // ...
469
- }
470
-
471
- protected async afterEach() {
472
- await super.afterEach()
473
- stopQueue()
474
- }
475
- ```
476
-
477
459
  ### Effect on the queue
478
460
 
479
461
  ```ts
@@ -507,6 +489,8 @@ queueShow(someRef) // fires instantly, no setTimeout
507
489
 
508
490
  A singleton FIFO queue. All consumers share one queue. Items are dequeued every 40ms (default) by toggling the `hidden` CSS class on DOM elements.
509
491
 
492
+ > **Elements need a CSS `transition` to animate.** The queue only toggles the `hidden` class — the browser's CSS engine drives the visual animation. Without a `transition` on the element, show and hide operations still work but snap instantly with no fade or movement. See the [Required CSS](#required-css) section for the rules to include.
493
+
510
494
  ### When to use which function
511
495
 
512
496
  | Situation | Use |
@@ -1055,15 +1039,6 @@ hideRightAway(this.panelRef)
1055
1039
  Clears the interval and marks the queue as stopped. Does **not** drain the queue — pending items are abandoned. Always call this in test teardown.
1056
1040
 
1057
1041
  ```ts
1058
- // AbstractHeartwoodTest.ts — every Heartwood test calls this in afterEach
1059
- protected async afterEach() {
1060
- await super.afterEach()
1061
- await this.components.afterEach()
1062
- await AppEngine.reset()
1063
- stopQueue()
1064
- }
1065
-
1066
- // ButtonGroup.test.tsx — standalone test teardown
1067
1042
  protected async afterEach() {
1068
1043
  await super.afterEach()
1069
1044
  stopQueue()
@@ -1282,7 +1257,7 @@ async function handleSizeChange(props: any) {
1282
1257
 
1283
1258
  ```tsx
1284
1259
  // When the parent tells Sizer the layout changed, Sizer re-measures:
1285
- const [emitter] = useState(() => new SimpleEmitter())
1260
+ const emitter = SkillViewEmitter.getInstance()
1286
1261
 
1287
1262
  useEffect(() => {
1288
1263
  // After every React render, notify Sizer to re-check its height
@@ -1393,39 +1368,32 @@ placeRightAway()
1393
1368
  6. setTimeout(() => emitter.emit('did-place-cards'), 100)
1394
1369
  ```
1395
1370
 
1396
- ### Pattern 1 — Card with optional absolute positioning
1397
-
1398
- The only usage in Heartwood. `isDelayedPlacerEnabled` comes from the card's view controller.
1371
+ ### Pattern 1 — Component with optional absolute positioning
1399
1372
 
1400
1373
  ```tsx
1401
- // Card.tsx — wraps every card; isEnabled defaults to false
1402
1374
  private delayedPlacerRef = React.createRef<React.ElementRef<typeof DelayedPlacer>>()
1375
+ private emitter = SkillViewEmitter.getInstance()
1403
1376
 
1404
1377
  private handleRender() {
1405
- this.setRenderingClass(true)
1406
-
1407
1378
  clearTimeout(this.renderTimeout)
1408
1379
  this.renderTimeout = setTimeout(() => {
1409
1380
  // Force re-placement after content changes
1410
1381
  this.delayedPlacerRef.current?.placeRightAway()
1411
1382
  this.props.onRender?.()
1412
- this.renderingClassTimeout = setTimeout(() => {
1413
- this.setRenderingClass(false)
1414
- this.setupClassNames()
1415
- }, Settings.animationDuration)
1416
1383
  }, 50)
1417
1384
  }
1418
1385
 
1419
1386
  render() {
1387
+ const { isFloating } = this.props
1420
1388
  return (
1421
1389
  <DelayedPlacer
1422
1390
  className="placer__card"
1423
- isEnabled={isDelayedPlacerEnabled ?? false}
1424
- emitter={this.emitter} // engine.getEmitter()
1391
+ isEnabled={isFloating ?? false}
1392
+ emitter={this.emitter}
1425
1393
  ref={this.delayedPlacerRef}
1426
- isFocused={this.props.isFocusedHandler ?? (() => true)}
1394
+ isFocused={this.props.isFocused ?? (() => true)}
1427
1395
  >
1428
- {card}
1396
+ {this.props.children}
1429
1397
  </DelayedPlacer>
1430
1398
  )
1431
1399
  }
@@ -1594,11 +1562,8 @@ private sizeMyselfToCorrectHeight() {
1594
1562
  Since `sizeUtil` is a plain object, methods can be replaced directly:
1595
1563
 
1596
1564
  ```ts
1597
- // ToolBelt.test.ts — stub viewport width for layout-dependent logic
1565
+ // Stub viewport width for layout-dependent logic:
1598
1566
  sizeUtil.bodyWidth = () => 1200
1599
- this.setApp(AppRenderingToolBelt)
1600
- await this.pushSvcWithNoToolBeltDeclared()
1601
- this.assertRenderingTool(activeApp.toolId)
1602
1567
  ```
1603
1568
 
1604
1569
  Restore after test if needed (or rely on test module isolation).
@@ -1670,21 +1635,14 @@ DelayedPlacer.sizeEverything() fires
1670
1635
  ### Setting up all three systems together
1671
1636
 
1672
1637
  ```tsx
1673
- import React, { useEffect, useRef, useState } from 'react'
1638
+ import React, { useEffect, useRef } from 'react'
1674
1639
  import {
1675
- Sizer, DelayedPlacer, AnimationEmitter,
1640
+ Sizer, DelayedPlacer, SkillViewEmitter,
1676
1641
  queueShow, stopQueue, Settings
1677
1642
  } from '@sprucelabs/spruce-heartwood-utils'
1678
1643
 
1679
- class SimpleEmitter implements AnimationEmitter {
1680
- private listeners: Record<string, Array<() => void>> = {}
1681
- on(event: string, h: () => void) { (this.listeners[event] ??= []).push(h) }
1682
- off(event: string, h: () => void) { this.listeners[event] = (this.listeners[event] ?? []).filter(x => x !== h) }
1683
- emit(event: string) { for (const h of this.listeners[event] ?? []) h() }
1684
- }
1685
-
1686
1644
  function MyPanel({ isFloating }: { isFloating: boolean }) {
1687
- const [emitter] = useState(() => new SimpleEmitter())
1645
+ const emitter = SkillViewEmitter.getInstance()
1688
1646
  const delayedPlacerRef = useRef<any>(null)
1689
1647
 
1690
1648
  // Wire React renders → emitter
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.16.2",
4
+ "version": "38.17.0",
5
5
  "skill": {
6
6
  "namespace": "heartwood"
7
7
  },