From b7ff727e1efc9e1ab5776c1e5449e7f27dbf8a15 Mon Sep 17 00:00:00 2001 From: Rachael Rose Renk <91027132+rachaelrenk@users.noreply.github.com> Date: Thu, 9 Jul 2026 14:17:35 -0600 Subject: [PATCH] feat: add in-page docs feedback widget Adds a two-layer feedback widget to the bottom of every docs page. - Layer 1: "Was this page useful?" with thumbs-up Yes / thumbs-down No buttons - Layer 2: rating-specific category form (radio + description) plus an optional freeform textarea and a Send button - On submit, fires a rudderanalytics.track('docs_feedback_submitted') event with page_url, page_slug, page_title, rating, category, and optional comment - Success state shown after submission; widget stays visible and becomes read-only for the session - Styled with Warp CSS design tokens; supports dark and light themes - Mounted via client:idle in FeedbackFooter.astro so it doesn't block initial page load Co-Authored-By: Oz --- src/components/DocsFeedback.css | 208 ++++++++++++++++++++++++++++ src/components/DocsFeedback.tsx | 155 +++++++++++++++++++++ src/components/FeedbackFooter.astro | 7 + 3 files changed, 370 insertions(+) create mode 100644 src/components/DocsFeedback.css create mode 100644 src/components/DocsFeedback.tsx diff --git a/src/components/DocsFeedback.css b/src/components/DocsFeedback.css new file mode 100644 index 000000000..b40f5d67c --- /dev/null +++ b/src/components/DocsFeedback.css @@ -0,0 +1,208 @@ +/* ========================================================================== + DocsFeedback — in-page docs feedback widget + Uses Warp design tokens from custom.css. + ========================================================================== */ + +.docs-feedback { + margin-top: 2.5rem; +} + +.docs-feedback__divider { + border: none; + border-top: 1px solid var(--sl-color-hairline-light); + margin: 0 0 1.75rem; +} + +/* -------------------------------------------------------------------------- + Layer 1 — Rating prompt + -------------------------------------------------------------------------- */ + +.docs-feedback__prompt { + display: flex; + align-items: center; + gap: 1rem; + flex-wrap: wrap; +} + +.docs-feedback__question { + font-size: var(--sl-text-sm); + font-weight: 500; + color: var(--sl-color-gray-2); +} + +.docs-feedback__buttons { + display: flex; + gap: 0.5rem; +} + +.docs-feedback__vote-btn { + display: inline-flex; + align-items: center; + gap: 0.375rem; + padding: 0.375rem 0.875rem; + border: 1px solid var(--sl-color-gray-5); + border-radius: var(--sl-radius-sm); + background: transparent; + color: var(--sl-color-gray-2); + font-size: var(--sl-text-sm); + font-family: var(--sl-font); + cursor: pointer; + transition: border-color 0.15s ease, color 0.15s ease; +} + +.docs-feedback__vote-btn:hover { + border-color: var(--sl-color-gray-3); + color: var(--sl-color-white); +} + +.docs-feedback__vote-btn:focus-visible { + outline: 2px solid var(--sl-color-accent); + outline-offset: 2px; +} + +.docs-feedback__vote-icon { + width: 0.875rem; + height: 0.875rem; + flex-shrink: 0; +} + +/* -------------------------------------------------------------------------- + Layer 2 — Category + freeform form + -------------------------------------------------------------------------- */ + +.docs-feedback__form { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.docs-feedback__form-header { + font-size: var(--sl-text-h4); + font-weight: 600; + color: var(--sl-color-white); + margin: 0; + line-height: var(--sl-line-height-headings); +} + +.docs-feedback__categories { + display: flex; + flex-direction: column; + gap: 0; +} + +.docs-feedback__category { + display: grid; + grid-template-columns: 1.25rem 1fr; + grid-template-rows: auto auto; + column-gap: 0.625rem; + align-items: start; + cursor: pointer; + padding: 0.5rem 0; +} + +.docs-feedback__radio { + grid-column: 1; + grid-row: 1 / span 2; + align-self: start; + margin-top: 0.125rem; + accent-color: var(--sl-color-accent); + cursor: pointer; + width: 1rem; + height: 1rem; +} + +.docs-feedback__category-label { + grid-column: 2; + grid-row: 1; + font-size: var(--sl-text-sm); + font-weight: 500; + color: var(--sl-color-gray-1); + line-height: 1.4; +} + +.docs-feedback__category-desc { + grid-column: 2; + grid-row: 2; + font-size: var(--sl-text-xs); + color: var(--sl-color-gray-3); + line-height: 1.4; + margin-top: 0.125rem; +} + +.docs-feedback__textarea { + width: 100%; + min-height: 7.5rem; + padding: 0.75rem; + background: var(--sl-color-gray-6); + border: 1px solid var(--sl-color-gray-5); + border-radius: var(--sl-radius-sm); + color: var(--sl-color-gray-1); + font-size: var(--sl-text-sm); + font-family: var(--sl-font); + line-height: var(--sl-line-height); + resize: vertical; + box-sizing: border-box; + transition: border-color 0.15s ease; +} + +.docs-feedback__textarea::placeholder { + color: var(--sl-color-gray-4); +} + +.docs-feedback__textarea:focus { + outline: none; + border-color: var(--sl-color-accent); +} + +.docs-feedback__send-btn { + align-self: flex-start; + padding: 0.5rem 1.25rem; + border: 1px solid var(--sl-color-gray-5); + border-radius: var(--sl-radius-sm); + background: var(--sl-color-gray-6); + color: var(--sl-color-gray-1); + font-size: var(--sl-text-sm); + font-weight: 600; + font-family: var(--sl-font); + letter-spacing: 0.04em; + cursor: pointer; + transition: border-color 0.15s ease, background-color 0.15s ease, color 0.15s ease; +} + +.docs-feedback__send-btn:hover { + border-color: var(--sl-color-gray-3); + background: rgba(255, 255, 255, 0.08); + color: var(--sl-color-white); +} + +.docs-feedback__send-btn:focus-visible { + outline: 2px solid var(--sl-color-accent); + outline-offset: 2px; +} + +/* -------------------------------------------------------------------------- + Success state + -------------------------------------------------------------------------- */ + +.docs-feedback__success { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: var(--sl-text-sm); + color: var(--sl-color-gray-2); + padding: 0.25rem 0; +} + +.docs-feedback__success-check { + color: var(--sl-color-accent); + flex-shrink: 0; +} + +/* -------------------------------------------------------------------------- + Light mode overrides + -------------------------------------------------------------------------- */ + +:root[data-theme='light'] .docs-feedback__vote-btn:hover, +:root[data-theme='light'] .docs-feedback__send-btn:hover { + background: rgba(0, 0, 0, 0.05); +} diff --git a/src/components/DocsFeedback.tsx b/src/components/DocsFeedback.tsx new file mode 100644 index 000000000..8157c91a5 --- /dev/null +++ b/src/components/DocsFeedback.tsx @@ -0,0 +1,155 @@ +import { useState } from 'react'; +import { LuThumbsUp, LuThumbsDown, LuCheck } from 'react-icons/lu'; +import './DocsFeedback.css'; + +interface Props { + pageSlug: string; + pageTitle: string; +} + +type Step = 'idle' | 'expanded' | 'submitted'; +type Rating = 'positive' | 'negative'; + +interface Category { + label: string; + description: string; +} + +const POSITIVE_CATEGORIES: Category[] = [ + { label: 'Accurate', description: 'Accurately describes the feature.' }, + { label: 'Solved my problem', description: 'Helped me resolve an issue.' }, + { label: 'Easy to understand', description: 'Clear and well-written.' }, + { label: 'Another reason', description: '' }, +]; + +const NEGATIVE_CATEGORIES: Category[] = [ + { label: 'Inaccurate', description: 'Contains incorrect information.' }, + { label: 'Missing information', description: "Couldn't find what I needed." }, + { label: 'Hard to understand', description: 'Too complicated or confusing.' }, + { label: 'Outdated', description: 'Information is no longer current.' }, + { label: 'Another reason', description: '' }, +]; + +export default function DocsFeedback({ pageSlug, pageTitle }: Props) { + const [step, setStep] = useState('idle'); + const [rating, setRating] = useState(null); + const [category, setCategory] = useState(''); + const [comment, setComment] = useState(''); + + const handleRating = (r: Rating) => { + setRating(r); + setCategory(''); + setComment(''); + setStep('expanded'); + }; + + const handleSend = () => { + const payload: Record = { + page_url: window.location.href, + page_slug: pageSlug, + page_title: pageTitle.replace(/ \| Warp$/, ''), + rating: rating === 'positive' ? 'positive' : 'negative', + }; + if (category && category !== 'Another reason') { + payload.category = category; + } + const trimmedComment = comment.trim(); + if (trimmedComment) { + payload.comment = trimmedComment; + } + + // rudderanalytics is stubbed as a queue array by RudderStackAnalytics.astro + // before the full SDK loads, so calling .track() here is always safe. + const ra = (window as any).rudderanalytics; + if (ra) { + ra.track('docs_feedback_submitted', payload); + } + + setStep('submitted'); + }; + + const categories = rating === 'positive' ? POSITIVE_CATEGORIES : NEGATIVE_CATEGORIES; + const formHeader = rating === 'positive' ? 'What did you like?' : 'What could we improve?'; + + return ( +
+ + + {step === 'idle' && ( +
+ Was this page useful? +
+ + +
+
+ )} + + {step === 'expanded' && ( +
+

{formHeader}

+
+ {categories.map(({ label, description }) => ( + + ))} +
+