UNPKG

orcs-design-system

Version:
520 lines (515 loc) 20 kB
import _defineProperty from "@babel/runtime/helpers/defineProperty"; import _objectWithoutProperties from "@babel/runtime/helpers/objectWithoutProperties"; const _excluded = ["children", "language", "variant", "showLineNumbers", "showCopyButton", "showHeader", "maxHeight", "theme", "editable", "value", "onChange", "valid", "invalid", "rows"]; function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } import React, { useState, useEffect, useRef } from "react"; import PropTypes from "prop-types"; import styled, { ThemeProvider } from "styled-components"; import { Highlight, themes } from "prism-react-renderer"; import { space, layout, compose } from "styled-system"; import { themeGet } from "@styled-system/theme-get"; import { css } from "@styled-system/css"; import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; const codeBlockStyles = compose(space, layout); const CodeBlockWrapper = /*#__PURE__*/styled("div").withConfig({ displayName: "CodeBlockWrapper", componentId: "sc-1yib7z6-0" })(props => css({ position: "relative", borderRadius: themeGet("radii.1")(props), backgroundColor: props.variant === "dark" ? themeGet("colors.greyDarkest")(props) : themeGet("colors.white")(props), border: props.variant === "light" ? "1px solid ".concat(themeGet("colors.greyLighter")(props)) : "none", overflow: "hidden" }), codeBlockStyles); const CodeBlockHeader = /*#__PURE__*/styled("div").withConfig({ displayName: "CodeBlockHeader", componentId: "sc-1yib7z6-1" })(props => css({ display: "flex", justifyContent: "space-between", alignItems: "center", padding: themeGet("space.3")(props), backgroundColor: props.variant === "dark" ? themeGet("colors.black")(props) : themeGet("colors.greyLightest")(props), borderBottom: props.variant === "dark" ? "1px solid ".concat(themeGet("colors.greyDarker")(props)) : "1px solid ".concat(themeGet("colors.greyLighter")(props)) })); const LanguageLabel = /*#__PURE__*/styled("span").withConfig({ displayName: "LanguageLabel", componentId: "sc-1yib7z6-2" })(props => css({ fontSize: themeGet("fontSizes.0")(props), fontWeight: themeGet("fontWeights.2")(props), color: props.variant === "dark" ? themeGet("colors.greyLight")(props) : themeGet("colors.greyDark")(props), textTransform: "uppercase", letterSpacing: "0.05em" })); const CopyButton = /*#__PURE__*/styled("button").attrs({ type: "button" }).withConfig({ displayName: "CopyButton", componentId: "sc-1yib7z6-3" })(props => css({ appearance: "none", backgroundColor: "transparent", border: props.variant === "dark" ? "1px solid ".concat(themeGet("colors.grey")(props)) : "1px solid ".concat(themeGet("colors.grey")(props)), borderRadius: themeGet("radii.1")(props), color: props.variant === "dark" ? themeGet("colors.greyLight")(props) : themeGet("colors.greyDark")(props), cursor: "pointer", fontSize: themeGet("fontSizes.0")(props), fontWeight: themeGet("fontWeights.2")(props), padding: "".concat(themeGet("space.2")(props), " ").concat(themeGet("space.3")(props)), transition: themeGet("transition.transitionDefault")(props), "&:hover": { backgroundColor: props.variant === "dark" ? themeGet("colors.greyDarker")(props) : themeGet("colors.greyLightest")(props), borderColor: props.variant === "dark" ? themeGet("colors.greyLight")(props) : themeGet("colors.greyDark")(props), color: props.variant === "dark" ? themeGet("colors.white")(props) : themeGet("colors.greyDarkest")(props) }, "&:focus": { outline: "none", boxShadow: "".concat(themeGet("shadows.thinOutline")(props), " ").concat(themeGet("colors.primary30")(props)) }, "&:disabled": { cursor: "default", opacity: 0.6 } })); const omitMaxHeightProp = (prop, defaultValidatorFn) => { const isValidProp = typeof defaultValidatorFn === "function" ? defaultValidatorFn(prop) : true; return prop !== "maxHeight" && isValidProp; }; const PreTag = /*#__PURE__*/styled("pre").withConfig({ shouldForwardProp: omitMaxHeightProp, displayName: "PreTag", componentId: "sc-1yib7z6-4" })(props => css({ margin: 0, padding: themeGet("space.r")(props), overflow: "auto", maxHeight: props.maxHeight || "500px", fontSize: themeGet("fontSizes.1")(props), lineHeight: "1.5", fontFamily: "monospace" })); const EditableCodeContainer = /*#__PURE__*/styled("div").withConfig({ shouldForwardProp: omitMaxHeightProp, displayName: "EditableCodeContainer", componentId: "sc-1yib7z6-5" })(props => css({ position: "relative", maxHeight: props.maxHeight || "500px", overflowY: "auto", overflowX: "hidden" })); const CodeTextArea = /*#__PURE__*/styled("textarea").withConfig({ displayName: "CodeTextArea", componentId: "sc-1yib7z6-6" })(props => css({ position: "absolute", top: 0, left: 0, width: "100%", minHeight: "100%", padding: themeGet("space.r")(props), margin: 0, border: "none", outline: "none", resize: "none", fontFamily: "monospace", fontSize: themeGet("fontSizes.1")(props), lineHeight: "1.5", backgroundColor: "transparent", color: "transparent", caretColor: props.variant === "dark" ? themeGet("colors.white")(props) : themeGet("colors.greyDarkest")(props), zIndex: 1, overflow: "hidden", "&::selection": { backgroundColor: themeGet("colors.primary30")(props) } })); const HighlightedCode = /*#__PURE__*/styled("pre").withConfig({ displayName: "HighlightedCode", componentId: "sc-1yib7z6-7" })(props => css({ margin: 0, padding: themeGet("space.r")(props), overflow: "visible", fontSize: themeGet("fontSizes.1")(props), lineHeight: "1.5", fontFamily: "monospace", whiteSpace: "pre-wrap", wordBreak: "break-word", flexShrink: 0 })); const LineNumber = /*#__PURE__*/styled("span").withConfig({ displayName: "LineNumber", componentId: "sc-1yib7z6-8" })(props => css({ display: "inline-block", width: "2em", userSelect: "none", opacity: 0.5, marginRight: themeGet("space.3")(props), textAlign: "right" })); /** * CodeBlock component for displaying syntax-highlighted code with optional features. * * Supports JSON, JavaScript, TypeScript, Python, SQL, Shell, and more. * Includes copy-to-clipboard functionality and optional line numbers. * Can be made editable with the `editable` prop. */ const CodeBlock = _ref => { let { children, language = "json", variant = "light", showLineNumbers = false, showCopyButton = true, showHeader = true, maxHeight, theme, editable = false, value, onChange, valid, invalid, rows = 20 } = _ref, props = _objectWithoutProperties(_ref, _excluded); const [copied, setCopied] = useState(false); const timeoutRef = useRef(null); const textareaRef = useRef(null); const [lineCount, setLineCount] = useState(rows); // For editable mode, use value prop; for read-only, use children const codeValue = editable ? value || "" : typeof children === "string" ? children.trim() : String(children).trim(); // Update line count for editable mode useEffect(() => { if (editable && codeValue) { const lines = codeValue.split("\n").length; setLineCount(Math.max(lines, rows)); } }, [editable, codeValue, rows]); // Select prism theme based on variant const prismTheme = variant === "dark" ? themes.vsDark : themes.github; // Cleanup timeout on unmount to prevent memory leaks useEffect(() => { return () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } }; }, []); const handleCopy = async () => { try { await navigator.clipboard.writeText(codeValue); setCopied(true); // Clear any existing timeout if (timeoutRef.current) { clearTimeout(timeoutRef.current); } // Set new timeout and store reference for cleanup timeoutRef.current = setTimeout(() => { setCopied(false); timeoutRef.current = null; }, 2000); } catch (err) { console.error("Failed to copy code:", err); } }; const handleChange = e => { if (onChange) { onChange(e); } }; const component = /*#__PURE__*/_jsxs(CodeBlockWrapper, _objectSpread(_objectSpread({ variant: variant }, props), {}, { children: [showHeader && /*#__PURE__*/_jsxs(CodeBlockHeader, { variant: variant, children: [/*#__PURE__*/_jsx(LanguageLabel, { variant: variant, children: language }), showCopyButton && !editable && /*#__PURE__*/_jsx(CopyButton, { variant: variant, onClick: handleCopy, disabled: copied, "aria-label": copied ? "Copied!" : "Copy code", children: copied ? "Copied!" : "Copy" })] }), editable ? /*#__PURE__*/_jsx(EditableCodeContainer, { maxHeight: maxHeight, children: /*#__PURE__*/_jsxs("div", { style: { position: "relative", minHeight: "fit-content" }, children: [/*#__PURE__*/_jsx(Highlight, { theme: prismTheme, code: codeValue, language: language, children: _ref2 => { let { className, style, tokens, getLineProps, getTokenProps } = _ref2; return /*#__PURE__*/_jsx(HighlightedCode, { className: className, style: _objectSpread(_objectSpread({}, style), {}, { border: invalid ? "1px solid ".concat(themeGet("colors.danger")({ theme })) : valid ? "1px solid ".concat(themeGet("colors.success")({ theme })) : "none" }), children: tokens.map((line, i) => /*#__PURE__*/_jsxs("div", _objectSpread(_objectSpread({}, getLineProps({ line })), {}, { children: [showLineNumbers && /*#__PURE__*/_jsx(LineNumber, { children: i + 1 }), line.map((token, key) => /*#__PURE__*/_jsx("span", _objectSpread({}, getTokenProps({ token })), key))] }), i)) }); } }), /*#__PURE__*/_jsx(CodeTextArea, { ref: textareaRef, variant: variant, value: codeValue, onChange: handleChange, rows: lineCount, spellCheck: false, style: { height: "auto", minHeight: "100%" } })] }) }) : /*#__PURE__*/_jsx(Highlight, { theme: prismTheme, code: codeValue, language: language, children: _ref3 => { let { className, style, tokens, getLineProps, getTokenProps } = _ref3; return /*#__PURE__*/_jsx(PreTag, { className: className, style: style, maxHeight: maxHeight, children: tokens.map((line, i) => /*#__PURE__*/_jsxs("div", _objectSpread(_objectSpread({}, getLineProps({ line })), {}, { children: [showLineNumbers && /*#__PURE__*/_jsx(LineNumber, { children: i + 1 }), line.map((token, key) => /*#__PURE__*/_jsx("span", _objectSpread({}, getTokenProps({ token })), key))] }), i)) }); } })] })); return theme ? /*#__PURE__*/_jsx(ThemeProvider, { theme: theme, children: component }) : component; }; CodeBlock.propTypes = { /** The code to display. Should be a string. Required when not using editable mode. */ children: (props, propName, componentName) => { if (!props.editable) { if (props[propName] === undefined || props[propName] === null) { return new Error("Prop `".concat(propName, "` is required in `").concat(componentName, "` when `editable` is false or not provided.")); } if (typeof props[propName] !== "string") { return new Error("Invalid prop `".concat(propName, "` of type `").concat(typeof props[propName], "` supplied to `").concat(componentName, "`, expected `string`.")); } } else if (props[propName] !== undefined && typeof props[propName] !== "string") { return new Error("Invalid prop `".concat(propName, "` of type `").concat(typeof props[propName], "` supplied to `").concat(componentName, "`, expected `string`.")); } return null; }, /** The programming language for syntax highlighting. Defaults to 'json'. Also supports: 'javascript', 'python', 'typescript', 'sql', 'bash', and many more. */ language: PropTypes.string, /** Visual style variant: 'light' (white background, dark text - default) or 'dark' (dark background, light text) */ variant: PropTypes.oneOf(["light", "dark"]), /** Show line numbers on the left side */ showLineNumbers: PropTypes.bool, /** Show the copy button in the header */ showCopyButton: PropTypes.bool, /** Show the header with language label and copy button */ showHeader: PropTypes.bool, /** Maximum height before scrolling (e.g., '300px', '50vh') */ maxHeight: PropTypes.string, /** Specifies the system design theme */ theme: PropTypes.object, /** Enable editable mode. When true, use `value` and `onChange` props instead of `children`. */ editable: PropTypes.bool, /** The value for editable mode. Required when `editable` is true. */ value: (props, propName, componentName) => { if (props.editable && props[propName] !== undefined && typeof props[propName] !== "string") { return new Error("Invalid prop `".concat(propName, "` of type `").concat(typeof props[propName], "` supplied to `").concat(componentName, "`, expected `string` when `editable` is true.")); } return null; }, /** Change handler for editable mode. Required when `editable` is true. Receives the event object. */ onChange: PropTypes.func, /** Validation state - shows success border when true */ valid: PropTypes.bool, /** Validation state - shows error border when true */ invalid: PropTypes.bool, /** Number of rows for editable mode (default: 20) */ rows: PropTypes.number // Also accepts all styled-system space and layout props (m, mt, mb, ml, mr, mx, my, p, pt, pb, pl, pr, px, py, width, height, etc.) }; CodeBlock.__docgenInfo = { "description": "CodeBlock component for displaying syntax-highlighted code with optional features.\n\nSupports JSON, JavaScript, TypeScript, Python, SQL, Shell, and more.\nIncludes copy-to-clipboard functionality and optional line numbers.\nCan be made editable with the `editable` prop.", "methods": [], "displayName": "CodeBlock", "props": { "language": { "defaultValue": { "value": "\"json\"", "computed": false }, "description": "The programming language for syntax highlighting. Defaults to 'json'. Also supports: 'javascript', 'python', 'typescript', 'sql', 'bash', and many more.", "type": { "name": "string" }, "required": false }, "variant": { "defaultValue": { "value": "\"light\"", "computed": false }, "description": "Visual style variant: 'light' (white background, dark text - default) or 'dark' (dark background, light text)", "type": { "name": "enum", "value": [{ "value": "\"light\"", "computed": false }, { "value": "\"dark\"", "computed": false }] }, "required": false }, "showLineNumbers": { "defaultValue": { "value": "false", "computed": false }, "description": "Show line numbers on the left side", "type": { "name": "bool" }, "required": false }, "showCopyButton": { "defaultValue": { "value": "true", "computed": false }, "description": "Show the copy button in the header", "type": { "name": "bool" }, "required": false }, "showHeader": { "defaultValue": { "value": "true", "computed": false }, "description": "Show the header with language label and copy button", "type": { "name": "bool" }, "required": false }, "editable": { "defaultValue": { "value": "false", "computed": false }, "description": "Enable editable mode. When true, use `value` and `onChange` props instead of `children`.", "type": { "name": "bool" }, "required": false }, "rows": { "defaultValue": { "value": "20", "computed": false }, "description": "Number of rows for editable mode (default: 20)", "type": { "name": "number" }, "required": false }, "children": { "description": "The code to display. Should be a string. Required when not using editable mode.", "type": { "name": "custom", "raw": "(props, propName, componentName) => {\n if (!props.editable) {\n if (props[propName] === undefined || props[propName] === null) {\n return new Error(\n `Prop \\`${propName}\\` is required in \\`${componentName}\\` when \\`editable\\` is false or not provided.`\n );\n }\n if (typeof props[propName] !== \"string\") {\n return new Error(\n `Invalid prop \\`${propName}\\` of type \\`${typeof props[\n propName\n ]}\\` supplied to \\`${componentName}\\`, expected \\`string\\`.`\n );\n }\n } else if (props[propName] !== undefined && typeof props[propName] !== \"string\") {\n return new Error(\n `Invalid prop \\`${propName}\\` of type \\`${typeof props[\n propName\n ]}\\` supplied to \\`${componentName}\\`, expected \\`string\\`.`\n );\n }\n return null;\n}" }, "required": false }, "maxHeight": { "description": "Maximum height before scrolling (e.g., '300px', '50vh')", "type": { "name": "string" }, "required": false }, "theme": { "description": "Specifies the system design theme", "type": { "name": "object" }, "required": false }, "value": { "description": "The value for editable mode. Required when `editable` is true.", "type": { "name": "custom", "raw": "(props, propName, componentName) => {\n if (props.editable && props[propName] !== undefined && typeof props[propName] !== \"string\") {\n return new Error(\n `Invalid prop \\`${propName}\\` of type \\`${typeof props[\n propName\n ]}\\` supplied to \\`${componentName}\\`, expected \\`string\\` when \\`editable\\` is true.`\n );\n }\n return null;\n}" }, "required": false }, "onChange": { "description": "Change handler for editable mode. Required when `editable` is true. Receives the event object.", "type": { "name": "func" }, "required": false }, "valid": { "description": "Validation state - shows success border when true", "type": { "name": "bool" }, "required": false }, "invalid": { "description": "Validation state - shows error border when true", "type": { "name": "bool" }, "required": false } } }; export default CodeBlock;