1
0
Fork 0

wip: Work on editor

This commit is contained in:
joostdecock 2025-01-05 13:51:35 +01:00
parent 1fde267288
commit 922bd04130
38 changed files with 1092 additions and 1105 deletions

123
package-lock.json generated
View file

@ -33534,6 +33534,14 @@
"@types/node": "*"
}
},
"node_modules/@types/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
@ -50681,18 +50689,6 @@
"url": "https://opencollective.com/webpack"
}
},
"node_modules/nuqs": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/nuqs/-/nuqs-1.19.1.tgz",
"integrity": "sha512-oixldNThB1wbu6B5K961++7wpTz/EZFPWnraGmIQhibDT+YxRJNplWMIoPJgL4dlsiSDVI5bbUWKpzsIWVh3Pg==",
"license": "MIT",
"dependencies": {
"mitt": "^3.0.1"
},
"peerDependencies": {
"next": ">=13.4 <14.0.2 || ^14.0.3"
}
},
"node_modules/nx": {
"version": "20.2.1",
"resolved": "https://registry.npmjs.org/nx/-/nx-20.2.1.tgz",
@ -56787,6 +56783,14 @@
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"license": "ISC"
},
"node_modules/set-cookie-parser": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/set-function-length": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz",
@ -58898,6 +58902,14 @@
"node": "^16.14.0 || >=18.0.0"
}
},
"node_modules/turbo-stream": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz",
"integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==",
"license": "ISC",
"optional": true,
"peer": true
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@ -62204,7 +62216,7 @@
"highlight.js": "^11.11.0",
"html-react-parser": "^5.0.7",
"luxon": "^3.5.0",
"nuqs": "^1.17.6",
"nuqs": "^2.3.0",
"react-markdown": "^9.0.1",
"tlds": "^1.255.0",
"use-local-storage-state": "19.1.0",
@ -62233,6 +62245,91 @@
"proxy-from-env": "^1.1.0"
}
},
"packages/react/node_modules/cookie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=18"
}
},
"packages/react/node_modules/nuqs": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/nuqs/-/nuqs-2.3.0.tgz",
"integrity": "sha512-ChS56bJZdaTQzCJb6jPel6cIHYh8/V/GSIjZoIe5yAssGdcrVaBFBgzHfJW6IewbR6yc1Zch2CmGsdgztR+xmA==",
"license": "MIT",
"dependencies": {
"mitt": "^3.0.1"
},
"peerDependencies": {
"@remix-run/react": ">=2",
"next": ">=14.2.0",
"react": ">=18.2.0 || ^19.0.0-0",
"react-router": "^7",
"react-router-dom": "^6 || ^7"
},
"peerDependenciesMeta": {
"@remix-run/react": {
"optional": true
},
"next": {
"optional": true
},
"react-router": {
"optional": true
},
"react-router-dom": {
"optional": true
}
}
},
"packages/react/node_modules/react-router": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.1.1.tgz",
"integrity": "sha512-39sXJkftkKWRZ2oJtHhCxmoCrBCULr/HAH4IT5DHlgu/Q0FCPV0S4Lx+abjDTx/74xoZzNYDYbOZWlJjruyuDQ==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@types/cookie": "^0.6.0",
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0",
"turbo-stream": "2.4.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"packages/react/node_modules/react-router-dom": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.1.1.tgz",
"integrity": "sha512-vSrQHWlJ5DCfyrhgo0k6zViOe9ToK8uT5XGSmnuC2R3/g261IdIMpZVqfjD6vWSXdnf5Czs4VA/V60oVR6/jnA==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"react-router": "7.1.1"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
}
},
"packages/react/node_modules/tlds": {
"version": "1.255.0",
"resolved": "https://registry.npmjs.org/tlds/-/tlds-1.255.0.tgz",

View file

@ -50,7 +50,7 @@ import {
DesignInput,
MarkdownInput,
ListInput,
MeasieInput,
MeasurementInput,
PassiveImageInput,
StringInput,
ToggleInput,
@ -455,7 +455,7 @@ export const Set = ({ id, publicOnly = false, Link = false }) => {
/>
</div>
{filterMeasurements().map((m) => (
<MeasieInput
<MeasurementInput
id={`measie-${m}`}
key={m}
m={m}

View file

@ -55,8 +55,10 @@ export const useFilter = () => {
*
* @param {object} props - All React props
* @param {function} Link - An optional framework specific Link component for client-side routing
* @param {bool} editor - Set this to when loaded in the editor (this will make the display more dense)
* @param {bool} onClick - Set this to trigger an onClick event, rather than using links
*/
export const Collection = ({ Link = false, linkTo = 'about' }) => {
export const Collection = ({ Link = false, linkTo = 'about', editor = false, onClick = false }) => {
if (!Link) Link = WebLink
// State
@ -104,7 +106,16 @@ export const Collection = ({ Link = false, linkTo = 'about' }) => {
<div className="tw-flex tw-flex-row tw-flex-wrap tw-gap-1 tw-justify-center tw-font-medium tw-mb-2">
{Object.keys(filtered)
.sort()
.map((d) => (
.map((d) =>
onClick ? (
<button
key={d}
onClick={() => onClick(d)}
className="tw-text-secondary tw-decoration-2 tw-underline tw-capitalize hover:tw-decoration-4 hover:tw-text-secondary tw-bg-transparent tw-border-0 tw-font-medium tw-p-0 tw-text-base hover:tw-cursor-pointer"
>
{d}
</button>
) : (
<Link
key={d}
href={linkBuilders[linkTo](d)}
@ -112,7 +123,8 @@ export const Collection = ({ Link = false, linkTo = 'about' }) => {
>
{d}
</Link>
))}
)
)}
</div>
{showFilters ? (
<>
@ -212,7 +224,9 @@ export const Collection = ({ Link = false, linkTo = 'about' }) => {
</div>
)}
</div>
<div className="tw-grid tw-grid-cols-2 tw-gap-2 tw-mt-4 tw-justify-center sm:tw-grid-cols-3 md:tw-grid-cols-4 tw-mb-8">
<div
className={`tw-grid tw-grid-cols-2 tw-gap-2 tw-mt-4 tw-justify-center sm:tw-grid-cols-3 md:tw-grid-cols-4 ${editor ? 'lg:tw-grid-cols-6 2xl:tw-grid-cols-12' : ''} tw-mb-8`}
>
{Object.keys(filtered)
.sort()
.map((d) => (
@ -220,6 +234,7 @@ export const Collection = ({ Link = false, linkTo = 'about' }) => {
name={d}
key={d}
linkTo={linkTo}
onClick={onClick}
lineDrawing={filter.example ? false : true}
/>
))}
@ -260,7 +275,7 @@ const Tag = ({ Link = WebLink, technique }) => (
</Link>
)
const DesignCard = ({ name, lineDrawing = false, linkTo, Link }) => {
const DesignCard = ({ name, lineDrawing = false, linkTo, Link, onClick }) => {
if (!Link) Link = WebLink
const LineDrawing =
@ -275,12 +290,7 @@ const DesignCard = ({ name, lineDrawing = false, linkTo, Link }) => {
bg.backgroundPosition = 'center center'
}
return (
<Link
href={linkTo === 'new' ? `/-/` : `/designs/${name}/`}
className="hover:tw-bg-secondary hover:tw-bg-opacity-10 tw-rounded-lg tw-group hover:tw-no-underline"
title={about[name].description}
>
const inner = (
<div
className={`tw-flex tw-flex-col tw-flex-nowrap tw-items-start tw-justify-between tw-gap-2 tw-border-neutral-500 group-hover:tw-border-secondary
tw-w-full tw-h-full tw-border tw-border-2 tw-border-solid tw-p-0 tw-relative tw-rounded-lg tw-rounded-lg`}
@ -306,6 +316,23 @@ const DesignCard = ({ name, lineDrawing = false, linkTo, Link }) => {
<Difficulty score={about[name].difficulty} className="group-hover:tw-text-secondary" />
</div>
</div>
)
return onClick ? (
<button
onClick={() => onClick(name)}
className="hover:tw-bg-secondary hover:tw-bg-opacity-10 tw-rounded-lg tw-group hover:tw-no-underline tw-bg-transparent tw-border-0 hover:tw-cursor-pointer tw-p-0"
title={about[name].description}
>
{inner}
</button>
) : (
<Link
href={linkTo === 'new' ? `/-/` : `/designs/${name}/`}
className="hover:tw-bg-secondary hover:tw-bg-opacity-10 tw-rounded-lg tw-group hover:tw-no-underline"
title={about[name].description}
>
{inner}
</Link>
)
}

View file

@ -1,22 +1,22 @@
import { useState } from 'react'
import React, { useState } from 'react'
/*
* DaisyUI's accordion seems rather unreliable.
* So instead, we handle this in React state
*/
const getProps = (isActive) => ({
className: `p-2 px-4 rounded-lg bg-transparent shadow
w-full mt-2 py-4 h-auto content-start text-left bg-opacity-20
${isActive ? 'hover:bg-transparent' : 'hover:bg-secondary hover:bg-opacity-10'}`,
className: `tw-p-2 tw-px-4 tw-rounded-lg tw-bg-transparent tw-shadow
tw-w-full tw-mt-2 tw-py-4 tw-h-auto tw-content-start tw-text-left tw-bg-opacity-20
${isActive ? 'hover:tw-bg-transparent' : 'hover:tw-bg-secondary hover:tw-bg-opacity-10'}`,
})
const getSubProps = (isActive) => ({
className: ` p-2 px-4 rounded bg-transparent w-full mt-2 py-4 h-auto
content-start bg-secondary text-left bg-opacity-20
className: ` tw-p-2 tw-px-4 tw-rounded tw-bg-transparent tw-w-full tw-mt-2 tw-py-4 tw-h-auto
tw-content-start tw-bg-secondary tw-text-left tw-bg-opacity-20
${
isActive
? 'bg-opacity-100 hover:bg-transparent shadow'
: 'hover:bg-opacity-10 hover:bg-secondary '
? 'tw-bg-opacity-100 hover:tw-bg-transparent tw-shadow'
: 'hover:tw-bg-opacity-10 hover:tw-bg-secondary '
}`,
})
@ -41,7 +41,7 @@ export const BaseAccordion = ({
.map((item, i) =>
active === item[2] ? (
<div key={i} {...propsGetter(true)}>
<Component onClick={setActive} className="w-full hover:cursor-pointer">
<Component onClick={setActive} className="tw-w-full hover:tw-cursor-pointer">
{item[0]}
</Component>
{item[1]}

View file

@ -1,4 +1,4 @@
import { useState } from 'react'
import React, { useState } from 'react'
export const AsideViewMenuButton = ({
href,
@ -29,13 +29,6 @@ export const AsideViewMenuButton = ({
)
}
export const ViewTypeIcon = ({ Swizzled, view, className = 'h-6 w-6 grow-0' }) => {
const Icon = Swizzled.components[`View${Swizzled.methods.capitalize(view)}Icon`]
if (!Icon) return <Swizzled.components.OptionsIcon />
return <Icon className={className} />
}
export const AsideViewMenuSpacer = () => (
<hr className="my-1 w-full opacity-20 font-bold border-t-2" />
)

View file

@ -1,6 +1,15 @@
import { useState } from 'react'
// Dependencies
import { missingMeasurements } from '../lib/index.mjs'
// Hooks
import React, { useState } from 'react'
// Components
import { Null } from './Null.mjs'
import { AsideViewMenuSpacer } from './AsideViewMenu.mjs'
import { ViewIcon, viewLabels } from './views/index.mjs'
import { Tooltip } from './Tooltip.mjs'
import { ErrorIcon } from '@freesewing/react/components/Icon'
export const HeaderMenu = ({ state, Swizzled, update, Design, pattern }) => {
export const HeaderMenu = ({ config, Design, pattern, state, update }) => {
const [open, setOpen] = useState()
/*
@ -8,10 +17,10 @@ export const HeaderMenu = ({ state, Swizzled, update, Design, pattern }) => {
* and make sure there's a view-specific header menu
*/
const ViewMenu =
!Swizzled.methods.missingMeasurements(state) &&
Swizzled.components[`HeaderMenu${Swizzled.config.viewComponents[state.view]}`]
? Swizzled.components[`HeaderMenu${Swizzled.config.viewComponents[state.view]}`]
: Swizzled.components.Null
!missingMeasurements(state, config) &&
Swizzled.components[`HeaderMenu${config.viewComponents[state.view]}`]
? Swizzled.components[`HeaderMenu${config.viewComponents[state.view]}`]
: Null
return (
<div
@ -22,38 +31,37 @@ export const HeaderMenu = ({ state, Swizzled, update, Design, pattern }) => {
<div
className={`flex flex-row flex-wrap gap-1 md:gap-4 w-full items-start justify-center border-b border-base-300 py-1 md:py-1.5`}
>
<Swizzled.components.HeaderMenuAllViews {...{ state, Swizzled, update, open, setOpen }} />
<ViewMenu {...{ state, Swizzled, update, Design, pattern, open, setOpen }} />
<HeaderMenuAllViews {...{ config, state, update, open, setOpen }} />
<ViewMenu {...{ config, state, update, Design, pattern, open, setOpen }} />
</div>
</div>
)
}
export const HeaderMenuAllViews = ({ state, Swizzled, update, open, setOpen }) => (
<Swizzled.components.HeaderMenuViewMenu {...{ state, Swizzled, update, open, setOpen }} />
export const HeaderMenuAllViews = ({ config, state, update, open, setOpen }) => (
<HeaderMenuViewMenu {...{ config, state, update, open, setOpen }} />
)
export const HeaderMenuDraftView = (props) => {
const { Swizzled } = props
const flags = props.pattern?.setStores?.[0]?.plugins?.['plugin-annotations']?.flags
return (
<>
<div className="flex flex-row gap-1">
<Swizzled.components.HeaderMenuDraftViewDesignOptions {...props} />
<Swizzled.components.HeaderMenuDraftViewCoreSettings {...props} />
<Swizzled.components.HeaderMenuDraftViewUiPreferences {...props} />
{flags ? <Swizzled.components.HeaderMenuDraftViewFlags {...props} flags={flags} /> : null}
<HeaderMenuDraftViewDesignOptions {...props} />
<HeaderMenuDraftViewCoreSettings {...props} />
<HeaderMenuDraftViewUiPreferences {...props} />
{flags ? <HeaderMenuDraftViewFlags {...props} flags={flags} /> : null}
</div>
<Swizzled.components.HeaderMenuDraftViewIcons {...props} />
<Swizzled.components.HeaderMenuUndoIcons {...props} />
<Swizzled.components.HeaderMenuSaveIcons {...props} />
<HeaderMenuDraftViewIcons {...props} />
<HeaderMenuUndoIcons {...props} />
<HeaderMenuSaveIcons {...props} />
</>
)
}
export const HeaderMenuDropdown = (props) => {
const { Swizzled, tooltip, toggle, open, setOpen, id } = props
const { tooltip, toggle, open, setOpen, id } = props
/*
* We need to use both !fixed and md:!absolute here to override DaisyUI's
* classes on dropdown-content to force the dropdown to use the available
@ -61,7 +69,7 @@ export const HeaderMenuDropdown = (props) => {
*/
return props.disabled ? (
<Swizzled.components.Tooltip tip={tooltip}>
<Tooltip tip={tooltip}>
<button
disabled
tabIndex={0}
@ -70,9 +78,9 @@ export const HeaderMenuDropdown = (props) => {
>
{toggle}
</button>
</Swizzled.components.Tooltip>
</Tooltip>
) : (
<Swizzled.components.Tooltip tip={tooltip}>
<Tooltip tip={tooltip}>
<div className={`dropdown ${open === id ? 'dropdown-open z-20' : ''}`}>
<div
tabIndex={0}
@ -96,120 +104,114 @@ export const HeaderMenuDropdown = (props) => {
></div>
)}
</div>
</Swizzled.components.Tooltip>
</Tooltip>
)
}
export const HeaderMenuDraftViewDesignOptions = (props) => {
const { Swizzled } = props
return (
<Swizzled.components.HeaderMenuDropdown
<HeaderMenuDropdown
{...props}
id="designOptions"
tooltip={Swizzled.methods.t('pe:designOptions.d')}
tooltip="fixme: 'pe:designOptions.d'"
toggle={
<>
<Swizzled.components.HeaderMenuIcon name="options" extraClasses="text-secondary" />
<span className="hidden lg:inline">{Swizzled.methods.t('pe:designOptions.t')}</span>
<HeaderMenuIcon name="options" extraClasses="text-secondary" />
<span className="hidden lg:inline">fixme: pe:designOptions.t</span>
</>
}
>
<Swizzled.components.DesignOptionsMenu {...props} />
</Swizzled.components.HeaderMenuDropdown>
<DesignOptionsMenu {...props} />
</HeaderMenuDropdown>
)
}
export const HeaderMenuDraftViewCoreSettings = (props) => {
const { Swizzled } = props
return (
<Swizzled.components.HeaderMenuDropdown
<HeaderMenuDropdown
{...props}
tooltip={Swizzled.methods.t('pe:coreSettings.d')}
tooltip="fixme: pe:coreSettings.d"
id="coreSettings"
toggle={
<>
<Swizzled.components.HeaderMenuIcon name="settings" extraClasses="text-secondary" />
<span className="hidden lg:inline">{Swizzled.methods.t('pe:coreSettings.t')}</span>
<HeaderMenuIcon name="settings" extraClasses="text-secondary" />
<span className="hidden lg:inline">fixme: pe:coreSettings.t</span>
</>
}
>
<Swizzled.components.CoreSettingsMenu {...props} />
</Swizzled.components.HeaderMenuDropdown>
<CoreSettingsMenu {...props} />
</HeaderMenuDropdown>
)
}
export const HeaderMenuDraftViewUiPreferences = (props) => {
const { Swizzled } = props
return (
<Swizzled.components.HeaderMenuDropdown
<HeaderMenuDropdown
{...props}
tooltip={Swizzled.methods.t('pe:uiPreferences.d')}
tooltip="fixme: pe:uiPreferences.d"
id="uiPreferences"
toggle={
<>
<Swizzled.components.HeaderMenuIcon name="ui" extraClasses="text-secondary" />
<span className="hidden lg:inline">{Swizzled.methods.t('pe:uiPreferences.t')}</span>
<HeaderMenuIcon name="ui" extraClasses="text-secondary" />
<span className="hidden lg:inline">fixme: pe:uiPreferences.t</span>
</>
}
>
<Swizzled.components.UiPreferencesMenu {...props} />
</Swizzled.components.HeaderMenuDropdown>
<UiPreferencesMenu {...props} />
</HeaderMenuDropdown>
)
}
export const HeaderMenuDraftViewFlags = (props) => {
const { Swizzled } = props
const count = Object.keys(Swizzled.methods.flattenFlags(props.flags)).length
const count = Object.keys(flattenFlags(props.flags)).length
return (
<Swizzled.components.HeaderMenuDropdown
<HeaderMenuDropdown
{...props}
tooltip={Swizzled.methods.t('pe:flagMenuMany.d')}
tooltip="fixme: pe:flagMenuMany.d"
id="flags"
toggle={
<>
<Swizzled.components.HeaderMenuIcon name="flag" extraClasses="text-secondary" />
<HeaderMenuIcon name="flag" extraClasses="text-secondary" />
<span className="hidden lg:inline">
{Swizzled.methods.t('pe:flags')}
Flags
<span>({count})</span>
</span>
</>
}
>
<Swizzled.components.FlagsAccordionEntries {...props} />
</Swizzled.components.HeaderMenuDropdown>
<FlagsAccordionEntries {...props} />
</HeaderMenuDropdown>
)
}
export const HeaderMenuDraftViewIcons = (props) => {
const { Swizzled, update } = props
const Button = Swizzled.components.HeaderMenuButton
const { update } = props
const Button = HeaderMenuButton
const size = 'w-5 h-5'
const muted = 'text-current opacity-50'
const ux = props.state.ui.ux
const levels = {
...props.Swizzled.config.uxLevels.core,
...props.Swizzled.config.uxLevels.ui,
...props.config.uxLevels.core,
...props.config.uxLevels.ui,
}
return (
<div className="flex flex-row flex-wrap items-center justify-center px-2">
{ux >= levels.sa ? (
<Button updateHandler={update.toggleSa} tooltip={Swizzled.methods.t('pe:tt.toggleSa')}>
<Swizzled.components.SaIcon
className={`${size} ${props.state.settings.sabool ? 'text-secondary' : muted}`}
/>
<Button
updateHandler={update.toggleSa}
tooltip="Turns Seam Allowance on or off (see Core Settings)"
>
<SaIcon className={`${size} ${props.state.settings.sabool ? 'text-secondary' : muted}`} />
</Button>
) : null}
{ux >= levels.paperless ? (
<Button
updateHandler={() => update.settings('paperless', props.state.settings.paperless ? 0 : 1)}
tooltip={Swizzled.methods.t('pe:tt.togglePaperless')}
tooltip="Turns Paperless on or off (see Core Settings)"
>
<Swizzled.components.PaperlessIcon
<PaperlessIcon
className={`${size} ${props.state.settings.paperless ? 'text-secondary' : muted}`}
/>
</Button>
@ -217,9 +219,9 @@ export const HeaderMenuDraftViewIcons = (props) => {
{ux >= levels.complete ? (
<Button
updateHandler={() => update.settings('complete', props.state.settings.complete ? 0 : 1)}
tooltip={Swizzled.methods.t('pe:tt.toggleComplete')}
tooltip="Turns Details on or off (see Core Settings)"
>
<Swizzled.components.DetailIcon
<DetailIcon
className={`${size} ${!props.state.settings.complete ? 'text-secondary' : muted}`}
/>
</Button>
@ -227,9 +229,9 @@ export const HeaderMenuDraftViewIcons = (props) => {
{ux >= levels.expand ? (
<Button
updateHandler={() => update.settings('expand', props.state.settings.expand ? 0 : 1)}
tooltip={Swizzled.methods.t('pe:tt.toggleExpand')}
tooltip="Turns Expand on or off (see Core Settings)"
>
<Swizzled.components.ExpandIcon
<ExpandIcon
className={`${size} ${props.state.settings.expand ? 'text-secondary' : muted}`}
/>
</Button>
@ -242,26 +244,26 @@ export const HeaderMenuDraftViewIcons = (props) => {
props.state.settings.units === 'imperial' ? 'metric' : 'imperial'
)
}
tooltip={Swizzled.methods.t('pe:tt.toggleUnits')}
tooltip="Switches Units between metric and imperial (see Core Settings)"
>
<Swizzled.components.UnitsIcon
<UnitsIcon
className={`${size} ${
props.state.settings.units === 'imperial' ? 'text-secondary' : muted
}`}
/>
</Button>
) : null}
<Swizzled.components.HeaderMenuIconSpacer />
<HeaderMenuIconSpacer />
{ux >= levels.ux ? (
<div className="flex flex-row px-1">
<Swizzled.components.Tooltip tip={Swizzled.methods.t('pe:tt.changeUx')}>
<Tooltip tip="Changes your UX setting (see UI Preferences)">
{[0, 1, 2, 3, 4].map((i) => (
<button
key={i}
className="btn btn-ghost btn-sm px-0 -mx-0.5"
onClick={() => update.ui('ux', i + 1)}
>
<Swizzled.components.CircleIcon
<CircleIcon
key={i}
fill={i < props.state.ui.ux ? true : false}
className={`${size} ${
@ -271,37 +273,31 @@ export const HeaderMenuDraftViewIcons = (props) => {
/>
</button>
))}
</Swizzled.components.Tooltip>
</Tooltip>
</div>
) : null}
{ux >= levels.aside ? (
<Button
updateHandler={() => update.ui('aside', props.state.ui.aside ? 0 : 1)}
tooltip={Swizzled.methods.t('pe:tt.toggleAside')}
tooltip="Turn the Aside Menu on or off (see UI Preferences)"
>
<Swizzled.components.MenuIcon
className={`${size} ${!props.state.ui.aside ? 'text-secondary' : muted}`}
/>
<MenuIcon className={`${size} ${!props.state.ui.aside ? 'text-secondary' : muted}`} />
</Button>
) : null}
{ux >= levels.kiosk ? (
<Button
updateHandler={() => update.ui('kiosk', props.state.ui.kiosk ? 0 : 1)}
tooltip={Swizzled.methods.t('pe:tt.toggleKiosk')}
tooltip="Turns Kiosk Mode on or off (see UI Preferences)"
>
<Swizzled.components.KioskIcon
className={`${size} ${props.state.ui.kiosk ? 'text-secondary' : muted}`}
/>
<KioskIcon className={`${size} ${props.state.ui.kiosk ? 'text-secondary' : muted}`} />
</Button>
) : null}
{ux >= levels.rotate ? (
<Button
updateHandler={() => update.ui('rotate', props.state.ui.rotate ? 0 : 1)}
tooltip={Swizzled.methods.t('pe:tt.toggleRotate')}
tooltip="Turns Rotate Pattern on or off (see UI Preferences)"
>
<Swizzled.components.RotateIcon
className={`${size} ${props.state.ui.rotate ? 'text-secondary' : muted}`}
/>
<RotateIcon className={`${size} ${props.state.ui.rotate ? 'text-secondary' : muted}`} />
</Button>
) : null}
{ux >= levels.renderer ? (
@ -309,9 +305,9 @@ export const HeaderMenuDraftViewIcons = (props) => {
updateHandler={() =>
update.ui('renderer', props.state.ui.renderer === 'react' ? 'svg' : 'react')
}
tooltip={Swizzled.methods.t('pe:tt.toggleRenderer')}
tooltip="Switches the Render Engine between React and SVG (see UI Preferences)"
>
<Swizzled.components.RocketIcon
<RocketIcon
className={`${size} ${props.state.ui.renderer === 'svg' ? 'text-secondary' : muted}`}
/>
</Button>
@ -321,8 +317,8 @@ export const HeaderMenuDraftViewIcons = (props) => {
}
export const HeaderMenuUndoIcons = (props) => {
const { Swizzled, update, state, Design } = props
const Button = Swizzled.components.HeaderMenuButton
const { update, state, Design } = props
const Button = HeaderMenuButton
const size = 'w-5 h-5'
const undos = props.state._?.undos && props.state._.undos.length > 0 ? props.state._.undos : false
@ -330,33 +326,27 @@ export const HeaderMenuUndoIcons = (props) => {
<div className="flex flex-row flex-wrap items-center justify-center px-2">
<Button
updateHandler={() => update.restore(0, state._)}
tooltip={Swizzled.methods.t('pe:tt.undo')}
tooltip="Undo the most recent change"
disabled={undos ? false : true}
>
<Swizzled.components.UndoIcon
className={`${size} ${undos ? 'text-secondary' : ''}`}
text="1"
/>
<UndoIcon className={`${size} ${undos ? 'text-secondary' : ''}`} text="1" />
</Button>
<Button
updateHandler={() => update.restore(undos.length - 1, state._)}
tooltip={Swizzled.methods.t('pe:tt.undoAll')}
tooltip="Undo all changes since the last save point"
disabled={undos ? false : true}
>
<Swizzled.components.UndoIcon
className={`${size} ${undos ? 'text-secondary' : ''}`}
text={Swizzled.methods.t('pe:allFirstLetter')}
/>
<UndoIcon className={`${size} ${undos ? 'text-secondary' : ''}`} text="A" />
</Button>
<Swizzled.components.HeaderMenuDropdown
<HeaderMenuDropdown
{...props}
tooltip={Swizzled.methods.t('pe:view.undos.t')}
tooltip={viewLabels.undo.t}
id="undos"
disabled={undos ? false : true}
toggle={
<>
<Swizzled.components.UndoIcon className="w-4 h-4" stroke={3} />
<span className="hidden lg:inline">{Swizzled.methods.t('pe:undo')}</span>
<UndoIcon className="w-4 h-4" stroke={3} />
<span className="hidden lg:inline">Undo</span>
</>
}
>
@ -364,11 +354,11 @@ export const HeaderMenuUndoIcons = (props) => {
<ul className="dropdown-content bg-base-100 bg-opacity-90 z-20 shadow left-0 !fixed md:!absolute w-screen md:w-96 px-4 md:p-2 md:pt-0">
{undos.slice(0, 9).map((step, index) => (
<li key={index}>
<Swizzled.components.UndoStep {...{ step, update, state, Design, index }} compact />
<UndoStep {...{ step, update, state, Design, index }} compact />
</li>
))}
<li key="view">
<Swizzled.components.ButtonFrame
<ButtonFrame
dense
onClick={() => {
return null /*update.state(index, state._) */
@ -376,29 +366,32 @@ export const HeaderMenuUndoIcons = (props) => {
>
<div className="flex flex-row items-center align-center justify-between gap-2 w-full">
<div className="flex flex-row items-center align-start gap-2 grow">
<Swizzled.components.UndoIcon className="w-5 h-5 text-secondary" />
{Swizzled.methods.t(`pe:view.undos.t`)}...
<UndoIcon className="w-5 h-5 text-secondary" />
{viewLabels.undo.t}
</div>
{undos.length}
</div>
</Swizzled.components.ButtonFrame>
</ButtonFrame>
</li>
</ul>
) : null}
</Swizzled.components.HeaderMenuDropdown>
<Button updateHandler={update.clearAll} tooltip={Swizzled.methods.t('pe:tt.resetDesign')}>
<Swizzled.components.TrashIcon className={`${size} text-secondary`} />
</HeaderMenuDropdown>
<Button
updateHandler={update.clearAll}
tooltip="Reset all settings, but keep the design and measurements"
>
<TrashIcon className={`${size} text-secondary`} />
</Button>
<Button updateHandler={update.clearAll} tooltip={Swizzled.methods.t('pe:tt.resetAll')}>
<Swizzled.components.ResetAllIcon className={`${size} text-secondary`} />
<Button updateHandler={update.clearAll} tooltip="Reset the editor completely">
<ResetAllIcon className={`${size} text-secondary`} />
</Button>
</div>
)
}
export const HeaderMenuSaveIcons = (props) => {
const { Swizzled, update } = props
const Button = Swizzled.components.HeaderMenuButton
const { update } = props
const Button = HeaderMenuButton
const size = 'w-5 h-5'
const saveable = props.state._?.undos && props.state._.undos.length > 0
@ -406,43 +399,30 @@ export const HeaderMenuSaveIcons = (props) => {
<div className="flex flex-row flex-wrap items-center justify-center px-2">
<Button
updateHandler={update.clearPattern}
tooltip={Swizzled.methods.t('pe:tt.savePattern')}
tooltip="Save pattern"
disabled={saveable ? false : true}
>
<Swizzled.components.SaveIcon className={`${size} ${saveable ? 'text-success' : ''}`} />
<SaveIcon className={`${size} ${saveable ? 'text-success' : ''}`} />
</Button>
<Button
updateHandler={() => update.view('save')}
tooltip={Swizzled.methods.t('pe:tt.savePatternAs')}
>
<Swizzled.components.SaveAsIcon className={`${size} text-secondary`} />
<Button updateHandler={() => update.view('save')} tooltip="Save pattern as...">
<SaveAsIcon className={`${size} text-secondary`} />
</Button>
<Button
updateHandler={update.clearPattern}
tooltip={Swizzled.methods.t('pe:tt.exportPattern')}
>
<Swizzled.components.ExportIcon className={`${size} text-secondary`} />
<Button updateHandler={update.clearPattern} tooltip="Export pattern">
<ExportIcon className={`${size} text-secondary`} />
</Button>
</div>
)
}
export const HeaderMenuIcon = (props) => {
const { Swizzled, name, extraClasses = '' } = props
const Icon =
Swizzled.components[`${Swizzled.methods.capitalize(name)}Icon`] || Swizzled.components.Noop
return <Icon {...props} className={`h-5 w-5 ${extraClasses}`} />
const { name, extraClasses = '' } = props
//const Icon = Swizzled.components[`${Swizzled.methods.capitalize(name)}Icon`] || Swizzled.components.Noop
return <ErrorIcon {...props} className={`h-5 w-5 ${extraClasses}`} />
}
export const HeaderMenuIconSpacer = () => <span className="px-1 font-bold opacity-30">|</span>
export const HeaderMenuButton = ({
Swizzled,
updateHandler,
children,
tooltip,
disabled = false,
}) => (
<Swizzled.components.Tooltip tip={tooltip}>
export const HeaderMenuButton = ({ updateHandler, children, tooltip, disabled = false }) => (
<Tooltip tip={tooltip}>
<button
className="btn btn-ghost btn-sm px-1 disabled:bg-transparent"
onClick={updateHandler}
@ -450,30 +430,29 @@ export const HeaderMenuButton = ({
>
{children}
</button>
</Swizzled.components.Tooltip>
</Tooltip>
)
export const HeaderMenuViewMenu = (props) => {
const { Swizzled, update, state } = props
const { config, update, state } = props
const output = []
let i = 1
for (const viewName of [
'spacer',
...Swizzled.config.mainViews,
...config.mainViews,
'spacer',
...Swizzled.config.extraViews,
...config.extraViews,
'spacerOver3',
...Swizzled.config.devViews,
...config.devViews,
'spacer',
'picker',
]) {
if (viewName === 'spacer') output.push(<Swizzled.components.AsideViewMenuSpacer key={i} />)
if (viewName === 'spacer') output.push(<AsideViewMenuSpacer key={i} />)
else if (viewName === 'spacerOver3')
output.push(state.ui.ux > 3 ? <Swizzled.components.AsideViewMenuSpacer key={i} /> : null)
output.push(state.ui.ux > 3 ? <AsideViewMenuSpacer key={i} /> : null)
else if (
state.ui.ux >= Swizzled.config.uxLevels.views[viewName] &&
(Swizzled.config.measurementsFreeViews.includes(viewName) ||
state._.missingMeasurements.length < 1)
state.ui.ux >= config.uxLevels.views[viewName] &&
(config.measurementsFreeViews.includes(viewName) || state._.missingMeasurements.length < 1)
)
output.push(
<li key={i} className="mb-1 flex flex-row items-center justify-between w-full">
@ -486,12 +465,8 @@ export const HeaderMenuViewMenu = (props) => {
}`}
onClick={() => update.view(viewName)}
>
<Swizzled.components.ViewTypeIcon
view={viewName}
className="w-6 h-6 grow-0"
Swizzled={Swizzled}
/>
<b className="text-left grow">{Swizzled.methods.t(`pe:view.${viewName}.t`)}</b>
<ViewIcon view={viewName} className="w-6 h-6 grow-0" />
<b className="text-left grow">{viewLabels[viewName].t}</b>
</a>
</li>
)
@ -499,18 +474,14 @@ export const HeaderMenuViewMenu = (props) => {
}
return (
<Swizzled.components.HeaderMenuDropdown
<HeaderMenuDropdown
{...props}
tooltip={Swizzled.methods.t('pe:views.d')}
tooltip="Choose between the main views of the pattern editor"
id="views"
toggle={
<>
<Swizzled.components.HeaderMenuIcon
name="right"
stroke={3}
extraClasses="text-secondary rotate-90"
/>
<span className="hidden lg:inline">{Swizzled.methods.t('pe:views.t')}</span>
<HeaderMenuIcon name="right" stroke={3} extraClasses="text-secondary rotate-90" />
<span className="hidden lg:inline">Views</span>
</>
}
>
@ -520,6 +491,6 @@ export const HeaderMenuViewMenu = (props) => {
>
{output}
</ul>
</Swizzled.components.HeaderMenuDropdown>
</HeaderMenuDropdown>
)
}

View file

@ -1 +1,3 @@
import React from 'react'
export const HtmlSpan = ({ html }) => <span dangerouslySetInnerHTML={{ __html: html }} />

View file

@ -1,6 +1,15 @@
import { useEffect } from 'react'
import React, { useEffect } from 'react'
import { Spinner } from '@freesewing/react/components/Spinner'
export const LoadingStatus = ({ Swizzled, state, update }) => {
const config = {
timeout: 2,
defaults: {
color: 'secondary',
icon: 'Spinner',
},
}
export const LoadingStatus = ({ state, update }) => {
useEffect(() => {
if (typeof state._.loading === 'object') {
for (const conf of Object.values(state._.loading)) {
@ -21,16 +30,12 @@ export const LoadingStatus = ({ Swizzled, state, update }) => {
return (
<div className="fixed bottom-0 md:buttom-28 left-0 w-full z-30 md:px-4 md:mx-auto mb-4">
<div className="flex flex-col gap-2">
{Object.entries(state._.loading).map(([id, config]) => {
{Object.entries(state._.loading).map(([id, custom]) => {
const conf = {
...Swizzled.config.loadingStatus.defaults,
...config,
...config.defaults,
...custom,
}
const Icon =
typeof conf.icon === 'undefined'
? Swizzled.components.Spinner
: Swizzled.components[`${Swizzled.methods.capitalize(conf.icon)}Icon`] ||
Swizzled.components.Noop
const Icon = typeof conf.icon === 'undefined' ? Spinner : Spinner //Swizzled.components[`${Swizzled.methods.capitalize(conf.icon)}Icon`] || Swizzled.components.Noop
return (
<div
key={id}
@ -39,9 +44,7 @@ export const LoadingStatus = ({ Swizzled, state, update }) => {
} flex flex-row items-center gap-4 p-4 px-4 ${
conf.fading ? 'opacity-0' : 'opacity-100'
}
transition-opacity delay-[${
Swizzled.config.loadingStatus.timeout * 1000 - 400
}ms] duration-300
transition-opacity delay-[${config.timeout * 1000 - 400}ms] duration-300
md:rounded-lg shadow text-secondary-content text-lg lg:text-xl font-medium md:bg-opacity-90`}
>
<span className="shrink-0">

View file

@ -1,3 +1,5 @@
import { MeasurementInput } from '@freesewing/react/components/Input'
/**
* This MeasurementsEditor component allows inline-editing of the measurements
*
@ -6,10 +8,9 @@
* @param {object} props.state - The ViewWrapper state object
* @param {object} props.state.settings - The current settings
* @param {object} props.update - Helper object for updating the ViewWrapper state
* @param {object} props.Swizzled - An object holding swizzled code
* @return {function} MeasurementsEditor - React component
*/
export const MeasurementsEditor = ({ Design, update, state, Swizzled }) => {
export const MeasurementsEditor = ({ Design, update, state }) => {
/*
* Helper method to handle state updates for measurements
*/
@ -19,13 +20,13 @@ export const MeasurementsEditor = ({ Design, update, state, Swizzled }) => {
return (
<div className="max-w-2xl">
<h5>{Swizzled.methods.t('pe:requiredMeasurements')}</h5>
<h5>Required Measurments</h5>
{Object.keys(Design.patternConfig.measurements).length === 0 ? (
<p>({Swizzled.methods.t('account:none')})</p>
<p>This design does not require any measurements.</p>
) : (
<div>
{Design.patternConfig.measurements.map((m) => (
<Swizzled.components.MeasurementInput
<MeasurementInput
key={m}
m={m}
imperial={state.settings.units === 'imperial' ? true : false}
@ -37,12 +38,12 @@ export const MeasurementsEditor = ({ Design, update, state, Swizzled }) => {
<br />
</div>
)}
<h5>{Swizzled.methods.t('pe:optionalMeasurements')}</h5>
<h5>Optional Measurements</h5>
{Object.keys(Design.patternConfig.optionalMeasurements).length === 0 ? (
<p>({Swizzled.methods.t('account:none')})</p>
<p>This design does not use any optional measurements.</p>
) : (
Design.patternConfig.optionalMeasurements.map((m) => (
<Swizzled.components.MeasurementInput
<MeasurementInput
key={m}
m={m}
imperial={state.settings.units === 'umperial' ? true : false}

View file

@ -1,3 +1,5 @@
import React from 'react'
export const Tooltip = (props) => {
const { children, tip, ...rest } = props

View file

@ -1,137 +0,0 @@
// Hooks
import { useState } from 'react'
/**
* The editor view wrapper component
*
* Figures out what view to load initially,
* and handles state for the pattern, inclding the view
*
* @param (object) props - All the props
* @param {object} props.designs - An object holding all designs
* @param {object} props.preload - Object holding state to preload
* @param {object} props.locale - The locale (language) code
* @param {object} props.design - A design name to force the editor to use this design
* @param {object} props.Swizzled - An object holding swizzled code
*/
export const ViewWrapper = ({ designs = {}, preload = {}, design = false, Swizzled }) => {
/*
* Ephemeral state will not be stored in the state backend
* It is used for things like loading state and so on
*/
const [ephemeralState, setEphemeralState] = useState({})
// Editor state
const allState = Swizzled.hooks.useEditorState(
Swizzled.methods.initialEditorState(preload),
setEphemeralState
)
const state = allState[0]
const update = allState[2]
// Don't bother before state is initialized
if (!state) return <Swizzled.components.TemporaryLoader />
// Figure out what view to load
const [View, extraProps] = viewfinder({ design, designs, preload, state, Swizzled })
/*
* Pass this down to allow disabling features that require measurements
*/
const { missingMeasurements = [] } = extraProps
/*
* Almost all editor state has a default settings, and when that is selected
* we just unset that value in the state. This way, state holds only what is
* customized, and it makes it a lot easier to see how a pattern was edited.
* The big exception is the 'ui.ux' setting. If it is unset, a bunch of
* components will not function properly. We could guard against this by passing
* the default to all of these components, but instead, we just check that state
* is undefined, and if so pass down the default ux value here.
* This way, should more of these exceptions get added over time, we can use
* the same centralized solution.
*/
const passDownState =
state.ui.ux === undefined
? {
...state,
ui: { ...state.ui, ux: Swizzled.config.defaultUx },
_: { ...ephemeralState, missingMeasurements },
}
: { ...state, _: { ...ephemeralState, missingMeasurements } }
return (
<div className="flex flex-row items-top">
{Swizzled.config.withAside ? (
<Swizzled.components.AsideViewMenu update={update} state={passDownState} />
) : null}
<div
className={
state.ui.kiosk
? 'md:z-30 md:w-screen md:h-screen md:fixed md:top-0 md:left-0 md:bg-base-100'
: 'grow w-full'
}
>
<Swizzled.components.LoadingStatus state={passDownState} update={update} />
<View {...extraProps} {...{ update, designs }} state={passDownState} />
</div>
</div>
)
}
/**
* Helper method to figure out what view to load
* based on the props passed in, and destructure
* the props we need for it.
*
* @param (object) props - All the props
* @param {object} props.design - The (name of the) current design
* @param {object} props.designs - An object holding all designs
* @param (object) props.state - React state passed down from the wrapper view
*/
const viewfinder = ({ design, designs, state, Swizzled }) => {
/*
* Grab Design from props or state and make them extra props
*/
if (!design && state?.design) design = state.design
const Design = designs[design] || false
const extraProps = { design, Design }
/*
* If no design is set, return the designs view
*/
if (!designs[design]) return [getViewComponent('designs', Swizzled), extraProps]
/*
* If we have a design, do we have the measurements?
*/
const [measurementsOk, missingMeasurements] = Swizzled.methods.hasRequiredMeasurements(
designs[design],
state.settings?.measurements
)
if (missingMeasurements) extraProps.missingMeasurements = missingMeasurements
/*
* Allow all views that do not require measurements before
* we force the user to the measurements view
*/
if (state.view && Swizzled.config.measurementsFreeViews.includes(state.view)) {
const view = getViewComponent(state.view, Swizzled)
if (view) return [view, extraProps]
}
if (!measurementsOk) return [getViewComponent('measurements', Swizzled), extraProps]
/*
* If a view is set, return that
*/
const view = getViewComponent(state.view, Swizzled)
if (view) return [view, extraProps]
/*
* If no obvious view was found, return the view picker
*/
return [getViewComponent('picker', Swizzled), extraProps]
}
const getViewComponent = (view = false, Swizzled) =>
view ? Swizzled.components[Swizzled.config.viewComponents[view]] : false

View file

@ -0,0 +1,22 @@
import React from 'react'
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 }) => (
<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
editor
onClick={(design) => {
update.design(design)
update.view('draft')
}}
/>
</div>
)

View file

@ -1,5 +1,21 @@
import { Fragment, useEffect } from 'react'
// Dependencies
import { horFlexClasses } from '../../utils.mjs'
import { t, designMeasurements } from '../../lib/index.mjs'
import { capitalize } from '@freesewing/utils'
// Hooks
import React, { Fragment, useEffect } from 'react'
// Components
import { Popout } from '@freesewing/react/components/Popout'
import {
BookmarkIcon,
CuratedMeasurementsSetIcon,
EditIcon,
MeasurementsSetIcon,
} from '@freesewing/react/components/Icon'
import { Accordion } from '../Accordion.mjs'
import { MeasurementsEditor } from '../MeasurementsEditor.mjs'
import { SetPicker, BookmarkedSetPicker, CuratedSetPicker, UserSetPicker } from '../Set.mjs'
import { HeaderMenu } from '../HeaderMenu.mjs'
const iconClasses = { className: 'w-8 h-8 md:w-10 md:h-10 lg:w-12 lg:h-12 shrink-0', stroke: 1.5 }
@ -9,41 +25,20 @@ const iconClasses = { className: 'w-8 h-8 md:w-10 md:h-10 lg:w-12 lg:h-12 shrink
* It will be automatically loaded if we do not have all required measurements for a design.
*
* @param {Object} props - All the props
* @param {Object} props.Swizzled - An object with swizzled components, hooks, methods, and config
* @param {Function} props.config - The editor configuration
* @param {Function} props.Design - The design constructor
* @param {string} props.design - The design name
* @param {Object} props.state - The editor state object
* @param {Object} props.update - Helper object for updating the ViewWrapper state
* @param {Array} props.missingMeasurements - List of missing measurements for the current design
* @param {Object} props.state - The editor state object
* @param {Object} props.update - Helper object for updating the editor state
* @return {Function} MeasurementsView - React component
*/
export const MeasurementsView = ({ Design, missingMeasurements, update, Swizzled, state }) => {
// Swizzled components
const {
Accordion,
Popout,
MeasurementsEditor,
MeasurementsSetIcon,
UserSetPicker,
BookmarkIcon,
BookmarkedSetPicker,
CuratedMeasurementsSetIcon,
CuratedSetPicker,
EditIcon,
} = Swizzled.components
// Swizzled methods
const { t, designMeasurements, capitalize } = Swizzled.methods
// Swizzled config
const { config } = Swizzled
// Editor state
const { locale } = state
export const MeasurementsView = ({ config, Design, missingMeasurements, state, update }) => {
/*
* If there is no view set, completing measurements will switch to the view picker
* Which is a bit confusing. So in this case, set the view to measurements.
*/
useEffect(() => {
if (!config.views.includes(state.view)) update.view('measurements')
if (!config?.views || !config.views.includes(state.view)) update.view('measurements')
if (state._.missingMeasurements && state._.missingMeasurements.length > 0)
update.notify({ msg: t('pe:missingMeasurementsNotify'), icon: 'tip' }, 'missingMeasurements')
else update.notifySuccess(t('pe:measurementsAreOk'))
@ -78,7 +73,7 @@ export const MeasurementsView = ({ Design, missingMeasurements, update, Swizzled
size="md"
clickHandler={loadMeasurements}
missingClickHandler={loadMeasurements}
{...{ Swizzled, Design }}
{...{ config, Design }}
/>,
'ownSets',
],
@ -95,7 +90,7 @@ export const MeasurementsView = ({ Design, missingMeasurements, update, Swizzled
size="md"
clickHandler={loadMeasurements}
missingClickHandler={loadMeasurements}
{...{ Swizzled, Design }}
{...{ config, Design }}
/>,
'bmSets',
],
@ -107,11 +102,7 @@ export const MeasurementsView = ({ Design, missingMeasurements, update, Swizzled
</div>
<p className="text-left">{t('pe:chooseFromCuratedSetsDesc')}</p>
</Fragment>,
<CuratedSetPicker
key={2}
clickHandler={loadMeasurements}
{...{ Swizzled, Design, locale }}
/>,
<CuratedSetPicker key={2} clickHandler={loadMeasurements} {...{ config, Design }} />,
'csets',
]
)
@ -124,13 +115,13 @@ export const MeasurementsView = ({ Design, missingMeasurements, update, Swizzled
</div>
<p className="text-left">{t('pe:editMeasurementsDesc')}</p>
</Fragment>,
<MeasurementsEditor key={2} {...{ Design, Swizzled, update, state }} />,
<MeasurementsEditor key={2} {...{ Design, config, update, state }} />,
'edit',
])
return (
<>
<Swizzled.components.HeaderMenu state={state} {...{ Swizzled, update }} />
<HeaderMenu state={state} {...{ config, update }} />
<div className="max-w-7xl mt-8 mx-auto px-4">
<h2>{t('pe:measurements')}</h2>
{missingMeasurements && missingMeasurements.length > 0 ? (

View file

@ -0,0 +1,117 @@
import React from 'react'
import { ViewPicker } from './ViewPicker.mjs'
import { DesignsView } from './DesignsView.mjs'
import { MeasurementsView } from './MeasurementsView.mjs'
import { ErrorIcon } from '@freesewing/react/components/Icon'
/*
* This allows us to load a view component from the view name
*/
export const viewComponents = {
// DraftView,
DesignsView,
// SaveView,
ViewPicker,
MeasurementsView,
// UndosView,
}
/*
* This returns a view-specific component
*/
export const View = (props) => {
const { view } = props
if (view === 'designs') return <DesignsView {...props} />
if (view === 'measurements') return <MeasurementsView {...props} />
/*
viewComponents: {
draft: 'DraftView',
designs: 'DesignsView',
save: 'SaveView',
export: 'ViewPicker',
measurements: 'MeasurementsView',
undos: 'UndosView',
printLayout: 'ViewPicker',
editSettings: 'ViewPicker',
docs: 'ViewPicker',
inspect: 'ViewPicker',
logs: 'ViewPicker',
test: 'ViewPicker',
timing: 'ViewPicker',
picker: 'ViewPicker',
error: 'ViewPicker',
},
*/
return <p>No view component for view {props.view}</p>
}
/*
* This returns a view-specific icon
*/
export const ViewIcon = ({ view, className = 'tw-w-6 tw-h-6' }) => {
//designs: <ErrorIcon className={className} />,
//measurements: <ErrorIcon className={className} />,
//
return <ErrorIcon />
}
export const viewLabels = {
draft: {
t: 'Draft Pattern',
d: 'Choose this if you are not certain what to pick',
},
measurements: {
t: 'Pattern Measurements',
d: 'Update or load measurements to generate a pattern for',
},
test: {
t: 'Test Design',
d: 'See how different options or changes in measurements influence the design',
},
timing: {
t: 'Time Design',
d: 'Shows detailed timing of the pattern being drafted, allowing you to find bottlenecks in performance',
},
layout: {
t: 'Pattern Layout',
d: 'Organize your pattern parts to minimize paper use',
},
save: {
t: 'Save pattern as...',
d: 'Save the changes to this pattern in your account, or save it as a new pattern',
},
export: {
t: 'Export Pattern',
d: 'Export this pattern into a variety of formats',
},
edit: {
t: 'Edit settings by hand',
d: "Throw caution to the wind, and hand-edit the pattern's settings",
},
logs: {
t: 'Pattern Logs',
d: 'Show the logs generated by the pattern, useful to troubleshoot problems',
},
inspect: {
t: 'Pattern inspector',
d: "Load the pattern in the inspector, giving you in-depth info about a pattern's components",
},
docs: {
t: 'Documentation',
d: 'More information and links to documentation',
},
designs: {
t: 'Choose a different Design',
d: 'Current design: {- design }',
},
picker: {
t: 'Choose a different view',
d: 'fixme',
},
undos: {
t: 'Undo History',
d: 'Time-travel through your recent pattern changes',
},
}

View file

@ -1,23 +1,5 @@
/*************************************************************************
* *
* FreeSewing's pattern editor allows swizzling the config *
* *
* To 'swizzle' means to replace a default implementation with a *
* custom one. It allows one to customize the pattern editor. *
* *
* This file holds the 'swizzleConfig' method that will return *
* the merged configuration. *
* *
* To use a custom config, simply pas it as a prop into the editor *
* under the 'config' key. So to pass a custom 'newSet' link (used to *
* link to a page to create a new measurements set), you do: *
* *
* <PatternEditor config={{ newSet: '/my/custom/page' }} /> *
* *
*************************************************************************/
/*
* Default config for the FreeSewing pattern editor
* Default configuration for the FreeSewing pattern editor
*/
export const defaultConfig = {
// Enable use of a (FreeSewing) backend to load data from
@ -145,14 +127,6 @@ export const defaultConfig = {
aside: 1,
ux: 4,
},
locale: 'en',
},
loadingStatus: {
timeout: 2,
defaults: {
color: 'secondary',
icon: 'Spinner',
},
},
classes: {
horFlex: 'flex flex-row items-center justify-between gap-4 w-full',
@ -180,9 +154,9 @@ export const defaultConfig = {
}
/*
* This method returns the swizzled configuration
* This method returns a merged configuration
*/
export const swizzleConfig = (config = {}) => {
export const mergeConfig = (config = {}) => {
const mergedConfig = {
...defaultConfig,
...config,

View file

@ -0,0 +1,86 @@
// Dependenicies
import { atomWithHash } from 'jotai-location'
import { stateUpdateFactory } from '../lib/index.mjs'
// Hooks
import { useAtom } from 'jotai'
import React, { useMemo, useState, useEffect } from 'react'
/*
* Set up the atom
*/
const urlAtom = atomWithHash('s', {})
/**
* Url state backend
*
* This holds the editor state, using session storage.
* It also provides helper methods to manipulate state.
*
* @params {object} init - Initial pattern settings
* @params {function} setEphemeralState - Method to set the ephemeral state
* @params {object] config - The editor config
* @return {array} return - And array with get, set, and update methods
*/
export const useEditorState = (init = {}, setEphemeralState, config) => {
const [state, setState] = useAtom(urlAtom)
const update = useMemo(() => stateUpdateFactory(setState, setEphemeralState, config), [setState])
/*
* Set the initial state
*/
useEffect(() => {
// Handle state on a hard reload or cold start
if (typeof URLSearchParams !== 'undefined') {
try {
const data = getHashData()
if (data.s === 'object') setState(data.s)
else setState(init)
} catch (err) {
setState(init)
}
}
}, [])
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
const hash = window.location.hash
.slice(1)
.split('&')
.map((chunk) => chunk.split('='))
const data = {}
for (const [key, val] of hash) {
data[key] = JSON.parse(decodeURIComponent(decodeURI(val)))
}
return data
}

View file

@ -1,89 +1,159 @@
import { useState, useEffect } from 'react'
import { swizzleConfig } from './swizzle/config.mjs'
import { swizzleComponents } from './swizzle/components/index.mjs'
import { swizzleHooks } from './swizzle/hooks/index.mjs'
import { swizzleMethods } from './swizzle/methods/index.mjs'
import { ViewWrapper } from './components/view-wrapper.mjs'
// This is an exception as we need to show something before Swizzled components are ready
import { TemporaryLoader as UnswizzledTemporaryLoader } from './swizzle/components/loaders.mjs'
/*
* Namespaces used by the pattern editor
*/
export const ns = ['pe', 'measurements']
// Dependencies
import { designs } from '@freesewing/collection'
import { hasRequiredMeasurements, initialEditorState } from './lib/index.mjs'
import { mergeConfig } from './config/index.mjs'
// Hooks
import React, { useState } from 'react'
import { useEditorState } from './hooks/useEditorState.mjs'
// Components
import { View } from './components/views/index.mjs'
import { Spinner } from '@freesewing/react/components/Spinner'
import { AsideViewMenu } from './components/AsideViewMenu.mjs'
import { LoadingStatus } from './components/LoadingStatus.mjs'
/**
* PatternEditor is the high-level FreeSewing component
* FreeSewing's pattern editor
*
* Editor is the high-level FreeSewing component
* that provides the entire pattern editing environment
* This is a high-level wrapper that figures out what view to load initially,
* and handles state for the pattern, including the view
*
* @param {object} props.design = The name of the design we are editing
* @param {object} props.designs = An object holding the designs code
* @param {object} props.components = An object holding components to swizzle
* @param {object} props.hooks = An object holding hooks to swizzle
* @param {object} props.methods = An object holding methods to swizzle
* @param {object} props.config = An object holding the editor config to swizzle
* @param {object} props.locale = The locale (language) code
* @param {object} props.preload = Any state to preload
*
* @param {object} props - All React props
* @param {object} props.config - A configuration object for the editor
* @param {object} props.design - A design name to force the editor to use this design
* @param {object} props.preload - Any state to preload
*/
export const PatternEditor = (props) => {
const [swizzled, setSwizzled] = useState(false)
useEffect(() => {
if (!swizzled) {
const merged = {
config: swizzleConfig(props.config),
}
merged.methods = swizzleMethods(props.methods, merged)
merged.components = swizzleComponents(props.components, merged)
merged.hooks = swizzleHooks(props.hooks, merged)
setSwizzled(merged)
}
}, [swizzled, props.components, props.config, props.hooks, props.methods])
if (!swizzled?.hooks) return <UnswizzledTemporaryLoader />
export const Editor = ({ config = {}, design = false, preload = {} }) => {
/*
* First of all, make sure we have all the required props
* Ephemeral state will not be stored in the state backend
* It is used for things like loading state and so on
*/
const lackingProps = lackingPropsCheck(props)
if (lackingProps !== false) return <LackingPropsError error={lackingProps} />
const [ephemeralState, setEphemeralState] = useState({})
/*
* Extract props we care about
* Merge custom and default configuration
*/
const { designs = {}, locale = 'en', preload } = props
const editorConfig = mergeConfig(config)
/*
* Now return the view wrapper and pass it the relevant props and the swizzled props
* The Editor state is kept in a state backend (URL)
*/
return <ViewWrapper {...{ designs, locale, preload }} Swizzled={swizzled} />
const allState = useEditorState(
initialEditorState(preload, config),
setEphemeralState,
editorConfig
)
const state = allState[0]
const update = allState[2]
/*
* Don't bother before state is initialized
*/
if (!state) return <Spinner />
// Figure out what view to load
const [view, extraProps] = viewfinder({ design, designs, preload, state, config: editorConfig })
/*
* Pass this down to allow disabling features that require measurements
*/
const { missingMeasurements = [] } = extraProps
/*
* Almost all editor state has a default settings, and when that is selected
* we just unset that value in the state. This way, state holds only what is
* customized, and it makes it a lot easier to see how a pattern was edited.
* The big exception is the 'ui.ux' setting. If it is unset, a bunch of
* components will not function properly. We could guard against this by passing
* the default to all of these components, but instead, we just check that state
* is undefined, and if so pass down the default ux value here.
* This way, should more of these exceptions get added over time, we can use
* the same centralized solution.
*/
const passDownState =
state.ui?.ux === undefined
? {
...state,
ui: { ...(state.ui || {}), ux: editorConfig.defaultUx },
_: { ...ephemeralState, missingMeasurements },
}
: { ...state, _: { ...ephemeralState, missingMeasurements } }
/**
* Helper function to verify that all props that are required to
* run the editor are present.
*
* Note that these errors are not translation, because they are
* not intended for end-users, but rather for developers.
*
* @param {object} props - The props passed to the PatternEditor component
* @return {bool} result - Either true or false depending on required props being present
*/
const lackingPropsCheck = (props) => {
if (typeof props.designs !== 'object')
return "Please pass a 'designs' prop with the designs supported by this editor"
if (Object.keys(props.designs).length < 1) return "The 'designs' prop does not hold any designs"
return false
return (
<div className="flex flex-row items-top">
{editorConfig.withAside ? <AsideViewMenu update={update} state={passDownState} /> : null}
<div
className={
state.ui?.kiosk
? 'md:z-30 md:w-screen md:h-screen md:fixed md:top-0 md:left-0 md:bg-base-100'
: 'grow w-full'
}
/**
* A component to inform the user that the editor cannot be started
* because there are missing required props
*/
const LackingPropsError = ({ error }) => (
<div className="w-full p-0 text-center py-24">
<h2>Unable to initialize pattern editor</h2>
<p>{error}</p>
>
<LoadingStatus state={passDownState} update={update} />
<View
{...extraProps}
{...{ view, update, designs, config: editorConfig }}
state={passDownState}
/>
</div>
</div>
)
}
/**
* Helper method to figure out what view to load
* based on the props passed in, and destructure
* the props we need for it.
*
* @param (object) props - All the props
* @param {object} props.design - The (name of the) current design
* @param {object} props.designs - An object holding all designs
* @param (object) props.state - React state passed down from the wrapper view
* @param (object) props.config - The editor config
*/
const viewfinder = ({ design, designs, state, config }) => {
/*
* Grab Design from props or state and make them extra props
*/
if (!design && state?.design) design = state.design
const Design = designs[design] || false
const extraProps = { design, Design }
/*
* If no design is set, return the designs view
*/
if (!designs[design]) return ['designs', extraProps]
/*
* If we have a design, do we have the measurements?
*/
const [measurementsOk, missingMeasurements] = hasRequiredMeasurements(
designs[design],
state.settings?.measurements
)
if (missingMeasurements) extraProps.missingMeasurements = missingMeasurements
/*
* Allow all views that do not require measurements before
* we force the user to the measurements view
*/
if (state.view && config.measurementsFreeViews.includes(state.view))
return [state.view, extraProps]
/*
* Force the measurements view if measurements are missing
*/
if (!measurementsOk) return ['measurements', extraProps]
/*
* If a view is set, return that
*/
if (state.view) return [state.view, extraProps]
/*
* If no obvious view was found, return the view picker
*/
return ['picker', extraProps]
}

View file

@ -1,12 +1,21 @@
// Components
import {
ErrorIcon,
MeasurementsIcon,
OptionsIcon,
SettingsIcon,
UiIcon,
} from '@freesewing/react/components/Icon'
import { HtmlSpan } from '../components/HtmlSpan.mjs'
/*
* This method drafts the pattern
*
* @param {object} Swizzled - Swizzled code, not used here
* @param {function} Design - The Design constructor
* @param {object} settings - The settings for the pattern
* @return {object} data - The drafted pattern, along with errors and failure data
*/
export function draft(Swizzled, Design, settings) {
export function draft(Design, settings) {
const data = {
// The pattern
pattern: new Design(settings),
@ -26,9 +35,9 @@ export function draft(Swizzled, Design, settings) {
return data
}
export function flattenFlags(Swizzled, flags) {
export function flattenFlags(flags, config) {
const all = {}
for (const type of Swizzled.config.flagTypes) {
for (const type of config.flagTypes) {
let i = 0
if (flags[type]) {
for (const flag of Object.values(flags[type])) {
@ -41,22 +50,22 @@ export function flattenFlags(Swizzled, flags) {
return all
}
export function getUiPreferenceUndoStepData(Swizzled, { step }) {
export function getUiPreferenceUndoStepData({ step }) {
/*
* We'll need these
*/
const field = step.name === 'ui' ? step.path[1] : step.path[2]
const structure = Swizzled.methods.menuUiPreferencesStructure()[field]
const structure = menuUiPreferencesStructure()[field]
/*
* This we'll end up returning
*/
const data = {
icon: <Swizzled.components.UiIcon />,
icon: <UiIcon />,
field,
optCode: `${field}.t`,
titleCode: 'uiPreferences.t',
structure: Swizzled.methods.menuUiPreferencesStructure()[field],
structure: menuUiPreferencesStructure()[field],
}
const FieldIcon = data.structure.icon
data.fieldIcon = <FieldIcon />
@ -65,7 +74,7 @@ export function getUiPreferenceUndoStepData(Swizzled, { step }) {
* Add oldval and newVal if they exist, or fall back to default
*/
for (const key of ['old', 'new'])
data[key + 'Val'] = Swizzled.methods.t(
data[key + 'Val'] = t(
structure.choiceTitles[
structure.choiceTitles[String(step[key])] ? String(step[key]) : String(structure.dflt)
] + '.t'
@ -74,9 +83,9 @@ export function getUiPreferenceUndoStepData(Swizzled, { step }) {
return data
}
export function getCoreSettingUndoStepData(Swizzled, { step, state, Design }) {
export function getCoreSettingUndoStepData({ step, state, Design }) {
const field = step.path[1]
const structure = Swizzled.methods.menuCoreSettingsStructure({
const structure = menuCoreSettingsStructure({
language: state.language,
units: state.settings.units,
sabool: state.settings.sabool,
@ -87,19 +96,19 @@ export function getCoreSettingUndoStepData(Swizzled, { step, state, Design }) {
field,
titleCode: 'coreSettings.t',
optCode: `${field}.t`,
icon: <Swizzled.components.SettingsIcon />,
icon: <SettingsIcon />,
structure: structure[field],
}
if (!data.structure && field === 'sa') data.structure = structure.samm
const FieldIcon = data.structure?.icon || Swizzled.components.FixmeIcon
const FieldIcon = data.structure?.icon || ErrorIcon
data.fieldIcon = <FieldIcon />
/*
* Save us some typing
*/
const cord = Swizzled.methods.settingsValueCustomOrDefault
const formatMm = Swizzled.methods.formatMm
const Html = Swizzled.components.HtmlSpan
const cord = settingsValueCustomOrDefault
const formatMm = formatMm
const Html = HtmlSpan
/*
* Need to allow HTML in some of these in case this is
@ -120,24 +129,20 @@ export function getCoreSettingUndoStepData(Swizzled, { step, state, Design }) {
data.newVal = cord(step.new, data.structure.dflt)
return data
case 'units':
data.oldVal = Swizzled.methods.t(
step.new === 'imperial' ? 'pe:metricUnits' : 'pe:imperialUnits'
)
data.newVal = Swizzled.methods.t(
step.new === 'imperial' ? 'pe:imperialUnits' : 'pe:metricUnits'
)
data.oldVal = t(step.new === 'imperial' ? 'pe:metricUnits' : 'pe:imperialUnits')
data.newVal = t(step.new === 'imperial' ? 'pe:imperialUnits' : 'pe:metricUnits')
return data
case 'only':
data.oldVal = cord(step.old, data.structure.dflt) || Swizzled.methods.t('pe:includeAllParts')
data.newVal = cord(step.new, data.structure.dflt) || Swizzled.methods.t('pe:includeAllParts')
data.oldVal = cord(step.old, data.structure.dflt) || t('pe:includeAllParts')
data.newVal = cord(step.new, data.structure.dflt) || t('pe:includeAllParts')
return data
default:
data.oldVal = Swizzled.methods.t(
data.oldVal = t(
(data.structure.choiceTitles[String(step.old)]
? data.structure.choiceTitles[String(step.old)]
: data.structure.choiceTitles[String(data.structure.dflt)]) + '.t'
)
data.newVal = Swizzled.methods.t(
data.newVal = t(
(data.structure.choiceTitles[String(step.new)]
? data.structure.choiceTitles[String(step.new)]
: data.structure.choiceTitles[String(data.structure.dflt)]) + '.t'
@ -146,32 +151,32 @@ export function getCoreSettingUndoStepData(Swizzled, { step, state, Design }) {
}
}
export function getDesignOptionUndoStepData(Swizzled, { step, state, Design }) {
export function getDesignOptionUndoStepData({ step, state, Design }) {
const option = Design.patternConfig.options[step.path[2]]
const data = {
icon: <Swizzled.components.OptionsIcon />,
icon: <OptionsIcon />,
field: step.path[2],
optCode: `${state.design}:${step.path[2]}.t`,
titleCode: `designOptions.t`,
oldVal: Swizzled.methods.formatDesignOptionValue(option, step.old, state.units === 'imperial'),
newVal: Swizzled.methods.formatDesignOptionValue(option, step.new, state.units === 'imperial'),
oldVal: formatDesignOptionValue(option, step.old, state.units === 'imperial'),
newVal: formatDesignOptionValue(option, step.new, state.units === 'imperial'),
}
return data
}
export function getUndoStepData(Swizzled, props) {
export function getUndoStepData(props) {
/*
* UI Preferences
*/
if ((props.step.name === 'settings' && props.step.path[1] === 'ui') || props.step.name === 'ui')
return Swizzled.methods.getUiPreferenceUndoStepData(props)
return getUiPreferenceUndoStepData(props)
/*
* Design options
*/
if (props.step.name === 'settings' && props.step.path[1] === 'options')
return Swizzled.methods.getDesignOptionUndoStepData(props)
return getDesignOptionUndoStepData(props)
/*
* Core Settings
@ -191,14 +196,14 @@ export function getUndoStepData(Swizzled, props) {
'expand',
].includes(props.step.path[1])
)
return Swizzled.methods.getCoreSettingUndoStepData(props)
return getCoreSettingUndoStepData(props)
/*
* Measurements
*/
if (props.step.name === 'settings' && props.step.path[1] === 'measurements') {
const data = {
icon: <Swizzled.components.MeasurementsIcon />,
icon: <MeasurementsIcon />,
field: 'measurements',
optCode: `measurements`,
titleCode: 'measurements',
@ -210,14 +215,14 @@ export function getUndoStepData(Swizzled, props) {
return {
...data,
field: props.step.path[2],
oldVal: Swizzled.methods.formatMm(props.step.old, props.imperial),
newVal: Swizzled.methods.formatMm(props.step.new, props.imperial),
oldVal: formatMm(props.step.old, props.imperial),
newVal: formatMm(props.step.new, props.imperial),
}
let count = 0
for (const m of Object.keys(props.step.new)) {
if (props.step.new[m] !== props.step.old?.[m]) count++
}
return { ...data, msg: Swizzled.methods.t('pe:xMeasurementsChanged', { count }) }
return { ...data, msg: t('pe:xMeasurementsChanged', { count }) }
}
/*
@ -229,13 +234,12 @@ export function getUndoStepData(Swizzled, props) {
* This helper method constructs the initial state object.
*
* If they are not present, it will fall back to the relevant defaults
* @param {object} Swizzled - The swizzled data
*/
export function initialEditorState(Swizzled) {
export function initialEditorState(preload = {}, config) {
/*
* Create initial state object
*/
const initial = { ...Swizzled.config.initialState }
const initial = { ...config.initialState, ...preload }
/*
* FIXME: Add preload support, from URL or other sources, rather than just passing in an object
@ -243,6 +247,7 @@ export function initialEditorState(Swizzled) {
return initial
}
/**
* round a value to the correct number of decimal places to display all supplied digits after multiplication
* this is a workaround for floating point errors
@ -250,13 +255,11 @@ export function initialEditorState(Swizzled) {
* roundPct(0.72, 100) === 72
* roundPct(7.5, 0.01) === 0.075
* roundPct(7.50, 0.01) === 0.0750
* @param {object} Swizzled - Swizzled code, not used here
* @param {Number} num the number to be operated on
* @param {Number} factor the number to multiply by
* @return {Number} the given num multiplied by the factor, rounded appropriately
*/
export function menuRoundPct(Swizzled, num, factor) {
const { round } = Swizzled.methods
export function menuRoundPct(num, factor) {
// stringify
const str = '' + num
// get the index of the decimal point in the number
@ -273,7 +276,6 @@ const menuFractionInputMatcher = /^-?[0-9]*(\s?[0-9]+\/|[.,eE])?[0-9]+$/ // matc
/**
* Validate and parse a value that should be a number
* @param {object} Swizzled - Swizzled code, not used here
* @param {any} val the value to validate
* @param {Boolean} allowFractions should fractions be considered valid input?
* @param {Number} min the minimum allowable value
@ -283,7 +285,6 @@ const menuFractionInputMatcher = /^-?[0-9]*(\s?[0-9]+\/|[.,eE])?[0-9]+$/ // matc
* or the value parsed to a number if it is valid
*/
export function menuValidateNumericValue(
Swizzled,
val,
allowFractions = true,
min = -Infinity,
@ -302,7 +303,7 @@ export function menuValidateNumericValue(
// replace comma with period
const parsedVal = val.replace(',', '.')
// if fractions are allowed, parse for fractions, otherwise use the number as a value
const useVal = allowFractions ? Swizzled.methods.fractionToDecimal(parsedVal) : parsedVal
const useVal = allowFractions ? fractionToDecimal(parsedVal) : parsedVal
// check that it's a number and it's in the range
if (isNaN(useVal) || useVal > max || useVal < min) return false
@ -313,12 +314,11 @@ export function menuValidateNumericValue(
/**
* Check to see if a value is different from its default
* @param {object} Swizzled - Swizzled code, not used here
* @param {Number|String|Boolean} current the current value
* @param {Object} config configuration containing a dflt key
* @return {Boolean} was the value changed?
*/
export function menuValueWasChanged(Swizzled, current, config) {
export function menuValueWasChanged(current, config) {
if (typeof current === 'undefined') return false
if (current == config.dflt) return false
@ -332,13 +332,12 @@ const UNSET = '__UNSET__'
/*
* Helper method to handle object updates
*
* @param {object} methods - An object holding possibly swizzled methods (unused here)
* @param {object} obj - The object to update
* @param {string|array} path - The path to the key to update, either as array or dot notation
* @param {mixed} val - The new value to set or 'unset' to unset the value
* @return {object} result - The updated object
*/
export function objUpdate(Swizzled, obj = {}, path, val = '__UNSET__') {
export function objUpdate(obj = {}, path, val = '__UNSET__') {
if (val === UNSET) unset(obj, path)
else set(obj, path, val)
@ -348,21 +347,13 @@ export function objUpdate(Swizzled, obj = {}, path, val = '__UNSET__') {
/*
* Helper method to handle object updates that also updates the undo history in ephemeral state
*
* @param {object} methods - An object holding possibly swizzled methods (unused here)
* @param {object} obj - The object to update
* @param {string|array} path - The path to the key to update, either as array or dot notation
* @param {mixed} val - The new value to set or 'unset' to unset the value
* @param {function} setEphemeralState - The ephemeral state setter
* @return {object} result - The updated object
*/
export function undoableObjUpdate(
Swizzled,
name,
obj = {},
path,
val = '__UNSET__',
setEphemeralState
) {
export function undoableObjUpdate(name, obj = {}, path, val = '__UNSET__', setEphemeralState) {
const current = get(obj, path)
setEphemeralState((cur) => {
if (!Array.isArray(cur.undos)) cur.undos = []
@ -375,14 +366,14 @@ export function undoableObjUpdate(
path,
old: current,
new: val,
restore: Swizzled.methods.cloneObject(obj),
restore: cloneObject(obj),
},
...cur.undos,
],
}
})
return Swizzled.methods.objUpdate(obj, path, val)
return objUpdate(obj, path, val)
}
/*
@ -393,20 +384,16 @@ export function undoableObjUpdate(
* - sa: sa value for core
* - samm: Holds the sa value in mm even when sa is off
*
* @param {object} Swizzled - An object holding possibly swizzled code (unused here)
* @param {object} undo - The undo step to add
* @param {object} restore - The state to restore for this step
* @param {function} setEphemeralState - The ephemeral state setter
*/
export function addUndoStep(Swizzled, undo, restore, setEphemeralState) {
export function addUndoStep(undo, restore, setEphemeralState) {
setEphemeralState((cur) => {
if (!Array.isArray(cur.undos)) cur.undos = []
return {
...cur,
undos: [
{ time: Date.now(), ...undo, restore: Swizzled.methods.cloneObject(restore) },
...cur.undos,
],
undos: [{ time: Date.now(), ...undo, restore: cloneObject(restore) }, ...cur.undos],
}
})
}
@ -414,9 +401,10 @@ export function addUndoStep(Swizzled, undo, restore, setEphemeralState) {
/*
* Helper method to clone an object
*/
export function cloneObject(Swizzled, obj) {
export function cloneObject(obj) {
return JSON.parse(JSON.stringify(obj))
}
/**
* Helper method to push a prefix to a set path
*
@ -427,19 +415,20 @@ export function cloneObject(Swizzled, obj) {
* @param {string|array} path - The path to prefix either as array or a string in dot notation
* @return {array} newPath - The prefixed path
*/
export function statePrefixPath(Swizzled, prefix, path) {
export function statePrefixPath(prefix, path) {
if (Array.isArray(path)) return [prefix, ...path]
else return [prefix, ...path.split('.')]
}
/*
* This creates the helper object for state updates
*/
export function stateUpdateFactory(Swizzled, setState, setEphemeralState) {
export function stateUpdateFactory(setState, setEphemeralState, config) {
return {
/*
* This allows raw access to the entire state object
*/
state: (path, val) => setState((cur) => Swizzled.methods.objUpdate({ ...cur }, path, val)),
state: (path, val) => setState((cur) => objUpdate({ ...cur }, path, val)),
/*
* These hold an object, so we take a path
*/
@ -453,10 +442,10 @@ export function stateUpdateFactory(Swizzled, setState, setEphemeralState) {
)
}
return setState((cur) =>
Swizzled.methods.undoableObjUpdate(
undoableObjUpdate(
'settings',
{ ...cur },
Swizzled.methods.statePrefixPath('settings', path),
statePrefixPath('settings', path),
val,
setEphemeralState
)
@ -479,7 +468,7 @@ export function stateUpdateFactory(Swizzled, setState, setEphemeralState) {
toggleSa: () =>
setState((cur) => {
const sa = cur.settings?.samm || (cur.settings?.units === 'imperial' ? 15.3125 : 10)
const restore = Swizzled.methods.cloneObject(cur)
const restore = cloneObject(cur)
// This requires 3 changes
const update = cur.settings.sabool
? [
@ -492,9 +481,9 @@ export function stateUpdateFactory(Swizzled, setState, setEphemeralState) {
['sa', sa],
['samm', sa],
]
for (const [key, val] of update) Swizzled.methods.objUpdate(cur, `settings.${key}`, val)
for (const [key, val] of update) objUpdate(cur, `settings.${key}`, val)
// Which we'll group as 1 undo action
Swizzled.methods.addUndoStep(
addUndoStep(
{
name: 'settings',
path: ['settings', 'sa'],
@ -509,21 +498,15 @@ export function stateUpdateFactory(Swizzled, setState, setEphemeralState) {
}),
ui: (path, val) =>
setState((cur) =>
Swizzled.methods.undoableObjUpdate(
'ui',
{ ...cur },
Swizzled.methods.statePrefixPath('ui', path),
val,
setEphemeralState
)
undoableObjUpdate('ui', { ...cur }, statePrefixPath('ui', path), val, setEphemeralState)
),
/*
* These only hold a string, so we only take a value
*/
design: (val) => setState((cur) => Swizzled.methods.objUpdate({ ...cur }, 'design', val)),
design: (val) => setState((cur) => objUpdate({ ...cur }, 'design', val)),
view: (val) => {
// Only take valid view names
if (!Swizzled.config.views.includes(val)) return console.log('not a valid view:', val)
if (!config.views.includes(val)) return console.log('not a valid view:', val)
setState((cur) => ({ ...cur, view: val }))
// Also add it onto the views (history)
setEphemeralState((cur) => {
@ -533,7 +516,7 @@ export function stateUpdateFactory(Swizzled, setState, setEphemeralState) {
},
viewBack: () => {
setEphemeralState((eph) => {
if (Array.isArray(eph.views) && Swizzled.config.views.includes(eph.views[1])) {
if (Array.isArray(eph.views) && config.views.includes(eph.views[1])) {
// Load view at the 1 position of the history
setState((cur) => ({ ...cur, view: eph.views[1] }))
return { ...eph, views: eph.views.slice(1) }
@ -542,20 +525,20 @@ export function stateUpdateFactory(Swizzled, setState, setEphemeralState) {
return eph
})
},
ux: (val) => setState((cur) => Swizzled.methods.objUpdate({ ...cur }, 'ux', val)),
ux: (val) => setState((cur) => objUpdate({ ...cur }, 'ux', val)),
clearPattern: () =>
setState((cur) => {
const newState = { ...cur }
Swizzled.methods.objUpdate(newState, 'settings', {
objUpdate(newState, 'settings', {
measurements: cur.settings.measurements,
})
/*
* Let's also reset the renderer to React as that feels a bit like a pattern setting even though it's UI
*/
Swizzled.methods.objUpdate(newState, 'ui', { ...newState.ui, renderer: 'react' })
objUpdate(newState, 'ui', { ...newState.ui, renderer: 'react' })
return newState
}),
clearAll: () => setState(Swizzled.config.initialState),
clearAll: () => setState(config.initialState),
/*
* These are setters for the ephemeral state which is passed down as part of the
* state object, but is not managed in the state backend because it's ephemeral
@ -566,7 +549,7 @@ export function stateUpdateFactory(Swizzled, setState, setEphemeralState) {
if (typeof newState.loading !== 'object') newState.loading = {}
if (typeof conf.color === 'undefined') conf.color = 'info'
newState.loading[id] = {
msg: Swizzled.methods.t('pe:genericLoadingMsg'),
msg: t('pe:genericLoadingMsg'),
...conf,
}
return newState
@ -588,7 +571,7 @@ export function stateUpdateFactory(Swizzled, setState, setEphemeralState) {
if (id && cur.loading?.[id]) return newState
if (typeof newState.loading !== 'object') newState.loading = {}
if (id === false) id = Date.now()
newState.loading[id] = { ...conf, id, fadeTimer: Swizzled.config.notifyTimeout }
newState.loading[id] = { ...conf, id, fadeTimer: config.notifyTimeout }
return newState
}),
notifySuccess: (msg, id = false) =>
@ -606,7 +589,7 @@ export function stateUpdateFactory(Swizzled, setState, setEphemeralState) {
icon: 'success',
color: 'success',
id,
fadeTimer: Swizzled.config.notifyTimeout,
fadeTimer: config.notifyTimeout,
}
return newState
}),
@ -625,7 +608,7 @@ export function stateUpdateFactory(Swizzled, setState, setEphemeralState) {
icon: 'failure',
color: 'error',
id,
fadeTimer: Swizzled.config.notifyTimeout,
fadeTimer: config.notifyTimeout,
}
return newState
}),
@ -646,21 +629,20 @@ export function stateUpdateFactory(Swizzled, setState, setEphemeralState) {
}
/*
* Returns the URL of a cloud-hosted image (cloudflare in this case) based on the ID and Variant
* @param {object} Swizzled - Swizzled code, not used here
*/
export function cloudImageUrl(Swizzled, { id = 'default-avatar', variant = 'public' }) {
export function cloudImageUrl({ id = 'default-avatar', variant = 'public' }) {
/*
* Return something default so that people will actually change it
*/
if (!id || id === 'default-avatar') return Swizzled.config.cloudImageDflt
if (!id || id === 'default-avatar') return config.cloudImageDflt
/*
* If the variant is invalid, set it to the smallest thumbnail so
* people don't load enourmous images by accident
*/
if (!Swizzled.config.cloudImageVariants.includes(variant)) variant = 'sq100'
if (!config.cloudImageVariants.includes(variant)) variant = 'sq100'
return `${Swizzled.config.cloudImageUrl}${id}/${variant}`
return `${config.cloudImageUrl}${id}/${variant}`
}
/**
* This method does nothing. It is used to disable certain methods
@ -682,11 +664,10 @@ export function notEmpty(value) {
*
* Note that this method is variadic
*
* @param {object} methods - An object holding possibly swizzled methods (unused here)
* @param {[string]} namespaces - A string or array of strings of namespaces
* @return {[string]} namespaces - A merged array of all namespaces
*/
export function nsMerge(Swizzled, ...args) {
export function nsMerge(...args) {
const ns = new Set()
for (const arg of args) {
if (typeof arg === 'string') ns.add(arg)
@ -697,24 +678,21 @@ export function nsMerge(Swizzled, ...args) {
return [...ns]
}
/*
* A translation fallback method in case none is passed in
*
* @param {object} Swizzled - Swizzled code, not used here
* @param {string} key - The input
* @return {string} key - The input is returned
*/
export function t(Swizzled, key) {
/*
* Make sure this works when Swizzled is not passed in
*/
if (typeof Swizzled.components === 'undefined') key = Swizzled
export function t(key) {
return Array.isArray(key) ? key[0] : key
}
export function settingsValueIsCustom(Swizzled, val, dflt) {
export function settingsValueIsCustom(val, dflt) {
return typeof val === 'undefined' || val === '__UNSET__' || val === dflt ? false : true
}
export function settingsValueCustomOrDefault(Swizzled, val, dflt) {
export function settingsValueCustomOrDefault(val, dflt) {
return typeof val === 'undefined' || val === '__UNSET__' || val === dflt ? dflt : val
}

View file

@ -0,0 +1,129 @@
/*
* Import of all methods used by the editor
*/
import {
defaultSa,
defaultSamm,
menuCoreSettingsOnlyHandler,
menuCoreSettingsSaboolHandler,
menuCoreSettingsSammHandler,
menuCoreSettingsStructure,
} from './core-settings.mjs'
import {
designOptionType,
findOption,
getOptionStructure,
menuDesignOptionsStructure,
} from './design-options.mjs'
import {
addUndoStep,
cloneObject,
cloudImageUrl,
draft,
flattenFlags,
getCoreSettingUndoStepData,
getDesignOptionUndoStepData,
getUiPreferenceUndoStepData,
getUndoStepData,
initialEditorState,
menuRoundPct,
menuValidateNumericValue,
menuValueWasChanged,
noop,
notEmpty,
nsMerge,
objUpdate,
settingsValueIsCustom,
settingsValueCustomOrDefault,
statePrefixPath,
stateUpdateFactory,
t,
undoableObjUpdate,
} from './editor.mjs'
import {
capitalize,
formatDesignOptionValue,
formatFraction128,
formatImperial,
formatMm,
formatPercentage,
round,
roundMm,
fractionToDecimal,
measurementAsMm,
measurementAsUnits,
shortDate,
parseDistanceInput,
} from './formatting.mjs'
import {
designMeasurements,
hasRequiredMeasurements,
isDegreeMeasurement,
missingMeasurements,
structureMeasurementsAsDesign,
} from './measurements.mjs'
import { menuUiPreferencesStructure } from './ui-preferences.mjs'
/*
* Re-export as named exports
*/
export {
// core-settings.mjs
defaultSa,
defaultSamm,
menuCoreSettingsOnlyHandler,
menuCoreSettingsSaboolHandler,
menuCoreSettingsSammHandler,
menuCoreSettingsStructure,
// design-options.mjs
designOptionType,
findOption,
getOptionStructure,
menuDesignOptionsStructure,
// editor.mjs
addUndoStep,
cloneObject,
cloudImageUrl,
draft,
flattenFlags,
getCoreSettingUndoStepData,
getDesignOptionUndoStepData,
getUiPreferenceUndoStepData,
getUndoStepData,
initialEditorState,
menuRoundPct,
menuValidateNumericValue,
menuValueWasChanged,
noop,
notEmpty,
nsMerge,
objUpdate,
settingsValueIsCustom,
settingsValueCustomOrDefault,
statePrefixPath,
stateUpdateFactory,
t,
undoableObjUpdate,
// formatting.mjs
capitalize,
formatDesignOptionValue,
formatFraction128,
formatImperial,
formatMm,
formatPercentage,
round,
roundMm,
fractionToDecimal,
measurementAsMm,
measurementAsUnits,
shortDate,
parseDistanceInput,
// measurements.mjs
designMeasurements,
hasRequiredMeasurements,
isDegreeMeasurement,
missingMeasurements,
structureMeasurementsAsDesign,
// ui-preferences.mjs
menuUiPreferencesStructure,
}

View file

@ -1,12 +1,15 @@
// Dependencies
import { defaultConfig as config } from '../config/index.mjs'
import { degreeMeasurements } from '@freesewing/config'
/*
* Returns a list of measurements for a design
*
* @param {object} Swizzled - Swizzled code, not used here
* @param {object} Design - The Design object
* @param {object} measies - The current set of measurements
* @return {object} measurements - Object holding measurements that are relevant for this design
*/
export function designMeasurements(Swizzled, Design, measies = {}) {
export function designMeasurements(Design, measies = {}) {
const measurements = {}
for (const m of Design.patternConfig?.measurements || []) measurements[m] = measies[m]
for (const m of Design.patternConfig?.optionalMeasurements || []) measurements[m] = measies[m]
@ -16,19 +19,18 @@ export function designMeasurements(Swizzled, Design, measies = {}) {
/**
* Helper method to determine whether all required measurements for a design are present
*
* @param {object} Swizzled - Swizzled code, including methods
* @param {object} Design - The FreeSewing design (or a plain object holding measurements)
* @param {object} measurements - An object holding the user's measurements
* @return {array} result - An array where the first element is true when we
* have all measurements, and false if not. The second element is a list of
* missing measurements.
*/
export function hasRequiredMeasurements(Swizzled, Design, measurements = {}) {
export function hasRequiredMeasurements(Design, measurements = {}) {
/*
* If design is just a plain object holding measurements, we restructure it as a Design
* AS it happens, this method is smart enough to test for this, so we call it always
*/
Design = Swizzled.methods.structureMeasurementsAsDesign(Design)
Design = structureMeasurementsAsDesign(Design)
/*
* Walk required measuremnets, and keep track of what's missing
@ -46,28 +48,27 @@ export function hasRequiredMeasurements(Swizzled, Design, measurements = {}) {
/**
* Helper method to determine whether a measurement uses degrees
*
* @param {object} Swizzled - Swizzled code, not used here
* @param {string} name - The name of the measurement
* @return {bool} isDegree - True if the measurment is a degree measurement
*/
export function isDegreeMeasurement(Swizzled, name) {
return Swizzled.config.degreeMeasurements.indexOf(name) !== -1
export function isDegreeMeasurement(name) {
return degreeMeasurements.indexOf(name) !== -1
}
/*
* Helper method to check whether measururements are missing
*
* Note that this does not actually check the settings against
* the chose design, but rather relies on the missing measurements
* the chosen design, but rather relies on the missing measurements
* being set in state. That's because checking is more expensive,
* so we do it only once in the non-Swizzled ViewWrapper components
* so we do it only once.
*
* @param {object} Swizzled - Object holding Swizzled code
* @param {object} state - The Editor state
* @param {object} config - The Editor configuration
* @return {bool} missing - True if there are missing measurments, false if not
*/
export function missingMeasurements(Swizzled, state) {
export function missingMeasurements(state, config) {
return (
!Swizzled.config.measurementsFreeViews.includes(state.view) &&
!config.measurementsFreeViews.includes(state.view) &&
state._.missingMeasurements &&
state._.missingMeasurements.length > 0
)
@ -75,10 +76,9 @@ export function missingMeasurements(Swizzled, state) {
/*
* This takes a POJO of measurements, and turns it into a structure that matches a design object
*
* @param {object} Swizzled - Swizzled code, not used here
* @param {object} measurements - The POJO of measurments
* @return {object} design - The measurements structured as a design object
*/
export function structureMeasurementsAsDesign(Swizzled, measurements) {
export function structureMeasurementsAsDesign(measurements) {
return measurements.patternConfig ? measurements : { patternConfig: { measurements } }
}

View file

@ -1,28 +0,0 @@
/**
* The design view is loaded if and only if not design is passed to the editor
*
* @param {Object} props - All the props
* @param {Object} designs - Object holding all designs
* @param {object} props.Swizzled - An object holding swizzled code
* @param {Object} update - ViewWrapper state update object
*/
export const DesignsView = ({ designs = {}, Swizzled, update }) => (
<div className="text-center mt-8 mb-24">
<h2>{Swizzled.methods.t('pe:chooseADesign')}</h2>
<ul className="flex flex-row flex-wrap gap-2 items-center justify-center max-w-2xl px-8 mx-auto">
{Object.keys(designs).map((name) => (
<li key={name}>
<button
className={`btn btn-primary btn-outline btn-sm capitalize font-bold `}
onClick={() => {
update.design(name)
update.view('draft')
}}
>
{name}
</button>
</li>
))}
</ul>
</div>
)

View file

@ -1,6 +0,0 @@
/**
* A temporary loader 'one moment please' style
* Just a spinner in this case, but could also be branded.
*/
export const TemporaryLoader = ({ Swizzled = false }) =>
Swizzled ? <Swizzled.components.Spinner /> : <div className="text-center m-auto">...</div>

View file

@ -1,120 +0,0 @@
import { useMemo, useState, useEffect } from 'react'
import useLocalStorageState from 'use-local-storage-state'
import useSessionStorageState from 'use-session-storage-state'
import { useQueryState, createParser } from 'nuqs'
/**
* react
* This holds the editor state, using React state.
* It also provides helper methods to manipulate state.
*
* @params {object} init - Initial pattern settings
* @return {array} return - And array with get, set, and update methods
*/
export const useReactEditorState = (Swizzled, init = {}, setEphemeralState) => {
const [state, setState] = useState(init)
const update = useMemo(
() => Swizzled.methods.stateUpdateFactory(setState, setEphemeralState),
[setState]
)
return [state, setState, update]
}
/**
* storage
* This holds the editor state, using local storage.
* It also provides helper methods to manipulate state.
*
* @params {object} init - Initial pattern settings
* @return {array} return - And array with get, set, and update methods
*/
export const useStorageEditorState = (Swizzled, init = {}, setEphemeralState) => {
const [state, setState] = useLocalStorageState('fs-editor', { defaultValue: init })
const update = useMemo(
() => Swizzled.methods.stateUpdateFactory(setState, setEphemeralState),
[setState]
)
return [state, setState, update]
}
/**
* session
* This holds the editor state, using session storage.
* It also provides helper methods to manipulate state.
*
* @params {object} init - Initial pattern settings
* @return {array} return - And array with get, set, and update methods
*/
export const useSessionEditorState = (Swizzled, init = {}, setEphemeralState) => {
const [state, setState] = useSessionStorageState('fs-editor', { defaultValue: init })
const update = useMemo(
() => Swizzled.methods.stateUpdateFactory(setState, setEphemeralState),
[setState]
)
return [state, setState, update]
}
/**
* url
* This holds the editor state, using session storage.
* It also provides helper methods to manipulate state.
*
* @params {object} init - Initial pattern settings
* @return {array} return - And array with get, set, and update methods
*/
export const useUrlEditorState = (Swizzled, init = {}, setEphemeralState) => {
const [state, setState] = useQueryState('s', pojoParser)
const update = useMemo(
() => Swizzled.methods.stateUpdateFactory(setState, setEphemeralState),
[setState]
)
/*
* Set the initial state
*/
useEffect(() => {
// Handle state on a hard reload or cold start
if (typeof URLSearchParams !== 'undefined') {
let urlState = false
try {
const params = new URLSearchParams(document.location.search)
const s = params.get('s')
if (typeof s === 'string' && s.length > 0) urlState = JSON.parse(s)
if (urlState) setState(urlState)
else setState(init)
} catch (err) {
setState(init)
}
}
}, [])
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 = createParser({
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
},
})

View file

@ -1,240 +0,0 @@
/*************************************************************************
* *
* FreeSewing's pattern editor allows swizzling methods *
* *
* To 'swizzle' means to replace the default implementation of a *
* method with a custom one. It allows one to customize *
* the pattern editor. *
* *
* This file holds the 'swizzleMethods' method that will return *
* the various methods that can be swizzled, or their default *
* implementation. *
* *
* To use a custom version, simply pas it as a prop into the editor *
* under the 'methods' key. So to pass a custom 't' method (used for *
* translation(, you do: *
* *
* <PatternEditor methods={{ t: myCustomTranslationMethod }} /> *
* *
*************************************************************************/
/*
* Import of methods that can be swizzled
*/
import {
defaultSa,
defaultSamm,
menuCoreSettingsOnlyHandler,
menuCoreSettingsSaboolHandler,
menuCoreSettingsSammHandler,
menuCoreSettingsStructure,
} from './core-settings.mjs'
import {
designOptionType,
findOption,
getOptionStructure,
menuDesignOptionsStructure,
} from './design-options.mjs'
import {
addUndoStep,
cloneObject,
cloudImageUrl,
draft,
flattenFlags,
getCoreSettingUndoStepData,
getDesignOptionUndoStepData,
getUiPreferenceUndoStepData,
getUndoStepData,
initialEditorState,
menuRoundPct,
menuValidateNumericValue,
menuValueWasChanged,
noop,
notEmpty,
nsMerge,
objUpdate,
settingsValueIsCustom,
settingsValueCustomOrDefault,
statePrefixPath,
stateUpdateFactory,
t,
undoableObjUpdate,
} from './editor.mjs'
import {
capitalize,
formatDesignOptionValue,
formatFraction128,
formatImperial,
formatMm,
formatPercentage,
round,
roundMm,
fractionToDecimal,
measurementAsMm,
measurementAsUnits,
shortDate,
parseDistanceInput,
} from './formatting.mjs'
import {
designMeasurements,
hasRequiredMeasurements,
isDegreeMeasurement,
missingMeasurements,
structureMeasurementsAsDesign,
} from './measurements.mjs'
import { menuUiPreferencesStructure } from './ui-preferences.mjs'
/**
* This object holds all methods that can be swizzled
*/
const defaultMethods = {
// core-settings.mjs
defaultSa,
defaultSamm,
menuCoreSettingsOnlyHandler,
menuCoreSettingsSaboolHandler,
menuCoreSettingsSammHandler,
menuCoreSettingsStructure,
// design-options.mjs
designOptionType,
findOption,
getOptionStructure,
menuDesignOptionsStructure,
// editor.mjs
addUndoStep,
cloneObject,
cloudImageUrl,
draft,
flattenFlags,
getCoreSettingUndoStepData,
getDesignOptionUndoStepData,
getUiPreferenceUndoStepData,
getUndoStepData,
initialEditorState,
menuRoundPct,
menuValidateNumericValue,
menuValueWasChanged,
noop,
notEmpty,
nsMerge,
objUpdate,
settingsValueIsCustom,
settingsValueCustomOrDefault,
statePrefixPath,
stateUpdateFactory,
t,
undoableObjUpdate,
// formatting.mjs
capitalize,
formatDesignOptionValue,
formatFraction128,
formatImperial,
formatMm,
formatPercentage,
round,
roundMm,
fractionToDecimal,
measurementAsMm,
measurementAsUnits,
shortDate,
parseDistanceInput,
// measurements.mjs
designMeasurements,
hasRequiredMeasurements,
isDegreeMeasurement,
missingMeasurements,
structureMeasurementsAsDesign,
// ui-preferences.mjs
menuUiPreferencesStructure,
}
/*
* This method returns methods that can be swizzled
* So either the passed-in methods, or the default ones
*/
const swizzleMethods = (methods, Swizzled) => {
/*
* We need to pass down the resulting methods, swizzled or not
* because some methods rely on other (possibly swizzled) methods.
* So we put this in this object so we can pass that down
*/
const all = {}
for (const [name, method] of Object.entries(defaultMethods)) {
if (typeof method !== 'function')
console.warn(`${name} is not defined as default method in swizzleMethods`)
all[name] = methods[name]
? (...params) => methods[name](all, ...params)
: (...params) => method(Swizzled, ...params)
}
/*
* Return all methods
*/
return all
}
/*
* Named exports
*/
export {
swizzleMethods,
// Re-export all methods for specific imports
// core-settings.mjs
defaultSa,
defaultSamm,
menuCoreSettingsOnlyHandler,
menuCoreSettingsSaboolHandler,
menuCoreSettingsSammHandler,
menuCoreSettingsStructure,
// design-options.mjs
designOptionType,
findOption,
getOptionStructure,
menuDesignOptionsStructure,
// editor.mjs
addUndoStep,
cloneObject,
cloudImageUrl,
draft,
flattenFlags,
getCoreSettingUndoStepData,
getDesignOptionUndoStepData,
getUiPreferenceUndoStepData,
getUndoStepData,
initialEditorState,
menuRoundPct,
menuValidateNumericValue,
menuValueWasChanged,
noop,
notEmpty,
nsMerge,
objUpdate,
settingsValueIsCustom,
settingsValueCustomOrDefault,
statePrefixPath,
stateUpdateFactory,
t,
undoableObjUpdate,
// formatting.mjs
capitalize,
formatDesignOptionValue,
formatFraction128,
formatImperial,
formatMm,
formatPercentage,
round,
roundMm,
fractionToDecimal,
measurementAsMm,
measurementAsUnits,
shortDate,
parseDistanceInput,
// measurements.mjs
designMeasurements,
hasRequiredMeasurements,
isDegreeMeasurement,
missingMeasurements,
structureMeasurementsAsDesign,
// ui-preferences.mjs
menuUiPreferencesStructure,
}

View file

@ -489,7 +489,7 @@ export const MarkdownInput = ({
</FormControl>
)
export const MeasieInput = ({
export const MeasurementInput = ({
imperial, // True for imperial, False for metric
m, // The measurement name
original, // The original value

View file

@ -81,13 +81,12 @@
"highlight.js": "^11.11.0",
"html-react-parser": "^5.0.7",
"luxon": "^3.5.0",
"nuqs": "^1.17.6",
"nuqs": "^2.3.0",
"react-markdown": "^9.0.1",
"tlds": "^1.255.0",
"use-local-storage-state": "19.1.0",
"use-session-storage-state": "^19.0.0"
},
"devDependencies": {},
"files": [
"components/**",
"hooks/**",

View file

@ -1,44 +1,44 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketchjs="https://sketch.io/dtd/" sketchjs:metadata="eyJuYW1lIjoiRHJhd2luZy5za2V0Y2hwYWQiLCJzdXJmYWNlIjp7ImlzUGFpbnQiOnRydWUsIm1ldGhvZCI6ImZpbGwiLCJibGVuZCI6Im5vcm1hbCIsImVuYWJsZWQiOnRydWUsIm9wYWNpdHkiOjEsInR5cGUiOiJwYXR0ZXJuIiwicGF0dGVybiI6eyJ0eXBlIjoicGF0dGVybiIsInJlZmxlY3QiOiJuby1yZWZsZWN0IiwicmVwZWF0IjoicmVwZWF0Iiwic21vb3RoaW5nIjpmYWxzZSwic3JjIjoidHJhbnNwYXJlbnRMaWdodCIsInN4IjoxLCJzeSI6MSwieDAiOjAuNSwieDEiOjEsInkwIjowLjUsInkxIjoxfSwiaXNGaWxsIjp0cnVlfSwiY2xpcFBhdGgiOnsiZW5hYmxlZCI6dHJ1ZSwic3R5bGUiOnsic3Ryb2tlU3R5bGUiOiJibGFjayIsImxpbmVXaWR0aCI6MX19LCJkZXNjcmlwdGlvbiI6Ik1hZGUgd2l0aCBTa2V0Y2hwYWQiLCJtZXRhZGF0YSI6e30sImV4cG9ydERQSSI6NzIsImV4cG9ydEZvcm1hdCI6InBuZyIsImV4cG9ydFF1YWxpdHkiOjAuOTUsInVuaXRzIjoicHgiLCJ3aWR0aCI6MTkyMCwiaGVpZ2h0Ijo5NjksInBhZ2VzIjpbeyJ3aWR0aCI6MTkyMCwiaGVpZ2h0Ijo5Njl9XSwidXVpZCI6ImI4YTA3OWY0LWZmNTUtNGEzMy1hYjA4LWU2NTdhNjFmMmQ3NyJ9" width="1920" height="969" viewBox="0 0 1920 969">
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" sketchjs:tool="polyline" points="0,31.88 123.13,0" transform="matrix(1,0,0,1,328.6225,542.9975)"/>
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" sketchjs:tool="polyline" points="0,0 123.75,35" transform="matrix(1,0,0,1,614.875,541.125)"/>
<path style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" sketchjs:tool="path" d="M16.25 0 C18.96 42.08 42.5 184.38 0 242.5 " transform="matrix(1,0,0,1,312.3747126,576.125)"/>
<path style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" sketchjs:tool="path" d="M7.91 0 C-1.67 32.5 -8.96 222.5 26.04 251.25 " transform="matrix(1,0,0,1,731.9613741,576.125)"/>
<path style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" sketchjs:tool="path" d="M163.75 0 C174.16 15.62 30 40 0 1.87 " transform="matrix(1,0,0,1,451.1230028,540.5024367)"/>
<path style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" sketchjs:tool="path" d="M0 45.62 C-0.21 24.99 120.63 -22.51 152.5 12.49 " transform="matrix(1,0,0,1,740.4997288,530.5026223)"/>
<path style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" sketchjs:tool="path" d="M0 45.62 C-0.21 24.99 120.63 -22.51 152.5 12.49 " transform="matrix(-1.0131220593052985,0,0,1.0131220593052983,326.29088688138944,530.0440812186106)"/>
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" sketchjs:tool="polyline" points="0,107.5 25,0 197.5,26.25" transform="matrix(1,0,0,1,708.625,399.25)"/>
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" sketchjs:tool="polyline" points="0,107.5 25,0 197.5,26.25" transform="matrix(-0.9827784156142365,0,0,0.9827784156142364,365.90944316877153,400.51076353122846)"/>
<polyline style="fill: none; stroke: #ac49ff; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" sketchjs:tool="polyline" points="0,0 0.01,0.01" transform="matrix(1,0,0,1,362.375,488.625)"/>
<path style="fill: none; stroke: #4a87ff; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" sketchjs:tool="path" d="M0 0 L0.01 0.01 " transform="matrix(1,0,0,1,362.375,489.25)"/>
<path style="fill: #4a87ff; stroke: #4a87ff; mix-blend-mode: source-over; paint-order: stroke fill markers; fill-opacity: 1; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" sketchjs:tool="path" d="M0 0 L0.01 0.01 " transform="matrix(1,0,0,1,362.5,489.9985821809644)"/>
<path style="fill: #4a87ff; stroke: #4a87ff; mix-blend-mode: source-over; paint-order: stroke fill markers; fill-opacity: 1; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" sketchjs:tool="path" d="M0 0 L4 16.5 C6.83 19.5 12 25.5 12.5 25.5 13 25.5 33.5 37 33.5 37 33.5 37 51.5 45 51.5 45 51.5 45 78 52.5 78 52.5 78 52.5 94 56.5 94 56.5 94 56.5 114.5 67 114.5 67 114.5 67 146.5 71.5 147 71.5 147.5 71.5 173.5 72.5 173.5 72.5 173.5 72.5 209 69 209 69 209 69 224.5 64.5 224.5 64.5 224.5 64.5 250 58.5 250 58.5 250 58.5 273 48 273 48 273 48 294 40.5 294 40.5 294 40.5 318.5 28.5 318.5 28.5 318.5 28.5 339 22 339 22 339 22 345 16 345 16 345 16 349.5 1 349.5 1 349.5 1 320 10 320 10 320 10 303 15 303 15 303 15 275.5 22.5 275.5 22.5 275.5 22.5 254 30.5 254 30.5 254 30.5 226.5 38 226.5 38 226.5 38 196.5 42 196.5 42 196.5 42 170 46.5 170 46.5 170 46.5 141.5 45 141.5 45 141.5 45 111 40 111 40 111 40 86 32.5 86 32.5 86 32.5 60.5 25.5 60.5 25.5 60.5 25.5 31.5 15 31.5 15 31.5 15 13 5.5 13 5.5 13 5.5 4 0 0 0 z" transform="matrix(1,0,0,1,363,491)"/>
<path style="fill: #4a87ff; stroke: #4a87ff; mix-blend-mode: source-over; paint-order: stroke fill markers; fill-opacity: 1; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" sketchjs:tool="path" d="M0 0 L0.01 0.01 " transform="matrix(1,0,0,1,444.5,534)"/>
<path style="fill: #ff3737; stroke: #ff6c20; mix-blend-mode: source-over; paint-order: stroke fill markers; fill-opacity: 1; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" sketchjs:tool="path" d="M364.5 81 L378 20 357 1 334 6 C330 7.83 315.5 10 315.5 10.5 315.5 11 299 11 299 11 299 11 272 12 272 12 272 12 247 12 247 12 247 12 216.5 12 216.5 12 216.5 12 170.5 11 170.5 11 170.5 11 140 11 140 11 140 11 116 11 116 11 116 11 93.5 10.5 93.5 10.5 93.5 10.5 80.5 10 80.5 10 80.5 10 68.5 9 68.5 9 68.5 9 48.5 5 48.5 5 48.5 5 29 0 29 0 29 0 0 16.5 0 16.5 0 16.5 14 78 14 78 14 78 33 87.5 33 87.5 33 87.5 61 100.5 61.5 100.5 62 100.5 82.5 107.5 82.5 107.5 82.5 107.5 109.5 115 109.5 115 109.5 115 143 122.5 143 122.5 143 122.5 174.5 126 174.5 126 174.5 126 197 127 197 127 197 127 228 121 228 121 228 121 267.5 112 267.5 112 267.5 112 306.5 99 306.5 99 306.5 99 347 88 347 88 347 88 360 85.5 364.5 81 z" transform="matrix(1,0,0,1,348,410)"/>
<g style="mix-blend-mode: source-over;" sketchjs:tool="clipart" transform="matrix(-0.2487254723560043,0,0,0.24872547235600428,750.06,495.5275986339937)">
<path d="M95.36,19.84c11-5.17,20.18-5.08,27.52-.1l-65,37.45a12.92,12.92,0,0,1-7.22,3c-2.42.13-3.29-.56-5.39-.95a7.22,7.22,0,0,0-5.4,1l-1.71,1.56c-1.53,1.39-3.37,2.6-3.07,5,.2,1.58,1.55,2.91,3.69,4.06A15.4,15.4,0,0,1,42.56,76,17.92,17.92,0,0,1,44,82.5c-.39,7.55-3.92,14.07-12.41,19-9.68,6-17.86,5.27-25.18.86C2,98.93,0,94.82,0,90.16,0,83,3.41,77.26,8.73,72.78A73.68,73.68,0,0,1,20.66,65.1l14-10.95.25.85L95.36,19.84Zm0,66.1c11,5.17,20.18,5.08,27.52.1L69.16,55.09,55.53,62.78,95.36,85.94ZM42.29,46.56a7.4,7.4,0,0,1-2.41-1L38.17,44c-1.53-1.39-3.37-2.6-3.07-5,.2-1.59,1.55-2.92,3.69-4.06a15.44,15.44,0,0,0,3.77-5.2A17.86,17.86,0,0,0,44,23.29c-.39-7.55-3.92-14.08-12.41-19C21.93-1.63,13.75-.95,6.43,3.46,2,6.85,0,11,0,15.63,0,22.74,3.41,28.53,8.73,33a73,73,0,0,0,11.93,7.68L34.24,51.31l.6-.35.06-.18.12.08,7.27-4.3ZM32.85,30.43c3.91-2.55,4.31-6.85,2.87-10.95C34.57,16.19,32.1,13.35,28.4,11A21.36,21.36,0,0,0,15.88,7.77c-8.44.77-10,8.29-6.35,14.86a17.07,17.07,0,0,0,7.37,6.92A24.19,24.19,0,0,0,24.44,32a12.16,12.16,0,0,0,8.41-1.56ZM55.08,53.11a3.58,3.58,0,1,1-3.57-3.58,3.57,3.57,0,0,1,3.57,3.58ZM32.85,75.36c3.91,2.55,4.31,6.84,2.87,10.94-1.15,3.3-3.62,6.13-7.32,8.53A21.44,21.44,0,0,1,15.88,98c-8.44-.78-10-8.3-6.35-14.87a17,17,0,0,1,7.37-6.91,23.94,23.94,0,0,1,7.54-2.45,12.16,12.16,0,0,1,8.41,1.57Z" sketchjs:uid="1" style="fill-rule: evenodd;"/>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1920" height="969" viewBox="0 0 1920 969">
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" points="0,31.88 123.13,0" transform="matrix(1,0,0,1,328.6225,542.9975)"/>
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" points="0,0 123.75,35" transform="matrix(1,0,0,1,614.875,541.125)"/>
<path style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" d="M16.25 0 C18.96 42.08 42.5 184.38 0 242.5 " transform="matrix(1,0,0,1,312.3747126,576.125)"/>
<path style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" d="M7.91 0 C-1.67 32.5 -8.96 222.5 26.04 251.25 " transform="matrix(1,0,0,1,731.9613741,576.125)"/>
<path style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" d="M163.75 0 C174.16 15.62 30 40 0 1.87 " transform="matrix(1,0,0,1,451.1230028,540.5024367)"/>
<path style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" d="M0 45.62 C-0.21 24.99 120.63 -22.51 152.5 12.49 " transform="matrix(1,0,0,1,740.4997288,530.5026223)"/>
<path style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" d="M0 45.62 C-0.21 24.99 120.63 -22.51 152.5 12.49 " transform="matrix(-1.0131220593052985,0,0,1.0131220593052983,326.29088688138944,530.0440812186106)"/>
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" points="0,107.5 25,0 197.5,26.25" transform="matrix(1,0,0,1,708.625,399.25)"/>
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" points="0,107.5 25,0 197.5,26.25" transform="matrix(-0.9827784156142365,0,0,0.9827784156142364,365.90944316877153,400.51076353122846)"/>
<polyline style="fill: none; stroke: #ac49ff; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" points="0,0 0.01,0.01" transform="matrix(1,0,0,1,362.375,488.625)"/>
<path style="fill: none; stroke: #4a87ff; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" d="M0 0 L0.01 0.01 " transform="matrix(1,0,0,1,362.375,489.25)"/>
<path style="fill: #4a87ff; stroke: #4a87ff; mix-blend-mode: source-over; paint-order: stroke fill markers; fill-opacity: 1; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" d="M0 0 L0.01 0.01 " transform="matrix(1,0,0,1,362.5,489.9985821809644)"/>
<path style="fill: #4a87ff; stroke: #4a87ff; mix-blend-mode: source-over; paint-order: stroke fill markers; fill-opacity: 1; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" d="M0 0 L4 16.5 C6.83 19.5 12 25.5 12.5 25.5 13 25.5 33.5 37 33.5 37 33.5 37 51.5 45 51.5 45 51.5 45 78 52.5 78 52.5 78 52.5 94 56.5 94 56.5 94 56.5 114.5 67 114.5 67 114.5 67 146.5 71.5 147 71.5 147.5 71.5 173.5 72.5 173.5 72.5 173.5 72.5 209 69 209 69 209 69 224.5 64.5 224.5 64.5 224.5 64.5 250 58.5 250 58.5 250 58.5 273 48 273 48 273 48 294 40.5 294 40.5 294 40.5 318.5 28.5 318.5 28.5 318.5 28.5 339 22 339 22 339 22 345 16 345 16 345 16 349.5 1 349.5 1 349.5 1 320 10 320 10 320 10 303 15 303 15 303 15 275.5 22.5 275.5 22.5 275.5 22.5 254 30.5 254 30.5 254 30.5 226.5 38 226.5 38 226.5 38 196.5 42 196.5 42 196.5 42 170 46.5 170 46.5 170 46.5 141.5 45 141.5 45 141.5 45 111 40 111 40 111 40 86 32.5 86 32.5 86 32.5 60.5 25.5 60.5 25.5 60.5 25.5 31.5 15 31.5 15 31.5 15 13 5.5 13 5.5 13 5.5 4 0 0 0 z" transform="matrix(1,0,0,1,363,491)"/>
<path style="fill: #4a87ff; stroke: #4a87ff; mix-blend-mode: source-over; paint-order: stroke fill markers; fill-opacity: 1; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" d="M0 0 L0.01 0.01 " transform="matrix(1,0,0,1,444.5,534)"/>
<path style="fill: #ff3737; stroke: #ff6c20; mix-blend-mode: source-over; paint-order: stroke fill markers; fill-opacity: 1; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" d="M364.5 81 L378 20 357 1 334 6 C330 7.83 315.5 10 315.5 10.5 315.5 11 299 11 299 11 299 11 272 12 272 12 272 12 247 12 247 12 247 12 216.5 12 216.5 12 216.5 12 170.5 11 170.5 11 170.5 11 140 11 140 11 140 11 116 11 116 11 116 11 93.5 10.5 93.5 10.5 93.5 10.5 80.5 10 80.5 10 80.5 10 68.5 9 68.5 9 68.5 9 48.5 5 48.5 5 48.5 5 29 0 29 0 29 0 0 16.5 0 16.5 0 16.5 14 78 14 78 14 78 33 87.5 33 87.5 33 87.5 61 100.5 61.5 100.5 62 100.5 82.5 107.5 82.5 107.5 82.5 107.5 109.5 115 109.5 115 109.5 115 143 122.5 143 122.5 143 122.5 174.5 126 174.5 126 174.5 126 197 127 197 127 197 127 228 121 228 121 228 121 267.5 112 267.5 112 267.5 112 306.5 99 306.5 99 306.5 99 347 88 347 88 347 88 360 85.5 364.5 81 z" transform="matrix(1,0,0,1,348,410)"/>
<g style="mix-blend-mode: source-over;" transform="matrix(-0.2487254723560043,0,0,0.24872547235600428,750.06,495.5275986339937)">
<path d="M95.36,19.84c11-5.17,20.18-5.08,27.52-.1l-65,37.45a12.92,12.92,0,0,1-7.22,3c-2.42.13-3.29-.56-5.39-.95a7.22,7.22,0,0,0-5.4,1l-1.71,1.56c-1.53,1.39-3.37,2.6-3.07,5,.2,1.58,1.55,2.91,3.69,4.06A15.4,15.4,0,0,1,42.56,76,17.92,17.92,0,0,1,44,82.5c-.39,7.55-3.92,14.07-12.41,19-9.68,6-17.86,5.27-25.18.86C2,98.93,0,94.82,0,90.16,0,83,3.41,77.26,8.73,72.78A73.68,73.68,0,0,1,20.66,65.1l14-10.95.25.85L95.36,19.84Zm0,66.1c11,5.17,20.18,5.08,27.52.1L69.16,55.09,55.53,62.78,95.36,85.94ZM42.29,46.56a7.4,7.4,0,0,1-2.41-1L38.17,44c-1.53-1.39-3.37-2.6-3.07-5,.2-1.59,1.55-2.92,3.69-4.06a15.44,15.44,0,0,0,3.77-5.2A17.86,17.86,0,0,0,44,23.29c-.39-7.55-3.92-14.08-12.41-19C21.93-1.63,13.75-.95,6.43,3.46,2,6.85,0,11,0,15.63,0,22.74,3.41,28.53,8.73,33a73,73,0,0,0,11.93,7.68L34.24,51.31l.6-.35.06-.18.12.08,7.27-4.3ZM32.85,30.43c3.91-2.55,4.31-6.85,2.87-10.95C34.57,16.19,32.1,13.35,28.4,11A21.36,21.36,0,0,0,15.88,7.77c-8.44.77-10,8.29-6.35,14.86a17.07,17.07,0,0,0,7.37,6.92A24.19,24.19,0,0,0,24.44,32a12.16,12.16,0,0,0,8.41-1.56ZM55.08,53.11a3.58,3.58,0,1,1-3.57-3.58,3.57,3.57,0,0,1,3.57,3.58ZM32.85,75.36c3.91,2.55,4.31,6.84,2.87,10.94-1.15,3.3-3.62,6.13-7.32,8.53A21.44,21.44,0,0,1,15.88,98c-8.44-.78-10-8.3-6.35-14.87a17,17,0,0,1,7.37-6.91,23.94,23.94,0,0,1,7.54-2.45,12.16,12.16,0,0,1,8.41,1.57Z" style="fill-rule: evenodd;"/>
</g>
<g style="mix-blend-mode: source-over;" sketchjs:tool="clipart" transform="matrix(0.24496325183424408,0,0,0.24496325183424406,323.39552957150215,498.4257691130884)">
<path d="M95.36,19.84c11-5.17,20.18-5.08,27.52-.1l-65,37.45a12.92,12.92,0,0,1-7.22,3c-2.42.13-3.29-.56-5.39-.95a7.22,7.22,0,0,0-5.4,1l-1.71,1.56c-1.53,1.39-3.37,2.6-3.07,5,.2,1.58,1.55,2.91,3.69,4.06A15.4,15.4,0,0,1,42.56,76,17.92,17.92,0,0,1,44,82.5c-.39,7.55-3.92,14.07-12.41,19-9.68,6-17.86,5.27-25.18.86C2,98.93,0,94.82,0,90.16,0,83,3.41,77.26,8.73,72.78A73.68,73.68,0,0,1,20.66,65.1l14-10.95.25.85L95.36,19.84Zm0,66.1c11,5.17,20.18,5.08,27.52.1L69.16,55.09,55.53,62.78,95.36,85.94ZM42.29,46.56a7.4,7.4,0,0,1-2.41-1L38.17,44c-1.53-1.39-3.37-2.6-3.07-5,.2-1.59,1.55-2.92,3.69-4.06a15.44,15.44,0,0,0,3.77-5.2A17.86,17.86,0,0,0,44,23.29c-.39-7.55-3.92-14.08-12.41-19C21.93-1.63,13.75-.95,6.43,3.46,2,6.85,0,11,0,15.63,0,22.74,3.41,28.53,8.73,33a73,73,0,0,0,11.93,7.68L34.24,51.31l.6-.35.06-.18.12.08,7.27-4.3ZM32.85,30.43c3.91-2.55,4.31-6.85,2.87-10.95C34.57,16.19,32.1,13.35,28.4,11A21.36,21.36,0,0,0,15.88,7.77c-8.44.77-10,8.29-6.35,14.86a17.07,17.07,0,0,0,7.37,6.92A24.19,24.19,0,0,0,24.44,32a12.16,12.16,0,0,0,8.41-1.56ZM55.08,53.11a3.58,3.58,0,1,1-3.57-3.58,3.57,3.57,0,0,1,3.57,3.58ZM32.85,75.36c3.91,2.55,4.31,6.84,2.87,10.94-1.15,3.3-3.62,6.13-7.32,8.53A21.44,21.44,0,0,1,15.88,98c-8.44-.78-10-8.3-6.35-14.87a17,17,0,0,1,7.37-6.91,23.94,23.94,0,0,1,7.54-2.45,12.16,12.16,0,0,1,8.41,1.57Z" sketchjs:uid="1" style="fill-rule: evenodd;"/>
<g style="mix-blend-mode: source-over;" transform="matrix(0.24496325183424408,0,0,0.24496325183424406,323.39552957150215,498.4257691130884)">
<path d="M95.36,19.84c11-5.17,20.18-5.08,27.52-.1l-65,37.45a12.92,12.92,0,0,1-7.22,3c-2.42.13-3.29-.56-5.39-.95a7.22,7.22,0,0,0-5.4,1l-1.71,1.56c-1.53,1.39-3.37,2.6-3.07,5,.2,1.58,1.55,2.91,3.69,4.06A15.4,15.4,0,0,1,42.56,76,17.92,17.92,0,0,1,44,82.5c-.39,7.55-3.92,14.07-12.41,19-9.68,6-17.86,5.27-25.18.86C2,98.93,0,94.82,0,90.16,0,83,3.41,77.26,8.73,72.78A73.68,73.68,0,0,1,20.66,65.1l14-10.95.25.85L95.36,19.84Zm0,66.1c11,5.17,20.18,5.08,27.52.1L69.16,55.09,55.53,62.78,95.36,85.94ZM42.29,46.56a7.4,7.4,0,0,1-2.41-1L38.17,44c-1.53-1.39-3.37-2.6-3.07-5,.2-1.59,1.55-2.92,3.69-4.06a15.44,15.44,0,0,0,3.77-5.2A17.86,17.86,0,0,0,44,23.29c-.39-7.55-3.92-14.08-12.41-19C21.93-1.63,13.75-.95,6.43,3.46,2,6.85,0,11,0,15.63,0,22.74,3.41,28.53,8.73,33a73,73,0,0,0,11.93,7.68L34.24,51.31l.6-.35.06-.18.12.08,7.27-4.3ZM32.85,30.43c3.91-2.55,4.31-6.85,2.87-10.95C34.57,16.19,32.1,13.35,28.4,11A21.36,21.36,0,0,0,15.88,7.77c-8.44.77-10,8.29-6.35,14.86a17.07,17.07,0,0,0,7.37,6.92A24.19,24.19,0,0,0,24.44,32a12.16,12.16,0,0,0,8.41-1.56ZM55.08,53.11a3.58,3.58,0,1,1-3.57-3.58,3.57,3.57,0,0,1,3.57,3.58ZM32.85,75.36c3.91,2.55,4.31,6.84,2.87,10.94-1.15,3.3-3.62,6.13-7.32,8.53A21.44,21.44,0,0,1,15.88,98c-8.44-.78-10-8.3-6.35-14.87a17,17,0,0,1,7.37-6.91,23.94,23.94,0,0,1,7.54-2.45,12.16,12.16,0,0,1,8.41,1.57Z" style="fill-rule: evenodd;"/>
</g>
<g style="mix-blend-mode: source-over;" sketchjs:tool="clipart" transform="matrix(0.08803663033961473,-0.22859690826354873,0.2285969082635487,0.08803663033961472,485.9403302518847,614.7749235660337)">
<path d="M95.36,19.84c11-5.17,20.18-5.08,27.52-.1l-65,37.45a12.92,12.92,0,0,1-7.22,3c-2.42.13-3.29-.56-5.39-.95a7.22,7.22,0,0,0-5.4,1l-1.71,1.56c-1.53,1.39-3.37,2.6-3.07,5,.2,1.58,1.55,2.91,3.69,4.06A15.4,15.4,0,0,1,42.56,76,17.92,17.92,0,0,1,44,82.5c-.39,7.55-3.92,14.07-12.41,19-9.68,6-17.86,5.27-25.18.86C2,98.93,0,94.82,0,90.16,0,83,3.41,77.26,8.73,72.78A73.68,73.68,0,0,1,20.66,65.1l14-10.95.25.85L95.36,19.84Zm0,66.1c11,5.17,20.18,5.08,27.52.1L69.16,55.09,55.53,62.78,95.36,85.94ZM42.29,46.56a7.4,7.4,0,0,1-2.41-1L38.17,44c-1.53-1.39-3.37-2.6-3.07-5,.2-1.59,1.55-2.92,3.69-4.06a15.44,15.44,0,0,0,3.77-5.2A17.86,17.86,0,0,0,44,23.29c-.39-7.55-3.92-14.08-12.41-19C21.93-1.63,13.75-.95,6.43,3.46,2,6.85,0,11,0,15.63,0,22.74,3.41,28.53,8.73,33a73,73,0,0,0,11.93,7.68L34.24,51.31l.6-.35.06-.18.12.08,7.27-4.3ZM32.85,30.43c3.91-2.55,4.31-6.85,2.87-10.95C34.57,16.19,32.1,13.35,28.4,11A21.36,21.36,0,0,0,15.88,7.77c-8.44.77-10,8.29-6.35,14.86a17.07,17.07,0,0,0,7.37,6.92A24.19,24.19,0,0,0,24.44,32a12.16,12.16,0,0,0,8.41-1.56ZM55.08,53.11a3.58,3.58,0,1,1-3.57-3.58,3.57,3.57,0,0,1,3.57,3.58ZM32.85,75.36c3.91,2.55,4.31,6.84,2.87,10.94-1.15,3.3-3.62,6.13-7.32,8.53A21.44,21.44,0,0,1,15.88,98c-8.44-.78-10-8.3-6.35-14.87a17,17,0,0,1,7.37-6.91,23.94,23.94,0,0,1,7.54-2.45,12.16,12.16,0,0,1,8.41,1.57Z" sketchjs:uid="1" style="fill-rule: evenodd;"/>
<g style="mix-blend-mode: source-over;" transform="matrix(0.08803663033961473,-0.22859690826354873,0.2285969082635487,0.08803663033961472,485.9403302518847,614.7749235660337)">
<path d="M95.36,19.84c11-5.17,20.18-5.08,27.52-.1l-65,37.45a12.92,12.92,0,0,1-7.22,3c-2.42.13-3.29-.56-5.39-.95a7.22,7.22,0,0,0-5.4,1l-1.71,1.56c-1.53,1.39-3.37,2.6-3.07,5,.2,1.58,1.55,2.91,3.69,4.06A15.4,15.4,0,0,1,42.56,76,17.92,17.92,0,0,1,44,82.5c-.39,7.55-3.92,14.07-12.41,19-9.68,6-17.86,5.27-25.18.86C2,98.93,0,94.82,0,90.16,0,83,3.41,77.26,8.73,72.78A73.68,73.68,0,0,1,20.66,65.1l14-10.95.25.85L95.36,19.84Zm0,66.1c11,5.17,20.18,5.08,27.52.1L69.16,55.09,55.53,62.78,95.36,85.94ZM42.29,46.56a7.4,7.4,0,0,1-2.41-1L38.17,44c-1.53-1.39-3.37-2.6-3.07-5,.2-1.59,1.55-2.92,3.69-4.06a15.44,15.44,0,0,0,3.77-5.2A17.86,17.86,0,0,0,44,23.29c-.39-7.55-3.92-14.08-12.41-19C21.93-1.63,13.75-.95,6.43,3.46,2,6.85,0,11,0,15.63,0,22.74,3.41,28.53,8.73,33a73,73,0,0,0,11.93,7.68L34.24,51.31l.6-.35.06-.18.12.08,7.27-4.3ZM32.85,30.43c3.91-2.55,4.31-6.85,2.87-10.95C34.57,16.19,32.1,13.35,28.4,11A21.36,21.36,0,0,0,15.88,7.77c-8.44.77-10,8.29-6.35,14.86a17.07,17.07,0,0,0,7.37,6.92A24.19,24.19,0,0,0,24.44,32a12.16,12.16,0,0,0,8.41-1.56ZM55.08,53.11a3.58,3.58,0,1,1-3.57-3.58,3.57,3.57,0,0,1,3.57,3.58ZM32.85,75.36c3.91,2.55,4.31,6.84,2.87,10.94-1.15,3.3-3.62,6.13-7.32,8.53A21.44,21.44,0,0,1,15.88,98c-8.44-.78-10-8.3-6.35-14.87a17,17,0,0,1,7.37-6.91,23.94,23.94,0,0,1,7.54-2.45,12.16,12.16,0,0,1,8.41,1.57Z" style="fill-rule: evenodd;"/>
</g>
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" sketchjs:tool="polyline" points="0,9 10,4.5 19,0" transform="matrix(1,0,0,1,356.5,503)"/>
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" sketchjs:tool="polyline" points="18,17 0,0" transform="matrix(1,0,0,1,701,502.5)"/>
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" sketchjs:tool="polyline" points="0,11 8,0" transform="matrix(1,0,0,1,379,518.5)"/>
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" sketchjs:tool="polyline" points="8,0 0,11.5" transform="matrix(1,0,0,1,399,527.5)"/>
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" sketchjs:tool="polyline" points="8.5,0 0,14" transform="matrix(1,0,0,1,417,533)"/>
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" sketchjs:tool="polyline" points="3.5,0 0,15" transform="matrix(1,0,0,1,440,540)"/>
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" sketchjs:tool="polyline" points="2,0 0,15.5" transform="matrix(1,0,0,1,467.5,548.5)"/>
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" sketchjs:tool="polyline" points="4,0 0,20" transform="matrix(1,0,0,1,482.5,554)"/>
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" sketchjs:tool="polyline" points="0,0 0.5,14.5" transform="matrix(1,0,0,1,511,556.5)"/>
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" sketchjs:tool="polyline" points="0,0 1.5,19" transform="matrix(1,0,0,1,540,556.5)"/>
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" sketchjs:tool="polyline" points="0,0 4,14" transform="matrix(1,0,0,1,566,554)"/>
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" sketchjs:tool="polyline" points="0,0 4,13" transform="matrix(1,0,0,1,591,548)"/>
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" sketchjs:tool="polyline" points="0,0 0,18" transform="matrix(1,0,0,1,612,544)"/>
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" sketchjs:tool="polyline" points="0,0 9.5,10.5" transform="matrix(1,0,0,1,634.5,534)"/>
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" sketchjs:tool="polyline" points="0,0 10.5,13.5" transform="matrix(1,0,0,1,652.5,526.5)"/>
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" sketchjs:tool="polyline" points="0,0 12.5,16" transform="matrix(1,0,0,1,671.5,518)"/>
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" sketchjs:tool="polyline" points="0,0 9.5,13" transform="matrix(1,0,0,1,689.5,513)"/>
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" points="0,9 10,4.5 19,0" transform="matrix(1,0,0,1,356.5,503)"/>
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" points="18,17 0,0" transform="matrix(1,0,0,1,701,502.5)"/>
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" points="0,11 8,0" transform="matrix(1,0,0,1,379,518.5)"/>
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" points="8,0 0,11.5" transform="matrix(1,0,0,1,399,527.5)"/>
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" points="8.5,0 0,14" transform="matrix(1,0,0,1,417,533)"/>
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" points="3.5,0 0,15" transform="matrix(1,0,0,1,440,540)"/>
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" points="2,0 0,15.5" transform="matrix(1,0,0,1,467.5,548.5)"/>
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" points="4,0 0,20" transform="matrix(1,0,0,1,482.5,554)"/>
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" points="0,0 0.5,14.5" transform="matrix(1,0,0,1,511,556.5)"/>
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" points="0,0 1.5,19" transform="matrix(1,0,0,1,540,556.5)"/>
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" points="0,0 4,14" transform="matrix(1,0,0,1,566,554)"/>
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" points="0,0 4,13" transform="matrix(1,0,0,1,591,548)"/>
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" points="0,0 0,18" transform="matrix(1,0,0,1,612,544)"/>
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" points="0,0 9.5,10.5" transform="matrix(1,0,0,1,634.5,534)"/>
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" points="0,0 10.5,13.5" transform="matrix(1,0,0,1,652.5,526.5)"/>
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" points="0,0 12.5,16" transform="matrix(1,0,0,1,671.5,518)"/>
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" points="0,0 9.5,13" transform="matrix(1,0,0,1,689.5,513)"/>
</svg>

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before After
Before After

View file

@ -1,12 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketchjs="https://sketch.io/dtd/" sketchjs:metadata="eyJuYW1lIjoiRHJhd2luZyIsInN1cmZhY2UiOnsiaXNQYWludCI6dHJ1ZSwibWV0aG9kIjoiZmlsbCIsImJsZW5kIjoibm9ybWFsIiwiZW5hYmxlZCI6dHJ1ZSwib3BhY2l0eSI6MSwidHlwZSI6InBhdHRlcm4iLCJwYXR0ZXJuIjp7InR5cGUiOiJwYXR0ZXJuIiwicmVmbGVjdCI6Im5vLXJlZmxlY3QiLCJyZXBlYXQiOiJyZXBlYXQiLCJzbW9vdGhpbmciOmZhbHNlLCJzcmMiOiJ0cmFuc3BhcmVudExpZ2h0Iiwic3giOjEsInN5IjoxLCJ4MCI6MC41LCJ4MSI6MSwieTAiOjAuNSwieTEiOjF9LCJpc0ZpbGwiOnRydWV9LCJjbGlwUGF0aCI6eyJlbmFibGVkIjp0cnVlLCJzdHlsZSI6eyJzdHJva2VTdHlsZSI6ImJsYWNrIiwibGluZVdpZHRoIjoxfX0sImRlc2NyaXB0aW9uIjoiTWFkZSB3aXRoIFNrZXRjaHBhZCIsIm1ldGFkYXRhIjp7fSwiZXhwb3J0RFBJIjo3MiwiZXhwb3J0Rm9ybWF0IjoicG5nIiwiZXhwb3J0UXVhbGl0eSI6MC45NSwidW5pdHMiOiJweCIsIndpZHRoIjoxOTIwLCJoZWlnaHQiOjk2OSwicGFnZXMiOlt7IndpZHRoIjoxOTIwLCJoZWlnaHQiOjk2OX1dLCJ1dWlkIjoiYzA2NmVmZjItZDU3OC00NDcwLWI1ZDktYzQyM2FhMmY0M2JmIn0=" width="1920" height="969" viewBox="0 0 1920 969">
<path style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" sketchjs:tool="path" d="M0 0 L0.01 0.01 " transform="matrix(1,0,0,1,696.25,296.875)"/>
<path style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" sketchjs:tool="path" d="M0 24.46 C166.67 -11.79 573.13 -4.29 702.5 24.46 " transform="matrix(1,0,0,1,338.125,189.9194023264709)"/>
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" sketchjs:tool="polyline" points="16.25,0 0,120.63 37.5,170" transform="matrix(1,0,0,1,321.25,215)"/>
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" sketchjs:tool="polyline" points="22.5,0 38.12,120.63 0,168.75" transform="matrix(1,0,0,1,1019.3775,215)"/>
<path style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" sketchjs:tool="path" d="M658.75 72.08 C540.42 -0.63 205 -45.42 0 73.33 " transform="matrix(1,0,0,1,359.375,312.9238117)"/>
<polyline style="fill: none; stroke: #ff3737; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" sketchjs:tool="polyline" points="18.13,0 2.5,3.13 0,16.88" transform="matrix(1,0,0,1,343.125,219.375)"/>
<polyline style="fill: none; stroke: #ff3737; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" sketchjs:tool="polyline" points="7.5,23.75 0,15.63 1.88,0" transform="matrix(1,0,0,1,330,318.125)"/>
<polyline style="fill: none; stroke: #ff3737; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" sketchjs:tool="polyline" points="0,27.5 10.63,15.63 7.5,0" transform="matrix(1,0,0,1,1040,317.5)"/>
<polyline style="fill: none; stroke: #ff3737; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" sketchjs:tool="polyline" points="0,0 20.63,2.5 22.5,15" transform="matrix(1,0,0,1,1013.75,217.5)"/>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1920" height="969" viewBox="0 0 1920 969">
<path style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" d="M0 0 L0.01 0.01 " transform="matrix(1,0,0,1,696.25,296.875)"/>
<path style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" d="M0 24.46 C166.67 -11.79 573.13 -4.29 702.5 24.46 " transform="matrix(1,0,0,1,338.125,189.9194023264709)"/>
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" points="16.25,0 0,120.63 37.5,170" transform="matrix(1,0,0,1,321.25,215)"/>
<polyline style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" points="22.5,0 38.12,120.63 0,168.75" transform="matrix(1,0,0,1,1019.3775,215)"/>
<path style="fill: none; stroke: #000000; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" d="M658.75 72.08 C540.42 -0.63 205 -45.42 0 73.33 " transform="matrix(1,0,0,1,359.375,312.9238117)"/>
<polyline style="fill: none; stroke: #ff3737; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" points="18.13,0 2.5,3.13 0,16.88" transform="matrix(1,0,0,1,343.125,219.375)"/>
<polyline style="fill: none; stroke: #ff3737; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" points="7.5,23.75 0,15.63 1.88,0" transform="matrix(1,0,0,1,330,318.125)"/>
<polyline style="fill: none; stroke: #ff3737; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" points="0,27.5 10.63,15.63 7.5,0" transform="matrix(1,0,0,1,1040,317.5)"/>
<polyline style="fill: none; stroke: #ff3737; mix-blend-mode: source-over; stroke-dasharray: none; stroke-dashoffset: 0; stroke-linecap: round; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 2;" points="0,0 20.63,2.5 22.5,15" transform="matrix(1,0,0,1,1013.75,217.5)"/>
</svg>

Before

Width:  |  Height:  |  Size: 4 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Before After
Before After

View file

@ -0,0 +1,46 @@
import React from 'react'
import ErrorBoundary from '@docusaurus/ErrorBoundary'
import { PageMetadata, SkipToContentFallbackId, ThemeClassNames } from '@docusaurus/theme-common'
import { useKeyboardNavigation } from '@docusaurus/theme-common/internal'
import SkipToContent from '@theme/SkipToContent'
import AnnouncementBar from '@theme/AnnouncementBar'
import Navbar from '@theme/Navbar'
import Footer from '@theme/Footer'
import LayoutProvider from '@theme/Layout/Provider'
import ErrorPageContent from '@theme/ErrorPageContent'
/*
* A layout to keep the docusaurus header/footer and other stuff
* but use the entire page for the content. Like our editor.
*
* @params {object} props - All React props
* @params {object} children - The React children to render
* @params {bool} noFooter - Set this to true to not render a footer
* @params {string} className - CSS classes for the main content wrapper
* @params {string} title - Page title
* @params {string} description - Page description
*/
export function BareLayout({
children = null,
noFooter = false,
noHeader = false,
className = 'tw-bg-transparent tw-p-0 tw-m-0',
title = 'FreeSewing.org',
description = 'Free bespoke sewing patterns',
}) {
useKeyboardNavigation()
return (
<LayoutProvider>
<PageMetadata title={title} description={description} />
<SkipToContent />
<AnnouncementBar />
{!noHeader && <Navbar />}
<div id={SkipToContentFallbackId} className={className}>
<ErrorBoundary fallback={(params) => <ErrorPageContent {...params} />}>
{children}
</ErrorBoundary>
</div>
{!noFooter && <Footer />}
</LayoutProvider>
)
}

View file

@ -0,0 +1,15 @@
import { BareLayout } from '@site/src/components/bare-layout.mjs'
import { Editor } from '@freesewing/react/components/Editor'
/*
* This hinges on two things:
* - BareLayout: Gives us a docusaurus header/footer and so on, but the full page for content
* - Editor: FreeSewing's drop-in pattern editor
*/
const EditorPage = () => (
<BareLayout>
<Editor />
</BareLayout>
)
export default EditorPage

View file

@ -1,5 +0,0 @@
---
title: Editor
---
FIXME: Editor goes here