Merge pull request #735 from bigcapitalhq/add-pdf-templates-package

feat: add shared package to pdf templates to render in the server and…
This commit is contained in:
Ahmed Bouhuolia
2024-11-05 17:20:12 +02:00
committed by GitHub
31 changed files with 1904 additions and 408 deletions

View File

@@ -20,10 +20,11 @@
"bigcapital": "./bin/bigcapital.js"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.576.0",
"@aws-sdk/s3-request-presigner": "^3.583.0",
"@bigcapital/utils": "*",
"@bigcapital/email-components": "*",
"@bigcapital/pdf-templates": "*",
"@aws-sdk/client-s3": "^3.576.0",
"@aws-sdk/s3-request-presigner": "^3.583.0",
"@casl/ability": "^5.4.3",
"@hapi/boom": "^7.4.3",
"@lemonsqueezy/lemonsqueezy.js": "^2.2.0",

View File

@@ -450,8 +450,8 @@ export default class SaleInvoicesController extends BaseController {
ACCEPT_TYPE.APPLICATION_JSON,
ACCEPT_TYPE.APPLICATION_PDF,
]);
// Retrieves invoice in pdf format.
if (ACCEPT_TYPE.APPLICATION_PDF == acceptType) {
// Retrieves invoice in PDF format.
if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
const [pdfContent, filename] =
await this.saleInvoiceApplication.saleInvoicePdf(
tenantId,

View File

@@ -1,4 +1,5 @@
import { Inject, Service } from 'typedi';
import { renderInvoicePaperTemplateHtml } from '@bigcapital/pdf-templates';
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
import { GetSaleInvoice } from './GetSaleInvoice';
@@ -8,6 +9,7 @@ import { InvoicePdfTemplateAttributes } from '@/interfaces';
import { SaleInvoicePdfTemplate } from './SaleInvoicePdfTemplate';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import { renderInvoicePaymentEmail } from '@bigcapital/email-components';
@Service()
export class SaleInvoicePdf {
@@ -45,11 +47,9 @@ export class SaleInvoicePdf {
tenantId,
invoiceId
);
const htmlContent = await this.templateInjectable.render(
tenantId,
'modules/invoice-standard',
brandingAttributes
);
const htmlContent = renderInvoicePaperTemplateHtml({
...brandingAttributes,
});
// Converts the given html content to pdf document.
const buffer = await this.chromiumlyTenancy.convertHtmlContent(
tenantId,

View File

@@ -4,6 +4,7 @@
"private": true,
"dependencies": {
"@bigcapital/utils": "*",
"@bigcapital/pdf-templates": "*",
"@blueprintjs-formik/core": "^0.3.7",
"@blueprintjs-formik/datetime": "^0.3.7",
"@blueprintjs-formik/select": "^0.3.5",

1077
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

24
shared/pdf-templates/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,23 @@
import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'@storybook/addon-styling',
],
framework: {
name: '@storybook/react-vite',
options: {
builder: {
viteConfigPath: '.storybook/vite.config.ts',
},
},
},
docs: {
autodocs: 'tag',
},
};
export default config;

View File

@@ -0,0 +1,15 @@
import type { Preview } from '@storybook/react';
const preview: Preview = {
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
};
export default preview;

View File

@@ -0,0 +1,17 @@
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vitest/config';
import tailwindcss from 'tailwindcss';
import { UserConfigExport } from 'vite';
const app = async (): Promise<UserConfigExport> => {
return defineConfig({
plugins: [react()],
css: {
postcss: {
plugins: [tailwindcss],
},
},
});
};
// https://vitejs.dev/config/
export default app;

View File

@@ -0,0 +1,28 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)

View File

@@ -0,0 +1,62 @@
{
"name": "@bigcapital/pdf-templates",
"version": "0.0.0",
"scripts": {
"build": "webpack --config webpack.config.js",
"lint": "eslint .",
"preview": "vite preview",
"storybook:dev": "storybook dev -p 6006",
"storybook:build": "storybook build"
},
"main": "./dist/components.umd.js",
"module": "./dist/components.es.js",
"types": "./dist/src/index.d.ts",
"exports": {
".": {
"types": "./dist/src/index.d.ts",
"import": "./dist/components.es.js",
"require": "./dist/components.umd.js"
}
},
"dependencies": {
"@emotion/cache": "^11.13.1",
"@emotion/css": "^11.13.4",
"@emotion/react": "^11.13.3",
"@emotion/server": "^11.11.0",
"@types/lodash": "^4.17.13",
"@xstyled/emotion": "^3.8.1",
"@xstyled/system": "^3.8.1",
"classnames": "^2.3.2",
"css-loader": "^6.x",
"declaration-bundler-webpack-plugin": "^1.0.3",
"fork-ts-checker-webpack-plugin": "^9.0.2",
"lodash": "^4.17.15",
"react": "18.3.1",
"react-dom": "18.3.1",
"style-loader": "^3.x",
"tailwindcss": "^3.4.14",
"ts-loader": "^9.x",
"webpack": "^5.x",
"webpack-cli": "^5.x"
},
"devDependencies": {
"@eslint/js": "^9.13.0",
"@storybook/addon-essentials": "7.2.2",
"@storybook/addon-interactions": "7.2.2",
"@storybook/addon-links": "7.2.2",
"@storybook/addon-styling": "1.3.6",
"@storybook/blocks": "7.2.2",
"@storybook/react": "7.2.2",
"@storybook/testing-library": "0.2.0",
"@types/react": "18.3.4",
"@types/react-dom": "18.3.0",
"eslint": "^9.13.0",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.13",
"eslint-plugin-storybook": "0.6.13",
"globals": "^15.11.0",
"storybook": "7.2.2",
"typescript": "~5.6.2",
"typescript-eslint": "^8.10.0"
}
}

View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@@ -0,0 +1,326 @@
import {
PaperTemplate,
PaperTemplateProps,
PaperTemplateTotalBorder,
} from './PaperTemplate';
import { Box } from '../lib/layout/Box';
import { Text } from '../lib/text/Text';
import { Stack } from '../lib/layout/Stack';
import { Group } from '../lib/layout/Group';
import {
DefaultPdfTemplateTerms,
DefaultPdfTemplateItemDescription,
DefaultPdfTemplateStatement,
DefaultPdfTemplateItemName,
DefaultPdfTemplateAddressBilledTo,
DefaultPdfTemplateAddressBilledFrom,
} from './_constants';
interface PapaerLine {
item?: string;
description?: string;
quantity?: string;
rate?: string;
total?: string;
}
interface PaperTax {
label: string;
amount: string;
}
export interface InvoicePaperTemplateProps extends PaperTemplateProps {
primaryColor?: string;
secondaryColor?: string;
showCompanyLogo?: boolean;
companyLogoUri?: string;
showInvoiceNumber?: boolean;
invoiceNumber?: string;
invoiceNumberLabel?: string;
showDateIssue?: boolean;
dateIssue?: string;
dateIssueLabel?: string;
showDueDate?: boolean;
dueDate?: string;
dueDateLabel?: string;
companyName?: string;
bigtitle?: string;
// Address
showCustomerAddress?: boolean;
customerAddress?: string;
showCompanyAddress?: boolean;
companyAddress?: string;
billedToLabel?: string;
// Entries
lineItemLabel?: string;
lineQuantityLabel?: string;
lineRateLabel?: string;
lineTotalLabel?: string;
// Totals
showTotal?: boolean;
totalLabel?: string;
total?: string;
showDiscount?: boolean;
discountLabel?: string;
discount?: string;
showSubtotal?: boolean;
subtotalLabel?: string;
subtotal?: string;
showPaymentMade?: boolean;
paymentMadeLabel?: string;
paymentMade?: string;
showTaxes?: boolean;
showDueAmount?: boolean;
showBalanceDue?: boolean;
balanceDueLabel?: string;
balanceDue?: string;
// Footer
termsConditionsLabel?: string;
showTermsConditions?: boolean;
termsConditions?: string;
statementLabel?: string;
showStatement?: boolean;
statement?: string;
lines?: Array<PapaerLine>;
taxes?: Array<PaperTax>;
}
export function InvoicePaperTemplate({
primaryColor,
secondaryColor,
companyName = 'Bigcapital Technology, Inc.',
showCompanyLogo = true,
companyLogoUri = '',
dueDate = 'September 3, 2024',
dueDateLabel = 'Date due',
showDueDate = true,
dateIssue = 'September 3, 2024',
dateIssueLabel = 'Date of issue',
showDateIssue = true,
// dateIssue,
invoiceNumberLabel = 'Invoice number',
invoiceNumber = '346D3D40-0001',
showInvoiceNumber = true,
// Address
showCustomerAddress = true,
customerAddress = DefaultPdfTemplateAddressBilledTo,
showCompanyAddress = true,
companyAddress = DefaultPdfTemplateAddressBilledFrom,
billedToLabel = 'Billed To',
// Entries
lineItemLabel = 'Item',
lineQuantityLabel = 'Qty',
lineRateLabel = 'Rate',
lineTotalLabel = 'Total',
totalLabel = 'Total',
subtotalLabel = 'Subtotal',
discountLabel = 'Discount',
paymentMadeLabel = 'Payment Made',
balanceDueLabel = 'Balance Due',
// Totals
showTotal = true,
showSubtotal = true,
showDiscount = true,
showTaxes = true,
showPaymentMade = true,
showDueAmount = true,
showBalanceDue = true,
total = '$662.75',
subtotal = '630.00',
discount = '0.00',
paymentMade = '100.00',
balanceDue = '$562.75',
// Footer paragraphs.
termsConditionsLabel = 'Terms & Conditions',
showTermsConditions = true,
termsConditions = DefaultPdfTemplateTerms,
lines = [
{
item: DefaultPdfTemplateItemName,
description: DefaultPdfTemplateItemDescription,
rate: '1',
quantity: '1000',
total: '$1000.00',
},
],
taxes = [
{ label: 'Sample Tax1 (4.70%)', amount: '11.75' },
{ label: 'Sample Tax2 (7.00%)', amount: '21.74' },
],
statementLabel = 'Statement',
showStatement = true,
statement = DefaultPdfTemplateStatement,
...props
}: InvoicePaperTemplateProps) {
return (
<PaperTemplate
primaryColor={primaryColor}
secondaryColor={secondaryColor}
{...props}
>
<Stack spacing={24}>
<Group align="start" spacing={10}>
<Stack flex={1}>
<PaperTemplate.BigTitle title={'Invoice'} />
<PaperTemplate.TermsList>
{showInvoiceNumber && (
<PaperTemplate.TermsItem label={invoiceNumberLabel}>
{invoiceNumber}
</PaperTemplate.TermsItem>
)}
{showDateIssue && (
<PaperTemplate.TermsItem label={dateIssueLabel}>
{dateIssue}
</PaperTemplate.TermsItem>
)}
{showDueDate && (
<PaperTemplate.TermsItem label={dueDateLabel}>
{dueDate}
</PaperTemplate.TermsItem>
)}
</PaperTemplate.TermsList>
</Stack>
{companyLogoUri && showCompanyLogo && (
<PaperTemplate.Logo logoUri={companyLogoUri} />
)}
</Group>
<PaperTemplate.AddressesGroup>
{showCompanyAddress && (
<PaperTemplate.Address>
<Box dangerouslySetInnerHTML={{ __html: companyAddress }} />
</PaperTemplate.Address>
)}
{showCustomerAddress && (
<PaperTemplate.Address>
<strong>{billedToLabel}</strong>
<Box dangerouslySetInnerHTML={{ __html: customerAddress }} />
</PaperTemplate.Address>
)}
</PaperTemplate.AddressesGroup>
<Stack spacing={0}>
<PaperTemplate.Table
columns={[
{
label: lineItemLabel,
accessor: (data) => (
<Stack spacing={2}>
<Text>{data.item}</Text>
<Text
color={'#5f6b7c'}
fontSize={12}
>
{data.description}
</Text>
</Stack>
),
},
{ label: lineQuantityLabel, accessor: 'quantity', align: 'right' },
{ label: lineRateLabel, accessor: 'rate', align: 'right' },
{ label: lineTotalLabel, accessor: 'total', align: 'right' },
]}
data={lines}
/>
<PaperTemplate.Totals>
{showSubtotal && (
<PaperTemplate.TotalLine
label={subtotalLabel}
amount={subtotal}
border={PaperTemplateTotalBorder.Gray}
/>
)}
{showDiscount && (
<PaperTemplate.TotalLine
label={discountLabel}
amount={discount}
/>
)}
{showTaxes && (
<>
{taxes.map((tax, index) => (
<PaperTemplate.TotalLine
key={index}
label={tax.label}
amount={tax.amount}
/>
))}
</>
)}
{showTotal && (
<PaperTemplate.TotalLine
label={totalLabel}
amount={total}
border={PaperTemplateTotalBorder.Dark}
style={{ fontWeight: 500 }}
/>
)}
{showPaymentMade && (
<PaperTemplate.TotalLine
label={paymentMadeLabel}
amount={paymentMade}
/>
)}
{showBalanceDue && (
<PaperTemplate.TotalLine
label={balanceDueLabel}
amount={balanceDue}
border={PaperTemplateTotalBorder.Dark}
style={{ fontWeight: 500 }}
/>
)}
</PaperTemplate.Totals>
</Stack>
<Stack spacing={0}>
{showTermsConditions && termsConditions && (
<PaperTemplate.Statement label={termsConditionsLabel}>
{termsConditions}
</PaperTemplate.Statement>
)}
{showStatement && statement && (
<PaperTemplate.Statement label={statementLabel}>
{statement}
</PaperTemplate.Statement>
)}
</Stack>
</Stack>
</PaperTemplate>
);
}

View File

@@ -0,0 +1,279 @@
import React from 'react';
import clsx from 'classnames';
import { get, isFunction } from 'lodash';
import { x } from '@xstyled/emotion';
import { css } from '@emotion/css';
import { Box, BoxProps } from '../lib/layout/Box';
import { Group, GroupProps } from '../lib/layout/Group';
export interface PaperTemplateProps extends BoxProps {
primaryColor?: string;
secondaryColor?: string;
children?: React.ReactNode;
}
export function PaperTemplate({
primaryColor,
secondaryColor,
children,
...restProps
}: PaperTemplateProps) {
return (
<Box
backgroundColor="#fff"
color="#111"
boxShadow="inset 0 4px 0px 0 var(--invoice-primary-color)"
padding="30px 30px"
fontSize="12px"
position="relative"
m="0 auto"
h="1123px"
w="794px"
{...restProps}
className={clsx(
restProps?.className,
css`
@media print {
width: auto !important;
height: auto !important;
}
`
)}
>
<style>{`:root { --invoice-primary-color: ${primaryColor}; --invoice-secondary-color: ${secondaryColor}; }`}</style>
{children}
</Box>
);
}
interface PaperTemplateBigTitleProps {
title: string;
}
PaperTemplate.BigTitle = ({ title }: PaperTemplateBigTitleProps) => {
return (
<x.h1
fontSize={'30px'}
margin={0}
lineHeight={1}
fontWeight={500}
color={'#333'}
>
{title}
</x.h1>
);
};
interface PaperTemplateLogoProps {
logoUri: string;
}
PaperTemplate.Logo = ({ logoUri }: PaperTemplateLogoProps) => {
return (
<x.div overflow={'hidden'}>
<x.img
width={'100%'}
height={'100%'}
maxWidth={'260px'}
maxHeight={'100px'}
alt=""
src={logoUri}
/>
</x.div>
);
};
interface PaperTemplateTableProps {
columns: Array<{
accessor: string | ((data: Record<string, any>) => JSX.Element);
label: string;
value?: JSX.Element;
align?: 'left' | 'center' | 'right';
}>;
data: Array<Record<string, any>>;
}
PaperTemplate.Table = ({ columns, data }: PaperTemplateTableProps) => {
return (
<table
className={css`
width: 100%;
border-collapse: collapse;
text-align: left;
thead th {
font-weight: 400;
border-bottom: 1px solid #000;
padding: 2px 10px;
color: #333;
&.rate,
&.total {
text-align: right;
}
&:first-of-type {
padding-left: 0;
}
&:last-of-type {
padding-right: 0;
}
}
tbody {
td {
border-bottom: 1px solid #f6f6f6;
padding: 12px 10px;
&:first-of-type {
padding-left: 0;
}
&:last-of-type {
padding-right: 0;
}
&.rate,
&.total {
text-align: right;
}
}
}
`}
>
<thead>
<tr>
{columns.map((col, index) => (
<x.th key={index} textAlign={col.align}>
{col.label}
</x.th>
))}
</tr>
</thead>
<tbody>
{data.map((_data: any) => (
<tr>
{columns.map((column, index) => (
<x.td textAlign={column.align} key={index}>
{isFunction(column?.accessor)
? column?.accessor(_data)
: get(_data, column.accessor)}
</x.td>
))}
</tr>
))}
</tbody>
</table>
);
};
export enum PaperTemplateTotalBorder {
Gray = 'gray',
Dark = 'dark',
}
PaperTemplate.Totals = ({ children }: { children: React.ReactNode }) => {
return (
<x.div
style={{
display: 'flex',
flexDirection: 'column',
marginLeft: 'auto',
width: '300px',
}}
>
{children}
</x.div>
);
};
const totalBottomBordered = css`
border-bottom: 1px solid #000;
`;
const totalBottomGrayBordered = css`
border-bottom: 1px solid #dadada;
`;
PaperTemplate.TotalLine = ({
label,
amount,
border,
style,
}: {
label: string;
amount: string;
border?: PaperTemplateTotalBorder;
style?: any;
}) => {
return (
<x.div
display={'flex'}
padding={'4px 0'}
className={clsx({
[totalBottomBordered]: border === PaperTemplateTotalBorder.Dark,
[totalBottomGrayBordered]: border === PaperTemplateTotalBorder.Gray,
})}
>
<x.div min-w="160px">{label}</x.div>
<x.div flex={'1 1 auto'} textAlign={'right'}>
{amount}
</x.div>
</x.div>
);
};
PaperTemplate.AddressesGroup = (props: GroupProps) => {
return (
<Group
spacing={10}
align={'flex-start'}
{...props}
className={css`
> div {
flex: 1;
}
`}
/>
);
};
PaperTemplate.Address = ({ children }: { children: React.ReactNode }) => {
return <Box>{children}</Box>;
};
PaperTemplate.Statement = ({
label,
children,
}: {
label: string;
children: React.ReactNode;
}) => {
return (
<x.div mb={'20px'}>
{label && <x.div color={'#666'}>{label}</x.div>}
<x.div>{children}</x.div>
</x.div>
);
};
PaperTemplate.TermsList = ({ children }: { children: React.ReactNode }) => {
return (
<x.div display={'flex'} flexDirection={'column'} gap={'4px'}>
{children}
</x.div>
);
};
PaperTemplate.TermsItem = ({
label,
children,
}: {
label: string;
children: React.ReactNode;
}) => {
return (
<Group spacing={12}>
<x.div minWidth={'120px'} color={'#333'}>
{label}
</x.div>
<x.div>{children}</x.div>
</Group>
);
};

View File

@@ -0,0 +1,66 @@
import { CacheProvider, ThemeProvider } from '@emotion/react';
import { EmotionCache } from '@emotion/cache';
import { defaultTheme } from '@xstyled/system';
import { createGlobalStyle, Preflight } from '@xstyled/emotion';
const theme = {
...defaultTheme,
};
export function PaperTemplateLayout({
cache,
children,
}: {
children: React.ReactNode;
cache: EmotionCache;
}) {
return (
<CacheProvider value={cache}>
<ThemeProvider theme={theme}>
<Preflight />
<GlobalStyles />
{children}
</ThemeProvider>
</CacheProvider>
);
}
// Create global styles to set the body font
const GlobalStyles = createGlobalStyle`
*,
*::before,
*::after {
box-sizing: border-box;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
body{
margin: 0;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #000;
background-color: #fff;
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: transparent;
}
body, h1, h2, h3, h4, h5, h6{
font-family: "Open Sans", sans-serif;
font-optical-sizing: auto;
font-style: normal;
}
`;

View File

@@ -0,0 +1,25 @@
export const DefaultPdfTemplateTerms =
'All services provided are non-refundable. For any disputes, please contact us within 7 days of receiving this invoice.';
export const DefaultPdfTemplateStatement =
'Thank you for your business. We look forward to working with you again!';
export const DefaultPdfTemplateItemName = 'Web development';
export const DefaultPdfTemplateItemDescription =
'Website development with content and SEO optimization';
export const DefaultPdfTemplateAddressBilledTo = `Bigcapital Technology, Inc.<br />
131 Continental Dr, <br />
Suite 305, <br />
Newark, Delaware 19713, <br />
United States,<br />
+1 762-339-5634
`;
export const DefaultPdfTemplateAddressBilledFrom = `131 Continental Dr Suite 305 Newark, <br />
Delaware 19713,<br />
United States, <br />
+1 762-339-5634, <br />
ahmed@bigcapital.app
`;

View File

@@ -0,0 +1,5 @@
export const OpenSansFontLink = `
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet">
`;

View File

@@ -0,0 +1,3 @@
export * from './components/PaperTemplate';
export * from './components/InvoicePaperTemplate';
export * from './renders/render-invoice-paper-template';

View File

@@ -0,0 +1,19 @@
import React, { forwardRef, Ref } from 'react';
import { SystemProps, x } from '@xstyled/emotion';
interface IProps {
className?: string;
}
export interface BoxProps
extends SystemProps,
IProps,
Omit<React.HTMLProps<HTMLDivElement>, 'color' | 'as'> { }
export const Box = forwardRef(
({ className, ...rest }: BoxProps, ref: Ref<HTMLDivElement>) => {
const Element = x.div;
return <Element className={className} ref={ref} {...rest} />;
},
);
Box.displayName = '@bigcapital/Box';

View File

@@ -0,0 +1,58 @@
import React from 'react';
import { SystemProps } from '@xstyled/emotion';
import { Box } from './Box';
import { filterFalsyChildren } from './utils';
export type GroupPosition = 'right' | 'center' | 'left' | 'apart';
export const GROUP_POSITIONS = {
left: 'flex-start',
center: 'center',
right: 'flex-end',
apart: 'space-between',
};
export interface GroupProps
extends SystemProps,
Omit<React.ComponentPropsWithoutRef<'div'>, 'color'> {
/** Defines justify-content property */
position?: GroupPosition;
/** Defined flex-wrap property */
noWrap?: boolean;
/** Defines flex-grow property for each element, true -> 1, false -> 0 */
grow?: boolean;
/** Space between elements */
spacing?: number;
/** Defines align-items css property */
align?: React.CSSProperties['alignItems'];
}
export function Group({
position = 'left',
spacing = 20,
align = 'center',
noWrap,
children,
...props
}: GroupProps) {
const filteredChildren = filterFalsyChildren(children);
return (
<Box
boxSizing={'border-box'}
display={'flex'}
flexDirection={'row'}
alignItems={align}
flexWrap={noWrap ? 'nowrap' : 'wrap'}
justifyContent={GROUP_POSITIONS[position]}
gap={`${spacing}px`}
{...props}
>
{filteredChildren}
</Box>
);
}

View File

@@ -0,0 +1,33 @@
import React from 'react';
import { x, SystemProps } from '@xstyled/emotion';
export interface StackProps
extends SystemProps,
Omit<React.ComponentPropsWithoutRef<'div'>, 'color'> {
/** Key of theme.spacing or number to set gap in px */
spacing?: number;
/** align-items CSS property */
align?: React.CSSProperties['alignItems'];
/** justify-content CSS property */
justify?: React.CSSProperties['justifyContent'];
}
export function Stack({
spacing = 20,
align = 'stretch',
justify = 'top',
...restProps
}: StackProps) {
return (
<x.div
display={'flex'}
flexDirection="column"
justifyContent="justify"
gap={`${spacing}px`}
alignItems={align}
{...restProps}
/>
);
}

View File

@@ -0,0 +1,5 @@
import React from 'react';
export const filterFalsyChildren = (children: React.ReactNode) => {
return React.Children.toArray(children).filter(Boolean);
};

View File

@@ -0,0 +1,4 @@
export * from './layout/Stack';
export * from './layout/Group';
export * from './layout/Box';
export * from './text/Text';

View File

@@ -0,0 +1,9 @@
import { SystemProps, x } from '@xstyled/emotion';
export interface TextProps extends SystemProps {
children?: React.ReactNode;
}
export const Text = ({ children, ...restProps }: TextProps) => {
return <x.div {...restProps}>{children}</x.div>;
};

View File

@@ -0,0 +1,21 @@
import { renderToString } from 'react-dom/server';
import createCache from '@emotion/cache';
import { css } from '@emotion/css';
import {
InvoicePaperTemplate,
InvoicePaperTemplateProps,
} from '../components/InvoicePaperTemplate';
import { PaperTemplateLayout } from '../components/PaperTemplateLayout';
import { extractCritical } from '@emotion/server';
import { OpenSansFontLink } from '../constants';
import { renderSSR } from './render-ssr';
export const renderInvoicePaperTemplateHtml = (
props: InvoicePaperTemplateProps
) => {
return renderSSR(
<InvoicePaperTemplate
{...props}
/>
);
};

View File

@@ -0,0 +1,31 @@
import { renderToString } from 'react-dom/server';
import createCache from '@emotion/cache';
import { extractCritical } from '@emotion/server';
import { OpenSansFontLink } from '../constants';
import { PaperTemplateLayout } from '../components/PaperTemplateLayout';
export const renderSSR = (children: React.ReactNode) => {
const key = 'invoice-paper-template';
const cache = createCache({ key });
const renderedHtml = renderToString(
<PaperTemplateLayout cache={cache}>{children}</PaperTemplateLayout>
);
const extractedHtml = extractCritical(renderedHtml);
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Invoice</title>
${OpenSansFontLink}
<style data-emotion="${key} ${extractedHtml.ids.join(' ')}">${extractedHtml.css
}</style>
</head>
<body>
<div id="root">${extractedHtml.html}</div>
</body>
</html>`;
};

View File

@@ -0,0 +1,8 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{ts,tsx}'],
theme: {
extend: {},
},
plugins: [],
};

View File

@@ -0,0 +1,28 @@
{
"compilerOptions": {
// "noEmit": true,
"composite": false,
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"inlineSources": false,
"isolatedModules": true,
"moduleResolution": "node",
"noUnusedLocals": false,
"noUnusedParameters": false,
"preserveWatchOutput": true,
"skipLibCheck": true,
"strict": true,
"strictNullChecks": true,
"jsx": "react-jsx",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
"target": "ESNext",
"types": ["vitest/globals"],
"resolveJsonModule": true,
"outDir": "dist",
},
"include": ["."],
"exclude": ["dist", "build", "node_modules"]
}

View File

@@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -0,0 +1,61 @@
import react from '@vitejs/plugin-react';
import path from 'node:path';
import { defineConfig } from 'vitest/config';
import dts from 'vite-plugin-dts';
import tailwindcss from 'tailwindcss';
import { UserConfigExport } from 'vite';
import { name } from './package.json';
const app = async (): Promise<UserConfigExport> => {
/**
* Removes everything before the last
* @octocat/library-repo -> library-repo
* vite-component-library-template -> vite-component-library-template
*/
const formattedName = name.match(/[^/]+$/)?.[0] ?? name;
return defineConfig({
define: {
isBrowser: 'false', // This will replace isBrowser with false in the bundled code
},
ssr: {
noExternal: true,
},
plugins: [
react(),
dts({
insertTypesEntry: true,
}),
],
css: {
postcss: {
plugins: [tailwindcss],
},
},
build: {
lib: {
entry: path.resolve(__dirname, 'src/lib/main.ts'),
name: formattedName,
formats: ['es', 'umd'],
fileName: (format: string) => `${formattedName}.${format}.js`,
},
rollupOptions: {
// external: ['react', 'react/jsx-runtime', 'react-dom', 'tailwindcss'],
// output: {
// globals: {
// react: 'React',
// 'react/jsx-runtime': 'react/jsx-runtime',
// 'react-dom': 'ReactDOM',
// tailwindcss: 'tailwindcss',
// },
// },
},
},
test: {
globals: true,
environment: 'jsdom',
},
});
};
// https://vitejs.dev/config/
export default app;

View File

@@ -0,0 +1,36 @@
// webpack.config.js
const path = require('path');
module.exports = {
mode: 'production',
target: 'node',
entry: './src/index.ts',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'components.umd.js',
library: {
name: '@bigcapital/library-components',
type: 'umd',
},
globalObject: 'this',
clean: true,
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx'],
},
module: {
rules: [
{
test: /\.(ts|tsx)$/,
use: {
loader: 'ts-loader',
},
exclude: /node_modules/,
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
};