From 68dc4b8adc7c8509c4dc9bb6b474a72ea5267393 Mon Sep 17 00:00:00 2001 From: wangdicoder Date: Wed, 18 Mar 2026 19:48:26 +1100 Subject: [PATCH 1/4] feat: update code editor --- apps/docs/package.json | 7 +- .../src/components/code-block/code-block.scss | 43 +- apps/docs/src/components/code-block/index.tsx | 45 +- .../src/components/demo-block/demo-block.scss | 136 ++ apps/docs/src/components/demo-block/index.tsx | 237 +++ .../src/components/markdown-tag/index.jsx | 2 + apps/docs/src/locale/en_US.ts | 2 + apps/docs/src/locale/types.ts | 2 + apps/docs/src/locale/zh_CN.ts | 2 + apps/docs/src/utils/sandbox.ts | 102 + pnpm-lock.yaml | 1874 ++++++++++++++++- 11 files changed, 2434 insertions(+), 18 deletions(-) create mode 100644 apps/docs/src/components/demo-block/demo-block.scss create mode 100644 apps/docs/src/components/demo-block/index.tsx create mode 100644 apps/docs/src/utils/sandbox.ts diff --git a/apps/docs/package.json b/apps/docs/package.json index 94bc886b..63b6c04b 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -8,14 +8,17 @@ "preview": "vite preview" }, "dependencies": { + "@mdx-js/react": "^3.1.1", + "@stackblitz/sdk": "^1.11.0", "@tiny-design/icons": "workspace:*", "@tiny-design/react": "workspace:*", "@tiny-design/tokens": "workspace:*", - "@mdx-js/react": "^3.1.1", + "codesandbox": "^2.2.3", "react": "^18.2.0", "react-dom": "^18.2.0", "react-live": "^4.1.0", - "react-router-dom": "^6.0.0" + "react-router-dom": "^6.0.0", + "react-runner": "^1.0.5" }, "devDependencies": { "@mdx-js/rollup": "^3.1.1", diff --git a/apps/docs/src/components/code-block/code-block.scss b/apps/docs/src/components/code-block/code-block.scss index ac76d93d..e9b3a2a4 100755 --- a/apps/docs/src/components/code-block/code-block.scss +++ b/apps/docs/src/components/code-block/code-block.scss @@ -35,19 +35,52 @@ width: 100%; } - &__controller { + &__actions { border-top: dashed 1px var(--ty-color-border-secondary); - height: 36px; - line-height: 36px; + height: 40px; box-sizing: border-box; background-color: var(--ty-color-bg-elevated); border-bottom-left-radius: 3px; border-bottom-right-radius: 3px; - text-align: center; + display: flex; + align-items: center; + justify-content: center; + gap: 4px; + padding: 0 12px; + position: relative; + } + + &__action-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border: none; + background: none; + color: var(--ty-color-text-tertiary); + cursor: pointer; + border-radius: 4px; + font-size: 15px; + transition: color 0.2s, background-color 0.2s; + padding: 0; + + &:hover { + color: var(--ty-color-primary); + background-color: var(--ty-color-fill); + } + } + + &__action-toggle { + display: inline-flex; + align-items: center; color: var(--ty-color-text-tertiary); cursor: pointer; - transition: 0.2s; font-size: 12px; + padding: 4px 8px; + border-radius: 4px; + transition: color 0.2s, background-color 0.2s; + user-select: none; &:hover { color: var(--ty-color-primary); diff --git a/apps/docs/src/components/code-block/index.tsx b/apps/docs/src/components/code-block/index.tsx index 8fb62711..f1f1a9b4 100755 --- a/apps/docs/src/components/code-block/index.tsx +++ b/apps/docs/src/components/code-block/index.tsx @@ -26,8 +26,9 @@ import { LightCodeTheme, DarkCodeTheme } from './code-theme'; import * as Components from '@tiny-design/react'; import * as SvgIcons from '@tiny-design/icons'; import CollapseTransition from '@tiny-design/react/collapse-transition'; -import { useTheme } from '@tiny-design/react'; +import { useTheme, Tooltip } from '@tiny-design/react'; import { useLocaleContext } from '../../context/locale-context'; +import { openInStackBlitz, openInCodeSandbox } from '../../utils/sandbox'; type Props = { children: string; @@ -35,6 +36,20 @@ type Props = { live?: boolean; }; +/** StackBlitz logo icon (inline SVG) */ +const StackBlitzIcon = () => ( + + + +); + +/** CodeSandbox logo icon (inline SVG) */ +const CodeSandboxIcon = () => ( + + + +); + export const CodeBlock = ({ children, className, live }: Props): React.ReactElement => { const [showCode, setShowCode] = useState(false); const ref = useRef(null); @@ -48,9 +63,11 @@ export const CodeBlock = ({ children, className, live }: Props): React.ReactElem } if (live) { + const code = children.trim(); + return (
- + @@ -64,8 +81,28 @@ export const CodeBlock = ({ children, className, live }: Props): React.ReactElem
-
setShowCode(!showCode)}> - {showCode ? s.codeBlock.hideCode : s.codeBlock.showCode} +
+ + + + + + + setShowCode(!showCode)}> + {showCode ? s.codeBlock.hideCode : s.codeBlock.showCode} +
diff --git a/apps/docs/src/components/demo-block/demo-block.scss b/apps/docs/src/components/demo-block/demo-block.scss new file mode 100644 index 00000000..d4f6f31d --- /dev/null +++ b/apps/docs/src/components/demo-block/demo-block.scss @@ -0,0 +1,136 @@ +@use '../../variables' as *; + +.demo-block { + &__container { + border-top: solid 1px var(--ty-color-border-secondary); + transition: 0.2s; + margin: 20px 0 0; + } + + &__previewer { + color: var(--ty-color-text-secondary); + padding: 24px; + overflow-x: auto; + + @media (max-width: $size-md) { + padding: 16px; + } + } + + &__error { + color: #e53e3e; + font-size: 13px; + padding: 12px; + margin: 0; + background-color: #fff5f5; + border-radius: 4px; + font-family: Menlo, Consolas, "Droid Sans Mono", monospace; + white-space: pre-wrap; + word-break: break-word; + } + + &__editor-container { + background-color: var(--ty-color-fill); + border-top: solid 1px var(--ty-color-border-secondary); + overflow-x: auto; + font-family: Menlo, Consolas, "Droid Sans Mono", monospace; + font-size: 13px; + line-height: 1.6; + tab-size: 2; + } + + &__editor-wrapper { + max-width: 800px; + width: 100%; + } + + &__editor-overlay { + position: relative; + + pre { + margin: 0; + pointer-events: none; + } + } + + &__editor-textarea { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + padding: 15px; + margin: 0; + border: none; + outline: none; + resize: none; + overflow: hidden; + background: transparent; + color: transparent; + -webkit-text-fill-color: transparent; + caret-color: var(--ty-color-text-secondary, #000); + font-family: inherit; + font-size: inherit; + line-height: inherit; + tab-size: inherit; + white-space: pre; + box-sizing: border-box; + + &::selection { + background: rgba(0, 0, 0, 0.15); + } + } + + &__actions { + border-top: dashed 1px var(--ty-color-border-secondary); + height: 40px; + box-sizing: border-box; + background-color: var(--ty-color-bg-elevated); + border-bottom-left-radius: 3px; + border-bottom-right-radius: 3px; + display: flex; + align-items: center; + justify-content: center; + gap: 4px; + padding: 0 12px; + position: relative; + } + + &__action-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border: none; + background: none; + color: var(--ty-color-text-tertiary); + cursor: pointer; + border-radius: 4px; + font-size: 15px; + transition: color 0.2s, background-color 0.2s; + padding: 0; + + &:hover { + color: var(--ty-color-primary); + background-color: var(--ty-color-fill); + } + } + + &__action-toggle { + display: inline-flex; + align-items: center; + color: var(--ty-color-text-tertiary); + cursor: pointer; + font-size: 12px; + padding: 4px 8px; + border-radius: 4px; + transition: color 0.2s, background-color 0.2s; + user-select: none; + + &:hover { + color: var(--ty-color-primary); + background-color: var(--ty-color-fill); + } + } +} \ No newline at end of file diff --git a/apps/docs/src/components/demo-block/index.tsx b/apps/docs/src/components/demo-block/index.tsx new file mode 100644 index 00000000..0dd33664 --- /dev/null +++ b/apps/docs/src/components/demo-block/index.tsx @@ -0,0 +1,237 @@ +import React, { useState, useCallback, useRef } from 'react'; +import { useRunner } from 'react-runner'; +import { Highlight, themes } from 'prism-react-renderer'; +import { LightCodeTheme, DarkCodeTheme } from '../code-block/code-theme'; +import * as TinyDesign from '@tiny-design/react'; +import * as TinyIcons from '@tiny-design/icons'; +import CollapseTransition from '@tiny-design/react/collapse-transition'; +import { useTheme, Tooltip } from '@tiny-design/react'; +import { useLocaleContext } from '../../context/locale-context'; +import { openInStackBlitz, openInCodeSandbox } from '../../utils/sandbox'; +import './demo-block.scss'; + +/** StackBlitz logo icon (inline SVG) */ +const StackBlitzIcon = () => ( + + + +); + +/** CodeSandbox logo icon (inline SVG) */ +const CodeSandboxIcon = () => ( + + + +); + +/** Copy icon (inline SVG) */ +const CopyIcon = () => ( + + + + +); + +/** Reset icon (inline SVG) */ +const ResetIcon = () => ( + + + + +); + +// Build scope for react-runner to resolve imports +const scope = { + import: { + react: React, + '@tiny-design/react': TinyDesign, + '@tiny-design/icons': TinyIcons, + }, +}; + +type DemoBlockProps = { + /** The rendered demo component */ + component: React.ComponentType; + /** The raw source code string (from ?raw import) */ + source: string; + /** Optional title for the demo */ + title?: string; + /** Optional description for the demo */ + description?: string; +}; + +export const DemoBlock = ({ component: Component, source, title, description }: DemoBlockProps) => { + const [showCode, setShowCode] = useState(false); + const [editedCode, setEditedCode] = useState(null); + const [copied, setCopied] = useState(false); + const containerRef = useRef(null); + const textareaRef = useRef(null); + const { resolvedTheme } = useTheme(); + const { siteLocale: s } = useLocaleContext(); + const codeTheme = (resolvedTheme === 'dark' ? DarkCodeTheme : LightCodeTheme) as typeof themes.github; + + // Normalize source once: trim trailing whitespace from ?raw import + const normalizedSource = React.useMemo(() => source.trim(), [source]); + + const isEditing = editedCode !== null; + const displayCode = editedCode ?? normalizedSource; + + // react-runner: execute edited code with scope-based import resolution + const { element: liveElement, error: runnerError } = useRunner({ + code: isEditing ? editedCode! : '', + scope, + }); + + const handleCodeChange = useCallback((e: React.ChangeEvent) => { + const newCode = e.target.value; + if (newCode !== normalizedSource) { + setEditedCode(newCode); + } else { + setEditedCode(null); + } + }, [normalizedSource]); + + const handleReset = useCallback(() => { + setEditedCode(null); + }, []); + + const handleCopy = useCallback(async () => { + try { + await navigator.clipboard.writeText(displayCode); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch { + // fallback: ignore + } + }, [displayCode]); + + // Handle Tab key in textarea to insert spaces instead of moving focus + const handleKeyDown = useCallback((e: React.KeyboardEvent) => { + if (e.key === 'Tab') { + e.preventDefault(); + const textarea = e.currentTarget; + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + const value = textarea.value; + const newValue = value.substring(0, start) + ' ' + value.substring(end); + textarea.value = newValue; + textarea.selectionStart = textarea.selectionEnd = start + 2; + // Trigger React's onChange + const nativeInputValueSetter = Object.getOwnPropertyDescriptor( + window.HTMLTextAreaElement.prototype, 'value' + )?.set; + nativeInputValueSetter?.call(textarea, newValue); + textarea.dispatchEvent(new Event('input', { bubbles: true })); + } + }, []); + + return ( +
+ {/* Preview area */} +
+ {isEditing ? ( + runnerError ? ( +
{runnerError}
+ ) : ( + liveElement + ) + ) : ( + + )} +
+ + {/* Action bar */} +
+ + + + + + + + + + {isEditing && ( + + + + )} + setShowCode(!showCode)}> + {showCode ? s.codeBlock.hideCode : s.codeBlock.showCode} + +
+ + {/* Code panel: textarea overlay on syntax-highlighted pre */} + +
+
+
+ {/* Syntax-highlighted background layer */} + + {({ className, style, tokens, getLineProps, getTokenProps }) => ( + + )} + + {/* Transparent textarea on top for editing */} +