feat(panel): dense tooltip pass — projects, products, social, KB, business, settings, notifications (178 tips)

Density bar: every dialog field, column header, filter, badge, and
status pill explains itself — grounded in the backend (ladder
normalization, rate-limit park/probe, self-hosted fallback, reindex
scope). Disabled buttons span-wrap so tips fire; tabs re-assert
data-state.
This commit is contained in:
Renn F
2026-07-15 17:56:12 +02:00
parent 84563e84b6
commit 57254b3ce2
38 changed files with 1205 additions and 655 deletions
@@ -144,9 +144,14 @@ function UnifiedHistoryRow({ row }: { row: UnifiedRow }) {
{PLATFORM_LABELS[platform] ?? platform}: {id}
</a>
) : (
<span key={platform} className="text-muted-foreground">
{PLATFORM_LABELS[platform] ?? platform}: {id}
</span>
<HelpTip
key={platform}
label="TikTok's inbox-upload API returns no public URL — this is the platform-side post id"
>
<span className="text-muted-foreground">
{PLATFORM_LABELS[platform] ?? platform}: {id}
</span>
</HelpTip>
),
)}
</div>
@@ -211,7 +216,9 @@ export function SocialHistorySection({ className }: { className?: string }) {
size="sm"
className="w-full justify-between"
>
<span>{open ? "Hide" : "Show"} history</span>
<HelpTip label="Fetches both sources once, on first expand — up to 50 rows each">
<span>{open ? "Hide" : "Show"} history</span>
</HelpTip>
<ChevronDown
className={`h-4 w-4 transition-transform ${open ? "rotate-180" : ""}`}
/>
@@ -76,9 +76,11 @@ function PipelineRow({ item }: { item: VideoPipelineItem }) {
prefetch={false}
className="ml-auto"
>
<Button variant="outline" size="sm">
Review
</Button>
<HelpTip label="Open the task to approve or reject the rendered video">
<Button variant="outline" size="sm">
Review
</Button>
</HelpTip>
</Link>
)}
{canRerender && (
@@ -110,7 +112,9 @@ export function VideoPipelineStrip({ className }: { className?: string }) {
<CardTitle className="flex items-center gap-2">
<Film className="h-5 w-5" />
Video Pipeline
<Badge variant="secondary">{items.length}</Badge>
<HelpTip label="Videos currently authoring, in review, or rendering — not yet in the queue below">
<Badge variant="secondary">{items.length}</Badge>
</HelpTip>
</CardTitle>
<CardDescription>
Every video in flight authoring, review, or rendering before it
@@ -483,7 +483,9 @@ function RequestVideoDialog({
<>
<div className="space-y-4">
<div className="space-y-2">
<Label>Project</Label>
<HelpTip label="Only projects with the video engine enabled (Projects → Edit) can be picked">
<Label>Project</Label>
</HelpTip>
<ProjectSelector
value={effectiveProjectId}
onChange={setProjectId}
@@ -493,7 +495,9 @@ function RequestVideoDialog({
/>
</div>
<div className="space-y-2">
<Label htmlFor="video-request-occasion">Occasion</Label>
<HelpTip label="Short label for the trigger event — shown as a badge throughout the pipeline, queue, and history">
<Label htmlFor="video-request-occasion">Occasion</Label>
</HelpTip>
<Input
id="video-request-occasion"
placeholder="e.g. v0.19.0 launch, Founder's Day..."
@@ -502,7 +506,9 @@ function RequestVideoDialog({
/>
</div>
<div className="space-y-2">
<Label htmlFor="video-request-brief">Brief</Label>
<HelpTip label="What the video should communicate — read by the assigned UX/UI dev when building the composition">
<Label htmlFor="video-request-brief">Brief</Label>
</HelpTip>
<Textarea
id="video-request-brief"
placeholder="What should this video cover?"
@@ -512,7 +518,9 @@ function RequestVideoDialog({
/>
</div>
<div className="space-y-2">
<Label>Platforms</Label>
<HelpTip label="Where the rendered clip can post once you approve it — editable again before approving">
<Label>Platforms</Label>
</HelpTip>
<div className="flex gap-4">
{REQUEST_PLATFORMS.map((platform) => (
<div key={platform} className="flex items-center gap-2">
@@ -521,12 +529,20 @@ function RequestVideoDialog({
checked={platforms.includes(platform)}
onCheckedChange={() => togglePlatform(platform)}
/>
<Label
htmlFor={`video-request-${platform}`}
className="text-sm font-normal"
<HelpTip
label={
platform === "x"
? "Posts via X's OAuth 1.0a API, 280-char caption"
: "Posts via TikTok's inbox-upload OAuth2 API, 2200-char caption"
}
>
{PLATFORM_LABELS[platform]}
</Label>
<Label
htmlFor={`video-request-${platform}`}
className="text-sm font-normal"
>
{PLATFORM_LABELS[platform]}
</Label>
</HelpTip>
</div>
))}
</div>
@@ -654,10 +670,12 @@ export function VideoPostQueue({ className }: { className?: string }) {
if (isLoading) return null;
const requestButton = (
<Button variant="outline" size="sm" onClick={() => setRequestOpen(true)}>
<Sparkles className="mr-1 h-4 w-4" />
Request a video
</Button>
<HelpTip label="Opens a dialog to brief a UX/UI dev on an on-demand video, independent of a release or spotlight">
<Button variant="outline" size="sm" onClick={() => setRequestOpen(true)}>
<Sparkles className="mr-1 h-4 w-4" />
Request a video
</Button>
</HelpTip>
);
if (!posts || posts.length === 0) {
@@ -696,7 +714,9 @@ export function VideoPostQueue({ className }: { className?: string }) {
<CardTitle className="flex items-center gap-2">
<Film className="h-5 w-5" />
Video Post Queue
<Badge variant="secondary">{posts.length}</Badge>
<HelpTip label="Rendered clips waiting for your preview, caption edits, approve, or reject">
<Badge variant="secondary">{posts.length}</Badge>
</HelpTip>
</CardTitle>
<CardAction>{requestButton}</CardAction>
<CardDescription>
@@ -727,7 +747,9 @@ export function VideoPostQueue({ className }: { className?: string }) {
</DialogDescription>
</DialogHeader>
<div className="space-y-2">
<Label htmlFor="video-reject-reason">Reason</Label>
<HelpTip label="Stored on the draft and shown in History next to this rejection">
<Label htmlFor="video-reject-reason">Reason</Label>
</HelpTip>
<Textarea
id="video-reject-reason"
placeholder="e.g. off-brand tone, wrong occasion..."
@@ -109,17 +109,23 @@ function XPostRow({
</span>
</HelpTip>
{post.release_version && (
<Badge variant="outline">v{post.release_version}</Badge>
<HelpTip label="The release this post announces">
<Badge variant="outline">v{post.release_version}</Badge>
</HelpTip>
)}
{post.mention && (
<Badge variant="secondary" className="max-w-56 truncate">
re: {post.mention.text}
</Badge>
<HelpTip label="The X mention this draft replies to">
<Badge variant="secondary" className="max-w-56 truncate">
re: {post.mention.text}
</Badge>
</HelpTip>
)}
{post.feature && (
<Badge variant="outline" className="max-w-56 truncate">
feature: {post.feature.title}
</Badge>
<HelpTip label="The shipped feature this spotlight covers">
<Badge variant="outline" className="max-w-56 truncate">
feature: {post.feature.title}
</Badge>
</HelpTip>
)}
</div>
@@ -266,7 +272,9 @@ export function XPostQueue({ className }: { className?: string }) {
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Rocket className="h-5 w-5" />X Post Queue
<Badge variant="secondary">{posts.length}</Badge>
<HelpTip label="Drafts waiting for your edit, approve, or reject">
<Badge variant="secondary">{posts.length}</Badge>
</HelpTip>
</CardTitle>
<CardDescription>
Drafted release announcements, feature spotlights, and mention
@@ -297,7 +305,9 @@ export function XPostQueue({ className }: { className?: string }) {
</DialogDescription>
</DialogHeader>
<div className="space-y-2">
<Label htmlFor="x-reject-reason">Reason</Label>
<HelpTip label="Stored on the draft and shown in History next to this rejection">
<Label htmlFor="x-reject-reason">Reason</Label>
</HelpTip>
<Textarea
id="x-reject-reason"
placeholder="e.g. tone doesn't match our voice; not worth a public reply..."