@apliteni/apliteni-ui 0.130.0 → 0.132.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.
@@ -5,6 +5,7 @@
5
5
  // the published name docs/components.md documents.
6
6
  // why: docs/components.md#the-page-shell
7
7
  import { esc } from './index.js';
8
+ import { avatarInitials } from '../logic/avatar.js';
8
9
 
9
10
  // nav.js item objects. Labels are raw text — every nav primitive escapes, so a
10
11
  // pre-escaped `&` would come out as `&`. `key` means credentials;
@@ -38,10 +39,6 @@ export const accountMenuNav = () => ACCOUNT_NAV.map(toMenuTuple);
38
39
  // beside ACCOUNT_NAV for the same reason ACCOUNT_NAV lives here: both files
39
40
  // need it and shell.js already imports topbar.js.
40
41
  //
41
- // The display name is what a reader recognises, so it wins; the address is the
42
- // fallback. `?` is what nobody at all comes out as.
43
- export const initials = (name, email) => {
44
- const from = String(name ?? '').trim() || String(email ?? '').split('@')[0] || '';
45
- const parts = from.split(/[\s._-]+/).filter(Boolean).map((w) => w[0]);
46
- return (parts.slice(0, 2).join('') || '?').toUpperCase();
47
- };
42
+ // The displayed name alone supplies the letters. The part draws its user glyph
43
+ // when no name is shown.
44
+ export const initials = (name) => avatarInitials(name);
@@ -0,0 +1,53 @@
1
+ import { esc, icon } from './index.js';
2
+ import { safeUrl } from '../html.js';
3
+ import { avatarHue, avatarInitials } from '../logic/avatar.js';
4
+ export { avatarHue, avatarInitials, AVATAR_HUES } from '../logic/avatar.js';
5
+
6
+ // Avatar — a person's round mark. The letters come from `name`, the hue from a hash of
7
+ // `key`, and a photo covers the letter. A photo that fails to load removes itself, so the
8
+ // letter shows and not the browser's broken-image glyph.
9
+ // With no name the mark is the `user` glyph; `empty` is a dashed ring for "no one yet".
10
+ // It is decoration beside the name it stands for, so it is hidden from assistive
11
+ // technology, and it has no cursor, hover or focus of its own.
12
+ // why: docs/components.md#avatar
13
+ const cx = (...a) => a.filter(Boolean).join(' ');
14
+ const wiredDocs = new WeakSet();
15
+
16
+ function wireAvatarPhotos(doc) {
17
+ if (!doc || wiredDocs.has(doc)) return;
18
+ wiredDocs.add(doc);
19
+ doc.querySelectorAll('.ui-avatar > img[data-avatar-photo]').forEach((img) => {
20
+ if (!img.complete) return;
21
+ if (img.naturalWidth > 0) img.classList.add('is-loaded');
22
+ else img.remove();
23
+ });
24
+ doc.addEventListener('load', (event) => {
25
+ if (event.target?.matches?.('.ui-avatar > img[data-avatar-photo]')) event.target.classList.add('is-loaded');
26
+ }, true);
27
+ doc.addEventListener('error', (event) => {
28
+ if (event.target?.matches?.('.ui-avatar > img[data-avatar-photo]')) event.target.remove();
29
+ }, true);
30
+ }
31
+
32
+ if (typeof document !== 'undefined') wireAvatarPhotos(document);
33
+
34
+ /**
35
+ * @param {object} [o]
36
+ * @param {string|number} [o.key] the app's stable id for the person; picks the hue
37
+ * @param {string} [o.name] the person's name; empty draws the `user` glyph
38
+ * @param {string} [o.initials] the letters to draw, when the caller has already chosen them
39
+ * @param {string} [o.src] a photo URL; the letters show if it fails to load
40
+ * @param {24|32} [o.size] the mark's size in px
41
+ * @param {boolean} [o.empty] a dashed ring in place of a person
42
+ * @returns {string} html
43
+ */
44
+ export function avatar({ key, name, initials, src, size = 24, empty = false } = {}) {
45
+ if (typeof document !== 'undefined') wireAvatarPhotos(document);
46
+ const base = ['ui-avatar', size === 32 && 'ui-avatar--32'];
47
+ if (empty) return `<span class="${esc(cx(...base, 'ui-avatar--empty'))}" aria-hidden="true"></span>`;
48
+ const letters = initials || avatarInitials(name);
49
+ if (!letters) return `<span class="${esc(cx(...base, 'ui-avatar--anon'))}" aria-hidden="true">${icon('user')}</span>`;
50
+ const hue = avatarHue(key ?? name ?? letters);
51
+ const img = src ? `<img data-avatar-photo src="${esc(safeUrl(src))}" alt="" loading="lazy">` : '';
52
+ return `<span class="${esc(cx(...base, `ui-avatar--h${hue}`))}" data-initials="${letters.length}" aria-hidden="true">${esc(letters)}${img}</span>`;
53
+ }