mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-13 21:01:21 +08:00
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> [](https://discord.gg/n3SsVDAW6U) [ <!-- 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> for5547ddb9c3. 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:
parent
d37b7ea7c8
commit
6bc8054ac4
@ -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>
|
||||
)}
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user