@c2n/avatar 0.0.8 → 0.0.10
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 +11 -2
- package/dist/avatar.js +295 -10
- package/package.json +3 -3
- package/react.d.ts +2 -1
- package/types/src/avatar-group.d.ts +78 -0
- package/types/src/avatar-group.d.ts.map +1 -0
- package/types/src/avatar.d.ts +56 -0
- package/types/src/avatar.d.ts.map +1 -1
- package/vue.d.ts +13 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @c2n/avatar
|
|
2
2
|
|
|
3
|
-
Avatar built with Lit: image, initials or
|
|
3
|
+
Avatar components built with Lit: `c2-avatar` renders an image, initials or slotted content, while `c2-avatar-group` creates a responsive overlapping stack with an automatic overflow count.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npm install @c2n/avatar
|
|
@@ -14,6 +14,14 @@ npm install @c2n/avatar
|
|
|
14
14
|
<c2-avatar name="Ada Lovelace" initial-count="2"></c2-avatar>
|
|
15
15
|
<c2-avatar name="Ada Lovelace" src="/ada.jpg" status="online"></c2-avatar>
|
|
16
16
|
<c2-avatar name="Ada Lovelace" auto-color initial-count="2"></c2-avatar>
|
|
17
|
+
<c2-avatar name="Ada Lovelace" src="/ada.jpg" editable></c2-avatar>
|
|
18
|
+
|
|
19
|
+
<c2-avatar-group aria-label="Contributors" style="--c2-avatar-group--max-width: 160px">
|
|
20
|
+
<c2-avatar auto-color name="Ada Lovelace"></c2-avatar>
|
|
21
|
+
<c2-avatar auto-color name="Grace Hopper"></c2-avatar>
|
|
22
|
+
<c2-avatar auto-color name="Alan Turing"></c2-avatar>
|
|
23
|
+
<c2-avatar auto-color name="Katherine Johnson"></c2-avatar>
|
|
24
|
+
</c2-avatar-group>
|
|
17
25
|
```
|
|
18
26
|
|
|
19
27
|
- **Content resolution**: the `src` image first (a failed load fires `error` and falls back to the initials), then slotted content (an icon, an emoji, an `<img>`), then the initials computed from `name`. `initial-count="2"` uses the first and last word of a longer name; `initials` overrides the computation.
|
|
@@ -21,5 +29,6 @@ npm install @c2n/avatar
|
|
|
21
29
|
- **Status and badge**: `status="online|away|busy|offline"` draws a dot in the bottom-right corner; the `badge` slot replaces it with your own element (a count, an icon) and gets the same ring.
|
|
22
30
|
- **Sizing**: one `--c2-avatar--size` token drives width, height and the font size (40% of the size by default); `--c2-avatar--width` and `--height` override it for non-square shapes.
|
|
23
31
|
- **Accessibility**: the avatar is `role="img"` labelled by `alt` or `name`.
|
|
32
|
+
- **Editing**: `editable` lets the user choose, replace, and remove an image. Listen for `avatar-change` to upload its `detail.file`, and `avatar-remove` to delete the persisted image. The component immediately displays a local preview; use `accept` and `max-size` for client-side validation.
|
|
24
33
|
|
|
25
|
-
Every visual aspect is a CSS custom property (`--c2-avatar--*`, `--c2-avatar__badge--*`, `--c2-avatar__image--*`).
|
|
34
|
+
Every visual aspect is a CSS custom property (`--c2-avatar--*`, `--c2-avatar-group--*`, `--c2-avatar__badge--*`, `--c2-avatar__image--*`). The full list is in `custom-elements.json` and on the docs site.
|
package/dist/avatar.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { LitElement, html, nothing, unsafeCSS } from "lit";
|
|
2
|
-
import { property, state } from "lit/decorators.js";
|
|
2
|
+
import { property, query, state } from "lit/decorators.js";
|
|
3
3
|
import { customElement } from "@c2n/core/element-helper.js";
|
|
4
4
|
import { classMap } from "lit/directives/class-map.js";
|
|
5
5
|
import { ifDefined } from "lit/directives/if-defined.js";
|
|
6
6
|
//#region src/avatar.scss?inline
|
|
7
|
-
var avatar_default = "/* ex : var((width: 24px), width, c2-checkbox) returns var(--c2-checkbox-width, 24px) */\n:host {\n display: inline-flex;\n position: relative;\n vertical-align: middle;\n flex: none;\n}\n\n.c2-avatar {\n display: flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n overflow: hidden;\n user-select: none;\n line-height: 1;\n width: var(--c2-avatar--width,var(--c2-avatar--size,32px));\n height: var(--c2-avatar--height,var(--c2-avatar--size,32px));\n font-size: var(--c2-avatar--font-size,calc(var(--c2-avatar--size, 32px) * 0.4));\n font-weight: var(--c2-avatar--font-weight,600);\n font-style: var(--c2-avatar--font-style);\n font-family: var(--c2-avatar--font-family);\n letter-spacing: var(--c2-avatar--letter-spacing,0.02em);\n border-top-left-radius: var(--c2-avatar--border-top-left-radius,999px);\n border-top-right-radius: var(--c2-avatar--border-top-right-radius,999px);\n border-bottom-left-radius: var(--c2-avatar--border-bottom-left-radius,999px);\n border-bottom-right-radius: var(--c2-avatar--border-bottom-right-radius,999px);\n border-top: var(--c2-avatar--border-top);\n border-bottom: var(--c2-avatar--border-bottom);\n border-right: var(--c2-avatar--border-right);\n border-left: var(--c2-avatar--border-left);\n background: var(--c2-avatar--background,rgb(0, 122, 77));\n color: var(--c2-avatar--color,#ffffff);\n box-shadow: var(--c2-avatar--box-shadow);\n}\n.c2-avatar ::slotted(svg),\n.c2-avatar ::slotted(img) {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: var(--c2-avatar__image--object-fit,cover);\n}\n.c2-avatar ::slotted(svg) {\n width: 60%;\n height: 60%;\n}\n\n:host([auto-color]) .c2-avatar {\n background: hsl(var(--c2-avatar--hue, 160), var(--c2-avatar__auto-color--saturation,55%), var(--c2-avatar__auto-color--lightness,42%));\n}\n\n.c2-avatar-image {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: var(--c2-avatar__image--object-fit,cover);\n}\n\n.c2-avatar-badge {\n position: absolute;\n right: calc(-1 * var(--c2-avatar__badge--offset,0px));\n bottom: calc(-1 * var(--c2-avatar__badge--offset,0px));\n display: flex;\n z-index: 1;\n line-height: 1;\n}\n.c2-avatar-badge::slotted(*) {\n box-sizing: border-box;\n border: var(--c2-avatar__badge--border,2px solid #ffffff);\n border-radius: 999px;\n}\n\n.c2-avatar-status {\n display: block;\n box-sizing: content-box;\n width: var(--c2-avatar__badge--size,10px);\n height: var(--c2-avatar__badge--size,10px);\n border: var(--c2-avatar__badge--border,2px solid #ffffff);\n border-radius: 999px;\n background: var(--c2-avatar__badge__offline--color,rgb(161, 161, 170));\n}\n\n:host([status=online]) .c2-avatar-status {\n background: var(--c2-avatar__badge__online--color,rgb(34, 197, 94));\n}\n\n:host([status=away]) .c2-avatar-status {\n background: var(--c2-avatar__badge__away--color,rgb(245, 158, 11));\n}\n\n:host([status=busy]) .c2-avatar-status {\n background: var(--c2-avatar__badge__busy--color,rgb(239, 68, 68));\n}";
|
|
7
|
+
var avatar_default = "/* ex : var((width: 24px), width, c2-checkbox) returns var(--c2-checkbox-width, 24px) */\n:host {\n display: inline-flex;\n position: relative;\n vertical-align: middle;\n flex: none;\n}\n\ninput {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n\n.c2-avatar {\n display: flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n overflow: hidden;\n user-select: none;\n line-height: 1;\n width: var(--c2-avatar--width,var(--c2-avatar--size,32px));\n height: var(--c2-avatar--height,var(--c2-avatar--size,32px));\n font-size: var(--c2-avatar--font-size,calc(var(--c2-avatar--size, 32px) * 0.4));\n font-weight: var(--c2-avatar--font-weight,600);\n font-style: var(--c2-avatar--font-style);\n font-family: var(--c2-avatar--font-family);\n letter-spacing: var(--c2-avatar--letter-spacing,0.02em);\n border-top-left-radius: var(--c2-avatar--border-top-left-radius,999px);\n border-top-right-radius: var(--c2-avatar--border-top-right-radius,999px);\n border-bottom-left-radius: var(--c2-avatar--border-bottom-left-radius,999px);\n border-bottom-right-radius: var(--c2-avatar--border-bottom-right-radius,999px);\n border-top: var(--c2-avatar--border-top);\n border-bottom: var(--c2-avatar--border-bottom);\n border-right: var(--c2-avatar--border-right);\n border-left: var(--c2-avatar--border-left);\n background: var(--c2-avatar--background,rgb(0, 122, 77));\n color: var(--c2-avatar--color,#ffffff);\n box-shadow: var(--c2-avatar--box-shadow);\n}\n.c2-avatar ::slotted(svg),\n.c2-avatar ::slotted(img) {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: var(--c2-avatar__image--object-fit,cover);\n}\n.c2-avatar ::slotted(svg) {\n width: 60%;\n height: 60%;\n}\n\n:host([auto-color]) .c2-avatar {\n background: hsl(var(--c2-avatar--hue, 160), var(--c2-avatar__auto-color--saturation,55%), var(--c2-avatar__auto-color--lightness,42%));\n}\n\n.c2-avatar-image {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: var(--c2-avatar__image--object-fit,cover);\n}\n\n.avatar-trigger {\n all: unset;\n position: relative;\n display: inline-flex;\n border-radius: var(--c2-avatar--border-top-left-radius,999px) var(--c2-avatar--border-top-right-radius,999px) var(--c2-avatar--border-bottom-right-radius,999px) var(--c2-avatar--border-bottom-left-radius,999px);\n cursor: pointer;\n}\n.avatar-trigger:focus-visible {\n outline: var(--c2-avatar__editor__focus--outline,2px solid rgba(2, 101, 220, 0.45));\n outline-offset: var(--c2-avatar__editor__focus--outline-offset,2px);\n}\n.avatar-trigger:disabled {\n cursor: default;\n}\n\n.edit-overlay {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n border-top-left-radius: var(--c2-avatar--border-top-left-radius,999px);\n border-top-right-radius: var(--c2-avatar--border-top-right-radius,999px);\n border-bottom-left-radius: var(--c2-avatar--border-bottom-left-radius,999px);\n border-bottom-right-radius: var(--c2-avatar--border-bottom-right-radius,999px);\n background: var(--c2-avatar__editor--background,rgba(0, 0, 0, 0.5));\n color: var(--c2-avatar__editor--color,#ffffff);\n opacity: 0;\n transition: opacity 250ms cubic-bezier(0.2, 0, 0, 1);\n}\n.edit-overlay svg,\n.edit-overlay ::slotted(*) {\n display: block;\n width: var(--c2-avatar__editor-icon--size,18px);\n height: var(--c2-avatar__editor-icon--size,18px);\n}\n\n.avatar-trigger:hover:not(:disabled) .edit-overlay,\n.avatar-trigger:focus-visible .edit-overlay {\n opacity: 1;\n}\n\n.remove-action {\n all: unset;\n position: absolute;\n z-index: 2;\n top: -6px;\n right: -6px;\n display: flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n width: var(--c2-avatar__remove--size,22px);\n height: var(--c2-avatar__remove--size,22px);\n border: var(--c2-avatar__remove--border,1px solid #e4e4e7);\n border-radius: 999px;\n background: var(--c2-avatar__remove--background,#ffffff);\n color: var(--c2-avatar__remove--color,#52525b);\n box-shadow: var(--c2-avatar__remove--box-shadow,0 1px 3px rgba(0, 0, 0, 0.18));\n cursor: pointer;\n}\n.remove-action:focus-visible {\n outline: var(--c2-avatar__editor__focus--outline,2px solid rgba(2, 101, 220, 0.45));\n outline-offset: var(--c2-avatar__editor__focus--outline-offset,2px);\n}\n.remove-action:disabled {\n cursor: default;\n opacity: 0.38;\n}\n.remove-action svg,\n.remove-action ::slotted(*) {\n display: block;\n width: 60%;\n height: 60%;\n}\n\n.c2-avatar-badge {\n position: absolute;\n right: calc(-1 * var(--c2-avatar__badge--offset,0px));\n bottom: calc(-1 * var(--c2-avatar__badge--offset,0px));\n display: flex;\n z-index: 1;\n line-height: 1;\n}\n.c2-avatar-badge::slotted(*) {\n box-sizing: border-box;\n border: var(--c2-avatar__badge--border,2px solid #ffffff);\n border-radius: 999px;\n}\n\n.c2-avatar-status {\n display: block;\n box-sizing: content-box;\n width: var(--c2-avatar__badge--size,10px);\n height: var(--c2-avatar__badge--size,10px);\n border: var(--c2-avatar__badge--border,2px solid #ffffff);\n border-radius: 999px;\n background: var(--c2-avatar__badge__offline--color,rgb(161, 161, 170));\n}\n\n:host([status=online]) .c2-avatar-status {\n background: var(--c2-avatar__badge__online--color,rgb(34, 197, 94));\n}\n\n:host([status=away]) .c2-avatar-status {\n background: var(--c2-avatar__badge__away--color,rgb(245, 158, 11));\n}\n\n:host([status=busy]) .c2-avatar-status {\n background: var(--c2-avatar__badge__busy--color,rgb(239, 68, 68));\n}";
|
|
8
8
|
//#endregion
|
|
9
9
|
//#region \0@oxc-project+runtime@0.148.0/helpers/esm/decorate.js
|
|
10
10
|
function __decorate(decorators, target, key, desc) {
|
|
@@ -14,6 +14,141 @@ function __decorate(decorators, target, key, desc) {
|
|
|
14
14
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
15
15
|
}
|
|
16
16
|
//#endregion
|
|
17
|
+
//#region src/avatar-group.scss?inline
|
|
18
|
+
var avatar_group_default = "/* ex : var((width: 24px), width, c2-checkbox) returns var(--c2-checkbox-width, 24px) */\n:host {\n display: flex;\n box-sizing: border-box;\n width: 100%;\n max-width: var(--c2-avatar-group--max-width,320px);\n min-width: 0;\n}\n\n:host([hidden]) {\n display: none;\n}\n\n.group,\nslot {\n display: flex;\n align-items: center;\n min-width: 0;\n}\n\n.group {\n width: 100%;\n overflow: hidden;\n}\n\nslot {\n flex: none;\n padding-inline-start: var(--c2-avatar-group--overlap,10px);\n}\n\n::slotted(c2-avatar) {\n flex: none;\n margin-inline-start: calc(-1 * var(--c2-avatar-group--overlap,10px));\n --c2-avatar--box-shadow: var(--c2-avatar-group__avatar--box-shadow,0 0 0 2px #ffffff);\n}\n\n::slotted([data-c2-avatar-group-hidden]) {\n display: none;\n}\n\n.overflow {\n position: relative;\n z-index: 1;\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n width: var(--c2-avatar-group__overflow--size,32px);\n height: var(--c2-avatar-group__overflow--size,32px);\n margin-inline-start: calc(-1 * var(--c2-avatar-group--overlap,10px));\n border: var(--c2-avatar-group__overflow--border,2px solid #ffffff);\n border-radius: var(--c2-avatar-group__overflow--border-radius,999px);\n background: var(--c2-avatar-group__overflow--background,#52525b);\n color: var(--c2-avatar-group__overflow--color,#ffffff);\n font-size: var(--c2-avatar-group__overflow--font-size,12px);\n font-weight: var(--c2-avatar-group__overflow--font-weight,600);\n line-height: 1;\n white-space: nowrap;\n}\n\n.overflow[data-empty] {\n position: absolute;\n visibility: hidden;\n pointer-events: none;\n}";
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/avatar-group.ts
|
|
21
|
+
var HIDDEN_ATTRIBUTE = "data-c2-avatar-group-hidden";
|
|
22
|
+
var AvatarGroup = class AvatarGroup extends LitElement {
|
|
23
|
+
constructor(..._args) {
|
|
24
|
+
super(..._args);
|
|
25
|
+
this.maxVisible = 0;
|
|
26
|
+
this.countMode = "hidden";
|
|
27
|
+
this.ariaLabel = "Avatar group";
|
|
28
|
+
this.renderedVisibleCount = 0;
|
|
29
|
+
this.renderedTotalCount = 0;
|
|
30
|
+
this.measureFrame = 0;
|
|
31
|
+
}
|
|
32
|
+
static {
|
|
33
|
+
this.styles = unsafeCSS(avatar_group_default);
|
|
34
|
+
}
|
|
35
|
+
/** Current number of slotted avatars. */
|
|
36
|
+
get total() {
|
|
37
|
+
return this.renderedTotalCount;
|
|
38
|
+
}
|
|
39
|
+
/** Current number of visible avatars. */
|
|
40
|
+
get visible() {
|
|
41
|
+
return this.renderedVisibleCount;
|
|
42
|
+
}
|
|
43
|
+
/** Current number of avatars represented by the overflow count. */
|
|
44
|
+
get hiddenCount() {
|
|
45
|
+
return Math.max(0, this.total - this.visible);
|
|
46
|
+
}
|
|
47
|
+
firstUpdated() {
|
|
48
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
49
|
+
this.resizeObserver = new ResizeObserver(() => this.scheduleMeasure());
|
|
50
|
+
this.resizeObserver.observe(this);
|
|
51
|
+
this.scheduleMeasure();
|
|
52
|
+
}
|
|
53
|
+
updated(changed) {
|
|
54
|
+
if (changed.has("maxVisible")) this.scheduleMeasure();
|
|
55
|
+
}
|
|
56
|
+
disconnectedCallback() {
|
|
57
|
+
super.disconnectedCallback();
|
|
58
|
+
this.resizeObserver?.disconnect();
|
|
59
|
+
cancelAnimationFrame(this.measureFrame);
|
|
60
|
+
this.avatarItems.forEach((item) => item.removeAttribute(HIDDEN_ATTRIBUTE));
|
|
61
|
+
}
|
|
62
|
+
get avatarItems() {
|
|
63
|
+
if (!this.slotElement) return [];
|
|
64
|
+
return this.slotElement.assignedElements({ flatten: true }).filter((element) => element instanceof HTMLElement && element.localName === "c2-avatar");
|
|
65
|
+
}
|
|
66
|
+
handleSlotChange() {
|
|
67
|
+
this.scheduleMeasure();
|
|
68
|
+
}
|
|
69
|
+
/** Recalculates the responsive limit after application code changes a slotted avatar's dimensions. */
|
|
70
|
+
refresh() {
|
|
71
|
+
this.scheduleMeasure();
|
|
72
|
+
}
|
|
73
|
+
scheduleMeasure() {
|
|
74
|
+
cancelAnimationFrame(this.measureFrame);
|
|
75
|
+
this.measureFrame = requestAnimationFrame(() => this.measure());
|
|
76
|
+
}
|
|
77
|
+
measure() {
|
|
78
|
+
const items = this.avatarItems;
|
|
79
|
+
items.forEach((item) => item.removeAttribute(HIDDEN_ATTRIBUTE));
|
|
80
|
+
const total = items.length;
|
|
81
|
+
const available = this.getBoundingClientRect().width;
|
|
82
|
+
if (!available || !total) {
|
|
83
|
+
this.commitCounts(total, total);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const styles = getComputedStyle(this);
|
|
87
|
+
const configuredOverlap = Number.parseFloat(styles.getPropertyValue("--c2-avatar-group--overlap"));
|
|
88
|
+
const overlap = Number.isFinite(configuredOverlap) ? Math.max(0, configuredOverlap) : 10;
|
|
89
|
+
const widths = items.map((item) => item.getBoundingClientRect().width);
|
|
90
|
+
const hardLimit = this.maxVisible > 0 ? Math.min(total, Math.floor(this.maxVisible)) : total;
|
|
91
|
+
const avatarsWidth = (count) => widths.slice(0, count).reduce((sum, width) => sum + width, 0) - Math.max(0, count - 1) * overlap;
|
|
92
|
+
const allAllowedFit = hardLimit === total && avatarsWidth(total) <= available;
|
|
93
|
+
let visible = total;
|
|
94
|
+
if (!allAllowedFit) {
|
|
95
|
+
const overflow = this.renderRoot.querySelector(".overflow")?.getBoundingClientRect().width ?? 0;
|
|
96
|
+
visible = 0;
|
|
97
|
+
for (let count = 0; count <= hardLimit; count += 1) if (avatarsWidth(count) + overflow - (count > 0 ? overlap : 0) <= available) visible = count;
|
|
98
|
+
else break;
|
|
99
|
+
}
|
|
100
|
+
items.forEach((item, index) => item.toggleAttribute(HIDDEN_ATTRIBUTE, index >= visible));
|
|
101
|
+
this.commitCounts(total, visible);
|
|
102
|
+
}
|
|
103
|
+
commitCounts(total, visible) {
|
|
104
|
+
if (total === this.renderedTotalCount && visible === this.renderedVisibleCount) return;
|
|
105
|
+
this.renderedTotalCount = total;
|
|
106
|
+
this.renderedVisibleCount = visible;
|
|
107
|
+
const detail = {
|
|
108
|
+
total,
|
|
109
|
+
visible,
|
|
110
|
+
hidden: Math.max(0, total - visible)
|
|
111
|
+
};
|
|
112
|
+
this.dispatchEvent(new CustomEvent("overflow-change", {
|
|
113
|
+
detail,
|
|
114
|
+
bubbles: true,
|
|
115
|
+
composed: true
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
render() {
|
|
119
|
+
const hidden = this.hiddenCount;
|
|
120
|
+
const count = this.countMode === "total" ? this.total : hidden;
|
|
121
|
+
const label = this.countMode === "total" ? `${this.total} avatars total` : `${hidden} more avatar${hidden === 1 ? "" : "s"}`;
|
|
122
|
+
return html`
|
|
123
|
+
<div class="group" role="group" aria-label=${this.ariaLabel || nothing}>
|
|
124
|
+
<slot @slotchange=${this.handleSlotChange}></slot>
|
|
125
|
+
<span
|
|
126
|
+
class="overflow"
|
|
127
|
+
part="overflow"
|
|
128
|
+
?data-empty=${hidden === 0}
|
|
129
|
+
aria-hidden=${hidden === 0 ? "true" : nothing}
|
|
130
|
+
aria-label=${hidden ? label : nothing}
|
|
131
|
+
>
|
|
132
|
+
${this.countMode === "hidden" ? "+" : ""}${count}
|
|
133
|
+
</span>
|
|
134
|
+
</div>
|
|
135
|
+
`;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
__decorate([property({
|
|
139
|
+
type: Number,
|
|
140
|
+
attribute: "max-visible"
|
|
141
|
+
})], AvatarGroup.prototype, "maxVisible", void 0);
|
|
142
|
+
__decorate([property({
|
|
143
|
+
reflect: true,
|
|
144
|
+
attribute: "count-mode"
|
|
145
|
+
})], AvatarGroup.prototype, "countMode", void 0);
|
|
146
|
+
__decorate([property({ attribute: "aria-label" })], AvatarGroup.prototype, "ariaLabel", void 0);
|
|
147
|
+
__decorate([state()], AvatarGroup.prototype, "renderedVisibleCount", void 0);
|
|
148
|
+
__decorate([state()], AvatarGroup.prototype, "renderedTotalCount", void 0);
|
|
149
|
+
__decorate([query("slot")], AvatarGroup.prototype, "slotElement", void 0);
|
|
150
|
+
AvatarGroup = __decorate([customElement("c2-avatar-group")], AvatarGroup);
|
|
151
|
+
//#endregion
|
|
17
152
|
//#region src/avatar.ts
|
|
18
153
|
var LEGACY_COUNT_ATTRIBUTE = "initialcount";
|
|
19
154
|
var Avatar = class Avatar extends LitElement {
|
|
@@ -26,7 +161,12 @@ var Avatar = class Avatar extends LitElement {
|
|
|
26
161
|
this.alt = void 0;
|
|
27
162
|
this.autoColor = false;
|
|
28
163
|
this.status = void 0;
|
|
164
|
+
this.editable = false;
|
|
165
|
+
this.disabled = false;
|
|
166
|
+
this.accept = "image/*";
|
|
167
|
+
this.maxSize = 0;
|
|
29
168
|
this.imageFailed = false;
|
|
169
|
+
this.previewUrl = void 0;
|
|
30
170
|
}
|
|
31
171
|
static {
|
|
32
172
|
this.styles = unsafeCSS(avatar_default);
|
|
@@ -56,14 +196,105 @@ var Avatar = class Avatar extends LitElement {
|
|
|
56
196
|
return (hash >>> 0) % 360;
|
|
57
197
|
}
|
|
58
198
|
willUpdate(changed) {
|
|
59
|
-
if (changed.has("src"))
|
|
199
|
+
if (changed.has("src")) {
|
|
200
|
+
this.imageFailed = false;
|
|
201
|
+
if (this.previewUrl) {
|
|
202
|
+
this.revokePreview();
|
|
203
|
+
this.selectedFile = void 0;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
disconnectedCallback() {
|
|
208
|
+
super.disconnectedCallback();
|
|
209
|
+
this.revokePreview();
|
|
210
|
+
}
|
|
211
|
+
connectedCallback() {
|
|
212
|
+
super.connectedCallback();
|
|
213
|
+
if (this.selectedFile && !this.previewUrl) this.previewUrl = URL.createObjectURL(this.selectedFile);
|
|
214
|
+
}
|
|
215
|
+
/** Locally selected image file, if the avatar was changed through its picker. */
|
|
216
|
+
get file() {
|
|
217
|
+
return this.selectedFile;
|
|
218
|
+
}
|
|
219
|
+
/** Opens the avatar image picker. */
|
|
220
|
+
browse() {
|
|
221
|
+
if (this.editable && !this.disabled) this.input.click();
|
|
222
|
+
}
|
|
223
|
+
/** Clears the selected preview and the current `src` image. */
|
|
224
|
+
removeImage() {
|
|
225
|
+
if (this.disabled || !this.previewUrl && !this.src) return;
|
|
226
|
+
const detail = {
|
|
227
|
+
file: this.selectedFile,
|
|
228
|
+
previousSrc: this.previewUrl ?? this.src
|
|
229
|
+
};
|
|
230
|
+
this.revokePreview();
|
|
231
|
+
this.selectedFile = void 0;
|
|
232
|
+
this.removeAttribute("src");
|
|
233
|
+
this.src = void 0;
|
|
234
|
+
this.imageFailed = false;
|
|
235
|
+
if (this.input) this.input.value = "";
|
|
236
|
+
this.dispatchEvent(new CustomEvent("avatar-remove", {
|
|
237
|
+
detail,
|
|
238
|
+
bubbles: true,
|
|
239
|
+
composed: true
|
|
240
|
+
}));
|
|
60
241
|
}
|
|
61
242
|
handleImageError() {
|
|
62
243
|
this.imageFailed = true;
|
|
63
244
|
this.dispatchEvent(new Event("error"));
|
|
64
245
|
}
|
|
65
|
-
|
|
66
|
-
|
|
246
|
+
handleFileChange(event) {
|
|
247
|
+
event.stopPropagation();
|
|
248
|
+
const input = event.currentTarget;
|
|
249
|
+
const file = input.files?.[0];
|
|
250
|
+
input.value = "";
|
|
251
|
+
if (!file) return;
|
|
252
|
+
const rejection = this.validateFile(file);
|
|
253
|
+
if (rejection) {
|
|
254
|
+
this.dispatchEvent(new CustomEvent("file-reject", {
|
|
255
|
+
detail: rejection,
|
|
256
|
+
bubbles: true,
|
|
257
|
+
composed: true
|
|
258
|
+
}));
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
this.revokePreview();
|
|
262
|
+
this.selectedFile = file;
|
|
263
|
+
this.previewUrl = URL.createObjectURL(file);
|
|
264
|
+
this.imageFailed = false;
|
|
265
|
+
this.dispatchEvent(new CustomEvent("avatar-change", {
|
|
266
|
+
detail: {
|
|
267
|
+
file,
|
|
268
|
+
previewUrl: this.previewUrl
|
|
269
|
+
},
|
|
270
|
+
bubbles: true,
|
|
271
|
+
composed: true
|
|
272
|
+
}));
|
|
273
|
+
}
|
|
274
|
+
stopNativeInput(event) {
|
|
275
|
+
event.stopPropagation();
|
|
276
|
+
}
|
|
277
|
+
validateFile(file) {
|
|
278
|
+
const rules = this.accept.split(",").map((rule) => rule.trim().toLowerCase()).filter(Boolean);
|
|
279
|
+
const name = file.name.toLowerCase();
|
|
280
|
+
const type = file.type.toLowerCase();
|
|
281
|
+
if (!(rules.length === 0 || rules.some((rule) => rule.startsWith(".") ? name.endsWith(rule) : rule.endsWith("/*") ? type.startsWith(rule.slice(0, -1)) : type === rule))) return {
|
|
282
|
+
file,
|
|
283
|
+
reason: "type",
|
|
284
|
+
message: `${file.name} is not an accepted image type.`
|
|
285
|
+
};
|
|
286
|
+
if (this.maxSize > 0 && file.size > this.maxSize) return {
|
|
287
|
+
file,
|
|
288
|
+
reason: "size",
|
|
289
|
+
message: `${file.name} exceeds the avatar size limit.`
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
revokePreview() {
|
|
293
|
+
if (this.previewUrl) URL.revokeObjectURL(this.previewUrl);
|
|
294
|
+
this.previewUrl = void 0;
|
|
295
|
+
}
|
|
296
|
+
renderVisual(source, interactive) {
|
|
297
|
+
const showImage = !!source && !this.imageFailed;
|
|
67
298
|
const label = this.alt ?? this.name;
|
|
68
299
|
return html`
|
|
69
300
|
<div
|
|
@@ -72,18 +303,57 @@ var Avatar = class Avatar extends LitElement {
|
|
|
72
303
|
"has-image": showImage
|
|
73
304
|
})}
|
|
74
305
|
style=${this.autoColor ? `--c2-avatar--hue: ${this.hue}` : nothing}
|
|
75
|
-
role
|
|
76
|
-
aria-label=${label || nothing}
|
|
306
|
+
role=${interactive ? nothing : "img"}
|
|
307
|
+
aria-label=${interactive ? nothing : label || nothing}
|
|
308
|
+
aria-hidden=${interactive ? "true" : nothing}
|
|
77
309
|
>
|
|
78
310
|
${showImage ? html`<img
|
|
79
311
|
class="c2-avatar-image"
|
|
80
|
-
src=${
|
|
81
|
-
alt=${ifDefined(this.alt ?? (this.name || void 0))}
|
|
312
|
+
src=${source}
|
|
313
|
+
alt=${interactive ? "" : ifDefined(this.alt ?? (this.name || void 0))}
|
|
82
314
|
loading="lazy"
|
|
83
315
|
@error=${this.handleImageError}
|
|
84
316
|
/>` : html`<slot>${this.initial}</slot>`}
|
|
85
317
|
</div>
|
|
318
|
+
`;
|
|
319
|
+
}
|
|
320
|
+
render() {
|
|
321
|
+
const source = this.previewUrl ?? this.src;
|
|
322
|
+
const showImage = !!source && !this.imageFailed;
|
|
323
|
+
const subject = this.name ? `${this.name} avatar` : "avatar";
|
|
324
|
+
return html`
|
|
325
|
+
<input
|
|
326
|
+
type="file"
|
|
327
|
+
tabindex="-1"
|
|
328
|
+
.accept=${this.accept}
|
|
329
|
+
?disabled=${!this.editable || this.disabled}
|
|
330
|
+
aria-label=${`${subject} image file`}
|
|
331
|
+
@input=${this.stopNativeInput}
|
|
332
|
+
@change=${this.handleFileChange}
|
|
333
|
+
/>
|
|
334
|
+
${this.editable ? html`<button
|
|
335
|
+
type="button"
|
|
336
|
+
class="avatar-trigger"
|
|
337
|
+
?disabled=${this.disabled}
|
|
338
|
+
aria-label=${showImage ? `Change ${subject}` : `Upload ${subject}`}
|
|
339
|
+
@click=${this.browse}
|
|
340
|
+
>
|
|
341
|
+
${this.renderVisual(source, true)}
|
|
342
|
+
<span class="edit-overlay" aria-hidden="true">
|
|
343
|
+
<slot name="edit-icon">
|
|
344
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
345
|
+
<path d="M14.5 4h-5L8 6H5a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-3l-1.5-2z"></path>
|
|
346
|
+
<circle cx="12" cy="13" r="3"></circle>
|
|
347
|
+
</svg>
|
|
348
|
+
</slot>
|
|
349
|
+
</span>
|
|
350
|
+
</button>` : this.renderVisual(source, false)}
|
|
86
351
|
<slot name="badge" class="c2-avatar-badge"> ${this.status ? html`<span class="c2-avatar-status" aria-label=${this.status}></span>` : nothing} </slot>
|
|
352
|
+
${this.editable && showImage ? html`<button type="button" class="remove-action" ?disabled=${this.disabled} aria-label=${`Remove ${subject}`} @click=${this.removeImage}>
|
|
353
|
+
<slot name="remove-icon">
|
|
354
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"></path></svg>
|
|
355
|
+
</slot>
|
|
356
|
+
</button>` : nothing}
|
|
87
357
|
`;
|
|
88
358
|
}
|
|
89
359
|
};
|
|
@@ -101,7 +371,22 @@ __decorate([property({
|
|
|
101
371
|
attribute: "auto-color"
|
|
102
372
|
})], Avatar.prototype, "autoColor", void 0);
|
|
103
373
|
__decorate([property({ reflect: true })], Avatar.prototype, "status", void 0);
|
|
374
|
+
__decorate([property({
|
|
375
|
+
type: Boolean,
|
|
376
|
+
reflect: true
|
|
377
|
+
})], Avatar.prototype, "editable", void 0);
|
|
378
|
+
__decorate([property({
|
|
379
|
+
type: Boolean,
|
|
380
|
+
reflect: true
|
|
381
|
+
})], Avatar.prototype, "disabled", void 0);
|
|
382
|
+
__decorate([property()], Avatar.prototype, "accept", void 0);
|
|
383
|
+
__decorate([property({
|
|
384
|
+
type: Number,
|
|
385
|
+
attribute: "max-size"
|
|
386
|
+
})], Avatar.prototype, "maxSize", void 0);
|
|
104
387
|
__decorate([state()], Avatar.prototype, "imageFailed", void 0);
|
|
388
|
+
__decorate([state()], Avatar.prototype, "previewUrl", void 0);
|
|
389
|
+
__decorate([query("input", true)], Avatar.prototype, "input", void 0);
|
|
105
390
|
Avatar = __decorate([customElement("c2-avatar")], Avatar);
|
|
106
391
|
//#endregion
|
|
107
|
-
export { Avatar };
|
|
392
|
+
export { Avatar, AvatarGroup };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c2n/avatar",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/avatar.js",
|
|
6
6
|
"exports": {
|
|
@@ -61,12 +61,12 @@
|
|
|
61
61
|
}
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"@c2n/core": "0.0.
|
|
64
|
+
"@c2n/core": "0.0.10",
|
|
65
65
|
"lit": "3.3.3"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@c2n/config": "*"
|
|
69
69
|
},
|
|
70
70
|
"customElements": "custom-elements.json",
|
|
71
|
-
"gitHead": "
|
|
71
|
+
"gitHead": "8d57710b3345fcb01cdb4cb15d2d43ba8bf21642"
|
|
72
72
|
}
|
package/react.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// attribute and it stringifies.
|
|
9
9
|
|
|
10
10
|
import type { DetailedHTMLProps, HTMLAttributes } from 'react'
|
|
11
|
-
import type { Avatar } from '@c2n/avatar'
|
|
11
|
+
import type { Avatar, AvatarGroup } from '@c2n/avatar'
|
|
12
12
|
|
|
13
13
|
/** Standard React host-element attributes plus the element's own public properties. */
|
|
14
14
|
type C2Props<T> = DetailedHTMLProps<HTMLAttributes<T>, T> & Partial<Omit<T, keyof HTMLElement>>
|
|
@@ -17,6 +17,7 @@ declare module 'react' {
|
|
|
17
17
|
namespace JSX {
|
|
18
18
|
interface IntrinsicElements {
|
|
19
19
|
'c2-avatar': C2Props<Avatar>
|
|
20
|
+
'c2-avatar-group': C2Props<AvatarGroup>
|
|
20
21
|
}
|
|
21
22
|
}
|
|
22
23
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { LitElement, type PropertyValues } from 'lit';
|
|
2
|
+
import type { TypedAddEventListener, TypedRemoveEventListener } from '@c2n/core/event-helper.js';
|
|
3
|
+
export type AvatarGroupCountMode = 'hidden' | 'total';
|
|
4
|
+
export interface AvatarGroupOverflowDetail {
|
|
5
|
+
total: number;
|
|
6
|
+
visible: number;
|
|
7
|
+
hidden: number;
|
|
8
|
+
}
|
|
9
|
+
export interface AvatarGroupEventMap {
|
|
10
|
+
'overflow-change': CustomEvent<AvatarGroupOverflowDetail>;
|
|
11
|
+
}
|
|
12
|
+
export interface AvatarGroup {
|
|
13
|
+
addEventListener: TypedAddEventListener<AvatarGroup, AvatarGroupEventMap>;
|
|
14
|
+
removeEventListener: TypedRemoveEventListener<AvatarGroup, AvatarGroupEventMap>;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Responsive stack of `c2-avatar` elements. It observes its rendered width and each avatar's actual size, then hides
|
|
18
|
+
* the avatars that no longer fit and replaces them with a count. The host fills the available inline space up to
|
|
19
|
+
* `--c2-avatar-group--max-width`, so it also reacts when its parent becomes narrower.
|
|
20
|
+
*
|
|
21
|
+
* @tag c2-avatar-group
|
|
22
|
+
*
|
|
23
|
+
* @slotcomponent c2-avatar
|
|
24
|
+
*
|
|
25
|
+
* @slot - `c2-avatar` elements displayed in source order.
|
|
26
|
+
*
|
|
27
|
+
* @event {CustomEvent<AvatarGroupOverflowDetail>} overflow-change - Fired when resizing changes the visible or hidden counts.
|
|
28
|
+
*
|
|
29
|
+
* @csspart overflow - Count shown after the visible avatars.
|
|
30
|
+
*
|
|
31
|
+
* @cssproperty {pixel} [--c2-avatar-group--max-width=320px] - Maximum responsive width of the group.
|
|
32
|
+
* @cssproperty {pixel} [--c2-avatar-group--overlap=10px] - Amount that adjacent avatars overlap.
|
|
33
|
+
* @cssproperty {box-shadow} [--c2-avatar-group__avatar--box-shadow=0 0 0 2px #ffffff] - Ring applied to slotted avatars so overlaps stay distinct.
|
|
34
|
+
* @cssproperty {pixel} [--c2-avatar-group__overflow--size=32px] - Width and height of the overflow count.
|
|
35
|
+
* @cssproperty {color} [--c2-avatar-group__overflow--background=#52525b] - Overflow count background.
|
|
36
|
+
* @cssproperty {color} [--c2-avatar-group__overflow--color=#ffffff] - Overflow count text colour.
|
|
37
|
+
* @cssproperty {border} [--c2-avatar-group__overflow--border=2px solid #ffffff] - Ring around the overflow count.
|
|
38
|
+
* @cssproperty {border-radius} [--c2-avatar-group__overflow--border-radius=999px] - Overflow count corner radius.
|
|
39
|
+
* @cssproperty {font-size} [--c2-avatar-group__overflow--font-size=12px] - Overflow count text size.
|
|
40
|
+
* @cssproperty {font-weight} [--c2-avatar-group__overflow--font-weight=600] - Overflow count text weight.
|
|
41
|
+
*/
|
|
42
|
+
export declare class AvatarGroup extends LitElement {
|
|
43
|
+
static styles: import("lit").CSSResult;
|
|
44
|
+
/** Optional hard cap in addition to the automatically calculated responsive limit. Zero means no hard cap. */
|
|
45
|
+
maxVisible: number;
|
|
46
|
+
/** Whether the overflow indicator shows `+hidden` or the total number of avatars. */
|
|
47
|
+
countMode: AvatarGroupCountMode;
|
|
48
|
+
/** Accessible name for the group. */
|
|
49
|
+
ariaLabel: string;
|
|
50
|
+
private renderedVisibleCount;
|
|
51
|
+
private renderedTotalCount;
|
|
52
|
+
private slotElement;
|
|
53
|
+
private resizeObserver?;
|
|
54
|
+
private measureFrame;
|
|
55
|
+
/** Current number of slotted avatars. */
|
|
56
|
+
get total(): number;
|
|
57
|
+
/** Current number of visible avatars. */
|
|
58
|
+
get visible(): number;
|
|
59
|
+
/** Current number of avatars represented by the overflow count. */
|
|
60
|
+
get hiddenCount(): number;
|
|
61
|
+
protected firstUpdated(): void;
|
|
62
|
+
protected updated(changed: PropertyValues<this>): void;
|
|
63
|
+
disconnectedCallback(): void;
|
|
64
|
+
private get avatarItems();
|
|
65
|
+
private handleSlotChange;
|
|
66
|
+
/** Recalculates the responsive limit after application code changes a slotted avatar's dimensions. */
|
|
67
|
+
refresh(): void;
|
|
68
|
+
private scheduleMeasure;
|
|
69
|
+
private measure;
|
|
70
|
+
private commitCounts;
|
|
71
|
+
render(): import("lit-html").TemplateResult<1>;
|
|
72
|
+
}
|
|
73
|
+
declare global {
|
|
74
|
+
interface HTMLElementTagNameMap {
|
|
75
|
+
'c2-avatar-group': AvatarGroup;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=avatar-group.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"avatar-group.d.ts","sourceRoot":"","sources":["../../src/avatar-group.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAA4B,KAAK,cAAc,EAAE,MAAM,KAAK,CAAA;AAG/E,OAAO,KAAK,EAAE,qBAAqB,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAA;AAGhG,MAAM,MAAM,oBAAoB,GAAG,QAAQ,GAAG,OAAO,CAAA;AAErD,MAAM,WAAW,yBAAyB;IACxC,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,mBAAmB;IAClC,iBAAiB,EAAE,WAAW,CAAC,yBAAyB,CAAC,CAAA;CAC1D;AAED,MAAM,WAAW,WAAW;IAC1B,gBAAgB,EAAE,qBAAqB,CAAC,WAAW,EAAE,mBAAmB,CAAC,CAAA;IACzE,mBAAmB,EAAE,wBAAwB,CAAC,WAAW,EAAE,mBAAmB,CAAC,CAAA;CAChF;AAID;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,qBACa,WAAY,SAAQ,UAAU;IACzC,OAAgB,MAAM,0BAAoB;IAE1C,8GAA8G;IACxD,UAAU,SAAI;IAEpE,qFAAqF;IAC/B,SAAS,EAAE,oBAAoB,CAAW;IAEhG,qCAAqC;IACW,SAAS,SAAiB;IAEjE,OAAO,CAAC,oBAAoB,CAAI;IAChC,OAAO,CAAC,kBAAkB,CAAI;IACxB,OAAO,CAAC,WAAW,CAAkB;IAEpD,OAAO,CAAC,cAAc,CAAC,CAAgB;IACvC,OAAO,CAAC,YAAY,CAAI;IAExB,yCAAyC;IACzC,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,yCAAyC;IACzC,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,mEAAmE;IACnE,IAAI,WAAW,IAAI,MAAM,CAExB;cAEkB,YAAY,IAAI,IAAI;cAOpB,OAAO,CAAC,OAAO,EAAE,cAAc,CAAC,IAAI,CAAC,GAAG,IAAI;IAItD,oBAAoB,IAAI,IAAI;IAOrC,OAAO,KAAK,WAAW,GAKtB;IAED,OAAO,CAAC,gBAAgB;IAIxB,sGAAsG;IACtG,OAAO,IAAI,IAAI;IAIf,OAAO,CAAC,eAAe;IAKvB,OAAO,CAAC,OAAO;IAkCf,OAAO,CAAC,YAAY;IAQX,MAAM;CAoBhB;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,iBAAiB,EAAE,WAAW,CAAA;KAC/B;CACF"}
|
package/types/src/avatar.d.ts
CHANGED
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
import { LitElement, type PropertyValues } from 'lit';
|
|
2
2
|
import type { TypedAddEventListener, TypedRemoveEventListener } from '@c2n/core/event-helper.js';
|
|
3
3
|
export type AvatarStatus = 'online' | 'away' | 'busy' | 'offline';
|
|
4
|
+
export interface AvatarChangeDetail {
|
|
5
|
+
file: File;
|
|
6
|
+
previewUrl: string;
|
|
7
|
+
}
|
|
8
|
+
export interface AvatarRemoveDetail {
|
|
9
|
+
file?: File;
|
|
10
|
+
previousSrc?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface AvatarFileRejection {
|
|
13
|
+
file: File;
|
|
14
|
+
reason: 'type' | 'size';
|
|
15
|
+
message: string;
|
|
16
|
+
}
|
|
4
17
|
/** Events fired by {@link Avatar}, keyed for `addEventListener`. */
|
|
5
18
|
export interface AvatarEventMap {
|
|
6
19
|
error: Event;
|
|
20
|
+
'avatar-change': CustomEvent<AvatarChangeDetail>;
|
|
21
|
+
'avatar-remove': CustomEvent<AvatarRemoveDetail>;
|
|
22
|
+
'file-reject': CustomEvent<AvatarFileRejection>;
|
|
7
23
|
}
|
|
8
24
|
export interface Avatar {
|
|
9
25
|
addEventListener: TypedAddEventListener<Avatar, AvatarEventMap>;
|
|
@@ -19,8 +35,13 @@ export interface Avatar {
|
|
|
19
35
|
*
|
|
20
36
|
* @slot - Custom content shown instead of the initials (an icon, an emoji, an `<img>`).
|
|
21
37
|
* @slot badge - Small element pinned to the bottom-right corner (a count, an icon). Replaces the `status` dot.
|
|
38
|
+
* @slot edit-icon - Icon shown over an editable avatar. Defaults to a camera.
|
|
39
|
+
* @slot remove-icon - Icon shown in the remove action. Defaults to a close mark.
|
|
22
40
|
*
|
|
23
41
|
* @event {Event} error - Fired when the `src` image fails to load; the initials are shown instead.
|
|
42
|
+
* @event {CustomEvent<AvatarChangeDetail>} avatar-change - Fired after an editable avatar accepts a local image. Upload `detail.file` and use `detail.previewUrl` for the immediate preview.
|
|
43
|
+
* @event {CustomEvent<AvatarRemoveDetail>} avatar-remove - Fired after the current local preview or `src` image is removed.
|
|
44
|
+
* @event {CustomEvent<AvatarFileRejection>} file-reject - Fired when an editable avatar rejects a file by type or size.
|
|
24
45
|
*
|
|
25
46
|
* @cssproperty {pixel} [--c2-avatar--size=32px]
|
|
26
47
|
* @cssproperty {pixel} --c2-avatar--width - Falls back to `--c2-avatar--size`.
|
|
@@ -49,6 +70,16 @@ export interface Avatar {
|
|
|
49
70
|
* @cssproperty {lightness} [--c2-avatar__auto-color--lightness=42%] - Lightness of the hue derived from the name.
|
|
50
71
|
*
|
|
51
72
|
* @cssproperty {object-fit} [--c2-avatar__image--object-fit=cover]
|
|
73
|
+
* @cssproperty {color} [--c2-avatar__editor--background=rgba(0, 0, 0, 0.5)]
|
|
74
|
+
* @cssproperty {color} [--c2-avatar__editor--color=#ffffff]
|
|
75
|
+
* @cssproperty {outline} [--c2-avatar__editor__focus--outline=2px solid rgba(2, 101, 220, 0.45)]
|
|
76
|
+
* @cssproperty {pixel} [--c2-avatar__editor__focus--outline-offset=2px]
|
|
77
|
+
* @cssproperty {pixel} [--c2-avatar__editor-icon--size=18px]
|
|
78
|
+
* @cssproperty {pixel} [--c2-avatar__remove--size=22px]
|
|
79
|
+
* @cssproperty {color} [--c2-avatar__remove--background=#ffffff]
|
|
80
|
+
* @cssproperty {color} [--c2-avatar__remove--color=#52525b]
|
|
81
|
+
* @cssproperty {border} [--c2-avatar__remove--border=1px solid #e4e4e7]
|
|
82
|
+
* @cssproperty {box-shadow} [--c2-avatar__remove--box-shadow=0 1px 3px rgba(0, 0, 0, 0.18)]
|
|
52
83
|
*
|
|
53
84
|
* @cssproperty {pixel} [--c2-avatar__badge--size=10px] - Diameter of the status dot; slotted badges size themselves.
|
|
54
85
|
* @cssproperty {border} [--c2-avatar__badge--border=2px solid #ffffff] - Ring separating the badge from the avatar.
|
|
@@ -74,7 +105,18 @@ export declare class Avatar extends LitElement {
|
|
|
74
105
|
autoColor: boolean;
|
|
75
106
|
/** Presence dot in the bottom-right corner. */
|
|
76
107
|
status: AvatarStatus | undefined;
|
|
108
|
+
/** Allows the avatar to open an image picker and exposes a remove action for the current image. */
|
|
109
|
+
editable: boolean;
|
|
110
|
+
/** Disables editable avatar actions. */
|
|
111
|
+
disabled: boolean;
|
|
112
|
+
/** Accepted image MIME types or file extensions, using native file-input syntax. */
|
|
113
|
+
accept: string;
|
|
114
|
+
/** Maximum accepted image size in bytes. Zero means unlimited. */
|
|
115
|
+
maxSize: number;
|
|
77
116
|
private imageFailed;
|
|
117
|
+
private previewUrl;
|
|
118
|
+
private selectedFile;
|
|
119
|
+
private input;
|
|
78
120
|
/** Accepts the pre-1.0 camelCase `initialCount="2"` attribute as well. */
|
|
79
121
|
static get observedAttributes(): string[];
|
|
80
122
|
attributeChangedCallback(name: string, old: string | null, value: string | null): void;
|
|
@@ -83,7 +125,20 @@ export declare class Avatar extends LitElement {
|
|
|
83
125
|
/** Hue (0–359) derived from the name, used by `auto-color`. */
|
|
84
126
|
get hue(): number;
|
|
85
127
|
willUpdate(changed: PropertyValues<this>): void;
|
|
128
|
+
disconnectedCallback(): void;
|
|
129
|
+
connectedCallback(): void;
|
|
130
|
+
/** Locally selected image file, if the avatar was changed through its picker. */
|
|
131
|
+
get file(): File | undefined;
|
|
132
|
+
/** Opens the avatar image picker. */
|
|
133
|
+
browse(): void;
|
|
134
|
+
/** Clears the selected preview and the current `src` image. */
|
|
135
|
+
removeImage(): void;
|
|
86
136
|
private handleImageError;
|
|
137
|
+
private handleFileChange;
|
|
138
|
+
private stopNativeInput;
|
|
139
|
+
private validateFile;
|
|
140
|
+
private revokePreview;
|
|
141
|
+
private renderVisual;
|
|
87
142
|
render(): import("lit-html").TemplateResult<1>;
|
|
88
143
|
}
|
|
89
144
|
declare global {
|
|
@@ -91,4 +146,5 @@ declare global {
|
|
|
91
146
|
'c2-avatar': Avatar;
|
|
92
147
|
}
|
|
93
148
|
}
|
|
149
|
+
export { AvatarGroup, type AvatarGroupCountMode, type AvatarGroupOverflowDetail } from './avatar-group';
|
|
94
150
|
//# sourceMappingURL=avatar.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"avatar.d.ts","sourceRoot":"","sources":["../../src/avatar.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAA4B,KAAK,cAAc,EAAE,MAAM,KAAK,CAAA;AAG/E,OAAO,KAAK,EAAE,qBAAqB,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAA;AAKhG,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"avatar.d.ts","sourceRoot":"","sources":["../../src/avatar.ts"],"names":[],"mappings":"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"}
|
package/vue.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// nothing: template.compilerOptions.isCustomElement = (tag) => tag.startsWith('c2-') in vite.config.ts.
|
|
9
9
|
|
|
10
10
|
import type { DefineComponent, HTMLAttributes } from 'vue'
|
|
11
|
-
import type { Avatar, AvatarEventMap } from '@c2n/avatar'
|
|
11
|
+
import type { Avatar, AvatarEventMap, AvatarGroup, AvatarGroupEventMap } from '@c2n/avatar'
|
|
12
12
|
|
|
13
13
|
/** The element's own public properties, plus every attribute Vue understands on a host element. */
|
|
14
14
|
type C2Props<T> = Partial<Omit<T, keyof HTMLElement>> & HTMLAttributes
|
|
@@ -19,7 +19,19 @@ declare module 'vue' {
|
|
|
19
19
|
C2Props<Avatar> & {
|
|
20
20
|
'initial-count'?: unknown
|
|
21
21
|
'auto-color'?: unknown
|
|
22
|
+
'max-size'?: unknown
|
|
23
|
+
onAvatarRemove?: (event: AvatarEventMap['avatar-remove']) => void
|
|
22
24
|
onError?: (event: AvatarEventMap['error']) => void
|
|
25
|
+
onFileReject?: (event: AvatarEventMap['file-reject']) => void
|
|
26
|
+
onAvatarChange?: (event: AvatarEventMap['avatar-change']) => void
|
|
27
|
+
}
|
|
28
|
+
>
|
|
29
|
+
'c2-avatar-group': DefineComponent<
|
|
30
|
+
C2Props<AvatarGroup> & {
|
|
31
|
+
'max-visible'?: unknown
|
|
32
|
+
'count-mode'?: unknown
|
|
33
|
+
'aria-label'?: unknown
|
|
34
|
+
onOverflowChange?: (event: AvatarGroupEventMap['overflow-change']) => void
|
|
23
35
|
}
|
|
24
36
|
>
|
|
25
37
|
}
|