From 16a6b710eecb4f71046c3ec0ced822e003a5ac97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Berg=C3=A9?= Date: Wed, 30 Oct 2024 15:26:13 +0100 Subject: [PATCH 1/2] Nested list support --- .../DocumentView/ListItem.module.css | 13 - .../src/components/DocumentView/ListItem.tsx | 299 ++++++++++++------ .../components/DocumentView/ListOrdered.tsx | 24 +- .../components/DocumentView/ListUnordered.tsx | 30 +- .../src/components/DocumentView/spacing.ts | 26 +- .../src/components/primitives/Checkbox.tsx | 17 +- packages/gitbook/tailwind.config.ts | 1 + 7 files changed, 233 insertions(+), 177 deletions(-) delete mode 100644 packages/gitbook/src/components/DocumentView/ListItem.module.css diff --git a/packages/gitbook/src/components/DocumentView/ListItem.module.css b/packages/gitbook/src/components/DocumentView/ListItem.module.css deleted file mode 100644 index 1fb3dbd5c2..0000000000 --- a/packages/gitbook/src/components/DocumentView/ListItem.module.css +++ /dev/null @@ -1,13 +0,0 @@ -.olListItemBullet { - /* Align the bullet container with the first line of text */ - line-height: inherit; - height: 1lh; - /* Align the bullet content with the middle of the text */ - display: flex; - align-items: center; -} -.olListItemBullet::before { - @apply text-base; - --tw-content: attr(data-value) '. '; - content: var(--tw-content); -} diff --git a/packages/gitbook/src/components/DocumentView/ListItem.tsx b/packages/gitbook/src/components/DocumentView/ListItem.tsx index 6cd49563e4..3eb0f022f4 100644 --- a/packages/gitbook/src/components/DocumentView/ListItem.tsx +++ b/packages/gitbook/src/components/DocumentView/ListItem.tsx @@ -1,114 +1,227 @@ import { + DocumentBlock, DocumentBlockListItem, DocumentBlockListOrdered, - DocumentBlockListTasks, DocumentBlockListUnordered, } from '@gitbook/api'; -import classNames from 'classnames'; +import assertNever from 'assert-never'; +import { assert } from 'ts-essentials'; import { Checkbox } from '@/components/primitives'; import { tcls } from '@/lib/tailwind'; import { BlockProps } from './Block'; import { Blocks } from './Blocks'; -import styles from './ListItem.module.css'; import { getBlockTextStyle } from './spacing'; export function ListItem(props: BlockProps) { const { block, ancestorBlocks, ...contextProps } = props; - const textStyle = getBlockTextStyle(block); - - const parent = ancestorBlocks[ancestorBlocks.length - 1] as - | DocumentBlockListOrdered - | DocumentBlockListUnordered - | DocumentBlockListTasks - | undefined; - - const ListItemType = () => { - switch (parent?.type) { - case 'list-tasks': - return ( -
  • -
    -
    - -
    - - -
    -
  • - ); - case 'list-ordered': - const start = parent.data.start ?? 1; - const indexInParent = parent.nodes.findIndex((node) => node.key === block.key) ?? 0; - const index = indexInParent + start; - - return ( -
  • -
    - {/* zero width space to force layouts with empty lists */} - + ); + + switch (parent.type) { + case 'list-tasks': + return ( + + + + + + + + ); + case 'list-ordered': + return ( + + + -
  • - ); - default: - return ( -
  • -
    - + {blocksElement} + + ); + case 'list-unordered': + return ( + + + -
  • - ); + + {blocksElement} + + ); + default: + assertNever(parent); + } +} + +function getListItemDepth(input: { + ancestorBlocks: DocumentBlock[]; + type: DocumentBlockListOrdered['type'] | DocumentBlockListUnordered['type']; +}): number { + const { ancestorBlocks, type } = input; + + let depth = -1; + + for (let i = ancestorBlocks.length - 1; i >= 0; i--) { + const block = ancestorBlocks[i]; + if (block.type === type) { + depth = depth + 1; + continue; } - }; + if (block.type === 'list-item') { + continue; + } + break; + } + + return depth; +} + +function ListItemLI(props: { block: DocumentBlockListItem; children: React.ReactNode }) { + const textStyle = getBlockTextStyle(props.block); + return
  • {props.children}
  • ; +} - return ; +function ListItemPrefix(props: { block: DocumentBlockListItem; children: React.ReactNode }) { + const textStyle = getBlockTextStyle(props.block); + return ( +
    + {props.children} +
    + ); +} + +function getUnorderedListItemsPrefixContent(input: { depth: number }): string { + switch (input.depth % 3) { + case 0: + return '•'; + case 1: + return '◦'; + case 2: + return '▪'; + default: + return '•'; + } +} + +function PseudoBefore(props: { + style?: React.CSSProperties; + content: string; + fontFamily?: string; +}) { + return ( +
    + ); +} + +function getOrderedListItemPrefixContent(input: { + depth: number; + parent: DocumentBlockListOrdered; + block: DocumentBlockListItem; +}): string { + const { parent, block } = input; + const start = parent.data.start ?? 1; + const index = parent.nodes.findIndex((node) => node.key === block.key) ?? 0; + const value = index + start; + switch (input.depth % 3) { + // Use numbers + case 0: { + return `${value}.`; + } + // Use letters + case 1: { + const letters = 'abcdefghijklmnopqrstuvwxyz'; + return `${letters[(value - 1) % letters.length]}.`; + } + // Use roman numbers + case 2: { + return `${toRoman(value).toLowerCase()}.`; + } + default: + return '•'; + } +} + +function toRoman(input: number): string { + const lookup = { + M: 1000, + CM: 900, + D: 500, + CD: 400, + C: 100, + XC: 90, + L: 50, + XL: 40, + X: 10, + IX: 9, + V: 5, + IV: 4, + I: 1, + }; + let roman = ''; + let number = input; + for (const i in lookup) { + while (number >= lookup[i as keyof typeof lookup]) { + roman += i; + number -= lookup[i as keyof typeof lookup]; + } + } + return roman; } diff --git a/packages/gitbook/src/components/DocumentView/ListOrdered.tsx b/packages/gitbook/src/components/DocumentView/ListOrdered.tsx index 80a7c9e345..ad4ef0a3b5 100644 --- a/packages/gitbook/src/components/DocumentView/ListOrdered.tsx +++ b/packages/gitbook/src/components/DocumentView/ListOrdered.tsx @@ -14,30 +14,8 @@ export function ListOrdered(props: BlockProps) { ancestorBlocks={[...ancestorBlocks, block]} style={[ 'space-y-2', - 'flex', - 'flex-col', - '[&>li]:gap-[1ch]', - - '[counter-reset:list-decimal]', - - '[&>li]:flex', - '[&>li]:flex-row', - - /* '[&>li>.bullet]:w-[1ch]', */ - '[&>li>.bullet]:tabular-nums', - '[&>li>.bullet]:whitespace-nowrap', - '[&>li>.bullet]:list-decimal', - - '[&>li>.bullet]:before:h-[1lh]', - '[&>li>.bullet]:before:leading-[inherit]', - '[&>li>.bullet]:before:flex', - /* '[&>li>.bullet]:before:pr-[1ch]', */ - '[&>li>.bullet]:text-dark/6', - - //remove any spacing when using heading as list item + // remove any spacing when using heading as list item '[&>li>div_div]:mt-0', - - 'dark:[&>li>.bullet]:text-light/6', style, ]} /> diff --git a/packages/gitbook/src/components/DocumentView/ListUnordered.tsx b/packages/gitbook/src/components/DocumentView/ListUnordered.tsx index 32ab36bad2..8c68a0f5a5 100644 --- a/packages/gitbook/src/components/DocumentView/ListUnordered.tsx +++ b/packages/gitbook/src/components/DocumentView/ListUnordered.tsx @@ -6,21 +6,6 @@ import { Blocks } from './Blocks'; export function ListUnordered(props: BlockProps) { const { block, style, ancestorBlocks, ...contextProps } = props; - const nestedBulletStyle = [ - // Level 1 - '[&>li>.bullet:before]:bullet-circleFilled', - // Level 2 - `[&_&>li>.bullet:before]:bullet-circle`, - // Level 3 - `[&_&_&>li>.bullet:before]:bullet-dash`, - // Level 4 - `[&_&_&_&>li>.bullet:before]:bullet-squareFilled`, - // Level 5 - `[&_&_&_&_&>li>.bullet:before]:bullet-square`, - // Level 6 // reset back to reg bullets here - `[&_&_&_&_&_&>li>.bullet:before]:bullet-circleFilled`, - ]; - return ( ) { nodes={block.nodes} ancestorBlocks={[...ancestorBlocks, block]} style={[ - 'list-none', 'space-y-2', - '[&>li]:relative', - '[&>li]:ps-[2.25ch]', - //remove any spacing when using heading as list item + // remove any spacing when using heading as list item '[&>li>div_div]:mt-0', - //custom content setup for lists - '[&>li>.bullet]:before:bg-dark/6', - '[&>li>.bullet]:before:absolute', - '[&>li>.bullet]:before:left-0', - '[&>li>.bullet]:before:w-[1ch]', - '[&>li>.bullet]:before:h-[1lh]', - '[&>li>.bullet]:before:[mask-repeat:no-repeat]', - '[&>li>.bullet]:before:[mask-position:left]', - 'dark:[&>li>.bullet]:before:bg-light/6', - nestedBulletStyle, style, ]} /> diff --git a/packages/gitbook/src/components/DocumentView/spacing.ts b/packages/gitbook/src/components/DocumentView/spacing.ts index 8e03527c93..c628722ba3 100644 --- a/packages/gitbook/src/components/DocumentView/spacing.ts +++ b/packages/gitbook/src/components/DocumentView/spacing.ts @@ -6,46 +6,44 @@ import { } from '@gitbook/api'; import { assertNever } from 'assert-never'; -import { ClassValue } from '@/lib/tailwind'; - /** * Get the line height of a block */ export function getBlockTextStyle(block: DocumentBlock): { /** Tailwind class for the text size */ - textSize: ClassValue; + textSize: string; /** Tailwind class for the height (h-*) */ - lineHeight: ClassValue; + lineHeight: string; /** Tailwind class for the margin top (mt-*) */ - marginTop?: ClassValue; + marginTop?: string; } { switch (block.type) { case 'paragraph': return { - textSize: ['text-base'], + textSize: 'text-base', lineHeight: 'leading-normal', }; case 'heading-1': return { - textSize: ['text-3xl', 'font-semibold'], + textSize: 'text-3xl font-semibold', lineHeight: 'leading-tight', - marginTop: '[margin-top:_1em]', + marginTop: 'mt-[1em]', }; case 'heading-2': return { - textSize: ['text-2xl', 'font-semibold'], + textSize: 'text-2xl font-semibold', lineHeight: 'leading-snug', - marginTop: '[margin-top:_0.75em]', + marginTop: 'mt-[0.75em]', }; case 'heading-3': return { - textSize: ['text-base', 'font-semibold'], + textSize: 'text-base font-semibold', lineHeight: 'leading-snug', - marginTop: '[margin-top:_0.5em]', + marginTop: 'mt-[0.5em]', }; case 'divider': return { - textSize: [], + textSize: '', lineHeight: 'leading-none', }; case 'list-ordered': @@ -56,7 +54,7 @@ export function getBlockTextStyle(block: DocumentBlock): { return getBlockTextStyle(block.nodes[0]); default: return { - textSize: ['text-base'], + textSize: 'text-base', lineHeight: 'leading-normal', }; } diff --git a/packages/gitbook/src/components/primitives/Checkbox.tsx b/packages/gitbook/src/components/primitives/Checkbox.tsx index fcd60d5c89..cc1a8417e8 100644 --- a/packages/gitbook/src/components/primitives/Checkbox.tsx +++ b/packages/gitbook/src/components/primitives/Checkbox.tsx @@ -6,16 +6,22 @@ import React from 'react'; import { tcls } from '@/lib/tailwind'; +export type CheckboxProps = React.ComponentProps & { + /** + * The size of the checkbox. + * @default medium + */ + size?: 'small' | 'medium'; +}; + export const Checkbox = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( + CheckboxProps +>(({ className, size = 'medium', ...props }, ref) => ( - + {props.checked ? ( ) : null} diff --git a/packages/gitbook/tailwind.config.ts b/packages/gitbook/tailwind.config.ts index 5a89db26c0..cffcb27313 100644 --- a/packages/gitbook/tailwind.config.ts +++ b/packages/gitbook/tailwind.config.ts @@ -64,6 +64,7 @@ const config: Config = { fontFamily: { sans: ['var(--font-content)'], mono: ['var(--font-mono)'], + var: ['var(--font-family)'], }, colors: { // Dynamic colors matching the customization settings From 30752f2d606bb05056fb2d70c5ef72c965759382 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Berg=C3=A9?= Date: Wed, 30 Oct 2024 16:49:46 +0100 Subject: [PATCH 2/2] Refactor list, fix display of heading list item --- .../src/components/DocumentView/Block.tsx | 8 +--- .../src/components/DocumentView/List.tsx | 44 +++++++++++++++++++ .../src/components/DocumentView/ListItem.tsx | 6 ++- .../components/DocumentView/ListOrdered.tsx | 23 ---------- .../src/components/DocumentView/ListTasks.tsx | 25 ----------- .../components/DocumentView/ListUnordered.tsx | 23 ---------- .../src/components/RootLayout/globals.css | 17 ------- 7 files changed, 51 insertions(+), 95 deletions(-) create mode 100644 packages/gitbook/src/components/DocumentView/List.tsx delete mode 100644 packages/gitbook/src/components/DocumentView/ListOrdered.tsx delete mode 100644 packages/gitbook/src/components/DocumentView/ListTasks.tsx delete mode 100644 packages/gitbook/src/components/DocumentView/ListUnordered.tsx diff --git a/packages/gitbook/src/components/DocumentView/Block.tsx b/packages/gitbook/src/components/DocumentView/Block.tsx index 87ec13f51c..d38553bf3d 100644 --- a/packages/gitbook/src/components/DocumentView/Block.tsx +++ b/packages/gitbook/src/components/DocumentView/Block.tsx @@ -22,10 +22,8 @@ import { Heading } from './Heading'; import { Hint } from './Hint'; import { Images } from './Images'; import { IntegrationBlock } from './Integration'; +import { List } from './List'; import { ListItem } from './ListItem'; -import { ListOrdered } from './ListOrdered'; -import { ListTasks } from './ListTasks'; -import { ListUnordered } from './ListUnordered'; import { BlockMath } from './Math'; import { OpenAPI } from './OpenAPI'; import { Paragraph } from './Paragraph'; @@ -65,11 +63,9 @@ export function Block(props: BlockProps) { case 'heading-3': return ; case 'list-ordered': - return ; case 'list-unordered': - return ; case 'list-tasks': - return ; + return ; case 'list-item': return ; case 'code': diff --git a/packages/gitbook/src/components/DocumentView/List.tsx b/packages/gitbook/src/components/DocumentView/List.tsx new file mode 100644 index 0000000000..00f5da1f5c --- /dev/null +++ b/packages/gitbook/src/components/DocumentView/List.tsx @@ -0,0 +1,44 @@ +import { + DocumentBlockListOrdered, + DocumentBlockListTasks, + DocumentBlockListUnordered, +} from '@gitbook/api'; +import assertNever from 'assert-never'; + +import { BlockProps } from './Block'; +import { Blocks } from './Blocks'; + +export function List( + props: BlockProps< + DocumentBlockListUnordered | DocumentBlockListOrdered | DocumentBlockListTasks + >, +) { + const { block, style, ancestorBlocks, ...contextProps } = props; + + return ( + + ); +} + +function getListTag( + type: + | DocumentBlockListUnordered['type'] + | DocumentBlockListOrdered['type'] + | DocumentBlockListTasks['type'], +) { + switch (type) { + case 'list-ordered': + return 'ol'; + case 'list-unordered': + case 'list-tasks': + return 'ul'; + default: + assertNever(type); + } +} diff --git a/packages/gitbook/src/components/DocumentView/ListItem.tsx b/packages/gitbook/src/components/DocumentView/ListItem.tsx index 3eb0f022f4..8274d41174 100644 --- a/packages/gitbook/src/components/DocumentView/ListItem.tsx +++ b/packages/gitbook/src/components/DocumentView/ListItem.tsx @@ -34,8 +34,12 @@ export function ListItem(props: BlockProps) { 'min-h-[1lh]', // flip heading hash icon if list item is a heading 'flip-heading-hash', + // remove margin-top for the first heading in a list + '[&:is(h2)>div]:mt-0', + '[&:is(h3)>div]:mt-0', + '[&:is(h4)>div]:mt-0', )} - style="space-y-2 flex flex-col" + style="space-y-2 flex flex-col flex-1" /> ); diff --git a/packages/gitbook/src/components/DocumentView/ListOrdered.tsx b/packages/gitbook/src/components/DocumentView/ListOrdered.tsx deleted file mode 100644 index ad4ef0a3b5..0000000000 --- a/packages/gitbook/src/components/DocumentView/ListOrdered.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { DocumentBlockListOrdered } from '@gitbook/api'; - -import { BlockProps } from './Block'; -import { Blocks } from './Blocks'; - -export function ListOrdered(props: BlockProps) { - const { block, style, ancestorBlocks, ...contextProps } = props; - - return ( - li>div_div]:mt-0', - style, - ]} - /> - ); -} diff --git a/packages/gitbook/src/components/DocumentView/ListTasks.tsx b/packages/gitbook/src/components/DocumentView/ListTasks.tsx deleted file mode 100644 index 3db02eab02..0000000000 --- a/packages/gitbook/src/components/DocumentView/ListTasks.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { DocumentBlockListTasks, DocumentBlockListUnordered } from '@gitbook/api'; - -import { BlockProps } from './Block'; -import { Blocks } from './Blocks'; - -export function ListTasks(props: BlockProps) { - const { block, style, ancestorBlocks, ...contextProps } = props; - - return ( - li]:pl-[.25ch]', - //remove any spacing when using heading as list item - '[&>li>div_div]:mt-0', - 'space-y-2', - style, - ]} - /> - ); -} diff --git a/packages/gitbook/src/components/DocumentView/ListUnordered.tsx b/packages/gitbook/src/components/DocumentView/ListUnordered.tsx deleted file mode 100644 index 8c68a0f5a5..0000000000 --- a/packages/gitbook/src/components/DocumentView/ListUnordered.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { DocumentBlockListUnordered } from '@gitbook/api'; - -import { BlockProps } from './Block'; -import { Blocks } from './Blocks'; - -export function ListUnordered(props: BlockProps) { - const { block, style, ancestorBlocks, ...contextProps } = props; - - return ( - li>div_div]:mt-0', - style, - ]} - /> - ); -} diff --git a/packages/gitbook/src/components/RootLayout/globals.css b/packages/gitbook/src/components/RootLayout/globals.css index d648205434..4b5f7b3df3 100644 --- a/packages/gitbook/src/components/RootLayout/globals.css +++ b/packages/gitbook/src/components/RootLayout/globals.css @@ -126,23 +126,6 @@ @apply [&:is(h1,h2,h3,h4)>div:first-child]:[grid-area:1/2]; @apply [&:is(h1,h2,h3,h4)>div:first-child]:ml-1; } - - /* bullet icons */ - .bullet-dash { - @apply [mask-image:url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iNyIgdmlld0JveD0iMCAwIDEwIDciIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHg9IjEiIHk9IjIuNSIgd2lkdGg9IjgiIGhlaWdodD0iMS41IiByeD0iMC42NiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==")]; - } - .bullet-circleFilled { - @apply [mask-image:url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iNyIgdmlld0JveD0iMCAwIDEwIDciIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHg9IjIiIHdpZHRoPSI2IiBoZWlnaHQ9IjYiIHJ4PSIzIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K")]; - } - .bullet-circle { - @apply [mask-image:url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iNyIgdmlld0JveD0iMCAwIDEwIDciIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHg9IjIuNzUiIHk9IjAuNzUiIHdpZHRoPSI1LjUiIGhlaWdodD0iNS41IiByeD0iMi43NSIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIxLjUiLz4KPC9zdmc+Cg==")]; - } - .bullet-squareFilled { - @apply [mask-image:url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iNyIgdmlld0JveD0iMCAwIDEwIDciIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHg9IjIiIHdpZHRoPSI2IiBoZWlnaHQ9IjYiIHJ4PSIxIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K")]; - } - .bullet-square { - @apply [mask-image:url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iNyIgdmlld0JveD0iMCAwIDEwIDciIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxtYXNrIGlkPSJwYXRoLTEtaW5zaWRlLTFfOV80OCIgZmlsbD0id2hpdGUiPgo8cmVjdCB4PSIyIiB3aWR0aD0iNyIgaGVpZ2h0PSI3IiByeD0iMSIvPgo8L21hc2s+CjxyZWN0IHg9IjIiIHdpZHRoPSI3IiBoZWlnaHQ9IjciIHJ4PSIxIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjMiIG1hc2s9InVybCgjcGF0aC0xLWluc2lkZS0xXzlfNDgpIi8+Cjwvc3ZnPgo=")]; - } } @layer utilities {