add codeblock tooltips and fix spacing (#934)

<img width="549" height="576" alt="Screenshot 2025-10-07 at 4 15 53 PM"
src="https://github.com/user-attachments/assets/7dd63333-691d-42f2-996f-80a22e9effca"
/>
<!--

Make sure you've read the CONTRIBUTING.md guidelines:
https://github.com/stack-auth/stack-auth/blob/dev/CONTRIBUTING.md

-->

<!-- RECURSEML_SUMMARY:START -->
## High-level PR Summary
This PR adds an optional tooltip feature to code blocks and improves the
UI layout and spacing in the product catalog view. The `CodeBlock`
component now accepts a `tooltip` prop that displays an info tooltip
next to the copy button. The product catalog view receives several
layout improvements including better spacing, reorganized form controls,
and the addition of tooltips to code blocks that explain their purpose.
The changes also swap item ID display for item display names in some
places for better readability.

⏱️ Estimated Review Time: 15-30 minutes

<details>
<summary>💡 Review Order Suggestion</summary>

| Order | File Path |
|-------|-----------|
| 1 | `apps/dashboard/src/components/code-block.tsx` |
| 2 |
`apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx`
|
</details>



[![Need help? Join our
Discord](https://img.shields.io/badge/Need%20help%3F%20Join%20our%20Discord-5865F2?style=plastic&logo=discord&logoColor=white)](https://discord.gg/n3SsVDAW6U)


[![Analyze latest
changes](af2bef317b/?repo_owner=stack-auth&repo_name=stack-auth&pr_number=934)
<!-- RECURSEML_SUMMARY:END -->
<!-- ELLIPSIS_HIDDEN -->

----

> [!IMPORTANT]
> Add tooltips to code blocks and adjust spacing in `ProductItemRow` for
improved UI and readability.
> 
>   - **Code Block Enhancements**:
> - Add `tooltip` prop to `CodeBlock` in `code-block.tsx` for displaying
tooltips with additional information.
>     - Use `SimpleTooltip` to show tooltips next to the copy button.
>   - **UI Adjustments**:
> - Adjust spacing in `ProductItemRow` in
`page-client-catalogs-view.tsx` for better layout.
>     - Replace `itemId` with `itemDisplayName` for better readability.
> - Add tooltips to buttons in `ProductCard` for checkout and item
retrieval.
> 
> <sup>This description was created by </sup>[<img alt="Ellipsis"
src="https://img.shields.io/badge/Ellipsis-blue?color=175173">](https://www.ellipsis.dev?ref=stack-auth%2Fstack-auth&utm_source=github&utm_medium=referral)<sup>
for 5547ddb9c3. You can
[customize](https://app.ellipsis.dev/stack-auth/settings/summaries) this
summary. It will automatically update as commits are pushed.</sup>

<!-- ELLIPSIS_HIDDEN -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- New Features
  - Inline tooltips on code blocks and checkout actions.
- Per-item expiration editing via a dropdown, including a “Never
expires” option.
  - Remove item action integrated directly into the editing row.

- UI Improvements
- Redesigned product item editing layout with clearer headers, spacing,
and grouped controls.
  - Consistent use of item display names across views and cards.
- Enhanced checkout code block in product cards with explanatory
tooltip.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Konsti Wohlwend <n2d4xc@gmail.com>
This commit is contained in:
BilalG1 2025-10-10 12:03:46 -07:00 committed by GitHub
parent d37b7ea7c8
commit 6bc8054ac4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 71 additions and 55 deletions

View File

@ -497,12 +497,14 @@ function ProductItemRow({
if (isEditing) {
return (
<div className="flex flex-col gap-1 mb-4">
<div className="flex flex-row">
<div className="flex flex-col gap-2 mb-4">
<div className="flex w-full items-center justify-between gap-2">
<Popover open={itemSelectOpen} onOpenChange={setItemSelectOpen}>
<PopoverTrigger>
<div className="text-sm px-2 py-0.5 rounded bg-muted hover:bg-muted/70 cursor-pointer select-none flex items-center gap-1">
{itemId}
<span className="overflow-x-auto max-w-24">
{itemDisplayName}
</span>
<ChevronsUpDown className="h-4 w-4" />
</div>
</PopoverTrigger>
@ -554,16 +556,54 @@ function ProductItemRow({
</div>
</PopoverContent>
</Popover>
<Input
className="ml-auto w-20 text-right tabular-nums mr-2"
inputMode="numeric"
value={quantity}
onChange={(e) => {
const v = e.target.value;
if (v === '' || /^\d*$/.test(v)) setQuantity(v);
if (!readOnly && (v === '' || /^\d*$/.test(v))) updateParent(v);
}}
/>
<div className="flex items-center gap-2">
<Input
className="w-24 text-right tabular-nums"
inputMode="numeric"
value={quantity}
onChange={(e) => {
const v = e.target.value;
if (v === '' || /^\d*$/.test(v)) setQuantity(v);
if (!readOnly && (v === '' || /^\d*$/.test(v))) updateParent(v);
}}
/>
{onRemove && (
<button className="text-muted-foreground hover:text-foreground" onClick={onRemove} aria-label="Remove item">
<X className="h-4 w-4" />
</button>
)}
</div>
</div>
<div className="flex w-full items-center justify-between gap-2">
<div className="flex items-center gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="text-xs px-2 py-0.5 w-fit rounded bg-muted text-muted-foreground cursor-pointer select-none flex items-center gap-1">
{item.expires === 'never' ? 'Never expires' : `${EXPIRES_OPTIONS.find(o => o.value === item.expires)?.label.toLowerCase()}`}
<ChevronsUpDown className="h-4 w-4" />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-2">
<div className="flex flex-col gap-2">
{EXPIRES_OPTIONS.map((option) => (
<DropdownMenuItem key={option.value}>
<Button
key={option.value}
variant="ghost"
size="sm"
className="flex flex-col items-start"
onClick={() => {
onSave(itemId, { ...item, expires: option.value });
}}>
{option.label}
<span className="text-xs text-muted-foreground">{option.description}</span>
</Button>
</DropdownMenuItem>
))}
</div>
</DropdownMenuContent>
</DropdownMenu>
</div>
<IntervalPopover
readOnly={readOnly}
intervalText={repeatText}
@ -583,41 +623,6 @@ function ProductItemRow({
onSave(itemId, updated);
}}
/>
{onRemove && (
<button className="ml-auto" onClick={onRemove} aria-label="Remove item">
<X className="h-4 w-4" />
</button>
)}
</div>
<div className="flex flex-row items-center gap-2">
<span className="text-xs text-muted-foreground">Expires:</span>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="text-xs px-2 py-0.5 w-fit rounded bg-muted text-muted-foreground cursor-pointer select-none flex items-center gap-1">
{item.expires === 'never' ? 'Never expires' : `${EXPIRES_OPTIONS.find(o => o.value === item.expires)?.label.toLowerCase()}`}
<ChevronsUpDown className="h-4 w-4" />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-2">
<div className="flex flex-col gap-2">
{EXPIRES_OPTIONS.map((option) => (
<DropdownMenuItem key={option.value}>
<Button
key={option.value}
variant="ghost"
size="sm"
className="flex flex-col items-start"
onClick={() => {
onSave(itemId, { ...item, expires: option.value });
}}>
{option.label}
<span className="text-xs text-muted-foreground">{option.description}</span>
</Button>
</DropdownMenuItem>
))}
</div>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
);
@ -633,7 +638,7 @@ function ProductItemRow({
<ChevronDown className={cn("h-4 w-4 transition-transform", isOpen ? "rotate-0" : "-rotate-90")} />
</button>
</CollapsibleTrigger >
<div className="text-sm">{itemId}</div>
<div className="text-sm">{itemDisplayName}</div>
<div className="ml-auto w-16 text-right text-sm text-muted-foreground tabular-nums">{prettyPrintWithMagnitudes(item.quantity)}</div>
<div className="ml-2">
<div className="text-xs px-2 py-0.5 rounded bg-muted text-muted-foreground">{shortRepeatText}</div>
@ -667,6 +672,7 @@ function ProductItemRow({
title="Example"
icon="code"
compact
tooltip="Retrieves this item for the active customer and reads the current quantity they hold."
/>
</div>
</div>
@ -966,7 +972,7 @@ function ProductCard({ id, activeType, product, allProducts, existingItems, onSa
<div className="space-y-2">
{itemsList.map(([itemId, item]) => {
const itemMeta = existingItems.find(i => i.id === itemId);
const itemLabel = itemMeta ? (itemMeta.displayName || itemMeta.id) : 'Select item';
const itemLabel = itemMeta ? itemMeta.id : 'Select item';
return (
<ProductItemRow
key={itemId}
@ -1072,6 +1078,7 @@ function ProductCard({ id, activeType, product, allProducts, existingItems, onSa
title="Checkout"
icon="code"
compact
tooltip="Creates a checkout URL for this product and opens it so the customer can finish their purchase."
/>
</div>
)}

View File

@ -1,7 +1,7 @@
'use client';
import { useThemeWatcher } from '@/lib/theme';
import { CopyButton } from "@stackframe/stack-ui";
import { CopyButton, SimpleTooltip } from "@stackframe/stack-ui";
import { Code, Terminal } from "lucide-react";
import { PrismLight as SyntaxHighlighter } from 'react-syntax-highlighter';
import bash from 'react-syntax-highlighter/dist/esm/languages/prism/bash';
@ -9,21 +9,25 @@ import python from 'react-syntax-highlighter/dist/esm/languages/prism/python';
import tsx from 'react-syntax-highlighter/dist/esm/languages/prism/tsx';
import typescript from 'react-syntax-highlighter/dist/esm/languages/prism/typescript';
import { dark, prism } from 'react-syntax-highlighter/dist/esm/styles/prism';
import type { ReactNode } from 'react';
import { cn } from '@/lib/utils';
Object.entries({ tsx, bash, typescript, python }).forEach(([key, value]) => {
SyntaxHighlighter.registerLanguage(key, value);
});
export function CodeBlock(props: {
type CodeBlockProps = {
language: string,
content: string,
customRender?: React.ReactNode,
customRender?: ReactNode,
title: string,
icon: 'terminal' | 'code',
maxHeight?: number,
compact?: boolean,
}) {
tooltip?: ReactNode,
};
export function CodeBlock(props: CodeBlockProps) {
const { theme, mounted } = useThemeWatcher();
let icon = null;
@ -45,7 +49,12 @@ export function CodeBlock(props: {
{icon}
{props.title}
</h5>
<CopyButton content={props.content} />
<div className="flex items-center gap-2">
{props.tooltip && (
<SimpleTooltip type="info" tooltip={props.tooltip} />
)}
<CopyButton content={props.content} />
</div>
</div>
<div>
{props.customRender ?? <SyntaxHighlighter