wuira 0.2.3 → 0.2.4

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,24 @@ with the release date and highlight breaking changes explicitly.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## 0.2.4 — 2026-10-15
11
+
12
+ ### Added
13
+
14
+ - Observable keyed ForEach rendering. Stable keys retain native widgets and their
15
+ subscriptions across joins, leaves, reorder and metadata updates; only added or
16
+ removed records mount or dispose. Supports grouped layout and wrapping.
17
+ - Native GroupRoster metadata with GUID identity, full-name/realm fallback, pet
18
+ ownership and raid subgroup membership. Retains the last readable roster if
19
+ native membership data becomes restricted.
20
+ - Cell keys, event-driven GroupMembers subscriptions and shared UnitSource
21
+ retargeting. HP/Mana readers and event filters follow the existing unit reference.
22
+
23
+ ### Changed
24
+
25
+ - Defer keyed structural updates and unit retargeting until combat ends, keeping
26
+ protected click targets aligned with displayed data.
27
+
10
28
  ## 0.2.3 — 2026-10-15
11
29
 
12
30
  ### Added
package/README.md CHANGED
@@ -270,9 +270,10 @@ function SealButtons() {
270
270
 
271
271
  The render function receives the item and its zero-based index. Use `ForEach`
272
272
  inside a layout for multiple siblings; Cell still accepts at most one expanded
273
- child. Nested loops are flattened in order. `ForEach` creates no subscriptions:
273
+ child. Nested loops are flattened in order. Array inputs create no subscriptions:
274
274
  recreate the view from `known.onChange` when needed, outside combat for protected
275
- trees. Each filtered rank remains a separate entry.
275
+ trees. Each filtered rank remains a separate entry. Observable list sources also
276
+ support keyed updates, as shown below under Group members.
276
277
 
277
278
  All visual cells accept `condition={boolean}` or `condition={() => boolean}`.
278
279
  This controls visibility and retains the layout slot. It combines with datasource
@@ -542,10 +543,45 @@ const raid = new RaidMembers();
542
543
  `GroupMembers` returns `["player"]` while solo, `player` plus `partyN` tokens
543
544
  in a party, and `raidN` tokens in a raid. `PartyMembers` is empty outside a
544
545
  party; `RaidMembers` is empty outside a raid. The player is included once.
545
- Offline members are retained. Each `.Value` access reads the current roster;
546
- there is no automatic subscription, polling, or UI rerender. Read again when
547
- handling `GROUP_ROSTER_UPDATE`. Tokens may refer to different members after
548
- roster changes and should not be used as persistent identities.
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.
549
585
 
550
586
  ### Player class and specialization
551
587
 
@@ -567,6 +603,9 @@ specs.RequestInspect();
567
603
  These sources accept real players only; NPCs produce no data or inspect request.
568
604
  The same `UnitSource` can be passed to `Buffs`, `Debuffs`, `HP`, and `Mana`.
569
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.
570
609
  `Is.PC` and `Is.Self` return `undefined` when their native result is secret.
571
610
  String unit tokens are still supported as a convenience shorthand.
572
611
  Own specialization data is read directly. Other players require an explicit,
@@ -4,7 +4,13 @@ export type GroupScope = "auto" | "party" | "raid";
4
4
  export declare class GroupMembers extends DataSource<readonly string[]> {
5
5
  readonly scope: GroupScope;
6
6
  private restricted;
7
+ private readonly listeners;
8
+ private frame?;
7
9
  constructor(scope?: GroupScope);
10
+ /** Roster/connection events, with one listener frame while observed. No polling. */
11
+ onChange(listener: (units: readonly string[]) => void): () => void;
12
+ private StopListening;
13
+ Dispose(): void;
8
14
  protected HasSecretFields(): boolean;
9
15
  protected GetValue(): readonly string[];
10
16
  }
@@ -32,6 +32,284 @@ local function __TS__ClassExtends(target, base)
32
32
  end
33
33
  end
34
34
 
35
+ local __TS__Symbol, Symbol
36
+ do
37
+ local symbolMetatable = {__tostring = function(self)
38
+ return ("Symbol(" .. (self.description or "")) .. ")"
39
+ end}
40
+ function __TS__Symbol(description)
41
+ return setmetatable({description = description}, symbolMetatable)
42
+ end
43
+ Symbol = {
44
+ asyncDispose = __TS__Symbol("Symbol.asyncDispose"),
45
+ dispose = __TS__Symbol("Symbol.dispose"),
46
+ iterator = __TS__Symbol("Symbol.iterator"),
47
+ hasInstance = __TS__Symbol("Symbol.hasInstance"),
48
+ species = __TS__Symbol("Symbol.species"),
49
+ toStringTag = __TS__Symbol("Symbol.toStringTag")
50
+ }
51
+ end
52
+
53
+ local __TS__Iterator
54
+ do
55
+ local function iteratorGeneratorStep(self)
56
+ local co = self.____coroutine
57
+ local status, value = coroutine.resume(co)
58
+ if not status then
59
+ error(value, 0)
60
+ end
61
+ if coroutine.status(co) == "dead" then
62
+ return
63
+ end
64
+ return true, value
65
+ end
66
+ local function iteratorIteratorStep(self)
67
+ local result = self:next()
68
+ if result.done then
69
+ return
70
+ end
71
+ return true, result.value
72
+ end
73
+ local function iteratorStringStep(self, index)
74
+ index = index + 1
75
+ if index > #self then
76
+ return
77
+ end
78
+ return index, string.sub(self, index, index)
79
+ end
80
+ function __TS__Iterator(iterable)
81
+ if type(iterable) == "string" then
82
+ return iteratorStringStep, iterable, 0
83
+ elseif iterable.____coroutine ~= nil then
84
+ return iteratorGeneratorStep, iterable
85
+ elseif iterable[Symbol.iterator] then
86
+ local iterator = iterable[Symbol.iterator](iterable)
87
+ return iteratorIteratorStep, iterator
88
+ else
89
+ return ipairs(iterable)
90
+ end
91
+ end
92
+ end
93
+
94
+ local function __TS__New(target, ...)
95
+ local instance = setmetatable({}, target.prototype)
96
+ instance:____constructor(...)
97
+ return instance
98
+ end
99
+
100
+ local Set
101
+ do
102
+ Set = __TS__Class()
103
+ Set.name = "Set"
104
+ function Set.prototype.____constructor(self, values)
105
+ self[Symbol.toStringTag] = "Set"
106
+ self.size = 0
107
+ self.nextKey = {}
108
+ self.previousKey = {}
109
+ if values == nil then
110
+ return
111
+ end
112
+ local iterable = values
113
+ if iterable[Symbol.iterator] then
114
+ local iterator = iterable[Symbol.iterator](iterable)
115
+ while true do
116
+ local result = iterator:next()
117
+ if result.done then
118
+ break
119
+ end
120
+ self:add(result.value)
121
+ end
122
+ else
123
+ local array = values
124
+ for ____, value in ipairs(array) do
125
+ self:add(value)
126
+ end
127
+ end
128
+ end
129
+ function Set.prototype.add(self, value)
130
+ local isNewValue = not self:has(value)
131
+ if isNewValue then
132
+ self.size = self.size + 1
133
+ end
134
+ if self.firstKey == nil then
135
+ self.firstKey = value
136
+ self.lastKey = value
137
+ elseif isNewValue then
138
+ self.nextKey[self.lastKey] = value
139
+ self.previousKey[value] = self.lastKey
140
+ self.lastKey = value
141
+ end
142
+ return self
143
+ end
144
+ function Set.prototype.clear(self)
145
+ self.nextKey = {}
146
+ self.previousKey = {}
147
+ self.firstKey = nil
148
+ self.lastKey = nil
149
+ self.size = 0
150
+ end
151
+ function Set.prototype.delete(self, value)
152
+ local contains = self:has(value)
153
+ if contains then
154
+ self.size = self.size - 1
155
+ local next = self.nextKey[value]
156
+ local previous = self.previousKey[value]
157
+ if next ~= nil and previous ~= nil then
158
+ self.nextKey[previous] = next
159
+ self.previousKey[next] = previous
160
+ elseif next ~= nil then
161
+ self.firstKey = next
162
+ self.previousKey[next] = nil
163
+ elseif previous ~= nil then
164
+ self.lastKey = previous
165
+ self.nextKey[previous] = nil
166
+ else
167
+ self.firstKey = nil
168
+ self.lastKey = nil
169
+ end
170
+ self.nextKey[value] = nil
171
+ self.previousKey[value] = nil
172
+ end
173
+ return contains
174
+ end
175
+ function Set.prototype.forEach(self, callback)
176
+ for ____, key in __TS__Iterator(self:keys()) do
177
+ callback(nil, key, key, self)
178
+ end
179
+ end
180
+ function Set.prototype.has(self, value)
181
+ return self.nextKey[value] ~= nil or self.lastKey == value
182
+ end
183
+ Set.prototype[Symbol.iterator] = function(self)
184
+ return self:values()
185
+ end
186
+ function Set.prototype.entries(self)
187
+ local function getFirstKey()
188
+ return self.firstKey
189
+ end
190
+ local nextKey = self.nextKey
191
+ local key
192
+ local started = false
193
+ return {
194
+ [Symbol.iterator] = function(self)
195
+ return self
196
+ end,
197
+ next = function(self)
198
+ if not started then
199
+ started = true
200
+ key = getFirstKey(nil)
201
+ else
202
+ key = nextKey[key]
203
+ end
204
+ return {done = not key, value = {key, key}}
205
+ end
206
+ }
207
+ end
208
+ function Set.prototype.keys(self)
209
+ local function getFirstKey()
210
+ return self.firstKey
211
+ end
212
+ local nextKey = self.nextKey
213
+ local key
214
+ local started = false
215
+ return {
216
+ [Symbol.iterator] = function(self)
217
+ return self
218
+ end,
219
+ next = function(self)
220
+ if not started then
221
+ started = true
222
+ key = getFirstKey(nil)
223
+ else
224
+ key = nextKey[key]
225
+ end
226
+ return {done = not key, value = key}
227
+ end
228
+ }
229
+ end
230
+ function Set.prototype.values(self)
231
+ local function getFirstKey()
232
+ return self.firstKey
233
+ end
234
+ local nextKey = self.nextKey
235
+ local key
236
+ local started = false
237
+ return {
238
+ [Symbol.iterator] = function(self)
239
+ return self
240
+ end,
241
+ next = function(self)
242
+ if not started then
243
+ started = true
244
+ key = getFirstKey(nil)
245
+ else
246
+ key = nextKey[key]
247
+ end
248
+ return {done = not key, value = key}
249
+ end
250
+ }
251
+ end
252
+ function Set.prototype.union(self, other)
253
+ local result = __TS__New(Set, self)
254
+ for ____, item in __TS__Iterator(other) do
255
+ result:add(item)
256
+ end
257
+ return result
258
+ end
259
+ function Set.prototype.intersection(self, other)
260
+ local result = __TS__New(Set)
261
+ for ____, item in __TS__Iterator(self) do
262
+ if other:has(item) then
263
+ result:add(item)
264
+ end
265
+ end
266
+ return result
267
+ end
268
+ function Set.prototype.difference(self, other)
269
+ local result = __TS__New(Set, self)
270
+ for ____, item in __TS__Iterator(other) do
271
+ result:delete(item)
272
+ end
273
+ return result
274
+ end
275
+ function Set.prototype.symmetricDifference(self, other)
276
+ local result = __TS__New(Set, self)
277
+ for ____, item in __TS__Iterator(other) do
278
+ if self:has(item) then
279
+ result:delete(item)
280
+ else
281
+ result:add(item)
282
+ end
283
+ end
284
+ return result
285
+ end
286
+ function Set.prototype.isSubsetOf(self, other)
287
+ for ____, item in __TS__Iterator(self) do
288
+ if not other:has(item) then
289
+ return false
290
+ end
291
+ end
292
+ return true
293
+ end
294
+ function Set.prototype.isSupersetOf(self, other)
295
+ for ____, item in __TS__Iterator(other) do
296
+ if not self:has(item) then
297
+ return false
298
+ end
299
+ end
300
+ return true
301
+ end
302
+ function Set.prototype.isDisjointFrom(self, other)
303
+ for ____, item in __TS__Iterator(self) do
304
+ if other:has(item) then
305
+ return false
306
+ end
307
+ end
308
+ return true
309
+ end
310
+ Set[Symbol.species] = Set
311
+ end
312
+
35
313
  local function __TS__NumberIsFinite(value)
36
314
  return type(value) == "number" and value == value and value ~= math.huge and value ~= -math.huge
37
315
  end
@@ -55,6 +333,60 @@ function GroupMembers.prototype.____constructor(self, scope)
55
333
  DataSource.prototype.____constructor(self, {})
56
334
  self.scope = scope
57
335
  self.restricted = false
336
+ self.listeners = __TS__New(Set)
337
+ end
338
+ function GroupMembers.prototype.onChange(self, listener)
339
+ self.listeners:add(listener)
340
+ if self.listeners.size == 1 then
341
+ if self.frame == nil then
342
+ self.frame = CreateFrame("Frame", nil, UIParent)
343
+ end
344
+ for ____, event in ipairs({
345
+ "GROUP_ROSTER_UPDATE",
346
+ "UNIT_PET",
347
+ "UNIT_CONNECTION",
348
+ "UNIT_NAME_UPDATE",
349
+ "PLAYER_ENTERING_WORLD"
350
+ }) do
351
+ self.frame:RegisterEvent(event)
352
+ end
353
+ self.frame:SetScript(
354
+ "OnEvent",
355
+ function()
356
+ local units = self:GetValue()
357
+ if self.restricted then
358
+ return
359
+ end
360
+ for ____, callback in __TS__Iterator(self.listeners) do
361
+ callback(nil, units)
362
+ end
363
+ end
364
+ )
365
+ end
366
+ return function()
367
+ self.listeners:delete(listener)
368
+ if self.listeners.size == 0 then
369
+ self:StopListening()
370
+ end
371
+ end
372
+ end
373
+ function GroupMembers.prototype.StopListening(self)
374
+ local ____opt_0 = self.frame
375
+ if ____opt_0 ~= nil then
376
+ ____opt_0:UnregisterAllEvents()
377
+ end
378
+ local ____opt_2 = self.frame
379
+ if ____opt_2 ~= nil then
380
+ ____opt_2:SetScript("OnEvent", nil)
381
+ end
382
+ local ____opt_4 = self.frame
383
+ if ____opt_4 ~= nil then
384
+ ____opt_4:Hide()
385
+ end
386
+ end
387
+ function GroupMembers.prototype.Dispose(self)
388
+ self.listeners:clear()
389
+ self:StopListening()
58
390
  end
59
391
  function GroupMembers.prototype.HasSecretFields(self)
60
392
  return self.restricted
@@ -0,0 +1,28 @@
1
+ import { DataSource } from "./DataSource";
2
+ import type { GroupScope } from "./GroupMembers";
3
+ export interface GroupRosterMember {
4
+ readonly key: string;
5
+ readonly guid?: string;
6
+ readonly unit: string;
7
+ readonly pet: boolean;
8
+ readonly group: string;
9
+ }
10
+ export declare function getPetUnitToken(unit: string): string;
11
+ /** Small snapshots describe identity/order only; keyed rendering owns widgets and unit sources. */
12
+ export declare class GroupRoster extends DataSource<readonly GroupRosterMember[]> {
13
+ private readonly scope;
14
+ private readonly options;
15
+ private readonly roster;
16
+ private readonly knownNames;
17
+ private snapshot;
18
+ private restricted;
19
+ get Status(): "ready" | "restricted";
20
+ constructor(scope?: GroupScope, options?: {
21
+ pets?: boolean;
22
+ includeSelfWhenSolo?: boolean;
23
+ });
24
+ protected GetValue(): readonly GroupRosterMember[];
25
+ private Read;
26
+ onChange(listener: (members: readonly GroupRosterMember[]) => void): () => void;
27
+ Dispose(): void;
28
+ }