Compare commits

...
Author SHA1 Message Date
Shivam Mishra 1b31b8b93c fix: spacing 2025-06-17 13:22:59 +05:30
Shivam Mishra 4502ed6213 feat: allow disabling private note 2025-06-17 13:17:06 +05:30
Shivam Mishra f0e376c0d9 feat: better versioning 2025-06-12 14:43:11 +05:30
Shivam Mishra 0be8af9c3d feat: disable context menu 2025-06-12 14:37:48 +05:30
Shivam Mishra f6c4224400 docs: development instructions 2025-06-12 14:36:12 +05:30
Shivam Mishra 8f47496373 feat: update package scripts 2025-06-12 14:26:04 +05:30
Shivam Mishra 0990e33860 fix: enter to send 2025-06-12 14:24:08 +05:30
Shivam Mishra c93c6aed5f fix: realtime events not working 2025-06-12 14:13:39 +05:30
Shivam Mishra 0381aa188a fix: clearMessage error 2025-06-12 13:07:32 +05:30
Shivam Mishra 90e212a81a feat: add copy mode 2025-06-12 13:06:24 +05:30
Shivam Mishra 76e1643ccf feat: expose disable-editor 2025-06-12 09:50:50 +05:30
Shivam Mishra 0b56add844 fix: version 2025-06-11 18:28:04 +05:30
Shivam Mishra e9b99a5811 fix: uploads 2025-06-11 18:09:08 +05:30
Shivam Mishra a910758fd8 fix: colors 2025-06-11 18:08:53 +05:30
Shivam Mishra aa68525b6b fix: name 2025-06-11 17:34:38 +05:30
Shivam Mishra ba1f363a28 fix: gap and text color 2025-06-11 17:34:08 +05:30
Shivam Mishra b8e64722a7 feat: correct name 2025-06-10 20:11:34 +05:30
Shivam Mishra 153d0371ff feat: update readme 2025-06-10 18:54:04 +05:30
Shivam Mishra c66f8600ec feat: disable upload 2025-06-10 18:46:06 +05:30
Shivam Mishra 38b5c61d51 feat: make tailwind work 2025-06-10 18:45:34 +05:30
Shivam Mishra 81f5c9aa12 chore: remove comment 2025-06-10 17:51:20 +05:30
Shivam Mishra 532c5c3324 chore: remove unused file 2025-06-10 17:49:34 +05:30
Shivam Mishra 2dca1eee89 refactor: simplify component loading 2025-06-10 17:49:23 +05:30
Shivam Mishra 851f622fe1 fix: exports 2025-06-10 17:35:45 +05:30
Shivam Mishra 1503ddeda4 feat: remove unused files 2025-06-10 17:32:18 +05:30
Shivam Mishra c645ba3922 feat: add store from CE options 2025-06-10 17:31:52 +05:30
Shivam Mishra 5d64079a5e feat: simplify provider and vue wrapper 2025-06-10 17:31:42 +05:30
Shivam Mishra 82e296b152 feat: add dompurify and fix i18n ijection 2025-06-10 16:31:55 +05:30
Shivam Mishra 016a8541c1 feat: use i18 and other ce options 2025-06-10 16:29:44 +05:30
Shivam Mishra d289daf6ca refactor: component cleanup 2025-06-10 16:29:33 +05:30
Shivam Mishra c8cc157683 refactor: provider 2025-06-10 16:07:35 +05:30
Shivam Mishra 3fd506c27f feat: update publish script 2025-06-10 14:50:59 +05:30
Shivam Mishra d8a1fdbce5 feat: setup vue component 2025-06-10 14:48:37 +05:30
Shivam Mishra 6840108f30 feat: add publishing pipeline 2025-06-10 13:39:57 +05:30
Shivam Mishra d7288835fa feat: vue component for PoC 2025-06-10 13:00:33 +05:30
Shivam Mishra 31e1d59d58 feat: kickstart basic react component 2025-06-10 12:40:52 +05:30
Shivam Mishra 4f55a66fba Merge branch 'develop' of github.com:chatwoot/chatwoot into feat/ui-lib 2025-06-10 10:17:19 +05:30
Shivam Mishra 312c8a1a74 fix: upload toggle 2025-06-04 16:43:57 +05:30
Shivam Mishra 79f7afaeec feat: allow disabling editor 2025-06-04 16:43:08 +05:30
Shivam Mishra 4e7ca49297 fix: menubar not empty 2025-06-04 16:40:47 +05:30
Shivam Mishra db51d2d011 fix: options 2025-06-04 16:34:58 +05:30
Shivam Mishra 6f86944758 feat: allow option to disable upload 2025-06-04 16:34:47 +05:30
Shivam Mishra d74798fcd5 feat: disable editor menu for ios 2025-06-04 16:34:26 +05:30
Shivam Mishra 394419ce95 feat: allow new confg 2025-06-04 16:09:42 +05:30
Shivam MishraandGitHub 4fe9b88642 Merge branch 'develop' into feat/ui-lib 2025-06-04 15:51:21 +05:30
Shivam Mishra 0f8037b6fc fix: ios virtual keyboard issue on editor focus 2025-05-21 19:59:26 +05:30
Shivam Mishra 38938fe776 feat: hide name in mobile 2025-05-20 16:39:37 +05:30
Shivam Mishra 02ca4136ad fix: message attachments 2025-05-20 16:28:08 +05:30
Shivam Mishra da4049d18a feat: register UI kit and enable gallery 2025-05-20 16:07:33 +05:30
Shivam Mishra 088608abcf feat: disable elastic scrolling 2025-05-19 19:45:02 +05:30
Shivam Mishra afb3c0109b fix: list item styling 2025-05-19 19:12:08 +05:30
Shivam Mishra c54973eeb2 feat: use enter for next line in isolated shell mode 2025-05-19 19:11:07 +05:30
Shivam Mishra cb2785096e feat: ensure inbox was always there 2025-05-19 16:27:21 +05:30
Shivam Mishra 27f35a7dd8 style: if block 2025-05-19 16:11:47 +05:30
Shivam Mishra 59790e38da fix: editor loads with private note color 2025-05-19 16:10:51 +05:30
Shivam Mishra 461f1b3bf0 chore: attempt to fix CW-4353 2025-05-19 15:39:57 +05:30
Shivam Mishra 4056a144e4 feat: add spinner 2025-05-19 15:37:51 +05:30
Shivam Mishra 31681610f2 feat: disable lightbox in UI shell 2025-05-16 22:01:16 +05:30
Shivam Mishra a8b33023f8 feat: auto init if callback is not present 2025-05-16 16:55:31 +05:30
Shivam Mishra 8c5204d0c9 feat: use computed for conversation ID 2025-05-16 16:55:17 +05:30
Shivam Mishra aa5242fdb0 feat: run on callback 2025-05-16 16:35:18 +05:30
Shivam Mishra b591893486 feat: better init 2025-05-16 16:23:56 +05:30
Shivam Mishra a118bef98e feat: use init fn 2025-05-16 16:18:37 +05:30
Shivam Mishra daa54f6866 feat: disable unused options 2025-05-16 16:12:05 +05:30
Shivam Mishra c38e7af0e7 feat: allow cors for api 2025-05-16 09:48:48 +05:30
Shivam Mishra d6f862a85c feat: new lite reply box 2025-05-15 17:06:32 +05:30
Shivam Mishra 13f742fd68 feat: allow customizing reply box 2025-05-15 17:06:20 +05:30
Shivam Mishra b74e78f698 feat: add spinner 2025-05-15 16:10:27 +05:30
Shivam Mishra 5d8646a3f3 feat: smaller bundle 2025-05-14 19:48:10 +05:30
Shivam Mishra 453eba9a81 feat: conditionally show typing indicator 2025-05-14 16:21:14 +05:30
Shivam Mishra 6beb355f29 feat: add typing indicator 2025-05-14 16:01:18 +05:30
Shivam Mishra 980eca46c6 feat: start typing indicator 2025-05-14 15:53:30 +05:30
Shivam Mishra 0868a9af70 refactor: use vue 2025-05-14 14:05:58 +05:30
Shivam Mishra da6fbe8579 feat: make message sending work 2025-05-13 19:59:14 +05:30
Shivam Mishra 2c814fff99 feat: kickstart message editor\ 2025-05-13 19:52:18 +05:30
Shivam Mishra c1c44512d4 feat: remove test component 2025-05-13 19:30:01 +05:30
Shivam Mishra 1138195846 feat: include ui in tailwind content 2025-05-13 19:24:29 +05:30
Shivam Mishra 84d31b91f4 feat: add space for editor 2025-05-13 19:22:36 +05:30
Shivam Mishra 2df2459674 chore: run lint 2025-05-13 19:03:37 +05:30
Shivam Mishra 81e18aaafa feat: fetch inboxes 2025-05-13 19:00:39 +05:30
Shivam Mishra f07a5215cf feat: allow bypassing indexedb cache 2025-05-13 19:00:30 +05:30
Shivam Mishra 1899a31416 feat: infinite scrolling 2025-05-13 18:50:48 +05:30
Shivam Mishra 60f5145365 fix: test 2025-05-12 19:04:53 +05:30
Shivam Mishra e58736e746 feat: kickstart scroll 2025-05-12 18:55:36 +05:30
Shivam Mishra a9fd459cf4 feat: wait for auth 2025-05-12 17:53:27 +05:30
Shivam Mishra 7235e44a9d feat: ignore audio events for ui mode 2025-05-12 17:53:09 +05:30
Shivam Mishra ddd1fb5396 fix: auth payload 2025-05-12 17:52:59 +05:30
Shivam Mishra a97cf6e4ad feat: allow api based auth 2025-05-12 17:35:06 +05:30
Shivam Mishra 4ecc92ad6d feat: setup wss 2025-05-12 17:13:41 +05:30
Shivam Mishra ff70fbe2e4 Merge branch 'develop' of github.com:chatwoot/chatwoot into feat/ui-lib 2025-05-09 09:39:24 +05:30
Shivam Mishra 4f27f1605a feat: setup conversation list 2025-05-08 20:55:51 +05:30
Shivam Mishra e00b54ce07 feat: include dompurify 2025-05-08 19:47:35 +05:30
Shivam Mishra df20a59a6c fix: route may not be present 2025-05-08 19:47:28 +05:30
Shivam Mishra bb6f7b9fcd feat: messages 2025-05-08 19:40:10 +05:30
Shivam Mishra e28c6312e4 feat: make i18n work 2025-05-08 10:11:01 +05:30
Shivam Mishra fc119d14d9 feat: styles are working 2025-05-08 09:06:49 +05:30
Shivam Mishra 9243b6367c feat: configure i18n (try atleat) 2025-05-08 08:59:59 +05:30
Shivam Mishra 5af8881a5a feat: tester 2025-05-07 22:36:47 +05:30
Shivam Mishra 70029177da debug: patch process.env 2025-05-07 22:36:38 +05:30
Shivam Mishra 1abb04f592 feat: update ui.js 2025-05-07 22:24:27 +05:30
Shivam Mishra 355b587c2c feat: try 2025-05-07 22:13:17 +05:30
Shivam Mishra b22a903446 feat: import all styles 2025-05-07 22:03:01 +05:30
Shivam Mishra 2d5993dfe3 fix: import name 2025-05-07 19:32:22 +05:30
Shivam Mishra db50f42054 feat: kickstart ui 2025-05-07 19:17:37 +05:30
Shivam Mishra f8afc1509f feat: kickstart ui 2025-05-07 19:12:44 +05:30
38 changed files with 2110 additions and 71 deletions
+3
View File
@@ -94,3 +94,6 @@ yarn-debug.log*
.vscode
.claude/settings.local.json
.cursor
# react component
dist
@@ -15,6 +15,12 @@ class ApiClient {
// eslint-disable-next-line class-methods-use-this
get accountIdFromRoute() {
// eslint-disable-next-line no-underscore-dangle
if (window.__WOOT_ACCOUNT_ID__) {
// eslint-disable-next-line no-underscore-dangle
return window.__WOOT_ACCOUNT_ID__;
}
const isInsideAccountScopedURLs =
window.location.pathname.includes('/app/accounts');
+12 -1
View File
@@ -8,7 +8,14 @@ import {
} from '../store/utils/api';
export default {
validityCheck() {
async validityCheck() {
if (this.hasAuthToken()) {
const urlData = endPoints('profileUpdate');
const response = await axios.get(urlData.url);
// to match the response signature of the validityCheck endpoint
return Promise.resolve({ data: { payload: response } });
}
const urlData = endPoints('validityCheck');
return axios.get(urlData.url);
},
@@ -31,6 +38,10 @@ export default {
hasAuthCookie() {
return !!Cookies.get('cw_d_session_info');
},
hasAuthToken() {
// eslint-disable-next-line no-underscore-dangle
return !!window.__WOOT_ACCESS_TOKEN__;
},
getAuthData() {
if (this.hasAuthCookie()) {
const savedAuthInfo = Cookies.get('cw_d_session_info');
+8 -1
View File
@@ -1,7 +1,14 @@
/* global axios */
import CacheEnabledApiClient from './CacheEnabledApiClient';
// import ApiClient from './ApiClient';
import ApiClient from './ApiClient';
class Inboxes extends CacheEnabledApiClient {
// eslint-disable-next-line no-underscore-dangle
const BaseClass = window.__WOOT_ISOLATED_SHELL__
? ApiClient
: CacheEnabledApiClient;
class Inboxes extends BaseClass {
constructor() {
super('inboxes', { accountScoped: true });
}
@@ -315,6 +315,9 @@ const componentToRender = computed(() => {
});
const shouldShowContextMenu = computed(() => {
// eslint-disable-next-line no-underscore-dangle
if (window.__WOOT_ISOLATED_SHELL__) return false;
return !props.contentAttributes?.isUnsupported;
});
@@ -441,7 +444,7 @@ const avatarTooltip = computed(() => {
});
const setupHighlightTimer = () => {
if (Number(route.query.messageId) !== Number(props.id)) {
if (Number(route?.query?.messageId) !== Number(props.id)) {
return;
}
@@ -0,0 +1,45 @@
<template>
<svg
class="w-8 h-4"
viewBox="0 0 120 30"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
>
<circle cx="15" cy="15" r="12">
<animate
attributeName="cy"
from="15"
to="5"
begin="0s"
dur="0.9s"
values="15;5;15;15;15;15"
calcMode="linear"
repeatCount="indefinite"
/>
</circle>
<circle cx="55" cy="15" r="12">
<animate
attributeName="cy"
from="15"
to="5"
begin="0.1s"
dur="0.9s"
values="15;5;15;15;15;15"
calcMode="linear"
repeatCount="indefinite"
/>
</circle>
<circle cx="95" cy="15" r="12">
<animate
attributeName="cy"
from="15"
to="5"
begin="0.2s"
dur="0.9s"
values="15;5;15;15;15;15"
calcMode="linear"
repeatCount="indefinite"
/>
</circle>
</svg>
</template>
@@ -5,6 +5,7 @@ import { useAlert } from 'dashboard/composables';
import BaseBubble from './Base.vue';
import Button from 'next/button/Button.vue';
import Icon from 'next/icon/Icon.vue';
import { useGallery } from '../useGallery';
import { useSnakeCase } from 'dashboard/composables/useTransformKeys';
import { useMessageContext } from '../provider.js';
import { downloadFile } from '@chatwoot/utils';
@@ -21,8 +22,8 @@ const attachment = computed(() => {
});
const hasError = ref(false);
const showGallery = ref(false);
const isDownloading = ref(false);
const { showGallery, isGalleryAllowed, toggleGallery } = useGallery();
const handleError = () => {
hasError.value = true;
@@ -46,7 +47,7 @@ const downloadAttachment = async () => {
<BaseBubble
class="overflow-hidden p-3"
data-bubble-name="image"
@click="showGallery = true"
@click="toggleGallery(true)"
>
<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" />
@@ -82,7 +83,7 @@ const downloadAttachment = async () => {
</div>
</BaseBubble>
<GalleryView
v-if="showGallery"
v-if="showGallery && isGalleryAllowed"
v-model:show="showGallery"
:attachment="useSnakeCase(attachment)"
:all-attachments="filteredCurrentChatAttachments"
@@ -2,6 +2,7 @@
import { ref, computed } from 'vue';
import BaseBubble from './Base.vue';
import Icon from 'next/icon/Icon.vue';
import { useGallery } from '../useGallery';
import { useSnakeCase } from 'dashboard/composables/useTransformKeys';
import { useMessageContext } from '../provider.js';
import GalleryView from 'dashboard/components/widgets/conversation/components/GalleryView.vue';
@@ -9,7 +10,7 @@ import { ATTACHMENT_TYPES } from '../constants';
const emit = defineEmits(['error']);
const hasError = ref(false);
const showGallery = ref(false);
const { showGallery, isGalleryAllowed, toggleGallery } = useGallery();
const { filteredCurrentChatAttachments, attachments } = useMessageContext();
const handleError = () => {
@@ -30,7 +31,7 @@ const isReel = computed(() => {
<BaseBubble
class="overflow-hidden p-3"
data-bubble-name="video"
@click="showGallery = true"
@click="toggleGallery(true)"
>
<div class="relative group rounded-lg overflow-hidden">
<div
@@ -52,7 +53,7 @@ const isReel = computed(() => {
</div>
</BaseBubble>
<GalleryView
v-if="showGallery"
v-if="showGallery && isGalleryAllowed"
v-model:show="showGallery"
:attachment="useSnakeCase(attachment)"
:all-attachments="filteredCurrentChatAttachments"
@@ -1,6 +1,7 @@
<script setup>
import { ref } from 'vue';
import Icon from 'next/icon/Icon.vue';
import { useGallery } from '../useGallery';
import { useSnakeCase } from 'dashboard/composables/useTransformKeys';
import { useMessageContext } from '../provider.js';
@@ -13,7 +14,7 @@ defineProps({
},
});
const hasError = ref(false);
const showGallery = ref(false);
const { showGallery, toggleGallery, isGalleryAllowed } = useGallery();
const { filteredCurrentChatAttachments } = useMessageContext();
@@ -25,7 +26,7 @@ const handleError = () => {
<template>
<div
class="size-[72px] overflow-hidden contain-content rounded-xl cursor-pointer"
@click="showGallery = true"
@click="toggleGallery(true)"
>
<div
v-if="hasError"
@@ -42,7 +43,7 @@ const handleError = () => {
/>
</div>
<GalleryView
v-if="showGallery"
v-if="showGallery && isGalleryAllowed"
v-model:show="showGallery"
:attachment="useSnakeCase(attachment)"
:all-attachments="filteredCurrentChatAttachments"
@@ -1,6 +1,6 @@
<script setup>
import { ref } from 'vue';
import Icon from 'next/icon/Icon.vue';
import { useGallery } from '../useGallery';
import { useSnakeCase } from 'dashboard/composables/useTransformKeys';
import { useMessageContext } from '../provider.js';
import GalleryView from 'dashboard/components/widgets/conversation/components/GalleryView.vue';
@@ -12,7 +12,7 @@ defineProps({
},
});
const showGallery = ref(false);
const { showGallery, isGalleryAllowed, toggleGallery } = useGallery();
const { filteredCurrentChatAttachments } = useMessageContext();
</script>
@@ -20,7 +20,7 @@ const { filteredCurrentChatAttachments } = useMessageContext();
<template>
<div
class="size-[72px] overflow-hidden contain-content rounded-xl cursor-pointer relative group"
@click="showGallery = true"
@click="toggleGallery(true)"
>
<video
:src="attachment.dataUrl"
@@ -42,7 +42,7 @@ const { filteredCurrentChatAttachments } = useMessageContext();
</div>
</div>
<GalleryView
v-if="showGallery"
v-if="showGallery && isGalleryAllowed"
v-model:show="showGallery"
:attachment="useSnakeCase(attachment)"
:all-attachments="filteredCurrentChatAttachments"
@@ -0,0 +1,23 @@
import { computed } from 'vue';
import { useToggle } from '@vueuse/core';
export function useGallery() {
const [showGallery] = useToggle(false);
const isGalleryAllowed = computed(() => {
return true;
// return !window.__WOOT_ISOLATED_SHELL__;
});
const toggleGallery = value => {
if (!isGalleryAllowed.value) return;
showGallery.value = value;
};
return {
showGallery,
isGalleryAllowed,
toggleGallery,
};
}
@@ -170,6 +170,10 @@ const shouldShowCannedResponses = computed(() => {
});
const editorMenuOptions = computed(() => {
// eslint-disable-next-line no-underscore-dangle
if (window.__WOOT_ISOLATED_SHELL__) {
return [];
}
return props.enabledMenuOptions.length
? props.enabledMenuOptions
: MESSAGE_EDITOR_MENU_OPTIONS;
@@ -422,10 +426,14 @@ function updateImgToolbarOnDelete() {
}
function isEnterToSendEnabled() {
// eslint-disable-next-line no-underscore-dangle
if (window.__WOOT_ISOLATED_SHELL__) return false;
return isEditorHotKeyEnabled('enter');
}
function isCmdPlusEnterToSendEnabled() {
// eslint-disable-next-line no-underscore-dangle
if (window.__WOOT_ISOLATED_SHELL__) return true;
return isEditorHotKeyEnabled('cmd_enter');
}
@@ -653,7 +661,7 @@ watch(sendWithSignature, newValue => {
}
});
onMounted(() => {
onMounted(async () => {
// [VITE] state assignment was done in created before
state = createState(
props.modelValue,
@@ -734,6 +742,10 @@ useEmitter(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, insertContentIntoEditor);
.ProseMirror-menubar-wrapper {
@apply flex flex-col;
.ProseMirror-menubar:empty {
display: none;
}
.ProseMirror-menubar {
min-height: var(--space-two) !important;
@apply -ml-2.5 pb-0 bg-transparent text-n-slate-11;
@@ -55,10 +55,16 @@ const translateValue = computed(() => {
class="flex items-center w-auto h-8 p-1 transition-all border rounded-full bg-n-alpha-2 group relative duration-300 ease-in-out z-0"
@click="$emit('toggleMode')"
>
<div ref="wootEditorReplyMode" class="flex items-center gap-1 px-2 z-20">
<div
ref="wootEditorReplyMode"
class="flex items-center gap-1 px-2 z-20 text-n-slate-11"
>
{{ $t('CONVERSATION.REPLYBOX.REPLY') }}
</div>
<div ref="wootEditorPrivateMode" class="flex items-center gap-1 px-2 z-20">
<div
ref="wootEditorPrivateMode"
class="flex items-center gap-1 px-2 z-20 text-n-slate-11"
>
{{ $t('CONVERSATION.REPLYBOX.PRIVATE_NOTE') }}
</div>
<div
@@ -118,6 +118,12 @@ export default {
type: String,
default: '',
},
allowSignature: { type: Boolean, default: false },
allowEmoji: { type: Boolean, default: false },
allowAiAssist: { type: Boolean, default: false },
allowVideoCall: { type: Boolean, default: false },
allowFileUpload: { type: Boolean, default: false },
allowAudioRecorder: { type: Boolean, default: false },
},
emits: [
'replaceText',
@@ -264,6 +270,7 @@ export default {
<div class="flex justify-between p-3" :class="wrapClass">
<div class="left-wrap">
<NextButton
v-if="allowEmoji"
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_EMOJI_ICON')"
icon="i-ph-smiley-sticker"
slate
@@ -272,6 +279,7 @@ export default {
@click="toggleEmojiPicker"
/>
<FileUpload
v-if="allowFileUpload"
ref="uploadRef"
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_ATTACH_ICON')"
input-id="conversationAttachment"
@@ -295,36 +303,40 @@ export default {
sm
/>
</FileUpload>
<template v-if="allowAudioRecorder">
<NextButton
v-if="showAudioRecorderButton"
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_ICON')"
:icon="
!isRecordingAudio ? 'i-ph-microphone' : 'i-ph-microphone-slash'
"
slate
faded
sm
@click="toggleAudioRecorder"
/>
<NextButton
v-if="showEditorToggle"
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_FORMAT_ICON')"
icon="i-ph-quotes"
slate
faded
sm
@click="$emit('toggleEditor')"
/>
<NextButton
v-if="showAudioPlayStopButton"
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_FORMAT_ICON')"
:icon="audioRecorderPlayStopIcon"
slate
faded
sm
:label="recordingAudioDurationText"
@click="toggleAudioRecorderPlayPause"
/>
</template>
<NextButton
v-if="showAudioRecorderButton"
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_ICON')"
:icon="!isRecordingAudio ? 'i-ph-microphone' : 'i-ph-microphone-slash'"
slate
faded
sm
@click="toggleAudioRecorder"
/>
<NextButton
v-if="showEditorToggle"
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_FORMAT_ICON')"
icon="i-ph-quotes"
slate
faded
sm
@click="$emit('toggleEditor')"
/>
<NextButton
v-if="showAudioPlayStopButton"
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_FORMAT_ICON')"
:icon="audioRecorderPlayStopIcon"
slate
faded
sm
:label="recordingAudioDurationText"
@click="toggleAudioRecorderPlayPause"
/>
<NextButton
v-if="showMessageSignatureButton"
v-if="allowSignature && showMessageSignatureButton"
v-tooltip.top-end="signatureToggleTooltip"
icon="i-ph-signature"
slate
@@ -342,11 +354,15 @@ export default {
@click="$emit('selectWhatsappTemplate')"
/>
<VideoCallButton
v-if="(isAWebWidgetInbox || isAPIInbox) && !isOnPrivateNote"
v-if="
allowVideoCall &&
(isAWebWidgetInbox || isAPIInbox) &&
!isOnPrivateNote
"
:conversation-id="conversationId"
/>
<AIAssistanceButton
v-if="!isFetchingAppIntegrations"
v-if="allowAiAssist && !isFetchingAppIntegrations"
:conversation-id="conversationId"
:is-private-note="isOnPrivateNote"
:message="message"
@@ -15,6 +15,10 @@ export default {
type: String,
default: REPLY_EDITOR_MODES.REPLY,
},
disablePopout: {
type: Boolean,
default: false,
},
isMessageLengthReachingThreshold: {
type: Boolean,
default: () => false,
@@ -85,7 +89,7 @@ export default {
</script>
<template>
<div class="flex justify-between h-[3.25rem] gap-2 ltr:pl-3 rtl:pr-3">
<div class="flex justify-between h-[3.25rem] gap-2 ms-3">
<EditorModeToggle
:mode="mode"
class="mt-3"
@@ -99,6 +103,7 @@ export default {
</div>
</div>
<NextButton
v-if="!disablePopout"
ghost
class="ltr:rounded-bl-md rtl:rounded-br-md ltr:rounded-br-none rtl:rounded-bl-none ltr:rounded-tl-none rtl:rounded-tr-none text-n-slate-11 ltr:rounded-tr-[11px] rtl:rounded-tl-[11px]"
icon="i-lucide-maximize-2"
@@ -1240,6 +1240,12 @@ export default {
:message="message"
:portal-slug="connectedPortalSlug"
:new-conversation-modal-active="newConversationModalActive"
allow-signature
allow-emoji
allow-ai-assist
allow-video-call
allow-audio-recorder
allow-file-upload
@select-whatsapp-template="openWhatsappTemplateModal"
@toggle-editor="toggleRichContentEditor"
@replace-text="replaceText"
@@ -209,7 +209,7 @@ onMounted(() => {
</div>
<div
class="flex-1 mx-2 px-2 truncate text-sm font-medium text-center text-n-slate-12"
class="flex-1 mx-2 px-2 truncate text-sm font-medium text-center text-n-slate-12 hidden sm:block"
>
<span v-dompurify-html="fileNameFromDataUrl" class="truncate" />
</div>
@@ -2,6 +2,12 @@ import { computed, unref } from 'vue';
import { getCurrentInstance } from 'vue';
export const useStore = () => {
// eslint-disable-next-line no-underscore-dangle
if (window.__CHATWOOT_STORE__) {
// eslint-disable-next-line no-underscore-dangle
return window.__CHATWOOT_STORE__;
}
const vm = getCurrentInstance();
if (!vm) throw new Error('must be called in setup');
return vm.proxy.$store;
+11 -3
View File
@@ -10,7 +10,10 @@ const { isImpersonating } = useImpersonation();
class ActionCableConnector extends BaseActionCableConnector {
constructor(app, pubsubToken) {
const { websocketURL = '' } = window.chatwootConfig || {};
super(app, pubsubToken, websocketURL);
// eslint-disable-next-line no-underscore-dangle
const wsURL = websocketURL || window.__WEBSOCKET_URL__ || '';
super(app, pubsubToken, wsURL);
this.CancelTyping = [];
this.events = {
'message.created': this.onMessageCreated,
@@ -48,7 +51,9 @@ class ActionCableConnector extends BaseActionCableConnector {
};
isAValidEvent = data => {
return this.app.$store.getters.getCurrentAccountId === data.account_id;
// eslint-disable-next-line no-underscore-dangle
const currentAccountId = this.app.$store.getters.getCurrentAccountId;
return currentAccountId === data.account_id;
};
onMessageUpdated = data => {
@@ -98,7 +103,10 @@ class ActionCableConnector extends BaseActionCableConnector {
conversation: { last_activity_at: lastActivityAt },
conversation_id: conversationId,
} = data;
DashboardAudioNotificationHelper.onNewMessage(data);
// eslint-disable-next-line no-underscore-dangle
if (!window.__WOOT_ISOLATED_SHELL__) {
DashboardAudioNotificationHelper.onNewMessage(data);
}
this.app.$store.dispatch('addMessage', data);
this.app.$store.dispatch('updateConversationLastActivity', {
lastActivityAt,
@@ -53,6 +53,9 @@ export const getters = {
},
getCurrentAccountId(_, __, rootState) {
// eslint-disable-next-line no-underscore-dangle
if (window.__WOOT_ACCOUNT_ID__) return window.__WOOT_ACCOUNT_ID__;
if (rootState.route.params && rootState.route.params.accountId) {
return Number(rootState.route.params.accountId);
}
@@ -114,7 +117,7 @@ export const actions = {
}
},
async setUser({ commit, dispatch }) {
if (authAPI.hasAuthCookie()) {
if (authAPI.hasAuthCookie() || authAPI.hasAuthToken()) {
await dispatch('validityCheck');
} else {
commit(types.CLEAR_USER);
+56
View File
@@ -0,0 +1,56 @@
import { createApp } from 'vue';
import '../dashboard/assets/scss/app.scss';
import 'vue-multiselect/dist/vue-multiselect.css';
import 'floating-vue/dist/style.css';
// import tailwindStyles from '../dashboard/assets/scss/_woot.scss?inline';
import VueDOMPurifyHTML from 'vue-dompurify-html';
import WootUiKit from 'dashboard/components';
import { domPurifyConfig } from '../shared/helpers/HTMLSanitizer.js';
import store from '../dashboard/store';
import constants from '../dashboard/constants/globals';
import axios from 'axios';
import createAxios from '../ui/axios';
import commonHelpers from '../dashboard/helper/commons';
import vueActionCable from '../dashboard/helper/actionCable';
import MessageList from '../ui/MessageList.vue';
import en from '../dashboard/i18n/locale/en';
import { createI18n } from 'vue-i18n';
const i18n = createI18n({
legacy: false,
locale: 'en',
messages: {
en,
},
});
export const init = async () => {
commonHelpers();
// eslint-disable-next-line no-underscore-dangle
window.__CHATWOOT_STORE__ = store;
window.WootConstants = constants;
window.axios = createAxios(axios);
return store.dispatch('setUser').then(() => {
const app = createApp(MessageList);
app.use(store);
app.use(i18n);
app.use(WootUiKit);
app.use(VueDOMPurifyHTML, domPurifyConfig);
// eslint-disable-next-line no-underscore-dangle
vueActionCable.init(store, window.__PUBSUB_TOKEN__);
app.mount('#app');
});
};
if (typeof window.chatwootCallback === 'function') {
window.chatwootCallback(init);
} else {
init();
}
@@ -0,0 +1,95 @@
# Chatwoot React Components
This module exports Chatwoot UI components as React components that can be embedded in other applications. It uses a hybrid approach combining Vue Web Components with React wrappers.
## Architecture
The system works by:
1. **Vue Components** → Compiled as custom web components using Vue's `customElement: true`
2. **React Wrappers** → Provide React-friendly interfaces to the Vue web components
3. **Vite Build** → Packages everything into distributable ES/CJS modules
## Build System
### Build Modes (vite.config.ts)
- **`BUILD_MODE=react-components`** - Builds React components library
- Entry point: `./app/javascript/react-components/src/index.jsx`
- Output: ES modules (`react-components.es.js`) and CommonJS (`react-components.cjs.js`)
- External dependencies: `react` and `react-dom` (peer dependencies)
### Available Scripts (package.json)
```bash
# Build the React components once
pnpm build:react
# Build and watch for changes during development
pnpm dev:react
# Package for NPM distribution (builds + creates dist/react-components/)
pnpm package:react
```
## Usage
In any React app, version 16+, you could use the following snippet
```jsx
import * as ChatwootComponents from '@chatwoot/agent-react-components';
import '@chatwoot/agent-react-components/style.css'
const { ChatwootProvider, ChatwootConversation } = ChatwootComponents;
function App() {
return (
<ChatwootProvider
baseURL="http://localhost:3000"
accountId="1"
conversationId={13918}
userToken="XLwgvCQiewUJMGuYpZqa4J3M"
pubsubToken="HkAhoq8aWm2ecdY2tn9FV4BU"
websocketURL="ws://localhost:3000"
>
<ChatwootConversation/>
</ChatwootProvider>
)
}
export default App
```
The `<ChatwootProvider>` component ensures all the data necessary for mounting the web component is present before rendering the ChatwootConversation.
The package also exposes a `useChatwoot` hook for accessing the config instance being used by the conversation, it can be used as a read-only source of truth for the config being used.
## Development Workflow
To test this package, you can create a separate React application where you can use the snippet from above, run the following command in your Rails App:
```bash
pnpm dev:react
```
This will start a development server that serves the React components and their dependencies.
Following this, in a separate terminal, run the following command
```bash
watchexec --restart -N --watch public/packs/react-components.es.js "pnpm package:react --copyMode"
```
This will ensure that the built files are copied correctly to the dist/react-components folder.
The `package:react` script has a copy mode, which just copies the build from the `public/` folder to the dist folder for linking purposes. It also creates a package.json file with proper entry points, ensure you run the script without copyMode once before.
Once done, in your React app, you can use `pnpm link <rails-app-dir>/dist/react-components` to link the package. Running the development server will automatically link the package and any changes to it.
## Publishing
The `scripts/package-react-components.js` handles:
- Building the components with Vite
- Copying built files to `dist/react-components/`
- Generating `package.json` with proper entry points
- Creating development versions with git hash suffixes
Once the script is packaged, you can run `npm publish` to publish the package. It will be published as `@chatwoot/agent-react-components` with a version that is the latest commit hash
@@ -0,0 +1,26 @@
import { useChatwoot } from './ChatwootProvider';
import { ChatwootMessageListWrapper } from './ChatwootMessageListWrapper';
export const ChatwootConversation = ({
conversationId,
className = '',
style = {},
...otherProps
}) => {
// Ensure we're inside a ChatwootProvider
useChatwoot(); // This will throw if not in Provider context
return (
<div
className={`chatwoot-conversation ${className} text-n-slate-11`}
style={{
height: '100%',
width: '100%',
position: 'relative',
...style,
}}
>
<ChatwootMessageListWrapper className="h-full w-full" {...otherProps} />
</div>
);
};
@@ -0,0 +1,59 @@
import { useRef, useEffect } from 'react';
export const ChatwootMessageListWrapper = ({
conversationId,
className = '',
style = {},
onError = null,
onLoad = null,
...otherProps
}) => {
const elementRef = useRef();
// Update Web Component props when React props change
useEffect(() => {
if (!elementRef.current) return;
const element = elementRef.current;
// Set conversation ID on the Web Component
element.conversationId = conversationId;
}, [conversationId]);
// Handle Web Component events
useEffect(() => {
if (!elementRef.current) return;
const element = elementRef.current;
const handleLoad = () => {
onLoad?.();
};
const handleError = (event) => {
const errorMessage = event.detail?.message || 'Unknown error occurred';
onError?.(new Error(errorMessage));
};
// Listen for custom events from the Web Component
element.addEventListener('chatwoot:loaded', handleLoad);
element.addEventListener('chatwoot:error', handleError);
return () => {
element.removeEventListener('chatwoot:loaded', handleLoad);
element.removeEventListener('chatwoot:error', handleError);
};
}, [onLoad, onError]);
// Render the Web Component
// Global setup is handled by ChatwootProvider
return (
<chatwoot-message-list
ref={elementRef}
className={className}
style={style}
{...otherProps}
/>
);
};
@@ -0,0 +1,113 @@
import { createContext, useContext, useEffect, useRef, useState } from 'react';
import { registerVueWebComponents } from '../vue-components/registerWebComponents';
import store from '../../../dashboard/store';
import constants from '../../../dashboard/constants/globals';
import axios from 'axios';
import createAxios from '../../../ui/axios';
import commonHelpers from '../../../dashboard/helper/commons';
import vueActionCable from '../../../dashboard/helper/actionCable';
const ChatwootContext = createContext();
export const ChatwootProvider = ({
baseURL,
accountId,
userToken,
websocketURL,
pubsubToken,
conversationId,
disableUpload,
disableEditor,
disablePrivateNote,
children,
}) => {
const isInitialized = useRef(false);
const [initializationComplete, setInitializationComplete] = useState(false);
// Validate required props
if (!baseURL) {
throw new Error('ChatwootProvider: baseURL is required');
}
if (!userToken) {
throw new Error('ChatwootProvider: userToken is required');
}
// Configuration object passed to all child components
const config = {
baseURL: baseURL.replace(/\/$/, ''), // Remove trailing slash
userToken,
accountId: Number(accountId),
conversationId: Number(conversationId),
disableEditor: disableEditor || false,
disablePrivateNote: disablePrivateNote || false,
disableUpload: disableUpload || false,
websocketURL: websocketURL,
pubsubToken: pubsubToken,
};
function initializeChatwootGlobals() {
// Register Web Components
registerVueWebComponents();
// Set up global variables that Vue components expect
/* eslint-disable no-underscore-dangle */
window.__WOOT_API_HOST__ = config.baseURL;
window.__WOOT_ACCOUNT_ID__ = config.accountId;
window.__WOOT_ACCESS_TOKEN__ = config.userToken;
window.__WEBSOCKET_URL__ = config.websocketURL;
window.__PUBSUB_TOKEN__ = config.pubsubToken;
window.__WOOT_CONVERSATION_ID__ = config.conversationId;
window.__EDITOR_DISABLE_UPLOAD__ = config.disableUpload;
window.__EDITOR_DISABLE_PRIVATE_NOTE__ = config.disablePrivateNote;
window.__DISABLE_EDITOR__ = config.disableEditor;
window.__WOOT_ISOLATED_SHELL__ = true;
/* eslint-enable no-underscore-dangle */
// Initialize common helpers
commonHelpers();
// Set up global objects
// eslint-disable-next-line no-underscore-dangle
window.WootConstants = constants;
window.axios = createAxios(axios);
// Initialize user in store and ActionCable
store.dispatch('setUser').then(() => {
vueActionCable.init(store, config.pubsubToken);
setInitializationComplete(true);
});
}
useEffect(() => {
if (isInitialized.current) return;
initializeChatwootGlobals();
isInitialized.current = true;
}, [
config.baseURL,
config.userToken,
config.websocketURL,
config.pubsubToken,
]);
if (!initializationComplete) {
return null;
}
return (
<ChatwootContext.Provider value={config}>
{children}
</ChatwootContext.Provider>
);
};
export const useChatwoot = () => {
const context = useContext(ChatwootContext);
if (!context) {
throw new Error('useChatwoot must be used within ChatwootProvider');
}
return context;
};
// For backwards compatibility and explicit configuration access
export const useChawootConfig = useChatwoot;
@@ -0,0 +1,8 @@
// Export all React components for the Chatwoot React Components library
// Main API components
export { ChatwootProvider, useChatwoot } from './components/ChatwootProvider';
export { ChatwootConversation } from './components/ChatwootConversation';
// Lower-level components (advanced usage)
export { ChatwootMessageListWrapper } from './components/ChatwootMessageListWrapper';
@@ -0,0 +1,49 @@
<script setup>
import MessageList from '../../../ui/MessageList.vue';
</script>
<template>
<MessageList />
</template>
<style>
@import '../../../dashboard/assets/scss/app.scss';
@import 'vue-multiselect/dist/vue-multiselect.css';
@import 'floating-vue/dist/style.css';
.file-uploads {
overflow: hidden;
position: relative;
text-align: center;
display: inline-block;
}
.file-uploads.file-uploads-html4 input,
.file-uploads.file-uploads-html5 label {
/* background fix ie click */
background: #fff;
opacity: 0;
font-size: 20em;
z-index: 1;
top: 0;
left: 0;
right: 0;
bottom: 0;
position: absolute;
width: 100%;
height: 100%;
}
.file-uploads.file-uploads-html5 input,
.file-uploads.file-uploads-html4 label {
/* background fix ie click */
position: absolute;
background: rgba(255, 255, 255, 0);
overflow: hidden;
position: fixed;
width: 1px;
height: 1px;
z-index: -1;
opacity: 0;
}
</style>
@@ -0,0 +1,52 @@
import { defineCustomElement } from 'vue';
import ChatwootMessageListWebComponent from './ChatwootMessageListWebComponent.vue';
import '../../../dashboard/assets/scss/app.scss';
import en from '../../../dashboard/i18n/locale/en';
import store from '../../../dashboard/store';
import vueActionCable from '../../../dashboard/helper/actionCable';
import { createI18n, I18nInjectionKey } from 'vue-i18n';
import VueDOMPurifyHTML from 'vue-dompurify-html';
import { domPurifyConfig } from 'shared/helpers/HTMLSanitizer.js';
const i18n = createI18n({
legacy: false,
locale: 'en',
messages: {
en,
},
});
const ceOptions = {
configureApp(app) {
app.use(store);
app.use(VueDOMPurifyHTML, domPurifyConfig);
// I18n has to be injected inside that can be picked
// up by the compononent, the API stays the same, just use `useI18n`
// https://vue-i18n.intlify.dev/guide/advanced/wc
// Adding this link for my goldfish brain
app.use(i18n);
app.provide(I18nInjectionKey, i18n);
// eslint-disable-next-line no-underscore-dangle
vueActionCable.init(store, window.__PUBSUB_TOKEN__);
},
};
// Convert Vue components to Web Components
const ChatwootMessageListElement = defineCustomElement(
ChatwootMessageListWebComponent,
ceOptions
);
// Register Web Components
export const registerVueWebComponents = () => {
if (!customElements.get('chatwoot-message-list')) {
customElements.define('chatwoot-message-list', ChatwootMessageListElement);
// eslint-disable-next-line no-console
console.log('✅ Registered chatwoot-message-list Web Component');
}
};
// Export for manual registration if needed
export { ChatwootMessageListElement };
+538
View File
@@ -0,0 +1,538 @@
<script>
import { useTemplateRef } from 'vue';
import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import ReplyBottomPanel from 'dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue';
import AttachmentPreview from 'dashboard/components/widgets/AttachmentsPreview.vue';
import ReplyTopPanel from 'dashboard/components/widgets/WootWriter/ReplyTopPanel.vue';
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper';
import inboxMixin from 'shared/mixins/inboxMixin';
import fileUploadMixin from 'dashboard/mixins/fileUploadMixin';
export default {
components: {
AttachmentPreview,
ReplyTopPanel,
ReplyBottomPanel,
WootMessageEditor,
},
mixins: [inboxMixin, fileUploadMixin, keyboardEventListenerMixins],
setup() {
const replyEditor = useTemplateRef('replyEditor');
return { replyEditor };
},
data() {
return {
message: '',
isFocused: false,
attachedFiles: [],
isUploading: false,
replyType: REPLY_EDITOR_MODES.REPLY,
hasSlashCommand: false,
doAutoSaveDraft: () => {},
updateEditorSelectionWith: '',
};
},
computed: {
...mapGetters({
currentChat: 'getSelectedChat',
currentUser: 'getCurrentUser',
globalConfig: 'globalConfig/get',
}),
currentContact() {
return this.$store.getters['contacts/getContact'](
this.currentChat.meta.sender.id
);
},
showRichContentEditor() {
return true;
},
isPrivate() {
// if the current chat is not loaded, assume we can reply
// this avoids rendering the editor with is-private yellow bg
// optimisitaclly defaulting to reply editor
if (!this.currentChat || !Object.keys(this.currentChat).length) {
return false;
}
if (this.currentChat.can_reply) {
return this.isOnPrivateNote;
}
return true;
},
inboxId() {
return this.currentChat.inbox_id;
},
inbox() {
return this.$store.getters['inboxes/getInbox'](this.inboxId);
},
messagePlaceHolder() {
return this.isPrivate
? this.$t('CONVERSATION.FOOTER.PRIVATE_MSG_INPUT')
: this.$t('CONVERSATION.FOOTER.MSG_INPUT');
},
isMessageLengthReachingThreshold() {
return this.message.length > this.maxLength - 50;
},
charactersRemaining() {
return this.maxLength - this.message.length;
},
isReplyButtonDisabled() {
if (this.hasAttachments || this.hasRecordedAudio) return false;
return (
this.isMessageEmpty ||
this.message.length === 0 ||
this.message.length > this.maxLength
);
},
sender() {
return {
name: this.currentUser.name,
thumbnail: this.currentUser.avatar_url,
};
},
conversationType() {
const { additional_attributes: additionalAttributes } = this.currentChat;
const type = additionalAttributes ? additionalAttributes.type : '';
return type || '';
},
maxLength() {
return MESSAGE_MAX_LENGTH.GENERAL;
},
allowPrivateNote() {
// eslint-disable-next-line no-underscore-dangle
return window.__EDITOR_DISABLE_PRIVATE_NOTE__ !== true;
},
isEditorDisabled() {
// eslint-disable-next-line no-underscore-dangle
return window.__DISABLE_EDITOR__;
},
allowFileUpload() {
// eslint-disable-next-line no-underscore-dangle
return window.__EDITOR_DISABLE_UPLOAD__ !== true;
},
replyButtonLabel() {
if (this.isPrivate) {
return this.$t('CONVERSATION.REPLYBOX.CREATE');
}
return this.$t('CONVERSATION.REPLYBOX.SEND');
},
replyBoxClass() {
return {
'is-private': this.isPrivate && this.allowPrivateNote,
'is-focused': this.isFocused || this.hasAttachments,
'pointer-events-none grayscale opacity-70': this.isEditorDisabled,
};
},
hasAttachments() {
return this.attachedFiles.length;
},
isOnPrivateNote() {
if (!this.allowPrivateNote) return false;
return this.replyType === REPLY_EDITOR_MODES.NOTE;
},
isOnExpandedLayout() {
return false;
},
isMessageEmpty() {
if (!this.message) {
return true;
}
return !this.message.trim().replace(/\n/g, '').length;
},
showReplyHead() {
return !this.isOnPrivateNote;
},
enableMultipleFileUpload() {
return true;
},
editorMessageKey() {
const { editor_message_key: isEnabled } = this.uiSettings;
return isEnabled;
},
conversationId() {
return this.currentChat.id;
},
editorStateId() {
return `draft-${this.conversationId}-${this.replyType}`;
},
},
mounted() {
document.addEventListener('paste', this.onPaste);
document.addEventListener('keydown', this.handleKeyEvents);
// // A hacky fix to solve the drag and drop
// // Is showing on top of new conversation modal drag and drop
// // TODO need to find a better solution
// emitter.on(
// BUS_EVENTS.NEW_CONVERSATION_MODAL,
// this.onNewConversationModalActive
// );
// emitter.on(BUS_EVENTS.INSERT_INTO_NORMAL_EDITOR, this.addIntoEditor);
},
unmounted() {
document.removeEventListener('paste', this.onPaste);
document.removeEventListener('keydown', this.handleKeyEvents);
},
methods: {
getElementToBind() {
return this.replyEditor;
},
onPaste(e) {
const data = e.clipboardData.files;
if (!this.showRichContentEditor && data.length !== 0) {
this.$refs.messageInput.$el.blur();
}
if (!data.length || !data[0]) {
return;
}
data.forEach(file => {
const { name, type, size } = file;
this.onFileUpload({ name, type, size, file: file });
});
},
confirmOnSendReply() {
if (this.isReplyButtonDisabled) {
return;
}
if (!this.showMentions) {
const messagePayload = this.getMessagePayload(this.message);
this.sendMessage(messagePayload);
this.clearMessage();
}
},
async onSendReply() {
this.confirmOnSendReply();
},
async sendMessage(messagePayload) {
try {
await this.$store.dispatch(
'createPendingMessageAndSend',
messagePayload
);
// emitter.emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
// emitter.emit(BUS_EVENTS.MESSAGE_SENT);
} catch (error) {
const errorMessage =
error?.response?.data?.error || this.$t('CONVERSATION.MESSAGE_ERROR');
useAlert(errorMessage);
}
},
setReplyMode(mode = REPLY_EDITOR_MODES.REPLY) {
if (!this.allowPrivateNote) return;
const { can_reply: canReply } = this.currentChat;
this.$store.dispatch('draftMessages/setReplyEditorMode', {
mode,
});
if (canReply) this.replyType = mode;
this.$nextTick(() => {
// This block addresses a scrolling issue on iOS Safari when the virtual
// keyboard opens and can obscure the focused ProseMirror editor.
// This behavior is primarily an iOS Safari quirk related to how it handles
// the viewport with the virtual keyboard. It's not something we
// directly control or can easily fix at the component levels.
//
// `this.$nextTick()`: Ensures this code runs after Vue has finished its
// DOM update cycle. This is crucial if the editor's visibility or focus
// was just programmatically changed, guaranteeing we operate on the
// finalized DOM.
//
// `setTimeout(() => { ... }, 300)`: A delay is necessary because iOS
// Safari needs time for the virtual keyboard to fully animate into view
// and for the page layout to adjust. Attempting to scroll immediately
// (even after $nextTick) often fails as the keyboard isn't yet fully
// present or the viewport dimensions haven't updated. The 300ms is a
// common heuristic.
//
// The `scrollIntoView()` method then attempts to bring the editor
// into the visible part of the viewport. This manual scroll is a
// pragmatic workaround for this specific iOS Safari behavior.
setTimeout(() => {
document.querySelector('.ProseMirror')?.scrollIntoView();
}, 300);
});
if (this.showRichContentEditor) {
if (this.isRecordingAudio) {
this.toggleAudioRecorder();
}
return;
}
this.$nextTick(() => this.$refs.messageInput.focus());
},
clearEditorSelection() {
this.updateEditorSelectionWith = '';
},
insertIntoTextEditor(text, selectionStart, selectionEnd) {
const { message } = this;
const newMessage =
message.slice(0, selectionStart) +
text +
message.slice(selectionEnd, message.length);
this.message = newMessage;
},
addIntoEditor(content) {
if (this.showRichContentEditor) {
this.updateEditorSelectionWith = content;
this.onFocus();
}
if (!this.showRichContentEditor) {
const { selectionStart, selectionEnd } = this.$refs.messageInput.$el;
this.insertIntoTextEditor(content, selectionStart, selectionEnd);
}
},
clearMessage() {
this.message = '';
this.attachedFiles = [];
this.isRecordingAudio = false;
},
onTypingOn() {
this.toggleTyping('on');
},
onTypingOff() {
this.toggleTyping('off');
},
onBlur() {
this.isFocused = false;
},
onFocus() {
this.isFocused = true;
},
toggleTyping(status) {
const conversationId = this.currentChat.id;
const isPrivate = this.isPrivate;
if (!conversationId) {
return;
}
this.$store.dispatch('conversationTypingStatus/toggleTyping', {
status,
conversationId,
isPrivate,
});
},
attachFile({ blob, file }) {
const reader = new FileReader();
reader.readAsDataURL(file.file);
reader.onloadend = () => {
this.attachedFiles.push({
currentChatId: this.currentChat.id,
resource: blob || file,
isPrivate: this.isPrivate,
thumb: reader.result,
blobSignedId: blob ? blob.signed_id : undefined,
isRecordedAudio: file?.isRecordedAudio || false,
});
};
},
removeAttachment(attachments) {
this.attachedFiles = attachments;
},
setReplyToInPayload(payload) {
if (this.inReplyTo?.id) {
return {
...payload,
contentAttributes: {
...payload.contentAttributes,
in_reply_to: this.inReplyTo.id,
},
};
}
return payload;
},
getMessagePayload(message) {
let messagePayload = {
conversationId: this.currentChat.id,
message,
private: this.isPrivate,
sender: this.sender,
};
messagePayload = this.setReplyToInPayload(messagePayload);
if (this.attachedFiles && this.attachedFiles.length) {
messagePayload.files = [];
this.attachedFiles.forEach(attachment => {
if (this.globalConfig.directUploadsEnabled) {
messagePayload.files.push(attachment.blobSignedId);
} else {
messagePayload.files.push(attachment.resource.file);
}
});
}
return messagePayload;
},
getKeyboardEvents() {
return {
'$mod+Enter': {
action: this.onSendReply,
allowOnFocusedInput: true,
},
};
},
},
};
</script>
<template>
<div ref="replyEditor" class="reply-box" :class="replyBoxClass">
<ReplyTopPanel
v-if="allowPrivateNote"
:mode="replyType"
disable-popout
:is-message-length-reaching-threshold="isMessageLengthReachingThreshold"
:characters-remaining="charactersRemaining"
@set-reply-mode="setReplyMode"
/>
<div class="reply-box__top">
<WootMessageEditor
v-model="message"
:editor-id="editorStateId"
:disabled="isEditorDisabled"
class="input"
:is-private="isOnPrivateNote"
:placeholder="messagePlaceHolder"
:update-selection-with="updateEditorSelectionWith"
:min-height="4"
enable-variables
:variables="messageVariables"
:signature="signatureToApply"
allow-signature
:channel-type="channelType"
@typing-off="onTypingOff"
@typing-on="onTypingOn"
@focus="onFocus"
@blur="onBlur"
@toggle-user-mention="toggleUserMention"
@toggle-canned-menu="toggleCannedMenu"
@toggle-variables-menu="toggleVariablesMenu"
@clear-selection="clearEditorSelection"
/>
</div>
<div
v-if="hasAttachments && !showAudioRecorderEditor"
class="attachment-preview-box"
@paste="onPaste"
>
<AttachmentPreview
class="flex-col mt-4"
:attachments="attachedFiles"
@remove-attachment="removeAttachment"
/>
</div>
<ReplyBottomPanel
:conversation-id="conversationId"
:inbox="inbox"
:is-on-private-note="isOnPrivateNote"
:is-send-disabled="isReplyButtonDisabled"
:mode="replyType"
:on-file-upload="onFileUpload"
:on-send="onSendReply"
:conversation-type="conversationType"
:send-button-text="replyButtonLabel"
:show-audio-recorder="showAudioRecorder"
:show-editor-toggle="isAPIInbox && !isOnPrivateNote"
:show-emoji-picker="false"
:show-file-upload="allowFileUpload"
:message="message"
:enable-multiple-file-upload="allowFileUpload"
:allow-file-upload="allowFileUpload"
/>
</div>
</template>
<style lang="scss" scoped>
.send-button {
@apply mb-0;
}
.banner--self-assign {
@apply py-2;
}
.attachment-preview-box {
@apply bg-transparent py-0 px-4;
}
.reply-box {
transition: height 2s cubic-bezier(0.37, 0, 0.63, 1);
@apply relative mb-2 mx-2 border border-n-weak rounded-xl bg-n-solid-1;
&.is-private {
@apply bg-n-solid-amber dark:border-n-amber-3/10 border-n-amber-12/5;
}
}
/* in safari the list marker is truncated */
/* We could add, the following, but in Safari, it behaves weirdly with the curosr */
/* ::v-deep .ProseMirror li {
list-style-position: inside !important;
} */
@supports (-webkit-hyphens: none) {
::v-deep .ProseMirror ul {
margin-left: 2ch !important;
}
::v-deep .ProseMirror ol {
margin-left: 2ch !important;
}
}
.send-button {
@apply mb-0;
}
.reply-box__top {
@apply relative py-0 px-4 -mt-px;
textarea {
@apply shadow-none outline-none border-transparent bg-transparent m-0 max-h-60 min-h-[3rem] pt-4 pb-0 px-0 resize-none;
}
}
.emoji-dialog {
@apply top-[unset] -bottom-10 -left-80 right-[unset];
&::before {
transform: rotate(270deg);
filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.08));
@apply -right-4 bottom-2 rtl:right-0 rtl:-left-4;
}
}
.emoji-dialog--rtl {
@apply left-[unset] -right-80;
&::before {
transform: rotate(90deg);
filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.08));
}
}
.emoji-dialog--expanded {
@apply left-[unset] bottom-0 absolute z-[100];
&::before {
transform: rotate(0deg);
@apply left-1 -bottom-2;
}
}
.normal-editor__canned-box {
width: calc(100% - 2 * var(--space-normal));
left: var(--space-normal);
}
</style>
+149
View File
@@ -0,0 +1,149 @@
<script setup>
import { ref, onMounted, computed, watch, useTemplateRef } from 'vue';
import Message from 'next/message/Message.vue';
import TypingIndicator from 'next/message/TypingIndicator.vue';
import Snipper from 'next/spinner/Spinner.vue';
import LiteReplyBox from './LiteReplyBox.vue';
import {
useStore,
useMapGetter,
useFunctionGetter,
//
} from '../dashboard/composables/store';
import { getTypingUsersText } from '../dashboard/helper/commons';
import { useCamelCase } from '../dashboard/composables/useTransformKeys';
import { useInfiniteScroll, useThrottleFn } from '@vueuse/core';
import { useI18n } from 'vue-i18n';
const conversationId = computed(() => {
// eslint-disable-next-line no-underscore-dangle
return window.__WOOT_CONVERSATION_ID__;
});
const { t } = useI18n();
const messageListRef = useTemplateRef('messageListRef');
const store = useStore();
const isAllLoaded = useMapGetter('getAllMessagesLoaded');
const isFetching = ref(false);
const typingUserList = useFunctionGetter(
'conversationTypingStatus/getUserList',
conversationId.value
);
const isAnyoneTyping = computed(() => {
return typingUserList.value.length > 0;
});
const typingUserNames = computed(() => {
if (!isAnyoneTyping.value) return '';
const [i18nKey, params] = getTypingUsersText(typingUserList.value);
// eslint-disable-next-line @intlify/vue-i18n/no-dynamic-keys
return t(i18nKey, params);
});
const conversation = computed(() => {
return store.getters.getConversationById(conversationId.value);
});
const allMessages = computed(() => {
if (!conversation.value) return [];
return useCamelCase(conversation.value.messages, { deep: true }).reverse();
});
const disablePrivateNote = computed(() => {
// eslint-disable-next-line no-underscore-dangle
return window.__EDITOR_DISABLE_PRIVATE_NOTE__ === true;
});
const fetchMore = async () => {
if (isFetching.value) return;
if (!conversation?.value?.id) return;
if (!allMessages.value?.length) return;
try {
isFetching.value = true;
await store.dispatch('fetchPreviousMessages', {
conversationId: conversation.value.id,
before: allMessages.value[allMessages.value.length - 1].id,
});
} finally {
isFetching.value = false;
}
};
onMounted(async () => {
await store.dispatch('inboxes/get');
await Promise.all([
store.dispatch('getConversation', conversationId.value),
store.dispatch('fetchAllAttachments', conversationId.value),
]);
});
watch(conversation, () => {
store.dispatch('setActiveChat', {
data: {
id: conversation.value.id,
},
});
});
useInfiniteScroll(messageListRef, useThrottleFn(fetchMore, 1000), {
canLoadMore: () => {
return !isAllLoaded.value;
},
distance: 10,
direction: 'top',
});
</script>
<template>
<div class="relative">
<ul
ref="messageListRef"
class="px-4 pt-4 flex flex-col-reverse bg-n-background overflow-scroll h-screen"
:class="{
'pb-60': !disablePrivateNote,
'pb-48': disablePrivateNote,
}"
>
<div
v-if="isAnyoneTyping"
id="conversationFooter"
class="my-2 py-2 flex items-center w-full"
>
<div
class="flex py-2 px-4 shadow-md rounded-full bg-white dark:bg-slate-700 text-n-slate-11 text-xs font-semibold mx-auto items-center gap-1"
>
{{ typingUserNames }}
<TypingIndicator class="text-n-slate-9" />
</div>
</div>
<template v-for="message in allMessages" :key="message.id">
<Message
v-bind="message"
:is-email-inbox="isAnEmailChannel"
:group-with-next="false"
:inbox-supports-reply-to="inboxSupportsReplyTo"
:current-user-id="currentUserId"
/>
</template>
<div v-show="isFetching" class="w-full py-4">
<Snipper class="mx-auto" />
</div>
</ul>
<div class="p-2 w-full bg-white absolute bottom-0">
<LiteReplyBox />
</div>
</div>
</template>
<style>
/* this will disable elastic scrolling on the y axis */
* {
overscroll-behavior-y: none;
}
</style>
+19
View File
@@ -0,0 +1,19 @@
const parseErrorCode = error => Promise.reject(error);
export default axios => {
// eslint-disable-next-line no-underscore-dangle
const apiHost = window.__WOOT_API_HOST__;
// eslint-disable-next-line no-underscore-dangle
const accessToken = window.__WOOT_ACCESS_TOKEN__;
const wootApi = axios.create({ baseURL: `${apiHost}/` });
Object.assign(wootApi.defaults.headers.common, {
api_access_token: accessToken,
});
wootApi.interceptors.response.use(
response => response,
error => parseErrorCode(error)
);
return wootApi;
};
+46
View File
@@ -0,0 +1,46 @@
# Chatwoot UI Components Usage Guide
This document explains how to use the Chatwoot UI components library, which can be used both as Web Components (Custom Elements) and as Vue components.
## Installation
```bash
npm install @chatwoot/ui
# or
yarn add @chatwoot/ui
# or
pnpm add @chatwoot/ui
```
## Usage
### As Web Components (Custom Elements)
Web Components can be used in any HTML page or application, regardless of the framework.
```html
<!-- Include the built JS file -->
<script src="https://unpkg.com/@chatwoot/ui/dist/ui.js"></script>
<!-- Use the custom element anywhere in your HTML -->
<chat-button label="Click me"></chat-button>
```
When the script loads, it automatically registers all custom elements with the browser, making them immediately available for use.
#### Accessing the Store in Web Components
The components have access to a global store instance. When using Web Components, the store is available through `window.__CHATWOOT_STORE__`.
You can interact with the store from your JavaScript:
```javascript
// Access the store
const store = window.__CHATWOOT_STORE__;
// Check store state
console.log(store.state.someData);
// Dispatch actions
store.dispatch('someAction', { data: 'example' });
```
+7
View File
@@ -13,6 +13,10 @@
"dev": "overmind start -f ./Procfile.dev",
"ruby:prettier": "bundle exec rubocop -a",
"build:sdk": "BUILD_MODE=library vite build",
"build:ui": "BUILD_MODE=ui vite build",
"build:react": "BUILD_MODE=react-components vite build",
"dev:react": "BUILD_MODE=react-components vite build --watch",
"package:react": "node scripts/package-react-components.js",
"prepare": "husky install",
"size": "size-limit",
"story:dev": "histoire dev",
@@ -115,6 +119,7 @@
"@iconify-json/teenyicons": "^1.2.1",
"@intlify/eslint-plugin-vue-i18n": "^3.2.0",
"@size-limit/file": "^8.2.4",
"@vitejs/plugin-react": "^4.5.2",
"@vitest/coverage-v8": "3.0.5",
"@vue/test-utils": "^2.4.6",
"autoprefixer": "^10.4.20",
@@ -136,6 +141,8 @@
"postcss-preset-env": "^8.5.1",
"prettier": "^3.3.3",
"prosemirror-model": "^1.22.3",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"size-limit": "^8.2.4",
"tailwindcss": "^3.4.13",
"vite": "^5.4.19",
+361
View File
@@ -260,6 +260,9 @@ importers:
'@size-limit/file':
specifier: ^8.2.4
version: 8.2.6(size-limit@8.2.6)
'@vitejs/plugin-react':
specifier: ^4.5.2
version: 4.5.2(vite@5.4.19(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))
'@vitest/coverage-v8':
specifier: 3.0.5
version: 3.0.5(vitest@3.0.5(@types/node@22.7.0)(jsdom@24.1.3)(sass@1.79.3)(terser@5.33.0))
@@ -323,6 +326,12 @@ importers:
prosemirror-model:
specifier: ^1.22.3
version: 1.22.3
react:
specifier: ^19.1.0
version: 19.1.0
react-dom:
specifier: ^19.1.0
version: 19.1.0(react@19.1.0)
size-limit:
specifier: ^8.2.4
version: 8.2.6
@@ -363,14 +372,64 @@ packages:
'@antfu/utils@0.7.10':
resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==}
'@babel/code-frame@7.27.1':
resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
engines: {node: '>=6.9.0'}
'@babel/compat-data@7.27.5':
resolution: {integrity: sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==}
engines: {node: '>=6.9.0'}
'@babel/core@7.27.4':
resolution: {integrity: sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==}
engines: {node: '>=6.9.0'}
'@babel/generator@7.27.5':
resolution: {integrity: sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==}
engines: {node: '>=6.9.0'}
'@babel/helper-compilation-targets@7.27.2':
resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==}
engines: {node: '>=6.9.0'}
'@babel/helper-module-imports@7.27.1':
resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==}
engines: {node: '>=6.9.0'}
'@babel/helper-module-transforms@7.27.3':
resolution: {integrity: sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
'@babel/helper-plugin-utils@7.27.1':
resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==}
engines: {node: '>=6.9.0'}
'@babel/helper-string-parser@7.25.9':
resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==}
engines: {node: '>=6.9.0'}
'@babel/helper-string-parser@7.27.1':
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-identifier@7.25.9':
resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-identifier@7.27.1':
resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-option@7.27.1':
resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
engines: {node: '>=6.9.0'}
'@babel/helpers@7.27.6':
resolution: {integrity: sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==}
engines: {node: '>=6.9.0'}
'@babel/parser@7.25.6':
resolution: {integrity: sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==}
engines: {node: '>=6.0.0'}
@@ -381,6 +440,23 @@ packages:
engines: {node: '>=6.0.0'}
hasBin: true
'@babel/parser@7.27.5':
resolution: {integrity: sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==}
engines: {node: '>=6.0.0'}
hasBin: true
'@babel/plugin-transform-react-jsx-self@7.27.1':
resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
'@babel/plugin-transform-react-jsx-source@7.27.1':
resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
'@babel/runtime@7.25.6':
resolution: {integrity: sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==}
engines: {node: '>=6.9.0'}
@@ -389,10 +465,22 @@ packages:
resolution: {integrity: sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==}
engines: {node: '>=6.9.0'}
'@babel/template@7.27.2':
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
engines: {node: '>=6.9.0'}
'@babel/traverse@7.27.4':
resolution: {integrity: sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==}
engines: {node: '>=6.9.0'}
'@babel/types@7.26.0':
resolution: {integrity: sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==}
engines: {node: '>=6.9.0'}
'@babel/types@7.27.6':
resolution: {integrity: sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==}
engines: {node: '>=6.9.0'}
'@bcoe/v8-coverage@1.0.2':
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
engines: {node: '>=18'}
@@ -1039,6 +1127,9 @@ packages:
'@rails/ujs@7.1.400':
resolution: {integrity: sha512-YwvXm3BR5tn+VCAKYGycLejMRVZE3Ionj5gFjEeGXCZnI0Rpi+7dKpmyu90kdUY7dRUFpHTdu9zZceEzFLl38w==}
'@rolldown/pluginutils@1.0.0-beta.11':
resolution: {integrity: sha512-L/gAA/hyCSuzTF1ftlzUSI/IKr2POHsv1Dd78GfqkR83KMNuswWD61JxGV2L7nRwBBBSDr6R1gCkdTmoN7W4ag==}
'@rollup/rollup-android-arm-eabi@4.40.2':
resolution: {integrity: sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==}
cpu: [arm]
@@ -1746,6 +1837,18 @@ packages:
resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==}
engines: {node: '>= 10'}
'@types/babel__core@7.20.5':
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
'@types/babel__generator@7.27.0':
resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==}
'@types/babel__template@7.4.4':
resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==}
'@types/babel__traverse@7.20.7':
resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==}
'@types/estree@1.0.7':
resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
@@ -1796,6 +1899,12 @@ packages:
'@videojs/xhr@2.6.0':
resolution: {integrity: sha512-7J361GiN1tXpm+gd0xz2QWr3xNWBE+rytvo8J3KuggFaLg+U37gZQ2BuPLcnkfGffy2e+ozY70RHC8jt7zjA6Q==}
'@vitejs/plugin-react@4.5.2':
resolution: {integrity: sha512-QNVT3/Lxx99nMQWJWF7K4N6apUEuT0KlZA3mx/mVaoGj3smm/8rc8ezz15J1pcbcjDK0V15rpHetVfya08r76Q==}
engines: {node: ^14.18.0 || >=16.0.0}
peerDependencies:
vite: 5.4.19
'@vitejs/plugin-vue@5.1.4':
resolution: {integrity: sha512-N2XSI2n3sQqp5w7Y/AN/L2XDjBIRGqXko+eDp42sydYSBeJuSm5a1sLf8zakmo8u7tA8NmBgoDLA1HeOESjp9A==}
engines: {node: ^18.0.0 || >=20.0.0}
@@ -2162,6 +2271,11 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
browserslist@4.25.0:
resolution: {integrity: sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
@@ -2206,6 +2320,9 @@ packages:
caniuse-lite@1.0.30001651:
resolution: {integrity: sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==}
caniuse-lite@1.0.30001721:
resolution: {integrity: sha512-cOuvmUVtKrtEaoKiO0rSc29jcjwMwX5tOHDy4MgVFEWiUXj4uBMJkwI8MDySkgXidpMiHUcviogAvFi4pA2hDQ==}
capital-case@1.0.4:
resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==}
@@ -2332,6 +2449,9 @@ packages:
constant-case@3.0.4:
resolution: {integrity: sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==}
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
core-js@3.38.1:
resolution: {integrity: sha512-OP35aUorbU3Zvlx7pjsFdu1rGNnD4pgw/CWoYzRY3t2EzoVT7shKHY1dlAy3f41cGIO7ZDPQimhGFTlEYkG/Hw==}
@@ -2573,6 +2693,9 @@ packages:
electron-to-chromium@1.5.13:
resolution: {integrity: sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q==}
electron-to-chromium@1.5.166:
resolution: {integrity: sha512-QPWqHL0BglzPYyJJ1zSSmwFFL6MFXhbACOCcsCdUMCkzPdS9/OIBVxg516X/Ado2qwAq8k0nJJ7phQPCqiaFAw==}
email-validator@2.0.4:
resolution: {integrity: sha512-gYCwo7kh5S3IDyZPLZf6hSS0MnZT8QmJFqYvbqlDZSbwdZlY6QZWxJ4i/6UhITOJ4XzyI647Bm2MXKCLqnJ4nQ==}
engines: {node: '>4.0'}
@@ -2948,6 +3071,10 @@ packages:
functions-have-names@1.2.3:
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
gensync@1.0.0-beta.2:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'}
get-caller-file@2.0.5:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
@@ -3003,6 +3130,10 @@ packages:
global@4.4.0:
resolution: {integrity: sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==}
globals@11.12.0:
resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
engines: {node: '>=4'}
globals@13.24.0:
resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==}
engines: {node: '>=8'}
@@ -3377,6 +3508,9 @@ packages:
resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
engines: {node: '>=14'}
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
js-yaml@3.14.1:
resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==}
hasBin: true
@@ -3403,6 +3537,11 @@ packages:
canvas:
optional: true
jsesc@3.1.0:
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
engines: {node: '>=6'}
hasBin: true
json-buffer@3.0.1:
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
@@ -3547,6 +3686,9 @@ packages:
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
lru-cache@6.0.0:
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
engines: {node: '>=10'}
@@ -3751,6 +3893,9 @@ packages:
node-releases@2.0.18:
resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==}
node-releases@2.0.19:
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
nopt@7.2.1:
resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
@@ -4280,6 +4425,19 @@ packages:
resolution: {integrity: sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==}
engines: {node: '>=12'}
react-dom@19.1.0:
resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==}
peerDependencies:
react: ^19.1.0
react-refresh@0.17.0:
resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
engines: {node: '>=0.10.0'}
react@19.1.0:
resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}
engines: {node: '>=0.10.0'}
read-cache@1.0.0:
resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
@@ -4398,6 +4556,9 @@ packages:
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
engines: {node: '>=v12.22.7'}
scheduler@0.26.0:
resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
sdp@3.2.0:
resolution: {integrity: sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw==}
@@ -4815,6 +4976,12 @@ packages:
peerDependencies:
browserslist: '>= 4.21.0'
update-browserslist-db@1.1.3:
resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==}
hasBin: true
peerDependencies:
browserslist: '>= 4.21.0'
upper-case-first@2.0.2:
resolution: {integrity: sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==}
@@ -5150,6 +5317,9 @@ packages:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
yallist@4.0.0:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
@@ -5202,10 +5372,83 @@ snapshots:
'@antfu/utils@0.7.10': {}
'@babel/code-frame@7.27.1':
dependencies:
'@babel/helper-validator-identifier': 7.27.1
js-tokens: 4.0.0
picocolors: 1.1.1
'@babel/compat-data@7.27.5': {}
'@babel/core@7.27.4':
dependencies:
'@ampproject/remapping': 2.3.0
'@babel/code-frame': 7.27.1
'@babel/generator': 7.27.5
'@babel/helper-compilation-targets': 7.27.2
'@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.4)
'@babel/helpers': 7.27.6
'@babel/parser': 7.27.5
'@babel/template': 7.27.2
'@babel/traverse': 7.27.4
'@babel/types': 7.27.6
convert-source-map: 2.0.0
debug: 4.4.0
gensync: 1.0.0-beta.2
json5: 2.2.3
semver: 6.3.1
transitivePeerDependencies:
- supports-color
'@babel/generator@7.27.5':
dependencies:
'@babel/parser': 7.27.5
'@babel/types': 7.27.6
'@jridgewell/gen-mapping': 0.3.8
'@jridgewell/trace-mapping': 0.3.25
jsesc: 3.1.0
'@babel/helper-compilation-targets@7.27.2':
dependencies:
'@babel/compat-data': 7.27.5
'@babel/helper-validator-option': 7.27.1
browserslist: 4.25.0
lru-cache: 5.1.1
semver: 6.3.1
'@babel/helper-module-imports@7.27.1':
dependencies:
'@babel/traverse': 7.27.4
'@babel/types': 7.27.6
transitivePeerDependencies:
- supports-color
'@babel/helper-module-transforms@7.27.3(@babel/core@7.27.4)':
dependencies:
'@babel/core': 7.27.4
'@babel/helper-module-imports': 7.27.1
'@babel/helper-validator-identifier': 7.27.1
'@babel/traverse': 7.27.4
transitivePeerDependencies:
- supports-color
'@babel/helper-plugin-utils@7.27.1': {}
'@babel/helper-string-parser@7.25.9': {}
'@babel/helper-string-parser@7.27.1': {}
'@babel/helper-validator-identifier@7.25.9': {}
'@babel/helper-validator-identifier@7.27.1': {}
'@babel/helper-validator-option@7.27.1': {}
'@babel/helpers@7.27.6':
dependencies:
'@babel/template': 7.27.2
'@babel/types': 7.27.6
'@babel/parser@7.25.6':
dependencies:
'@babel/types': 7.26.0
@@ -5214,6 +5457,20 @@ snapshots:
dependencies:
'@babel/types': 7.26.0
'@babel/parser@7.27.5':
dependencies:
'@babel/types': 7.27.6
'@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.27.4)':
dependencies:
'@babel/core': 7.27.4
'@babel/helper-plugin-utils': 7.27.1
'@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.27.4)':
dependencies:
'@babel/core': 7.27.4
'@babel/helper-plugin-utils': 7.27.1
'@babel/runtime@7.25.6':
dependencies:
regenerator-runtime: 0.14.1
@@ -5222,11 +5479,34 @@ snapshots:
dependencies:
regenerator-runtime: 0.14.1
'@babel/template@7.27.2':
dependencies:
'@babel/code-frame': 7.27.1
'@babel/parser': 7.27.5
'@babel/types': 7.27.6
'@babel/traverse@7.27.4':
dependencies:
'@babel/code-frame': 7.27.1
'@babel/generator': 7.27.5
'@babel/parser': 7.27.5
'@babel/template': 7.27.2
'@babel/types': 7.27.6
debug: 4.4.0
globals: 11.12.0
transitivePeerDependencies:
- supports-color
'@babel/types@7.26.0':
dependencies:
'@babel/helper-string-parser': 7.25.9
'@babel/helper-validator-identifier': 7.25.9
'@babel/types@7.27.6':
dependencies:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.27.1
'@bcoe/v8-coverage@1.0.2': {}
'@breezystack/lamejs@1.2.7': {}
@@ -5944,6 +6224,8 @@ snapshots:
'@rails/ujs@7.1.400': {}
'@rolldown/pluginutils@1.0.0-beta.11': {}
'@rollup/rollup-android-arm-eabi@4.40.2':
optional: true
@@ -6744,6 +7026,27 @@ snapshots:
'@tootallnate/once@2.0.0': {}
'@types/babel__core@7.20.5':
dependencies:
'@babel/parser': 7.26.2
'@babel/types': 7.26.0
'@types/babel__generator': 7.27.0
'@types/babel__template': 7.4.4
'@types/babel__traverse': 7.20.7
'@types/babel__generator@7.27.0':
dependencies:
'@babel/types': 7.26.0
'@types/babel__template@7.4.4':
dependencies:
'@babel/parser': 7.26.2
'@babel/types': 7.26.0
'@types/babel__traverse@7.20.7':
dependencies:
'@babel/types': 7.26.0
'@types/estree@1.0.7': {}
'@types/flexsearch@0.7.6': {}
@@ -6802,6 +7105,18 @@ snapshots:
global: 4.4.0
is-function: 1.0.2
'@vitejs/plugin-react@4.5.2(vite@5.4.19(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))':
dependencies:
'@babel/core': 7.27.4
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.4)
'@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.27.4)
'@rolldown/pluginutils': 1.0.0-beta.11
'@types/babel__core': 7.20.5
react-refresh: 0.17.0
vite: 5.4.19(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)
transitivePeerDependencies:
- supports-color
'@vitejs/plugin-vue@5.1.4(vite@5.4.19(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))(vue@3.5.12(typescript@5.6.2))':
dependencies:
vite: 5.4.19(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)
@@ -7294,6 +7609,13 @@ snapshots:
node-releases: 2.0.18
update-browserslist-db: 1.1.0(browserslist@4.23.3)
browserslist@4.25.0:
dependencies:
caniuse-lite: 1.0.30001721
electron-to-chromium: 1.5.166
node-releases: 2.0.19
update-browserslist-db: 1.1.3(browserslist@4.25.0)
buffer-from@1.1.2:
optional: true
@@ -7339,6 +7661,8 @@ snapshots:
caniuse-lite@1.0.30001651: {}
caniuse-lite@1.0.30001721: {}
capital-case@1.0.4:
dependencies:
no-case: 3.0.4
@@ -7492,6 +7816,8 @@ snapshots:
tslib: 2.8.1
upper-case: 2.0.2
convert-source-map@2.0.0: {}
core-js@3.38.1: {}
countries-and-timezones@3.6.0: {}
@@ -7702,6 +8028,8 @@ snapshots:
electron-to-chromium@1.5.13: {}
electron-to-chromium@1.5.166: {}
email-validator@2.0.4: {}
emoji-regex@10.4.0: {}
@@ -8225,6 +8553,8 @@ snapshots:
functions-have-names@1.2.3: {}
gensync@1.0.0-beta.2: {}
get-caller-file@2.0.5: {}
get-east-asian-width@1.2.0: {}
@@ -8303,6 +8633,8 @@ snapshots:
min-document: 2.19.0
process: 0.11.10
globals@11.12.0: {}
globals@13.24.0:
dependencies:
type-fest: 0.20.2
@@ -8703,6 +9035,8 @@ snapshots:
js-cookie@3.0.5: {}
js-tokens@4.0.0: {}
js-yaml@3.14.1:
dependencies:
argparse: 1.0.10
@@ -8773,6 +9107,8 @@ snapshots:
- supports-color
- utf-8-validate
jsesc@3.1.0: {}
json-buffer@3.0.1: {}
json-logic-js@2.0.5: {}
@@ -8931,6 +9267,10 @@ snapshots:
lru-cache@10.4.3: {}
lru-cache@5.1.1:
dependencies:
yallist: 3.1.1
lru-cache@6.0.0:
dependencies:
yallist: 4.0.0
@@ -9127,6 +9467,8 @@ snapshots:
node-releases@2.0.18: {}
node-releases@2.0.19: {}
nopt@7.2.1:
dependencies:
abbrev: 2.0.0
@@ -9706,6 +10048,15 @@ snapshots:
quick-lru@6.1.2: {}
react-dom@19.1.0(react@19.1.0):
dependencies:
react: 19.1.0
scheduler: 0.26.0
react-refresh@0.17.0: {}
react@19.1.0: {}
read-cache@1.0.0:
dependencies:
pify: 2.3.0
@@ -9853,6 +10204,8 @@ snapshots:
dependencies:
xmlchars: 2.2.0
scheduler@0.26.0: {}
sdp@3.2.0: {}
section-matter@1.0.0:
@@ -10313,6 +10666,12 @@ snapshots:
escalade: 3.2.0
picocolors: 1.0.1
update-browserslist-db@1.1.3(browserslist@4.25.0):
dependencies:
browserslist: 4.25.0
escalade: 3.2.0
picocolors: 1.1.1
upper-case-first@2.0.2:
dependencies:
tslib: 2.8.1
@@ -10665,6 +11024,8 @@ snapshots:
y18n@5.0.8: {}
yallist@3.1.1: {}
yallist@4.0.0: {}
yaml-eslint-parser@1.2.3:
+61
View File
@@ -0,0 +1,61 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Web Components Test</title>
<script>
window.__WOOT_ISOLATED_SHELL__ = true;
window.__WOOT_ACCOUNT_ID__ = 1;
window.__WOOT_API_HOST__ = 'http://localhost:3000';
window.__WOOT_ACCESS_TOKEN__ = 'token';
window.__PUBSUB_TOKEN__ = 'token';
window.__WEBSOCKET_URL__ = 'ws://localhost:3000';
window.__WOOT_CONVERSATION_ID__ = 13918;
window.__DISABLE_EDITOR__ = true;
window.__EDITOR_DISABLE_UPLOAD__ = true;
</script>
</head>
<body>
<div id="app" dir="ltr">
<div class="py-20 grid place-content-center">
<svg
width="40"
height="40"
viewBox="0 0 40 40"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="20"
cy="20"
r="18"
fill="none"
stroke="#ccc"
stroke-width="4"
/>
<circle
cx="20"
cy="20"
r="18"
fill="none"
stroke="#333"
stroke-width="4"
stroke-dasharray="56.5487 56.5487"
stroke-dashoffset="0"
>
<animateTransform
attributeName="transform"
type="rotate"
from="0 20 20"
to="360 20 20"
dur="1s"
repeatCount="indefinite"
/>
</circle>
</svg>
</div>
</div>
<link rel="stylesheet" href="/packs/style.css" />
<script src="/packs/js/ui.js"></script>
</body>
</html>
+184
View File
@@ -0,0 +1,184 @@
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
/* eslint-disable no-console */
// Check for copy-only mode
const isCopyMode = process.argv.includes('--copyMode');
if (isCopyMode) {
console.log('📋 Copy mode: Copying built files to dist/react-components...');
} else {
console.log('🚀 Building Chatwoot React Components for NPM...');
}
function copyBuildFiles(packageDir) {
// Copy main build outputs
const files = [
{ src: 'public/packs/react-components.es.js', dest: 'index.js' }, // ES module (main)
{ src: 'public/packs/react-components.cjs.js', dest: 'index.cjs' }, // CommonJS
{ src: 'public/packs/style.css', dest: 'style.css' }, // CSS styles
];
files.forEach(({ src, dest }) => {
if (fs.existsSync(src)) {
fs.copyFileSync(src, path.join(packageDir, dest));
console.log(` ✓ Copied ${dest}`);
} else {
console.warn(` ⚠️ Warning: ${src} not found`);
}
});
}
function generatePackageJson(packageDir) {
// Read version from main package.json
const mainPackage = JSON.parse(fs.readFileSync('package.json', 'utf8'));
// Generate a clean epoch-based version
function generateTimestampVersion() {
try {
const epochSeconds = Math.floor(Date.now() / 1000);
// Get git hash for reference
const gitHash = execSync('git rev-parse --short HEAD', {
encoding: 'utf8',
stdio: 'pipe',
}).trim();
// Parse base version and increment patch
const [major, minor, patch] = mainPackage.version.split('.').map(Number);
const newPatch = patch + 1;
return `${major}.${minor}.${newPatch}-${epochSeconds}.${gitHash}`;
} catch (error) {
console.warn(
' ⚠️ Could not generate timestamp version, using incremented patch'
);
const [major, minor, patch] = mainPackage.version.split('.').map(Number);
return `${major}.${minor}.${patch + 1}`;
}
}
const finalVersion = generateTimestampVersion();
console.log(` 📋 Package version: ${finalVersion}`);
const packageJson = {
name: '@chatwoot/agent-react-components',
version: finalVersion,
description:
'React components for Chatwoot messaging interface with Vue Web Components',
// Entry points for different module systems
main: 'index.cjs', // CommonJS entry point for Node.js
module: 'index.js', // ES module entry point for bundlers
exports: {
'.': {
import: './index.js', // ES modules
require: './index.cjs', // CommonJS
},
'./style.css': './style.css',
},
// Peer dependencies - what the consuming app must provide
peerDependencies: {
react: '>=16.8.0', // Hooks support
'react-dom': '>=16.8.0',
},
// Package metadata
keywords: [
'chatwoot',
'react',
'vue',
'webcomponents',
'chat',
'messaging',
'customer-support',
'ui-components',
],
author: 'Chatwoot',
license: 'MIT',
repository: {
type: 'git',
url: 'https://github.com/chatwoot/chatwoot.git',
directory: 'app/javascript/react-components',
},
homepage: 'https://chatwoot.com',
bugs: {
url: 'https://github.com/chatwoot/chatwoot/issues',
},
// npm publish configuration
files: ['index.js', 'index.cjs', 'style.css', 'package.json'],
// Engine requirements
engines: {
node: '>=16.0.0',
},
};
fs.writeFileSync(
path.join(packageDir, 'package.json'),
JSON.stringify(packageJson, null, 2)
);
console.log(' ✓ Generated package.json');
}
async function publishReactComponents() {
try {
if (!isCopyMode) {
// Step 1: Clean previous builds
console.log('📦 Cleaning previous builds...');
if (fs.existsSync('dist')) {
fs.rmSync('dist', { recursive: true, force: true });
}
// Clean previous React component builds
const reactFiles = [
'public/packs/react-components.es.js',
'public/packs/react-components.cjs.js',
'public/packs/style.css',
];
reactFiles.forEach(file => {
if (fs.existsSync(file)) {
fs.unlinkSync(file);
console.log(` 🗑️ Removed ${file}`);
}
});
// Step 2: Build the React components library
console.log('🔨 Building React components...');
execSync('pnpm build:react', { stdio: 'inherit' });
}
// Step 3: Create package directory
console.log('📁 Creating package directory...');
const packageDir = 'dist/react-components';
fs.mkdirSync(packageDir, { recursive: true });
// Step 4: Copy built files
console.log('📋 Copying built files...');
copyBuildFiles(packageDir);
// Step 5: Generate package.json
console.log('📄 Generating package.json...');
generatePackageJson(packageDir);
console.log('✅ Package ready in dist/react-components/');
console.log('');
console.log('📦 Publishing options:');
console.log(' • npm publish: cd dist/react-components && npm publish');
console.log(' • yalc publish: pnpm package:react:yalc-publish');
console.log(' • local test: cd dist/react-components && npm pack');
} catch (error) {
console.error('❌ Build failed:', error.message);
process.exit(1);
}
}
// Run the script
publishReactComponents();
/* eslint-enable no-console */
+3
View File
@@ -29,10 +29,13 @@ const tailwindConfig = {
'./app/javascript/portal/**/*.vue',
'./app/javascript/shared/**/*.vue',
'./app/javascript/survey/**/*.vue',
'./app/javascript/ui/**/*.vue',
'./app/javascript/dashboard/components-next/**/*.vue',
'./app/javascript/dashboard/helper/**/*.js',
'./app/javascript/dashboard/components-next/**/*.js',
'./app/javascript/dashboard/routes/dashboard/**/**/*.js',
'./app/javascript/react-components/**/*.{js,jsx,vue}',
'./app/javascript/dashboard/assets/scss/**/*.scss',
'./app/views/**/*.html.erb',
],
theme: {
+65 -15
View File
@@ -22,8 +22,11 @@ import { defineConfig } from 'vite';
import ruby from 'vite-plugin-ruby';
import path from 'path';
import vue from '@vitejs/plugin-vue';
import react from '@vitejs/plugin-react';
const isLibraryMode = process.env.BUILD_MODE === 'library';
const uiMode = process.env.BUILD_MODE === 'ui';
const reactComponentMode = process.env.BUILD_MODE === 'react-components';
const isTestMode = process.env.TEST === 'true';
const vueOptions = {
@@ -38,38 +41,85 @@ let plugins = [ruby(), vue(vueOptions)];
if (isLibraryMode) {
plugins = [];
} else if (uiMode) {
plugins = [vue()];
} else if (reactComponentMode) {
plugins = [vue({ ...vueOptions, customElement: true }), react()];
} else if (isTestMode) {
plugins = [vue(vueOptions)];
}
let lib;
let rollupOptions = {};
if (isLibraryMode) {
lib = {
entry: path.resolve(__dirname, './app/javascript/entrypoints/sdk.js'),
formats: ['iife'], // IIFE format for single file
name: 'sdk',
};
} else if (uiMode) {
lib = {
entry: path.resolve(__dirname, './app/javascript/entrypoints/ui.js'),
formats: ['iife'], // IIFE format for single file
name: 'ui',
};
} else if (reactComponentMode) {
lib = {
entry: path.resolve(
__dirname,
'./app/javascript/react-components/src/index.jsx'
),
formats: ['es', 'cjs'], // ES modules and CommonJS only
fileName: format => `react-components.${format}.js`,
};
rollupOptions = {
external: ['react', 'react-dom'],
};
}
const chunkBuilder = chunkInfo => {
if (chunkInfo.name === 'sdk') {
return 'js/sdk.js';
}
if (chunkInfo.name === 'ui') {
return `js/ui.js`;
}
// For react components, we need to return different names but can't access format here
// So we'll handle this differently
return '[name].js';
};
export default defineConfig({
plugins: plugins,
define: {
'process.env.NODE_ENV': JSON.stringify('production'),
},
build: {
rollupOptions: {
...rollupOptions,
output: {
...rollupOptions.output,
// [NOTE] when not in library mode, no new keys will be addedd or overwritten
// setting dir: isLibraryMode ? 'public/packs' : undefined will not work
...(isLibraryMode
...(isLibraryMode || uiMode
? {
dir: 'public/packs',
entryFileNames: chunkInfo => {
if (chunkInfo.name === 'sdk') {
return 'js/sdk.js';
}
return '[name].js';
},
entryFileNames: chunkBuilder,
}
: {}),
inlineDynamicImports: isLibraryMode, // Disable code-splitting for SDK
...(reactComponentMode
? {
dir: 'public/packs',
}
: {}),
inlineDynamicImports: isLibraryMode || uiMode || reactComponentMode, // Disable code-splitting for SDK
},
},
lib: isLibraryMode
? {
entry: path.resolve(__dirname, './app/javascript/entrypoints/sdk.js'),
formats: ['iife'], // IIFE format for single file
name: 'sdk',
}
: undefined,
lib,
},
resolve: {
alias: {