wip: Work on editor
This commit is contained in:
parent
1fde267288
commit
922bd04130
38 changed files with 1092 additions and 1105 deletions
123
package-lock.json
generated
123
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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]}
|
|
@ -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" />
|
||||
)
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -1 +1,3 @@
|
|||
import React from 'react'
|
||||
|
||||
export const HtmlSpan = ({ html }) => <span dangerouslySetInnerHTML={{ __html: html }} />
|
|
@ -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">
|
|
@ -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}
|
|
@ -1,3 +1,5 @@
|
|||
import React from 'react'
|
||||
|
||||
export const Tooltip = (props) => {
|
||||
const { children, tip, ...rest } = props
|
||||
|
|
@ -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
|
|
@ -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>
|
||||
)
|
|
@ -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 ? (
|
117
packages/react/components/Editor/components/views/index.mjs
Normal file
117
packages/react/components/Editor/components/views/index.mjs
Normal 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',
|
||||
},
|
||||
}
|
|
@ -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,
|
86
packages/react/components/Editor/hooks/useEditorState.mjs
Normal file
86
packages/react/components/Editor/hooks/useEditorState.mjs
Normal 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
|
||||
}
|
|
@ -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]
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
129
packages/react/components/Editor/lib/index.mjs
Normal file
129
packages/react/components/Editor/lib/index.mjs
Normal 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,
|
||||
}
|
|
@ -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 } }
|
||||
}
|
|
@ -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>
|
||||
)
|
|
@ -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>
|
|
@ -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
|
||||
},
|
||||
})
|
|
@ -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,
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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/**",
|
||||
|
|
|
@ -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 |
|
@ -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 |
46
sites/org/src/components/bare-layout.mjs
Normal file
46
sites/org/src/components/bare-layout.mjs
Normal 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>
|
||||
)
|
||||
}
|
15
sites/org/src/pages/editor.js
Normal file
15
sites/org/src/pages/editor.js
Normal 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
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
title: Editor
|
||||
---
|
||||
|
||||
FIXME: Editor goes here
|
Loading…
Add table
Add a link
Reference in a new issue