1
0
Fork 0

wip: work on editor

This commit is contained in:
joostdecock 2025-01-19 17:11:22 +01:00
parent 761ad0d5c9
commit 4916a6e805
19 changed files with 423 additions and 286 deletions

View file

@ -115,6 +115,8 @@ packageJson:
"./hooks/useAccount": "./hooks/useAccount/index.mjs"
"./hooks/useBackend": "./hooks/useBackend/index.mjs"
"./hooks/useControl": "./hooks/useControl/index.mjs"
"./hooks/useDesign": "./hooks/useDesign/index.mjs"
"./hooks/useDesignTranslation": "./hooks/useDesignTranslation/index.mjs"
"./hooks/useSelection": "./hooks/useSelection/index.mjs"
# Lib
"./lib/RestClient": "./lib/RestClient/index.mjs"

View file

@ -4,14 +4,14 @@ import React, { useState } from 'react'
* DaisyUI's accordion seems rather unreliable.
* So instead, we handle this in React state
*/
const getProps = (isActive) => ({
const getProps = (isActive = false) => ({
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
tw-w-full 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: ` tw-p-2 tw-px-4 tw-rounded tw-bg-transparent tw-w-full tw-mt-2 tw-py-4 tw-h-auto
className: `tw-p-2 tw-px-4 tw-rounded-none tw-bg-transparent tw-w-full tw-h-auto
tw-content-start tw-bg-secondary tw-text-left tw-bg-opacity-20
${
isActive

View file

@ -14,8 +14,10 @@ import {
ExpandIcon,
ExportIcon,
FixmeIcon,
FlagIcon,
KioskIcon,
MenuIcon,
OptionsIcon,
PaperlessIcon,
ResetAllIcon,
RightIcon,
@ -24,24 +26,28 @@ import {
SaIcon,
SaveAsIcon,
SaveIcon,
SettingsIcon,
TrashIcon,
UiIcon,
UndoIcon,
UnitsIcon,
} from '@freesewing/react/components/Icon'
import { ButtonFrame } from '@freesewing/react/components/Input'
import { DesignOptionsMenu } from './menus/DesignOptionsMenu.mjs'
import { CoreSettingsMenu } from './menus/CoreSettingsMenu.mjs'
import { UiPreferencesMenu } from './menus/UiPreferencesMenu.mjs'
import { FlagsAccordionEntries } from './Flag.mjs'
import { UndoStep } from './views/UndosView.mjs'
/*
* Lookup object for header menu icons
*/
const headerMenuIcons = {
flag: RocketIcon,
options: RocketIcon,
flag: FlagIcon,
options: OptionsIcon,
right: RightIcon,
settings: RocketIcon,
ui: RocketIcon,
settings: SettingsIcon,
ui: UiIcon,
}
export const HeaderMenuIcon = (props) => {
@ -101,7 +107,7 @@ export const HeaderMenuDropdown = (props) => {
<div
tabIndex={0}
role="button"
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"
className="tw-daisy-btn tw-daisy-btn-ghost hover:tw-bg-secondary hover:tw-bg-opacity-20 tw-border-secondary/10 hover:tw-boder-2 hover:tw-border-secondary tw-border tw-border-secondary tw-border-2 tw-border-solid tw-daisy-btn-sm tw-px-2 tw-z-20 tw-relative"
onClick={() => setOpen(open === id ? false : id)}
>
{toggle}
@ -129,11 +135,11 @@ export const HeaderMenuDraftViewDesignOptions = (props) => {
<HeaderMenuDropdown
{...props}
id="designOptions"
tooltip="fixme: 'pe:designOptions.d'"
tooltip="These options are specific to this design. You can use them to customize your pattern in a variety of ways."
toggle={
<>
<HeaderMenuIcon name="options" extraClasses="tw-text-secondary" />
<span className="tw-hidden lg:tw-inline">fixme: pe:designOptions.t</span>
<span className="tw-hidden lg:tw-inline">Design Options</span>
</>
}
>
@ -146,12 +152,12 @@ export const HeaderMenuDraftViewCoreSettings = (props) => {
return (
<HeaderMenuDropdown
{...props}
tooltip="fixme: pe:coreSettings.d"
tooltip="These settings are not specific to the design, but instead allow you to customize various parameters of the FreeSewing core library, which generates the design for you."
id="coreSettings"
toggle={
<>
<HeaderMenuIcon name="settings" extraClasses="tw-text-secondary" />
<span className="tw-hidden lg:tw-inline">fixme: pe:coreSettings.t</span>
<span className="tw-hidden lg:tw-inline">Core Settings</span>
</>
}
>
@ -164,12 +170,12 @@ export const HeaderMenuDraftViewUiPreferences = (props) => {
return (
<HeaderMenuDropdown
{...props}
tooltip="fixme: pe:uiPreferences.d"
tooltip="These preferences control the UI (User Interface) of the pattern editor"
id="uiPreferences"
toggle={
<>
<HeaderMenuIcon name="ui" extraClasses="tw-text-secondary" />
<span className="tw-hidden lg:tw-inline">fixme: pe:uiPreferences.t</span>
<span className="tw-hidden lg:tw-inline">UI Preferences</span>
</>
}
>
@ -184,7 +190,7 @@ export const HeaderMenuDraftViewFlags = (props) => {
return (
<HeaderMenuDropdown
{...props}
tooltip="fixme: pe:flagMenuMany.d"
tooltip="Some issues about your current pattern need your attention."
id="flags"
toggle={
<>
@ -389,7 +395,7 @@ export const HeaderMenuUndoIcons = (props) => {
<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}
{viewLabels.undos.t}
</div>
{undos.length}
</div>
@ -479,18 +485,18 @@ export const HeaderMenuViewMenu = (props) => {
className="tw-mb-1 tw-flex tw-flex-row tw-items-center tw-justify-between tw-w-full"
>
<a
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
className={`tw-w-full tw-text-base-content
tw-flex tw-flex-row tw-items-center tw-gap-2 md:tw-gap-4 tw-p-2 tw-px-4
hover:tw-cursor-pointer hover:tw-text-base-content
hover:tw-bg-secondary hover:tw-bg-opacity-10 hover:tw-border-solid ${
viewName === state.view
? 'tw-bg-secondary tw-border-solid tw-bg-opacity-20'
: 'tw-border-dotted'
hover:tw-bg-secondary hover:tw-bg-opacity-20 ${
viewName === state.view ? 'tw-bg-secondary tw-bg-opacity-20' : ''
}`}
onClick={() => update.view(viewName)}
>
<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>
<span className="tw-text-left tw-grow tw-font-medium">
{viewLabels[viewName]?.t || viewName}
</span>
</a>
</li>
)
@ -511,7 +517,7 @@ export const HeaderMenuViewMenu = (props) => {
>
<ul
tabIndex={i}
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"
className="tw-dropdown-content tw-bg-base-100 tw-bg-opacity-95 tw-z-20 tw-shadow tw-left-0 !tw-fixed md:!tw-absolute tw-w-screen md:tw-w-96 md:tw-pt-0 tw-mt-14 md:tw-mt-0"
>
{output}
</ul>

View file

@ -1,14 +1,16 @@
// Dependencies
import { menuValueWasChanged } from '../../lib/index.mjs'
import { designOptionType } from '@freesewing/utils'
// Hooks
import React, { useState, useMemo } from 'react'
// Components
import { SubAccordion } from '../Accordion.mjs'
import { GroupIcon, OptionsIcon } from '@freesewing/react/components/Icon'
import { EditIcon, GroupIcon, OptionsIcon, ResetIcon } from '@freesewing/react/components/Icon'
import { CoreSettingsMenu } from './CoreSettingsMenu.mjs'
import { FormControl } from '@freesewing/react/components/Input'
/** @type {String} class to apply to buttons on open menu items */
const iconButtonClass = 'btn btn-xs btn-ghost px-0 text-accent'
const iconButtonClass = 'tw-daisy-btn tw-daisy-btn-xs tw-daisy-btn-ghost tw-px-0 tw-text-accent'
/**
* A generic component for handling a menu item.
@ -37,6 +39,7 @@ export const MenuItem = ({
docs,
config,
Design,
i18n,
}) => {
// Local state - whether the override input should be shown
const [override, setOverride] = useState(false)
@ -78,15 +81,15 @@ export const MenuItem = ({
}}
>
<EditIcon
className={`w-6 h-6 ${
override ? 'bg-secondary text-secondary-content rounded' : 'text-secondary'
className={`tw-w-6 tw-h-6 ${
override ? 'tw-bg-secondary tw-text-secondary-content tw-rounded' : 'tw-text-secondary'
}`}
/>
</button>
)
const ResetButton = ({ disabled = false }) => (
<button
className={`${iconButtonClass} disabled:bg-opacity-0`}
className={`${iconButtonClass} disabled:tw-bg-opacity-0`}
disabled={disabled}
onClick={(evt) => {
evt.stopPropagation()
@ -101,17 +104,16 @@ export const MenuItem = ({
return (
<FormControl
label={<span className="text-base font-normal">{name}.d</span>}
label={<span className="tw-text-base tw-font-normal">{i18n.en.o[name].d}</span>}
id={config.name}
labelBR={<div className="flex flex-row items-center gap-2">{buttons}</div>}
labelBR={<div className="tw-flex tw-flex-row tw-items-center tw-gap-2">{buttons}</div>}
labelBL={
<span
className={`text-base font-medium -mt-2 block ${changed ? 'text-accent' : 'opacity-50'}`}
className={`tw-text-base tw-font-medium tw--mt-2 tw-block ${changed ? 'tw-text-accent' : 'tw-opacity-50'}`}
>
pe:youAreUsing{changed ? 'ACustom' : 'TheDefault'}Value
{changed ? 'This is a custom value' : 'This is the default value'}
</span>
}
docs={docs}
>
<Input {...drillProps} />
</FormControl>
@ -152,6 +154,7 @@ export const MenuItemGroup = ({
isDesignOptionsGroup = false,
Design,
state,
i18n,
}) => {
if (!Item) Item = MenuItem
@ -171,9 +174,9 @@ export const MenuItemGroup = ({
: () => <span role="img">fixme-icon</span>
const Value = item.isGroup
? () => (
<div className="flex flex-row gap-2 items-center font-medium">
<div className="tw-flex tw-flex-row tw-gap-2 tw-items-center tw-font-medium">
{Object.keys(item).filter((i) => i !== 'isGroup').length}
<OptionsIcon className="w-5 h-5" />
<OptionsIcon className="tw-w-5 tw-h-5" />
</div>
)
: isDesignOptionsGroup
@ -183,14 +186,14 @@ export const MenuItemGroup = ({
: () => <span>¯\_()_/¯</span>
return [
<div className="flex flex-row items-center justify-between w-full" key="a">
<div className="flex flex-row items-center gap-4 w-full">
<div className="tw-flex tw-flex-row tw-items-center tw-justify-between tw-w-full" key="a">
<div className="tw-flex tw-flex-row tw-items-center tw-gap-4 tw-w-full">
<ItemIcon />
<span className="font-medium">
pe:{itemName}.t pe:{itemName}
<span className="tw-font-medium tw-capitalize">
{i18n && i18n.en.o[itemName]?.t ? i18n.en.o[itemName].t : itemName}
</span>
</div>
<div className="font-bold">
<div className="tw-font-bold">
<Value
current={currentValues[itemName]}
config={item}
@ -218,6 +221,7 @@ export const MenuItemGroup = ({
updateHandler,
isDesignOptionsGroup,
Design,
i18n,
}}
/>
) : (
@ -234,6 +238,7 @@ export const MenuItemGroup = ({
updateHandler,
passProps,
Design,
i18n,
}}
/>
),
@ -241,7 +246,7 @@ export const MenuItemGroup = ({
]
})
return <SubAccordion items={content.filter((item) => item !== null)} />
return <SubAccordion items={content.filter((item) => item !== null)} dense />
}
/**
@ -253,11 +258,13 @@ export const MenuItemGroup = ({
* @param {String} options.emoji the emoji icon of the menu item
*/
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">
<div
className={`tw-flex tw-flex-row tw-gap-1 tw-items-center tw-w-full ${open ? '' : 'tw-justify-between'}`}
>
<span className="tw-font-medium tw-capitalize tw-flex tw-flex-row tw-gap-2">
{Icon ? <Icon /> : <span role="img">{emoji}</span>}
fixme: {name}
</span>
<span className="font-bold">{current}</span>
<span className="tw-font-bold">{current}</span>
</div>
)

View file

@ -1,7 +1,9 @@
// Dependencies
import { menuDesignOptionsStructure } from '../../lib/index.mjs'
import { designOptionType } from '@freesewing/utils'
// Hooks
import React, { useCallback, useMemo } from 'react'
import { useDesignTranslation } from '@freesewing/react/hooks/useDesignTranslation'
// Components
import {
MenuBoolInput,
@ -20,7 +22,7 @@ import {
MenyMmOptionValue,
MenuPctOptionValue,
} from './Value.mjs'
import { MenuItemGroup } from './Container.mjs'
import { MenuItemGroup, MenuItem } from './Container.mjs'
import { OptionsIcon } from '@freesewing/react/components/Icon'
//
@ -32,6 +34,7 @@ import { OptionsIcon } from '@freesewing/react/components/Icon'
* @param {Object} props.update - Object holding state handlers
*/
export const DesignOptionsMenu = ({ Design, isFirst = true, state, update }) => {
const i18n = useDesignTranslation(Design.designConfig.data.id)
const structure = useMemo(
() => menuDesignOptionsStructure(Design.patternConfig.options, state.settings),
[Design.patternConfig, state.settings]
@ -41,7 +44,7 @@ export const DesignOptionsMenu = ({ Design, isFirst = true, state, update }) =>
[update.settings]
)
const drillProps = { Design, state, update }
const drillProps = { Design, state, update, i18n }
const inputs = {
bool: (props) => <MenuBoolInput {...drillProps} {...props} />,
constant: (props) => <MenuConstantInput {...drillProps} {...props} />,
@ -72,8 +75,7 @@ export const DesignOptionsMenu = ({ Design, isFirst = true, state, update }) =>
Icon: OptionsIcon,
Item: (props) => <DesignOption {...{ inputs, values, update, Design }} {...props} />,
isFirst,
name: 'pe:designOptions',
language: state.locale,
name: 'Design Options',
passProps: {
ux: state.ui.ux,
settings: state.settings,
@ -85,6 +87,7 @@ export const DesignOptionsMenu = ({ Design, isFirst = true, state, update }) =>
Design,
inputs,
values,
i18n,
}}
/>
)

View file

@ -1,6 +1,9 @@
import React, { useMemo, useCallback, useState } from 'react'
import { round } from '@freesewing/utils'
import { ButtonFrame } from '@freesewing/react/components/Input'
import { designOptionType, round } from '@freesewing/utils'
import { menuRoundPct } from '../../lib/index.mjs'
import { ButtonFrame, NumberInput } from '@freesewing/react/components/Input'
import { defaultConfig } from '../../config/index.mjs'
import { ApplyIcon } from '@freesewing/react/components/Icon'
/** A boolean version of {@see MenuListInput} that sets up the necessary configuration */
export const MenuBoolInput = (props) => {
@ -23,8 +26,8 @@ export const MenuConstantInput = ({
<input
type={type}
className={`
input input-bordered w-full text-base-content
input-${changed ? 'secondary' : 'accent'}
tw-daisy-input tw-daisy-input-bordered tw-w-full tw-text-base-content
${changed ? 'tw-daisy-input-secondary' : 'tw-daisy-input-accent'}
`}
value={changed ? current : config.dflt}
onChange={(evt) => updateHandler([name], evt.target.value)}
@ -99,12 +102,12 @@ export const MenuListInput = ({
onClick={() => handleChange(entry)}
>
<div
className={`w-full flex items-start ${
sideBySide ? 'flex-row justify-between gap-2' : 'flex-col'
className={`tw-w-full tw-flex tw-items-start ${
sideBySide ? 'tw-flex-row tw-justify-between tw-gap-2' : 'tw-flex-col'
}`}
>
<div className="font-bold text-lg shrink-0">{title}</div>
{compact ? null : <div className="text-base font-normal">{desc}</div>}
<div className="tw-font-bold tw-text-lg tw-shrink-0">{title}</div>
{compact ? null : <div className="tw-text-base tw-font-normal">{desc}</div>}
</div>
</ButtonFrame>
)
@ -126,7 +129,7 @@ export const MenuListToggle = ({ config, changed, updateHandler, name }) => {
return (
<input
type="checkbox"
className={`toggle ${changed ? 'toggle-accent' : 'toggle-secondary'}`}
className={`tw-daisy-toggle ${changed ? 'tw-daisy-toggle-accent' : 'tw-daisy-toggle-secondary'}`}
checked={checked}
onChange={doToggle}
onClick={(evt) => evt.stopPropagation()}
@ -282,6 +285,7 @@ export const MenuSliderInput = ({
setReset,
children,
changed,
i18n,
}) => {
const { max, min } = config
const handleChange = useSharedHandlers({
@ -297,7 +301,7 @@ export const MenuSliderInput = ({
if (override)
return (
<>
<div className="flex flex-row justify-between">
<div className="tw-flex tw-flex-row tw-justify-between">
<MenuEditOption
{...{
config,
@ -314,14 +318,16 @@ export const MenuSliderInput = ({
return (
<>
<div className="flex flex-row justify-between">
<span className="opacity-50">
<div className="tw-flex tw-flex-row tw-justify-between">
<span className="tw-opacity-50">
<span dangerouslySetInnerHTML={{ __html: valFormatter(min) + suffix }} />
</span>
<span className={`font-bold ${val === config.dflt ? 'text-secondary' : 'text-accent'}`}>
<span
className={`tw-font-bold ${val === config.dflt ? 'tw-text-secondary' : 'tw-text-accent'}`}
>
<span dangerouslySetInnerHTML={{ __html: valFormatter(val) + suffix }} />
</span>
<span className="opacity-50">
<span className="tw-opacity-50">
<span dangerouslySetInnerHTML={{ __html: valFormatter(max) + suffix }} />
</span>
</div>
@ -330,8 +336,8 @@ export const MenuSliderInput = ({
{...{ min, max, value: val, step: config.step || 0.1 }}
onChange={(evt) => handleChange(evt.target.value)}
className={`
range range-sm mt-1
${changed ? 'range-accent' : 'range-secondary'}
tw-daisy-range tw-daisy-range-sm tw-mt-1
${changed ? 'tw-daisy-range-accent' : 'tw-daisy-range-secondary'}
`}
/>
{children}
@ -355,13 +361,16 @@ export const MenuEditOption = (props) => {
return <p>This design option type does not have a component to handle manual input.</p>
return (
<div className="form-control mb-2 w-full">
<label className="label font-medium text-accent">
<em>Enter a custom value ({config.menuOptionEditLabels[type]})</em>
<div className="tw-daisy-form-control tw-mb-2 tw-w-full">
<label className="tw-daisy-label tw-font-medium tw-text-accent">
<em>Enter a custom value ({defaultConfig.menuOptionEditLabels[type]})</em>
</label>
<label className="input-group input-group-sm flex flex-row items-center gap-2 -mt-4">
<label className="tw-daisy-input-group tw-daisy-input-group-sm tw-flex tw-flex-row tw-items-center tw-gap-2 tw--mt-4">
<NumberInput value={manualEdit} update={setManualEdit} />
<button className="btn btn-secondary mt-4" onClick={() => onUpdate(manualEdit)}>
<button
className="tw-daisy-btn tw-daisy-btn-secondary tw-mt-4"
onClick={() => onUpdate(manualEdit)}
>
<ApplyIcon />
</button>
</label>
@ -416,9 +425,11 @@ export const MenuOnlySettingInput = (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>
return <span className="tw-font-medium tw-text-base">{t(`${chunks[0]}:${chunks[1]}`)}</span>
}
config.valueMethod = (entry) => <span className="text-sm">{capitalize(entry.split('.')[0])}</span>
config.valueMethod = (entry) => (
<span className="tw-text-sm">{capitalize(entry.split('.')[0])}</span>
)
config.dense = true
// Sort alphabetically (translated)
const order = []

View file

@ -8,7 +8,6 @@ import { MenuItemGroup } from './Container.mjs'
import { Ux } from '@freesewing/react/components/Ux'
export const UiPreferencesMenu = ({ update, state, Design }) => {
console.log(state)
const structure = menuUiPreferencesStructure()
const drillProps = { Design, state, update }

View file

@ -1,6 +1,6 @@
import React from 'react'
import { mergeOptions } from '@freesewing/core'
import { formatMm } from '@freesewing/utils'
import { formatMm, formatPercentage } from '@freesewing/utils'
import { BoolYesIcon, BoolNoIcon } from '@freesewing/react/components/Icon'
/**

View file

@ -0,0 +1,134 @@
import React from 'react'
import { orderBy } from '@freesewing/utils'
import { ButtonFrame } from '@freesewing/react/components/Input'
import { UndoIcon } from '@freesewing/react/components/Icon'
import { getUndoStepData } from '../../lib/index.mjs'
/**
* The undos view shows the undo history, and allows restoring any undo state
*
* @param (object) props - All the props
* @param {object} designs - Object holding all designs
* @param {object} update - ViewWrapper state update object
*/
export const UndosView = ({ Design, update, state }) => {
const steps = orderBy(state._.undos, 'time', 'desc')
return (
<>
<HeaderMenu state={state} {...{ update, Design }} />
<div className="tw-text-left tw-mt-8 tw-mb-24 tw-px-4 tw-max-w-xl tw-mx-auto">
<h2>Undo History</h2>
<p>Time-travel through your recent pattern changes</p>
<small>
<b>Tip:</b> Click on any change to undo all changes up to, and including, that change.
</small>
{steps.length < 1 ? (
<Popout note>
<h4>Your undo history is currently empty</h4>
<p>When you make changes to your pattern, they will show up here.</p>
<p>For example, you can click the button below to change the pattern rotation:</p>
<button
className="tw-daisy-btn tw-daisy-btn-primary tw-capitalize"
onClick={() => update.settings('ui.rotate', state.settings?.ui?.rotate ? 0 : 1)}
>
Example: Rotate pattern
</button>
<p>As soon as you do, the change will show up here, and you can undo it.</p>
</Popout>
) : (
<div className="tw-flex tw-flex-col tw-gap-2 tw-mt-4">
{steps.map((step, index) => (
<UndoStep key={step.time} {...{ step, update, state, Design, index }} />
))}
</div>
)}
</div>
</>
)
}
export const UndoStepTimeAgo = ({ step }) => {
if (!step.time) return null
const secondsAgo = Math.floor((Date.now() - step.time) / 100) / 10
const minutesAgo = Math.floor(secondsAgo / 60)
const hoursAgo = Math.floor(minutesAgo / 60)
return hoursAgo ? (
<span>{hoursAgo} hours ago</span>
) : minutesAgo ? (
<span>{minutesAgo} minutes ago</span>
) : (
<span>{secondsAgo} seconds ago</span>
)
}
export const UndoStep = ({ update, state, step, Design, compact = false, index = 0 }) => {
/*
* Ensure path is always an array
*/
if (!Array.isArray(step.path)) step.path = step.path.split('.')
/*
* Figure this out once
*/
const imperial = state.settings?.units === 'imperial' ? true : false
/*
* Metadata can be ignored
*/
if (step.name === 'settings' && step.path[1] === 'metadata') return null
/*
* Defer for anything else to this method
*/
const data = getUndoStepData({ step, state, Design, imperial })
if (data === false) return <pre>{JSON.stringify(step, null, 2)}</pre> //null
if (data === null) return <p>Unsupported</p>
if (compact)
return (
<ButtonFrame dense onClick={() => update.restore(index, state._)}>
<div className="tw-flex tw-flex-row tw-items-center tw-align-start tw-gap-2 tw-w-full">
<UndoIcon text={index} className="tw-w-5 tw-h-5 tw-text-secondary" />
{data.msg ? data.msg : `pe:${data.optCode}`}
</div>
</ButtonFrame>
)
return (
<>
<p className="tw-text-sm tw-italic tw-font-medium tw-opacity-70 tw-text-right tw-p-0 tw-tw-m-0 tw--mb-2 tw-pr-2">
<UndoStepTimeAgo step={step} />
</p>
<ButtonFrame onClick={() => update.restore(index, state._)}>
<div className="tw-flex tw-flex-row tw-items-center tw-justify-between tw-gap-2 tw-w-full tw-m-0 tw-p-0 tw--mt-2 tw-text-lg">
<span className="tw-flex tw-flex-row tw-gap-2 tw-items-center">
{data.fieldIcon || null}
{`pe:${data.optCode}`}
</span>
<span className="tw-opacity-70 tw-flex tw-flex-row tw-gap-1 tw-items-center tw-text-base">
{data.icon || null} {`pe:${data.titleCode}`}
</span>
</div>
<div className="tw-flex tw-flex-row tw-gap-1 tw-items-center tw-align-start tw-w-full">
{data.msg ? (
data.msg
) : (
<>
<span className="">
{Array.isArray(data.newVal) ? data.newVal.join(', ') : data.newVal}
</span>
<LeftIcon className="tw-w-4 tw-h-4 tw-text-secondary tw-shrink-0" stroke={4} />
<span className="tw-line-through tw-decoration-1 tw-opacity-70">
{Array.isArray(data.oldVal) ? data.oldVal.join(', ') : data.oldVal}
</span>
</>
)}
</div>
</ButtonFrame>
</>
)
}

View file

@ -1,16 +1,5 @@
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'
if (typeof option?.deg !== 'undefined') return 'deg'
if (typeof option?.list !== 'undefined') return 'list'
if (typeof option?.mm !== 'undefined') return 'mm'
return 'constant'
}
import { mergeOptions } from '@freesewing/core'
import set from 'lodash.set'
import orderBy from 'lodash.orderby'
import { designOptionType, set, orderBy } from '@freesewing/utils'
export function menuDesignOptionsStructure(options, settings, asFullList = false) {
if (!options) return options
@ -59,6 +48,7 @@ export function menuDesignOptionsStructure(options, settings, asFullList = false
return menu
}
/*
* Helper method to grab an option from an Design options structure
*
@ -66,7 +56,6 @@ export function menuDesignOptionsStructure(options, settings, asFullList = false
*/
export function getOptionStructure(option, Design, state) {
const structure = menuDesignOptionsStructure(Design.patternConfig.options, state.settings)
console.log({ structure })
return findOption(structure, option)
}

View file

@ -1,5 +1,8 @@
// Dependencies
import React from 'react'
import { defaultConfig } from '../config/index.mjs'
import { round } from '@freesewing/utils'
import { formatDesignOptionValue } from './index.mjs'
// Components
import {
ErrorIcon,

View file

@ -1,28 +1,22 @@
import { designOptionType } from '@freesewing/utils'
/*
* Method that capitalizes a string (make the first character uppercase)
*
* @param {object} Swizzled - Swizzled code, not used here
* @param {string} string - The input string to capitalize
* @return {string} String - The capitalized input string
*/
export function capitalize(Swizzled, string) {
export function capitalize(string) {
return typeof string === 'string' ? string.charAt(0).toUpperCase() + string.slice(1) : ''
}
export function formatDesignOptionValue(Swizzled, option, value, imperial) {
const oType = Swizzled.methods.designOptionType(option)
if (oType === 'pct') return Swizzled.methods.formatPercentage(value ? value : option.pct / 100)
export function formatDesignOptionValue(option, value, imperial) {
const oType = designOptionType(option)
if (oType === 'pct') return formatPercentage(value ? value : option.pct / 100)
if (oType === 'deg') return `${value ? value : option.deg}°`
if (oType === 'bool')
return typeof value === 'undefined' ? (
option.bool
) : value ? (
<Swizzled.components.BoolYesIcon />
) : (
<Swizzled.components.BoolNoIcon />
)
if (oType === 'mm')
return Swizzled.methods.formatMm(typeof value === 'undefined' ? option.mm : value, imperial)
return typeof value === 'undefined' ? option.bool : value ? <BoolYesIcon /> : <BoolNoIcon />
if (oType === 'mm') return formatMm(typeof value === 'undefined' ? option.mm : value, imperial)
if (oType === 'list') return typeof value === 'undefined' ? option.dflt : value
return value
@ -36,7 +30,7 @@ export function formatDesignOptionValue(Swizzled, option, value, imperial) {
* fraction: the value to process
* format: the type of formatting to apply. html, notags, or anything else which will only return numbers
*/
export function formatFraction128(Swizzled, fraction, format = 'html') {
export function formatFraction128(fraction, format = 'html') {
let negative = ''
let inches = ''
let rest = ''
@ -50,19 +44,12 @@ export function formatFraction128(Swizzled, fraction, format = 'html') {
rest = fraction - inches
}
let fraction128 = Math.round(rest * 128)
if (fraction128 == 0)
return Swizzled.methods.formatImperial(negative, inches || fraction128, false, false, format)
if (fraction128 == 0) return formatImperial(negative, inches || fraction128, false, false, format)
for (let i = 1; i < 7; i++) {
const numoFactor = Math.pow(2, 7 - i)
if (fraction128 % numoFactor === 0)
return Swizzled.methods.formatImperial(
negative,
inches,
fraction128 / numoFactor,
Math.pow(2, i),
format
)
return formatImperial(negative, inches, fraction128 / numoFactor, Math.pow(2, i), format)
}
return (
@ -73,7 +60,7 @@ export function formatFraction128(Swizzled, fraction, format = 'html') {
}
// Formatting for imperial values
export function formatImperial(Swizzled, neg, inch, numo = false, deno = false, format = 'html') {
export function formatImperial(neg, inch, numo = false, deno = false, format = 'html') {
if (format === 'html') {
if (numo) return `${neg}${inch}&nbsp;<sup>${numo}</sup>/<sub>${deno}</sub>"`
else return `${neg}${inch}"`
@ -88,28 +75,27 @@ export function formatImperial(Swizzled, neg, inch, numo = false, deno = false,
// Format a value in mm based on the user's units
// Format can be html, notags, or anything else which will only return numbers
export function formatMm(Swizzled, val, units, format = 'html') {
val = Swizzled.methods.roundMm(val)
export function formatMm(val, units, format = 'html') {
val = roundMm(val)
if (units === 'imperial' || units === true) {
if (val == 0) return Swizzled.methods.formatImperial('', 0, false, false, format)
if (val == 0) return formatImperial('', 0, false, false, format)
let fraction = val / 25.4
return Swizzled.methods.formatFraction128(fraction, format)
return formatFraction128(fraction, format)
} else {
if (format === 'html' || format === 'notags') return Swizzled.methods.roundMm(val / 10) + 'cm'
else return Swizzled.methods.roundMm(val / 10)
if (format === 'html' || format === 'notags') return roundMm(val / 10) + 'cm'
else return roundMm(val / 10)
}
}
// Format a percentage (as in, between 0 and 1)
export function formatPercentage(Swizzled, val) {
export function formatPercentage(val) {
return Math.round(1000 * val) / 10 + '%'
}
/**
* A generic rounding method
*
* @param {object} Swizzled - Swizzled code, not used here
* @param {number} val - The input number to round
* @param {number} decimals - The number of decimal points to use when rounding
* @return {number} result - The rounded number
@ -119,7 +105,7 @@ export function round(methods, val, decimals = 1) {
}
// Rounds a value in mm
export function roundMm(Swizzled, val, units) {
export function roundMm(val, units) {
if (units === 'imperial') return Math.round(val * 1000000) / 1000000
else return Math.round(val * 10) / 10
}
@ -127,11 +113,10 @@ export function roundMm(Swizzled, val, units) {
/**
* Converts a value that contain a fraction to a decimal
*
* @param {object} Swizzled - Swizzled code, not used here
* @param {number} value - The input value
* @return {number} result - The resulting decimal value
*/
export function fractionToDecimal(Swizzled, value) {
export function fractionToDecimal(value) {
// if it's just a number, return it
if (!isNaN(value)) return value
@ -170,12 +155,11 @@ export function fractionToDecimal(Swizzled, value) {
/**
* Helper method to turn a measurement in millimeter regardless of units
*
* @param {object} Swizzled - Swizzled code, including methods
* @param {number} value - The input value
* @param {string} units - One of 'metric' or 'imperial'
* @return {number} result - Value in millimeter
*/
export function measurementAsMm(Swizzled, value, units = 'metric') {
export function measurementAsMm(value, units = 'metric') {
if (typeof value === 'number') return value * (units === 'imperial' ? 25.4 : 10)
if (String(value).endsWith('.')) return false
@ -185,7 +169,7 @@ export function measurementAsMm(Swizzled, value, units = 'metric') {
if (isNaN(value)) return false
return value * 10
} else {
const decimal = Swizzled.methods.fractionToDecimal(value)
const decimal = fractionToDecimal(value)
if (isNaN(decimal)) return false
return decimal * 24.5
}
@ -194,13 +178,12 @@ export function measurementAsMm(Swizzled, value, units = 'metric') {
/**
* Converts a millimeter value to a Number value in the given units
*
* @param {object} Swizzled - Swizzled code, not used here
* @param {number} mmValue - The input value in millimeter
* @param {string} units - One of 'metric' or 'imperial'
* @result {number} result - The result in millimeter
*/
export function measurementAsUnits(Swizzled, mmValue, units = 'metric') {
return Swizzled.methods.round(mmValue / (units === 'imperial' ? 25.4 : 10), 3)
export function measurementAsUnits(mmValue, units = 'metric') {
return round(mmValue / (units === 'imperial' ? 25.4 : 10), 3)
}
export function shortDate(locale = 'en', timestamp = false, withTime = true) {
const options = {
@ -220,12 +203,11 @@ export function shortDate(locale = 'en', timestamp = false, withTime = true) {
/*
* Parses value that should be a distance (cm or inch)
*
* @param {object} Swizzled - Swizzled code, including methods
* @param {number} val - The input value
* @param {bool} imperial - True if the units are imperial, false for metric
* @return {number} result - The distance in the relevant units
*/
export function parseDistanceInput(Swizzled, val = false, imperial = false) {
export function parseDistanceInput(val = false, imperial = false) {
// No input is not valid
if (!val) return false
@ -239,7 +221,7 @@ export function parseDistanceInput(Swizzled, val = false, imperial = false) {
if (!val.match(regex)) return false
// if fractions are allowed, parse for fractions, otherwise use the number as a value
if (imperial) val = Swizzled.methods.fractionToDecimal(val)
if (imperial) val = fractionToDecimal(val)
return isNaN(val) ? false : Number(val)
}

View file

@ -9,12 +9,7 @@ import {
menuCoreSettingsSammHandler,
menuCoreSettingsStructure,
} from './core-settings.mjs'
import {
designOptionType,
findOption,
getOptionStructure,
menuDesignOptionsStructure,
} from './design-options.mjs'
import { findOption, getOptionStructure, menuDesignOptionsStructure } from './design-options.mjs'
import {
addUndoStep,
cloneObject,
@ -70,7 +65,6 @@ export {
menuCoreSettingsSammHandler,
menuCoreSettingsStructure,
// design-options.mjs
designOptionType,
findOption,
getOptionStructure,
menuDesignOptionsStructure,

View file

@ -1,144 +0,0 @@
import orderBy from 'lodash.orderby'
/**
* The undos view shows the undo history, and allows restoring any undo state
*
* @param (object) props - All the props
* @param {object} props.swizzled - An object with swizzled components, hooks, methods, config, and defaults
* @param {object} designs - Object holding all designs
* @param {object} update - ViewWrapper state update object
*/
export const UndosView = ({ Design, Swizzled, update, state }) => {
const steps = orderBy(state._.undos, 'time', 'desc')
return (
<>
<Swizzled.components.HeaderMenu state={state} {...{ Swizzled, update, Design }} />
<div className="text-left mt-8 mb-24 px-4 max-w-xl mx-auto">
<h2>{Swizzled.methods.t('pe:view.undos.t')}</h2>
<p>{Swizzled.methods.t('pe:view.undos.d')}</p>
<small>
<b>Tip:</b> Click on any change to undo all changes up to, and including, that change.
</small>
{steps.length < 1 ? (
<Swizzled.components.Popout note>
<h4>Your undo history is currently empty</h4>
<p>When you make changes to your pattern, they will show up here.</p>
<p>For example, you can click the button below to change the pattern rotation:</p>
<button
className="btn btn-primary capitalize"
onClick={() => update.settings('ui.rotate', state.settings?.ui?.rotate ? 0 : 1)}
>
{Swizzled.methods.t('pe:example')}: {Swizzled.methods.t('pe:rotate.t')}
</button>
<p>As soon as you do, the change will show up here, and you can undo it.</p>
</Swizzled.components.Popout>
) : (
<div className="flex flex-col gap-2 mt-4">
{steps.map((step, index) => (
<Swizzled.components.UndoStep
key={step.time}
{...{ step, update, state, Design, index }}
/>
))}
</div>
)}
</div>
</>
)
}
export const UndoStepTimeAgo = ({ Swizzled, step }) => {
if (!step.time) return null
const secondsAgo = Math.floor((Date.now() - step.time) / 100) / 10
const minutesAgo = Math.floor(secondsAgo / 60)
const hoursAgo = Math.floor(minutesAgo / 60)
return hoursAgo ? (
<span>
{hoursAgo} {Swizzled.methods.t('pe:hoursAgo')}
</span>
) : minutesAgo ? (
<span>
{minutesAgo} {Swizzled.methods.t('pe:minutesAgo')}
</span>
) : (
<span>
{secondsAgo} {Swizzled.methods.t('pe:secondsAgo')}
</span>
)
}
export const UndoStep = ({ Swizzled, update, state, step, Design, compact = false, index = 0 }) => {
const { t } = Swizzled.methods
/*
* Ensure path is always an array
*/
if (!Array.isArray(step.path)) step.path = step.path.split('.')
/*
* Figure this out once
*/
const imperial = state.settings?.units === 'imperial' ? true : false
/*
* Metadata can be ignored
*/
if (step.name === 'settings' && step.path[1] === 'metadata') return null
/*
* Defer for anything else to this method
*/
const data = Swizzled.methods.getUndoStepData({ step, state, Design, imperial })
if (data === false) return <pre>{JSON.stringify(step, null, 2)}</pre> //null
if (data === null) return <p>Unsupported</p>
if (compact)
return (
<Swizzled.components.ButtonFrame dense onClick={() => update.restore(index, state._)}>
<div className="flex flex-row items-center align-start gap-2 w-full">
<Swizzled.components.UndoIcon text={index} className="w-5 h-5 text-secondary" />
{data.msg ? data.msg : t(`pe:${data.optCode}`)}
</div>
</Swizzled.components.ButtonFrame>
)
return (
<>
<p className="text-sm italic font-medium opacity-70 text-right p-0 m-0 -mb-2 pr-2">
<Swizzled.components.UndoStepTimeAgo step={step} />
</p>
<Swizzled.components.ButtonFrame onClick={() => update.restore(index, state._)}>
<div className="flex flex-row items-center justify-between gap-2 w-full m-0 p-0 -mt-2 text-lg">
<span className="flex flex-row gap-2 items-center">
{data.fieldIcon || null}
{t(`pe:${data.optCode}`)}
</span>
<span className="opacity-70 flex flex-row gap-1 items-center text-base">
{data.icon || null} {t(`pe:${data.titleCode}`)}
</span>
</div>
<div className="flex flex-row gap-1 items-center align-start w-full">
{data.msg ? (
data.msg
) : (
<>
<span className="">
{Array.isArray(data.newVal) ? data.newVal.join(', ') : data.newVal}
</span>
<Swizzled.components.LeftIcon
className="w-4 h-4 text-secondary shrink-0"
stroke={4}
/>
<span className="line-through decoration-1 opacity-70">
{Array.isArray(data.oldVal) ? data.oldVal.join(', ') : data.oldVal}
</span>
</>
)}
</div>
</Swizzled.components.ButtonFrame>
</>
)
}

View file

@ -493,10 +493,10 @@ export const ReloadIcon = (props) => (
</IconWrapper>
)
// Looks like a single rewind arrow
// Looks like a backspace key
export const ResetIcon = (props) => (
<IconWrapper {...props}>
<path d="M16 18 l 0 -12 l -8 6 z M 6 6 l 0 12 l 1 0 l 0 -10 z" />
<path d="M12 9.75 14.25 12m0 0 2.25 2.25M14.25 12l2.25-2.25M14.25 12 12 14.25m-2.58 4.92-6.374-6.375a1.125 1.125 0 0 1 0-1.59L9.42 4.83c.21-.211.497-.33.795-.33H19.5a2.25 2.25 0 0 1 2.25 2.25v10.5a2.25 2.25 0 0 1-2.25 2.25h-9.284c-.298 0-.585-.119-.795-.33Z" />
</IconWrapper>
)

View file

@ -0,0 +1,122 @@
import { i18n as aaron } from '@freesewing/aaron'
import { i18n as albert } from '@freesewing/albert'
import { i18n as bee } from '@freesewing/bee'
import { i18n as bella } from '@freesewing/bella'
import { i18n as benjamin } from '@freesewing/benjamin'
import { i18n as bent } from '@freesewing/bent'
import { i18n as bibi } from '@freesewing/bibi'
import { i18n as bob } from '@freesewing/bob'
import { i18n as breanna } from '@freesewing/breanna'
import { i18n as brian } from '@freesewing/brian'
import { i18n as bruce } from '@freesewing/bruce'
import { i18n as carlita } from '@freesewing/carlita'
import { i18n as carlton } from '@freesewing/carlton'
import { i18n as cathrin } from '@freesewing/cathrin'
import { i18n as charlie } from '@freesewing/charlie'
import { i18n as cornelius } from '@freesewing/cornelius'
import { i18n as diana } from '@freesewing/diana'
import { i18n as florence } from '@freesewing/florence'
import { i18n as florent } from '@freesewing/florent'
import { i18n as gozer } from '@freesewing/gozer'
import { i18n as hi } from '@freesewing/hi'
import { i18n as holmes } from '@freesewing/holmes'
import { i18n as hortensia } from '@freesewing/hortensia'
import { i18n as huey } from '@freesewing/huey'
import { i18n as hugo } from '@freesewing/hugo'
import { i18n as jaeger } from '@freesewing/jaeger'
import { i18n as jane } from '@freesewing/jane'
import { i18n as lucy } from '@freesewing/lucy'
import { i18n as lumina } from '@freesewing/lumina'
import { i18n as lumira } from '@freesewing/lumira'
import { i18n as lunetius } from '@freesewing/lunetius'
import { i18n as noble } from '@freesewing/noble'
import { i18n as octoplushy } from '@freesewing/octoplushy'
import { i18n as onyx } from '@freesewing/onyx'
import { i18n as opal } from '@freesewing/opal'
import { i18n as otis } from '@freesewing/otis'
import { i18n as paco } from '@freesewing/paco'
import { i18n as penelope } from '@freesewing/penelope'
import { i18n as sandy } from '@freesewing/sandy'
import { i18n as shelly } from '@freesewing/shelly'
import { i18n as shin } from '@freesewing/shin'
import { i18n as simon } from '@freesewing/simon'
import { i18n as simone } from '@freesewing/simone'
import { i18n as skully } from '@freesewing/skully'
import { i18n as sven } from '@freesewing/sven'
import { i18n as tamiko } from '@freesewing/tamiko'
import { i18n as teagan } from '@freesewing/teagan'
import { i18n as tiberius } from '@freesewing/tiberius'
import { i18n as titan } from '@freesewing/titan'
import { i18n as trayvon } from '@freesewing/trayvon'
import { i18n as tristan } from '@freesewing/tristan'
import { i18n as uma } from '@freesewing/uma'
import { i18n as umbra } from '@freesewing/umbra'
import { i18n as wahid } from '@freesewing/wahid'
import { i18n as walburga } from '@freesewing/walburga'
import { i18n as waralee } from '@freesewing/waralee'
import { i18n as yuri } from '@freesewing/yuri'
import { i18n as lily } from '@freesewing/lily'
export const designTranslations = {
aaron,
albert,
bee,
bella,
benjamin,
bent,
bibi,
bob,
breanna,
brian,
bruce,
carlita,
carlton,
cathrin,
charlie,
cornelius,
diana,
florence,
florent,
gozer,
hi,
holmes,
hortensia,
huey,
hugo,
jaeger,
jane,
lucy,
lumina,
lumira,
lunetius,
noble,
octoplushy,
onyx,
opal,
otis,
paco,
penelope,
sandy,
shelly,
shin,
simon,
simone,
skully,
sven,
tamiko,
teagan,
tiberius,
titan,
trayvon,
tristan,
uma,
umbra,
wahid,
walburga,
waralee,
yuri,
lily,
}
export const useDesignTranslation = (design) =>
designTranslations[design] ? designTranslations[design] : false

View file

@ -66,6 +66,8 @@
"./hooks/useAccount": "./hooks/useAccount/index.mjs",
"./hooks/useBackend": "./hooks/useBackend/index.mjs",
"./hooks/useControl": "./hooks/useControl/index.mjs",
"./hooks/useDesign": "./hooks/useDesign/index.mjs",
"./hooks/useDesignTranslation": "./hooks/useDesignTranslation/index.mjs",
"./hooks/useSelection": "./hooks/useSelection/index.mjs",
"./lib/RestClient": "./lib/RestClient/index.mjs",
"./lib/logoPath": "./components/Logo/path.mjs"

View file

@ -69,6 +69,23 @@ export function cloudflareImageUrl({ id = 'default-avatar', variant = 'public' }
return `${cloudflareConfig.url}${id}/${variant}`
}
/**
* Determines the design optino type based on the option's config
*
* @param {object} option - The option config
* @return {string} type - The option type
*/
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'
if (typeof option?.deg !== 'undefined') return 'deg'
if (typeof option?.list !== 'undefined') return 'list'
if (typeof option?.mm !== 'undefined') return 'mm'
return 'constant'
}
/*
* Parses value that should be a distance (cm or inch) into a value in mm
*
@ -179,6 +196,16 @@ export function formatMm(val, units, format = 'html') {
}
}
/**
* Format a percentage (as in, between 0 and 1)
*
* @param {number} val - The value
* @return {string} pct - The value formatted as percentage
*/
export function formatPercentage(val) {
return Math.round(1000 * val) / 10 + '%'
}
/** convert a value that may contain a fraction to a decimal */
export function fractionToDecimal(value) {
// if it's just a number, return it

View file

@ -13,7 +13,7 @@ export default {
'./tailwind-force.html',
],
plugins: [daisyui],
corePlugins: { preflight: false },
//corePlugins: { preflight: false },
darkMode: ['class', "[data-theme='dark']"],
prefix: 'tw-',
daisyui: {