diff --git a/packages/core/.babelrc b/packages/core/.babelrc new file mode 100644 index 00000000..ccae900b --- /dev/null +++ b/packages/core/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nrwl/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/packages/core/.eslintrc.json b/packages/core/.eslintrc.json new file mode 100644 index 00000000..0b918c2d --- /dev/null +++ b/packages/core/.eslintrc.json @@ -0,0 +1,17 @@ +{ + "extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md new file mode 100644 index 00000000..a309a216 --- /dev/null +++ b/packages/core/CHANGELOG.md @@ -0,0 +1,171 @@ +# @flowershow/core + +## 0.4.13 + +### Patch Changes + +- Fix url to Flowershow url in the footer. + +## 0.4.12 + +### Patch Changes + +- Remove next-contentlayer from peer dependencies. + +## 0.4.11 + +### Patch Changes + +- Fix author logo and add some spacing around it. +- f3d3a60: Fix path to JSON file with kbar search actions. + +## 0.4.10 + +### Patch Changes + +- e0f21bd: SiteToc adjustments to display infinitely nested page directories. +- 714d580: Fix tooltips for empty pages causing page render errors + +## 0.4.9 + +### Patch Changes + +- Minor fix of CustomLink. + +## 0.4.8 + +### Patch Changes + +- Fix: wiki links with tooltips not showing. + +## 0.4.7 + +### Patch Changes + +- 0db40da: Fix logic in CustomLink component for testing if link is internal. + +## 0.4.6 + +### Patch Changes + +- 6e04357: Add BEM classes to BlogItem. + +## 0.4.5 + +### Patch Changes + +- 0167dda: Minor style adjustements to Card, TableOfContents and nav logo. + +## 0.4.4 + +### Patch Changes + +- 6bc7200: Make date frontmatter the default field being used, created can still be used in frontmatter + +## 0.4.3 + +### Patch Changes + +- 19842b6: Extend public API of the core package. + +## 0.4.2 + +### Patch Changes + +- e5c6e08: Update all dependencies and refactor search components + +## 0.4.1 + +### Patch Changes + +- e811ddc: Create mermaid components and update dependencies + +## 0.4.0 + +### Minor Changes + +- 3588a8e: Fix: broken theme switch (use Themeprovider from @flowershow/core) + +## 0.3.2 + +### Patch Changes + +- e83bc87: Remove support for nav dropdown + +## 0.3.1 + +### Patch Changes + +- 0520975: Fix: remove twitter URLs displayed after embeds + +## 0.3.0 + +### Minor Changes + +- ffd2b6c: Feature: Twitter urls turn into embeds + +### Patch Changes + +- 124e24e: Site-wide ToC: grouping + +## 0.2.1 + +### Patch Changes + +- e629afc: Fix: add missing dependency for disqus-react + +## 0.2.0 + +### Minor Changes + +- 0ff6c06: Add: Page comments feature with support for three providers - giscus, utterances and disqus. + +## 0.1.0 + +### Minor Changes + +- 411995f: Feature: sidebar with site-wide table of contents. + +## 0.0.11 + +### Patch Changes + +- 1a2fd9f: Fix: ToC not showing up on some pages, and not rendering all the headings. + +## 0.0.10 + +### Patch Changes + +- Fix: `SearchProvider` return type. + +## 0.0.9 + +### Patch Changes + +- d8c918c: Fix import for `useDocSearchKeyboardEvents` in Algolia search component. +- e12b558: Fix: prop types in CustomLink + +## 0.0.8 + +### Patch Changes + +- 3a005dc: Fix: remove reference to "document" from Tooltip + +## 0.0.7 + +### Patch Changes + +- Revert: 1cf9bdf + +## 0.0.6 + +### Patch Changes + +- 51a1b31: Fix: make Avatar keys unique +- 1cf9bdf: Fix: import of `useDocSearchKeyboardEvents` from `@docsearch/react`; + +## 0.0.5 + +### Patch Changes + +- bd4c5b0: Fix: add missing file extensions to import statements. diff --git a/packages/core/README.md b/packages/core/README.md new file mode 100644 index 00000000..e372cb42 --- /dev/null +++ b/packages/core/README.md @@ -0,0 +1,3 @@ +# @flowershow/core + +Core Flowershow package containing components, styles, utils etc. used by Flowershow templates. diff --git a/packages/core/jest.config.ts b/packages/core/jest.config.ts new file mode 100644 index 00000000..598bf9ca --- /dev/null +++ b/packages/core/jest.config.ts @@ -0,0 +1,10 @@ +/* eslint-disable */ +export default { + displayName: "core", + preset: "../../jest.preset.js", + transform: { + "^.+\\.[tj]sx?$": "babel-jest", + }, + moduleFileExtensions: ["ts", "tsx", "js", "jsx"], + coverageDirectory: "../../coverage/packages/core", +}; diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 00000000..f0e1488f --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,41 @@ +{ + "name": "@flowershow/core", + "version": "0.4.13", + "description": "Core Flowershow components, configs and utils.", + "repository": { + "type": "git", + "url": "git+https://github.com/flowershow/flowershow.git", + "directory": "packages/core" + }, + "author": "Rufus Pollock", + "license": "MIT", + "bugs": { + "url": "https://github.com/flowershow/flowershow/issues" + }, + "homepage": "https://github.com/flowershow/flowershow#readme", + "publishConfig": { + "access": "public" + }, + "type": "module", + "main": "./dist/index.js", + "dependencies": { + "@docsearch/react": "^3.3.3", + "@floating-ui/react-dom": "^1.3.0", + "@floating-ui/react-dom-interactions": "^0.13.3", + "@giscus/react": "^2.2.6", + "@headlessui/react": "^1.7.12", + "clsx": "^1.2.1", + "disqus-react": "^1.1.5", + "framer-motion": "^10.0.1", + "kbar": "0.1.0-beta.40", + "mdx-mermaid": "2.0.0-rc7", + "mermaid": "10.0.1-rc.2", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "next": "^13.2.1", + "next-themes": "^0.2.1", + "react": "^18.2.0", + "react-dom": "^18.2.0" + } +} diff --git a/packages/core/project.json b/packages/core/project.json new file mode 100644 index 00000000..2fb41ea6 --- /dev/null +++ b/packages/core/project.json @@ -0,0 +1,73 @@ +{ + "name": "core", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/core/src", + "projectType": "library", + "tags": [], + "targets": { + "build": { + "executor": "@nrwl/web:rollup", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "packages/core/dist", + "tsConfig": "packages/core/tsconfig.lib.json", + "project": "packages/core/package.json", + "entryFile": "packages/core/src/index.ts", + "format": ["esm"], + "external": [ + "react/jsx-runtime", + "react", + "next/link", + "next/head", + "next/router", + "next/dynamic", + "next-contentlayer", + "next-themes" + ], + "generateExportsField": true, + "rollupConfig": "@nrwl/react/plugins/bundle-rollup", + "compiler": "babel", + "assets": [ + { + "glob": "packages/core/README.md", + "input": ".", + "output": "." + } + ] + } + }, + "lint": { + "executor": "@nrwl/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["packages/core/**/*.{ts,tsx,js,jsx}"] + } + }, + "test": { + "executor": "@nrwl/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "packages/core/jest.config.ts", + "passWithNoTests": true + } + }, + "publish:dry": { + "executor": "nx:run-commands", + "options": { + "commands": ["npm publish --dry-run"], + "parallel": false, + "cwd": "packages/core/dist" + }, + "dependsOn": ["build"] + }, + "publish": { + "executor": "nx:run-commands", + "options": { + "commands": ["npm publish"], + "parallel": false, + "cwd": "packages/core/dist" + }, + "dependsOn": ["build"] + } + } +} diff --git a/packages/core/src/config/default.ts b/packages/core/src/config/default.ts new file mode 100644 index 00000000..cb25e13c --- /dev/null +++ b/packages/core/src/config/default.ts @@ -0,0 +1,31 @@ +export const defaultConfig = { + title: "Flowershow", + description: "", + showEditLink: false, + showToc: true, + showSidebar: false, + showLinkPreviews: true, + author: "", + authorLogo: "", + domain: "", + // Google analytics key e.g. G-XXXX + analytics: "", + // content source directory for markdown files + // DO NOT CHANGE THIS VALUE + // if you have your notes in another (external) directory, + // /content dir should be a symlink to that directory + content: "content", + avatarPlaceholder: "/_flowershow/avatarplaceholder.png", + contentExclude: [], + contentInclude: [], + blogDir: "blog", + peopleDir: "people", + // Theme + theme: { + default: "dark", + toggleIcon: "/_flowershow/theme-button.svg", + }, + navLinks: [ + // { href: '/about', name: 'About' }, + ], +}; diff --git a/packages/core/src/config/index.ts b/packages/core/src/config/index.ts new file mode 100644 index 00000000..9f738efd --- /dev/null +++ b/packages/core/src/config/index.ts @@ -0,0 +1 @@ +export { defaultConfig } from "./default"; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 00000000..67df2365 --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,3 @@ +export * from "./ui"; +export * from "./utils"; +export * from "./config"; diff --git a/packages/core/src/types/index.d.ts b/packages/core/src/types/index.d.ts new file mode 100644 index 00000000..6dc68fca --- /dev/null +++ b/packages/core/src/types/index.d.ts @@ -0,0 +1,7 @@ +export {}; + +declare global { + interface Window { + gtag: any; // TODO + } +} diff --git a/packages/core/src/ui/Avatar/Avatar.tsx b/packages/core/src/ui/Avatar/Avatar.tsx new file mode 100644 index 00000000..247ed0df --- /dev/null +++ b/packages/core/src/ui/Avatar/Avatar.tsx @@ -0,0 +1,24 @@ +// TODO +type Props = any; + +export const Avatar: React.FC = ({ name, img, href }) => { + const Component = href ? "a" : "div"; + return ( + +
+
+ {name} +
+
+

+ {name} +

+
+
+
+ ); +}; diff --git a/packages/core/src/ui/Avatar/index.ts b/packages/core/src/ui/Avatar/index.ts new file mode 100644 index 00000000..4e1e5d99 --- /dev/null +++ b/packages/core/src/ui/Avatar/index.ts @@ -0,0 +1 @@ +export { Avatar } from "./Avatar"; diff --git a/packages/core/src/ui/Base/BaseLink.tsx b/packages/core/src/ui/Base/BaseLink.tsx new file mode 100644 index 00000000..a06c5af2 --- /dev/null +++ b/packages/core/src/ui/Base/BaseLink.tsx @@ -0,0 +1,15 @@ +import Link from "next/link.js"; +import { forwardRef } from "react"; + +const BaseLink = forwardRef((props: any, ref) => { + const { href, children, ...rest } = props; + return ( + + {children} + + ); +}); + +BaseLink.displayName = "BaseLink"; + +export { BaseLink }; diff --git a/packages/core/src/ui/Base/CustomLink.tsx b/packages/core/src/ui/Base/CustomLink.tsx new file mode 100644 index 00000000..84b77e42 --- /dev/null +++ b/packages/core/src/ui/Base/CustomLink.tsx @@ -0,0 +1,57 @@ +import Link from "next/link.js"; +import { Tooltip } from "../Tooltip"; +import TwitterEmbed from "./TwitterEmbed"; + +// TODO it's a mess, move twitter embeds support to remark-embed +const TWITTER_REGEX = + /^https?:\/\/twitter\.com\/(?:#!\/)?(\w+)\/status(es)?\/(\d+)/; + +interface Props { + href: string; + data: any; + usehook: any; + preview: boolean; + children: React.ReactNode; + className?: string; + [x: string]: unknown; +} + +export const CustomLink: React.FC = ({ + data, + usehook, + preview, + ...props +}) => { + const { href } = props; + const isInternalLink = !href.startsWith("http"); + // eslint-disable-next-line no-useless-escape + const isHeadingLink = href.startsWith("#"); + const isTwitterLink = TWITTER_REGEX.test(href); + + // Use next link for pages within app and for external links. + // https://nextjs.org/learn/basics/navigate-between-pages/client-side + if (isInternalLink) { + if (preview && !isHeadingLink) { + return ( + } + /> + ); + } else { + return ; + } + } + + if (isTwitterLink) { + return ; + } + + return ( + + {props.children} + + ); +}; diff --git a/packages/core/src/ui/Base/ThemeSelector.tsx b/packages/core/src/ui/Base/ThemeSelector.tsx new file mode 100644 index 00000000..a0e1dc31 --- /dev/null +++ b/packages/core/src/ui/Base/ThemeSelector.tsx @@ -0,0 +1,44 @@ +import { useTheme } from "next-themes"; +import { useEffect, useState } from "react"; + +interface Props { + defaultTheme: "dark" | "light"; + toggleIcon: string; +} + +export const ThemeSelector: React.FC = ({ + defaultTheme, + toggleIcon, +}) => { + const [mounted, setMounted] = useState(false); + const { theme, setTheme } = useTheme(); + + useEffect(() => setMounted(true), []); + + /** Avoid Hydration Mismatch + * https://github.com/pacocoursey/next-themes#avoid-hydration-mismatch + */ + if (!mounted) return null; + + // TODO why? + if (!defaultTheme) return null; + + return ( + + ); +}; diff --git a/packages/core/src/ui/Base/TwitterEmbed.tsx b/packages/core/src/ui/Base/TwitterEmbed.tsx new file mode 100644 index 00000000..ed5a10a0 --- /dev/null +++ b/packages/core/src/ui/Base/TwitterEmbed.tsx @@ -0,0 +1,105 @@ +// TODO dark and light theme + +import { useEffect, useState, useRef, RefObject } from "react"; + +const twitterWidgetJs = "https://platform.twitter.com/widgets.js"; +enum TweetState { + LOADING, + LOADED, + FAILED, +} + +interface TweetConfig { + theme: string; +} + +declare global { + interface Window { + twttr: { + widgets: { + createTweet: ( + id: string, + ref: RefObject, + options: TweetConfig + ) => Promise; // TODO type + load: (ref: RefObject) => void; + }; + }; + } +} + +export default function TwitterEmbed({ url, ...props }) { + const ref = useRef(null); + const [tweetState, setTweetState] = useState(TweetState.LOADING); + + const tweetId = url.split("status/").pop(); + + useEffect(() => { + const renderTweet = () => { + window.twttr.widgets + .createTweet(tweetId, ref.current as any, { + theme: "dark", + }) + .then((el) => { + if (el) { + setTweetState(TweetState.LOADED); + } else { + setTweetState(TweetState.FAILED); + } + }); + return window.twttr.widgets.load(ref.current as any); + }; + + if (!window.twttr) { + const script = document.createElement("script"); + script.src = twitterWidgetJs; + script.async = true; + script.onload = () => renderTweet(); + document.head.appendChild(script); + } else { + renderTweet(); + } + }, [tweetId]); + + return ( + <> + {tweetState === TweetState.LOADING && ( +
+
+ + Twitter + + +
+ {"Loading tweet..."} +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )} +
+ + ); +} diff --git a/packages/core/src/ui/Base/index.ts b/packages/core/src/ui/Base/index.ts new file mode 100644 index 00000000..eb887dab --- /dev/null +++ b/packages/core/src/ui/Base/index.ts @@ -0,0 +1,3 @@ +export { BaseLink } from "./BaseLink"; +export { ThemeSelector } from "./ThemeSelector"; +export { CustomLink } from "./CustomLink"; diff --git a/packages/core/src/ui/Blog/Avatar.jsx b/packages/core/src/ui/Blog/Avatar.jsx new file mode 100644 index 00000000..6d3cf4cf --- /dev/null +++ b/packages/core/src/ui/Blog/Avatar.jsx @@ -0,0 +1,21 @@ +export function Avatar({ name, img, href }) { + const Component = href ? "a" : "div"; + return ( + +
+
+ {name} +
+
+

+ {name} +

+
+
+
+ ); +} diff --git a/packages/core/src/ui/Blog/BlogItem.tsx b/packages/core/src/ui/Blog/BlogItem.tsx new file mode 100644 index 00000000..a68dfaa2 --- /dev/null +++ b/packages/core/src/ui/Blog/BlogItem.tsx @@ -0,0 +1,40 @@ +import { Card } from "../Card"; +import { formatDate } from "../../utils/formatDate"; +import { Blog } from "../types"; + +interface Props { + blog: Blog; +} + +export const BlogItem: React.FC = ({ blog }) => { + return ( +
+ + + {blog.title} + + + {formatDate(blog.date)} + + {blog.description && ( + + {blog.description} + + )} + Read article + + + {formatDate(blog.date)} + +
+ ); +}; diff --git a/packages/core/src/ui/Blog/BlogsList.tsx b/packages/core/src/ui/Blog/BlogsList.tsx new file mode 100644 index 00000000..c997463c --- /dev/null +++ b/packages/core/src/ui/Blog/BlogsList.tsx @@ -0,0 +1,36 @@ +import { useState } from "react"; +import { BlogItem } from "./BlogItem"; + +const BLOGS_LOAD_COUNT = 10; + +// TODO types +export const BlogsList: React.FC = ({ blogs }) => { + const [blogsCount, setBlogsCount] = useState(BLOGS_LOAD_COUNT); + + const handleLoadMore = () => { + setBlogsCount((prevCount) => prevCount + BLOGS_LOAD_COUNT); + }; + + return ( + <> +
+
+ {blogs.slice(0, blogsCount).map((blog) => { + return ; + })} +
+
+ {blogs.length > blogsCount && ( +
+ +
+ )} + + ); +}; diff --git a/packages/core/src/ui/Blog/index.ts b/packages/core/src/ui/Blog/index.ts new file mode 100644 index 00000000..53165b48 --- /dev/null +++ b/packages/core/src/ui/Blog/index.ts @@ -0,0 +1 @@ +export { BlogsList } from "./BlogsList"; diff --git a/packages/core/src/ui/BlogLayout/BlogLayout.tsx b/packages/core/src/ui/BlogLayout/BlogLayout.tsx new file mode 100644 index 00000000..56f05f7c --- /dev/null +++ b/packages/core/src/ui/BlogLayout/BlogLayout.tsx @@ -0,0 +1,38 @@ +/* eslint import/no-default-export: off */ +import { formatDate } from "../../utils/formatDate"; +import { Avatar } from "../Avatar"; + +// TODO +type Props = any; + +export const BlogLayout: React.FC = ({ children, ...frontMatter }) => { + const { title, date, authors } = frontMatter; + + return ( +
+
+
+ {title &&

{title}

} + {date && ( +

+ +

+ )} + {authors && ( +
+ {authors.map(({ name, avatar, urlPath }) => ( + + ))} +
+ )} +
+
+
{children}
+
+ ); +}; diff --git a/packages/core/src/ui/BlogLayout/index.ts b/packages/core/src/ui/BlogLayout/index.ts new file mode 100644 index 00000000..86541865 --- /dev/null +++ b/packages/core/src/ui/BlogLayout/index.ts @@ -0,0 +1 @@ +export { BlogLayout } from "./BlogLayout"; diff --git a/packages/core/src/ui/Card/Card.tsx b/packages/core/src/ui/Card/Card.tsx new file mode 100644 index 00000000..f10b2450 --- /dev/null +++ b/packages/core/src/ui/Card/Card.tsx @@ -0,0 +1,170 @@ +// import Link from 'next/link' +import clsx from "clsx"; +import { ChevronRightIcon } from "../Icons"; + +interface CardProps extends React.PropsWithChildren { + as?: React.ElementType; + className?: string; +} + +interface CardLinkProps extends React.PropsWithChildren { + href?: string; + className?: string; +} + +interface CardTitleProps extends React.PropsWithChildren { + as?: React.ElementType; + href?: string; + className?: string; +} + +interface CardDescriptionProps extends React.PropsWithChildren { + className?: string; +} + +interface CardCtaProps extends React.PropsWithChildren { + className?: string; +} + +interface CardEyebrowProps extends React.PropsWithChildren { + as?: React.ElementType; + decorate?: boolean; + className?: string; + [x: string]: unknown; +} + +type Card = React.FC & { Link: React.FC } & { + Title: React.FC; +} & { Description: React.FC } & { + Cta: React.FC; +} & { Eyebrow: React.FC }; + +export const Card: Card = ({ children, as: Component = "div", className }) => { + return ( + + {children} + + ); +}; + +Card.Link = function CardLink({ children, href, className, ...props }) { + // + // + // {children} + // + return ( + <> +
+ + + {children} + + + ); +}; + +Card.Title = function CardTitle({ + as: Component = "h2", + href, + children, + className, +}) { + return ( + + {href ? {children} : children} + + ); +}; + +Card.Description = function CardDescription({ children, className }) { + return ( +

+ {children} +

+ ); +}; + +Card.Cta = function CardCta({ children, className }) { + return ( + + ); +}; + +/* Card.Avatar = function CardAvatar({ name, src, href }) { + * return ( + * + *
+ *
+ * {src ? ( + * {name} + * ) : ( + * + * + * {initialsFromName(name)} + * + * + * )} + *
+ *
+ *

+ * {name} + *

+ *
+ *
+ *
+ * ); + * }; */ + +Card.Eyebrow = function CardEyebrow({ + as: Component = "p", + decorate = false, + className, + children, + ...props +}) { + return ( + + {decorate && ( + + ); +}; diff --git a/packages/core/src/ui/Card/index.ts b/packages/core/src/ui/Card/index.ts new file mode 100644 index 00000000..fbaf43c2 --- /dev/null +++ b/packages/core/src/ui/Card/index.ts @@ -0,0 +1 @@ +export { Card } from "./Card"; diff --git a/packages/core/src/ui/Comments/Disqus.tsx b/packages/core/src/ui/Comments/Disqus.tsx new file mode 100644 index 00000000..1c66c808 --- /dev/null +++ b/packages/core/src/ui/Comments/Disqus.tsx @@ -0,0 +1,25 @@ +import { DiscussionEmbed } from "disqus-react"; + +export interface DisqusConfig { + provider: "disqus"; + pages?: Array; + config: { + shortname: string; + }; +} + +export type DisqusProps = DisqusConfig["config"] & { + slug?: string; +}; + +export const Disqus: React.FC = ({ shortname, slug }) => { + return ( + + ); +}; diff --git a/packages/core/src/ui/Comments/Giscus.tsx b/packages/core/src/ui/Comments/Giscus.tsx new file mode 100644 index 00000000..9011a0cc --- /dev/null +++ b/packages/core/src/ui/Comments/Giscus.tsx @@ -0,0 +1,53 @@ +import Giscus, { BooleanString, Mapping, Repo } from "@giscus/react"; +import { useTheme } from "next-themes"; + +export interface GiscusConfig { + provider: "giscus"; + pages?: Array; + config: { + theme?: string; + mapping: Mapping; + repo: Repo; + repositoryId: string; + category: string; + categoryId: string; + reactions: BooleanString; + metadata: BooleanString; + inputPosition?: string; + lang?: string; + }; +} + +export type GiscusProps = GiscusConfig["config"]; + +export const GiscusReactComponent: React.FC = ({ + repo, + repositoryId, + category, + categoryId, + reactions = "0", + metadata = "0", + mapping = "pathname", + theme = "light", +}) => { + const { theme: nextTheme, resolvedTheme } = useTheme(); + const commentsTheme = + nextTheme === "dark" || resolvedTheme === "dark" + ? "transparent_dark" + : theme; + + return ( + + ); +}; diff --git a/packages/core/src/ui/Comments/Utterances.tsx b/packages/core/src/ui/Comments/Utterances.tsx new file mode 100644 index 00000000..780ca7bd --- /dev/null +++ b/packages/core/src/ui/Comments/Utterances.tsx @@ -0,0 +1,59 @@ +import { useEffect, useCallback } from "react"; +import { useTheme } from "next-themes"; + +export interface UtterancesConfig { + provider: "utterances"; + pages?: Array; + config: { + theme?: string; + repo: string; + label: string; + issueTerm: string; + }; +} + +export type UtterancesProps = UtterancesConfig["config"]; + +export const Utterances: React.FC = ({ + repo, + label = "comments", + issueTerm = "pathname", + theme = "github-light", +}) => { + const { theme: nextTheme, resolvedTheme } = useTheme(); + // TODO: remove preferred-color-scheme after theme toggle fix + const commentsTheme = nextTheme + ? nextTheme === "dark" || resolvedTheme === "dark" + ? "github-dark" + : theme + : "preferred-color-scheme"; + + const COMMENTS_ID = "comments-container"; + + const LoadComments = useCallback(() => { + const script = document.createElement("script"); + script.src = "https://utteranc.es/client.js"; + script.setAttribute("repo", repo); + script.setAttribute("issue-term", issueTerm); + script.setAttribute("label", label); + script.setAttribute("theme", commentsTheme); + script.setAttribute("crossorigin", "anonymous"); + script.async = true; + + const comments = document.getElementById(COMMENTS_ID); + if (comments) comments.appendChild(script); + + return () => { + const comments = document.getElementById(COMMENTS_ID); + if (comments) comments.innerHTML = ""; + }; + }, [commentsTheme, issueTerm]); + + // Reload on theme change + useEffect(() => { + LoadComments(); + }, [LoadComments]); + + // Added `relative` to fix a weird bug with `utterances-frame` position + return
; +}; diff --git a/packages/core/src/ui/Comments/index.tsx b/packages/core/src/ui/Comments/index.tsx new file mode 100644 index 00000000..31750bc9 --- /dev/null +++ b/packages/core/src/ui/Comments/index.tsx @@ -0,0 +1,53 @@ +import dynamic from "next/dynamic.js"; +import { GiscusReactComponent, GiscusConfig, GiscusProps } from "./Giscus"; +import { Utterances, UtterancesConfig, UtterancesProps } from "./Utterances"; +import { Disqus, DisqusConfig, DisqusProps } from "./Disqus"; + +export type CommentsConfig = GiscusConfig | UtterancesConfig | DisqusConfig; + +export interface CommentsProps { + commentsConfig: CommentsConfig; + slug?: string; +} + +const GiscusComponent = dynamic( + () => { + return import("./Giscus").then((mod) => mod.GiscusReactComponent); + }, + { ssr: false } +); + +const UtterancesComponent = dynamic( + () => { + return import("./Utterances").then((mod) => mod.Utterances); + }, + { ssr: false } +); + +const DisqusComponent = dynamic( + () => { + return import("./Disqus").then((mod) => mod.Disqus); + }, + { ssr: false } +); + +export const Comments = ({ commentsConfig, slug }: CommentsProps) => { + switch (commentsConfig.provider) { + case "giscus": + return ; + case "utterances": + return ; + case "disqus": + return ; + } +}; + +export { GiscusReactComponent, Utterances, Disqus }; +export type { + GiscusConfig, + GiscusProps, + UtterancesConfig, + UtterancesProps, + DisqusConfig, + DisqusProps, +}; diff --git a/packages/core/src/ui/DocsLayout/Docs.tsx b/packages/core/src/ui/DocsLayout/Docs.tsx new file mode 100644 index 00000000..49fdc668 --- /dev/null +++ b/packages/core/src/ui/DocsLayout/Docs.tsx @@ -0,0 +1,22 @@ +/* eslint import/no-default-export: off */ +import { formatDate } from "../../utils/formatDate"; + +// TODO types +export const DocsLayout: React.FC = ({ children, ...frontMatter }) => { + const { title, created } = frontMatter; + return ( +
+
+
+ {created && ( +

+ +

+ )} + {title &&

{title}

} +
+
+
{children}
+
+ ); +}; diff --git a/packages/core/src/ui/DocsLayout/index.ts b/packages/core/src/ui/DocsLayout/index.ts new file mode 100644 index 00000000..e977f0fd --- /dev/null +++ b/packages/core/src/ui/DocsLayout/index.ts @@ -0,0 +1 @@ +export { DocsLayout } from "./Docs"; diff --git a/packages/core/src/ui/Icons/ChevronRightIcon.tsx b/packages/core/src/ui/Icons/ChevronRightIcon.tsx new file mode 100644 index 00000000..87b6bcbb --- /dev/null +++ b/packages/core/src/ui/Icons/ChevronRightIcon.tsx @@ -0,0 +1,12 @@ +export const ChevronRightIcon: React.FC<{ [x: string]: unknown }> = (props) => { + return ( + + ); +}; diff --git a/packages/core/src/ui/Icons/CloseIcon.tsx b/packages/core/src/ui/Icons/CloseIcon.tsx new file mode 100644 index 00000000..a18907f3 --- /dev/null +++ b/packages/core/src/ui/Icons/CloseIcon.tsx @@ -0,0 +1,14 @@ +export const CloseIcon: React.FC<{ [x: string]: unknown }> = (props) => { + return ( + + ); +}; diff --git a/packages/core/src/ui/Icons/DiscordIcon.tsx b/packages/core/src/ui/Icons/DiscordIcon.tsx new file mode 100644 index 00000000..e340904a --- /dev/null +++ b/packages/core/src/ui/Icons/DiscordIcon.tsx @@ -0,0 +1,14 @@ +export const DiscordIcon: React.FC<{ [x: string]: unknown }> = (props) => { + return ( + + + + ); +}; diff --git a/packages/core/src/ui/Icons/GitHubIcon.tsx b/packages/core/src/ui/Icons/GitHubIcon.tsx new file mode 100644 index 00000000..7f5696df --- /dev/null +++ b/packages/core/src/ui/Icons/GitHubIcon.tsx @@ -0,0 +1,7 @@ +export const GitHubIcon: React.FC<{ [x: string]: unknown }> = (props) => { + return ( + + ); +}; diff --git a/packages/core/src/ui/Icons/MenuIcon.tsx b/packages/core/src/ui/Icons/MenuIcon.tsx new file mode 100644 index 00000000..e83445fa --- /dev/null +++ b/packages/core/src/ui/Icons/MenuIcon.tsx @@ -0,0 +1,14 @@ +export const MenuIcon: React.FC<{ [x: string]: unknown }> = (props) => { + return ( + + ); +}; diff --git a/packages/core/src/ui/Icons/SearchIcon.tsx b/packages/core/src/ui/Icons/SearchIcon.tsx new file mode 100644 index 00000000..c9315c4e --- /dev/null +++ b/packages/core/src/ui/Icons/SearchIcon.tsx @@ -0,0 +1,7 @@ +export const SearchIcon: React.FC<{ [x: string]: unknown }> = (props) => { + return ( + + ); +}; diff --git a/packages/core/src/ui/Icons/index.tsx b/packages/core/src/ui/Icons/index.tsx new file mode 100644 index 00000000..f31611df --- /dev/null +++ b/packages/core/src/ui/Icons/index.tsx @@ -0,0 +1,6 @@ +export { GitHubIcon } from "./GitHubIcon"; +export { DiscordIcon } from "./DiscordIcon"; +export { MenuIcon } from "./MenuIcon"; +export { CloseIcon } from "./CloseIcon"; +export { SearchIcon } from "./SearchIcon"; +export { ChevronRightIcon } from "./ChevronRightIcon"; diff --git a/packages/core/src/ui/Layout/EditThisPage.tsx b/packages/core/src/ui/Layout/EditThisPage.tsx new file mode 100644 index 00000000..40648864 --- /dev/null +++ b/packages/core/src/ui/Layout/EditThisPage.tsx @@ -0,0 +1,30 @@ +export const EditThisPage = ({ url }: { url: string }) => { + return ( + + ); +}; diff --git a/packages/core/src/ui/Layout/Footer.tsx b/packages/core/src/ui/Layout/Footer.tsx new file mode 100644 index 00000000..f3348b1b --- /dev/null +++ b/packages/core/src/ui/Layout/Footer.tsx @@ -0,0 +1,61 @@ +import Link from "next/link.js"; + +import { AuthorConfig, NavLink } from "../types"; + +interface Props { + links: Array; + author: AuthorConfig; +} + +export const Footer: React.FC = ({ links, author }) => { + return ( + + ); +}; diff --git a/packages/core/src/ui/Layout/Layout.tsx b/packages/core/src/ui/Layout/Layout.tsx new file mode 100644 index 00000000..e1e2f909 --- /dev/null +++ b/packages/core/src/ui/Layout/Layout.tsx @@ -0,0 +1,147 @@ +import { useEffect, useState } from "react"; +import Head from "next/head.js"; +import { NextRouter, useRouter } from "next/router.js"; +import clsx from "clsx"; + +import { useTableOfContents } from "./useTableOfContents"; +import { collectHeadings } from "../../utils"; + +import { Nav } from "../Nav"; +import { SiteToc, NavItem, NavGroup } from "../SiteToc"; +import { Comments, CommentsConfig } from "../Comments"; +import { Footer } from "./Footer"; +import { EditThisPage } from "./EditThisPage"; +import { TableOfContents, TocSection } from "./TableOfContents"; +import { NavConfig, ThemeConfig } from "../Nav"; +import { AuthorConfig } from "../types"; + +interface Props extends React.PropsWithChildren { + showComments: boolean; + showEditLink: boolean; + showSidebar: boolean; + showToc: boolean; + nav: NavConfig; + author: AuthorConfig; + theme: ThemeConfig; + urlPath: string; + commentsConfig: CommentsConfig; + siteMap: Array; + editUrl?: string; +} + +export const Layout: React.FC = ({ + children, + nav, + author, + theme, + showEditLink, + showToc, + showSidebar, + urlPath, + showComments, + commentsConfig, + editUrl, + siteMap, +}) => { + const [isScrolled, setIsScrolled] = useState(false); + const [tableOfContents, setTableOfContents] = useState([]); + const currentSection = useTableOfContents(tableOfContents); + const router: NextRouter = useRouter(); + + useEffect(() => { + if (!showToc) return; + const headingNodes: NodeListOf = + document.querySelectorAll("h1,h2,h3"); + const toc = collectHeadings(headingNodes); + setTableOfContents(toc ?? []); + }, [router.asPath, showToc]); // update table of contents on route change with next/link + + useEffect(() => { + function onScroll() { + setIsScrolled(window.scrollY > 0); + } + onScroll(); + window.addEventListener("scroll", onScroll, { passive: true }); + return () => { + window.removeEventListener("scroll", onScroll); + }; + }, []); + + return ( + <> + + + + + +
+ {/* NAVBAR */} +
+
+ +
+
+ {/* wrapper for sidebar, main content and ToC */} +
+ {/* SIDEBAR */} + {showSidebar && ( +
+ +
+ )} + {/* MAIN CONTENT & FOOTER */} +
+ {children} + {/* EDIT THIS PAGE LINK */} + {showEditLink && editUrl && } + {/* PAGE COMMENTS */} + {showComments && ( +
+ {} +
+ )} +
+
+ {/** TABLE OF CONTENTS */} + {showToc && tableOfContents.length > 0 && ( +
+ +
+ )} +
+
+ + ); +}; diff --git a/packages/core/src/ui/Layout/TableOfContents.tsx b/packages/core/src/ui/Layout/TableOfContents.tsx new file mode 100644 index 00000000..1fa9e067 --- /dev/null +++ b/packages/core/src/ui/Layout/TableOfContents.tsx @@ -0,0 +1,90 @@ +import Link from "next/link.js"; + +export interface TocSection { + id: string; + title: string; + level: string; + children?: any; +} + +interface Props { + tableOfContents: TocSection[]; + currentSection: string; +} + +export const TableOfContents: React.FC = ({ + tableOfContents, + currentSection, +}) => { + function isActiveSection(section) { + if (section.id === currentSection) { + return true; + } + if (!section.children) { + return false; + } + return section.children.findIndex(isActiveSection) > -1; + } + + return ( + + ); +}; diff --git a/packages/core/src/ui/Layout/index.ts b/packages/core/src/ui/Layout/index.ts new file mode 100644 index 00000000..fb13e952 --- /dev/null +++ b/packages/core/src/ui/Layout/index.ts @@ -0,0 +1,5 @@ +export { EditThisPage } from "./EditThisPage"; +export { Layout } from "./Layout"; +export { useTableOfContents } from "./useTableOfContents"; +export { TableOfContents, TocSection } from "./TableOfContents"; +export { Footer } from "./Footer"; diff --git a/packages/core/src/ui/Layout/useTableOfContents.ts b/packages/core/src/ui/Layout/useTableOfContents.ts new file mode 100644 index 00000000..c3131fab --- /dev/null +++ b/packages/core/src/ui/Layout/useTableOfContents.ts @@ -0,0 +1,51 @@ +import { useCallback, useEffect, useState } from "react"; + +// TODO types +export const useTableOfContents = (tableOfContents) => { + const [currentSection, setCurrentSection] = useState(tableOfContents[0]?.id); + + const getHeadings = useCallback((toc) => { + return toc + .flatMap((node) => [ + node.id, + ...node.children.flatMap((child) => [ + child.id, + ...child.children.map((subChild) => subChild.id), + ]), + ]) + .map((id) => { + const el = document.getElementById(id); + if (!el) return null; + + const style = window.getComputedStyle(el); + const scrollMt = parseFloat(style.scrollMarginTop); + + const top = window.scrollY + el.getBoundingClientRect().top - scrollMt; + return { id, top }; + }) + .filter((el) => !!el); + }, []); + + useEffect(() => { + if (tableOfContents.length === 0) return; + const headings = getHeadings(tableOfContents); + function onScroll() { + const top = window.scrollY + 4.5; + let current = headings[0].id; + headings.forEach((heading) => { + if (top >= heading.top) { + current = heading.id; + } + return current; + }); + setCurrentSection(current); + } + window.addEventListener("scroll", onScroll, { passive: true }); + onScroll(); + return () => { + window.removeEventListener("scroll", onScroll); + }; + }, [getHeadings, tableOfContents]); + + return currentSection; +}; diff --git a/packages/core/src/ui/Mermaid/Mermaid.tsx b/packages/core/src/ui/Mermaid/Mermaid.tsx new file mode 100644 index 00000000..f425ee7d --- /dev/null +++ b/packages/core/src/ui/Mermaid/Mermaid.tsx @@ -0,0 +1,25 @@ +import dynamic from "next/dynamic.js"; +// import { useTheme } from "next-themes"; + +import type { MermaidProps } from "mdx-mermaid/lib/Mermaid"; +// import type { Config } from "mdx-mermaid/lib/config.model"; + +const MdxMermaid = dynamic( + () => import("mdx-mermaid/lib/Mermaid").then((res) => res.Mermaid), + { ssr: false } +); + +export const Mermaid: React.FC = ({ ...props }) => { + // TODO: add light and dark theme configs + // currently Mermaid component doesn't render if configs are passed as props. + + // const { theme } = useTheme() + // const config: Config = { + // mermaid: { + // fontFamily: "inherit", + // theme: theme + // } + // } + + return ; +}; diff --git a/packages/core/src/ui/Mermaid/index.ts b/packages/core/src/ui/Mermaid/index.ts new file mode 100644 index 00000000..96684415 --- /dev/null +++ b/packages/core/src/ui/Mermaid/index.ts @@ -0,0 +1 @@ +export { Mermaid } from "./Mermaid"; diff --git a/packages/core/src/ui/Nav/Nav.tsx b/packages/core/src/ui/Nav/Nav.tsx new file mode 100644 index 00000000..72394c1d --- /dev/null +++ b/packages/core/src/ui/Nav/Nav.tsx @@ -0,0 +1,86 @@ +import { useEffect, useState } from "react"; + +import { ThemeSelector } from "../Base"; +import { SearchContext, SearchField } from "../Search"; +import { NavMobile } from "./NavMobile"; +import { NavItem } from "./NavItem"; +import { NavTitle } from "./NavTitle"; +import { NavSocial } from "./NavSocial"; +import { NavLink, SocialLink, SearchProviderConfig } from "../types"; + +export interface ThemeConfig { + defaultTheme: "dark" | "light"; + themeToggleIcon: string; +} + +export interface NavConfig { + title: string; + logo?: string; + version?: string; + links: Array; + search?: SearchProviderConfig; + social?: Array; +} + +interface Props extends NavConfig, ThemeConfig, React.PropsWithChildren {} + +export const Nav: React.FC = ({ + children, + title, + logo, + version, + links, + search, + social, + defaultTheme, + themeToggleIcon, +}) => { + const [modifierKey, setModifierKey] = useState(); + const [Search, setSearch] = useState(); // TODO types + + useEffect(() => { + const isMac = /(Mac|iPhone|iPod|iPad)/i.test(navigator.userAgent); + setModifierKey(isMac ? "⌘" : "Ctrl "); + }, []); + + useEffect(() => { + if (search) { + setSearch(SearchContext(search.provider)); + } + }, [search]); + + return ( + + ); +}; diff --git a/packages/core/src/ui/Nav/NavItem.tsx b/packages/core/src/ui/Nav/NavItem.tsx new file mode 100644 index 00000000..6ad05319 --- /dev/null +++ b/packages/core/src/ui/Nav/NavItem.tsx @@ -0,0 +1,21 @@ +import { Menu } from "@headlessui/react"; +import Link from "next/link.js"; +import { BaseLink } from "../Base"; +import { NavLink } from "../types"; + +interface Props { + link: NavLink; +} + +export const NavItem: React.FC = ({ link }) => { + return ( + + + {link.name} + + + ); +}; diff --git a/packages/core/src/ui/Nav/NavMobile.tsx b/packages/core/src/ui/Nav/NavMobile.tsx new file mode 100644 index 00000000..a975c073 --- /dev/null +++ b/packages/core/src/ui/Nav/NavMobile.tsx @@ -0,0 +1,116 @@ +import { Dialog, Menu } from "@headlessui/react"; +import Link from "next/link.js"; +import { useRouter } from "next/router.js"; +import { useEffect, useState } from "react"; +import { SearchContext, SearchField } from "../Search"; +import { MenuIcon, CloseIcon } from "../Icons"; +import { NavLink, SearchProviderConfig } from "../types"; + +interface Props extends React.PropsWithChildren { + author?: string; + links?: Array; + search?: SearchProviderConfig; +} + +// TODO why mobile navigation only accepts author and regular nav accepts different things like title, logo, version +export const NavMobile: React.FC = ({ + children, + links, + search, + author, +}) => { + const router = useRouter(); + const [isOpen, setIsOpen] = useState(false); + const [Search, setSearch] = useState(); // TODO types + + useEffect(() => { + if (!isOpen) return; + + function onRouteChange() { + setIsOpen(false); + } + + router.events.on("routeChangeComplete", onRouteChange); + router.events.on("routeChangeError", onRouteChange); + + return () => { + router.events.off("routeChangeComplete", onRouteChange); + router.events.off("routeChangeError", onRouteChange); + }; + }, [router, isOpen]); + + useEffect(() => { + if (search) { + setSearch(SearchContext(search.provider)); + } + }, [search]); + + return ( + <> + + + +
+ + + {/* */} +
+ {author} +
+ +
+ {Search && ( + + {({ query }: any) => } + + )} + {links && ( +
    + {links.map((link) => ( + + +
  • + + {link.name} + +
  • +
    +
    + ))} +
+ )} + {/*
+ {children} +
*/} +
+
+ + ); +}; diff --git a/packages/core/src/ui/Nav/NavSocial.tsx b/packages/core/src/ui/Nav/NavSocial.tsx new file mode 100644 index 00000000..760f72d6 --- /dev/null +++ b/packages/core/src/ui/Nav/NavSocial.tsx @@ -0,0 +1,27 @@ +import Link from "next/link.js"; +import { GitHubIcon, DiscordIcon } from "../Icons"; +import { SocialLink, SocialPlatform } from "../types"; + +interface Props { + links: Array; +} + +const icons: { [K in SocialPlatform]: React.FC } = { + github: GitHubIcon, + discord: DiscordIcon, +}; + +export const NavSocial: React.FC = ({ links }) => { + return ( + <> + {links.map(({ label, href }) => { + const Icon = icons[label]; + return ( + + + + ); + })} + + ); +}; diff --git a/packages/core/src/ui/Nav/NavTitle.tsx b/packages/core/src/ui/Nav/NavTitle.tsx new file mode 100644 index 00000000..3c8cb509 --- /dev/null +++ b/packages/core/src/ui/Nav/NavTitle.tsx @@ -0,0 +1,27 @@ +import Link from "next/link.js"; + +interface Props { + title: string; + logo?: string; + version?: string; +} + +export const NavTitle: React.FC = ({ title, logo, version }) => { + return ( + + {logo && ( + {title} + )} + {title && {title}} + {version && ( +
+ {version} +
+ )} + + ); +}; diff --git a/packages/core/src/ui/Nav/index.ts b/packages/core/src/ui/Nav/index.ts new file mode 100644 index 00000000..c6338b85 --- /dev/null +++ b/packages/core/src/ui/Nav/index.ts @@ -0,0 +1 @@ +export { Nav, NavConfig, ThemeConfig } from "./Nav"; diff --git a/packages/core/src/ui/Pre/Pre.tsx b/packages/core/src/ui/Pre/Pre.tsx new file mode 100644 index 00000000..92f3268b --- /dev/null +++ b/packages/core/src/ui/Pre/Pre.tsx @@ -0,0 +1,74 @@ +import { useRef, useState } from "react"; + +interface Props extends React.PropsWithChildren { + className?: string; +} + +export const Pre: React.FC = ({ children, ...props }) => { + const ref = useRef(); // TODO type + const [hovered, setHovered] = useState(false); + const [copied, setCopied] = useState(false); + + const onEnter = () => { + setHovered(true); + }; + const onExit = () => { + setHovered(false); + setCopied(false); + }; + const onCopy = () => { + setCopied(true); + navigator.clipboard.writeText(ref.current.textContent); + setTimeout(() => { + setCopied(false); + }, 2000); + }; + + return ( +
+ {hovered && ( + + )} +
{children}
+
+ ); +}; diff --git a/packages/core/src/ui/Pre/index.ts b/packages/core/src/ui/Pre/index.ts new file mode 100644 index 00000000..9cf7d3d7 --- /dev/null +++ b/packages/core/src/ui/Pre/index.ts @@ -0,0 +1 @@ +export { Pre } from "./Pre"; diff --git a/packages/core/src/ui/Search/Algolia.tsx b/packages/core/src/ui/Search/Algolia.tsx new file mode 100644 index 00000000..16d4c273 --- /dev/null +++ b/packages/core/src/ui/Search/Algolia.tsx @@ -0,0 +1,132 @@ +import * as docsearch from "@docsearch/react"; +import Head from "next/head.js"; +import Link from "next/link.js"; +import { useRouter } from "next/router.js"; +import { createContext, useCallback, useMemo, useRef, useState } from "react"; +import { createPortal } from "react-dom"; + +const { useDocSearchKeyboardEvents } = docsearch; + +let DocSearchModal: any = null; + +function Hit({ hit, children }) { + return {children}; +} + +export const AlgoliaSearchContext = createContext({}); + +export function AlgoliaSearchProvider({ children, config }) { + const router = useRouter(); + const [isOpen, setIsOpen] = useState(false); + const [initialQuery, setInitialQuery] = useState(undefined); + + const importDocSearchModalIfNeeded = useCallback(async () => { + if (DocSearchModal) { + return Promise.resolve(); + } + + const [{ DocSearchModal: Modal }] = await Promise.all([docsearch]); + // eslint-disable-next-line + DocSearchModal = Modal; + }, [DocSearchModal]); + + const onOpen = useCallback(() => { + importDocSearchModalIfNeeded().then(() => { + setIsOpen(true); + }); + }, [importDocSearchModalIfNeeded, setIsOpen]); + + const onClose = useCallback(() => { + setIsOpen(false); + }, [setIsOpen]); + + const onInput = useCallback( + (event) => { + importDocSearchModalIfNeeded().then(() => { + setIsOpen(true); + setInitialQuery(event.key); + }); + }, + [importDocSearchModalIfNeeded, setIsOpen, setInitialQuery] + ); + + // web accessibility + // https://www.algolia.com/doc/ui-libraries/autocomplete/core-concepts/keyboard-navigation/ + const navigator = useRef({ + navigate({ itemUrl }) { + // Algolia results could contain URL's from other domains which cannot + // be served through history and should navigate with window.location + const isInternalLink = itemUrl.startsWith("/"); + const isAnchorLink = itemUrl.startsWith("#"); + if (!isInternalLink && !isAnchorLink) { + window.location.href = itemUrl; + } else { + router.push(itemUrl); + } + }, + }).current; + + // https://docsearch.algolia.com/docs/api#transformitems + const transformItems = (items) => + items.map((item) => { + // If Algolia contains a external domain, we should navigate without + // relative URL + const isInternalLink = item.url.startsWith("/"); + const isAnchorLink = item.url.startsWith("#"); + if (!isInternalLink && !isAnchorLink) { + return item; + } + + // We transform the absolute URL into a relative URL. + const url = new URL(item.url); + return { + ...item, + // url: withBaseUrl(`${url.pathname}${url.hash}`), + url: `${url.pathname}${url.hash}`, + }; + }); + // ).current; + + useDocSearchKeyboardEvents({ + isOpen, + onOpen, + onClose, + onInput, + }); + + const providerValue = useMemo( + () => ({ query: { setSearch: setInitialQuery, toggle: onOpen } }), + [setInitialQuery, onOpen] + ); + + return ( + + + {/* This hints the browser that the website will load data from Algolia, + and allows it to preconnect to the DocSearch cluster. It makes the first + query faster, especially on mobile. */} + + + {children} + {isOpen && + DocSearchModal && + createPortal( + , + document.body + )} + + ); +} diff --git a/packages/core/src/ui/Search/KBar.tsx b/packages/core/src/ui/Search/KBar.tsx new file mode 100644 index 00000000..cd0f57ce --- /dev/null +++ b/packages/core/src/ui/Search/KBar.tsx @@ -0,0 +1,33 @@ +import router from "next/router.js"; +import { Action } from "kbar"; + +import { KBarModal } from "./KBarModal"; + +export const KBarSearchProvider = ({ config, children }) => { + const defaultActions = config?.defaultActions; + const searchDocumentsPath = "/search.json"; + + let startingActions: Action[] = [ + { + id: "homepage", + name: "Homepage", + keywords: "", + section: "Home", + perform: () => router.push("/"), + }, + ]; + + if (defaultActions && Array.isArray(defaultActions)) + startingActions = [...startingActions, ...defaultActions]; + + return KBarModal ? ( + + {children} + + ) : ( + children + ); +}; diff --git a/packages/core/src/ui/Search/KBarModal.tsx b/packages/core/src/ui/Search/KBarModal.tsx new file mode 100644 index 00000000..8184c742 --- /dev/null +++ b/packages/core/src/ui/Search/KBarModal.tsx @@ -0,0 +1,21 @@ +import { KBarProvider, Action } from "kbar"; + +import { Portal } from "./KBarPortal"; + +interface Props extends React.PropsWithChildren { + searchDocumentsPath: string; + startingActions?: Action[]; +} + +export const KBarModal: React.FC = ({ + searchDocumentsPath, + startingActions, + children, +}) => { + return ( + + + {children} + + ); +}; diff --git a/packages/core/src/ui/Search/KBarPortal.tsx b/packages/core/src/ui/Search/KBarPortal.tsx new file mode 100644 index 00000000..c2867ffe --- /dev/null +++ b/packages/core/src/ui/Search/KBarPortal.tsx @@ -0,0 +1,120 @@ +import { useEffect, useState } from "react"; +import { + Action, + KBarAnimator, + KBarPortal, + KBarPositioner, + KBarResults, + KBarSearch, + useMatches, + useRegisterActions, +} from "kbar"; +import { kbarActionsFromDocuments } from "./kbarActionsFromDocuments"; + +interface Props { + searchDocumentsPath: string; +} + +export const Portal: React.FC = ({ searchDocumentsPath }) => { + const [searchActions, setSearchActions] = useState([]); + + useEffect(() => { + const fetchData = async () => { + const res = await fetch(searchDocumentsPath); + const json = await res.json(); + const actions = kbarActionsFromDocuments(json); + setSearchActions(actions); + }; + fetchData(); + }, [searchDocumentsPath]); + + useRegisterActions(searchActions, [searchActions]); + + return ( + + + +
+
+ + + + + + + + ESC + +
+ +
+
+
+
+ ); +}; + +function RenderItem(props) { + const { item, active } = props; + return ( +
+ {typeof item === "string" ? ( +
+
+ {item} +
+
+ ) : ( +
+ {item?.subtitle && ( +
+ {item.subtitle} +
+ )} +
{item?.name}
+
+ )} +
+ ); +} + +function RenderResults() { + const { results } = useMatches(); + + if (results.length) { + return ; + } + return ( +
+ No results for your search... +
+ ); +} diff --git a/packages/core/src/ui/Search/SearchField.tsx b/packages/core/src/ui/Search/SearchField.tsx new file mode 100644 index 00000000..a7d07c70 --- /dev/null +++ b/packages/core/src/ui/Search/SearchField.tsx @@ -0,0 +1,48 @@ +import { SearchIcon } from "../Icons"; + +// TODO types +export const SearchField: React.FC = (props) => { + const { modifierKey, onOpen, mobile } = props; + return ( + + ); +}; diff --git a/packages/core/src/ui/Search/SearchProvider.tsx b/packages/core/src/ui/Search/SearchProvider.tsx new file mode 100644 index 00000000..e6ec7e3e --- /dev/null +++ b/packages/core/src/ui/Search/SearchProvider.tsx @@ -0,0 +1,69 @@ +import dynamic from "next/dynamic.js"; +import { + SearchProvider as SearchProviderType, + SearchProviderConfig, +} from "../types"; + +const AlgoliaSearchProvider = dynamic( + async () => { + return await import("./Algolia").then((mod) => mod.AlgoliaSearchProvider); + }, + { ssr: false } +); + +const AlgoliaSearchContext = dynamic( + async () => { + return await import("./Algolia").then( + (mod) => mod.AlgoliaSearchContext.Consumer + ); + }, + { ssr: false } +); + +const KBarProvider = dynamic( + async () => { + return await import("./KBar").then((mod) => mod.KBarSearchProvider); + }, + { ssr: false } +); + +const KBarSearchContext = dynamic( + async () => { + return await import("kbar").then((mod) => mod.KBarContext.Consumer); + }, + { ssr: false } +); + +export const SearchProvider = ({ + searchConfig, + children, +}: { + searchConfig: SearchProviderConfig; + children: React.ReactNode; +}) => { + switch (searchConfig?.provider) { + case "algolia": + return ( + + {children} + + ); + case "kbar": + return ( + {children} + ); + default: + return <>{children}; + } +}; + +export const SearchContext = (provider: SearchProviderType) => { + switch (provider) { + case "algolia": + return AlgoliaSearchContext; + case "kbar": + return KBarSearchContext; + default: + return undefined; + } +}; diff --git a/packages/core/src/ui/Search/index.ts b/packages/core/src/ui/Search/index.ts new file mode 100644 index 00000000..7d27d4da --- /dev/null +++ b/packages/core/src/ui/Search/index.ts @@ -0,0 +1,3 @@ +// TODO tidy up this API +export { SearchField } from "./SearchField"; +export { SearchContext, SearchProvider } from "./SearchProvider"; diff --git a/packages/core/src/ui/Search/kbarActionsFromDocuments.ts b/packages/core/src/ui/Search/kbarActionsFromDocuments.ts new file mode 100644 index 00000000..01e4f707 --- /dev/null +++ b/packages/core/src/ui/Search/kbarActionsFromDocuments.ts @@ -0,0 +1,26 @@ +// TODO don't import router here? +import router from "next/router.js"; +import { Action } from "kbar"; + +import { formatDate } from "../../utils/formatDate"; +import { nameFromUrl } from "../../utils/nameFromUrl"; + +// TODO temp type +type Document = any; + +export const kbarActionsFromDocuments = (docs: Document[]): Action[] => { + const actions: Action[] = []; + for (const doc of docs) { + // excluding home path as this is defined in starting actions + doc.url_path && + actions.push({ + id: doc.url_path, + name: doc.title ?? nameFromUrl(doc.url_path), + keywords: doc.description ?? "", + section: doc.sourceDir ?? "Page", + subtitle: doc.date && formatDate(doc.date, "en-US"), + perform: () => router.push(`/${doc.url_path}`), + }); + } + return actions; +}; diff --git a/packages/core/src/ui/SimpleLayout/Container.tsx b/packages/core/src/ui/SimpleLayout/Container.tsx new file mode 100644 index 00000000..3790a39c --- /dev/null +++ b/packages/core/src/ui/SimpleLayout/Container.tsx @@ -0,0 +1,39 @@ +import { forwardRef } from "react"; +import clsx from "clsx"; + +const OuterContainer = forwardRef< + HTMLDivElement, + React.PropsWithChildren & { className?: string } +>(({ className, children, ...props }, ref) => { + return ( +
+
{children}
+
+ ); +}); + +const InnerContainer = forwardRef< + HTMLDivElement, + React.PropsWithChildren & { className?: string } +>(({ className, children, ...props }, ref) => { + return ( +
+
{children}
+
+ ); +}); + +export const Container = forwardRef< + HTMLDivElement, + React.PropsWithChildren & { className?: string } +>(({ children, ...props }, ref) => { + return ( + + {children} + + ); +}); diff --git a/packages/core/src/ui/SimpleLayout/SimpleLayout.tsx b/packages/core/src/ui/SimpleLayout/SimpleLayout.tsx new file mode 100644 index 00000000..9447726f --- /dev/null +++ b/packages/core/src/ui/SimpleLayout/SimpleLayout.tsx @@ -0,0 +1,20 @@ +/* eslint import/no-default-export: off */ +import { Container } from "./Container"; + +// TODO types +export const SimpleLayout: React.FC = ({ children, ...frontMatter }) => { + const { title, description } = frontMatter; + return ( + +
+

+ {title} +

+

+ {description} +

+
+
{children}
+
+ ); +}; diff --git a/packages/core/src/ui/SimpleLayout/index.ts b/packages/core/src/ui/SimpleLayout/index.ts new file mode 100644 index 00000000..087f30b7 --- /dev/null +++ b/packages/core/src/ui/SimpleLayout/index.ts @@ -0,0 +1 @@ +export { SimpleLayout } from "./SimpleLayout"; diff --git a/packages/core/src/ui/SiteToc/SiteToc.tsx b/packages/core/src/ui/SiteToc/SiteToc.tsx new file mode 100644 index 00000000..8d4ff9d7 --- /dev/null +++ b/packages/core/src/ui/SiteToc/SiteToc.tsx @@ -0,0 +1,108 @@ +import Link from "next/link.js"; +import clsx from "clsx"; +import { Disclosure, Transition } from "@headlessui/react"; + +export interface NavItem { + name: string; + href: string; +} + +export interface NavGroup { + name: string; + path: string; + level: number; + children: Array; +} + +interface Props { + currentPath: string; + nav: Array; +} + +function isNavGroup(item: NavItem | NavGroup): item is NavGroup { + return (item as NavGroup).children !== undefined; +} + +function navItemBeforeNavGroup(a, b) { + if (isNavGroup(a) === isNavGroup(b)) { + return 0; + } + if (isNavGroup(a) && !isNavGroup(b)) { + return 1; + } + return -1; +} + +function sortNavGroupChildren(items: Array) { + return items.sort( + (a, b) => navItemBeforeNavGroup(a, b) || a.name.localeCompare(b.name) + ); +} + +export const SiteToc: React.FC = ({ currentPath, nav }) => { + function isActiveItem(item: NavItem) { + return item.href === currentPath; + } + + return ( + + ); +}; + +const NavComponent: React.FC<{ + item: NavItem | NavGroup; + isActive: boolean; +}> = ({ item, isActive }) => { + return !isNavGroup(item) ? ( + + {item.name} + + ) : ( + + {({ open }) => ( +
+ + + {item.name} + + + + {sortNavGroupChildren(item.children).map((subItem) => ( + + ))} + + +
+ )} +
+ ); +}; diff --git a/packages/core/src/ui/SiteToc/index.ts b/packages/core/src/ui/SiteToc/index.ts new file mode 100644 index 00000000..2075f65e --- /dev/null +++ b/packages/core/src/ui/SiteToc/index.ts @@ -0,0 +1 @@ +export { SiteToc, NavItem, NavGroup } from "./SiteToc"; diff --git a/packages/core/src/ui/Tooltip/Tooltip.tsx b/packages/core/src/ui/Tooltip/Tooltip.tsx new file mode 100644 index 00000000..f5cb59f0 --- /dev/null +++ b/packages/core/src/ui/Tooltip/Tooltip.tsx @@ -0,0 +1,225 @@ +import React, { useState, useEffect, useRef, Fragment } from "react"; + +// importing separately due to build error +// Module '"@floating-ui/react-dom-interactions"' has no exported member 'autoPlacement' ... +import { + arrow, + autoPlacement, + inline, + offset, + shift, +} from "@floating-ui/react-dom"; + +import { + FloatingPortal, + useDismiss, + useFloating, + useHover, + useFocus, + useInteractions, + useRole, +} from "@floating-ui/react-dom-interactions"; + +import { motion, AnimatePresence } from "framer-motion"; + +interface Props extends React.PropsWithChildren { + render: (t) => React.ReactNode; + href: string; + data: any; + usehook?: any; + className?: string; +} + +const tooltipBoxStyle = (theme: string) => + ({ + height: "auto", + maxWidth: "40rem", + padding: "1rem", + background: theme === "light" ? "#fff" : "#000", + color: theme === "light" ? "rgb(99, 98, 98)" : "#A8A8A8", + borderRadius: "4px", + boxShadow: "rgba(0, 0, 0, 0.55) 0px 0px 16px -3px", + } as React.CSSProperties); + +const tooltipBodyStyle = (theme: string) => + ({ + maxHeight: "4.8rem", + position: "relative", + lineHeight: "1.2rem", + overflow: "hidden", + } as React.CSSProperties); + +const tooltipArrowStyle = ({ theme, x, y, side }) => + ({ + position: "absolute", + left: x != null ? `${x}px` : "", + top: y != null ? `${y}px` : "", + right: "", + bottom: "", + [side]: "-4px", + height: "8px", + width: "8px", + background: theme === "light" ? "#fff" : "#000", + transform: "rotate(45deg)", + } as React.CSSProperties); + +export const Tooltip: React.FC = ({ + render, + data, + usehook, + ...props +}) => { + const theme = "light"; // temporarily hard-coded; light theme tbd in next PR + + const arrowRef = useRef(null); + const [showTooltip, setShowTooltip] = useState(false); + const [tooltipData, setTooltipData] = useState({ + content: null || , + image: "", + }); + const [tooltipContentLoaded, setTooltipContentLoaded] = useState(false); + // floating-ui hook + const { + x, + y, + reference, // trigger element back ref + floating, // tooltip back ref + placement, // default: 'bottom' + strategy, // default: 'absolute' + context, + middlewareData: { arrow: { x: arrowX = 0, y: arrowY = 0 } = {} }, // data for arrow positioning + } = useFloating({ + open: showTooltip, // state value binding + onOpenChange: setShowTooltip, // state value setter + middleware: [ + offset(5), // offset from container border + autoPlacement({ padding: 5 }), // auto place vertically + shift({ padding: 5 }), // flip horizontally if necessary + arrow({ element: arrowRef, padding: 4 }), // add arrow element + inline(), // correct position for multiline anchor tags + ], + }); + // floating-ui hook + const { getReferenceProps, getFloatingProps } = useInteractions([ + useHover(context, { delay: 100 }), + useFocus(context), + useRole(context, { role: "tooltip" }), + useDismiss(context, { ancestorScroll: true }), + ]); + + const triggerElementProps = getReferenceProps({ ...props, ref: reference }); + const tooltipProps = getFloatingProps({ + ref: floating, + style: { + position: strategy, + left: x ?? "", + top: y ?? "", + }, + }); + + const arrowPlacement = { + top: "bottom", + right: "left", + bottom: "top", + left: "right", + }[placement.split("-")[0]]; + + // get tooltip data + let image: string; + let PageContent; + const filePath = props.href.slice(1); // remove slash from the beginning + + const page = data.find((p) => p._raw.flattenedPath === filePath); + + if (page && page.body.code.length > 0) { + const Component = usehook(page.body.code); + PageContent = Component; + image = page.image ?? ""; + } + + const fetchTooltipContent = () => { + setTooltipContentLoaded(false); + + let Body: React.ReactElement = ; + + // strip out all other elements from tooltip content + // since we only need the paragraph + const elems = ["h1", "h2", "h3", "div", "img", "pre", "blockquote"].reduce( + (acc, elem) => ({ ...acc, [elem]: () => }), + {} + ); + + if (PageContent) { + Body = ( + , // avoid hydration errors + wrapper: (props) =>
, + }} + /> + ); + + setTooltipData({ + content: Body, + image: image, + }); + + setTooltipContentLoaded(true); + } + }; + + useEffect(() => { + if (showTooltip) { + fetchTooltipContent(); + } + }, [showTooltip]); + + return ( + + {render?.(triggerElementProps)} + + + {showTooltip && tooltipContentLoaded && ( + +
+ {tooltipData.image && ( + + )} + {tooltipData.content && ( +
+ {tooltipData.content} +
+ )} +
+
+
+ )} +
+
+
+ ); +}; diff --git a/packages/core/src/ui/Tooltip/index.ts b/packages/core/src/ui/Tooltip/index.ts new file mode 100644 index 00000000..4b0a2f87 --- /dev/null +++ b/packages/core/src/ui/Tooltip/index.ts @@ -0,0 +1 @@ +export { Tooltip } from "./Tooltip"; diff --git a/packages/core/src/ui/UnstyledLayout/Unstyled.tsx b/packages/core/src/ui/UnstyledLayout/Unstyled.tsx new file mode 100644 index 00000000..e037ff1c --- /dev/null +++ b/packages/core/src/ui/UnstyledLayout/Unstyled.tsx @@ -0,0 +1,6 @@ +/* eslint import/no-default-export: off */ +export const UnstyledLayout: React.FC = ({ + children, +}) => { + return
{children}
; +}; diff --git a/packages/core/src/ui/UnstyledLayout/index.ts b/packages/core/src/ui/UnstyledLayout/index.ts new file mode 100644 index 00000000..54d7b803 --- /dev/null +++ b/packages/core/src/ui/UnstyledLayout/index.ts @@ -0,0 +1 @@ +export { UnstyledLayout } from "./Unstyled"; diff --git a/packages/core/src/ui/index.ts b/packages/core/src/ui/index.ts new file mode 100644 index 00000000..45a860cc --- /dev/null +++ b/packages/core/src/ui/index.ts @@ -0,0 +1,22 @@ +export { ThemeProvider } from "next-themes"; +export { Nav, NavConfig, ThemeConfig } from "./Nav"; +export { SearchProvider } from "./Search"; +export { + Layout, + TableOfContents, + TocSection, + EditThisPage, + useTableOfContents, +} from "./Layout"; +export { Pre } from "./Pre"; +export { CustomLink } from "./Base/CustomLink"; +export { BlogsList } from "./Blog"; +export { SimpleLayout } from "./SimpleLayout"; +export { DocsLayout } from "./DocsLayout"; +export { UnstyledLayout } from "./UnstyledLayout"; +export { BlogLayout } from "./BlogLayout"; +export { Mermaid } from "./Mermaid"; +export { SiteToc, NavItem, NavGroup } from "./SiteToc"; +export { Comments, CommentsConfig } from "./Comments"; + +export { AuthorConfig } from "./types"; diff --git a/packages/core/src/ui/types.ts b/packages/core/src/ui/types.ts new file mode 100644 index 00000000..dbbc5b60 --- /dev/null +++ b/packages/core/src/ui/types.ts @@ -0,0 +1,57 @@ +// shared types used in more than one component +// TODO find out what's the best place to put them, what's the best practice + +// layout +export interface NavLink { + name: string; + href: string; +} + +export interface AuthorConfig { + name: string; + url: string; + logo: string; +} + +// social +export type SocialPlatform = "github" | "discord"; + +export interface SocialLink { + label: SocialPlatform; + href: string; +} + +// search +export type SearchProvider = "algolia" | "kbar"; + +export interface SearchProviderConfig { + provider: SearchProvider; + config: object; +} + +// TEMP contentlayer +interface SharedFields { + title?: string; + description?: string; + image?: string; + layout: string; + showEditLink?: boolean; + showToc?: boolean; + showComments?: boolean; + isDraft?: boolean; + data: Array; +} + +interface ComputedFields { + urlPath: string; + editUrl?: string; + date?: string; +} + +export interface Page extends SharedFields, ComputedFields {} + +export interface Blog extends SharedFields, ComputedFields { + date: string; // TODO type? + authors?: Array; + tags?: Array; +} diff --git a/packages/core/src/utils/collectHeadings.ts b/packages/core/src/utils/collectHeadings.ts new file mode 100644 index 00000000..482de071 --- /dev/null +++ b/packages/core/src/utils/collectHeadings.ts @@ -0,0 +1,60 @@ +// ToC: get the html nodelist for headings +import { TocSection } from "../ui/Layout"; + +export function collectHeadings(nodes: NodeListOf) { + const sections: Array = []; + + Array.from(nodes).forEach((node) => { + const { id, innerText: title, tagName: level } = node; + + if (!(id && title)) { + return; + } + + if (level === "H1") { + sections.push({ id, title, level, children: [] }); + } + + const parentSection = sections[sections.length - 1]; + + if (level === "H2") { + if (parentSection && level > parentSection.level) { + (parentSection as TocSection).children.push({ + id, + title, + level, + children: [], + }); + } else { + sections.push({ id, title, level, children: [] }); + } + } + + if (level === "H3") { + const subSection = + parentSection?.children[parentSection?.children?.length - 1]; + if (subSection && level > subSection.level) { + (subSection as TocSection).children.push({ + id, + title, + level, + children: [], + }); + } else if (parentSection && level > parentSection.level) { + (parentSection as TocSection).children.push({ + id, + title, + level, + children: [], + }); + } else { + sections.push({ id, title, level, children: [] }); + } + } + + // TODO types + sections.push(...collectHeadings((node.children as any) ?? [])); + }); + + return sections; +} diff --git a/packages/core/src/utils/formatDate.ts b/packages/core/src/utils/formatDate.ts new file mode 100644 index 00000000..2ca49c1f --- /dev/null +++ b/packages/core/src/utils/formatDate.ts @@ -0,0 +1,8 @@ +export const formatDate = (date: string, locales = "en-US") => { + const options: Intl.DateTimeFormatOptions = { + year: "numeric", + month: "long", + day: "numeric", + }; + return new Date(date).toLocaleDateString(locales, options); +}; diff --git a/packages/core/src/utils/gtag.ts b/packages/core/src/utils/gtag.ts new file mode 100644 index 00000000..59b2baf8 --- /dev/null +++ b/packages/core/src/utils/gtag.ts @@ -0,0 +1,25 @@ +// https://developers.google.com/analytics/devguides/collection/gtagjs/pages +export const pageview = ({ + url, + analyticsID, +}: { + url: string; + analyticsID: string; +}) => { + if (typeof window.gtag !== undefined) { + window.gtag("config", analyticsID, { + page_path: url, + }); + } +}; + +// https://developers.google.com/analytics/devguides/collection/gtagjs/events +export const event = ({ action, category, label, value }) => { + if (typeof window.gtag !== undefined) { + window.gtag("event", action, { + event_category: category, + event_label: label, + value, + }); + } +}; diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts new file mode 100644 index 00000000..9d9efea6 --- /dev/null +++ b/packages/core/src/utils/index.ts @@ -0,0 +1,2 @@ +export { pageview } from "./gtag"; +export { collectHeadings } from "./collectHeadings"; diff --git a/packages/core/src/utils/nameFromUrl.ts b/packages/core/src/utils/nameFromUrl.ts new file mode 100644 index 00000000..e9315117 --- /dev/null +++ b/packages/core/src/utils/nameFromUrl.ts @@ -0,0 +1,4 @@ +export const nameFromUrl = (url: string) => { + const name = url.split("/").slice(-1)[0].replace("-", " "); + return name.charAt(0).toUpperCase() + name.slice(1); +}; diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 00000000..53ca81e4 --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,28 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "jsx": "react-jsx", + "module": "es2020", + "moduleResolution": "node", + "allowJs": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitAny": false, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/packages/core/tsconfig.lib.json b/packages/core/tsconfig.lib.json new file mode 100644 index 00000000..f9ffa212 --- /dev/null +++ b/packages/core/tsconfig.lib.json @@ -0,0 +1,23 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "types": ["node"] + }, + "files": [ + "../../node_modules/@nrwl/react/typings/cssmodule.d.ts", + "../../node_modules/@nrwl/react/typings/image.d.ts" + ], + "exclude": [ + "jest.config.ts", + "**/*.spec.ts", + "**/*.test.ts", + "**/*.spec.tsx", + "**/*.test.tsx", + "**/*.spec.js", + "**/*.test.js", + "**/*.spec.jsx", + "**/*.test.jsx" + ], + "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] +} diff --git a/packages/core/tsconfig.spec.json b/packages/core/tsconfig.spec.json new file mode 100644 index 00000000..ff08addd --- /dev/null +++ b/packages/core/tsconfig.spec.json @@ -0,0 +1,20 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "jest.config.ts", + "**/*.test.ts", + "**/*.spec.ts", + "**/*.test.tsx", + "**/*.spec.tsx", + "**/*.test.js", + "**/*.spec.js", + "**/*.test.jsx", + "**/*.spec.jsx", + "**/*.d.ts" + ] +} diff --git a/tsconfig.base.json b/tsconfig.base.json index 8220788f..8392fc32 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -17,7 +17,8 @@ "paths": { "@portaljs/portaljs-components": [ "packages/portaljs-components/src/index.ts" - ] + ], + "@portaljs/core": ["packages/core/src/index.ts"] } }, "exclude": ["node_modules", "tmp"]