diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 44702ea3..a777697d 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -5,6 +5,7 @@ import type { Metadata } from 'next' import { ThemeProvider } from 'next-themes' import { Inconsolata, Inter } from 'next/font/google' import './globals.css' +import './sandpack.css' const inter = Inter({ subsets: ['latin'] }) const inconsolata = Inconsolata({ subsets: ['latin'] }) diff --git a/src/app/sandpack.css b/src/app/sandpack.css new file mode 100644 index 00000000..55962ffc --- /dev/null +++ b/src/app/sandpack.css @@ -0,0 +1,615 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * https://github.com/reactjs/react.dev/blob/main/src/styles/sandpack.css + */ + +.sandpack { + color-scheme: inherit; + -webkit-font-smoothing: antialiased; + + --sp-space-1: 4px; + --sp-space-2: 8px; + --sp-space-3: 12px; + --sp-space-4: 16px; + --sp-space-5: 20px; + --sp-space-6: 24px; + --sp-space-7: 28px; + --sp-space-8: 32px; + --sp-space-9: 36px; + --sp-space-10: 40px; + --sp-space-11: 44px; + --sp-border-radius: 4px; + --sp-layout-height: 300px; + --sp-layout-headerHeight: 40px; + --sp-transitions-default: 150ms ease; + --sp-zIndices-base: 1; + --sp-zIndices-overlay: 2; + --sp-zIndices-top: 3; + + --sp-font-body: Optimistic Display, -apple-system, ui-sans-serif, system-ui, -apple-system, + BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, + Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji; + --sp-font-mono: Source Code Pro, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, + Liberation Mono, Courier New, monospace; + --sp-font-size: calc(1em - 20%); + --sp-font-lineHeight: 24px; +} + +/* Default theme */ +html .sandpack { + --sp-colors-accent: #087ea4; + --sp-colors-clickable: #959da5; + --sp-colors-disabled: #24292e; + --sp-colors-error: #811e18; + --sp-colors-error-surface: #ffcdca; + --sp-colors-surface1: #fff; + --sp-colors-surface2: #e4e7eb; + + --sp-syntax-color-plain: #24292e; + --sp-syntax-color-comment: #6a737d; + --sp-syntax-color-keyword: #d73a49; + --sp-syntax-color-tag: #22863a; + --sp-syntax-color-punctuation: #24292e; + --sp-syntax-color-definition: #6f42c1; + --sp-syntax-color-property: #005cc5; + --sp-syntax-color-static: #032f62; + --sp-syntax-color-string: #032f62; +} + +/* Dark theme */ +html.dark .sp-wrapper { + --sp-colors-accent: #58c4dc; + --sp-colors-clickable: #999; + --sp-colors-disabled: #fff; + --sp-colors-error: #811e18; + --sp-colors-error-surface: #ffcdca; + --sp-colors-surface1: #16181d; + --sp-colors-surface2: #343a46; + + --sp-syntax-color-plain: #ffffff; + --sp-syntax-color-comment: #757575; + --sp-syntax-color-keyword: #77b7d7; + --sp-syntax-color-tag: #dfab5c; + --sp-syntax-color-punctuation: #ffffff; + --sp-syntax-color-definition: #86d9ca; + --sp-syntax-color-property: #77b7d7; + --sp-syntax-color-static: #c64640; + --sp-syntax-color-string: #977cdc; +} + +/** + * Reset + */ +.sandpack .sp-wrapper { + width: 100%; + + font-size: var(--sp-font-size); + font-family: var(--sp-font-body); + line-height: var(--sp-font-lineHeight); +} + +/** + * Layout + */ +.sandpack .sp-layout { + display: flex; + flex-wrap: wrap; + align-items: stretch; + background-color: var(--sp-colors-surface2); + + -webkit-mask-image: -webkit-radial-gradient( + var(--sp-colors-surface1), + var(--sp-colors-surface1) + ); /* safest way to make all corner rounded */ + + border-bottom-left-radius: 0.5rem; + border-bottom-right-radius: 0.5rem; + overflow: initial; + + gap: 1px; +} + +.sandpack .sp-stack { + display: flex; + flex-direction: column; + width: 100%; + position: relative; +} + +@media screen and (max-width: 768px) { + .sandpack .sp-layout > .sp-stack { + height: auto; + min-width: 100% !important; + } +} + +.sandpack .sp-layout > .sp-stack { + flex: 1 1 0px; + height: var(--sp-layout-height); +} + +/** + * Focus ring + */ +.sandpack--playground .sp-tab-button { + transition: none; +} + +.sandpack--playground .sp-tab-button:focus { + outline: revert; +} + +.sandpack--playground .sp-tab-button:focus-visible { + box-shadow: none; +} + +.sandpack .sp-cm:focus-visible { + box-shadow: inset 0 0 0 4px rgba(20, 158, 202, 0.4); + outline: none; + height: 100%; +} + +/** + * Navigation + */ +.sandpack .sp-tabs-scrollable-container { + overflow: auto; + display: flex; + flex-wrap: nowrap; + align-items: stretch; + min-height: 40px; + margin-bottom: -1px; +} + +.sp-tabs .sp-tab-button { + padding: 0 6px; + border-bottom: 2px solid transparent; +} + +@media (min-width: 768px) { + .sp-tabs .sp-tab-button { + margin: 0 12px 0 0; + } +} + +.sp-tabs .sp-tab-button, +.sp-tabs .sp-tab-button:hover:not(:disabled, [data-active='true']), +.sp-tabs .sp-tab-button[data-active='true'] { + color: var(--sp-colors-accent); +} + +.sp-tabs .sp-tab-button[data-active='true'] { + border-bottom: 2px solid var(--sp-colors-accent); +} + +/** + * Code block + */ +.cm-line { + padding-left: var(--sp-space-5); +} + +/** + * Editor + */ +.sandpack .sp-code-editor { + flex: 1 1; + position: relative; + overflow: auto; + background: var(--sp-colors-surface1); +} + +.sandpack .sp-code-editor .cm-editor { + background-color: transparent; +} + +.sandpack .sp-code-editor .cm-content, +.sandpack .sp-code-editor .cm-gutters, +.sandpack .sp-code-editor .cm-gutterElement { + padding: 0; + -webkit-font-smoothing: auto; /* Improve the legibility */ +} + +.sandpack .sp-code-editor .cm-content { + padding-bottom: 18px; +} + +.sandpack--playground .sp-code-editor .cm-line { + padding: 0 var(--sp-space-3); + width: max-content; +} + +.sandpack--playground .sp-code-editor .cm-lineNumbers { + padding-left: var(--sp-space-3); + padding-right: var(--sp-space-1); + font-size: 13.6px; +} + +.sandpack--playground .sp-code-editor .cm-line.cm-errorLine { + /* @apply bg-red-400; */ + --tw-bg-opacity: 0.1; /* Background tweak: base color + opacity */ + position: relative; + padding-right: 2em; + display: inline-block; + min-width: 100%; +} + +.sp-code-editor .cm-errorLine:after { + /* @apply text-red-500; */ + position: absolute; + right: 8px; + top: 0; + content: '\26A0'; + font-size: 22px; + line-height: 20px; +} + +.sp-code-editor .cm-tooltip { + border: 0; + max-width: 200px; +} + +.sp-code-editor .cm-diagnostic-error { + /* @apply border-red-40; */ +} + +.sandpack .sp-cm { + margin: 0px; + outline: none; + height: 100%; +} + +.sp-code-editor .sp-cm .cm-scroller { + padding-top: 18px; +} + +/** + * Syntax highlight (code editor + code block) + */ +.sandpack .sp-syntax-string { + color: var(--sp-syntax-color-string); +} + +.sandpack .sp-syntax-plain { + color: var(--sp-syntax-color-plain); +} + +.sandpack .sp-syntax-comment { + color: var(--sp-syntax-color-comment); +} + +.sandpack .sp-syntax-keyword { + color: var(--sp-syntax-color-keyword); +} + +.sandpack .sp-syntax-definition { + color: var(--sp-syntax-color-definition); +} + +.sandpack .sp-syntax-punctuation { + color: var(--sp-syntax-color-punctuation); +} + +.sandpack .sp-syntax-property { + color: var(--sp-syntax-color-property); +} + +.sandpack .sp-syntax-tag { + color: var(--sp-syntax-color-tag); +} + +.sandpack .sp-syntax-static { + color: var(--sp-syntax-color-static); +} + +/** + * Loading & error overlay component + */ +.sandpack .sp-cube-wrapper { + background-color: var(--sp-colors-surface1); + position: absolute; + right: var(--sp-space-2); + bottom: var(--sp-space-2); + z-index: var(--sp-zIndices-top); + width: 32px; + height: 32px; + border-radius: var(--sp-border-radius); +} + +.sandpack .sp-button { + display: flex; + align-items: center; + margin: auto; + width: 100%; + height: 100%; +} + +.sandpack .sp-button svg { + min-width: var(--sp-space-5); + width: var(--sp-space-5); + height: var(--sp-space-5); + margin: auto; +} + +.sandpack .sp-cube-wrapper .sp-cube { + display: flex; +} + +.sandpack .sp-cube-wrapper .sp-button { + display: none; +} + +.sandpack .sp-cube-wrapper:hover .sp-button { + display: block; +} + +.sandpack .sp-cube-wrapper:hover .sp-cube { + display: none; +} + +.sandpack .sp-cube { + transform: translate(-4px, 9px) scale(0.13, 0.13); +} + +.sandpack .sp-cube * { + position: absolute; + width: 96px; + height: 96px; +} + +@keyframes cubeRotate { + 0% { + transform: rotateX(-25.5deg) rotateY(45deg); + } + + 100% { + transform: rotateX(-25.5deg) rotateY(405deg); + } +} + +.sandpack .sp-sides { + animation: cubeRotate 1s linear infinite; + animation-fill-mode: forwards; + transform-style: preserve-3d; + transform: rotateX(-25.5deg) rotateY(45deg); +} + +.sandpack .sp-sides * { + border: 10px solid var(--sp-colors-clickable); + border-radius: 8px; + background: var(--sp-colors-surface1); +} + +.sandpack .sp-sides .top { + transform: rotateX(90deg) translateZ(44px); + transform-origin: 50% 50%; +} +.sandpack .sp-sides .bottom { + transform: rotateX(-90deg) translateZ(44px); + transform-origin: 50% 50%; +} +.sandpack .sp-sides .front { + transform: rotateY(0deg) translateZ(44px); + transform-origin: 50% 50%; +} +.sandpack .sp-sides .back { + transform: rotateY(-180deg) translateZ(44px); + transform-origin: 50% 50%; +} +.sandpack .sp-sides .left { + transform: rotateY(-90deg) translateZ(44px); + transform-origin: 50% 50%; +} +.sandpack .sp-sides .right { + transform: rotateY(90deg) translateZ(44px); + transform-origin: 50% 50%; +} + +.sandpack .sp-overlay { + /* @apply bg-card; */ + position: absolute; + inset: 0; + z-index: var(--sp-zIndices-top); +} + +.sandpack .sp-error { + padding: var(--sp-space-4); + white-space: pre-wrap; + font-family: var(--sp-font-mono); + background-color: var(--sp-colors-error-surface); +} + +@keyframes fadeIn { + 0% { + opacity: 0; + transform: translateY(4px); + } + + 100% { + opacity: 1; + transform: translateY(0); + } +} + +.sandpack .sp-error-message { + animation: fadeIn 150ms ease; + color: var(--sp-colors-error); +} + +html.dark .sandpack--playground .sp-overlay { + /* @apply bg-wash-dark; */ +} + +/** + * Placeholder + */ +.sandpack .sp-code-editor .sp-pre-placeholder { + /* @apply font-mono; */ + font-size: 13.6px; + line-height: 24px; + padding: 18px 0; + -webkit-font-smoothing: auto; +} + +.sandpack--playground .sp-code-editor .sp-pre-placeholder { + padding-left: 48px !important; + margin-left: 0px !important; +} + +.text-xl .sp-pre-placeholder { + font-size: 16px !important; + line-height: 24px !important; +} + +/** + * Expand button + */ +.sandpack .sp-layout { + min-height: 216px; +} + +.sandpack .sp-layout > .sp-stack:nth-child(1) { + /* Force vertical if there isn't enough space. */ + min-width: 431px; + /* No min height on mobile because we know code in advance. */ + /* Max height is needed to avoid too long files. */ + max-height: 40vh; +} + +.sandpack .sp-layout > .sp-stack:nth-child(2) { + /* Force vertical if there isn't enough space. */ + min-width: 431px; + /* Keep preview a fixed size on mobile to avoid jumps. */ + /* This is because we don't know its content in advance. */ + min-height: 40vh; + max-height: 40vh; +} +.sandpack .sp-layout.sp-layout-expanded > .sp-stack:nth-child(1) { + /* Clicking "show more" lets mobile editor go full height. */ + max-height: unset; + height: auto; +} + +.sandpack .sp-layout.sp-layout-expanded > .sp-stack:nth-child(2) { + /* Clicking "show more" lets mobile preview go full height. */ + max-height: unset; + height: auto; +} + +@media (min-width: 1280px) { + .sandpack .sp-layout > .sp-stack:nth-child(1) { + /* On desktop, clamp height by pixels instead. */ + height: auto; + min-height: unset; + max-height: 406px; + } + .sandpack .sp-layout > .sp-stack:nth-child(2) { + /* On desktop, clamp height by pixels instead. */ + height: auto; + min-height: unset; + max-height: 406px; + } + .sandpack .sp-layout.sp-layout-expanded > .sp-stack:nth-child(1) { + max-height: unset; + } + .sandpack .sp-layout.sp-layout-expanded > .sp-stack:nth-child(2) { + max-height: unset; + } +} + +.sandpack .sp-layout .sandpack-expand { + border-left: none; + margin-left: 0; +} + +.expandable-callout .sp-stack:nth-child(2) { + min-width: 431px; + min-height: 40vh; + max-height: 40vh; +} + +/** + * Integrations: console + */ +.sandpack .console .sp-cm, +.sandpack .console .sp-cm .cm-scroller, +.sandpack .console .sp-cm .cm-line { + padding: 0px !important; +} + +/** + * Integrations: eslint + */ +.sandpack .sp-code-editor .cm-diagnostic { + /* @apply text-secondary; */ +} + +/** + * Overwrite inline sty + */ +.sandpack .sp-devtools > div { + --color-background: var(--sp-colors-surface1) !important; + --color-background-inactive: var(--sp-colors-surface2) !important; + --color-background-selected: var(--sp-colors-accent) !important; + --color-background-hover: transparent !important; + --color-modal-background: #ffffffd2 !important; + + --color-tab-selected-border: #087ea4 !important; + + --color-component-name: var(--sp-syntax-color-definition) !important; + --color-attribute-name: var(--sp-syntax-color-property) !important; + --color-attribute-value: var(--sp-syntax-color-string) !important; + --color-attribute-editable-value: var(--sp-syntax-color-property) !important; + --color-attribute-name-not-editable: var(--sp-colors-clickable) !important; + --color-button-background-focus: var(--sp-colors-surface2) !important; + + --color-button-active: var(--sp-colors-accent) !important; + --color-button-background: transparent !important; + --color-button: var(--sp-colors-clickable) !important; + --color-button-hover: var(--sp-colors-disabled) !important; + + --color-border: var(--sp-colors-surface2) !important; + --color-text: rgb(35, 39, 47) !important; +} + +html.dark .sp-devtools > div { + --color-text: var(--sp-colors-clickable) !important; + --color-modal-background: #16181de0 !important; +} + +.sandpack .sp-devtools table td { + border: 1px solid var(--sp-colors-surface2); +} + +/** + * Hard fixes + */ + +/** + * The text-size-adjust CSS property controls the text inflation + * algorithm used on some smartphones and tablets + */ +.sandpack .sp-cm { + -webkit-text-size-adjust: none; +} + +/** + * For iOS: prevent browser zoom when clicking on sandbox. + * Does NOT apply to code blocks. + */ +@media screen and (max-width: 768px) { + @supports (-webkit-overflow-scrolling: touch) { + .sandpack--playground .cm-content, + .sandpack--playground .sp-code-editor .sp-pre-placeholder { + font-size: initial; + } + .DocSearch-Input { + font-size: initial; + } + } +} + +.sp-loading .sp-icon-standalone span { + display: none; +} diff --git a/src/components/mdx/index.tsx b/src/components/mdx/index.tsx index e7d2eb49..ae00d872 100644 --- a/src/components/mdx/index.tsx +++ b/src/components/mdx/index.tsx @@ -6,7 +6,7 @@ export * from './Toc' import cn from '@/lib/cn' import { MARKDOWN_REGEX } from '@/utils/docs' -import { Sandpack } from '@codesandbox/sandpack-react' +import { Sandpack as SP } from '@codesandbox/sandpack-react' import { ComponentProps } from 'react' type Hn = 'h2' | 'h3' | 'h4' | 'h5' | 'h6' @@ -108,4 +108,8 @@ export const summary = (props: ComponentProps<'summary'>) => ( ) -export { Sandpack } +export const Sandpack = (props: ComponentProps) => ( +
+ +
+)