diff --git a/package-lock.json b/package-lock.json index fe433bebdd0..2f7eeef0e5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/packages/react/components/Account/Set.mjs b/packages/react/components/Account/Set.mjs index d528b6f3d25..6db517767da 100644 --- a/packages/react/components/Account/Set.mjs +++ b/packages/react/components/Account/Set.mjs @@ -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 }) => { /> {filterMeasurements().map((m) => ( - { * * @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,15 +106,25 @@ export const Collection = ({ Link = false, linkTo = 'about' }) => {
{Object.keys(filtered) .sort() - .map((d) => ( - - {d} - - ))} + .map((d) => + onClick ? ( + + ) : ( + + {d} + + ) + )}
{showFilters ? ( <> @@ -212,7 +224,9 @@ export const Collection = ({ Link = false, linkTo = 'about' }) => { )} -
+
{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 }) => ( ) -const DesignCard = ({ name, lineDrawing = false, linkTo, Link }) => { +const DesignCard = ({ name, lineDrawing = false, linkTo, Link, onClick }) => { if (!Link) Link = WebLink const LineDrawing = @@ -275,37 +290,49 @@ const DesignCard = ({ name, lineDrawing = false, linkTo, Link }) => { bg.backgroundPosition = 'center center' } - return ( + const inner = ( +
+
+ {about[name].name} +
+ {lineDrawing ? ( +
+ +
+ ) : ( + + )} +
+ +
+
+ ) + + return onClick ? ( + + ) : ( -
-
- {about[name].name} -
- {lineDrawing ? ( -
- -
- ) : ( - - )} -
- -
-
+ {inner} ) } diff --git a/packages/react/components/Editor/swizzle/components/accordion.mjs b/packages/react/components/Editor/components/Accordion.mjs similarity index 67% rename from packages/react/components/Editor/swizzle/components/accordion.mjs rename to packages/react/components/Editor/components/Accordion.mjs index d2d561f0cbe..72c19356815 100644 --- a/packages/react/components/Editor/swizzle/components/accordion.mjs +++ b/packages/react/components/Editor/components/Accordion.mjs @@ -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] ? (
- + {item[0]} {item[1]} diff --git a/packages/react/components/Editor/swizzle/components/aside-view-menu.mjs b/packages/react/components/Editor/components/AsideViewMenu.mjs similarity index 91% rename from packages/react/components/Editor/swizzle/components/aside-view-menu.mjs rename to packages/react/components/Editor/components/AsideViewMenu.mjs index 13bca1d376a..fc6c5db7921 100644 --- a/packages/react/components/Editor/swizzle/components/aside-view-menu.mjs +++ b/packages/react/components/Editor/components/AsideViewMenu.mjs @@ -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 - - return -} - export const AsideViewMenuSpacer = () => (
) diff --git a/packages/react/components/Editor/swizzle/components/header-menu.mjs b/packages/react/components/Editor/components/HeaderMenu.mjs similarity index 52% rename from packages/react/components/Editor/swizzle/components/header-menu.mjs rename to packages/react/components/Editor/components/HeaderMenu.mjs index e88d0812c77..5de6ef05f83 100644 --- a/packages/react/components/Editor/swizzle/components/header-menu.mjs +++ b/packages/react/components/Editor/components/HeaderMenu.mjs @@ -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 (
{
- - + +
) } -export const HeaderMenuAllViews = ({ state, Swizzled, update, open, setOpen }) => ( - +export const HeaderMenuAllViews = ({ config, state, update, open, setOpen }) => ( + ) export const HeaderMenuDraftView = (props) => { - const { Swizzled } = props const flags = props.pattern?.setStores?.[0]?.plugins?.['plugin-annotations']?.flags return ( <>
- - - - {flags ? : null} + + + + {flags ? : null}
- - - + + + ) } 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 ? ( - + - + ) : ( - +
{ >
)}
-
+ ) } export const HeaderMenuDraftViewDesignOptions = (props) => { - const { Swizzled } = props - return ( - - - {Swizzled.methods.t('pe:designOptions.t')} + + fixme: pe:designOptions.t } > - - + + ) } export const HeaderMenuDraftViewCoreSettings = (props) => { - const { Swizzled } = props - return ( - - - {Swizzled.methods.t('pe:coreSettings.t')} + + fixme: pe:coreSettings.t } > - - + + ) } export const HeaderMenuDraftViewUiPreferences = (props) => { - const { Swizzled } = props - return ( - - - {Swizzled.methods.t('pe:uiPreferences.t')} + + fixme: pe:uiPreferences.t } > - - + + ) } 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.methods.t('pe:flags')} + Flags ({count}) } > - - + + ) } 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 (
{ux >= levels.sa ? ( - ) : null} {ux >= levels.paperless ? ( @@ -217,9 +219,9 @@ export const HeaderMenuDraftViewIcons = (props) => { {ux >= levels.complete ? ( @@ -227,9 +229,9 @@ export const HeaderMenuDraftViewIcons = (props) => { {ux >= levels.expand ? ( @@ -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)" > - ) : null} - + {ux >= levels.ux ? (
- + {[0, 1, 2, 3, 4].map((i) => ( ))} - +
) : null} {ux >= levels.aside ? ( ) : null} {ux >= levels.kiosk ? ( ) : null} {ux >= levels.rotate ? ( ) : 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)" > - @@ -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) => {
- - - {Swizzled.methods.t('pe:undo')} + + Undo } > @@ -364,11 +354,11 @@ export const HeaderMenuUndoIcons = (props) => {
    {undos.slice(0, 9).map((step, index) => (
  • - +
  • ))}
  • - { return null /*update.state(index, state._) */ @@ -376,29 +366,32 @@ export const HeaderMenuUndoIcons = (props) => { >
    - - {Swizzled.methods.t(`pe:view.undos.t`)}... + + {viewLabels.undo.t}
    {undos.length}
    -
    +
) : null} -
- -
) } 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) => {
- -
) } export const HeaderMenuIcon = (props) => { - const { Swizzled, name, extraClasses = '' } = props - const Icon = - Swizzled.components[`${Swizzled.methods.capitalize(name)}Icon`] || Swizzled.components.Noop - return + const { name, extraClasses = '' } = props + //const Icon = Swizzled.components[`${Swizzled.methods.capitalize(name)}Icon`] || Swizzled.components.Noop + return } export const HeaderMenuIconSpacer = () => | -export const HeaderMenuButton = ({ - Swizzled, - updateHandler, - children, - tooltip, - disabled = false, -}) => ( - +export const HeaderMenuButton = ({ updateHandler, children, tooltip, disabled = false }) => ( + - + ) 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() + if (viewName === 'spacer') output.push() else if (viewName === 'spacerOver3') - output.push(state.ui.ux > 3 ? : null) + output.push(state.ui.ux > 3 ? : 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(
  • @@ -486,12 +465,8 @@ export const HeaderMenuViewMenu = (props) => { }`} onClick={() => update.view(viewName)} > - - {Swizzled.methods.t(`pe:view.${viewName}.t`)} + + {viewLabels[viewName].t}
  • ) @@ -499,18 +474,14 @@ export const HeaderMenuViewMenu = (props) => { } return ( - - - {Swizzled.methods.t('pe:views.t')} + + Views } > @@ -520,6 +491,6 @@ export const HeaderMenuViewMenu = (props) => { > {output} - + ) } diff --git a/packages/react/components/Editor/swizzle/components/html-span.mjs b/packages/react/components/Editor/components/HtmlSpan.mjs similarity index 76% rename from packages/react/components/Editor/swizzle/components/html-span.mjs rename to packages/react/components/Editor/components/HtmlSpan.mjs index 3d98e8acc26..3112e13f28a 100644 --- a/packages/react/components/Editor/swizzle/components/html-span.mjs +++ b/packages/react/components/Editor/components/HtmlSpan.mjs @@ -1 +1,3 @@ +import React from 'react' + export const HtmlSpan = ({ html }) => diff --git a/packages/react/components/Editor/swizzle/components/loading-status.mjs b/packages/react/components/Editor/components/LoadingStatus.mjs similarity index 67% rename from packages/react/components/Editor/swizzle/components/loading-status.mjs rename to packages/react/components/Editor/components/LoadingStatus.mjs index c0741fd69a7..e41b8a4bc3f 100644 --- a/packages/react/components/Editor/swizzle/components/loading-status.mjs +++ b/packages/react/components/Editor/components/LoadingStatus.mjs @@ -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 (
    - {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 (
    { } 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`} > diff --git a/packages/react/components/Editor/swizzle/components/measurements-editor.mjs b/packages/react/components/Editor/components/MeasurementsEditor.mjs similarity index 77% rename from packages/react/components/Editor/swizzle/components/measurements-editor.mjs rename to packages/react/components/Editor/components/MeasurementsEditor.mjs index b65d0534d8c..446c11ab55e 100644 --- a/packages/react/components/Editor/swizzle/components/measurements-editor.mjs +++ b/packages/react/components/Editor/components/MeasurementsEditor.mjs @@ -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 (
    -
    {Swizzled.methods.t('pe:requiredMeasurements')}
    +
    Required Measurments
    {Object.keys(Design.patternConfig.measurements).length === 0 ? ( -

    ({Swizzled.methods.t('account:none')})

    +

    This design does not require any measurements.

    ) : (
    {Design.patternConfig.measurements.map((m) => ( - {
    )} -
    {Swizzled.methods.t('pe:optionalMeasurements')}
    +
    Optional Measurements
    {Object.keys(Design.patternConfig.optionalMeasurements).length === 0 ? ( -

    ({Swizzled.methods.t('account:none')})

    +

    This design does not use any optional measurements.

    ) : ( Design.patternConfig.optionalMeasurements.map((m) => ( - { const { children, tip, ...rest } = props diff --git a/packages/react/components/Editor/components/view-wrapper.mjs b/packages/react/components/Editor/components/view-wrapper.mjs deleted file mode 100644 index ff6f1949d06..00000000000 --- a/packages/react/components/Editor/components/view-wrapper.mjs +++ /dev/null @@ -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 - - // 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 ( -
    - {Swizzled.config.withAside ? ( - - ) : null} -
    - - -
    -
    - ) -} - -/** - * 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 diff --git a/packages/react/components/Editor/components/views/DesignsView.mjs b/packages/react/components/Editor/components/views/DesignsView.mjs new file mode 100644 index 00000000000..c1c0a0fc921 --- /dev/null +++ b/packages/react/components/Editor/components/views/DesignsView.mjs @@ -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 }) => ( +
    +

    Choose a design from the FreeSewing collection

    + { + update.design(design) + update.view('draft') + }} + /> +
    +) diff --git a/packages/react/components/Editor/swizzle/components/measurements-view.mjs b/packages/react/components/Editor/components/views/MeasurementsView.mjs similarity index 80% rename from packages/react/components/Editor/swizzle/components/measurements-view.mjs rename to packages/react/components/Editor/components/views/MeasurementsView.mjs index fd46ebae11a..a04e6ec53fc 100644 --- a/packages/react/components/Editor/swizzle/components/measurements-view.mjs +++ b/packages/react/components/Editor/components/views/MeasurementsView.mjs @@ -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

    {t('pe:chooseFromCuratedSetsDesc')}

    , - , + , 'csets', ] ) @@ -124,13 +115,13 @@ export const MeasurementsView = ({ Design, missingMeasurements, update, Swizzled

    {t('pe:editMeasurementsDesc')}

    , - , + , 'edit', ]) return ( <> - +

    {t('pe:measurements')}

    {missingMeasurements && missingMeasurements.length > 0 ? ( diff --git a/packages/react/components/Editor/swizzle/components/view-picker.mjs b/packages/react/components/Editor/components/views/ViewPicker.mjs similarity index 100% rename from packages/react/components/Editor/swizzle/components/view-picker.mjs rename to packages/react/components/Editor/components/views/ViewPicker.mjs diff --git a/packages/react/components/Editor/components/views/index.mjs b/packages/react/components/Editor/components/views/index.mjs new file mode 100644 index 00000000000..99394f8e728 --- /dev/null +++ b/packages/react/components/Editor/components/views/index.mjs @@ -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 + if (view === 'measurements') return + /* + 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

    No view component for view {props.view}

    +} + +/* + * This returns a view-specific icon + */ +export const ViewIcon = ({ view, className = 'tw-w-6 tw-h-6' }) => { + //designs: , + //measurements: , + // + return +} + +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', + }, +} diff --git a/packages/react/components/Editor/swizzle/config.mjs b/packages/react/components/Editor/config/index.mjs similarity index 72% rename from packages/react/components/Editor/swizzle/config.mjs rename to packages/react/components/Editor/config/index.mjs index 5ea04e81f04..6ac0db8bbb5 100644 --- a/packages/react/components/Editor/swizzle/config.mjs +++ b/packages/react/components/Editor/config/index.mjs @@ -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: * - * * - * * - * * - *************************************************************************/ - /* - * 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, diff --git a/packages/react/components/Editor/hooks/useEditorState.mjs b/packages/react/components/Editor/hooks/useEditorState.mjs new file mode 100644 index 00000000000..3d4c8d02878 --- /dev/null +++ b/packages/react/components/Editor/hooks/useEditorState.mjs @@ -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 +} diff --git a/packages/react/components/Editor/index.mjs b/packages/react/components/Editor/index.mjs index 13f00b88dd6..5f1fabead29 100644 --- a/packages/react/components/Editor/index.mjs +++ b/packages/react/components/Editor/index.mjs @@ -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 +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 + 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 + 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 + + // 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 } } + + return ( +
    + {editorConfig.withAside ? : null} +
    + + +
    +
    + ) } /** - * Helper function to verify that all props that are required to - * run the editor are present. + * Helper method to figure out what view to load + * based on the props passed in, and destructure + * the props we need for it. * - * 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 + * @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 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" +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 } - return false + /* + * 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] } - -/** - * A component to inform the user that the editor cannot be started - * because there are missing required props - */ -const LackingPropsError = ({ error }) => ( -
    -

    Unable to initialize pattern editor

    -

    {error}

    -
    -) diff --git a/packages/react/components/Editor/swizzle/methods/core-settings.mjs b/packages/react/components/Editor/lib/core-settings.mjs similarity index 100% rename from packages/react/components/Editor/swizzle/methods/core-settings.mjs rename to packages/react/components/Editor/lib/core-settings.mjs diff --git a/packages/react/components/Editor/swizzle/methods/design-options.mjs b/packages/react/components/Editor/lib/design-options.mjs similarity index 100% rename from packages/react/components/Editor/swizzle/methods/design-options.mjs rename to packages/react/components/Editor/lib/design-options.mjs diff --git a/packages/react/components/Editor/swizzle/methods/editor.mjs b/packages/react/components/Editor/lib/editor.mjs similarity index 74% rename from packages/react/components/Editor/swizzle/methods/editor.mjs rename to packages/react/components/Editor/lib/editor.mjs index ddbd67d3b51..0e0b1566ec1 100644 --- a/packages/react/components/Editor/swizzle/methods/editor.mjs +++ b/packages/react/components/Editor/lib/editor.mjs @@ -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: , + icon: , field, optCode: `${field}.t`, titleCode: 'uiPreferences.t', - structure: Swizzled.methods.menuUiPreferencesStructure()[field], + structure: menuUiPreferencesStructure()[field], } const FieldIcon = data.structure.icon data.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: , + icon: , 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 = /* * 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: , + icon: , 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: , + icon: , 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 } diff --git a/packages/react/components/Editor/swizzle/methods/formatting.mjs b/packages/react/components/Editor/lib/formatting.mjs similarity index 100% rename from packages/react/components/Editor/swizzle/methods/formatting.mjs rename to packages/react/components/Editor/lib/formatting.mjs diff --git a/packages/react/components/Editor/lib/index.mjs b/packages/react/components/Editor/lib/index.mjs new file mode 100644 index 00000000000..5422eb18be5 --- /dev/null +++ b/packages/react/components/Editor/lib/index.mjs @@ -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, +} diff --git a/packages/react/components/Editor/swizzle/methods/measurements.mjs b/packages/react/components/Editor/lib/measurements.mjs similarity index 70% rename from packages/react/components/Editor/swizzle/methods/measurements.mjs rename to packages/react/components/Editor/lib/measurements.mjs index e75b1cf22bd..0373867cf81 100644 --- a/packages/react/components/Editor/swizzle/methods/measurements.mjs +++ b/packages/react/components/Editor/lib/measurements.mjs @@ -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} 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 } } } diff --git a/packages/react/components/Editor/swizzle/methods/ui-preferences.mjs b/packages/react/components/Editor/lib/ui-preferences.mjs similarity index 100% rename from packages/react/components/Editor/swizzle/methods/ui-preferences.mjs rename to packages/react/components/Editor/lib/ui-preferences.mjs diff --git a/packages/react/components/Editor/swizzle/components/designs-view.mjs b/packages/react/components/Editor/swizzle/components/designs-view.mjs deleted file mode 100644 index f6ed0a3a326..00000000000 --- a/packages/react/components/Editor/swizzle/components/designs-view.mjs +++ /dev/null @@ -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 }) => ( -
    -

    {Swizzled.methods.t('pe:chooseADesign')}

    -
      - {Object.keys(designs).map((name) => ( -
    • - -
    • - ))} -
    -
    -) diff --git a/packages/react/components/Editor/swizzle/components/loaders.mjs b/packages/react/components/Editor/swizzle/components/loaders.mjs deleted file mode 100644 index 774cdd91141..00000000000 --- a/packages/react/components/Editor/swizzle/components/loaders.mjs +++ /dev/null @@ -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 ? :
    ...
    diff --git a/packages/react/components/Editor/swizzle/hooks/use-editor-state.mjs b/packages/react/components/Editor/swizzle/hooks/use-editor-state.mjs deleted file mode 100644 index e949f0bf902..00000000000 --- a/packages/react/components/Editor/swizzle/hooks/use-editor-state.mjs +++ /dev/null @@ -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 - }, -}) diff --git a/packages/react/components/Editor/swizzle/methods/index.mjs b/packages/react/components/Editor/swizzle/methods/index.mjs deleted file mode 100644 index d3b9ed47f82..00000000000 --- a/packages/react/components/Editor/swizzle/methods/index.mjs +++ /dev/null @@ -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: * - * * - * * - * * - *************************************************************************/ - -/* - * 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, -} diff --git a/packages/react/components/Input/index.mjs b/packages/react/components/Input/index.mjs index c548c15a706..6687d54fd0a 100644 --- a/packages/react/components/Input/index.mjs +++ b/packages/react/components/Input/index.mjs @@ -489,7 +489,7 @@ export const MarkdownInput = ({ ) -export const MeasieInput = ({ +export const MeasurementInput = ({ imperial, // True for imperial, False for metric m, // The measurement name original, // The original value diff --git a/packages/react/package.json b/packages/react/package.json index 1f408a1c1c4..74dc6d1af64 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -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/**", diff --git a/sites/org/docs/docs/designs/jaeger/instructions/collarCuts.svg b/sites/org/docs/docs/designs/jaeger/instructions/collarCuts.svg index e44d46c1465..6a8cd451f07 100644 --- a/sites/org/docs/docs/designs/jaeger/instructions/collarCuts.svg +++ b/sites/org/docs/docs/designs/jaeger/instructions/collarCuts.svg @@ -1,44 +1,44 @@ - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - + + - - + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + diff --git a/sites/org/docs/docs/designs/jaeger/instructions/collarMarkings.svg b/sites/org/docs/docs/designs/jaeger/instructions/collarMarkings.svg index f8a082df422..c0b6d936b7c 100644 --- a/sites/org/docs/docs/designs/jaeger/instructions/collarMarkings.svg +++ b/sites/org/docs/docs/designs/jaeger/instructions/collarMarkings.svg @@ -1,12 +1,12 @@ - - - - - - - - - - + + + + + + + + + + diff --git a/sites/org/src/components/bare-layout.mjs b/sites/org/src/components/bare-layout.mjs new file mode 100644 index 00000000000..2d88cdde834 --- /dev/null +++ b/sites/org/src/components/bare-layout.mjs @@ -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 ( + + + + + {!noHeader && } +
    + }> + {children} + +
    + {!noFooter &&