wuira 0.2.4 → 0.2.6

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
@@ -1,22 +1,14 @@
1
- # wuira
2
-
3
- Build WoW Forever interfaces with TypeScript and JSX.
4
-
5
- wuira is a UI framework and addon toolchain for developers who want to describe their interface in code, compose reusable components, and keep game data separate from presentation. Write TypeScript, arrange native widgets with JSX, and compile your addon to Lua.
6
1
 
7
- ## Why wuira?
8
2
 
9
- An interface should be easy to understand by reading its source. A health display can be a progress bar inside a layout; a player display can be a composition of bars, labels, and icons rather than a special unit-frame class. wuira provides the foundations for that approach, with TypeScript checking your component props before the addon reaches the game.
10
-
11
- The snippets below use the lowercase JSX primitives. In a generated project, import `{ UI, Wuira }` from `wuira` before mounting a view. Each view example is independent. Wuira is a static accessor; Wuira.App is the current addon's WuiraApp instance and owns its mounted trees and cleanup. Separate addons keep independent application state. The lowercase wuira export remains available for existing addons.
12
-
13
- ### JSX composition
3
+ # wuira
14
4
 
15
- <!-- Screenshot: a reusable ResourceBar component inside a Cell. -->
5
+ Build WoW Forever interfaces with TypeScript and JSX.
16
6
 
17
- Describe UI trees with components and reusable functions. No separate XML view files are required.
7
+ wuira is a UI framework and addon toolchain for developers who want to describe interfaces in code, compose reusable components, and keep game data separate from presentation. Write TypeScript, arrange native widgets with JSX, and compile your addon to Lua.
18
8
 
19
9
  ```tsx
10
+ import { UI, wuira } from "wuira";
11
+
20
12
  function ResourceBar({ current }: { current: number }) {
21
13
  return <progress width="inherit" height={24} min={0} max={100} current={current} />;
22
14
  }
@@ -29,675 +21,26 @@ function App() {
29
21
  );
30
22
  }
31
23
 
32
- Wuira.Mount(App);
33
- ```
34
-
35
- ### Native widgets
36
-
37
- <!-- Screenshot: a native StatusBar with a dark background. -->
38
-
39
- Cells mount to WoW frames, and progress bars use the game's native StatusBar widget.
40
-
41
- ```tsx
42
- const view = (
43
- <progress
44
- width={200} height={24}
45
- min={0} max={100} current={65}
46
- foreground={{ color: { r: 0.1, g: 0.75, b: 0.4 } }}
47
- background={{ color: { r: 0.08, g: 0.08, b: 0.08 } }}
48
- />
49
- );
50
-
51
- Wuira.Mount(view);
52
- ```
53
-
54
- ### Layout primitives
55
-
56
- <!-- Screenshot: two vertically stacked progress bars with a gap. -->
57
-
58
- Flow and grid layouts arrange children, with spacing and sizing options shared across components.
59
-
60
- ```tsx
61
- const view = (
62
- <flow width={200} height={56} direction="vertical" gap={8}>
63
- <progress height={24} min={0} max={100} current={80} />
64
- <progress height={24} min={0} max={100} current={45} />
65
- </flow>
66
- );
67
-
68
- Wuira.Mount(view);
69
- ```
70
-
71
- For rows and columns, use `<grid columns={2} gap={8}>` with an explicit width and height.
72
-
73
- ### Data sources
74
-
75
- <!-- Screenshot: live player health and mana bars. -->
76
-
77
- Connect health and mana values to progress bars without writing a separate resource-specific container class.
78
-
79
- ```tsx
80
- import { HP, Mana, Unit } from "wuira/datasources";
81
-
82
- function App() {
83
- const hp = new HP(Unit.Player);
84
- const mana = new Mana(Unit.Player);
85
-
86
- return (
87
- <flow width={200} height={52} direction="vertical" gap={4}>
88
- <progress height={24} source={hp} />
89
- <progress height={24} source={mana} />
90
- </flow>
91
- );
92
- }
93
-
94
- Wuira.Mount(App);
95
- ```
96
-
97
- ### Conditional sources and health predictions
98
-
99
- ```tsx
100
- import { UI } from "wuira";
101
- import { ProgressBar } from "wuira/ui";
102
- import { HP, Unit } from "wuira/datasources";
103
-
104
- const hp = new HP(Unit.Player)
105
- .UseHealPrediction()
106
- .UseAbsorbPrediction()
107
- .When(value => value >= 20); // Current health points, not a percentage.
108
-
109
- const view = (
110
- <ProgressBar width={220} height={24} source={hp}
111
- healPrediction={{ texture: "blizzard", color: { r: 0.4, g: 1, b: 0.5, a: 0.5 } }}
112
- absorbPrediction={{ texture: "solid", color: { r: 0.8, g: 0.9, b: 1, a: 0.55 } }} />
113
- );
114
- ```
115
-
116
- Configure sources before mounting. `.When()` filters native binding updates using
117
- the source's own value; repeated calls combine with AND. A false condition hides
118
- the bound bar and suppresses writes until a relevant event makes it true again.
119
- The basic health events remain subscribed so the source can reactivate; there is
120
- no health polling. Predicates should depend on this source's event-driven state,
121
- not unrelated mutable values. `.Value` remains a fresh, checked snapshot rather
122
- than a filtered subscription. Secret values never reach condition callbacks:
123
- a conditional binding becomes inactive until readable data returns. Without a
124
- condition, opaque values continue directly to native widgets.
125
-
126
- Each prediction channel is allocated and its event registered only when explicitly
127
- enabled and supported. No prediction snapshots are cached. Access
128
- `hp.HealPrediction?.Value` and `hp.AbsorbPrediction?.Value` when readable, or check
129
- the channel's `IsAvailable` flag. Direct reads retain the usual secret-value guard.
130
- The APIs provide incoming healing and existing damage-absorb shields, not a
131
- forecast of future damage. The adapter uses
132
- [Blizzard's unit prediction APIs](https://github.com/Gethe/wow-ui-source/blob/live/Interface/AddOns/Blizzard_APIDocumentationGenerated/UnitDocumentation.lua).
133
-
134
- Enabled channels draw by default; `healPrediction={false}` or
135
- `absorbPrediction={false}` suppresses an individual overlay. Incoming healing starts
136
- at the native health-fill edge and clips at the bar boundary. Shields overlay
137
- inward from the opposite edge, remaining visible even at full health. Both accept
138
- independent textures/colors and support horizontal, vertical, and reversed bars.
139
- There is no arithmetic on secret health or prediction amounts. Raw incoming heals
140
- are not reduced by heal-absorb debuffs; overheal is visually clipped. Prediction
141
- overlays require the native health scale, so min/max/current overrides are rejected.
142
- Prediction reads pause while the parent HP condition is inactive. The renderer
143
- cleans up the overlays and resource subscriptions on unmount.
144
-
145
- ### Slash commands
146
-
147
- ```ts
148
- import { Wuira } from "wuira";
149
-
150
- const unregister = Wuira.registerCommand("/myaddon", (...args: string[]) => {
151
- console.log("Arguments:", ...args);
152
- });
153
-
154
- // /myaddon arg1 arg2 arg3 passes three string arguments.
155
- // /myaddon alone calls the callback with no arguments.
156
- // Call unregister() when the command is no longer needed.
157
- ```
158
-
159
- Command names start with a slash and a letter, followed by letters, digits,
160
- underscores or hyphens. Names are case-insensitive and cannot replace an existing
161
- command. Register aliases separately with the same callback.
162
-
163
- Arguments are separated by whitespace and retain their case. Single or double
164
- quotes group words into one argument; `""` passes an empty string. Within quotes,
165
- a backslash can escape the matching quote or another backslash. An unclosed quote
166
- reports an error without calling the callback. Argument counts are unrestricted;
167
- validate required arguments inside your callback.
168
-
169
- ### Addon settings and editing tools
170
-
171
- Create a standalone window with mounted JSX content. Bind a header as its drag
172
- handle; Escape or `Close()` releases the content while preserving the host's
173
- screen position for the next opening:
174
-
175
- ```tsx
176
- import { Window, FlowLayout, Label } from "wuira/ui";
177
-
178
- const window = new Window({ width: 1000, height: 680 });
179
- window.Open(() => (
180
- <FlowLayout onMount={frame => window.BindDragHandle(frame)}>
181
- <Label height={40} text="My addon" />
182
- </FlowLayout>
183
- ));
184
- ```
185
-
186
- Register a normal JSX view in Forever's native addon settings list:
187
-
188
- ```tsx
189
- import { UI, AddonSettings } from "wuira";
190
- import { FlowLayout, Button, Label } from "wuira/ui";
191
-
192
- const settings = new AddonSettings("MyAddon", () => (
193
- <FlowLayout width={560} height={300} padding={20} gap={12}>
194
- <Label height={28} text="MyAddon settings" />
195
- <Button width={180} height={32} onClick={() => console.log("Settings opened")}>
196
- <Label text="Example control" align="CENTER" />
197
- </Button>
198
- </FlowLayout>
199
- ));
200
- settings.Open();
201
- ```
202
-
203
- `Cell.onMount` receives the configured native frame and may return an unmount
204
- cleanup. `FrameMover` provides a drag proxy with screen-center coordinates
205
- (positive y down). `KeybindCapture` provides hover-to-bind overlays, modifier keys,
206
- Delete/Backspace clearing, and an Escape callback. These tools stop editing in
207
- combat. Addons own mode controls, saved positions, binding conflict policy and
208
- rebuilding protected buttons outside combat.
209
-
210
- ### Learned spells
211
-
212
- `KnownSpells` keeps a cached snapshot of learned player spellbook IDs. It refreshes
213
- on world entry, level, training and talent events without polling. Named entries
214
- in `Spells` are numeric IDs; `Has` also matches a representative ID against learned
215
- ranks by localized name. Future and inactive-spec spellbook entries are excluded.
216
-
217
- ```ts
218
- import { Spells } from "wuira";
219
- import { KnownSpells } from "wuira/datasources";
220
-
221
- const known = new KnownSpells();
222
- const spellIds = known.Value; // readonly number[], read from the cache
223
- const canJudge = known.Has(Spells.Judgement);
224
- const activeSpells = known.Entries.filter(spell => spell.isPassive === false);
225
- const learnedRank = known.Find(Spells.Judgement); // { id, name, isPassive } or undefined
226
- const seals = known.Filter("Seal*"); // Detached matching entries, including learned ranks.
227
- const stop = known.onChange(ids => console.log("Learned spells:", ids.length));
228
- // On disposal: stop(); known.Dispose();
24
+ wuira.Mount(App);
229
25
  ```
230
26
 
231
- `Entries` returns detached cached metadata. An unknown passive flag stays
232
- `undefined`; unreadable spellbooks return no entries. Metadata changes also notify
233
- `onChange` observers, even when the spell IDs have not changed.
27
+ ## Highlights
234
28
 
235
- `Filter` matches the entire localized name. `*` matches any sequence and `?` one
236
- byte; matching ignores ASCII case. Filtering reads the cache without polling the
237
- spellbook. Patterns must use names in the client's language.
238
-
239
- Protected button trees must be rebuilt outside combat; applications can defer a
240
- changed selection until `PLAYER_REGEN_ENABLED`. `Cell.alpha` changes appearance
241
- without changing a button's protected action or its interaction state.
242
-
243
- For wrapped action rows, `FlowLayout` supports `grow` and per-line `justify`:
244
-
245
- ```tsx
246
- <FlowLayout width={220} height={52} direction="horizontal" gap={4} grow justify="center">
247
- {buttons /* 24-unit buttons wrap after eight; the last row is centered. */}
248
- </FlowLayout>
249
- ```
250
-
251
- Use `ForEach` to expand a snapshot into layout children without wrapper frames:
252
-
253
- ```tsx
254
- import { UI } from "wuira";
255
- import { FlowLayout, ForEach, SecureButton } from "wuira/ui";
256
-
257
- function SealButtons() {
258
- return (
259
- <FlowLayout width={220} height={24} direction="horizontal" gap={4}>
260
- <ForEach items={known.Filter("Seal*")}>
261
- {(spell, index) => (
262
- <SecureButton width={24} height={24}
263
- action={{ type: "spell", spell: spell.id }} condition={spell.isPassive === false} />
264
- )}
265
- </ForEach>
266
- </FlowLayout>
267
- );
268
- }
269
- ```
270
-
271
- The render function receives the item and its zero-based index. Use `ForEach`
272
- inside a layout for multiple siblings; Cell still accepts at most one expanded
273
- child. Nested loops are flattened in order. Array inputs create no subscriptions:
274
- recreate the view from `known.onChange` when needed, outside combat for protected
275
- trees. Each filtered rank remains a separate entry. Observable list sources also
276
- support keyed updates, as shown below under Group members.
277
-
278
- All visual cells accept `condition={boolean}` or `condition={() => boolean}`.
279
- This controls visibility and retains the layout slot. It combines with datasource
280
- activity and the existing `visible` predicate. Callbacks use the shared visibility
281
- update driver; protected changes wait until combat ends. Secret conditions hide
282
- the cell without inspecting their value.
283
-
284
- ### Empty native action bars
285
-
286
- This is opt-in and removes real action-slot assignments, not just their visuals.
287
- Declare a character-specific backup variable in the addon's TOC:
288
-
289
- ```text
290
- ## SavedVariablesPerCharacter: MyAddonActionBarBackup
291
- ```
292
-
293
- ```ts
294
- import { ActionBars } from "wuira";
295
-
296
- ActionBars.InstallEmptyMode({
297
- addon: "MyAddon",
298
- variable: "MyAddonActionBarBackup",
299
- clearCommand: "/myaddonclearbars",
300
- restoreCommand: "/myaddonrestorebars",
301
- });
302
- ```
303
-
304
- The first world entry captures supported spell, item, and macro assignments.
305
- Use `/reload` to persist a new backup, then `/myaddonclearbars` outside combat to
306
- clear the 180 action slots used by the installed Forever implementation. Slot
307
- changes run directly from the command, never from world-entry or cursor events.
308
- Independently, initialization hides stock bars, removes their descendant event/update handlers, disables their
309
- shared action-bar event managers, and detaches native action UI registrations.
310
- It does not create a replacement bar or remove key-binding assignments.
311
- Changes wait until combat ends, and a busy cursor blocks slot changes.
312
-
313
- The restore command puts assignments back into empty or identical slots. Use
314
- `/reload` afterwards to restore native handlers and visibility. The stock bars
315
- remain enabled on future loads. A refused slot operation leaves bar suppression
316
- active and is not automatically retried. Blocked/forbidden native calls report
317
- their API name in the local console; command execution does not bypass client restrictions.
318
- Unsupported action types, secret slot data, uncovered assignments, and conflicting
319
- restores stop the operation. `Capture()`, `Clear(backup)`, and `Restore(backup)` are
320
- also available for applications that manage their own persistence and lifecycle.
321
-
322
- ### Secure action buttons
323
-
324
- `SecureButton` extends Cell's descriptor and props, including size, positioning,
325
- decorations and one child. Mount it like any other component:
326
-
327
- ```tsx
328
- import { UI, Wuira } from "wuira";
329
- import { SecureButton, Label } from "wuira/ui";
330
-
331
- Wuira.Mount(() => (
332
- <SecureButton width={80} height={24}
333
- action={{ type: "spell", spell: 635, unit: "player" }}>
334
- <Label text="Holy Light" align="CENTER" />
335
- </SecureButton>
336
- ));
337
- ```
338
-
339
- Supported actions are `{ type: "spell", spell, unit? }`,
340
- `{ type: "item", item, unit? }`, `{ type: "macro", macro }`,
341
- `{ type: "action", slot, unit? }` and `{ type: "target", unit }`.
342
- Spell and macro references accept IDs/indices or names. Items accept an ID
343
- (converted to `item:ID`) or the native item string. Macros refer to existing
344
- game macros, not arbitrary Lua callbacks or generated macro text.
345
-
346
- The default is left-button release; `button="RightButton"` selects another
347
- button. Secure buttons always receive mouse input, while visual children remain
348
- click-through by default. Omitting `action` creates an inert secure button.
349
-
350
- Actions and units are static mount-time attributes. Changing the descriptor
351
- later does not rewrite them. Mounting a protected tree during combat raises an
352
- error before any frame is created. Visibility changes and unmount requests wait
353
- until combat ends, including changes to ancestors containing secure buttons.
354
- Click-cast presets are not provided by this primitive yet. The native action mapping
355
- follows [Forever's secure templates](https://github.com/Gethe/wow-ui-source/blob/forever/Interface/AddOns/Blizzard_FrameXML/SecureTemplates.lua).
356
-
357
- For an icon action with a radial cooldown and temporary key binding:
358
-
359
- ```tsx
360
- import { Spells } from "wuira";
361
- import { SpellSource } from "wuira/datasources";
362
- import { Icon } from "wuira/ui";
363
-
364
- const judgement = new SpellSource(Spells.Judgement);
365
- const button = (
366
- <SecureButton width={40} height={40} action={judgement.Action}
367
- binding="1" cooldown={judgement.Cooldown} finishGlow>
368
- <Icon textureSource={judgement.Texture} />
369
- </SecureButton>
370
- );
371
- ```
372
-
373
- `binding` temporarily routes the key to this native button and adds its label;
374
- `showHotkey={false}` suppresses the label. Unmount removes the override and leaves
375
- saved bindings intact. The radial widget owns countdown timing and the short
376
- completion flash, including opaque combat durations. `SpellSource.Cooldown`
377
- ignores the GCD so unrelated casts do not trigger a completion flash.
378
-
379
- ### Native skins
380
-
381
- Skin Forever's existing character, professions, spellbook and other micro-menu
382
- buttons while keeping the game's click handlers, tooltips and notifications:
383
-
384
- ```tsx
385
- import { UI } from "wuira";
386
- import { MicroBar } from "wuira/skins";
387
- import { Cell, Background } from "wuira/ui";
388
-
389
- const micro = new MicroBar();
390
- micro.render(bar => {
391
- bar.Style({
392
- background: { r: 0.035, g: 0.045, b: 0.065, a: 0.9 },
393
- accent: { r: 0.4, g: 0.75, b: 1 },
394
- padding: 6,
395
- });
396
- bar.mount(() => (
397
- <Cell width={12} height={2} decorations={{
398
- background: new Background({ color: { r: 0.4, g: 0.75, b: 1 } }),
399
- }} />
400
- ), "TOP");
401
- bar.OnHover(hovered => console.log("Micro menu hover:", hovered));
402
- return bar;
403
- });
404
- ```
405
-
406
- `render` receives the skin adapter once its game-owned frame is ready. The same
407
- object exposes `Frame`, `Style`, `OnHover`, and `mount`; mounts become children of
408
- the native frame and are released by `Dispose()`. Native clicks, events and
409
- tooltips remain intact. Rendering waits until combat ends.
410
-
411
- `HoverTracker` supplies generic secure trigger/popup frames and auto-hide regions;
412
- the addon owns their visuals, contents and positioning. Compact menu launchers
413
- and themes belong in the consuming UI rather than the framework. Hover callbacks
414
- are ordinary addon callbacks; use secure hover visibility for protected panels.
415
-
416
- `BlizzardUI.SkinMicroMenu(options)` is also available for styling alone. Stopping
417
- styling hides its added panel; reload restores original native textures.
418
-
419
- ### Resource observation and mana regen
420
-
421
- Resource changes can also be observed without mounting a widget:
422
-
423
- ```ts
424
- const mana = new Mana(Unit.Player).UseRegenTimer();
425
- const unsubscribe = mana.onChange(value => console.log(value));
426
- // Call unsubscribe() when this consumer is removed.
427
- ```
428
-
429
- `onChange` reports future readable value changes, with no initial callback.
430
- It honors `.When()` and never sends secret values to user code. Native bar
431
- bindings still accept secret values directly. Bars, observers, and the regen
432
- accessory share one lazy native event listener; it unregisters when the last
433
- consumer detaches. `.Value` is still available for a one-time read.
434
-
435
- `mana.RegenTimer` can be bound to a thin `ProgressBar`. Its `Phase` is
436
- `fiveSecondRule`, `estimatedTick`, or `inactive`. Successful mana-cost casts
437
- start/restart a five-second window, followed by estimated two-second cycles.
438
- Mana events keep those cycles alive without resetting a running cycle. No
439
- server tick clock is exposed: this is an estimate, including when the cost is
440
- secret. Zero-cost casts do not restart the window. The native widget animates
441
- the fill; one-shot callbacks handle phase transitions, with no Lua frame polling.
442
- Full readable mana cancels the clock. When mana is secret, a native step curve
443
- hides full mana visually; Lua cannot use that hidden result to dispose the timer,
444
- so the estimate stops when mana events cease. The frame is reused on the next
445
- cast. Configure this player-only accessory before mounting.
446
-
447
- ### Native aura, cooldown, and cast timers
448
-
449
- ```tsx
450
- import { UI, Spells } from "wuira";
451
- import { FlowLayout, ProgressBar, Icon, Label } from "wuira/ui";
452
- import { AuraTimer, GlobalCooldown, CastTimer, Unit } from "wuira/datasources";
453
-
454
- const aura = new AuraTimer(Unit.Player, { spellIds: [Spells.SealOfRighteousness], filter: "HELPFUL" });
455
- const gcd = new GlobalCooldown();
456
- const cast = new CastTimer(Unit.Player);
457
-
458
- function Timers() {
459
- return (
460
- <FlowLayout width={200} height={94} direction="vertical" gap={2}>
461
- <Icon width={24} height={24} textureSource={aura.Texture} />
462
- <ProgressBar height={24} source={aura}><Label text="Aura" /></ProgressBar>
463
- <ProgressBar height={18} source={gcd} />
464
- <ProgressBar height={22} source={cast} />
465
- </FlowLayout>
466
- );
467
- }
468
- ```
469
-
470
- These timers pass opaque native duration objects to `SetTimerDuration`; they do
471
- not calculate restricted durations in Lua. `SpellCooldown(spellId)` tracks a
472
- specific spell, and `GlobalCooldown` supplies the GCD identifier internally.
473
- `CastTimer` follows casts and channels. `AuraTimer` resolves localized spell names
474
- and exposes an associated `Texture` source, with status indicating active,
475
- inactive, restricted, or unavailable data. Aura access still follows the client's
476
- restrictions; missing readable data is not reconstructed from combat events.
477
- `Label` and `Icon` are visual Cells. Application code supplies which spells to
478
- track; native API declarations and reads remain in wuira.
479
-
480
- ### Swing timers
481
-
482
- ```tsx
483
- import { UI, Wuira } from "wuira";
484
- import { ProgressBar } from "wuira/ui";
485
- import { SwingTimer, SwingHand } from "wuira/datasources";
486
-
487
- const mainHand = new SwingTimer(SwingHand.MainHand);
488
- const view = Wuira.Mount(<ProgressBar width={200} height={12} source={mainHand} />);
489
-
490
- // Other choices: SwingHand.OffHand and SwingHand.Ranged.
491
- // mainHand.Value: remaining seconds; Max.Value: duration; Percent.Value: remaining percent.
492
- // When removing the feature:
493
- if (view !== undefined) wuira.Unmount(view);
494
- mainHand.Dispose();
495
- ```
29
+ - **JSX composition** — Describe UI trees with components. No separate XML view files.
30
+ - **Native widgets** — Cells mount to WoW frames; progress bars use the native StatusBar widget.
31
+ - **Layout primitives** — Arrange children with `flow` and `grid`.
32
+ - **Data sources** — HP, Mana, auras, cooldowns, casts, swings, group members, class/spec, spellbook.
33
+ - **Source converters** — Transform checked values with typed callbacks. See [source converters](docs/source-converters.md).
34
+ - **Slash commands** — Register commands with quoting and escaping support.
35
+ - **Windows and settings** — `Window`, `AddonSettings`, `FrameMover`, `KeybindCapture`.
36
+ - **Secure buttons** — Spell, item, macro, action, and target actions with `SecureButton`.
37
+ - **Native skins** — Style Blizzard UI through adapters such as `MicroBar`.
38
+ - **Stores** — Snapshot-based data storage with SavedVariables support.
39
+ - **Client catalogs** — Generated [spell enums](docs/spells.md) and [icon enums](docs/icons.md) from the pinned Forever client data.
496
40
 
497
- This source follows the player's `PLAYER_SWING(duration, swingType)` events.
498
- It does not reconstruct swings from the combat log or track enemy attack cycles.
499
- `HasTiming` is false until the first readable event and after a weapon/world reset;
500
- zero before that event is not evidence of a synchronized swing. After a recorded
501
- cycle expires, remaining time reaches zero. Secret payloads block datasource
502
- computation until a readable event or reset arrives.
41
+ ## Installation
503
42
 
504
- Native progress binding uses `C_DurationUtil` and `StatusBar:SetTimerDuration`,
505
- with no Lua per-frame animation. Range/current overrides are not supported for
506
- this binding. Unmount detaches each bar; call `Dispose()` to stop the source's
507
- event listener when its owning feature ends. Support is capability-checked;
508
- actual timing still needs validation in Forever. The adapter follows the
509
- [Forever swing API definition](https://github.com/Gethe/wow-ui-source/blob/bd2470aed543f72697a044e989285b6c83e63f73/Interface/AddOns/Blizzard_APIDocumentationGenerated/SwingTimerDocumentation.lua).
510
-
511
- ### Native Edit Mode layouts
512
-
513
- ```ts
514
- import { EditModeLayouts } from "wuira/datasources";
515
-
516
- const layouts = new EditModeLayouts();
517
- console.log(layouts.Active?.name);
518
- const stop = layouts.onChange(snapshot => console.log(snapshot.active?.key));
519
- // On teardown: stop(); layouts.Dispose();
520
- ```
521
-
522
- `Layouts` lists native indices, names, types and derived keys. `Value` is a detached
523
- snapshot; native events refresh it without polling. Check `Status` for availability.
524
- Forever exposes an index rather than a persistent UUID: `key` combines type,
525
- interface style and name, survives index shifts, and changes on rename. These keys
526
- describe local game layouts, not portable layout exports.
527
-
528
- ### Group members
529
-
530
- ```ts
531
- import { GroupMembers, PartyMembers, RaidMembers, Buffs } from "wuira/datasources";
532
-
533
- const group = new GroupMembers();
534
- for (const unit of group.Value) {
535
- const buffs = new Buffs(unit);
536
- // Apply your own buff-tracking rules to buffs.Value when readable.
537
- }
538
-
539
- const party = new PartyMembers();
540
- const raid = new RaidMembers();
541
- ```
542
-
543
- `GroupMembers` returns `["player"]` while solo, `player` plus `partyN` tokens
544
- in a party, and `raidN` tokens in a raid. `PartyMembers` is empty outside a
545
- party; `RaidMembers` is empty outside a raid. The player is included once.
546
- Offline members are retained. Each `.Value` access reads the current roster.
547
- `onChange` subscribes to roster, pet, connection and name events without polling;
548
- unsubscribe or call `Dispose()` when done. Tokens may refer to different members
549
- after roster changes and should not be used as persistent identities.
550
-
551
- `GroupRoster` adds GUID-based keys, full-name/realm fallback, pet ownership and
552
- raid subgroup metadata. Pass it directly to `ForEach` to retain mounted cells and
553
- their resource subscriptions while the roster changes:
554
-
555
- ```tsx
556
- import { UI, Wuira } from "wuira";
557
- import { ForEach, ProgressBar, type Element } from "wuira/ui";
558
- import { GroupRoster, HP, UnitSource } from "wuira/datasources";
559
-
560
- const roster = new GroupRoster("party", { pets: true, includeSelfWhenSolo: true });
561
- const units = new Map<Element, UnitSource>();
562
- const view = (
563
- <ForEach items={roster} key={member => member.key}
564
- direction="vertical" wrapAfter={5} gap={4} lineGap={8}
565
- onUpdate={(view, member) => units.get(view)!.Retarget(member.unit)}
566
- onDispose={view => { units.delete(view); }}>
567
- {member => {
568
- const unit = new UnitSource(member.unit);
569
- const view = <ProgressBar width={160} height={24} source={new HP(unit)} />;
570
- units.set(view, unit);
571
- return view;
572
- }}
573
- </ForEach>
574
- );
575
- const frame = Wuira.Mount(view)!;
576
- // On teardown: Wuira.Unmount(frame); roster.Dispose();
577
- ```
578
-
579
- Observable inputs expose `Value` and `onChange`, and require unique, public keys.
580
- Only new keys call the child factory; removed keys unmount and call `onDispose`.
581
- Use `onUpdate` to update retained views when their unit token or metadata changes.
582
- Structural changes wait until combat ends. The observable `ForEach` owns its
583
- layout and sizes its native host; mount it as a root or reserve an explicit slot
584
- in its parent layout. `groupBy` can arrange raid subgroups in separate columns.
585
-
586
- ### Player class and specialization
587
-
588
- ```ts
589
- import { UnitSource, ClassSource, SpecSource, Unit } from "wuira/datasources";
590
-
591
- const target = new UnitSource(Unit.Target);
592
- const playerClass = new ClassSource(target);
593
- const specs = new SpecSource(target);
594
-
595
- console.log(target.Is.PC); // boolean | undefined when restricted
596
- console.log(playerClass.Value?.token);
597
- specs.RequestInspect();
598
- // Later, after INSPECT_READY: specs.Value contains native specialization data.
599
- // Each entry exposes id, name, icon, role, and pointsSpent when provided.
600
- // Call specs.Dispose() when the owning feature is removed or cancels the request.
601
- ```
602
-
603
- These sources accept real players only; NPCs produce no data or inspect request.
604
- The same `UnitSource` can be passed to `Buffs`, `Debuffs`, `HP`, and `Mana`.
605
- Its `Value` contains the current token and GUID, rather than a cached identity.
606
- `Retarget(token)` changes a shared reference outside combat; bound HP/Mana widgets
607
- refresh without replacing their sources. `onChange` observes these explicit token
608
- changes, not every change to the native unit's identity.
609
- `Is.PC` and `Is.Self` return `undefined` when their native result is secret.
610
- String unit tokens are still supported as a convenience shorthand.
611
- Own specialization data is read directly. Other players require an explicit,
612
- out-of-combat inspect request and a matching response GUID. An unknown result
613
- remains `undefined`, and a changed unit token cannot expose another player's
614
- cached result. Secret values are guarded by the datasource API. Requests are
615
- not globally queued or automatically retried; callers own cancellation and
616
- request timing. Inspect permission, range, hostile-player support, and the
617
- meaning of talent-point fields must be verified on Forever. Being a player
618
- does not by itself guarantee inspect access.
619
-
620
- ### Aura ownership
621
-
622
- Buff and debuff sources expose fresh snapshots with caster information. Application
623
- code decides which blessings or other effects should be displayed.
624
-
625
- ```ts
626
- import { Buffs, Debuffs, Unit } from "wuira/datasources";
627
-
628
- const buffs = new Buffs(Unit.Target);
629
- const debuffs = new Debuffs(Unit.Target);
630
-
631
- buffs.Compute(auras => {
632
- for (const aura of auras) {
633
- if (aura.isSourceSelf() === true) {
634
- console.log(aura.name, "applied by me");
635
- } else if (aura.isSourceSelf() === false) {
636
- console.log(aura.name, "applied by", aura.sourceUnit);
637
- }
638
- // undefined means ownership is unknown, not that another player cast it.
639
- }
640
- });
641
- ```
642
-
643
- `sourceStatus` is `known`, `unknown`, or `restricted`. Restricted caster values
644
- are not exposed. Secret spell/name/icon data blocks `.Value` and `.Compute` through
645
- the normal datasource guard; `.IsSecret()` reports it. Missing native API support
646
- raises an explicit error instead of returning an apparently empty list. Reads do
647
- not install event listeners or poll automatically. The current adapter requires
648
- `C_UnitAuras.GetAuraDataByIndex`; Forever availability and restrictions still need
649
- in-game verification. Unit tokens refer to snapshot time and are not persistent
650
- caster identities. No Paladin-specific spell catalog or filtering is built in.
651
-
652
- ### Native tooltips
653
-
654
- `Tooltip` attaches to any Cell through decorations. It forwards hover to the
655
- game's native tooltip API and creates no visual regions or update loop.
656
-
657
- ```tsx
658
- import { Cell, Tooltip } from "wuira/ui";
659
-
660
- <Cell width={40} height={40} decorations={{
661
- tooltip: new Tooltip({ text: "My tooltip", lines: [{ text: "Extra details" }] }),
662
- }} />;
663
-
664
- <Cell width={40} height={40} decorations={{
665
- tooltip: new Tooltip({ show: tooltip => tooltip.SetSpellByID(635) }),
666
- }} />;
667
- ```
668
-
669
- The same attachment works on SecureButton. Call SetItemByID, SetUnit, SetHyperlink
670
- or SetAction inside `show` for other native content. `text` may be a function
671
- evaluated on hover; `show` may return false to suppress display. Optional `target`
672
- selects another native tooltip, and `anchor` defaults to ANCHOR_RIGHT.
673
- Hover preserves click-through and existing scripts. Leave, hide and unmount close
674
- only a tooltip still owned by that Cell. Use one decoration instance per Cell.
675
-
676
- ### Visual styling
677
-
678
- <!-- Screenshot: a dark Cell with a thin colored border. -->
679
-
680
- Configure backgrounds, borders, textures, and glow through reusable decoration objects.
681
-
682
- ```tsx
683
- import { Background, Border } from "wuira/ui";
684
-
685
-
686
- const view = (
687
- <cell width={200} height={64} decorations={{
688
- background: new Background({ color: { r: 0.06, g: 0.08, b: 0.1 } }),
689
- border: new Border({ thickness: 1, color: { r: 0.3, g: 0.8, b: 0.7 } }),
690
- }} />
691
- );
692
-
693
- Wuira.Mount(view);
694
- ```
695
-
696
- ### Addon tooling
697
-
698
- <!-- Screenshot: creating and building an addon in the terminal. -->
699
-
700
- Create a project, compile TypeScriptToLua, and generate the TOC and module-loading files through one CLI.
43
+ Requires Node.js 22.13 or newer.
701
44
 
702
45
  ```sh
703
46
  npx wuira@latest create MyAddon
@@ -706,170 +49,78 @@ npm install
706
49
  npm run build
707
50
  ```
708
51
 
709
- ### Readable Lua output
710
-
711
- <!-- Screenshot: source files alongside their emitted Lua modules. -->
712
-
713
- Emitted modules remain split across files following the source structure.
714
-
715
- ```text
716
- src/index.tsx → dist/index.lua
717
- src/components/Panel.tsx → dist/components/Panel.lua
718
- wuira/ui dependency → dist/lua_modules/wuira/lib/ui/index.lua
719
-
720
- MyAddon.toc loads the module registry, emitted files, and startup entry.
721
- ```
722
-
723
- Use wuira when you want to build and distribute an addon UI in TypeScript, share visual components, and let the toolchain handle Lua module loading. Node.js runs during development; the game loads the generated Lua files without a JavaScript runtime.
724
-
725
- ## Scope and current status
52
+ `create` refuses to overwrite an existing directory. It generates TypeScriptToLua configuration, addon metadata, a module loader, an MIT license, and a minimal `src/index.tsx`.
726
53
 
727
- wuira is an early framework for addon authors. 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.
54
+ ## Data sources
728
55
 
729
- The current core includes Cells, flow and grid layouts, progress bars, decorations, health and mana sources, a local console polyfill, and project tooling. A complete WoW API surface, secure action and click-cast abstractions, nameplate adapters, and preset management are not implemented yet. Combat restrictions and secret-value rules still apply; framework abstractions cannot bypass them.
730
-
731
- The target client is WoW Forever. Compatibility with Retail or other Classic clients is not promised without verification. Expect API changes while the framework takes shape.
732
-
733
- ## Getting started
734
-
735
- The framework, npm package, CLI command, and public API are named `wuira`. Future extension packages can use the `@wuira/` organization scope once that organization is registered.
56
+ ```tsx
57
+ import { HP, Mana, Unit } from "wuira/datasources";
736
58
 
737
- Available on npm as [`wuira`](https://www.npmjs.com/package/wuira):
59
+ const hp = new HP(Unit.Player).UseHealPrediction().UseAbsorbPrediction();
60
+ const mana = new Mana(Unit.Player).UseRegenTimer();
738
61
 
739
- ```sh
740
- npx wuira create MyAddon
62
+ <progress height={24} source={hp} />
63
+ <progress height={24} source={mana} />
741
64
  ```
742
65
 
743
- A TypeScript and JSX UI framework for WoW Forever. TypeScriptToLua emits separate Lua files following the source directory structure. No Rollup or single-file Lua bundle is required.
744
-
745
- ## CLI
66
+ Other sources include `AuraTimer`, `GlobalCooldown`, `CastTimer`, `SpellCooldown`, `SwingTimer`, `GroupMembers`, `PartyMembers`, `RaidMembers`, `Buffs`, `Debuffs`, `KnownSpells`, `Spells`, `UnitSource`, `ClassSource`, `SpecSource`, and `SpellSource`.
746
67
 
747
- Create an addon with Node.js 22.13 or newer:
68
+ ## Secure buttons
748
69
 
749
- ```sh
750
- npx wuira@latest create MyAddon
751
- cd MyAddon
752
- npm install
753
- npm run build
70
+ ```tsx
71
+ <SecureButton width={80} height={24}
72
+ action={{ type: "spell", spell: 635, unit: "player" }}>
73
+ <Label text="Holy Light" align="CENTER" />
74
+ </SecureButton>
754
75
  ```
755
76
 
756
- Alternatively, install globally with `npm install -g wuira`, then use `wuira create MyAddon`. To test unpublished changes, run `npm install` and `npm link` in this repository, or invoke `node /path/to/wuira/cli/wuira.mjs create MyAddon` directly. Templates reflect the installed CLI version; repository changes become available through npm after the next release.
757
-
758
- `create` refuses to overwrite an existing directory. It generates TypeScriptToLua configuration, addon metadata, a module loader, an MIT license, and this minimal `src/index.tsx`:
759
-
760
- ```tsx
761
- import "wuira/console-polyfill";
762
- import { UI, Wuira } from "wuira";
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.
763
78
 
79
+ ## Stores
764
80
 
765
- function App() {
766
- console.log("MyAddon loaded");
767
- return <></>;
81
+ ```ts
82
+ class LumaSettings extends Store {
83
+ static readonly Token = "luma.settings";
84
+ CastSpellId = 1232;
85
+ Width = 220;
768
86
  }
769
87
 
770
- Wuira.Mount(App);
88
+ const store = new StoreRegistry({ addon: "LumaUI", variable: "LumaUIDB" });
89
+ store.onReady(() => console.log(store.get(LumaSettings).Width));
771
90
  ```
772
91
 
773
- An empty fragment creates no native frame. Only empty fragments are currently supported; use a layout for multiple visual elements.
774
-
775
- `console.log/info/warn/error` use a global WoW console polyfill. Output goes to the local chat window, never to other players. The addon uses no Node.js runtime or stdin/stdout. `console.error` does not throw. Arguments are formatted with Lua `tostring`; objects are not deeply inspected. The CLI uses its own Node.js terminal console.
776
-
777
- Importing `wuira` installs the global console polyfill automatically. The shared TypeScript config loads its global declarations and uses Node16 module resolution for package exports; DOM typings are not required. `Wuira.Mount` accepts a `Cell`, no value, or a function returning `Cell | void`, so addons without a visual tree can simply log and return.
92
+ `get()` does not subscribe. `subscribe()` observes future commits. `set()` works on a detached draft, validates it, commits it, and sends one notification per observer. Supported values are finite numbers, strings, booleans, arrays, and nested data objects.
778
93
 
779
94
  ## Build and distribution
780
95
 
781
- `wuira build` transpiles through `tsconfig.json`. It writes a standalone addon directory containing individual Lua modules, a loader, a startup file, and `<addonName>.toc`. The default output directory is `dist/`. The addon name lives in `wuira.config.mjs`; Interface version and other metadata live in `addon.toc.in`. The default Interface version, 16001, targets the Forever playground.
782
-
783
- Copy the contents of `dist/` to `Interface/AddOns/<addonName>/`, or configure the build to output there directly. No Node.js runtime or separate wuira addon is required in game. Module registries are isolated by addon name.
784
-
785
- Framework code and generated `.d.ts` declarations come from the installed npm package. Import UI components from `wuira/ui`, data sources from `wuira/datasources`, and the mounting API from `wuira`. No framework source files or build scripts are copied into addon projects. This repository contains only the framework and CLI; addon entry points and TOC metadata are generated in consumer projects.
786
-
787
- Project templates live in `templates/addon/`. Generated projects depend on `wuira` and extend `wuira/config/tsconfig.json`. Shared settings and the TSTL module plugin stay in the installed package; build scripts are not copied into projects. Project-local `rootDir`, `outDir`, and source includes remain in the generated `tsconfig.json`.
788
-
789
- `npm run build` resolves the locally installed `wuira` command automatically. Use `npx wuira build` when invoking the CLI directly.
790
-
791
- Configure the addon output in `wuira.config.mjs`:
96
+ `wuira build` transpiles through `tsconfig.json`. It writes individual Lua modules, a loader, a startup file, and `<addonName>.toc` into `dist/`. Copy the output to `Interface/AddOns/<addonName>/`, or configure the build to output there directly.
792
97
 
793
98
  ```js
99
+ // wuira.config.mjs
794
100
  export default {
795
101
  name: "MyAddon",
796
102
  outDir: "./dist",
797
- // Or: outDir: "G:/Games/WoW/Interface/AddOns/MyAddon",
798
103
  };
799
104
  ```
800
105
 
801
- Relative output paths resolve from the project directory. Absolute paths can target the game's addon directory, keeping the development project elsewhere. Builds compile into a temporary staging directory before copying successful output to the destination. Existing files with matching names are overwritten; unrelated files are retained. The emitted TOC uses paths relative to the output folder, so the entire folder can be moved independently.
802
-
803
- The CLI, configuration, and build plugin use ESM (`.mjs`). Only Lua and TOC files are needed in game. `npm run build` in the framework repository generates distributable Lua modules and TypeScript declarations in `lib/`; addon builds include the framework modules they import.
804
-
805
- ## Stores
806
-
807
- Use data classes to define defaults. Without a static token, records live only
808
- in memory and are keyed by the class itself:
106
+ No Node.js runtime or separate wuira addon is required in game. Framework code comes from the npm package and is not copied into addon projects.
809
107
 
810
- ```ts
811
- import { Store, StoreRegistry } from "wuira/store";
812
-
813
- class SessionSettings extends Store {
814
- CastSpellId = 1232;
815
- }
816
-
817
- const store = new StoreRegistry();
818
- const snapshot = store.get(SessionSettings); // One-time, deeply readonly copy.
819
-
820
- const unsubscribe = store.subscribe(SessionSettings, settings => {
821
- console.log("New spell:", settings.CastSpellId);
822
- });
823
-
824
- store.set(SessionSettings, settings => {
825
- settings.CastSpellId = 635;
826
- return settings;
827
- });
108
+ ## CLI
828
109
 
829
- unsubscribe();
110
+ ```sh
111
+ npx wuira create MyAddon
112
+ npx wuira build
830
113
  ```
831
114
 
832
- `get()` does not subscribe. `subscribe()` observes future commits and returns an
833
- unsubscribe function; call `get()` for initial data. Each subscriber receives an
834
- independent snapshot. `set()` works on a detached draft, validates the returned
835
- data, commits it, and sends one notification per observer. If the updater throws,
836
- the previous record is preserved. Stores contain data fields, not class methods.
115
+ For global installation, use `npm install -g wuira`. To test unpublished changes, run `npm install && npm link` in this repository, or invoke `node /path/to/wuira/cli/wuira.mjs` directly.
837
116
 
838
- For persistence, declare a stable token and the owning addon's SavedVariables:
117
+ ## Scope and status
839
118
 
840
- ```toc
841
- ## SavedVariables: LumaUIDB
842
- ```
119
+ 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.
843
120
 
844
- ```ts
845
- class LumaSettings extends Store {
846
- static readonly Token = "luma.settings";
847
- CastSpellId = 1232;
848
- Width = 220;
849
- }
850
-
851
- const store = new StoreRegistry({ addon: "LumaUI", variable: "LumaUIDB" });
852
- store.onReady(() => {
853
- console.log(store.get(LumaSettings).Width);
854
- store.set(LumaSettings, settings => {
855
- settings.Width = 240;
856
- return settings;
857
- });
858
- });
859
- ```
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.
860
122
 
861
- Add the TOC declaration to the addon's `addon.toc.in`. The registry waits for
862
- `ADDON_LOADED` before opening persistent records. Tokenless records can still be
863
- used immediately. WoW writes the saved table on normal logout/reload; `set()`
864
- does not force a disk flush. Keep tokens stable across versions. New default
865
- fields are filled in and incompatible saved field types fall back to defaults;
866
- renaming keys or changing schema meaning still requires an application migration.
867
-
868
- Supported values are finite numbers, strings, booleans, arrays, and nested data
869
- objects. Functions, cycles, and secret values cannot be saved. Two classes cannot
870
- claim the same token within a registry. Share one registry instance per database.
871
- `Dispose()` removes its observers and pending initialization listener. Profile
872
- selection, schema migrations, and reactive widget binding are separate features.
123
+ The target client is WoW Forever. Compatibility with Retail or other Classic clients is not promised without verification.
873
124
 
874
125
  ## Development
875
126
 
@@ -880,36 +131,16 @@ npm run check
880
131
  npm test
881
132
  ```
882
133
 
883
- `.github/workflows/ci.yml` runs type checks, builds, tests, and a package dry run on pushes and pull requests, on both Linux and Windows with Node.js 22. CI does not publish packages and requires no npm token.
134
+ CI runs type checks, builds, tests, and a package dry run on Linux and Windows with Node.js 22. It does not publish packages and requires no npm token.
884
135
 
885
- Cell is the basic visual box. FlowLayout and GridLayout arrange children. ProgressBar uses a native StatusBar, while data sources connect values to presentation. Behavior from other WoW versions must be verified against the Forever API before use.
136
+ ## License
886
137
 
887
- Licensed under MIT; see [LICENSE](LICENSE).
138
+ MIT — see [LICENSE](LICENSE).
888
139
 
889
140
  ## AI disclosure
890
141
 
891
- wuira is developed with AI assistance for implementation, documentation, and
892
- tests. AI-assisted code is reviewed line by line. Releases also undergo
893
- TypeScript checks, Lua runtime tests, and package build validation. Project
894
- maintainers remain responsible for the code and release decisions.
142
+ wuira is developed with AI assistance for implementation, documentation, and tests. AI-assisted code is reviewed line by line. Releases also undergo TypeScript checks, Lua runtime tests, and package build validation. Project maintainers remain responsible for the code and release decisions.
895
143
 
896
144
  ## Releases
897
145
 
898
- See [CHANGELOG.md](CHANGELOG.md) for release history. Record new changes under
899
- `Unreleased`, then move them into a dated version section before publishing.
900
-
901
- The first version is published. To publish the next version locally, validate the project, increment the patch version, and publish:
902
-
903
- ```sh
904
- npm run check
905
- npm run build
906
- npm test
907
- npm version patch --no-git-tag-version
908
- npm publish --access public
909
- ```
910
-
911
- Publishing requires npm authentication and 2FA, or a granular access token with write access and Bypass 2FA enabled. Commit both `package.json` and `package-lock.json` after changing the version. npm versions cannot be published twice.
912
-
913
- For future automated releases, every pushed Git tag triggers `.github/workflows/publish.yml`. It checks types, builds, tests, and publishes to npm. Tags must match `package.json` version, optionally prefixed with `v`. Do not push a release tag for a version already published locally; the workflow would attempt to publish it again.
914
-
915
- To enable automated publishing, configure the GitHub repository secret `NPM_TOKEN` with an npm granular access token authorized to publish `wuira` and bypass 2FA. Never commit the token. GitHub Actions must be enabled. Local setup does not publish a package or push a tag.
146
+ See [CHANGELOG.md](CHANGELOG.md) for release history. Record new changes under `Unreleased`, then move them into a dated version section before publishing.