@octopusdeploy/design-system-components
Version:
The design systems component library.
183 lines (182 loc) • 7.42 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.renderDescription = exports.descriptionText = exports.descriptionWithLink = exports.descriptionCode = exports.descriptionLink = void 0;
const jsx_runtime_1 = require("react/jsx-runtime");
const css_1 = require("@emotion/css");
const design_system_tokens_1 = require("@octopusdeploy/design-system-tokens");
const React = __importStar(require("react"));
/**
* Validates if a URL is safe to use in links
* @param url - The URL to validate
* @returns true if the URL uses a safe protocol or is a relative path, false otherwise
*/
const isValidUrl = (url) => {
// Block dangerous protocols immediately
if (url.startsWith("javascript:") || url.startsWith("data:") || url.startsWith("vbscript:")) {
return false;
}
// Allow relative URLs (paths that don't start with a protocol)
if (!url.includes("://")) {
return true;
}
// For absolute URLs, validate the protocol
try {
const parsed = new URL(url);
return ["http:", "https:", "mailto:", "tel:"].includes(parsed.protocol);
}
catch {
return false;
}
};
/**
* Helper function to create a link object for use in description templates
* @param text - The link text to display
* @param url - The URL to link to (supports both absolute and relative URLs)
* @param newTab - Whether to open the link in a new tab (defaults to false)
* @returns HelperLink object for valid URLs, empty string for invalid URLs
* @example
* // Relative link (opens in same tab)
* descriptionLink("Visit our docs", "/docs/getting-started")
*
* // Absolute link (opens in same tab)
* descriptionLink("Visit our docs", "https://docs.example.com")
*
* // External link that opens in new tab
* descriptionLink("External resource", "https://external.com", true)
*
* // Invalid/dangerous URLs return empty string
* descriptionLink("Bad link", "javascript:alert('xss')") // returns ""
*/
const descriptionLink = (text, url, newTab = false) => {
if (!isValidUrl(url)) {
return "";
}
return {
type: "link",
text,
url,
newTab,
};
};
exports.descriptionLink = descriptionLink;
/**
* Helper function to create a code object for use in description templates
* @param text - The code text to display
* @returns HelperCode object
* @example
* // Inline code snippet
* code("propName")
*
* // Code with special characters
* code("useState()")
*/
const descriptionCode = (text) => ({
type: "code",
text,
});
exports.descriptionCode = descriptionCode;
/**
* For backwards compatibility
* @deprecated Use `link` instead
*/
exports.descriptionWithLink = exports.descriptionLink;
/**
* Tagged template literal function for creating rich descriptions with embedded links and code.
* Allows natural template literal syntax with embedded link() and code() calls.
* @param strings - Template literal strings
* @param values - Interpolated values (including link and code objects)
* @returns DescriptionContent array
* @example
* // Using template literal syntax with links
* descriptionText`Visit our ${descriptionLink("documentation", "/docs")} for more info.`
*
* // With code snippets
* descriptionText`Set the ${code("isVisible")} prop to control visibility.`
*/
const descriptionText = (strings, ...values) => {
const isValidValue = (value) => typeof value === "string" || (value && typeof value === "object" && (value.type === "link" || value.type === "code"));
const isNonEmptyString = (item) => typeof item !== "string" || item !== "";
return strings
.flatMap((str, index) => [str, ...(values[index] ? [values[index]] : [])])
.filter(isValidValue)
.filter(isNonEmptyString);
};
exports.descriptionText = descriptionText;
/**
* Renders a description array containing strings, links, and code snippets
* @param content - Array of strings, HelperLink, and HelperCode objects
* @returns JSX element containing the rendered content
*/
const renderDescription = (content) => {
const renderItem = (item, index) => {
if (typeof item === "string") {
return (0, jsx_runtime_1.jsx)(React.Fragment, { children: item }, index);
}
if (item.type === "link") {
// Double-check URL safety during rendering as an additional security layer
if (!isValidUrl(item.url)) {
// Unsafe URL detected - render as plain text instead of link for security
return (0, jsx_runtime_1.jsx)("span", { children: item.text }, index);
}
return ((0, jsx_runtime_1.jsx)("a", { href: item.url, rel: "noopener noreferrer", className: linkStyles, target: item.newTab ? "_blank" : undefined, children: item.text }, index));
}
if (item.type === "code") {
return ((0, jsx_runtime_1.jsx)("code", { className: codeStyles, children: item.text }, index));
}
return (0, jsx_runtime_1.jsx)(React.Fragment, {}, index);
};
return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: content.map(renderItem) });
};
exports.renderDescription = renderDescription;
const linkStyles = (0, css_1.css)({
color: design_system_tokens_1.themeTokens.color.text.link.default,
textDecoration: "underline",
"&:hover": {
textDecoration: "none",
color: design_system_tokens_1.themeTokens.color.text.link.hover,
},
"&:focus-visible": {
boxShadow: design_system_tokens_1.themeTokens.shadow.focused,
},
});
const codeStyles = (0, css_1.css)({
font: design_system_tokens_1.text.code.regular.medium,
backgroundColor: design_system_tokens_1.themeTokens.color.background.tertiary,
border: `1px solid ${design_system_tokens_1.themeTokens.color.border.secondary}`,
borderRadius: design_system_tokens_1.borderRadius.extraSmall,
padding: `${design_system_tokens_1.space[2]} ${design_system_tokens_1.space[4]}`,
color: design_system_tokens_1.themeTokens.color.text.primary,
});