vue3-avatar
Version:
A lightweight, fully customizable, accessible, and SSR-safe user avatar component for Vue 3 and Nuxt. Supports initials, custom images, pixel-art generation (identicons), groups with overflow, and auto-contrast text. Perfect for user profiles, team displa
1,067 lines (941 loc) • 37.5 kB
JavaScript
;var vue=require('vue');function ownKeys(object, enumerableOnly) {
var keys = Object.keys(object);
if (Object.getOwnPropertySymbols) {
var symbols = Object.getOwnPropertySymbols(object);
enumerableOnly && (symbols = symbols.filter(function (sym) {
return Object.getOwnPropertyDescriptor(object, sym).enumerable;
})), keys.push.apply(keys, symbols);
}
return keys;
}
function _objectSpread2(target) {
for (var i = 1; i < arguments.length; i++) {
var source = null != arguments[i] ? arguments[i] : {};
i % 2 ? ownKeys(Object(source), !0).forEach(function (key) {
_defineProperty(target, key, source[key]);
}) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) {
Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
});
}
return target;
}
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
function _slicedToArray(arr, i) {
return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest();
}
function _toConsumableArray(arr) {
return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread();
}
function _arrayWithoutHoles(arr) {
if (Array.isArray(arr)) return _arrayLikeToArray(arr);
}
function _arrayWithHoles(arr) {
if (Array.isArray(arr)) return arr;
}
function _iterableToArray(iter) {
if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter);
}
function _iterableToArrayLimit(arr, i) {
var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"];
if (_i == null) return;
var _arr = [];
var _n = true;
var _d = false;
var _s, _e;
try {
for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) {
_arr.push(_s.value);
if (i && _arr.length === i) break;
}
} catch (err) {
_d = true;
_e = err;
} finally {
try {
if (!_n && _i["return"] != null) _i["return"]();
} finally {
if (_d) throw _e;
}
}
return _arr;
}
function _unsupportedIterableToArray(o, minLen) {
if (!o) return;
if (typeof o === "string") return _arrayLikeToArray(o, minLen);
var n = Object.prototype.toString.call(o).slice(8, -1);
if (n === "Object" && o.constructor) n = o.constructor.name;
if (n === "Map" || n === "Set") return Array.from(o);
if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen);
}
function _arrayLikeToArray(arr, len) {
if (len == null || len > arr.length) len = arr.length;
for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i];
return arr2;
}
function _nonIterableSpread() {
throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
function _nonIterableRest() {
throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
function _createForOfIteratorHelper(o, allowArrayLike) {
var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"];
if (!it) {
if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") {
if (it) o = it;
var i = 0;
var F = function () {};
return {
s: F,
n: function () {
if (i >= o.length) return {
done: true
};
return {
done: false,
value: o[i++]
};
},
e: function (e) {
throw e;
},
f: F
};
}
throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
var normalCompletion = true,
didErr = false,
err;
return {
s: function () {
it = it.call(o);
},
n: function () {
var step = it.next();
normalCompletion = step.done;
return step;
},
e: function (e) {
didErr = true;
err = e;
},
f: function () {
try {
if (!normalCompletion && it.return != null) it.return();
} finally {
if (didErr) throw err;
}
}
};
}function getInitials(name) {
if (!name || typeof name !== 'string') return '';
var words = name.trim().split(/[- ]/); // Filter out empty strings from multiple spaces
words = words.filter(function (word) {
return word !== "";
});
if (words.length === 0) return "";
if (words.length >= 3) {
return words[0][0].toUpperCase() + words[1][0].toUpperCase() + words[words.length - 1][0].toUpperCase();
} else if (words.length === 2) {
return words[0][0].toUpperCase() + words[1][0].toUpperCase();
} else if (words.length === 1) {
return words[0][0].toUpperCase();
}
return "";
}var lightColors = ["#F0F8FF", "#FAEBD7", "#00FFFF", "#7FFFD4", "#F0FFFF", "#F5F5DC", "#FFE4C4", "#FFEBCD", "#DEB887", "#5F9EA0", "#7FFF00", "#D2691E", "#FF7F50", "#6495ED", "#FFF8DC", "#00FFFF", "#B8860B", "#A9A9A9", "#A9A9A9", "#BDB76B", "#FF8C00", "#E9967A", "#8FBC8F", "#00CED1", "#FF1493", "#00BFFF", "#1E90FF", "#FFFAF0", "#FF00FF", "#DCDCDC", "#F8F8FF", "#FFD700", "#DAA520", "#808080", "#808080", "#ADFF2F", "#F0FFF0", "#FF69B4", "#CD5C5C", "#FFFFF0", "#F0E68C", "#E6E6FA", "#FFF0F5", "#7CFC00", "#FFFACD", "#ADD8E6", "#F08080", "#E0FFFF", "#FAFAD2", "#D3D3D3", "#D3D3D3", "#90EE90", "#FFB6C1", "#FFA07A", "#20B2AA", "#87CEFA", "#B0C4DE", "#FFFFE0", "#00FF00", "#32CD32", "#FAF0E6", "#FF00FF", "#66CDAA", "#BA55D3", "#9370D8", "#3CB371", "#7B68EE", "#00FA9A", "#48D1CC", "#F5FFFA", "#FFE4E1", "#FFE4B5", "#FFDEAD", "#FDF5E6", "#FFA500", "#FF4500", "#DA70D6", "#EEE8AA", "#98FB98", "#AFEEEE", "#D87093", "#FFEFD5", "#FFDAB9", "#CD853F", "#FFC0CB", "#DDA0DD", "#B0E0E6", "#FF0000", "#BC8F8F", "#FA8072", "#F4A460", "#FFF5EE", "#C0C0C0", "#87CEEB", "#FFFAFA", "#00FF7F", "#D2B48C", "#D8BFD8", "#FF6347", "#40E0D0", "#EE82EE", "#F5DEB3", "#FFFFFF", "#F5F5F5", "#FFFF00", "#9ACD32"];
var darkColors = ["#000000", "#0000FF", "#8A2BE2", "#A52A2A", "#DC143C", "#00008B", "#008B8B", "#006400", "#8B008B", "#556B2F", "#9932CC", "#8B0000", "#483D8B", "#2F4F4F", "#2F4F4F", "#9400D3", "#696969", "#696969", "#B22222", "#228B22", "#008000", "#4B0082", "#800000", "#0000CD", "#C71585", "#191970", "#000080", "#808000", "#6B8E23", "#800080", "#4169E1", "#8B4513", "#2E8B57", "#A0522D", "#6A5ACD", "#708090", "#708090", "#4682B4", "#008080"];
var legacyBackgroundColors = ['#F44336', '#FF4081', '#9C27B0', '#673AB7', '#3F51B5', '#2196F3', '#03A9F4', '#00BCD4', '#009688', '#4CAF50', '#8BC34A', '#CDDC39', '#FFC107', '#FF9800', '#FF5722', '#795548', '#9E9E9E', '#607D8B'];
function getAsciiValue(name) {
if (!name) return 0;
var username = name.trim();
var ascii = 0;
for (var index = 0; index < username.length; index++) {
ascii += username.charCodeAt(index);
}
return ascii;
}
function lightenColor(hex, amt) {
if (!/^#[0-9A-Fa-f]{6}$/i.test(hex)) {
return '#FFFFFF';
}
var r = parseInt(hex.slice(1, 3), 16);
var g = parseInt(hex.slice(3, 5), 16);
var b = parseInt(hex.slice(5, 7), 16);
var newR = Math.min(255, r + amt);
var newG = Math.min(255, g + amt);
var newB = Math.min(255, b + amt);
var toHex = function toHex(n) {
return n.toString(16).padStart(2, '0').toUpperCase();
};
return "#".concat(toHex(newR)).concat(toHex(newG)).concat(toHex(newB));
}
function getAvatarColors(name) {
var useLegacyColors = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
var ascii = getAsciiValue(name);
if (useLegacyColors) {
var backgroundColor = legacyBackgroundColors[0];
if (name && typeof name === 'string') {
var index = (name.length || 0) % legacyBackgroundColors.length;
backgroundColor = legacyBackgroundColors[index];
}
var color = lightenColor(backgroundColor, 80);
return {
background: backgroundColor,
color: color
};
} // Modern logic
var darkColor = darkColors[ascii % darkColors.length];
var lightColor = lightColors[ascii % lightColors.length]; // Secondary color for gradients
var secondaryColor = darkColors[(ascii + 5) % darkColors.length];
var gradient = "linear-gradient(135deg, ".concat(darkColor, " 0%, ").concat(secondaryColor, " 100%)"); // Default: Dark Background, Light Text
return {
background: darkColor,
color: lightColor,
gradient: gradient,
// We also expose the pair if needed for inversion
light: lightColor,
dark: darkColor
};
}/**
* Generate a deterministic 8x8 pixel grid from a string
* @param {string} str - Input string to hash
* @returns {boolean[][]} - 8x8 grid of boolean values
*/
function generatePixelGrid(str) {
if (!str) return Array(8).fill(null).map(function () {
return Array(8).fill(false);
}); // Simple hash function
var hash = 0;
for (var i = 0; i < str.length; i++) {
var char = str.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash; // Convert to 32-bit integer
} // Generate symmetric 8x8 grid (mirror horizontally for better aesthetics)
var grid = [];
for (var y = 0; y < 8; y++) {
var row = [];
for (var x = 0; x < 4; x++) {
// Use different parts of the hash for each position
var bitIndex = (y * 4 + x) % 32;
var bit = Math.abs(hash) >> bitIndex & 1;
row.push(bit === 1);
} // Mirror the row for symmetry
grid.push([].concat(row, _toConsumableArray(row.reverse())));
}
return grid;
}
/**
* Color themes for pixel avatars
*/
var PIXEL_THEMES = {
earth: {
background: "#8B7355",
foreground: "#D4A574"
},
neon: {
background: "#FF006E",
foreground: "#00F5FF"
},
ocean: {
background: "#006994",
foreground: "#4FC3F7"
},
forest: {
background: "#2D5016",
foreground: "#7CB342"
},
sunset: {
background: "#FF6B35",
foreground: "#FFD23F"
},
midnight: {
background: "#1A1A2E",
foreground: "#16213E"
},
candy: {
background: "#FF69B4",
foreground: "#FFB6C1"
},
retro: {
background: "#8B4513",
foreground: "#DEB887"
}
};
/**
* Generate SVG for pixel avatar
* @param {boolean[][]} grid - 8x8 boolean grid
* @param {object} theme - Color theme with background and foreground
* @param {number} size - Size of the avatar
* @returns {string} - SVG string
*/
function generatePixelSVG(grid, theme) {
var size = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 40;
var pixelSize = size / 8;
var pixels = [];
for (var y = 0; y < 8; y++) {
for (var x = 0; x < 8; x++) {
if (grid[y][x]) {
pixels.push("<rect x=\"".concat(x * pixelSize, "\" y=\"").concat(y * pixelSize, "\" width=\"").concat(pixelSize, "\" height=\"").concat(pixelSize, "\" fill=\"").concat(theme.foreground, "\"/>"));
}
}
}
return "\n <svg width=\"".concat(size, "\" height=\"").concat(size, "\" viewBox=\"0 0 ").concat(size, " ").concat(size, "\" xmlns=\"http://www.w3.org/2000/svg\">\n <rect width=\"").concat(size, "\" height=\"").concat(size, "\" fill=\"").concat(theme.background, "\"/>\n ").concat(pixels.join("\n "), "\n </svg>\n ").trim();
}/**
* Calculate the relative luminance of a color
* @param {string} color - Hex color string (e.g., '#FF5733')
* @returns {number} - Luminance value between 0 and 1
*/
function getLuminance(color) {
// Remove # if present
var hex = color.replace("#", ""); // Parse RGB values
var r = parseInt(hex.substring(0, 2), 16) / 255;
var g = parseInt(hex.substring(2, 4), 16) / 255;
var b = parseInt(hex.substring(4, 6), 16) / 255; // Apply gamma correction
var rLinear = r <= 0.03928 ? r / 12.92 : Math.pow((r + 0.055) / 1.055, 2.4);
var gLinear = g <= 0.03928 ? g / 12.92 : Math.pow((g + 0.055) / 1.055, 2.4);
var bLinear = b <= 0.03928 ? b / 12.92 : Math.pow((b + 0.055) / 1.055, 2.4); // Calculate relative luminance
return 0.2126 * rLinear + 0.7152 * gLinear + 0.0722 * bLinear;
}
/**
* Calculate YIQ value for a color (simpler alternative to luminance)
* @param {string} color - Hex color string
* @returns {number} - YIQ value
*/
function getYIQ(color) {
var hex = color.replace("#", "");
var r = parseInt(hex.substring(0, 2), 16);
var g = parseInt(hex.substring(2, 4), 16);
var b = parseInt(hex.substring(4, 6), 16);
return (r * 299 + g * 587 + b * 114) / 1000;
}
/**
* Get contrasting text color (black or white) based on background
* @param {string} backgroundColor - Hex color string
* @param {string} method - 'luminance' or 'yiq' (default: 'yiq')
* @returns {string} - '#FFFFFF' or '#000000'
*/
function getContrastColor(backgroundColor) {
var method = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "yiq";
if (!backgroundColor || !backgroundColor.startsWith("#")) {
return "#FFFFFF"; // Default to white for invalid colors
}
if (method === "luminance") {
var luminance = getLuminance(backgroundColor); // Threshold of 0.5 works well for most cases
return luminance > 0.5 ? "#000000" : "#FFFFFF";
} else {
var yiq = getYIQ(backgroundColor); // YIQ threshold of 128 is standard
return yiq >= 128 ? "#000000" : "#FFFFFF";
}
}// Symbol for provide/inject
var AvatarConfigKey = Symbol("AvatarConfig");var _hoisted_1 = ["role", "tabindex", "aria-label", "title", "onKeydown"];
var _hoisted_2 = ["height", "width", "src", "loading"];
var _hoisted_3 = ["innerHTML"];
var script$1 = {
__name: 'Avatar',
props: {
name: {
type: String,
required: true
},
color: {
type: String
},
background: {
type: String
},
size: {
type: Number,
default: 40
},
dark: {
type: Boolean,
default: false
},
inline: {
type: Boolean,
default: false
},
rounded: {
type: Boolean,
default: true
},
shape: {
type: String,
validator: function validator(value) {
return ["circle", "square", "squircle", "hexagon"].includes(value);
}
},
imageSrc: {
type: String
},
alt: {
type: String,
default: undefined
},
loading: {
type: String,
default: "lazy",
validator: function validator(value) {
return ["lazy", "eager"].includes(value);
}
},
transition: {
type: Boolean,
default: true
},
border: {
type: Boolean,
default: true
},
borderColor: {
type: String,
default: "white"
},
customAvatarStyle: {
type: Object,
default: function _default() {
return {};
}
},
status: {
type: String,
default: null,
validator: function validator(value) {
return ["away", "online", "offline", "busy"].includes(value);
}
},
statusPosition: {
type: String,
default: "bottom-right",
validator: function validator(value) {
return ["top-right", "top-left", "bottom-right", "bottom-left"].includes(value);
}
},
customStatusStyle: {
type: Object,
default: function _default() {
return {};
}
},
sameBorder: {
type: Boolean,
default: false
},
interactive: {
type: Boolean,
default: false
},
/**
* @deprecated Use original vue-avatar color palette for backwards compatibility
*/
useLegacyColors: {
type: Boolean,
default: false
},
useTextColorForBorder: {
type: Boolean,
default: false
},
gradient: {
type: Boolean,
default: false
},
pointer: {
type: Boolean,
default: false
},
onClick: {
type: Function,
default: null
},
variant: {
type: String,
default: "initials",
validator: function validator(value) {
return ["initials", "pixel"].includes(value);
}
},
pixelTheme: {
type: String,
default: "earth",
validator: function validator(value) {
return Object.keys(PIXEL_THEMES).includes(value);
}
},
autoContrast: {
type: Boolean,
default: false
}
},
emits: ["error", "activate", "load"],
setup: function setup(__props, _ref) {
var __emit = _ref.emit;
var BORDERCOLORS = {
ONLINE: "green",
OFFLINE: "grey",
AWAY: "orange",
BUSY: "red"
};
var props = __props;
var emit = __emit;
var imageError = vue.ref(false);
var isLoaded = vue.ref(false);
var globalConfig = vue.inject(AvatarConfigKey, {});
var getConfig = function getConfig(key, localValue, defaultValue) {
if (localValue !== undefined && localValue !== defaultValue) return localValue;
return globalConfig[key] !== undefined ? globalConfig[key] : defaultValue;
};
var isClickable = vue.computed(function () {
return props.pointer || props.interactive || typeof props.onClick === "function";
});
function onActivate(event) {
if (typeof props.onClick === "function") {
props.onClick(event);
}
if (props.interactive) {
emit("activate", event);
}
}
var computedColors = vue.computed(function () {
return getAvatarColors(props.name, props.useLegacyColors);
});
var displayName = vue.computed(function () {
return getInitials(props.name);
});
var pixelGrid = vue.computed(function () {
return generatePixelGrid(props.name);
});
var pixelSVG = vue.computed(function () {
var baseTheme = PIXEL_THEMES[props.pixelTheme] || PIXEL_THEMES.earth; // Allow custom overrides via props
var customBg = getConfig("background", props.background);
var customColor = getConfig("color", props.color);
var theme = {
background: customBg || baseTheme.background,
foreground: customColor || baseTheme.foreground
}; // If no custom overrides, handle dark/light toggle
// Consistent with initials: dark=true is dark background, dark=false is light background
if (!customBg && !customColor && !props.dark) {
theme = {
background: baseTheme.foreground,
foreground: baseTheme.background
};
}
return generatePixelSVG(pixelGrid.value, theme, props.size);
});
var displayBackground = vue.computed(function () {
var bg = getConfig("background", props.background);
if (bg) return bg;
var colors = computedColors.value;
if (props.useLegacyColors) return colors.background;
if (props.gradient && colors.gradient) return colors.gradient;
return props.dark ? colors.dark : colors.light;
});
var displayColor = vue.computed(function () {
var color = getConfig("color", props.color);
if (color) return color; // If auto-contrast is enabled, calculate based on background
if (getConfig("autoContrast", props.autoContrast, false)) {
var bg = displayBackground.value; // Only apply auto-contrast if background is a valid hex color
if (bg && bg.startsWith("#")) {
return getContrastColor(bg);
}
}
var colors = computedColors.value;
if (props.useLegacyColors) return colors.color;
return props.dark ? colors.light : colors.dark;
});
var displayBorderColor = vue.computed(function () {
return props.useTextColorForBorder ? displayColor.value : getConfig("borderColor", props.borderColor, "white");
});
var fontSize = vue.computed(function () {
var size = getConfig("size", props.size, 40);
if (displayName.value.length == 1) return size / 2;else if (displayName.value.length == 2) return size / 2.5;
if (displayName.value.length == 3) return size / 3;else return 14;
});
var statusBackgroundColor = vue.computed(function () {
var color;
switch (props.status && props.status.toLowerCase()) {
case "away":
color = BORDERCOLORS.AWAY;
break;
case "online":
color = BORDERCOLORS.ONLINE;
break;
case "offline":
color = BORDERCOLORS.OFFLINE;
break;
default:
color = BORDERCOLORS.BUSY;
}
return color;
});
var statusStyle = vue.computed(function () {
// Calculate position based on statusPosition prop
var positionStyles = {};
var offset = 0; // Can be adjusted based on shape
var pos = getConfig("statusPosition", props.statusPosition, "bottom-right");
if (pos.includes("bottom")) {
positionStyles.bottom = "".concat(offset, "px");
positionStyles.top = "auto";
} else {
positionStyles.top = "".concat(offset, "px");
positionStyles.bottom = "auto";
}
if (pos.includes("right")) {
positionStyles.right = "".concat(offset, "px");
positionStyles.left = "auto";
} else {
positionStyles.left = "".concat(offset, "px");
positionStyles.right = "auto";
}
var size = getConfig("size", props.size, 40);
var defaultStatusStyle = _objectSpread2({
height: "".concat(size / 4, "px"),
width: "".concat(size / 4, "px"),
backgroundColor: statusBackgroundColor.value,
border: "".concat(size / 30, "px solid ").concat(props.sameBorder ? displayBorderColor.value : "white")
}, positionStyles);
return Object.assign({}, defaultStatusStyle, props.customStatusStyle);
});
var imageStyle = vue.computed(function () {
var size = getConfig("size", props.size, 40);
var defaultImageStyle = {
display: props.inline ? "inline-flex" : "flex",
borderRadius: shapeStyle.value.borderRadius,
clipPath: shapeStyle.value.clipPath,
margin: 0,
padding: 0,
alignItems: "center",
justifyContent: "center",
border: props.border ? "".concat(size / 20, "px solid ").concat(displayBorderColor.value) : "none"
};
return Object.assign({}, defaultImageStyle, props.customAvatarStyle);
});
var avatarStyle = vue.computed(function () {
var size = getConfig("size", props.size, 40);
var defaultAvatarStyle = {
color: displayColor.value,
width: size + "px",
height: size + "px",
fontSize: fontSize.value + "px",
background: displayBackground.value,
display: props.inline && "inline-flex",
borderRadius: shapeStyle.value.borderRadius,
clipPath: shapeStyle.value.clipPath,
border: props.border && "".concat(size / 20, "px solid ").concat(displayBorderColor.value)
};
return Object.assign({}, defaultAvatarStyle, props.customAvatarStyle);
});
var shapeStyle = vue.computed(function () {
var shape = props.shape || (props.rounded ? "circle" : "square");
if (shape === "square") return {
borderRadius: "0"
};
if (shape === "circle") return {
borderRadius: "50%"
};
if (shape === "squircle") return {
borderRadius: "25%"
};
if (shape === "hexagon") return {
borderRadius: "0",
clipPath: "polygon(25% 5%, 75% 5%, 95% 50%, 75% 95%, 25% 95%, 5% 50%)"
};
return {
borderRadius: "0"
};
});
var rootStyle = vue.computed(function () {
var size = getConfig("size", props.size, 40);
return {
"--va-size": "".concat(size, "px"),
"--va-bg": displayBackground.value,
"--va-color": displayColor.value,
"--va-border-color": displayBorderColor.value,
"--va-radius": shapeStyle.value.borderRadius,
"--va-clip-path": shapeStyle.value.clipPath || "none",
"--va-font-size": "".concat(fontSize.value, "px")
};
});
var accessibleLabel = vue.computed(function () {
var label = props.alt || (props.name ? "Avatar of ".concat(props.name) : "User avatar");
if (props.status) {
return "".concat(label, ". User is ").concat(props.status);
}
return label;
});
function onImageError(event) {
imageError.value = true;
isLoaded.value = false;
emit("error", event);
}
function onImageLoad(event) {
isLoaded.value = true;
emit("load", event);
}
function showImage() {
return props.imageSrc && !imageError.value;
}
return function (_ctx, _cache) {
return vue.openBlock(), vue.createElementBlock("div", {
class: vue.normalizeClass(["container", {
'is-clickable': isClickable.value
}]),
style: vue.normalizeStyle(rootStyle.value),
role: isClickable.value ? 'button' : 'img',
tabindex: isClickable.value ? 0 : undefined,
"aria-label": accessibleLabel.value,
title: __props.name,
onClick: onActivate,
onKeydown: [vue.withKeys(vue.withModifiers(onActivate, ["prevent"]), ["enter"]), vue.withKeys(vue.withModifiers(onActivate, ["prevent"]), ["space"])]
}, [showImage() && _ctx.$slots.image ? vue.renderSlot(_ctx.$slots, "image", {
src: __props.imageSrc,
alt: accessibleLabel.value,
size: __props.size,
style: vue.normalizeStyle(imageStyle.value),
class: vue.normalizeClass({
'image-loaded': isLoaded.value,
'image-transition': __props.transition
}),
onError: onImageError,
onLoad: onImageLoad
}, undefined, undefined, 0) : showImage() ? (vue.openBlock(), vue.createElementBlock("img", {
key: 1,
style: vue.normalizeStyle(imageStyle.value),
height: __props.size,
width: __props.size,
src: __props.imageSrc,
loading: __props.loading,
class: vue.normalizeClass({
'image-loaded': isLoaded.value,
'image-transition': __props.transition
}),
alt: "",
onError: onImageError,
onLoad: onImageLoad
}, null, 46, _hoisted_2)) : !__props.name && _ctx.$slots.placeholder ? vue.renderSlot(_ctx.$slots, "placeholder", {
size: __props.size,
style: vue.normalizeStyle(avatarStyle.value)
}, undefined, undefined, 2) : __props.variant === 'pixel' ? (vue.openBlock(), vue.createElementBlock("div", {
key: 3,
style: vue.normalizeStyle(avatarStyle.value),
class: "avatar avatar-pixel noselect",
"aria-hidden": "true",
innerHTML: pixelSVG.value
}, null, 12, _hoisted_3)) : (vue.openBlock(), vue.createElementBlock("div", {
key: 4,
style: vue.normalizeStyle(avatarStyle.value),
class: "avatar noselect",
"aria-hidden": "true"
}, vue.toDisplayString(displayName.value), 5)), __props.status || _ctx.$slots.status ? (vue.openBlock(), vue.createElementBlock("div", {
key: 5,
class: "status-indicator",
style: vue.normalizeStyle(statusStyle.value),
"aria-hidden": "true"
}, [vue.renderSlot(_ctx.$slots, "status")], 4)) : vue.createCommentVNode("", true), vue.renderSlot(_ctx.$slots, "overlay")], 46, _hoisted_1);
};
}
};function styleInject(css, ref) {
if ( ref === void 0 ) ref = {};
var insertAt = ref.insertAt;
if (!css || typeof document === 'undefined') { return; }
var head = document.head || document.getElementsByTagName('head')[0];
var style = document.createElement('style');
style.type = 'text/css';
if (insertAt === 'top') {
if (head.firstChild) {
head.insertBefore(style, head.firstChild);
} else {
head.appendChild(style);
}
} else {
head.appendChild(style);
}
if (style.styleSheet) {
style.styleSheet.cssText = css;
} else {
style.appendChild(document.createTextNode(css));
}
}var css_248z$1 = "\n@import url(\"https://fonts.googleapis.com/css2?family=Domine:wght@700&display=swap\");\n.avatar[data-v-a6123cce] {\n font-family: \"Domine\", serif;\n color: white;\n background: navy;\n font-size: 14px;\n width: 45px;\n height: 45px;\n border-radius: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n font-weight: 700;\n}\n.avatar-pixel[data-v-a6123cce] {\n padding: 0;\n overflow: hidden;\n}\n.avatar-pixel svg[data-v-a6123cce] {\n display: block;\n}\n.noselect[data-v-a6123cce] {\n -webkit-touch-callout: none; /* iOS Safari */\n -webkit-user-select: none; /* Safari */\n -khtml-user-select: none; /* Konqueror HTML */\n -moz-user-select: none; /* Old versions of Firefox */\n -ms-user-select: none; /* Internet Explorer/Edge */\n user-select: none; /* Non-prefixed version, currently supported by Chrome, Edge, Opera and Firefox */\n}\n.container[data-v-a6123cce] {\n position: relative;\n}\n.container.is-clickable[data-v-a6123cce],\n.container.is-clickable[data-v-a6123cce] * {\n cursor: pointer !important;\n}\n.status-indicator[data-v-a6123cce] {\n position: absolute;\n border-radius: 50%;\n}\n.container img.image-transition[data-v-a6123cce] {\n opacity: 0;\n transition: opacity 0.3s ease-in-out;\n}\n.container img.image-transition.image-loaded[data-v-a6123cce] {\n opacity: 1;\n}\n";
styleInject(css_248z$1);script$1.__scopeId = "data-v-a6123cce";var script = vue.defineComponent({
name: "AvatarGroup",
props: {
max: {
type: Number
},
overlap: {
type: Number,
default: 10
},
borderColor: {
type: String,
default: "white"
},
size: {
type: Number,
default: 40
},
layout: {
type: String,
default: "stack",
// stack | triangle
validator: function validator(value) {
return ["stack", "triangle"].includes(value);
}
},
onClick: {
type: Function,
default: null
},
pointer: {
type: Boolean,
default: false
}
},
emits: ["overflow-click"],
setup: function setup(props, _ref) {
var slots = _ref.slots,
emit = _ref.emit;
var flatten = function flatten(nodes) {
var result = [];
var _iterator = _createForOfIteratorHelper(nodes),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var node = _step.value;
if (node.type === vue.Fragment && Array.isArray(node.children)) {
result = result.concat(flatten(node.children));
} else if (node.type !== vue.Comment) {
// Skip empty text nodes
if (node.type === vue.Text) {
if (node.children.trim().length === 0) continue;
}
result.push(node);
}
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
return result;
};
var globalConfig = vue.inject(AvatarConfigKey, {});
var getConfig = function getConfig(key, localValue, defaultValue) {
if (localValue !== undefined && localValue !== defaultValue) return localValue;
return globalConfig[key] !== undefined ? globalConfig[key] : defaultValue;
};
return function () {
var defaultSlot = slots.default ? slots.default() : [];
var children = flatten(defaultSlot);
var visible = children;
var overflowCount = 0;
var effectiveMax = props.max;
if (props.layout === "triangle") {
var limit = props.max ? Math.min(props.max, 3) : 3;
if (children.length > limit) {
effectiveMax = limit - 1;
} else {
effectiveMax = limit;
}
}
if (effectiveMax && children.length > effectiveMax) {
visible = children.slice(0, effectiveMax);
overflowCount = children.length - effectiveMax;
}
var allNames = children.map(function (child) {
return child.props && child.props.name;
}).filter(Boolean).join(", ");
var hiddenChildren = overflowCount > 0 ? children.slice(effectiveMax) : [];
var hiddenNames = hiddenChildren.map(function (child) {
return child.props && child.props.name;
}).filter(Boolean).join(", "); // Extract all user data
var allUsers = children.map(function (child) {
return child.props;
}).filter(Boolean); // Extract hidden user data for the event payload
var hiddenUsers = hiddenChildren.map(function (child) {
return child.props;
}).filter(Boolean);
var handleOverflowClick = function handleOverflowClick(e) {
e.stopPropagation();
emit("overflow-click", hiddenUsers, allUsers);
};
var handleGroupKeydown = function handleGroupKeydown(event) {
if (!props.onClick || !["Enter", " ", "Spacebar"].includes(event.key)) {
return;
}
event.preventDefault();
props.onClick(event);
};
var size = getConfig("size", props.size, 40);
var borderColor = getConfig("borderColor", props.borderColor, "white");
var overlap = getConfig("overlap", props.overlap, 10);
var overflowBadge = overflowCount > 0 ? vue.h("button", {
type: "button",
class: "avatar-overflow",
title: hiddenNames,
"aria-label": "Show ".concat(overflowCount, " more avatar").concat(overflowCount === 1 ? "" : "s").concat(hiddenNames ? ": ".concat(hiddenNames) : ""),
onClick: handleOverflowClick,
style: {
width: "".concat(size, "px"),
height: "".concat(size, "px"),
fontSize: "".concat(size / 2.5, "px"),
cursor: "pointer"
}
}, "+".concat(overflowCount)) : null;
var visibleWithProps = visible.map(function (child) {
return vue.cloneVNode(child, {
size: size,
borderColor: borderColor
});
});
return vue.h("div", {
class: ["avatar-group", "layout-".concat(props.layout), {
"is-clickable": props.pointer || !!props.onClick
}],
title: allNames,
role: props.onClick ? "button" : undefined,
tabindex: props.onClick ? 0 : undefined,
"aria-label": props.onClick ? "Avatar group".concat(allNames ? ": ".concat(allNames) : "") : undefined,
onClick: function onClick(e) {
return props.onClick && props.onClick(e);
},
onKeydown: handleGroupKeydown,
style: {
"--va-group-overlap": "-".concat(overlap, "px"),
"--va-size": "".concat(size, "px")
}
}, [].concat(_toConsumableArray(visibleWithProps), [overflowBadge]));
};
}
});var css_248z = "\n.avatar-group {\n display: flex;\n align-items: center;\n}\n.avatar-group.is-clickable {\n cursor: pointer;\n}\n.avatar-group.is-clickable * {\n cursor: pointer !important;\n}\n.avatar-group.layout-stack > * {\n margin-left: 0;\n}\n.avatar-group.layout-stack > * + * {\n margin-left: var(--va-group-overlap);\n}\n.avatar-group.layout-triangle {\n position: relative;\n display: inline-block;\n width: calc(\n var(--va-size) * 1.8\n ); /* Wider for better visibility of back avatars */\n height: calc(var(--va-size) * 1.45);\n}\n.avatar-group.layout-triangle > * {\n position: absolute !important;\n}\n.avatar-group.layout-triangle > *:nth-child(1) {\n top: 0;\n left: 50%;\n transform: translateX(-50%);\n z-index: 10 !important;\n}\n.avatar-group.layout-triangle > *:nth-child(2) {\n bottom: 0;\n left: 0;\n z-index: 5 !important;\n}\n.avatar-group.layout-triangle > *:nth-child(3) {\n bottom: 0;\n right: 0;\n z-index: 1 !important;\n}\n.avatar-overflow {\n display: flex;\n align-items: center;\n justify-content: center;\n appearance: none;\n background: #ccc;\n color: white;\n border-radius: 50%;\n font-weight: bold;\n font-family: sans-serif;\n border: 2px solid white; /* Hardcoded default? */\n box-sizing: border-box;\n position: relative; /* To stack properly */\n padding: 0;\n}\n.avatar-overflow:focus-visible {\n outline: 2px solid #2563eb;\n outline-offset: 2px;\n}\n";
styleInject(css_248z);// IIFE injects install function into component, allowing component
// to be registered via Vue.use() as well as Vue.component(),
var install = function install(app) {
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
// Provide global configuration
var globalDefaults = options.defaults || {};
app.provide(AvatarConfigKey, globalDefaults);
app.component("Avatar", script$1);
app.component("AvatarGroup", script);
};
script$1.install = install;var namedExports=/*#__PURE__*/Object.freeze({__proto__:null,Avatar:script$1,AvatarGroup:script,AvatarConfigKey:AvatarConfigKey,'default':script$1});// only expose one global var, with named exports exposed as properties of
// that global var (eg. plugin.namedExport)
Object.entries(namedExports).forEach(function (_ref) {
var _ref2 = _slicedToArray(_ref, 2),
exportName = _ref2[0],
exported = _ref2[1];
if (exportName !== 'default') script$1[exportName] = exported;
});module.exports=script$1;