orcs-design-system
Version:
TeamForm's Design System, aka: ORCS
520 lines (515 loc) • 20 kB
JavaScript
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;