Improve Button and ButtonGroup styling (#3540)

This commit is contained in:
Zeno Kapitein
2025-08-06 16:10:55 +02:00
committed by GitHub
parent fb858a1941
commit d30bcbabdb
12 changed files with 102 additions and 86 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Improve `Button` and `ButtonGroup` styling
@@ -231,13 +231,15 @@ function AIActionWrapper(props: {
}
size="xsmall"
variant="secondary"
label={shortLabel || label}
label={label ?? shortLabel}
className="bg-tint-base text-sm"
onClick={onClick}
href={href}
target={href ? '_blank' : undefined}
disabled={disabled || loading}
/>
>
{shortLabel}
</Button>
);
}
@@ -8,6 +8,7 @@ import {
} from '@/components/AIActions/AIActions';
import { Button, ButtonGroup } from '@/components/primitives/Button';
import { DropdownMenu, DropdownMenuSeparator } from '@/components/primitives/DropdownMenu';
import { tString, useLanguage } from '@/intl/client';
import type { SiteCustomizationSettings } from '@gitbook/api';
import { Icon } from '@gitbook/icons';
@@ -25,6 +26,7 @@ interface AIActionsDropdownProps {
*/
export function AIActionsDropdown(props: AIActionsDropdownProps) {
const ref = useRef<HTMLDivElement>(null);
const language = useLanguage();
return (
<ButtonGroup ref={ref}>
@@ -41,6 +43,7 @@ export function AIActionsDropdown(props: AIActionsDropdownProps) {
className="size-3 transition-transform group-data-[state=open]/button:rotate-180"
/>
}
label={tString(language, 'more')}
iconOnly
size="xsmall"
variant="secondary"
@@ -11,8 +11,8 @@ import { AIChatIcon } from './AIChatIcon';
/**
* Button to open/close the AI chat.
*/
export function AIChatButton(props: { trademark: boolean }) {
const { trademark } = props;
export function AIChatButton(props: { trademark: boolean; withLabel: boolean }) {
const { trademark, withLabel } = props;
const chatController = useAIChatController();
const language = useLanguage();
@@ -20,8 +20,8 @@ export function AIChatButton(props: { trademark: boolean }) {
<Button
icon={<AIChatIcon trademark={trademark} />}
data-testid="ai-chat-button"
iconOnly
size="default"
iconOnly={!withLabel}
size="medium"
variant="header"
className={tcls('h-9 px-2.5')}
label={
@@ -33,6 +33,8 @@ export function AIChatButton(props: { trademark: boolean }) {
onClick={() => {
chatController.open();
}}
/>
>
{withLabel ? <span className="max-md:hidden">{t(language, 'ask')}</span> : null}
</Button>
);
}
@@ -17,7 +17,6 @@ export default function AIChatSuggestedQuestions(props: { chatController: AIChat
{DEFAULT_SUGGESTED_QUESTIONS.map((question, index) => (
<Button
key={question}
label={question}
variant="secondary"
size="medium"
className="max-w-full animate-[present_500ms_both] whitespace-normal"
@@ -43,7 +43,7 @@ export const CodeBlockRenderer = forwardRef(function CodeBlockRenderer(
</div>
<CopyCodeButton
codeId={id}
style="z-2 mt-2 mr-2 self-start justify-self-end rounded-md bg-transparent p-1 text-tint text-xs leading-none opacity-0 ring-1 ring-tint backdrop-blur-md transition-opacity duration-75 [grid-area:2/1] hover:ring-tint-hover group-hover/codeblock:opacity-11"
style="z-2 mt-2 mr-2 self-start justify-self-end leading-none opacity-0 backdrop-blur-md [grid-area:2/1] group-hover/codeblock:opacity-11"
/>
<pre
className={tcls(
@@ -2,6 +2,7 @@
import React from 'react';
import { Button } from '@/components/primitives';
import { t, useLanguage } from '@/intl/client';
import { type ClassValue, tcls } from '@/lib/tailwind';
@@ -41,9 +42,14 @@ export function CopyCodeButton(props: { codeId: string; style: ClassValue }) {
};
return (
<button onClick={onClick} className={tcls(style, 'print:hidden')}>
<Button
size="xsmall"
variant="secondary"
onClick={onClick}
className={tcls(style, 'translate-y-0!', 'print:hidden')}
>
{t(language, copied ? 'code_copied' : 'code_copy')}
</button>
</Button>
);
}
@@ -8,7 +8,7 @@ import { t, tString } from '@/intl/translate';
import { tcls } from '@/lib/tailwind';
import { useTrackEvent } from '../Insights';
import { Button } from '../primitives';
import { Button, ButtonGroup } from '../primitives';
const MAX_COMMENT_LENGTH = 512;
@@ -61,31 +61,29 @@ export function PageFeedbackForm(props: {
<div className={tcls('flex flex-col gap-3 text-sm', className)}>
<div className="flex flex-wrap items-center gap-2">
<p>{t(languages, 'was_this_helpful')}</p>
<div className="rounded-full border border-tint-subtle bg-tint-base contrast-more:border-tint-12">
<div className="flex">
<RatingButton
rating={PageFeedbackRating.Good}
label={tString(languages, 'was_this_helpful_positive')}
onClick={() => onSubmitRating(PageFeedbackRating.Good)}
active={rating === PageFeedbackRating.Good}
disabled={rating !== undefined}
/>
<RatingButton
rating={PageFeedbackRating.Ok}
label={tString(languages, 'was_this_helpful_neutral')}
onClick={() => onSubmitRating(PageFeedbackRating.Ok)}
active={rating === PageFeedbackRating.Ok}
disabled={rating !== undefined}
/>
<RatingButton
rating={PageFeedbackRating.Bad}
label={tString(languages, 'was_this_helpful_negative')}
onClick={() => onSubmitRating(PageFeedbackRating.Bad)}
active={rating === PageFeedbackRating.Bad}
disabled={rating !== undefined}
/>
</div>
</div>
<ButtonGroup>
<RatingButton
rating={PageFeedbackRating.Good}
label={tString(languages, 'was_this_helpful_positive')}
onClick={() => onSubmitRating(PageFeedbackRating.Good)}
active={rating === PageFeedbackRating.Good}
disabled={rating !== undefined}
/>
<RatingButton
rating={PageFeedbackRating.Ok}
label={tString(languages, 'was_this_helpful_neutral')}
onClick={() => onSubmitRating(PageFeedbackRating.Ok)}
active={rating === PageFeedbackRating.Ok}
disabled={rating !== undefined}
/>
<RatingButton
rating={PageFeedbackRating.Bad}
label={tString(languages, 'was_this_helpful_negative')}
onClick={() => onSubmitRating(PageFeedbackRating.Bad)}
active={rating === PageFeedbackRating.Bad}
disabled={rating !== undefined}
/>
</ButtonGroup>
</div>
{rating ? (
<div className="flex flex-col gap-2">
@@ -148,9 +146,9 @@ function RatingButton(
}[rating] ?? null;
return (
<button
<Button
className={tcls(
'p-2 first:rounded-l-full first:pl-2.5 last:rounded-r-full last:pr-2.5 hover:bg-primary-hover hover:text-primary-strong',
'border-x-0 bg-tint-base p-2 first:rounded-l-full first:border-l-1 first:pl-2.5 last:rounded-r-full last:border-r-1 last:pr-2.5 hover:bg-primary-hover hover:text-primary-strong',
'disabled:cursor-not-allowed disabled:hover:bg-inherit disabled:hover:text-inherit dark:disabled:hover:text-inherit',
'ring-tint contrast-more:hover:ring-1',
active
@@ -158,13 +156,13 @@ function RatingButton(
: 'disabled:opacity-7 contrast-more:disabled:ring-0'
)}
type="button"
{...attr}
aria-label={label}
title={label}
variant="secondary"
label={label}
onClick={onClick}
>
{ratingIcon}
</button>
iconOnly
{...attr}
icon={ratingIcon}
/>
);
}
@@ -1,5 +1,9 @@
import type { GitBookSiteContext } from '@/lib/context';
import { CustomizationAIMode, CustomizationHeaderPreset } from '@gitbook/api';
import {
CustomizationAIMode,
CustomizationHeaderPreset,
CustomizationSearchStyle,
} from '@gitbook/api';
import React from 'react';
import { Footer } from '@/components/Footer';
@@ -62,7 +66,13 @@ export function SpaceLayout(props: {
/>
</React.Suspense>
{aiMode === CustomizationAIMode.Assistant ? (
<AIChatButton trademark={customization.trademark.enabled} />
<AIChatButton
withLabel={
withTopHeader &&
customization.styling.search === CustomizationSearchStyle.Prominent
}
trademark={customization.trademark.enabled}
/>
) : null}
</div>
);
@@ -1,11 +1,11 @@
'use client';
import { Icon, type IconName } from '@gitbook/icons';
import type { IconName } from '@gitbook/icons';
import { useTheme } from 'next-themes';
import React from 'react';
import { tString, useLanguage } from '@/intl/client';
import { tcls } from '@/lib/tailwind';
import { Button, ButtonGroup } from '../primitives';
type ThemeMode = 'light' | 'system' | 'dark';
@@ -27,7 +27,7 @@ export function ThemeToggler() {
};
return (
<div role="radiogroup" className="flex flex-row gap-2">
<ButtonGroup role="radiogroup" className="gap-2" combinedShape={false}>
<ThemeButton
active={mounted && theme === 'light'}
icon="sun-bright"
@@ -46,7 +46,7 @@ export function ThemeToggler() {
onClick={() => onSwitchMode('dark')}
title={tString(language, 'switch_to_dark_theme')}
/>
</div>
</ButtonGroup>
);
}
@@ -58,38 +58,21 @@ function ThemeButton(props: {
}) {
const { icon, onClick, title, active } = props;
return (
<button
<Button
type="button"
role="radio"
onClick={onClick}
aria-label={title}
title={title}
label={title}
aria-checked={active}
className={tcls(
'p-2',
'rounded-sm',
'straight-corners:rounded-none',
'circular-corners:rounded-full',
'transition-all',
'text-tint',
'contrast-more:text-tint-strong',
'hover:bg-tint-hover',
'contrast-more:hover:ring-2',
'contrast-more:focus:ring-2',
'ring-tint',
active && [
'bg-primary',
'theme-muted:bg-primary-hover',
'[html.sidebar-filled.theme-bold.tint_&]:bg-primary-hover',
'hover:bg-primary',
'text-primary-strong',
'contrast-more:text-primary-strong',
'contrast-more:ring-1',
'ring-primary',
]
)}
>
<Icon icon={icon} className={tcls('size-4')} />
</button>
variant="blank"
size="default"
className={
active
? 'bg-primary theme-muted:bg-primary-hover text-primary-strong ring-primary hover:bg-primary contrast-more:text-primary-strong contrast-more:ring-1 [html.sidebar-filled.theme-bold.tint_&]:bg-primary-hover'
: ''
}
icon={icon}
iconOnly
/>
);
}
@@ -135,7 +135,7 @@ export const Button = React.forwardRef<
icon
)
) : null}
{iconOnly ? null : label}
{iconOnly ? null : (children ?? label)}
</>
);
@@ -171,7 +171,7 @@ export const Button = React.forwardRef<
</button>
);
return iconOnly && label ? (
return (children || iconOnly) && label ? (
<Tooltip
rootProps={{ open: disabled === true ? false : undefined }}
label={label}
@@ -185,14 +185,21 @@ export const Button = React.forwardRef<
}
);
export const ButtonGroup = React.forwardRef<HTMLDivElement, ButtonProps>(({ children }, ref) => {
export const ButtonGroup = React.forwardRef<
HTMLDivElement,
ButtonProps & { combinedShape?: boolean }
>(({ children, className, combinedShape = true, ...rest }, ref) => {
return (
<div
ref={ref}
className={tcls(
'flex h-fit items-stretch justify-start *:translate-y-0! *:shadow-none!',
'[&>*:not(:first-child)]:border-l-0 [&>*:not(:first-child,:last-child)]:rounded-none [&>*:not(:only-child):first-child]:rounded-r-none [&>*:not(:only-child):last-child]:rounded-l-none'
'flex h-fit items-stretch justify-start',
combinedShape
? '*:translate-y-0! *:shadow-none! [&>*:not(:first-child)]:border-l-0 [&>*:not(:first-child,:last-child)]:rounded-none [&>*:not(:only-child):first-child]:rounded-r-none [&>*:not(:only-child):last-child]:rounded-l-none'
: '',
className
)}
{...rest}
>
{children}
</div>
@@ -17,7 +17,7 @@ export const ButtonStyles = [
'depth-subtle:shadow-xs',
'hover:depth-subtle:shadow-md',
'focus-visible:depth-subtle:shadow-md',
'active:shadow-none',
'active:depth-subtle:shadow-xs',
'shadow-tint/6',
'dark:shadow-tint-1',
@@ -27,6 +27,7 @@ export const ButtonStyles = [
'hover:depth-subtle:-translate-y-px',
'focus-visible:depth-subtle:-translate-y-px',
'active:depth-subtle:translate-y-0',
'transition-all',
'grow-0',