加入客服反馈和社区二维码

增加桌面端全站客服反馈悬浮入口。

悬停入口时展示 QQ 和微信社区二维码浮窗。

补充入口拖拽边界、点击跳转和移动端隐藏测试。

同步全站客服悬浮入口接入文档。
This commit is contained in:
2026-06-23 21:02:13 +08:00
parent 972a974f26
commit fcd9470dcf
11 changed files with 722 additions and 0 deletions
+2
View File
@@ -34,6 +34,8 @@ Expo React Native 移动壳和 Tauri 桌面壳的工程结构、同源 WebView
图片画布生成类面板的模型泥点默认 JSON、运行时 override、后台“模型定价”页面和主站动态下发口径见 [【编辑器】模型定价配置管理方案-2026-06-22.md](./%E3%80%90%E7%BC%96%E8%BE%91%E5%99%A8%E3%80%91%E6%A8%A1%E5%9E%8B%E5%AE%9A%E4%BB%B7%E9%85%8D%E7%BD%AE%E7%AE%A1%E7%90%86%E6%96%B9%E6%A1%88-2026-06-22.md)。
桌面端全站客服悬浮入口的全局接入点、固定层级、拖拽边界和移动端隐藏口径见 [【前端架构】全站客服悬浮入口接入约定-2026-06-23.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91%E5%85%A8%E7%AB%99%E5%AE%A2%E6%9C%8D%E6%82%AC%E6%B5%AE%E5%85%A5%E5%8F%A3%E6%8E%A5%E5%85%A5%E7%BA%A6%E5%AE%9A-2026-06-23.md)。
桌面端 `/creation` 创作工具主页、顶级“草稿”入口替换为“项目”、最近项目、新建项目和陶泥儿精选素材瀑布流的落地计划见 [【玩法创作】创作主页与项目入口改版计划-2026-06-18.md](./%E3%80%90%E7%8E%A9%E6%B3%95%E5%88%9B%E4%BD%9C%E3%80%91%E5%88%9B%E4%BD%9C%E4%B8%BB%E9%A1%B5%E4%B8%8E%E9%A1%B9%E7%9B%AE%E5%85%A5%E5%8F%A3%E6%94%B9%E7%89%88%E8%AE%A1%E5%88%92-2026-06-18.md)。
本地通过 SSH alias 管理多台服务器、查看硬件 / systemd / HTTP 健康状态并执行受控服务启停的 egui 桌面工具见 [【开发运维】本地SSH服务器管理面板技术方案-2026-06-11.md](./technical/【开发运维】本地SSH服务器管理面板技术方案-2026-06-11.md)。
@@ -0,0 +1,36 @@
# 全站客服悬浮入口接入约定
日期:2026-06-23
## 背景
主站需要在桌面端每个页面提供稳定可见的客服入口,包括 `/editor/canvas` 这类全屏画布页面。入口使用陶泥儿客服形象图片,不进入具体业务页布局,避免被滚动、画布缩放或面板层级影响。
## 接入口
- 全局 React 入口为 `src/components/common/FloatingFeedbackEntry.tsx`
-`src/main.tsx``RouteImageReadyGate` 内、当前路由组件之后渲染,覆盖主站 `resolveAppRoute` 管理的所有页面。
- 图片资产放在 `public/branding/taonier-feedback-entry.png`,并通过 `BRAND_ASSETS.taonierFeedbackEntry` 引用。
- 悬浮层样式使用 `.floating-feedback-entry`,通过 `position: fixed` 和最高层级保持在页面最上方。
## 交互约定
- 仅桌面端展示,媒体查询为 `(min-width: 1024px)`;移动端不渲染入口。
- 初始位置靠近页面左下角,随窗口滚动保持固定。
- 用户可以按住拖动,拖拽过程中实时夹在当前 viewport 内,保证图片完整可见,不被窗口边界遮挡。
- 窗口尺寸变化后重新夹取当前位置,避免缩小窗口后入口跑出视口。
- 点击入口使用新页面打开客服表单:
```text
https://kcnz41bksl1c.feishu.cn/share/base/form/shrcn1p9WpuZMDxrI5ysLLJhw7k
```
- 拖拽释放后短时间内的补发 click 会被吞掉,避免用户拖动入口时误打开表单。
## 验收口径
- 桌面端主站任意路由均能看到客服入口,画布页也可见。
- 滚动页面时入口位置相对视口不变。
- 拖到窗口四边时入口完整留在可见区域内。
- 移动端视口不渲染入口。
- 单击入口新开页面跳转到飞书表单。
Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

@@ -0,0 +1,193 @@
/* @vitest-environment jsdom */
import { fireEvent, render, screen } from '@testing-library/react';
import { afterEach, expect, test, vi } from 'vitest';
import { FloatingFeedbackEntry } from './FloatingFeedbackEntry';
import {
clampFloatingFeedbackPosition,
FLOATING_FEEDBACK_FORM_URL,
resolveFloatingFeedbackContactPanelOffset,
resolveInitialFloatingFeedbackPosition,
} from './floatingFeedbackEntryModel';
function mockMatchMedia(matches: boolean) {
Object.defineProperty(window, 'matchMedia', {
configurable: true,
writable: true,
value: vi.fn().mockImplementation((query: string) => ({
matches,
media: query,
onchange: null,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
addListener: vi.fn(),
removeListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
}
function mockViewport(width: number, height: number) {
Object.defineProperty(window, 'innerWidth', {
configurable: true,
writable: true,
value: width,
});
Object.defineProperty(window, 'innerHeight', {
configurable: true,
writable: true,
value: height,
});
}
afterEach(() => {
document.body.innerHTML = '';
vi.restoreAllMocks();
});
test('desktop renders floating feedback entry near lower left', () => {
mockMatchMedia(true);
mockViewport(1440, 900);
render(<FloatingFeedbackEntry />);
const button = screen.getByRole('button', { name: '反馈点我' });
const shell = button.closest('.floating-feedback-entry-shell') as HTMLElement;
expect(button.className).toContain('floating-feedback-entry');
expect(shell.style.left).toBe('24px');
expect(shell.style.top).toBe('746.4px');
expect(button.querySelector('img')?.getAttribute('src')).toBe(
'/branding/taonier-feedback-entry.png',
);
});
test('hover shows contact QR images and mouse leave hides them', () => {
mockMatchMedia(true);
mockViewport(1440, 900);
render(<FloatingFeedbackEntry />);
const button = screen.getByRole('button', { name: '反馈点我' });
expect(document.querySelector('.floating-feedback-entry-contact-panel')).toBeNull();
fireEvent.pointerEnter(button);
const panel = document.querySelector('.floating-feedback-entry-contact-panel');
expect(panel).not.toBeNull();
expect(
(panel as HTMLElement).style.getPropertyValue(
'--floating-feedback-contact-offset',
),
).toBe('141.2px');
const images = Array.from(panel?.querySelectorAll('img') ?? []);
expect(images.map((image) => image.getAttribute('src'))).toEqual([
'/branding/taonier-feedback-qq.png',
'/branding/taonier-feedback-wechat.png',
]);
fireEvent.pointerLeave(button);
expect(document.querySelector('.floating-feedback-entry-contact-panel')).toBeNull();
});
test('mobile does not render feedback entry', () => {
mockMatchMedia(false);
mockViewport(390, 780);
render(<FloatingFeedbackEntry />);
expect(screen.queryByRole('button', { name: '反馈点我' })).toBeNull();
});
test('click opens feedback form in a new page', () => {
mockMatchMedia(true);
mockViewport(1440, 900);
const openSpy = vi.spyOn(window, 'open').mockReturnValue(null);
render(<FloatingFeedbackEntry />);
fireEvent.click(screen.getByRole('button', { name: '反馈点我' }));
expect(openSpy).toHaveBeenCalledWith(
FLOATING_FEEDBACK_FORM_URL,
'_blank',
'noopener,noreferrer',
);
});
test('hover contact panel does not block click opening feedback form', () => {
mockMatchMedia(true);
mockViewport(1440, 900);
const openSpy = vi.spyOn(window, 'open').mockReturnValue(null);
render(<FloatingFeedbackEntry />);
const button = screen.getByRole('button', { name: '反馈点我' });
fireEvent.pointerEnter(button);
fireEvent.click(button);
expect(document.querySelector('.floating-feedback-entry-contact-panel')).not.toBeNull();
expect(openSpy).toHaveBeenCalledWith(
FLOATING_FEEDBACK_FORM_URL,
'_blank',
'noopener,noreferrer',
);
});
test('drag keeps entry fully inside the viewport and does not open form', () => {
mockMatchMedia(true);
mockViewport(360, 320);
const openSpy = vi.spyOn(window, 'open').mockReturnValue(null);
render(<FloatingFeedbackEntry />);
const button = screen.getByRole('button', { name: '反馈点我' });
fireEvent.mouseEnter(button);
expect(document.querySelector('.floating-feedback-entry-contact-panel')).not.toBeNull();
fireEvent.mouseDown(button, {
button: 0,
clientX: 40,
clientY: 220,
});
expect(document.querySelector('.floating-feedback-entry-contact-panel')).toBeNull();
fireEvent.mouseMove(window, {
clientX: 900,
clientY: 900,
});
fireEvent.mouseUp(window, {
clientX: 900,
clientY: 900,
});
expect(document.querySelector('.floating-feedback-entry-contact-panel')).not.toBeNull();
fireEvent.click(button);
const shell = button.closest('.floating-feedback-entry-shell') as HTMLElement;
expect(shell.style.left).toBe('282.4px');
expect(shell.style.top).toBe('242.4px');
expect(openSpy).not.toHaveBeenCalled();
});
test('floating feedback position helpers clamp to visible viewport', () => {
expect(
clampFloatingFeedbackPosition(
{ left: -100, top: 1000 },
{ width: 360, height: 320 },
),
).toEqual({ left: 20, top: 242.4 });
expect(
resolveInitialFloatingFeedbackPosition({ width: 1440, height: 900 }),
).toEqual({ left: 24, top: 746.4 });
expect(
resolveFloatingFeedbackContactPanelOffset(
{ left: 20, top: 100 },
{ width: 1440 },
),
).toBe(145.2);
expect(
resolveFloatingFeedbackContactPanelOffset(
{ left: 20, top: 100 },
{ width: 360 },
),
).toBe(0);
});
@@ -0,0 +1,336 @@
import {
type CSSProperties,
type MouseEvent,
type PointerEvent,
useEffect,
useRef,
useState,
} from 'react';
import { createPortal } from 'react-dom';
import { BRAND_ASSETS } from '../../uiAssets';
import {
clampFloatingFeedbackPosition,
FLOATING_FEEDBACK_DESKTOP_QUERY,
FLOATING_FEEDBACK_DRAG_CLICK_SUPPRESS_MS,
FLOATING_FEEDBACK_DRAG_CLICK_THRESHOLD,
FLOATING_FEEDBACK_FORM_URL,
FLOATING_FEEDBACK_INITIAL_LEFT,
FLOATING_FEEDBACK_MARGIN,
type FloatingFeedbackPosition,
resolveFloatingFeedbackContactPanelOffset,
resolveInitialFloatingFeedbackPosition,
} from './floatingFeedbackEntryModel';
type FloatingFeedbackDragState = {
pointerId: number;
source: 'mouse' | 'pointer';
startClientX: number;
startClientY: number;
startLeft: number;
startTop: number;
hasMoved: boolean;
};
function isSameFloatingFeedbackPointer(
currentPointerId: number,
nextPointerId: number,
) {
if (!Number.isFinite(currentPointerId) || !Number.isFinite(nextPointerId)) {
return true;
}
return currentPointerId === nextPointerId;
}
function shouldUseMouseDragFallback() {
return (
typeof window !== 'undefined' &&
typeof window.PointerEvent !== 'function'
);
}
function getViewportSize() {
return {
width: window.innerWidth || document.documentElement.clientWidth,
height: window.innerHeight || document.documentElement.clientHeight,
};
}
function getInitialDesktopVisibility() {
if (
typeof window === 'undefined' ||
typeof window.matchMedia !== 'function'
) {
return false;
}
return window.matchMedia(FLOATING_FEEDBACK_DESKTOP_QUERY).matches;
}
export function FloatingFeedbackEntry() {
const [isDesktopVisible, setIsDesktopVisible] = useState(
getInitialDesktopVisibility,
);
const [isPointerOverEntry, setIsPointerOverEntry] = useState(false);
const [isDragging, setIsDragging] = useState(false);
const [position, setPosition] = useState<FloatingFeedbackPosition>(() =>
typeof window === 'undefined'
? { left: FLOATING_FEEDBACK_INITIAL_LEFT, top: FLOATING_FEEDBACK_MARGIN }
: resolveInitialFloatingFeedbackPosition(getViewportSize()),
);
const dragStateRef = useRef<FloatingFeedbackDragState | null>(null);
const suppressClickUntilRef = useRef(0);
useEffect(() => {
if (
typeof window === 'undefined' ||
typeof window.matchMedia !== 'function'
) {
return;
}
const mediaQuery = window.matchMedia(FLOATING_FEEDBACK_DESKTOP_QUERY);
const updateVisibility = (event?: MediaQueryListEvent) => {
setIsDesktopVisible(event?.matches ?? mediaQuery.matches);
};
updateVisibility();
if (typeof mediaQuery.addEventListener === 'function') {
mediaQuery.addEventListener('change', updateVisibility);
return () => mediaQuery.removeEventListener('change', updateVisibility);
}
mediaQuery.addListener(updateVisibility);
return () => mediaQuery.removeListener(updateVisibility);
}, []);
useEffect(() => {
const clampToViewport = () => {
setPosition((current) =>
clampFloatingFeedbackPosition(current, getViewportSize()),
);
};
window.addEventListener('resize', clampToViewport);
return () => window.removeEventListener('resize', clampToViewport);
}, []);
const updateDrag = (
pointerId: number,
source: FloatingFeedbackDragState['source'],
clientX: number,
clientY: number,
) => {
const dragState = dragStateRef.current;
if (
!dragState ||
dragState.source !== source ||
!isSameFloatingFeedbackPointer(dragState.pointerId, pointerId)
) {
return;
}
const deltaX = clientX - dragState.startClientX;
const deltaY = clientY - dragState.startClientY;
if (
!dragState.hasMoved &&
Math.hypot(deltaX, deltaY) >= FLOATING_FEEDBACK_DRAG_CLICK_THRESHOLD
) {
dragState.hasMoved = true;
}
setPosition(
clampFloatingFeedbackPosition(
{
left: dragState.startLeft + deltaX,
top: dragState.startTop + deltaY,
},
getViewportSize(),
),
);
};
const stopDragById = (
pointerId: number,
source: FloatingFeedbackDragState['source'],
) => {
const dragState = dragStateRef.current;
if (
!dragState ||
dragState.source !== source ||
!isSameFloatingFeedbackPointer(dragState.pointerId, pointerId)
) {
return;
}
suppressClickUntilRef.current = dragState.hasMoved
? Date.now() + FLOATING_FEEDBACK_DRAG_CLICK_SUPPRESS_MS
: 0;
dragStateRef.current = null;
setIsDragging(false);
};
useEffect(() => {
const handlePointerMove = (event: globalThis.PointerEvent) => {
const dragState = dragStateRef.current;
if (
!dragState ||
!isSameFloatingFeedbackPointer(dragState.pointerId, event.pointerId)
) {
return;
}
updateDrag(event.pointerId, 'pointer', event.clientX, event.clientY);
};
const stopPointerDrag = (event: globalThis.PointerEvent) => {
stopDragById(event.pointerId, 'pointer');
};
window.addEventListener('pointermove', handlePointerMove);
window.addEventListener('pointerup', stopPointerDrag);
window.addEventListener('pointercancel', stopPointerDrag);
return () => {
window.removeEventListener('pointermove', handlePointerMove);
window.removeEventListener('pointerup', stopPointerDrag);
window.removeEventListener('pointercancel', stopPointerDrag);
};
}, []);
useEffect(() => {
const handleMouseMove = (event: globalThis.MouseEvent) => {
const dragState = dragStateRef.current;
if (!dragState || dragState.source !== 'mouse') {
return;
}
updateDrag(Number.NaN, 'mouse', event.clientX, event.clientY);
};
const stopMouseDrag = () => {
stopDragById(Number.NaN, 'mouse');
};
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', stopMouseDrag);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', stopMouseDrag);
};
}, []);
const startPointerDrag = (event: PointerEvent<HTMLButtonElement>) => {
if (event.button > 0) {
return;
}
event.currentTarget.setPointerCapture?.(event.pointerId);
setIsDragging(true);
dragStateRef.current = {
pointerId: event.pointerId,
source: 'pointer',
startClientX: event.clientX,
startClientY: event.clientY,
startLeft: position.left,
startTop: position.top,
hasMoved: false,
};
};
const startMouseDrag = (event: MouseEvent<HTMLButtonElement>) => {
if (!shouldUseMouseDragFallback() || event.button > 0) {
return;
}
setIsDragging(true);
dragStateRef.current = {
pointerId: Number.NaN,
source: 'mouse',
startClientX: event.clientX,
startClientY: event.clientY,
startLeft: position.left,
startTop: position.top,
hasMoved: false,
};
};
const openFeedbackForm = () => {
if (suppressClickUntilRef.current >= Date.now()) {
suppressClickUntilRef.current = 0;
dragStateRef.current = null;
return;
}
window.open(FLOATING_FEEDBACK_FORM_URL, '_blank', 'noopener,noreferrer');
};
if (!isDesktopVisible || typeof document === 'undefined') {
return null;
}
const shouldShowContactPanel = isPointerOverEntry && !isDragging;
const contactPanelOffset = resolveFloatingFeedbackContactPanelOffset(
position,
getViewportSize(),
);
return createPortal(
<div
className="floating-feedback-entry-shell"
style={{
left: `${position.left}px`,
top: `${position.top}px`,
}}
>
{shouldShowContactPanel ? (
<div
className="floating-feedback-entry-contact-panel"
style={
{
'--floating-feedback-contact-offset': `${contactPanelOffset}px`,
} as CSSProperties
}
aria-hidden="true"
>
<img
src={BRAND_ASSETS.taonierFeedbackQq}
alt=""
draggable={false}
/>
<img
src={BRAND_ASSETS.taonierFeedbackWechat}
alt=""
draggable={false}
/>
</div>
) : null}
<button
type="button"
className="floating-feedback-entry"
aria-label="反馈点我"
onPointerEnter={() => setIsPointerOverEntry(true)}
onPointerLeave={() => setIsPointerOverEntry(false)}
onPointerDown={startPointerDrag}
onMouseEnter={() => setIsPointerOverEntry(true)}
onMouseLeave={() => setIsPointerOverEntry(false)}
onMouseDown={startMouseDrag}
onPointerMove={(event) =>
updateDrag(event.pointerId, 'pointer', event.clientX, event.clientY)
}
onPointerUp={(event) => stopDragById(event.pointerId, 'pointer')}
onPointerCancel={(event) => stopDragById(event.pointerId, 'pointer')}
onClick={openFeedbackForm}
>
<img
src={BRAND_ASSETS.taonierFeedbackEntry}
alt=""
aria-hidden="true"
draggable={false}
/>
</button>
</div>,
document.body,
);
}
@@ -0,0 +1,81 @@
export const FLOATING_FEEDBACK_FORM_URL =
'https://kcnz41bksl1c.feishu.cn/share/base/form/shrcn1p9WpuZMDxrI5ysLLJhw7k';
export const FLOATING_FEEDBACK_DESKTOP_QUERY = '(min-width: 1024px)';
export const FLOATING_FEEDBACK_SIZE = 57.6;
export const FLOATING_FEEDBACK_MARGIN = 20;
export const FLOATING_FEEDBACK_INITIAL_LEFT = 24;
export const FLOATING_FEEDBACK_INITIAL_BOTTOM = 96;
export const FLOATING_FEEDBACK_DRAG_CLICK_THRESHOLD = 4;
export const FLOATING_FEEDBACK_DRAG_CLICK_SUPPRESS_MS = 250;
export const FLOATING_FEEDBACK_CONTACT_PANEL_WIDTH = 348;
export type FloatingFeedbackPosition = {
left: number;
top: number;
};
function roundFloatingFeedbackCoordinate(value: number) {
return Math.round(value * 10) / 10;
}
export function clampFloatingFeedbackPosition(
position: FloatingFeedbackPosition,
viewport: { width: number; height: number },
) {
const minLeft = FLOATING_FEEDBACK_MARGIN;
const minTop = FLOATING_FEEDBACK_MARGIN;
const maxLeft = Math.max(
minLeft,
viewport.width - FLOATING_FEEDBACK_SIZE - FLOATING_FEEDBACK_MARGIN,
);
const maxTop = Math.max(
minTop,
viewport.height - FLOATING_FEEDBACK_SIZE - FLOATING_FEEDBACK_MARGIN,
);
return {
left: roundFloatingFeedbackCoordinate(
Math.min(maxLeft, Math.max(minLeft, position.left)),
),
top: roundFloatingFeedbackCoordinate(
Math.min(maxTop, Math.max(minTop, position.top)),
),
};
}
export function resolveInitialFloatingFeedbackPosition(viewport: {
width: number;
height: number;
}) {
return clampFloatingFeedbackPosition(
{
left: FLOATING_FEEDBACK_INITIAL_LEFT,
top:
viewport.height -
FLOATING_FEEDBACK_SIZE -
FLOATING_FEEDBACK_INITIAL_BOTTOM,
},
viewport,
);
}
export function resolveFloatingFeedbackContactPanelOffset(
position: FloatingFeedbackPosition,
viewport: { width: number },
) {
const entryCenter = position.left + FLOATING_FEEDBACK_SIZE / 2;
const minCenter = FLOATING_FEEDBACK_MARGIN + FLOATING_FEEDBACK_CONTACT_PANEL_WIDTH / 2;
const maxCenter =
viewport.width -
FLOATING_FEEDBACK_MARGIN -
FLOATING_FEEDBACK_CONTACT_PANEL_WIDTH / 2;
if (maxCenter < minCenter) {
return 0;
}
return roundFloatingFeedbackCoordinate(
Math.min(maxCenter, Math.max(minCenter, entryCenter)) - entryCenter,
);
}
+69
View File
@@ -1562,6 +1562,75 @@ html[data-mobile-keyboard-open='true'] #root {
color: var(--platform-brand-logo-subtitle);
}
.floating-feedback-entry-shell {
position: fixed;
z-index: 2147483647;
width: 57.6px;
height: 57.6px;
}
.floating-feedback-entry-contact-panel {
position: absolute;
left: 50%;
bottom: calc(100% + 12px);
display: flex;
box-sizing: border-box;
width: 348px;
gap: 12px;
align-items: center;
justify-content: center;
padding: 10px;
border: 1px solid rgba(214, 154, 111, 0.24);
border-radius: 12px;
background: rgba(255, 252, 248, 0.97);
box-shadow: 0 18px 36px rgba(74, 34, 15, 0.18);
pointer-events: none;
transform: translateX(calc(-50% + var(--floating-feedback-contact-offset, 0px)));
}
.floating-feedback-entry-contact-panel img {
display: block;
width: 156px;
height: 156px;
border-radius: 8px;
object-fit: contain;
user-select: none;
}
.floating-feedback-entry {
position: relative;
display: inline-flex;
width: 57.6px;
height: 57.6px;
align-items: center;
justify-content: center;
border: 0;
border-radius: 999px;
background: transparent;
padding: 0;
cursor: grab;
touch-action: none;
user-select: none;
filter: drop-shadow(0 0.7rem 1.05rem rgba(98, 48, 24, 0.24));
}
.floating-feedback-entry:active {
cursor: grabbing;
}
.floating-feedback-entry:focus-visible {
outline: 3px solid rgba(204, 117, 76, 0.72);
outline-offset: 4px;
}
.floating-feedback-entry img {
display: block;
width: 100%;
height: 100%;
pointer-events: none;
user-select: none;
}
.platform-main-shell {
position: relative;
width: 100%;
+2
View File
@@ -5,6 +5,7 @@ import './index.css';
import {StrictMode, Suspense} from 'react';
import {createRoot} from 'react-dom/client';
import {FloatingFeedbackEntry} from './components/common/FloatingFeedbackEntry';
import {stabilizeMobileViewportKeyboardFocus} from './mobileViewportKeyboardFocus';
import {lockMobileViewportZoom} from './mobileViewportZoomLock';
import {resolveAppRoute} from './routing/appRoutes';
@@ -54,5 +55,6 @@ root.render(
<RouteComponent {...(route.componentProps ?? {})} />
</RouteImageReadyGate>
</Suspense>
<FloatingFeedbackEntry />
</StrictMode>,
);
+3
View File
@@ -54,6 +54,9 @@ export const CUSTOM_WORLD_THEME_ICONS = {
export const BRAND_ASSETS = {
kuranGamesLogo: '/branding/kuran-games-logo.svg',
taonierProductIp: '/branding/taonier-product-ip.png',
taonierFeedbackEntry: '/branding/taonier-feedback-entry.png',
taonierFeedbackQq: '/branding/taonier-feedback-qq.png',
taonierFeedbackWechat: '/branding/taonier-feedback-wechat.png',
} as const;
export const UI_CHROME = {