chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@nx/react/babel",
|
||||
{
|
||||
"runtime": "automatic",
|
||||
"useBuiltIns": "usage"
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": []
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
# nx-dev-ui-markdown
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `nx test nx-dev-ui-markdown` to execute the unit tests via [Jest](https://jestjs.io).
|
||||
@@ -0,0 +1,4 @@
|
||||
import { baseConfig, reactHooksV7Off } from '../../eslint.config.mjs';
|
||||
import nx from '@nx/eslint-plugin';
|
||||
|
||||
export default [...baseConfig, ...nx.configs['flat/react'], ...reactHooksV7Off];
|
||||
@@ -0,0 +1,14 @@
|
||||
module.exports = {
|
||||
displayName: 'nx-dev-ui-markdoc',
|
||||
preset: '../../jest.preset.js',
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
||||
coverageDirectory: '../../coverage/nx-dev/ui-markdoc',
|
||||
moduleNameMapper: {
|
||||
// Override for nx-dev packages - point to packages directory
|
||||
'^@nx/devkit$': '<rootDir>/../../packages/devkit/index.ts',
|
||||
'^@nx/devkit/testing$': '<rootDir>/../../packages/devkit/testing.ts',
|
||||
'^@nx/devkit/internal-testing-utils$':
|
||||
'<rootDir>/../../packages/devkit/internal-testing-utils.ts',
|
||||
'^@nx/devkit/src/(.*)$': '<rootDir>/../../packages/devkit/src/$1',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@nx/nx-dev-ui-markdoc",
|
||||
"version": "0.0.1",
|
||||
"type": "commonjs",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"dependencies": {
|
||||
"@nx/devkit": "workspace:*",
|
||||
"@nx/graph-internal-ui-project-details": "workspace:*",
|
||||
"@nx/graph-ui-common": "workspace:*",
|
||||
"@nx/graph-ui-icons": "workspace:*",
|
||||
"@nx/nx-dev-ui-fence": "workspace:*",
|
||||
"@nx/nx-dev-ui-primitives": "workspace:*",
|
||||
"@nx/nx-dev-ui-theme": "workspace:*",
|
||||
"@nx/nx-dev-ui-icons": "workspace:*",
|
||||
"@nx/nx-dev-ui-common": "workspace:*",
|
||||
"@nx/nx-dev-feature-analytics": "workspace:*"
|
||||
},
|
||||
"devDependencies": {}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "nx-dev-ui-markdoc",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "nx-dev/ui-markdoc/src",
|
||||
"projectType": "library",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"lint": {
|
||||
"options": {
|
||||
"max-warnings": 7
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
import { Node, RenderableTreeNode } from '@markdoc/markdoc';
|
||||
import markdoc from '@markdoc/markdoc';
|
||||
import { load as yamlLoad } from '@zkochan/js-yaml';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { Heading } from './lib/nodes/heading.component';
|
||||
import { getHeadingSchema } from './lib/nodes/heading.schema';
|
||||
import { getImageSchema } from './lib/nodes/image.schema';
|
||||
import { CustomLink } from './lib/nodes/link.component';
|
||||
import { link } from './lib/nodes/link.schema';
|
||||
import { Callout } from './lib/tags/callout.component';
|
||||
import { callout } from './lib/tags/callout.schema';
|
||||
import { CallToAction } from './lib/tags/call-to-action.component';
|
||||
import { callToAction } from './lib/tags/call-to-action.schema';
|
||||
import { Card, Cards, LinkCard } from './lib/tags/cards.component';
|
||||
import { card, cards, linkCard } from './lib/tags/cards.schema';
|
||||
import { GithubRepository } from './lib/tags/github-repository.component';
|
||||
import { githubRepository } from './lib/tags/github-repository.schema';
|
||||
import { StackblitzButton } from './lib/tags/stackblitz-button.component';
|
||||
import { stackblitzButton } from './lib/tags/stackblitz-button.schema';
|
||||
import { Graph } from './lib/tags/graph.component';
|
||||
import { graph } from './lib/tags/graph.schema';
|
||||
import { Iframe } from './lib/tags/iframe.component';
|
||||
import { iframe } from './lib/tags/iframe.schema';
|
||||
import { InstallNxConsole } from './lib/tags/install-nx-console.component';
|
||||
import { installNxConsole } from './lib/tags/install-nx-console.schema';
|
||||
import { Persona, Personas } from './lib/tags/personas.component';
|
||||
import { persona, personas } from './lib/tags/personas.schema';
|
||||
import { ProjectDetails } from './lib/tags/project-details.component';
|
||||
import { projectDetails } from './lib/tags/project-details.schema';
|
||||
import {
|
||||
ShortEmbeds,
|
||||
shortEmbeds,
|
||||
shortVideo,
|
||||
ShortVideo,
|
||||
} from './lib/tags/short-embed';
|
||||
import { SideBySide } from './lib/tags/side-by-side.component';
|
||||
import { sideBySide } from './lib/tags/side-by-side.schema';
|
||||
import { Tab, Tabs } from './lib/tags/tabs.component';
|
||||
import { tab, tabs } from './lib/tags/tabs.schema';
|
||||
import { Step, Steps } from './lib/tags/steps.component';
|
||||
import { step, steps } from './lib/tags/steps.schema';
|
||||
import { courseVideo } from './lib/tags/course-video.schema';
|
||||
import { VideoLink, videoLink } from './lib/tags/video-link.component';
|
||||
import {
|
||||
CourseVideo,
|
||||
YouTube,
|
||||
Tweet,
|
||||
youtube,
|
||||
tweet,
|
||||
} from '@nx/nx-dev-ui-common';
|
||||
// import { SvgAnimation, svgAnimation } from './lib/tags/svg-animation.component';
|
||||
import { Pill } from './lib/tags/pill.component';
|
||||
import { pill } from './lib/tags/pill.schema';
|
||||
import { fence } from './lib/nodes/fence.schema';
|
||||
import { FenceWrapper } from './lib/nodes/fence-wrapper.component';
|
||||
import { VideoPlayer, videoPlayer } from './lib/tags/video-player.component';
|
||||
import { TableOfContents } from './lib/tags/table-of-contents.component';
|
||||
import { tableOfContents } from './lib/tags/table-of-contents.schema';
|
||||
import { Testimonial, testimonial } from './lib/tags/testimonial.component';
|
||||
import { metrics } from './lib/tags/metrics.schema';
|
||||
import { Metrics } from './lib/tags/metrics.component';
|
||||
export { CallToAction };
|
||||
|
||||
const { parse, renderers, Tokenizer, transform } = markdoc;
|
||||
|
||||
export const getMarkdocCustomConfig = (
|
||||
documentFilePath: string,
|
||||
headingClass: string = ''
|
||||
): { config: any; components: any } => ({
|
||||
config: {
|
||||
nodes: {
|
||||
fence,
|
||||
heading: getHeadingSchema(headingClass),
|
||||
image: getImageSchema(documentFilePath),
|
||||
link,
|
||||
},
|
||||
tags: {
|
||||
callout,
|
||||
'call-to-action': callToAction,
|
||||
card,
|
||||
cards,
|
||||
'link-card': linkCard,
|
||||
'github-repository': githubRepository,
|
||||
'stackblitz-button': stackblitzButton,
|
||||
graph,
|
||||
iframe,
|
||||
'install-nx-console': installNxConsole,
|
||||
'video-player': videoPlayer,
|
||||
persona,
|
||||
personas,
|
||||
'project-details': projectDetails,
|
||||
pill,
|
||||
'short-embeds': shortEmbeds,
|
||||
'short-video': shortVideo,
|
||||
'side-by-side': sideBySide,
|
||||
step,
|
||||
steps,
|
||||
tab,
|
||||
tabs,
|
||||
testimonial,
|
||||
toc: tableOfContents,
|
||||
tweet,
|
||||
youtube,
|
||||
'course-video': courseVideo,
|
||||
'video-link': videoLink,
|
||||
metrics,
|
||||
// 'svg-animation': svgAnimation,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
Callout,
|
||||
CallToAction,
|
||||
Card,
|
||||
Cards,
|
||||
LinkCard,
|
||||
CustomLink,
|
||||
FenceWrapper,
|
||||
GithubRepository,
|
||||
StackblitzButton,
|
||||
Graph,
|
||||
Heading,
|
||||
Iframe,
|
||||
InstallNxConsole,
|
||||
Persona,
|
||||
Personas,
|
||||
ProjectDetails,
|
||||
Pill,
|
||||
ShortEmbeds,
|
||||
ShortVideo,
|
||||
SideBySide,
|
||||
Step,
|
||||
Steps,
|
||||
Tab,
|
||||
Tabs,
|
||||
TableOfContents,
|
||||
Testimonial,
|
||||
Tweet,
|
||||
YouTube,
|
||||
CourseVideo,
|
||||
VideoLink,
|
||||
VideoPlayer,
|
||||
Metrics,
|
||||
// SvgAnimation,
|
||||
},
|
||||
});
|
||||
|
||||
const tokenizer = new Tokenizer({
|
||||
// Per https://markdoc.dev/docs/syntax#comments this will be on by default in a future version
|
||||
allowComments: true,
|
||||
});
|
||||
|
||||
const parseMarkdown: (markdown: string) => Node = (markdown) => {
|
||||
const tokens = tokenizer.tokenize(markdown);
|
||||
return parse(tokens);
|
||||
};
|
||||
|
||||
export const extractFrontmatter = (
|
||||
documentContent: string
|
||||
): Record<string, any> => {
|
||||
const ast = parseMarkdown(documentContent);
|
||||
const frontmatter = ast.attributes['frontmatter']
|
||||
? (yamlLoad(ast.attributes['frontmatter']) as Record<string, any>)
|
||||
: {};
|
||||
return frontmatter;
|
||||
};
|
||||
|
||||
export const renderMarkdown: (
|
||||
documentContent: string,
|
||||
options: { filePath: string; headingClass?: string }
|
||||
) => {
|
||||
metadata: Record<string, any>;
|
||||
node: ReactNode;
|
||||
treeNode: RenderableTreeNode;
|
||||
} = (documentContent, options = { filePath: '' }) => {
|
||||
const ast = parseMarkdown(documentContent);
|
||||
const configuration = getMarkdocCustomConfig(
|
||||
options.filePath,
|
||||
options.headingClass
|
||||
);
|
||||
const treeNode = transform(ast, configuration.config);
|
||||
|
||||
return {
|
||||
metadata: ast.attributes['frontmatter']
|
||||
? (yamlLoad(ast.attributes['frontmatter']) as Record<string, any>)
|
||||
: {},
|
||||
node: renderers.react(transform(ast, configuration.config), React, {
|
||||
components: configuration.components,
|
||||
}),
|
||||
treeNode,
|
||||
};
|
||||
};
|
||||
|
||||
export { Metrics, VideoLink, GithubRepository /*, CourseVideo*/ };
|
||||
@@ -0,0 +1,179 @@
|
||||
'use client';
|
||||
|
||||
import type {
|
||||
ProjectGraphDependency,
|
||||
ProjectGraphProjectNode,
|
||||
} from 'nx/src/config/project-graph';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { ElementData, RenderTheme } from '@nx/graph';
|
||||
import {
|
||||
NxGraphProjectGraphProvider,
|
||||
useProjectGraphContext,
|
||||
} from '@nx/graph/projects';
|
||||
import { affectedNodeStyles, useThemeSync } from './resolve-theme';
|
||||
import {
|
||||
NxGraphCompositeProjectNodePanelContent,
|
||||
NxGraphCompositeProjectNodePanelHeader,
|
||||
NxGraphElementPanel,
|
||||
NxGraphProjectNodePanelContent,
|
||||
NxGraphProjectNodePanelHeader,
|
||||
useElementPanel,
|
||||
} from '@nx/graph/ui';
|
||||
|
||||
interface NxDevProjectGraphProps {
|
||||
theme: RenderTheme | 'system';
|
||||
projects: ProjectGraphProjectNode[];
|
||||
dependencies?: Record<string, ProjectGraphDependency[]>;
|
||||
affectedProjects?: string[];
|
||||
enableContextMenu?: boolean;
|
||||
composite?: boolean;
|
||||
showAffectedWithNodes?: boolean;
|
||||
}
|
||||
|
||||
export function NxDevProjectGraph(props: NxDevProjectGraphProps) {
|
||||
return (
|
||||
<NxGraphProjectGraphProvider
|
||||
renderPlatform="nx-dev"
|
||||
styles={[affectedNodeStyles]}
|
||||
>
|
||||
<NxDevProjectGraphInner {...props} />
|
||||
</NxGraphProjectGraphProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function NxDevProjectGraphInner({
|
||||
projects,
|
||||
dependencies = {},
|
||||
affectedProjects = [],
|
||||
theme = 'system',
|
||||
composite = false,
|
||||
enableContextMenu = false,
|
||||
showAffectedWithNodes = false,
|
||||
}: NxDevProjectGraphProps) {
|
||||
const graphContext = useProjectGraphContext();
|
||||
|
||||
const { containerRef, orchestrator, sendRendererConfigEvent, send } =
|
||||
graphContext;
|
||||
|
||||
const eventBus = useMemo(
|
||||
() => (enableContextMenu ? orchestrator : null),
|
||||
[orchestrator, enableContextMenu]
|
||||
);
|
||||
|
||||
const [element] = useElementPanel<
|
||||
ElementData.ProjectNode | ElementData.CompositeProjectNode
|
||||
>(eventBus);
|
||||
|
||||
const handleThemeChange = useCallback(
|
||||
(resolvedTheme: RenderTheme) => {
|
||||
sendRendererConfigEvent({
|
||||
type: 'themeChange',
|
||||
theme: resolvedTheme,
|
||||
});
|
||||
},
|
||||
[sendRendererConfigEvent]
|
||||
);
|
||||
|
||||
useThemeSync(theme, handleThemeChange);
|
||||
|
||||
useEffect(() => {
|
||||
if (!orchestrator) return;
|
||||
|
||||
send({ type: 'initGraph', projects, dependencies, affectedProjects });
|
||||
|
||||
send({
|
||||
type: 'updateRendererConfig',
|
||||
updater: (config) => ({
|
||||
mode: composite ? 'composite' : 'individual',
|
||||
autoExpand: composite ? config.autoExpand : 0,
|
||||
showMode: showAffectedWithNodes
|
||||
? 'all'
|
||||
: affectedProjects.length
|
||||
? 'affected'
|
||||
: 'all',
|
||||
}),
|
||||
});
|
||||
|
||||
send({ type: 'showAll' });
|
||||
// make sure the graph sized to fix into the box
|
||||
const el = orchestrator['renderer'].cy.elements();
|
||||
orchestrator['renderer'].cy.fit(el, 1).center().resize();
|
||||
// other values are static from the docs and we don't need to update for them
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [orchestrator, send]);
|
||||
|
||||
return (
|
||||
<div className="relative h-full w-full overflow-hidden">
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="flex h-full w-full cursor-pointer"
|
||||
></div>
|
||||
|
||||
<NxGraphElementPanel
|
||||
element={element}
|
||||
panelContainerClassName="border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900"
|
||||
panelHeaderClassName="border-zinc-300 dark:border-zinc-700"
|
||||
panelContentContainerClassName="divide-zinc-300 dark:divide-zinc-700"
|
||||
header={{
|
||||
project: (element, { open, close }) => (
|
||||
<NxGraphProjectNodePanelHeader
|
||||
element={element}
|
||||
open={open}
|
||||
close={close}
|
||||
badgeClassName="bg-zinc-100 dark:bg-zinc-700 text-zinc-700 dark:text-zinc-300 border-zinc-300 dark:border-zinc-600 uppercase"
|
||||
elementNameClassName="text-zinc-900 dark:text-zinc-100"
|
||||
closeButtonClassName="hover:bg-zinc-100 dark:hover:bg-zinc-700"
|
||||
/>
|
||||
),
|
||||
'composite-project': (element, { open, close }) => (
|
||||
<NxGraphCompositeProjectNodePanelHeader
|
||||
element={element}
|
||||
open={open}
|
||||
close={close}
|
||||
badgeClassName="bg-zinc-100 dark:bg-zinc-700 text-zinc-700 dark:text-zinc-300 border-zinc-300 dark:border-zinc-600 uppercase"
|
||||
elementNameClassName="text-zinc-900 dark:text-zinc-100"
|
||||
closeButtonClassName="hover:bg-zinc-100 dark:hover:bg-zinc-700"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{{
|
||||
project: (element) => (
|
||||
<NxGraphProjectNodePanelContent
|
||||
element={element}
|
||||
sectionHeadingClassName="text-zinc-900 dark:text-zinc-100"
|
||||
sectionTextClassName="text-zinc-700 dark:text-zinc-300"
|
||||
tagBadgeClassName="bg-zinc-100 dark:bg-zinc-700 text-zinc-700 dark:text-zinc-300 border-zinc-300 dark:border-zinc-600"
|
||||
actionButtonClassName="bg-zinc-100/60 dark:bg-zinc-700/60 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700 hover:text-zinc-900 dark:hover:text-zinc-100 border-zinc-300 dark:border-zinc-600"
|
||||
cancelActionButtonClassName="bg-red-500 dark:bg-red-600 text-white hover:bg-red-600 dark:hover:bg-red-700 border-red-500 dark:border-red-600"
|
||||
dependencyItemClassName="text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700"
|
||||
dependentItemClassName="text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700"
|
||||
emptyItemListClassName="text-zinc-600 dark:text-zinc-400"
|
||||
traceAlgorithmButtonClassName="bg-zinc-100/60 dark:bg-zinc-700/60 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700 hover:text-zinc-900 dark:hover:text-zinc-100 border-zinc-300 dark:border-zinc-600"
|
||||
traceAlgorithmActiveButtonClassName="bg-blue-500 dark:bg-blue-600 text-white hover:bg-blue-600 dark:hover:bg-blue-700 border-blue-500 dark:border-blue-600"
|
||||
traceableProjectItemClassName="text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700 justify-between"
|
||||
traceableProjectSelectedItemClassName="bg-blue-500/10 dark:bg-blue-600/10 text-zinc-900 dark:text-zinc-100"
|
||||
/>
|
||||
),
|
||||
'composite-project': (element) => (
|
||||
<NxGraphCompositeProjectNodePanelContent
|
||||
element={element}
|
||||
sectionHeadingClassName="text-zinc-900 dark:text-zinc-100"
|
||||
sectionTextClassName="text-zinc-700 dark:text-zinc-300"
|
||||
actionButtonClassName="bg-zinc-100/60 dark:bg-zinc-700/60 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700 hover:text-zinc-900 dark:hover:text-zinc-100 border-zinc-300 dark:border-zinc-600"
|
||||
cancelActionButtonClassName="bg-red-500 dark:bg-red-600 text-white hover:bg-red-600 dark:hover:bg-red-700 border-red-500 dark:border-red-600"
|
||||
confirmActionButtonClassName="bg-blue-500 dark:bg-blue-600 text-white hover:bg-blue-600 dark:hover:bg-blue-700 border-blue-500 dark:border-blue-600"
|
||||
multiselectContainerClassName="text-zinc-700 dark:text-zinc-300"
|
||||
multiselectFilterInputClassName="bg-white dark:bg-zinc-800 border-zinc-300 dark:border-zinc-600 text-zinc-900 dark:text-zinc-100 focus:border-blue-500 dark:focus:border-blue-400"
|
||||
multiselectEmptyStateClassName="text-zinc-600 dark:text-zinc-400"
|
||||
multiselectListItemClassName="hover:bg-zinc-100 dark:hover:bg-zinc-700"
|
||||
multiselectCheckboxClassName="border-zinc-300 dark:border-zinc-600 accent-blue-500"
|
||||
multiselectLabelClassName="text-zinc-700 dark:text-zinc-300"
|
||||
multiselectSectionHeaderClassName="text-zinc-900 dark:text-zinc-100 border-zinc-300 dark:border-zinc-700"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
</NxGraphElementPanel>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
import type { RenderTheme } from '@nx/graph';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
export function resolveTheme(theme: RenderTheme | 'system'): RenderTheme {
|
||||
// Astro theme switcher sets this data attr when
|
||||
// user manually changes theme or on first render
|
||||
const astroTheme = document.documentElement.dataset.theme;
|
||||
|
||||
if (astroTheme === 'light' || astroTheme === 'dark') {
|
||||
return astroTheme as RenderTheme;
|
||||
}
|
||||
|
||||
if (astroTheme === 'auto' || !astroTheme || theme === 'system') {
|
||||
const prefersDark = window.matchMedia(
|
||||
'(prefers-color-scheme: dark)'
|
||||
).matches;
|
||||
return prefersDark ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
return theme === 'dark' || theme === 'light' ? theme : 'light';
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to track theme changes from both Astro theme switcher and system preference
|
||||
*/
|
||||
export function useThemeSync(
|
||||
fallbackTheme: RenderTheme | 'system' = 'system',
|
||||
onThemeChange?: (theme: RenderTheme) => void
|
||||
): RenderTheme {
|
||||
const [currentTheme, setCurrentTheme] = useState<RenderTheme>(() => {
|
||||
if (typeof window === 'undefined') return 'light';
|
||||
return resolveTheme(fallbackTheme);
|
||||
});
|
||||
|
||||
// Use a ref to keep the latest callback without re-triggering the effect
|
||||
const onThemeChangeRef = useRef(onThemeChange);
|
||||
useEffect(() => {
|
||||
onThemeChangeRef.current = onThemeChange;
|
||||
}, [onThemeChange]);
|
||||
|
||||
useEffect(() => {
|
||||
const updateTheme = () => {
|
||||
const newTheme = resolveTheme(fallbackTheme);
|
||||
setCurrentTheme((prevTheme) => {
|
||||
// Only update state if theme actually changed
|
||||
if (prevTheme !== newTheme) {
|
||||
onThemeChangeRef.current?.(newTheme);
|
||||
return newTheme;
|
||||
}
|
||||
return prevTheme;
|
||||
});
|
||||
};
|
||||
|
||||
// Check theme on mount
|
||||
updateTheme();
|
||||
|
||||
// Listen for changes to the document's theme attribute astro theme switcher sets
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (
|
||||
mutation.type === 'attributes' &&
|
||||
mutation.attributeName === 'data-theme'
|
||||
) {
|
||||
updateTheme();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['data-theme'],
|
||||
});
|
||||
|
||||
// Listen for system theme preference changes
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const handleSystemThemeChange = () => updateTheme();
|
||||
|
||||
mediaQuery.addEventListener('change', handleSystemThemeChange);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
mediaQuery.removeEventListener('change', handleSystemThemeChange);
|
||||
};
|
||||
}, [fallbackTheme]);
|
||||
|
||||
return currentTheme;
|
||||
}
|
||||
|
||||
export const affectedNodeStyles: Stylesheet = {
|
||||
selector: 'node[?affected]',
|
||||
style: {
|
||||
color: (node: NodeSingular) => {
|
||||
const { theme } = getThemedScratch(node);
|
||||
return NODES_THEME[theme].defaultColor;
|
||||
},
|
||||
'border-color': (node) => {
|
||||
const { theme } = getThemedScratch(node);
|
||||
return NODES_THEME[theme].affectedBorderColor;
|
||||
},
|
||||
backgroundColor: (node) => {
|
||||
const { theme } = getThemedScratch(node);
|
||||
return NODES_THEME[theme].affectedBackgroundColor;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Nx Graph doesn't expose this palette, so copied here
|
||||
const NxGraphPalette = {
|
||||
blue_500: 'hsla(217, 91%, 60%, 1)',
|
||||
blue_600: 'hsla(221, 83%, 53%, 1)',
|
||||
blue_700: 'rgb(29, 78, 216)',
|
||||
sky_500: 'hsla(199, 89%, 48%, 1)',
|
||||
sky_600: 'hsla(200, 98%, 39%, 1)',
|
||||
pink_400: 'hsla(329, 86%, 70%, 1)',
|
||||
pink_500: 'hsla(330, 81%, 60%, 1)',
|
||||
fuchsia_500: 'hsla(292, 84%, 61%, 1)',
|
||||
fuchsia_700: 'hsla(295, 72%, 40%, 1)',
|
||||
fuchsia_800: 'hsla(295, 70%, 33%, 1)',
|
||||
slate_50: 'hsla(210, 40%, 98%, 1)',
|
||||
slate_200: 'hsla(214, 32%, 91%, 1)',
|
||||
slate_400: 'hsla(213, 27%, 84%, 1)',
|
||||
slate_500: 'hsla(215, 16%, 47%, 1)',
|
||||
slate_600: 'hsla(215, 19%, 35%, 1)',
|
||||
slate_700: 'hsla(215, 25%, 27%, 1)',
|
||||
slate_800: 'hsla(217, 33%, 17%, 1)',
|
||||
slate_900: 'rgb(15, 23, 42)',
|
||||
slate_950: 'rgb(2, 6, 23)',
|
||||
white: '#fff',
|
||||
|
||||
// New Purples
|
||||
purple_400: 'hsla(270, 60%, 70%, 1)',
|
||||
purple_800: 'hsla(270, 40%, 30%, 1)',
|
||||
|
||||
// Nx colors
|
||||
nxBlue_500: 'rgb(59,130,246)',
|
||||
nxBlue_600: 'rgb(37,99,235)',
|
||||
nxBlue_700: 'rgb(29,78,216)',
|
||||
nxBlue_800: 'rgb(22,52,161)',
|
||||
|
||||
nxGreen_500: 'rgb(16,185,129)',
|
||||
nxGreen_600: 'rgb(5,150,105)',
|
||||
nxGreen_700: 'rgb(4,120,87)',
|
||||
|
||||
nxYellow_500: 'rgb(234,179,8)',
|
||||
nxYellow_600: 'rgb(202,138,4)',
|
||||
nxYellow_700: 'rgb(161,98,7)',
|
||||
|
||||
nxRed_500: 'rgb(239,68,68)',
|
||||
nxRed_600: 'rgb(220,38,38)',
|
||||
nxRed_700: 'rgb(185,28,28)',
|
||||
|
||||
nxLightGrey_50: 'rgb(248,250,252)',
|
||||
nxLightGrey_100: 'rgb(241,245,249)',
|
||||
nxLightGrey_200: 'rgb(224,227,231)',
|
||||
|
||||
// Zinc colors from Tailwind v4
|
||||
zinc_50: 'rgb(250,250,250)',
|
||||
zinc_100: 'rgb(244,244,245)',
|
||||
zinc_200: 'rgb(228,228,231)',
|
||||
zinc_300: 'rgb(212,212,216)',
|
||||
zinc_400: 'rgb(161,161,170)',
|
||||
zinc_500: 'rgb(113,113,122)',
|
||||
zinc_600: 'rgb(82,82,91)',
|
||||
zinc_700: 'rgb(63,63,70)',
|
||||
zinc_800: 'rgb(39,39,42)',
|
||||
zinc_900: 'rgb(24,24,27)',
|
||||
zinc_950: 'rgb(9,9,11)',
|
||||
};
|
||||
|
||||
const NODES_THEME = {
|
||||
light: {
|
||||
defaultColor: NxGraphPalette.slate_700,
|
||||
projectBorderColor: NxGraphPalette.slate_400,
|
||||
affectedColor: NxGraphPalette.zinc_800,
|
||||
affectedBorderColor: NxGraphPalette.pink_500,
|
||||
affectedBackgroundColor: NxGraphPalette.pink_400,
|
||||
expandedCompositeProjectColor: NxGraphPalette.slate_700,
|
||||
expandedCompositeProjectBackgroundColor: NxGraphPalette.slate_50,
|
||||
selectedBackgroundColor: NxGraphPalette.sky_500,
|
||||
expandedCompositeTaskBackgroundOpacity: 0.8,
|
||||
expandedCompositeTaskBackgroundColor: NxGraphPalette.slate_50,
|
||||
expandedCompositeTaskBorderColor: NxGraphPalette.slate_400,
|
||||
expandedCompositeTaskColor: NxGraphPalette.slate_700,
|
||||
taskBackgroundColor: NxGraphPalette.white,
|
||||
taskBorderColor: NxGraphPalette.slate_400,
|
||||
taskColor: NxGraphPalette.slate_900,
|
||||
highlightedColor: NxGraphPalette.zinc_800,
|
||||
highlightedBackgroundColor: NxGraphPalette.sky_500,
|
||||
highlightedBorderColor: NxGraphPalette.sky_600,
|
||||
anchorBorderColor: NxGraphPalette.nxYellow_600,
|
||||
anchorBackgroundColor: NxGraphPalette.nxYellow_500,
|
||||
},
|
||||
dark: {
|
||||
defaultColor: NxGraphPalette.zinc_200,
|
||||
projectBorderColor: NxGraphPalette.slate_600,
|
||||
affectedColor: NxGraphPalette.zinc_100,
|
||||
affectedBorderColor: NxGraphPalette.fuchsia_800,
|
||||
affectedBackgroundColor: NxGraphPalette.fuchsia_700,
|
||||
expandedCompositeProjectColor: NxGraphPalette.zinc_200,
|
||||
expandedCompositeProjectBackgroundColor: NxGraphPalette.slate_800,
|
||||
selectedBackgroundColor: NxGraphPalette.sky_600,
|
||||
expandedCompositeTaskBackgroundOpacity: 0.5,
|
||||
expandedCompositeTaskBackgroundColor: NxGraphPalette.slate_800,
|
||||
expandedCompositeTaskBorderColor: NxGraphPalette.slate_600,
|
||||
expandedCompositeTaskColor: NxGraphPalette.zinc_200,
|
||||
taskBackgroundColor: NxGraphPalette.slate_800,
|
||||
taskBorderColor: NxGraphPalette.slate_600,
|
||||
taskColor: NxGraphPalette.zinc_100,
|
||||
highlightedColor: NxGraphPalette.white,
|
||||
highlightedBackgroundColor: NxGraphPalette.sky_500,
|
||||
highlightedBorderColor: NxGraphPalette.sky_600,
|
||||
anchorBorderColor: NxGraphPalette.nxYellow_500,
|
||||
anchorBackgroundColor: NxGraphPalette.nxYellow_700,
|
||||
},
|
||||
};
|
||||
|
||||
function getThemedScratch(element: NodeSingular | EdgeSingular): {
|
||||
theme: RenderTheme;
|
||||
platform: 'nxCloud' | 'nx';
|
||||
} {
|
||||
const { _nxGraphTheme = 'light', _nxGraphPlatform = 'nxCloud' } =
|
||||
element.scratch();
|
||||
return { theme: _nxGraphTheme, platform: _nxGraphPlatform };
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
'use client';
|
||||
|
||||
import type { ProjectGraphProjectNode } from 'nx/src/config/project-graph';
|
||||
import type { TaskGraph } from 'nx/src/config/task-graph';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { RenderTheme } from '@nx/graph';
|
||||
import { NxGraphTaskGraphProvider, useTaskGraphContext } from '@nx/graph/tasks';
|
||||
import { useThemeSync } from './resolve-theme';
|
||||
import {
|
||||
NxGraphElementPanel,
|
||||
NxGraphTaskNodePanelContent,
|
||||
NxGraphTaskNodePanelHeader,
|
||||
useElementPanel,
|
||||
} from '@nx/graph/ui';
|
||||
|
||||
interface NxDevTaskGraphProps {
|
||||
theme: RenderTheme | 'system';
|
||||
projects: ProjectGraphProjectNode[];
|
||||
taskGraph: TaskGraph;
|
||||
taskId: string;
|
||||
taskIds?: string[];
|
||||
enableContextMenu?: boolean;
|
||||
}
|
||||
|
||||
export function NxDevTaskGraph(props: NxDevTaskGraphProps) {
|
||||
return (
|
||||
<NxGraphTaskGraphProvider renderPlatform="nx-dev">
|
||||
<NxDevTaskGraphInner {...props} />
|
||||
</NxGraphTaskGraphProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function NxDevTaskGraphInner({
|
||||
projects,
|
||||
taskGraph,
|
||||
taskId,
|
||||
taskIds = [],
|
||||
theme = 'system',
|
||||
enableContextMenu = false,
|
||||
}: NxDevTaskGraphProps) {
|
||||
const graphContext = useTaskGraphContext();
|
||||
const { containerRef, orchestrator, sendRendererConfigEvent, send } =
|
||||
graphContext;
|
||||
|
||||
const eventBus = useMemo(
|
||||
() => (enableContextMenu ? orchestrator : null),
|
||||
[orchestrator, enableContextMenu]
|
||||
);
|
||||
|
||||
const [element] = useElementPanel(eventBus);
|
||||
|
||||
const handleThemeChange = useCallback(
|
||||
(resolvedTheme: RenderTheme) => {
|
||||
sendRendererConfigEvent({
|
||||
type: 'themeChange',
|
||||
theme: resolvedTheme,
|
||||
});
|
||||
},
|
||||
[sendRendererConfigEvent]
|
||||
);
|
||||
|
||||
useThemeSync(theme, handleThemeChange);
|
||||
|
||||
useEffect(() => {
|
||||
if (!orchestrator) return;
|
||||
|
||||
const showTaskIds = taskIds.length ? taskIds : [taskId];
|
||||
|
||||
send(
|
||||
{ type: 'initGraph', projects, taskGraph },
|
||||
{
|
||||
type: 'show',
|
||||
taskIds: showTaskIds.map((id) =>
|
||||
id.startsWith('task-') ? id : `task-${id}`
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
// make sure the graph sized to fix into the box
|
||||
const el = orchestrator['renderer'].cy.elements();
|
||||
orchestrator['renderer'].cy.fit(el, 10).center().resize();
|
||||
// other values are static from the docs and we don't need to update for them
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [orchestrator, send]);
|
||||
|
||||
return (
|
||||
<div className="relative h-full w-full overflow-hidden">
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="flex h-full w-full cursor-pointer"
|
||||
></div>
|
||||
|
||||
<NxGraphElementPanel
|
||||
element={element}
|
||||
panelContainerClassName="border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900"
|
||||
panelHeaderClassName="border-zinc-300 dark:border-zinc-700"
|
||||
panelContentContainerClassName="divide-zinc-300 dark:divide-zinc-700"
|
||||
header={{
|
||||
task: (element, { open, close }) => (
|
||||
<NxGraphTaskNodePanelHeader
|
||||
element={element}
|
||||
open={open}
|
||||
close={close}
|
||||
elementNameClassName="text-zinc-900 dark:text-zinc-100"
|
||||
closeButtonClassName="hover:bg-zinc-100 dark:hover:bg-zinc-700"
|
||||
taskFlagBadgeClassName="bg-zinc-100 dark:bg-zinc-700 text-zinc-700 dark:text-zinc-300 border-zinc-300 dark:border-zinc-600"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{{
|
||||
task: (element) => (
|
||||
<NxGraphTaskNodePanelContent
|
||||
element={element}
|
||||
sectionHeadingClassName="text-zinc-900 dark:text-zinc-100"
|
||||
sectionTextClassName="text-zinc-700 dark:text-zinc-300"
|
||||
actionButtonClassName="bg-zinc-100/60 dark:bg-zinc-700/60 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700 hover:text-zinc-900 dark:hover:text-zinc-100 border-zinc-300 dark:border-zinc-600"
|
||||
sectionListContainerClassName="border-zinc-200 dark:border-zinc-700"
|
||||
sectionListSectionClassName="bg-zinc-50 dark:bg-zinc-800"
|
||||
sectionListHeaderClassName="text-zinc-700 dark:text-zinc-300 bg-zinc-200 dark:bg-zinc-800"
|
||||
sectionListHeaderLabelClassName="text-zinc-600 dark:text-zinc-400"
|
||||
sectionListItemsClassName="divide-zinc-200 dark:divide-zinc-600"
|
||||
sectionListItemClassName="bg-zinc-50 dark:bg-zinc-700"
|
||||
sectionListItemLabelClassName="text-zinc-900 dark:text-zinc-100"
|
||||
loadingSkeletonHeaderClassName="bg-zinc-200 dark:bg-zinc-600"
|
||||
loadingSkeletonItemClassName="bg-zinc-100 dark:bg-zinc-700"
|
||||
loadingSkeletonListClassName="bg-zinc-50 dark:bg-zinc-800"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
</NxGraphElementPanel>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
'use client';
|
||||
import { Fence, FenceProps } from '@nx/nx-dev-ui-fence';
|
||||
import { useRouter, usePathname, useParams } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const useUrlHash = (initialValue: string) => {
|
||||
const [hash, setHash] = useState(initialValue);
|
||||
|
||||
const updateHash = (str: string) => {
|
||||
if (!str) return;
|
||||
setHash(str.split('#')[1]);
|
||||
};
|
||||
|
||||
const params = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
updateHash(window.location.hash);
|
||||
}, [params]);
|
||||
|
||||
return hash;
|
||||
};
|
||||
|
||||
export function FenceWrapper(props: FenceProps) {
|
||||
const { push } = useRouter();
|
||||
const pathName = usePathname();
|
||||
const hash = decodeURIComponent(useUrlHash(''));
|
||||
|
||||
const modifiedProps: FenceProps = {
|
||||
...props,
|
||||
selectedLineGroup: hash,
|
||||
onLineGroupSelectionChange: (selection: string) => {
|
||||
push(pathName.split('#')[0] + '#' + selection);
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<Fence {...modifiedProps} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Schema } from '@markdoc/markdoc';
|
||||
import markdoc from '@markdoc/markdoc';
|
||||
const { Tag } = markdoc;
|
||||
|
||||
export const fence: Schema = {
|
||||
render: 'FenceWrapper',
|
||||
attributes: {
|
||||
content: { type: 'String', render: false, required: true },
|
||||
language: { type: 'String' },
|
||||
fileName: { type: 'String', default: '' },
|
||||
highlightLines: { type: 'Array', default: [] },
|
||||
lineGroups: { type: 'Object', default: {} },
|
||||
command: { type: 'String', default: '' },
|
||||
title: { type: 'String', default: '' },
|
||||
path: { type: 'String', default: '' },
|
||||
process: { type: 'Boolean', render: false, default: true },
|
||||
skipRescope: { type: 'Boolean', default: false },
|
||||
enableCopy: { type: 'Boolean', default: true },
|
||||
lineWrap: { type: 'Number', default: 0 },
|
||||
},
|
||||
transform(node, config) {
|
||||
const attributes = node.transformAttributes(config);
|
||||
const children = node.children.length
|
||||
? node.transformChildren(config)
|
||||
: [node.attributes['content']];
|
||||
return new Tag('FenceWrapper', attributes, children);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import { LinkIcon } from '@heroicons/react/24/outline';
|
||||
import Link from 'next/link';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export function Heading({
|
||||
id = '',
|
||||
level = 1,
|
||||
children,
|
||||
className,
|
||||
highlightColor,
|
||||
}: {
|
||||
id: string;
|
||||
level: number;
|
||||
children: ReactNode;
|
||||
className: string;
|
||||
highlightColor?: 'green' | 'blue' | 'yellow' | 'red';
|
||||
}) {
|
||||
const Component: any = `h${level}`;
|
||||
|
||||
return (
|
||||
<Component
|
||||
id={id}
|
||||
className={[
|
||||
'group',
|
||||
highlightColor && 'xl:-ml-5 xl:border-l-4 xl:pl-4',
|
||||
highlightColor === 'blue' && 'xl:border-blue-500',
|
||||
highlightColor === 'green' && 'xl:border-green-500',
|
||||
highlightColor === 'yellow' && 'xl:border-yellow-500',
|
||||
highlightColor === 'red' && 'xl:border-red-500',
|
||||
className,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
{children}
|
||||
<Link aria-hidden="true" href={`#${id}`}>
|
||||
<LinkIcon className="mb-1 ml-2 inline h-5 w-5 opacity-0 group-hover:opacity-100" />
|
||||
</Link>
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { generateID } from './heading.schema';
|
||||
|
||||
describe('heading schema: generateID', () => {
|
||||
it('should create ID with ony ASCII characters', () => {
|
||||
expect(generateID(['Hello', 'World'], {})).toEqual('hello-world');
|
||||
expect(generateID(['🎉 Pro: Simple Setup'], {})).toEqual(
|
||||
'pro-simple-setup'
|
||||
);
|
||||
});
|
||||
|
||||
it('should create id for code based headers', () => {
|
||||
const codeHeader = [
|
||||
{
|
||||
$$mdtype: 'Tag',
|
||||
name: 'code',
|
||||
attributes: {},
|
||||
children: ['launch-templates.<template-name>.init-steps[*].env'],
|
||||
},
|
||||
];
|
||||
|
||||
expect(generateID(codeHeader, {})).toEqual(
|
||||
'launchtemplatestemplatenameinitstepsenv'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import { RenderableTreeNode, Schema } from '@markdoc/markdoc';
|
||||
import markdoc from '@markdoc/markdoc';
|
||||
const { Tag } = markdoc;
|
||||
|
||||
export function generateID(
|
||||
children: RenderableTreeNode[],
|
||||
attributes: Record<string, any>
|
||||
) {
|
||||
if (attributes['id'] && typeof attributes['id'] === 'string') {
|
||||
return attributes['id'];
|
||||
}
|
||||
|
||||
const validChildrenNodes: RenderableTreeNode[] = [];
|
||||
|
||||
for (const child of children) {
|
||||
if (!child) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof child === 'string') {
|
||||
validChildrenNodes.push(child);
|
||||
} else if (
|
||||
// allow rendering titles that are wrapped in `code` tags
|
||||
typeof child === 'object' &&
|
||||
'children' in child &&
|
||||
child.name === 'code' &&
|
||||
Array.isArray(child.children)
|
||||
) {
|
||||
const validNestedChild = child.children.filter(
|
||||
(c) => typeof c === 'string'
|
||||
);
|
||||
validChildrenNodes.push(...validNestedChild);
|
||||
}
|
||||
}
|
||||
|
||||
return validChildrenNodes
|
||||
.join(' ')
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9 ]/g, '')
|
||||
.trim()
|
||||
.replace(/\s+/g, '-');
|
||||
}
|
||||
|
||||
export const getHeadingSchema = (headingClass: string): Schema => ({
|
||||
render: 'Heading',
|
||||
children: ['inline'],
|
||||
attributes: {
|
||||
id: { type: 'String' },
|
||||
level: { type: 'Number', required: true, default: 1 },
|
||||
className: { type: 'String' },
|
||||
highlightColor: { type: 'String' },
|
||||
},
|
||||
transform(node, config) {
|
||||
const attributes = node.transformAttributes(config);
|
||||
const children = node.transformChildren(config);
|
||||
const id = generateID(children, attributes);
|
||||
|
||||
return new Tag(
|
||||
this.render,
|
||||
// `h${node.attributes['level']}`,
|
||||
{ ...attributes, id, className: headingClass },
|
||||
children
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { transformImagePath } from './transform-image-path';
|
||||
import { join, sep } from 'path';
|
||||
|
||||
describe('transformImagePath', () => {
|
||||
it('should transform relative paths', () => {
|
||||
const transform = transformImagePath(
|
||||
'nx-dev/nx-dev/public/documentation/shared/using-nx/dte.md'
|
||||
);
|
||||
|
||||
expect(transform('./test.png')).toEqual(
|
||||
sep + join('documentation', 'shared', 'using-nx', 'test.png')
|
||||
);
|
||||
expect(transform('../test.png')).toEqual(
|
||||
sep + join('documentation', 'shared', 'test.png')
|
||||
);
|
||||
expect(transform('../../test.png')).toEqual(
|
||||
sep + join('documentation', 'test.png')
|
||||
);
|
||||
});
|
||||
|
||||
it('should transform absolute paths', () => {
|
||||
const transform = transformImagePath(
|
||||
'nx-dev/nx-dev/public/documentation/angular/generators/workspace-generators.md'
|
||||
);
|
||||
|
||||
expect(transform('/shared/test.png')).toEqual(
|
||||
'/documentation/shared/test.png'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import { uriTransformer } from './uri-transformer';
|
||||
|
||||
export function transformImagePath(
|
||||
documentFilePath: string
|
||||
): (src: string) => string {
|
||||
return (src) => {
|
||||
const isRelative = src.startsWith('.');
|
||||
|
||||
if (!/\.(gif|jpe?g|tiff?|png|webp|bmp|svg|avif)$/i.test(src)) {
|
||||
return uriTransformer(src);
|
||||
}
|
||||
|
||||
if (isRelative) {
|
||||
return uriTransformer(
|
||||
new URL(
|
||||
[
|
||||
'http://example.com',
|
||||
documentFilePath.split('/').splice(3).join('/'),
|
||||
'..',
|
||||
src,
|
||||
].join('/')
|
||||
).pathname
|
||||
);
|
||||
}
|
||||
|
||||
return uriTransformer(`/documentation`.concat(src));
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* @param {string} uri
|
||||
* @returns {string}
|
||||
*/
|
||||
export function uriTransformer(uri: string): string {
|
||||
const protocols = ['http', 'https', 'mailto', 'tel'];
|
||||
const url = (uri || '').trim();
|
||||
const first = url.charAt(0);
|
||||
|
||||
if (first === '#' || first === '/') {
|
||||
return url;
|
||||
}
|
||||
|
||||
const colon = url.indexOf(':');
|
||||
if (colon === -1) {
|
||||
return url;
|
||||
}
|
||||
|
||||
let index = -1;
|
||||
|
||||
while (++index < protocols.length) {
|
||||
const protocol = protocols[index];
|
||||
|
||||
if (
|
||||
colon === protocol.length &&
|
||||
url.slice(0, protocol.length).toLowerCase() === protocol
|
||||
) {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
index = url.indexOf('?');
|
||||
if (index !== -1 && colon > index) {
|
||||
return url;
|
||||
}
|
||||
|
||||
index = url.indexOf('#');
|
||||
if (index !== -1 && colon > index) {
|
||||
return url;
|
||||
}
|
||||
|
||||
return 'javascript:void(0)';
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Config, Node, RenderableTreeNodes, Schema } from '@markdoc/markdoc';
|
||||
import { transformImagePath } from './helpers/transform-image-path';
|
||||
import markdoc from '@markdoc/markdoc';
|
||||
const { Tag } = markdoc;
|
||||
|
||||
export const getImageSchema = (documentFilePath: string): Schema => ({
|
||||
render: 'img',
|
||||
attributes: {
|
||||
src: { type: 'String', required: true },
|
||||
alt: { type: 'String', required: true },
|
||||
},
|
||||
transform(node: Node, config: Config): RenderableTreeNodes {
|
||||
const attributes = node.transformAttributes(config);
|
||||
const children = node.transformChildren(config);
|
||||
const src = transformImagePath(documentFilePath)(attributes['src']);
|
||||
|
||||
return new Tag(
|
||||
this.render,
|
||||
{ ...attributes, src, loading: 'lazy' },
|
||||
children
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import Link from 'next/link';
|
||||
import { cx } from '@nx/nx-dev-ui-primitives';
|
||||
|
||||
export function CustomLink(props: any) {
|
||||
const target =
|
||||
props.target ||
|
||||
(props.href.startsWith('http') || props.href.indexOf('.pdf') > -1
|
||||
? '_blank'
|
||||
: undefined);
|
||||
|
||||
return (
|
||||
<Link
|
||||
prefetch={false}
|
||||
{...props}
|
||||
passHref
|
||||
target={target}
|
||||
rel={target === '_blank' ? 'noreferrer' : undefined}
|
||||
className={cx(
|
||||
props.className,
|
||||
'text-blue-600 transition-colors ease-out hover:text-blue-700 dark:text-blue-500 dark:hover:text-blue-400'
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
export const link = {
|
||||
render: 'CustomLink',
|
||||
description: 'Displays a Next.js link',
|
||||
attributes: {
|
||||
href: {
|
||||
description: 'The path or URL to navigate to.',
|
||||
type: String,
|
||||
errorLevel: 'critical',
|
||||
required: true,
|
||||
},
|
||||
as: {
|
||||
description:
|
||||
'Optional decorator for the path that will be shown in the browser URL bar.',
|
||||
type: String,
|
||||
},
|
||||
passHref: {
|
||||
description: 'Forces Link to send the href property to its child.',
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
prefetch: {
|
||||
description: 'Prefetch the page in the background.',
|
||||
type: Boolean,
|
||||
},
|
||||
replace: {
|
||||
description:
|
||||
'Replace the current history state instead of adding a new url into the stack.',
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
scroll: {
|
||||
description: 'Scroll to the top of the page after a navigation.',
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
shallow: {
|
||||
description:
|
||||
'Update the path of the current page without rerunning getStaticProps, getServerSideProps or getInitialProps.',
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
locale: {
|
||||
description: 'The active locale is automatically prepended.',
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,144 @@
|
||||
import { ChevronRightIcon } from '@heroicons/react/24/outline';
|
||||
import { type Framework, frameworkIcons } from '@nx/graph-ui-icons';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const iconSizeClasses: Record<
|
||||
string,
|
||||
{ icon: string; chevron: string; width: string }
|
||||
> = {
|
||||
sm: { icon: 'h-10 w-10', chevron: 'h-6 w-6', width: 'max-w-md' },
|
||||
md: { icon: 'h-16 w-16', chevron: 'h-10 w-10', width: 'max-w-lg' },
|
||||
lg: { icon: 'h-24 w-24', chevron: 'h-16 w-16', width: 'max-w-xl' },
|
||||
};
|
||||
|
||||
const variantClasses: Record<
|
||||
string,
|
||||
{
|
||||
container: string;
|
||||
accent: string;
|
||||
hoverText: string;
|
||||
expandBg: string;
|
||||
}
|
||||
> = {
|
||||
default: {
|
||||
container: 'bg-zinc-50 dark:bg-zinc-800/60',
|
||||
accent: 'bg-blue-500 dark:bg-blue-500',
|
||||
hoverText: 'hover:text-white',
|
||||
expandBg: 'group-hover:w-full',
|
||||
},
|
||||
gradient: {
|
||||
container:
|
||||
'bg-gradient-to-r from-blue-500 via-blue-400 to-blue-500 dark:from-blue-600 dark:via-blue-500 dark:to-blue-600',
|
||||
accent: 'dark:bg-white bg-blue-500',
|
||||
hoverText: 'hover:text-blue-100 dark:hover:text-zinc-900 text-white',
|
||||
expandBg: 'group-hover:w-full',
|
||||
},
|
||||
inverted: {
|
||||
container: 'bg-zinc-800 dark:bg-zinc-100',
|
||||
accent: 'bg-blue-400 dark:bg-blue-600',
|
||||
hoverText:
|
||||
'hover:text-zinc-900 dark:hover:text-white text-white dark:text-zinc-900',
|
||||
expandBg: 'group-hover:w-full',
|
||||
},
|
||||
'gradient-alt': {
|
||||
container:
|
||||
'bg-gradient-to-br from-blue-600 via-blue-400 to-blue-300 dark:from-blue-800 dark:via-blue-600 dark:to-blue-400',
|
||||
accent: 'bg-blue-800 dark:bg-blue-500',
|
||||
hoverText: 'hover:text-blue-100 dark:hover:text-blue-200 text-white',
|
||||
expandBg: 'group-hover:w-full',
|
||||
},
|
||||
simple: {
|
||||
container: 'bg-blue-600 dark:bg-blue-600',
|
||||
accent: 'bg-transparent',
|
||||
hoverText: 'text-white',
|
||||
expandBg: '',
|
||||
},
|
||||
};
|
||||
|
||||
export type CallToActionProps = {
|
||||
url: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
variant?: 'default' | 'gradient' | 'inverted' | 'gradient-alt' | 'simple';
|
||||
};
|
||||
|
||||
export function CallToAction({
|
||||
url,
|
||||
title,
|
||||
description,
|
||||
icon = 'nx',
|
||||
size = 'sm',
|
||||
variant = 'default',
|
||||
}: CallToActionProps): JSX.Element {
|
||||
const iconClasses = iconSizeClasses[size];
|
||||
const colorClasses = variantClasses?.[variant] ?? variantClasses['default'];
|
||||
|
||||
if (variant === 'simple') {
|
||||
return (
|
||||
<div className="not-content not-prose mx-auto my-12 flex justify-center">
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={classNames(
|
||||
colorClasses.container,
|
||||
colorClasses.hoverText,
|
||||
'inline-flex items-center gap-2 rounded-md px-6 py-3 font-medium no-underline shadow-sm'
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
<ChevronRightIcon className="h-5 w-5" />
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
iconClasses.width,
|
||||
colorClasses.container,
|
||||
colorClasses.hoverText,
|
||||
'not-content not-prose group relative mx-auto my-12 flex w-full items-center gap-3 overflow-hidden rounded-lg shadow-md transition'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'absolute inset-0 z-0 w-2 transition-all duration-150',
|
||||
colorClasses.accent,
|
||||
colorClasses.expandBg
|
||||
)}
|
||||
></div>
|
||||
<div className={classNames('w-2', colorClasses.accent)}></div>
|
||||
|
||||
<div className="z-10 flex flex-grow items-center py-3">
|
||||
<div className={iconClasses.icon}>
|
||||
{icon && frameworkIcons[icon as Framework]?.image}
|
||||
</div>
|
||||
|
||||
<div className="mx-3">
|
||||
<p>
|
||||
{title}
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="block text-sm font-medium text-inherit no-underline opacity-80"
|
||||
>
|
||||
<span className="absolute inset-0" aria-hidden="true"></span>
|
||||
{description || ''}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRightIcon
|
||||
className={classNames(
|
||||
iconClasses.chevron,
|
||||
'mr-4 transition-all group-hover:translate-x-3'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Schema } from '@markdoc/markdoc';
|
||||
|
||||
export const callToAction: Schema = {
|
||||
// 'Display content in a large button.',
|
||||
render: 'CallToAction',
|
||||
attributes: {
|
||||
url: {
|
||||
// 'The url to link to',
|
||||
type: 'String',
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
// 'Title of the call to action',
|
||||
type: 'String',
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
// 'Description of the call to action. Defaults to an empty string',
|
||||
type: 'String',
|
||||
required: false,
|
||||
},
|
||||
icon: {
|
||||
// 'Icon displayed to the left of the call to actions. Defaults to the Nx icon',
|
||||
// Choose from the list in nx-dev/ui-markdoc/src/lib/icons.tsx
|
||||
type: 'String',
|
||||
required: false,
|
||||
},
|
||||
variant: {
|
||||
type: 'String',
|
||||
required: false,
|
||||
default: 'default',
|
||||
matches: ['default', 'gradient', 'inverted', 'gradient-alt', 'simple'],
|
||||
},
|
||||
size: {
|
||||
// 'Size of the call to action. Defaults to "sm".',
|
||||
type: 'String',
|
||||
required: false,
|
||||
default: 'sm',
|
||||
matches: ['sm', 'md', 'lg'],
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,167 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, type ReactNode, type ReactElement } from 'react';
|
||||
import cx from 'classnames';
|
||||
import {
|
||||
ChevronRightIcon,
|
||||
ChevronDownIcon,
|
||||
CheckCircleIcon,
|
||||
ExclamationCircleIcon,
|
||||
HandRaisedIcon,
|
||||
InformationCircleIcon,
|
||||
AcademicCapIcon,
|
||||
MegaphoneIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
|
||||
type CalloutType =
|
||||
| 'announcement'
|
||||
| 'caution'
|
||||
| 'check'
|
||||
| 'deepdive'
|
||||
| 'note'
|
||||
| 'warning';
|
||||
|
||||
const typeMap: Record<
|
||||
CalloutType,
|
||||
{
|
||||
icon: ReactElement;
|
||||
backgroundColor: string;
|
||||
borderColor: string;
|
||||
titleColor: string;
|
||||
textColor: string;
|
||||
}
|
||||
> = {
|
||||
note: {
|
||||
icon: (
|
||||
<InformationCircleIcon
|
||||
className="h-5 w-5 text-zinc-500 dark:text-zinc-300"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
),
|
||||
backgroundColor: 'bg-zinc-50 dark:bg-zinc-800/40',
|
||||
borderColor: 'border-zinc-200 dark:border-zinc-700',
|
||||
titleColor: 'text-zinc-600 dark:text-zinc-300',
|
||||
textColor: 'text-zinc-700 dark:text-zinc-400',
|
||||
},
|
||||
announcement: {
|
||||
icon: (
|
||||
<MegaphoneIcon
|
||||
className="h-5 w-5 text-blue-500 dark:text-blue-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
),
|
||||
backgroundColor: 'bg-blue-50 dark:bg-blue-900/30',
|
||||
borderColor: 'border-blue-200 dark:border-blue-800',
|
||||
titleColor: 'text-blue-600 dark:text-blue-400',
|
||||
textColor: 'text-blue-700 dark:text-blue-600',
|
||||
},
|
||||
warning: {
|
||||
icon: (
|
||||
<ExclamationCircleIcon
|
||||
className="h-5 w-5 text-yellow-500 dark:text-yellow-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
),
|
||||
backgroundColor: 'bg-yellow-50 dark:bg-yellow-900/30',
|
||||
borderColor: 'border-yellow-200 dark:border-yellow-800',
|
||||
titleColor: 'text-yellow-600 dark:text-yellow-400',
|
||||
textColor: 'text-yellow-700 dark:text-yellow-600',
|
||||
},
|
||||
check: {
|
||||
icon: (
|
||||
<CheckCircleIcon
|
||||
className="h-5 w-5 text-green-500 dark:text-green-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
),
|
||||
backgroundColor: 'bg-green-50 dark:bg-green-900/30',
|
||||
borderColor: 'border-green-200 dark:border-green-800',
|
||||
titleColor: 'text-green-600 dark:text-green-400',
|
||||
textColor: 'text-green-700 dark:text-green-600',
|
||||
},
|
||||
caution: {
|
||||
icon: (
|
||||
<HandRaisedIcon className="h-5 w-5 text-red-500" aria-hidden="true" />
|
||||
),
|
||||
backgroundColor: 'bg-red-50 dark:bg-red-900/30',
|
||||
borderColor: 'border-red-200 dark:border-red-800',
|
||||
titleColor: 'text-red-600 dark:text-red-400',
|
||||
textColor: 'text-red-700 dark:text-red-600',
|
||||
},
|
||||
deepdive: {
|
||||
icon: (
|
||||
<AcademicCapIcon className="h-5 w-5 text-blue-500" aria-hidden="true" />
|
||||
),
|
||||
backgroundColor: 'bg-blue-50 dark:bg-blue-900/30',
|
||||
borderColor: 'border-blue-200 dark:border-blue-800',
|
||||
titleColor: 'text-white-600 dark:text-white-400',
|
||||
textColor: 'text-white-700 dark:text-white-600',
|
||||
},
|
||||
};
|
||||
|
||||
export type CalloutProps = {
|
||||
title: string;
|
||||
type: CalloutType;
|
||||
children: ReactNode;
|
||||
expanded?: boolean;
|
||||
};
|
||||
|
||||
export function Callout({
|
||||
title,
|
||||
type,
|
||||
children,
|
||||
expanded = false,
|
||||
}: CalloutProps): ReactElement {
|
||||
const [isOpen, setIsOpen] = useState(type !== 'deepdive' || expanded);
|
||||
const ui = typeMap[type] || typeMap.note;
|
||||
const isCollapsible = type === 'deepdive';
|
||||
|
||||
useEffect(() => {
|
||||
if (isCollapsible) {
|
||||
setIsOpen(expanded);
|
||||
}
|
||||
}, [expanded, isCollapsible]);
|
||||
|
||||
const toggleOpen = () => {
|
||||
if (isCollapsible) {
|
||||
setIsOpen(!isOpen);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
'not-content callout my-6 overflow-hidden rounded-md border',
|
||||
ui.borderColor,
|
||||
ui.backgroundColor
|
||||
)}
|
||||
>
|
||||
<div
|
||||
onClick={toggleOpen}
|
||||
className={cx(
|
||||
'flex w-full items-center justify-between p-4',
|
||||
'transition-colors duration-200 hover:bg-opacity-80',
|
||||
{ 'cursor-pointer': isCollapsible }
|
||||
)}
|
||||
>
|
||||
<span className="flex items-center">
|
||||
<span className="flex-shrink-0">{ui.icon}</span>
|
||||
<span className={cx('ml-3 text-sm font-medium', ui.titleColor)}>
|
||||
{title}
|
||||
</span>
|
||||
</span>
|
||||
{isCollapsible &&
|
||||
(isOpen ? (
|
||||
<ChevronDownIcon className="h-5 w-5" />
|
||||
) : (
|
||||
<ChevronRightIcon className="h-5 w-5" />
|
||||
))}
|
||||
</div>
|
||||
{isOpen && (
|
||||
<div className="px-4 pb-4 pt-0">
|
||||
<span className={cx('prose-sm block', ui.textColor)}>{children}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Schema } from '@markdoc/markdoc';
|
||||
|
||||
export const callout: Schema = {
|
||||
render: 'Callout',
|
||||
children: ['paragraph', 'tag', 'list'],
|
||||
attributes: {
|
||||
type: {
|
||||
type: 'String',
|
||||
default: 'note',
|
||||
matches: [
|
||||
'announcement',
|
||||
'caution',
|
||||
'check',
|
||||
'note',
|
||||
'warning',
|
||||
'deepdive',
|
||||
],
|
||||
errorLevel: 'critical',
|
||||
},
|
||||
title: {
|
||||
type: 'String',
|
||||
required: true,
|
||||
},
|
||||
expanded: {
|
||||
type: 'Boolean',
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,242 @@
|
||||
import {
|
||||
ArrowRightCircleIcon,
|
||||
ArrowTopRightOnSquareIcon,
|
||||
DocumentIcon,
|
||||
PlayCircleIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { type Framework, frameworkIcons } from '@nx/graph-ui-icons';
|
||||
import * as nxDevIcons from '@nx/nx-dev-ui-icons';
|
||||
import * as heroIcons from '@heroicons/react/24/outline';
|
||||
|
||||
import { cx } from '@nx/nx-dev-ui-primitives';
|
||||
import { ReactNode } from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
const colsClasses: Record<number, string> = {
|
||||
1: 'grid-cols-1',
|
||||
2: 'grid-cols-2',
|
||||
3: 'grid-cols-3',
|
||||
4: 'grid-cols-4',
|
||||
5: 'grid-cols-5',
|
||||
6: 'grid-cols-6',
|
||||
7: 'grid-cols-7',
|
||||
8: 'grid-cols-8',
|
||||
};
|
||||
const smColsClasses: Record<number, string> = {
|
||||
1: 'sm:grid-cols-1',
|
||||
2: 'sm:grid-cols-2',
|
||||
3: 'sm:grid-cols-3',
|
||||
4: 'sm:grid-cols-4',
|
||||
5: 'sm:grid-cols-5',
|
||||
6: 'sm:grid-cols-6',
|
||||
7: 'sm:grid-cols-7',
|
||||
8: 'sm:grid-cols-8',
|
||||
};
|
||||
const mdColsClasses: Record<number, string> = {
|
||||
1: 'md:grid-cols-1',
|
||||
2: 'md:grid-cols-2',
|
||||
3: 'md:grid-cols-3',
|
||||
4: 'md:grid-cols-4',
|
||||
5: 'md:grid-cols-5',
|
||||
6: 'md:grid-cols-6',
|
||||
7: 'md:grid-cols-7',
|
||||
8: 'md:grid-cols-8',
|
||||
};
|
||||
const lgColsClasses: Record<number, string> = {
|
||||
1: 'lg:grid-cols-1',
|
||||
2: 'lg:grid-cols-2',
|
||||
3: 'lg:grid-cols-3',
|
||||
4: 'lg:grid-cols-4',
|
||||
5: 'lg:grid-cols-5',
|
||||
6: 'lg:grid-cols-6',
|
||||
7: 'lg:grid-cols-7',
|
||||
8: 'lg:grid-cols-8',
|
||||
};
|
||||
|
||||
export type CardsProps = {
|
||||
cols: number;
|
||||
smCols: number;
|
||||
mdCols: number;
|
||||
lgCols: number;
|
||||
children: ReactNode;
|
||||
moreLink?: string;
|
||||
};
|
||||
|
||||
export function Cards({
|
||||
cols = 2,
|
||||
smCols = cols,
|
||||
mdCols = smCols,
|
||||
lgCols = mdCols,
|
||||
children,
|
||||
moreLink,
|
||||
}: CardsProps): JSX.Element {
|
||||
// <div className="mt-8 grid grid-cols-2 lg:grid-cols-5 md:grid-cols-3 sm:grid-cols-2 gap-4">
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
'not-content mt-8 grid gap-4',
|
||||
colsClasses[cols] || '',
|
||||
smColsClasses[smCols] || '',
|
||||
mdColsClasses[mdCols] || '',
|
||||
lgColsClasses[lgCols] || ''
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
{moreLink && (
|
||||
<div className="col-span-full mt-2 flex justify-end">
|
||||
<Link
|
||||
className="group flex items-center border-transparent px-4 py-0 text-sm font-semibold whitespace-nowrap no-underline transition-all duration-200 ease-in-out hover:text-zinc-900 dark:hover:text-blue-400"
|
||||
href={moreLink}
|
||||
prefetch={false}
|
||||
>
|
||||
Browse more
|
||||
<span
|
||||
className="pl-1 transition-all duration-200 ease-in-out group-hover:translate-x-1"
|
||||
aria-hidden="true"
|
||||
>
|
||||
→
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function callIfFunction(fn: any, props: { [key: string]: string } = {}) {
|
||||
if (typeof fn === 'function') {
|
||||
return fn(props);
|
||||
}
|
||||
return fn;
|
||||
}
|
||||
|
||||
export type LinkCardProps = {
|
||||
title: string;
|
||||
type: string;
|
||||
icon: string; // Can be either a component name or a direct image URL
|
||||
url: string;
|
||||
appearance?: 'default' | 'small';
|
||||
};
|
||||
|
||||
export function LinkCard({
|
||||
title,
|
||||
type,
|
||||
icon,
|
||||
url,
|
||||
appearance = 'default',
|
||||
}: LinkCardProps): JSX.Element {
|
||||
return (
|
||||
<Link
|
||||
key={title}
|
||||
href={url}
|
||||
className="no-prose relative col-span-1 mx-auto flex w-full max-w-md flex-col items-center rounded-md border border-zinc-200 bg-zinc-50/40 p-4 text-center font-semibold shadow-xs transition focus-within:ring-2 focus-within:ring-blue-500 focus-within:ring-offset-2 hover:bg-zinc-100 dark:border-zinc-800/40 dark:bg-zinc-800/60 dark:hover:bg-zinc-800"
|
||||
style={{ textDecorationLine: 'none' }}
|
||||
prefetch={false}
|
||||
>
|
||||
{icon && (
|
||||
<div
|
||||
className={cx(
|
||||
'mb-2 flex h-24 w-24 items-center justify-center rounded-lg text-black dark:text-white',
|
||||
{
|
||||
'h-12 w-12': appearance === 'small',
|
||||
}
|
||||
)}
|
||||
>
|
||||
{icon.startsWith('/') ? (
|
||||
<img
|
||||
src={icon}
|
||||
alt={title}
|
||||
className="h-full w-full object-contain"
|
||||
/>
|
||||
) : (
|
||||
frameworkIcons[icon as Framework]?.image ||
|
||||
callIfFunction(nxDevIcons[icon as keyof typeof nxDevIcons]) ||
|
||||
callIfFunction(
|
||||
(heroIcons[icon as keyof typeof heroIcons] as any)?.render,
|
||||
{ className: 'w-full h-full' }
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={cx({ 'pt-4': !!icon }, { 'pt-2': appearance === 'small' })}
|
||||
>
|
||||
{appearance === 'small' && type ? null : (
|
||||
<div className="mb-1 text-xs font-medium text-zinc-600 uppercase dark:text-zinc-300">
|
||||
{type}
|
||||
</div>
|
||||
)}
|
||||
<h3
|
||||
className={cx(
|
||||
'm-0 text-lg font-semibold text-zinc-900 dark:text-white',
|
||||
{ 'text-sm font-normal': appearance === 'small' }
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</h3>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export type CardProps = {
|
||||
title: string;
|
||||
description: string;
|
||||
type: 'documentation' | 'external' | 'video';
|
||||
url: string;
|
||||
};
|
||||
|
||||
export function Card({
|
||||
description,
|
||||
title,
|
||||
type = 'documentation',
|
||||
url,
|
||||
}: CardProps): JSX.Element {
|
||||
const iconMap = {
|
||||
documentation: <DocumentIcon className="mr-3 h-5 w-5 shrink-0" />,
|
||||
external: <ArrowTopRightOnSquareIcon className="mr-3 h-5 w-5 shrink-0" />,
|
||||
video: <PlayCircleIcon className="mr-3 h-5 w-5 shrink-0" />,
|
||||
};
|
||||
const youtubeRegex =
|
||||
/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/gi.exec(
|
||||
url
|
||||
);
|
||||
const hasYoutubeId = !!youtubeRegex ? youtubeRegex[1] : '';
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={title}
|
||||
href={url}
|
||||
title={title}
|
||||
className="not-content group flex flex-col items-stretch rounded-md border border-zinc-200 bg-zinc-50/40 text-sm no-underline shadow-xs transition focus-within:ring-2 focus-within:ring-blue-500 focus-within:ring-offset-2 hover:bg-zinc-50 dark:border-zinc-800/40 dark:bg-zinc-800/60 dark:hover:bg-zinc-800"
|
||||
prefetch={false}
|
||||
>
|
||||
{!!hasYoutubeId && (
|
||||
<div className="max-h-24">
|
||||
<img
|
||||
className="!m-0 h-full !w-full rounded-t-md bg-black object-contain"
|
||||
alt="Youtube Link"
|
||||
src={`https://img.youtube.com/vi/${hasYoutubeId}/default.jpg`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="relative flex flex-col p-3 pr-8">
|
||||
<h3 className="m-0 flex items-center text-base font-bold text-zinc-900 dark:text-white">
|
||||
<span className="absolute inset-0" aria-hidden="true"></span>
|
||||
{!hasYoutubeId ? iconMap[type] : null}
|
||||
{title}
|
||||
</h3>
|
||||
{description ? (
|
||||
<p className="mt-2 w-full text-sm font-normal text-zinc-600 no-underline dark:text-zinc-300">
|
||||
{description}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{/*HOVER ICON*/}
|
||||
<span className="absolute top-1/2 right-2 -translate-x-2 -translate-y-2.5 opacity-0 transition-all group-hover:translate-x-0 group-hover:opacity-100">
|
||||
<ArrowRightCircleIcon className="h-5 w-5" />
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { Schema } from '@markdoc/markdoc';
|
||||
|
||||
export const cards: Schema = {
|
||||
render: 'Cards',
|
||||
attributes: {
|
||||
cols: {
|
||||
type: 'Number',
|
||||
required: true,
|
||||
},
|
||||
smCols: {
|
||||
type: 'Number',
|
||||
required: true,
|
||||
},
|
||||
mdCols: {
|
||||
type: 'Number',
|
||||
required: true,
|
||||
},
|
||||
lgCols: {
|
||||
type: 'Number',
|
||||
required: true,
|
||||
},
|
||||
moreLink: {
|
||||
type: 'String',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const linkCard: Schema = {
|
||||
render: 'LinkCard',
|
||||
attributes: {
|
||||
title: {
|
||||
type: 'String',
|
||||
required: true,
|
||||
},
|
||||
type: {
|
||||
type: 'String',
|
||||
required: true,
|
||||
},
|
||||
icon: {
|
||||
type: 'String',
|
||||
required: false,
|
||||
},
|
||||
url: {
|
||||
type: 'String',
|
||||
default: '',
|
||||
},
|
||||
appearance: {
|
||||
type: 'String',
|
||||
default: 'default',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const card: Schema = {
|
||||
render: 'Card',
|
||||
attributes: {
|
||||
title: {
|
||||
type: 'String',
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
type: 'String',
|
||||
default: '',
|
||||
},
|
||||
type: {
|
||||
type: 'String',
|
||||
default: 'documentation',
|
||||
},
|
||||
url: {
|
||||
type: 'String',
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
'use client';
|
||||
import { useRef } from 'react';
|
||||
|
||||
export function ClientVideo({
|
||||
src,
|
||||
alt,
|
||||
showControls = false,
|
||||
autoPlay = true,
|
||||
loop: explicitLoop,
|
||||
}: {
|
||||
src: string;
|
||||
alt: string;
|
||||
showControls?: boolean;
|
||||
autoPlay?: boolean;
|
||||
loop?: boolean;
|
||||
}): JSX.Element {
|
||||
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||
const loop = explicitLoop ?? autoPlay;
|
||||
|
||||
return (
|
||||
<video
|
||||
ref={videoRef}
|
||||
autoPlay={autoPlay}
|
||||
muted
|
||||
loop={loop}
|
||||
playsInline
|
||||
controls={showControls}
|
||||
className="m-0 p-0"
|
||||
>
|
||||
<source src={src} type="video/mp4" />
|
||||
<div className="p-4 text-center">
|
||||
<p className="pb-3 font-bold">
|
||||
Your browser does not support the video tag. Here is a description of
|
||||
the video:
|
||||
</p>
|
||||
<p>{alt}</p>
|
||||
</div>
|
||||
</video>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Schema } from '@markdoc/markdoc';
|
||||
|
||||
export const courseVideo: Schema = {
|
||||
render: 'CourseVideo',
|
||||
attributes: {
|
||||
src: {
|
||||
type: 'String',
|
||||
required: true,
|
||||
},
|
||||
courseTitle: {
|
||||
type: 'String',
|
||||
required: true,
|
||||
},
|
||||
courseUrl: {
|
||||
type: 'String',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
import { ChevronRightIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
export type GithubRepositoryProps = {
|
||||
url: string;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
export function GithubRepository({
|
||||
url,
|
||||
title,
|
||||
}: GithubRepositoryProps): JSX.Element {
|
||||
return (
|
||||
<div className="not-content not-prose group relative mx-auto my-12 flex w-full max-w-md items-center gap-3 overflow-hidden rounded-lg bg-zinc-50 shadow-md transition hover:text-white dark:bg-zinc-800/60">
|
||||
<div className="absolute inset-0 z-0 w-2 bg-blue-500 transition-all duration-150 group-hover:w-full dark:bg-blue-500"></div>
|
||||
<div className="w-2 bg-blue-500 dark:bg-blue-500"></div>
|
||||
|
||||
<div className="z-10 flex flex-grow items-center py-3">
|
||||
<svg
|
||||
className="h-10 w-10 rounded-full object-cover"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"
|
||||
></path>
|
||||
</svg>
|
||||
|
||||
<div className="mx-3">
|
||||
<p>
|
||||
{title ? title : 'Example repository'}
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="block text-sm font-medium text-white no-underline opacity-80"
|
||||
>
|
||||
<span className="absolute inset-0" aria-hidden="true"></span>
|
||||
{url.replace(/^.*\/\/[^\/]+/, '')}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRightIcon className="mr-4 h-6 w-6 transition-all group-hover:translate-x-3" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Schema } from '@markdoc/markdoc';
|
||||
|
||||
export const githubRepository: Schema = {
|
||||
render: 'GithubRepository',
|
||||
description: 'Display the provided repository link into a clickable button.',
|
||||
attributes: {
|
||||
url: {
|
||||
type: 'String',
|
||||
required: true,
|
||||
description: 'The url of the GitHub repository',
|
||||
},
|
||||
title: {
|
||||
type: 'String',
|
||||
required: false,
|
||||
description:
|
||||
'Title of the repository, otherwise it will default to "Example repository"',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,137 @@
|
||||
'use client';
|
||||
import { useTheme } from '@nx/nx-dev-ui-theme';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { ReactElement, useEffect, useState } from 'react';
|
||||
|
||||
export function Loading() {
|
||||
return (
|
||||
<div className="flex h-[450px] w-full items-center justify-center">
|
||||
<div
|
||||
className="spinner-border inline-block h-8 w-8 animate-spin rounded-full border-4 border-zinc-200 border-r-zinc-400 dark:border-zinc-700 dark:border-r-zinc-500"
|
||||
role="status"
|
||||
>
|
||||
<span className="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* dynamic() can't be used inside of React rendering as it needs to be marked
|
||||
* in the top level of the module for preloading to work, similar to React.lazy.
|
||||
*/
|
||||
const NxDevProjectGraph = dynamic(
|
||||
() =>
|
||||
import('../graphs/project-graph').then(
|
||||
(module) => module.NxDevProjectGraph
|
||||
),
|
||||
{ ssr: false, loading: () => <Loading /> }
|
||||
);
|
||||
const NxDevTaskGraph = dynamic(
|
||||
() => import('../graphs/task-graph').then((module) => module.NxDevTaskGraph),
|
||||
{ ssr: false, loading: () => <Loading /> }
|
||||
);
|
||||
|
||||
export type GraphProps = {
|
||||
height: string;
|
||||
title: string;
|
||||
type: 'project' | 'task';
|
||||
jsonFile?: string;
|
||||
children: ReactElement;
|
||||
isAstro?: boolean;
|
||||
astroRawData?: string;
|
||||
};
|
||||
|
||||
function safeParse(jsonString: string) {
|
||||
try {
|
||||
return JSON.parse(jsonString);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function Graph({
|
||||
height,
|
||||
title,
|
||||
type,
|
||||
jsonFile,
|
||||
children,
|
||||
astroRawData,
|
||||
}: GraphProps): JSX.Element {
|
||||
const [theme] = useTheme();
|
||||
const [parsedProps, setParsedProps] = useState<any>(
|
||||
astroRawData ? safeParse(astroRawData) : null
|
||||
);
|
||||
const getData = async (path: string) => {
|
||||
const response = await fetch('/documentation/' + path, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
});
|
||||
setParsedProps(await response.json());
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (jsonFile) {
|
||||
getData(jsonFile);
|
||||
}
|
||||
}, [jsonFile, setParsedProps]);
|
||||
if (!jsonFile && !parsedProps) {
|
||||
if (!astroRawData) {
|
||||
return (
|
||||
<div className="no-prose block rounded-md bg-red-50 p-4 text-red-700 ring-1 ring-red-100 dark:bg-red-900/30 dark:text-red-600 dark:ring-red-900">
|
||||
<p className="mb-4">
|
||||
No JSON provided for graph, use JSON code fence to embed data for
|
||||
the graph.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// If raw data is passed but props are not set, it must be invalid JSON
|
||||
if (astroRawData && !parsedProps) {
|
||||
return (
|
||||
<div className="not-prose block rounded-md bg-red-50 p-4 text-red-700 ring-1 ring-red-100 dark:bg-red-900/30 dark:text-red-600 dark:ring-red-900">
|
||||
<p className="mb-4">Could not parse JSON for graph:</p>
|
||||
<pre className="p-4 text-sm">{astroRawData}</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return parsedProps ? (
|
||||
<div className="not-content mt-4 w-full place-content-center overflow-hidden rounded-md ring-1 ring-zinc-200 dark:ring-zinc-700">
|
||||
{title ? (
|
||||
<div className="relative flex justify-center border-b border-zinc-200 bg-zinc-100/50 p-2 font-bold dark:border-zinc-700 dark:bg-zinc-700/50">
|
||||
{title}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div style={{ height }}>
|
||||
{type === 'project' ? (
|
||||
<NxDevProjectGraph
|
||||
theme={theme}
|
||||
projects={parsedProps.projects}
|
||||
dependencies={parsedProps.dependencies}
|
||||
affectedProjects={parsedProps.affectedProjectIds}
|
||||
enableContextMenu={parsedProps.enableTooltips}
|
||||
composite={parsedProps.composite}
|
||||
showAffectedWithNodes={parsedProps.showAffectedWithNodes}
|
||||
/>
|
||||
) : (
|
||||
<NxDevTaskGraph
|
||||
theme={theme}
|
||||
projects={parsedProps.projects}
|
||||
taskGraph={parsedProps.taskGraph}
|
||||
taskId={parsedProps.taskId}
|
||||
taskIds={parsedProps.taskIds}
|
||||
enableContextMenu={parsedProps.enableTooltips}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Loading />
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Schema } from '@markdoc/markdoc';
|
||||
|
||||
export const graph: Schema = {
|
||||
render: 'Graph',
|
||||
children: [],
|
||||
|
||||
attributes: {
|
||||
jsonFile: {
|
||||
type: 'String',
|
||||
},
|
||||
title: {
|
||||
type: 'String',
|
||||
},
|
||||
type: {
|
||||
type: 'String',
|
||||
matches: ['project', 'task'],
|
||||
default: 'project',
|
||||
},
|
||||
height: {
|
||||
type: 'String',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
export type IframeProps = {
|
||||
src: string;
|
||||
title: string;
|
||||
width?: string;
|
||||
};
|
||||
|
||||
export function Iframe(props: IframeProps) {
|
||||
return (
|
||||
<iframe
|
||||
{...props}
|
||||
title={props.title}
|
||||
frameBorder="0"
|
||||
className="not-content rounded-lg border border-zinc-200 shadow-lg dark:border-zinc-700"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Schema } from '@markdoc/markdoc';
|
||||
|
||||
export const iframe: Schema = {
|
||||
render: 'Iframe',
|
||||
attributes: {
|
||||
src: {
|
||||
type: 'String',
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: 'String',
|
||||
required: true,
|
||||
},
|
||||
width: {
|
||||
type: 'String',
|
||||
default: '50%',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
import { ChevronRightIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
export const InstallNxConsole = () => (
|
||||
<div className="not-content my-12 grid grid-cols-1 gap-6 lg:grid-cols-2 lg:gap-16">
|
||||
{/* VSCode */}
|
||||
<div className="not-prose group relative mx-auto flex w-full max-w-md items-center gap-4 overflow-hidden rounded-lg bg-zinc-50 shadow-md transition hover:text-white dark:bg-zinc-800/60">
|
||||
<div className="absolute inset-0 z-0 w-2 bg-blue-500 transition-all duration-150 group-hover:w-full dark:bg-blue-500" />
|
||||
<div className="w-2 bg-blue-500 dark:bg-blue-500" />
|
||||
|
||||
<div className="z-10 flex flex-grow items-center gap-4 py-3">
|
||||
<svg
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
className="h-10 w-10 flex-shrink-0 rounded-full object-cover"
|
||||
>
|
||||
<title>Visual Studio Code</title>
|
||||
<path d="M23.15 2.587L18.21.21a1.494 1.494 0 0 0-1.705.29l-9.46 8.63-4.12-3.128a.999.999 0 0 0-1.276.057L.327 7.261A1 1 0 0 0 .326 8.74L3.899 12 .326 15.26a1 1 0 0 0 .001 1.479L1.65 17.94a.999.999 0 0 0 1.276.057l4.12-3.128 9.46 8.63a1.492 1.492 0 0 0 1.704.29l4.942-2.377A1.5 1.5 0 0 0 24 20.06V3.939a1.5 1.5 0 0 0-.85-1.352zm-5.146 14.861L10.826 12l7.178-5.448v10.896z" />
|
||||
</svg>
|
||||
|
||||
<div>
|
||||
<p>
|
||||
Install Nx Console for VSCode
|
||||
<a
|
||||
href="https://marketplace.visualstudio.com/items?itemName=nrwl.angular-console"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="block text-xs font-medium text-black no-underline opacity-80 dark:text-white"
|
||||
>
|
||||
<span className="absolute inset-0" aria-hidden="true"></span>
|
||||
The official VSCode extension for Nx.
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRightIcon className="mr-4 h-6 w-6 flex-shrink-0 transition-all group-hover:translate-x-3" />
|
||||
</div>
|
||||
{/* JetBrains */}
|
||||
<div className="not-prose group relative mx-auto flex w-full max-w-md items-center gap-4 overflow-hidden rounded-lg bg-zinc-50 shadow-md transition hover:text-white dark:bg-zinc-800/60">
|
||||
<div className="absolute inset-0 z-0 w-2 bg-blue-500 transition-all duration-150 group-hover:w-full dark:bg-blue-500" />
|
||||
<div className="w-2 bg-blue-500 dark:bg-blue-500" />
|
||||
|
||||
<div className="z-10 flex flex-grow items-center gap-4 py-3">
|
||||
<svg
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
className="h-10 w-10 flex-shrink-0 rounded-xs object-cover"
|
||||
>
|
||||
<title>IntelliJ IDEA</title>
|
||||
<path d="M0 0v24h24V0zm3.723 3.111h5v1.834h-1.39v6.277h1.39v1.834h-5v-1.834h1.444V4.945H3.723zm11.055 0H17v6.5c0 .612-.055 1.111-.222 1.556-.167.444-.39.777-.723 1.11-.277.279-.666.557-1.11.668a3.933 3.933 0 0 1-1.445.278c-.778 0-1.444-.167-1.944-.445a4.81 4.81 0 0 1-1.279-1.056l1.39-1.555c.277.334.555.555.833.722.277.167.611.278.945.278.389 0 .721-.111 1-.389.221-.278.333-.667.333-1.278zM2.222 19.5h9V21h-9z" />
|
||||
</svg>
|
||||
|
||||
<div>
|
||||
<p>
|
||||
Install Nx Console for JetBrains
|
||||
<a
|
||||
href="https://plugins.jetbrains.com/plugin/21060-nx-console"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="block text-xs font-medium text-black no-underline opacity-80 dark:text-white"
|
||||
>
|
||||
<span className="absolute inset-0" aria-hidden="true"></span>
|
||||
Available for WebStorm, IntelliJ IDEA Ultimate and more!
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRightIcon className="mr-4 h-6 w-6 flex-shrink-0 transition-all group-hover:translate-x-3" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Schema } from '@markdoc/markdoc';
|
||||
|
||||
export const installNxConsole: Schema = {
|
||||
render: 'InstallNxConsole',
|
||||
attributes: {},
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
'use client';
|
||||
|
||||
/* this is a separate component s.t. it can be client-side only to avoid hydration errors*/
|
||||
|
||||
import { ButtonLink } from '@nx/nx-dev-ui-common';
|
||||
import { sendCustomEventViaGtm } from '@nx/nx-dev-feature-analytics';
|
||||
|
||||
export function MetricsCTA() {
|
||||
return (
|
||||
<div className="not-prose flex flex-col space-y-3">
|
||||
<h3 className="text-lg font-semibold text-zinc-900 dark:text-white">
|
||||
Ready to get started?
|
||||
</h3>
|
||||
<ButtonLink
|
||||
href="/contact/sales"
|
||||
title="Reach out"
|
||||
variant="contrast"
|
||||
size="default"
|
||||
onClick={() =>
|
||||
sendCustomEventViaGtm(
|
||||
'contact-sales-click',
|
||||
'metrics-cta-customer-success',
|
||||
'blog'
|
||||
)
|
||||
}
|
||||
>
|
||||
Reach out
|
||||
</ButtonLink>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
'use client';
|
||||
|
||||
import { MetricsCTA } from './metrics-cta';
|
||||
import { Metric } from './metrics.schema';
|
||||
|
||||
export interface MetricsProps {
|
||||
metrics: Metric[];
|
||||
variant?: 'horizontal' | 'vertical';
|
||||
}
|
||||
|
||||
export function Metrics({
|
||||
metrics,
|
||||
variant = 'vertical',
|
||||
}: MetricsProps): JSX.Element {
|
||||
if (variant === 'horizontal') {
|
||||
return (
|
||||
<div className="not-content mx-auto w-full max-w-none">
|
||||
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
|
||||
{metrics.map((metric, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex flex-col items-center space-y-1 text-center"
|
||||
>
|
||||
<div className="text-2xl font-bold text-zinc-700 dark:text-zinc-200">
|
||||
{metric.value}
|
||||
</div>
|
||||
<div className="text-sm leading-snug text-zinc-500 dark:text-zinc-400">
|
||||
{metric.label}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="not-content space-y-8">
|
||||
{metrics.map((metric, index) => (
|
||||
<div key={index} className="flex flex-col space-y-2">
|
||||
<div className="text-4xl font-bold text-zinc-700 dark:text-zinc-200">
|
||||
{metric.value}
|
||||
</div>
|
||||
<div className="text-sm leading-snug text-zinc-500 dark:text-zinc-400">
|
||||
{metric.label}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<MetricsCTA />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Schema } from '@markdoc/markdoc';
|
||||
|
||||
export interface Metric {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export const metrics: Schema = {
|
||||
render: 'Metrics',
|
||||
attributes: {
|
||||
metrics: {
|
||||
type: 'Array',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,39 @@
|
||||
import { Schema } from '@markdoc/markdoc';
|
||||
|
||||
export const personas: Schema = {
|
||||
render: 'Personas',
|
||||
description: 'Display multiple persons in a grid 2x2',
|
||||
};
|
||||
export const persona: Schema = {
|
||||
render: 'Persona',
|
||||
description: 'Display the enclosed content in a persona box',
|
||||
children: ['paragraph', 'tag', 'list'],
|
||||
attributes: {
|
||||
title: {
|
||||
type: 'String',
|
||||
description: 'The title displayed at the top of the persona callout.',
|
||||
},
|
||||
type: {
|
||||
type: 'String',
|
||||
default: 'integrated',
|
||||
required: true,
|
||||
matches: [
|
||||
'cache',
|
||||
'distribute',
|
||||
'javascript',
|
||||
'lerna',
|
||||
'react',
|
||||
'angular',
|
||||
'integrated',
|
||||
],
|
||||
errorLevel: 'critical',
|
||||
description: 'Controls the icon of the persona.',
|
||||
},
|
||||
url: {
|
||||
type: 'String',
|
||||
required: true,
|
||||
errorLevel: 'critical',
|
||||
description: 'Link to navigate to when the card is clicked.',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import Link from 'next/link';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export type PillProps = {
|
||||
url: string;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export function Pill({ url, children, ...rest }: PillProps): JSX.Element {
|
||||
return (
|
||||
<span className="not-content group relative mr-2 mb-2 inline-flex rounded-md border border-zinc-200 bg-zinc-50/40 text-sm shadow-xs transition focus-within:ring-2 focus-within:ring-blue-500 focus-within:ring-offset-2 hover:bg-zinc-50 dark:border-zinc-800/40 dark:bg-zinc-800/60 dark:hover:bg-zinc-800">
|
||||
<span className="flex flex-col p-3">
|
||||
<Link
|
||||
href={url}
|
||||
className="flex items-center font-semibold no-underline group-hover:underline"
|
||||
prefetch={false}
|
||||
>
|
||||
<span className="absolute inset-0" aria-hidden="true"></span>
|
||||
{children}
|
||||
</Link>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Schema } from '@markdoc/markdoc';
|
||||
|
||||
export const pill: Schema = {
|
||||
render: 'Pill',
|
||||
attributes: {
|
||||
url: {
|
||||
type: 'String',
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,147 @@
|
||||
'use client';
|
||||
import {
|
||||
createRef,
|
||||
JSX,
|
||||
ReactElement,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import {
|
||||
ProjectDetails as ProjectDetailsUi,
|
||||
ExpandedTargetsProvider,
|
||||
} from '@nx/graph-internal-ui-project-details';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function Loading() {
|
||||
return (
|
||||
<div className="flex h-[450px] w-full items-center justify-center">
|
||||
<div
|
||||
className="spinner-border inline-block h-8 w-8 animate-spin rounded-full border-4 border-zinc-200 border-r-zinc-400 dark:border-zinc-700 dark:border-r-zinc-500"
|
||||
role="status"
|
||||
>
|
||||
<span className="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function safeParse(jsonString: string) {
|
||||
try {
|
||||
return JSON.parse(jsonString);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export type ProjectDetailsProps = {
|
||||
height: string;
|
||||
title: string;
|
||||
jsonFile?: string;
|
||||
expandedTargets?: string[];
|
||||
children: ReactElement;
|
||||
astroRawData?: string;
|
||||
};
|
||||
|
||||
export function ProjectDetails({
|
||||
height,
|
||||
title,
|
||||
jsonFile,
|
||||
expandedTargets = [],
|
||||
children,
|
||||
astroRawData,
|
||||
}: ProjectDetailsProps): JSX.Element {
|
||||
const [parsedProps, setParsedProps] = useState<any>(
|
||||
astroRawData ? safeParse(astroRawData) : null
|
||||
);
|
||||
const elementRef = createRef<HTMLDivElement>();
|
||||
const getData = async (path: string) => {
|
||||
const response = await fetch('/documentation/' + path, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
});
|
||||
setParsedProps(await response.json());
|
||||
};
|
||||
useEffect(() => {
|
||||
if (jsonFile) {
|
||||
getData(jsonFile);
|
||||
}
|
||||
}, [jsonFile, setParsedProps]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (elementRef && expandedTargets?.length > 0) {
|
||||
const header = document.getElementById('target-' + expandedTargets[0]);
|
||||
const container = document.getElementById('project-details-container');
|
||||
if (header && container) {
|
||||
const groupHeaderOffset =
|
||||
header
|
||||
.closest('[id^="target-group-container-"]')
|
||||
?.querySelector('[id^="target-group-header-"]')
|
||||
?.getBoundingClientRect().height ?? 0;
|
||||
elementRef.current?.scrollTo({
|
||||
top:
|
||||
header?.getBoundingClientRect().top -
|
||||
container.getBoundingClientRect()?.top -
|
||||
groupHeaderOffset,
|
||||
behavior: 'instant',
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [elementRef, expandedTargets]);
|
||||
|
||||
if (!jsonFile && !parsedProps) {
|
||||
if (!astroRawData) {
|
||||
return (
|
||||
<div className="not-content no-prose my-6 block rounded-md bg-red-50 p-4 text-red-700 ring-1 ring-red-100 dark:bg-red-900/30 dark:text-red-600 dark:ring-red-900">
|
||||
<p className="mb-4">
|
||||
No JSON provided for graph, use JSON code fence to embed data for
|
||||
the graph.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// If raw data is passed but props are not set, it must be invalid JSON
|
||||
if (astroRawData && !parsedProps) {
|
||||
return (
|
||||
<div className="not-content not-prose my-6 block rounded-md bg-red-50 p-4 text-red-700 ring-1 ring-red-100 dark:bg-red-900/30 dark:text-red-600 dark:ring-red-900">
|
||||
<p className="mb-4">Could not parse JSON for graph:</p>
|
||||
<pre className="p-4 text-sm">{astroRawData}</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return parsedProps ? (
|
||||
<div className="not-content w-full place-content-center overflow-hidden rounded-md ring-1 ring-zinc-200 dark:ring-zinc-700">
|
||||
{title && (
|
||||
<div className="relative flex justify-center border-b border-zinc-200 bg-zinc-100/50 p-2 font-bold dark:border-zinc-700 dark:bg-zinc-700/50">
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
id="project-details-container"
|
||||
className={twMerge(
|
||||
'not-content not-prose',
|
||||
height && 'overflow-y-auto'
|
||||
)}
|
||||
style={{ height }}
|
||||
ref={elementRef}
|
||||
>
|
||||
<div className="m-4">
|
||||
<ExpandedTargetsProvider initialExpanededTargets={expandedTargets}>
|
||||
<ProjectDetailsUi
|
||||
project={parsedProps.project}
|
||||
sourceMap={parsedProps.sourceMap}
|
||||
variant="compact"
|
||||
/>
|
||||
</ExpandedTargetsProvider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Loading />
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Schema } from '@markdoc/markdoc';
|
||||
|
||||
export const projectDetails: Schema = {
|
||||
render: 'ProjectDetails',
|
||||
children: [],
|
||||
|
||||
attributes: {
|
||||
jsonFile: {
|
||||
type: 'String',
|
||||
},
|
||||
title: {
|
||||
type: 'String',
|
||||
},
|
||||
height: {
|
||||
type: 'String',
|
||||
},
|
||||
expandedTargets: {
|
||||
type: 'Array',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,185 @@
|
||||
'use client';
|
||||
import {
|
||||
createContext,
|
||||
ReactNode,
|
||||
useContext,
|
||||
useLayoutEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Schema } from '@markdoc/markdoc';
|
||||
import markdoc from '@markdoc/markdoc';
|
||||
import { Transition } from '@headlessui/react';
|
||||
import { Button } from '@nx/nx-dev-ui-common';
|
||||
import { XMarkIcon } from '@heroicons/react/24/outline';
|
||||
const { Tag } = markdoc;
|
||||
|
||||
interface VideoData {
|
||||
title: string;
|
||||
embedUrl: `https://www.youtube.com/embed/${string}`;
|
||||
}
|
||||
|
||||
export const shortEmbeds: Schema = {
|
||||
render: 'ShortEmbeds',
|
||||
attributes: {},
|
||||
transform(node, config) {
|
||||
const videoData = node
|
||||
.transformChildren(config)
|
||||
.filter((child) => child && child.name === 'ShortVideo')
|
||||
.map((short) =>
|
||||
typeof short === 'object'
|
||||
? {
|
||||
title: short?.attributes.title,
|
||||
embedUrl: short?.attributes.embedUrl,
|
||||
}
|
||||
: null
|
||||
);
|
||||
|
||||
return new Tag(this.render, { videoData }, node.transformChildren(config));
|
||||
},
|
||||
};
|
||||
|
||||
export const shortVideo: Schema = {
|
||||
render: 'ShortVideo',
|
||||
attributes: {
|
||||
title: {
|
||||
type: 'String',
|
||||
},
|
||||
embedUrl: {
|
||||
type: 'String',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const ShortEmbedContext = createContext<{
|
||||
current: VideoData | null;
|
||||
userInteraction: boolean;
|
||||
}>({ current: null, userInteraction: false });
|
||||
|
||||
export type ShortEmbedsProps = {
|
||||
videoData: VideoData[];
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export function ShortEmbeds({
|
||||
videoData,
|
||||
children,
|
||||
}: ShortEmbedsProps): JSX.Element | null {
|
||||
const [currentVideo, setCurrentVideo] = useState<VideoData>(videoData[0]);
|
||||
const [isShowing, setIsShowing] = useState(false);
|
||||
const [userInteraction, setUserInteraction] = useState(false);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const [wrapperWidth, viewHeight] = [
|
||||
document.querySelector('#wrapper')?.clientWidth,
|
||||
window.innerHeight,
|
||||
];
|
||||
const showShort =
|
||||
!!wrapperWidth && wrapperWidth > 800 && !!viewHeight && viewHeight > 750;
|
||||
|
||||
const t = setTimeout(() => setIsShowing(showShort), 500);
|
||||
return () => {
|
||||
t && clearTimeout(t);
|
||||
};
|
||||
}, [setIsShowing]);
|
||||
|
||||
if (!isShowing) return null;
|
||||
|
||||
return (
|
||||
<ShortEmbedContext.Provider
|
||||
value={{ current: currentVideo, userInteraction }}
|
||||
>
|
||||
<aside
|
||||
id="short-embed"
|
||||
className="not-content fixed right-5 bottom-5 z-50 w-80"
|
||||
>
|
||||
<Transition
|
||||
appear={true}
|
||||
show={isShowing}
|
||||
enter="transition-all duration-1000"
|
||||
enterFrom="opacity-0 translate-y-full"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition-all duration-1000"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-full"
|
||||
>
|
||||
<div className="coding relative mt-12 flex h-full w-full flex-col rounded-xl border border-zinc-200 bg-zinc-50 p-2 leading-normal text-zinc-800 subpixel-antialiased shadow-xl dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-200">
|
||||
<Button
|
||||
size="small"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setTimeout(() => setIsShowing(false), 500);
|
||||
}}
|
||||
className="absolute top-2 right-2"
|
||||
title="Close"
|
||||
>
|
||||
<XMarkIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
<h3 className="not-prose text-center">Relevant Videos</h3>
|
||||
<div className="grid w-full grid-cols-1 justify-items-center gap-4">
|
||||
{children}
|
||||
<div>
|
||||
<div className="pb-1 text-base font-medium">Continue with:</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
{videoData
|
||||
.filter(
|
||||
({ embedUrl }) => embedUrl !== currentVideo.embedUrl
|
||||
)
|
||||
.map((config) => {
|
||||
const ytUrlPath = config.embedUrl.split('/');
|
||||
const ytId = ytUrlPath[ytUrlPath.length - 1];
|
||||
const imgUrl = `https://img.youtube.com/vi/${ytId}/0.jpg`;
|
||||
return (
|
||||
<div
|
||||
key={ytId}
|
||||
onClick={() => {
|
||||
setUserInteraction(true);
|
||||
setCurrentVideo(config);
|
||||
}}
|
||||
className="flex h-24 overflow-hidden rounded-lg border border-zinc-200 bg-white/40 text-sm shadow-xs transition focus-within:ring-2 focus-within:ring-blue-500 focus-within:ring-offset-2 hover:cursor-pointer hover:bg-white dark:border-zinc-800/40 dark:bg-zinc-800/60 dark:hover:bg-zinc-800"
|
||||
>
|
||||
<div className="w-32 shrink-0">
|
||||
<img
|
||||
className="!m-0"
|
||||
src={imgUrl}
|
||||
alt={`Another recommendation: ${config.title}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid h-full w-full shrink grid-cols-1 content-center p-2 overflow-ellipsis">
|
||||
{config.title}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</aside>
|
||||
</ShortEmbedContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export type ShortVideoProps = VideoData;
|
||||
|
||||
export function ShortVideo({ embedUrl, title }: ShortVideoProps) {
|
||||
const { current, userInteraction } = useContext(ShortEmbedContext);
|
||||
|
||||
if (embedUrl !== current?.embedUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="not-content h-96 w-full overflow-hidden rounded-lg">
|
||||
<iframe
|
||||
className="!m-0 border-0"
|
||||
width="100%"
|
||||
height="100%"
|
||||
src={`${embedUrl}?autoplay=1&loop=1${userInteraction ? '' : '&mute=1'}`}
|
||||
title={title || 'Two Places to Define Tasks | Nx Workspaces'}
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowFullScreen
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { cx } from '@nx/nx-dev-ui-primitives';
|
||||
import { Children, ReactNode } from 'react';
|
||||
|
||||
export type SideBySideProps = {
|
||||
align: string;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export function SideBySide({ align, children }: SideBySideProps) {
|
||||
const [first, ...rest] = Children.toArray(children);
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
'not-prose grid divide-x divide-solid divide-zinc-50 md:grid-cols-2 dark:divide-zinc-800',
|
||||
align === 'top' ? 'items-start' : 'items-center'
|
||||
)}
|
||||
>
|
||||
<div className="md:pr-6">{first}</div>
|
||||
<div className="md:pl-6">{rest}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Schema } from '@markdoc/markdoc';
|
||||
|
||||
export const sideBySide: Schema = {
|
||||
render: 'SideBySide',
|
||||
attributes: {
|
||||
align: {
|
||||
type: 'String',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
import { ChevronRightIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
export type StackblitzButtonProps = {
|
||||
url: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
export function StackblitzButton({
|
||||
url,
|
||||
title,
|
||||
}: StackblitzButtonProps): JSX.Element {
|
||||
const resolvedUrl = url.replace('https://', '');
|
||||
|
||||
return (
|
||||
<div className="not-content not-prose group relative mx-auto my-12 flex w-full max-w-md items-center gap-3 overflow-hidden rounded-lg bg-zinc-50 shadow-md transition hover:text-white dark:bg-zinc-800/60">
|
||||
<div className="absolute inset-0 z-0 w-2 bg-blue-500 transition-all duration-150 group-hover:w-full dark:bg-blue-500"></div>
|
||||
<div className="w-2 bg-blue-500 dark:bg-blue-500"></div>
|
||||
|
||||
<div className="z-10 flex flex-grow items-center py-3">
|
||||
<svg
|
||||
className="h-10 w-10 rounded-full object-cover"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<title>StackBlitz</title>
|
||||
<path d="M10.797 14.182H3.635L16.728 0l-3.525 9.818h7.162L7.272 24l3.524-9.818Z" />
|
||||
</svg>
|
||||
|
||||
<div className="mx-3">
|
||||
<p>
|
||||
{title ? title : 'Open in Stackblitz'}
|
||||
<a
|
||||
href={`https://stackblitz.com/${resolvedUrl}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="block text-sm font-medium opacity-80"
|
||||
>
|
||||
<span className="absolute inset-0" aria-hidden="true"></span>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRightIcon className="mr-4 h-6 w-6 transition-all group-hover:translate-x-3" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Schema } from '@markdoc/markdoc';
|
||||
|
||||
export const stackblitzButton: Schema = {
|
||||
render: 'StackblitzButton',
|
||||
description: 'Renders a button to open a given repository in Stackblitz',
|
||||
attributes: {
|
||||
url: {
|
||||
type: 'String',
|
||||
required: true,
|
||||
description: 'The url of the GitHub repository to open in Stackblitz',
|
||||
},
|
||||
title: {
|
||||
type: 'String',
|
||||
required: false,
|
||||
description:
|
||||
'An optional title to display on the button; "Open in Stackblitz" by default',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
'use client';
|
||||
import cx from 'classnames';
|
||||
import React, { ReactNode } from 'react';
|
||||
|
||||
export type StepsProps = { children: ReactNode };
|
||||
|
||||
export function Steps({ children }: StepsProps) {
|
||||
const steps = React.Children.toArray(children);
|
||||
const stepsCount = steps.length;
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{steps.map((step, index) => (
|
||||
<div key={index} className="relative flex pb-8 last:pb-0">
|
||||
{/* Vertical line connecting steps */}
|
||||
{index < stepsCount - 1 && (
|
||||
<div
|
||||
className="absolute left-5 top-10 h-full w-0.5 bg-zinc-200 dark:bg-zinc-700"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Step number circle */}
|
||||
<div className="relative z-10 flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full bg-blue-500 text-white dark:bg-blue-500">
|
||||
<span className="text-sm font-semibold">{index + 1}</span>
|
||||
</div>
|
||||
|
||||
{/* Step content */}
|
||||
<div className="ml-6 flex-1">{step}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export type StepProps = {
|
||||
title?: string;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export function Step({ title, children }: StepProps) {
|
||||
const passPropsToChildren = (children: ReactNode) => {
|
||||
return React.Children.map(children, (child) => {
|
||||
if (React.isValidElement(child) && typeof child.type !== 'string') {
|
||||
return React.cloneElement(child, { isWithinStep: true });
|
||||
}
|
||||
return child;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="prose prose-zinc dark:prose-invert max-w-none">
|
||||
{title && (
|
||||
<h3 className="mt-0 text-lg font-semibold text-zinc-900 dark:text-zinc-100">
|
||||
{title}
|
||||
</h3>
|
||||
)}
|
||||
{passPropsToChildren(children)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Schema } from '@markdoc/markdoc';
|
||||
import markdoc from '@markdoc/markdoc';
|
||||
const { Tag } = markdoc;
|
||||
|
||||
export const steps: Schema = {
|
||||
render: 'Steps',
|
||||
attributes: {},
|
||||
transform(node, config) {
|
||||
return new Tag(this.render, {}, node.transformChildren(config));
|
||||
},
|
||||
};
|
||||
|
||||
export const step: Schema = {
|
||||
render: 'Step',
|
||||
attributes: {
|
||||
title: {
|
||||
type: 'String',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Schema } from '@markdoc/markdoc';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
export const svgAnimation: Schema = {
|
||||
render: 'SvgAnimation',
|
||||
attributes: {
|
||||
src: {
|
||||
type: 'String',
|
||||
required: true,
|
||||
},
|
||||
|
||||
title: {
|
||||
type: 'String',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function SvgAnimation({
|
||||
src,
|
||||
title,
|
||||
}: {
|
||||
src: string;
|
||||
title: string;
|
||||
}): JSX.Element {
|
||||
const objectRef = useRef<HTMLObjectElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (objectRef.current) {
|
||||
fetch(src)
|
||||
.then((response) => response.text())
|
||||
.then((svgContent) => {
|
||||
objectRef.current!.data = `data:image/svg+xml,${encodeURIComponent(
|
||||
svgContent
|
||||
)}`;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error fetching SVG:', error);
|
||||
});
|
||||
}
|
||||
}, [src]);
|
||||
|
||||
return (
|
||||
<div className="w-full rounded-md border border-zinc-100 bg-zinc-50/20 p-4 dark:border-zinc-800 dark:bg-zinc-800/60">
|
||||
<object
|
||||
ref={objectRef}
|
||||
type="image/svg+xml"
|
||||
title={title}
|
||||
className="rounded-xs bg-white p-2 ring-1 ring-zinc-50 dark:bg-zinc-800/80 dark:ring-zinc-800"
|
||||
>
|
||||
{title}
|
||||
</object>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
interface TocItem {
|
||||
id: string;
|
||||
text: string;
|
||||
level: number;
|
||||
}
|
||||
|
||||
export interface TableOfContentsProps {
|
||||
maxDepth?: number;
|
||||
}
|
||||
|
||||
export function TableOfContents({
|
||||
maxDepth = 3,
|
||||
}: TableOfContentsProps): JSX.Element {
|
||||
const [headings, setHeadings] = useState<TocItem[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
// Find the content area where markdown content is rendered
|
||||
const content = document.querySelector('[data-content-area]');
|
||||
if (!content) return;
|
||||
|
||||
// Get all headings h1-h6 within the content
|
||||
const headingElements = content.querySelectorAll('h2, h3, h4, h5, h6');
|
||||
|
||||
const items: TocItem[] = Array.from(headingElements)
|
||||
.map((heading) => {
|
||||
const level = parseInt(heading.tagName[1]);
|
||||
if (level > maxDepth) return null;
|
||||
|
||||
return {
|
||||
id: heading.id,
|
||||
text: heading.textContent || '',
|
||||
level,
|
||||
};
|
||||
})
|
||||
.filter((item): item is TocItem => item !== null);
|
||||
|
||||
setHeadings(items);
|
||||
}, [maxDepth]);
|
||||
|
||||
if (headings.length === 0) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className="toc not-prose mb-8 rounded-lg border border-zinc-200 bg-zinc-50 p-4 dark:border-zinc-800 dark:bg-zinc-900/50">
|
||||
<p className="mb-3 text-sm font-semibold text-zinc-900 dark:text-zinc-100">
|
||||
Table of Contents
|
||||
</p>
|
||||
<ul className="space-y-2 text-sm">
|
||||
{headings.map((heading) => (
|
||||
<li
|
||||
key={heading.id}
|
||||
style={{ paddingLeft: `${(heading.level - 2) * 1}rem` }}
|
||||
>
|
||||
<a
|
||||
href={`#${heading.id}`}
|
||||
className="text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-200"
|
||||
>
|
||||
{heading.text}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Schema } from '@markdoc/markdoc';
|
||||
|
||||
export const tableOfContents: Schema = {
|
||||
render: 'TableOfContents',
|
||||
attributes: {
|
||||
maxDepth: {
|
||||
type: 'Number',
|
||||
default: 3,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,117 @@
|
||||
'use client';
|
||||
import cx from 'classnames';
|
||||
import React, {
|
||||
createContext,
|
||||
ReactNode,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
cloneElement,
|
||||
} from 'react';
|
||||
|
||||
export const TabContext = createContext('');
|
||||
export const SELECTED_TAB_KEY = 'selectedTab';
|
||||
export const TAB_SELECTED_EVENT = 'tabSelectedEvent';
|
||||
|
||||
export type TabsProps = {
|
||||
labels: string[];
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export function Tabs({ labels, children }: TabsProps) {
|
||||
const [currentTab, setCurrentTab] = useState<string>(labels[0]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleTabSelectedEvent = (
|
||||
tabSelectedEvent: CustomEvent<{ defaultTab?: string }>
|
||||
) => {
|
||||
const selectedTab = localStorage.getItem(SELECTED_TAB_KEY);
|
||||
const defaultTab = tabSelectedEvent.detail.defaultTab;
|
||||
if (selectedTab && labels.includes(selectedTab)) {
|
||||
setCurrentTab(selectedTab);
|
||||
} else if (defaultTab) {
|
||||
setCurrentTab(defaultTab);
|
||||
}
|
||||
};
|
||||
|
||||
handleTabSelectedEvent(
|
||||
new CustomEvent(TAB_SELECTED_EVENT, { detail: { defaultTab: labels[0] } })
|
||||
);
|
||||
window.addEventListener(
|
||||
TAB_SELECTED_EVENT,
|
||||
handleTabSelectedEvent as EventListener
|
||||
);
|
||||
return () =>
|
||||
window.removeEventListener(
|
||||
TAB_SELECTED_EVENT,
|
||||
handleTabSelectedEvent as EventListener
|
||||
);
|
||||
}, [labels]);
|
||||
|
||||
const handleTabClick = (label: string) => {
|
||||
localStorage.setItem(SELECTED_TAB_KEY, label);
|
||||
window.dispatchEvent(new CustomEvent(TAB_SELECTED_EVENT, { detail: {} }));
|
||||
setCurrentTab(label);
|
||||
};
|
||||
|
||||
return (
|
||||
<TabContext.Provider value={currentTab}>
|
||||
<nav className="not-prose -mb-px flex space-x-8" aria-label="Tabs">
|
||||
{labels.map((label, index) => (
|
||||
<button
|
||||
key={label}
|
||||
role="tab"
|
||||
aria-selected={label === currentTab}
|
||||
onClick={() => handleTabClick(label)}
|
||||
className={cx(
|
||||
'whitespace-nowrap border-b-2 p-2 text-sm font-medium',
|
||||
label === currentTab
|
||||
? 'border-blue-500 text-zinc-800 dark:border-blue-500 dark:text-zinc-300'
|
||||
: 'border-transparent text-zinc-500 hover:border-blue-500 hover:text-zinc-800 dark:text-zinc-400 dark:hover:border-blue-500 dark:hover:text-zinc-300'
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
<div
|
||||
className={cx(
|
||||
'border border-zinc-200 pb-2 pl-4 pr-4 pt-2 dark:border-zinc-700',
|
||||
currentTab === labels[0]
|
||||
? 'rounded-b-md rounded-tr-md'
|
||||
: 'rounded-b-md rounded-t-md'
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</TabContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export type TabProps = {
|
||||
label: string;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export function Tab({ label, children }: TabProps) {
|
||||
const currentTab = useContext(TabContext);
|
||||
const isActive = label === currentTab;
|
||||
|
||||
const passPropsToChildren = (children: ReactNode) => {
|
||||
return React.Children.map(children, (child) => {
|
||||
if (React.isValidElement(child) && typeof child.type !== 'string') {
|
||||
return cloneElement(child, { isWithinTab: true });
|
||||
}
|
||||
return child;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="prose prose-zinc dark:prose-invert mt-2 max-w-none"
|
||||
hidden={!isActive}
|
||||
>
|
||||
{isActive && passPropsToChildren(children)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Schema } from '@markdoc/markdoc';
|
||||
import markdoc from '@markdoc/markdoc';
|
||||
const { Tag } = markdoc;
|
||||
|
||||
export const tabs: Schema = {
|
||||
render: 'Tabs',
|
||||
attributes: {},
|
||||
transform(node, config) {
|
||||
const labels = node
|
||||
.transformChildren(config)
|
||||
.filter((child) => child && child.name === 'Tab')
|
||||
.map((tab) => (typeof tab === 'object' ? tab.attributes.label : null));
|
||||
|
||||
return new Tag(this.render, { labels }, node.transformChildren(config));
|
||||
},
|
||||
};
|
||||
|
||||
export const tab: Schema = {
|
||||
render: 'Tab',
|
||||
attributes: {
|
||||
label: {
|
||||
type: 'String',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Schema } from '@markdoc/markdoc';
|
||||
import { image } from '@markdoc/markdoc/dist/src/schema';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export const testimonial: Schema = {
|
||||
render: 'Testimonial',
|
||||
children: ['paragraph'],
|
||||
attributes: {
|
||||
name: {
|
||||
type: 'String',
|
||||
},
|
||||
title: {
|
||||
type: 'String',
|
||||
},
|
||||
image: {
|
||||
type: 'String',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export type TestimonialProps = {
|
||||
title: string;
|
||||
name: string;
|
||||
children: ReactNode;
|
||||
image: string;
|
||||
};
|
||||
|
||||
export function Testimonial({
|
||||
children,
|
||||
name,
|
||||
title,
|
||||
image,
|
||||
}: TestimonialProps) {
|
||||
return (
|
||||
<figure className="not-content not-prose">
|
||||
<blockquote className="relative pt-6">
|
||||
<svg
|
||||
className="absolute start-0 top-0 size-24 -translate-x-8 -translate-y-4 transform text-zinc-200 dark:text-zinc-800"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M7.39762 10.3C7.39762 11.0733 7.14888 11.7 6.6514 12.18C6.15392 12.6333 5.52552 12.86 4.76621 12.86C3.84979 12.86 3.09047 12.5533 2.48825 11.94C1.91222 11.3266 1.62421 10.4467 1.62421 9.29999C1.62421 8.07332 1.96459 6.87332 2.64535 5.69999C3.35231 4.49999 4.33418 3.55332 5.59098 2.85999L6.4943 4.25999C5.81354 4.73999 5.26369 5.27332 4.84476 5.85999C4.45201 6.44666 4.19017 7.12666 4.05926 7.89999C4.29491 7.79332 4.56983 7.73999 4.88403 7.73999C5.61716 7.73999 6.21938 7.97999 6.69067 8.45999C7.16197 8.93999 7.39762 9.55333 7.39762 10.3ZM14.6242 10.3C14.6242 11.0733 14.3755 11.7 13.878 12.18C13.3805 12.6333 12.7521 12.86 11.9928 12.86C11.0764 12.86 10.3171 12.5533 9.71484 11.94C9.13881 11.3266 8.85079 10.4467 8.85079 9.29999C8.85079 8.07332 9.19117 6.87332 9.87194 5.69999C10.5789 4.49999 11.5608 3.55332 12.8176 2.85999L13.7209 4.25999C13.0401 4.73999 12.4903 5.27332 12.0713 5.85999C11.6786 6.44666 11.4168 7.12666 11.2858 7.89999C11.5215 7.79332 11.7964 7.73999 12.1106 7.73999C12.8437 7.73999 13.446 7.97999 13.9173 8.45999C14.3886 8.93999 14.6242 9.55333 14.6242 10.3Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<div className="relative z-10">
|
||||
<div className="text-xl font-medium italic text-zinc-800 md:text-2xl md:leading-normal xl:text-3xl xl:leading-normal dark:text-neutral-200">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<figcaption className="mt-6 flex flex-wrap items-center gap-4 sm:flex-nowrap">
|
||||
<img
|
||||
alt={name}
|
||||
src={image}
|
||||
className="!m-0 !size-12 flex-none rounded-full bg-gray-50"
|
||||
/>
|
||||
<div className="flex-auto">
|
||||
<div className="text-base font-semibold">{name}</div>
|
||||
<div className="text-xs text-zinc-600 dark:text-zinc-500">
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
</figcaption>
|
||||
</blockquote>
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Schema } from '@markdoc/markdoc';
|
||||
|
||||
export const videoLink: Schema = {
|
||||
render: 'VideoLink',
|
||||
attributes: {
|
||||
link: {
|
||||
type: 'String',
|
||||
required: true,
|
||||
},
|
||||
text: {
|
||||
type: 'String',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const youtubeIcon = (
|
||||
<svg
|
||||
fill="currentColor"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-6 w-6"
|
||||
>
|
||||
<path d="M23.5 6.19a3.02 3.02 0 0 0-2.12-2.14c-1.88-.5-9.38-.5-9.38-.5s-7.5 0-9.38.5A3.02 3.02 0 0 0 .5 6.19C0 8.07 0 12 0 12s0 3.93.5 5.81a3.02 3.02 0 0 0 2.12 2.14c1.87.5 9.38.5 9.38.5s7.5 0 9.38-.5a3.02 3.02 0 0 0 2.12-2.14C24 15.93 24 12 24 12s0-3.93-.5-5.81zM9.54 15.57V8.43L15.82 12l-6.28 3.57z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export type VideoLinkProps = { text: string; link: string };
|
||||
|
||||
export function VideoLink({ text, link }: VideoLinkProps) {
|
||||
return (
|
||||
<div className="not-content no-prose my-4 flex">
|
||||
<a
|
||||
href={link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center space-x-2 border-transparent !text-inherit !no-underline dark:text-white"
|
||||
>
|
||||
<div className="flex items-center justify-between space-x-2 rounded-md border border-zinc-200 py-1 pl-2 pl-3 pr-2 transition hover:border-zinc-500 dark:border-zinc-700/40 dark:hover:border-zinc-700">
|
||||
{youtubeIcon}
|
||||
<span className="text-md font-semibold hover:text-zinc-900 dark:hover:text-blue-400">
|
||||
{text || 'Jump to section in video'}
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { Schema } from '@markdoc/markdoc';
|
||||
import { ClientVideo } from './client-video.component';
|
||||
|
||||
export const videoPlayer: Schema = {
|
||||
render: 'VideoPlayer',
|
||||
attributes: {
|
||||
src: {
|
||||
type: 'String',
|
||||
required: true,
|
||||
},
|
||||
alt: {
|
||||
type: 'String',
|
||||
required: false,
|
||||
},
|
||||
link: {
|
||||
type: 'String',
|
||||
required: false,
|
||||
},
|
||||
showDescription: {
|
||||
type: 'Boolean',
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
showControls: {
|
||||
type: 'Boolean',
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
autoPlay: {
|
||||
type: 'Boolean',
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
loop: {
|
||||
type: 'Boolean',
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export type VideoPlayerProps = {
|
||||
src: string;
|
||||
alt: string;
|
||||
link?: string;
|
||||
showDescription?: boolean;
|
||||
showControls?: boolean;
|
||||
autoPlay?: boolean;
|
||||
loop?: boolean;
|
||||
};
|
||||
|
||||
export function VideoPlayer({
|
||||
src,
|
||||
alt,
|
||||
link,
|
||||
showDescription = false,
|
||||
showControls,
|
||||
autoPlay,
|
||||
loop,
|
||||
}: VideoPlayerProps): JSX.Element {
|
||||
return (
|
||||
<div className="not-content mb-4 overflow-x-auto">
|
||||
<div className="rounded-lg border border-zinc-200 bg-zinc-50/50 dark:border-zinc-700 dark:bg-zinc-800/60">
|
||||
<div
|
||||
className={
|
||||
showDescription && alt
|
||||
? 'overflow-hidden rounded-t-lg'
|
||||
: 'overflow-hidden rounded-lg'
|
||||
}
|
||||
>
|
||||
{link ? (
|
||||
<a href={link} target="_blank" rel="noreferrer">
|
||||
<ClientVideo
|
||||
src={src}
|
||||
alt={alt}
|
||||
showControls={showControls}
|
||||
autoPlay={autoPlay}
|
||||
loop={loop}
|
||||
/>
|
||||
</a>
|
||||
) : (
|
||||
<ClientVideo
|
||||
src={src}
|
||||
alt={alt}
|
||||
showControls={showControls}
|
||||
autoPlay={autoPlay}
|
||||
loop={loop}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{showDescription && alt && (
|
||||
<div className="py-2 text-center text-sm text-zinc-600 dark:text-zinc-400">
|
||||
{alt}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": false,
|
||||
"strict": false,
|
||||
"noImplicitAny": false,
|
||||
"noImplicitReturns": false,
|
||||
"noImplicitThis": false,
|
||||
"noImplicitOverride": false,
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
"noFallthroughCasesInSwitch": false,
|
||||
"noUncheckedIndexedAccess": false,
|
||||
"exactOptionalPropertyTypes": false,
|
||||
"skipLibCheck": true,
|
||||
"noEmitOnError": false
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"lib": ["DOM", "ES2022"],
|
||||
"composite": true,
|
||||
"declaration": true,
|
||||
"jsx": "react-jsx",
|
||||
"skipLibCheck": true,
|
||||
"allowJs": true,
|
||||
"strict": false,
|
||||
"noImplicitAny": false
|
||||
},
|
||||
"files": [
|
||||
"../../node_modules/@nx/react/typings/cssmodule.d.ts",
|
||||
"../../node_modules/@nx/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"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../feature-analytics/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../ui-common/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../ui-icons/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../ui-theme/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../ui-primitives/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../ui-fence/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../../graph/ui-icons/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../../graph/ui-common/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../../graph/ui-project-details/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/devkit/tsconfig.lib.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist/spec",
|
||||
"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"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user