1
0
Fork 0

feat[react]: Linting with eslint 9

This commit is contained in:
joostdecock 2025-05-30 11:29:55 +02:00
parent 14eab04d5b
commit f69093b0dc
99 changed files with 1260 additions and 956 deletions

View file

@ -4,12 +4,12 @@ import React, { useState } from 'react'
* DaisyUI's accordion seems rather unreliable.
* So instead, we handle this in React state
*/
const getProps = (isActive = false) => ({
const getProps = () => ({
className: `tw:p-0 tw:rounded-lg tw:bg-transparent tw:hover:cursor-pointer
tw:w-full tw:h-auto tw:content-start tw:text-left tw:list-none`,
})
const getSubProps = (isActive) => ({
const getSubProps = () => ({
className: `tw:p-0 tw:rounded-none tw:bg-transparent tw:w-full tw:h-auto
tw:content-start tw:text-left tw:list-none`,
})

View file

@ -1,6 +1,6 @@
import React from 'react'
import mustache from 'mustache'
import { flattenFlags, stripNamespace, bundlePatternTranslations } from '../lib/index.mjs'
import { flattenFlags } from '../lib/index.mjs'
import {
ChatIcon,
ErrorIcon,
@ -115,7 +115,7 @@ export const FlagsAccordionTitle = ({ flags }) => {
)
}
export const FlagsAccordionEntries = ({ flags, update, pattern, strings }) => {
export const FlagsAccordionEntries = ({ flags, update, strings }) => {
const flagList = flattenFlags(flags)
if (Object.keys(flagList).length < 1) return null

View file

@ -24,7 +24,6 @@ import {
ResetIcon,
RightIcon,
RocketIcon,
RotateIcon,
SaIcon,
SaveAsIcon,
SaveIcon,
@ -133,7 +132,7 @@ export const HeaderMenuTestViewDesignMeasurements = (props) => {
}
export const HeaderMenuDropdown = (props) => {
const { tooltip, toggle, open, setOpen, id, end = false } = props
const { toggle, open, setOpen, id } = props
const [localOpen, setLocalOpen] = useState(false)
useEffect(() => {
@ -460,10 +459,8 @@ export const HeaderMenuUndoIcons = (props) => {
}
export const HeaderMenuTestIcons = (props) => {
const { update, state, Design } = props
const { update } = props
const Button = HeaderMenuButton
const size = 'tw:w-5 tw:h-5'
const undos = state._?.undos && state._.undos.length > 0 ? state._.undos : false
return (
<div className="tw:flex tw:flex-row tw:flex-wrap tw:items-center tw:justify-center tw:px-0.5 tw:lg:px-1">
@ -660,11 +657,6 @@ export const HeaderMenuLayoutViewIcons = (props) => {
}
const pages = pattern.setStores[0].get('pages', {})
const format = state.ui.print?.pages?.size
? state.ui.print.pages.size
: settings.units === 'imperial'
? 'letter'
: 'a4'
const { cols, rows, count } = pages
const blank = cols * rows - count

View file

@ -35,11 +35,6 @@ export const LoadingStatus = ({ state, update }) => {
if (!state._.loading || Object.keys(state._.loading).length < 1) return null
const colorClasses = {
info: 'tw:bg-info tw:text-info-content',
primary: 'tw:bg-primary tw:text-primary-content',
}
return (
<div className="tw:fixed tw:bottom-4 md:tw:buttom-28 tw:left-0 tw:w-full tw:z-30 tw:md:px-4 tw:md:mx-auto mb-4">
<div className="tw:flex tw:flex-col tw:gap-2">

View file

@ -85,7 +85,7 @@ export const MovablePattern = ({
const sortedRenderProps = { ...renderProps, stacks: sortedStacks }
const Stack = ({ stackName, stack, settings, components, t }) => (
const Stack = ({ stackName, stack, settings, components }) => (
<MovableStack
{...{
stackName,
@ -397,7 +397,7 @@ function angle(pointA, pointB) {
const rectSize = 24
const Button = ({ onClickCb, transform, Icon, children, title = '' }) => {
const Button = ({ onClickCb, transform, Icon, title = '' }) => {
const _onClick = (event) => {
event.stopPropagation()
onClickCb(event)
@ -413,23 +413,6 @@ const Button = ({ onClickCb, transform, Icon, children, title = '' }) => {
)
}
export const ShowButtonsToggle = ({ ui, update }) => {
const hideButtons = (evt) => {
update.ui('hideMovableButtons', !evt.target.checked)
}
return (
<label className="label cursor-pointer">
<span className="label-text text-lg mr-2">{t('showMovableButtons')}</span>
<input
type="checkbox"
className="toggle toggle-primary"
checked={!ui.hideMovableButtons}
onChange={hideButtons}
/>
</label>
)
}
/** buttons for manipulating the part */
export const Buttons = ({ transform, flip, rotate, resetPart, rotate90, iconSize }) => {
return (

View file

@ -1,17 +1,9 @@
import React from 'react'
import { useDesignTranslation } from '@freesewing/react/hooks/useDesignTranslation'
import { ZoomContextProvider } from './ZoomablePattern.mjs'
import {
HeaderMenu,
HeaderMenuDraftViewDesignOptions,
HeaderMenuDraftViewCoreSettings,
HeaderMenuDraftViewUiPreferences,
HeaderMenuDraftViewFlags,
} from './HeaderMenu.mjs'
import { HeaderMenu } from './HeaderMenu.mjs'
import { DesignOptionsMenu } from './menus/DesignOptionsMenu.mjs'
import { CoreSettingsMenu } from './menus/CoreSettingsMenu.mjs'
import { UiPreferencesMenu } from './menus/UiPreferencesMenu.mjs'
import { Accordion } from './Accordion.mjs'
/**
* A layout for views that include a drafted pattern
@ -24,9 +16,7 @@ import { Accordion } from './Accordion.mjs'
* @param {object] pattern - The drafted pattern
*/
export const PatternLayout = (props) => {
const { menu = null, Design, pattern, update, config, state } = props
const i18n = useDesignTranslation(Design.designConfig.data.id)
const flags = props.pattern?.setStores?.[0]?.plugins?.['plugin-annotations']?.flags
const { Design, pattern, update, config, state } = props
return (
<ZoomContextProvider>

View file

@ -58,7 +58,7 @@ export const UserSetPicker = ({
href={config.hrefNewSet}
className="tw:daisy-btn tw:daisy-btn-accent tw:capitalize"
target="_BLANK"
rel="nofollow"
rel="noreferrer"
>
Create a new measurements set
</a>

View file

@ -7,7 +7,7 @@ import { ZoomInIcon, ZoomOutIcon, RotateIcon } from '@freesewing/react/component
* A pattern you can pan and zoom
*/
export const ZoomablePattern = forwardRef(function ZoomablePatternRef(props, ref) {
const { renderProps, rotate, update, components = {}, strings = {} } = props
const { renderProps, rotate, components = {}, strings = {} } = props
const { onTransformed, zoomFunctions, setZoomFunctions } = useContext(ZoomContext)
return (

View file

@ -36,10 +36,8 @@ export const MenuItem = ({
allowOverride = false,
ux = 5,
state,
docs,
config,
Design,
i18n,
}) => {
// Local state - whether the override input should be shown
const [override, setOverride] = useState(false)

View file

@ -21,7 +21,7 @@ import {
MenuScaleSettingValue,
} from './Value.mjs'
import { MenuItemGroup, MenuItem } from './Container.mjs'
import { SettingsIcon } from '@freesewing/react/components/Icon'
import { SettingsIcon, TrashIcon } from '@freesewing/react/components/Icon'
/**
* The core settings menu

View file

@ -3,7 +3,7 @@ import { OptionsIcon, SettingsIcon, UiIcon } from '@freesewing/react/components/
import { DesignOptionsMenu } from './DesignOptionsMenu.mjs'
import { CoreSettingsMenu } from './CoreSettingsMenu.mjs'
import { UiPreferencesMenu } from './UiPreferencesMenu.mjs'
import { FlagsAccordionEntries } from '../Flag.mjs'
import { FlagsAccordionEntries, FlagsAccordionTitle } from '../Flag.mjs'
import { Accordion } from '../Accordion.mjs'
export const DraftMenu = ({ Design, pattern, state, update, i18n }) => {

View file

@ -16,12 +16,7 @@ import { mergeOptions } from '@freesewing/core'
import { KeyVal } from '@freesewing/react/components/KeyVal'
/** A boolean version of {@see MenuListInput} that sets up the necessary configuration */
export const MenuBoolInput = (props) => {
const { name, config } = props
//const boolConfig = useBoolConfig(name, config)
return <MenuListInput {...props} />
}
export const MenuBoolInput = (props) => <MenuListInput {...props} />
/** A placeholder for an input to handle constant values */
export const MenuConstantInput = ({
@ -59,41 +54,6 @@ export const MenuDegInput = (props) => {
)
}
const getTitleAndDesc = (config = {}, i18n = {}, isDesignOption = false) => {
if (config.choiceTitles && config.choiceDescriptions) {
const current = typeof config.current === 'undefined' ? config.dflt : config.current
return {
title: config.choiceTitles[current],
desc: config.choiceDescriptions[current],
}
}
let titleKey = config.choiceTitles
? 'fixme' //config.choiceTitles[entry]
: isDesignOption
? i18n?.en?.o?.[name] || name
: `${name}.o.${entry}`
if (!config.choiceTitles && i18n && i18n.en.o[`${name}.${entry}`])
titleKey = i18n.en.o[`${name}.${entry}`]
const title = config.title
? config.title
: config.titleMethod
? config.titleMethod(entry)
: typeof titleKey === 'string'
? i18n.en.o[titleKey]?.t
: titleKey.t
const desc = config.valueMethod
? config.valueMethod(entry)
: typeof titleKey === 'string'
? i18n.en.o[titleKey]?.d
: titleKey.d
return {
title: 'fixmeTitle',
desc: 'fixmeDesc',
}
}
/**
* An input for selecting and item from a list
* @param {String} options.name the name of the property this input changes
@ -111,11 +71,9 @@ export const MenuListInput = ({
current,
updateHandler,
compact = false,
t,
changed,
design,
isDesignOption = false,
i18n,
}) => {
const handleChange = useSharedHandlers({
dflt: config.dflt,
@ -125,7 +83,7 @@ export const MenuListInput = ({
})
return config.list.map((entry) => {
const { title = false, about = false } = config //getTitleAndDesc(config, i18n, isDesignOption)
const { title = false, about = false } = config
if (!title || !about) console.log('No title or about in', { name, config, design })
const sideBySide = config.sideBySide || about.length + title.length < 42
@ -286,7 +244,6 @@ export const MenuSliderInput = ({
setReset,
children,
changed,
i18n,
state,
Design,
}) => {

View file

@ -92,7 +92,7 @@ const SampleOptionButton = ({ name, i18n, update }) => (
</button>
)
const SampleMeasurementButton = ({ name, i18n, update }) => (
const SampleMeasurementButton = ({ name, update }) => (
<button
className={
'tw:daisy-btn tw:daisy-btn-outline tw:daisy-btn-sm tw:mx-2 ' +

View file

@ -19,7 +19,7 @@ export const UiPreferencesMenu = ({ update, state, Design }) => {
}
const values = {
aside: MenuListValue,
ux: (props) => <span>{state.ui.ux}/5</span>,
ux: () => <span>{state.ui.ux}/5</span>,
rotate: MenuListValue,
renderer: MenuListValue,
}

View file

@ -160,11 +160,10 @@ export const MenuScaleSettingValue = ({ current, config, changed }) => (
/**
* Displays the value for core's only setting
*
* @param {object} config - The option config
* @param {number} current - The current (count) value
* @param {bool} changed - Whether or not the value is non-default
*/
export const MenuOnlySettingValue = ({ current, config }) => (
export const MenuOnlySettingValue = ({ current }) => (
<MenuHighlightValue changed={current !== undefined}>
{current === undefined ? '-' : current.length}
</MenuHighlightValue>

View file

@ -5,10 +5,9 @@ import { Collection } from '@freesewing/react/components/Collection'
* The designs view is loaded if and only if no design name is passed to the editor
*
* @param {Object} props - All the props
* @param {Object} designs - Object holding all designs
* @param {Object} update - ViewWrapper state update object
*/
export const DesignsView = ({ designs = {}, update }) => (
export const DesignsView = ({ update }) => (
<div className="tw:text-center tw:mt-8 tw:mb-24 tw:p-2 lg: tw:p-8">
<h1>Choose a design from the FreeSewing collection</h1>
<Collection

View file

@ -27,8 +27,8 @@ export const DraftErrorHandler = ({ failure, errors }) => {
.
</p>
<p>
If you believe your measurements are correct and/or if you'd like further assistance, you
can ask for help <Link href="https://forum.freesewing.eu">on our forum</Link>,{' '}
If you believe your measurements are correct and/or if you&apos;d like further assistance,
you can ask for help <Link href="https://forum.freesewing.eu">on our forum</Link>,{' '}
<Link href="https://discord.freesewing.org">our Discord server</Link>, or{' '}
<Link href="https://codeberg.org/freesewing/freesewing/issues">report an issue</Link>.
</p>

View file

@ -23,7 +23,6 @@ import { yaml as yamlLang } from '@codemirror/lang-yaml'
* @param {Object} props.update - Helper object for updating the editor state
*/
export const EditSettingsView = (props) => {
const [settings, setSettings] = useState(props.state?.settings || {})
const { state, config, update } = props
return (
@ -96,7 +95,7 @@ export const PrimedSettingsEditor = (props) => {
setSettings(newSettings)
}
} catch (err) {
// This is fine
console.log(err)
}
}

View file

@ -23,10 +23,10 @@ import { EditIcon, CodeIcon, TipIcon, PrintIcon } from '@freesewing/react/compon
export const ExportView = (props) => {
const { config, state, update } = props
const { settings = {} } = state // Guard against undefined settings
const [link, setLink] = useState(false)
const [format, setFormat] = useState(false)
const setLink = useState(false)[1]
const setFormat = useState(false)[1]
const { protocol, hostname, port } = window.location
const { protocol, port } = window.location
const site =
(protocol === 'https:' && port === 443) || (protocol === 'http:' && port === 80)
? `${window.location.protocol}//${window.location.hostname}`
@ -102,6 +102,7 @@ export const ExportView = (props) => {
<H3>ISO paper sizes</H3>
{['a4', 'a3', 'a2', 'a1', 'a0'].map((format) => (
<button
key={format}
className={`${horFlexClasses} tw:daisy-btn tw:daisy-btn-primary tw:uppercase`}
onClick={() => exportPattern({ ...exportProps, format })}
>
@ -114,6 +115,7 @@ export const ExportView = (props) => {
<H3>Other paper sizes</H3>
{['letter', 'legal', 'tabloid'].map((format) => (
<button
key={format}
className={`${horFlexClasses} tw:daisy-btn tw:daisy-btn-primary tw:uppercase`}
onClick={() => exportPattern({ ...exportProps, format })}
>
@ -130,6 +132,7 @@ export const ExportView = (props) => {
<div className="tw:grid tw:grid-cols-1 tw:lg:grid-cols-2 tw:gap-2 tw:mt-2">
{['svg', 'pdf'].map((format) => (
<button
key={format}
className={`${horFlexClasses} tw:daisy-btn tw:daisy-btn-primary tw:uppercase`}
onClick={() => exportPattern({ ...exportProps, format })}
>
@ -143,6 +146,7 @@ export const ExportView = (props) => {
<div className="tw:grid tw:grid-cols-1 tw:lg:grid-cols-2 tw:gap-2 tw:mt-2">
{['json', 'yaml'].map((format) => (
<button
key={format}
className={`${horFlexClasses} tw:daisy-btn tw:daisy-btn-primary tw:uppercase`}
onClick={() => exportPattern({ ...exportProps, format })}
>

View file

@ -11,7 +11,7 @@ import { DraftErrorHandler } from './DraftErrorHandler.mjs'
export const LayoutView = (props) => {
const { config, state, update, Design } = props
const { ui, settings } = state
const { settings } = state
const defaultSettings = defaultPrintSettings(settings?.units)
// Settings for the tiler plugin

View file

@ -8,8 +8,8 @@ import { useBackend } from '@freesewing/react/hooks/useBackend'
// Components
import { RoleBlock } from '@freesewing/react/components/Role'
import { Popout } from '@freesewing/react/components/Popout'
import { StringInput } from '@freesewing/react/components/Input'
import { SaveAsIcon } from '@freesewing/react/components/Icon'
import { StringInput, MarkdownInput } from '@freesewing/react/components/Input'
import { SaveAsIcon, SaveIcon } from '@freesewing/react/components/Icon'
import { H1 } from '@freesewing/react/components/Heading'
import { Link, SuccessLink } from '@freesewing/react/components/Link'
import { HeaderMenu } from '../HeaderMenu.mjs'
@ -67,7 +67,8 @@ export const SaveView = ({ config, state, update }) => {
}
const savePattern = async () => {
setLoadingStatus([true, 'Saving pattern...'])
const loadingId = 'savePattern'
update.startLoading(loadingId)
const patternData = {
design: state.design,
settings,
@ -77,8 +78,9 @@ export const SaveView = ({ config, state, update }) => {
}
const result = await backend.updatePattern(saveAs.pattern, patternData)
if (result.success) {
update.stopLoading(loadingId)
setSavedId(saveAs.pattern)
update.notify({ color: 'success', msg: 'boom' }, saveAs.pattern)
update.notifySuccess('Pattern saved', loadingId)
}
}
@ -96,11 +98,11 @@ export const SaveView = ({ config, state, update }) => {
</Popout>
)}
<button
className={`${classeshorFlexNoSm} tw:btn tw:btn-primary tw:btn-lg tw:w-full tw:mt-2 tw:my-8`}
className={`tw:flex tw:flex-row tw:items-center tw:gap-2 tw:btn tw:btn-primary tw:btn-lg tw:w-full tw:mt-2 tw:my-8`}
onClick={savePattern}
>
<SaveIcon className="tw:h-8 tw:w-8" />
Save Patter #{saveAs.pattern}
Save Pattern #{saveAs.pattern}
</button>
</>
) : null}
@ -137,11 +139,7 @@ export const SaveView = ({ config, state, update }) => {
}
/>
{withNotes ? (
<Swizzled.components.MarkdownInput
label="Pattern notes"
current={notes}
update={setNotes}
/>
<MarkdownInput label="Pattern notes" current={notes} update={setNotes} />
) : null}
<div className="tw:flex tw:flex-row tw:gap-2 tw:mt-8">
<button

View file

@ -69,7 +69,6 @@ export const TestView = ({ Design, state, update, config }) => {
patternLocale={state.locale || 'en'}
rotate={state.ui.rotate}
strings={strings}
rotate={state.ui.rotate}
/>
)

View file

@ -1,6 +1,5 @@
// Dependencies
import React, { useState, useEffect } from 'react'
import * as echarts from 'echarts'
import { timingPlugin } from '@freesewing/plugin-timing'
// Components
import { ChartWrapper } from '@freesewing/react/components/Echart'
@ -36,18 +35,6 @@ const TimingHeader = ({ timing, parts }) => {
) : null
}
const resolveColor = (color) => {
const [c, i] = color.split('-')
return tailwindColors[c][i]
}
const getColor = (i, colors) =>
new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: resolveColor(colors[i % colors.length]) + 'dd' },
{ offset: 1, color: resolveColor(colors[i % colors.length]) },
])
const timeScore = (took) => {
if (took < 25) return 'Very Fast'
if (took < 50) return 'Fast'
@ -58,7 +45,6 @@ const timeScore = (took) => {
}
const option = (parts, data, took, setData) => ({
//color: colors.map((color) => resolveColor(color)),
title: {
text: `Timing of most recent draft: ${timeScore(took)}`,
left: 'center',
@ -127,7 +113,7 @@ const option = (parts, data, took, setData) => ({
},
],
yAxis: [{ type: 'value' }],
series: parts.map((name, i) => ({
series: parts.map((name) => ({
name,
type: 'line',
stack: 'Total',

View file

@ -70,7 +70,7 @@ export const ViewPicker = ({ Design, update, state }) => {
)
}
const MainCard = ({ view, update, Design }) => {
const MainCard = ({ view, update }) => {
const Icon = viewIcons[view]
return (

View file

@ -37,6 +37,7 @@ export const useEditorState = (init = {}, setEphemeralState, config) => {
if (typeof data.s === 'object') setState(data.s)
else setState(init)
} catch (err) {
console.log(err)
setState(init)
}
}
@ -45,31 +46,6 @@ export const useEditorState = (init = {}, setEphemeralState, config) => {
return [state, setState, update]
}
/*
* Our URL state library does not support storing Javascript objects out of the box.
* But it allows us to pass a customer parser to handle them, so this is that parser
*/
const pojoParser = {
parse: (v) => {
let val
try {
val = JSON.parse(v)
} catch (err) {
val = null
}
return val
},
serialize: (v) => {
let val
try {
val = JSON.stringify(v)
} catch (err) {
val = null
}
return val
},
}
function getHashData() {
if (!window) return false

View file

@ -101,7 +101,7 @@ export function menuCoreSettingsStructure({
title: 'Seam Allowance Size',
about: (
<>
Controls the size of the pattern's seam allowance.
Controls the size of the pattern&apos;s seam allowance.
<CoreDocsLink item="sa" />
</>
),

View file

@ -9,6 +9,7 @@ const DesignDocsLink = ({ design, item }) => (
href={`/docs/designs/${design}/options/#${item.toLowerCase()}`}
className={`${linkClasses} tw:px-2`}
target="_BLANK"
rel="noreferrer"
>
Learn more
</a>
@ -62,14 +63,14 @@ export function menuDesignOptionsStructure(design, options, settings, asFullList
option.valueTitles = {}
option.choiceTitles = {}
option.choiceDescriptions = {}
for (const entry of option.list) {
option.list.forEach(() => {
option.choiceTitles.false = eno[`${option.name}No`]?.t || option.name
option.choiceDescriptions.false = eno[`${option.name}No`]?.d || 'No'
option.valueTitles.false = 'No'
option.choiceTitles.true = eno[`${option.name}Yes`]?.t || 'Yes'
option.choiceDescriptions.true = eno[`${option.name}Yes`]?.d || 'No'
option.valueTitles.true = 'Yes'
}
})
}
if (typeof option.menu === 'function')
option.menu = asFullList

View file

@ -2,7 +2,7 @@
import React from 'react'
import { defaultConfig } from '../config/index.mjs'
import { round, formatMm, randomLoadingMessage } from '@freesewing/utils'
import { formatDesignOptionValue, menuCoreSettingsStructure } from './index.mjs'
import { formatDesignOptionValue, menuCoreSettingsStructure, fractionToDecimal } from './index.mjs'
import { menuUiPreferencesStructure } from './ui-preferences.mjs'
import { i18n } from '@freesewing/collection'
import { i18n as pluginI18n } from '@freesewing/core-plugins'
@ -721,15 +721,15 @@ export function cloudImageUrl({ id = 'default-avatar', variant = 'public' }) {
/*
* Return something default so that people will actually change it
*/
if (!id || id === 'default-avatar') return config.cloudImageDflt
if (!id || id === 'default-avatar') return defaultConfig.cloudImageDflt
/*
* If the variant is invalid, set it to the smallest thumbnail so
* people don't load enourmous images by accident
*/
if (!config.cloudImageVariants.includes(variant)) variant = 'sq100'
if (!defaultConfig.cloudImageVariants.includes(variant)) variant = 'sq100'
return `${config.cloudImageUrl}${id}/${variant}`
return `${defaultConfig.cloudImageUrl}${id}/${variant}`
}
/**
* This method does nothing. It is used to disable certain methods

View file

@ -3,7 +3,7 @@ import fileSaver from 'file-saver'
import { themePlugin } from '@freesewing/plugin-theme'
import { pluginI18n } from '@freesewing/plugin-i18n'
import { tilerPlugin } from './plugin-tiler.mjs'
import { capitalize, escapeSvgText, formatMm, get } from '@freesewing/utils'
import { capitalize, escapeSvgText, get } from '@freesewing/utils'
import mustache from 'mustache'
import he from 'he'
import yaml from 'js-yaml'

View file

@ -1,5 +1,4 @@
import React from 'react'
import { linkClasses } from '@freesewing/utils'
import {
CoverPageIcon,
PageMarginIcon,
@ -9,13 +8,6 @@ import {
ScaleIcon,
} from '@freesewing/react/components/Icon'
const UiDocsLink = ({ item }) => (
<a href={`/docs/about/site/draft/#${item.toLowerCase()}`} className={`${linkClasses} tw:px-2`}>
Learn more
</a>
)
const sizes = ['a4', 'a3', 'a2', 'a1', 'a0', 'letter', 'legal', 'tabloid']
const defaultPrintSettings = (units) => ({
size: units === 'imperial' ? 'letter' : 'a4',
orientation: 'portrait',