@helpwave/hightide
Version:
helpwave's component and theming library
1,559 lines (1,526 loc) • 675 kB
JavaScript
// src/components/branding/AppZumDocBadge.tsx
import clsx2 from "clsx";
// src/components/branding/AppZumDocLogo.tsx
import { useId } from "react";
import { clsx } from "clsx";
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
var backFillPath = "M33.1817 37.511L29.7804 37.511C21.7695 37.511 15.2754 44.0051 15.2754 52.016C15.2754 60.0268 21.7695 66.5209 29.7804 66.5209H35.4017C35.4208 66.5209 35.4274 66.5239 35.4301 66.5251C35.436 66.5275 35.4456 66.5331 35.4559 66.5434C35.4662 66.5536 35.4717 66.5633 35.4742 66.5691C35.4754 66.5719 35.4783 66.5785 35.4783 66.5975L35.4783 72.852C35.4783 80.8628 41.9724 87.3569 49.9833 87.3569C57.9942 87.3569 64.4883 80.8628 64.4883 72.852L64.4883 54.7684C64.4883 45.2374 56.7619 37.511 47.2309 37.511L43.6515 37.511V42.1072L47.2309 42.1072C54.2235 42.1072 59.8921 47.7758 59.8921 54.7684V72.852C59.8921 78.3244 55.4558 82.7608 49.9833 82.7608C44.5108 82.7608 40.0745 78.3244 40.0745 72.852L40.0745 66.5975C40.0745 64.0168 37.9824 61.9248 35.4017 61.9248L29.7804 61.9248C24.3079 61.9248 19.8716 57.4884 19.8716 52.016C19.8716 46.5435 24.3079 42.1072 29.7804 42.1072L33.1817 42.1072V37.511Z";
var frontFillPath = "M65.0734 29.3412C65.0734 21.3303 58.5793 14.8362 50.5685 14.8362C42.5576 14.8362 36.0635 21.3303 36.0635 29.3412V47.4247C36.0635 56.9557 43.7899 64.6821 53.3209 64.6821H57.4673V60.086H53.3209C46.3283 60.086 40.6597 54.4173 40.6597 47.4247V29.3412C40.6597 23.8687 45.096 19.4324 50.5685 19.4324C56.0409 19.4324 60.4773 23.8687 60.4773 29.3412V35.5956C60.4773 38.1763 62.5693 40.2684 65.15 40.2684H70.7714C76.2439 40.2684 80.6802 44.7047 80.6802 50.1772C80.6802 55.6496 76.2439 60.086 70.7714 60.086H66.7949V64.6821H70.7714C78.7823 64.6821 85.2764 58.188 85.2764 50.1772C85.2764 42.1663 78.7823 35.6722 70.7714 35.6722H65.15C65.131 35.6722 65.1244 35.6692 65.1216 35.668C65.1158 35.6656 65.1061 35.66 65.0959 35.6497C65.0856 35.6395 65.08 35.6298 65.0776 35.624L65.0775 35.6238C65.0763 35.6209 65.0734 35.6142 65.0734 35.5956V29.3412Z";
var backStrokePath = "M43.65 39.81 L47.23 39.81 A14.96 14.96 0 0 1 62.19 54.77 L62.19 72.85 A12.21 12.21 0 0 1 37.78 72.85 L37.78 66.6 Q37.78 64.22 35.4 64.22 L29.78 64.22 A12.21 12.21 0 0 1 29.78 39.81 L33.18 39.81";
var frontStrokePath = "M57.47 62.38 L53.32 62.38 A14.96 14.96 0 0 1 38.36 47.42 L38.36 29.34 A12.21 12.21 0 0 1 62.78 29.34 L62.78 35.6 Q62.78 37.97 65.15 37.97 L70.77 37.97 A12.21 12.21 0 0 1 70.77 62.38 L66.79 62.38";
var AppZumDocLogo = ({
frontColor = "#095763",
backColor = "#4E97A2",
animate = "none",
size,
animationDuration = 1.7,
...props
}) => {
const id = useId();
const isLoadingAnimation = animate === "loading";
let svgAnimationKey = "";
if (animate === "pulse") {
svgAnimationKey = "animate-pulse";
} else if (animate === "bounce") {
svgAnimationKey = "animate-bounce";
}
const style = animationDuration !== void 0 && isLoadingAnimation ? { ...props.style, "--helpwave-loading-duration": `${animationDuration}s` } : props.style;
return /* @__PURE__ */ jsx(
"svg",
{
...props,
style,
viewBox: "14 14 72 74",
fill: "none",
className: clsx({
"max-w-16 max-h-16 min-w-16 min-h-16": size === "lg",
"max-w-12 max-h-12 min-w-12 min-h-12": size === "md",
"max-w-8 max-h-8 min-w-8 min-h-8": size === "sm"
}, props.className),
children: /* @__PURE__ */ jsx("g", { className: clsx(svgAnimationKey), children: isLoadingAnimation ? /* @__PURE__ */ jsxs(Fragment, { children: [
/* @__PURE__ */ jsx("clipPath", { id: `${id}-back`, children: /* @__PURE__ */ jsx("path", { d: backFillPath }) }),
/* @__PURE__ */ jsx("clipPath", { id: `${id}-front`, children: /* @__PURE__ */ jsx("path", { d: frontFillPath }) }),
/* @__PURE__ */ jsx("g", { clipPath: `url(#${id}-back)`, children: /* @__PURE__ */ jsx(
"path",
{
className: "animate-azd-fill-back",
d: backStrokePath,
stroke: backColor,
strokeWidth: 7,
strokeLinecap: "round",
pathLength: 1e3,
strokeDasharray: "1000 2000"
}
) }),
/* @__PURE__ */ jsx("g", { clipPath: `url(#${id}-front)`, children: /* @__PURE__ */ jsx(
"path",
{
className: "animate-azd-fill-front",
d: frontStrokePath,
stroke: frontColor,
strokeWidth: 7,
strokeLinecap: "round",
pathLength: 1e3,
strokeDasharray: "1000 2000"
}
) })
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
/* @__PURE__ */ jsx("path", { fillRule: "evenodd", clipRule: "evenodd", d: backFillPath, fill: backColor }),
/* @__PURE__ */ jsx("path", { fillRule: "evenodd", clipRule: "evenodd", d: frontFillPath, fill: frontColor })
] }) })
}
);
};
// src/components/branding/AppZumDocBadge.tsx
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
var AppZumDocBadge = ({
size = "sm",
...props
}) => {
return /* @__PURE__ */ jsxs2(
"span",
{
...props,
className: clsx2(
"flex flex-row items-center font-bold font-space rounded-md text-[#057986]",
{
"gap-x-1": size === "sm",
"gap-x-2 text-lg": size === "md",
"gap-x-2 text-xl": size === "lg"
},
props.className
),
children: [
/* @__PURE__ */ jsx2(AppZumDocLogo, { size }),
"App zum Doc"
]
}
);
};
// src/components/branding/HelpwaveLogo.tsx
import { clsx as clsx3 } from "clsx";
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
var HelpwaveLogo = ({
color = "currentColor",
size,
animate = "none",
animationDuration = 1.7,
...props
}) => {
const isLoadingAnimation = animate === "loading";
let svgAnimationKey = "";
if (animate === "pulse") {
svgAnimationKey = "animate-pulse";
} else if (animate === "bounce") {
svgAnimationKey = "animate-bounce";
}
const style = animationDuration !== void 0 && isLoadingAnimation ? { ...props.style, "--helpwave-loading-duration": `${animationDuration}s` } : props.style;
return /* @__PURE__ */ jsx3(
"svg",
{
...props,
style,
viewBox: "0 0 1024 1024",
fill: "none",
strokeLinecap: "round",
strokeWidth: 65,
className: clsx3({
"max-w-16 max-h-16 min-w-16 min-h-16": size === "lg",
"max-w-12 max-h-12 min-w-12 min-h-12": size === "md",
"max-w-8 max-h-8 min-w-8 min-h-8": size === "sm"
}, props.className),
children: /* @__PURE__ */ jsxs3("g", { className: clsx3(svgAnimationKey), children: [
/* @__PURE__ */ jsx3(
"path",
{
className: clsx3({ "animate-wave-big-left-up": isLoadingAnimation }),
d: "M146 644.214C146 498.088 253.381 379.629 385.843 379.629",
stroke: color,
strokeDasharray: "1000 1000"
}
),
/* @__PURE__ */ jsx3(
"path",
{
className: clsx3({ "animate-wave-big-right-down": isLoadingAnimation }),
d: "M625.686 645.272C493.224 645.272 385.843 526.813 385.843 380.687",
stroke: color,
strokeDasharray: "1000 1000"
}
),
/* @__PURE__ */ jsx3(
"path",
{
className: clsx3({ "animate-wave-small-left-up": isLoadingAnimation }),
d: "M533.585 613.522C533.585 508.895 610.47 424.079 705.312 424.079",
stroke: color,
strokeDasharray: "1000 1000"
}
),
/* @__PURE__ */ jsx3(
"path",
{
className: clsx3({ "animate-wave-small-right-down": isLoadingAnimation }),
d: "M878 615.639C782.628 615.639 705.313 530.822 705.313 426.196",
stroke: color,
strokeDasharray: "1000 1000"
}
)
] })
}
);
};
// src/components/branding/HelpwaveBadge.tsx
import clsx4 from "clsx";
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
var HelpwaveBadge = ({
size = "sm",
...props
}) => {
return /* @__PURE__ */ jsxs4(
"span",
{
...props,
className: clsx4(
"flex flex-row items-center font-bold font-space rounded-md",
{
"gap-x-1": size === "sm",
"gap-x-2 text-lg": size === "md",
"gap-x-2 text-xl": size === "lg"
},
props.className
),
children: [
/* @__PURE__ */ jsx4(HelpwaveLogo, { size }),
"helpwave"
]
}
);
};
// src/components/chat/ChatAttachmentCard.tsx
import clsx6 from "clsx";
import { Download, FileText } from "lucide-react";
// src/components/user-interaction/IconButton.tsx
import { forwardRef as forwardRef3 } from "react";
// src/components/user-interaction/Tooltip.tsx
import { forwardRef as forwardRef2, useContext as useContext2, useEffect as useEffect5 } from "react";
import { useId as useId3 } from "react";
import { useCallback as useCallback3, useMemo as useMemo3, useRef as useRef4, useState as useState5 } from "react";
// src/hooks/useOverlayRegistry.ts
import { useEffect, useId as useId2, useMemo, useState } from "react";
var OverlayRegistry = class _OverlayRegistry {
constructor() {
// The frontmost overlay is at the end of the array
this.overlayIds = [];
this.overlayItems = {};
this.listeners = /* @__PURE__ */ new Set();
}
static {
this.instance = null;
}
static getInstance() {
if (!_OverlayRegistry.instance) {
_OverlayRegistry.instance = new _OverlayRegistry();
}
return _OverlayRegistry.instance;
}
register(item) {
this.overlayIds = this.overlayIds.filter((id) => id !== item.id);
this.overlayIds.push(item.id);
this.overlayItems[item.id] = item;
this.notify();
return () => {
this.overlayIds = this.overlayIds.filter((id) => id !== item.id);
delete this.overlayItems[item.id];
this.notify();
};
}
update(item) {
this.overlayItems[item.id] = item;
this.notify();
}
addListener(callback) {
this.listeners.add(callback);
return () => {
this.listeners.delete(callback);
};
}
notify() {
const itemInformation = {};
const ids = [...this.overlayIds];
const startZIndex = 100;
const tagCount = {};
for (let index = ids.length - 1; index >= 0; index--) {
const id = ids[index];
const item = this.overlayItems[id];
itemInformation[id] = {
...item,
position: index,
tagPositions: {},
zIndex: startZIndex + index
};
for (const tag of item.tags ?? []) {
const count = tagCount[tag] ?? 0;
const nextPosition = count;
tagCount[tag] = count + 1;
itemInformation[id].tagPositions[tag] = nextPosition;
}
}
for (const callback of this.listeners) {
callback({ activeId: ids[ids.length - 1] ?? null, itemInformation, tagItemCounts: tagCount });
}
}
};
var useOverlayRegistry = (props = {}) => {
const generatedId = useId2();
const item = useMemo(() => ({
id: props.id ?? generatedId,
tags: props.tags
}), [props.id, generatedId, props.tags]);
const [value, setValue] = useState({
activeId: null,
itemInformation: {},
tagItemCounts: {}
});
const registry = useMemo(() => OverlayRegistry.getInstance(), []);
useEffect(() => {
if (!props.isActive) {
return;
}
function callback(value2) {
setValue(value2);
}
const removeListener = registry.addListener(callback);
const unregister = registry.register(item);
return () => {
removeListener();
unregister();
setValue({
activeId: null,
itemInformation: {},
tagItemCounts: {}
});
};
}, [props.isActive, item, registry]);
const itemInformation = value.itemInformation[item.id];
return {
isInFront: value.activeId === item.id,
zIndex: itemInformation?.zIndex,
position: itemInformation?.position,
tagPositions: itemInformation?.tagPositions,
tagItemCounts: value.tagItemCounts
};
};
// src/hooks/useTransitionState.ts
import { useCallback, useEffect as useEffect2, useLayoutEffect, useReducer, useRef, useState as useState2 } from "react";
var reducer = (state, action) => {
switch (action) {
case "open":
return "opening";
case "close":
return "closing";
case "finished": {
if (state === "opening") {
return "opened";
}
if (state === "closing") {
return "closed";
}
return state;
}
default:
return state;
}
};
var useTransitionState = ({
isOpen,
initialState = "closed",
ref,
timeout: initialTimeout = 1e3
}) => {
const [state, dispatch] = useReducer(reducer, initialState);
const timer = useRef(void 0);
const hasAnimation = useRef(false);
const [timeout] = useState2(initialTimeout);
useEffect2(() => {
if (isOpen && (state !== "opened" && state !== "opening")) {
dispatch("open");
} else if (!isOpen && (state !== "closed" && state !== "closing")) {
dispatch("close");
}
}, [isOpen, state]);
useEffect2(() => {
if (state === "opening" || state === "closing") {
if (timer.current) {
clearTimeout(timer.current);
}
if (timeout > 0) {
timer.current = setTimeout(() => {
dispatch("finished");
}, timeout);
}
} else {
if (timer.current) {
clearTimeout(timer.current);
}
}
}, [state, timeout]);
useLayoutEffect(() => {
if (state === "opening" || state === "closing") {
let element = ref.current;
if (!element) {
console.warn("useTransitionState: ref is not set to an element using window instead");
element = window.document.body;
}
const animations = element.getAnimations({ subtree: true }).filter((animation) => animation.effect?.getTiming().duration !== "0s");
if (animations.length > 0) {
hasAnimation.current = true;
Promise.all(animations.map((animation) => animation.finished)).then(() => {
dispatch("finished");
}).catch(() => {
dispatch("finished");
});
} else {
dispatch("finished");
hasAnimation.current = false;
clearTimeout(timer.current);
}
}
}, [ref, state]);
const onStart = useCallback(() => {
hasAnimation.current = true;
}, []);
const onEnd = useCallback(() => {
dispatch("finished");
hasAnimation.current = false;
}, []);
useEffect2(() => {
if (ref?.current && (state === "opening" || state === "closing")) {
const element = ref.current;
element.addEventListener("animationstart", onStart);
element.addEventListener("animationend", onEnd);
element.addEventListener("transitionstart", onStart);
element.addEventListener("transitionend", onEnd);
element.addEventListener("transitioncancel", onEnd);
return () => {
element.removeEventListener("animationstart", onStart);
element.removeEventListener("animationend", onEnd);
element.removeEventListener("transitionstart", onStart);
element.removeEventListener("transitionend", onEnd);
element.removeEventListener("transitioncancel", onEnd);
};
}
}, [state, ref, onStart, onEnd]);
return {
transitionState: state,
isVisible: state !== "closed"
};
};
// src/global-contexts/HightideConfigContext.tsx
import { createContext, useContext, useState as useState3 } from "react";
import { jsx as jsx5 } from "react/jsx-runtime";
var defaultConfig = {
tooltip: {
appearDelay: 0,
isAnimated: false,
screenPadding: 1
},
theme: {
initialTheme: "light"
},
locale: {
defaultLocale: "de-DE"
}
};
function mergeConfig(config, overwrite) {
return {
locale: {
...config.locale,
...overwrite?.locale
},
theme: {
...config.theme,
...overwrite.theme
},
tooltip: {
...config.tooltip,
...overwrite.tooltip
}
};
}
var HightideConfigContext = createContext(null);
var HightideConfigProvider = ({
children,
...initialOverwrite
}) => {
const [config, setConfig] = useState3(mergeConfig(defaultConfig, initialOverwrite));
return /* @__PURE__ */ jsx5(
HightideConfigContext.Provider,
{
value: {
config,
setConfig: (value) => setConfig((prevState) => mergeConfig(prevState, value))
},
children
}
);
};
var useHightideConfig = () => {
const context = useContext(HightideConfigContext);
if (!context) {
return {
config: defaultConfig,
setConfig: () => {
console.error("useHightideConfig.setConfig is not available without a HightideConfigProvider. Try wrapping your app a HightideConfigProvider.");
}
};
}
return context;
};
// src/components/utils/Portal.tsx
import { createPortal } from "react-dom";
var Portal = ({ children, container }) => {
return createPortal(children, container ?? document.body);
};
// src/components/layout/AnchoredFloatingContainer.tsx
import { forwardRef, useRef as useRef3 } from "react";
// src/hooks/useAnchoredPosition.ts
import { useCallback as useCallback2, useEffect as useEffect4, useLayoutEffect as useLayoutEffect4, useMemo as useMemo2, useRef as useRef2, useState as useState4 } from "react";
// src/utils/math.ts
function clamp(value, min, max) {
if (min !== void 0) value = Math.max(value, min);
if (max !== void 0) value = Math.min(value, max);
return value;
}
function clamp01(value) {
return clamp(value, 0, 1);
}
function closestStep(value, stepSize) {
return Math.round(value / stepSize) * stepSize;
}
function toStepRange(value, stepSize, min, max) {
const minStep = min !== void 0 ? Math.ceil(min / stepSize) * stepSize : void 0;
const maxStep = max !== void 0 ? Math.floor(max / stepSize) * stepSize : void 0;
value = closestStep(value, stepSize);
if (minStep !== void 0) value = Math.max(value, minStep);
if (maxStep !== void 0) value = Math.min(value, maxStep);
return value;
}
function resolveLoopingRangeValue(value, minimum, maximum) {
const range2 = maximum - minimum;
if (value > maximum) {
return {
value: (value - maximum) % range2 + minimum,
loopedOver: "maximum"
};
}
if (value < minimum) {
return {
value: (value - minimum) % range2 + minimum,
loopedOver: "minimum"
};
}
return {
value,
loopedOver: null
};
}
var MathUtil = {
clamp,
clamp01,
toStepRange,
closestStep,
resolveLoopingRangeValue
};
// src/hooks/useWindowResizeObserver.ts
import { useEffect as useEffect3 } from "react";
var useWindowResizeObserver = (onResize) => {
useEffect3(() => {
window.addEventListener("resize", onResize);
return () => {
window.removeEventListener("resize", onResize);
};
}, [onResize]);
};
// src/hooks/useResizeObserver.ts
import { useLayoutEffect as useLayoutEffect2 } from "react";
function useResizeObserver({ observedElementRef, onResize, isActive = true }) {
useLayoutEffect2(() => {
const el = observedElementRef?.current;
if (!el || !isActive) return;
const observer = new ResizeObserver(() => {
onResize();
});
observer.observe(el);
return () => {
observer.disconnect();
};
}, [observedElementRef, onResize, isActive]);
}
// src/hooks/useScrollObserver.ts
import { useLayoutEffect as useLayoutEffect3 } from "react";
function getScrollParents(el) {
const parents = [];
let parent = el.parentElement;
while (parent) {
const { overflow, overflowX, overflowY } = getComputedStyle(parent);
if (/(auto|scroll|overlay)/.test(overflow + overflowX + overflowY)) {
parents.push(parent);
}
parent = parent.parentElement;
}
parents.push(window);
return parents;
}
function useScrollObserver({ observedElementRef, onScroll, isActive = true }) {
useLayoutEffect3(() => {
if (!observedElementRef?.current || !isActive) return;
const parents = getScrollParents(observedElementRef.current);
parents.forEach((p) => p.addEventListener("scroll", onScroll, { passive: true }));
return () => {
parents.forEach((p) => p.removeEventListener("scroll", onScroll));
};
}, [observedElementRef, onScroll, isActive]);
}
// src/hooks/useAnchoredPosition.ts
function calculatePosition({
windowRect,
containerRect,
anchorRect,
options
}) {
const { verticalAlignment, horizontalAlignment, gap, screenPadding, avoidOverlap } = options;
const calculateBasicPosition = (hAlign, vAlign) => {
const windowWidth = windowRect.width;
const windowHeight = windowRect.height;
const maxWidth = windowWidth - 2 * screenPadding;
const maxHeight = windowHeight - 2 * screenPadding;
const width = Math.min(containerRect.width, maxWidth);
const height = Math.min(containerRect.height, maxHeight);
const anchorCenterX = anchorRect.left + anchorRect.width / 2;
const anchorCenterY = anchorRect.top + anchorRect.height / 2;
let left;
let top;
let translateXPercent;
let translateYPercent;
switch (hAlign) {
case "beforeStart":
left = anchorRect.left;
translateXPercent = -100;
break;
case "afterStart":
left = anchorRect.left;
translateXPercent = 0;
break;
case "center":
left = anchorCenterX;
translateXPercent = -50;
break;
case "beforeEnd":
left = anchorRect.right;
translateXPercent = -100;
break;
case "afterEnd":
left = anchorRect.right;
translateXPercent = 0;
break;
}
switch (vAlign) {
case "beforeStart":
top = anchorRect.top;
translateYPercent = -100;
break;
case "afterStart":
top = anchorRect.top;
translateYPercent = 0;
break;
case "center":
top = anchorCenterY;
translateYPercent = -50;
break;
case "beforeEnd":
top = anchorRect.bottom;
translateYPercent = -100;
break;
case "afterEnd":
top = anchorRect.bottom;
translateYPercent = 0;
break;
}
if (gap !== 0) {
if (hAlign === "afterEnd") {
left += gap;
} else if (hAlign === "beforeStart") {
left -= gap;
}
if (vAlign === "afterEnd") {
top += gap;
} else if (vAlign === "beforeStart") {
top -= gap;
}
}
const targetLeft = left + width * translateXPercent / 100;
const targetTop = top + height * translateYPercent / 100;
const clampedLeft = MathUtil.clamp(targetLeft, screenPadding, windowWidth - screenPadding - width);
const clampedTop = MathUtil.clamp(targetTop, screenPadding, windowHeight - screenPadding - height);
return {
left,
top,
clampedLeft,
clampedTop,
width,
height,
maxWidth,
maxHeight
};
};
const calculateOverlap = (left, top, width, height) => {
const floatingRect = {
left,
top,
right: left + width,
bottom: top + height
};
const overlapLeft = Math.max(floatingRect.left, anchorRect.left);
const overlapTop = Math.max(floatingRect.top, anchorRect.top);
const overlapRight = Math.min(floatingRect.right, anchorRect.right);
const overlapBottom = Math.min(floatingRect.bottom, anchorRect.bottom);
const overlapWidth = Math.max(0, overlapRight - overlapLeft);
const overlapHeight = Math.max(0, overlapBottom - overlapTop);
return overlapWidth * overlapHeight;
};
const originalPos = calculateBasicPosition(horizontalAlignment, verticalAlignment);
let bestPosition = originalPos;
if (avoidOverlap && (horizontalAlignment === "beforeStart" || horizontalAlignment === "afterEnd" || verticalAlignment === "beforeStart" || verticalAlignment === "afterEnd")) {
let bestOverlap = calculateOverlap(originalPos.clampedLeft, originalPos.clampedTop, originalPos.width, originalPos.height);
let altHorizontalAlignment = horizontalAlignment;
let altVerticalAlignment = verticalAlignment;
if (horizontalAlignment === "beforeStart") {
altHorizontalAlignment = "afterEnd";
} else if (horizontalAlignment === "afterEnd") {
altHorizontalAlignment = "beforeStart";
}
if (verticalAlignment === "beforeStart") {
altVerticalAlignment = "afterEnd";
} else if (verticalAlignment === "afterEnd") {
altVerticalAlignment = "beforeStart";
}
const altPos = calculateBasicPosition(altHorizontalAlignment, altVerticalAlignment);
const altOverlap = calculateOverlap(altPos.clampedLeft, altPos.clampedTop, altPos.width, altPos.height);
if (altOverlap < bestOverlap) {
bestPosition = altPos;
bestOverlap = altOverlap;
}
}
return {
left: bestPosition.clampedLeft,
top: bestPosition.clampedTop,
maxWidth: bestPosition.maxWidth,
maxHeight: bestPosition.maxHeight,
transformOrigin: "top left"
};
}
var measureSizes = ({
containerRef,
anchorRef,
windowRef
}) => {
const visualViewport = window.visualViewport;
const fallback = visualViewport ? {
top: 0,
bottom: visualViewport.height - visualViewport.offsetTop,
left: 0,
right: visualViewport.width - visualViewport.offsetLeft,
width: visualViewport.width - visualViewport.offsetLeft,
height: visualViewport.height - visualViewport.offsetTop
} : {
top: 0,
bottom: window.innerHeight,
left: 0,
right: window.innerWidth,
width: window.innerWidth,
height: window.innerHeight
};
return {
container: containerRef?.current?.getBoundingClientRect(),
anchor: anchorRef?.current?.getBoundingClientRect(),
window: windowRef?.current?.getBoundingClientRect() ?? fallback
};
};
var isSameMeasurment = (a, b) => !!a && !!b && a.container?.width === b.container?.width && a.container?.height === b.container?.height && a.anchor?.top === b.anchor?.top && a.anchor?.left === b.anchor?.left && a.anchor?.right === b.anchor?.right && a.anchor?.bottom === b.anchor?.bottom && a.anchor?.width === b.anchor?.width && a.anchor?.height === b.anchor?.height && a.window?.top === b.window?.top && a.window?.left === b.window?.left && a.window?.right === b.window?.right && a.window?.bottom === b.window?.bottom && a.window?.width === b.window?.width && a.window?.height === b.window?.height;
var isSameOptions = (a, b) => !!a && !!b && a.horizontalAlignment === b.horizontalAlignment && a.verticalAlignment === b.verticalAlignment && a.screenPadding === b.screenPadding && a.gap === b.gap && a.avoidOverlap === b.avoidOverlap;
function useAnchoredPosition({
active = true,
window: windowRef,
anchor: anchorRef,
container: containerRef,
isPolling = false,
isReactingToResize = true,
isReactingToScroll = false,
pollingInterval = 100,
verticalAlignment = "afterEnd",
horizontalAlignment = "afterStart",
avoidOverlap = false,
screenPadding = 16,
gap = 4
}) {
const [style, setStyle] = useState4();
const lastSizes = useRef2(null);
const lastOptions = useRef2(null);
const options = useMemo2(() => ({
horizontalAlignment,
verticalAlignment,
screenPadding,
gap,
avoidOverlap
}), [horizontalAlignment, verticalAlignment, screenPadding, gap, avoidOverlap]);
const calculate2 = useCallback2(() => {
const newMeasurement = measureSizes({ containerRef, anchorRef, windowRef });
if (!newMeasurement) return;
if (isSameMeasurment(lastSizes.current, newMeasurement) && isSameOptions(options, lastOptions.current)) return;
lastSizes.current = newMeasurement;
lastOptions.current = options;
if (!newMeasurement.container) {
return;
}
const containerRect = newMeasurement.container;
const windowRect = newMeasurement.window;
const anchorRect = newMeasurement.anchor ?? newMeasurement.window;
if (containerRect.width === 0 || containerRect.height === 0) {
return;
}
const calculateProps = {
windowRect,
anchorRect,
containerRect,
options
};
setStyle(calculatePosition(calculateProps));
}, [anchorRef, containerRef, options, windowRef]);
useLayoutEffect4(() => {
if (!active) {
setStyle(void 0);
lastSizes.current = null;
return;
} else {
calculate2();
}
}, [active, options, calculate2]);
useWindowResizeObserver(calculate2);
useResizeObserver({ observedElementRef: containerRef, onResize: calculate2, isActive: isReactingToResize && active });
useResizeObserver({ observedElementRef: anchorRef, onResize: calculate2, isActive: isReactingToResize && active });
useResizeObserver({ observedElementRef: windowRef, onResize: calculate2, isActive: isReactingToResize && active });
useScrollObserver({ observedElementRef: anchorRef, onScroll: calculate2, isActive: isReactingToScroll && active });
useEffect4(() => {
if (!containerRef.current || !active) {
return;
}
let timeout;
if (isPolling) {
timeout = setInterval(calculate2, pollingInterval);
}
return () => {
if (timeout) {
clearInterval(timeout);
}
};
}, [active, calculate2, containerRef, isPolling, pollingInterval]);
return style;
}
// src/utils/reactRefs.ts
function assignForwardRef(element, ref) {
if (!ref) return;
if (typeof ref === "function") {
ref(element);
} else {
ref.current = element;
}
}
function assingRefsBuilder(refs) {
return (element) => {
refs.forEach((ref) => {
assignForwardRef(element, ref);
});
};
}
var ReactRefsUtil = {
assignForwardRef,
assingRefsBuilder
};
// src/components/layout/AnchoredFloatingContainer.tsx
import { jsx as jsx6 } from "react/jsx-runtime";
var AnchoredFloatingContainer = forwardRef(function FloatingContainer({
children,
anchor,
options = {},
active = true,
...props
}, forwardRef38) {
const innerRef = useRef3(null);
const position = useAnchoredPosition({
...options,
container: innerRef,
anchor,
active
});
return /* @__PURE__ */ jsx6(
"div",
{
...props,
ref: ReactRefsUtil.assingRefsBuilder([innerRef, forwardRef38]),
"data-positioned": position ? "" : void 0,
style: {
overflow: "hidden",
...position,
...props.style
},
children
}
);
});
// src/components/user-interaction/Tooltip.tsx
import { createContext as createContext2 } from "react";
// src/utils/bagFunctions.ts
var resolve = (bagFunctionOrValue, bag) => {
if (typeof bagFunctionOrValue === "function") {
return bagFunctionOrValue(bag);
}
return bagFunctionOrValue;
};
var BagFunctionUtil = {
resolve
};
// src/components/user-interaction/Tooltip.tsx
import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
var TooltipContext = createContext2(null);
var useTooltip = () => {
const context = useContext2(TooltipContext);
if (!context) {
throw new Error("useTooltip must be used within a TooltipContext.Provider or TooltipRoot");
}
return context;
};
var TooltipRoot = ({
children,
isInitiallyShown = false,
onIsShownChange,
appearDelay: appearOverwrite,
disabled = false
}) => {
const generatedId = "tooltip-" + useId3();
const [tooltipId, setTooltipId] = useState5(generatedId);
const [isShown, setIsShown] = useState5(isInitiallyShown);
const timeoutRef = useRef4(void 0);
const { config } = useHightideConfig();
const appearDelay = useMemo3(
() => appearOverwrite ?? config.tooltip.appearDelay,
[appearOverwrite, config.tooltip.appearDelay]
);
const triggerRef = useRef4(null);
useEffect5(() => {
onIsShownChange?.(isShown);
}, [isShown, onIsShownChange]);
const openWithDelay = useCallback3(() => {
if (timeoutRef.current || isShown) return;
if (appearDelay < 0) {
setIsShown(true);
return;
}
timeoutRef.current = setTimeout(() => {
timeoutRef.current = void 0;
setIsShown(true);
}, appearDelay);
}, [appearDelay, isShown]);
const close2 = useCallback3(() => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = void 0;
}
setIsShown(false);
}, []);
useEffect5(() => {
if (!isShown) return;
const closeOnBlur = () => close2();
const closeOnScroll = () => close2();
window.addEventListener("blur", closeOnBlur);
window.addEventListener("scroll", closeOnScroll, true);
return () => {
window.removeEventListener("blur", closeOnBlur);
window.removeEventListener("scroll", closeOnScroll, true);
};
}, [isShown, close2]);
useEffect5(() => {
if (disabled) {
setIsShown(false);
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = void 0;
}
}
}, [disabled]);
const contextValue = useMemo3(() => ({
tooltip: {
id: tooltipId,
setId: setTooltipId
},
trigger: {
ref: triggerRef,
callbackRef: (el) => triggerRef.current = el,
props: {
"onPointerEnter": openWithDelay,
"onPointerLeave": close2,
"onPointerCancel": close2,
"onClick": openWithDelay,
"onBlur": close2,
"aria-describedby": tooltipId
}
},
disabled,
isShown,
open: openWithDelay,
close: close2
}), [tooltipId, openWithDelay, close2, isShown, disabled]);
return /* @__PURE__ */ jsx7(TooltipContext.Provider, { value: contextValue, children });
};
var TooltipDisplay = forwardRef2(function TooltipAnchoredFloatingContainer({
children,
alignment = "bottom",
disabled: disabledOverwrite,
isShown: isShownOverwrite,
isAnimated: isAnimatedOverwrite,
anchor: anchorOverwrite,
...props
}, forwardRef38) {
const { config } = useHightideConfig();
const tooltipContext = useContext2(TooltipContext);
const anchorFallback = useRef4(null);
const disabled = disabledOverwrite ?? tooltipContext?.disabled;
const isShown = isShownOverwrite ?? tooltipContext?.isShown ?? false;
const anchor = anchorOverwrite ?? tooltipContext?.trigger.ref ?? anchorFallback;
const id = tooltipContext?.tooltip.id ?? props.id;
useEffect5(() => {
if (!tooltipContext || !props.id) return;
tooltipContext?.tooltip.setId(props.id);
}, [props.id, tooltipContext]);
const isAnimated = useMemo3(
() => isAnimatedOverwrite ?? config.tooltip.isAnimated,
[isAnimatedOverwrite, config.tooltip.isAnimated]
);
const container = useRef4(null);
const isActive = !disabled && isShown;
const { isVisible, transitionState } = useTransitionState(
useMemo3(() => ({ isOpen: isShown, ref: container }), [isShown])
);
const verticalAlignment = useMemo3(
() => alignment === "top" ? "beforeStart" : alignment === "bottom" ? "afterEnd" : "center",
[alignment]
);
const horizontalAlignment = useMemo3(
() => alignment === "left" ? "beforeStart" : alignment === "right" ? "afterEnd" : "center",
[alignment]
);
const { zIndex } = useOverlayRegistry({ isActive });
if (disabled) return null;
return /* @__PURE__ */ jsx7(Portal, { children: /* @__PURE__ */ jsx7(
AnchoredFloatingContainer,
{
...props,
id,
ref: ReactRefsUtil.assingRefsBuilder([container, forwardRef38]),
active: isVisible,
anchor,
options: {
verticalAlignment,
horizontalAlignment,
avoidOverlap: true,
screenPadding: config.tooltip.screenPadding,
...props.options
},
"data-name": props["data-name"] ?? "tooltip",
"data-state": transitionState,
"data-animated": isAnimated ? "" : void 0,
role: "tooltip",
style: {
zIndex,
position: "fixed",
opacity: isVisible ? void 0 : 0,
...props.style
},
children
}
) });
});
var TooltipTrigger = ({
children
}) => {
const context = useTooltip();
const bag = { ...context.trigger, disabled: context.disabled, isShown: context.isShown };
return BagFunctionUtil.resolve(children, bag);
};
var Tooltip = ({
tooltip,
children,
isInitiallyShown,
appearDelay,
disabled,
containerClassName,
alignment,
isAnimated,
onIsShownChange,
displayProps
}) => {
return /* @__PURE__ */ jsxs5(
TooltipRoot,
{
isInitiallyShown,
onIsShownChange,
appearDelay,
disabled,
children: [
/* @__PURE__ */ jsx7(TooltipTrigger, { children: ({ props, callbackRef, disabled: disabled2 }) => /* @__PURE__ */ jsx7(
"div",
{
ref: callbackRef,
className: containerClassName,
...disabled2 ? void 0 : props,
children
}
) }),
/* @__PURE__ */ jsx7(
TooltipDisplay,
{
alignment,
isAnimated,
...displayProps,
children: tooltip
}
)
]
}
);
};
// src/components/layout/Visibility.tsx
import { Fragment as Fragment2, jsx as jsx8 } from "react/jsx-runtime";
function Visibility({ children, isVisible }) {
return /* @__PURE__ */ jsx8(Fragment2, { children: isVisible && children });
}
// src/hooks/useLogOnce.ts
import { useEffect as useEffect6, useState as useState6 } from "react";
var defaultOptions = {
type: "warning"
};
var useLogOnce = (message, condition, options) => {
const [hasLogged, setHasLogged] = useState6(false);
const { type } = { ...defaultOptions, ...options };
useEffect6(() => {
if (!hasLogged && condition) {
switch (type) {
case "info":
console.info(message);
break;
case "error":
console.error(message);
break;
case "warning":
console.warn(message);
break;
}
setHasLogged(true);
}
}, [condition, type, hasLogged, message]);
};
// src/components/user-interaction/IconButton.tsx
import clsx5 from "clsx";
import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
var IconButtonBase = forwardRef3(function IconButtonBase2({
children,
size = "md",
color = "primary",
coloringStyle = "solid",
disabled,
...props
}, ref) {
return /* @__PURE__ */ jsx9(
"button",
{
...props,
ref,
disabled,
type: props["type"] ?? "button",
onClick: (event) => {
props.onClick?.(event);
},
className: clsx5("icon-button", props.className),
"data-disabled": disabled ? "" : void 0,
"data-size": size ?? void 0,
"data-color": color ?? void 0,
"data-coloringstyle": coloringStyle ?? void 0,
children
}
);
});
var IconButtonTooltipTrigger = forwardRef3(function IconButtonTooltipTrigger2({
disabled,
...props
}, ref) {
const { trigger: { ref: triggerRef, props: tooltipTriggerProps } } = useTooltip();
return /* @__PURE__ */ jsx9(
IconButtonBase,
{
...props,
ref: ReactRefsUtil.assingRefsBuilder([ref, triggerRef]),
disabled,
type: props["type"] ?? "button",
onClick: (event) => {
if (!disabled) {
tooltipTriggerProps.onClick();
}
props.onClick?.(event);
},
onKeyDown: (e) => {
if (!disabled) {
if (e.key === "Enter" || e.key === " ") {
tooltipTriggerProps.onClick();
}
}
props.onKeyDown?.(e);
},
onPointerEnter: (e) => {
if (!disabled) {
tooltipTriggerProps.onPointerEnter();
}
props.onPointerEnter?.(e);
},
onPointerLeave: (e) => {
if (!disabled) {
tooltipTriggerProps.onPointerLeave();
}
props.onPointerLeave?.(e);
},
onPointerCancel: (e) => {
if (!disabled) {
tooltipTriggerProps.onPointerCancel();
}
props.onPointerCancel?.(e);
},
onBlur: (e) => {
if (!disabled) {
tooltipTriggerProps.onBlur();
}
props.onBlur?.(e);
}
}
);
});
var IconButton = forwardRef3(function IconButton2({
tooltip,
tooltipProps,
"aria-label": ariaLabel,
"aria-labelledby": ariaLabelledby,
useTooltipAsLabel = true,
color = "neutral",
disabled,
...props
}, ref) {
const isLabeled = !!ariaLabel || !!ariaLabelledby;
const isTooltipLabel = useTooltipAsLabel && !!tooltip;
useLogOnce(
'IconButton: Either provide "aria-label" or "aria-labelledby" or use "useTooltipAsLabel" and "tooltip" to give the icon button a semantic meaning',
!isLabeled && !isTooltipLabel,
{ type: "warning" }
);
return /* @__PURE__ */ jsxs6(TooltipRoot, { disabled, children: [
/* @__PURE__ */ jsx9(TooltipContext.Consumer, { children: (context) => {
const id = context?.tooltip.id;
return /* @__PURE__ */ jsx9(
IconButtonTooltipTrigger,
{
...props,
ref,
color,
disabled,
"aria-describedby": props["aria-describedby"] ?? (isLabeled && !!tooltip ? id : void 0),
"aria-labelledby": isLabeled ? ariaLabelledby : isTooltipLabel ? id : void 0,
"aria-label": ariaLabel
}
);
} }),
/* @__PURE__ */ jsx9(Visibility, { isVisible: !!tooltip, children: /* @__PURE__ */ jsx9(TooltipDisplay, { "aria-hidden": !useTooltipAsLabel && !!props["aria-hidden"], ...tooltipProps, children: tooltip }) })
] });
});
// src/components/chat/ChatAttachmentCard.tsx
import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
var ChatAttachmentCard = ({
name,
metadata,
icon,
direction = "incoming",
downloadLabel = "Download",
onDownload,
...props
}) => {
return /* @__PURE__ */ jsxs7(
"div",
{
...props,
className: clsx6("chat-attachment-card", props.className),
"data-direction": direction,
children: [
/* @__PURE__ */ jsx10("span", { className: "chat-attachment-card-icon", children: icon ?? /* @__PURE__ */ jsx10(FileText, {}) }),
/* @__PURE__ */ jsxs7("span", { className: "chat-attachment-card-info", children: [
/* @__PURE__ */ jsx10("span", { className: "chat-attachment-card-filename", children: name }),
metadata && /* @__PURE__ */ jsx10("span", { className: "chat-attachment-card-metadata", children: metadata })
] }),
onDownload && /* @__PURE__ */ jsx10(
IconButton,
{
tooltip: downloadLabel,
size: "sm",
color: "primary",
coloringStyle: "text",
onClick: onDownload,
children: /* @__PURE__ */ jsx10(Download, {})
}
)
]
}
);
};
// src/components/chat/ChatConversationList.tsx
import clsx8 from "clsx";
// src/components/layout/ScrollableList.tsx
import clsx7 from "clsx";
import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
var ScrollableList = ({
as,
header,
footer,
footerClassName,
headerClassName,
contentClassName,
className,
children,
...props
}) => {
const Component = as ?? "div";
return /* @__PURE__ */ jsxs8(Component, { ...props, className: clsx7("scrollable-list", className), children: [
header && /* @__PURE__ */ jsx11("div", { className: clsx7("scrollable-list-header", headerClassName), children: header }),
/* @__PURE__ */ jsx11("div", { className: clsx7("scrollable-list-content", contentClassName), children }),
footer && /* @__PURE__ */ jsx11("div", { className: clsx7("scrollable-list-footer", footerClassName), children: footer })
] });
};
// src/components/chat/ChatConversationList.tsx
import { jsx as jsx12 } from "react/jsx-runtime";
var ChatConversationList = ({
className,
headerClassName,
contentClassName,
footerClassName,
...props
}) => /* @__PURE__ */ jsx12(
ScrollableList,
{
...props,
className: clsx8("chat-conversation-list", className),
headerClassName: clsx8("chat-conversation-list-header", headerClassName),
contentClassName: clsx8("chat-conversation-list-scroll", contentClassName),
footerClassName: clsx8("chat-conversation-list-footer", footerClassName)
}
);
// src/components/chat/ChatConversationRow.tsx
import clsx10 from "clsx";
import { Check, CheckCheck } from "lucide-react";
// src/components/display-and-visualization/Avatar.tsx
import { useEffect as useEffect7, useMemo as useMemo4, useState as useState7 } from "react";
import clsx9 from "clsx";
import { UserIcon } from "lucide-react";
import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
import { createElement } from "react";
var DefaultAvatarImage = "img";
var Avatar = ({
image: initialImage,
name,
size = "md",
ImageComponent = DefaultAvatarImage,
...props
}) => {
const [hasError, setHasError] = useState7(false);
const [hasLoaded, setHasLoaded] = useState7(false);
const [image, setImage] = useState7(initialImage);
const Image = ImageComponent;
const displayName = useMemo4(() => {
const maxLetters = size === "sm" ? 1 : 2;
return (name ?? "").split(" ").filter((_, index) => index < maxLetters).map((value) => value[0]).join("").toUpperCase();
}, [name, size]);
const isShowingImage = !!image && (!hasError || !hasLoaded);
useEffect7(() => {
if (initialImage?.avatarUrl !== image?.avatarUrl) {
setHasError(false);
setHasLoaded(false);
}
setImage(initialImage);
}, [image?.avatarUrl, initialImage]);
return /* @__PURE__ */ jsxs9(
"div",
{
...props,
"data-name": "avatar",
"data-size": size ?? void 0,
children: [
/* @__PURE__ */ jsx13(Visibility, { isVisible: isShowingImage, children: /* @__PURE__ */ jsx13(
Image,
{
src: image?.avatarUrl,
alt: image?.alt,
"data-name": "avatar-image",
onLoad: () => setHasLoaded(true),
onError: () => setHasError(true),
"data-error": hasError ? "" : void 0,
"data-loaded": hasLoaded ? "" : void 0
},
image?.avatarUrl
) }),
name ? displayName : /* @__PURE__ */ jsx13(UserIcon, {})
]
}
);
};
var AvatarGroup = ({
avatars,
showTotalNumber = true,
size = "md",
ImageComponent,
...props
}) => {
const maxShownProfiles = 5;
const displayedProfiles = avatars.length < maxShownProfiles ? avatars : avatars.slice(0, maxShownProfiles);
const notDisplayedProfiles = avatars.length - maxShownProfiles;
const group = /* @__PURE__ */ jsx13("div", { className: "avatar-group-container", children: displayedProfiles.map((avatar, index) => /* @__PURE__ */ createElement(
Avatar,
{
...avatar,
key: index,
size,
"data-group": "",
ImageComponent: avatar.ImageComponent ?? ImageComponent
}
)) });
return /* @__PURE__ */ jsxs9(
"div",
{
...props,
"data-name": props["data-name"] ?? "avatar-group",
"data-size": size ?? void 0,
children: [
group,
showTotalNumber && notDisplayedProfiles > 0 && /* @__PURE__ */ jsx13(
"span",
{
"data-name": "avatar-group-more",
"data-size": size,
children: `+ ${notDisplayedProfiles}`
}
)
]
}
);
};
var AvatarWithStatus = ({
status = "unknown",
className,
size = "md",
...avatarProps
}) => {
return /* @__PURE__ */ jsxs9(
"div",
{
className: clsx9(className),
"data-name": "avatar-with-status",
"data-size": size ?? void 0,
children: [
/* @__PURE__ */ jsx13(Avatar, { ...avatarProps, size }),
/* @__PURE__ */ jsx13(
"div",
{
"data-name": "avatar-with-status-dot",
"data-size": size ?? void 0,
"data-status": status
}
)
]
}
);
};
var AvatarWithLabel = ({
label,
labelPosition = "left",
className,
size = "md",
...avatarProps
}) => {
const avatar = /* @__PURE__ */ jsx13(Avatar, { ...avatarProps, size });
const labelElement = /* @__PURE__ */ jsx13("span", { className: "avatar-with-label-text", children: label });
return /* @__PURE__ */ jsxs9(
"div",
{
className: clsx9("avatar-with-label", className),
"data-label-position": labelPosition,
"data-size": size ?? void 0,
children: [
labelPosition === "left" ? labelElement : avatar,
labelPosition === "left" ? avatar : labelElement
]
}
);
};
// src/utils/array.ts
var equalSizeGroups = (array, groupSize) => {
if (groupSize <= 0) {
console.warn(`group size should be greater than 0: groupSize = ${groupSize}`);
return [[...array]];
}
const groups = [];
for (let i = 0; i < array.length; i += groupSize) {
groups.push(array.slice(i, Math.min(i + groupSize, array.length)));
}
return groups;
};
var defaultRangeOptions = {
allowEmptyRange: false,
stepSize: 1,
exclusiveStart: false,
exclusiveEnd: true
};
var range = (endOrRange, options) => {
const { allowEmptyRange, stepSize, exclusiveStart, exclusiveEnd } = { ...defaultRangeOptions, ...options };
let start = 0;
let end;
if (typeof endOrRange === "number") {
end = endOrRange;
} else {
start = endOrRange[0];
end = endOrRange[1];
}
if (!exclusiveEnd) {
end -= 1;
}
if (exclusiveStart) {
start += 1;
}
if (end - 1 < start) {
if (!allowEmptyRange) {
console.warn(`range: end (${end}) < start (${start}) should be allowed explicitly, set options.allowEmptyRange to true`);
}
return [];
}
return Array.from({ length: end - start }, (_, index) => index * stepSize + start);
};
var closestMatch = (list, firstCloser) => {
return list.reduce((item1, item2) => {
return firstCloser(item1, item2) ? item1 : item2;
});
};
var getNeighbours = (list, item, neighbourDistance = 2) => {
const index = list.indexOf(item);
const totalItems = neighbourDistance * 2 + 1;
if (list.length < totalItems) {