From b62e28e70ea6b81a93e60a369367280303f0859b Mon Sep 17 00:00:00 2001 From: Julien Deniau Date: Sun, 1 Jun 2025 21:46:09 +0000 Subject: [PATCH 1/2] Better sidebar for v5 navigation --- website/src/{Sidebar.tsx => SidebarV4.tsx} | 5 +- website/src/TypeDocumentation.tsx | 5 +- website/src/app/browser-extension/page.tsx | 4 +- website/src/app/docs/[version]/page.tsx | 4 +- website/src/app/docs/currentVersion.tsx | 104 ++++++++++----------- website/src/app/docs/v5/[type]/page.tsx | 40 +++++++- website/src/app/docs/v5/layout.tsx | 11 +-- website/src/app/docs/v5/page.tsx | 34 ++++--- website/src/app/play/page.tsx | 8 +- website/src/getSidebarLinks.tsx | 2 +- website/src/sidebar/Focus.tsx | 31 ++++++ website/src/sidebar/FocusGroup.tsx | 29 ++++++ website/src/sidebar/FunctionLink.tsx | 15 +++ website/src/sidebar/Sidebar.tsx | 46 +++++++++ website/src/sidebar/SidebarMainLink.tsx | 47 ++++++++++ website/src/sidebar/index.ts | 3 + website/src/utils/doc.tsx | 41 +++++++- website/styles/globals.css | 9 ++ 18 files changed, 338 insertions(+), 100 deletions(-) rename website/src/{Sidebar.tsx => SidebarV4.tsx} (98%) create mode 100644 website/src/sidebar/Focus.tsx create mode 100644 website/src/sidebar/FocusGroup.tsx create mode 100644 website/src/sidebar/FunctionLink.tsx create mode 100644 website/src/sidebar/Sidebar.tsx create mode 100644 website/src/sidebar/SidebarMainLink.tsx create mode 100644 website/src/sidebar/index.ts diff --git a/website/src/Sidebar.tsx b/website/src/SidebarV4.tsx similarity index 98% rename from website/src/Sidebar.tsx rename to website/src/SidebarV4.tsx index d7d41f660f..7de09465ce 100644 --- a/website/src/Sidebar.tsx +++ b/website/src/SidebarV4.tsx @@ -6,8 +6,7 @@ import type { TypeDefinition } from './TypeDefs'; import { collectMemberGroups } from './collectMemberGroups'; import { ArrowDown } from './ArrowDown'; import { SIDEBAR_LINKS } from './app/docs/currentVersion'; - -export type SidebarLinks = Array<{ label: string; url: string }>; +import { SidebarLinks } from './sidebar'; function Links({ links, @@ -130,7 +129,7 @@ function Focus({ ); } -export function SideBar({ +export function SideBarV4({ links = SIDEBAR_LINKS, focus, toggleShowInherited, diff --git a/website/src/TypeDocumentation.tsx b/website/src/TypeDocumentation.tsx index c8118de8c2..46d76d8802 100644 --- a/website/src/TypeDocumentation.tsx +++ b/website/src/TypeDocumentation.tsx @@ -3,7 +3,8 @@ import { Fragment, useReducer } from 'react'; import { InterfaceDef, CallSigDef } from './Defs'; -import { SideBar, SidebarLinks } from './Sidebar'; +import { SidebarLinks } from './sidebar/Sidebar'; +import { SideBarV4 } from './SidebarV4'; import { MemberDoc } from './MemberDoc'; import { MarkdownContent } from './MarkdownContent'; import { collectMemberGroups } from './collectMemberGroups'; @@ -46,7 +47,7 @@ export function TypeDocumentation({ return ( <> - { return { @@ -13,7 +13,7 @@ export default async function BrowserExtensionPage() { return ( <> - +
diff --git a/website/src/app/docs/[version]/page.tsx b/website/src/app/docs/[version]/page.tsx index c44e68f5e9..2fe3ca3a0b 100644 --- a/website/src/app/docs/[version]/page.tsx +++ b/website/src/app/docs/[version]/page.tsx @@ -3,7 +3,7 @@ import { getVersionFromGitTag } from '../../../static/getVersions'; import { getTypeDefs } from '../../../static/getTypeDefs'; import { DocOverview, getOverviewData } from '../../../DocOverview'; import { DocSearch } from '../../../DocSearch'; -import { SideBar } from '../../../Sidebar'; +import { SideBarV4 } from '../../../SidebarV4'; import { getSidebarLinks } from '../../../getSidebarLinks'; import { getVersionFromParams } from '../../getVersionFromParams'; import { VERSION } from '../currentVersion'; @@ -45,7 +45,7 @@ export default async function OverviewDocPage(props: Props) { return ( <> - +

Immutable.js ({version})

diff --git a/website/src/app/docs/currentVersion.tsx b/website/src/app/docs/currentVersion.tsx index 92f8c53754..0eb00bd539 100644 --- a/website/src/app/docs/currentVersion.tsx +++ b/website/src/app/docs/currentVersion.tsx @@ -5,257 +5,257 @@ export const SIDEBAR_LINKS = [ label: 'List', description: 'Lists are ordered indexed dense collections, much like a JavaScript Array.', - url: `/docs/${VERSION}/List`, + url: `/docs/${VERSION}/List/`, }, { label: 'Map', description: 'Immutable Map is an unordered Collection.Keyed of (key, value) pairs with O(log32 N) gets and O(log32 N) persistent sets.', - url: `/docs/${VERSION}/Map`, + url: `/docs/${VERSION}/Map/`, }, { label: 'OrderedMap', description: 'A type of Map that has the additional guarantee that the iteration order of entries will be the order in which they were set().', - url: `/docs/${VERSION}/OrdererMap`, + url: `/docs/${VERSION}/OrdererMap/`, }, { label: 'Set', description: 'A Collection of unique values with O(log32 N) adds and has.', - url: `/docs/${VERSION}/Set`, + url: `/docs/${VERSION}/Set/`, }, { label: 'OrderedSet', description: 'A type of Set that has the additional guarantee that the iteration order of values will be the order in which they were added.', - url: `/docs/${VERSION}/OrderedSet`, + url: `/docs/${VERSION}/OrderedSet/`, }, { label: 'Stack', description: 'Stacks are indexed collections which support very efficient O(1) addition and removal from the front using unshift(v) and shift().', - url: `/docs/${VERSION}/Stack`, + url: `/docs/${VERSION}/Stack/`, }, { label: 'Range()', description: 'Returns a Seq.Indexed of numbers from start (inclusive) to end (exclusive), by step, where start defaults to 0, step to 1, and end to infinity. When start is equal to end, returns empty range.', - url: `/docs/${VERSION}/Range()`, + url: `/docs/${VERSION}/Range()/`, }, { label: 'Repeat()', description: 'Returns a Seq.Indexed of value repeated times times. When times is not defined, returns an infinite Seq of value.', - url: `/docs/${VERSION}/Repeat()`, + url: `/docs/${VERSION}/Repeat()/`, }, { label: 'Record', description: 'A record is similar to a JS object, but enforces a specific set of allowed string keys, and has default values.', - url: `/docs/${VERSION}/Record`, + url: `/docs/${VERSION}/Record/`, }, { label: 'Record.Factory', description: 'A Record.Factory is created by the Record() function. Record instances are created by passing it some of the accepted values for that Record type:', - url: `/docs/${VERSION}/Record.Factory`, + url: `/docs/${VERSION}/Record.Factory/`, }, { label: 'Seq', description: 'Seq describes a lazy operation, allowing them to efficiently chain use of all the higher-order collection methods (such as map and filter) by not creating intermediate collections.', - url: `/docs/${VERSION}/Seq`, + url: `/docs/${VERSION}/Seq/`, }, { label: 'Seq.Keyed', description: 'Seq which represents key-value pairs.', - url: `/docs/${VERSION}/Seq.Keyed`, + url: `/docs/${VERSION}/Seq.Keyed/`, }, { label: 'Seq.Indexed', description: 'Seq which represents an ordered indexed list of values.', - url: `/docs/${VERSION}/Seq.Indexed`, + url: `/docs/${VERSION}/Seq.Indexed/`, }, { label: 'Seq.Set', description: 'Seq which represents a set of values.', - url: `/docs/${VERSION}/Seq.Set`, + url: `/docs/${VERSION}/Seq.Set/`, }, { label: 'Collection', description: 'The Collection is a set of (key, value) entries which can be iterated, and is the base class for all collections in immutable, allowing them to make use of all the Collection methods (such as map and filter).', - url: `/docs/${VERSION}/Collection`, + url: `/docs/${VERSION}/Collection/`, }, { label: 'Collection.Keyed', description: 'Keyed Collections have discrete keys tied to each value.', - url: `/docs/${VERSION}/Collection.Keyed`, + url: `/docs/${VERSION}/Collection.Keyed/`, }, { label: 'Collection.Indexed', description: "Indexed Collections have incrementing numeric keys. They exhibit slightly different behavior than Collection.Keyed for some methods in order to better mirror the behavior of JavaScript's Array, and add methods which do not make sense on non-indexed Collections such as indexOf.", - url: `/docs/${VERSION}/Collection.Indexed`, + url: `/docs/${VERSION}/Collection.Indexed/`, }, { label: 'Collection.Set', description: 'Set Collections only represent values. They have no associated keys or indices. Duplicate values are possible in the lazy Seq.Sets, however the concrete Set Collection does not allow duplicate values.', - url: `/docs/${VERSION}/Collection.Set`, + url: `/docs/${VERSION}/Collection.Set/`, }, { label: 'ValueObject', description: '', - url: `/docs/${VERSION}/ValueObject`, + url: `/docs/${VERSION}/ValueObject/`, }, { label: 'OrderedCollection', description: '', - url: `/docs/${VERSION}/OrderedCollection`, + url: `/docs/${VERSION}/OrderedCollection/`, }, // functions - { label: 'fromJS()', description: '', url: `/docs/${VERSION}/fromJS()` }, + { label: 'fromJS()', description: '', url: `/docs/${VERSION}/fromJS()/` }, { label: 'is()', description: 'Value equality check with semantics similar to Object.is, but treats Immutable Collections as values, equal if the second Collection includes equivalent values.', - url: `/docs/${VERSION}/is()`, + url: `/docs/${VERSION}/is()/`, }, { label: 'hash()', description: 'The hash() function is an important part of how Immutable determines if two values are equivalent and is used to determine how to store those values. Provided with any value, hash() will return a 31-bit integer.', - url: `/docs/${VERSION}/hash()`, + url: `/docs/${VERSION}/hash()/`, }, { label: 'isImmutable()', description: 'True if maybeImmutable is an Immutable Collection or Record.', - url: `/docs/${VERSION}/isImmutable()`, + url: `/docs/${VERSION}/isImmutable()/`, }, { label: 'isCollection()', description: 'True if maybeCollection is a Collection, or any of its subclasses.', - url: `/docs/${VERSION}/isCollection()`, + url: `/docs/${VERSION}/isCollection()/`, }, { label: 'isKeyed()', description: 'True if maybeKeyed is a Collection.Keyed, or any of its subclasses.', - url: `/docs/${VERSION}/isKeyed()`, + url: `/docs/${VERSION}/isKeyed()/`, }, { label: 'isIndexed()', description: 'True if maybeIndexed is a Collection.Indexed, or any of its subclasses.', - url: `/docs/${VERSION}/isIndexed()`, + url: `/docs/${VERSION}/isIndexed()/`, }, { label: 'isAssociative()', description: 'True if maybeAssociative is either a Keyed or Indexed Collection.', - url: `/docs/${VERSION}/isAssociative()`, + url: `/docs/${VERSION}/isAssociative()/`, }, { label: 'isOrdered()', description: '', - url: `/docs/${VERSION}/isOrdered()`, + url: `/docs/${VERSION}/isOrdered()/`, }, { label: 'isValueObject()', description: 'True if maybeValue is a JavaScript Object which has both equals() and hashCode() methods.', - url: `/docs/${VERSION}/isValueObject()`, + url: `/docs/${VERSION}/isValueObject()/`, }, { label: 'isSeq()', description: 'True if maybeSeq is a Seq.', - url: `/docs/${VERSION}/isSeq()`, + url: `/docs/${VERSION}/isSeq()/`, }, { label: 'isList()', description: 'True if maybeList is a List.', - url: `/docs/${VERSION}/isList()`, + url: `/docs/${VERSION}/isList()/`, }, { label: 'isMap()', description: 'True if maybeMap is a Map.', - url: `/docs/${VERSION}/isMap()`, + url: `/docs/${VERSION}/isMap()/`, }, { label: 'isOrderedMap()', description: 'True if maybeOrderedMap is an OrderedMap.', - url: `/docs/${VERSION}/isOrderedMap()`, + url: `/docs/${VERSION}/isOrderedMap()/`, }, { label: 'isStack()', description: 'True if maybeStack is a Stack.', - url: `/docs/${VERSION}/isStack()`, + url: `/docs/${VERSION}/isStack()/`, }, { label: 'isSet()', description: 'True if maybeSet is a Set.', - url: `/docs/${VERSION}/isSet()`, + url: `/docs/${VERSION}/isSet()/`, }, { label: 'isOrderedSet()', description: 'True if maybeOrderedSet is an OrderedSet.', - url: `/docs/${VERSION}/isOrderedSet()`, + url: `/docs/${VERSION}/isOrderedSet()/`, }, { label: 'isRecord()', description: 'True if maybeRecord is a Record.', - url: `/docs/${VERSION}/isRecord()`, + url: `/docs/${VERSION}/isRecord()/`, }, { label: 'get()', description: 'Returns true if the key is defined in the provided collection.', - url: `/docs/${VERSION}/get()`, + url: `/docs/${VERSION}/get()/`, }, - { label: 'has()', description: '', url: `/docs/${VERSION}/has()` }, - { label: 'remove()', description: '', url: `/docs/${VERSION}/remove()` }, - { label: 'set()', description: '', url: `/docs/${VERSION}/set()` }, - { label: 'update()', description: '', url: `/docs/${VERSION}/update()` }, - { label: 'getIn()', description: '', url: `/docs/${VERSION}/getIn()` }, - { label: 'hasIn()', description: '', url: `/docs/${VERSION}/hasIn()` }, + { label: 'has()', description: '', url: `/docs/${VERSION}/has()/` }, + { label: 'remove()', description: '', url: `/docs/${VERSION}/remove()/` }, + { label: 'set()', description: '', url: `/docs/${VERSION}/set()/` }, + { label: 'update()', description: '', url: `/docs/${VERSION}/update()/` }, + { label: 'getIn()', description: '', url: `/docs/${VERSION}/getIn()/` }, + { label: 'hasIn()', description: '', url: `/docs/${VERSION}/hasIn()/` }, { label: 'removeIn()', description: 'Returns a copy of the collection with the value at the key path removed.', - url: `/docs/${VERSION}/removeIn()`, + url: `/docs/${VERSION}/removeIn()/`, }, { label: 'setIn()', description: 'Returns a copy of the collection with the value at the key path set to the provided value.', - url: `/docs/${VERSION}/setIn()`, + url: `/docs/${VERSION}/setIn()/`, }, - { label: 'updateIn()', description: '', url: `/docs/${VERSION}/updateIn()` }, + { label: 'updateIn()', description: '', url: `/docs/${VERSION}/updateIn()/` }, { label: 'merge()', description: 'Returns a copy of the collection with the remaining collections merged in.', - url: `/docs/${VERSION}/merge()`, + url: `/docs/${VERSION}/merge()/`, }, { label: 'mergeWith()', description: 'Returns a copy of the collection with the remaining collections merged in, calling the merger function whenever an existing value is encountered.', - url: `/docs/${VERSION}/mergeWith()`, + url: `/docs/${VERSION}/mergeWith()/`, }, { label: 'mergeDeep()', description: 'Like merge(), but when two compatible collections are encountered with the same key, it merges them as well, recursing deeply through the nested data. Two collections are considered to be compatible (and thus will be merged together) if they both fall into one of three categories: keyed (e.g., Maps, Records, and objects), indexed (e.g., Lists and arrays), or set-like (e.g., Sets). If they fall into separate categories, mergeDeep will replace the existing collection with the collection being merged in. This behavior can be customized by using mergeDeepWith().', - url: `/docs/${VERSION}/mergeDeep()`, + url: `/docs/${VERSION}/mergeDeep()/`, }, { label: 'mergeDeepWith()', description: 'Like mergeDeep(), but when two non-collections or incompatible collections are encountered at the same key, it uses the merger function to determine the resulting value. Collections are considered incompatible if they fall into separate categories between keyed, indexed, and set-like.', - url: `/docs/${VERSION}/mergeDeepWith()`, + url: `/docs/${VERSION}/mergeDeepWith()/`, }, ]; diff --git a/website/src/app/docs/v5/[type]/page.tsx b/website/src/app/docs/v5/[type]/page.tsx index 326c86246c..1a5f99276d 100644 --- a/website/src/app/docs/v5/[type]/page.tsx +++ b/website/src/app/docs/v5/[type]/page.tsx @@ -1,4 +1,6 @@ -import { getDocFiles } from '../../../../utils/doc'; +import { getDocFiles, getDocDetail } from '../../../../utils/doc'; +import { Sidebar, FocusType } from '../../../../sidebar'; +import { DocSearch } from '../../../../DocSearch'; export async function generateStaticParams() { const docFiles = getDocFiles(); @@ -20,8 +22,6 @@ type Props = { export async function generateMetadata(props: Props) { const params = await props.params; - console.log('params', params); - return { title: `${params.type} — Immutable.js`, }; @@ -32,7 +32,39 @@ export default async function TypeDocPage(props: Props) { const { type } = params; + const detail = getDocDetail(type); + const focus = detail.reduce((carry, item) => { + if (item.type === 'title') { + const focus = { + qualifiedName: item.name, + label: item.name, // Like a name, but with () for callables. + functions: {}, + }; + return [...carry, focus]; + } + + const lastItem = carry[carry.length - 1]; + + if (lastItem) { + lastItem.functions[item.name] = { + label: item.name, + url: `#${item.name}`, + }; + } + + return carry; + }, []); + const { default: MdxContent } = await import(`@/docs/${type}.mdx`); - return ; + return ( +
+ + +
+ + ; +
+
+ ); } diff --git a/website/src/app/docs/v5/layout.tsx b/website/src/app/docs/v5/layout.tsx index ca54797ef9..0a21808f9c 100644 --- a/website/src/app/docs/v5/layout.tsx +++ b/website/src/app/docs/v5/layout.tsx @@ -1,5 +1,3 @@ -import { SideBar } from '../../../Sidebar'; -import { DocSearch } from '../../../DocSearch'; import { DocHeader } from '../../../DocHeader'; import { ImmutableConsole } from '../../../ImmutableConsole'; import { getVersions } from '../../../static/getVersions'; @@ -18,14 +16,7 @@ export default async function VersionLayout(props: {
-
- - -
- - {children} -
-
+
{children}
); diff --git a/website/src/app/docs/v5/page.tsx b/website/src/app/docs/v5/page.tsx index 9932d012df..9185f93551 100644 --- a/website/src/app/docs/v5/page.tsx +++ b/website/src/app/docs/v5/page.tsx @@ -1,6 +1,8 @@ import { Metadata } from 'next'; import Link from 'next/link'; import { SIDEBAR_LINKS, VERSION } from '../currentVersion'; +import { Sidebar } from '../../../sidebar'; +import { DocSearch } from '../../../DocSearch'; export async function generateMetadata(): Promise { return { @@ -13,21 +15,25 @@ export default async function OverviewDocPage() { return ( <> -
-

Immutable.js ({VERSION})

- {/* */} - + - {SIDEBAR_LINKS.map((link) => ( -
-

- {link.label} -

-
-

{link.description}

-
-
- ))} +
+ +
+

Immutable.js ({VERSION})

+ + + {SIDEBAR_LINKS.map((link) => ( +
+

+ {link.label} +

+
+

{link.description}

+
+
+ ))} +
); diff --git a/website/src/app/play/page.tsx b/website/src/app/play/page.tsx index 5e47b40e25..0e125e722f 100644 --- a/website/src/app/play/page.tsx +++ b/website/src/app/play/page.tsx @@ -1,13 +1,9 @@ import { Metadata } from 'next'; import { DocSearch } from '../../DocSearch'; -import { SideBar } from '../../Sidebar'; +import { Sidebar } from '../../sidebar'; import Playground from './Playground'; import { VERSION } from '../docs/currentVersion'; -export async function generateStaticParams() { - // return [...getVersions().map((version) => ({ version }))]; -} - export async function generateMetadata(): Promise { return { title: `Playground — Immutable.js`, @@ -17,7 +13,7 @@ export async function generateMetadata(): Promise { export default function OverviewDocPage() { return ( <> - +

Playgroud ({VERSION})

diff --git a/website/src/getSidebarLinks.tsx b/website/src/getSidebarLinks.tsx index bb86bf8a85..66a0364787 100644 --- a/website/src/getSidebarLinks.tsx +++ b/website/src/getSidebarLinks.tsx @@ -1,5 +1,5 @@ import type { TypeDefs } from './TypeDefs'; -import { SidebarLinks } from './Sidebar'; +import { SidebarLinks } from './sidebar'; export function getSidebarLinks(defs: TypeDefs): SidebarLinks { return Object.values(defs.types).map(({ label, url }) => ({ label, url })); diff --git a/website/src/sidebar/Focus.tsx b/website/src/sidebar/Focus.tsx new file mode 100644 index 0000000000..773cb4b1d9 --- /dev/null +++ b/website/src/sidebar/Focus.tsx @@ -0,0 +1,31 @@ +import { JSX } from 'react'; +import FocusGroup from './FocusGroup'; + +type FocusItem = { + label: string; + functions: Record; +}; + +export type FocusType = Array; + +export default function Focus({ + focus, +}: { + focus?: FocusType; +}): JSX.Element | null { + if (focus?.length === 0) { + return null; + } + + return ( +
+ {focus?.map((def) => ( + + ))} +
+ ); +} diff --git a/website/src/sidebar/FocusGroup.tsx b/website/src/sidebar/FocusGroup.tsx new file mode 100644 index 0000000000..0eff6272da --- /dev/null +++ b/website/src/sidebar/FocusGroup.tsx @@ -0,0 +1,29 @@ +import { JSX } from 'react'; +import FunctionLink from './FunctionLink'; + +type FunctionDefinition = { + label: string; + url: string; +}; + +type Props = { + title: string; + functions: Array; +}; + +export default function FocusGroup({ title, functions }: Props): JSX.Element { + return ( +
+

+ {title} +

+ {functions.map((member) => ( + + ))} +
+ ); +} diff --git a/website/src/sidebar/FunctionLink.tsx b/website/src/sidebar/FunctionLink.tsx new file mode 100644 index 0000000000..b83fd8e5f1 --- /dev/null +++ b/website/src/sidebar/FunctionLink.tsx @@ -0,0 +1,15 @@ +import Link from 'next/link'; +import { JSX } from 'react'; + +type Props = { + label: string; + url: string; +}; + +export default function FunctionLink({ label, url }: Props): JSX.Element { + return ( +
+ {label} +
+ ); +} diff --git a/website/src/sidebar/Sidebar.tsx b/website/src/sidebar/Sidebar.tsx new file mode 100644 index 0000000000..8b2cf11d23 --- /dev/null +++ b/website/src/sidebar/Sidebar.tsx @@ -0,0 +1,46 @@ +'use client'; + +import { Fragment, useState } from 'react'; +import { SIDEBAR_LINKS } from '../app/docs/currentVersion'; +import SidebarMainLink from './SidebarMainLink'; +import Focus, { FocusType } from './Focus'; + +export type SidebarLinks = Array<{ label: string; url: string }>; + +export default function SideBar({ + links = SIDEBAR_LINKS, + focus, + activeType, +}: { + links?: SidebarLinks; + focus?: FocusType; + activeType?: string; +}) { + const [isForcedClosed, setIsForcedClosed] = useState(false); + + return ( +
+
+
+

Immutable.js

+ {links.map((link) => { + const isCurrent = activeType === link.label; + const isActive = isCurrent && !isForcedClosed; + return ( + + setIsForcedClosed((prev) => !prev)} + /> + + {isActive && } + + ); + })} +
+
+ ); +} diff --git a/website/src/sidebar/SidebarMainLink.tsx b/website/src/sidebar/SidebarMainLink.tsx new file mode 100644 index 0000000000..61a60bfa87 --- /dev/null +++ b/website/src/sidebar/SidebarMainLink.tsx @@ -0,0 +1,47 @@ +import { JSX } from 'react'; +import Link from 'next/link'; +import { ArrowDown } from '..//ArrowDown'; +import { usePathname } from 'next/navigation'; + +type Props = { + label: string; + url: string; + canBeFocused: boolean; + isActive: boolean; + onClick: () => void; +}; + +export default function SidebarMainLink({ + label, + url, + canBeFocused, + isActive, + onClick, +}: Props): JSX.Element { + const pathname = usePathname(); + + const isCurrent = pathname === url; + + return ( +
+ { + if (isCurrent) { + e.preventDefault(); + + onClick(); + } + }} + > + {label} + {isActive && canBeFocused && ( + <> + {' '} + + + )} + +
+ ); +} diff --git a/website/src/sidebar/index.ts b/website/src/sidebar/index.ts new file mode 100644 index 0000000000..57410fadf2 --- /dev/null +++ b/website/src/sidebar/index.ts @@ -0,0 +1,3 @@ +export { default as Sidebar } from './Sidebar'; +export type { SidebarLinks } from './Sidebar'; +export type { FocusType } from './Focus'; diff --git a/website/src/utils/doc.tsx b/website/src/utils/doc.tsx index d519730bb4..2c39b07aa6 100644 --- a/website/src/utils/doc.tsx +++ b/website/src/utils/doc.tsx @@ -5,10 +5,12 @@ function getMDXFiles(dir: string): Array { return fs.readdirSync(dir).filter((file) => path.extname(file) === '.mdx'); } -function getMDXData(dir: string): Array<{ slug: string }> { - const files = getMDXFiles(dir); +type MDXFile = { + slug: string; +}; - console.log('files', files); +function getMDXData(dir: string): Array { + const files = getMDXFiles(dir); return files.map((file) => { const slug = path.basename(file, path.extname(file)); @@ -19,8 +21,39 @@ function getMDXData(dir: string): Array<{ slug: string }> { }); } -export function getDocFiles() { +export function getDocFiles(): Array { const docsDir = path.join(process.cwd(), 'docs'); return getMDXData(docsDir); } + +export function getDocDetail( + slug: string +): Array<{ type: 'title' | 'functionName'; name: string }> { + const docsDir = path.join(process.cwd(), 'docs'); + const file = path.join(docsDir, `${slug}.mdx`); + if (!fs.existsSync(file)) { + return []; + } + + const content = fs.readFileSync(file, 'utf-8'); + + const regex = new RegExp( + '^(## (?.*)|<MemberLabel.*label="(?<functionName>[^"]*)")', + 'gm' + ); + + const titleMatch = content.matchAll(regex); + + return Array.from(titleMatch).map((match) => { + if (match.groups?.title) { + return { type: 'title', name: match.groups.title }; + } + + if (match.groups?.functionName) { + return { type: 'functionName', name: match.groups.functionName }; + } + + throw new Error(`Unexpected match groups: ${JSON.stringify(match.groups)}`); + }); +} diff --git a/website/styles/globals.css b/website/styles/globals.css index 6ee9cd3262..86383aaf48 100644 --- a/website/styles/globals.css +++ b/website/styles/globals.css @@ -47,6 +47,10 @@ body { } } +html { + scroll-behavior: smooth; +} + body, input { color: var(--body-color); @@ -259,6 +263,11 @@ img { top: var(--header-content-padding); } +/* Anchor links: margin-top of 60px, like the header height */ +[id] { + scroll-margin-top: 60px; +} + .MenuButton__Toggle { display: none; } From 1d6fd8f30c0ceb4550f562d138ca2c304a4f8d99 Mon Sep 17 00:00:00 2001 From: Julien Deniau <julien.deniau@gmail.com> Date: Sun, 1 Jun 2025 21:58:56 +0000 Subject: [PATCH 2/2] migrate old doc component into [version] pages --- website/src/{ => app/docs/[version]}/Defs.tsx | 2 +- .../src/{ => app/docs/[version]}/DocOverview.tsx | 4 ++-- .../src/{ => app/docs/[version]}/MemberDoc.tsx | 2 +- .../src/{ => app/docs/[version]}/SidebarV4.tsx | 6 +++--- website/src/{ => app/docs/[version]}/TypeDefs.ts | 0 .../docs/[version]/[type]}/TypeDocumentation.tsx | 16 ++++++++-------- website/src/app/docs/[version]/[type]/page.tsx | 8 ++++---- .../docs/[version]}/collectMemberGroups.ts | 0 .../{ => app/docs/[version]}/getSidebarLinks.tsx | 2 +- .../docs/[version]}/getTypeDefs.ts | 6 +++--- .../{ => docs/[version]}/getVersionFromParams.ts | 0 website/src/app/docs/[version]/layout.tsx | 2 +- website/src/app/docs/[version]/page.tsx | 10 +++++----- website/src/static/markdown.ts | 6 +++++- 14 files changed, 34 insertions(+), 30 deletions(-) rename website/src/{ => app/docs/[version]}/Defs.tsx (99%) rename website/src/{ => app/docs/[version]}/DocOverview.tsx (94%) rename website/src/{ => app/docs/[version]}/MemberDoc.tsx (98%) rename website/src/{ => app/docs/[version]}/SidebarV4.tsx (96%) rename website/src/{ => app/docs/[version]}/TypeDefs.ts (100%) rename website/src/{ => app/docs/[version]/[type]}/TypeDocumentation.tsx (91%) rename website/src/{ => app/docs/[version]}/collectMemberGroups.ts (100%) rename website/src/{ => app/docs/[version]}/getSidebarLinks.tsx (79%) rename website/src/{static => app/docs/[version]}/getTypeDefs.ts (99%) rename website/src/app/{ => docs/[version]}/getVersionFromParams.ts (100%) diff --git a/website/src/Defs.tsx b/website/src/app/docs/[version]/Defs.tsx similarity index 99% rename from website/src/Defs.tsx rename to website/src/app/docs/[version]/Defs.tsx index a8a464643d..5b3a3e9a34 100644 --- a/website/src/Defs.tsx +++ b/website/src/app/docs/[version]/Defs.tsx @@ -96,7 +96,7 @@ export function CallSigDef({ ); } -export function TypeDef({ type, prefix }: { type: Type; prefix?: number }) { +function TypeDef({ type, prefix }: { type: Type; prefix?: number }) { switch (type.k) { case TypeKind.Never: return wrap('primitive', 'never'); diff --git a/website/src/DocOverview.tsx b/website/src/app/docs/[version]/DocOverview.tsx similarity index 94% rename from website/src/DocOverview.tsx rename to website/src/app/docs/[version]/DocOverview.tsx index 71a62ce155..77a468a841 100644 --- a/website/src/DocOverview.tsx +++ b/website/src/app/docs/[version]/DocOverview.tsx @@ -1,8 +1,8 @@ import Link from 'next/link'; -import { MarkdownContent } from './MarkdownContent'; +import { MarkdownContent } from '../../../MarkdownContent'; import type { TypeDefs, TypeDoc } from './TypeDefs'; -export type OverviewData = { +type OverviewData = { doc: TypeDoc | null; api: Array<APIMember>; }; diff --git a/website/src/MemberDoc.tsx b/website/src/app/docs/[version]/MemberDoc.tsx similarity index 98% rename from website/src/MemberDoc.tsx rename to website/src/app/docs/[version]/MemberDoc.tsx index 7d36ff2c52..e2706a7db5 100644 --- a/website/src/MemberDoc.tsx +++ b/website/src/app/docs/[version]/MemberDoc.tsx @@ -1,7 +1,7 @@ import Link from 'next/link'; import { Fragment } from 'react'; import { CallSigDef, MemberDef } from './Defs'; -import { MarkdownContent } from './MarkdownContent'; +import { MarkdownContent } from '../../../MarkdownContent'; import type { MemberDefinition } from './TypeDefs'; export function MemberDoc({ member }: { member: MemberDefinition }) { diff --git a/website/src/SidebarV4.tsx b/website/src/app/docs/[version]/SidebarV4.tsx similarity index 96% rename from website/src/SidebarV4.tsx rename to website/src/app/docs/[version]/SidebarV4.tsx index 7de09465ce..ec793dc62f 100644 --- a/website/src/SidebarV4.tsx +++ b/website/src/app/docs/[version]/SidebarV4.tsx @@ -4,9 +4,9 @@ import Link from 'next/link'; import { Fragment, useEffect, useState } from 'react'; import type { TypeDefinition } from './TypeDefs'; import { collectMemberGroups } from './collectMemberGroups'; -import { ArrowDown } from './ArrowDown'; -import { SIDEBAR_LINKS } from './app/docs/currentVersion'; -import { SidebarLinks } from './sidebar'; +import { ArrowDown } from '../../../ArrowDown'; +import { SIDEBAR_LINKS } from '../currentVersion'; +import { SidebarLinks } from '../../../sidebar'; function Links({ links, diff --git a/website/src/TypeDefs.ts b/website/src/app/docs/[version]/TypeDefs.ts similarity index 100% rename from website/src/TypeDefs.ts rename to website/src/app/docs/[version]/TypeDefs.ts diff --git a/website/src/TypeDocumentation.tsx b/website/src/app/docs/[version]/[type]/TypeDocumentation.tsx similarity index 91% rename from website/src/TypeDocumentation.tsx rename to website/src/app/docs/[version]/[type]/TypeDocumentation.tsx index 46d76d8802..e6a04fe57c 100644 --- a/website/src/TypeDocumentation.tsx +++ b/website/src/app/docs/[version]/[type]/TypeDocumentation.tsx @@ -2,14 +2,14 @@ import { Fragment, useReducer } from 'react'; -import { InterfaceDef, CallSigDef } from './Defs'; -import { SidebarLinks } from './sidebar/Sidebar'; -import { SideBarV4 } from './SidebarV4'; -import { MemberDoc } from './MemberDoc'; -import { MarkdownContent } from './MarkdownContent'; -import { collectMemberGroups } from './collectMemberGroups'; -import type { TypeDefinition, MemberDefinition } from './TypeDefs'; -import { DocSearch } from './DocSearch'; +import { InterfaceDef, CallSigDef } from '../Defs'; +import { SidebarLinks } from '../../../../sidebar'; +import { SideBarV4 } from '../SidebarV4'; +import { MemberDoc } from '../MemberDoc'; +import { MarkdownContent } from '../../../../MarkdownContent'; +import { collectMemberGroups } from '../collectMemberGroups'; +import type { TypeDefinition, MemberDefinition } from '../TypeDefs'; +import { DocSearch } from '../../../../DocSearch'; const typeDefURL = 'https://github.com/immutable-js/immutable-js/blob/main/type-definitions/immutable.d.ts'; diff --git a/website/src/app/docs/[version]/[type]/page.tsx b/website/src/app/docs/[version]/[type]/page.tsx index bab7e400be..83d1c1cbdb 100644 --- a/website/src/app/docs/[version]/[type]/page.tsx +++ b/website/src/app/docs/[version]/[type]/page.tsx @@ -1,8 +1,8 @@ -import { getSidebarLinks } from '../../../../getSidebarLinks'; -import { getTypeDefs } from '../../../../static/getTypeDefs'; +import { getSidebarLinks } from '../getSidebarLinks'; +import { getTypeDefs } from '../getTypeDefs'; import { getVersionFromGitTag } from '../../../../static/getVersions'; -import { TypeDocumentation } from '../../../../TypeDocumentation'; -import { getVersionFromParams } from '../../../getVersionFromParams'; +import { TypeDocumentation } from './TypeDocumentation'; +import { getVersionFromParams } from '../getVersionFromParams'; import { VERSION } from '../../currentVersion'; export async function generateStaticParams() { diff --git a/website/src/collectMemberGroups.ts b/website/src/app/docs/[version]/collectMemberGroups.ts similarity index 100% rename from website/src/collectMemberGroups.ts rename to website/src/app/docs/[version]/collectMemberGroups.ts diff --git a/website/src/getSidebarLinks.tsx b/website/src/app/docs/[version]/getSidebarLinks.tsx similarity index 79% rename from website/src/getSidebarLinks.tsx rename to website/src/app/docs/[version]/getSidebarLinks.tsx index 66a0364787..d564f81a89 100644 --- a/website/src/getSidebarLinks.tsx +++ b/website/src/app/docs/[version]/getSidebarLinks.tsx @@ -1,5 +1,5 @@ import type { TypeDefs } from './TypeDefs'; -import { SidebarLinks } from './sidebar'; +import { SidebarLinks } from '../../../sidebar'; export function getSidebarLinks(defs: TypeDefs): SidebarLinks { return Object.values(defs.types).map(({ label, url }) => ({ label, url })); diff --git a/website/src/static/getTypeDefs.ts b/website/src/app/docs/[version]/getTypeDefs.ts similarity index 99% rename from website/src/static/getTypeDefs.ts rename to website/src/app/docs/[version]/getTypeDefs.ts index 967e3e27f6..d4653aa138 100644 --- a/website/src/static/getTypeDefs.ts +++ b/website/src/app/docs/[version]/getTypeDefs.ts @@ -13,9 +13,9 @@ import { TypeDoc, TypeKind, MemberDefinition, -} from '../TypeDefs'; -import { markdown, MarkdownContext } from './markdown'; -import { stripUndefineds } from './stripUndefineds'; +} from './TypeDefs'; +import { markdown, MarkdownContext } from '../../../static/markdown'; +import { stripUndefineds } from '../../../static/stripUndefineds'; const generatedTypeDefs = new Map<string, TypeDefs>(); export function getTypeDefs(version: string) { diff --git a/website/src/app/getVersionFromParams.ts b/website/src/app/docs/[version]/getVersionFromParams.ts similarity index 100% rename from website/src/app/getVersionFromParams.ts rename to website/src/app/docs/[version]/getVersionFromParams.ts diff --git a/website/src/app/docs/[version]/layout.tsx b/website/src/app/docs/[version]/layout.tsx index ab824ad184..0dcfbb0526 100644 --- a/website/src/app/docs/[version]/layout.tsx +++ b/website/src/app/docs/[version]/layout.tsx @@ -1,7 +1,7 @@ import { DocHeader } from '../../../DocHeader'; import { ImmutableConsole } from '../../../ImmutableConsole'; import { getVersions } from '../../../static/getVersions'; -import { getVersionFromParams } from '../../getVersionFromParams'; +import { getVersionFromParams } from './getVersionFromParams'; export default async function VersionLayout(props: { children: React.ReactNode; diff --git a/website/src/app/docs/[version]/page.tsx b/website/src/app/docs/[version]/page.tsx index 2fe3ca3a0b..09c9613cfd 100644 --- a/website/src/app/docs/[version]/page.tsx +++ b/website/src/app/docs/[version]/page.tsx @@ -1,11 +1,11 @@ import { Metadata } from 'next'; import { getVersionFromGitTag } from '../../../static/getVersions'; -import { getTypeDefs } from '../../../static/getTypeDefs'; -import { DocOverview, getOverviewData } from '../../../DocOverview'; +import { getTypeDefs } from './getTypeDefs'; +import { DocOverview, getOverviewData } from './DocOverview'; import { DocSearch } from '../../../DocSearch'; -import { SideBarV4 } from '../../../SidebarV4'; -import { getSidebarLinks } from '../../../getSidebarLinks'; -import { getVersionFromParams } from '../../getVersionFromParams'; +import { SideBarV4 } from './SidebarV4'; +import { getSidebarLinks } from './getSidebarLinks'; +import { getVersionFromParams } from './getVersionFromParams'; import { VERSION } from '../currentVersion'; export async function generateStaticParams() { diff --git a/website/src/static/markdown.ts b/website/src/static/markdown.ts index 244b0f9e32..d6080d08c5 100644 --- a/website/src/static/markdown.ts +++ b/website/src/static/markdown.ts @@ -1,7 +1,11 @@ import { Marked } from 'marked'; import { markedHighlight } from 'marked-highlight'; import prism from 'prismjs'; -import type { TypeDefs, CallSignature, TypeDefinition } from '../TypeDefs'; +import type { + TypeDefs, + CallSignature, + TypeDefinition, +} from '../app/docs/[version]/TypeDefs'; export type MarkdownContext = { defs: TypeDefs;