feat: integrate new bubbles (#10550)

To test this, set the `useNextBubble` value to `true` in the
localstorage. Here's a quick command to run in the console

```js
localStorage.setItem('useNextBubble', true)
```

```js
localStorage.setItem('useNextBubble', false)
```

---------

Co-authored-by: Pranav <pranavrajs@gmail.com>
This commit is contained in:
Shivam Mishra
2024-12-19 18:41:55 +05:30
committed by GitHub
parent 9279175199
commit eef70b9bd7
30 changed files with 922 additions and 866 deletions

View File

@@ -124,6 +124,19 @@
--teal-11: 0 133 115;
--teal-12: 13 61 56;
--gray-1: 252 252 252;
--gray-2: 249 249 249;
--gray-3: 240 240 240;
--gray-4: 232 232 232;
--gray-5: 224 224 224;
--gray-6: 217 217 217;
--gray-7: 206 206 206;
--gray-8: 187 187 187;
--gray-9: 141 141 141;
--gray-10: 131 131 131;
--gray-11: 100 100 100;
--gray-12: 32 32 32;
--background-color: 253 253 253;
--text-blue: 8 109 224;
--border-container: 236 236 236;
@@ -213,6 +226,19 @@
--teal-11: 11 216 182;
--teal-12: 173 240 221;
--gray-1: 17 17 17;
--gray-2: 25 25 25;
--gray-3: 34 34 34;
--gray-4: 42 42 42;
--gray-5: 49 49 49;
--gray-6: 58 58 58;
--gray-7: 72 72 72;
--gray-8: 96 96 96;
--gray-9: 110 110 110;
--gray-10: 123 123 123;
--gray-11: 180 180 180;
--gray-12: 238 238 238;
--background-color: 18 18 19;
--border-strong: 52 52 52;
--border-weak: 38 38 42;
@@ -232,7 +258,7 @@
--black-alpha-2: 0, 0, 0, 0.2;
--border-blue: 39, 129, 246, 0.5;
--border-container: 236, 236, 236, 0;
--white-alpha: 255, 255, 255, 0.8;
--white-alpha: 255, 255, 255, 0.1;
}
/* NEXT COLORS END */

View File

@@ -1,6 +1,12 @@
<script setup>
import { computed, defineAsyncComponent } from 'vue';
import { computed, ref, toRefs } from 'vue';
import { provideMessageContext } from './provider.js';
import { useTrack } from 'dashboard/composables';
import { emitter } from 'shared/helpers/mitt';
import { LocalStorage } from 'shared/helpers/localStorage';
import { ACCOUNT_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import {
MESSAGE_TYPES,
ATTACHMENT_TYPES,
@@ -8,6 +14,7 @@ import {
SENDER_TYPES,
ORIENTATION,
MESSAGE_STATUS,
CONTENT_TYPES,
} from './constants';
import Avatar from 'next/avatar/Avatar.vue';
@@ -19,17 +26,14 @@ import FileBubble from './bubbles/File.vue';
import AudioBubble from './bubbles/Audio.vue';
import VideoBubble from './bubbles/Video.vue';
import InstagramStoryBubble from './bubbles/InstagramStory.vue';
import AttachmentsBubble from './bubbles/Attachments.vue';
import EmailBubble from './bubbles/Email/Index.vue';
import UnsupportedBubble from './bubbles/Unsupported.vue';
import ContactBubble from './bubbles/Contact.vue';
import DyteBubble from './bubbles/Dyte.vue';
const LocationBubble = defineAsyncComponent(
() => import('./bubbles/Location.vue')
);
import LocationBubble from './bubbles/Location.vue';
import MessageError from './MessageError.vue';
import MessageMeta from './MessageMeta.vue';
import ContextMenu from 'dashboard/modules/conversations/components/MessageContextMenu.vue';
/**
* @typedef {Object} Attachment
@@ -65,7 +69,7 @@ import MessageMeta from './MessageMeta.vue';
/**
* @typedef {Object} Props
* @property {('sent'|'delivered'|'read'|'failed')} status - The delivery status of the message
* @property {('sent'|'delivered'|'read'|'failed'|'progress')} status - The delivery status of the message
* @property {ContentAttributes} [contentAttributes={}] - Additional attributes of the message content
* @property {Attachment[]} [attachments=[]] - The attachments associated with the message
* @property {Sender|null} [sender=null] - The sender information
@@ -78,6 +82,11 @@ import MessageMeta from './MessageMeta.vue';
* @property {string|null} [error=null] - Error message if the message failed to send
* @property {string|null} [senderType=null] - The type of the sender
* @property {string} content - The message content
* @property {boolean} [groupWithNext=false] - Whether the message should be grouped with the next message
* @property {Object|null} [inReplyTo=null] - The message to which this message is a reply
* @property {boolean} [isEmailInbox=false] - Whether the message is from an email inbox
* @property {number} conversationId - The ID of the conversation to which the message belongs
* @property {number} inboxId - The ID of the inbox to which the message belongs
*/
// eslint-disable-next-line vue/define-macros-order
@@ -93,70 +102,51 @@ const props = defineProps({
required: true,
validator: value => Object.values(MESSAGE_STATUS).includes(value),
},
attachments: {
type: Array,
default: () => [],
},
private: {
type: Boolean,
default: false,
},
createdAt: {
type: Number,
required: true,
},
sender: {
type: Object,
default: null,
},
senderId: {
type: Number,
default: null,
},
senderType: {
attachments: { type: Array, default: () => [] },
content: { type: String, default: null },
contentAttributes: { type: Object, default: () => ({}) },
contentType: {
type: String,
default: null,
},
content: {
type: String,
required: true,
},
contentAttributes: {
type: Object,
default: () => {},
},
currentUserId: {
type: Number,
required: true,
},
groupWithNext: {
type: Boolean,
default: false,
},
inReplyTo: {
type: Object,
default: null,
},
isEmailInbox: {
type: Boolean,
default: false,
default: 'text',
validator: value => Object.values(CONTENT_TYPES).includes(value),
},
conversationId: { type: Number, required: true },
createdAt: { type: Number, required: true }, // eslint-disable-line vue/no-unused-properties
currentUserId: { type: Number, required: true },
groupWithNext: { type: Boolean, default: false },
inboxId: { type: Number, required: true }, // eslint-disable-line vue/no-unused-properties
inboxSupportsReplyTo: { type: Object, default: () => ({}) },
inReplyTo: { type: Object, default: null }, // eslint-disable-line vue/no-unused-properties
isEmailInbox: { type: Boolean, default: false },
private: { type: Boolean, default: false },
sender: { type: Object, default: null },
senderId: { type: Number, default: null },
senderType: { type: String, default: null },
sourceId: { type: String, default: '' }, // eslint-disable-line vue/no-unused-properties
});
const contextMenuPosition = ref({});
const showContextMenu = ref(false);
/**
* Computes the message variant based on props
* @type {import('vue').ComputedRef<'user'|'agent'|'activity'|'private'|'bot'|'template'>}
*/
const variant = computed(() => {
if (props.private) return MESSAGE_VARIANTS.PRIVATE;
if (props.isEmailInbox) {
const emailInboxTypes = [MESSAGE_TYPES.INCOMING, MESSAGE_TYPES.OUTGOING];
if (emailInboxTypes.includes(props.messageType)) {
return MESSAGE_VARIANTS.EMAIL;
}
}
if (props.contentType === CONTENT_TYPES.INCOMING_EMAIL) {
return MESSAGE_VARIANTS.EMAIL;
}
if (props.status === MESSAGE_STATUS.FAILED) return MESSAGE_VARIANTS.ERROR;
if (props.contentAttributes.isUnsupported)
if (props.contentAttributes?.isUnsupported)
return MESSAGE_VARIANTS.UNSUPPORTED;
const variants = {
@@ -170,10 +160,20 @@ const variant = computed(() => {
});
const isMyMessage = computed(() => {
// if an outgoing message is still processing, then it's definitely a
// message sent by the current user
if (
props.status === MESSAGE_STATUS.PROGRESS &&
props.messageType === MESSAGE_TYPES.OUTGOING
) {
return true;
}
const senderId = props.senderId ?? props.sender?.id;
const senderType = props.senderType ?? props.sender?.type;
if (!senderType || !senderId) return false;
if (!senderType || !senderId) {
return false;
}
return (
senderType.toLowerCase() === SENDER_TYPES.USER.toLowerCase() &&
@@ -218,11 +218,9 @@ const gridTemplate = computed(() => {
const map = {
[ORIENTATION.LEFT]: `
"avatar bubble"
"spacer meta"
`,
[ORIENTATION.RIGHT]: `
"bubble"
"meta"
`,
};
@@ -248,7 +246,11 @@ const componentToRender = computed(() => {
if (emailInboxTypes.includes(props.messageType)) return EmailBubble;
}
if (props.contentAttributes.isUnsupported) {
if (props.contentType === CONTENT_TYPES.INCOMING_EMAIL) {
return EmailBubble;
}
if (props.contentAttributes?.isUnsupported) {
return UnsupportedBubble;
}
@@ -260,7 +262,7 @@ const componentToRender = computed(() => {
return InstagramStoryBubble;
}
if (props.attachments.length === 1) {
if (Array.isArray(props.attachments) && props.attachments.length === 1) {
const fileType = props.attachments[0].fileType;
if (!props.content) {
@@ -275,23 +277,93 @@ const componentToRender = computed(() => {
if (fileType === ATTACHMENT_TYPES.CONTACT) return ContactBubble;
}
if (props.attachments.length > 1 && !props.content) {
return AttachmentsBubble;
}
return TextBubble;
});
const shouldShowContextMenu = computed(() => {
return !(
props.status === MESSAGE_STATUS.FAILED ||
props.status === MESSAGE_STATUS.PROGRESS ||
props.contentAttributes?.isUnsupported
);
});
const isBubble = computed(() => {
return props.messageType !== MESSAGE_TYPES.ACTIVITY;
});
const isMessageDeleted = computed(() => {
return props.contentAttributes?.deleted;
});
const payloadForContextMenu = computed(() => {
return {
id: props.id,
content_attributes: props.contentAttributes,
content: props.content,
conversation_id: props.conversationId,
};
});
const contextMenuEnabledOptions = computed(() => {
const hasText = !!props.content;
const hasAttachments = !!(props.attachments && props.attachments.length > 0);
const isOutgoing = props.messageType === MESSAGE_TYPES.OUTGOING;
return {
copy: hasText,
delete: hasText || hasAttachments,
cannedResponse: isOutgoing && hasText,
replyTo: !props.private && props.inboxSupportsReplyTo.outgoing,
};
});
function openContextMenu(e) {
const shouldSkipContextMenu =
e.target?.classList.contains('skip-context-menu') ||
e.target?.tagName.toLowerCase() === 'a';
if (shouldSkipContextMenu || getSelection().toString()) {
return;
}
e.preventDefault();
if (e.type === 'contextmenu') {
useTrack(ACCOUNT_EVENTS.OPEN_MESSAGE_CONTEXT_MENU);
}
contextMenuPosition.value = {
x: e.pageX || e.clientX,
y: e.pageY || e.clientY,
};
showContextMenu.value = true;
}
function closeContextMenu() {
showContextMenu.value = false;
contextMenuPosition.value = { x: null, y: null };
}
function handleReplyTo() {
const replyStorageKey = LOCAL_STORAGE_KEYS.MESSAGE_REPLY_TO;
const { conversationId, id: replyTo } = props;
LocalStorage.updateJsonStore(replyStorageKey, conversationId, replyTo);
emitter.emit(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, props);
}
provideMessageContext({
...toRefs(props),
isPrivate: computed(() => props.private),
variant,
inReplyTo: props.inReplyTo,
orientation,
isMyMessage,
shouldGroupWithNext,
});
</script>
<template>
<div
:id="`message${props.id}`"
class="flex w-full"
:data-message-id="props.id"
:class="[flexOrientationClass, shouldGroupWithNext ? 'mb-2' : 'mb-4']"
@@ -324,12 +396,13 @@ provideMessageContext({
/>
</div>
<div
class="[grid-area:bubble]"
class="[grid-area:bubble] flex"
:class="{
'pl-9': ORIENTATION.RIGHT === orientation,
'pl-9 justify-end': orientation === ORIENTATION.RIGHT,
}"
@contextmenu="openContextMenu($event)"
>
<Component :is="componentToRender" v-bind="props" />
<Component :is="componentToRender" />
</div>
<MessageError
v-if="contentAttributes.externalError"
@@ -337,15 +410,18 @@ provideMessageContext({
:class="flexOrientationClass"
:error="contentAttributes.externalError"
/>
<MessageMeta
v-else-if="!shouldGroupWithNext"
class="[grid-area:meta]"
:class="flexOrientationClass"
:sender="props.sender"
:status="props.status"
:private="props.private"
:is-my-message="isMyMessage"
:created-at="props.createdAt"
</div>
<div v-if="shouldShowContextMenu" class="context-menu-wrap">
<ContextMenu
v-if="isBubble && !isMessageDeleted"
:context-menu-position="contextMenuPosition"
:is-open="showContextMenu"
:enabled-options="contextMenuEnabledOptions"
:message="payloadForContextMenu"
hide-button
@open="openContextMenu"
@close="closeContextMenu"
@reply-to="handleReplyTo"
/>
</div>
</div>

View File

@@ -0,0 +1,124 @@
<script setup>
import { defineProps, computed } from 'vue';
import Message from './Message.vue';
import { useCamelCase } from 'dashboard/composables/useTransformKeys';
/**
* Props definition for the component
* @typedef {Object} Props
* @property {Array} readMessages - Array of read messages
* @property {Array} unReadMessages - Array of unread messages
* @property {Number} currentUserId - ID of the current user
* @property {Boolean} isAnEmailChannel - Whether this is an email channel
* @property {Object} inboxSupportsReplyTo - Inbox reply support configuration
* @property {Array} messages - Array of all messages
*/
const props = defineProps({
readMessages: {
type: Array,
default: () => [],
},
unReadMessages: {
type: Array,
default: () => [],
},
currentUserId: {
type: Number,
required: true,
},
isAnEmailChannel: {
type: Boolean,
default: false,
},
inboxSupportsReplyTo: {
type: Object,
default: () => ({ incoming: false, outgoing: false }),
},
messages: {
type: Array,
default: () => [],
},
});
const unread = computed(() => {
return useCamelCase(props.unReadMessages, { deep: true });
});
const read = computed(() => {
return useCamelCase(props.readMessages, { deep: true });
});
/**
* Determines if a message should be grouped with the next message
* @param {Number} index - Index of the current message
* @param {Array} messages - Array of messages to check
* @returns {Boolean} - Whether the message should be grouped with next
*/
const shouldGroupWithNext = (index, messages) => {
if (index === messages.length - 1) return false;
const current = messages[index];
const next = messages[index + 1];
if (next.status === 'failed') return false;
const nextSenderId = next.senderId ?? next.sender?.id;
const currentSenderId = current.senderId ?? current.sender?.id;
if (currentSenderId !== nextSenderId) return false;
// Check if messages are in the same minute by rounding down to nearest minute
return Math.floor(next.createdAt / 60) === Math.floor(current.createdAt / 60);
};
/**
* Gets the message that was replied to
* @param {Object} parentMessage - The message containing the reply reference
* @returns {Object|null} - The message being replied to, or null if not found
*/
const getInReplyToMessage = parentMessage => {
if (!parentMessage) return null;
const inReplyToMessageId =
parentMessage.contentAttributes?.inReplyTo ??
parentMessage.content_attributes?.in_reply_to;
if (!inReplyToMessageId) return null;
// Find in-reply-to message in the messages prop
const replyMessage = props.messages?.find(
message => message.id === inReplyToMessageId
);
return replyMessage ? useCamelCase(replyMessage) : null;
};
</script>
<template>
<ul class="px-4 bg-n-background">
<slot name="beforeAll" />
<template v-for="(message, index) in read" :key="message.id">
<Message
v-bind="message"
:is-email-inbox="isAnEmailChannel"
:in-reply-to="getInReplyToMessage(message)"
:group-with-next="shouldGroupWithNext(index, readMessages)"
:inbox-supports-reply-to="inboxSupportsReplyTo"
:current-user-id="currentUserId"
data-clarity-mask="True"
/>
</template>
<slot name="beforeUnread" />
<template v-for="(message, index) in unread" :key="message.id">
<Message
v-bind="message"
:in-reply-to="getInReplyToMessage(message)"
:group-with-next="shouldGroupWithNext(index, unReadMessages)"
:inbox-supports-reply-to="inboxSupportsReplyTo"
:current-user-id="currentUserId"
:is-email-inbox="isAnEmailChannel"
data-clarity-mask="True"
/>
</template>
<slot name="after" />
</ul>
</template>

View File

@@ -4,74 +4,116 @@ import { messageTimestamp } from 'shared/helpers/timeHelper';
import MessageStatus from './MessageStatus.vue';
import Icon from 'next/icon/Icon.vue';
import { useInbox } from 'dashboard/composables/useInbox';
import { useMessageContext } from './provider.js';
import { MESSAGE_STATUS } from './constants';
import { MESSAGE_STATUS, MESSAGE_TYPES } from './constants';
/**
* @typedef {Object} Sender
* @property {Object} additional_attributes - Additional attributes of the sender
* @property {Object} custom_attributes - Custom attributes of the sender
* @property {string} email - Email of the sender
* @property {number} id - ID of the sender
* @property {string|null} identifier - Identifier of the sender
* @property {string} name - Name of the sender
* @property {string|null} phone_number - Phone number of the sender
* @property {string} thumbnail - Thumbnail URL of the sender
* @property {string} type - Type of sender
*/
const {
isAFacebookInbox,
isALineChannel,
isAPIInbox,
isASmsInbox,
isATelegramChannel,
isATwilioChannel,
isAWebWidgetInbox,
isAWhatsAppChannel,
isAnEmailChannel,
} = useInbox();
/**
* @typedef {Object} Props
* @property {('sent'|'delivered'|'read'|'failed')} status - The delivery status of the message
* @property {boolean} [private=false] - Whether the message is private
* @property {isMyMessage} [private=false] - Whether the message is sent by the current user or not
* @property {number} createdAt - Timestamp when the message was created
* @property {Sender|null} [sender=null] - The sender information
*/
const props = defineProps({
sender: {
type: Object,
required: true,
},
status: {
type: String,
required: true,
validator: value => Object.values(MESSAGE_STATUS).includes(value),
},
private: {
type: Boolean,
default: false,
},
isMyMessage: {
type: Boolean,
default: false,
},
createdAt: {
type: Number,
required: true,
},
});
const { status, isPrivate, createdAt, sourceId, messageType } =
useMessageContext();
const readableTime = computed(() =>
messageTimestamp(props.createdAt, 'LLL d, h:mm a')
messageTimestamp(createdAt.value, 'LLL d, h:mm a')
);
const showSender = computed(() => !props.isMyMessage && props.sender);
const showStatusIndicator = computed(() => {
if (isPrivate.value) return false;
if (messageType.value === MESSAGE_TYPES.OUTGOING) return true;
if (messageType.value === MESSAGE_TYPES.TEMPLATE) return true;
return false;
});
const isSent = computed(() => {
if (!showStatusIndicator.value) return false;
// Messages will be marked as sent for the Email channel if they have a source ID.
if (isAnEmailChannel.value) return !!sourceId.value;
if (
isAWhatsAppChannel.value ||
isATwilioChannel.value ||
isAFacebookInbox.value ||
isASmsInbox.value ||
isATelegramChannel.value
) {
return sourceId.value && status.value === MESSAGE_STATUS.SENT;
}
// All messages will be mark as sent for the Line channel, as there is no source ID.
if (isALineChannel.value) return true;
return false;
});
const isDelivered = computed(() => {
if (!showStatusIndicator.value) return false;
if (
isAWhatsAppChannel.value ||
isATwilioChannel.value ||
isASmsInbox.value ||
isAFacebookInbox.value
) {
return sourceId.value && status.value === MESSAGE_STATUS.DELIVERED;
}
// All messages marked as delivered for the web widget inbox and API inbox once they are sent.
if (isAWebWidgetInbox.value || isAPIInbox.value) {
return status.value === MESSAGE_STATUS.SENT;
}
if (isALineChannel.value) {
return status.value === MESSAGE_STATUS.DELIVERED;
}
return false;
});
const isRead = computed(() => {
if (!showStatusIndicator.value) return false;
if (
isAWhatsAppChannel.value ||
isATwilioChannel.value ||
isAFacebookInbox.value
) {
return sourceId.value && status.value === MESSAGE_STATUS.READ;
}
if (isAWebWidgetInbox.value || isAPIInbox.value) {
return status.value === MESSAGE_STATUS.READ;
}
return false;
});
const statusToShow = computed(() => {
if (isRead.value) return MESSAGE_STATUS.READ;
if (isDelivered.value) return MESSAGE_STATUS.DELIVERED;
if (isSent.value) return MESSAGE_STATUS.SENT;
return MESSAGE_STATUS.PROGRESS;
});
</script>
<template>
<div class="text-xs text-n-slate-11 flex items-center gap-1.5">
<div class="text-xs flex items-center gap-1.5">
<div class="inline">
<span v-if="showSender" class="inline capitalize">{{ sender.name }}</span>
<span v-if="showSender && readableTime" class="inline"> </span>
<span class="inline">{{ readableTime }}</span>
</div>
<Icon
v-if="props.private"
icon="i-lucide-lock-keyhole"
class="text-n-slate-10 size-3"
/>
<MessageStatus v-if="props.isMyMessage" :status />
<Icon v-if="isPrivate" icon="i-lucide-lock-keyhole" class="size-3" />
<MessageStatus v-if="showStatusIndicator" :status="statusToShow" />
</div>
</template>
`

View File

@@ -1,16 +1,25 @@
<script setup>
import { computed } from 'vue';
import { messageTimestamp } from 'shared/helpers/timeHelper';
import BaseBubble from './Base.vue';
import { useMessageContext } from '../provider.js';
defineProps({
content: {
type: String,
required: true,
},
});
const { content, createdAt } = useMessageContext();
const readableTime = computed(() =>
messageTimestamp(createdAt.value, 'LLL d, h:mm a')
);
</script>
<template>
<BaseBubble class="px-2 py-0.5" data-bubble-name="activity">
<BaseBubble
class="px-2 py-0.5 !rounded-full flex items-center gap-2"
data-bubble-name="activity"
>
<span v-dompurify-html="content" />
<div v-if="readableTime" class="w-px h-3 rounded-full bg-n-slate-7" />
<span class="text-n-slate-10">
{{ readableTime }}
</span>
</BaseBubble>
</template>

View File

@@ -1,30 +0,0 @@
<script setup>
import BaseBubble from 'next/message/bubbles/Base.vue';
import AttachmentChips from 'next/message/chips/AttachmentChips.vue';
/**
* @typedef {Object} Attachment
* @property {number} id - Unique identifier for the attachment
* @property {number} messageId - ID of the associated message
* @property {'image'|'audio'|'video'|'file'|'location'|'fallback'|'share'|'story_mention'|'contact'|'ig_reel'} fileType - Type of the attachment (file or image)
* @property {number} accountId - ID of the associated account
* @property {string|null} extension - File extension
* @property {string} dataUrl - URL to access the full attachment data
* @property {string} thumbUrl - URL to access the thumbnail version
* @property {number} fileSize - Size of the file in bytes
* @property {number|null} width - Width of the image if applicable
* @property {number|null} height - Height of the image if applicable
*/
defineProps({
attachments: {
type: Array,
default: () => [],
},
});
</script>
<template>
<BaseBubble class="grid gap-2 bg-transparent" data-bubble-name="attachments">
<AttachmentChips :attachments="attachments" class="gap-1" />
</BaseBubble>
</template>

View File

@@ -2,43 +2,17 @@
import { computed } from 'vue';
import BaseBubble from './Base.vue';
import AudioChip from 'next/message/chips/Audio.vue';
import { useMessageContext } from '../provider.js';
/**
* @typedef {Object} Attachment
* @property {number} id - Unique identifier for the attachment
* @property {number} messageId - ID of the associated message
* @property {'image'|'audio'|'video'|'file'|'location'|'fallback'|'share'|'story_mention'|'contact'|'ig_reel'} fileType - Type of the attachment (file or image)
* @property {number} accountId - ID of the associated account
* @property {string|null} extension - File extension
* @property {string} dataUrl - URL to access the full attachment data
* @property {string} thumbUrl - URL to access the thumbnail version
* @property {number} fileSize - Size of the file in bytes
* @property {number|null} width - Width of the image if applicable
* @property {number|null} height - Height of the image if applicable
*/
/**
* @typedef {Object} Props
* @property {Attachment[]} [attachments=[]] - The attachments associated with the message
*/
const props = defineProps({
attachments: {
type: Array,
required: true,
},
});
const { attachments } = useMessageContext();
const attachment = computed(() => {
return props.attachments[0];
return attachments.value[0];
});
</script>
<template>
<BaseBubble class="bg-transparent" data-bubble-name="audio">
<AudioChip
:attachment="attachment"
class="p-2 text-n-slate-12 bg-n-alpha-3"
/>
<AudioChip :attachment="attachment" class="p-2 text-n-slate-12" />
</BaseBubble>
</template>

View File

@@ -1,6 +1,8 @@
<script setup>
import { computed } from 'vue';
import MessageMeta from '../MessageMeta.vue';
import { emitter } from 'shared/helpers/mitt';
import { useMessageContext } from '../provider.js';
import { useI18n } from 'vue-i18n';
@@ -8,14 +10,15 @@ import { useI18n } from 'vue-i18n';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import { MESSAGE_VARIANTS, ORIENTATION } from '../constants';
const { variant, orientation, inReplyTo } = useMessageContext();
const { variant, orientation, inReplyTo, shouldGroupWithNext } =
useMessageContext();
const { t } = useI18n();
const varaintBaseMap = {
[MESSAGE_VARIANTS.AGENT]: 'bg-n-solid-blue text-n-slate-12',
[MESSAGE_VARIANTS.PRIVATE]:
'bg-n-solid-amber text-n-amber-12 [&_.prosemirror-mention-node]:font-semibold',
[MESSAGE_VARIANTS.USER]: 'bg-n-slate-4 text-n-slate-12',
[MESSAGE_VARIANTS.USER]: 'bg-n-gray-4 text-n-slate-12',
[MESSAGE_VARIANTS.ACTIVITY]: 'bg-n-alpha-1 text-n-slate-11 text-sm',
[MESSAGE_VARIANTS.BOT]: 'bg-n-solid-iris text-n-slate-12',
[MESSAGE_VARIANTS.TEMPLATE]: 'bg-n-solid-iris text-n-slate-12',
@@ -31,6 +34,16 @@ const orientationMap = {
[ORIENTATION.CENTER]: 'rounded-md',
};
const flexOrientationClass = computed(() => {
const map = {
[ORIENTATION.LEFT]: 'justify-start',
[ORIENTATION.RIGHT]: 'justify-end',
[ORIENTATION.CENTER]: 'justify-center',
};
return map[orientation.value];
});
const messageClass = computed(() => {
const classToApply = [varaintBaseMap[variant.value]];
@@ -72,7 +85,7 @@ const previewMessage = computed(() => {
:class="[
messageClass,
{
'max-w-md': variant !== MESSAGE_VARIANTS.EMAIL,
'max-w-lg': variant !== MESSAGE_VARIANTS.EMAIL,
},
]"
>
@@ -86,5 +99,16 @@ const previewMessage = computed(() => {
</span>
</div>
<slot />
<MessageMeta
v-if="!shouldGroupWithNext && variant !== MESSAGE_VARIANTS.ACTIVITY"
:class="[
flexOrientationClass,
variant === MESSAGE_VARIANTS.EMAIL ? 'px-3 pb-3' : '',
variant === MESSAGE_VARIANTS.PRIVATE
? 'text-n-amber-12/50'
: 'text-n-slate-11',
]"
class="mt-2"
/>
</div>
</template>

View File

@@ -3,11 +3,11 @@ import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import BaseBubble from './Base.vue';
import Icon from 'next/icon/Icon.vue';
import { useMessageContext } from '../provider.js';
const props = defineProps({
defineProps({
icon: { type: [String, Object], required: true },
iconBgColor: { type: String, default: 'bg-n-alpha-3' },
sender: { type: Object, default: () => ({}) },
senderTranslationKey: { type: String, required: true },
content: { type: String, required: true },
action: {
@@ -19,60 +19,62 @@ const props = defineProps({
},
});
const { sender } = useMessageContext();
const { t } = useI18n();
const senderName = computed(() => {
return props.sender.name;
return sender?.value.name;
});
</script>
<template>
<BaseBubble
class="overflow-hidden grid gap-4 min-w-64 p-0"
class="overflow-hidden p-3 !bg-n-solid-2 shadow-[0px_0px_12px_0px_rgba(0,0,0,0.05)]"
data-bubble-name="attachment"
>
<slot name="before" />
<div class="grid gap-3 px-3 pt-3 z-20">
<div
class="size-8 rounded-lg grid place-content-center"
:class="iconBgColor"
>
<slot name="icon">
<Icon :icon="icon" class="text-white size-4" />
</slot>
</div>
<div class="space-y-1">
<div v-if="senderName" class="text-n-slate-12 text-sm truncate">
{{
t(senderTranslationKey, {
sender: senderName,
})
}}
<div class="grid gap-4 min-w-64">
<div class="grid gap-3 z-20">
<div
class="size-8 rounded-lg grid place-content-center"
:class="iconBgColor"
>
<slot name="icon">
<Icon :icon="icon" class="text-white size-4" />
</slot>
</div>
<slot>
<div v-if="content" class="truncate text-sm text-n-slate-11">
{{ content }}
<div class="space-y-1">
<div v-if="senderName" class="text-n-slate-12 text-sm truncate">
{{
t(senderTranslationKey, {
sender: senderName,
})
}}
</div>
</slot>
<slot>
<div v-if="content" class="truncate text-sm text-n-slate-11">
{{ content }}
</div>
</slot>
</div>
</div>
<div v-if="action">
<a
v-if="action.href"
:href="action.href"
rel="noreferrer noopener nofollow"
target="_blank"
class="w-full block bg-n-solid-3 px-4 py-2 rounded-lg text-sm text-center border border-n-container"
>
{{ action.label }}
</a>
<button
v-else
class="w-full bg-n-solid-3 px-4 py-2 rounded-lg text-sm text-center border border-n-container"
@click="action.onClick"
>
{{ action.label }}
</button>
</div>
</div>
<div v-if="action" class="px-3 pb-3">
<a
v-if="action.href"
:href="action.href"
rel="noreferrer noopener nofollow"
target="_blank"
class="w-full block bg-n-solid-3 px-4 py-2 rounded-lg text-sm text-center"
>
{{ action.label }}
</a>
<button
v-else
class="w-full bg-n-solid-3 px-4 py-2 rounded-lg text-sm"
@click="action.onClick"
>
{{ action.label }}
</button>
</div>
</BaseBubble>
</template>

View File

@@ -3,6 +3,7 @@ import { computed } from 'vue';
import { useAlert } from 'dashboard/composables';
import { useStore } from 'dashboard/composables/store';
import { useI18n } from 'vue-i18n';
import { useMessageContext } from '../provider.js';
import BaseAttachmentBubble from './BaseAttachment.vue';
import {
@@ -10,45 +11,13 @@ import {
ExceptionWithMessage,
} from 'shared/helpers/CustomErrors';
/**
* @typedef {Object} Attachment
* @property {number} id - Unique identifier for the attachment
* @property {number} messageId - ID of the associated message
* @property {'image'|'audio'|'video'|'file'|'location'|'fallback'|'share'|'story_mention'|'contact'|'ig_reel'} fileType - Type of the attachment (file or image)
* @property {number} accountId - ID of the associated account
* @property {string|null} extension - File extension
* @property {string} dataUrl - URL to access the full attachment data
* @property {string} thumbUrl - URL to access the thumbnail version
* @property {number} fileSize - Size of the file in bytes
* @property {number|null} width - Width of the image if applicable
* @property {number|null} height - Height of the image if applicable
*/
/**
* @typedef {Object} Props
* @property {Attachment[]} [attachments=[]] - The attachments associated with the message
*/
const props = defineProps({
content: {
type: String,
required: true,
},
attachments: {
type: Array,
required: true,
},
sender: {
type: Object,
default: () => ({}),
},
});
const { content, attachments } = useMessageContext();
const $store = useStore();
const { t } = useI18n();
const attachment = computed(() => {
return props.attachments[0];
return attachments.value[0];
});
const phoneNumber = computed(() => {
@@ -64,7 +33,7 @@ const rawPhoneNumber = computed(() => {
});
const name = computed(() => {
return props.content;
return content.value;
});
function getContactObject() {
@@ -129,7 +98,6 @@ const action = computed(() => ({
<BaseAttachmentBubble
icon="i-teenyicons-user-circle-solid"
icon-bg-color="bg-[#D6409F]"
:sender="sender"
sender-translation-key="CONVERSATION.SHARED_ATTACHMENT.CONTACT"
:content="phoneNumber"
:action="formattedPhoneNumber ? action : null"

View File

@@ -5,23 +5,16 @@ import { buildDyteURL } from 'shared/helpers/IntegrationHelper';
import { useCamelCase } from 'dashboard/composables/useTransformKeys';
import { useAlert } from 'dashboard/composables';
import { useI18n } from 'vue-i18n';
import { useMessageContext } from '../provider.js';
import BaseAttachmentBubble from './BaseAttachment.vue';
const props = defineProps({
contentAttributes: {
type: String,
required: true,
},
sender: {
type: Object,
default: () => ({}),
},
});
const { contentAttributes } = useMessageContext();
const { t } = useI18n();
const meetingData = computed(() => {
return useCamelCase(props.contentAttributes.data);
return useCamelCase(contentAttributes.value.data);
});
const isLoading = ref(false);
@@ -57,7 +50,6 @@ const action = computed(() => ({
<BaseAttachmentBubble
icon="i-ph-video-camera-fill"
icon-bg-color="bg-[#2781F6]"
:sender="sender"
sender-translation-key="CONVERSATION.SHARED_ATTACHMENT.MEETING"
:action="action"
>

View File

@@ -1,57 +1,44 @@
<script setup>
import { computed } from 'vue';
import { MESSAGE_STATUS } from '../../constants';
import { useMessageContext } from '../../provider.js';
const props = defineProps({
contentAttributes: {
type: Object,
default: () => ({}),
},
status: {
type: String,
required: true,
validator: value => Object.values(MESSAGE_STATUS).includes(value),
},
sender: {
type: Object,
default: () => ({}),
},
});
const { contentAttributes, status, sender } = useMessageContext();
const hasError = computed(() => {
return props.status === MESSAGE_STATUS.FAILED;
return status.value === MESSAGE_STATUS.FAILED;
});
const fromEmail = computed(() => {
return props.contentAttributes?.email?.from ?? [];
return contentAttributes.value?.email?.from ?? [];
});
const toEmail = computed(() => {
return props.contentAttributes?.email?.to ?? [];
return contentAttributes.value?.email?.to ?? [];
});
const ccEmail = computed(() => {
return (
props.contentAttributes?.ccEmails ??
props.contentAttributes?.email?.cc ??
contentAttributes.value?.ccEmails ??
contentAttributes.value?.email?.cc ??
[]
);
});
const senderName = computed(() => {
return props.sender.name ?? '';
return sender.value.name ?? '';
});
const bccEmail = computed(() => {
return (
props.contentAttributes?.bccEmails ??
props.contentAttributes?.email?.bcc ??
contentAttributes.value?.bccEmails ??
contentAttributes.value?.email?.bcc ??
[]
);
});
const subject = computed(() => {
return props.contentAttributes?.email?.subject ?? '';
return contentAttributes.value?.email?.subject ?? '';
});
const showMeta = computed(() => {

View File

@@ -7,37 +7,13 @@ import { EmailQuoteExtractor } from './removeReply.js';
import BaseBubble from 'next/message/bubbles/Base.vue';
import FormattedContent from 'next/message/bubbles/Text/FormattedContent.vue';
import AttachmentChips from 'next/message/chips/AttachmentChips.vue';
import EmailMeta from './EmailMeta.vue';
import { MESSAGE_STATUS, MESSAGE_TYPES } from '../../constants';
const props = defineProps({
content: {
type: String,
required: true,
},
contentAttributes: {
type: Object,
default: () => ({}),
},
attachments: {
type: Array,
default: () => [],
},
status: {
type: String,
required: true,
validator: value => Object.values(MESSAGE_STATUS).includes(value),
},
sender: {
type: Object,
default: () => ({}),
},
messageType: {
type: Number,
required: true,
},
});
import { useMessageContext } from '../../provider.js';
import { MESSAGE_TYPES } from 'next/message/constants.js';
const { content, contentAttributes, attachments, messageType } =
useMessageContext();
const isExpandable = ref(false);
const isExpanded = ref(false);
@@ -49,11 +25,11 @@ onMounted(() => {
});
const isOutgoing = computed(() => {
return props.messageType === MESSAGE_TYPES.OUTGOING;
return messageType.value === MESSAGE_TYPES.OUTGOING;
});
const fullHTML = computed(() => {
return props.contentAttributes?.email?.htmlContent?.full ?? props.content;
return contentAttributes?.value?.email?.htmlContent?.full ?? content.value;
});
const unquotedHTML = computed(() => {
@@ -66,14 +42,14 @@ const hasQuotedMessage = computed(() => {
const textToShow = computed(() => {
const text =
props.contentAttributes?.email?.textContent?.full ?? props.content;
return text.replace(/\n/g, '<br>');
contentAttributes?.value?.email?.textContent?.full ?? content.value;
return text?.replace(/\n/g, '<br>');
});
</script>
<template>
<BaseBubble class="w-full overflow-hidden" data-bubble-name="email">
<EmailMeta :status :sender :content-attributes />
<EmailMeta />
<section
ref="contentContainer"
class="p-4"

View File

@@ -2,43 +2,16 @@
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useMessageContext } from '../provider.js';
import BaseAttachmentBubble from './BaseAttachment.vue';
import FileIcon from 'next/icon/FileIcon.vue';
/**
* @typedef {Object} Attachment
* @property {number} id - Unique identifier for the attachment
* @property {number} messageId - ID of the associated message
* @property {'image'|'audio'|'video'|'file'|'location'|'fallback'|'share'|'story_mention'|'contact'|'ig_reel'} fileType - Type of the attachment (file or image)
* @property {number} accountId - ID of the associated account
* @property {string|null} extension - File extension
* @property {string} dataUrl - URL to access the full attachment data
* @property {string} thumbUrl - URL to access the thumbnail version
* @property {number} fileSize - Size of the file in bytes
* @property {number|null} width - Width of the image if applicable
* @property {number|null} height - Height of the image if applicable
*/
/**
* @typedef {Object} Props
* @property {Attachment[]} [attachments=[]] - The attachments associated with the message
*/
const props = defineProps({
attachments: {
type: Array,
required: true,
},
sender: {
type: Object,
default: () => ({}),
},
});
const { attachments } = useMessageContext();
const { t } = useI18n();
const url = computed(() => {
return props.attachments[0].dataUrl;
return attachments.value[0].dataUrl;
});
const fileName = computed(() => {
@@ -58,7 +31,6 @@ const fileType = computed(() => {
<BaseAttachmentBubble
icon="i-teenyicons-user-circle-solid"
icon-bg-color="bg-n-alpha-3 dark:bg-n-alpha-white"
:sender="sender"
sender-translation-key="CONVERSATION.SHARED_ATTACHMENT.FILE"
:content="decodeURI(fileName)"
:action="{

View File

@@ -4,44 +4,18 @@ import BaseBubble from './Base.vue';
import Button from 'next/button/Button.vue';
import Icon from 'next/icon/Icon.vue';
import { useSnakeCase } from 'dashboard/composables/useTransformKeys';
import { useMessageContext } from 'next/message/provider.js';
import { useMessageContext } from '../provider.js';
import GalleryView from 'dashboard/components/widgets/conversation/components/GalleryView.vue';
/**
* @typedef {Object} Attachment
* @property {number} id - Unique identifier for the attachment
* @property {number} messageId - ID of the associated message
* @property {'image'|'audio'|'video'|'file'|'location'|'fallback'|'share'|'story_mention'|'contact'|'ig_reel'} fileType - Type of the attachment (file or image)
* @property {number} accountId - ID of the associated account
* @property {string|null} extension - File extension
* @property {string} dataUrl - URL to access the full attachment data
* @property {string} thumbUrl - URL to access the thumbnail version
* @property {number} fileSize - Size of the file in bytes
* @property {number|null} width - Width of the image if applicable
* @property {number|null} height - Height of the image if applicable
*/
/**
* @typedef {Object} Props
* @property {Attachment[]} [attachments=[]] - The attachments associated with the message
*/
const props = defineProps({
attachments: {
type: Array,
required: true,
},
});
const emit = defineEmits(['error']);
const { filteredCurrentChatAttachments, attachments } = useMessageContext();
const attachment = computed(() => {
return props.attachments[0];
return attachments.value[0];
});
const hasError = ref(false);
const showGallery = ref(false);
const { filteredCurrentChatAttachments } = useMessageContext();
const handleError = () => {
hasError.value = true;
@@ -64,20 +38,17 @@ const downloadAttachment = async () => {
<template>
<BaseBubble
class="overflow-hidden relative group border-[4px] border-n-weak"
class="overflow-hidden p-3"
data-bubble-name="image"
@click="showGallery = true"
>
<div
v-if="hasError"
class="flex items-center gap-1 px-5 py-4 text-center rounded-lg bg-n-alpha-1"
>
<div v-if="hasError" class="flex items-center gap-1 text-center rounded-lg">
<Icon icon="i-lucide-circle-off" class="text-n-slate-11" />
<p class="mb-0 text-n-slate-11">
{{ $t('COMPONENTS.MEDIA.IMAGE_UNAVAILABLE') }}
</p>
</div>
<template v-else>
<div v-else class="relative group rounded-lg overflow-hidden">
<img
:src="attachment.dataUrl"
:width="attachment.width"
@@ -98,7 +69,7 @@ const downloadAttachment = async () => {
@click="downloadAttachment"
/>
</div>
</template>
</div>
</BaseBubble>
<GalleryView
v-if="showGallery"

View File

@@ -7,46 +7,22 @@ import BaseBubble from 'next/message/bubbles/Base.vue';
import MessageFormatter from 'shared/helpers/MessageFormatter.js';
import { MESSAGE_VARIANTS } from '../constants';
/**
* @typedef {Object} Attachment
* @property {number} id - Unique identifier for the attachment
* @property {number} messageId - ID of the associated message
* @property {'image'|'audio'|'video'|'file'|'location'|'fallback'|'share'|'story_mention'|'contact'|'ig_reel'} fileType - Type of the attachment (file or image)
* @property {number} accountId - ID of the associated account
* @property {string|null} extension - File extension
* @property {string} dataUrl - URL to access the full attachment data
* @property {string} thumbUrl - URL to access the thumbnail version
* @property {number} fileSize - Size of the file in bytes
* @property {number|null} width - Width of the image if applicable
* @property {number|null} height - Height of the image if applicable
*/
const props = defineProps({
content: {
type: String,
required: true,
},
attachments: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(['error']);
const { variant, content, attachments } = useMessageContext();
const attachment = computed(() => {
return props.attachments[0];
return attachments.value[0];
});
const { variant } = useMessageContext();
const hasImgStoryError = ref(false);
const hasVideoStoryError = ref(false);
const formattedContent = computed(() => {
if (variant.value === MESSAGE_VARIANTS.ACTIVITY) {
return props.content;
return content.value;
}
return new MessageFormatter(props.content).formattedMessage;
return new MessageFormatter(content.value).formattedMessage;
});
const onImageLoadError = () => {

View File

@@ -1,43 +1,14 @@
<script setup>
import { computed, onMounted, nextTick, useTemplateRef } from 'vue';
import { computed } from 'vue';
import BaseAttachmentBubble from './BaseAttachment.vue';
import { useI18n } from 'vue-i18n';
import maplibregl from 'maplibre-gl';
/**
* @typedef {Object} Attachment
* @property {number} id - Unique identifier for the attachment
* @property {number} messageId - ID of the associated message
* @property {'image'|'audio'|'video'|'file'|'location'|'fallback'|'share'|'story_mention'|'contact'|'ig_reel'} fileType - Type of the attachment (file or image)
* @property {number} accountId - ID of the associated account
* @property {string|null} extension - File extension
* @property {string} dataUrl - URL to access the full attachment data
* @property {string} thumbUrl - URL to access the thumbnail version
* @property {number} fileSize - Size of the file in bytes
* @property {number|null} width - Width of the image if applicable
* @property {number|null} height - Height of the image if applicable
*/
/**
* @typedef {Object} Props
* @property {Attachment[]} [attachments=[]] - The attachments associated with the message
*/
const props = defineProps({
attachments: {
type: Array,
required: true,
},
sender: {
type: Object,
default: () => ({}),
},
});
import { useMessageContext } from '../provider.js';
const { attachments } = useMessageContext();
const { t } = useI18n();
const attachment = computed(() => {
return props.attachments[0];
return attachments.value[0];
});
const lat = computed(() => {
@@ -48,61 +19,23 @@ const long = computed(() => {
});
const title = computed(() => {
return attachment.value.fallbackTitle;
return attachment.value.fallbackTitle ?? attachment.value.fallback_title;
});
const mapUrl = computed(
() => `https://maps.google.com/?q=${lat.value},${long.value}`
);
const mapContainer = useTemplateRef('mapContainer');
const setupMap = () => {
const map = new maplibregl.Map({
style: 'https://tiles.openfreemap.org/styles/positron',
center: [long.value, lat.value],
zoom: 9.5,
container: mapContainer.value,
attributionControl: false,
dragPan: false,
dragRotate: false,
scrollZoom: false,
touchZoom: false,
touchRotate: false,
keyboard: false,
doubleClickZoom: false,
});
return map;
};
onMounted(async () => {
await nextTick();
setupMap();
});
</script>
<template>
<BaseAttachmentBubble
icon="i-ph-navigation-arrow-fill"
icon-bg-color="bg-[#0D9B8A]"
:sender="sender"
sender-translation-key="CONVERSATION.SHARED_ATTACHMENT.LOCATION"
:content="title"
:action="{
label: t('COMPONENTS.LOCATION_BUBBLE.SEE_ON_MAP'),
href: mapUrl,
}"
>
<template #before>
<div
ref="mapContainer"
class="z-10 w-full max-w-md -mb-12 min-w-64 h-28"
/>
</template>
</BaseAttachmentBubble>
/>
</template>
<style>
@import 'maplibre-gl/dist/maplibre-gl.css';
</style>

View File

@@ -4,47 +4,25 @@ import BaseBubble from 'next/message/bubbles/Base.vue';
import FormattedContent from './FormattedContent.vue';
import AttachmentChips from 'next/message/chips/AttachmentChips.vue';
import { MESSAGE_TYPES } from '../../constants';
import { useMessageContext } from '../../provider.js';
/**
* @typedef {Object} Attachment
* @property {number} id - Unique identifier for the attachment
* @property {number} messageId - ID of the associated message
* @property {'image'|'audio'|'video'|'file'|'location'|'fallback'|'share'|'story_mention'|'contact'|'ig_reel'} fileType - Type of the attachment (file or image)
* @property {number} accountId - ID of the associated account
* @property {string|null} extension - File extension
* @property {string} dataUrl - URL to access the full attachment data
* @property {string} thumbUrl - URL to access the thumbnail version
* @property {number} fileSize - Size of the file in bytes
* @property {number|null} width - Width of the image if applicable
* @property {number|null} height - Height of the image if applicable
*/
const props = defineProps({
content: {
type: String,
required: true,
},
attachments: {
type: Array,
default: () => [],
},
contentAttributes: {
type: Object,
default: () => ({}),
},
messageType: {
type: Number,
required: true,
validator: value => Object.values(MESSAGE_TYPES).includes(value),
},
});
const { content, attachments, contentAttributes, messageType } =
useMessageContext();
const isTemplate = computed(() => {
return props.messageType === MESSAGE_TYPES.TEMPLATE;
return messageType.value === MESSAGE_TYPES.TEMPLATE;
});
const isEmpty = computed(() => {
return !content.value && !attachments.value.length;
});
</script>
<template>
<BaseBubble class="flex flex-col gap-3 px-4 py-3" data-bubble-name="text">
<span v-if="isEmpty" class="text-n-slate-11">
{{ $t('CONVERSATION.NO_CONTENT') }}
</span>
<FormattedContent v-if="content" :content="content" />
<AttachmentChips :attachments="attachments" class="gap-2" />
<template v-if="isTemplate">

View File

@@ -3,39 +3,14 @@ import { ref, computed } from 'vue';
import BaseBubble from './Base.vue';
import Icon from 'next/icon/Icon.vue';
import { useSnakeCase } from 'dashboard/composables/useTransformKeys';
import { useMessageContext } from 'next/message/provider.js';
import { useMessageContext } from '../provider.js';
import GalleryView from 'dashboard/components/widgets/conversation/components/GalleryView.vue';
import { ATTACHMENT_TYPES } from '../constants';
/**
* @typedef {Object} Attachment
* @property {number} id - Unique identifier for the attachment
* @property {number} messageId - ID of the associated message
* @property {'image'|'audio'|'video'|'file'|'location'|'fallback'|'share'|'story_mention'|'contact'|'ig_reel'} fileType - Type of the attachment (file or image)
* @property {number} accountId - ID of the associated account
* @property {string|null} extension - File extension
* @property {string} dataUrl - URL to access the full attachment data
* @property {string} thumbUrl - URL to access the thumbnail version
* @property {number} fileSize - Size of the file in bytes
* @property {number|null} width - Width of the image if applicable
* @property {number|null} height - Height of the image if applicable
*/
/**
* @typedef {Object} Props
* @property {Attachment[]} [attachments=[]] - The attachments associated with the message
*/
const props = defineProps({
attachments: {
type: Array,
required: true,
},
});
const emit = defineEmits(['error']);
const hasError = ref(false);
const showGallery = ref(false);
const { filteredCurrentChatAttachments } = useMessageContext();
const { filteredCurrentChatAttachments, attachments } = useMessageContext();
const handleError = () => {
hasError.value = true;
@@ -43,7 +18,7 @@ const handleError = () => {
};
const attachment = computed(() => {
return props.attachments[0];
return attachments.value[0];
});
const isReel = computed(() => {
@@ -53,25 +28,28 @@ const isReel = computed(() => {
<template>
<BaseBubble
class="overflow-hidden relative group border-[4px] border-n-weak"
class="overflow-hidden p-3"
data-bubble-name="video"
@click="showGallery = true"
>
<div
v-if="isReel"
class="absolute p-2 flex items-start justify-end size-12 bg-gradient-to-bl from-n-alpha-black1 to-transparent right-0"
>
<Icon icon="i-lucide-instagram" class="text-white" />
<div class="relative group rounded-lg overflow-hidden">
<div
v-if="isReel"
class="absolute p-2 flex items-start justify-end right-0"
>
<Icon icon="i-lucide-instagram" class="text-white shadow-lg" />
</div>
<video
controls
class="rounded-lg"
:src="attachment.dataUrl"
:class="{
'max-w-48': isReel,
'max-w-full': !isReel,
}"
@error="handleError"
/>
</div>
<video
controls
:src="attachment.dataUrl"
:class="{
'max-w-48': isReel,
'max-w-full': !isReel,
}"
@error="handleError"
/>
</BaseBubble>
<GalleryView
v-if="showGallery"

View File

@@ -53,7 +53,7 @@ const textColorClass = computed(() => {
<template>
<div
class="h-9 bg-n-alpha-white gap-2 items-center flex px-2 rounded-lg border border-n-strong"
class="h-9 bg-n-alpha-white gap-2 items-center flex px-2 rounded-lg border border-n-container"
>
<FileIcon class="flex-shrink-0" :file-type="fileType" />
<span class="mr-1 max-w-32 truncate" :class="textColorClass">

View File

@@ -5,6 +5,112 @@ import { ATTACHMENT_TYPES } from './constants';
const MessageControl = Symbol('MessageControl');
/**
* @typedef {Object} Attachment
* @property {number} id - Unique identifier for the attachment
* @property {number} messageId - ID of the associated message
* @property {'image'|'audio'|'video'|'file'|'location'|'fallback'|'share'|'story_mention'|'contact'|'ig_reel'} fileType - Type of the attachment (file or image)
* @property {number} accountId - ID of the associated account
* @property {string|null} extension - File extension
* @property {string} dataUrl - URL to access the full attachment data
* @property {string} thumbUrl - URL to access the thumbnail version
* @property {number} fileSize - Size of the file in bytes
* @property {number|null} width - Width of the image if applicable
* @property {number|null} height - Height of the image if applicable
*/
/**
* @typedef {Object} Sender
* @property {Object} additional_attributes - Additional attributes of the sender
* @property {Object} custom_attributes - Custom attributes of the sender
* @property {string} email - Email of the sender
* @property {number} id - ID of the sender
* @property {string|null} identifier - Identifier of the sender
* @property {string} name - Name of the sender
* @property {string|null} phone_number - Phone number of the sender
* @property {string} thumbnail - Thumbnail URL of the sender
* @property {string} type - Type of sender
*/
/**
* @typedef {Object} EmailContent
* @property {string[]|null} bcc - BCC recipients
* @property {string[]|null} cc - CC recipients
* @property {string} contentType - Content type of the email
* @property {string} date - Date the email was sent
* @property {string[]} from - From email address
* @property {Object} htmlContent - HTML content of the email
* @property {string} htmlContent.full - Full HTML content
* @property {string} htmlContent.reply - Reply HTML content
* @property {string} htmlContent.quoted - Quoted HTML content
* @property {string|null} inReplyTo - Message ID being replied to
* @property {string} messageId - Unique message identifier
* @property {boolean} multipart - Whether the email is multipart
* @property {number} numberOfAttachments - Number of attachments
* @property {string} subject - Email subject line
* @property {Object} textContent - Text content of the email
* @property {string} textContent.full - Full text content
* @property {string} textContent.reply - Reply text content
* @property {string} textContent.quoted - Quoted text content
* @property {string[]} to - To email addresses
*/
/**
* @typedef {Object} ContentAttributes
* @property {string} externalError - an error message to be shown if the message failed to send
* @property {Object} [data] - Optional data object containing roomName and messageId
* @property {string} data.roomName - Name of the room
* @property {string} data.messageId - ID of the message
* @property {'story_mention'} [imageType] - Flag to indicate this is a story mention
* @property {'dyte'} [type] - Flag to indicate this is a dyte call
* @property {EmailContent} [email] - Email content and metadata
* @property {string|null} [ccEmail] - CC email addresses
* @property {string|null} [bccEmail] - BCC email addresses
*/
/**
* @typedef {'sent'|'delivered'|'read'|'failed'|'progress'} MessageStatus
* @typedef {'text'|'input_text'|'input_textarea'|'input_email'|'input_select'|'cards'|'form'|'article'|'incoming_email'|'input_csat'|'integrations'|'sticker'} MessageContentType
* @typedef {0|1|2|3} MessageType
* @typedef {'contact'|'user'|'Contact'|'User'} SenderType
* @typedef {'user'|'agent'|'activity'|'private'|'bot'|'error'|'template'|'email'|'unsupported'} MessageVariant
* @typedef {'left'|'center'|'right'} MessageOrientation
* @typedef {Object} MessageContext
* @property {import('vue').Ref<string>} content - The message content
* @property {import('vue').Ref<number>} conversationId - The ID of the conversation to which the message belongs
* @property {import('vue').Ref<number>} createdAt - Timestamp when the message was created
* @property {import('vue').Ref<number>} currentUserId - The ID of the current user
* @property {import('vue').Ref<number>} id - The unique identifier for the message
* @property {import('vue').Ref<number>} inboxId - The ID of the inbox to which the message belongs
* @property {import('vue').Ref<boolean>} [groupWithNext=false] - Whether the message should be grouped with the next message
* @property {import('vue').Ref<boolean>} [isEmailInbox=false] - Whether the message is from an email inbox
* @property {import('vue').Ref<boolean>} [private=false] - Whether the message is private
* @property {import('vue').Ref<number|null>} [senderId=null] - The ID of the sender
* @property {import('vue').Ref<string|null>} [error=null] - Error message if the message failed to send
* @property {import('vue').Ref<Attachment[]>} [attachments=[]] - The attachments associated with the message
* @property {import('vue').Ref<ContentAttributes>} [contentAttributes={}] - Additional attributes of the message content
* @property {import('vue').Ref<MessageContentType>} contentType - Content type of the message
* @property {import('vue').Ref<MessageStatus>} status - The delivery status of the message
* @property {import('vue').Ref<MessageType>} messageType - The type of message (must be one of MESSAGE_TYPES)
* @property {import('vue').Ref<Object|null>} [inReplyTo=null] - The message to which this message is a reply
* @property {import('vue').Ref<SenderType>} [senderType=null] - The type of the sender
* @property {import('vue').Ref<Sender|null>} [sender=null] - The sender information
* @property {import('vue').ComputedRef<MessageOrientation>} orientation - The visual variant of the message
* @property {import('vue').ComputedRef<MessageVariant>} variant - The visual variant of the message
* @property {import('vue').ComputedRef<boolean>} isMyMessage - Does the message belong to the current user
* @property {import('vue').ComputedRef<boolean>} isPrivate - Proxy computed value for private
* @property {import('vue').ComputedRef<boolean>} shouldGroupWithNext - Should group with the next message or not, it is differnt from groupWithNext, this has a bypass for a failed message
*/
/**
* Retrieves the message context from the parent Message component.
* Must be used within a component that is a child of a Message component.
*
* @returns {MessageContext & { filteredCurrentChatAttachments: import('vue').ComputedRef<Attachment[]> }}
* Message context object containing message properties and computed values
* @throws {Error} If used outside of a Message component context
*/
export function useMessageContext() {
const context = inject(MessageControl, null);
if (context === null) {

View File

@@ -8,6 +8,7 @@ import { useAI } from 'dashboard/composables/useAI';
// components
import ReplyBox from './ReplyBox.vue';
import Message from './Message.vue';
import NextMessageList from 'next/message/MessageList.vue';
import ConversationLabelSuggestion from './conversation/LabelSuggestion.vue';
import Banner from 'dashboard/components/ui/Banner.vue';
@@ -37,6 +38,7 @@ import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
export default {
components: {
Message,
NextMessageList,
ReplyBox,
Banner,
ConversationLabelSuggestion,
@@ -80,6 +82,10 @@ export default {
fetchLabelSuggestions,
} = useAI();
const showNextBubbles = LocalStorage.get(
LOCAL_STORAGE_KEYS.USE_NEXT_BUBBLE
);
return {
isEnterprise,
isPopOutReplyBox,
@@ -89,6 +95,7 @@ export default {
isLabelSuggestionFeatureEnabled,
fetchIntegrationsIfRequired,
fetchLabelSuggestions,
showNextBubbles,
};
},
data() {
@@ -106,6 +113,7 @@ export default {
computed: {
...mapGetters({
currentChat: 'getSelectedChat',
currentUserId: 'getCurrentUserID',
listLoadingStatus: 'getAllMessagesLoaded',
currentAccountId: 'getCurrentAccountId',
}),
@@ -436,7 +444,6 @@ export default {
makeMessagesRead() {
this.$store.dispatch('markMessagesRead', { id: this.currentChat.id });
},
getInReplyToMessage(parentMessage) {
if (!parentMessage) return {};
const inReplyToMessageId = parentMessage.content_attributes?.in_reply_to;
@@ -473,7 +480,46 @@ export default {
@click="onToggleContactPanel"
/>
</div>
<ul class="conversation-panel">
<NextMessageList
v-if="showNextBubbles"
class="conversation-panel"
:read-messages="readMessages"
:un-read-messages="unReadMessages"
:current-user-id="currentUserId"
:is-an-email-channel="isAnEmailChannel"
:inbox-supports-reply-to="inboxSupportsReplyTo"
:messages="currentChat ? currentChat.messages : []"
>
<template #beforeAll>
<transition name="slide-up">
<!-- eslint-disable-next-line vue/require-toggle-inside-transition -->
<li class="min-h-[4rem]">
<span v-if="shouldShowSpinner" class="spinner message" />
</li>
</transition>
</template>
<template #beforeUnread>
<li v-show="unreadMessageCount != 0" class="unread--toast">
<span>
{{ unreadMessageCount > 9 ? '9+' : unreadMessageCount }}
{{
unreadMessageCount > 1
? $t('CONVERSATION.UNREAD_MESSAGES')
: $t('CONVERSATION.UNREAD_MESSAGE')
}}
</span>
</li>
</template>
<template #after>
<ConversationLabelSuggestion
v-if="shouldShowLabelSuggestions"
:suggested-labels="labelSuggestions"
:chat-labels="currentChat.labels"
:conversation-id="currentChat.id"
/>
</template>
</NextMessageList>
<ul v-else class="conversation-panel">
<transition name="slide-up">
<!-- eslint-disable-next-line vue/require-toggle-inside-transition -->
<li class="min-h-[4rem]">
@@ -556,7 +602,6 @@ export default {
<style scoped>
@tailwind components;
@layer components {
.rounded-bl-calc {
border-bottom-left-radius: calc(1.5rem + 1px);

View File

@@ -0,0 +1,141 @@
import { computed } from 'vue';
import { useMapGetter } from 'dashboard/composables/store';
import { useCamelCase } from 'dashboard/composables/useTransformKeys';
import { INBOX_TYPES } from 'dashboard/helper/inbox';
export const INBOX_FEATURES = {
REPLY_TO: 'replyTo',
REPLY_TO_OUTGOING: 'replyToOutgoing',
};
// This is a single source of truth for inbox features
// This is used to check if a feature is available for a particular inbox or not
export const INBOX_FEATURE_MAP = {
[INBOX_FEATURES.REPLY_TO]: [
INBOX_TYPES.FB,
INBOX_TYPES.WEB,
INBOX_TYPES.TWITTER,
INBOX_TYPES.WHATSAPP,
INBOX_TYPES.TELEGRAM,
INBOX_TYPES.API,
],
[INBOX_FEATURES.REPLY_TO_OUTGOING]: [
INBOX_TYPES.WEB,
INBOX_TYPES.TWITTER,
INBOX_TYPES.WHATSAPP,
INBOX_TYPES.TELEGRAM,
INBOX_TYPES.API,
],
};
/**
* Composable for handling macro-related functionality
* @returns {Object} An object containing the getMacroDropdownValues function
*/
export const useInbox = () => {
const currentChat = useMapGetter('getSelectedChat');
const inboxGetter = useMapGetter('inboxes/getInboxById');
const inbox = computed(() => {
const inboxId = currentChat.value.inbox_id;
return useCamelCase(inboxGetter.value(inboxId), { deep: true });
});
const channelType = computed(() => {
return inbox.value.channelType;
});
const isAPIInbox = computed(() => {
return channelType.value === INBOX_TYPES.API;
});
const isAFacebookInbox = computed(() => {
return channelType.value === INBOX_TYPES.FB;
});
const isAWebWidgetInbox = computed(() => {
return channelType.value === INBOX_TYPES.WEB;
});
const isATwilioChannel = computed(() => {
return channelType.value === INBOX_TYPES.TWILIO;
});
const isALineChannel = computed(() => {
return channelType.value === INBOX_TYPES.LINE;
});
const isAnEmailChannel = computed(() => {
return channelType.value === INBOX_TYPES.EMAIL;
});
const isATelegramChannel = computed(() => {
return channelType.value === INBOX_TYPES.TELEGRAM;
});
const whatsAppAPIProvider = computed(() => {
return inbox.value.provider || '';
});
const isAMicrosoftInbox = computed(() => {
return isAnEmailChannel.value && inbox.value.provider === 'microsoft';
});
const isAGoogleInbox = computed(() => {
return isAnEmailChannel.value && inbox.value.provider === 'google';
});
const isATwilioSMSChannel = computed(() => {
const { medium: medium = '' } = inbox.value;
return isATwilioChannel.value && medium === 'sms';
});
const isASmsInbox = computed(() => {
return channelType.value === INBOX_TYPES.SMS || isATwilioSMSChannel.value;
});
const isATwilioWhatsAppChannel = computed(() => {
const { medium: medium = '' } = inbox.value;
return isATwilioChannel.value && medium === 'whatsapp';
});
const isAWhatsAppCloudChannel = computed(() => {
return (
channelType.value === INBOX_TYPES.WHATSAPP &&
whatsAppAPIProvider.value === 'whatsapp_cloud'
);
});
const is360DialogWhatsAppChannel = computed(() => {
return (
channelType.value === INBOX_TYPES.WHATSAPP &&
whatsAppAPIProvider.value === 'default'
);
});
const isAWhatsAppChannel = computed(() => {
return (
channelType.value === INBOX_TYPES.WHATSAPP ||
isATwilioWhatsAppChannel.value
);
});
return {
inbox,
isAFacebookInbox,
isALineChannel,
isAPIInbox,
isASmsInbox,
isATelegramChannel,
isATwilioChannel,
isAWebWidgetInbox,
isAWhatsAppChannel,
isAMicrosoftInbox,
isAGoogleInbox,
isATwilioWhatsAppChannel,
isAWhatsAppCloudChannel,
is360DialogWhatsAppChannel,
isAnEmailChannel,
};
};

View File

@@ -3,6 +3,7 @@
import { unref } from 'vue';
import camelcaseKeys from 'camelcase-keys';
import snakecaseKeys from 'snakecase-keys';
import * as Sentry from '@sentry/vue';
/**
* Vue composable that converts object keys to camelCase
@@ -12,8 +13,18 @@ import snakecaseKeys from 'snakecase-keys';
* @returns {Object|Array} Converted payload with camelCase keys
*/
export function useCamelCase(payload, options) {
const unrefPayload = unref(payload);
return camelcaseKeys(unrefPayload, options);
try {
const unrefPayload = unref(payload);
return camelcaseKeys(unrefPayload, options);
} catch (e) {
Sentry.setContext('transform-keys-error', {
payload,
options,
op: 'camelCase',
});
Sentry.captureException(e);
return payload;
}
}
/**
@@ -24,6 +35,16 @@ export function useCamelCase(payload, options) {
* @returns {Object|Array} Converted payload with snake_case keys
*/
export function useSnakeCase(payload, options) {
const unrefPayload = unref(payload);
return snakecaseKeys(unrefPayload, options);
try {
const unrefPayload = unref(payload);
return snakecaseKeys(unrefPayload, options);
} catch (e) {
Sentry.setContext('transform-keys-error', {
payload,
options,
op: 'snakeCase',
});
Sentry.captureException(e);
return payload;
}
}

View File

@@ -5,4 +5,5 @@ export const LOCAL_STORAGE_KEYS = {
COLOR_SCHEME: 'color_scheme',
DISMISSED_LABEL_SUGGESTIONS: 'labelSuggestionsDismissed',
MESSAGE_REPLY_TO: 'messageReplyTo',
USE_NEXT_BUBBLE: 'useNextBubble',
};

View File

@@ -39,6 +39,7 @@
"DOWNLOAD": "Download",
"UNKNOWN_FILE_TYPE": "Unknown File",
"SAVE_CONTACT": "Save Contact",
"NO_CONTENT": "No content to display",
"SHARED_ATTACHMENT": {
"CONTACT": "{sender} has shared a contact",
"LOCATION": "{sender} has shared a location",

View File

@@ -4,6 +4,7 @@ import { mapGetters } from 'vuex';
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
import ContextMenu from 'dashboard/components/ui/ContextMenu.vue';
import AddCannedModal from 'dashboard/routes/dashboard/settings/canned/AddCanned.vue';
import { useSnakeCase } from 'dashboard/composables/useTransformKeys';
import { copyTextToClipboard } from 'shared/helpers/clipboard';
import { conversationUrl, frontendURL } from '../../../helper/URLHelper';
import {
@@ -38,6 +39,10 @@ export default {
type: Object,
default: () => ({}),
},
hideButton: {
type: Boolean,
default: false,
},
},
emits: ['open', 'close', 'replyTo'],
setup() {
@@ -62,7 +67,7 @@ export default {
return this.getPlainText(this.messageContent);
},
conversationId() {
return this.message.conversation_id;
return this.message.conversation_id ?? this.message.conversationId;
},
messageId() {
return this.message.id;
@@ -71,7 +76,9 @@ export default {
return this.message.content;
},
contentAttributes() {
return this.message.content_attributes;
return useSnakeCase(
this.message.content_attributes ?? this.message.contentAttributes
);
},
},
methods: {
@@ -183,6 +190,7 @@ export default {
:reject-text="$t('CONVERSATION.CONTEXT_MENU.DELETE_CONFIRMATION.CANCEL')"
/>
<woot-button
v-if="!hideButton"
icon="more-vertical"
color-scheme="secondary"
variant="clear"

View File

@@ -72,7 +72,6 @@
"idb": "^8.0.0",
"js-cookie": "^3.0.5",
"libphonenumber-js": "^1.11.9",
"maplibre-gl": "^4.7.1",
"markdown-it": "^13.0.2",
"markdown-it-link-attributes": "^4.0.1",
"md5": "^2.3.0",

259
pnpm-lock.yaml generated
View File

@@ -139,9 +139,6 @@ importers:
libphonenumber-js:
specifier: ^1.11.9
version: 1.11.9
maplibre-gl:
specifier: ^4.7.1
version: 4.7.1
markdown-it:
specifier: ^13.0.2
version: 13.0.2
@@ -994,34 +991,6 @@ packages:
resolution: {integrity: sha512-dUz8OmYvlY5A9wXaroHIMSPASpSYRLCqbPvxGSyHguhtTQIy24lC+EGxQlwv71AhRCO55WOtgwhzQLpw27JaJQ==}
engines: {node: '>=8'}
'@mapbox/geojson-rewind@0.5.2':
resolution: {integrity: sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==}
hasBin: true
'@mapbox/jsonlint-lines-primitives@2.0.2':
resolution: {integrity: sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==}
engines: {node: '>= 0.6'}
'@mapbox/point-geometry@0.1.0':
resolution: {integrity: sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==}
'@mapbox/tiny-sdf@2.0.6':
resolution: {integrity: sha512-qMqa27TLw+ZQz5Jk+RcwZGH7BQf5G/TrutJhspsca/3SHwmgKQ1iq+d3Jxz5oysPVYTGP6aXxCo5Lk9Er6YBAA==}
'@mapbox/unitbezier@0.0.1':
resolution: {integrity: sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==}
'@mapbox/vector-tile@1.3.1':
resolution: {integrity: sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==}
'@mapbox/whoots-js@3.1.0':
resolution: {integrity: sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==}
engines: {node: '>=6.0.0'}
'@maplibre/maplibre-gl-style-spec@20.4.0':
resolution: {integrity: sha512-AzBy3095fTFPjDjmWpR2w6HVRAZJ6hQZUCwk5Plz6EyfnfuQW1odeW5i2Ai47Y6TBA2hQnC+azscjBSALpaWgw==}
hasBin: true
'@material/mwc-icon@0.25.3':
resolution: {integrity: sha512-36076AWZIRSr8qYOLjuDDkxej/HA0XAosrj7TS1ZeLlUBnLUtbDtvc1S7KSa0hqez7ouzOqGaWK24yoNnTa2OA==}
deprecated: MWC beta is longer supported. Please upgrade to @material/web
@@ -1771,24 +1740,12 @@ packages:
'@types/fs-extra@9.0.13':
resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==}
'@types/geojson-vt@3.2.5':
resolution: {integrity: sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==}
'@types/geojson@7946.0.14':
resolution: {integrity: sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==}
'@types/json5@0.0.29':
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
'@types/linkify-it@5.0.0':
resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
'@types/mapbox__point-geometry@0.1.4':
resolution: {integrity: sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==}
'@types/mapbox__vector-tile@1.3.4':
resolution: {integrity: sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg==}
'@types/markdown-it@12.2.3':
resolution: {integrity: sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==}
@@ -1798,12 +1755,6 @@ packages:
'@types/node@22.7.0':
resolution: {integrity: sha512-MOdOibwBs6KW1vfqz2uKMlxq5xAfAZ98SZjO8e3XnAbFnTJtAspqhWk7hrdSAs9/Y14ZWMiy7/MxMUzAOadYEw==}
'@types/pbf@3.0.5':
resolution: {integrity: sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==}
'@types/supercluster@7.1.3':
resolution: {integrity: sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==}
'@types/trusted-types@2.0.2':
resolution: {integrity: sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==}
@@ -2609,9 +2560,6 @@ packages:
resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==}
engines: {node: '>=4'}
earcut@3.0.0:
resolution: {integrity: sha512-41Fs7Q/PLq1SDbqjsgcY7GA42T0jvaCNGXgGtsNdvg+Yv8eIu06bxv4/PoREkZ9nMDNwnUSG9OFB9+yv8eKhDg==}
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
@@ -2982,9 +2930,6 @@ packages:
functions-have-names@1.2.3:
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
geojson-vt@4.0.2:
resolution: {integrity: sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==}
get-caller-file@2.0.5:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
@@ -3016,9 +2961,6 @@ packages:
resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==}
engines: {node: '>= 0.4'}
gl-matrix@3.4.3:
resolution: {integrity: sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==}
glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
@@ -3039,10 +2981,6 @@ packages:
resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==}
engines: {node: '>=18'}
global-prefix@4.0.0:
resolution: {integrity: sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA==}
engines: {node: '>=16'}
global@4.4.0:
resolution: {integrity: sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==}
@@ -3182,9 +3120,6 @@ packages:
idb@8.0.0:
resolution: {integrity: sha512-l//qvlAKGmQO31Qn7xdzagVPPaHTxXx199MhrAFuVBTPqydcPYBWjkrbv4Y0ktB+GmWOiwHl237UUOrLmQxLvw==}
ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
ignore@5.2.4:
resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==}
engines: {node: '>= 4'}
@@ -3217,10 +3152,6 @@ packages:
resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
ini@4.1.3:
resolution: {integrity: sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
internal-slot@1.0.5:
resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==}
engines: {node: '>= 0.4'}
@@ -3372,10 +3303,6 @@ packages:
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
isexe@3.1.1:
resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==}
engines: {node: '>=16'}
istanbul-lib-coverage@3.2.2:
resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
engines: {node: '>=8'}
@@ -3453,9 +3380,6 @@ packages:
json-stable-stringify-without-jsonify@1.0.1:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
json-stringify-pretty-compact@4.0.0:
resolution: {integrity: sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==}
json5@1.0.2:
resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==}
hasBin: true
@@ -3463,9 +3387,6 @@ packages:
jsonfile@6.1.0:
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
kdbush@4.0.2:
resolution: {integrity: sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==}
keycode@2.2.1:
resolution: {integrity: sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg==}
@@ -3619,10 +3540,6 @@ packages:
resolution: {integrity: sha512-2L3MIgJynYrZ3TYMriLDLWocz15okFakV6J12HXvMXDHui2x/zgChzg1u9mFFGbbGWE+GsLpQByt4POb9Or+uA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
maplibre-gl@4.7.1:
resolution: {integrity: sha512-lgL7XpIwsgICiL82ITplfS7IGwrB1OJIw/pCvprDp2dhmSSEBgmPzYRvwYYYvJGJD7fxUv1Tvpih4nZ6VrLuaA==}
engines: {node: '>=16.14.0', npm: '>=8.1.0'}
markdown-it-anchor@8.6.7:
resolution: {integrity: sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==}
peerDependencies:
@@ -3755,9 +3672,6 @@ packages:
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
murmurhash-js@1.0.0:
resolution: {integrity: sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==}
mux.js@6.0.1:
resolution: {integrity: sha512-22CHb59rH8pWGcPGW5Og7JngJ9s+z4XuSlYvnxhLuc58cA1WqGDQPzuG8I+sPm1/p0CdgpzVTaKW408k5DNn8w==}
engines: {node: '>=8', npm: '>=5'}
@@ -3985,10 +3899,6 @@ packages:
resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==}
engines: {node: '>= 14.16'}
pbf@3.3.0:
resolution: {integrity: sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==}
hasBin: true
picocolors@1.0.1:
resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==}
@@ -4253,9 +4163,6 @@ packages:
resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==}
engines: {node: ^10 || ^12 || >=14}
potpack@2.0.0:
resolution: {integrity: sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw==}
prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
@@ -4328,9 +4235,6 @@ packages:
proto-list@1.2.4:
resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==}
protocol-buffers-schema@3.6.0:
resolution: {integrity: sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==}
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
@@ -4355,12 +4259,6 @@ packages:
resolution: {integrity: sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==}
engines: {node: '>=12'}
quickselect@2.0.0:
resolution: {integrity: sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==}
quickselect@3.0.0:
resolution: {integrity: sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==}
radix-vue@1.9.9:
resolution: {integrity: sha512-DuL2o7jxNjzlSP5Ko+kJgrW5db+jC3RlnYQIs3WITTqgzfdeP7hXjcqIUveY1f0uXRpOAN3OAd5MZ/SpRyQzQQ==}
peerDependencies:
@@ -4409,9 +4307,6 @@ packages:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
resolve-protobuf-schema@2.1.0:
resolution: {integrity: sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==}
resolve@1.22.8:
resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==}
hasBin: true
@@ -4456,9 +4351,6 @@ packages:
rust-result@1.0.0:
resolution: {integrity: sha512-6cJzSBU+J/RJCF063onnQf0cDUOHs9uZI1oroSGnHOph+CQTIJ5Pp2hK5kEQq1+7yE/EEWfulSNXAQ2jikPthA==}
rw@1.3.3:
resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==}
sade@1.8.1:
resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
engines: {node: '>=6'}
@@ -4696,9 +4588,6 @@ packages:
engines: {node: '>=16 || 14 >=14.17'}
hasBin: true
supercluster@8.0.1:
resolution: {integrity: sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==}
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
@@ -4777,9 +4666,6 @@ packages:
resolution: {integrity: sha512-KIKExllK7jp3uvrNtvRBYBWBOAXSX8ZvoaD8T+7KB/QHIuoJW3Pmr60zucywjAlMb5TeXUkcs/MWeWLu0qvuAQ==}
engines: {node: ^18.0.0 || >=20.0.0}
tinyqueue@3.0.0:
resolution: {integrity: sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==}
tinyspy@3.0.2:
resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==}
engines: {node: '>=14.0.0'}
@@ -5037,9 +4923,6 @@ packages:
jsdom:
optional: true
vt-pbf@3.1.3:
resolution: {integrity: sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==}
vue-chartjs@5.3.1:
resolution: {integrity: sha512-rZjqcHBxKiHrBl0CIvcOlVEBwRhpWAVf6rDU3vUfa7HuSRmGtCslc0Oc8m16oAVuk0erzc1FCtH1VCriHsrz+A==}
peerDependencies:
@@ -5229,11 +5112,6 @@ packages:
engines: {node: '>= 8'}
hasBin: true
which@4.0.0:
resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==}
engines: {node: ^16.13.0 || >=18.0.0}
hasBin: true
why-is-node-running@2.3.0:
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
engines: {node: '>=8'}
@@ -6040,35 +5918,6 @@ snapshots:
dependencies:
'@lukeed/csprng': 1.0.1
'@mapbox/geojson-rewind@0.5.2':
dependencies:
get-stream: 6.0.1
minimist: 1.2.8
'@mapbox/jsonlint-lines-primitives@2.0.2': {}
'@mapbox/point-geometry@0.1.0': {}
'@mapbox/tiny-sdf@2.0.6': {}
'@mapbox/unitbezier@0.0.1': {}
'@mapbox/vector-tile@1.3.1':
dependencies:
'@mapbox/point-geometry': 0.1.0
'@mapbox/whoots-js@3.1.0': {}
'@maplibre/maplibre-gl-style-spec@20.4.0':
dependencies:
'@mapbox/jsonlint-lines-primitives': 2.0.2
'@mapbox/unitbezier': 0.0.1
json-stringify-pretty-compact: 4.0.0
minimist: 1.2.8
quickselect: 2.0.0
rw: 1.3.3
tinyqueue: 3.0.0
'@material/mwc-icon@0.25.3':
dependencies:
lit: 2.2.6
@@ -6910,24 +6759,10 @@ snapshots:
dependencies:
'@types/node': 22.7.0
'@types/geojson-vt@3.2.5':
dependencies:
'@types/geojson': 7946.0.14
'@types/geojson@7946.0.14': {}
'@types/json5@0.0.29': {}
'@types/linkify-it@5.0.0': {}
'@types/mapbox__point-geometry@0.1.4': {}
'@types/mapbox__vector-tile@1.3.4':
dependencies:
'@types/geojson': 7946.0.14
'@types/mapbox__point-geometry': 0.1.4
'@types/pbf': 3.0.5
'@types/markdown-it@12.2.3':
dependencies:
'@types/linkify-it': 5.0.0
@@ -6939,12 +6774,6 @@ snapshots:
dependencies:
undici-types: 6.19.8
'@types/pbf@3.0.5': {}
'@types/supercluster@7.1.3':
dependencies:
'@types/geojson': 7946.0.14
'@types/trusted-types@2.0.2': {}
'@types/web-bluetooth@0.0.20': {}
@@ -7878,8 +7707,6 @@ snapshots:
dset@3.1.4: {}
earcut@3.0.0: {}
eastasianwidth@0.2.0: {}
editorconfig@1.0.4:
@@ -8407,8 +8234,6 @@ snapshots:
functions-have-names@1.2.3: {}
geojson-vt@4.0.2: {}
get-caller-file@2.0.5: {}
get-east-asian-width@1.2.0: {}
@@ -8438,8 +8263,6 @@ snapshots:
es-errors: 1.3.0
get-intrinsic: 1.2.4
gl-matrix@3.4.3: {}
glob-parent@5.1.2:
dependencies:
is-glob: 4.0.3
@@ -8470,12 +8293,6 @@ snapshots:
dependencies:
ini: 4.1.1
global-prefix@4.0.0:
dependencies:
ini: 4.1.3
kind-of: 6.0.3
which: 4.0.0
global@4.4.0:
dependencies:
min-document: 2.19.0
@@ -8671,8 +8488,6 @@ snapshots:
idb@8.0.0: {}
ieee754@1.2.1: {}
ignore@5.2.4: {}
immutable@4.3.7:
@@ -8698,8 +8513,6 @@ snapshots:
ini@4.1.1: {}
ini@4.1.3: {}
internal-slot@1.0.5:
dependencies:
get-intrinsic: 1.2.4
@@ -8832,8 +8645,6 @@ snapshots:
isexe@2.0.0: {}
isexe@3.1.1: {}
istanbul-lib-coverage@3.2.2: {}
istanbul-lib-report@3.0.1:
@@ -8955,8 +8766,6 @@ snapshots:
json-stable-stringify-without-jsonify@1.0.1: {}
json-stringify-pretty-compact@4.0.0: {}
json5@1.0.2:
dependencies:
minimist: 1.2.8
@@ -8967,8 +8776,6 @@ snapshots:
optionalDependencies:
graceful-fs: 4.2.11
kdbush@4.0.2: {}
keycode@2.2.1: {}
keyv@4.5.3:
@@ -9142,35 +8949,6 @@ snapshots:
map-obj@5.0.0: {}
maplibre-gl@4.7.1:
dependencies:
'@mapbox/geojson-rewind': 0.5.2
'@mapbox/jsonlint-lines-primitives': 2.0.2
'@mapbox/point-geometry': 0.1.0
'@mapbox/tiny-sdf': 2.0.6
'@mapbox/unitbezier': 0.0.1
'@mapbox/vector-tile': 1.3.1
'@mapbox/whoots-js': 3.1.0
'@maplibre/maplibre-gl-style-spec': 20.4.0
'@types/geojson': 7946.0.14
'@types/geojson-vt': 3.2.5
'@types/mapbox__point-geometry': 0.1.4
'@types/mapbox__vector-tile': 1.3.4
'@types/pbf': 3.0.5
'@types/supercluster': 7.1.3
earcut: 3.0.0
geojson-vt: 4.0.2
gl-matrix: 3.4.3
global-prefix: 4.0.0
kdbush: 4.0.2
murmurhash-js: 1.0.0
pbf: 3.3.0
potpack: 2.0.0
quickselect: 3.0.0
supercluster: 8.0.1
tinyqueue: 3.0.0
vt-pbf: 3.1.3
markdown-it-anchor@8.6.7(@types/markdown-it@12.2.3)(markdown-it@12.3.2):
dependencies:
'@types/markdown-it': 12.2.3
@@ -9297,8 +9075,6 @@ snapshots:
ms@2.1.3: {}
murmurhash-js@1.0.0: {}
mux.js@6.0.1:
dependencies:
'@babel/runtime': 7.25.6
@@ -9519,11 +9295,6 @@ snapshots:
pathval@2.0.0: {}
pbf@3.3.0:
dependencies:
ieee754: 1.2.1
resolve-protobuf-schema: 2.1.0
picocolors@1.0.1: {}
picocolors@1.1.0: {}
@@ -9814,8 +9585,6 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
potpack@2.0.0: {}
prelude-ls@1.2.1: {}
prettier-linter-helpers@1.0.0:
@@ -9921,8 +9690,6 @@ snapshots:
proto-list@1.2.4: {}
protocol-buffers-schema@3.6.0: {}
proxy-from-env@1.1.0: {}
psl@1.9.0: {}
@@ -9937,10 +9704,6 @@ snapshots:
quick-lru@6.1.2: {}
quickselect@2.0.0: {}
quickselect@3.0.0: {}
radix-vue@1.9.9(vue@3.5.12(typescript@5.6.2)):
dependencies:
'@floating-ui/dom': 1.6.12
@@ -9996,10 +9759,6 @@ snapshots:
resolve-from@4.0.0: {}
resolve-protobuf-schema@2.1.0:
dependencies:
protocol-buffers-schema: 3.6.0
resolve@1.22.8:
dependencies:
is-core-module: 2.15.1
@@ -10060,8 +9819,6 @@ snapshots:
dependencies:
individual: 2.0.0
rw@1.3.3: {}
sade@1.8.1:
dependencies:
mri: 1.2.0
@@ -10329,10 +10086,6 @@ snapshots:
pirates: 4.0.6
ts-interface-checker: 0.1.13
supercluster@8.0.1:
dependencies:
kdbush: 4.0.2
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0
@@ -10433,8 +10186,6 @@ snapshots:
tinypool@1.0.0: {}
tinyqueue@3.0.0: {}
tinyspy@3.0.2: {}
to-fast-properties@2.0.0: {}
@@ -10719,12 +10470,6 @@ snapshots:
- supports-color
- terser
vt-pbf@3.1.3:
dependencies:
'@mapbox/point-geometry': 0.1.0
'@mapbox/vector-tile': 1.3.1
pbf: 3.3.0
vue-chartjs@5.3.1(chart.js@4.4.4)(vue@3.5.12(typescript@5.6.2)):
dependencies:
chart.js: 4.4.4
@@ -10912,10 +10657,6 @@ snapshots:
dependencies:
isexe: 2.0.0
which@4.0.0:
dependencies:
isexe: 3.1.1
why-is-node-running@2.3.0:
dependencies:
siginfo: 2.0.0

View File

@@ -360,6 +360,21 @@ export const colors = {
12: 'rgb(var(--teal-12) / <alpha-value>)',
},
gray: {
1: 'rgb(var(--gray-1) / <alpha-value>)',
2: 'rgb(var(--gray-2) / <alpha-value>)',
3: 'rgb(var(--gray-3) / <alpha-value>)',
4: 'rgb(var(--gray-4) / <alpha-value>)',
5: 'rgb(var(--gray-5) / <alpha-value>)',
6: 'rgb(var(--gray-6) / <alpha-value>)',
7: 'rgb(var(--gray-7) / <alpha-value>)',
8: 'rgb(var(--gray-8) / <alpha-value>)',
9: 'rgb(var(--gray-9) / <alpha-value>)',
10: 'rgb(var(--gray-10) / <alpha-value>)',
11: 'rgb(var(--gray-11) / <alpha-value>)',
12: 'rgb(var(--gray-12) / <alpha-value>)',
},
black: '#000000',
brand: '#2781F6',
background: 'rgb(var(--background-color) / <alpha-value>)',