Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
208 changes: 208 additions & 0 deletions src/components/DocsFeedback.css
Original file line number Diff line number Diff line change
@@ -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);
}
155 changes: 155 additions & 0 deletions src/components/DocsFeedback.tsx
Original file line number Diff line number Diff line change
@@ -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<Step>('idle');
const [rating, setRating] = useState<Rating | null>(null);
const [category, setCategory] = useState<string>('');
const [comment, setComment] = useState<string>('');

const handleRating = (r: Rating) => {
setRating(r);
setCategory('');
setComment('');
setStep('expanded');
};

const handleSend = () => {
const payload: Record<string, string> = {
page_url: window.location.href,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 [SUGGESTION] [SECURITY] Avoid sending query strings or fragments in this telemetry URL; match the existing docs_404 event pattern and report only the canonical page path.

Suggested change
page_url: window.location.href,
page_url: window.location.origin + window.location.pathname,

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 (
<div className="docs-feedback">
<hr className="docs-feedback__divider" aria-hidden="true" />

{step === 'idle' && (
<div className="docs-feedback__prompt">
<span className="docs-feedback__question">Was this page useful?</span>
<div className="docs-feedback__buttons">
<button
type="button"
className="docs-feedback__vote-btn"
onClick={() => handleRating('positive')}
aria-label="Yes, this page was useful"
>
<LuThumbsUp className="docs-feedback__vote-icon" aria-hidden="true" />
Yes
</button>
<button
type="button"
className="docs-feedback__vote-btn"
onClick={() => handleRating('negative')}
aria-label="No, this page was not useful"
>
<LuThumbsDown className="docs-feedback__vote-icon" aria-hidden="true" />
No
</button>
</div>
</div>
)}

{step === 'expanded' && (
<div className="docs-feedback__form">
<h3 className="docs-feedback__form-header">{formHeader}</h3>
<div
className="docs-feedback__categories"
role="radiogroup"
aria-label={formHeader}
>
{categories.map(({ label, description }) => (
<label key={label} className="docs-feedback__category">
<input
type="radio"
name="docs-feedback-category"
value={label}
checked={category === label}
onChange={() => setCategory(label)}
className="docs-feedback__radio"
/>
<span className="docs-feedback__category-label">{label}</span>
{description && (
<span className="docs-feedback__category-desc">{description}</span>
)}
</label>
))}
</div>
<textarea
className="docs-feedback__textarea"
placeholder="Any additional details (optional)"
value={comment}
onChange={(e) => setComment(e.target.value)}
rows={4}
aria-label="Additional details (optional)"
/>
<button
type="button"
className="docs-feedback__send-btn"
onClick={handleSend}
>
SEND
</button>
</div>
)}

{step === 'submitted' && (
<div className="docs-feedback__success" aria-live="polite" role="status">
<LuCheck className="docs-feedback__success-check" aria-hidden="true" />
<span>Thanks for your feedback!</span>
</div>
)}
</div>
);
}
7 changes: 7 additions & 0 deletions src/components/FeedbackFooter.astro
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,15 @@
// footer and keeps Pagination + the Starlight credits link.
import Pagination from 'virtual:starlight/components/Pagination';
import config from 'virtual:starlight/user-config';
import DocsFeedback from './DocsFeedback.tsx';

// starlightRoute.id is the content collection entry ID (URL-style slug).
// Strip any file extension (.mdx, .md, .astro) to get a clean path segment.
const pageSlug = Astro.locals.starlightRoute.id.replace(/\.(mdx?|astro)$/, '');
const pageTitle = Astro.locals.starlightRoute.entry.data.title;
---

<DocsFeedback client:idle pageSlug={pageSlug} pageTitle={pageTitle} />
<footer class="sl-flex">
<Pagination />

Expand Down
Loading