1
0
Fork 0

wip: Work on editor

This commit is contained in:
joostdecock 2025-01-05 17:58:31 +01:00
parent 922bd04130
commit 94b8efa4a2
31 changed files with 677 additions and 614 deletions

View file

@ -1,6 +1,8 @@
// Dependencies
import { measurements } from '@freesewing/config'
import { cloudflareImageUrl, capitalize } from '@freesewing/utils'
import { measurements as measurementsTranslations } from '@freesewing/i18n'
import { requiredMeasurements as designMeasurements } from '@freesewing/collection'
import { cloudflareImageUrl, capitalize, hasRequiredMeasurements } from '@freesewing/utils'
// Context
import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus'
import { ModalContext } from '@freesewing/react/context/Modal'
@ -201,7 +203,8 @@ export const MsetCard = ({
const s = sizes[size]
const wrapperProps = {
className: `tw-bg-base-300 tw-aspect-square tw-h-${s} tw-w-${s} tw-mb-2 tw-grow
className: `tw-bg-base-300 tw-aspect-square tw-h-${s} tw-w-${s} tw-mb-2 tw-grow tw-w-full
hover:tw-cursor-pointer tw-border-0 tw-opacity-80 hover:tw-opacity-100
tw-mx-auto tw-flex tw-flex-col tw-items-start tw-text-center tw-justify-between tw-rounded-none md:tw-rounded shadow`,
style: {
backgroundImage: `url(${cloudflareImageUrl({ type: 'w500', id: set.img })})`,
@ -230,13 +233,11 @@ export const MsetCard = ({
<NoIcon className={`${iconClasses} tw-bg-error tw-text-error-content`} stroke={3} />
)
if (missing.length > 0) {
const translated = missing.map((m) => {
return t(m)
})
let missingString = t('missing') + ': ' + translated.join(', ')
const translated = missing.map((m) => measurementsTranslations[m])
let missingString = 'Missing:' + translated.join(', ')
if (missingString.length > maxLength) {
const lastSpace = missingString.lastIndexOf(', ', maxLength)
missingString = missingString.substring(0, lastSpace) + ', ' + t('andMore') + '...'
missingString = missingString.substring(0, lastSpace) + ', and more...'
}
const measieClasses = 'tw-font-normal tw-text-xs'
missingMeasies = <span className={`${measieClasses}`}>{missingString}</span>

View file

@ -5,7 +5,7 @@ import React, { useState } from 'react'
* So instead, we handle this in React state
*/
const getProps = (isActive) => ({
className: `tw-p-2 tw-px-4 tw-rounded-lg tw-bg-transparent tw-shadow
className: `tw-p-2 tw-px-4 tw-rounded-lg tw-bg-transparent tw-shadow hover:tw-cursor-pointer
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'}`,
})
@ -41,7 +41,10 @@ export const BaseAccordion = ({
.map((item, i) =>
active === item[2] ? (
<div key={i} {...propsGetter(true)}>
<Component onClick={setActive} className="tw-w-full hover:tw-cursor-pointer">
<Component
onClick={setActive}
className="tw-w-full tw-bg-transparent tw-border-0 hover:tw-bg-secondary hover:tw-bg-opacity-20 hover:tw-cursor-pointer"
>
{item[0]}
</Component>
{item[1]}

View file

@ -8,35 +8,8 @@ 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 = ({ config, Design, pattern, state, update }) => {
const [open, setOpen] = useState()
/*
* Guard views that require measurements agains missing measurements
* and make sure there's a view-specific header menu
*/
const ViewMenu =
!missingMeasurements(state, config) &&
Swizzled.components[`HeaderMenu${config.viewComponents[state.view]}`]
? Swizzled.components[`HeaderMenu${config.viewComponents[state.view]}`]
: Null
return (
<div
className={`flex sticky top-0 ${
state.ui.kiosk ? 'z-50' : 'z-20'
} transition-[top] duration-300 ease-in-out`}
>
<div
className={`flex flex-row flex-wrap gap-1 md:gap-4 w-full items-start justify-center border-b border-base-300 py-1 md:py-1.5`}
>
<HeaderMenuAllViews {...{ config, state, update, open, setOpen }} />
<ViewMenu {...{ config, state, update, Design, pattern, open, setOpen }} />
</div>
</div>
)
}
import { DesignOptionsMenu } from './menus/DesignOptionsMenu.mjs'
import { CoreSettingsMenu } from './menus/CoreSettingsMenu.mjs'
export const HeaderMenuAllViews = ({ config, state, update, open, setOpen }) => (
<HeaderMenuViewMenu {...{ config, state, update, open, setOpen }} />
@ -47,7 +20,7 @@ export const HeaderMenuDraftView = (props) => {
return (
<>
<div className="flex flex-row gap-1">
<div className="tw-flex tw-flex-row tw-gap-1">
<HeaderMenuDraftViewDesignOptions {...props} />
<HeaderMenuDraftViewCoreSettings {...props} />
<HeaderMenuDraftViewUiPreferences {...props} />
@ -74,31 +47,31 @@ export const HeaderMenuDropdown = (props) => {
disabled
tabIndex={0}
role="button"
className="btn btn-ghost hover:bg-secondary hover:bg-opacity-20 hover:border-solid hover:boder-2 hover:border-secondary border border-secondary border-2 border-dotted btn-sm px-2 z-20 relative"
className="tw-daisy-btn tw-daisy-btn-ghost hover:tw-bg-secondary hover:tw-bg-opacity-20 hover:tw-border-solid hover:tw-border-2 hover:tw-border-secondary tw-border tw-border-secondary tw-border-2 tw-border-dotted tw-daisy-btn-sm tw-px-2 tw-z-20 tw-relative"
>
{toggle}
</button>
</Tooltip>
) : (
<Tooltip tip={tooltip}>
<div className={`dropdown ${open === id ? 'dropdown-open z-20' : ''}`}>
<div className={`tw-daisy-dropdown ${open === id ? 'tw-daisy-dropdown-open tw-z-20' : ''}`}>
<div
tabIndex={0}
role="button"
className="btn btn-ghost hover:bg-secondary hover:bg-opacity-20 hover:border-solid hover:boder-2 hover:border-secondary border border-secondary border-2 border-dotted btn-sm px-2 z-20 relative"
className="tw-daisy-btn tw-daisy-btn-ghost hover:tw-bg-secondary hover:tw-bg-opacity-20 hover:tw-border-solid hover:tw-boder-2 hover:tw-border-secondary tw-border tw-border-secondary tw-border-2 tw-border-dotted tw-daisy-btn-sm tw-px-2 tw-z-20 tw-relative"
onClick={() => setOpen(open === id ? false : id)}
>
{toggle}
</div>
<div
tabIndex={0}
className="dropdown-content bg-base-100 bg-opacity-90 z-20 shadow left-0 !fixed md:!absolute top-12 w-screen md:w-96"
className="tw-daisy-dropdown-content tw-bg-base-100 tw-bg-opacity-90 tw-z-20 tw-shadow tw-left-0 !tw-fixed md:!tw-absolute tw-top-12 tw-w-screen md:tw-w-96"
>
{props.children}
</div>
{open === id && (
<div
className="w-screen h-screen absolute top-10 left-0 opacity-0"
className="tw-w-screen tw-h-screen tw-absolute tw-top-10 tw-left-0 tw-opacity-0"
style={{ width: '200vw', transform: 'translateX(-100vw)' }}
onClick={() => setOpen(false)}
></div>
@ -116,8 +89,8 @@ export const HeaderMenuDraftViewDesignOptions = (props) => {
tooltip="fixme: 'pe:designOptions.d'"
toggle={
<>
<HeaderMenuIcon name="options" extraClasses="text-secondary" />
<span className="hidden lg:inline">fixme: pe:designOptions.t</span>
<HeaderMenuIcon name="options" extraClasses="tw-text-secondary" />
<span className="tw-hidden lg:tw-inline">fixme: pe:designOptions.t</span>
</>
}
>
@ -134,8 +107,8 @@ export const HeaderMenuDraftViewCoreSettings = (props) => {
id="coreSettings"
toggle={
<>
<HeaderMenuIcon name="settings" extraClasses="text-secondary" />
<span className="hidden lg:inline">fixme: pe:coreSettings.t</span>
<HeaderMenuIcon name="settings" extraClasses="tw-text-secondary" />
<span className="tw-hidden lg:tw-inline">fixme: pe:coreSettings.t</span>
</>
}
>
@ -152,8 +125,8 @@ export const HeaderMenuDraftViewUiPreferences = (props) => {
id="uiPreferences"
toggle={
<>
<HeaderMenuIcon name="ui" extraClasses="text-secondary" />
<span className="hidden lg:inline">fixme: pe:uiPreferences.t</span>
<HeaderMenuIcon name="ui" extraClasses="tw-text-secondary" />
<span className="tw-hidden lg:tw-inline">fixme: pe:uiPreferences.t</span>
</>
}
>
@ -172,8 +145,8 @@ export const HeaderMenuDraftViewFlags = (props) => {
id="flags"
toggle={
<>
<HeaderMenuIcon name="flag" extraClasses="text-secondary" />
<span className="hidden lg:inline">
<HeaderMenuIcon name="flag" extraClasses="tw-text-secondary" />
<span className="tw-hidden lg:tw-inline">
Flags
<span>({count})</span>
</span>
@ -188,8 +161,8 @@ export const HeaderMenuDraftViewFlags = (props) => {
export const HeaderMenuDraftViewIcons = (props) => {
const { update } = props
const Button = HeaderMenuButton
const size = 'w-5 h-5'
const muted = 'text-current opacity-50'
const size = 'tw-w-5 tw-h-5'
const muted = 'tw-text-current tw-opacity-50'
const ux = props.state.ui.ux
const levels = {
...props.config.uxLevels.core,
@ -197,13 +170,15 @@ export const HeaderMenuDraftViewIcons = (props) => {
}
return (
<div className="flex flex-row flex-wrap items-center justify-center px-2">
<div className="tw-flex tw-flex-row tw-flex-wrap tw-items-center tw-justify-center tw-px-2">
{ux >= levels.sa ? (
<Button
updateHandler={update.toggleSa}
tooltip="Turns Seam Allowance on or off (see Core Settings)"
>
<SaIcon className={`${size} ${props.state.settings.sabool ? 'text-secondary' : muted}`} />
<SaIcon
className={`${size} ${props.state.settings.sabool ? 'tw-text-secondary' : muted}`}
/>
</Button>
) : null}
{ux >= levels.paperless ? (
@ -212,7 +187,7 @@ export const HeaderMenuDraftViewIcons = (props) => {
tooltip="Turns Paperless on or off (see Core Settings)"
>
<PaperlessIcon
className={`${size} ${props.state.settings.paperless ? 'text-secondary' : muted}`}
className={`${size} ${props.state.settings.paperless ? 'tw-text-secondary' : muted}`}
/>
</Button>
) : null}
@ -222,7 +197,7 @@ export const HeaderMenuDraftViewIcons = (props) => {
tooltip="Turns Details on or off (see Core Settings)"
>
<DetailIcon
className={`${size} ${!props.state.settings.complete ? 'text-secondary' : muted}`}
className={`${size} ${!props.state.settings.complete ? 'tw-text-secondary' : muted}`}
/>
</Button>
) : null}
@ -232,7 +207,7 @@ export const HeaderMenuDraftViewIcons = (props) => {
tooltip="Turns Expand on or off (see Core Settings)"
>
<ExpandIcon
className={`${size} ${props.state.settings.expand ? 'text-secondary' : muted}`}
className={`${size} ${props.state.settings.expand ? 'tw-text-secondary' : muted}`}
/>
</Button>
) : null}
@ -248,26 +223,28 @@ export const HeaderMenuDraftViewIcons = (props) => {
>
<UnitsIcon
className={`${size} ${
props.state.settings.units === 'imperial' ? 'text-secondary' : muted
props.state.settings.units === 'imperial' ? 'tw-text-secondary' : muted
}`}
/>
</Button>
) : null}
<HeaderMenuIconSpacer />
{ux >= levels.ux ? (
<div className="flex flex-row px-1">
<div className="tw-flex tw-flex-row tw-px-1">
<Tooltip tip="Changes your UX setting (see UI Preferences)">
{[0, 1, 2, 3, 4].map((i) => (
<button
key={i}
className="btn btn-ghost btn-sm px-0 -mx-0.5"
className="tw-daisy-btn tw-daisy-btn-ghost tw-daisy-btn-sm tw-px-0 tw--mx-0.5"
onClick={() => update.ui('ux', i + 1)}
>
<CircleIcon
key={i}
fill={i < props.state.ui.ux ? true : false}
className={`${size} ${
i < props.state.ui.ux ? 'stroke-secondary fill-secondary' : 'stroke-current'
i < props.state.ui.ux
? 'tw-stroke-secondary tw-fill-secondary'
: 'tw-stroke-current'
}`}
fillOpacity={0.3}
/>
@ -281,7 +258,7 @@ export const HeaderMenuDraftViewIcons = (props) => {
updateHandler={() => update.ui('aside', props.state.ui.aside ? 0 : 1)}
tooltip="Turn the Aside Menu on or off (see UI Preferences)"
>
<MenuIcon className={`${size} ${!props.state.ui.aside ? 'text-secondary' : muted}`} />
<MenuIcon className={`${size} ${!props.state.ui.aside ? 'tw-text-secondary' : muted}`} />
</Button>
) : null}
{ux >= levels.kiosk ? (
@ -289,7 +266,7 @@ export const HeaderMenuDraftViewIcons = (props) => {
updateHandler={() => update.ui('kiosk', props.state.ui.kiosk ? 0 : 1)}
tooltip="Turns Kiosk Mode on or off (see UI Preferences)"
>
<KioskIcon className={`${size} ${props.state.ui.kiosk ? 'text-secondary' : muted}`} />
<KioskIcon className={`${size} ${props.state.ui.kiosk ? 'tw-text-secondary' : muted}`} />
</Button>
) : null}
{ux >= levels.rotate ? (
@ -297,7 +274,9 @@ export const HeaderMenuDraftViewIcons = (props) => {
updateHandler={() => update.ui('rotate', props.state.ui.rotate ? 0 : 1)}
tooltip="Turns Rotate Pattern on or off (see UI Preferences)"
>
<RotateIcon className={`${size} ${props.state.ui.rotate ? 'text-secondary' : muted}`} />
<RotateIcon
className={`${size} ${props.state.ui.rotate ? 'tw-text-secondary' : muted}`}
/>
</Button>
) : null}
{ux >= levels.renderer ? (
@ -308,7 +287,7 @@ export const HeaderMenuDraftViewIcons = (props) => {
tooltip="Switches the Render Engine between React and SVG (see UI Preferences)"
>
<RocketIcon
className={`${size} ${props.state.ui.renderer === 'svg' ? 'text-secondary' : muted}`}
className={`${size} ${props.state.ui.renderer === 'svg' ? 'tw-text-secondary' : muted}`}
/>
</Button>
) : null}
@ -323,20 +302,20 @@ export const HeaderMenuUndoIcons = (props) => {
const undos = props.state._?.undos && props.state._.undos.length > 0 ? props.state._.undos : false
return (
<div className="flex flex-row flex-wrap items-center justify-center px-2">
<div className="tw-flex tw-flex-row tw-flex-wrap tw-items-center tw-justify-center tw-px-2">
<Button
updateHandler={() => update.restore(0, state._)}
tooltip="Undo the most recent change"
disabled={undos ? false : true}
>
<UndoIcon className={`${size} ${undos ? 'text-secondary' : ''}`} text="1" />
<UndoIcon className={`${size} ${undos ? 'tw-text-secondary' : ''}`} text="1" />
</Button>
<Button
updateHandler={() => update.restore(undos.length - 1, state._)}
tooltip="Undo all changes since the last save point"
disabled={undos ? false : true}
>
<UndoIcon className={`${size} ${undos ? 'text-secondary' : ''}`} text="A" />
<UndoIcon className={`${size} ${undos ? 'tw-text-secondary' : ''}`} text="A" />
</Button>
<HeaderMenuDropdown
{...props}
@ -345,13 +324,13 @@ export const HeaderMenuUndoIcons = (props) => {
disabled={undos ? false : true}
toggle={
<>
<UndoIcon className="w-4 h-4" stroke={3} />
<span className="hidden lg:inline">Undo</span>
<UndoIcon className="tw-w-4 tw-h-4" stroke={3} />
<span className="tw-hidden lg:tw-inline">Undo</span>
</>
}
>
{undos ? (
<ul className="dropdown-content bg-base-100 bg-opacity-90 z-20 shadow left-0 !fixed md:!absolute w-screen md:w-96 px-4 md:p-2 md:pt-0">
<ul className="tw-daisy-dropdown-content tw-bg-base-100 tw-bg-opacity-90 tw-z-20 tw-shadow tw-left-0 !tw-fixed md:!tw-absolute tw-w-screen md:tw-w-96 tw-px-4 md:tw-p-2 md:tw-pt-0">
{undos.slice(0, 9).map((step, index) => (
<li key={index}>
<UndoStep {...{ step, update, state, Design, index }} compact />
@ -364,9 +343,9 @@ export const HeaderMenuUndoIcons = (props) => {
return null /*update.state(index, state._) */
}}
>
<div className="flex flex-row items-center align-center justify-between gap-2 w-full">
<div className="flex flex-row items-center align-start gap-2 grow">
<UndoIcon className="w-5 h-5 text-secondary" />
<div className="tw-flex tw-flex-row tw-items-center tw-align-center tw-justify-between tw-gap-2 tw-w-full">
<div className="tw-flex tw-flex-row tw-items-center tw-align-start tw-gap-2 tw-grow">
<UndoIcon className="tw-w-5 tw-h-5 tw-text-secondary" />
{viewLabels.undo.t}
</div>
{undos.length}
@ -380,10 +359,10 @@ export const HeaderMenuUndoIcons = (props) => {
updateHandler={update.clearAll}
tooltip="Reset all settings, but keep the design and measurements"
>
<TrashIcon className={`${size} text-secondary`} />
<TrashIcon className={`${size} tw-text-secondary`} />
</Button>
<Button updateHandler={update.clearAll} tooltip="Reset the editor completely">
<ResetAllIcon className={`${size} text-secondary`} />
<ResetAllIcon className={`${size} tw-text-secondary`} />
</Button>
</div>
)
@ -392,23 +371,23 @@ export const HeaderMenuUndoIcons = (props) => {
export const HeaderMenuSaveIcons = (props) => {
const { update } = props
const Button = HeaderMenuButton
const size = 'w-5 h-5'
const size = 'tw-w-5 tw-h-5'
const saveable = props.state._?.undos && props.state._.undos.length > 0
return (
<div className="flex flex-row flex-wrap items-center justify-center px-2">
<div className="tw-flex tw-flex-row tw-flex-wrap tw-items-center tw-justify-center tw-px-2">
<Button
updateHandler={update.clearPattern}
tooltip="Save pattern"
disabled={saveable ? false : true}
>
<SaveIcon className={`${size} ${saveable ? 'text-success' : ''}`} />
<SaveIcon className={`${size} ${saveable ? 'tw-text-success' : ''}`} />
</Button>
<Button updateHandler={() => update.view('save')} tooltip="Save pattern as...">
<SaveAsIcon className={`${size} text-secondary`} />
<SaveAsIcon className={`${size} tw-text-secondary`} />
</Button>
<Button updateHandler={update.clearPattern} tooltip="Export pattern">
<ExportIcon className={`${size} text-secondary`} />
<ExportIcon className={`${size} tw-text-secondary`} />
</Button>
</div>
)
@ -417,14 +396,16 @@ export const HeaderMenuSaveIcons = (props) => {
export const HeaderMenuIcon = (props) => {
const { name, extraClasses = '' } = props
//const Icon = Swizzled.components[`${Swizzled.methods.capitalize(name)}Icon`] || Swizzled.components.Noop
return <ErrorIcon {...props} className={`h-5 w-5 ${extraClasses}`} />
return <ErrorIcon {...props} className={`tw-h-5 tw-w-5 ${extraClasses}`} />
}
export const HeaderMenuIconSpacer = () => <span className="px-1 font-bold opacity-30">|</span>
export const HeaderMenuIconSpacer = () => (
<span className="tw-px-1 tw-font-bold tw-opacity-30">|</span>
)
export const HeaderMenuButton = ({ updateHandler, children, tooltip, disabled = false }) => (
<Tooltip tip={tooltip}>
<button
className="btn btn-ghost btn-sm px-1 disabled:bg-transparent"
className="tw-daisy-btn tw-daisy-btn-ghost tw-daisy-btn-sm tw-px-1 disabled:tw-bg-transparent"
onClick={updateHandler}
disabled={disabled}
>
@ -455,18 +436,23 @@ export const HeaderMenuViewMenu = (props) => {
(config.measurementsFreeViews.includes(viewName) || state._.missingMeasurements.length < 1)
)
output.push(
<li key={i} className="mb-1 flex flex-row items-center justify-between w-full">
<li
key={i}
className="tw-mb-1 tw-flex tw-flex-row tw-items-center tw-justify-between tw-w-full"
>
<a
className={`w-full rounded-lg border-2 border-secondary text-base-content
flex flex-row items-center gap-2 md:gap-4 p-2
hover:cursor-pointer
hover:bg-secondary hover:bg-opacity-20 hover:border-solid ${
viewName === state.view ? 'bg-secondary border-solid bg-opacity-20' : 'border-dotted'
className={`tw-w-full tw-rounded-lg tw-border-2 tw-border-secondary tw-text-base-content
tw-flex tw-flex-row tw-items-center tw-gap-2 md:tw-gap-4 tw-p-2
hover:tw-cursor-pointer
hover:tw-bg-secondary hover:tw-bg-opacity-20 hover:tw-border-solid ${
viewName === state.view
? 'tw-bg-secondary tw-border-solid tw-bg-opacity-20'
: 'tw-border-dotted'
}`}
onClick={() => update.view(viewName)}
>
<ViewIcon view={viewName} className="w-6 h-6 grow-0" />
<b className="text-left grow">{viewLabels[viewName].t}</b>
<ViewIcon view={viewName} className="tw-w-6 tw-h-6 tw-grow-0" />
<b className="tw-text-left tw-grow">{viewLabels[viewName]?.t || viewName}</b>
</a>
</li>
)
@ -480,17 +466,52 @@ export const HeaderMenuViewMenu = (props) => {
id="views"
toggle={
<>
<HeaderMenuIcon name="right" stroke={3} extraClasses="text-secondary rotate-90" />
<span className="hidden lg:inline">Views</span>
<HeaderMenuIcon name="right" stroke={3} extraClasses="tw-text-secondary tw-rotate-90" />
<span className="tw-hidden lg:tw-inline">Views</span>
</>
}
>
<ul
tabIndex={i}
className="dropdown-content bg-base-100 bg-opacity-90 z-20 shadow left-0 !fixed md:!absolute w-screen md:w-96 px-4 md:p-2 md:pt-0"
className="tw-dropdown-content tw-bg-base-100 tw-bg-opacity-90 tw-z-20 tw-shadow tw-left-0 !tw-fixed md:!tw-absolute tw-w-screen md:tw-w-96 tw-px-4 md:tw-p-2 md:tw-pt-0"
>
{output}
</ul>
</HeaderMenuDropdown>
)
}
const headerMenus = {
draft: HeaderMenuDraftView,
//HeaderMenuDraftViewDesignOptions,
//HeaderMenuDraftViewCoreSettings,
//HeaderMenuDraftViewUiPreferences,
//HeaderMenuDraftViewFlags,
//HeaderMenuDraftViewIcons,
}
export const HeaderMenu = ({ config, Design, pattern, state, update }) => {
const [open, setOpen] = useState()
/*
* Guard views that require measurements agains missing measurements
* and make sure there's a view-specific header menu
*/
const ViewMenu =
!missingMeasurements(state) && headerMenus[state.view] ? headerMenus[state.view] : Null
return (
<div
className={`tw-flex tw-sticky tw-top-0 ${
state.ui.kiosk ? 'tw-z-50' : 'tw-z-20'
} tw-transition-[top] tw-duration-300 tw-ease-in-out`}
>
<div
className={`tw-flex tw-flex-row tw-flex-wrap tw-gap-1 md:tw-gap-4 tw-w-full tw-items-start tw-justify-center tw-border-b tw-border-base-300 tw-py-1 md:tw-py-1.5`}
>
<HeaderMenuAllViews {...{ config, state, update, open, setOpen }} />
<ViewMenu {...{ config, state, update, Design, pattern, open, setOpen }} />
</div>
</div>
)
}

View file

@ -1,5 +1,7 @@
import React, { useEffect } from 'react'
import { Spinner } from '@freesewing/react/components/Spinner'
import { TipIcon } from '@freesewing/react/components/Icon'
import { Null } from './Null.mjs'
const config = {
timeout: 2,
@ -9,6 +11,11 @@ const config = {
},
}
const icons = {
tip: TipIcon,
spinner: Spinner,
}
export const LoadingStatus = ({ state, update }) => {
useEffect(() => {
if (typeof state._.loading === 'object') {
@ -28,26 +35,26 @@ export const LoadingStatus = ({ state, update }) => {
if (!state._.loading || Object.keys(state._.loading).length < 1) return null
return (
<div className="fixed bottom-0 md:buttom-28 left-0 w-full z-30 md:px-4 md:mx-auto mb-4">
<div className="flex flex-col gap-2">
<div className="tw-fixed tw-bottom-4 md:tw-buttom-28 tw-left-0 tw-w-full tw-z-30 md:tw-px-4 md:tw-mx-auto mb-4">
<div className="tw-flex tw-flex-col tw-gap-2">
{Object.entries(state._.loading).map(([id, custom]) => {
const conf = {
...config.defaults,
...custom,
}
const Icon = typeof conf.icon === 'undefined' ? Spinner : Spinner //Swizzled.components[`${Swizzled.methods.capitalize(conf.icon)}Icon`] || Swizzled.components.Noop
const Icon = icons[conf.icon] ? icons[conf.icon] : Null
return (
<div
key={id}
className={`w-full md:max-w-2xl m-auto bg-${
className={`tw-w-full md:tw-max-w-2xl tw-m-auto tw-bg-${
conf.color
} flex flex-row items-center gap-4 p-4 px-4 ${
conf.fading ? 'opacity-0' : 'opacity-100'
} tw-flex tw-flex-row tw-items-center tw-gap-4 tw-p-4 tw-px-4 ${
conf.fading ? 'tw-opacity-0' : 'tw-opacity-100'
}
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`}
tw-transition-opacity tw-delay-[${config.timeout * 1000 - 400}ms] tw-duration-300
md:tw-rounded-lg tw-shadow tw-text-secondary-content tw-text-lg lg:tw-text-xl tw-font-medium md:tw-bg-opacity-90`}
>
<span className="shrink-0">
<span className="tw-shrink-0">
<Icon />
</span>
{conf.msg}

View file

@ -1,4 +1,6 @@
import React from 'react'
import { MeasurementInput } from '@freesewing/react/components/Input'
import { measurements as measurementsTranslations } from '@freesewing/i18n'
/**
* This MeasurementsEditor component allows inline-editing of the measurements
@ -19,8 +21,8 @@ export const MeasurementsEditor = ({ Design, update, state }) => {
}
return (
<div className="max-w-2xl">
<h5>Required Measurments</h5>
<div className="tw-max-w-2xl tw-mx-auto">
<h4>Required Measurements</h4>
{Object.keys(Design.patternConfig.measurements).length === 0 ? (
<p>This design does not require any measurements.</p>
) : (
@ -38,7 +40,7 @@ export const MeasurementsEditor = ({ Design, update, state }) => {
<br />
</div>
)}
<h5>Optional Measurements</h5>
<h4>Optional Measurements</h4>
{Object.keys(Design.patternConfig.optionalMeasurements).length === 0 ? (
<p>This design does not use any optional measurements.</p>
) : (

View file

@ -1,23 +1,24 @@
import React from 'react'
import { ZoomContextProvider } from './ZoomablePattern.mjs'
import { HeaderMenu } from './HeaderMenu.mjs'
/**
* A layout for views that include a drafted pattern
*
* @param {object} config - The editor configuration
* @param {object} settings - The pattern settings/state
* @param {object} ui - The UI settings/state
* @param {object} update - Object holding methods to manipulate state
* @param {function} Design - The Design contructor
* @param {object] pattern - The drafted pattern
* @param {object} props.Swizzled - An object holding swizzled code
*/
export const PatternLayout = (props) => {
const { menu = null, Design, pattern, update, Swizzled } = props
const { menu = null, Design, pattern, update, config } = props
return (
<Swizzled.components.ZoomContextProvider>
<ZoomContextProvider>
<div className="flex flex-col h-full">
<Swizzled.components.HeaderMenu
state={props.state}
{...{ Swizzled, update, Design, pattern }}
/>
<HeaderMenu state={props.state} {...{ update, Design, pattern, config }} />
<div className="flex lg:flex-row grow lg:max-h-[90vh] max-h-[calc(100vh-3rem)] h-full py-4 lg:mt-6">
<div className="lg:w-2/3 flex flex-col h-full grow px-4">{props.output}</div>
{menu ? (
@ -29,6 +30,6 @@ export const PatternLayout = (props) => {
) : null}
</div>
</div>
</Swizzled.components.ZoomContextProvider>
</ZoomContextProvider>
)
}

View file

@ -1,33 +1,32 @@
import { useState, useEffect } from 'react'
import orderBy from 'lodash.orderby'
// Dependencies
import { capitalize, cloudflareImageUrl, hasRequiredMeasurements, orderBy } from '@freesewing/utils'
// Hooks
import React, { useState, useEffect } from 'react'
import { useBackend } from '@freesewing/react/hooks/useBackend'
// Components
import { Popout } from '@freesewing/react/components/Popout'
import { MsetCard } from '@freesewing/react/components/Account'
export const UserSetPicker = ({
Design,
clickHandler,
missingClickHandler,
size = 'lg',
Swizzled,
config,
}) => {
// Swizzled components
const { Popout, MeasurementsSetCard } = Swizzled.components
// Swizzled hooks
const { useBackend } = Swizzled.hooks
// Hooks
const backend = useBackend()
// Swizzled methods
const { t, hasRequiredMeasurements } = Swizzled.methods
// Swizzled config
const { config } = Swizzled
// Local state
// State (local)
const [sets, setSets] = useState({})
// Effects
useEffect(() => {
const getSets = async () => {
const result = await backend.getSets()
if (result.success) {
const [status, body] = await backend.getSets()
if (status === 200 && body.result === 'success') {
const all = {}
for (const set of result.data.sets) all[set.id] = set
for (const set of body.sets) all[set.id] = set
setSets(all)
}
}
@ -47,21 +46,26 @@ export const UserSetPicker = ({
if (!hasSets)
return (
<div className="w-full max-w-3xl mx-auto">
<div className="tw-w-full tw-max-w-3xl tw-mx-auto">
<Popout tip>
<h5>{t('pe:noOwnSets')}</h5>
<p className="">{t('pe:noOwnSetsMsg')}</p>
<h5> You do not (yet) have any of your own measurements sets</h5>
<p>
You can store your measurements as a measurements set, after which you can generate as
many patterns as you want for them.
</p>
{config.hrefNewSet ? (
<a
href={config.hrefNewSet}
className="btn btn-accent capitalize"
className="tw-daisy-btn tw-daisy-btn-accent tw-capitalize"
target="_BLANK"
rel="nofollow"
>
{t('pe:newSet')}
Create a new measurements set
</a>
) : null}
<p className="text-sm">{t('pe:pleaseMtm')}</p>
<p className="tw-text-sm">
Because our patterns are bespoke, we strongly suggest you take accurate measurements.
</p>
</Popout>
</div>
)
@ -69,10 +73,11 @@ export const UserSetPicker = ({
return (
<>
{okSets.length > 0 && (
<div className="flex flex-row flex-wrap gap-2">
<div className="tw-flex tw-flex-row tw-flex-wrap tw-gap-2">
{okSets.map((set) => (
<MeasurementsSetCard
<MsetCard
href={false}
design={Design.designConfig.data.id}
{...{ set, Design, config }}
onClick={clickHandler}
key={set.id}
@ -82,14 +87,17 @@ export const UserSetPicker = ({
</div>
)}
{lackingSets.length > 0 ? (
<div className="my-4">
<Popout note compact>
{t('pe:someSetsLacking')}
<div className="tw-my-4">
<Popout note>
<h5>
Some of your measurements sets lack the measurements required to generate this pattern
</h5>
</Popout>
<div className="grid grid-cols-1 md:grid-cols-2 2xl:grid-cols-3 gap-2">
<div className="tw-grid tw-grid-cols-2 md:tw-grid-cols-4 lg:tw-grid-cols-6 tw-gap-2">
{lackingSets.map((set) => (
<MeasurementsSetCard
<MsetCard
{...{ set, Design }}
design={Design.designConfig.data.id}
onClick={missingClickHandler}
href={false}
key={set.id}
@ -103,41 +111,26 @@ export const UserSetPicker = ({
)
}
export const BookmarkedSetPicker = ({
Design,
clickHandler,
missingClickHandler,
size = 'lg',
Swizzled,
}) => {
// Swizzled components
const { Popout, MeasurementsSetCard } = Swizzled.components
// Swizzled hooks
const { useBackend } = Swizzled.hooks
export const BookmarkedSetPicker = ({ Design, clickHandler, missingClickHandler, size = 'lg' }) => {
// Hooks
const backend = useBackend()
// Swizzled methods
const { t, hasRequiredMeasurements } = Swizzled.methods
// Swizzled config
const { config } = Swizzled
// Local state
// State (local)
const [sets, setSets] = useState({})
// Effects
useEffect(() => {
const getBookmarks = async () => {
const result = await backend.getBookmarks()
const [status, body] = await backend.getBookmarks()
const loadedSets = {}
if (result.success) {
for (const bookmark of result.data.bookmarks.filter(
(bookmark) => bookmark.type === 'set'
)) {
if (status === 200 && body.result === 'success') {
for (const bookmark of body.bookmarks.filter((bookmark) => bookmark.type === 'set')) {
let set
try {
set = await backend.getSet(bookmark.url.slice(6))
if (set.success) {
const [hasMeasies] = hasRequiredMeasurements(Design, set.data.set.measies)
loadedSets[set.data.set.id] = { ...set.data.set, hasMeasies }
const [status, body] = await backend.getSet(bookmark.url.slice(6))
if (status === 200 && body.result === 'success') {
const [hasMeasies] = hasRequiredMeasurements(Design, body.set.measies)
loadedSets[body.set.id] = { ...body.set, hasMeasies }
}
} catch (err) {
console.log(err)
@ -155,11 +148,12 @@ export const BookmarkedSetPicker = ({
return (
<>
{okSets.length > 0 && (
<div className="flex flex-row flex-wrap gap-2">
<div className="tw-grid tw-grid-cols-2 md:tw-grid-cols-4 lg:tw-grid-cols-6 tw-gap-2">
{okSets.map((set) => (
<MeasurementsSetCard
<MsetCard
href={false}
{...{ set, Design, config }}
design={Design.designConfig.data.id}
onClick={clickHandler}
key={set.id}
size={size}
@ -168,15 +162,19 @@ export const BookmarkedSetPicker = ({
</div>
)}
{lackingSets.length > 0 && (
<div className="my-4">
<Popout note compact>
{t('pe:someSetsLacking')}
<div className="tw-my-4">
<Popout note>
<h5>
Some of these measurements sets lack the measurements required to generate this
pattern
</h5>
</Popout>
<div className="grid grid-cols-1 md:grid-cols-2 2xl:grid-cols-3 gap-2">
<div className="tw-grid tw-grid-cols-2 md:tw-grid-cols-4 lg:tw-grid-cols-6 tw-gap-2">
{lackingSets.map((set) => (
<MeasurementsSetCard
<MsetCard
href={false}
{...{ set, Design }}
design={Design.designConfig.data.id}
onClick={missingClickHandler}
key={set.id}
size={size}
@ -189,23 +187,20 @@ export const BookmarkedSetPicker = ({
)
}
export const CuratedSetPicker = ({ clickHandler, Swizzled, locale }) => {
// Swizzled components
const { CuratedMeasurementsSetLineup } = Swizzled.components
// Swizzled hooks
const { useBackend } = Swizzled.hooks
export const CuratedSetPicker = ({ clickHandler }) => {
// Hooks
const backend = useBackend()
// Local state
// State (local)
const [sets, setSets] = useState([])
// Effects
useEffect(() => {
const getSets = async () => {
const result = await backend.getCuratedSets()
if (result.success) {
const [status, body] = await backend.getCuratedSets()
if (status === 200 && body.result === 'success') {
const allSets = {}
for (const set of result.data.curatedSets) {
for (const set of body.curatedSets) {
if (set.published) allSets[set.id] = set
}
setSets(allSets)
@ -215,11 +210,50 @@ export const CuratedSetPicker = ({ clickHandler, Swizzled, locale }) => {
}, [])
return (
<div className="max-w-7xl xl:pl-4">
<div className="tw-max-w-7xl">
<CuratedMeasurementsSetLineup
{...{ locale, clickHandler }}
clickHandler={clickHandler}
sets={orderBy(sets, 'height', 'asc')}
/>
</div>
)
}
export const CuratedMeasurementsSetLineup = ({ sets = [], clickHandler }) => (
<div
className={`tw-w-full tw-flex tw-flex-row ${
sets.length > 1 ? 'tw-justify-start tw-px-8' : 'tw-justify-center'
} tw-overflow-x-scroll`}
style={{
backgroundImage: `url(/img/lineup-backdrop.svg)`,
width: 'auto',
backgroundSize: 'auto 100%',
backgroundRepeat: 'repeat-x',
}}
>
{sets.map((set) => {
const props = {
className:
'tw-aspect-[1/3] tw-w-auto tw-h-96 tw-bg-transparent tw-border-0 hover:tw-cursor-pointer tw-grayscale hover:tw-grayscale-0',
style: {
backgroundImage: `url(${cloudflareImageUrl({
id: `cset-${set.id}`,
type: 'lineup',
})})`,
width: 'auto',
backgroundSize: 'contain',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center',
},
onClick: () => clickHandler(set),
}
return (
<div className="tw-flex tw-flex-col tw-items-center" key={set.id}>
<button {...props} key={set.id}></button>
<b>{set.nameEn}</b>
</div>
)
})}
</div>
)

View file

@ -1,4 +1,11 @@
import { useState, useMemo } from 'react'
// Dependencies
import { menuValueWasChanged } from '../../lib/index.mjs'
// Hooks
import React, { useState, useMemo } from 'react'
// Components
import { SubAccordion } from '../Accordion.mjs'
import { GroupIcon, OptionsIcon } from '@freesewing/react/components/Icon'
import { CoreSettingsMenu } from './CoreSettingsMenu.mjs'
/** @type {String} class to apply to buttons on open menu items */
const iconButtonClass = 'btn btn-xs btn-ghost px-0 text-accent'
@ -17,11 +24,9 @@ const iconButtonClass = 'btn btn-xs btn-ghost px-0 text-accent'
* @param {React.Component} Value a value display component this menu item will use
* @param {Boolean} allowOverride all a text input to be used to override the given input component
* @param {Number} ux the user-defined ux level
* @param {object} props.Swizzled - An object holding swizzled code
*/
export const MenuItem = ({
name,
Swizzled,
current,
updateHandler,
passProps = {},
@ -44,7 +49,6 @@ export const MenuItem = ({
ux,
current,
updateHandler,
t: Swizzled.methods.t,
changed,
override,
Design,
@ -73,7 +77,7 @@ export const MenuItem = ({
setOverride(!override)
}}
>
<Swizzled.components.EditIcon
<EditIcon
className={`w-6 h-6 ${
override ? 'bg-secondary text-secondary-content rounded' : 'text-secondary'
}`}
@ -89,28 +93,28 @@ export const MenuItem = ({
updateHandler([name], '__UNSET__')
}}
>
<Swizzled.components.ResetIcon />
<ResetIcon />
</button>
)
buttons.push(<ResetButton open disabled={!changed} key="clear" />)
return (
<Swizzled.components.FormControl
label={<span className="text-base font-normal">{Swizzled.methods.t(`${name}.d`)}</span>}
<FormControl
label={<span className="text-base font-normal">{name}.d</span>}
id={config.name}
labelBR={<div className="flex flex-row items-center gap-2">{buttons}</div>}
labelBL={
<span
className={`text-base font-medium -mt-2 block ${changed ? 'text-accent' : 'opacity-50'}`}
>
{Swizzled.methods.t(`pe:youAreUsing${changed ? 'ACustom' : 'TheDefault'}Value`)}
pe:youAreUsing{changed ? 'ACustom' : 'TheDefault'}Value
</span>
}
docs={docs}
>
<Input {...drillProps} />
</Swizzled.components.FormControl>
</FormControl>
)
}
@ -132,8 +136,6 @@ export const MenuItem = ({
* @param {Function} updateHandler the function called by change handlers on inputs within menu items
* @param {Boolean} topLevel is this group the top level group? false for nested
* @param {Function} t translation function
* @param {object} props.Swizzled - An object holding swizzled code
*/
export const MenuItemGroup = ({
collapsible = true,
@ -149,10 +151,9 @@ export const MenuItemGroup = ({
topLevel = false,
isDesignOptionsGroup = false,
Design,
Swizzled,
state,
}) => {
if (!Item) Item = Swizzled.components.MenuItem
if (!Item) Item = MenuItem
// map the entries in the structure
const content = Object.entries(structure).map(([itemName, item]) => {
@ -164,7 +165,7 @@ export const MenuItemGroup = ({
const ItemIcon = item.icon
? item.icon
: item.isGroup
? Swizzled.components.GroupIcon
? GroupIcon
: Icon
? Icon
: () => <span role="img">fixme-icon</span>
@ -172,11 +173,11 @@ export const MenuItemGroup = ({
? () => (
<div className="flex flex-row gap-2 items-center font-medium">
{Object.keys(item).filter((i) => i !== 'isGroup').length}
<Swizzled.components.OptionsIcon className="w-5 h-5" />
<OptionsIcon className="w-5 h-5" />
</div>
)
: isDesignOptionsGroup
? values[Swizzled.methods.designOptionType(item)]
? values[designOptionType(item)]
: values[itemName]
? values[itemName]
: () => <span>¯\_()_/¯</span>
@ -186,15 +187,14 @@ export const MenuItemGroup = ({
<div className="flex flex-row items-center gap-4 w-full">
<ItemIcon />
<span className="font-medium">
{Swizzled.methods.t([`pe:${itemName}.t`, `pe:${itemName}`])}
pe:{itemName}.t pe:{itemName}
</span>
</div>
<div className="font-bold">
<Value
current={currentValues[itemName]}
config={item}
t={Swizzled.methods.t}
changed={Swizzled.methods.menuValueWasChanged(currentValues[itemName], item)}
changed={menuValueWasChanged(currentValues[itemName], item)}
Design={Design}
/>
</div>
@ -218,7 +218,6 @@ export const MenuItemGroup = ({
updateHandler,
isDesignOptionsGroup,
Design,
Swizzled,
}}
/>
) : (
@ -229,13 +228,12 @@ export const MenuItemGroup = ({
current: currentValues[itemName],
config: item,
state,
changed: Swizzled.methods.menuValueWasChanged(currentValues[itemName], item),
changed: menuValueWasChanged(currentValues[itemName], item),
Value: values[itemName],
Input: inputs[itemName],
updateHandler,
passProps,
Design,
Swizzled,
}}
/>
),
@ -243,7 +241,7 @@ export const MenuItemGroup = ({
]
})
return <Swizzled.components.SubAccordion items={content.filter((item) => item !== null)} />
return <SubAccordion items={content.filter((item) => item !== null)} />
}
/**
@ -254,18 +252,11 @@ export const MenuItemGroup = ({
* @param {Boolean} options.open is the menu item open?
* @param {String} options.emoji the emoji icon of the menu item
*/
export const MenuItemTitle = ({
name,
current = null,
open = false,
emoji = '',
Icon = false,
Swizzled,
}) => (
export const MenuItemTitle = ({ name, current = null, open = false, emoji = '', Icon = false }) => (
<div className={`flex flex-row gap-1 items-center w-full ${open ? '' : 'justify-between'}`}>
<span className="font-medium capitalize flex flex-row gap-2">
{Icon ? <Icon /> : <span role="img">{emoji}</span>}
{Swizzled.methods.t([`${name}.t`, name])}
fixme: {name}
</span>
<span className="font-bold">{current}</span>
</div>

View file

@ -1,3 +1,11 @@
import React from 'react'
import {
menuCoreSettingsOnlyHandler,
menuCoreSettingsSaboolHandler,
menuCoreSettingsSammHandler,
menuCoreSettingsStructure,
} from '../../lib/index.mjs'
/**
* The core settings menu
* @param {Object} options.update settings and ui update functions
@ -7,8 +15,8 @@
* @param {Object} options.account the user account data
* @param {object} props.Swizzled - An object holding swizzled code
*/
export const CoreSettingsMenu = ({ update, state, language, Design, Swizzled }) => {
const structure = Swizzled.methods.menuCoreSettingsStructure({
export const CoreSettingsMenu = ({ update, state, language, Design }) => {
const structure = menuCoreSettingsStructure({
language,
units: state.settings?.units,
sabool: state.settings?.sabool,
@ -16,42 +24,38 @@ export const CoreSettingsMenu = ({ update, state, language, Design, Swizzled })
})
const inputs = {
complete: Swizzled.components.MenuListInput,
expand: Swizzled.components.MenuListInput,
margin: Swizzled.components.MenuMmInput,
only: Swizzled.components.MenuOnlySettingInput,
paperless: Swizzled.components.MenuBoolInput,
sabool: Swizzled.components.MenuBoolInput,
samm: Swizzled.components.MenuMmInput,
scale: Swizzled.components.MenuSliderInput,
units: Swizzled.components.MenuBoolInput,
complete: MenuListInput,
expand: MenuListInput,
margin: MenuMmInput,
only: MenuOnlySettingInput,
paperless: MenuBoolInput,
sabool: MenuBoolInput,
samm: MenuMmInput,
scale: MenuSliderInput,
units: MenuBoolInput,
}
const values = {
complete: Swizzled.components.MenuListValue,
expand: Swizzled.components.MenuListValue,
margin: Swizzled.components.MenuMmValue,
only: Swizzled.components.MenuOnlySettingValue,
paperless: Swizzled.components.MenuListValue,
sabool: Swizzled.components.MenuListValue,
samm: Swizzled.components.MenuMmValue,
scale: Swizzled.components.MenuScaleSettingValue,
units: Swizzled.components.MenuListValue,
complete: MenuListValue,
expand: MenuListValue,
margin: MenuMmValue,
only: MenuOnlySettingValue,
paperless: MenuListValue,
sabool: MenuListValue,
samm: MenuMmValue,
scale: MenuScaleSettingValue,
units: MenuListValue,
}
return (
<Swizzled.components.MenuItemGroup
<MenuItemGroup
{...{
structure,
ux: state.ui.ux,
currentValues: state.settings || {},
Icon: Swizzled.components.SettingsIcon,
Icon: SettingsIcon,
Item: (props) => (
<Swizzled.components.CoreSetting
updateHandler={update}
{...{ inputs, values, Swizzled, Design }}
{...props}
/>
<CoreSetting updateHandler={update} {...{ inputs, values, Design }} {...props} />
),
isFirst: true,
name: 'pe:designOptions',
@ -64,7 +68,6 @@ export const CoreSettingsMenu = ({ update, state, language, Design, Swizzled })
},
updateHandler: update.settings,
isDesignOptionsGroup: false,
Swizzled,
state,
Design,
inputs,
@ -74,17 +77,15 @@ export const CoreSettingsMenu = ({ update, state, language, Design, Swizzled })
)
}
// Facilitate custom handlers for core settings
const coreSettingsHandlerMethods = {
only: menuCoreSettingsOnlyHandler,
sabool: menuCoreSettingsSaboolHandler,
samm: menuCoreSettingsSammHandler,
}
/** A wrapper for {@see MenuItem} to handle core settings-specific business */
export const CoreSetting = ({
Swizzled,
name,
config,
ux,
updateHandler,
current,
passProps,
...rest
}) => {
export const CoreSetting = ({ name, config, ux, updateHandler, current, passProps, ...rest }) => {
// is toggling allowed?
const allowToggle = ux > 3 && config.list?.length === 2
@ -98,10 +99,8 @@ export const CoreSetting = ({
/*
* Load a specific update handler if one is configured
*/
const handler = Swizzled.config.menuCoreSettingsHandlerMethods?.[name.toLowerCase()]
? Swizzled.methods[Swizzled.config.menuCoreSettingsHandlerMethods[name.toLowerCase()]](
handlerArgs
)
const handler = coreSettingsHandlerMethods[name.toLowerCase()]
? coreSettingsHandlerMethods[name.toLowerCase()](handlerArgs)
: updateHandler
return (

View file

@ -1,16 +1,39 @@
import { useCallback, useMemo } from 'react'
// Dependencies
import { menuDesignOptionsStructure } from '../../lib/index.mjs'
// Hooks
import React, { useCallback, useMemo } from 'react'
// Components
import {
MenuBoolInput,
MenuConstantInput,
MenuSliderInput,
MenuDegInput,
MenuListInput,
MenuPctInput,
} from './Input.mjs'
import {
MenuBoolValue,
MenuConstantOptionValue,
MenuCountOptionValue,
MenuDegOptionValue,
MenuListOptionValue,
MenyMmOptionValue,
MenuPctOptionValue,
} from './Value.mjs'
import { MenuItemGroup } from './Container.mjs'
import { OptionsIcon } from '@freesewing/react/components/Icon'
//
/**
* The design options menu
* @param {object} props.Design - An object holding the Design instance
* @param {String} props.isFirst - Boolean indicating whether this is the first/top entry of the menu
* @param {Object} props.state - Object holding state
* @param {Object} props.update - Object holding state handlers
* @param {object} props.Swizzled - An object holding swizzled code
*/
export const DesignOptionsMenu = ({ Design, isFirst = true, state, update, Swizzled }) => {
export const DesignOptionsMenu = ({ Design, isFirst = true, state, update }) => {
const structure = useMemo(
() => Swizzled.methods.menuDesignOptionsStructure(Design.patternConfig.options, state.settings),
() => menuDesignOptionsStructure(Design.patternConfig.options, state.settings),
[Design.patternConfig, state.settings]
)
const updateHandler = useCallback(
@ -20,45 +43,34 @@ export const DesignOptionsMenu = ({ Design, isFirst = true, state, update, Swizz
const drillProps = { Design, state, update }
const inputs = {
bool: (props) => <Swizzled.components.MenuBoolInput {...drillProps} {...props} />,
constant: (props) => <Swizzled.components.MenuConstantInput {...drillProps} {...props} />,
bool: (props) => <MenuBoolInput {...drillProps} {...props} />,
constant: (props) => <MenuConstantInput {...drillProps} {...props} />,
count: (props) => (
<Swizzled.components.MenuSliderInput
{...drillProps}
{...props}
config={{ ...props.config, step: 1 }}
/>
),
deg: (props) => <Swizzled.components.MenuDegInput {...drillProps} {...props} />,
list: (props) => (
<Swizzled.components.MenuListInput {...drillProps} {...props} isDesignOption />
<MenuSliderInput {...drillProps} {...props} config={{ ...props.config, step: 1 }} />
),
deg: (props) => <MenuDegInput {...drillProps} {...props} />,
list: (props) => <MenuListInput {...drillProps} {...props} isDesignOption />,
mm: () => <span>FIXME: Mm options are deprecated. Please report this </span>,
pct: (props) => <Swizzled.components.MenuPctInput {...drillProps} {...props} />,
pct: (props) => <MenuPctInput {...drillProps} {...props} />,
}
const values = {
bool: (props) => <Swizzled.components.MenuBoolValue {...drillProps} {...props} />,
constant: (props) => <Swizzled.components.MenuConstantOptionValue {...drillProps} {...props} />,
count: (props) => <Swizzled.components.MenuCountOptionValue {...drillProps} {...props} />,
deg: (props) => <Swizzled.components.MenuDegOptionValue {...drillProps} {...props} />,
list: (props) => <Swizzled.components.MenuListOptionValue {...drillProps} {...props} />,
mm: (props) => <Swizzled.components.MenuMmOptionValue {...drillProps} {...props} />,
pct: (props) => <Swizzled.components.MenuPctOptionValue {...drillProps} {...props} />,
bool: (props) => <MenuBoolValue {...drillProps} {...props} />,
constant: (props) => <MenuConstantOptionValue {...drillProps} {...props} />,
count: (props) => <MenuCountOptionValue {...drillProps} {...props} />,
deg: (props) => <MenuDegOptionValue {...drillProps} {...props} />,
list: (props) => <MenuListOptionValue {...drillProps} {...props} />,
mm: (props) => <MenuMmOptionValue {...drillProps} {...props} />,
pct: (props) => <MenuPctOptionValue {...drillProps} {...props} />,
}
return (
<Swizzled.components.MenuItemGroup
<MenuItemGroup
{...{
structure,
ux: state.ui.ux,
currentValues: state.settings.options || {},
Icon: Swizzled.components.OptionsIcon,
Item: (props) => (
<Swizzled.components.DesignOption
{...{ inputs, values, Swizzled, update, Design }}
{...props}
/>
),
Icon: OptionsIcon,
Item: (props) => <DesignOption {...{ inputs, values, update, Design }} {...props} />,
isFirst,
name: 'pe:designOptions',
language: state.locale,
@ -69,7 +81,6 @@ export const DesignOptionsMenu = ({ Design, isFirst = true, state, update, Swizz
},
updateHandler,
isDesignOptionsGroup: true,
Swizzled,
state,
Design,
inputs,
@ -85,8 +96,8 @@ export const DesignOptionsMenu = ({ Design, isFirst = true, state, update, Swizz
* @param {Object} options.settings core settings
* @param {Object} options.rest the rest of the props
*/
export const DesignOption = ({ config, settings, ux, inputs, values, Swizzled, ...rest }) => {
const type = Swizzled.methods.designOptionType(config)
export const DesignOption = ({ config, settings, ux, inputs, values, ...rest }) => {
const type = designOptionType(config)
const Input = inputs[type]
const Value = values[type]
const allowOverride = ['pct', 'count', 'deg'].includes(type)
@ -96,12 +107,11 @@ export const DesignOption = ({ config, settings, ux, inputs, values, Swizzled, .
if (config?.hide || (typeof config?.hide === 'function' && config.hide(settings))) return null
return (
<Swizzled.components.MenuItem
<MenuItem
{...{
config,
ux,
...rest,
Swizzled,
Input,
Value,
allowOverride,

View file

@ -1,11 +1,13 @@
import { useMemo, useCallback, useState } from 'react'
import React, { useMemo, useCallback, useState } from 'react'
import { round } from '@freesewing/utils'
import { ButtonFrame } from '@freesewing/react/components/Input'
/** A boolean version of {@see MenuListInput} that sets up the necessary configuration */
export const MenuBoolInput = (props) => {
const { name, config, Swizzled } = props
const { name, config } = props
const boolConfig = useBoolConfig(name, config)
return <Swizzled.components.MenuListInput {...props} config={boolConfig} />
return <MenuListInput {...props} config={boolConfig} />
}
/** A placeholder for an input to handle constant values */
@ -33,8 +35,6 @@ export const MenuConstantInput = ({
/** A {@see MenuSliderInput} to handle degree values */
export const MenuDegInput = (props) => {
const { updateHandler } = props
const { MenuSliderInput } = props.Swizzled.components
const { round } = props.Swizzled.methods
const degUpdateHandler = useCallback(
(path, newVal) => {
updateHandler(path, newVal === undefined ? undefined : Number(newVal))
@ -67,7 +67,6 @@ export const MenuListInput = ({
changed,
design,
isDesignOption = false,
Swizzled,
}) => {
const handleChange = useSharedHandlers({
dflt: config.dflt,
@ -87,7 +86,7 @@ export const MenuListInput = ({
const sideBySide = config.sideBySide || desc.length + title.length < 42
return (
<Swizzled.components.ButtonFrame
<ButtonFrame
dense={config.dense || false}
key={entry}
active={
@ -107,7 +106,7 @@ export const MenuListInput = ({
<div className="font-bold text-lg shrink-0">{title}</div>
{compact ? null : <div className="text-base font-normal">{desc}</div>}
</div>
</Swizzled.components.ButtonFrame>
</ButtonFrame>
)
})
}
@ -137,13 +136,10 @@ export const MenuListToggle = ({ config, changed, updateHandler, name }) => {
export const MenuMmInput = (props) => {
const { units, updateHandler, current, config } = props
const { MenuSliderInput } = props.Swizzled.components
const mmUpdateHandler = useCallback(
(path, newCurrent) => {
const calcCurrent =
typeof newCurrent === 'undefined'
? undefined
: props.Swizzled.methods.measurementAsMm(newCurrent, units)
typeof newCurrent === 'undefined' ? undefined : measurementAsMm(newCurrent, units)
updateHandler(path, calcCurrent)
},
[updateHandler, units]
@ -159,15 +155,11 @@ export const MenuMmInput = (props) => {
config: {
step: defaultStep,
...config,
dflt: props.Swizzled.methods.measurementAsUnits(config.dflt, units),
dflt: measurementAsUnits(config.dflt, units),
},
current:
current === undefined
? undefined
: props.Swizzled.methods.measurementAsUnits(current, units),
current: current === undefined ? undefined : measurementAsUnits(current, units),
updateHandler: mmUpdateHandler,
valFormatter: (val) =>
units === 'imperial' ? props.Swizzle.methods.formatFraction128(val, null) : val,
valFormatter: (val) => (units === 'imperial' ? formatFraction128(val, null) : val),
suffix: units === 'imperial' ? '"' : 'cm',
}}
/>
@ -243,27 +235,24 @@ export const MenuMmInput = (props) => {
//}
/** A {@see SliderInput} to handle percentage values */
export const MenuPctInput = ({ current, changed, updateHandler, config, Swizzled, ...rest }) => {
export const MenuPctInput = ({ current, changed, updateHandler, config, ...rest }) => {
const factor = 100
let pctCurrent = changed ? Swizzled.methods.menuRoundPct(current, factor) : current
let pctCurrent = changed ? menuRoundPct(current, factor) : current
const pctUpdateHandler = useCallback(
(path, newVal) =>
updateHandler(
path,
newVal === undefined ? undefined : Swizzled.methods.menuRoundPct(newVal, 1 / factor)
),
updateHandler(path, newVal === undefined ? undefined : menuRoundPct(newVal, 1 / factor)),
[updateHandler]
)
return (
<Swizzled.components.MenuSliderInput
<MenuSliderInput
{...{
...rest,
config: { ...config, dflt: Swizzled.methods.menuRoundPct(config.dflt, factor) },
config: { ...config, dflt: menuRoundPct(config.dflt, factor) },
current: pctCurrent,
updateHandler: pctUpdateHandler,
suffix: '%',
valFormatter: Swizzled.methods.round,
valFormatter: round,
changed,
}}
/>
@ -293,7 +282,6 @@ export const MenuSliderInput = ({
setReset,
children,
changed,
Swizzled,
}) => {
const { max, min } = config
const handleChange = useSharedHandlers({
@ -310,10 +298,9 @@ export const MenuSliderInput = ({
return (
<>
<div className="flex flex-row justify-between">
<Swizzled.components.MenuEditOption
<MenuEditOption
{...{
config,
Swizzled,
current: val,
handleChange,
min,
@ -354,8 +341,8 @@ export const MenuSliderInput = ({
export const MenuEditOption = (props) => {
const [manualEdit, setManualEdit] = useState(props.current)
const { config, handleChange, Swizzled } = props
const type = Swizzled.methods.designOptionType(config)
const { config, handleChange } = props
const type = designOptionType(config)
const onUpdate = useCallback(
(validVal) => {
@ -370,14 +357,12 @@ export const MenuEditOption = (props) => {
return (
<div className="form-control mb-2 w-full">
<label className="label font-medium text-accent">
<em>
{Swizzled.methods.t('pe:enterCustomValue')} ({Swizzled.config.menuOptionEditLabels[type]})
</em>
<em>Enter a custom value ({config.menuOptionEditLabels[type]})</em>
</label>
<label className="input-group input-group-sm flex flex-row items-center gap-2 -mt-4">
<Swizzled.components.NumberInput value={manualEdit} update={setManualEdit} />
<NumberInput value={manualEdit} update={setManualEdit} />
<button className="btn btn-secondary mt-4" onClick={() => onUpdate(manualEdit)}>
<Swizzled.components.ApplyIcon />
<ApplyIcon />
</button>
</label>
</div>
@ -427,16 +412,13 @@ const useBoolConfig = (name, config) => {
/** an input for the 'only' setting. toggles individual parts*/
export const MenuOnlySettingInput = (props) => {
const { Swizzled, config } = props
const { t } = Swizzled.methods
const { config } = props
config.sideBySide = true
config.titleMethod = (entry, t) => {
const chunks = entry.split('.')
return <span className="font-medium text-base">{t(`${chunks[0]}:${chunks[1]}`)}</span>
}
config.valueMethod = (entry) => (
<span className="text-sm">{Swizzled.methods.capitalize(entry.split('.')[0])}</span>
)
config.valueMethod = (entry) => <span className="text-sm">{capitalize(entry.split('.')[0])}</span>
config.dense = true
// Sort alphabetically (translated)
const order = []
@ -447,13 +429,11 @@ export const MenuOnlySettingInput = (props) => {
order.sort()
config.list = order.map((entry) => entry.split('|')[1])
return <Swizzled.components.MenuListInput {...props} />
return <MenuListInput {...props} />
}
export const MenuUxSettingInput = (props) => {
const { state, update, Swizzled } = props
const { state, update } = props
return (
<Swizzled.components.MenuListInput {...props} updateHandler={update.ui} current={state.ui.ux} />
)
return <MenuListInput {...props} updateHandler={update.ui} current={state.ui.ux} />
}

View file

@ -1,3 +1,4 @@
import React from 'react'
import { mergeOptions } from '@freesewing/core'
/** Displays that constant values are not implemented in the front end */
@ -6,16 +7,13 @@ export const MenuConstantOptionValue = () => (
)
/** Displays a count value*/
export const MenuCountOptionValue = ({ Swizzled, config, current, changed }) => (
<Swizzled.components.MenuShowValue {...{ current, changed, dflt: config.count }} />
export const MenuCountOptionValue = ({ config, current, changed }) => (
<MenuShowValue {...{ current, changed, dflt: config.count }} />
)
/** Displays a degree value */
export const MenuDegOptionValue = ({ config, current, changed, Swizzled }) => (
<Swizzled.components.MenuHighlightValue changed={changed}>
{' '}
{changed ? current : config.deg}&deg;
</Swizzled.components.MenuHighlightValue>
export const MenuDegOptionValue = ({ config, current, changed }) => (
<MenuHighlightValue changed={changed}> {changed ? current : config.deg}&deg;</MenuHighlightValue>
)
/**
@ -29,10 +27,7 @@ export const MenuHighlightValue = ({ changed, children }) => (
/** Displays a list option value */
export const MenuListOptionValue = (props) => (
<MenuListValue
{...props}
t={(input) => props.Swizzled.methods.t(`${props.design}:${props.config.name}.${input}.t`)}
/>
<MenuListValue {...props} t={(input) => 'fixme handle option translation'} />
)
/**
@ -41,9 +36,8 @@ export const MenuListOptionValue = (props) => (
* @param {Function} options.t a translation function
* @param {Object} options.config the item config
* @param {Boolean} options.changed has the value been changed?
* @param {object} props.Swizzled - An object holding swizzled code
*/
export const MenuListValue = ({ current, config, changed, Swizzled }) => {
export const MenuListValue = ({ current, config, changed }) => {
// get the values
const val = changed ? current : config.dflt
@ -54,17 +48,12 @@ export const MenuListValue = ({ current, config, changed, Swizzled }) => {
// if not, is the value a string
else if (typeof val === 'string') key = val
// otherwise stringify booleans
else if (val) key = <Swizzled.components.BoolYesIcon />
else key = <Swizzled.components.BoolNoIcon />
else if (val) key = <BoolYesIcon />
else key = <BoolNoIcon />
const translated =
config.doNotTranslate || typeof key !== 'string' ? key : Swizzled.methods.t(key)
const translated = config.doNotTranslate || typeof key !== 'string' ? key : t(key)
return (
<Swizzled.components.MenuHighlightValue changed={changed}>
{translated}
</Swizzled.components.MenuHighlightValue>
)
return <MenuHighlightValue changed={changed}>{translated}</MenuHighlightValue>
}
/** Displays the corrent, translated value for a boolean */
@ -76,14 +65,14 @@ export const MenuMmOptionValue = () => (
)
/** Displays a formated mm value based on the current units */
export const MenuMmValue = ({ current, config, units, changed, Swizzled }) => (
<Swizzled.components.MenuHighlightValue changed={changed}>
export const MenuMmValue = ({ current, config, units, changed }) => (
<MenuHighlightValue changed={changed}>
<span
dangerouslySetInnerHTML={{
__html: Swizzled.methods.formatMm(changed ? current : config.dflt, units),
__html: formatMm(changed ? current : config.dflt, units),
}}
/>
</Swizzled.components.MenuHighlightValue>
</MenuHighlightValue>
)
/** Displays the current percentage value, and the absolute value if configured
@ -95,25 +84,18 @@ export const MenuMmValue = ({ current, config, units, changed, Swizzled }) => (
* msg PencilIcon ResetIcon *
**************************************************************************
* */
export const MenuPctOptionValue = ({
config,
current,
settings,
changed,
patternConfig,
Swizzled,
}) => {
export const MenuPctOptionValue = ({ config, current, settings, changed, patternConfig }) => {
const val = changed ? current : config.pct / 100
return (
<Swizzled.components.MenuHighlightValue changed={changed}>
{Swizzled.methods.formatPercentage(val)}
<MenuHighlightValue changed={changed}>
{formatPercentage(val)}
{config.toAbs && settings?.measurements
? ` | ${Swizzled.methods.formatMm(
? ` | ${formatMm(
config.toAbs(val, settings, mergeOptions(settings, patternConfig.options))
)}`
: null}
</Swizzled.components.MenuHighlightValue>
</MenuHighlightValue>
)
}
@ -123,18 +105,16 @@ export const MenuPctOptionValue = ({
* @param {Number|String|Boolean} options.dflt - The default value
* @param {Boolean} options.changed - Has the value been changed?
*/
export const MenuShowValue = ({ Swizzled, current, dflt, changed }) => {
const { MenuHighlightValue } = Swizzled.components
export const MenuShowValue = ({ current, dflt, changed }) => {
return <MenuHighlightValue changed={changed}> {changed ? current : dflt} </MenuHighlightValue>
}
export const MenuScaleSettingValue = ({ Swizzled, current, config, changed }) => (
<Swizzled.components.MenuHighlightValue current={current} dflt={config.dflt} changed={changed} />
export const MenuScaleSettingValue = ({ current, config, changed }) => (
<MenuHighlightValue current={current} dflt={config.dflt} changed={changed} />
)
export const MenuOnlySettingValue = ({ Swizzled, current, config }) => (
<Swizzled.components.MenuHighlightValue
export const MenuOnlySettingValue = ({ current, config }) => (
<MenuHighlightValue
current={current?.length}
dflt={config.parts.length}
changed={current !== undefined}

View file

@ -1,34 +1,43 @@
// Dependencies
import React from 'react'
import { draft, missingMeasurements } from '../../lib/index.mjs'
// Components
import { Null } from '../Null.mjs'
import { ZoomablePattern } from '../ZoomablePattern.mjs'
import { PatternLayout } from '../PatternLayout.mjs'
import { DraftMenu } from '../menus/DraftMenu.mjs'
/**
* The draft view allows users to tweak their pattern
*
* @param (object) props - All the props
* @param {function} props.config - The editor configuration
* @param {function} props.Design - The design constructor
* @param {array} props.missingMeasurements - List of missing measurements for the current design
* @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} DraftView - React component
*/
export const DraftView = ({ Design, state, update, Swizzled }) => {
export const DraftView = ({ Design, state, update, config }) => {
/*
* Don't trust that we have all measurements
*
* We do not need to change the view here. That is done in the central
* ViewWrapper componenet. However, checking the measurements against
* the design takes a brief moment, so this component will typically
* render before that happens, and if measurments are missing it will
* render before that happens, and if measurements are missing it will
* throw and error.
*
* So when measurements are missing, we just return here and the view
* will switch on the next render loop.
*/
if (Swizzled.methods.missingMeasurements(state)) return null
if (missingMeasurements(state)) return <Null />
/*
* First, attempt to draft
*/
const { pattern } = Swizzled.methods.draft(Design, state.settings)
const { pattern } = draft(Design, state.settings)
let output = null
let renderProps = false
@ -36,9 +45,9 @@ export const DraftView = ({ Design, state, update, Swizzled }) => {
try {
const __html = pattern.render()
output = (
<Swizzled.components.ZoomablePattern>
<ZoomablePattern>
<div className="w-full h-full" dangerouslySetInnerHTML={{ __html }} />
</Swizzled.components.ZoomablePattern>
</ZoomablePattern>
)
} catch (err) {
console.log(err)
@ -46,7 +55,7 @@ export const DraftView = ({ Design, state, update, Swizzled }) => {
} else {
renderProps = pattern.getRenderProps()
output = (
<Swizzled.components.ZoomablePattern
<ZoomablePattern
renderProps={renderProps}
patternLocale={state.locale || 'en'}
rotate={state.ui.rotate}
@ -55,13 +64,9 @@ export const DraftView = ({ Design, state, update, Swizzled }) => {
}
return (
<Swizzled.components.PatternLayout
{...{ update, Design, output, state, pattern }}
menu={
state.ui?.aside ? (
<Swizzled.components.DraftMenu {...{ Design, pattern, update, state }} />
) : null
}
<PatternLayout
{...{ update, Design, output, state, pattern, config }}
menu={state.ui?.aside ? <DraftMenu {...{ Design, pattern, update, state }} /> : null}
/>
)
}

View file

@ -1,7 +1,7 @@
// Dependencies
import { horFlexClasses } from '../../utils.mjs'
import { t, designMeasurements } from '../../lib/index.mjs'
import { capitalize } from '@freesewing/utils'
import { measurements as measurementsTranslations } from '@freesewing/i18n'
// Hooks
import React, { Fragment, useEffect } from 'react'
// Components
@ -17,7 +17,11 @@ 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 }
const iconClasses = {
className: 'tw-w-8 tw-h-8 md:tw-w-10 md:tw-h-10 lg:tw-w-12 lg:tw-h-12 tw-shrink-0',
stroke: 1.5,
}
const horFlexClasses = 'tw-flex tw-flex-row tw-items-center tw-justify-between tw-gap-4 tw-w-full'
/**
* The measurements view is loaded to update/set measurements
@ -40,7 +44,13 @@ export const MeasurementsView = ({ config, Design, missingMeasurements, state, u
useEffect(() => {
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')
update.notify(
{
msg: 'To generate this pattern, we need some additional measurements',
icon: 'tip',
},
'missingMeasurements'
)
else update.notifySuccess(t('pe:measurementsAreOk'))
}, [state.view, update])
@ -48,9 +58,9 @@ export const MeasurementsView = ({ config, Design, missingMeasurements, state, u
update.settings(['measurements'], designMeasurements(Design, set.measies))
update.settings(['units'], set.imperial ? 'imperial' : 'metric')
// Save the measurement set name to pattern settings
if (set[`name${capitalize(locale)}`])
if (set.nameEn)
// Curated measurement set
update.settings(['metadata'], { setName: set[`name${capitalize(locale)}`] })
update.settings(['metadata'], { setName: set.nameEn })
else if (set.name)
// User measurement set
update.settings(['metadata'], { setName: set.name })
@ -63,10 +73,13 @@ export const MeasurementsView = ({ config, Design, missingMeasurements, state, u
[
<Fragment key={1}>
<div className={horFlexClasses}>
<h5 id="ownsets">{t('pe:chooseFromOwnSets')}</h5>
<h4 id="ownsets">Choose one of your own measurements sets</h4>
<MeasurementsSetIcon {...iconClasses} />
</div>
<p className="text-left">{t('pe:chooseFromOwnSetsDesc')}</p>
<p className="tw-text-left">
Pick any of your own measurements sets that have all required measurements to generate
this pattern.
</p>
</Fragment>,
<UserSetPicker
key={2}
@ -80,10 +93,12 @@ export const MeasurementsView = ({ config, Design, missingMeasurements, state, u
[
<Fragment key={1}>
<div className={horFlexClasses}>
<h5 id="bookmarkedsets">{t('pe:chooseFromBookmarkedSets')}</h5>
<h4 id="bookmarkedsets">Choose one of the measurements sets you have bookmarked</h4>
<BookmarkIcon {...iconClasses} />
</div>
<p className="text-left">{t('pe:chooseFromBookmarkedSetsDesc')}</p>
<p className="tw-text-left">
If you have bookmarked any measurements sets, you can select from those too.
</p>
</Fragment>,
<BookmarkedSetPicker
key={2}
@ -97,10 +112,13 @@ export const MeasurementsView = ({ config, Design, missingMeasurements, state, u
[
<Fragment key={1}>
<div className={horFlexClasses}>
<h5 id="curatedsets">{t('pe:chooseFromCuratedSets')}</h5>
<h4 id="curatedsets">Choose one of FreeSewing&apos;s curated measurements sets</h4>
<CuratedMeasurementsSetIcon {...iconClasses} />
</div>
<p className="text-left">{t('pe:chooseFromCuratedSetsDesc')}</p>
<p className="tw-text-left">
If you&apos;re just looking to try out our platform, you can select from our list of
curated measurements sets.
</p>
</Fragment>,
<CuratedSetPicker key={2} clickHandler={loadMeasurements} {...{ config, Design }} />,
'csets',
@ -110,10 +128,10 @@ export const MeasurementsView = ({ config, Design, missingMeasurements, state, u
items.push([
<Fragment key={1}>
<div className={horFlexClasses}>
<h5 id="editmeasurements">{t('pe:editMeasurements')}</h5>
<h4 id="editmeasurements">Edit Measurements</h4>
<EditIcon {...iconClasses} />
</div>
<p className="text-left">{t('pe:editMeasurementsDesc')}</p>
<p className="tw-text-left">You can manually set or override measurements below.</p>
</Fragment>,
<MeasurementsEditor key={2} {...{ Design, config, update, state }} />,
'edit',
@ -122,35 +140,38 @@ export const MeasurementsView = ({ config, Design, missingMeasurements, state, u
return (
<>
<HeaderMenu state={state} {...{ config, update }} />
<div className="max-w-7xl mt-8 mx-auto px-4">
<h2>{t('pe:measurements')}</h2>
<div className="tw-max-w-7xl tw-mt-8 tw-mx-auto tw-px-4 tw-mb-4">
<h1 className="tw-text-center">Measurements</h1>
{missingMeasurements && missingMeasurements.length > 0 ? (
<Popout note dense noP>
<h5>{t('pe:missingMeasurementsInfo')}:</h5>
<ol className="list list-inside flex flex-row flex-wrap">
<h3>
To generate this pattern, we need {missingMeasurements.length} additional measurement
{missingMeasurements.length === 1 ? '' : 's'}:
</h3>
<ol className="tw-list tw-list-inside tw-flex tw-flex-row tw-flex-wrap tw-ml-0 tw-pl-0">
{missingMeasurements.map((m, i) => (
<li key={i}>
{i > 0 ? <span className="pr-2">,</span> : null}
<span className="font-medium">{t(`measurements:${m}`)}</span>
<li key={i} className="tw-flex">
{i > 0 ? <span className="tw-pr-2">,</span> : null}
<span className="tw-font-medium">{measurementsTranslations[m]}</span>
</li>
))}
</ol>
<p className="text-sm m-0 p-0 pt-2">
({missingMeasurements.length} {t('pe:missingMeasurements')})
</p>
</Popout>
) : (
<Popout tip dense noP>
<h5>{t('pe:measurementsAreOk')}</h5>
<div className="flex flex-row flex-wrap gap-2 mt-2">
<button className="btn btn-primary lg:btn-lg" onClick={() => update.view('draft')}>
{t('pe:view.draft.t')}
<h5>We have all required measurements to draft this pattern</h5>
<div className="tw-flex tw-flex-row tw-flex-wrap tw-gap-2 tw-mt-2">
<button
className="tw-daisy-btn tw-daisy-btn-primary lg:tw-daisy-btn-lg"
onClick={() => update.view('draft')}
>
{viewLabels.draft.t}
</button>
<button
className="btn btn-primary btn-outline lg:btn-lg"
className="tw-daisy-btn tw-daisy-btn-primary tw-daisy-btn-outline lg:tw-daisy-btn-lg"
onClick={() => update.view('picker')}
>
{t('pe:chooseAnotherActivity')}
Choose a different view
</button>
</div>
</Popout>

View file

@ -2,20 +2,9 @@ import React from 'react'
import { ViewPicker } from './ViewPicker.mjs'
import { DesignsView } from './DesignsView.mjs'
import { MeasurementsView } from './MeasurementsView.mjs'
import { DraftView } from './DraftView.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
*/
@ -24,6 +13,7 @@ export const View = (props) => {
if (view === 'designs') return <DesignsView {...props} />
if (view === 'measurements') return <MeasurementsView {...props} />
if (view === 'draft') return <DraftView {...props} />
/*
viewComponents: {
draft: 'DraftView',
@ -44,7 +34,7 @@ export const View = (props) => {
},
*/
return <p>No view component for view {props.view}</p>
return <h1 className="tw-ext-center tw-my-12">No view component for view {props.view}</h1>
}
/*

View file

@ -5,7 +5,7 @@ export const defaultConfig = {
// Enable use of a (FreeSewing) backend to load data from
enableBackend: true,
// Link to create a new measurements set, set to false to disable
hrefNewSet: 'https://freesewing.org/new/set',
hrefNewSet: '/new/set',
// Cloud default image
cloudImageDflt:
'https://imagedelivery.net/ouSuR9yY1bHt-fuAokSA5Q/365cc64e-1502-4d2b-60e0-cc8beee73f00/public',
@ -56,12 +56,6 @@ export const defaultConfig = {
mm: 'MmOptionValue',
pct: 'PctOptionValue',
},
// Facilitate custom handlers for core settings
menuCoreSettingsHandlerMethods: {
only: 'menuCoreSettingsOnlyHandler',
sabool: 'menuCoreSettingsSaboolHandler',
samm: 'menuCoreSettingsSammHandler',
},
menuGroupEmojis: {
advanced: '🤓',
fit: '👕',

View file

@ -34,7 +34,7 @@ export const useEditorState = (init = {}, setEphemeralState, config) => {
if (typeof URLSearchParams !== 'undefined') {
try {
const data = getHashData()
if (data.s === 'object') setState(data.s)
if (typeof data.s === 'object') setState(data.s)
else setState(init)
} catch (err) {
setState(init)

View file

@ -1,6 +1,7 @@
// Dependencies
import { designs } from '@freesewing/collection'
import { hasRequiredMeasurements, initialEditorState } from './lib/index.mjs'
import { hasRequiredMeasurements } from '@freesewing/utils'
import { initialEditorState } from './lib/index.mjs'
import { mergeConfig } from './config/index.mjs'
// Hooks
import React, { useState } from 'react'

View file

@ -1,13 +1,32 @@
export function defaultSa(Swizzled, units, inMm = true) {
// Dependencies
import { defaultConfig as config } from '../config/index.mjs'
import { measurementAsMm } from '@freesewing/utils'
/*
* Components
* Note that these are only used as returns values
* There's no JSX in here so no React import needed
*/
import {
DetailIcon,
ExpandIcon,
IncludeIcon,
MarginIcon,
PaperlessIcon,
SaIcon,
ScaleIcon,
UnitsIcon,
} from '@freesewing/react/components/Icon'
export function defaultSa(units, inMm = true) {
const dflt = units === 'imperial' ? 0.5 : 1
return inMm ? Swizzled.methods.measurementAsMm(dflt, units) : dflt
return inMm ? measurementAsMm(dflt, units) : dflt
}
export function defaultSamm(Swizzled, units, inMm = true) {
export function defaultSamm(units, inMm = true) {
const dflt = units === 'imperial' ? 0.5 : 1
return inMm ? Swizzled.methods.measurementAsMm(dflt, units) : dflt
return inMm ? measurementAsMm(dflt, units) : dflt
}
/** custom event handlers for inputs that need them */
export function menuCoreSettingsOnlyHandler(Swizzled, { updateHandler, current }) {
export function menuCoreSettingsOnlyHandler({ updateHandler, current }) {
return function (path, part) {
// Is this a reset?
if (part === undefined || part === '__UNSET__') return updateHandler(path, part)
@ -26,7 +45,7 @@ export function menuCoreSettingsOnlyHandler(Swizzled, { updateHandler, current }
}
}
export function menuCoreSettingsSammHandler(Swizzled, { updateHandler, config }) {
export function menuCoreSettingsSammHandler({ updateHandler, config }) {
return function (_path, newCurrent) {
// convert to millimeters if there's a value
newCurrent = newCurrent === undefined ? config.dflt : newCurrent
@ -36,16 +55,13 @@ export function menuCoreSettingsSammHandler(Swizzled, { updateHandler, config })
}
}
export function menuCoreSettingsSaboolHandler(Swizzled, { toggleSa }) {
export function menuCoreSettingsSaboolHandler({ toggleSa }) {
return toggleSa
}
export function menuCoreSettingsStructure(
Swizzled,
{ units = 'metric', sabool = false, parts = [] }
) {
export function menuCoreSettingsStructure({ units = 'metric', sabool = false, parts = [] }) {
return {
sabool: {
ux: Swizzled.config.uxLevels.core.sa,
ux: config.uxLevels.core.sa,
list: [0, 1],
choiceTitles: {
0: 'saNo',
@ -56,19 +72,19 @@ export function menuCoreSettingsStructure(
1: 'yes',
},
dflt: 0,
icon: Swizzled.components.SaIcon,
icon: SaIcon,
},
samm: sabool
? {
ux: Swizzled.config.uxLevels.core.sa,
ux: config.uxLevels.core.sa,
min: 0,
max: units === 'imperial' ? 2 : 2.5,
dflt: Swizzled.methods.defaultSamm(units),
icon: Swizzled.components.SaIcon,
dflt: defaultSamm(units),
icon: SaIcon,
}
: false,
paperless: {
ux: Swizzled.config.uxLevels.core.paperless,
ux: config.uxLevels.core.paperless,
list: [0, 1],
choiceTitles: {
0: 'paperlessNo',
@ -79,10 +95,10 @@ export function menuCoreSettingsStructure(
1: 'yes',
},
dflt: 0,
icon: Swizzled.components.PaperlessIcon,
icon: PaperlessIcon,
},
units: {
ux: Swizzled.config.uxLevels.core.units,
ux: config.uxLevels.core.units,
list: ['metric', 'imperial'],
dflt: 'metric',
choiceTitles: {
@ -93,10 +109,10 @@ export function menuCoreSettingsStructure(
metric: 'metric',
imperial: 'imperial',
},
icon: Swizzled.components.UnitsIcon,
icon: UnitsIcon,
},
complete: {
ux: Swizzled.config.uxLevels.core.complete,
ux: config.uxLevels.core.complete,
list: [1, 0],
dflt: 1,
choiceTitles: {
@ -107,10 +123,10 @@ export function menuCoreSettingsStructure(
0: 'no',
1: 'yes',
},
icon: Swizzled.components.DetailIcon,
icon: DetailIcon,
},
expand: {
ux: Swizzled.config.uxLevels.core.expand,
ux: config.uxLevels.core.expand,
list: [1, 0],
dflt: 1,
choiceTitles: {
@ -121,29 +137,29 @@ export function menuCoreSettingsStructure(
0: 'no',
1: 'yes',
},
icon: Swizzled.components.ExpandIcon,
icon: ExpandIcon,
},
only: {
ux: Swizzled.config.uxLevels.core.only,
ux: config.uxLevels.core.only,
dflt: false,
list: parts,
parts,
icon: Swizzled.components.IncludeIcon,
icon: IncludeIcon,
},
scale: {
ux: Swizzled.config.uxLevels.core.scale,
ux: config.uxLevels.core.scale,
min: 0.1,
max: 5,
dflt: 1,
step: 0.1,
icon: Swizzled.components.ScaleIcon,
icon: ScaleIcon,
},
margin: {
ux: Swizzled.config.uxLevels.core.margin,
ux: config.uxLevels.core.margin,
min: 0,
max: 2.5,
dflt: Swizzled.methods.measurementAsMm(units === 'imperial' ? 0.125 : 0.2, units),
icon: Swizzled.components.MarginIcon,
dflt: measurementAsMm(units === 'imperial' ? 0.125 : 0.2, units),
icon: MarginIcon,
},
}
}

View file

@ -1,4 +1,4 @@
export function designOptionType(Swizzled, option) {
export function designOptionType(option) {
if (typeof option?.pct !== 'undefined') return 'pct'
if (typeof option?.bool !== 'undefined') return 'bool'
if (typeof option?.count !== 'undefined') return 'count'
@ -12,7 +12,7 @@ import { mergeOptions } from '@freesewing/core'
import set from 'lodash.set'
import orderBy from 'lodash.orderby'
export function menuDesignOptionsStructure(Swizzled, options, settings, asFullList = false) {
export function menuDesignOptionsStructure(options, settings, asFullList = false) {
if (!options) return options
const sorted = {}
for (const [name, option] of Object.entries(options)) {
@ -23,7 +23,7 @@ export function menuDesignOptionsStructure(Swizzled, options, settings, asFullLi
// Fixme: One day we should sort this based on the translation
for (const option of orderBy(sorted, ['order', 'menu', 'name'], ['asc', 'asc', 'asc'])) {
if (typeof option === 'object') {
const oType = Swizzled.methods.designOptionType(option)
const oType = designOptionType(option)
option.dflt = option.dflt || option[oType]
if (oType === 'pct') option.dflt /= 100
if (typeof option.menu === 'function')
@ -64,17 +64,14 @@ export function menuDesignOptionsStructure(Swizzled, options, settings, asFullLi
*
* Since these structures can be nested with option groups, this needs some extra logic
*/
export function getOptionStructure(Swizzled, option, Design, state) {
const structure = Swizzled.methods.menuDesignOptionsStructure(
Design.patternConfig.options,
state.settings
)
export function getOptionStructure(option, Design, state) {
const structure = menuDesignOptionsStructure(Design.patternConfig.options, state.settings)
console.log({ structure })
return Swizzled.methods.findOption(structure, option)
return findOption(structure, option)
}
export function findOption(Swizzled, structure, option) {
export function findOption(structure, option) {
for (const [key, val] of Object.entries(structure)) {
if (key === option) return val
if (val.isGroup) {

View file

@ -55,13 +55,7 @@ import {
shortDate,
parseDistanceInput,
} from './formatting.mjs'
import {
designMeasurements,
hasRequiredMeasurements,
isDegreeMeasurement,
missingMeasurements,
structureMeasurementsAsDesign,
} from './measurements.mjs'
import { designMeasurements, missingMeasurements } from './measurements.mjs'
import { menuUiPreferencesStructure } from './ui-preferences.mjs'
/*
@ -120,10 +114,7 @@ export {
parseDistanceInput,
// measurements.mjs
designMeasurements,
hasRequiredMeasurements,
isDegreeMeasurement,
missingMeasurements,
structureMeasurementsAsDesign,
// ui-preferences.mjs
menuUiPreferencesStructure,
}

View file

@ -1,5 +1,5 @@
// Dependencies
import { defaultConfig as config } from '../config/index.mjs'
import { defaultConfig } from '../config/index.mjs'
import { degreeMeasurements } from '@freesewing/config'
/*
@ -16,46 +16,9 @@ export function designMeasurements(Design, measies = {}) {
return measurements
}
/**
* Helper method to determine whether all required measurements for a design are present
*
* @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(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 = structureMeasurementsAsDesign(Design)
/*
* Walk required measuremnets, and keep track of what's missing
*/
const missing = []
for (const m of Design.patternConfig?.measurements || []) {
if (typeof measurements[m] === 'undefined') missing.push(m)
}
/*
* Return true or false, plus a list of missing measurements
*/
return [missing.length === 0, missing]
}
/**
* Helper method to determine whether a measurement uses degrees
*
* @param {string} name - The name of the measurement
* @return {bool} isDegree - True if the measurment is a degree measurement
*/
export function isDegreeMeasurement(name) {
return degreeMeasurements.indexOf(name) !== -1
}
/*
* Helper method to check whether measururements are missing
* Helper method to check whether measurements are missing
*
* Note that this does not actually check the settings against
* the chosen design, but rather relies on the missing measurements
@ -63,22 +26,12 @@ export function isDegreeMeasurement(name) {
* so we do it only once.
*
* @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(state, config) {
export function missingMeasurements(state) {
return (
!config.measurementsFreeViews.includes(state.view) &&
!defaultConfig.measurementsFreeViews.includes(state.view) &&
state._.missingMeasurements &&
state._.missingMeasurements.length > 0
)
}
/*
* This takes a POJO of measurements, and turns it into a structure that matches a design object
*
* @param {object} measurements - The POJO of measurments
* @return {object} design - The measurements structured as a design object
*/
export function structureMeasurementsAsDesign(measurements) {
return measurements.patternConfig ? measurements : { patternConfig: { measurements } }
}

View file

@ -1,37 +0,0 @@
export const CuratedMeasurementsSetLineup = ({ sets = [], locale, clickHandler, Swizzled }) => (
<div
className={`w-full flex flex-row ${
sets.length > 1 ? 'justify-start px-8' : 'justify-center'
} overflow-x-scroll`}
style={{
backgroundImage: `url(/img/lineup-backdrop.svg)`,
width: 'auto',
backgroundSize: 'auto 100%',
backgroundRepeat: 'repeat-x',
}}
>
{sets.map((set) => {
const props = {
className: 'aspect-[1/3] w-auto h-96',
style: {
backgroundImage: `url(${Swizzled.methods.cloudImageUrl({
id: `cset-${set.id}`,
type: 'lineup',
})})`,
width: 'auto',
backgroundSize: 'contain',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center',
},
onClick: () => clickHandler(set),
}
return (
<div className="flex flex-col items-center" key={set.id}>
<button {...props} key={set.id}></button>
<b>{set[`name${Swizzled.methods.capitalize(locale)}`]}</b>
</div>
)
})}
</div>
)

View file

@ -6,6 +6,7 @@ import {
distanceAsMm,
} from '@freesewing/utils'
import { collection } from '@freesewing/collection'
import { measurements as measurementsTranslations } from '@freesewing/i18n'
// Context
import { ModalContext } from '@freesewing/react/context/Modal'
import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus'
@ -527,7 +528,7 @@ export const MeasurementInput = ({
let inputClasses = 'daisy-input-secondary'
let bottomLeftLabel = null
if (valid === true) {
inputClasses = 'daisy-input-success'
inputClasses = 'daisy-input-success tw-outline-success'
const val = `${validatedVal}${isDegree ? '°' : imperial ? '"' : 'cm'}`
bottomLeftLabel = (
<span className="tw-font-medium tw-text-base tw-text-success tw--mt-2 tw-block">{val}</span>
@ -549,17 +550,26 @@ export const MeasurementInput = ({
* See: https://github.com/facebook/react/issues/16554
*/
return (
<FormControl label={m + (isDegree ? ' (°)' : '')} forId={id} labelBL={bottomLeftLabel}>
<input
id={id}
type="text"
inputMode="numeric"
pattern="[0-9]*"
placeholder={placeholder}
value={localVal}
onChange={(evt) => localUpdate(evt.target.value)}
className={`tw-daisy-input tw-w-full tw-daisy-input-bordered ${inputClasses}`}
/>
<FormControl
label={measurementsTranslations[m] + (isDegree ? ' (°)' : '')}
forId={id}
labelBL={bottomLeftLabel}
>
<label
className={`tw-daisy-input tw-daisy-input-bordered tw-flex tw-items-center tw-gap-2 tw-border ${inputClasses} tw-mb-1 tw-outline tw-outline-base-300 tw-bg-transparent tw-outline-2 tw-outline-offset-2`}
>
<input
id={id}
type="text"
inputMode="numeric"
pattern="[0-9]*"
placeholder={placeholder}
value={localVal}
onChange={(evt) => localUpdate(evt.target.value)}
className={`tw-border-0 tw-grow-2 tw-w-full`}
/>
{isDegree ? '°' : imperial ? 'inch' : 'cm'}
</label>
</FormControl>
)
}

View file

@ -72,7 +72,7 @@ export const Popout = (props) => {
>
<div
className={`
tw-border-y-4 sm:tw-border-0 sm:tw-border-l-4 tw-px-6 sm:tw-px-8 tw-py-4 sm:tw-py-2
tw-border-y-4 tw-border-x-0 sm:tw-border-0 sm:tw-border-l-4 tw-px-6 sm:tw-px-8 tw-py-4 sm:tw-py-2
tw-shadow tw-text-base tw-border-${color} tw-border-solid
`}
>

View file

@ -3,15 +3,15 @@ import React from 'react'
/*
* A simple spinner
*/
export const Spinner = ({ className = 'h-6 w-6' }) => (
export const Spinner = ({ className = 'tw-h-6 tw-w-6' }) => (
<svg
className={`animate-spin ${className}`}
className={`tw-animate-spin ${className}`}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
className="tw-opacity-25"
cx="12"
cy="12"
r="10"
@ -19,7 +19,7 @@ export const Spinner = ({ className = 'h-6 w-6' }) => (
strokeWidth="4"
></circle>
<path
className="opacity-75"
className="tw-opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>

View file

@ -227,6 +227,36 @@ export function getSearchParam(name = 'id') {
return new URLSearchParams(window.location.search).get(name) // eslint-disable-line
}
/**
* Helper method to determine whether all required measurements for a design are present
*
* @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(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 = structureMeasurementsAsDesign(Design)
/*
* Walk required measurements, and keep track of what's missing
*/
const missing = []
for (const m of Design.patternConfig?.measurements || []) {
if (typeof measurements[m] === 'undefined') missing.push(m)
}
/*
* Return true or false, plus a list of missing measurements
*/
return [missing.length === 0, missing]
}
/*
* Convert a measurement to millimeter
*
@ -413,6 +443,15 @@ export function shortDate(timestamp = false, withTime = true) {
*/
export const shortUuid = (uuid) => uuid.slice(0, 5)
/*
* This takes a POJO of measurements, and turns it into a structure that matches a design object
*
* @param {object} measurements - The POJO of measurments
* @return {object} design - The measurements structured as a design object
*/
export function structureMeasurementsAsDesign(measurements) {
return measurements.patternConfig ? measurements : { patternConfig: { measurements } }
}
/*
* We used to use react-timeago but that's too much overhead
* This is a drop-in replacement that does not rerender

View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" viewBox="0 -20 300 220" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
<path d="M 0 200 L 300 200" id="floor" stroke="#ccc" stroke-width="1"/>
<g id="metric">
<g fill="#000" fill-opacity="0.1">
<rect x="0" y="0" width="300" height="20"/>
<rect x="0" y="40" width="300" height="20"/>
<rect x="0" y="80" width="300" height="20"/>
<rect x="0" y="120" width="300" height="20"/>
<rect x="0" y="160" width="300" height="20"/>
</g>
<g fill="#fff" fill-opacity="0.1">
<rect x="0" y="20" width="300" height="20"/>
<rect x="0" y="60" width="300" height="20"/>
<rect x="0" y="100" width="300" height="20"/>
<rect x="0" y="140" width="300" height="20"/>
<rect x="0" y="180" width="300" height="20"/>
</g>
<text fill="#000" font-size="4">
<tspan x="1" y="140">0.60m</tspan>
<tspan x="1" y="120">0.80m</tspan>
<tspan x="1" y="100">1.00m</tspan>
<tspan x="1" y="80">1.20m</tspan>
<tspan x="1" y="60">1.40m</tspan>
<tspan x="1" y="40">1.60m</tspan>
<tspan x="1" y="20">1.80m</tspan>
<tspan x="1" y="0">2.00m</tspan>
</text>
</g>
<g id="imperial">
<path d="M 0 -13.36 h 300" stroke-width="0.5" stroke="#000" stroke-dasharray="10 10" stroke-opacity="0.4"/>
<path d="M 0 -13.36 h 300" stroke-width="0.5" stroke="#fff" stroke-dasharray="10 10" stroke-dashoffset="10" stroke-opacity="0.4"/>
<path d="M 0 17.12 h 300" stroke-width="0.5" stroke="#000" stroke-dasharray="10 10" stroke-opacity="0.4"/>
<path d="M 0 17.12 h 300" stroke-width="0.5" stroke="#fff" stroke-dasharray="10 10" stroke-dashoffset="10" stroke-opacity="0.4"/>
<path d="M 0 47.6 h 300" stroke-width="0.5" stroke="#000" stroke-dasharray="10 10" stroke-opacity="0.4"/>
<path d="M 0 47.6 h 300" stroke-width="0.5" stroke="#fff" stroke-dasharray="10 10" stroke-dashoffset="10" stroke-opacity="0.4"/>
<path d="M 0 78.08 h 300" stroke-width="0.5" stroke="#000" stroke-dasharray="10 10" stroke-opacity="0.4"/>
<path d="M 0 78.08 h 300" stroke-width="0.5" stroke="#fff" stroke-dasharray="10 10" stroke-dashoffset="10" stroke-opacity="0.4"/>
<path d="M 0 108.56 h 300" stroke-width="0.5" stroke="#000" stroke-dasharray="10 10" stroke-opacity="0.4"/>
<path d="M 0 108.56 h 300" stroke-width="0.5" stroke="#fff" stroke-dasharray="10 10" stroke-dashoffset="10" stroke-opacity="0.4"/>
<path d="M 0 139.31 h 300" stroke-width="0.5" stroke="#000" stroke-dasharray="10 10" stroke-opacity="0.4"/>
<path d="M 0 139.31 h 300" stroke-width="0.5" stroke="#fff" stroke-dasharray="10 10" stroke-dashoffset="10" stroke-opacity="0.4"/>
<text fill="#000" font-size="4" text-anchor="end">
<tspan x="299" y="139.31">2f</tspan>
<tspan x="299" y="108.56">3f</tspan>
<tspan x="299" y="78.08">4f</tspan>
<tspan x="299" y="47.6">5f</tspan>
<tspan x="299" y="17.12">6f</tspan>
<tspan x="299" y="-13.36">7f</tspan>
</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3 KiB