Модальные окнаoverlaysfeedbackfloatingwidgetsurvey
Floating Feedback Widget
Плавающий виджет обратной связи с выбором смайликов и полем отзыва.
Живое превью
Send Feedback
Использование
Скопируйте и вставьте этот код в ваш React-компонент:
tsx
import { FloatingFeedbackWidget } from 'react-component-kit-ui/overlays';
<FloatingFeedbackWidget placeholder="How can we improve this experience?" />Ручная установка
Если вы не используете пакет npm, создайте файл и вставьте следующий код: src/components/FloatingFeedbackWidget.tsx + FloatingFeedbackWidget.css:
FloatingFeedbackWidget.tsx
'use client';
import { useEffect, useRef } from 'react';
import './FloatingFeedbackWidget.css';
/** class:list → className */
function cx(...args: unknown[]): string {
const out: string[] = [];
const walk = (v: unknown) => {
if (!v) return;
if (typeof v === 'string' || typeof v === 'number') out.push(String(v));
else if (Array.isArray(v)) v.forEach(walk);
else if (typeof v === 'object') for (const [k, on] of Object.entries(v as Record<string, unknown>)) if (on) out.push(k);
};
args.forEach(walk);
return out.join(' ');
}
/**
* FloatingFeedbackWidget — Fixed corner floating pill that expands into an interactive feedback survey card.
*
* @param {string} title - Widget title. Default is 'Was this helpful?'.
* @param {string} position - Screen anchor ('bottom-right' | 'bottom-left'). Default is 'bottom-right'.
*/
export interface FloatingFeedbackWidgetProps {
title?: string;
position?: 'bottom-right' | 'bottom-left';
}
export function FloatingFeedbackWidget({
title = 'Send Feedback',
position = 'bottom-right',
}: FloatingFeedbackWidgetProps) {
// Ported from the Astro <script>: runs once after mount (DOM logic kept as in the Astro kit).
const mounted = useRef(false);
useEffect(() => {
if (mounted.current) return;
mounted.current = true;
function setupFeedbackWidgets() {
document.querySelectorAll('[data-feedback-widget]').forEach((widget) => {
const trigger = widget.querySelector('[data-fb-trigger]');
const card = widget.querySelector('[data-fb-card]') as HTMLElement;
const close = widget.querySelector('[data-fb-close]');
const send = widget.querySelector('[data-fb-send]');
if (!trigger || !card) return;
trigger.addEventListener('click', () => {
card.hidden = !card.hidden;
});
close?.addEventListener('click', () => {
card.hidden = true;
});
send?.addEventListener('click', () => {
card.innerHTML = `<p style="text-align:center;font-size:0.85rem;color:#10b981;font-weight:700;margin:1rem 0;">✓ Thank you for your feedback!</p>`;
setTimeout(() => { card.hidden = true; }, 2000);
});
});
}
setupFeedbackWidgets();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<>
<div className={cx(['fb-widget', `fb-widget--${position}`])} data-feedback-widget data-rk-2e965a="">
<button type="button" className="fb-widget__trigger" aria-label="Open feedback modal" data-fb-trigger data-rk-2e965a="">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" data-rk-2e965a="">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" data-rk-2e965a="" />
</svg>
<span data-rk-2e965a="">Feedback</span>
</button>
<div className="fb-widget__card" data-fb-card hidden data-rk-2e965a="">
<div className="fb-widget__head" data-rk-2e965a="">
<h5 className="fb-widget__title" data-rk-2e965a="">{title}</h5>
<button type="button" className="fb-widget__close" aria-label="Close" data-fb-close data-rk-2e965a="">×</button>
</div>
<div className="fb-widget__emojis" data-rk-2e965a="">
<button type="button" className="emoji-btn" aria-label="Sad" data-rk-2e965a="">🙁</button>
<button type="button" className="emoji-btn" aria-label="Neutral" data-rk-2e965a="">😐</button>
<button type="button" className="emoji-btn emoji-btn--selected" aria-label="Happy" data-rk-2e965a="">😊</button>
<button type="button" className="emoji-btn" aria-label="Love it" data-rk-2e965a="">🤩</button>
</div>
<textarea placeholder="Any details to share? (optional)" className="fb-widget__textarea" rows={2} data-rk-2e965a=""></textarea>
<button type="button" className="fb-widget__send" data-fb-send data-rk-2e965a="">
Send Feedback
</button>
</div>
</div>
</>
);
}
export default FloatingFeedbackWidget;FloatingFeedbackWidget.css
/* FloatingFeedbackWidget — ported from astro-component-kit (overlays/FloatingFeedbackWidget.astro) */
.fb-widget:where([data-rk-2e965a]) {
position: fixed;
z-index: 90;
font-family: var(--font-sans, system-ui, sans-serif);
}
.fb-widget--bottom-right:where([data-rk-2e965a]) { bottom: 1.5rem; right: 1.5rem; }
.fb-widget--bottom-left:where([data-rk-2e965a]) { bottom: 1.5rem; left: 1.5rem; }
.fb-widget__trigger:where([data-rk-2e965a]) {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 0.6rem 1.1rem;
background: rgba(14, 14, 18, 0.9);
backdrop-filter: blur(16px);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: var(--r-full, 9999px);
color: var(--c-text-1, #ffffff);
font-size: 0.82rem;
font-weight: 700;
cursor: pointer;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
transition: transform 0.2s ease, border-color 0.2s ease;
}
.fb-widget__trigger:hover:where([data-rk-2e965a]) {
transform: translateY(-2px);
border-color: rgba(184, 134, 11, 0.4);
color: var(--c-primary-light, #d4a017);
}
.fb-widget__card:where([data-rk-2e965a]) {
position: absolute;
bottom: calc(100% + 12px);
right: 0;
width: 280px;
padding: 1.2rem;
background: rgba(12, 12, 16, 0.95);
backdrop-filter: blur(24px);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: var(--r-xl, 20px);
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.6);
display: flex;
flex-direction: column;
gap: 12px;
}
.fb-widget__head:where([data-rk-2e965a]) {
display: flex;
justify-content: space-between;
align-items: center;
}
.fb-widget__title:where([data-rk-2e965a]) {
font-size: 0.88rem;
font-weight: 800;
color: var(--c-text-1, #ffffff);
margin: 0;
}
.fb-widget__close:where([data-rk-2e965a]) {
background: transparent;
border: none;
color: var(--c-text-3, #737373);
font-size: 1.2rem;
line-height: 1;
cursor: pointer;
}
.fb-widget__emojis:where([data-rk-2e965a]) {
display: flex;
justify-content: space-between;
gap: 6px;
}
.emoji-btn:where([data-rk-2e965a]) {
flex: 1;
font-size: 1.4rem;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: var(--r-md, 10px);
padding: 6px 0;
cursor: pointer;
transition: transform 0.2s ease, background 0.2s ease;
}
.emoji-btn:hover:where([data-rk-2e965a]),
.emoji-btn--selected:where([data-rk-2e965a]) {
transform: scale(1.15);
background: rgba(184, 134, 11, 0.2);
border-color: rgba(184, 134, 11, 0.4);
}
.fb-widget__textarea:where([data-rk-2e965a]) {
width: 100%;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: var(--r-sm, 8px);
color: var(--c-text-1, #ffffff);
font-size: 0.8rem;
padding: 8px;
resize: none;
outline: none;
font-family: inherit;
}
.fb-widget__send:where([data-rk-2e965a]) {
padding: 0.6rem;
background: linear-gradient(135deg, #d4a017, #b8860b);
border: none;
border-radius: var(--r-sm, 8px);
color: #050505;
font-size: 0.8rem;
font-weight: 800;
cursor: pointer;
transition: opacity 0.2s;
}
.fb-widget__send:hover:where([data-rk-2e965a]) {
opacity: 0.9;
}Краткая информация
- Категория
- Модальные окна
- Имя файла
FloatingFeedbackWidget.tsx- Зависимости
- Нет — чистый React + CSS
- Теги
- overlaysfeedbackfloatingwidgetsurvey