UNPKG

react-pdf-builder

Version:
421 lines (399 loc) 22.1 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __asyncValues = (this && this.__asyncValues) || function (o) { if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); var m = o[Symbol.asyncIterator], i; return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i); function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; } function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); } }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.BackdropGenerator = void 0; require("bootstrap/dist/css/bootstrap.css"); const react_1 = __importDefault(require("react")); const react_bootstrap_1 = require("react-bootstrap"); const openai_1 = __importDefault(require("openai")); const react_storage_complete_1 = require("react-storage-complete"); const copy_to_clipboard_1 = __importDefault(require("copy-to-clipboard")); const StoryComponent = (props) => { const [apiKey, setApiKey] = (0, react_storage_complete_1.useLocalStorage)('openAiApiKey', ''); const [showApiKey, setShowApiKey] = react_1.default.useState(false); const [modelId, setModelId] = (0, react_storage_complete_1.useLocalStorage)('openAiModelId', 'gpt-4o'); const [testing, setTesting] = react_1.default.useState(false); const [testError, setTestError] = react_1.default.useState(undefined); const [successful, setSuccessful] = react_1.default.useState(false); const [css, setCss] = (0, react_storage_complete_1.useLocalStorage)('css', ''); const [generationError, setGenerationError] = react_1.default.useState(undefined); const [generating, setGenerating] = react_1.default.useState(false); const [component, setComponent] = (0, react_storage_complete_1.useLocalStorage)('component', ''); const [copied, setCopied] = react_1.default.useState(false); const createOpenAIClient = () => { const client = new openai_1.default({ apiKey: apiKey !== null && apiKey !== void 0 ? apiKey : '', dangerouslyAllowBrowser: true, }); return client; }; const handleTest = () => { setSuccessful(false); setTestError(undefined); setTesting(true); try { const client = createOpenAIClient(); client.chat.completions .create({ messages: [{ role: 'user', content: 'Say the word test' }], model: modelId !== null && modelId !== void 0 ? modelId : '', }) .then((chatCompletion) => { console.log(chatCompletion.choices[0]); setSuccessful(true); }) .catch((e) => setTestError(e)) .finally(() => setTesting(false)); } catch (e) { setTestError(e); setTesting(false); } }; const handlePaste = () => __awaiter(void 0, void 0, void 0, function* () { try { const text = yield navigator.clipboard.readText(); setCss(text); } catch (err) { setGenerationError(err); } }); const handleGenerate = () => __awaiter(void 0, void 0, void 0, function* () { var _a, e_1, _b, _c; var _d, _e; setComponent(''); setGenerationError(undefined); setGenerating(true); try { const content = prompt + '\n\n' + css; const client = createOpenAIClient(); const stream = yield client.chat.completions.create({ model: 'gpt-4o', messages: [{ role: 'user', content }], stream: true, }); let message = ''; try { for (var _f = true, stream_1 = __asyncValues(stream), stream_1_1; stream_1_1 = yield stream_1.next(), _a = stream_1_1.done, !_a; _f = true) { _c = stream_1_1.value; _f = false; const chunk = _c; message += ((_e = (_d = chunk.choices[0]) === null || _d === void 0 ? void 0 : _d.delta) === null || _e === void 0 ? void 0 : _e.content) || ''; setComponent(message.replace('```ts', '').replace('```typescript', '').replace('```', '').trim()); } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (!_f && !_a && (_b = stream_1.return)) yield _b.call(stream_1); } finally { if (e_1) throw e_1.error; } } setGenerating(false); } catch (e) { setGenerationError(e); setGenerating(false); } }); const handleCopy = () => { (0, copy_to_clipboard_1.default)(component !== null && component !== void 0 ? component : ''); setCopied(true); }; react_1.default.useEffect(() => { let timeout = undefined; if (copied) { timeout = setTimeout(() => { setCopied(false); }, 1500); } return () => clearTimeout(timeout); }, [copied]); return (react_1.default.createElement("div", null, react_1.default.createElement(react_bootstrap_1.Card, null, react_1.default.createElement(react_bootstrap_1.Card.Header, null, "Backdrop Generator (Experimental)"), react_1.default.createElement(react_bootstrap_1.Card.Body, { className: "d-flex flex-column gap-3" }, react_1.default.createElement(react_bootstrap_1.Card.Text, null, "You can generate React PDF Builder backdrops from CSS gradients (such as those found at", ' ', react_1.default.createElement("a", { href: "https://www.gradientmagic.com/", target: "_blank", rel: "noreferrer" }, "gradientmagic.com"), ") using this AI-powered tool."), react_1.default.createElement(react_bootstrap_1.Card.Text, null, "You will need to provide an", ' ', react_1.default.createElement("a", { href: "https://platform.openai.com/api-keys", target: "_blank", rel: "noreferrer" }, "OpenAI API key"), ' ', "in order to use this tool."), react_1.default.createElement(react_bootstrap_1.Card, null, react_1.default.createElement(react_bootstrap_1.Card.Header, null, "OpenAI Settings"), react_1.default.createElement(react_bootstrap_1.Card.Body, { className: "d-flex flex-column gap-2" }, successful && (react_1.default.createElement(react_bootstrap_1.Alert, { variant: "success", className: "mb-1", dismissible: true, onClose: () => setSuccessful(false) }, "Your API key is working as expected!")), testError && (react_1.default.createElement(react_bootstrap_1.Alert, { variant: "danger", className: "mb-1", dismissible: true, onClose: () => setTestError(undefined) }, `${testError}`)), react_1.default.createElement(react_bootstrap_1.Form.Group, { controlId: "api-key-group", className: "d-flex flex-column" }, react_1.default.createElement("div", { className: "d-flex gap-1" }, react_1.default.createElement(react_bootstrap_1.Form.Control, { type: showApiKey ? 'text' : 'password', placeholder: "Paste OpenAI API key", value: apiKey !== null && apiKey !== void 0 ? apiKey : '', onChange: (e) => setApiKey(e.target.value) }), react_1.default.createElement(react_bootstrap_1.Button, { variant: "secondary", onClick: () => setShowApiKey(!showApiKey) }, showApiKey ? 'Hide' : 'Show')), react_1.default.createElement(react_bootstrap_1.Form.Text, { className: "text-muted" }, "Get an", ' ', react_1.default.createElement("a", { href: "https://platform.openai.com/api-keys", target: "_blank", rel: "noreferrer" }, "API key here"), ".")), react_1.default.createElement(react_bootstrap_1.Form.Group, { controlId: "model-id-group" }, react_1.default.createElement(react_bootstrap_1.Form.Control, { type: "text", placeholder: "Enter a model ID", value: modelId !== null && modelId !== void 0 ? modelId : '', onChange: (e) => setModelId(e.target.value) }), react_1.default.createElement("div", { className: "d-flex justify-content-between" }, react_1.default.createElement(react_bootstrap_1.Form.Text, { className: "text-muted" }, "See all available ", react_1.default.createElement("a", { href: "https://platform.openai.com/docs/models#current-model-aliases" }, "models"), "."), react_1.default.createElement("div", { className: "my-1" }, react_1.default.createElement(react_bootstrap_1.Button, { variant: "secondary", size: "sm", onClick: handleTest, disabled: testing }, testing && react_1.default.createElement(react_bootstrap_1.Spinner, { animation: "border", role: "status", size: "sm" }), " Test")))))), react_1.default.createElement(react_bootstrap_1.Card, null, react_1.default.createElement(react_bootstrap_1.Card.Header, { className: "d-flex justify-content-between gap-3" }, "CSS Gradient Styles", react_1.default.createElement("div", { className: "d-flex gap-1" }, react_1.default.createElement(react_bootstrap_1.Button, { variant: "outline-secondary", size: "sm", onClick: () => setCss('') }, "Clear"), react_1.default.createElement(react_bootstrap_1.Button, { variant: "secondary", size: "sm", onClick: handlePaste }, "Paste"))), react_1.default.createElement(react_bootstrap_1.Card.Body, null, generationError && (react_1.default.createElement(react_bootstrap_1.Alert, { variant: "danger", className: "mb-1", dismissible: true, onClose: () => setGenerationError(undefined) }, `${generationError}`)), react_1.default.createElement(react_bootstrap_1.Card.Text, null, "Paste your background CSS below and click Generate to create a React PDF Backdrop component that approximates the CSS styles."), react_1.default.createElement("div", { className: "d-flex flex-column gap-2" }, react_1.default.createElement(react_bootstrap_1.Form.Control, { as: "textarea", className: "font-monospace", style: { fontSize: '80%' }, placeholder: "Paste your background CSS here", rows: 4, value: css !== null && css !== void 0 ? css : '', onChange: (e) => setCss(e.target.value) }), react_1.default.createElement("div", { className: "d-flex justify-content-end" }, react_1.default.createElement(react_bootstrap_1.Button, { variant: "primary", onClick: handleGenerate, disabled: !(css === null || css === void 0 ? void 0 : css.trim()) || generating }, generating && react_1.default.createElement(react_bootstrap_1.Spinner, { animation: "border", role: "status", size: "sm" }), " Generate"))))), component && (react_1.default.createElement(react_bootstrap_1.Card, null, react_1.default.createElement(react_bootstrap_1.Card.Header, { className: "d-flex justify-content-between align-items-center gap-3" }, "React PDF Backdrop Component", ' ', react_1.default.createElement("div", { className: "d-flex gap-1" }, react_1.default.createElement(react_bootstrap_1.Button, { variant: "outline-secondary", size: "sm", onClick: () => setComponent('') }, "Clear"), react_1.default.createElement(react_bootstrap_1.Button, { variant: "primary", size: "sm", onClick: handleCopy }, copied ? 'Copied!' : 'Copy'))), react_1.default.createElement(react_bootstrap_1.Card.Body, { className: "font-monospace small bg-dark text-light" }, react_1.default.createElement(react_bootstrap_1.Card.Text, null, react_1.default.createElement("pre", null, component, generating && '|'))))))))); }; // === Setup === const meta = { title: 'Tools/Backdrops', // <-- Set to your story title component: StoryComponent, parameters: { options: { showPanel: false }, // Don't show addons panel }, }; exports.default = meta; // === Stories === exports.BackdropGenerator = { args: {}, }; const prompt = ` Important information: We are working with a React PDF library that supports backdrops. You have a GradientBackdrop component with the following interface: \`\`\`ts export interface GradientBackdropProps extends BoxProps { size: PageSizeString | { width: number; height: number }; orientation?: 'portrait' | 'landscape'; gradient: string[] | GradientStop[]; gradientType?: GradientType; } export interface GradientStop { offset: string | number; stopColor: string; stopOpacity?: string | number; } export enum GradientType { leftToRight = 'leftToRight', rightToLeft = 'rightToLeft', topToBottom = 'topToBottom', bottomToTop = 'bottomToTop', topLeftToBottomRight = 'topLeftToBottomRight', bottomLeftToTopRight = 'bottomLeftToTopRight', bottomRightToTopLeft = 'bottomRightToTopLeft', topRightToBottomLeft = 'topRightToBottomLeft', radial = 'radial', } // Color scheme swatches, use key names for swatch prop. export const swatches = { blue: '#0d6efd', indigo: '#6610f2', purple: '#6f42c1', pink: '#d63384', red: '#dc3545', orange: '#fd7e14', yellow: '#ffc107', green: '#198754', teal: '#20c997', cyan: '#0dcaf0', white: '#ffffff', gray100: '#f8f9fa', gray200: '#e9ecef', gray300: '#dee2e6', gray400: '#ced4da', gray500: '#adb5bd', gray600: '#6c757d', gray700: '#495057', gray800: '#343a40', gray900: '#212529', black: '#000000', }; \`\`\` Using this component, you can create various gradients by defining gradient stops. For example, the following has a gradient background with a diagonal overlay and another overlay of a circle in the middle: \`\`\`ts import React from 'react'; import { Box, BoxProps, PageSizeString, GradientStop, GradientType, RectangleShape, Backdrops } from 'react-pdf-builder'; export interface CustomBackdropProps extends BoxProps { size: PageSizeString | { width: number; height: number }; orientation?: 'portrait' | 'landscape'; gradient?: string[] | GradientStop[]; } export const CustomBackdrop = ({ size, orientation, gradient = ['#09C9BA', '#1283DD'], ...props }: CustomBackdropProps) => { const d = Backdrops.getDimensions(size, orientation); const width = d.width; const height = d.height; return ( <Box dir="y" {...props} style={{ position: 'absolute', ...props.style }}> {/* Background gradient */} <RectangleShape style={{ position: 'absolute' }} width={width} height={height} gradientType={GradientType.topLeftToBottomRight} gradient={gradient} /> {/* Overlays */} <RectangleShape style={{ position: 'absolute' }} width={width} height={height} gradientType={GradientType.bottomLeftToTopRight} gradient={[ { offset: '0%', stopColor: '#000000', stopOpacity: 0.1 }, { offset: '10%', stopColor: '#000000', stopOpacity: 0.1 }, { offset: '10%', stopColor: '#000000', stopOpacity: 0.05 }, { offset: '50%', stopColor: '#000000', stopOpacity: 0.05 }, { offset: '50%', stopColor: '#000000', stopOpacity: 0 }, { offset: '90%', stopColor: '#000000', stopOpacity: 0.02 }, { offset: '100%', stopColor: '#000000', stopOpacity: 0.02 }, ]} /> <RectangleShape style={{ position: 'absolute' }} width={width} height={height} gradientType={GradientType.radial} gradient={[ { offset: '0%', stopColor: '#000000', stopOpacity: 0.03 }, { offset: '30%', stopColor: '#000000', stopOpacity: 0.03 }, { offset: '30%', stopColor: '#000000', stopOpacity: 0 }, { offset: '100%', stopColor: '#000000', stopOpacity: 0 }, ]} radialGradientProps={{ cx: 0.5, cy: 0.5, }} /> </Box> ); }; \`\`\` The gradients use hard stops to create defined edges. As you can see, we can layer them by placing them one after the other. A gradient type can be specified, which will determine the direction. You can also define radial gradients, too. So you can create circles if you want by using hard stops with that, too. RectangleShape takes the following additional props, should you want to define a custom direction for linear gradients (x1, y1, x2, y2) or a custom location for the radial gradient (x, y), any of which can be outside the bounds of the visible area (0 to 1) if need be: \`\`\`ts linearGradientCoords?: { x1: number; y1: number; x2: number; y2: number }; radialGradientCoords?: { x: number; y: number }; \`\`\` Use these when \`GradientType\` is not sufficient. For reference, this is how the linear GradientType is translated to linear gradient coordinates: \`\`\`ts public static toGradientCoords(gradientType: GradientType) { const leftToRight = { x1: 0, y1: 0.5, x2: 1, y2: 0.5 }; let gradientCoords = leftToRight; switch (gradientType) { case GradientType.rightToLeft: gradientCoords = { x1: 1, y1: 0.5, x2: 0, y2: 0.5 }; break; case GradientType.topToBottom: gradientCoords = { x1: 0.5, y1: 0, x2: 0.5, y2: 1 }; break; case GradientType.bottomToTop: gradientCoords = { x1: 0.5, y1: 1, x2: 0.5, y2: 0 }; break; case GradientType.topLeftToBottomRight: gradientCoords = { x1: 0, y1: 0, x2: 1, y2: 1 }; break; case GradientType.topRightToBottomLeft: gradientCoords = { x1: 1, y1: 0, x2: 0, y2: 1 }; break; case GradientType.bottomLeftToTopRight: gradientCoords = { x1: 0, y1: 1, x2: 1, y2: 0 }; break; case GradientType.bottomRightToTopLeft: gradientCoords = { x1: 1, y1: 1, x2: 0, y2: 0 }; break; } return gradientCoords; } \`\`\` What I'd like to do next is to convert existing background gradient CSS into a component called CustomBackdrop. I'll give you the background CSS, and you port the styles as best as you can to the React format I'm using above, with that format for defining gradient stops, and layering them as shown above. Here's the starting component. Fill this out with the result: \`\`\`ts import React from 'react'; import { Box, BoxProps, PageSizeString, GradientStop, GradientType, RectangleShape, Backdrops } from 'react-pdf-builder'; export interface CustomBackdropProps extends BoxProps { size: PageSizeString | { width: number; height: number }; orientation?: 'portrait' | 'landscape'; gradient?: string[] | GradientStop[]; } export const CustomBackdrop = ({ size, orientation, gradient = ['#09C9BA', '#1283DD'], ...props }: CustomBackdropProps) => { const d = Backdrops.getDimensions(size, orientation); const width = d.width; const height = d.height; return ( <Box dir="y" {...props} style={{ position: 'absolute', ...props.style }}> {/* RectangleShape components go here -- First RectangleShape must use the gradient prop */} </Box> ); }; \`\`\` Instructions: - You are to port the provided CSS to the component format shown above. - Reply with the React TypeScript code only and no other commentary. - DO NOT REPLY IN MARKDOWN. Reply in raw code only. - Put any solid gradients with opacity of 1 first. Overlays should follow below that. - Only use RGB hex values for colors (no alpha channel). For transparency/alpha, use the stop opacity. There is no color called "transparent", use \`"#000000"\` with \`stopOpacity\` 0 instead. Remember that opacity of 0 means it's not visible. So be sure to only set that to 0 to create gaps. Make sure the parts we want visible are actually visible! - The first gradient must use the gradient prop so the user can customize it. Use a default value of colors that are closest to the ones in the CSS. - Reply with the code for the entire file, including imports. Here's the CSS to port: `;