wuira 0.2.6 → 0.2.7

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,22 @@ with the release date and highlight breaking changes explicitly.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## 0.2.7 — 2026-10-16
11
+
12
+ ### Added
13
+
14
+ - Generic `Nameplates.Apply` / `Replace` registration groups, with global first-match
15
+ precedence, fresh per-unit JSX factories, reversible native visual helpers,
16
+ event-driven pooled-frame lifecycle, explicit refresh, and independent disposal.
17
+ Forbidden/protected frames and secret identities are skipped; secure JSX is rejected.
18
+ - Lua integration coverage for nameplate reuse, rule conflicts, rollback, native
19
+ restoration and HP subscription cleanup. These are mock tests, not in-game validation.
20
+
21
+ ### Fixed
22
+
23
+ - Continue unmount cleanup after a binding or decoration disposer throws, so remaining
24
+ subscriptions and frames are released before the first error is reported.
25
+
10
26
  ## 0.2.6 — 2026-10-16
11
27
 
12
28
  ### 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
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
+ }