Compare commits

...

5 Commits

Author SHA1 Message Date
Nicolas Dorseuil 8a3fdcfd3c review fix 2025-06-17 23:19:09 +02:00
Nicolas Dorseuil a3ddf7d575 fix e2e test 2025-06-17 10:43:16 +02:00
Nicolas Dorseuil 447c6f602f add toggleable link 2025-06-17 10:03:14 +02:00
Nicolas Dorseuil 71e52f7b0d add more primitives 2025-06-17 09:44:46 +02:00
Nicolas Dorseuil 6703dae4ea initial replacement of tailwind class with css modules for client components 2025-06-16 21:46:21 +02:00
15 changed files with 155 additions and 186 deletions
+1 -1
View File
@@ -25,7 +25,7 @@
},
"scripts": {
"generate": "rm -rf ./public && cp -r ../gitbook/public ./public",
"dev:v2": "env-cmd --silent -f ../../.env.local next --turbopack",
"dev:v2": "env-cmd --silent -f ../../.env.local next",
"build": "next build",
"build:v2": "next build",
"start": "next start",
+2 -2
View File
@@ -893,7 +893,7 @@ const testCases: TestsCase[] = [
name: 'Navigation to shared space',
url: '',
run: async (page) => {
const sharedSpaceLink = page.locator('a.underline');
const sharedSpaceLink = page.locator('a.styled-link');
await sharedSpaceLink.click();
await expect(
page.getByRole('heading', { level: 1, name: 'shared' })
@@ -914,7 +914,7 @@ const testCases: TestsCase[] = [
name: 'Navigation to shared space',
url: '',
run: async (page) => {
await page.locator('a.underline').click();
await page.locator('a.styled-link').click();
await expect(
page.getByRole('heading', { level: 1, name: 'shared' })
).toBeVisible();
@@ -7,12 +7,14 @@ import {
import { LinkBox, LinkOverlay } from '@/components/primitives';
import { Image } from '@/components/utils';
import { resolveContentRef } from '@/lib/references';
import { type ClassValue, tcls } from '@/lib/tailwind';
import { tcls } from '@/lib/tailwind';
import { RecordColumnValue } from './RecordColumnValue';
import type { TableRecordKV, TableViewProps } from './Table';
import { getRecordValue } from './utils';
import styles from './recordCard.module.css';
export async function RecordCard(
props: TableViewProps<DocumentTableViewCards> & {
record: TableRecordKV;
@@ -143,35 +145,20 @@ export async function RecordCard(
</div>
);
const style = [
'group',
'grid',
'shadow-1xs',
'shadow-tint-9/1',
'depth-flat:shadow-none',
'rounded',
'straight-corners:rounded-none',
'circular-corners:rounded-xl',
'dark:shadow-transparent',
'before:pointer-events-none',
'before:grid-area-1-1',
'before:transition-shadow',
'before:w-full',
'before:h-full',
'before:rounded-[inherit]',
'before:ring-1',
'before:ring-tint-12/2',
'before:z-10',
'before:relative',
] as ClassValue;
if (target && targetRef) {
return (
// We don't use `Link` directly here because we could end up in a situation where
// a link is rendered inside a link, which is not allowed in HTML.
// It causes an hydration error in React.
<LinkBox href={target.href} className={tcls(style, 'hover:before:ring-tint-12/5')}>
<LinkBox
href={target.href}
className={tcls(
styles.card,
'hover:before:ring-tint-12/5',
'group',
'before:grid-area-1-1'
)}
>
<LinkOverlay
href={target.href}
insights={{
@@ -187,5 +174,5 @@ export async function RecordCard(
);
}
return <div className={tcls(style)}>{body}</div>;
return <div className={tcls('group', styles.card)}>{body}</div>;
}
@@ -0,0 +1,3 @@
.card {
@apply grid shadow-1xs shadow-tint-9/1 depth-flat:shadow-none rounded straight-corners:rounded-none circular-corners:rounded-xl dark:shadow-transparent before:pointer-events-none before:transition-shadow before:w-full before:h-full before:rounded-[inherit] before:ring-1 before:ring-tint-12/2 before:z-10 before:relative;
}
@@ -9,6 +9,8 @@ import type { GitBookSiteContext } from '@v2/lib/context';
import { PageIcon } from '../PageIcon';
import { StyledLink } from '../primitives';
import styles from './pageBody.module.css';
export async function PageHeader(props: {
context: GitBookSiteContext;
page: RevisionPageDocument;
@@ -40,22 +42,7 @@ export async function PageHeader(props: {
return (
<Fragment key={breadcrumb.id}>
<li key={breadcrumb.id}>
<StyledLink
href={href}
className={tcls(
'no-underline',
'hover:underline',
'text-xs',
'tracking-wide',
'font-semibold',
'uppercase',
'flex',
'items-center',
'gap-1.5',
'contrast-more:underline',
'contrast-more:decoration-current'
)}
>
<StyledLink href={href} className={styles.headerLink}>
<PageIcon
page={breadcrumb}
style="flex size-4 items-center justify-center text-base leading-none"
@@ -0,0 +1,3 @@
.headerLink {
@apply no-underline hover:underline text-xs tracking-wide font-semibold uppercase flex items-center gap-1.5 contrast-more:underline contrast-more:decoration-current;
}
@@ -8,6 +8,8 @@ import { tcls } from '@/lib/tailwind';
import { TOCPageIcon } from './TOCPageIcon';
import styles from './toc.module.css';
export async function PageLinkItem(props: { page: RevisionPageLink; context: GitBookSiteContext }) {
const { page, context } = props;
@@ -17,25 +19,7 @@ export async function PageLinkItem(props: { page: RevisionPageLink; context: Git
<li className={tcls('flex', 'flex-col')}>
<Link
href={resolved?.href ?? '#'}
className={tcls(
'flex',
'justify-start',
'items-center',
'gap-3',
'p-1.5',
'pl-3',
'text-sm',
'transition-colors',
'duration-100',
'text-tint-strong/7',
'rounded-md',
'straight-corners:rounded-none',
'circular-corners:rounded-xl',
'before:content-none',
'font-normal',
'hover:bg-tint',
'hover:text-tint-strong'
)}
className={styles.pageLinkItem}
insights={{
type: 'link_click',
link: {
@@ -10,6 +10,8 @@ import { useCurrentPagePath } from '../hooks';
import { Link, type LinkInsightsProps, type LinkProps } from '../primitives';
import { useScrollToActiveTOCItem } from './TOCScroller';
import styles from './toc.module.css';
/**
* Client component for a page document to toggle its children and be marked as active.
*/
@@ -67,40 +69,12 @@ function LinkItem(
insights={insights}
aria-current={isActive ? 'page' : undefined}
className={tcls(
'group/toclink toclink relative transition-colors',
'flex flex-row justify-between',
'circular-corners:rounded-2xl rounded-md straight-corners:rounded-none p-1.5 pl-3',
'text-balance font-normal text-sm text-tint-strong/7 hover:bg-tint-hover hover:text-tint-strong contrast-more:text-tint-strong',
'hover:contrast-more:text-tint-strong hover:contrast-more:ring-1 hover:contrast-more:ring-tint-12',
'before:contents[] before:-left-px before:absolute before:inset-y-0',
'sidebar-list-line:rounded-l-none sidebar-list-line:before:w-px sidebar-list-default:[&+div_a]:rounded-l-none [&+div_a]:pl-5 sidebar-list-default:[&+div_a]:before:w-px',
'group/toclink',
'toclink',
'before:contents[]',
styles.toggleableLink,
isActive && [
'font-semibold',
'sidebar-list-line:before:w-0.5',
'before:bg-primary-solid',
'text-primary-subtle',
'contrast-more:text-primary',
'sidebar-list-pill:bg-primary',
'[html.sidebar-list-pill.theme-muted_&]:bg-primary-hover',
'[html.sidebar-list-pill.theme-bold.tint_&]:bg-primary-hover',
'[html.sidebar-filled.sidebar-list-pill.theme-muted_&]:bg-primary',
'[html.sidebar-filled.sidebar-list-pill.theme-bold.tint_&]:bg-primary',
'hover:bg-primary-hover',
'hover:text-primary',
'hover:before:bg-primary-solid-hover',
'sidebar-list-pill:hover:bg-primary-hover',
'contrast-more:text-primary',
'contrast-more:hover:text-primary-strong',
'contrast-more:bg-primary',
'contrast-more:ring-1',
'contrast-more:ring-primary',
'contrast-more:hover:ring-primary-hover',
]
isActive && styles.active
)}
>
{children}
@@ -0,0 +1,55 @@
.pageLinkItem {
@apply flex
justify-start
items-center
gap-3
p-1.5
pl-3
text-sm
transition-colors
duration-100
text-tint-strong/7
rounded-md
straight-corners:rounded-none
circular-corners:rounded-xl
before:content-none
font-normal
hover:bg-tint
hover:text-tint-strong;
}
.toggleableLink {
@apply relative transition-colors
flex flex-row justify-between
circular-corners:rounded-2xl rounded-md straight-corners:rounded-none p-1.5 pl-3
text-balance font-normal text-sm text-tint-strong/7 hover:bg-tint-hover hover:text-tint-strong contrast-more:text-tint-strong
hover:contrast-more:text-tint-strong hover:contrast-more:ring-1 hover:contrast-more:ring-tint-12
before:-left-px before:absolute before:inset-y-0
sidebar-list-line:rounded-l-none sidebar-list-line:before:w-px sidebar-list-default:[&+div_a]:rounded-l-none [&+div_a]:pl-5 sidebar-list-default:[&+div_a]:before:w-px;
}
.active {
@apply font-semibold
sidebar-list-line:before:w-0.5
before:bg-primary-solid
text-primary-subtle
sidebar-list-pill:bg-primary
[html.sidebar-list-pill.theme-muted_&]:bg-primary-hover
[html.sidebar-list-pill.theme-bold.tint_&]:bg-primary-hover
[html.sidebar-filled.sidebar-list-pill.theme-muted_&]:bg-primary
[html.sidebar-filled.sidebar-list-pill.theme-bold.tint_&]:bg-primary
hover:bg-primary-hover
hover:text-primary
hover:before:bg-primary-solid-hover
sidebar-list-pill:hover:bg-primary-hover
contrast-more:text-primary
contrast-more:hover:text-primary-strong
contrast-more:bg-primary
contrast-more:ring-1
contrast-more:ring-primary
contrast-more:hover:ring-primary-hover;
}
@@ -7,6 +7,8 @@ import { type ClassValue, tcls } from '@/lib/tailwind';
import { Icon, type IconName } from '@gitbook/icons';
import { Link, type LinkInsightsProps } from './Link';
import styles from './button.module.css';
type ButtonProps = {
href?: string;
variant?: 'primary' | 'secondary' | 'blank';
@@ -19,33 +21,14 @@ type ButtonProps = {
HTMLAttributes<HTMLElement>;
const variantClasses = {
primary: [
'bg-primary-solid',
'text-contrast-primary-solid',
'hover:bg-primary-solid-hover',
'hover:text-contrast-primary-solid-hover',
'ring-0',
'contrast-more:ring-1',
],
blank: [
'bg-transparent',
'text-tint',
'ring-0',
'shadow-none',
'hover:bg-primary-hover',
'hover:text-primary',
'hover:scale-1',
'hover:shadow-none',
'contrast-more:bg-tint-subtle',
],
primary: [styles.primary],
blank: [styles.blank, 'hover:scale-1'],
secondary: [
'bg-tint',
'depth-flat:bg-transparent',
'text-tint',
'hover:bg-tint-hover',
'depth-flat:hover:bg-tint-hover',
'hover:text-primary',
'contrast-more:bg-tint-subtle',
styles.secondary,
'bg-secondary-solid',
'text-contrast-secondary-solid',
'hover:bg-secondary-solid-hover',
'hover:text-contrast-secondary-solid-hover',
],
};
@@ -62,50 +45,14 @@ export function Button({
...rest
}: ButtonProps & { target?: HTMLAttributeAnchorTarget }) {
const sizes = {
default: ['text-base', 'font-semibold', 'px-5', 'py-2', 'circular-corners:px-6'],
medium: ['text-sm', 'px-3.5', 'py-1.5', 'circular-corners:px-4'],
small: ['text-xs', 'py-2', iconOnly ? 'px-2' : 'px-3'],
default: [styles.default],
medium: [styles.medium],
small: [styles.small, iconOnly ? 'px-2' : 'px-3'],
};
const sizeClasses = sizes[size] || sizes.default;
const domClassName = tcls(
'button',
'inline-flex',
'items-center',
'gap-2',
'rounded-md',
'straight-corners:rounded-none',
'circular-corners:rounded-full',
// 'place-self-start',
'ring-1',
'ring-tint',
'hover:ring-tint-hover',
'shadow-sm',
'shadow-tint',
'dark:shadow-tint-1',
'hover:shadow-md',
'active:shadow-none',
'depth-flat:shadow-none',
'contrast-more:ring-tint-12',
'contrast-more:hover:ring-2',
'contrast-more:hover:ring-tint-12',
'hover:scale-104',
'depth-flat:hover:scale-100',
'active:scale-100',
'transition-all',
'grow-0',
'shrink-0',
'truncate',
variantClasses[variant],
sizeClasses,
className
);
const domClassName = tcls(styles.styledButton, variantClasses[variant], sizeClasses, className);
if (href) {
return (
@@ -4,6 +4,8 @@ import { type ClassValue, tcls } from '@/lib/tailwind';
import { Link, type LinkInsightsProps } from './Link';
import styles from './card.module.css';
export async function Card(
props: {
href: string;
@@ -17,28 +19,7 @@ export async function Card(
const { title, leadingIcon, href, preTitle, postTitle, style, insights } = props;
return (
<Link
href={href}
className={tcls(
'group',
'flex',
'flex-row',
'justify-between',
'items-center',
'gap-4',
'ring-1',
'ring-tint-subtle',
'rounded',
'straight-corners:rounded-none',
'circular-corners:rounded-2xl',
'px-5',
'py-3',
'transition-shadow',
'hover:ring-primary-hover',
style
)}
insights={insights}
>
<Link href={href} className={tcls('group', styles.styledCard, style)} insights={insights}>
{leadingIcon}
<span className={tcls('flex', 'flex-col', 'flex-1')}>
{preTitle ? (
@@ -2,6 +2,8 @@ import { type ClassValue, tcls } from '@/lib/tailwind';
import { Link, type LinkProps } from '../primitives/Link';
import styles from './styledLink.module.css';
export const linkStyles = [
'underline',
'decoration-[max(0.07em,1px)]', // Set the underline to be proportional to the font size, with a minimum. The default is too thin.
@@ -29,7 +31,7 @@ export function StyledLink(props: Omit<LinkProps, 'style'> & { className?: Class
const { className, ...rest } = props;
return (
<Link {...rest} className={tcls(linkStyles, className)}>
<Link {...rest} className={tcls(styles.styledLink, className)}>
{props.children}
</Link>
);
@@ -0,0 +1,40 @@
.styledButton {
@apply inline-flex items-center gap-2 rounded-md straight-corners:rounded-none circular-corners:rounded-full ring-1 ring-tint hover:ring-tint-hover shadow-sm shadow-tint dark:shadow-tint-1 hover:shadow-md active:shadow-none depth-flat:shadow-none contrast-more:ring-tint-12 contrast-more:hover:ring-2 contrast-more:hover:ring-tint-12 hover:scale-104 depth-flat:hover:scale-100 active:scale-100 transition-all grow-0 shrink-0 truncate;
}
.primary {
@apply bg-primary-solid
text-contrast-primary-solid
hover:bg-primary-solid-hover
hover:text-contrast-primary-solid-hover
ring-0
contrast-more:ring-1;
}
.blank {
@apply bg-transparent
text-tint
ring-0
shadow-none
hover:bg-primary-hover
hover:text-primary
hover:shadow-none
contrast-more:bg-tint-subtle;
}
.secondary {
@apply ring-0
contrast-more:ring-1;
}
.default {
@apply text-base font-semibold px-5 py-2 circular-corners:px-6;
}
.medium {
@apply text-sm px-3.5 py-1.5 circular-corners:px-4;
}
.small {
@apply text-xs py-2;
}
@@ -0,0 +1,3 @@
.styledCard {
@apply flex flex-row justify-between items-center gap-4 ring-1 ring-tint-subtle rounded straight-corners:rounded-none circular-corners:rounded-2xl px-5 py-3 transition-shadow hover:ring-primary-hover;
}
@@ -0,0 +1,3 @@
.styledLink {
@apply underline decoration-[max(0.07em,1px)] underline-offset-2 links-accent:underline-offset-4 links-default:decoration-primary/6 links-default:text-primary-subtle links-default:hover:text-primary-strong links-default:contrast-more:text-primary links-default:contrast-more:hover:text-primary-strong links-accent:decoration-primary-subtle links-accent:hover:decoration-[3px] links-accent:hover:[text-decoration-skip-ink:none] transition-all duration-100;
}