@awflow/avatar-element 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +3 -1
  2. package/dist/index.js +361 -0
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -43,7 +43,9 @@ Methods: `react(name)`, `pulse(strength?)`, `setVoice(fn | null)`,
43
43
  Properties `ring`, `gaze`, `reaction` and `reactKey` accept objects directly.
44
44
 
45
45
  `defineAvatarElements(prefix = 'awf')` registers the tags under another prefix.
46
- Importing in Node is safe and defines nothing.
46
+ Importing in Node (SSR, tests) is safe from 1.0.1 and defines nothing; the
47
+ 1.0.0 build was broken (its `dist/index.js` lacked the definitions), so use
48
+ 1.0.1 or later.
47
49
 
48
50
  ## Docs
49
51
 
package/dist/index.js CHANGED
@@ -1,3 +1,364 @@
1
+ // src/element.ts
2
+ import { LATEST } from "@awflow/avatar";
3
+ import {
4
+ mount
5
+ } from "@awflow/avatar/adapter";
6
+
7
+ // src/group.ts
8
+ function createGroup() {
9
+ const members = [];
10
+ let speaker;
11
+ const aim = () => {
12
+ const at = members.find((m) => m.name === speaker)?.handle ?? null;
13
+ for (const m of members)
14
+ m.handle.lookAt(m.handle === at ? null : at);
15
+ };
16
+ return {
17
+ registerMember(name, handle) {
18
+ const m = { name, handle };
19
+ members.push(m);
20
+ aim();
21
+ return () => {
22
+ const i = members.indexOf(m);
23
+ if (i < 0)
24
+ return;
25
+ members.splice(i, 1);
26
+ if (m.name === speaker)
27
+ aim();
28
+ };
29
+ },
30
+ setSpeaker(name) {
31
+ speaker = name;
32
+ aim();
33
+ }
34
+ };
35
+ }
36
+
37
+ // src/parse.ts
38
+ var BOOLEANS = new Set(["animate", "dress", "riso", "square"]);
39
+ var NUMBERS = new Set(["size", "version"]);
40
+ var STRINGS = [
41
+ "name",
42
+ "theme",
43
+ "framing",
44
+ "frame",
45
+ "expression",
46
+ "activity",
47
+ "shape",
48
+ "color",
49
+ "eyes",
50
+ "brows",
51
+ "mouth",
52
+ "cheeks",
53
+ "trait",
54
+ "motif",
55
+ "headwear",
56
+ "eyewear",
57
+ "neckwear",
58
+ "face",
59
+ "prop",
60
+ "character",
61
+ "reaction"
62
+ ];
63
+ var ATTRIBUTES = {
64
+ ...Object.fromEntries([...NUMBERS, ...BOOLEANS, ...STRINGS, "gaze", "fx", "ring"].map((k) => [
65
+ k,
66
+ k
67
+ ])),
68
+ "react-key": "reactKey",
69
+ label: "title"
70
+ };
71
+ var clamp = (n) => Math.max(-1, Math.min(1, n));
72
+ function parseGaze(text) {
73
+ const t = text.trim();
74
+ if (t === "pointer")
75
+ return "pointer";
76
+ const parts = t.split(",");
77
+ if (parts.length !== 2 || parts.some((p) => p.trim() === ""))
78
+ return;
79
+ const [x, y] = parts.map((p) => Number(p));
80
+ if (Number.isNaN(x) || Number.isNaN(y))
81
+ return;
82
+ return [clamp(x), clamp(y)];
83
+ }
84
+ function parseAttribute(attr, text) {
85
+ const key = ATTRIBUTES[attr];
86
+ if (!key)
87
+ return;
88
+ if (text === null) {
89
+ return { key, value: undefined };
90
+ }
91
+ if (BOOLEANS.has(attr))
92
+ return { key, value: text !== "false" };
93
+ if (NUMBERS.has(attr)) {
94
+ if (text.trim() === "")
95
+ return;
96
+ const n = Number(text);
97
+ return Number.isNaN(n) ? undefined : { key, value: n };
98
+ }
99
+ if (attr === "gaze") {
100
+ const g = parseGaze(text);
101
+ return g === undefined ? undefined : { key, value: g };
102
+ }
103
+ if (attr === "ring") {
104
+ try {
105
+ const v = JSON.parse(text);
106
+ return v && typeof v === "object" && !Array.isArray(v) ? { key, value: v } : undefined;
107
+ } catch {
108
+ return;
109
+ }
110
+ }
111
+ if (attr === "fx") {
112
+ return {
113
+ key,
114
+ value: text === "false" ? false : text === "" || text === "true" ? true : text
115
+ };
116
+ }
117
+ if (attr === "label")
118
+ return { key, value: text === "false" ? false : text };
119
+ return { key, value: text };
120
+ }
121
+
122
+ // src/element.ts
123
+ var HANDLE = Symbol.for("awflow.avatar.element.handle");
124
+ var JOIN = Symbol.for("awflow.avatar.element.join");
125
+ var LEAVE = Symbol.for("awflow.avatar.element.leave");
126
+ var GROUP = Symbol.for("awflow.avatar.element.group");
127
+ var NOTIFY = Symbol.for("awflow.avatar.element.notify");
128
+ var AIM = Symbol.for("awflow.avatar.element.aim");
129
+ var serial = 0;
130
+ function domId(n, version) {
131
+ const v = typeof version === "number" ? version : LATEST;
132
+ return `${v >= 2 ? "awf" : "peek"}-e${n}`;
133
+ }
134
+ function upgradeProperties(el, keys) {
135
+ for (const k of keys) {
136
+ if (!Object.hasOwn(el, k))
137
+ continue;
138
+ const v = el[k];
139
+ delete el[k];
140
+ el[k] = v;
141
+ }
142
+ }
143
+ var isAvatar = (x) => typeof x === "object" && x !== null && (HANDLE in x);
144
+ var isGroup = (x) => typeof x === "object" && x !== null && (GROUP in x);
145
+ function createElements() {
146
+
147
+ class Avatar extends HTMLElement {
148
+ static get observedAttributes() {
149
+ return Object.keys(ATTRIBUTES);
150
+ }
151
+ opts = {};
152
+ mounted = null;
153
+ leaveGroup = null;
154
+ lookTarget = null;
155
+ rendered = false;
156
+ serial = ++serial;
157
+ [NOTIFY] = new Set;
158
+ constructor() {
159
+ super();
160
+ upgradeProperties(this, ["ring", "gaze", "reaction", "reactKey"]);
161
+ }
162
+ [AIM]() {
163
+ const t = this.lookTarget;
164
+ this.mounted?.lookAt(isAvatar(t) ? t[HANDLE] : t);
165
+ }
166
+ changed() {
167
+ for (const w of this[NOTIFY])
168
+ w[AIM]();
169
+ }
170
+ get handle() {
171
+ return this.mounted;
172
+ }
173
+ get [HANDLE]() {
174
+ return this.mounted;
175
+ }
176
+ connectedCallback() {
177
+ for (const attr of Object.keys(ATTRIBUTES)) {
178
+ const p = this.hasAttribute(attr) ? parseAttribute(attr, this.getAttribute(attr)) : undefined;
179
+ if (p && !(p.key in this.opts))
180
+ this.opts[p.key] = p.value;
181
+ }
182
+ if (isAvatar(this.lookTarget))
183
+ this.lookTarget[NOTIFY].add(this);
184
+ this.mountNow();
185
+ }
186
+ disconnectedCallback() {
187
+ if (isAvatar(this.lookTarget))
188
+ this.lookTarget[NOTIFY].delete(this);
189
+ this[LEAVE]();
190
+ this.mounted?.destroy();
191
+ this.mounted = null;
192
+ this.changed();
193
+ }
194
+ attributeChangedCallback(attr, _old, value) {
195
+ const p = parseAttribute(attr, value);
196
+ if (!p)
197
+ return;
198
+ this.apply({ [p.key]: p.value });
199
+ }
200
+ apply(input) {
201
+ Object.assign(this.opts, input);
202
+ if (!this.isConnected)
203
+ return;
204
+ const patch = "version" in input ? { ...input, id: domId(this.serial, this.opts.version) } : input;
205
+ if (patch.name !== undefined || this.opts.name === undefined) {
206
+ if (patch.name === undefined) {
207
+ this.unmount();
208
+ return;
209
+ }
210
+ if (this.mounted) {
211
+ this.mounted.set(patch);
212
+ this[LEAVE]();
213
+ this[JOIN]();
214
+ return;
215
+ }
216
+ this.mountNow();
217
+ return;
218
+ }
219
+ this.mounted?.set(patch);
220
+ }
221
+ mountNow() {
222
+ this.mounted?.destroy();
223
+ this.mounted = null;
224
+ if (typeof this.opts.name !== "string") {
225
+ this.unmount();
226
+ return;
227
+ }
228
+ this.mounted = mount(this, {
229
+ ...this.opts,
230
+ id: domId(this.serial, this.opts.version)
231
+ });
232
+ this.rendered = true;
233
+ this[AIM]();
234
+ this[JOIN]();
235
+ this.changed();
236
+ }
237
+ unmount() {
238
+ this[LEAVE]();
239
+ this.mounted?.destroy();
240
+ this.mounted = null;
241
+ if (this.rendered)
242
+ this.replaceChildren();
243
+ this.rendered = false;
244
+ this.changed();
245
+ }
246
+ [JOIN]() {
247
+ this[LEAVE]();
248
+ const handle = this.mounted;
249
+ if (!handle)
250
+ return;
251
+ for (let p = this.parentElement;p; p = p.parentElement) {
252
+ if (isGroup(p)) {
253
+ const off = p[GROUP].registerMember(String(this.opts.name ?? ""), handle);
254
+ this.leaveGroup = off;
255
+ return;
256
+ }
257
+ }
258
+ }
259
+ [LEAVE]() {
260
+ if (!this.leaveGroup)
261
+ return;
262
+ this.leaveGroup();
263
+ this.leaveGroup = null;
264
+ this[AIM]();
265
+ }
266
+ react(name) {
267
+ this.mounted?.react(name);
268
+ }
269
+ pulse(strength) {
270
+ this.mounted?.pulse(strength);
271
+ }
272
+ setVoice(source) {
273
+ this.mounted?.setVoice(source);
274
+ }
275
+ lookAt(target) {
276
+ if (isAvatar(this.lookTarget))
277
+ this.lookTarget[NOTIFY].delete(this);
278
+ this.lookTarget = target;
279
+ if (isAvatar(target) && this.isConnected)
280
+ target[NOTIFY].add(this);
281
+ this[AIM]();
282
+ }
283
+ get ring() {
284
+ return this.opts.ring;
285
+ }
286
+ set ring(v) {
287
+ this.apply({ ring: v ?? undefined });
288
+ }
289
+ get gaze() {
290
+ return this.opts.gaze;
291
+ }
292
+ set gaze(v) {
293
+ this.apply({ gaze: v ?? undefined });
294
+ }
295
+ get reaction() {
296
+ return this.opts.reaction;
297
+ }
298
+ set reaction(v) {
299
+ this.apply({ reaction: v ?? undefined });
300
+ }
301
+ get reactKey() {
302
+ return this.opts.reactKey;
303
+ }
304
+ set reactKey(v) {
305
+ this.apply({ reactKey: v });
306
+ }
307
+ }
308
+
309
+ class AvatarGroup extends HTMLElement {
310
+ static get observedAttributes() {
311
+ return ["speaker"];
312
+ }
313
+ [GROUP] = createGroup();
314
+ constructor() {
315
+ super();
316
+ upgradeProperties(this, ["speaker"]);
317
+ }
318
+ get speaker() {
319
+ return this.getAttribute("speaker");
320
+ }
321
+ set speaker(v) {
322
+ if (v === null)
323
+ this.removeAttribute("speaker");
324
+ else
325
+ this.setAttribute("speaker", v);
326
+ }
327
+ attributeChangedCallback(_attr, _old, value) {
328
+ this[GROUP].setSpeaker(value ?? undefined);
329
+ }
330
+ connectedCallback() {
331
+ for (const el of Array.from(this.querySelectorAll("*")))
332
+ if (isAvatar(el))
333
+ el[JOIN]();
334
+ }
335
+ disconnectedCallback() {
336
+ for (const el of Array.from(this.querySelectorAll("*")))
337
+ if (isAvatar(el))
338
+ el[LEAVE]();
339
+ }
340
+ }
341
+ return {
342
+ Avatar,
343
+ Group: AvatarGroup
344
+ };
345
+ }
346
+
347
+ // src/define.ts
348
+ function defineAvatarElements(prefix = "awf") {
349
+ if (typeof customElements === "undefined" || typeof HTMLElement === "undefined")
350
+ return;
351
+ const avatar = `${prefix}-avatar`;
352
+ const group = `${prefix}-avatar-group`;
353
+ if (customElements.get(avatar) && customElements.get(group))
354
+ return;
355
+ const { Avatar, Group } = createElements();
356
+ if (!customElements.get(avatar))
357
+ customElements.define(avatar, Avatar);
358
+ if (!customElements.get(group))
359
+ customElements.define(group, Group);
360
+ }
361
+
1
362
  // src/index.ts
2
363
  defineAvatarElements();
3
364
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awflow/avatar-element",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "types": "./dist/index.d.ts",
6
6
  "sideEffects": [
@@ -19,7 +19,7 @@
19
19
  "LICENSE"
20
20
  ],
21
21
  "dependencies": {
22
- "@awflow/avatar": "^1.0.0"
22
+ "@awflow/avatar": "^1.1.0"
23
23
  },
24
24
  "description": "Deterministic SVG avatars from a name, as <awf-avatar> custom elements.",
25
25
  "license": "MIT",