Skip to content

Commit

Permalink
🛫 Add landing page blocks to book theme (#531)
Browse files Browse the repository at this point in the history
Co-authored-by: Rowan Cockett <[email protected]>
  • Loading branch information
agoose77 and rowanc1 authored Mar 5, 2025
1 parent 55e0435 commit a46ef25
Show file tree
Hide file tree
Showing 23 changed files with 578 additions and 10 deletions.
6 changes: 6 additions & 0 deletions .changeset/blue-seals-arrive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@myst-theme/landing-pages': patch
'myst-to-react': patch
---

Allow for no-width pilcrows on headings.
1 change: 1 addition & 0 deletions .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@myst-theme/icons",
"@myst-theme/search",
"@myst-theme/search-minisearch",
"@myst-theme/landing-pages",
"@myst-theme/book",
"@myst-theme/article",
"myst-to-react",
Expand Down
42 changes: 42 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions packages/landing-pages/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
root: true,
extends: ['curvenote'],
};
6 changes: 6 additions & 0 deletions packages/landing-pages/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# @myst-theme/landing-pages

[![myst-demo on npm](https://img.shields.io/npm/v/@myst-theme/landing-pages.svg)](https://www.npmjs.com/package/@myst-theme/landing-pages)
[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/curvenote/curvenote/blob/main/LICENSE)

A set of landing-page components for MyST
40 changes: 40 additions & 0 deletions packages/landing-pages/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "@myst-theme/landing-pages",
"version": "0.0.0",
"type": "module",
"exports": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
"dist"
],
"engines": {
"node": ">=16"
},
"license": "MIT",
"scripts": {
"clean": "rimraf dist",
"lint": "eslint \"src/**/*.ts*\" \"src/**/*.tsx\" -c ./.eslintrc.cjs",
"lint:format": "prettier --check \"src/**/*.{ts,tsx,md}\"",
"dev": "npm-run-all --parallel \"build:* -- --watch\"",
"build:esm": "tsc",
"build": "npm-run-all -l clean -p build:esm"
},
"dependencies": {
"@myst-theme/providers": "^0.14.1",
"classnames": "^2.5.1",
"myst-common": "^1.7.9",
"myst-config": "^1.7.9",
"myst-frontmatter": "^1.7.9",
"myst-spec": "^0.0.5",
"myst-spec-ext": "^1.7.9",
"myst-to-react": "^0.14.1",
"unist-util-select": "^4.0.3",
"unist-util-filter": "^4.0.0"
},
"peerDependencies": {
"@types/react": "^16.8 || ^17.0 || ^18.0",
"@types/react-dom": "^16.8 || ^17.0 || ^18.0",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
}
}
24 changes: 24 additions & 0 deletions packages/landing-pages/src/BlockHeading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { createElement as e } from 'react';
import type { GenericParent } from 'myst-common';
import { MyST, HashLink } from 'myst-to-react';
import classNames from 'classnames';

export function BlockHeading({ node, className }: { node: GenericParent; className?: string }) {
const { enumerator, depth, key, identifier, html_id } = node;
const id = html_id || identifier || key;

return e(
`h${depth}`,
{
className: classNames(node.class, className, 'group'),
id: id,
},
<>
{enumerator && <span className="mr-3 select-none">{enumerator}</span>}
<span className="heading-text">
<MyST ast={node.children} />
</span>
<HashLink id={id} kind="Section" className="font-normal" hover hideInPopup noWidth />
</>,
);
}
75 changes: 75 additions & 0 deletions packages/landing-pages/src/CenteredBlock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { useMemo } from 'react';
import type { GenericParent, GenericNode } from 'myst-common';
import { MyST } from 'myst-to-react';

import { select, selectAll, matches } from 'unist-util-select';
import { filter } from 'unist-util-filter';
import type { NodeRenderers } from '@myst-theme/providers';

import { InvalidBlock } from './InvalidBlock.js';
import { BlockHeading } from './BlockHeading.js';
import { splitByHeader } from './utils.js';
import { LandingBlock, type LandingBlockProps } from './LandingBlock.js';

export function CenteredBlock(props: Omit<LandingBlockProps, 'children'>) {
const { node } = props;
const { body, links, subtitle, heading } = useMemo(() => {
const { head, body: rawBody } = splitByHeader(node);

const linksNode = selectAll('link[class*=button], crossReference[class*=button]', rawBody);
const subtitleNode = select('paragraph', head) as GenericParent | null;
const headingNode = select('heading', head) as GenericParent | null;
const bodyNodes =
filter(
rawBody,
(otherNode: GenericNode) =>
!matches('link[class*=button], crossReference[class*=button]', otherNode),
)?.children ?? [];

return {
body: bodyNodes,
links: linksNode,
subtitle: subtitleNode,
heading: headingNode,
};
}, [node]);

if (!body) {
return <InvalidBlock {...props} blockName="centered" />;
}
return (
<LandingBlock {...props}>
<div className="relative text-center">
<div className="py-20 sm:py-28">
{subtitle && (
<p className="my-0 font-semibold text-indigo-400 uppercase">
<MyST ast={subtitle.children} />
</p>
)}
{heading && (
<BlockHeading
node={heading}
className="mt-2 mb-0 text-5xl font-semibold tracking-tight"
/>
)}
{body && (
<div className="mt-6">
<MyST ast={body} />
</div>
)}
{links && (
<div className="flex items-center justify-center gap-4 mt-8">
<MyST ast={links} />
</div>
)}
</div>
</div>
</LandingBlock>
);
}

export const CENTERED_RENDERERS: NodeRenderers = {
block: {
'block[kind=centered]': CenteredBlock,
},
};
24 changes: 24 additions & 0 deletions packages/landing-pages/src/InvalidBlock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { MyST } from 'myst-to-react';
import { LandingBlock, type LandingBlockProps } from './LandingBlock.js';

export function InvalidBlock(props: Omit<LandingBlockProps, 'children'> & { blockName: string }) {
const { node, blockName } = props;
return (
<LandingBlock {...props}>
<div className="relative" role="alert">
<div className="px-4 py-2 font-bold text-white bg-red-500 rounded-t">
Invalid block <span className="font-mono">{blockName}</span>
</div>
<div className="border border-t-0 border-red-400 rounded-b ">
<div className="px-4 py-3 text-red-700 bg-red-100">
<p>This '{blockName}' block does not conform to the expected AST structure.</p>
</div>

<div className="px-4 py-3">
<MyST ast={node.children} />
</div>
</div>
</div>
</LandingBlock>
);
}
80 changes: 80 additions & 0 deletions packages/landing-pages/src/JustifiedBlock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { useMemo } from 'react';
import type { GenericParent, GenericNode } from 'myst-common';
import { MyST } from 'myst-to-react';

import { select, selectAll, matches } from 'unist-util-select';
import { filter } from 'unist-util-filter';
import type { NodeRenderers } from '@myst-theme/providers';

import { InvalidBlock } from './InvalidBlock.js';
import { BlockHeading } from './BlockHeading.js';
import { splitByHeader } from './utils.js';
import { LandingBlock, type LandingBlockProps } from './LandingBlock.js';

export function JustifiedBlock(props: Omit<LandingBlockProps, 'children'>) {
const { node } = props;

const { body, links, subtitle, heading } = useMemo(() => {
const { head, body: rawBody } = splitByHeader(node);

const linksNode = selectAll('link[class*=button], crossReference[class*=button]', rawBody);
const subtitleNode = select('paragraph', head) as GenericParent | null;
const headingNode = select('heading', head) as GenericParent | null;
const bodyNodes =
filter(
rawBody,
(otherNode: GenericNode) =>
!matches('link[class*=button], crossReference[class*=button]', otherNode),
)?.children ?? [];

return {
body: bodyNodes,
links: linksNode,
subtitle: subtitleNode,
heading: headingNode,
};
}, [node]);

if (!body) {
return <InvalidBlock {...props} blockName="justified" />;
}
return (
<LandingBlock {...props}>
<div className="py-20 sm:py-28 lg:px-8">
{subtitle && (
<p className="my-0 font-semibold text-indigo-400 uppercase">
<MyST ast={subtitle.children} />
</p>
)}
<div className="flex flex-col lg:content-center lg:justify-between lg:flex-row">
<div className="flex flex-col">
{heading && (
<BlockHeading
node={heading}
className="mt-2 mb-0 text-5xl font-semibold tracking-tight"
/>
)}
{body && (
<div className="mt-6">
<MyST ast={body} />
</div>
)}
</div>
<div className="flex flex-col mt-8 lg:mt-0">
{links && (
<div className="flex flex-row items-center gap-4">
<MyST ast={links} />
</div>
)}
</div>
</div>
</div>
</LandingBlock>
);
}

export const JUSTIFIED_RENDERERS: NodeRenderers = {
block: {
'block[kind=justified]': JustifiedBlock,
},
};
35 changes: 35 additions & 0 deletions packages/landing-pages/src/LandingBlock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { ReactNode } from 'react';
import type { GenericParent } from 'myst-common';

import { useGridSystemProvider } from '@myst-theme/providers';
import classNames from 'classnames';

export type LandingBlockProps = {
node: GenericParent;
className?: string;
children: ReactNode;
};

export function LandingBlock({ node, className, children }: LandingBlockProps) {
const grid = useGridSystemProvider();
const { key } = node;

const subGrid = node.visibility === 'hide' ? '' : `${grid} subgrid-gap col-page [&>*]:col-page`;
const dataClassName = typeof node.data?.class === 'string' ? node.data?.class : undefined;
// Hide the subgrid if either the dataClass or the className exists and includes `col-`
const noSubGrid =
(dataClassName && dataClassName.includes('col-')) || (className && className.includes('col-'));

return (
<div
key={`block-${key}`}
id={key}
className={classNames('relative group/block py-6', className, dataClassName, {
[subGrid]: !noSubGrid,
hidden: node.visibility === 'remove',
})}
>
{children}
</div>
);
}
Loading

0 comments on commit a46ef25

Please sign in to comment.