Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement numbered step in doc #2741

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions content/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,34 @@ To change text in CTA block, you can pass to the component props `title`, `descr
<CTA title="Try it on Neon!" description="Neon is Serverless Postgres built for the cloud. Explore Postgres features and functions in our user-friendly SQL Editor. Sign up for a free account to get started." buttonText="Sign Up" buttonUrl="https://console.neon.tech/signup" />
```

## Numbered Steps

To display numbered steps, wrap the content with `NumberedSteps` and `NumberedStep` components.

```md
<NumberedSteps>

<NumberedStep title="Step 1">

Create a new database called `people` on the `main` branch and add some sample data to it.

</NumberedStep>

<NumberedStep title="Step 2">

Create a new development branch called `dev/jordan`.

</NumberedStep>

<NumberedStep title="Step 3">

Use the **Schema Diff** tool on the **Branches** page to get a side-by-side, GitHub-style visual comparison between the `dev/jordan` development branch and `main`.

</NumberedStep>

</NumberedSteps>
```

## Images

The images should be sourced in `public/docs` directory and be used in `.md` with the relative path, that begins with a `/` slash
Expand Down
24 changes: 18 additions & 6 deletions content/docs/guides/schema-diff-tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ To complete this tutorial, you'll need:
- Install the [Neon CLI](/docs/reference/cli-install)
- Download and install the [psql](https://www.postgresql.org/download/) client

## Step 1: Create the Initial Schema
<NumberedSteps>

<NumberedStep title="Step 1: Create the Initial Schema">

First, create a new database called `people` on the `main` branch and add some sample data to it.

Expand Down Expand Up @@ -90,7 +92,9 @@ First, create a new database called `people` on the `main` branch and add some s
</TabItem>
</Tabs>

## Step 2: Create a development branch
</NumberedStep>

<NumberedStep title="Step 2: Create a development branch">

Create a new development branch off of `main`. This branch will be an exact, isolated copy of `main`.

Expand Down Expand Up @@ -174,7 +178,9 @@ For the purposes of this tutorial, name the branch `dev/jordan`, following our r
</TabItem>
</Tabs>

## Step 3: Update schema on a dev branch
</NumberedStep>

<NumberedStep title="Step 3: Update schema on a dev branch">

Let's introduce some differences between the two branches. Add a new table to store addresses on the `dev/jordan` branch.

Expand Down Expand Up @@ -237,7 +243,9 @@ CREATE TABLE address (
</TabItem>
</Tabs>

## Step 4: View the schema differences
</NumberedStep>

<NumberedStep title="Step 4: View the schema differences">

Now that you have some differences between your branches, you can view the schema differences.

Expand Down Expand Up @@ -270,8 +278,8 @@ neon branches schema-diff main dev/jordan --database people
The result shows a comparison between the `dev/jordan` branch and its parent branch for the database `people`. The output indicates that the `address` table and its related sequences and constraints have been added in the `dev/jordan` branch but are not present in its parent branch `main`.

```diff
--- Database: people (Branch: br-falling-dust-a5bakdqt) // [!code --]
+++ Database: people (Branch: br-morning-heart-a5ltt10i) // [!code ++]
--- Database: people (Branch: br-falling-dust-a5bakdqt) // [!code --]
+++ Database: people (Branch: br-morning-heart-a5ltt10i) // [!code ++]
@@ -20,8 +20,46 @@

SET default_table_access_method = heap;
Expand All @@ -298,3 +306,7 @@ The result shows a comparison between the `dev/jordan` branch and its parent bra
</TabItem>

</Tabs>

</NumberedStep>

</NumberedSteps>
3 changes: 3 additions & 0 deletions src/components/pages/doc/numbered-step/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import NumberedStep from './numbered-step';

export default NumberedStep;
37 changes: 37 additions & 0 deletions src/components/pages/doc/numbered-step/numbered-step.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import clsx from 'clsx';
import PropTypes from 'prop-types';
import slugify from 'slugify';

const NumberedStep = ({ title, children, tag: Tag = 'h2' }) => {
const id = slugify(title, { lower: true, strict: true, remove: /[*+~.()'"!:@]/g });
return (
<li
className={clsx(
'relative !mb-0 !mt-10 flex w-full items-start gap-3 !pl-0',
'before:flex before:size-6 before:items-center before:justify-center before:rounded-full before:bg-gray-new-15 before:text-sm before:leading-snug before:tracking-extra-tight before:text-white before:content-[counter(section)] before:[counter-increment:section]',
'after:absolute after:left-3 after:top-[34px] after:h-[calc(100%+4px)] after:w-px after:bg-gray-new-80',
'first:!mt-7 last:overflow-hidden',
'dark:before:bg-gray-new-94 dark:before:text-black-new dark:after:bg-gray-new-15',
{
'before:mt-1': Tag === 'h2',
'before:mt-0.5': Tag === 'h3',
}
)}
>
<div className="w-[calc(100%-100px)] flex-1 [&>*:first-child]:mt-0 [&>*:last-child]:mb-0">
<Tag className="!my-0 scroll-mt-20 lg:scroll-mt-5" id={id}>
{title}
</Tag>
{children}
</div>
</li>
);
};

export default NumberedStep;

NumberedStep.propTypes = {
title: PropTypes.string.isRequired,
children: PropTypes.node.isRequired,
tag: PropTypes.string,
};
3 changes: 3 additions & 0 deletions src/components/pages/doc/numbered-steps/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import NumberedSteps from './numbered-steps';

export default NumberedSteps;
18 changes: 18 additions & 0 deletions src/components/pages/doc/numbered-steps/numbered-steps.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import PropTypes from 'prop-types';

const NumberedSteps = ({ children }) => (
<ol
className="!mt-0 inline-flex w-full flex-col !pl-0"
style={{
counterReset: 'section',
}}
>
{children}
</ol>
);

export default NumberedSteps;

NumberedSteps.propTypes = {
children: PropTypes.node.isRequired,
};
4 changes: 4 additions & 0 deletions src/components/shared/content/content.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import DocsList from 'components/pages/doc/docs-list';
import IncludeBlock from 'components/pages/doc/include-block';
import InfoBlock from 'components/pages/doc/info-block';
import LinkPreview from 'components/pages/doc/link-preview';
import NumberedStep from 'components/pages/doc/numbered-step';
import NumberedSteps from 'components/pages/doc/numbered-steps';
import Tabs from 'components/pages/doc/tabs';
import TabItem from 'components/pages/doc/tabs/tab-item';
import TechnologyNavigation from 'components/pages/doc/technology-navigation';
Expand Down Expand Up @@ -177,6 +179,8 @@ const getComponents = (withoutAnchorHeading, isReleaseNote, isPostgres, isUseCas
ComputeCalculator,
SubscriptionForm,
Video,
NumberedStep,
NumberedSteps,
...sharedComponents,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const NavigationLinks = ({ previousLink = null, nextLink = null, basePath }) =>
const nextLinkUrl = nextLink?.slug && getUrl(nextLink.slug, basePath);

return (
<div className="mt-10 flex w-full space-x-10 sm:mt-7 sm:space-x-0">
<div className="mt-16 flex w-full space-x-10 sm:mt-[50px] sm:space-x-0">
{previousLink?.title && previousLink?.slug && (
<Link
to={previousLinkUrl}
Expand Down
42 changes: 38 additions & 4 deletions src/components/shared/table-of-contents/item/item.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@ import { AnimatePresence, LazyMotion, domAnimation, m } from 'framer-motion';
import PropTypes from 'prop-types';

const linkClassName =
'py-1.5 block text-sm leading-tight transition-colors duration-200 text-gray-new-40 hover:text-black-new dark:text-gray-new-90 dark:hover:text-white [&_code]:rounded-sm [&_code]:leading-none [&_code]:py-px [&_code]:bg-gray-new-94 [&_code]:px-1.5 [&_code]:font-mono [&_code]:font-normal dark:[&_code]:bg-gray-new-15';
'relative py-1.5 flex items-start gap-[11px] text-sm leading-tight transition-colors duration-200 text-gray-new-40 hover:text-black-new dark:text-gray-new-90 dark:hover:text-white [&_code]:rounded-sm [&_code]:leading-none [&_code]:py-px [&_code]:bg-gray-new-94 [&_code]:px-1.5 [&_code]:font-mono [&_code]:font-normal dark:[&_code]:bg-gray-new-15';

const Item = ({
title,
level,
id,
numberedStep,
items,
currentAnchor,
isUserScrolling,
setIsUserScrolling,
isUseCase,
index,
currentIndex,
}) => {
const href = `#${id}`;
const shouldRenderSubItems =
Expand Down Expand Up @@ -59,9 +62,34 @@ const Item = ({
marginLeft: level === 1 ? '' : `${(level - 1) * 0.5}rem`,
}}
href={href}
dangerouslySetInnerHTML={{ __html: title.split('\\').join('') }}
onClick={(e) => handleAnchorClick(e, href, id)}
/>
>
{numberedStep && (
<>
<span
className={clsx(
'flex size-4 shrink-0 items-center justify-center rounded-full bg-gray-new-15 text-[10px] font-medium leading-none tracking-extra-tight transition-colors duration-200',
currentAnchor === id || index < currentIndex
? 'bg-gray-new-15 text-white dark:bg-gray-new-94 dark:text-black-new'
: 'bg-gray-new-90 text-black-new dark:bg-gray-new-20 dark:text-gray-new-98'
)}
>
{numberedStep}
</span>
<span
className={clsx(
'absolute left-2 top-[25px] h-[calc(100%-22px)] w-px transition-colors duration-200',
level === 1 && 'group-last/item:hidden',
level === 2 && 'group-last/child:hidden',
currentAnchor === id || index < currentIndex
? 'bg-gray-new-40 dark:bg-gray-new-60'
: 'bg-gray-new-80 dark:bg-gray-new-15'
)}
/>
</>
)}
<span dangerouslySetInnerHTML={{ __html: title.split('\\').join('') }} />
</a>
<AnimatePresence initial={false}>
{shouldRenderSubItems && (
<m.ul
Expand All @@ -71,9 +99,12 @@ const Item = ({
transition={{ duration: 0.2 }}
>
{items.map((item, index) => (
<li key={index}>
<li className="group/child" key={index}>
<Item
index={index}
currentIndex={currentIndex}
currentAnchor={currentAnchor}
numberedStep={numberedStep}
isUserScrolling={isUserScrolling}
setIsUserScrolling={setIsUserScrolling}
{...item}
Expand All @@ -98,6 +129,9 @@ Item.propTypes = {
})
),
id: PropTypes.string.isRequired,
numberedStep: PropTypes.string,
index: PropTypes.number,
currentIndex: PropTypes.number,
currentAnchor: PropTypes.string,
setIsUserScrolling: PropTypes.func.isRequired,
isUserScrolling: PropTypes.bool.isRequired,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const CURRENT_ANCHOR_GAP_PX = 100;
const TableOfContents = ({ items, isUseCase }) => {
const titles = useRef([]);
const [currentAnchor, setCurrentAnchor] = useState(null);
const [currentIndex, setCurrentIndex] = useState(null);
const [isUserScrolling, setIsUserScrolling] = useState(true);

const flatItems = useMemo(
Expand Down Expand Up @@ -45,10 +46,11 @@ const TableOfContents = ({ items, isUseCase }) => {
const currentTitle = titles.current[idx];

setCurrentAnchor(currentTitle?.id);

setCurrentIndex(idx);
if (isUserScrolling) {
// Open sub-items only if it's user-initiated scrolling
setCurrentAnchor(currentTitle?.id);
setCurrentIndex(idx);
}
}, [isUserScrolling]);

Expand All @@ -72,8 +74,10 @@ const TableOfContents = ({ items, isUseCase }) => {
</h3>
<ul className="no-scrollbars overflow-y-auto">
{items.map((item, index) => (
<li key={index}>
<li className="group/item" key={index}>
<Item
index={index}
currentIndex={currentIndex}
currentAnchor={currentAnchor}
isUserScrolling={isUserScrolling}
setIsUserScrolling={setIsUserScrolling}
Expand Down
10 changes: 10 additions & 0 deletions src/styles/doc-content.css
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,16 @@
@apply prose-a:border-b prose-a:border-transparent prose-a:font-normal prose-a:text-secondary-8 prose-a:no-underline prose-a:transition-[border-color] prose-a:duration-200 prose-a:ease-in-out hover:prose-a:border-secondary-8 dark:prose-a:text-primary-1 dark:hover:prose-a:border-primary-1 prose-a:sm:break-words;
@apply prose-pre:px-0;

> * {
&:first-child {
@apply mt-0;
}

&:last-child {
@apply mb-0;
}
}

h2 {
@apply mb-3 mt-10;
}
Expand Down
18 changes: 16 additions & 2 deletions src/utils/get-table-of-contents.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,24 @@ const parseMDXHeading = require('./parse-mdx-heading');
const buildNestedToc = (headings, currentLevel) => {
const toc = [];

let numberedStep = 0;

while (headings.length > 0) {
const [depth, title] = parseMDXHeading(headings[0]);
const [depth, title, , isNumberedStep] = parseMDXHeading(headings[0]);
const titleWithInlineCode = title.replace(/`([^`]+)`/g, '<code>$1</code>');

if (depth === currentLevel) {
const tocItem = {
title: titleWithInlineCode,
id: slugify(title, { lower: true, strict: true, remove: /[*+~.()'"!:@]/g }),
level: depth,
numberedStep: isNumberedStep ? numberedStep + 1 : null,
};

if (isNumberedStep) {
numberedStep += 1;
}

headings.shift(); // remove the current heading

if (headings.length > 0 && parseMDXHeading(headings[0])[0] > currentLevel) {
Expand Down Expand Up @@ -60,7 +67,14 @@ const getTableOfContents = (content) => {
const headingRegex = /^(#+)\s(.*)$/gm;

const contentWithoutCodeBlocks = content.replace(codeBlockRegex, '');
const headings = contentWithoutCodeBlocks.match(headingRegex) || [];
const standardHeadings = contentWithoutCodeBlocks.match(headingRegex) || [];

const customHeadings =
contentWithoutCodeBlocks.match(
/<NumberedStep[^>]*title="([^"]+)"[^>]*(?:tag="h(\d)")?[^>]*>/g
) || [];

const headings = [...standardHeadings, ...customHeadings];

const arr = headings.map((item) => item.replace(/(#+)\s/, '$1 '));

Expand Down
12 changes: 12 additions & 0 deletions src/utils/parse-mdx-heading.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
function parseMDXHeading(line) {
const match = line.match(/^#+\s*\[(.*?)\]\((.*?)\)$/);
const matchWithoutLink = line.match(/^#+\s*(.*?)$/);
const matchCustomComponent = line.match(
/<NumberedStep[^>]*(?:title="([^"]+)")[^>]*(?:tag="h(\d)")?[^>]*/
);

if (match) {
const len = match[0]?.match(/^#+/)?.[0]?.length;
Expand All @@ -18,6 +21,15 @@ function parseMDXHeading(line) {

return [depth, title, null];
}

if (matchCustomComponent) {
const title = matchCustomComponent[1];
const depth = parseInt(matchCustomComponent[2], 10) - 1 || 1;
const isNumberedStep = true;

return [depth, title, null, isNumberedStep];
}

return [null, null, null];
}

Expand Down