diff --git a/biome.json b/biome.json
index e15beb58..4ddf0413 100644
--- a/biome.json
+++ b/biome.json
@@ -18,7 +18,8 @@
},
"suspicious": {
"noAssignInExpressions": "off",
- "noExplicitAny": "off"
+ "noExplicitAny": "off",
+ "noArrayIndexKey": "off"
}
}
},
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a8db251c..5493c588 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -114,6 +114,9 @@ importers:
react-dom:
specifier: ^18.2.0
version: 18.2.0(react@18.2.0)
+ vocs:
+ specifier: workspace:*
+ version: link:../src
src:
dependencies:
diff --git a/site/docs/index.mdx b/site/docs/index.mdx
index cbaddc46..ff4b28e0 100644
--- a/site/docs/index.mdx
+++ b/site/docs/index.mdx
@@ -34,7 +34,9 @@ bun x vocs init -i bun
You can install Vocs in an existing project, or start from scratch, by installing Vocs as a dependency.
-### 1. Install
+::::steps
+
+### Install
First, we will install `vocs` as a dependency in our project.
@@ -58,7 +60,7 @@ bun i vocs
:::
-### 2. Add Scripts to `package.json`
+### Add Scripts to `package.json`
After that, let's add some scripts to our `package.json` for Vocs.
@@ -74,7 +76,7 @@ After that, let's add some scripts to our `package.json` for Vocs.
}
```
-### 3. Build your First Page
+### Build your First Page
Create a directory called `docs`, and add a file inside of it called `index.mdx`:
@@ -84,7 +86,7 @@ Create a directory called `docs`, and add a file inside of it called `index.mdx`
Welcome to my docs.
```
-### 4. Run
+### Run
Next, run the development server:
@@ -92,4 +94,6 @@ Next, run the development server:
npm run dev
```
-Then open up your browser to `http://localhost:5173`, and you can see your first page!
\ No newline at end of file
+Then open up your browser to `http://localhost:5173`, and you can see your first page!
+
+::::
\ No newline at end of file
diff --git a/site/docs/kitchen-sink/index.mdx b/site/docs/kitchen-sink/index.mdx
index 2fdf3685..3ad4a3bc 100644
--- a/site/docs/kitchen-sink/index.mdx
+++ b/site/docs/kitchen-sink/index.mdx
@@ -245,6 +245,32 @@ bun i viem
:::
+## Steps
+
+::::steps
+### Step one
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce vestibulum ante non neque convallis tempor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nam a iaculis libero.
+
+### Step two
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce vestibulum ante non neque convallis tempor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nam a iaculis libero.
+
+:::code-group
+```tsx [console.log]
+console.log('hello world')
+```
+
+```tsx [alert]
+alert('hello world')
+```
+:::
+
+### Step three
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce vestibulum ante non neque convallis tempor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nam a iaculis libero.
+::::
+
## Blockquote
> Blockquotes are very handy in email to emulate reply text.
diff --git a/site/package.json b/site/package.json
index f9acdd72..8b282f66 100644
--- a/site/package.json
+++ b/site/package.json
@@ -5,10 +5,14 @@
"scripts": {
"dev": "node --loader @swc-node/register/esm ../src/cli/index.ts dev",
"build": "NODE_ENV=production node --loader @swc-node/register/esm ../src/cli/index.ts build",
- "preview": "node --loader @swc-node/register/esm ../src/cli/index.ts preview"
+ "preview": "node --loader @swc-node/register/esm ../src/cli/index.ts preview",
+ "dist:dev": "vocs dev",
+ "dist:build": "vocs build",
+ "dist:preview": "vocs preview"
},
"dependencies": {
"react": "^18.2.0",
- "react-dom": "^18.2.0"
+ "react-dom": "^18.2.0",
+ "vocs": "workspace:*"
}
}
diff --git a/src/app/components/Step.css.ts b/src/app/components/Step.css.ts
new file mode 100644
index 00000000..337e4b7c
--- /dev/null
+++ b/src/app/components/Step.css.ts
@@ -0,0 +1,100 @@
+import { globalStyle, style } from '@vanilla-extract/css'
+
+import { fontWeightVars, primitiveColorVars, spaceVars, viewportVars } from '../styles/vars.css.js'
+import { root as H2 } from './mdx/H2.css.js'
+import { root as H3 } from './mdx/H3.css.js'
+import { root as H4 } from './mdx/H4.css.js'
+import { root as H5 } from './mdx/H5.css.js'
+import { root as H6 } from './mdx/H6.css.js'
+import { root as CodeGroup, tabsList } from './mdx/CodeGroup.css.js'
+
+export const root = style({
+ selectors: {
+ '&:not(:last-child)': {
+ marginBottom: spaceVars['24'],
+ },
+ },
+})
+
+export const title = style(
+ {
+ marginBottom: spaceVars['8'],
+ position: 'relative',
+ '::before': {
+ alignItems: 'center',
+ backgroundColor: primitiveColorVars.background4,
+ borderRadius: '100%',
+ border: `0.5em solid ${primitiveColorVars.background}`,
+ boxSizing: 'content-box',
+ color: primitiveColorVars.text2,
+ content: 'counter(step)',
+ counterIncrement: 'step',
+ display: 'flex',
+ fontSize: '0.625em',
+ fontWeight: fontWeightVars.regular,
+ height: '2em',
+ justifyContent: 'center',
+ left: 'calc(-25.125px - 1.45em)',
+ position: 'absolute',
+ top: '-0.5em',
+ width: '2em',
+ },
+ },
+ 'title',
+)
+
+export const content = style(
+ {
+ selectors: {
+ [`${H2}+&,${H3}+&,${H4}+&,${H5}+&,${H6}+&`]: {
+ marginTop: `calc(${spaceVars['8']} * -1)`,
+ },
+ },
+ },
+ 'content',
+)
+
+globalStyle(`${content} > *:not(:last-child)`, {
+ marginBottom: spaceVars['16'],
+})
+
+globalStyle(`${content} > *:last-child`, {
+ marginBottom: spaceVars['0'],
+})
+
+globalStyle(`${content} [data-rehype-pretty-code-fragment]`, {
+ '@media': {
+ [viewportVars['max-720px']]: {
+ borderTop: `6px solid ${primitiveColorVars.background}`,
+ borderBottom: `6px solid ${primitiveColorVars.background}`,
+ marginLeft: `calc(-1 * ${spaceVars['28']} - 2px)`,
+ },
+ },
+})
+
+globalStyle(`${content} ${CodeGroup}`, {
+ '@media': {
+ [viewportVars['max-720px']]: {
+ marginLeft: 0,
+ marginRight: 0,
+ },
+ },
+})
+
+globalStyle(`${content} ${tabsList}`, {
+ '@media': {
+ [viewportVars['max-720px']]: {
+ borderTop: `6px solid ${primitiveColorVars.background}`,
+ marginLeft: `calc(-1 * ${spaceVars['44']})`,
+ marginRight: `calc(-1 * ${spaceVars['16']})`,
+ },
+ },
+})
+
+globalStyle(`${content} ${CodeGroup} [data-rehype-pretty-code-fragment]`, {
+ '@media': {
+ [viewportVars['max-720px']]: {
+ borderTop: 'none',
+ },
+ },
+})
diff --git a/src/app/components/Step.tsx b/src/app/components/Step.tsx
new file mode 100644
index 00000000..1bf00cee
--- /dev/null
+++ b/src/app/components/Step.tsx
@@ -0,0 +1,34 @@
+import { type ClassValue, clsx } from 'clsx'
+import type { ReactNode } from 'react'
+
+import * as styles from './Step.css.js'
+import { H2 } from './mdx/H2.js'
+import { H3 } from './mdx/H3.js'
+import { H4 } from './mdx/H4.js'
+import { H5 } from './mdx/H5.js'
+import { H6 } from './mdx/H6.js'
+
+export type StepProps = {
+ children: ReactNode
+ className?: ClassValue
+ title: ReactNode | string
+ titleLevel?: 2 | 3 | 4 | 5 | 6
+}
+
+export function Step({ children, className, title, titleLevel = 2 }: StepProps) {
+ const Element = (() => {
+ if (titleLevel === 2) return H2
+ if (titleLevel === 3) return H3
+ if (titleLevel === 4) return H4
+ if (titleLevel === 5) return H5
+ if (titleLevel === 6) return H6
+ throw new Error('Invalid.')
+ })()
+
+ return (
+
+ )
+}
diff --git a/src/app/components/Steps.css.ts b/src/app/components/Steps.css.ts
new file mode 100644
index 00000000..9b5994a7
--- /dev/null
+++ b/src/app/components/Steps.css.ts
@@ -0,0 +1,16 @@
+import { style } from '@vanilla-extract/css'
+
+import { primitiveColorVars, spaceVars, viewportVars } from '../styles/vars.css.js'
+
+export const root = style({
+ borderLeft: `1.5px solid ${primitiveColorVars.border}`,
+ counterReset: 'step',
+ paddingLeft: spaceVars['24'],
+ marginLeft: spaceVars['12'],
+ marginTop: spaceVars['24'],
+ '@media': {
+ [viewportVars['max-720px']]: {
+ marginLeft: spaceVars['4'],
+ },
+ },
+})
diff --git a/src/app/components/Steps.tsx b/src/app/components/Steps.tsx
new file mode 100644
index 00000000..8fd8d92c
--- /dev/null
+++ b/src/app/components/Steps.tsx
@@ -0,0 +1,13 @@
+import { type ClassValue, clsx } from 'clsx'
+import type { ReactNode } from 'react'
+
+import * as styles from './Steps.css.js'
+
+export type StepsProps = {
+ children: ReactNode
+ className?: ClassValue
+}
+
+export function Steps({ children, className }: StepsProps) {
+ return {children}
+}
diff --git a/src/app/components/mdx/Div.tsx b/src/app/components/mdx/Div.tsx
index 34794de0..32b816a4 100644
--- a/src/app/components/mdx/Div.tsx
+++ b/src/app/components/mdx/Div.tsx
@@ -6,6 +6,7 @@ import { CodeGroup } from './CodeGroup.js'
import { CodeTitle } from './CodeTitle.js'
import * as styles from './Div.css.js'
import { Subtitle } from './Subtitle.js'
+import { Steps } from './Steps.js'
export function Div(props: DetailedHTMLProps, HTMLDivElement>) {
const className = clsx(props.className, styles.root)
@@ -15,6 +16,7 @@ export function Div(props: DetailedHTMLProps, HTM
return
if ('data-rehype-pretty-code-fragment' in props)
return
+ if ('data-vocs-steps' in props) return
if (props.role === 'doc-subtitle') return
return
}
diff --git a/src/app/components/mdx/Pre.css.ts b/src/app/components/mdx/Pre.css.ts
index c781719e..de1b3987 100644
--- a/src/app/components/mdx/Pre.css.ts
+++ b/src/app/components/mdx/Pre.css.ts
@@ -2,11 +2,15 @@ import { globalStyle, style } from '@vanilla-extract/css'
import { spaceVars, viewportVars } from '../../styles/vars.css.js'
export const root = style({
- '@media': {
- [viewportVars['max-720px']]: {
- borderRadius: 0,
- marginLeft: `calc(-1 * ${spaceVars['16']})`,
- marginRight: `calc(-1 * ${spaceVars['16']})`,
+ selectors: {
+ '&&': {
+ '@media': {
+ [viewportVars['max-720px']]: {
+ borderRadius: 0,
+ marginLeft: `calc(-1 * ${spaceVars['16']})`,
+ marginRight: `calc(-1 * ${spaceVars['16']})`,
+ },
+ },
},
},
})
diff --git a/src/app/components/mdx/Steps.tsx b/src/app/components/mdx/Steps.tsx
new file mode 100644
index 00000000..257eb6f8
--- /dev/null
+++ b/src/app/components/mdx/Steps.tsx
@@ -0,0 +1,24 @@
+import type { ReactNode } from 'react'
+
+import { Steps as Steps_ } from '../Steps.js'
+import { Step } from '../Step.js'
+
+export function Steps({ children }: { children: ReactNode }) {
+ if (!Array.isArray(children)) return null
+ return (
+
+ {children.map(({ props }, i) => {
+ const [title, ...children] = props.children
+ return (
+
+ {children}
+
+ )
+ })}
+
+ )
+}
diff --git a/src/app/styles/vars.css.ts b/src/app/styles/vars.css.ts
index 838e91ad..19539837 100644
--- a/src/app/styles/vars.css.ts
+++ b/src/app/styles/vars.css.ts
@@ -92,7 +92,7 @@ createGlobalTheme(':root.dark', primitiveColorVars, {
backgroundIrisTint: globalColors.irisA.irisA4,
backgroundRedTint: globalColors.redA.redA3,
backgroundYellowTint: globalColors.yellowA.yellowA2,
- border: globalColors.mauveDark.mauve5,
+ border: globalColors.mauveDark.mauve6,
border2: globalColors.mauveDark.mauve9,
borderAccent: globalColors.amberDark.amber11,
borderBlue: globalColors.blueA.blueA4,
@@ -322,6 +322,7 @@ export const spaceVars = createGlobalThemeContract(
'28': '28',
'32': '32',
'40': '40',
+ '44': '44',
'48': '48',
'56': '56',
'64': '64',
@@ -348,6 +349,7 @@ createGlobalTheme(':root', spaceVars, {
'28': '1.75rem',
'32': '2rem',
'40': '2.5rem',
+ '44': '2.75rem',
'48': '3rem',
'56': '3.5rem',
'64': '4rem',
diff --git a/src/package.json b/src/package.json
index 379639d3..cad06ad3 100644
--- a/src/package.json
+++ b/src/package.json
@@ -9,7 +9,8 @@
"exports": {
".": "./_lib/index.js",
"./head": "./_lib/head.js",
- "./internal": "./_lib/internal.js"
+ "./internal": "./_lib/internal.js",
+ "./react": "./_lib/react.js"
},
"peerDependencies": {
"react": "^18.2.0",
diff --git a/src/react.ts b/src/react.ts
new file mode 100644
index 00000000..34f713b7
--- /dev/null
+++ b/src/react.ts
@@ -0,0 +1,3 @@
+export { Callout, type CalloutProps } from './app/components/Callout.js'
+export { Steps, type StepsProps } from './app/components/Steps.js'
+export { Step, type StepProps } from './app/components/Step.js'
diff --git a/src/vite/plugins/mdx.ts b/src/vite/plugins/mdx.ts
index 024d0c66..e8bd7025 100644
--- a/src/vite/plugins/mdx.ts
+++ b/src/vite/plugins/mdx.ts
@@ -16,6 +16,7 @@ import { remarkCallout } from './remark/callout.js'
import { remarkCodeGroup } from './remark/code-group.js'
import { remarkCode } from './remark/code.js'
import { remarkInferFrontmatter } from './remark/inferred-frontmatter.js'
+import { remarkSteps } from './remark/steps.js'
import { remarkStrongBlock } from './remark/strong-block.js'
import { remarkSubheading } from './remark/subheading.js'
@@ -30,6 +31,7 @@ export function mdx() {
remarkCallout,
remarkCode,
remarkCodeGroup,
+ remarkSteps,
remarkStrongBlock,
remarkSubheading,
],
diff --git a/src/vite/plugins/remark/code.ts b/src/vite/plugins/remark/code.ts
index 0d5a5d0a..70b65d90 100644
--- a/src/vite/plugins/remark/code.ts
+++ b/src/vite/plugins/remark/code.ts
@@ -8,7 +8,7 @@ export function remarkCode() {
return (tree: Root) => {
visit(tree, (node, _, parent) => {
if (node.type !== 'code') return
- if (parent?.type === 'containerDirective') return
+ if (parent?.type === 'containerDirective' && parent.name !== 'steps') return
const [match, title] = node.meta?.match(/\[(.*)\]/) || []
if (match) node.meta = node.meta?.replace(match, `title=\"${title}\"`)
diff --git a/src/vite/plugins/remark/steps.ts b/src/vite/plugins/remark/steps.ts
new file mode 100644
index 00000000..f9d65aaa
--- /dev/null
+++ b/src/vite/plugins/remark/steps.ts
@@ -0,0 +1,50 @@
+///
+///
+
+import { h } from 'hastscript'
+import type { Heading, Root } from 'mdast'
+import { visit } from 'unist-util-visit'
+
+export function remarkSteps() {
+ return (tree: Root) => {
+ visit(tree, (node) => {
+ if (node.type !== 'containerDirective') return
+ if (node.name !== 'steps') return
+
+ const data = node.data || (node.data = {})
+ const tagName = 'div'
+
+ node.attributes = {
+ ...node.attributes,
+ 'data-vocs-steps': 'true',
+ }
+
+ data.hName = tagName
+ data.hProperties = h(tagName, node.attributes || {}).properties
+
+ const depth = (node.children.find((child) => child.type === 'heading') as Heading)?.depth ?? 2
+
+ let currentChild
+ const children = []
+ for (const child of node.children) {
+ if (child.type === 'heading' && child.depth === depth) {
+ if (currentChild && currentChild.children.length > 0) children.push(currentChild)
+ currentChild = {
+ type: 'paragraph',
+ children: [],
+ data: {
+ hName: 'div',
+ hProperties: {
+ 'data-depth': depth,
+ },
+ },
+ } as any
+ }
+ currentChild!.children.push(child)
+ }
+ children.push(currentChild)
+
+ node.children = children
+ })
+ }
+}