wuira 0.2.6 → 0.2.8

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/CHANGELOG.md CHANGED
@@ -7,6 +7,32 @@ with the release date and highlight breaking changes explicitly.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## 0.2.8 — 2026-10-17
11
+
12
+ ### Fixed
13
+
14
+ - `UnitSource("target").onChange` invalidates on `PLAYER_TARGET_CHANGED`, including
15
+ combat and target removal, without reading secret identities or notifying other
16
+ unit sources. Event subscriptions stop with the last consumer.
17
+ - Add `onTokenChange` for secure consumers that should only rewrite bindings after
18
+ an explicit `Retarget`, not when a stable target address points to another unit.
19
+
20
+ ## 0.2.7 — 2026-10-16
21
+
22
+ ### Added
23
+
24
+ - Generic `Nameplates.Apply` / `Replace` registration groups, with global first-match
25
+ precedence, fresh per-unit JSX factories, reversible native visual helpers,
26
+ event-driven pooled-frame lifecycle, explicit refresh, and independent disposal.
27
+ Forbidden/protected frames and secret identities are skipped; secure JSX is rejected.
28
+ - Lua integration coverage for nameplate reuse, rule conflicts, rollback, native
29
+ restoration and HP subscription cleanup. These are mock tests, not in-game validation.
30
+
31
+ ### Fixed
32
+
33
+ - Continue unmount cleanup after a binding or decoration disposer throws, so remaining
34
+ subscriptions and frames are released before the first error is reported.
35
+
10
36
  ## 0.2.6 — 2026-10-16
11
37
 
12
38
  ### Changed
package/README.md CHANGED
@@ -76,6 +76,29 @@ Other sources include `AuraTimer`, `GlobalCooldown`, `CastTimer`, `SpellCooldown
76
76
 
77
77
  Supported action types: `spell`, `item`, `macro`, `action`, and `target`. Actions and units are static mount-time attributes. Protected trees cannot be mounted during combat.
78
78
 
79
+ ## Nameplates
80
+
81
+ ```tsx
82
+ import { Nameplates, UI } from "wuira";
83
+ import { HP } from "wuira/datasources";
84
+
85
+ const plates = Nameplates
86
+ .Apply(p => p.GUID === configuredGUID, p => {
87
+ if (p.Native.healthBar) p.Size(p.Native.healthBar, 140, 12);
88
+ })
89
+ .Replace(() => true, p => (
90
+ <progress width={140} height={12} source={new HP(p.Unit)} />
91
+ ));
92
+
93
+ // plates.Dispose();
94
+ ```
95
+
96
+ The first matching rule wins across Apply and Replace; they do not stack.
97
+ Each factory creates fresh JSX for its unit. Use `Nameplates.Create()` with
98
+ `collection.forEach(...)` to register presets, and `Refresh()` after preset changes.
99
+ See [nameplate lifecycle and cleanup](docs/nameplates.md). Secure nameplate controls
100
+ require a separate lifecycle and are not supported by this visual API.
101
+
79
102
  ## Stores
80
103
 
81
104
  ```ts
@@ -118,7 +141,7 @@ For global installation, use `npm install -g wuira`. To test unpublished changes
118
141
 
119
142
  wuira is an early framework. It is not a complete UI replacement, an in-game visual editor, or a WeakAuras replacement. JSX describes widgets; there is no React runtime or automatic reactive rendering layer. Combat restrictions and secret-value rules still apply and cannot be bypassed by the framework.
120
143
 
121
- Current core includes Cells, flow and grid layouts, progress bars, decorations, health and mana sources, a local console polyfill, and project tooling. Not yet implemented: a complete WoW API surface, click-cast abstractions, nameplate adapters, and preset management.
144
+ Current core includes Cells, layouts, progress bars, decorations, unit sources, native nameplate adapters, and project tooling. Not yet implemented: a complete WoW API surface, nameplate click-cast abstractions, and preset management. Nameplate lifecycle tests use a mock Lua runtime; real-client validation is still required.
122
145
 
123
146
  The target client is WoW Forever. Compatibility with Retail or other Classic clients is not promised without verification.
124
147
 
@@ -0,0 +1,89 @@
1
+ # Nameplates
2
+
3
+ Import `Nameplates` from `wuira` or `wuira/skins`. Wuira owns native access,
4
+ events, unit bindings and visual cleanup. Preset selection, NPC rules, editor
5
+ data and component collections belong to the consuming addon (for example LumaUI).
6
+ No class or mob presets are built into Wuira.
7
+
8
+ `NativeSkin` remains the adapter for fixed named frames. Nameplates use a separate
9
+ event-driven manager because both carriers and unit visuals are dynamically pooled.
10
+
11
+ ## Rules and ownership
12
+
13
+ `Nameplates.Apply(selector, customize)` and `Nameplates.Replace(selector, render)`
14
+ start independent groups. Chained calls append to that same group. `Nameplates.Create()`
15
+ starts an empty group, useful for collections:
16
+
17
+ ```tsx
18
+ const plates = Nameplates.Create();
19
+ collection.forEach(component => {
20
+ plates.Replace(p => component.matches(p.Unit, p.GUID), p => component.render(p));
21
+ });
22
+ // When preset data changes:
23
+ plates.Refresh();
24
+ // On addon feature shutdown:
25
+ plates.Dispose();
26
+ ```
27
+
28
+ Rules are ordered by registration time across all groups. **The first matching
29
+ rule wins, regardless of whether it is Apply or Replace.** Apply rules do not
30
+ accumulate before/after a replacement. Put specific rules before fallbacks.
31
+ Disposing a losing group leaves the winning binding intact. Disposing a winning
32
+ group restores its changes and then activates the next matching rule, if any.
33
+ Dispose is idempotent; adding rules to a disposed group is an error.
34
+
35
+ Selectors receive `Unit`, public `GUID`, engine-owned `Frame`, and native visual
36
+ `Native` (with optional `healthBar` / `castBar`). Restricted identities/frames
37
+ never reach selectors. A selector error or secret result leaves the plate native;
38
+ it does not activate a less specific fallback. Callback errors go to WoW's error
39
+ handler. A failing winner is retried on refresh or the next relevant native event.
40
+ Callbacks must not add/dispose/refresh groups while reconciliation is running.
41
+
42
+ ## Apply
43
+
44
+ Apply reuses Blizzard's visuals and bars. The context exposes reversible
45
+ `Alpha(frame, value)`, `Scale(frame, value)`, `Size(frame, width, height)`, and
46
+ `BarColor(bar, r, g, b, a?)`. These accept the native visual or its accessible,
47
+ unprotected descendants; the engine-owned carrier is excluded.
48
+
49
+ Cleanup restores a property only if it still equals the value this binding wrote,
50
+ so a later external write is preserved. Blizzard or another addon can overwrite
51
+ Apply styling; it is reapplied on relevant identity/faction/target/world events or
52
+ explicit refresh, rather than continuously polled. Direct native writes through
53
+ `Frame`/`Native` are outside rollback ownership and should be avoided.
54
+
55
+ Register each owned subscription immediately with `p.OnDispose(stop)` before
56
+ doing more work that could throw. Disposers run in reverse order, and a failing
57
+ disposer cannot prevent the remaining cleanup operations.
58
+
59
+ ## Replace
60
+
61
+ Replace mounts the factory's fresh visual JSX under the native carrier, centered
62
+ on it, and suppresses native UnitFrame alpha only after mounting succeeds. It
63
+ keeps native scripts and events registered. A dormant hook per pooled UnitFrame
64
+ maintains suppression and remembers subsequent public native alpha writes for
65
+ restoration. This does not arbitrate competing nameplate addons that also hide,
66
+ reparent or replace native visuals.
67
+
68
+ Factories run per unit binding. Do not return one cached JSX instance for several
69
+ plates: mounting rejects simultaneous reuse. Bind sources inside the factory,
70
+ for example `new HP(p.Unit)`; mounting owns their widget subscriptions. Register
71
+ additional manual subscriptions through `OnDispose`.
72
+
73
+ Nameplate add/remove events, GUID changes and reuse of either carrier or UnitFrame
74
+ dispose the previous binding before a fresh factory runs. Already visible plates
75
+ are discovered on registration and world entry. Name/faction/target events and
76
+ `Refresh()` reevaluate selection and visuals. Health events are handled by data
77
+ sources, without polling selectors. Refresh also rebuilds matching views so edited
78
+ preset values take effect. Protected/forbidden frames and secret GUIDs are skipped.
79
+
80
+ Native positioning, movement, click bounds and ordinary targeting stay with the
81
+ engine. This API does not resize the carrier or alter native click areas. Ordinary
82
+ unprotected visuals can mount/unmount during combat. Secure JSX is rejected,
83
+ including outside combat: secure nameplate buttons need a separate design for
84
+ combat-time creation, positioning and correct targeting. Fixed arena secure cells
85
+ do not establish that dynamic nameplate buttons are safe.
86
+
87
+ The Lua integration tests exercise the compiled framework with mock WoW APIs,
88
+ including actual HP source subscription cleanup and mounting failures. They do
89
+ not establish real-client behavior or compatibility with other nameplate addons.
package/lib/WuiraApp.lua CHANGED
@@ -372,22 +372,58 @@ function WuiraApp.prototype.Unmount(self, frame)
372
372
  tree.pendingUnmount = true
373
373
  return
374
374
  end
375
+ local failed = false
376
+ local failure
377
+ local function attempt(____, action)
378
+ do
379
+ local function ____catch(caught)
380
+ if not failed then
381
+ failed = true
382
+ failure = caught
383
+ end
384
+ end
385
+ local ____try, ____hasReturned = pcall(function()
386
+ action(nil)
387
+ end)
388
+ if not ____try then
389
+ ____catch(____hasReturned)
390
+ end
391
+ end
392
+ end
375
393
  for ____, dispose in ipairs(tree.bindings) do
376
- dispose(nil)
394
+ attempt(nil, dispose)
377
395
  end
378
396
  for ____, decoration in ipairs(tree.decorations) do
379
- decoration:dispose()
397
+ attempt(
398
+ nil,
399
+ function() return decoration:dispose() end
400
+ )
380
401
  end
381
402
  for ____, child in ipairs(tree.frames) do
382
- child:UnregisterAllEvents()
383
- child:SetScript("OnEvent", nil)
384
- child:SetScript("OnUpdate", nil)
385
- child:Hide()
403
+ attempt(
404
+ nil,
405
+ function() return child:UnregisterAllEvents() end
406
+ )
407
+ attempt(
408
+ nil,
409
+ function() return child:SetScript("OnEvent", nil) end
410
+ )
411
+ attempt(
412
+ nil,
413
+ function() return child:SetScript("OnUpdate", nil) end
414
+ )
415
+ attempt(
416
+ nil,
417
+ function() return child:Hide() end
418
+ )
386
419
  end
387
420
  for ____, cell in ipairs(tree.cells) do
388
421
  self.mountedFrames:delete(cell.props)
389
422
  end
390
423
  self.mountedTrees:delete(frame)
424
+ if failed then
425
+ error(failure, 0)
426
+ end
391
427
  end
392
428
  function WuiraApp.prototype.Protect(self, root, tree)
393
429
  local ____temp_8 = not tree.protected
@@ -20,11 +20,16 @@ export declare class UnitSource extends DataSource<UnitIdentity | undefined> {
20
20
  readonly Is: UnitPredicates;
21
21
  private restricted;
22
22
  private readonly listeners;
23
+ private readonly tokenListeners;
24
+ private events?;
23
25
  constructor(token: string);
24
26
  get Token(): string;
25
27
  /** Keep source objects/subscriptions while a stable identity moves to another unit slot. */
26
28
  Retarget(token: string): void;
29
+ /** Secure consumers only need notification when the actual unit address changes. */
30
+ onTokenChange(listener: () => void): () => void;
27
31
  onChange(listener: () => void): () => void;
32
+ private UpdateEvents;
28
33
  protected HasSecretFields(): boolean;
29
34
  protected GetValue(): UnitIdentity | undefined;
30
35
  }
@@ -506,32 +506,76 @@ function UnitSource.prototype.____constructor(self, token)
506
506
  self.token = token
507
507
  self.restricted = false
508
508
  self.listeners = __TS__New(Set)
509
+ self.tokenListeners = __TS__New(Set)
509
510
  self.Is = __TS__New(
510
511
  ____exports.UnitPredicates,
511
512
  function() return self.Token end
512
513
  )
513
514
  end
514
515
  function UnitSource.prototype.Retarget(self, token)
516
+ if issecretvalue(token) or type(token) ~= "string" or __TS__StringTrim(token) == "" then
517
+ error("UnitSource: expected a public unit token")
518
+ end
515
519
  if token == self.token then
516
520
  return
517
521
  end
518
522
  if InCombatLockdown() then
519
523
  error("UnitSource: retarget after combat so secure actions and displayed data stay aligned")
520
524
  end
521
- if issecretvalue(token) or type(token) ~= "string" or __TS__StringTrim(token) == "" then
522
- error("UnitSource: expected a public unit token")
523
- end
524
525
  self.token = token
526
+ self:UpdateEvents()
527
+ for ____, listener in __TS__Iterator(self.tokenListeners) do
528
+ listener(nil)
529
+ end
525
530
  for ____, listener in __TS__Iterator(self.listeners) do
526
531
  listener(nil)
527
532
  end
528
533
  end
534
+ function UnitSource.prototype.onTokenChange(self, listener)
535
+ self.tokenListeners:add(listener)
536
+ return function()
537
+ self.tokenListeners:delete(listener)
538
+ end
539
+ end
529
540
  function UnitSource.prototype.onChange(self, listener)
530
541
  self.listeners:add(listener)
542
+ self:UpdateEvents()
531
543
  return function()
532
544
  self.listeners:delete(listener)
545
+ self:UpdateEvents()
533
546
  end
534
547
  end
548
+ function UnitSource.prototype.UpdateEvents(self)
549
+ local ____opt_2 = self.events
550
+ if ____opt_2 ~= nil then
551
+ ____opt_2:UnregisterAllEvents()
552
+ end
553
+ if self.listeners.size == 0 or self.token ~= "target" then
554
+ local ____opt_4 = self.events
555
+ if ____opt_4 ~= nil then
556
+ ____opt_4:SetScript("OnEvent", nil)
557
+ end
558
+ local ____opt_6 = self.events
559
+ if ____opt_6 ~= nil then
560
+ ____opt_6:Hide()
561
+ end
562
+ self.events = nil
563
+ return
564
+ end
565
+ if self.events == nil then
566
+ self.events = CreateFrame("Frame", nil, UIParent)
567
+ end
568
+ local frame = self.events
569
+ frame:RegisterEvent("PLAYER_TARGET_CHANGED")
570
+ frame:SetScript(
571
+ "OnEvent",
572
+ function()
573
+ for ____, listener in __TS__Iterator(self.listeners) do
574
+ listener(nil)
575
+ end
576
+ end
577
+ )
578
+ end
535
579
  function UnitSource.prototype.HasSecretFields(self)
536
580
  return self.restricted
537
581
  end
package/lib/index.d.ts CHANGED
@@ -16,6 +16,8 @@ export { UI } from "./ui/primitives/jsx";
16
16
  export { HoverTracker } from "./ui/HoverTracker";
17
17
  export type { HoverTrackerOptions, HoverFrame } from "./ui/HoverTracker";
18
18
  export { MicroBar } from "./skins/MicroBar";
19
+ export { Nameplates, NameplateRules, NameplateContext } from "./skins/Nameplates";
20
+ export type { NameplateInfo, NameplateSelector, NameplateCustomize, NameplateRender, NativeNameplate, NativeNameplateVisual } from "./skins/Nameplates";
19
21
  export type { Element, Component } from "./ui/primitives/jsx";
20
22
  export type { Cell } from "./ui/Cell";
21
23
  export { Store, StoreRegistry } from "./store/index";
package/lib/index.lua CHANGED
@@ -38,6 +38,12 @@ do
38
38
  local ____MicroBar = require("skins.MicroBar")
39
39
  ____exports.MicroBar = ____MicroBar.MicroBar
40
40
  end
41
+ do
42
+ local ____Nameplates = require("skins.Nameplates")
43
+ ____exports.Nameplates = ____Nameplates.Nameplates
44
+ ____exports.NameplateRules = ____Nameplates.NameplateRules
45
+ ____exports.NameplateContext = ____Nameplates.NameplateContext
46
+ end
41
47
  do
42
48
  local ____index = require("store.index")
43
49
  ____exports.Store = ____index.Store
@@ -35,6 +35,8 @@ interface wuiraNativeFrame {
35
35
  SetScript(script: "OnMouseWheel", handler: ((this: void, frame: wuiraNativeFrame, delta: number) => void) | undefined): void;
36
36
  GetName(): string | undefined;
37
37
  IsProtected?(): boolean;
38
+ IsForbidden?(): boolean;
39
+ GetAlpha(): number;
38
40
  GetParent(): wuiraNativeFrame;
39
41
  GetNumPoints(): number;
40
42
  GetPoint(index: number): wuiraNativeReturns<[wuiraAnchor, wuiraNativeFrame, wuiraAnchor, number, number]>;
@@ -95,6 +97,7 @@ declare const UIParent: wuiraNativeFrame;
95
97
  declare const _G: Record<string, unknown>;
96
98
  declare const SlashCmdList: Record<string, (this: void, message: string) => void>;
97
99
  interface wuiraNativeStatusBar extends wuiraNativeFrame {
100
+ GetStatusBarColor(): wuiraNativeReturns<[number, number, number, number]>;
98
101
  GetStatusBarTexture(): wuiraNativeTexture;
99
102
  SetMinMaxValues(min: number, max: number): void;
100
103
  SetValue(value: number): void;
@@ -0,0 +1,56 @@
1
+ import type { Cell } from "../ui/Cell";
2
+ export interface NativeNameplate extends wuiraNativeFrame {
3
+ readonly UnitFrame?: NativeNameplateVisual;
4
+ readonly namePlateUnitToken?: string;
5
+ GetUnit?(): string | undefined;
6
+ }
7
+ export interface NativeNameplateVisual extends wuiraNativeFrame {
8
+ readonly healthBar?: wuiraNativeStatusBar;
9
+ readonly castBar?: wuiraNativeStatusBar;
10
+ }
11
+ export interface NameplateInfo {
12
+ readonly Unit: string;
13
+ readonly GUID: string;
14
+ /** Engine-owned carrier. Do not mutate it or attach secure controls through this API. */
15
+ readonly Frame: NativeNameplate;
16
+ readonly Native: NativeNameplateVisual;
17
+ }
18
+ export type NameplateSelector = (this: void, plate: NameplateInfo) => boolean;
19
+ export type NameplateCustomize = (this: void, plate: NameplateContext) => void;
20
+ export type NameplateRender = (this: void, plate: NameplateContext) => Cell;
21
+ /** One unit binding. Use the reversible methods; direct native writes are not tracked. */
22
+ export declare class NameplateContext implements NameplateInfo {
23
+ readonly Unit: string;
24
+ readonly GUID: string;
25
+ readonly Frame: NativeNameplate;
26
+ readonly Native: NativeNameplateVisual;
27
+ private active;
28
+ private readonly cleanup;
29
+ constructor(Unit: string, GUID: string, Frame: NativeNameplate, Native: NativeNameplateVisual);
30
+ /** Register owned subscriptions/sources immediately, before subsequent work can throw. */
31
+ OnDispose(cleanup: () => void): void;
32
+ private check;
33
+ Alpha(frame: wuiraNativeFrame, value: number): void;
34
+ Scale(frame: wuiraNativeFrame, value: number): void;
35
+ Size(frame: wuiraNativeFrame, width: number, height: number): void;
36
+ BarColor(bar: wuiraNativeStatusBar, r: number, g: number, b: number, a?: number): void;
37
+ /** Internal lifecycle endpoint; cleanup failures cannot prevent other restorations. */
38
+ Dispose(): void;
39
+ }
40
+ /** An independently disposable ordered registration group. First matching rule wins globally. */
41
+ export declare class NameplateRules {
42
+ private disposed;
43
+ private add;
44
+ Apply(selector: NameplateSelector, customize: NameplateCustomize): this;
45
+ Replace(selector: NameplateSelector, render: NameplateRender): this;
46
+ /** Re-evaluate after external preset/selector data changes; native events handle unit changes. */
47
+ Refresh(): void;
48
+ Dispose(): void;
49
+ }
50
+ /** Each entry call creates a new group; chained calls append to that group. */
51
+ export declare class Nameplates {
52
+ private constructor();
53
+ static Apply(selector: NameplateSelector, customize: NameplateCustomize): NameplateRules;
54
+ static Replace(selector: NameplateSelector, render: NameplateRender): NameplateRules;
55
+ static Create(): NameplateRules;
56
+ }