feat: New pattern editor (#7080)
This is not exactly finished or ready for prime-time, but I do feel that leaving it in a feature branch longer will only cause the spread between the `develop` branch and this to grow. Given that I've taken care to not break the current site, I plan to merge this and then keep polishing things. Some views are not implemented yet, and overall there's a need to polish to limit the amount of re-renders and improve performance.
This commit is contained in:
parent
ea0746313e
commit
adefbe7d18
80 changed files with 9223 additions and 76 deletions
|
@ -23,7 +23,12 @@
|
|||
".": {
|
||||
"internal": "./src/index.mjs",
|
||||
"default": "./dist/index.mjs"
|
||||
}
|
||||
},
|
||||
"./pattern": "./src/pattern/index.mjs",
|
||||
"./xray": "./src/pattern-xray/index.mjs",
|
||||
"./editor": "./src/editor/index.mjs",
|
||||
"./icons": "./src/editor/swizzle/components/icons.mjs"
|
||||
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node build.mjs",
|
||||
|
@ -40,10 +45,15 @@
|
|||
"wbuild:all": "yarn wbuild"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=14"
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"html-react-parser": "^5.0.7"
|
||||
"axios": "1.6.8",
|
||||
"html-react-parser": "^5.0.7",
|
||||
"nuqs": "^1.17.6",
|
||||
"react-markdown": "^9.0.1",
|
||||
"use-local-storage-state": "19.1.0",
|
||||
"use-session-storage-state": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"files": [
|
||||
|
|
137
packages/react-components/src/editor/components/view-wrapper.mjs
Normal file
137
packages/react-components/src/editor/components/view-wrapper.mjs
Normal file
|
@ -0,0 +1,137 @@
|
|||
// Hooks
|
||||
import { useState } from 'react'
|
||||
|
||||
/**
|
||||
* The editor view wrapper component
|
||||
*
|
||||
* Figures out what view to load initially,
|
||||
* and handles state for the pattern, inclding the view
|
||||
*
|
||||
* @param (object) props - All the props
|
||||
* @param {object} props.designs - An object holding all designs
|
||||
* @param {object} props.preload - Object holding state to preload
|
||||
* @param {object} props.locale - The locale (language) code
|
||||
* @param {object} props.design - A design name to force the editor to use this design
|
||||
* @param {object} props.Swizzled - An object holding swizzled code
|
||||
*/
|
||||
export const ViewWrapper = ({ designs = {}, preload = {}, design = false, Swizzled }) => {
|
||||
/*
|
||||
* Ephemeral state will not be stored in the state backend
|
||||
* It is used for things like loading state and so on
|
||||
*/
|
||||
const [ephemeralState, setEphemeralState] = useState({})
|
||||
// Editor state
|
||||
const allState = Swizzled.hooks.useEditorState(
|
||||
Swizzled.methods.initialEditorState(preload),
|
||||
setEphemeralState
|
||||
)
|
||||
const state = allState[0]
|
||||
const update = allState[2]
|
||||
|
||||
// Don't bother before state is initialized
|
||||
if (!state) return <Swizzled.components.TemporaryLoader />
|
||||
|
||||
// Figure out what view to load
|
||||
const [View, extraProps] = viewfinder({ design, designs, preload, state, Swizzled })
|
||||
|
||||
/*
|
||||
* Pass this down to allow disabling features that require measurements
|
||||
*/
|
||||
const { missingMeasurements = [] } = extraProps
|
||||
|
||||
/*
|
||||
* Almost all editor state has a default settings, and when that is selected
|
||||
* we just unset that value in the state. This way, state holds only what is
|
||||
* customized, and it makes it a lot easier to see how a pattern was edited.
|
||||
* The big exception is the 'ui.ux' setting. If it is unset, a bunch of
|
||||
* components will not function properly. We could guard against this by passing
|
||||
* the default to all of these components, but instead, we just check that state
|
||||
* is undefined, and if so pass down the default ux value here.
|
||||
* This way, should more of these exceptions get added over time, we can use
|
||||
* the same centralized solution.
|
||||
*/
|
||||
const passDownState =
|
||||
state.ui.ux === undefined
|
||||
? {
|
||||
...state,
|
||||
ui: { ...state.ui, ux: Swizzled.config.defaultUx },
|
||||
_: { ...ephemeralState, missingMeasurements },
|
||||
}
|
||||
: { ...state, _: { ...ephemeralState, missingMeasurements } }
|
||||
|
||||
return (
|
||||
<div className="flex flex-row items-top">
|
||||
{Swizzled.config.withAside ? (
|
||||
<Swizzled.components.AsideViewMenu update={update} state={passDownState} />
|
||||
) : null}
|
||||
<div
|
||||
className={
|
||||
state.ui.kiosk
|
||||
? 'md:z-30 md:w-screen md:h-screen md:fixed md:top-0 md:left-0 md:bg-base-100'
|
||||
: 'grow w-full'
|
||||
}
|
||||
>
|
||||
<Swizzled.components.LoadingStatus state={passDownState} update={update} />
|
||||
<View {...extraProps} {...{ update, designs }} state={passDownState} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to figure out what view to load
|
||||
* based on the props passed in, and destructure
|
||||
* the props we need for it.
|
||||
*
|
||||
* @param (object) props - All the props
|
||||
* @param {object} props.design - The (name of the) current design
|
||||
* @param {object} props.designs - An object holding all designs
|
||||
* @param (object) props.state - React state passed down from the wrapper view
|
||||
*/
|
||||
const viewfinder = ({ design, designs, state, Swizzled }) => {
|
||||
/*
|
||||
* Grab Design from props or state and make them extra props
|
||||
*/
|
||||
if (!design && state?.design) design = state.design
|
||||
const Design = designs[design] || false
|
||||
const extraProps = { design, Design }
|
||||
|
||||
/*
|
||||
* If no design is set, return the designs view
|
||||
*/
|
||||
if (!designs[design]) return [getViewComponent('designs', Swizzled), extraProps]
|
||||
|
||||
/*
|
||||
* If we have a design, do we have the measurements?
|
||||
*/
|
||||
const [measurementsOk, missingMeasurements] = Swizzled.methods.hasRequiredMeasurements(
|
||||
designs[design],
|
||||
state.settings?.measurements
|
||||
)
|
||||
if (missingMeasurements) extraProps.missingMeasurements = missingMeasurements
|
||||
|
||||
/*
|
||||
* Allow all views that do not require measurements before
|
||||
* we force the user to the measurements view
|
||||
*/
|
||||
if (state.view && Swizzled.config.measurementsFreeViews.includes(state.view)) {
|
||||
const view = getViewComponent(state.view, Swizzled)
|
||||
if (view) return [view, extraProps]
|
||||
}
|
||||
|
||||
if (!measurementsOk) return [getViewComponent('measurements', Swizzled), extraProps]
|
||||
|
||||
/*
|
||||
* If a view is set, return that
|
||||
*/
|
||||
const view = getViewComponent(state.view, Swizzled)
|
||||
if (view) return [view, extraProps]
|
||||
|
||||
/*
|
||||
* If no obvious view was found, return the view picker
|
||||
*/
|
||||
return [getViewComponent('picker', Swizzled), extraProps]
|
||||
}
|
||||
|
||||
const getViewComponent = (view = false, Swizzled) =>
|
||||
view ? Swizzled.components[Swizzled.config.viewComponents[view]] : false
|
290
packages/react-components/src/editor/en.yaml
Normal file
290
packages/react-components/src/editor/en.yaml
Normal file
|
@ -0,0 +1,290 @@
|
|||
# Popout component
|
||||
comment: Comment
|
||||
note: Note
|
||||
tip: Tip
|
||||
warning: Warning
|
||||
fixme: FIXME
|
||||
link: Link
|
||||
related: Related
|
||||
|
||||
# Designs view
|
||||
chooseADesign: Choose a Design
|
||||
# Mesurements view
|
||||
measurements: Measurements
|
||||
measurementsAreOk: We have all required measurements to draft this pattern.
|
||||
editMeasurements: Edit Measurements
|
||||
editMeasurementsDesc: You can manually set or override measurements below.
|
||||
requiredMeasurements: Required Measurements
|
||||
optionalMeasurements: Optional Measurements
|
||||
missingMeasurements: Missing Measurements
|
||||
missingMeasurementsInfo: To generate this pattern, we need the following additional measurements
|
||||
missingMeasurementsNotify: To generate this pattern, we need some additional measurements
|
||||
|
||||
# Measurements sets
|
||||
noOwnSets: You do not have any of your own measurements sets (yet)
|
||||
pleaseMtm: Because our patterns are bespoke, we strongly suggest you take accurate measurements.
|
||||
noOwnSetsMsg: You can store your measurements as a measurements set, after which you can generate as many patterns as you want for them.
|
||||
chooseFromOwnSets: Choose one of your own measurements sets
|
||||
chooseFromOwnSetsDesc: Pick any of your own measurements sets that have all required measurements to generate this pattern.
|
||||
newSet: Create a new measurements set
|
||||
someSetsLacking: Some of your measurements sets lack the measurements required to generate this pattern
|
||||
chooseFromBookmarkedSets: Choose one of the measurements sets you've bookmarked
|
||||
chooseFromBookmarkedSetsDesc: If you've bookmarked any measurements sets, you can select from those too.
|
||||
chooseFromCuratedSets: Choose one of FreeSewing's curated measurements sets
|
||||
chooseFromCuratedSetsDesc: If you're just looking to try out our platform, you can select from our list of curated measurements sets.
|
||||
|
||||
# View wrapper
|
||||
requiredPropsMissing.t: Required props are missing
|
||||
requiredPropsMissing.d: The FreeSewing pattern editor needs to be initialized properly. It currently lacks some props to be able to bootstrap.
|
||||
|
||||
# View picker
|
||||
chooseAnActivity: Choose an activity
|
||||
chooseAnotherActivity: Choose a different activity
|
||||
view.draft.t: Draft Pattern
|
||||
view.draft.d: Choose this if you are not certain what to pick
|
||||
view.measurements.t: Pattern Measurements
|
||||
view.measurements.d: Update or load measurements to generate a pattern for
|
||||
view.test.t: Test Design
|
||||
view.test.d: See how different options or changes in measurements influence the design
|
||||
view.timing.t: Time Design
|
||||
view.timing.d: Shows detailed timing of the pattern being drafted, allowing you to find bottlenecks in performance
|
||||
view.printLayout.t: Print Layout
|
||||
view.printLayout.d: Organize your pattern parts to minimize paper use
|
||||
view.save.t: Save pattern as...
|
||||
view.save.d: Save the changes to this pattern in your account, or save it as a new pattern
|
||||
view.export.t: Export Pattern
|
||||
view.export.d: Export this pattern into a variety of formats
|
||||
view.editSettings.t: Edit settings by hand
|
||||
view.editSettings.d: Throw caution to the wind, and hand-edit the pattern's settings
|
||||
view.logs.t: Pattern Logs
|
||||
view.logs.d: Show the logs generated by the pattern, useful to troubleshoot problems
|
||||
view.inspect.t: Pattern inspector
|
||||
view.inspect.d: Load the pattern in the inspector, giving you in-depth info about a pattern's components
|
||||
view.docs.t: Documentation
|
||||
view.docs.d: More information and links to documentation
|
||||
view.designs.t: Choose a different Design
|
||||
view.designs.d: "Current design: {- design }"
|
||||
view.picker.t: Choose a different view
|
||||
view.undos.t: Undo History
|
||||
view.undos.d: Time-travel through your recent pattern changes
|
||||
showAdvancedOptions: Show advanced options
|
||||
hideAdvancedOptions: Hide advanced options
|
||||
views.t: Views
|
||||
views.d: Choose between the main views of the pattern editor
|
||||
measurementsFreeViewsOnly.t: We are only showing activities that do not require measurements
|
||||
measurementsFreeViewsOnly.d: Once we have all measurements required to generate a pattern, you will have more choices here.
|
||||
|
||||
|
||||
# menus
|
||||
youAreUsingTheDefaultValue: You are using the default value
|
||||
youAreUsingACustomValue: You are using a custom value
|
||||
designOptions.t: Design Options
|
||||
designOptions.d: These options are specific to this design. You can use them to customize your pattern in a variety of ways.
|
||||
fit.t: Fit
|
||||
style.t: Style
|
||||
advanced.t: Advanced
|
||||
coreSettings.t: Core Settings
|
||||
coreSettings.d: 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.
|
||||
paperless.t: Paperless
|
||||
paperless.d: Trees are awesome, and taping together sewing patterns is not much fun. Try our paperless mode to avoid the need to print out your pattern altogether.
|
||||
samm.t: Seam Allowance Size
|
||||
samm.d: Controls the amount of seam allowance used in your pattern
|
||||
sabool.t: Include Seam Allowance
|
||||
sabool.d: Controls whether or not to include seam allowance in your pattern
|
||||
complete.t: Details
|
||||
complete.d: Controls how detailed the pattern is; Either a complete pattern with all details, or a basic outline of the pattern parts
|
||||
expand.t: Expand
|
||||
expand.d: Controls efforts to save paper. Disable this to expand all pattern parts at the cost of using more space.
|
||||
only.t: Included Parts
|
||||
only.d: Use this to control exactly which pattern parts will be included in your pattern
|
||||
units.t: Units
|
||||
units.d: This setting determines how unit are displayed on your pattern
|
||||
margin.t: Margin
|
||||
margin.d: Controls the margin around pattern parts
|
||||
scale.t: Scale
|
||||
scale.d: Controls the overall line width, font size, and other elements that do not scale with the pattern's measurements
|
||||
yes: Yes
|
||||
no: No
|
||||
completeYes.t: Generate a complete pattern
|
||||
completeYes.d: This will generate a complete pattern with all notations, lines, markings. Use this if you are not certain what to choose.
|
||||
completeNo.t: Generate a pattern outline
|
||||
completeNo.d: Only generate the outline of the pattern parts. Use this if you are looking to use a laser cutter or have other specific needs.
|
||||
expandYes.t: Expand all pattern parts
|
||||
expandYes.d: This will generate a pattern where all pattern parts are drawn to their full size, even if they are simple rectangles.
|
||||
expandNo.t: Keep patterns parts compact where possible
|
||||
expandNo.d: This will draw a more dense representation of the pattern which includes all info without using up too much space & paper.
|
||||
saNo.t: Do not include seam allowance
|
||||
saNo.d: This generates a pattern which does not include any seam allowance. The size of the seam allowance does not matter as no seam allowance will be included.
|
||||
saYes.t: Include seam allowance
|
||||
saYes.d: This generates a pattern that will include seam allowance. The size of the seam allowance is set individually.
|
||||
paperlessNo.t: Generate a regular pattern
|
||||
paperlessNo.d: This will generate a regular pattern, which you can then print out.
|
||||
paperlessYes.t: Generate a paperless pattern
|
||||
paperlessYes.d: This generates a pattern with dimensions and a grid, which allows you to transfer it on fabric or another medium without the need to print out the pattern.
|
||||
metric: Metric
|
||||
uiPreferences.t: UI Preferences
|
||||
uiPreferences.d: These preferences control the UI (User Interface) of the pattern editor
|
||||
renderer.t: Render Engine
|
||||
renderer.d: Controls how the pattern is rendered (drawn) on the screen
|
||||
renderWithReact.t: Render with FreeSewing's React components
|
||||
renderWithReact.d: Render as SVG through our React components. Allows interactivity and is optimized for screen. Use this if you are not sure what to pick.
|
||||
renderWithCore.t: Render with FreeSewing's Core library
|
||||
renderWithCore.d: Render directly to SVG from Core. Allows no interactivity and is optimized for print. Use this if you want to know what it will look like when exported.
|
||||
kiosk.t: Kiosk Mode
|
||||
kiosk.d: Controls how the pattern editor is embedded in the web page.
|
||||
aside.t: Aside Menu
|
||||
aside.d: Controls whether or not to show menus on the side of larger screens
|
||||
withAside.t: Also show menus on the side
|
||||
withAside.d: Shows menus both on the side of the screen, as well as the drop-downs in the header (this only applies to larger screens)
|
||||
noAside.t: Only show menus in the header
|
||||
noAside.d: Only shows the drop-down variant of the menus, making more room for your pattern
|
||||
rotate.t: Rotate pattern
|
||||
rotate.d: Allows you to rotate your pattern 90 degrees to better fit your screen
|
||||
rotateNo.t: Do not rotate pattern
|
||||
rotateNo.d: Show the pattern as it is
|
||||
rotateYes.t: Rotate pattern 90 degrees
|
||||
rotateYes.d: Rotate the pattern 90 degrees counter clockwise
|
||||
websiteMode.t: Use inline mode
|
||||
websiteMode.d: Embeds the pattern editor in the natural flow of the web page.
|
||||
kioskMode.t: Use kiosk mode
|
||||
kioskMode.d: Breaks out the pattern editor to fill the entire page.
|
||||
ux.t: User Experience
|
||||
ux.d: Which user experience do you prefer? From keep it simple, to give me all the powers.
|
||||
inspect.t: Inspect
|
||||
inspect.d: Enabling this will allow you to drill down into the pattern, and pull up information about its various parts, paths, and points.
|
||||
inspectNo.t: Disable the inspector
|
||||
inspectNo.d: This is the default, the pattern inspector is disabled and the pattern is displayed as usual.
|
||||
inspectYes.t: Enable the inspector
|
||||
inspectYes.d: With the pattern inspector enabled and the React rendering engine selected, we will add interactivity to the pattern to allow you to inspect the various elements that make up the pattern.
|
||||
draft: Draft
|
||||
test: Test
|
||||
print: Print layout
|
||||
cut: Cut Layout
|
||||
save: Save
|
||||
export: Export
|
||||
edit: Edit
|
||||
draft.t: Draft your pattern
|
||||
draft.d: Launches FreeSewing flagship pattern editor, where you can tweak your pattern to your heart's desire
|
||||
test.t: Test your pattern
|
||||
test.d: See how your pattern adapts to changes in options, or measurements
|
||||
print.t: Print Layout
|
||||
print.d: Allows you to arrange your pattern pieces so you can printing your pattern on as little pages as possible
|
||||
cut.t: Cutting layout
|
||||
cut.d: Allows you to arrange your pattern pieces so you can determine exactly how much fabric you need to make it.
|
||||
save.t: Save your pattern
|
||||
save.d: Save the current pattern to your FreeSewing account
|
||||
export.t: Export your pattern
|
||||
export.d: Allows you to export this pattern to a variety of formats
|
||||
logs.t: Pattern logs
|
||||
enterCustomValue: Enter a custom value
|
||||
|
||||
# ux
|
||||
ux1.t: Keep it as simple as possible
|
||||
ux1.d: Hides all but the most essential features.
|
||||
ux2.t: Keep it simple, but not too simple
|
||||
ux2.d: Hides the majority of features.
|
||||
ux3.t: Balance simplicity with power
|
||||
ux3.d: Reveals the majority of features, but not all.
|
||||
ux4.t: Give me all powers, but keep me safe
|
||||
ux4.d: Reveals all features, keeps handrails and safety checks.
|
||||
ux5.t: Get out of my way
|
||||
ux5.d: Reveals all features, removes all handrails and safety checks.
|
||||
|
||||
# Tooltips
|
||||
tt.changeEditorView: Change to a different view
|
||||
tt.toggleSa: Turns Seam Allowance on or off (see Core Settings)
|
||||
tt.togglePaperless: Turns Paperless on or off (see Core Settings)
|
||||
tt.toggleComplete: Turns Details on or off (see Core Settings)
|
||||
tt.toggleExpand: Turns Expand on or off (see Core Settings)
|
||||
tt.toggleUnits: Switches Units between metric and imperial (see Core Settings)
|
||||
tt.changeUx: Changes your UX setting (see UI Preferences)
|
||||
tt.toggleAside: Turn the Aside Menu on or off (see UI Preferences)
|
||||
tt.toggleKiosk: Turns Kiosk Mode on or off (see UI Preferences)
|
||||
tt.toggleRotate: Turns Rotate Pattern on or off (see UI Preferences)
|
||||
tt.toggleRenderer: Switches the Render Engine between React and SVG (see UI Preferences)
|
||||
tt.exportPattern: Export pattern
|
||||
tt.savePattern: Save pattern
|
||||
tt.savePatternAs: Save pattern as...
|
||||
tt.undo: Undo most recent change
|
||||
tt.undoAll: Undo all changes since the last save point
|
||||
tt.resetDesign: Reset all settings, but keep the design and measurements
|
||||
tt.resetAll: Reset the editor completely
|
||||
|
||||
# flags
|
||||
apply: Apply
|
||||
decrease: Decrease
|
||||
disable: Disable
|
||||
dismiss: Dismiss
|
||||
expandIsOff.t: This design saves space (and trees) because expand is disabled
|
||||
expandIsOff.d: "Because the **expand** core setting is currently disabled, some parts are not fully drawn or not shown at all. Typically, these are simple rectangles that only take up space, or things that can be cut on the fold. \n\nTo expand all pattern parts to their full size, enable the expand setting."
|
||||
expandIsOn.t: This design can save space (and trees)
|
||||
expandIsOn.d: "Because the **expand** core setting is currently enabled, all parts are fully drawn. You can display this design in a more compact way by disabling the **expand** setting. \n\nDoing so will mean that some parts are not fully drawn or not shown at all. Typically, these are simple rectangles that only take up space, or things that can be cut on the fold."
|
||||
enable: Enable
|
||||
flags: Flags
|
||||
flagMenu.t: Flags
|
||||
flagMenuOne.d: A specific issue about your current pattern needs your attention.
|
||||
flagMenuMany.d: Some issues about your current pattern need your attention.
|
||||
hide: Hide
|
||||
increase: Increase
|
||||
show: Show
|
||||
saIncluded: (This includes seam allowance)
|
||||
saExcluded: (This does not include seam allowance)
|
||||
saUnused: (This part does not require any seam allowance)
|
||||
partHiddenByExpand: This part is not shown because the **expand** core setting is currently disabled. Enable it to show this pattern part.
|
||||
|
||||
# Auth
|
||||
authRequired: Authentication required
|
||||
membersOnly: This functionality requires a FreeSewing account.
|
||||
signUp: Sign Up
|
||||
signIn: Sign In
|
||||
statusUnknown: Account status warning
|
||||
statusUnknownMsg: Your account status prohibits us from processing your data. Please contact support.
|
||||
consentLacking: Consent lacking
|
||||
consentLackingMsg: We do not have your consent for processing your data. Without consent, we have no legal basis to process your data.
|
||||
accountProhibited: Your account has been disabled
|
||||
accountProhibitedMsg: Your account has been administratively disabled.
|
||||
accountDisabled: Account disabled
|
||||
accountDisabledMsg: You cannot re-enable a disabled account. You need to contact support to address this.
|
||||
accountInactive: Your account is inactive
|
||||
accountInactiveMsg: You must activate your account via the signup link we sent you.
|
||||
signupAgain: If you cannot find the link, you can receive a new one by signing up again.
|
||||
cannotUse: A disabled account cannot be used.
|
||||
contactSupport: Contact support
|
||||
reviewConsent: Review your consent
|
||||
roleLacking: You lack the required role to access this content
|
||||
roleLackingMsg: This content requires the <b>{ requiredRole }</b> role. Your role is <b>{ role }</b> which does not grant you access to this content.
|
||||
|
||||
# save pattern
|
||||
bookmarkPattern: Bookmark pattern
|
||||
savePattern: Save pattern
|
||||
saveAsNewPattern: Save as a New Pattern
|
||||
savePatternAs: Save pattern as...
|
||||
savePatternAsHellip: Save pattern as...
|
||||
patternBookmarkCreated: Pattern bookmark created
|
||||
see: See
|
||||
addNotes: Add notes
|
||||
addSettingsToNotes: Add settings to notes
|
||||
exporting: Exporting
|
||||
exportAsData: Export as data
|
||||
exportForEditing: Export for editing
|
||||
exportForPrinting: Export for printing
|
||||
exportPattern-txt: Export a PDF suitable for your printer, or download this pattern in a variety of formats
|
||||
exportPattern: Export pattern
|
||||
settings: Settings
|
||||
patternTitle: Pattern Title
|
||||
patternNotes: Pattern Notes
|
||||
toAccessPatternsGoTo: To access your patterns, go to
|
||||
genericLoadingMessage: Hang tight, we're working on it...
|
||||
patternSavedAs: Pattern saved as
|
||||
cancel: Cancel
|
||||
|
||||
# undo history
|
||||
secondsAgo: seconds ago
|
||||
minutesAgo: minutes ago
|
||||
hoursAgo: hours ago
|
||||
undos.unknown.t: Unknown Change
|
||||
defaultRestored: Cleared (default restored)
|
||||
includeAllParts: Include all parts
|
||||
allFirstLetter: A
|
||||
undo: Undo
|
||||
xMeasurementsChanged: "{ count } Measurements changed"
|
89
packages/react-components/src/editor/index.mjs
Normal file
89
packages/react-components/src/editor/index.mjs
Normal file
|
@ -0,0 +1,89 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { swizzleConfig } from './swizzle/config.mjs'
|
||||
import { swizzleComponents } from './swizzle/components/index.mjs'
|
||||
import { swizzleHooks } from './swizzle/hooks/index.mjs'
|
||||
import { swizzleMethods } from './swizzle/methods/index.mjs'
|
||||
import { ViewWrapper } from './components/view-wrapper.mjs'
|
||||
// This is an exception as we need to show something before Swizzled components are ready
|
||||
import { TemporaryLoader as UnswizzledTemporaryLoader } from './swizzle/components/loaders.mjs'
|
||||
|
||||
/*
|
||||
* Namespaces used by the pattern editor
|
||||
*/
|
||||
export const ns = ['pe', 'measurements']
|
||||
|
||||
/**
|
||||
* PatternEditor is the high-level FreeSewing component
|
||||
* that provides the entire pattern editing environment
|
||||
*
|
||||
* @param {object} props.design = The name of the design we are editing
|
||||
* @param {object} props.designs = An object holding the designs code
|
||||
* @param {object} props.components = An object holding components to swizzle
|
||||
* @param {object} props.hooks = An object holding hooks to swizzle
|
||||
* @param {object} props.methods = An object holding methods to swizzle
|
||||
* @param {object} props.config = An object holding the editor config to swizzle
|
||||
* @param {object} props.locale = The locale (language) code
|
||||
* @param {object} props.preload = Any state to preload
|
||||
*
|
||||
*/
|
||||
export const PatternEditor = (props) => {
|
||||
const [swizzled, setSwizzled] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!swizzled) {
|
||||
const merged = {
|
||||
config: swizzleConfig(props.config),
|
||||
}
|
||||
merged.methods = swizzleMethods(props.methods, merged)
|
||||
merged.components = swizzleComponents(props.components, merged)
|
||||
merged.hooks = swizzleHooks(props.hooks, merged)
|
||||
setSwizzled(merged)
|
||||
}
|
||||
}, [swizzled, props.components, props.config, props.hooks, props.methods])
|
||||
|
||||
if (!swizzled?.hooks) return <UnswizzledTemporaryLoader />
|
||||
/*
|
||||
* First of all, make sure we have all the required props
|
||||
*/
|
||||
const lackingProps = lackingPropsCheck(props)
|
||||
if (lackingProps !== false) return <LackingPropsError error={lackingProps} />
|
||||
|
||||
/*
|
||||
* Extract props we care about
|
||||
*/
|
||||
const { designs = {}, locale = 'en', preload } = props
|
||||
|
||||
/*
|
||||
* Now return the view wrapper and pass it the relevant props and the swizzled props
|
||||
*/
|
||||
return <ViewWrapper {...{ designs, locale, preload }} Swizzled={swizzled} />
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to verify that all props that are required to
|
||||
* run the editor are present.
|
||||
*
|
||||
* Note that these errors are not translation, because they are
|
||||
* not intended for end-users, but rather for developers.
|
||||
*
|
||||
* @param {object} props - The props passed to the PatternEditor component
|
||||
* @return {bool} result - Either true or false depending on required props being present
|
||||
*/
|
||||
const lackingPropsCheck = (props) => {
|
||||
if (typeof props.designs !== 'object')
|
||||
return "Please pass a 'designs' prop with the designs supported by this editor"
|
||||
if (Object.keys(props.designs).length < 1) return "The 'designs' prop does not hold any designs"
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* A component to inform the user that the editor cannot be started
|
||||
* because there are missing required props
|
||||
*/
|
||||
const LackingPropsError = ({ error }) => (
|
||||
<div className="w-full p-0 text-center py-24">
|
||||
<h2>Unable to initialize pattern editor</h2>
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
)
|
23
packages/react-components/src/editor/nl.yaml
Normal file
23
packages/react-components/src/editor/nl.yaml
Normal file
|
@ -0,0 +1,23 @@
|
|||
# Popout component
|
||||
comment: Opmerking
|
||||
note: Notitie
|
||||
tip: Tip
|
||||
warning: Waarschuwing
|
||||
fixme: FIXME
|
||||
link: Link
|
||||
related: Gerelateerd
|
||||
|
||||
# Mesurements view
|
||||
measurements: Maten
|
||||
measurementsAreOk: We hebben alle benodigde maten om dit patroon te tekenen.
|
||||
editMeasurements: Maten Aanpassen
|
||||
editMeasurementsDesc: Hier kan je manueel de maten aanpassen.
|
||||
requiredMeasurements: Vereiste Maten
|
||||
optionalMeasurements: Optionele Maten
|
||||
|
||||
# Designs view
|
||||
pickADesign: Kies een ontwerp
|
||||
|
||||
# View wrapper
|
||||
requiredPropsMissing.t: Vereiste props ontbreken
|
||||
requiredPropsMissing.d: De FreeSewing patroon editor moet correct geinitialiseerd worden. Momenteel ontbreken een aantal props die noodzakelijk zijn om de editor te starten.
|
26
packages/react-components/src/editor/props.md
Normal file
26
packages/react-components/src/editor/props.md
Normal file
|
@ -0,0 +1,26 @@
|
|||
# List of props that can be passed to the pattern editor
|
||||
|
||||
| Prop | Default | Description |
|
||||
| ---- | ------- | ----------- |
|
||||
| `design` | `undefined` | Name of the current design (key in the `objects` prop).<br>Note that this will set the initial state, but it can be changed by the user. |
|
||||
| `designs` | `{}` |Object holding all designs that are available. |
|
||||
| `locale` | `en` | Language code |
|
||||
| `imperial`| `false` | Whether to use imperial units as the default, or not |
|
||||
| `components` | `{}` | Object holding swizzled components |
|
||||
| `hooks` | `{}` | Object holding swizzled hooks |
|
||||
| `methods` | `{}` | Object holding swizzled methods |
|
||||
|
||||
|
||||
## Defaults object
|
||||
|
||||
```mjs
|
||||
{
|
||||
locale: 'en',
|
||||
imperial: 'false',
|
||||
ui: {
|
||||
renderer: 'react',
|
||||
kiosk: false,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
import { useState } from 'react'
|
||||
|
||||
/*
|
||||
* DaisyUI's accordion seems rather unreliable.
|
||||
* So instead, we handle this in React state
|
||||
*/
|
||||
const getProps = (isActive) => ({
|
||||
className: `p-2 px-4 rounded-lg bg-transparent shadow
|
||||
w-full mt-2 py-4 h-auto content-start text-left bg-opacity-20
|
||||
${isActive ? 'hover:bg-transparent' : 'hover:bg-secondary hover:bg-opacity-10'}`,
|
||||
})
|
||||
|
||||
const getSubProps = (isActive) => ({
|
||||
className: ` p-2 px-4 rounded bg-transparent w-full mt-2 py-4 h-auto
|
||||
content-start bg-secondary text-left bg-opacity-20
|
||||
${
|
||||
isActive
|
||||
? 'bg-opacity-100 hover:bg-transparent shadow'
|
||||
: 'hover:bg-opacity-10 hover:bg-secondary '
|
||||
}`,
|
||||
})
|
||||
|
||||
const components = {
|
||||
button: (props) => <button {...props}>{props.children}</button>,
|
||||
div: (props) => <div {...props}>{props.children}</div>,
|
||||
}
|
||||
|
||||
export const BaseAccordion = ({
|
||||
items, // Items in the accordion
|
||||
act, // Allows one to preset the active (opened) entry
|
||||
propsGetter = getProps, // Method to get the relevant props
|
||||
component = 'button',
|
||||
}) => {
|
||||
const [active, setActive] = useState(act)
|
||||
const Component = components[component]
|
||||
|
||||
return (
|
||||
<nav>
|
||||
{items
|
||||
.filter((item) => item[0])
|
||||
.map((item, i) =>
|
||||
active === item[2] ? (
|
||||
<div key={i} {...propsGetter(true)}>
|
||||
<Component onClick={setActive} className="w-full hover:cursor-pointer">
|
||||
{item[0]}
|
||||
</Component>
|
||||
{item[1]}
|
||||
</div>
|
||||
) : (
|
||||
<Component
|
||||
key={i}
|
||||
{...propsGetter(active === item[2])}
|
||||
onClick={() => setActive(item[2])}
|
||||
>
|
||||
{item[0]}
|
||||
</Component>
|
||||
)
|
||||
)}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
export const SubAccordion = (props) => <BaseAccordion {...props} propsGetter={getSubProps} />
|
||||
export const Accordion = (props) => <BaseAccordion {...props} propsGetter={getProps} />
|
|
@ -0,0 +1,122 @@
|
|||
import { useState } from 'react'
|
||||
|
||||
export const AsideViewMenuButton = ({
|
||||
href,
|
||||
label,
|
||||
children,
|
||||
onClick = false,
|
||||
active = false,
|
||||
extraClasses = 'lg:hover:bg-secondary lg:hover:text-secondary-content',
|
||||
Swizzled,
|
||||
}) => {
|
||||
const className = `w-full flex flex-row items-center px-4 py-2 ${extraClasses} ${
|
||||
active
|
||||
? 'font-bold lg:font-normal bg-secondary bg-opacity-10 lg:bg-secondary lg:text-secondary-content lg:bg-opacity-50'
|
||||
: 'lg:bg-neutral lg:text-neutral-content'
|
||||
}`
|
||||
const span = <span className="block grow text-left">{label}</span>
|
||||
|
||||
return onClick ? (
|
||||
<button {...{ onClick, className }} title={label}>
|
||||
{span}
|
||||
{children}
|
||||
</button>
|
||||
) : (
|
||||
<Swizzled.components.Link {...{ href, className }} title={label}>
|
||||
{span}
|
||||
{children}
|
||||
</Swizzled.components.Link>
|
||||
)
|
||||
}
|
||||
|
||||
export const ViewTypeIcon = ({ Swizzled, view, className = 'h-6 w-6 grow-0' }) => {
|
||||
const Icon = Swizzled.components[`View${Swizzled.methods.capitalize(view)}Icon`]
|
||||
if (!Icon) return <Swizzled.components.OptionsIcon />
|
||||
|
||||
return <Icon className={className} />
|
||||
}
|
||||
|
||||
export const AsideViewMenuSpacer = () => (
|
||||
<hr className="my-1 w-full opacity-20 font-bold border-t-2" />
|
||||
)
|
||||
|
||||
export const AsideViewMenuIcons = ({ state, update, setDense, dense, Swizzled }) => {
|
||||
const { t } = Swizzled.methods
|
||||
const iconSize = 'h-6 w-6 grow-0'
|
||||
|
||||
const output = [
|
||||
<Swizzled.components.AsideViewMenuButton
|
||||
key={1}
|
||||
onClick={() => setDense(!dense)}
|
||||
label={
|
||||
dense ? (
|
||||
''
|
||||
) : (
|
||||
<b>
|
||||
<em className="pl-4 opacity-60">Editor Views</em>
|
||||
</b>
|
||||
)
|
||||
}
|
||||
extraClasses="hidden lg:flex text-accent bg-neutral hover:bg-accent hover:text-neutral-content"
|
||||
>
|
||||
{dense ? (
|
||||
<Swizzled.components.RightIcon
|
||||
className={`${iconSize} group-hover:animate-[bounceright_1s_infinite] animate-[bounceright_1s_5]`}
|
||||
stroke={4}
|
||||
/>
|
||||
) : (
|
||||
<Swizzled.components.LeftIcon className={`${iconSize} animate-bounce-right`} stroke={4} />
|
||||
)}
|
||||
</Swizzled.components.AsideViewMenuButton>,
|
||||
]
|
||||
|
||||
let i = 1
|
||||
for (const view of [
|
||||
'spacer',
|
||||
...Swizzled.config.mainViews,
|
||||
'spacer',
|
||||
...Swizzled.config.extraViews,
|
||||
'spacerOver3',
|
||||
...Swizzled.config.devViews,
|
||||
'spacer',
|
||||
'picker',
|
||||
]) {
|
||||
if (view === 'spacer') output.push(<Swizzled.components.AsideViewMenuSpacer key={i} />)
|
||||
else if (view === 'spacerOver3')
|
||||
output.push(state.ui.ux > 3 ? <Swizzled.components.AsideViewMenuSpacer key={i} /> : null)
|
||||
else if (state.ui.ux >= Swizzled.config.uxLevels.views[view])
|
||||
output.push(
|
||||
<Swizzled.components.AsideViewMenuButton
|
||||
key={view}
|
||||
onClick={() => update.view(view)}
|
||||
label={t(`pe:view.${view}.t`)}
|
||||
active={state.view === view}
|
||||
>
|
||||
<Swizzled.components.ViewTypeIcon view={view} />
|
||||
</Swizzled.components.AsideViewMenuButton>
|
||||
)
|
||||
i++
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
export const AsideViewMenu = ({ Swizzled, update, state }) => {
|
||||
const [dense, setDense] = useState(true)
|
||||
return (
|
||||
<div
|
||||
className={`w-64 min-h-screen pt-4
|
||||
bg-neutral
|
||||
shrink-0 grow-0 self-stretch
|
||||
transition-all
|
||||
drop-shadow-xl
|
||||
${dense ? '-ml-52' : 'ml-0'}`}
|
||||
>
|
||||
<aside className="sticky top-4 group">
|
||||
<div className="flex flex-col items-center w-full">
|
||||
<Swizzled.components.AsideViewMenuIcons {...{ update, state, setDense, dense }} />
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,210 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
|
||||
export const AuthMessageWrapper = ({ children }) => (
|
||||
<div className="m-auto max-w-xl text-center mt-8 p-8">{children}</div>
|
||||
)
|
||||
|
||||
export const ContactSupport = ({ t, Swizzled }) => (
|
||||
<div className="flex flex-row items-center justify-center gap-4 mt-8">
|
||||
<Swizzled.components.Link href="/support" className="btn btn-success w-full">
|
||||
{t('contactSupport')}
|
||||
</Swizzled.components.Link>
|
||||
</div>
|
||||
)
|
||||
|
||||
export const AuthRequired = ({ t, banner, Swizzled }) => (
|
||||
<Swizzled.components.AuthMessageWrapper>
|
||||
{banner}
|
||||
<h2>{Swizzled.methods.t('pe:authRequired')}</h2>
|
||||
<p>{t('pe:membersOnly')}</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 mt-8">
|
||||
<Swizzled.components.Link
|
||||
href="/signup"
|
||||
className={`${Swizzled.config.classes.horFlex} btn btn-secondary w-full`}
|
||||
>
|
||||
<Swizzled.components.PlusIcon />
|
||||
{t('pe:signUp')}
|
||||
</Swizzled.components.Link>
|
||||
<Swizzled.components.Link
|
||||
href="/signin"
|
||||
className={`${Swizzled.config.classes.horFlex} btn btn-secondary btn-outline w-full`}
|
||||
>
|
||||
<Swizzled.components.LockIcon />
|
||||
{t('signIn')}
|
||||
</Swizzled.components.Link>
|
||||
</div>
|
||||
</Swizzled.components.AuthMessageWrapper>
|
||||
)
|
||||
|
||||
export const AccountInactive = ({ t, banner, Swizzled }) => (
|
||||
<Swizzled.components.AuthMessageWrapper>
|
||||
{banner}
|
||||
<h1>{t('accountInactive')}</h1>
|
||||
<p>{t('accountInactiveMsg')}</p>
|
||||
<p>{t('signupAgain')}</p>
|
||||
<div className="flex flex-row items-center justify-center gap-4 mt-8">
|
||||
<Swizzled.components.Link href="/signup" className="btn btn-primary w-full">
|
||||
{t('signUp')}
|
||||
</Swizzled.components.Link>
|
||||
</div>
|
||||
</Swizzled.components.AuthMessageWrapper>
|
||||
)
|
||||
|
||||
export const AccountDisabled = ({ t, banner, Swizzled }) => (
|
||||
<Swizzled.components.AuthMessageWrapper>
|
||||
{banner}
|
||||
<h1>{t('accountDisabled')}</h1>
|
||||
<p>{t('accountDisabledMsg')}</p>
|
||||
<ContactSupport t={t} />
|
||||
</Swizzled.components.AuthMessageWrapper>
|
||||
)
|
||||
|
||||
export const AccountProhibited = ({ t, banner, Swizzled }) => (
|
||||
<Swizzled.components.AuthMessageWrapper>
|
||||
{banner}
|
||||
<h1>{t('accountProhibited')}</h1>
|
||||
<p>{t('accountProhibitedMsg')}</p>
|
||||
<Swizzled.components.ContactSupport t={t} />
|
||||
</Swizzled.components.AuthMessageWrapper>
|
||||
)
|
||||
|
||||
export const AccountStatusUnknown = ({ t, banner, Swizzled }) => (
|
||||
<Swizzled.components.AuthMessageWrapper>
|
||||
{banner}
|
||||
<h1>{t('statusUnknown')}</h1>
|
||||
<p>{t('statusUnknownMsg')}</p>
|
||||
<Swizzled.components.ContactSupport t={t} />
|
||||
</Swizzled.components.AuthMessageWrapper>
|
||||
)
|
||||
|
||||
export const RoleLacking = ({ t, requiredRole, role, banner, Swizzled }) => (
|
||||
<Swizzled.components.AuthMessageWrapper>
|
||||
{banner}
|
||||
<h1>{t('roleLacking')}</h1>
|
||||
<p dangerouslySetInnerHTML={{ __html: t('roleLackingMsg', { requiredRole, role }) }} />
|
||||
<Swizzled.components.ContactSupport t={t} />
|
||||
</Swizzled.components.AuthMessageWrapper>
|
||||
)
|
||||
|
||||
export const ConsentLacking = ({ banner, Swizzled }) => {
|
||||
//const { setAccount, setToken, setSeenUser } = Swizzled.hooks.useAccount()
|
||||
//const backend = Swizzled.hooks.useBackend()
|
||||
|
||||
//const updateConsent = async ({ consent1, consent2 }) => {
|
||||
// let consent = 0
|
||||
// if (consent1) consent = 1
|
||||
// if (consent1 && consent2) consent = 2
|
||||
// if (consent > 0) {
|
||||
// const result = await backend.updateConsent(consent)
|
||||
// if (result.success) {
|
||||
// setToken(result.data.token)
|
||||
// setAccount(result.data.account)
|
||||
// setSeenUser(result.data.account.username)
|
||||
// refresh()
|
||||
// } else {
|
||||
// console.log('something went wrong', result)
|
||||
// refresh()
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
return (
|
||||
<Swizzled.components.AuthMessageWrapper>
|
||||
<div className="text-left">
|
||||
{banner}
|
||||
<p>FIXME: Handle content form</p>
|
||||
{/*<ConsentForm submit={updateConsent} />*/}
|
||||
</div>
|
||||
</Swizzled.components.AuthMessageWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export const AuthWrapper = ({ children, requiredRole = 'user', Swizzled }) => {
|
||||
const { t } = Swizzled.methods
|
||||
const { useAccount, useBackend } = Swizzled.hooks
|
||||
const { account, setAccount, token, admin, stopImpersonating, signOut } = useAccount()
|
||||
const backend = useBackend()
|
||||
|
||||
const [ready, setReady] = useState(false)
|
||||
const [impersonating, setImpersonating] = useState(false)
|
||||
const [error, setError] = useState(false)
|
||||
const [refreshCount, setRefreshCount] = useState(0)
|
||||
|
||||
/*
|
||||
* Avoid hydration errors
|
||||
*/
|
||||
useEffect(() => {
|
||||
const verifyAdmin = async () => {
|
||||
const result = await backend.adminPing(admin.token)
|
||||
if (result.success && result.data.account.role === 'admin') {
|
||||
setImpersonating({
|
||||
admin: result.data.account.username,
|
||||
user: account.username,
|
||||
})
|
||||
}
|
||||
setReady(true)
|
||||
}
|
||||
const verifyUser = async () => {
|
||||
const result = await backend.ping()
|
||||
if (result.success) {
|
||||
// Refresh account in local storage
|
||||
setAccount({
|
||||
...account,
|
||||
...result.data.account,
|
||||
})
|
||||
} else {
|
||||
if (result.data?.error?.error) setError(result.data.error.error)
|
||||
else signOut()
|
||||
}
|
||||
setReady(true)
|
||||
}
|
||||
if (admin && admin.token) verifyAdmin()
|
||||
if (token) verifyUser()
|
||||
else setReady(true)
|
||||
}, [admin, token, refreshCount])
|
||||
|
||||
const refresh = () => {
|
||||
setRefreshCount(refreshCount + 1)
|
||||
setError(false)
|
||||
}
|
||||
|
||||
if (!ready) return <Swizzled.components.Loading />
|
||||
|
||||
const banner = impersonating ? (
|
||||
<div className="bg-warning rounded-lg shadow py-4 px-6 flex flex-row items-center gap-4 justify-between">
|
||||
<span className="text-base-100 text-left">
|
||||
Hi <b>{impersonating.admin}</b>, you are currently impersonating <b>{impersonating.user}</b>
|
||||
</span>
|
||||
<button className="btn btn-neutral" onClick={stopImpersonating}>
|
||||
Stop Impersonating
|
||||
</button>
|
||||
</div>
|
||||
) : null
|
||||
|
||||
const childProps = { t, banner, Swizzled }
|
||||
|
||||
if (!token || !account.username) return <Swizzled.components.AuthRequired {...childProps} />
|
||||
if (error) {
|
||||
if (error === 'accountInactive') return <Swizzled.components.AccountInactive {...childProps} />
|
||||
if (error === 'accountDisabled') return <Swizzled.components.AccountDisabled {...childProps} />
|
||||
if (error === 'accountBlocked') return <Swizzled.components.AccountProhibited {...childProps} />
|
||||
if (error === 'consentLacking')
|
||||
return <Swizzled.components.ConsentLacking {...childProps} refresh={refresh} />
|
||||
return <Swizzled.components.AccountStatusUnknown {...childProps} />
|
||||
}
|
||||
|
||||
if (
|
||||
!Swizzled.config.roles.levels[account.role] ||
|
||||
Swizzled.config.roles.levels[account.role] < Swizzled.config.roles.levels[requiredRole]
|
||||
) {
|
||||
return (
|
||||
<Swizzled.components.RoleLacking
|
||||
{...childProps}
|
||||
role={account.role}
|
||||
requiredRole={requiredRole}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return children
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
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>
|
||||
)
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* The design view is loaded if and only if not design is passed to the editor
|
||||
*
|
||||
* @param {Object} props - All the props
|
||||
* @param {Object} designs - Object holding all designs
|
||||
* @param {object} props.Swizzled - An object holding swizzled code
|
||||
* @param {Object} update - ViewWrapper state update object
|
||||
*/
|
||||
export const DesignsView = ({ designs = {}, Swizzled, update }) => (
|
||||
<div className="text-center mt-8 mb-24">
|
||||
<h2>{Swizzled.methods.t('pe:chooseADesign')}</h2>
|
||||
<ul className="flex flex-row flex-wrap gap-2 items-center justify-center max-w-2xl px-8 mx-auto">
|
||||
{Object.keys(designs).map((name) => (
|
||||
<li key={name}>
|
||||
<button
|
||||
className={`btn btn-primary btn-outline btn-sm capitalize font-bold `}
|
||||
onClick={() => {
|
||||
update.design(name)
|
||||
update.view('draft')
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
|
@ -0,0 +1,67 @@
|
|||
/**
|
||||
* The draft view allows users to tweak their pattern
|
||||
*
|
||||
* @param (object) props - All the props
|
||||
* @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 }) => {
|
||||
/*
|
||||
* 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
|
||||
* 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
|
||||
|
||||
/*
|
||||
* First, attempt to draft
|
||||
*/
|
||||
const { pattern } = Swizzled.methods.draft(Design, state.settings)
|
||||
|
||||
let output = null
|
||||
let renderProps = false
|
||||
if (state.ui?.renderer === 'svg') {
|
||||
try {
|
||||
const __html = pattern.render()
|
||||
output = (
|
||||
<Swizzled.components.ZoomablePattern>
|
||||
<div className="w-full h-full" dangerouslySetInnerHTML={{ __html }} />
|
||||
</Swizzled.components.ZoomablePattern>
|
||||
)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
} else {
|
||||
renderProps = pattern.getRenderProps()
|
||||
output = (
|
||||
<Swizzled.components.ZoomablePattern
|
||||
renderProps={renderProps}
|
||||
patternLocale={state.locale || 'en'}
|
||||
rotate={state.ui.rotate}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Swizzled.components.PatternLayout
|
||||
{...{ update, Design, output, state, pattern }}
|
||||
menu={
|
||||
state.ui?.aside ? (
|
||||
<Swizzled.components.DraftMenu {...{ Design, pattern, update, state }} />
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* Loading MDX dynamically is non-trivial an depends on the
|
||||
* environment in which the component is loaded.
|
||||
* By default, we use this component, which disabled loading
|
||||
* inline docs. If you want this to work, you need to pass in
|
||||
* your own DynamicMdx component into the editor:
|
||||
*
|
||||
* <PatternEditor components={{ DynamicMdx: MyComponent }} />
|
||||
*/
|
||||
export const DynamicMdx = ({ Swizzled }) => (
|
||||
<Swizzled.components.Popout node>Not implemented</Swizzled.components.Popout>
|
||||
)
|
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
* The error view is loaded if and only an error occurs that we can't handle
|
||||
*
|
||||
* @param {object} props - The component's props
|
||||
* @param {object} props.Swizzled - Swizzled code
|
||||
* @param {function} props.Design - The design constructor
|
||||
* @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 {array} props.missingMeasurements - List of missing measurements for the current design
|
||||
* @param {object} props.components - The possibly swizzled components
|
||||
* @param {object} props.methods - The possibly swizzled methods
|
||||
* @param {function} props.methods.t - The translation method
|
||||
* @param {object} props.config - The possibly swizzled pattern editor configuration
|
||||
* @return {function} MeasurementsView - React component
|
||||
*/
|
||||
export const ErrorView = ({ Swizzled, state }) => (
|
||||
<div className="text-center mt-8">
|
||||
<h2>{Swizzled.methods.t('oops')}</h2>
|
||||
<p>FIXME: Something went wrong</p>
|
||||
<pre>{JSON.stringify(state, null, 2)}</pre>
|
||||
</div>
|
||||
)
|
|
@ -0,0 +1,103 @@
|
|||
import mustache from 'mustache'
|
||||
|
||||
export const FlagTypeIcon = ({ Swizzled, type, className = 'w-6 h-6' }) => {
|
||||
const Icon = Swizzled.components[`Flag${Swizzled.methods.capitalize(type)}Icon`]
|
||||
|
||||
return Icon ? <Icon className={className} /> : null
|
||||
}
|
||||
|
||||
export const Flag = ({ Swizzled, data, handleUpdate }) => {
|
||||
const btnIcon = data.suggest?.icon ? (
|
||||
<Swizzled.components.FlagTypeIcon type={data.suggest.icon} className="w-5 h-6 sm:w-6 h-6" />
|
||||
) : null
|
||||
const { t } = Swizzled.methods
|
||||
|
||||
const button =
|
||||
data.suggest?.text && data.suggest?.update ? (
|
||||
<button
|
||||
className={`btn btn-secondary btn-outline flex flex-row items-center ${
|
||||
btnIcon ? 'gap-6' : ''
|
||||
}`}
|
||||
onClick={() => handleUpdate(data.suggest.update)}
|
||||
>
|
||||
{btnIcon}
|
||||
{t(data.suggest.text)}
|
||||
</button>
|
||||
) : null
|
||||
|
||||
const desc = data.replace ? mustache.render(t(data.desc), data.replace) : t(data.desc)
|
||||
const notes = data.notes
|
||||
? Array.isArray(data.notes)
|
||||
? '\n\n' +
|
||||
data.notes
|
||||
.map((note) => (data.replace ? mustache.render(t(note), data.replace) : t(note)))
|
||||
.join('\n\n')
|
||||
: '\n\n' + (data.replace ? mustache.render(t(data.notes), data.replace) : t(data.notes))
|
||||
: null
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 items-start">
|
||||
<div className="first:mt-0 grow md flag">
|
||||
<pre>{desc}</pre>
|
||||
<pre>{notes}</pre>
|
||||
</div>
|
||||
{button ? <div className="mt-2 w-full flex flex-row justify-end">{button}</div> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
//<Mdx md={notes ? desc + notes : desc} />
|
||||
|
||||
export const FlagsAccordionTitle = ({ flags, Swizzled }) => {
|
||||
const { t } = Swizzled.methods
|
||||
const flagList = Swizzled.methods.flattenFlags(flags)
|
||||
|
||||
if (Object.keys(flagList).length < 1) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<h5 className="flex flex-row gap-2 items-center justify-between w-full">
|
||||
<span className="text-left">
|
||||
{t('pe:flagMenu.t')} ({Object.keys(flagList).length})
|
||||
</span>
|
||||
<Swizzled.components.FlagTypeIcon className="w-8 h-8" />
|
||||
</h5>
|
||||
<p className="text-left">
|
||||
{Object.keys(flagList).length > 1 ? t('pe:flagMenuMany.d') : t('pe:flagMenuOne.d')}
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const FlagsAccordionEntries = ({ flags, update, Swizzled }) => {
|
||||
const flagList = Swizzled.methods.flattenFlags(flags)
|
||||
const { t } = Swizzled.methods
|
||||
|
||||
if (Object.keys(flagList).length < 1) return null
|
||||
|
||||
const handleUpdate = (config) => {
|
||||
if (config.settings) update.settings(...config.settings)
|
||||
if (config.ui) update.ui(...config.settings)
|
||||
}
|
||||
|
||||
return (
|
||||
<Swizzled.components.SubAccordion
|
||||
items={Object.entries(flagList).map(([key, flag], i) => {
|
||||
const title = flag.replace ? mustache.render(t(flag.title), flag.replace) : t(flag.title)
|
||||
|
||||
return [
|
||||
<div className="w-full flex flex-row gap2 justify-between" key={i}>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<div className="no-shrink">
|
||||
<Swizzled.components.FlagIcon type={flag.type} />
|
||||
</div>
|
||||
<span className="font-medium text-left">{title}</span>
|
||||
</div>
|
||||
<span className="uppercase font-bold">{flag.type}</span>
|
||||
</div>,
|
||||
<Swizzled.components.Flag key={key} t={t} data={flag} handleUpdate={handleUpdate} />,
|
||||
key,
|
||||
]
|
||||
})}
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,525 @@
|
|||
import { useState } from 'react'
|
||||
|
||||
export const HeaderMenu = ({ state, Swizzled, update, Design, pattern }) => {
|
||||
const [open, setOpen] = useState()
|
||||
|
||||
/*
|
||||
* Guard views that require measurements agains missing measurements
|
||||
* and make sure there's a view-specific header menu
|
||||
*/
|
||||
const ViewMenu =
|
||||
!Swizzled.methods.missingMeasurements(state) &&
|
||||
Swizzled.components[`HeaderMenu${Swizzled.config.viewComponents[state.view]}`]
|
||||
? Swizzled.components[`HeaderMenu${Swizzled.config.viewComponents[state.view]}`]
|
||||
: Swizzled.components.Null
|
||||
|
||||
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`}
|
||||
>
|
||||
<Swizzled.components.HeaderMenuAllViews {...{ state, Swizzled, update, open, setOpen }} />
|
||||
<ViewMenu {...{ state, Swizzled, update, Design, pattern, open, setOpen }} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const HeaderMenuAllViews = ({ state, Swizzled, update, open, setOpen }) => (
|
||||
<Swizzled.components.HeaderMenuViewMenu {...{ state, Swizzled, update, open, setOpen }} />
|
||||
)
|
||||
|
||||
export const HeaderMenuDraftView = (props) => {
|
||||
const { Swizzled } = props
|
||||
const flags = props.pattern?.setStores?.[0]?.plugins?.['plugin-annotations']?.flags
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row gap-1">
|
||||
<Swizzled.components.HeaderMenuDraftViewDesignOptions {...props} />
|
||||
<Swizzled.components.HeaderMenuDraftViewCoreSettings {...props} />
|
||||
<Swizzled.components.HeaderMenuDraftViewUiPreferences {...props} />
|
||||
{flags ? <Swizzled.components.HeaderMenuDraftViewFlags {...props} flags={flags} /> : null}
|
||||
</div>
|
||||
<Swizzled.components.HeaderMenuDraftViewIcons {...props} />
|
||||
<Swizzled.components.HeaderMenuUndoIcons {...props} />
|
||||
<Swizzled.components.HeaderMenuSaveIcons {...props} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const HeaderMenuDropdown = (props) => {
|
||||
const { Swizzled, tooltip, toggle, open, setOpen, id } = props
|
||||
/*
|
||||
* We need to use both !fixed and md:!absolute here to override DaisyUI's
|
||||
* classes on dropdown-content to force the dropdown to use the available
|
||||
* screen space on mobile, rather than be positioned under its toggle button
|
||||
*/
|
||||
|
||||
return props.disabled ? (
|
||||
<Swizzled.components.Tooltip tip={tooltip}>
|
||||
<button
|
||||
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"
|
||||
>
|
||||
{toggle}
|
||||
</button>
|
||||
</Swizzled.components.Tooltip>
|
||||
) : (
|
||||
<Swizzled.components.Tooltip tip={tooltip}>
|
||||
<div className={`dropdown ${open === id ? 'dropdown-open 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"
|
||||
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"
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
{open === id && (
|
||||
<div
|
||||
className="w-screen h-screen absolute top-10 left-0 opacity-0"
|
||||
style={{ width: '200vw', transform: 'translateX(-100vw)' }}
|
||||
onClick={() => setOpen(false)}
|
||||
></div>
|
||||
)}
|
||||
</div>
|
||||
</Swizzled.components.Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export const HeaderMenuDraftViewDesignOptions = (props) => {
|
||||
const { Swizzled } = props
|
||||
|
||||
return (
|
||||
<Swizzled.components.HeaderMenuDropdown
|
||||
{...props}
|
||||
id="designOptions"
|
||||
tooltip={Swizzled.methods.t('pe:designOptions.d')}
|
||||
toggle={
|
||||
<>
|
||||
<Swizzled.components.HeaderMenuIcon name="options" extraClasses="text-secondary" />
|
||||
<span className="hidden lg:inline">{Swizzled.methods.t('pe:designOptions.t')}</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Swizzled.components.DesignOptionsMenu {...props} />
|
||||
</Swizzled.components.HeaderMenuDropdown>
|
||||
)
|
||||
}
|
||||
|
||||
export const HeaderMenuDraftViewCoreSettings = (props) => {
|
||||
const { Swizzled } = props
|
||||
|
||||
return (
|
||||
<Swizzled.components.HeaderMenuDropdown
|
||||
{...props}
|
||||
tooltip={Swizzled.methods.t('pe:coreSettings.d')}
|
||||
id="coreSettings"
|
||||
toggle={
|
||||
<>
|
||||
<Swizzled.components.HeaderMenuIcon name="settings" extraClasses="text-secondary" />
|
||||
<span className="hidden lg:inline">{Swizzled.methods.t('pe:coreSettings.t')}</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Swizzled.components.CoreSettingsMenu {...props} />
|
||||
</Swizzled.components.HeaderMenuDropdown>
|
||||
)
|
||||
}
|
||||
|
||||
export const HeaderMenuDraftViewUiPreferences = (props) => {
|
||||
const { Swizzled } = props
|
||||
|
||||
return (
|
||||
<Swizzled.components.HeaderMenuDropdown
|
||||
{...props}
|
||||
tooltip={Swizzled.methods.t('pe:uiPreferences.d')}
|
||||
id="uiPreferences"
|
||||
toggle={
|
||||
<>
|
||||
<Swizzled.components.HeaderMenuIcon name="ui" extraClasses="text-secondary" />
|
||||
<span className="hidden lg:inline">{Swizzled.methods.t('pe:uiPreferences.t')}</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Swizzled.components.UiPreferencesMenu {...props} />
|
||||
</Swizzled.components.HeaderMenuDropdown>
|
||||
)
|
||||
}
|
||||
|
||||
export const HeaderMenuDraftViewFlags = (props) => {
|
||||
const { Swizzled } = props
|
||||
const count = Object.keys(Swizzled.methods.flattenFlags(props.flags)).length
|
||||
|
||||
return (
|
||||
<Swizzled.components.HeaderMenuDropdown
|
||||
{...props}
|
||||
tooltip={Swizzled.methods.t('pe:flagMenuMany.d')}
|
||||
id="flags"
|
||||
toggle={
|
||||
<>
|
||||
<Swizzled.components.HeaderMenuIcon name="flag" extraClasses="text-secondary" />
|
||||
<span className="hidden lg:inline">
|
||||
{Swizzled.methods.t('pe:flags')}
|
||||
<span>({count})</span>
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Swizzled.components.FlagsAccordionEntries {...props} />
|
||||
</Swizzled.components.HeaderMenuDropdown>
|
||||
)
|
||||
}
|
||||
|
||||
export const HeaderMenuDraftViewIcons = (props) => {
|
||||
const { Swizzled, update } = props
|
||||
const Button = Swizzled.components.HeaderMenuButton
|
||||
const size = 'w-5 h-5'
|
||||
const muted = 'text-current opacity-50'
|
||||
const ux = props.state.ui.ux
|
||||
const levels = {
|
||||
...props.Swizzled.config.uxLevels.core,
|
||||
...props.Swizzled.config.uxLevels.ui,
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-row flex-wrap items-center justify-center px-2">
|
||||
{ux >= levels.sa ? (
|
||||
<Button updateHandler={update.toggleSa} tooltip={Swizzled.methods.t('pe:tt.toggleSa')}>
|
||||
<Swizzled.components.SaIcon
|
||||
className={`${size} ${props.state.settings.sabool ? 'text-secondary' : muted}`}
|
||||
/>
|
||||
</Button>
|
||||
) : null}
|
||||
{ux >= levels.paperless ? (
|
||||
<Button
|
||||
updateHandler={() => update.settings('paperless', props.state.settings.paperless ? 0 : 1)}
|
||||
tooltip={Swizzled.methods.t('pe:tt.togglePaperless')}
|
||||
>
|
||||
<Swizzled.components.PaperlessIcon
|
||||
className={`${size} ${props.state.settings.paperless ? 'text-secondary' : muted}`}
|
||||
/>
|
||||
</Button>
|
||||
) : null}
|
||||
{ux >= levels.complete ? (
|
||||
<Button
|
||||
updateHandler={() => update.settings('complete', props.state.settings.complete ? 0 : 1)}
|
||||
tooltip={Swizzled.methods.t('pe:tt.toggleComplete')}
|
||||
>
|
||||
<Swizzled.components.DetailIcon
|
||||
className={`${size} ${!props.state.settings.complete ? 'text-secondary' : muted}`}
|
||||
/>
|
||||
</Button>
|
||||
) : null}
|
||||
{ux >= levels.expand ? (
|
||||
<Button
|
||||
updateHandler={() => update.settings('expand', props.state.settings.expand ? 0 : 1)}
|
||||
tooltip={Swizzled.methods.t('pe:tt.toggleExpand')}
|
||||
>
|
||||
<Swizzled.components.ExpandIcon
|
||||
className={`${size} ${props.state.settings.expand ? 'text-secondary' : muted}`}
|
||||
/>
|
||||
</Button>
|
||||
) : null}
|
||||
{ux >= levels.units ? (
|
||||
<Button
|
||||
updateHandler={() =>
|
||||
update.settings(
|
||||
'units',
|
||||
props.state.settings.units === 'imperial' ? 'metric' : 'imperial'
|
||||
)
|
||||
}
|
||||
tooltip={Swizzled.methods.t('pe:tt.toggleUnits')}
|
||||
>
|
||||
<Swizzled.components.UnitsIcon
|
||||
className={`${size} ${
|
||||
props.state.settings.units === 'imperial' ? 'text-secondary' : muted
|
||||
}`}
|
||||
/>
|
||||
</Button>
|
||||
) : null}
|
||||
<Swizzled.components.HeaderMenuIconSpacer />
|
||||
{ux >= levels.ux ? (
|
||||
<div className="flex flex-row px-1">
|
||||
<Swizzled.components.Tooltip tip={Swizzled.methods.t('pe:tt.changeUx')}>
|
||||
{[0, 1, 2, 3, 4].map((i) => (
|
||||
<button
|
||||
key={i}
|
||||
className="btn btn-ghost btn-sm px-0 -mx-0.5"
|
||||
onClick={() => update.ui('ux', i + 1)}
|
||||
>
|
||||
<Swizzled.components.CircleIcon
|
||||
key={i}
|
||||
fill={i < props.state.ui.ux ? true : false}
|
||||
className={`${size} ${
|
||||
i < props.state.ui.ux ? 'stroke-secondary fill-secondary' : 'stroke-current'
|
||||
}`}
|
||||
fillOpacity={0.3}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</Swizzled.components.Tooltip>
|
||||
</div>
|
||||
) : null}
|
||||
{ux >= levels.aside ? (
|
||||
<Button
|
||||
updateHandler={() => update.ui('aside', props.state.ui.aside ? 0 : 1)}
|
||||
tooltip={Swizzled.methods.t('pe:tt.toggleAside')}
|
||||
>
|
||||
<Swizzled.components.MenuIcon
|
||||
className={`${size} ${!props.state.ui.aside ? 'text-secondary' : muted}`}
|
||||
/>
|
||||
</Button>
|
||||
) : null}
|
||||
{ux >= levels.kiosk ? (
|
||||
<Button
|
||||
updateHandler={() => update.ui('kiosk', props.state.ui.kiosk ? 0 : 1)}
|
||||
tooltip={Swizzled.methods.t('pe:tt.toggleKiosk')}
|
||||
>
|
||||
<Swizzled.components.KioskIcon
|
||||
className={`${size} ${props.state.ui.kiosk ? 'text-secondary' : muted}`}
|
||||
/>
|
||||
</Button>
|
||||
) : null}
|
||||
{ux >= levels.rotate ? (
|
||||
<Button
|
||||
updateHandler={() => update.ui('rotate', props.state.ui.rotate ? 0 : 1)}
|
||||
tooltip={Swizzled.methods.t('pe:tt.toggleRotate')}
|
||||
>
|
||||
<Swizzled.components.RotateIcon
|
||||
className={`${size} ${props.state.ui.rotate ? 'text-secondary' : muted}`}
|
||||
/>
|
||||
</Button>
|
||||
) : null}
|
||||
{ux >= levels.renderer ? (
|
||||
<Button
|
||||
updateHandler={() =>
|
||||
update.ui('renderer', props.state.ui.renderer === 'react' ? 'svg' : 'react')
|
||||
}
|
||||
tooltip={Swizzled.methods.t('pe:tt.toggleRenderer')}
|
||||
>
|
||||
<Swizzled.components.RocketIcon
|
||||
className={`${size} ${props.state.ui.renderer === 'svg' ? 'text-secondary' : muted}`}
|
||||
/>
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const HeaderMenuUndoIcons = (props) => {
|
||||
const { Swizzled, update, state, Design } = props
|
||||
const Button = Swizzled.components.HeaderMenuButton
|
||||
const size = 'w-5 h-5'
|
||||
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">
|
||||
<Button
|
||||
updateHandler={() => update.restore(0, state._)}
|
||||
tooltip={Swizzled.methods.t('pe:tt.undo')}
|
||||
disabled={undos ? false : true}
|
||||
>
|
||||
<Swizzled.components.UndoIcon
|
||||
className={`${size} ${undos ? 'text-secondary' : ''}`}
|
||||
text="1"
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
updateHandler={() => update.restore(undos.length - 1, state._)}
|
||||
tooltip={Swizzled.methods.t('pe:tt.undoAll')}
|
||||
disabled={undos ? false : true}
|
||||
>
|
||||
<Swizzled.components.UndoIcon
|
||||
className={`${size} ${undos ? 'text-secondary' : ''}`}
|
||||
text={Swizzled.methods.t('pe:allFirstLetter')}
|
||||
/>
|
||||
</Button>
|
||||
<Swizzled.components.HeaderMenuDropdown
|
||||
{...props}
|
||||
tooltip={Swizzled.methods.t('pe:view.undos.t')}
|
||||
id="undos"
|
||||
disabled={undos ? false : true}
|
||||
toggle={
|
||||
<>
|
||||
<Swizzled.components.UndoIcon className="w-4 h-4" stroke={3} />
|
||||
<span className="hidden lg:inline">{Swizzled.methods.t('pe:undo')}</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{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">
|
||||
{undos.slice(0, 9).map((step, index) => (
|
||||
<li key={index}>
|
||||
<Swizzled.components.UndoStep {...{ step, update, state, Design, index }} compact />
|
||||
</li>
|
||||
))}
|
||||
<li key="view">
|
||||
<Swizzled.components.ButtonFrame
|
||||
dense
|
||||
onClick={() => {
|
||||
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">
|
||||
<Swizzled.components.UndoIcon className="w-5 h-5 text-secondary" />
|
||||
{Swizzled.methods.t(`pe:view.undos.t`)}...
|
||||
</div>
|
||||
{undos.length}
|
||||
</div>
|
||||
</Swizzled.components.ButtonFrame>
|
||||
</li>
|
||||
</ul>
|
||||
) : null}
|
||||
</Swizzled.components.HeaderMenuDropdown>
|
||||
<Button updateHandler={update.clearAll} tooltip={Swizzled.methods.t('pe:tt.resetDesign')}>
|
||||
<Swizzled.components.TrashIcon className={`${size} text-secondary`} />
|
||||
</Button>
|
||||
<Button updateHandler={update.clearAll} tooltip={Swizzled.methods.t('pe:tt.resetAll')}>
|
||||
<Swizzled.components.ResetAllIcon className={`${size} text-secondary`} />
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const HeaderMenuSaveIcons = (props) => {
|
||||
const { Swizzled, update } = props
|
||||
const Button = Swizzled.components.HeaderMenuButton
|
||||
const size = 'w-5 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">
|
||||
<Button
|
||||
updateHandler={update.clearPattern}
|
||||
tooltip={Swizzled.methods.t('pe:tt.savePattern')}
|
||||
disabled={saveable ? false : true}
|
||||
>
|
||||
<Swizzled.components.SaveIcon className={`${size} ${saveable ? 'text-success' : ''}`} />
|
||||
</Button>
|
||||
<Button
|
||||
updateHandler={() => update.view('save')}
|
||||
tooltip={Swizzled.methods.t('pe:tt.savePatternAs')}
|
||||
>
|
||||
<Swizzled.components.SaveAsIcon className={`${size} text-secondary`} />
|
||||
</Button>
|
||||
<Button
|
||||
updateHandler={update.clearPattern}
|
||||
tooltip={Swizzled.methods.t('pe:tt.exportPattern')}
|
||||
>
|
||||
<Swizzled.components.ExportIcon className={`${size} text-secondary`} />
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const HeaderMenuIcon = (props) => {
|
||||
const { Swizzled, name, extraClasses = '' } = props
|
||||
const Icon =
|
||||
Swizzled.components[`${Swizzled.methods.capitalize(name)}Icon`] || Swizzled.components.Noop
|
||||
return <Icon {...props} className={`h-5 w-5 ${extraClasses}`} />
|
||||
}
|
||||
export const HeaderMenuIconSpacer = () => <span className="px-1 font-bold opacity-30">|</span>
|
||||
|
||||
export const HeaderMenuButton = ({
|
||||
Swizzled,
|
||||
updateHandler,
|
||||
children,
|
||||
tooltip,
|
||||
disabled = false,
|
||||
}) => (
|
||||
<Swizzled.components.Tooltip tip={tooltip}>
|
||||
<button
|
||||
className="btn btn-ghost btn-sm px-1 disabled:bg-transparent"
|
||||
onClick={updateHandler}
|
||||
disabled={disabled}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
</Swizzled.components.Tooltip>
|
||||
)
|
||||
|
||||
export const HeaderMenuViewMenu = (props) => {
|
||||
const { Swizzled, update, state } = props
|
||||
const output = []
|
||||
let i = 1
|
||||
for (const viewName of [
|
||||
'spacer',
|
||||
...Swizzled.config.mainViews,
|
||||
'spacer',
|
||||
...Swizzled.config.extraViews,
|
||||
'spacerOver3',
|
||||
...Swizzled.config.devViews,
|
||||
'spacer',
|
||||
'picker',
|
||||
]) {
|
||||
if (viewName === 'spacer') output.push(<Swizzled.components.AsideViewMenuSpacer key={i} />)
|
||||
else if (viewName === 'spacerOver3')
|
||||
output.push(state.ui.ux > 3 ? <Swizzled.components.AsideViewMenuSpacer key={i} /> : null)
|
||||
else if (
|
||||
state.ui.ux >= Swizzled.config.uxLevels.views[viewName] &&
|
||||
(Swizzled.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">
|
||||
<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'
|
||||
}`}
|
||||
onClick={() => update.view(viewName)}
|
||||
>
|
||||
<Swizzled.components.ViewTypeIcon
|
||||
view={viewName}
|
||||
className="w-6 h-6 grow-0"
|
||||
Swizzled={Swizzled}
|
||||
/>
|
||||
<b className="text-left grow">{Swizzled.methods.t(`pe:view.${viewName}.t`)}</b>
|
||||
</a>
|
||||
</li>
|
||||
)
|
||||
i++
|
||||
}
|
||||
|
||||
return (
|
||||
<Swizzled.components.HeaderMenuDropdown
|
||||
{...props}
|
||||
tooltip={Swizzled.methods.t('pe:views.d')}
|
||||
id="views"
|
||||
toggle={
|
||||
<>
|
||||
<Swizzled.components.HeaderMenuIcon
|
||||
name="right"
|
||||
stroke={3}
|
||||
extraClasses="text-secondary rotate-90"
|
||||
/>
|
||||
<span className="hidden lg:inline">{Swizzled.methods.t('pe:views.t')}</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<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"
|
||||
>
|
||||
{output}
|
||||
</ul>
|
||||
</Swizzled.components.HeaderMenuDropdown>
|
||||
)
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export const HtmlSpan = ({ html }) => <span dangerouslySetInnerHTML={{ __html: html }} />
|
1134
packages/react-components/src/editor/swizzle/components/icons.mjs
Normal file
1134
packages/react-components/src/editor/swizzle/components/icons.mjs
Normal file
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,453 @@
|
|||
/*************************************************************************
|
||||
* *
|
||||
* FreeSewing's pattern editor allows swizzling components *
|
||||
* *
|
||||
* To 'swizzle' means to replace the default implementation of a *
|
||||
* component with a custom one. It allows one to customize *
|
||||
* the pattern editor. *
|
||||
* *
|
||||
* This file holds the 'swizzleComponents' method that will return *
|
||||
* the various components that can be swizzled, or their default *
|
||||
* implementation. *
|
||||
* *
|
||||
* To use a custom version, simply pas it as a prop into the editor *
|
||||
* under the 'components' key. So to pass a custom 'TemporaryLoader' *
|
||||
* component, you do: *
|
||||
* *
|
||||
* <PatternEditor compnents={{ TemporaryLoader: MyTemporaryLoader }} /> *
|
||||
* *
|
||||
*************************************************************************/
|
||||
|
||||
/*
|
||||
* Import of components that can be swizzled
|
||||
*/
|
||||
import { Link, AnchorLink, PageLink, WebLink, CardLink } from './link.mjs'
|
||||
// Accordion
|
||||
import { BaseAccordion, SubAccordion, Accordion } from './accordion.mjs'
|
||||
// Auth wrapper
|
||||
import {
|
||||
AuthWrapper,
|
||||
AuthMessageWrapper,
|
||||
ContactSupport,
|
||||
AuthRequired,
|
||||
AccountInactive,
|
||||
AccountDisabled,
|
||||
AccountProhibited,
|
||||
AccountStatusUnknown,
|
||||
RoleLacking,
|
||||
ConsentLacking,
|
||||
} from './auth-wrapper.mjs'
|
||||
// Ux
|
||||
import { Ux } from './ux.mjs'
|
||||
// Popout
|
||||
import { Popout } from './popout.mjs'
|
||||
// Loader
|
||||
import { TemporaryLoader } from './loaders.mjs'
|
||||
// Measurements Sets
|
||||
import { UserSetPicker, BookmarkedSetPicker, CuratedSetPicker } from './sets.mjs'
|
||||
// Curated Measurements Sets
|
||||
import { CuratedMeasurementsSetLineup } from './curated-sets.mjs'
|
||||
import { MeasurementsSetCard } from './measurements-set-card.mjs'
|
||||
// Icons
|
||||
import {
|
||||
ApplyIcon,
|
||||
BackIcon,
|
||||
BeakerIcon,
|
||||
BookmarkIcon,
|
||||
BoolNoIcon,
|
||||
BoolYesIcon,
|
||||
CircleIcon,
|
||||
CloseIcon,
|
||||
CuratedMeasurementsSetIcon,
|
||||
DesignIcon,
|
||||
DetailIcon,
|
||||
DocsIcon,
|
||||
DownIcon,
|
||||
EditIcon,
|
||||
ExpandIcon,
|
||||
ExportIcon,
|
||||
FailureIcon,
|
||||
FlagIcon,
|
||||
FlagNoteIcon,
|
||||
FlagInfoIcon,
|
||||
FlagTipIcon,
|
||||
FlagWarningIcon,
|
||||
FlagErrorIcon,
|
||||
FlagFixmeIcon,
|
||||
FlagExpandIcon,
|
||||
FlagOtionsIcon,
|
||||
GaugeIcon,
|
||||
GroupIcon,
|
||||
IncludeIcon,
|
||||
HelpIcon,
|
||||
KioskIcon,
|
||||
LeftIcon,
|
||||
ListIcon,
|
||||
LockIcon,
|
||||
MarginIcon,
|
||||
MeasurementsIcon,
|
||||
MeasurementsSetIcon,
|
||||
MenuIcon,
|
||||
NoIcon,
|
||||
OkIcon,
|
||||
OptionsIcon,
|
||||
PaperlessIcon,
|
||||
PlusIcon,
|
||||
PrintIcon,
|
||||
ResetAllIcon,
|
||||
ResetIcon,
|
||||
RightIcon,
|
||||
RocketIcon,
|
||||
RotateIcon,
|
||||
SaIcon,
|
||||
SaveIcon,
|
||||
SaveAsIcon,
|
||||
ScaleIcon,
|
||||
SettingsIcon,
|
||||
SpinnerIcon,
|
||||
SuccessIcon,
|
||||
TipIcon,
|
||||
TrashIcon,
|
||||
UiIcon,
|
||||
UndoIcon,
|
||||
UnitsIcon,
|
||||
UpIcon,
|
||||
UploadIcon,
|
||||
UxIcon,
|
||||
XrayIcon,
|
||||
ViewDraftIcon,
|
||||
ViewMeasurementsIcon,
|
||||
ViewTestIcon,
|
||||
ViewTimingIcon,
|
||||
ViewPrintLayoutIcon,
|
||||
ViewSaveIcon,
|
||||
ViewExportIcon,
|
||||
ViewEditSettingsIcon,
|
||||
ViewLogsIcon,
|
||||
ViewInspectIcon,
|
||||
ViewDocsIcon,
|
||||
ViewDesignsIcon,
|
||||
ViewViewPickerIcon,
|
||||
ViewUndosIcon,
|
||||
} from './icons.mjs'
|
||||
// Measurements Editor
|
||||
import { MeasurementsEditor } from './measurements-editor.mjs'
|
||||
// Zoomable pattern
|
||||
import { ZoomablePattern, ZoomContextProvider } from './zoomable-pattern.mjs'
|
||||
import { PatternLayout } from './pattern-layout.mjs'
|
||||
// inputs
|
||||
import {
|
||||
FormControl,
|
||||
ButtonFrame,
|
||||
NumberInput,
|
||||
StringInput,
|
||||
ListInput,
|
||||
MarkdownInput,
|
||||
MeasurementInput,
|
||||
ToggleInput,
|
||||
} from './inputs.mjs'
|
||||
// Views
|
||||
import { DesignsView } from './designs-view.mjs'
|
||||
import { DraftView } from './draft-view.mjs'
|
||||
import { ErrorView } from './error-view.mjs'
|
||||
import { MeasurementsView } from './measurements-view.mjs'
|
||||
import { SaveView } from './save-view.mjs'
|
||||
import { ViewPicker } from './view-picker.mjs'
|
||||
import { UndoStep, UndoStepTimeAgo, UndosView } from './undos-view.mjs'
|
||||
// Pattern
|
||||
import { Pattern } from '@freesewing/react-components/pattern'
|
||||
// Menus
|
||||
import { DraftMenu } from './menus/draft-menu.mjs'
|
||||
import { CoreSettingsMenu, CoreSetting } from './menus/core-settings-menu.mjs'
|
||||
import { DesignOptionsMenu, DesignOption } from './menus/design-options-menu.mjs'
|
||||
import { UiPreferencesMenu, UiPreference } from './menus/ui-preferences-menu.mjs'
|
||||
import { MenuItem, MenuItemGroup, MenuItemTitle } from './menus/containers.mjs'
|
||||
import {
|
||||
MenuBoolInput,
|
||||
MenuConstantInput,
|
||||
MenuDegInput,
|
||||
MenuEditOption,
|
||||
MenuListInput,
|
||||
MenuListToggle,
|
||||
MenuMmInput,
|
||||
//MenuNumberInput,
|
||||
MenuUxSettingInput,
|
||||
MenuOnlySettingInput,
|
||||
MenuPctInput,
|
||||
MenuSliderInput,
|
||||
} from './menus/shared-inputs.mjs'
|
||||
import {
|
||||
MenuBoolValue,
|
||||
MenuConstantOptionValue,
|
||||
MenuCountOptionValue,
|
||||
MenuDegOptionValue,
|
||||
MenuHighlightValue,
|
||||
MenuListOptionValue,
|
||||
MenuListValue,
|
||||
MenuMmOptionValue,
|
||||
MenuMmValue,
|
||||
MenuOnlySettingValue,
|
||||
MenuPctOptionValue,
|
||||
MenuScaleSettingValue,
|
||||
MenuShowValue,
|
||||
} from './menus/shared-values.mjs'
|
||||
import {
|
||||
HeaderMenu,
|
||||
HeaderMenuAllViews,
|
||||
HeaderMenuDraftView,
|
||||
HeaderMenuButton,
|
||||
HeaderMenuDropdown,
|
||||
HeaderMenuDraftViewDesignOptions,
|
||||
HeaderMenuDraftViewCoreSettings,
|
||||
HeaderMenuDraftViewUiPreferences,
|
||||
HeaderMenuDraftViewFlags,
|
||||
HeaderMenuDraftViewIcons,
|
||||
HeaderMenuIcon,
|
||||
HeaderMenuIconSpacer,
|
||||
HeaderMenuSaveIcons,
|
||||
HeaderMenuUndoIcons,
|
||||
HeaderMenuViewMenu,
|
||||
} from './header-menu.mjs'
|
||||
// Flags
|
||||
import { Flag, FlagTypeIcon, FlagsAccordionTitle, FlagsAccordionEntries } from './flags.mjs'
|
||||
// View Menu
|
||||
import {
|
||||
AsideViewMenu,
|
||||
AsideViewMenuIcons,
|
||||
AsideViewMenuButton,
|
||||
AsideViewMenuSpacer,
|
||||
ViewTypeIcon,
|
||||
} from './aside-view-menu.mjs'
|
||||
import { Null } from './null.mjs'
|
||||
import { LargeScreenOnly } from './large-screen-only.mjs'
|
||||
import { Tooltip } from './tooltip.mjs'
|
||||
import { LoadingStatus } from './loading-status.mjs'
|
||||
import { Spinner, Loading } from './spinner.mjs'
|
||||
import { Tab, Tabs } from './tabs.mjs'
|
||||
import { Markdown } from './markdown.mjs'
|
||||
import { HtmlSpan } from './html-span.mjs'
|
||||
/**
|
||||
* This object holds all components that can be swizzled
|
||||
*/
|
||||
const defaultComponents = {
|
||||
Accordion,
|
||||
AuthWrapper,
|
||||
AuthMessageWrapper,
|
||||
BackIcon,
|
||||
ContactSupport,
|
||||
AuthRequired,
|
||||
AccountInactive,
|
||||
AccountDisabled,
|
||||
AccountProhibited,
|
||||
AccountStatusUnknown,
|
||||
AnchorLink,
|
||||
AsideViewMenu,
|
||||
AsideViewMenuIcons,
|
||||
AsideViewMenuButton,
|
||||
AsideViewMenuSpacer,
|
||||
RoleLacking,
|
||||
ConsentLacking,
|
||||
BaseAccordion,
|
||||
BookmarkedSetPicker,
|
||||
ButtonFrame,
|
||||
CardLink,
|
||||
CircleIcon,
|
||||
CoreSetting,
|
||||
CoreSettingsMenu,
|
||||
CuratedMeasurementsSetIcon,
|
||||
CuratedMeasurementsSetLineup,
|
||||
CuratedSetPicker,
|
||||
DesignOption,
|
||||
DesignOptionsMenu,
|
||||
DesignsView,
|
||||
DraftMenu,
|
||||
DraftView,
|
||||
ErrorView,
|
||||
SaveView,
|
||||
Flag,
|
||||
FlagsAccordionTitle,
|
||||
FlagsAccordionEntries,
|
||||
FlagTypeIcon,
|
||||
FormControl,
|
||||
HeaderMenu,
|
||||
HeaderMenuAllViews,
|
||||
HeaderMenuDraftView,
|
||||
HeaderMenuDraftViewDesignOptions,
|
||||
HeaderMenuDraftViewCoreSettings,
|
||||
HeaderMenuDraftViewUiPreferences,
|
||||
HeaderMenuDraftViewFlags,
|
||||
HeaderMenuDraftViewIcons,
|
||||
HeaderMenuButton,
|
||||
HeaderMenuDropdown,
|
||||
HeaderMenuIcon,
|
||||
HeaderMenuIconSpacer,
|
||||
HeaderMenuSaveIcons,
|
||||
HeaderMenuUndoIcons,
|
||||
HtmlSpan,
|
||||
LargeScreenOnly,
|
||||
Link,
|
||||
ListInput,
|
||||
Loading,
|
||||
LoadingStatus,
|
||||
Markdown,
|
||||
MarkdownInput,
|
||||
MeasurementInput,
|
||||
MeasurementsSetCard,
|
||||
MeasurementsView,
|
||||
MeasurementsEditor,
|
||||
MenuIcon,
|
||||
NumberInput,
|
||||
Null,
|
||||
PageLink,
|
||||
Pattern,
|
||||
PatternLayout,
|
||||
Popout,
|
||||
StringInput,
|
||||
SubAccordion,
|
||||
Spinner,
|
||||
SpinnerIcon,
|
||||
Tab,
|
||||
Tabs,
|
||||
TemporaryLoader,
|
||||
ToggleInput,
|
||||
Tooltip,
|
||||
UiPreferencesMenu,
|
||||
UiPreference,
|
||||
UndoStep,
|
||||
UndoStepTimeAgo,
|
||||
UndosView,
|
||||
UserSetPicker,
|
||||
Ux,
|
||||
HeaderMenuViewMenu,
|
||||
ViewPicker,
|
||||
ViewTypeIcon,
|
||||
WebLink,
|
||||
ZoomablePattern,
|
||||
ZoomContextProvider,
|
||||
// icons
|
||||
ApplyIcon,
|
||||
BeakerIcon,
|
||||
BookmarkIcon,
|
||||
BoolNoIcon,
|
||||
BoolYesIcon,
|
||||
CloseIcon,
|
||||
DesignIcon,
|
||||
DetailIcon,
|
||||
DocsIcon,
|
||||
DownIcon,
|
||||
EditIcon,
|
||||
ExpandIcon,
|
||||
ExportIcon,
|
||||
FailureIcon,
|
||||
FlagIcon,
|
||||
FlagNoteIcon,
|
||||
FlagInfoIcon,
|
||||
FlagTipIcon,
|
||||
FlagWarningIcon,
|
||||
FlagErrorIcon,
|
||||
FlagFixmeIcon,
|
||||
FlagExpandIcon,
|
||||
FlagOtionsIcon,
|
||||
GaugeIcon,
|
||||
GroupIcon,
|
||||
HelpIcon,
|
||||
IncludeIcon,
|
||||
KioskIcon,
|
||||
LeftIcon,
|
||||
ListIcon,
|
||||
LockIcon,
|
||||
MarginIcon,
|
||||
MeasurementsIcon,
|
||||
MeasurementsSetIcon,
|
||||
NoIcon,
|
||||
OkIcon,
|
||||
OptionsIcon,
|
||||
PaperlessIcon,
|
||||
PlusIcon,
|
||||
PrintIcon,
|
||||
ResetAllIcon,
|
||||
ResetIcon,
|
||||
RightIcon,
|
||||
RocketIcon,
|
||||
RotateIcon,
|
||||
SaIcon,
|
||||
SaveIcon,
|
||||
SaveAsIcon,
|
||||
ScaleIcon,
|
||||
SettingsIcon,
|
||||
SuccessIcon,
|
||||
TipIcon,
|
||||
TrashIcon,
|
||||
UiIcon,
|
||||
UndoIcon,
|
||||
UnitsIcon,
|
||||
UpIcon,
|
||||
UploadIcon,
|
||||
UxIcon,
|
||||
XrayIcon,
|
||||
ViewDraftIcon,
|
||||
ViewMeasurementsIcon,
|
||||
ViewTestIcon,
|
||||
ViewTimingIcon,
|
||||
ViewPrintLayoutIcon,
|
||||
ViewSaveIcon,
|
||||
ViewExportIcon,
|
||||
ViewEditSettingsIcon,
|
||||
ViewLogsIcon,
|
||||
ViewInspectIcon,
|
||||
ViewDocsIcon,
|
||||
ViewDesignsIcon,
|
||||
ViewViewPickerIcon,
|
||||
ViewUndosIcon,
|
||||
// menus
|
||||
MenuItem,
|
||||
MenuItemGroup,
|
||||
MenuItemTitle,
|
||||
MenuBoolInput,
|
||||
MenuConstantInput,
|
||||
MenuDegInput,
|
||||
MenuEditOption,
|
||||
MenuListInput,
|
||||
MenuListToggle,
|
||||
MenuMmInput,
|
||||
//MenuNumberInput,
|
||||
MenuUxSettingInput,
|
||||
MenuOnlySettingInput,
|
||||
MenuPctInput,
|
||||
MenuSliderInput,
|
||||
MenuBoolValue,
|
||||
MenuConstantOptionValue,
|
||||
MenuCountOptionValue,
|
||||
MenuDegOptionValue,
|
||||
MenuHighlightValue,
|
||||
MenuListOptionValue,
|
||||
MenuListValue,
|
||||
MenuMmOptionValue,
|
||||
MenuMmValue,
|
||||
MenuOnlySettingValue,
|
||||
MenuPctOptionValue,
|
||||
MenuScaleSettingValue,
|
||||
MenuShowValue,
|
||||
}
|
||||
|
||||
/*
|
||||
* This method returns a component that can be swizzled
|
||||
* So either the passed-in component, or the default one
|
||||
*/
|
||||
export const swizzleComponents = (components = {}, Swizzled) => {
|
||||
/*
|
||||
* We need to return all resulting components, swizzled or not
|
||||
* So we create this object so we can pass that down
|
||||
*/
|
||||
const all = {}
|
||||
for (let [name, Component] of Object.entries(defaultComponents)) {
|
||||
all[name] = components[name]
|
||||
? (props) => components[name]({ Swizzled, ...props })
|
||||
: (props) => <Component {...props} Swizzled={Swizzled} />
|
||||
}
|
||||
|
||||
/*
|
||||
* Return all components
|
||||
*/
|
||||
return all
|
||||
}
|
|
@ -0,0 +1,349 @@
|
|||
// Hooks
|
||||
import { useState } from 'react'
|
||||
|
||||
/*
|
||||
* Helper component to wrap a form control with a label
|
||||
*/
|
||||
export const FormControl = ({
|
||||
label, // the (top-left) label
|
||||
children, // Children to go inside the form control
|
||||
docs = false, // Optional top-right label
|
||||
labelBL = false, // Optional bottom-left label
|
||||
labelBR = false, // Optional bottom-right label
|
||||
forId = false, // ID of the for element we are wrapping
|
||||
Swizzled, // Object holding swizzled code
|
||||
}) => {
|
||||
if (labelBR && !labelBL) labelBL = <span></span>
|
||||
|
||||
const topLabelChildren = (
|
||||
<>
|
||||
<span className="label-text text-lg font-bold mb-0 text-inherit">{label}</span>
|
||||
{docs ? (
|
||||
<span className="label-text-alt">
|
||||
<button
|
||||
className="btn btn-ghost btn-sm btn-circle hover:btn-secondary"
|
||||
onClick={() =>
|
||||
Swizzled.methods.setModal(
|
||||
<Swizzled.components.ModalWrapper
|
||||
flex="col"
|
||||
justify="top lg:justify-center"
|
||||
slideFrom="right"
|
||||
keepOpenOnClick
|
||||
>
|
||||
<div className="mdx max-w-prose">{docs}</div>
|
||||
</Swizzled.components.ModalWrapper>
|
||||
)
|
||||
}
|
||||
>
|
||||
<Swizzled.components.DocsIcon />
|
||||
</button>
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
const bottomLabelChildren = (
|
||||
<>
|
||||
{labelBL ? <span className="label-text-alt">{labelBL}</span> : null}
|
||||
{labelBR ? <span className="label-text-alt">{labelBR}</span> : null}
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="form-control w-full mt-2">
|
||||
{forId ? (
|
||||
<label className="label pb-0" htmlFor={forId}>
|
||||
{topLabelChildren}
|
||||
</label>
|
||||
) : (
|
||||
<div className="label pb-0">{topLabelChildren}</div>
|
||||
)}
|
||||
{children}
|
||||
{labelBL || labelBR ? (
|
||||
forId ? (
|
||||
<label className="label" htmlFor={forId}>
|
||||
{bottomLabelChildren}
|
||||
</label>
|
||||
) : (
|
||||
<div className="label">{bottomLabelChildren}</div>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* Helper method to wrap content in a button
|
||||
*/
|
||||
export const ButtonFrame = ({
|
||||
children, // Children of the button
|
||||
onClick, // onClick handler
|
||||
active, // Whether or not to render the button as active/selected
|
||||
accordion = false, // Set this to true to not set a background color when active
|
||||
dense = false, // Use less padding
|
||||
}) => (
|
||||
<button
|
||||
className={`
|
||||
btn btn-ghost btn-secondary
|
||||
w-full ${dense ? 'mt-1 py-0 btn-sm' : 'mt-2 py-4 h-auto content-start'}
|
||||
border-2 border-secondary text-left bg-opacity-20
|
||||
${accordion ? 'hover:bg-transparent' : 'hover:bg-secondary hover:bg-opacity-10'}
|
||||
hover:border-secondary hover:border-solid hover:border-2
|
||||
${active ? 'border-solid' : 'border-dotted'}
|
||||
${active && !accordion ? 'bg-secondary' : 'bg-transparent'}
|
||||
`}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
|
||||
/*
|
||||
* Input for integers
|
||||
*/
|
||||
export const NumberInput = ({
|
||||
label, // Label to use
|
||||
update, // onChange handler
|
||||
valid, // Method that should return whether the value is valid or not
|
||||
current, // The current value
|
||||
original, // The original value
|
||||
placeholder, // The placeholder text
|
||||
docs = false, // Docs to load, if any
|
||||
id = '', // An id to tie the input to the label
|
||||
labelBL = false, // Bottom-Left label
|
||||
labelBR = false, // Bottom-Right label
|
||||
max = 0,
|
||||
min = 220,
|
||||
step = 1,
|
||||
Swizzled, // Object holding swizzled code
|
||||
}) => (
|
||||
<Swizzled.components.FormControl {...{ label, labelBL, labelBR, docs }} forId={id}>
|
||||
<input
|
||||
id={id}
|
||||
type="number"
|
||||
placeholder={placeholder}
|
||||
value={current}
|
||||
onChange={(evt) => update(evt.target.value)}
|
||||
className={`input w-full input-bordered ${
|
||||
current === original ? 'input-secondary' : valid(current) ? 'input-success' : 'input-error'
|
||||
}`}
|
||||
{...{ max, min, step }}
|
||||
/>
|
||||
</Swizzled.components.FormControl>
|
||||
)
|
||||
|
||||
/*
|
||||
* Input for strings
|
||||
*/
|
||||
export const StringInput = ({
|
||||
label, // Label to use
|
||||
update, // onChange handler
|
||||
valid, // Method that should return whether the value is valid or not
|
||||
current, // The current value
|
||||
original, // The original value
|
||||
placeholder, // The placeholder text
|
||||
docs = false, // Docs to load, if any
|
||||
id = '', // An id to tie the input to the label
|
||||
labelBL = false, // Bottom-Left label
|
||||
labelBR = false, // Bottom-Right label
|
||||
Swizzled, // Object holding swizzled code
|
||||
}) => (
|
||||
<Swizzled.components.FormControl {...{ label, labelBL, labelBR, docs }} forId={id}>
|
||||
<input
|
||||
id={id}
|
||||
type="text"
|
||||
placeholder={placeholder}
|
||||
value={current}
|
||||
onChange={(evt) => update(evt.target.value)}
|
||||
className={`input w-full input-bordered ${
|
||||
current === original ? 'input-secondary' : valid(current) ? 'input-success' : 'input-error'
|
||||
}`}
|
||||
/>
|
||||
</Swizzled.components.FormControl>
|
||||
)
|
||||
|
||||
/*
|
||||
* Input for a list of things to pick from
|
||||
*/
|
||||
export const ListInput = ({
|
||||
update, // the onChange handler
|
||||
label, // The label
|
||||
list, // The list of items to present { val, label, desc }
|
||||
current, // The (value of the) current item
|
||||
docs = false, // Docs to load, if any
|
||||
Swizzled, // Object holding swizzled code
|
||||
}) => (
|
||||
<Swizzled.components.FormControl label={label} docs={docs}>
|
||||
{list.map((item, i) => (
|
||||
<Swizzled.components.ButtonFrame
|
||||
key={i}
|
||||
active={item.val === current}
|
||||
onClick={() => update(item.val)}
|
||||
>
|
||||
<div className="w-full flex flex-col gap-2">
|
||||
<div className="w-full text-lg leading-5">{item.label}</div>
|
||||
{item.desc ? (
|
||||
<div className="w-full text-normal font-normal normal-case pt-1 leading-5">
|
||||
{item.desc}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</Swizzled.components.ButtonFrame>
|
||||
))}
|
||||
</Swizzled.components.FormControl>
|
||||
)
|
||||
|
||||
/*
|
||||
* Input for markdown content
|
||||
*/
|
||||
export const MarkdownInput = ({
|
||||
label, // The label
|
||||
current, // The current value (markdown)
|
||||
update, // The onChange handler
|
||||
placeholder, // The placeholder content
|
||||
docs = false, // Docs to load, if any
|
||||
id = '', // An id to tie the input to the label
|
||||
labelBL = false, // Bottom-Left label
|
||||
labelBR = false, // Bottom-Right label
|
||||
Swizzled, // Swizzled code
|
||||
}) => (
|
||||
<Swizzled.components.FormControl {...{ label, labelBL, labelBR, docs }} forId={id}>
|
||||
<Swizzled.components.Tabs tabs={['edit', 'preview']}>
|
||||
<Swizzled.components.Tab key="edit">
|
||||
<div className="flex flex-row items-center mt-4">
|
||||
<textarea
|
||||
id={id}
|
||||
rows="5"
|
||||
className="textarea textarea-bordered textarea-lg w-full"
|
||||
value={current}
|
||||
placeholder={placeholder}
|
||||
onChange={(evt) => update(evt.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</Swizzled.components.Tab>
|
||||
<Swizzled.components.Tab key="preview">
|
||||
<div className="mdx mt-4 shadow p-2 px-4 rounded">
|
||||
<Swizzled.components.Markdown>{current}</Swizzled.components.Markdown>
|
||||
</div>
|
||||
</Swizzled.components.Tab>
|
||||
</Swizzled.components.Tabs>
|
||||
</Swizzled.components.FormControl>
|
||||
)
|
||||
|
||||
export const MeasurementInput = ({
|
||||
imperial, // True for imperial, False for metric
|
||||
m, // The measurement name
|
||||
original, // The original value
|
||||
update, // The onChange handler
|
||||
placeholder, // The placeholder content
|
||||
docs = false, // Docs to load, if any
|
||||
id = '', // An id to tie the input to the label
|
||||
Swizzled, // Swizzled code
|
||||
}) => {
|
||||
const { t } = Swizzled.methods
|
||||
const isDegree = Swizzled.methods.isDegreeMeasurement(m)
|
||||
const units = imperial ? 'imperial' : 'metric'
|
||||
|
||||
const [localVal, setLocalVal] = useState(
|
||||
typeof original === 'undefined'
|
||||
? original
|
||||
: isDegree
|
||||
? Number(original)
|
||||
: Swizzled.methods.measurementAsUnits(original, units)
|
||||
)
|
||||
const [validatedVal, setValidatedVal] = useState(
|
||||
Swizzled.methods.measurementAsUnits(original, units)
|
||||
)
|
||||
const [valid, setValid] = useState(null)
|
||||
|
||||
// Update onChange
|
||||
const localUpdate = (newVal) => {
|
||||
setLocalVal(newVal)
|
||||
const parsedVal = isDegree
|
||||
? Number(newVal)
|
||||
: Swizzled.methods.parseDistanceInput(newVal, imperial)
|
||||
if (parsedVal) {
|
||||
update(m, isDegree ? parsedVal : Swizzled.methods.measurementAsMm(parsedVal, units))
|
||||
setValid(true)
|
||||
setValidatedVal(parsedVal)
|
||||
} else setValid(false)
|
||||
}
|
||||
|
||||
if (!m) return null
|
||||
|
||||
// Various visual indicators for validating the input
|
||||
let inputClasses = 'input-secondary'
|
||||
let bottomLeftLabel = null
|
||||
if (valid === true) {
|
||||
inputClasses = 'input-success'
|
||||
const val = `${validatedVal}${isDegree ? '°' : imperial ? '"' : 'cm'}`
|
||||
bottomLeftLabel = <span className="font-medium text-base text-success -mt-2 block">{val}</span>
|
||||
} else if (valid === false) {
|
||||
inputClasses = 'input-error'
|
||||
bottomLeftLabel = (
|
||||
<span className="font-medium text-error text-base -mt-2 block">¯\_(ツ)_/¯</span>
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* I'm on the fence here about using a text input rather than number
|
||||
* Obviously, number is the more correct option, but when the user enter
|
||||
* text, it won't fire an onChange event and thus they can enter text and it
|
||||
* will not be marked as invalid input.
|
||||
* See: https://github.com/facebook/react/issues/16554
|
||||
*/
|
||||
return (
|
||||
<Swizzled.components.FormControl
|
||||
label={t(m) + (isDegree ? ' (°)' : '')}
|
||||
docs={docs}
|
||||
forId={id}
|
||||
labelBL={bottomLeftLabel}
|
||||
>
|
||||
<input
|
||||
id={id}
|
||||
type="number"
|
||||
placeholder={placeholder}
|
||||
value={localVal}
|
||||
onChange={(evt) => localUpdate(evt.target.value)}
|
||||
className={`input w-full input-bordered ${inputClasses}`}
|
||||
/>
|
||||
</Swizzled.components.FormControl>
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* Input for booleans
|
||||
*/
|
||||
export const ToggleInput = ({
|
||||
label, // Label to use
|
||||
update, // onChange handler
|
||||
current, // The current value
|
||||
disabled = false, // Allows rendering a disabled view
|
||||
list = [true, false], // The values to chose between
|
||||
labels = ['Yes', 'No'], // The labels for the values
|
||||
on = true, // The value that should show the toggle in the 'on' state
|
||||
id = '', // An id to tie the input to the label
|
||||
labelTR = false, // Top-Right label
|
||||
labelBL = false, // Bottom-Left label
|
||||
labelBR = false, // Bottom-Right label
|
||||
Swizzled, // Object holding swizzled code
|
||||
}) => (
|
||||
<Swizzled.components.FormControl
|
||||
{...{ labelBL, labelBR, labelTR }}
|
||||
label={
|
||||
label
|
||||
? `${label} (${current === on ? labels[0] : labels[1]})`
|
||||
: `${current === on ? labels[0] : labels[1]}`
|
||||
}
|
||||
forId={id}
|
||||
>
|
||||
<input
|
||||
id={id}
|
||||
disabled={disabled}
|
||||
type="checkbox"
|
||||
value={current}
|
||||
onChange={() => update(list.indexOf(current) === 0 ? list[1] : list[0])}
|
||||
className="toggle my-3 toggle-primary"
|
||||
checked={list.indexOf(current) === 0 ? true : false}
|
||||
/>
|
||||
</Swizzled.components.FormControl>
|
||||
)
|
|
@ -0,0 +1 @@
|
|||
export const LargeScreenOnly = ({ children }) => <div className="hidden xl:block">{children}</div>
|
|
@ -0,0 +1,48 @@
|
|||
import Link from 'next/link'
|
||||
|
||||
const AnchorLink = ({ id, txt = false, children, Swizzled }) => (
|
||||
<a href={`#${id}`} className={Swizzled.config.classes.link} title={txt ? txt : ''}>
|
||||
{txt ? txt : children}
|
||||
</a>
|
||||
)
|
||||
|
||||
const PageLink = ({ href, txt = false, children, Swizzled }) => (
|
||||
<Swizzled.components.Link
|
||||
href={href}
|
||||
className={Swizzled.config.classes.link}
|
||||
title={txt ? txt : ''}
|
||||
>
|
||||
{children ? children : txt}
|
||||
</Swizzled.components.Link>
|
||||
)
|
||||
|
||||
const WebLink = ({ href, txt = false, children, Swizzled }) => (
|
||||
<a href={href} className={Swizzled.config.classes.link} title={txt ? txt : ''}>
|
||||
{children ? children : txt}
|
||||
</a>
|
||||
)
|
||||
|
||||
const CardLink = ({
|
||||
bg = 'bg-base-200',
|
||||
textColor = 'text-base-content',
|
||||
href,
|
||||
title,
|
||||
text,
|
||||
icon,
|
||||
Swizzled,
|
||||
}) => (
|
||||
<Swizzled.components.Link
|
||||
href={href}
|
||||
className={`px-8 ${bg} py-10 rounded-lg block ${textColor}
|
||||
hover:bg-secondary hover:bg-opacity-10 shadow-lg
|
||||
transition-color duration-300 grow`}
|
||||
>
|
||||
<h2 className="mb-4 text-inherit flex flex-row gap-4 justify-between items-center font-medium">
|
||||
{title}
|
||||
<span className="shrink-0">{icon}</span>
|
||||
</h2>
|
||||
<p className="font-medium text-inherit italic text-lg">{text}</p>
|
||||
</Swizzled.components.Link>
|
||||
)
|
||||
|
||||
export { Link, AnchorLink, PageLink, WebLink, CardLink }
|
|
@ -0,0 +1,6 @@
|
|||
/**
|
||||
* A temporary loader 'one moment please' style
|
||||
* Just a spinner in this case, but could also be branded.
|
||||
*/
|
||||
export const TemporaryLoader = ({ Swizzled = false }) =>
|
||||
Swizzled ? <Swizzled.components.Spinner /> : <div className="text-center m-auto">...</div>
|
|
@ -0,0 +1,57 @@
|
|||
import { useEffect } from 'react'
|
||||
|
||||
export const LoadingStatus = ({ Swizzled, state, update }) => {
|
||||
useEffect(() => {
|
||||
if (typeof state._.loading === 'object') {
|
||||
for (const conf of Object.values(state._.loading)) {
|
||||
if (conf.fadeTimer)
|
||||
window.setTimeout(function () {
|
||||
update.fadeNotify(conf.id)
|
||||
}, conf.fadeTimer)
|
||||
if (conf.clearTimer)
|
||||
window.setTimeout(function () {
|
||||
update.clearNotify(conf.id)
|
||||
}, conf.clearTimer)
|
||||
}
|
||||
}
|
||||
}, [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">
|
||||
{Object.entries(state._.loading).map(([id, config]) => {
|
||||
const conf = {
|
||||
...Swizzled.config.loadingStatus.defaults,
|
||||
...config,
|
||||
}
|
||||
const Icon =
|
||||
typeof conf.icon === 'undefined'
|
||||
? Swizzled.components.Spinner
|
||||
: Swizzled.components[`${Swizzled.methods.capitalize(conf.icon)}Icon`] ||
|
||||
Swizzled.components.Noop
|
||||
return (
|
||||
<div
|
||||
key={id}
|
||||
className={`w-full md:max-w-2xl m-auto bg-${
|
||||
conf.color
|
||||
} flex flex-row items-center gap-4 p-4 px-4 ${
|
||||
conf.fading ? 'opacity-0' : 'opacity-100'
|
||||
}
|
||||
transition-opacity delay-[${
|
||||
Swizzled.config.loadingStatus.timeout * 1000 - 400
|
||||
}ms] duration-300
|
||||
md:rounded-lg shadow text-secondary-content text-lg lg:text-xl font-medium md:bg-opacity-90`}
|
||||
>
|
||||
<span className="shrink-0">
|
||||
<Icon />
|
||||
</span>
|
||||
{conf.msg}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
import Markdown from 'react-markdown'
|
||||
|
||||
export { Markdown }
|
|
@ -0,0 +1,57 @@
|
|||
/**
|
||||
* This MeasurementsEditor component allows inline-editing of the measurements
|
||||
*
|
||||
* @param {object} props - The component's props
|
||||
* @param {function} props.Design - The design constructor
|
||||
* @param {object} props.state - The ViewWrapper state object
|
||||
* @param {object} props.state.settings - The current settings
|
||||
* @param {object} props.update - Helper object for updating the ViewWrapper state
|
||||
* @param {object} props.Swizzled - An object holding swizzled code
|
||||
* @return {function} MeasurementsEditor - React component
|
||||
*/
|
||||
export const MeasurementsEditor = ({ Design, update, state, Swizzled }) => {
|
||||
/*
|
||||
* Helper method to handle state updates for measurements
|
||||
*/
|
||||
const onUpdate = (m, newVal) => {
|
||||
update.settings(['measurements', m], newVal)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl">
|
||||
<h5>{Swizzled.methods.t('pe:requiredMeasurements')}</h5>
|
||||
{Object.keys(Design.patternConfig.measurements).length === 0 ? (
|
||||
<p>({Swizzled.methods.t('account:none')})</p>
|
||||
) : (
|
||||
<div>
|
||||
{Design.patternConfig.measurements.map((m) => (
|
||||
<Swizzled.components.MeasurementInput
|
||||
key={m}
|
||||
m={m}
|
||||
imperial={state.settings.units === 'imperial' ? true : false}
|
||||
original={state.settings.measurements?.[m]}
|
||||
update={(m, newVal) => onUpdate(m, newVal)}
|
||||
id={`edit-${m}`}
|
||||
/>
|
||||
))}
|
||||
<br />
|
||||
</div>
|
||||
)}
|
||||
<h5>{Swizzled.methods.t('pe:optionalMeasurements')}</h5>
|
||||
{Object.keys(Design.patternConfig.optionalMeasurements).length === 0 ? (
|
||||
<p>({Swizzled.methods.t('account:none')})</p>
|
||||
) : (
|
||||
Design.patternConfig.optionalMeasurements.map((m) => (
|
||||
<Swizzled.components.MeasurementInput
|
||||
key={m}
|
||||
m={m}
|
||||
imperial={state.settings.units === 'umperial' ? true : false}
|
||||
original={state.settings.measurements?.[m]}
|
||||
update={(m, newVal) => onUpdate(m, newVal)}
|
||||
id={`edit-${m}`}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
const sizes = { lg: 96, md: 52, sm: 36 }
|
||||
|
||||
export const MeasurementsSetCard = ({
|
||||
set,
|
||||
onClick = false,
|
||||
href = false,
|
||||
useA = false,
|
||||
Design = false,
|
||||
language = false,
|
||||
size = 'lg',
|
||||
Swizzled,
|
||||
}) => {
|
||||
const s = sizes[size]
|
||||
const { t, hasRequiredMeasurements } = Swizzled.methods
|
||||
const { NoIcon, OkIcon } = Swizzled.components
|
||||
|
||||
const wrapperProps = {
|
||||
className: `bg-base-300 aspect-square h-${s} w-${s} mb-2
|
||||
mx-auto flex flex-col items-start text-center justify-between rounded-none md:rounded shadow`,
|
||||
style: {
|
||||
backgroundImage: `url(${Swizzled.methods.cloudImageUrl({ type: 'w500', id: set.img })})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: '50%',
|
||||
},
|
||||
}
|
||||
if (!set.img || set.img === 'default-avatar')
|
||||
wrapperProps.style.backgroundPosition = 'bottom right'
|
||||
|
||||
let icon = <span></span>
|
||||
let missingMeasies = ''
|
||||
let linebreak = ''
|
||||
const maxLength = 75
|
||||
if (Design) {
|
||||
const [hasMeasies, missing] = hasRequiredMeasurements(Design, set.measies)
|
||||
const iconClasses = 'w-8 h-8 p-1 rounded-full -mt-2 -ml-2 shadow'
|
||||
icon = hasMeasies ? (
|
||||
<OkIcon className={`${iconClasses} bg-success text-success-content`} stroke={4} />
|
||||
) : (
|
||||
<NoIcon className={`${iconClasses} bg-error text-error-content`} stroke={3} />
|
||||
)
|
||||
if (missing.length > 0) {
|
||||
const translated = missing.map((m) => {
|
||||
return t(m)
|
||||
})
|
||||
let missingString = translated.join(', ')
|
||||
if (missingString.length > maxLength) {
|
||||
const lastSpace = missingString.lastIndexOf(', ', maxLength)
|
||||
missingString = missingString.substring(0, lastSpace) + ', ' + '...'
|
||||
}
|
||||
const measieClasses = 'font-normal text-xs'
|
||||
missingMeasies = <span className={`${measieClasses}`}>{missingString}</span>
|
||||
linebreak = <br />
|
||||
}
|
||||
}
|
||||
|
||||
const inner = (
|
||||
<>
|
||||
{icon}
|
||||
<span className="bg-neutral text-neutral-content px-4 w-full bg-opacity-50 py-2 rounded rounded-t-none font-bold leading-5">
|
||||
{language ? set[`name${Swizzled.methods.capitalize(language)}`] : set.name}
|
||||
{linebreak}
|
||||
{missingMeasies}
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
|
||||
// Is it a button with an onClick handler?
|
||||
if (onClick)
|
||||
return (
|
||||
<button {...wrapperProps} onClick={() => onClick(set)}>
|
||||
{inner}
|
||||
</button>
|
||||
)
|
||||
|
||||
// Returns a link to an internal page
|
||||
if (href && !useA)
|
||||
return (
|
||||
<Swizzled.components.Link {...wrapperProps} href={href}>
|
||||
{inner}
|
||||
</Swizzled.components.Link>
|
||||
)
|
||||
|
||||
// Returns a link to an external page
|
||||
if (href && useA)
|
||||
return (
|
||||
<a {...wrapperProps} href={href}>
|
||||
{inner}
|
||||
</a>
|
||||
)
|
||||
|
||||
// Returns a div
|
||||
return <div {...wrapperProps}>{inner}</div>
|
||||
}
|
|
@ -0,0 +1,171 @@
|
|||
import { Fragment, useEffect } from 'react'
|
||||
import { horFlexClasses } from '../../utils.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 }
|
||||
|
||||
/**
|
||||
* The measurements view is loaded to update/set measurements
|
||||
*
|
||||
* It will be automatically loaded if we do not have all required measurements for a design.
|
||||
*
|
||||
* @param {Object} props - All the props
|
||||
* @param {Object} props.Swizzled - An object with swizzled components, hooks, methods, and config
|
||||
* @param {Function} props.Design - The design constructor
|
||||
* @param {string} props.design - The design name
|
||||
* @param {Object} props.state - The editor state object
|
||||
* @param {Object} props.update - Helper object for updating the ViewWrapper state
|
||||
* @param {Array} props.missingMeasurements - List of missing measurements for the current design
|
||||
* @return {Function} MeasurementsView - React component
|
||||
*/
|
||||
export const MeasurementsView = ({ Design, missingMeasurements, update, Swizzled, state }) => {
|
||||
// Swizzled components
|
||||
const {
|
||||
Accordion,
|
||||
Popout,
|
||||
MeasurementsEditor,
|
||||
MeasurementsSetIcon,
|
||||
UserSetPicker,
|
||||
BookmarkIcon,
|
||||
BookmarkedSetPicker,
|
||||
CuratedMeasurementsSetIcon,
|
||||
CuratedSetPicker,
|
||||
EditIcon,
|
||||
} = Swizzled.components
|
||||
// Swizzled methods
|
||||
const { t, designMeasurements, capitalize } = Swizzled.methods
|
||||
// Swizzled config
|
||||
const { config } = Swizzled
|
||||
// Editor state
|
||||
const { locale } = state
|
||||
|
||||
/*
|
||||
* If there is no view set, completing measurements will switch to the view picker
|
||||
* Which is a bit confusing. So in this case, set the view to measurements.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!config.views.includes(state.view)) update.view('measurements')
|
||||
if (state._.missingMeasurements && state._.missingMeasurements.length > 0)
|
||||
update.notify({ msg: t('pe:missingMeasurementsNotify'), icon: 'tip' }, 'missingMeasurements')
|
||||
else update.notifySuccess(t('pe:measurementsAreOk'))
|
||||
}, [state.view, update])
|
||||
|
||||
const loadMeasurements = (set) => {
|
||||
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)}`])
|
||||
// Curated measurement set
|
||||
update.settings(['metadata'], { setName: set[`name${capitalize(locale)}`] })
|
||||
else if (set.name)
|
||||
// User measurement set
|
||||
update.settings(['metadata'], { setName: set.name })
|
||||
}
|
||||
|
||||
// Construct accordion items based on the editor configuration
|
||||
const items = []
|
||||
if (config.enableBackend)
|
||||
items.push(
|
||||
[
|
||||
<Fragment key={1}>
|
||||
<div className={horFlexClasses}>
|
||||
<h5 id="ownsets">{t('pe:chooseFromOwnSets')}</h5>
|
||||
<MeasurementsSetIcon {...iconClasses} />
|
||||
</div>
|
||||
<p className="text-left">{t('pe:chooseFromOwnSetsDesc')}</p>
|
||||
</Fragment>,
|
||||
<UserSetPicker
|
||||
key={2}
|
||||
size="md"
|
||||
clickHandler={loadMeasurements}
|
||||
missingClickHandler={loadMeasurements}
|
||||
{...{ Swizzled, Design }}
|
||||
/>,
|
||||
'ownSets',
|
||||
],
|
||||
[
|
||||
<Fragment key={1}>
|
||||
<div className={horFlexClasses}>
|
||||
<h5 id="bookmarkedsets">{t('pe:chooseFromBookmarkedSets')}</h5>
|
||||
<BookmarkIcon {...iconClasses} />
|
||||
</div>
|
||||
<p className="text-left">{t('pe:chooseFromBookmarkedSetsDesc')}</p>
|
||||
</Fragment>,
|
||||
<BookmarkedSetPicker
|
||||
key={2}
|
||||
size="md"
|
||||
clickHandler={loadMeasurements}
|
||||
missingClickHandler={loadMeasurements}
|
||||
{...{ Swizzled, Design }}
|
||||
/>,
|
||||
'bmSets',
|
||||
],
|
||||
[
|
||||
<Fragment key={1}>
|
||||
<div className={horFlexClasses}>
|
||||
<h5 id="curatedsets">{t('pe:chooseFromCuratedSets')}</h5>
|
||||
<CuratedMeasurementsSetIcon {...iconClasses} />
|
||||
</div>
|
||||
<p className="text-left">{t('pe:chooseFromCuratedSetsDesc')}</p>
|
||||
</Fragment>,
|
||||
<CuratedSetPicker
|
||||
key={2}
|
||||
clickHandler={loadMeasurements}
|
||||
{...{ Swizzled, Design, locale }}
|
||||
/>,
|
||||
'csets',
|
||||
]
|
||||
)
|
||||
// Manual editing is always an option
|
||||
items.push([
|
||||
<Fragment key={1}>
|
||||
<div className={horFlexClasses}>
|
||||
<h5 id="editmeasurements">{t('pe:editMeasurements')}</h5>
|
||||
<EditIcon {...iconClasses} />
|
||||
</div>
|
||||
<p className="text-left">{t('pe:editMeasurementsDesc')}</p>
|
||||
</Fragment>,
|
||||
<MeasurementsEditor key={2} {...{ Design, Swizzled, update, state }} />,
|
||||
'edit',
|
||||
])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Swizzled.components.HeaderMenu state={state} {...{ Swizzled, update }} />
|
||||
<div className="max-w-7xl mt-8 mx-auto px-4">
|
||||
<h2>{t('pe:measurements')}</h2>
|
||||
{missingMeasurements && missingMeasurements.length > 0 ? (
|
||||
<Popout note dense noP>
|
||||
<h5>{t('pe:missingMeasurementsInfo')}:</h5>
|
||||
<ol className="list list-inside flex flex-row flex-wrap">
|
||||
{missingMeasurements.map((m, i) => (
|
||||
<li key={i}>
|
||||
{i > 0 ? <span className="pr-2">,</span> : null}
|
||||
<span className="font-medium">{t(`measurements:${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')}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary btn-outline lg:btn-lg"
|
||||
onClick={() => update.view('picker')}
|
||||
>
|
||||
{t('pe:chooseAnotherActivity')}
|
||||
</button>
|
||||
</div>
|
||||
</Popout>
|
||||
)}
|
||||
{items.length > 1 ? <Accordion items={items} /> : items}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,272 @@
|
|||
import { useState, useMemo } from 'react'
|
||||
|
||||
/** @type {String} class to apply to buttons on open menu items */
|
||||
const iconButtonClass = 'btn btn-xs btn-ghost px-0 text-accent'
|
||||
|
||||
/**
|
||||
* A generic component for handling a menu item.
|
||||
* Wraps the given input in a {@see Collapse} with the appropriate buttons
|
||||
* @param {String} options.name the name of the item, for using as a key
|
||||
* @param {Object} options.config the configuration for the input
|
||||
* @param {Sting|Boolean|Number} options.current the current value of the item
|
||||
* @param {Function} options.updateFunc the function that will be called by event handlers to update the value
|
||||
* @param {Function} options.t the translation function
|
||||
* @param {Object} options.passProps props to pass to the Input component
|
||||
* @param {Boolean} changed has the value changed from default?
|
||||
* @param {React.Component} Input the input component this menu item will use
|
||||
* @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 = {},
|
||||
changed,
|
||||
Input = () => {},
|
||||
allowOverride = false,
|
||||
ux = 5,
|
||||
docs,
|
||||
config,
|
||||
Design,
|
||||
}) => {
|
||||
// Local state - whether the override input should be shown
|
||||
const [override, setOverride] = useState(false)
|
||||
|
||||
// generate properties to pass to the Input
|
||||
const drillProps = useMemo(
|
||||
() => ({
|
||||
name,
|
||||
config,
|
||||
ux,
|
||||
current,
|
||||
updateHandler,
|
||||
t: Swizzled.methods.t,
|
||||
changed,
|
||||
override,
|
||||
Design,
|
||||
...passProps,
|
||||
}),
|
||||
[name, config, current, updateHandler, changed, override, passProps, ux]
|
||||
)
|
||||
|
||||
if (!config) {
|
||||
console.log('no config in containers', { name, current, config })
|
||||
return null
|
||||
}
|
||||
|
||||
// don't render if this item is more advanced than the user has chosen to see
|
||||
if (config.ux && config.ux > ux) return null
|
||||
|
||||
// get buttons for open and closed states
|
||||
const buttons = []
|
||||
if (allowOverride)
|
||||
buttons.push(
|
||||
<button
|
||||
key="edit"
|
||||
className={iconButtonClass}
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation()
|
||||
setOverride(!override)
|
||||
}}
|
||||
>
|
||||
<Swizzled.components.EditIcon
|
||||
className={`w-6 h-6 ${
|
||||
override ? 'bg-secondary text-secondary-content rounded' : 'text-secondary'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
)
|
||||
const ResetButton = ({ disabled = false }) => (
|
||||
<button
|
||||
className={`${iconButtonClass} disabled:bg-opacity-0`}
|
||||
disabled={disabled}
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation()
|
||||
updateHandler([name], '__UNSET__')
|
||||
}}
|
||||
>
|
||||
<Swizzled.components.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>}
|
||||
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`)}
|
||||
</span>
|
||||
}
|
||||
docs={docs}
|
||||
>
|
||||
<Input {...drillProps} />
|
||||
</Swizzled.components.FormControl>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A component for recursively displaying groups of menu items.
|
||||
* Accepts any object where menu item configurations are keyed by name
|
||||
* Items that are group headings are expected to have an isGroup: true property
|
||||
* @param {Boolean} options.collapsible Should this group be collapsible (use false for the top level of a menu)
|
||||
* @param {Number} options.ux the user-defined ux level
|
||||
* @param {String} options.name the name of the group or item
|
||||
* @param {Object} options.currentValues a map of current values for items in the group, keyed by name
|
||||
* @param {Object} structure the configuration for the group.
|
||||
* @param {React.Component} Icon the icon to display next to closed groups
|
||||
* @param {React.Component} Item the component to use for menu items
|
||||
* @param {Object} values a map of Value display components to be used by menu items in the group
|
||||
* @param {Object} inputs a map of Input components to be used by menu items in the group
|
||||
* @param {Object} passProps properties to pass to Inputs within menu items
|
||||
* @param {Object} emojis a map of emojis to use as icons for groups or items
|
||||
* @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,
|
||||
ux,
|
||||
currentValues = {},
|
||||
structure,
|
||||
Icon,
|
||||
Item = false,
|
||||
inputs = {},
|
||||
values = {},
|
||||
passProps = {},
|
||||
updateHandler,
|
||||
topLevel = false,
|
||||
isDesignOptionsGroup = false,
|
||||
Design,
|
||||
Swizzled,
|
||||
state,
|
||||
}) => {
|
||||
if (!Item) Item = Swizzled.components.MenuItem
|
||||
|
||||
// map the entries in the structure
|
||||
const content = Object.entries(structure).map(([itemName, item]) => {
|
||||
// if it's the isGroup property, or it is false, it shouldn't be shown
|
||||
if (itemName === 'isGroup' || item === false) return null
|
||||
if (!item) return null
|
||||
if (item.ux && ux && item.ux > ux) return null
|
||||
|
||||
const ItemIcon = item.icon
|
||||
? item.icon
|
||||
: item.isGroup
|
||||
? Swizzled.components.GroupIcon
|
||||
: Icon
|
||||
? Icon
|
||||
: () => <span role="img">fixme-icon</span>
|
||||
const Value = item.isGroup
|
||||
? () => (
|
||||
<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" />
|
||||
</div>
|
||||
)
|
||||
: isDesignOptionsGroup
|
||||
? values[Swizzled.methods.designOptionType(item)]
|
||||
: values[itemName]
|
||||
? values[itemName]
|
||||
: () => <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">
|
||||
<ItemIcon />
|
||||
<span className="font-medium">
|
||||
{Swizzled.methods.t([`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)}
|
||||
Design={Design}
|
||||
/>
|
||||
</div>
|
||||
</div>,
|
||||
item.isGroup ? (
|
||||
<MenuItemGroup
|
||||
key={itemName}
|
||||
{...{
|
||||
collapsible: true,
|
||||
// it's the top level if the previous level was top but not wrapped
|
||||
topLevel: topLevel && !collapsible,
|
||||
state,
|
||||
name: itemName,
|
||||
currentValues,
|
||||
structure: item,
|
||||
Icon,
|
||||
Item,
|
||||
values,
|
||||
inputs,
|
||||
passProps,
|
||||
updateHandler,
|
||||
isDesignOptionsGroup,
|
||||
Design,
|
||||
Swizzled,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Item
|
||||
key={itemName}
|
||||
{...{
|
||||
name: itemName,
|
||||
current: currentValues[itemName],
|
||||
config: item,
|
||||
state,
|
||||
changed: Swizzled.methods.menuValueWasChanged(currentValues[itemName], item),
|
||||
Value: values[itemName],
|
||||
Input: inputs[itemName],
|
||||
updateHandler,
|
||||
passProps,
|
||||
Design,
|
||||
Swizzled,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
itemName,
|
||||
]
|
||||
})
|
||||
|
||||
return <Swizzled.components.SubAccordion items={content.filter((item) => item !== null)} />
|
||||
}
|
||||
|
||||
/**
|
||||
* A generic component to present the title of a menu item
|
||||
* @param {String} options.name the name of the item, to act as its translation key
|
||||
* @param {Function} options.t the translation function
|
||||
* @param {String|React.Component} options.current a the current value, or a Value component to display it
|
||||
* @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,
|
||||
}) => (
|
||||
<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])}
|
||||
</span>
|
||||
<span className="font-bold">{current}</span>
|
||||
</div>
|
||||
)
|
|
@ -0,0 +1,135 @@
|
|||
/**
|
||||
* The core settings menu
|
||||
* @param {Object} options.update settings and ui update functions
|
||||
* @param {Object} options.settings core settings
|
||||
* @param {Object} options.patternConfig the configuration from the pattern
|
||||
* @param {String} options.language the menu language
|
||||
* @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({
|
||||
language,
|
||||
units: state.settings?.units,
|
||||
sabool: state.settings?.sabool,
|
||||
parts: Design.patternConfig.draftOrder,
|
||||
})
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
return (
|
||||
<Swizzled.components.MenuItemGroup
|
||||
{...{
|
||||
structure,
|
||||
ux: state.ui.ux,
|
||||
currentValues: state.settings || {},
|
||||
Icon: Swizzled.components.SettingsIcon,
|
||||
Item: (props) => (
|
||||
<Swizzled.components.CoreSetting
|
||||
updateHandler={update}
|
||||
{...{ inputs, values, Swizzled, Design }}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
isFirst: true,
|
||||
name: 'pe:designOptions',
|
||||
language: state.locale,
|
||||
passProps: {
|
||||
ux: state.ui.ux,
|
||||
settings: state.settings,
|
||||
patternConfig: Design.patternConfig,
|
||||
toggleSa: update.toggleSa,
|
||||
},
|
||||
updateHandler: update.settings,
|
||||
isDesignOptionsGroup: false,
|
||||
Swizzled,
|
||||
state,
|
||||
Design,
|
||||
inputs,
|
||||
values,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/** A wrapper for {@see MenuItem} to handle core settings-specific business */
|
||||
export const CoreSetting = ({
|
||||
Swizzled,
|
||||
name,
|
||||
config,
|
||||
ux,
|
||||
updateHandler,
|
||||
current,
|
||||
passProps,
|
||||
...rest
|
||||
}) => {
|
||||
// is toggling allowed?
|
||||
const allowToggle = ux > 3 && config.list?.length === 2
|
||||
|
||||
const handlerArgs = {
|
||||
updateHandler,
|
||||
current,
|
||||
config,
|
||||
...passProps,
|
||||
}
|
||||
|
||||
/*
|
||||
* Load a specific update handler if one is configured
|
||||
*/
|
||||
const handler = Swizzled.config.menuCoreSettingsHandlerMethods?.[name.toLowerCase()]
|
||||
? Swizzled.methods[Swizzled.config.menuCoreSettingsHandlerMethods[name.toLowerCase()]](
|
||||
handlerArgs
|
||||
)
|
||||
: updateHandler
|
||||
|
||||
return (
|
||||
<Swizzled.components.MenuItem
|
||||
{...{
|
||||
name,
|
||||
config,
|
||||
ux,
|
||||
current,
|
||||
passProps,
|
||||
...rest,
|
||||
allowToggle,
|
||||
updateHandler: handler,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const ClearAllButton = ({ setSettings, compact = false, Swizzled }) => {
|
||||
return (
|
||||
<div className={`${compact ? '' : 'text-center mt-8'}`}>
|
||||
<button
|
||||
className={`justify-self-center btn btn-error btn-outline ${compact ? 'btn-sm' : ''}`}
|
||||
onClick={() => setSettings({})}
|
||||
>
|
||||
<Swizzled.components.TrashIcon />
|
||||
{Swizzled.methods.t('clearSettings')}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
import { useCallback, useMemo } from 'react'
|
||||
|
||||
/**
|
||||
* 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 }) => {
|
||||
const structure = useMemo(
|
||||
() => Swizzled.methods.menuDesignOptionsStructure(Design.patternConfig.options, state.settings),
|
||||
[Design.patternConfig, state.settings]
|
||||
)
|
||||
const updateHandler = useCallback(
|
||||
(name, value = '__UNSET__') => update.settings(['options', ...name], value),
|
||||
[update.settings]
|
||||
)
|
||||
|
||||
const drillProps = { Design, state, update }
|
||||
const inputs = {
|
||||
bool: (props) => <Swizzled.components.MenuBoolInput {...drillProps} {...props} />,
|
||||
constant: (props) => <Swizzled.components.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 />
|
||||
),
|
||||
mm: () => <span>FIXME: Mm options are deprecated. Please report this </span>,
|
||||
pct: (props) => <Swizzled.components.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} />,
|
||||
}
|
||||
|
||||
return (
|
||||
<Swizzled.components.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}
|
||||
/>
|
||||
),
|
||||
isFirst,
|
||||
name: 'pe:designOptions',
|
||||
language: state.locale,
|
||||
passProps: {
|
||||
ux: state.ui.ux,
|
||||
settings: state.settings,
|
||||
patternConfig: Design.patternConfig,
|
||||
},
|
||||
updateHandler,
|
||||
isDesignOptionsGroup: true,
|
||||
Swizzled,
|
||||
state,
|
||||
Design,
|
||||
inputs,
|
||||
values,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper for {@see MenuItem} to handle design option-specific business
|
||||
* @param {Object} options.config the config for the item
|
||||
* @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)
|
||||
const Input = inputs[type]
|
||||
const Value = values[type]
|
||||
const allowOverride = ['pct', 'count', 'deg'].includes(type)
|
||||
const allowToggle = (ux > 3 && type === 'bool') || (type == 'list' && config.list.length === 2)
|
||||
|
||||
// Hide option?
|
||||
if (config?.hide || (typeof config?.hide === 'function' && config.hide(settings))) return null
|
||||
|
||||
return (
|
||||
<Swizzled.components.MenuItem
|
||||
{...{
|
||||
config,
|
||||
ux,
|
||||
...rest,
|
||||
Swizzled,
|
||||
Input,
|
||||
Value,
|
||||
allowOverride,
|
||||
allowToggle,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
export const DraftMenu = ({ Design, pattern, state, Swizzled, update }) => {
|
||||
// Swizzled methods
|
||||
const { t } = Swizzled.methods
|
||||
// Swizzled components
|
||||
const { FlagsAccordionTitle, FlagsAccordionEntries, Accordion } = Swizzled.components
|
||||
|
||||
const menuProps = { Design, state, Swizzled, pattern, update }
|
||||
|
||||
const sections = [
|
||||
{
|
||||
name: 'designOptions',
|
||||
icon: <Swizzled.components.OptionsIcon className="w-8 h-8" />,
|
||||
menu: <Swizzled.components.DesignOptionsMenu {...menuProps} />,
|
||||
},
|
||||
{
|
||||
name: 'coreSettings',
|
||||
icon: <Swizzled.components.SettingsIcon className="w-8 h-8" />,
|
||||
menu: <Swizzled.components.CoreSettingsMenu {...menuProps} />,
|
||||
},
|
||||
{
|
||||
name: 'uiPreferences',
|
||||
icon: <Swizzled.components.UiIcon className="w-8 h-8" />,
|
||||
menu: <Swizzled.components.UiPreferencesMenu {...menuProps} />,
|
||||
},
|
||||
]
|
||||
|
||||
const items = []
|
||||
items.push(
|
||||
...sections.map((section) => [
|
||||
<>
|
||||
<h5 className="flex flex-row gap-2 items-center justify-between w-full">
|
||||
<span>{t(`pe:${section.name}.t`)}</span>
|
||||
{section.icon}
|
||||
</h5>
|
||||
<p className="text-left">{t(`pe:${section.name}.d`)}</p>
|
||||
</>,
|
||||
section.menu,
|
||||
section.name,
|
||||
])
|
||||
)
|
||||
|
||||
const flags = pattern.setStores?.[0]?.plugins?.['plugin-annotations']?.flags
|
||||
|
||||
if (flags)
|
||||
items.push([
|
||||
<FlagsAccordionTitle key={1} {...{ flags, Swizzled }} />,
|
||||
<FlagsAccordionEntries {...{ update, state, flags }} key={2} />,
|
||||
'flags',
|
||||
])
|
||||
|
||||
return <Accordion items={items} />
|
||||
}
|
|
@ -0,0 +1,459 @@
|
|||
import { useMemo, useCallback, useState } from 'react'
|
||||
|
||||
/** A boolean version of {@see MenuListInput} that sets up the necessary configuration */
|
||||
export const MenuBoolInput = (props) => {
|
||||
const { name, config, Swizzled } = props
|
||||
const boolConfig = useBoolConfig(name, config)
|
||||
|
||||
return <Swizzled.components.MenuListInput {...props} config={boolConfig} />
|
||||
}
|
||||
|
||||
/** A placeholder for an input to handle constant values */
|
||||
export const MenuConstantInput = ({
|
||||
type = 'number',
|
||||
name,
|
||||
current,
|
||||
updateHandler,
|
||||
changed,
|
||||
config,
|
||||
}) => (
|
||||
<>
|
||||
<input
|
||||
type={type}
|
||||
className={`
|
||||
input input-bordered w-full text-base-content
|
||||
input-${changed ? 'secondary' : 'accent'}
|
||||
`}
|
||||
value={changed ? current : config.dflt}
|
||||
onChange={(evt) => updateHandler([name], evt.target.value)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
/** 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))
|
||||
},
|
||||
[updateHandler]
|
||||
)
|
||||
return (
|
||||
<MenuSliderInput {...props} suffix="°" valFormatter={round} updateHandler={degUpdateHandler} />
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* An input for selecting and item from a list
|
||||
* @param {String} options.name the name of the property this input changes
|
||||
* @param {Object} options.config configuration for the input
|
||||
* @param {String|Number} options.current the current value of the input
|
||||
* @param {Function} options.updateFunc the function called by the event handler to update the value
|
||||
* @param {Boolean} options.compact include descriptions with the list items?
|
||||
* @param {Function} options.t translation function
|
||||
* @param {String} design name of the design
|
||||
* @param {Boolean} isDesignOption Whether or not it's a design option
|
||||
*/
|
||||
export const MenuListInput = ({
|
||||
name,
|
||||
config,
|
||||
current,
|
||||
updateHandler,
|
||||
compact = false,
|
||||
t,
|
||||
changed,
|
||||
design,
|
||||
isDesignOption = false,
|
||||
Swizzled,
|
||||
}) => {
|
||||
const handleChange = useSharedHandlers({
|
||||
dflt: config.dflt,
|
||||
updateHandler,
|
||||
name,
|
||||
isDesignOption,
|
||||
})
|
||||
|
||||
return config.list.map((entry) => {
|
||||
const titleKey = config.choiceTitles
|
||||
? config.choiceTitles[entry]
|
||||
: isDesignOption
|
||||
? `${design}:${name}.${entry}`
|
||||
: `${name}.o.${entry}`
|
||||
const title = config.titleMethod ? config.titleMethod(entry, t) : t(`${titleKey}.t`)
|
||||
const desc = config.valueMethod ? config.valueMethod(entry, t) : t(`${titleKey}.d`)
|
||||
const sideBySide = config.sideBySide || desc.length + title.length < 42
|
||||
|
||||
return (
|
||||
<Swizzled.components.ButtonFrame
|
||||
dense={config.dense || false}
|
||||
key={entry}
|
||||
active={
|
||||
changed
|
||||
? Array.isArray(current)
|
||||
? current.includes(entry)
|
||||
: current === entry
|
||||
: entry === config.dflt
|
||||
}
|
||||
onClick={() => handleChange(entry)}
|
||||
>
|
||||
<div
|
||||
className={`w-full flex items-start ${
|
||||
sideBySide ? 'flex-row justify-between gap-2' : 'flex-col'
|
||||
}`}
|
||||
>
|
||||
<div className="font-bold text-lg shrink-0">{title}</div>
|
||||
{compact ? null : <div className="text-base font-normal">{desc}</div>}
|
||||
</div>
|
||||
</Swizzled.components.ButtonFrame>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/** a toggle input for list/boolean values */
|
||||
export const MenuListToggle = ({ config, changed, updateHandler, name }) => {
|
||||
const boolConfig = useBoolConfig(name, config)
|
||||
const handleChange = useSharedHandlers({ dflt: boolConfig.dflt, updateHandler, name })
|
||||
|
||||
const dfltIndex = boolConfig.list.indexOf(boolConfig.dflt)
|
||||
|
||||
const doToggle = () =>
|
||||
handleChange(boolConfig.list[changed ? dfltIndex : Math.abs(dfltIndex - 1)])
|
||||
|
||||
const checked = boolConfig.dflt == false ? changed : !changed
|
||||
|
||||
return (
|
||||
<input
|
||||
type="checkbox"
|
||||
className={`toggle ${changed ? 'toggle-accent' : 'toggle-secondary'}`}
|
||||
checked={checked}
|
||||
onChange={doToggle}
|
||||
onClick={(evt) => evt.stopPropagation()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
updateHandler(path, calcCurrent)
|
||||
},
|
||||
[updateHandler, units]
|
||||
)
|
||||
|
||||
// add a default step that's appropriate to the unit. can be overwritten by config
|
||||
const defaultStep = units === 'imperial' ? 0.125 : 0.1
|
||||
|
||||
return (
|
||||
<MenuSliderInput
|
||||
{...props}
|
||||
{...{
|
||||
config: {
|
||||
step: defaultStep,
|
||||
...config,
|
||||
dflt: props.Swizzled.methods.measurementAsUnits(config.dflt, units),
|
||||
},
|
||||
current:
|
||||
current === undefined
|
||||
? undefined
|
||||
: props.Swizzled.methods.measurementAsUnits(current, units),
|
||||
updateHandler: mmUpdateHandler,
|
||||
valFormatter: (val) =>
|
||||
units === 'imperial' ? props.Swizzle.methods.formatFraction128(val, null) : val,
|
||||
suffix: units === 'imperial' ? '"' : 'cm',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A number input that accepts comma or period decimal separators.
|
||||
* Because our use case is almost never going to include thousands, we're using a very simple way of accepting commas:
|
||||
* The validator checks for the presence of a single comma or period followed by numbers
|
||||
* The parser replaces a single comma with a period
|
||||
*
|
||||
* optionally accepts fractions
|
||||
* @param {Number} options.val the value of the input
|
||||
* @param {Function} options.onUpdate a function to handle when the value is updated to a valid value
|
||||
* @param {Boolean} options.fractions should the input allow fractional input
|
||||
*/
|
||||
//export const MenuNumberInput = ({
|
||||
// value,
|
||||
// onUpdate,
|
||||
// onMount,
|
||||
// className,
|
||||
// fractions = true,
|
||||
// min = -Infinity,
|
||||
// max = Infinity,
|
||||
// swizzled,
|
||||
//}) => {
|
||||
// const valid = useRef(validateVal(value, fractions, min, max))
|
||||
//
|
||||
// const handleChange = useCallback(
|
||||
// (newVal) => {
|
||||
// // only actually update if the value is valid
|
||||
// if (typeof onUpdate === 'function') {
|
||||
// onUpdate(valid.current, newVal)
|
||||
// }
|
||||
// },
|
||||
// [onUpdate, valid]
|
||||
// )
|
||||
//
|
||||
// // onChange
|
||||
// const onChange = useCallback(
|
||||
// (evt) => {
|
||||
// const newVal = evt.target.value
|
||||
// // set validity so it will display
|
||||
// valid.current = validateVal(newVal, fractions, min, max)
|
||||
//
|
||||
// // handle the change
|
||||
// handleChange(newVal)
|
||||
// },
|
||||
// [fractions, min, max, valid]
|
||||
// )
|
||||
//
|
||||
// const val = typeof value === 'undefined' ? config.dflt : value
|
||||
//
|
||||
// useEffect(() => {
|
||||
// if (typeof onMount === 'function') {
|
||||
// onMount(valid.current)
|
||||
// }
|
||||
// }, [onMount, valid])
|
||||
//
|
||||
// return (
|
||||
// <input
|
||||
// type="text"
|
||||
// inputMode="number"
|
||||
// className={`input input-secondary ${className || 'input-sm grow text-base-content'}
|
||||
// ${valid.current === false && 'input-error'}
|
||||
// ${valid.current && 'input-success'}
|
||||
// `}
|
||||
// value={val}
|
||||
// onChange={onChange}
|
||||
// />
|
||||
// )
|
||||
//}
|
||||
|
||||
/** A {@see SliderInput} to handle percentage values */
|
||||
export const MenuPctInput = ({ current, changed, updateHandler, config, Swizzled, ...rest }) => {
|
||||
const factor = 100
|
||||
let pctCurrent = changed ? Swizzled.methods.menuRoundPct(current, factor) : current
|
||||
const pctUpdateHandler = useCallback(
|
||||
(path, newVal) =>
|
||||
updateHandler(
|
||||
path,
|
||||
newVal === undefined ? undefined : Swizzled.methods.menuRoundPct(newVal, 1 / factor)
|
||||
),
|
||||
[updateHandler]
|
||||
)
|
||||
|
||||
return (
|
||||
<Swizzled.components.MenuSliderInput
|
||||
{...{
|
||||
...rest,
|
||||
config: { ...config, dflt: Swizzled.methods.menuRoundPct(config.dflt, factor) },
|
||||
current: pctCurrent,
|
||||
updateHandler: pctUpdateHandler,
|
||||
suffix: '%',
|
||||
valFormatter: Swizzled.methods.round,
|
||||
changed,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* An input component that uses a slider to change a number value
|
||||
* @param {String} options.name the name of the property being changed by the input
|
||||
* @param {Object} options.config configuration for the input
|
||||
* @param {Number} options.current the current value of the input
|
||||
* @param {Function} options.updateHandler the function called by the event handler to update the value
|
||||
* @param {Function} options.t translation function
|
||||
* @param {Boolean} options.override open the text input to allow override of the slider?
|
||||
* @param {String} options.suffix a suffix to append to value labels
|
||||
* @param {Function} options.valFormatter a function that accepts a value and formats it for display as a label
|
||||
* @param {Function} options.setReset a setter for the reset function on the parent component
|
||||
*/
|
||||
export const MenuSliderInput = ({
|
||||
name,
|
||||
config,
|
||||
current,
|
||||
updateHandler,
|
||||
override,
|
||||
suffix = '',
|
||||
valFormatter = (val) => val,
|
||||
setReset,
|
||||
children,
|
||||
changed,
|
||||
Swizzled,
|
||||
}) => {
|
||||
const { max, min } = config
|
||||
const handleChange = useSharedHandlers({
|
||||
current,
|
||||
dflt: config.dflt,
|
||||
updateHandler,
|
||||
name,
|
||||
setReset,
|
||||
})
|
||||
|
||||
const val = typeof current === 'undefined' ? config.dflt : current
|
||||
|
||||
if (override)
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row justify-between">
|
||||
<Swizzled.components.MenuEditOption
|
||||
{...{
|
||||
config,
|
||||
Swizzled,
|
||||
current: val,
|
||||
handleChange,
|
||||
min,
|
||||
max,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row justify-between">
|
||||
<span className="opacity-50">
|
||||
<span dangerouslySetInnerHTML={{ __html: valFormatter(min) + suffix }} />
|
||||
</span>
|
||||
<span className={`font-bold ${val === config.dflt ? 'text-secondary' : 'text-accent'}`}>
|
||||
<span dangerouslySetInnerHTML={{ __html: valFormatter(val) + suffix }} />
|
||||
</span>
|
||||
<span className="opacity-50">
|
||||
<span dangerouslySetInnerHTML={{ __html: valFormatter(max) + suffix }} />
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
{...{ 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'}
|
||||
`}
|
||||
/>
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const MenuEditOption = (props) => {
|
||||
const [manualEdit, setManualEdit] = useState(props.current)
|
||||
const { config, handleChange, Swizzled } = props
|
||||
const type = Swizzled.methods.designOptionType(config)
|
||||
|
||||
const onUpdate = useCallback(
|
||||
(validVal) => {
|
||||
if (validVal !== null && validVal !== false) handleChange(validVal)
|
||||
},
|
||||
[handleChange]
|
||||
)
|
||||
|
||||
if (!['pct', 'count', 'deg', 'mm'].includes(type))
|
||||
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>
|
||||
{Swizzled.methods.t('pe:enterCustomValue')} ({Swizzled.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} />
|
||||
<button className="btn btn-secondary mt-4" onClick={() => onUpdate(manualEdit)}>
|
||||
<Swizzled.components.ApplyIcon />
|
||||
</button>
|
||||
</label>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A hook to get the change handler for an input.
|
||||
* @param {Number|String|Boolean} options.dflt the default value for the input
|
||||
* @param {Function} options.updateHandler the onChange
|
||||
* @param {string} options.name the name of the property being changed
|
||||
* @return the change handler for the input
|
||||
*/
|
||||
const useSharedHandlers = ({ dflt, updateHandler, name, isDesignOption }) => {
|
||||
return useCallback(
|
||||
(newCurrent = '__UNSET__') => {
|
||||
/*
|
||||
* When a design option is set to the default, just unset it instead
|
||||
* This is both more efficient, and makes it easy to see in which ways
|
||||
* your pattern differs from the defaults
|
||||
*/
|
||||
if (isDesignOption && newCurrent === dflt) newCurrent = '__UNSET__'
|
||||
updateHandler([name], newCurrent)
|
||||
},
|
||||
[dflt, updateHandler, name, isDesignOption]
|
||||
)
|
||||
}
|
||||
|
||||
/** get the configuration that allows a boolean value to use the list input */
|
||||
const useBoolConfig = (name, config) => {
|
||||
return useMemo(
|
||||
() => ({
|
||||
list: [false, true],
|
||||
choiceTitles: {
|
||||
false: `${name}No`,
|
||||
true: `${name}Yes`,
|
||||
},
|
||||
valueTitles: {
|
||||
false: 'no',
|
||||
true: 'yes',
|
||||
},
|
||||
...config,
|
||||
}),
|
||||
[name, config]
|
||||
)
|
||||
}
|
||||
|
||||
/** an input for the 'only' setting. toggles individual parts*/
|
||||
export const MenuOnlySettingInput = (props) => {
|
||||
const { Swizzled, config } = props
|
||||
const { t } = Swizzled.methods
|
||||
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.dense = true
|
||||
// Sort alphabetically (translated)
|
||||
const order = []
|
||||
for (const part of config.list) {
|
||||
const [ns, name] = part.split('.')
|
||||
order.push(t(`${ns}:${name}`) + `|${part}`)
|
||||
}
|
||||
order.sort()
|
||||
config.list = order.map((entry) => entry.split('|')[1])
|
||||
|
||||
return <Swizzled.components.MenuListInput {...props} />
|
||||
}
|
||||
|
||||
export const MenuUxSettingInput = (props) => {
|
||||
const { state, update, Swizzled } = props
|
||||
|
||||
return (
|
||||
<Swizzled.components.MenuListInput {...props} updateHandler={update.ui} current={state.ui.ux} />
|
||||
)
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
import { mergeOptions } from '@freesewing/core'
|
||||
|
||||
/** Displays that constant values are not implemented in the front end */
|
||||
export const MenuConstantOptionValue = () => (
|
||||
<span className="text-error">FIXME: No ConstantOptionvalue implemented</span>
|
||||
)
|
||||
|
||||
/** Displays a count value*/
|
||||
export const MenuCountOptionValue = ({ Swizzled, config, current, changed }) => (
|
||||
<Swizzled.components.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}°
|
||||
</Swizzled.components.MenuHighlightValue>
|
||||
)
|
||||
|
||||
/**
|
||||
* A component to highlight a changed value
|
||||
* @param {Boolean} changed - Whether the value is changed or not
|
||||
* @param {Function} children - The React children
|
||||
*/
|
||||
export const MenuHighlightValue = ({ changed, children }) => (
|
||||
<span className={changed ? 'text-accent' : ''}> {children} </span>
|
||||
)
|
||||
|
||||
/** Displays a list option value */
|
||||
export const MenuListOptionValue = (props) => (
|
||||
<MenuListValue
|
||||
{...props}
|
||||
t={(input) => props.Swizzled.methods.t(`${props.design}:${props.config.name}.${input}.t`)}
|
||||
/>
|
||||
)
|
||||
|
||||
/**
|
||||
* Displays the correct, translated value for a list
|
||||
* @param {String|Boolean} options.current the current value, if it has been changed
|
||||
* @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 }) => {
|
||||
// get the values
|
||||
const val = changed ? current : config.dflt
|
||||
|
||||
// key will be based on a few factors
|
||||
let key
|
||||
// are valueTitles configured?
|
||||
if (config.valueTitles) key = config.valueTitles[val]
|
||||
// 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 />
|
||||
|
||||
const translated =
|
||||
config.doNotTranslate || typeof key !== 'string' ? key : Swizzled.methods.t(key)
|
||||
|
||||
return (
|
||||
<Swizzled.components.MenuHighlightValue changed={changed}>
|
||||
{translated}
|
||||
</Swizzled.components.MenuHighlightValue>
|
||||
)
|
||||
}
|
||||
|
||||
/** Displays the corrent, translated value for a boolean */
|
||||
export const MenuBoolValue = MenuListOptionValue
|
||||
|
||||
/** Displays the MmOptions are not supported */
|
||||
export const MenuMmOptionValue = () => (
|
||||
<span className="text-error">FIXME: No Mm Options are not supported</span>
|
||||
)
|
||||
|
||||
/** Displays a formated mm value based on the current units */
|
||||
export const MenuMmValue = ({ current, config, units, changed, Swizzled }) => (
|
||||
<Swizzled.components.MenuHighlightValue changed={changed}>
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: Swizzled.methods.formatMm(changed ? current : config.dflt, units),
|
||||
}}
|
||||
/>
|
||||
</Swizzled.components.MenuHighlightValue>
|
||||
)
|
||||
|
||||
/** Displays the current percentage value, and the absolute value if configured
|
||||
*
|
||||
**************************************************************************
|
||||
* SliderIcon Title THIS *
|
||||
* min% max% *
|
||||
* ----------------------0----------------------------------------------- *
|
||||
* msg PencilIcon ResetIcon *
|
||||
**************************************************************************
|
||||
* */
|
||||
export const MenuPctOptionValue = ({
|
||||
config,
|
||||
current,
|
||||
settings,
|
||||
changed,
|
||||
patternConfig,
|
||||
Swizzled,
|
||||
}) => {
|
||||
const val = changed ? current : config.pct / 100
|
||||
|
||||
return (
|
||||
<Swizzled.components.MenuHighlightValue changed={changed}>
|
||||
{Swizzled.methods.formatPercentage(val)}
|
||||
{config.toAbs && settings?.measurements
|
||||
? ` | ${Swizzled.methods.formatMm(
|
||||
config.toAbs(val, settings, mergeOptions(settings, patternConfig.options))
|
||||
)}`
|
||||
: null}
|
||||
</Swizzled.components.MenuHighlightValue>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A component to display a value, highligting it if it changed
|
||||
* @param {Number|String|Boolean} options.current - The current value, if it has been changed
|
||||
* @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
|
||||
|
||||
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 MenuOnlySettingValue = ({ Swizzled, current, config }) => (
|
||||
<Swizzled.components.MenuHighlightValue
|
||||
current={current?.length}
|
||||
dflt={config.parts.length}
|
||||
changed={current !== undefined}
|
||||
/>
|
||||
)
|
|
@ -0,0 +1,60 @@
|
|||
export const UiPreferencesMenu = ({ Swizzled, update, state, Design }) => {
|
||||
const structure = Swizzled.methods.menuUiPreferencesStructure()
|
||||
|
||||
const drillProps = { Design, state, update }
|
||||
const inputs = {
|
||||
ux: (props) => <Swizzled.components.MenuUxSettingInput {...drillProps} {...props} />,
|
||||
aside: (props) => <Swizzled.components.MenuListInput {...drillProps} {...props} />,
|
||||
kiosk: (props) => <Swizzled.components.MenuListInput {...drillProps} {...props} />,
|
||||
rotate: (props) => <Swizzled.components.MenuListInput {...drillProps} {...props} />,
|
||||
renderer: (props) => <Swizzled.components.MenuListInput {...drillProps} {...props} />,
|
||||
}
|
||||
const values = {
|
||||
ux: (props) => <Swizzled.components.Ux ux={state.ui.ux} {...props} />,
|
||||
aside: Swizzled.components.MenuListValue,
|
||||
kiosk: Swizzled.components.MenuListValue,
|
||||
rotate: Swizzled.components.MenuListValue,
|
||||
renderer: Swizzled.components.MenuListValue,
|
||||
}
|
||||
|
||||
return (
|
||||
<Swizzled.components.MenuItemGroup
|
||||
{...{
|
||||
structure,
|
||||
ux: state.ui?.ux,
|
||||
currentValues: state.ui || {},
|
||||
Item: (props) => (
|
||||
<Swizzled.components.UiPreference
|
||||
updateHandler={update}
|
||||
{...{ inputs, values, Swizzled, Design }}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
isFirst: true,
|
||||
name: 'pe:uiPreferences',
|
||||
language: state.locale,
|
||||
passProps: {
|
||||
ux: state.ui?.ux,
|
||||
settings: state.settings,
|
||||
patternConfig: Design.patternConfig,
|
||||
},
|
||||
updateHandler: update.ui,
|
||||
isDesignOptionsGroup: false,
|
||||
Swizzled,
|
||||
state,
|
||||
Design,
|
||||
inputs,
|
||||
values,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const UiPreference = ({ Swizzled, name, ux, ...rest }) => (
|
||||
<Swizzled.components.MenuItem
|
||||
{...rest}
|
||||
name={name}
|
||||
allowToggle={!['ux', 'view'].includes(name) && ux > 3}
|
||||
ux={ux}
|
||||
/>
|
||||
)
|
|
@ -0,0 +1,5 @@
|
|||
/*
|
||||
* When debugging, you can override this component to catch errors
|
||||
* as it is typically used when a component can't be found.
|
||||
*/
|
||||
export const Null = () => null
|
|
@ -0,0 +1,34 @@
|
|||
/**
|
||||
* A layout for views that include a drafted pattern
|
||||
*
|
||||
* @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
|
||||
|
||||
return (
|
||||
<Swizzled.components.ZoomContextProvider>
|
||||
<div className="flex flex-col h-full">
|
||||
<Swizzled.components.HeaderMenu
|
||||
state={props.state}
|
||||
{...{ Swizzled, update, Design, pattern }}
|
||||
/>
|
||||
<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 ? (
|
||||
<div
|
||||
className={`hidden xl:block w-1/3 shrink grow-0 lg:p-4 max-w-2xl h-full overflow-scroll`}
|
||||
>
|
||||
{menu}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</Swizzled.components.ZoomContextProvider>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
import { useState } from 'react'
|
||||
|
||||
const colors = {
|
||||
comment: 'secondary',
|
||||
note: 'primary',
|
||||
tip: 'accent',
|
||||
warning: 'error',
|
||||
error: 'error',
|
||||
fixme: 'warning',
|
||||
link: 'secondary',
|
||||
related: 'info',
|
||||
tldr: 'info',
|
||||
none: '',
|
||||
}
|
||||
|
||||
/**
|
||||
* This popout component is a way to make some content stand out
|
||||
*
|
||||
* @param {object} components - Object holding possibly swizzled components
|
||||
* @param {object} methods - Object holding possibly swizzled methods
|
||||
*/
|
||||
export const Popout = (props) => {
|
||||
/*
|
||||
* Load (swizzled) components
|
||||
*/
|
||||
const { CloseIcon } = props.Swizzled.components
|
||||
|
||||
/*
|
||||
* Load (swizzled) methods
|
||||
*/
|
||||
const { t } = props.Swizzled.methods
|
||||
|
||||
const [hide, setHide] = useState(false)
|
||||
if (hide) return null
|
||||
|
||||
let type = 'none'
|
||||
for (const t in colors) {
|
||||
if (props[t]) type = t
|
||||
}
|
||||
const color = colors[type]
|
||||
const { className = '' } = props
|
||||
|
||||
return props.compact ? (
|
||||
<div
|
||||
className={`relative ${
|
||||
props.dense ? 'my-1' : 'my-8'
|
||||
} bg-${color} bg-opacity-5 -ml-4 -mr-4 sm:ml-0 sm:mr-0 ${className}`}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
border-y-4 sm:border-0 sm:border-l-4 px-4
|
||||
shadow text-base border-${color}
|
||||
flex flex-row items-center
|
||||
`}
|
||||
>
|
||||
<div className={`font-bold uppercase text-${color}`}>
|
||||
{props.title || (
|
||||
<>
|
||||
<span>{t(`pe:${type}`).toUpperCase()}</span>
|
||||
<span className="px-3">|</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="popout-content">{props.noP ? props.children : <p>{props.children}</p>}</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={`relative my-8 bg-${color} bg-opacity-5 -ml-4 -mr-4 sm:ml-0 sm:mr-0 ${className}`}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
border-y-4 sm:border-0 sm:border-l-4 px-6 sm:px-8 py-4 sm:py-2
|
||||
shadow text-base border-${color}
|
||||
`}
|
||||
>
|
||||
<div className={`font-bold flex flex-row gap-1 items-end justify-between`}>
|
||||
<div>
|
||||
<span className={`font-bold uppercase text-${color}`}>
|
||||
{type === 'tldr' ? 'TL;DR' : t(`pe:${type}`).toUpperCase()}
|
||||
</span>
|
||||
<span className={`font-normal text-base text-${color}`}>
|
||||
{type === 'comment' && (
|
||||
<>
|
||||
{' '}
|
||||
by <b>{props.by}</b>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{props.hideable && (
|
||||
<button onClick={() => setHide(true)} className="hover:text-secondary" title="Close">
|
||||
<CloseIcon />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="py-1 first:mt-0 popout-content">{props.children}</div>
|
||||
{type === 'comment' && <div className={`font-bold italic text-${color}`}>{props.by}</div>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,214 @@
|
|||
import { useState } from 'react'
|
||||
import yaml from 'js-yaml'
|
||||
|
||||
export const SaveView = ({ Swizzled, state, update }) => {
|
||||
// Hooks
|
||||
const backend = Swizzled.hooks.useBackend()
|
||||
const { t } = Swizzled.methods
|
||||
|
||||
// State
|
||||
const [name, setName] = useState(
|
||||
`${Swizzled.methods.capitalize(state.design)} / ${Swizzled.methods.shortDate(status.locale)}`
|
||||
)
|
||||
const [withNotes, setWithNotes] = useState(false)
|
||||
const [notes, setNotes] = useState('')
|
||||
const [savedId, setSavedId] = useState()
|
||||
const [bookmarkedId] = useState() // FIXME
|
||||
const [saveAs] = useState(false) // FIXME
|
||||
|
||||
const addSettingsToNotes = () => {
|
||||
setNotes(
|
||||
notes +
|
||||
'\n\n#### ' +
|
||||
Swizzled.methods.t('pe:settings') +
|
||||
'\n\n' +
|
||||
'```yaml\n' +
|
||||
yaml.dump(state.settings) +
|
||||
'````'
|
||||
)
|
||||
}
|
||||
|
||||
const saveAsNewPattern = async () => {
|
||||
const loadingId = 'savePatternAs'
|
||||
update.startLoading(loadingId)
|
||||
const patternData = {
|
||||
design: state.design,
|
||||
name,
|
||||
public: false,
|
||||
settings: state.settings,
|
||||
data: {},
|
||||
}
|
||||
if (withNotes) patternData.notes = notes
|
||||
const result = await backend.createPattern(patternData)
|
||||
if (result.success) {
|
||||
const id = result.data.pattern.id
|
||||
update.stopLoading(loadingId)
|
||||
update.view('draft')
|
||||
update.notifySuccess(
|
||||
<span>
|
||||
{t('pe:patternSavedAs')}:{' '}
|
||||
<Swizzled.components.Link
|
||||
href={`/account/pattern?id=${id}`}
|
||||
className={`${Swizzled.config.classes.link} text-secondary-content`}
|
||||
>
|
||||
/account/pattern?id={id}
|
||||
</Swizzled.components.Link>
|
||||
</span>,
|
||||
id
|
||||
)
|
||||
} else update.notifyFailure('oops', loadingId)
|
||||
}
|
||||
|
||||
const savePattern = async () => {
|
||||
//setLoadingStatus([true, 'savingPattern'])
|
||||
const patternData = {
|
||||
design: state.design,
|
||||
settings: state.settings,
|
||||
name,
|
||||
public: false,
|
||||
data: {},
|
||||
}
|
||||
const result = await backend.updatePattern(saveAs.pattern, patternData)
|
||||
if (result.success) {
|
||||
//setLoadingStatus([
|
||||
// true,
|
||||
// <>
|
||||
// {t('status:patternSaved')} <small>[#{saveAs.pattern}]</small>
|
||||
// </>,
|
||||
// true,
|
||||
// true,
|
||||
//])
|
||||
setSavedId(saveAs.pattern)
|
||||
update.notify({ color: 'success', msg: 'boom' }, saveAs.pattern)
|
||||
} //else setLoadingStatus([true, 'backendError', true, false])
|
||||
}
|
||||
|
||||
//const bookmarkPattern = async () => {
|
||||
// setLoadingStatus([true, 'creatingBookmark'])
|
||||
// const result = await backend.createBookmark({
|
||||
// type: 'pattern',
|
||||
// title: name,
|
||||
// url: window.location.pathname + window.location.search + window.location.hash,
|
||||
// })
|
||||
// if (result.success) {
|
||||
// const id = result.data.bookmark.id
|
||||
// setLoadingStatus([
|
||||
// true,
|
||||
// <>
|
||||
// {t('status:bookmarkCreated')} <small>[#{id}]</small>
|
||||
// </>,
|
||||
// true,
|
||||
// true,
|
||||
// ])
|
||||
// setBookmarkedId(id)
|
||||
// } else setLoadingStatus([true, 'backendError', true, false])
|
||||
//}
|
||||
|
||||
return (
|
||||
<Swizzled.components.AuthWrapper>
|
||||
<Swizzled.components.HeaderMenu {...{ state, update }} />
|
||||
<div className="m-auto mt-8 max-w-2xl px-4">
|
||||
{saveAs && saveAs.pattern ? (
|
||||
<>
|
||||
<h2>{t('pe:savePattern')}</h2>
|
||||
{savedId && (
|
||||
<Swizzled.components.Popout link>
|
||||
<h5>{t('workbend:patternSaved')}</h5>
|
||||
{t('pe:see')}:{' '}
|
||||
<Swizzled.components.PageLink
|
||||
href={`/account/patterns/${savedId}`}
|
||||
txt={`/account/patterns/${savedId}`}
|
||||
/>
|
||||
</Swizzled.components.Popout>
|
||||
)}
|
||||
<button
|
||||
className={`${Swizzled.config.classeshorFlexNoSm} btn btn-primary btn-lg w-full mt-2 my-8`}
|
||||
onClick={savePattern}
|
||||
>
|
||||
<Swizzled.components.SaveIcon className="h-8 w-8" />
|
||||
{t('pe:savePattern')} #{saveAs.pattern}
|
||||
</button>
|
||||
</>
|
||||
) : null}
|
||||
<h2>{t('pe:saveAsNewPattern')}</h2>
|
||||
{bookmarkedId && (
|
||||
<Swizzled.components.Popout link>
|
||||
<h5>{t('pe:patternBookmarkCreated')}</h5>
|
||||
{t('pe:see')}:{' '}
|
||||
<Swizzled.components.PageLink
|
||||
href={`/account/bookmarks/${bookmarkedId}`}
|
||||
txt={`/account/bookmarks/${bookmarkedId}`}
|
||||
/>
|
||||
</Swizzled.components.Popout>
|
||||
)}
|
||||
<div className="mb-4">
|
||||
<Swizzled.components.StringInput
|
||||
label={t('pe:patternTitle')}
|
||||
current={name}
|
||||
update={setName}
|
||||
valid={Swizzled.methods.notEmpty}
|
||||
labelBR={
|
||||
<>
|
||||
{withNotes ? (
|
||||
<div className="flex flex-row items-center gap-4">
|
||||
<button
|
||||
className={`font-bold ${Swizzled.config.classes.link}`}
|
||||
onClick={() => setWithNotes(false)}
|
||||
>
|
||||
{t('pe:hideNotes')}
|
||||
</button>
|
||||
<button
|
||||
className={`font-bold ${Swizzled.config.classes.link}`}
|
||||
onClick={addSettingsToNotes}
|
||||
>
|
||||
{t('pe:addSettingsToNotes')}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
className={`font-bold ${Swizzled.config.classes.link}`}
|
||||
onClick={() => setWithNotes(true)}
|
||||
>
|
||||
{t('pe:addNotes')}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
{withNotes ? (
|
||||
<Swizzled.components.MarkdownInput
|
||||
label={t('pe:patternNotes')}
|
||||
current={notes}
|
||||
update={setNotes}
|
||||
/>
|
||||
) : null}
|
||||
<div className="flex flex-row gap-2 mt-8">
|
||||
<button
|
||||
className={`btn btn-primary btn-lg btn-outline`}
|
||||
onClick={update.viewBack}
|
||||
title={t('pe:cancel')}
|
||||
>
|
||||
<span>{t('pe:cancel')}</span>
|
||||
</button>
|
||||
<button
|
||||
className={`flex flex-row items-center justify-between btn btn-primary btn-lg grow`}
|
||||
onClick={saveAsNewPattern}
|
||||
title={t('pe:saveAsNewPattern')}
|
||||
>
|
||||
<Swizzled.components.SaveAsIcon className="w-8 h-8" />
|
||||
<span>{t('pe:saveAsNewPattern')}</span>
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-sm text-right">
|
||||
To access your saved patterns, go to:{' '}
|
||||
<b>
|
||||
<Swizzled.components.PageLink href="//account/patterns">
|
||||
/account/patterns
|
||||
</Swizzled.components.PageLink>
|
||||
</b>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Swizzled.components.AuthWrapper>
|
||||
)
|
||||
}
|
225
packages/react-components/src/editor/swizzle/components/sets.mjs
Normal file
225
packages/react-components/src/editor/swizzle/components/sets.mjs
Normal file
|
@ -0,0 +1,225 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import orderBy from 'lodash.orderby'
|
||||
|
||||
export const UserSetPicker = ({
|
||||
Design,
|
||||
clickHandler,
|
||||
missingClickHandler,
|
||||
size = 'lg',
|
||||
Swizzled,
|
||||
}) => {
|
||||
// Swizzled components
|
||||
const { Popout, MeasurementsSetCard } = Swizzled.components
|
||||
// Swizzled hooks
|
||||
const { useBackend } = Swizzled.hooks
|
||||
const backend = useBackend()
|
||||
// Swizzled methods
|
||||
const { t, hasRequiredMeasurements } = Swizzled.methods
|
||||
// Swizzled config
|
||||
const { config } = Swizzled
|
||||
|
||||
// Local state
|
||||
const [sets, setSets] = useState({})
|
||||
|
||||
// Effects
|
||||
useEffect(() => {
|
||||
const getSets = async () => {
|
||||
const result = await backend.getSets()
|
||||
if (result.success) {
|
||||
const all = {}
|
||||
for (const set of result.data.sets) all[set.id] = set
|
||||
setSets(all)
|
||||
}
|
||||
}
|
||||
getSets()
|
||||
}, [backend])
|
||||
let hasSets = false
|
||||
const okSets = []
|
||||
const lackingSets = []
|
||||
if (Object.keys(sets).length > 0) {
|
||||
hasSets = true
|
||||
for (const setId in sets) {
|
||||
const [hasMeasies] = hasRequiredMeasurements(Design, sets[setId].measies)
|
||||
if (hasMeasies) okSets.push(sets[setId])
|
||||
else lackingSets.push(sets[setId])
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasSets)
|
||||
return (
|
||||
<div className="w-full max-w-3xl mx-auto">
|
||||
<Popout tip>
|
||||
<h5>{t('pe:noOwnSets')}</h5>
|
||||
<p className="">{t('pe:noOwnSetsMsg')}</p>
|
||||
{config.hrefNewSet ? (
|
||||
<a
|
||||
href={config.hrefNewSet}
|
||||
className="btn btn-accent capitalize"
|
||||
target="_BLANK"
|
||||
rel="nofollow"
|
||||
>
|
||||
{t('pe:newSet')}
|
||||
</a>
|
||||
) : null}
|
||||
<p className="text-sm">{t('pe:pleaseMtm')}</p>
|
||||
</Popout>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
{okSets.length > 0 && (
|
||||
<div className="flex flex-row flex-wrap gap-2">
|
||||
{okSets.map((set) => (
|
||||
<MeasurementsSetCard
|
||||
href={false}
|
||||
{...{ set, Design, config }}
|
||||
onClick={clickHandler}
|
||||
key={set.id}
|
||||
size={size}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{lackingSets.length > 0 ? (
|
||||
<div className="my-4">
|
||||
<Popout note compact>
|
||||
{t('pe:someSetsLacking')}
|
||||
</Popout>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 2xl:grid-cols-3 gap-2">
|
||||
{lackingSets.map((set) => (
|
||||
<MeasurementsSetCard
|
||||
{...{ set, Design }}
|
||||
onClick={missingClickHandler}
|
||||
href={false}
|
||||
key={set.id}
|
||||
size={size}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const BookmarkedSetPicker = ({
|
||||
Design,
|
||||
clickHandler,
|
||||
missingClickHandler,
|
||||
size = 'lg',
|
||||
Swizzled,
|
||||
}) => {
|
||||
// Swizzled components
|
||||
const { Popout, MeasurementsSetCard } = Swizzled.components
|
||||
// Swizzled hooks
|
||||
const { useBackend } = Swizzled.hooks
|
||||
const backend = useBackend()
|
||||
// Swizzled methods
|
||||
const { t, hasRequiredMeasurements } = Swizzled.methods
|
||||
// Swizzled config
|
||||
const { config } = Swizzled
|
||||
|
||||
// Local state
|
||||
const [sets, setSets] = useState({})
|
||||
|
||||
// Effects
|
||||
useEffect(() => {
|
||||
const getBookmarks = async () => {
|
||||
const result = await backend.getBookmarks()
|
||||
const loadedSets = {}
|
||||
if (result.success) {
|
||||
for (const bookmark of result.data.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 }
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
setSets(loadedSets)
|
||||
}
|
||||
getBookmarks()
|
||||
}, [])
|
||||
|
||||
const okSets = Object.values(sets).filter((set) => set.hasMeasies)
|
||||
const lackingSets = Object.values(sets).filter((set) => !set.hasMeasies)
|
||||
|
||||
return (
|
||||
<>
|
||||
{okSets.length > 0 && (
|
||||
<div className="flex flex-row flex-wrap gap-2">
|
||||
{okSets.map((set) => (
|
||||
<MeasurementsSetCard
|
||||
href={false}
|
||||
{...{ set, Design, config }}
|
||||
onClick={clickHandler}
|
||||
key={set.id}
|
||||
size={size}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{lackingSets.length > 0 && (
|
||||
<div className="my-4">
|
||||
<Popout note compact>
|
||||
{t('pe:someSetsLacking')}
|
||||
</Popout>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 2xl:grid-cols-3 gap-2">
|
||||
{lackingSets.map((set) => (
|
||||
<MeasurementsSetCard
|
||||
href={false}
|
||||
{...{ set, Design }}
|
||||
onClick={missingClickHandler}
|
||||
key={set.id}
|
||||
size={size}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const CuratedSetPicker = ({ clickHandler, Swizzled, locale }) => {
|
||||
// Swizzled components
|
||||
const { CuratedMeasurementsSetLineup } = Swizzled.components
|
||||
// Swizzled hooks
|
||||
const { useBackend } = Swizzled.hooks
|
||||
const backend = useBackend()
|
||||
|
||||
// Local state
|
||||
const [sets, setSets] = useState([])
|
||||
|
||||
// Effects
|
||||
useEffect(() => {
|
||||
const getSets = async () => {
|
||||
const result = await backend.getCuratedSets()
|
||||
if (result.success) {
|
||||
const allSets = {}
|
||||
for (const set of result.data.curatedSets) {
|
||||
if (set.published) allSets[set.id] = set
|
||||
}
|
||||
setSets(allSets)
|
||||
}
|
||||
}
|
||||
getSets()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl xl:pl-4">
|
||||
<CuratedMeasurementsSetLineup
|
||||
{...{ locale, clickHandler }}
|
||||
sets={orderBy(sets, 'height', 'asc')}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
export const Spinner = ({ className = 'h-6 w-6 animate-spin' }) => (
|
||||
<svg className={className} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
></circle>
|
||||
<path
|
||||
className="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>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const Loading = () => (
|
||||
<Spinner className="w-24 h-24 color-primary animate-spin m-auto mt-8" />
|
||||
)
|
|
@ -0,0 +1,66 @@
|
|||
import React, { useState } from 'react'
|
||||
|
||||
export const Tabs = ({ tabs = '', active = 0, children, withModal = false, Swizzled }) => {
|
||||
// Keep active tab in state
|
||||
const [activeTab, setActiveTab] = useState(active)
|
||||
|
||||
/*
|
||||
* In MDX, tabs are passed as a comma-seperated list.
|
||||
* In React, they come as an array.
|
||||
* So we need to handle both cases.
|
||||
*/
|
||||
const tablist = Array.isArray(tabs) ? tabs : tabs.split(',').map((tab) => tab.trim())
|
||||
|
||||
/*
|
||||
* Don't bother unless there's actual tabs to show
|
||||
*/
|
||||
if (!tablist) return null
|
||||
|
||||
/*
|
||||
* Pass down activeTab and tabId for conditional rendering
|
||||
*/
|
||||
const childrenWithTabSetter = children.map((child, tabId) =>
|
||||
React.cloneElement(child, { activeTab, tabId, key: tabId })
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="my-4">
|
||||
<div className="tabs">
|
||||
{tablist.map((title, tabId) => {
|
||||
const btnClasses = `text-lg font-bold capitalize tab h-auto tab-bordered grow py-2 ${
|
||||
activeTab === tabId ? 'tab-active' : ''
|
||||
}`
|
||||
|
||||
return withModal && activeTab === tabId ? (
|
||||
<button
|
||||
key={tabId}
|
||||
className={btnClasses}
|
||||
onClick={() =>
|
||||
Swizzled.methods.setModal(
|
||||
<Swizzled.components.ModalWrapper
|
||||
flex="col"
|
||||
justify="top lg:justify-center"
|
||||
slideFrom="right"
|
||||
fullWidth
|
||||
>
|
||||
{childrenWithTabSetter}
|
||||
</Swizzled.components.ModalWrapper>
|
||||
)
|
||||
}
|
||||
>
|
||||
<span className="pr-2">{title}</span>
|
||||
<Swizzled.components.KioskIcon className="w-6 h-6 hover:text-secondary" />
|
||||
</button>
|
||||
) : (
|
||||
<button key={tabId} className={btnClasses} onClick={() => setActiveTab(tabId)}>
|
||||
{title}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div>{childrenWithTabSetter}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Tab = ({ children, tabId, activeTab }) => (activeTab === tabId ? children : null)
|
|
@ -0,0 +1,13 @@
|
|||
export const Tooltip = (props) => {
|
||||
const { children, tip, ...rest } = props
|
||||
|
||||
return (
|
||||
<div
|
||||
{...rest}
|
||||
className={`tooltip tooltip-bottom before:bg-base-200 before:shadow before:text-base-content`}
|
||||
data-tip={tip}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
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>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
export const Ux = ({ Swizzled, ux = 0 }) => (
|
||||
<div className="flex flex-row">
|
||||
{[0, 1, 2, 3, 4].map((i) => (
|
||||
<Swizzled.components.CircleIcon
|
||||
key={i}
|
||||
fill={i < ux ? true : false}
|
||||
className={`w-6 h-6 ${i < ux ? 'stroke-secondary fill-secondary' : 'stroke-current'}`}
|
||||
fillOpacity={0.3}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
|
@ -0,0 +1,111 @@
|
|||
import { useState } from 'react'
|
||||
|
||||
/**
|
||||
* The design view is loaded if and only if not design is passed to the editor
|
||||
*
|
||||
* @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 ViewPicker = ({ Design, Swizzled, update, state }) => {
|
||||
const [showDev, setShowDev] = useState(false)
|
||||
|
||||
/*
|
||||
* If we don't have the measurments, only present measurements free views
|
||||
*/
|
||||
if (state._.missingMeasurements.length > 1)
|
||||
return (
|
||||
<div className="text-center mt-8 mb-24 px-4 max-w-xl mx-auto">
|
||||
<h2>{Swizzled.methods.t('Choose an activity')}</h2>
|
||||
<div className="flex flex-col mx-auto justify-center gap-2 mt-4">
|
||||
{Swizzled.config.measurementsFreeViews
|
||||
.filter((view) => view !== 'picker')
|
||||
.map((view) => (
|
||||
<MainCard key={view} {...{ view, update, Design, Swizzled }} />
|
||||
))}
|
||||
<Swizzled.components.Popout note>
|
||||
<div className="text-left">
|
||||
<h5>{Swizzled.methods.t('pe:measurementsFreeViewsOnly.t')}:</h5>
|
||||
<p>{Swizzled.methods.t('pe:measurementsFreeViewsOnly.d')}</p>
|
||||
</div>
|
||||
</Swizzled.components.Popout>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="text-center mt-8 mb-24 px-4">
|
||||
<h2>{Swizzled.methods.t('Choose an activity')}</h2>
|
||||
<div className="max-w-6xl grid grid-cols-1 lg:grid-cols-2 mx-auto justify-center gap-2 lg:gap-4 mt-4">
|
||||
{Swizzled.config.mainViews.map((view) => (
|
||||
<MainCard key={view} {...{ view, update, Design, Swizzled }} />
|
||||
))}
|
||||
</div>
|
||||
<div className="max-w-6xl grid grid-cols-1 lg:grid-cols-4 mx-auto justify-center gap-2 lg:gap-4 mt-4">
|
||||
{Swizzled.config.extraViews.map((view) => (
|
||||
<ExtraCard key={view} {...{ view, update, Swizzled }} />
|
||||
))}
|
||||
</div>
|
||||
{showDev || state.ui.ux > 3 ? (
|
||||
<div className="max-w-6xl grid grid-cols-1 lg:grid-cols-4 mx-auto justify-center gap-2 lg:gap-4 mt-4">
|
||||
{Swizzled.config.devViews.map((view) => (
|
||||
<ExtraCard key={view} {...{ view, update, Swizzled }} />
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
{state.ui.ux < 4 ? (
|
||||
<button className="btn btn-ghost mt-2" onClick={() => setShowDev(!showDev)}>
|
||||
{Swizzled.methods.t(`pe:${showDev ? 'hide' : 'show'}AdvancedOptions`)}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const MainCard = ({ view, Swizzled, update, Design }) => {
|
||||
const Icon = Swizzled.components[`View${Swizzled.methods.capitalize(view)}Icon`]
|
||||
const { NoIcon } = Swizzled.components
|
||||
const color = Swizzled.config.mainViewColors[view] || 'neutral'
|
||||
return (
|
||||
<button
|
||||
className={`border shadow p-4 rounded-lg w-full bg-${
|
||||
color === 'none' ? 'secondary' : color
|
||||
} ${
|
||||
color === 'none' ? 'bg-opacity-10 hover:bg-opacity-20' : 'hover:bg-opacity-90'
|
||||
} flex flex-col`}
|
||||
title={Swizzled.methods.t(`pe:view.${view}.t`)}
|
||||
onClick={() => update.view(view)}
|
||||
>
|
||||
<h4
|
||||
className={`flex flex-row items-center justify-between p-0 text-${color}-content mb-2 text-left`}
|
||||
>
|
||||
{Swizzled.methods.t(`pe:view.${view}.t`)}
|
||||
{Icon ? <Icon className="w-10 h-10" /> : <NoIcon className="w-10 h-10" />}
|
||||
</h4>
|
||||
<p className={`text-left text-lg m-0 p-0 text-${color}-content grow-2`}>
|
||||
{Swizzled.methods.t(`pe:view.${view}.d`, {
|
||||
design: `${Design.designConfig.data.name} v${Design.designConfig.data.version}`,
|
||||
})}
|
||||
</p>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
const ExtraCard = ({ view, Swizzled, update }) => {
|
||||
const Icon = Swizzled.components[`View${Swizzled.methods.capitalize(view)}Icon`]
|
||||
const { NoIcon } = Swizzled.components
|
||||
return (
|
||||
<button
|
||||
className="border shadow p-3 rounded-lg w-full hover:bg-secondary hover:bg-opacity-20 flex flex-col"
|
||||
title={Swizzled.methods.t(`pe:view.${view}.t`)}
|
||||
onClick={() => update.view(view)}
|
||||
>
|
||||
<h5 className="flex flex-row items-center justify-between p-0 mb-1 text-left">
|
||||
{Swizzled.methods.t(`pe:view.${view}.t`)}
|
||||
{Icon ? <Icon className="w-8 h-8" /> : <NoIcon className="w-8 h-8" />}
|
||||
</h5>
|
||||
<p className="text-left m-0 p-0 grow-2">{Swizzled.methods.t(`pe:view.${view}.d`)}</p>
|
||||
</button>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
import React, { useState, useMemo, useCallback, forwardRef, useContext } from 'react'
|
||||
import { TransformWrapper, TransformComponent } from 'react-zoom-pan-pinch'
|
||||
|
||||
/*
|
||||
* A pattern you can pan and zoom
|
||||
*/
|
||||
export const ZoomablePattern = forwardRef(function ZoomablePatternRef(props, ref) {
|
||||
const { renderProps, Swizzled, rotate } = props
|
||||
const { onTransformed, setZoomFunctions } = useContext(ZoomContext)
|
||||
|
||||
return (
|
||||
<TransformWrapper
|
||||
minScale={0.1}
|
||||
centerZoomedOut={true}
|
||||
wheel={{ activationKeys: ['Control'] }}
|
||||
doubleClick={{ mode: 'reset' }}
|
||||
onTransformed={onTransformed}
|
||||
onInit={setZoomFunctions}
|
||||
>
|
||||
<TransformComponent
|
||||
wrapperStyle={{ width: '100%', height: '100%' }}
|
||||
contentStyle={{ width: '100%', height: '100%' }}
|
||||
wrapperClass={'pan-zoom-pattern'}
|
||||
id="pan-zoom-pattern"
|
||||
>
|
||||
{props.children || (
|
||||
<Swizzled.components.Pattern
|
||||
{...{ renderProps }}
|
||||
t={Swizzled.methods.t}
|
||||
ref={ref}
|
||||
className={`freesewing pattern w-full ${rotate ? '-rotate-90' : ''}`}
|
||||
/>
|
||||
)}
|
||||
</TransformComponent>
|
||||
</TransformWrapper>
|
||||
)
|
||||
})
|
||||
|
||||
/*
|
||||
* Context
|
||||
*/
|
||||
|
||||
/**
|
||||
* A context for managing zoom state of a ZoomablePattern
|
||||
* Makes transform handlers to be available in components
|
||||
*/
|
||||
const ZoomContext = React.createContext({})
|
||||
|
||||
/**
|
||||
* Provider for the ZoomContext
|
||||
*/
|
||||
export const ZoomContextProvider = ({ children }) => {
|
||||
const [zoomed, setZoomed] = useState(false)
|
||||
const [zoomFunctions, _setZoomFunctions] = useState(false)
|
||||
|
||||
const setZoomFunctions = useCallback(
|
||||
(zoomInstance) => {
|
||||
const reset = () => {
|
||||
setZoomed(false)
|
||||
zoomInstance.resetTransform()
|
||||
}
|
||||
|
||||
if (zoomInstance) {
|
||||
const { zoomIn, zoomOut, resetTransform } = zoomInstance
|
||||
_setZoomFunctions({ zoomIn, zoomOut, resetTransform, reset })
|
||||
}
|
||||
},
|
||||
[_setZoomFunctions, setZoomed]
|
||||
)
|
||||
|
||||
const onTransformed = useCallback(
|
||||
(_ref, state) => {
|
||||
setZoomed(state.scale !== 1)
|
||||
},
|
||||
[setZoomed]
|
||||
)
|
||||
|
||||
const value = useMemo(() => {
|
||||
return {
|
||||
zoomed,
|
||||
zoomFunctions,
|
||||
setZoomFunctions,
|
||||
onTransformed,
|
||||
}
|
||||
}, [zoomed, zoomFunctions, setZoomFunctions, onTransformed])
|
||||
|
||||
return <ZoomContext.Provider value={value}>{children}</ZoomContext.Provider>
|
||||
}
|
198
packages/react-components/src/editor/swizzle/config.mjs
Normal file
198
packages/react-components/src/editor/swizzle/config.mjs
Normal file
|
@ -0,0 +1,198 @@
|
|||
/*************************************************************************
|
||||
* *
|
||||
* FreeSewing's pattern editor allows swizzling the config *
|
||||
* *
|
||||
* To 'swizzle' means to replace a default implementation with a *
|
||||
* custom one. It allows one to customize the pattern editor. *
|
||||
* *
|
||||
* This file holds the 'swizzleConfig' method that will return *
|
||||
* the merged configuration. *
|
||||
* *
|
||||
* To use a custom config, simply pas it as a prop into the editor *
|
||||
* under the 'config' key. So to pass a custom 'newSet' link (used to *
|
||||
* link to a page to create a new measurements set), you do: *
|
||||
* *
|
||||
* <PatternEditor config={{ newSet: '/my/custom/page' }} /> *
|
||||
* *
|
||||
*************************************************************************/
|
||||
|
||||
/*
|
||||
* Default config for the FreeSewing pattern editor
|
||||
*/
|
||||
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',
|
||||
// Cloud default image
|
||||
cloudImageDflt:
|
||||
'https://imagedelivery.net/ouSuR9yY1bHt-fuAokSA5Q/365cc64e-1502-4d2b-60e0-cc8beee73f00/public',
|
||||
// Cloud image base URL
|
||||
cloudImageUrl: 'https://imagedelivery.net/ouSuR9yY1bHt-fuAokSA5Q/',
|
||||
// Cloud image variants
|
||||
cloudImageVariants: ['public', 'sq100', 'sq200', 'sq500', 'w200', 'w500', 'w1000', 'w2000'],
|
||||
// Views
|
||||
mainViews: ['draft', 'designs', 'save', 'export'],
|
||||
extraViews: ['measurements', 'undos', 'printLayout', 'docs'],
|
||||
devViews: ['editSettings', 'inspect', 'logs', 'test', 'timing'],
|
||||
utilViews: ['picker'],
|
||||
measurementsFreeViews: ['designs', 'measurements', 'docs', 'picker'],
|
||||
mainViewColors: {
|
||||
draft: 'primary',
|
||||
save: 'none',
|
||||
export: 'none',
|
||||
designs: 'accent',
|
||||
measurements: 'primary',
|
||||
docs: 'secondary',
|
||||
},
|
||||
// View components
|
||||
// Facilitate lookup of view components
|
||||
viewComponents: {
|
||||
draft: 'DraftView',
|
||||
designs: 'DesignsView',
|
||||
save: 'SaveView',
|
||||
export: 'ViewPicker',
|
||||
measurements: 'MeasurementsView',
|
||||
undos: 'UndosView',
|
||||
printLayout: 'ViewPicker',
|
||||
editSettings: 'ViewPicker',
|
||||
docs: 'ViewPicker',
|
||||
inspect: 'ViewPicker',
|
||||
logs: 'ViewPicker',
|
||||
test: 'ViewPicker',
|
||||
timing: 'ViewPicker',
|
||||
picker: 'ViewPicker',
|
||||
error: 'ViewPicker',
|
||||
},
|
||||
// Facilitate lookup of menu value components
|
||||
menuValueComponents: {
|
||||
bool: 'BoolValue',
|
||||
constant: 'ConstantOptionValue',
|
||||
count: 'CountOptionValue',
|
||||
deg: 'DegOptionValue',
|
||||
list: 'ListOptionValue',
|
||||
mm: 'MmOptionValue',
|
||||
pct: 'PctOptionValue',
|
||||
},
|
||||
// Facilitate custom handlers for core settings
|
||||
menuCoreSettingsHandlerMethods: {
|
||||
only: 'menuCoreSettingsOnlyHandler',
|
||||
sabool: 'menuCoreSettingsSaboolHandler',
|
||||
samm: 'menuCoreSettingsSammHandler',
|
||||
},
|
||||
menuGroupEmojis: {
|
||||
advanced: '🤓',
|
||||
fit: '👕',
|
||||
style: '💃🏽',
|
||||
dflt: '🕹️',
|
||||
groupDflt: '📁',
|
||||
},
|
||||
menuOptionEditLabels: {
|
||||
pct: '%',
|
||||
count: '#',
|
||||
deg: '°',
|
||||
mm: 'mm',
|
||||
},
|
||||
// i18n
|
||||
i18nPatternNamespaces: ['plugin-annotations'],
|
||||
// State backend (one of 'react', 'storage', 'session', or 'url')
|
||||
stateBackend: 'url',
|
||||
// UX levels
|
||||
uxLevels: {
|
||||
core: {
|
||||
sa: 1,
|
||||
paperless: 2,
|
||||
units: 1,
|
||||
complete: 3,
|
||||
expand: 4,
|
||||
only: 4,
|
||||
scale: 3,
|
||||
margin: 3,
|
||||
},
|
||||
ui: {
|
||||
renderer: 4,
|
||||
aside: 3,
|
||||
kiosk: 3,
|
||||
rotate: 4,
|
||||
ux: 1,
|
||||
},
|
||||
views: {
|
||||
draft: 1,
|
||||
measurements: 1,
|
||||
test: 4,
|
||||
timing: 4,
|
||||
printLayout: 2,
|
||||
export: 1,
|
||||
save: 1,
|
||||
undos: 2,
|
||||
editSettings: 3,
|
||||
logs: 4,
|
||||
inspect: 4,
|
||||
docs: 1,
|
||||
designs: 1,
|
||||
picker: 1,
|
||||
},
|
||||
},
|
||||
defaultUx: 4,
|
||||
// Flag types
|
||||
flagTypes: ['error', 'warn', 'note', 'info', 'tip', 'fixme'],
|
||||
// Initial state
|
||||
initialState: {
|
||||
settings: {},
|
||||
ui: {
|
||||
renderer: 'react',
|
||||
kiosk: 0,
|
||||
aside: 1,
|
||||
ux: 4,
|
||||
},
|
||||
locale: 'en',
|
||||
},
|
||||
loadingStatus: {
|
||||
timeout: 2,
|
||||
defaults: {
|
||||
color: 'secondary',
|
||||
icon: 'Spinner',
|
||||
},
|
||||
},
|
||||
classes: {
|
||||
horFlex: 'flex flex-row items-center justify-between gap-4 w-full',
|
||||
horFlexNoSm: 'md:flex md:flex-row md:items-center md:justify-between md:gap-4 md-w-full',
|
||||
link: 'underline decoration-2 hover:decoration-4 text-secondary hover:text-secondary-focus',
|
||||
},
|
||||
roles: {
|
||||
levels: {
|
||||
readNone: 0,
|
||||
readSome: 1,
|
||||
readOnly: 2,
|
||||
writeSome: 3,
|
||||
user: 4,
|
||||
tester: 4,
|
||||
curator: 5,
|
||||
bughunter: 6,
|
||||
support: 8,
|
||||
admin: 9,
|
||||
},
|
||||
base: 'user',
|
||||
},
|
||||
// Ms before to fade out a notification
|
||||
notifyTimeout: 6660,
|
||||
degreeMeasurements: ['shoulderSlope'],
|
||||
}
|
||||
|
||||
/*
|
||||
* This method returns the swizzled configuration
|
||||
*/
|
||||
export const swizzleConfig = (config = {}) => {
|
||||
const mergedConfig = {
|
||||
...defaultConfig,
|
||||
...config,
|
||||
}
|
||||
mergedConfig.views = [
|
||||
...mergedConfig.mainViews,
|
||||
...mergedConfig.extraViews,
|
||||
...mergedConfig.devViews,
|
||||
...mergedConfig.utilViews,
|
||||
]
|
||||
|
||||
return mergedConfig
|
||||
}
|
73
packages/react-components/src/editor/swizzle/hooks/index.mjs
Normal file
73
packages/react-components/src/editor/swizzle/hooks/index.mjs
Normal file
|
@ -0,0 +1,73 @@
|
|||
/*************************************************************************
|
||||
* *
|
||||
* FreeSewing's pattern editor allows swizzling hooks *
|
||||
* *
|
||||
* To 'swizzle' means to replace the default implementation of a *
|
||||
* hook with a custom one. It allows one to customize *
|
||||
* the pattern editor. *
|
||||
* *
|
||||
* This file holds the 'swizzleHooks' method that will return *
|
||||
* the various hooks that can be swizzled, or their default *
|
||||
* implementation. *
|
||||
* *
|
||||
* To use a custom version, simply pas it as a prop into the editor *
|
||||
* under the 'hooks' key. So to pass a custom 'useAccount' method *
|
||||
* (used for loading the user's account data) you do: *
|
||||
* *
|
||||
* <PatternEditor hooks={{ useAccount: myCustomHook }} /> *
|
||||
* *
|
||||
*************************************************************************/
|
||||
|
||||
/*
|
||||
* Import of components that can be swizzled
|
||||
*/
|
||||
// useAccount
|
||||
import { useAccount } from './use-account.mjs'
|
||||
import { useBackend } from './use-backend.mjs'
|
||||
import {
|
||||
useReactEditorState,
|
||||
useStorageEditorState,
|
||||
useSessionEditorState,
|
||||
useUrlEditorState,
|
||||
} from './use-editor-state.mjs'
|
||||
|
||||
/*
|
||||
* We support different state backend, so let's handle those
|
||||
*/
|
||||
const stateBackends = {
|
||||
react: useReactEditorState,
|
||||
storage: useStorageEditorState,
|
||||
session: useSessionEditorState,
|
||||
url: useUrlEditorState,
|
||||
}
|
||||
|
||||
/**
|
||||
* This object holds all hooks that can be swizzled
|
||||
*/
|
||||
const defaultHooks = (config) => ({
|
||||
useAccount,
|
||||
useBackend,
|
||||
useEditorState: stateBackends[config.stateBackend] || useReactEditorState,
|
||||
})
|
||||
|
||||
/*
|
||||
* This method returns hooks that can be swizzled
|
||||
* So either the passed-in methods, or the default ones
|
||||
*/
|
||||
export const swizzleHooks = (hooks = {}, Swizzled) => {
|
||||
/*
|
||||
* We need to return the resulting hooks, swizzled or not
|
||||
* So we put this in this object so we can pass that down
|
||||
*/
|
||||
const all = {}
|
||||
for (const [name, hook] of Object.entries(defaultHooks(Swizzled.config))) {
|
||||
all[name] = hooks[name]
|
||||
? (...params) => hooks[name](Swizzled, ...params)
|
||||
: (...params) => hook(Swizzled, ...params)
|
||||
}
|
||||
|
||||
/*
|
||||
* Return all hooks
|
||||
*/
|
||||
return all
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
import useLocalStorageState from 'use-local-storage-state'
|
||||
|
||||
/*
|
||||
* Make it possible to always check for account.username and account.ux
|
||||
*/
|
||||
const noAccount = { username: false, ux: 3 }
|
||||
|
||||
/*
|
||||
* The useAccount hook
|
||||
*/
|
||||
export function useAccount(Swizzled) {
|
||||
// (persisted) State (saved to local storage)
|
||||
const [account, setAccount] = useLocalStorageState('fs-account', { defaultValue: noAccount })
|
||||
const [admin, setAdmin] = useLocalStorageState('fs-admin', { defaultValue: noAccount })
|
||||
const [token, setToken] = useLocalStorageState('fs-token', { defaultValue: null })
|
||||
const [seenUser, setSeenUser] = useLocalStorageState('fs-seen-user', { defaultValue: false })
|
||||
|
||||
// Clear user data. This gets called when signing out
|
||||
const signOut = () => {
|
||||
setAccount(noAccount)
|
||||
setToken(null)
|
||||
}
|
||||
|
||||
// Impersonate a user.
|
||||
// Only admins can do this but that is enforced at the backend.
|
||||
const impersonate = (data) => {
|
||||
setAdmin({ token, account })
|
||||
const newAccount = {
|
||||
...data.account,
|
||||
impersonatingAdmin: { id: account.id, username: account.username },
|
||||
}
|
||||
setAdmin({ token, account: { ...account } })
|
||||
setAccount(newAccount)
|
||||
setToken(data.token)
|
||||
}
|
||||
|
||||
const stopImpersonating = () => {
|
||||
setAccount(admin.account)
|
||||
setToken(admin.token)
|
||||
clearAdmin()
|
||||
}
|
||||
|
||||
const clearAdmin = () => setAdmin(noAccount)
|
||||
|
||||
return {
|
||||
account,
|
||||
setAccount,
|
||||
token,
|
||||
setToken,
|
||||
seenUser,
|
||||
setSeenUser,
|
||||
signOut,
|
||||
admin,
|
||||
clearAdmin,
|
||||
impersonate,
|
||||
stopImpersonating,
|
||||
ux: account?.control || account?.ux || Swizzled.config.defaultUx,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,633 @@
|
|||
// Dependencies
|
||||
import axios from 'axios'
|
||||
// Hooks
|
||||
import { useMemo } from 'react'
|
||||
import { freeSewingConfig } from 'shared/config/freesewing.config.mjs'
|
||||
|
||||
/*
|
||||
* Helper methods to interact with the FreeSewing backend
|
||||
*/
|
||||
const apiHandler = axios.create({
|
||||
baseURL: freeSewingConfig.backend,
|
||||
timeout: 6660,
|
||||
})
|
||||
|
||||
const auth = (token) =>
|
||||
token ? { headers: { Authorization: 'Bearer ' + token } } : { headers: {} }
|
||||
|
||||
/*
|
||||
* This api object handles async code for different HTTP methods
|
||||
*/
|
||||
const api = {
|
||||
get: async (uri, config = {}) => {
|
||||
let result
|
||||
try {
|
||||
result = await apiHandler.get(uri, config)
|
||||
return result
|
||||
} catch (err) {
|
||||
return err
|
||||
}
|
||||
},
|
||||
post: async (uri, data = null, config = {}) => {
|
||||
let result
|
||||
try {
|
||||
result = await apiHandler.post(uri, data, config)
|
||||
return result
|
||||
} catch (err) {
|
||||
return err
|
||||
}
|
||||
},
|
||||
put: async (uri, data = null, config = {}) => {
|
||||
let result
|
||||
try {
|
||||
result = await apiHandler.put(uri, data, config)
|
||||
return result
|
||||
} catch (err) {
|
||||
return err
|
||||
}
|
||||
},
|
||||
patch: async (uri, data = null, config = {}) => {
|
||||
let result
|
||||
try {
|
||||
result = await apiHandler.patch(uri, data, config)
|
||||
return result
|
||||
} catch (err) {
|
||||
return err
|
||||
}
|
||||
},
|
||||
delete: async (uri, config = {}) => {
|
||||
let result
|
||||
try {
|
||||
result = await apiHandler.delete(uri, config)
|
||||
return result
|
||||
} catch (err) {
|
||||
return err
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
/*
|
||||
* Helper method to handle the response and verify that it was successful
|
||||
*/
|
||||
const responseHandler = (response, expectedStatus = 200, expectData = true) => {
|
||||
if (response && response.status === expectedStatus) {
|
||||
if (!expectData || response.data) {
|
||||
return { success: true, data: response.data, response }
|
||||
}
|
||||
return { success: true, response }
|
||||
}
|
||||
|
||||
// Unpack axios errors
|
||||
if (response?.name === 'AxiosError')
|
||||
return {
|
||||
success: false,
|
||||
status: response.response?.status,
|
||||
data: response.response?.data,
|
||||
error: response.message,
|
||||
}
|
||||
|
||||
return { success: false, response }
|
||||
}
|
||||
|
||||
function Backend(auth) {
|
||||
this.auth = auth
|
||||
}
|
||||
|
||||
/*
|
||||
* backend.signUp: User signup
|
||||
*/
|
||||
Backend.prototype.signUp = async function ({ email, language }) {
|
||||
return responseHandler(await api.post('/signup', { email, language }), 201)
|
||||
}
|
||||
|
||||
/*
|
||||
* backend.oauthInit: Init Oauth flow for oauth provider
|
||||
*/
|
||||
Backend.prototype.oauthInit = async function ({ provider, language }) {
|
||||
return responseHandler(await api.post('/signin/oauth/init', { provider, language }))
|
||||
}
|
||||
|
||||
/*
|
||||
* backend.oauthSignIn: User sign in via oauth provider
|
||||
*/
|
||||
Backend.prototype.oauthSignIn = async function ({ state, code, provider }) {
|
||||
return responseHandler(await api.post('/signin/oauth', { state, code, provider }))
|
||||
}
|
||||
|
||||
/*
|
||||
* Backend.prototype.loadConfirmation: Load a confirmation
|
||||
*/
|
||||
Backend.prototype.loadConfirmation = async function ({ id, check }) {
|
||||
return responseHandler(await api.get(`/confirmations/${id}/${check}`))
|
||||
}
|
||||
/*
|
||||
* Backend.prototype.confirmSignup: Confirm a signup
|
||||
*/
|
||||
Backend.prototype.confirmSignup = async function ({ id, consent }) {
|
||||
return responseHandler(await api.post(`/confirm/signup/${id}`, { consent }))
|
||||
}
|
||||
|
||||
/*
|
||||
* Backend.prototype.signIn: User signin/login
|
||||
*/
|
||||
Backend.prototype.signIn = async function ({ username, password = false, token = false }) {
|
||||
return password === false
|
||||
? responseHandler(await api.post('/signinlink', { username }))
|
||||
: responseHandler(await api.post('/signin', { username, password, token }))
|
||||
}
|
||||
|
||||
/*
|
||||
* Backend.prototype.signInFromLink: Trade in sign-in link id & check for JWT token
|
||||
*/
|
||||
Backend.prototype.signInFromLink = async function ({ id, check }) {
|
||||
return responseHandler(await api.post(`/signinlink/${id}/${check}`))
|
||||
}
|
||||
|
||||
/*
|
||||
* Generic update account method
|
||||
*/
|
||||
Backend.prototype.updateAccount = async function (data) {
|
||||
return responseHandler(await api.patch(`/account/jwt`, data, this.auth))
|
||||
}
|
||||
|
||||
/*
|
||||
* Update consent (uses the jwt-guest middleware)
|
||||
*/
|
||||
Backend.prototype.updateConsent = async function (consent) {
|
||||
return responseHandler(await api.patch(`/consent/jwt`, { consent }, this.auth))
|
||||
}
|
||||
|
||||
/*
|
||||
* Checks whether a username is available
|
||||
*/
|
||||
Backend.prototype.isUsernameAvailable = async function (username) {
|
||||
const response = await api.post(`/available/username/jwt`, { username }, this.auth)
|
||||
|
||||
// 404 means username is available, which is success in this case
|
||||
return response.status === 404
|
||||
? { success: true, data: false, available: true, response }
|
||||
: { success: false, available: false, response }
|
||||
}
|
||||
|
||||
/*
|
||||
* Remove account method
|
||||
*/
|
||||
Backend.prototype.removeAccount = async function () {
|
||||
return responseHandler(await api.delete(`/account/jwt`, this.auth))
|
||||
}
|
||||
/*
|
||||
* Enable MFA
|
||||
*/
|
||||
Backend.prototype.enableMfa = async function () {
|
||||
return responseHandler(await api.post(`/account/mfa/jwt`, { mfa: true }, this.auth))
|
||||
}
|
||||
|
||||
/*
|
||||
* Confirm MFA
|
||||
*/
|
||||
Backend.prototype.confirmMfa = async function (data) {
|
||||
return responseHandler(await api.post(`/account/mfa/jwt`, { ...data, mfa: true }, this.auth))
|
||||
}
|
||||
|
||||
/*
|
||||
* Disable MFA
|
||||
*/
|
||||
Backend.prototype.disableMfa = async function (data) {
|
||||
return responseHandler(await api.post(`/account/mfa/jwt`, { ...data, mfa: false }, this.auth))
|
||||
}
|
||||
|
||||
/*
|
||||
* Reload account
|
||||
*/
|
||||
Backend.prototype.reloadAccount = async function () {
|
||||
return responseHandler(await api.get(`/whoami/jwt`, this.auth))
|
||||
}
|
||||
|
||||
/*
|
||||
* Export account data
|
||||
*/
|
||||
Backend.prototype.exportAccount = async function () {
|
||||
return responseHandler(await api.get(`/account/export/jwt`, this.auth))
|
||||
}
|
||||
|
||||
/*
|
||||
* Restrict processing of account data
|
||||
*/
|
||||
Backend.prototype.restrictAccount = async function () {
|
||||
return responseHandler(await api.get(`/account/restrict/jwt`, this.auth))
|
||||
}
|
||||
|
||||
/*
|
||||
* Remove account
|
||||
*/
|
||||
Backend.prototype.restrictAccount = async function () {
|
||||
return responseHandler(await api.delete(`/account/jwt`, this.auth))
|
||||
}
|
||||
|
||||
/*
|
||||
* Load all user data
|
||||
*/
|
||||
Backend.prototype.getUserData = async function (uid) {
|
||||
return responseHandler(await api.get(`/users/${uid}/jwt`, this.auth))
|
||||
}
|
||||
|
||||
/*
|
||||
* Load user profile
|
||||
*/
|
||||
Backend.prototype.getProfile = async function (uid) {
|
||||
return responseHandler(await api.get(`/users/${uid}`))
|
||||
}
|
||||
|
||||
/*
|
||||
* Load user count
|
||||
*/
|
||||
Backend.prototype.getUserCount = async function () {
|
||||
return responseHandler(await api.get(`/info/users`))
|
||||
}
|
||||
|
||||
/*
|
||||
* Load stats
|
||||
*/
|
||||
Backend.prototype.getStats = async function () {
|
||||
return responseHandler(await api.get(`/info/stats`))
|
||||
}
|
||||
|
||||
/*
|
||||
* Create bookmark
|
||||
*/
|
||||
Backend.prototype.createBookmark = async function (data) {
|
||||
return responseHandler(await api.post(`/bookmarks/jwt`, data, this.auth), 201)
|
||||
}
|
||||
|
||||
/*
|
||||
* Get bookmark
|
||||
*/
|
||||
Backend.prototype.getBookmark = async function (id) {
|
||||
return responseHandler(await api.get(`/bookmarks/${id}/jwt`, this.auth))
|
||||
}
|
||||
/*
|
||||
* Get bookmarks
|
||||
*/
|
||||
Backend.prototype.getBookmarks = async function () {
|
||||
return responseHandler(await api.get(`/bookmarks/jwt`, this.auth))
|
||||
}
|
||||
|
||||
/*
|
||||
* Remove bookmark
|
||||
*/
|
||||
Backend.prototype.removeBookmark = async function (id) {
|
||||
const response = await api.delete(`/bookmarks/${id}/jwt`, this.auth)
|
||||
|
||||
return response && response.status === 204 ? true : false
|
||||
}
|
||||
|
||||
/*
|
||||
* Create API key
|
||||
*/
|
||||
Backend.prototype.createApikey = async function (data) {
|
||||
return responseHandler(await api.post(`/apikeys/jwt`, data, this.auth), 201)
|
||||
}
|
||||
|
||||
/*
|
||||
* Get API key
|
||||
*/
|
||||
Backend.prototype.getApikey = async function (id) {
|
||||
return responseHandler(await api.get(`/apikeys/${id}/jwt`, this.auth))
|
||||
}
|
||||
|
||||
/*
|
||||
* Get API keys
|
||||
*/
|
||||
Backend.prototype.getApikeys = async function () {
|
||||
return responseHandler(await api.get(`/apikeys/jwt`, this.auth))
|
||||
}
|
||||
|
||||
/*
|
||||
* Remove API key
|
||||
*/
|
||||
Backend.prototype.removeApikey = async function (id) {
|
||||
const response = await api.delete(`/apikeys/${id}/jwt`, this.auth)
|
||||
|
||||
return response && response.status === 204 ? true : false
|
||||
}
|
||||
|
||||
/*
|
||||
* Get measurements sets
|
||||
*/
|
||||
Backend.prototype.getSets = async function () {
|
||||
return responseHandler(await api.get(`/sets/jwt`, this.auth))
|
||||
}
|
||||
|
||||
/*
|
||||
* Get measurements set
|
||||
*/
|
||||
Backend.prototype.getSet = async function (id) {
|
||||
return responseHandler(await api.get(`/sets/${id}/jwt`, this.auth))
|
||||
}
|
||||
|
||||
/*
|
||||
* Get public measurements set
|
||||
*/
|
||||
Backend.prototype.getPublicSet = async function (id) {
|
||||
return responseHandler(await api.get(`/sets/${id}.json`))
|
||||
}
|
||||
|
||||
/*
|
||||
* Create measurements set
|
||||
*/
|
||||
Backend.prototype.createSet = async function (data) {
|
||||
return responseHandler(await api.post(`/sets/jwt`, data, this.auth), 201)
|
||||
}
|
||||
|
||||
/*
|
||||
* Remove measurements set
|
||||
*/
|
||||
Backend.prototype.removeSet = async function (id) {
|
||||
const response = await api.delete(`/sets/${id}/jwt`, this.auth)
|
||||
|
||||
return response && response.status === 204 ? true : false
|
||||
}
|
||||
|
||||
/*
|
||||
* Generic update measurements set method
|
||||
*/
|
||||
Backend.prototype.updateSet = async function (id, data) {
|
||||
return responseHandler(await api.patch(`/sets/${id}/jwt`, data, this.auth))
|
||||
}
|
||||
|
||||
/*
|
||||
* Get curated measurements sets
|
||||
*/
|
||||
Backend.prototype.getCuratedSets = async function () {
|
||||
return responseHandler(await api.get(`/curated-sets`))
|
||||
}
|
||||
|
||||
/*
|
||||
* Get measurements sets suggested for curation
|
||||
*/
|
||||
Backend.prototype.getSuggestedSets = async function () {
|
||||
return responseHandler(await api.get(`/suggested-sets/jwt`, this.auth))
|
||||
}
|
||||
|
||||
/*
|
||||
* Get option packs suggested for curation
|
||||
*/
|
||||
Backend.prototype.getSuggestedPacks = async function () {
|
||||
return responseHandler(await api.get(`/suggested-packs/jwt`, this.auth))
|
||||
}
|
||||
|
||||
/*
|
||||
* Remove suggested measurements set
|
||||
*/
|
||||
Backend.prototype.removeSuggestedMeasurementsSet = async function (id) {
|
||||
const response = await api.delete(`/suggested-sets/${id}/jwt`, this.auth)
|
||||
|
||||
return response && response.status === 204 ? true : false
|
||||
}
|
||||
|
||||
/*
|
||||
* Get curated measurements set
|
||||
*/
|
||||
Backend.prototype.getCuratedSet = async function (id) {
|
||||
return responseHandler(await api.get(`/curated-sets/${id}`))
|
||||
}
|
||||
|
||||
/*
|
||||
* Generic update curated measurements set method
|
||||
*/
|
||||
Backend.prototype.updateCuratedSet = async function (id, data) {
|
||||
return responseHandler(await api.patch(`/curated-sets/${id}/jwt`, data, this.auth))
|
||||
}
|
||||
|
||||
/*
|
||||
* Remove curated measurements set
|
||||
*/
|
||||
Backend.prototype.removeCuratedMeasurementsSet = async function (id) {
|
||||
const response = await api.delete(`/curated-sets/${id}/jwt`, this.auth)
|
||||
|
||||
return response && response.status === 204 ? true : false
|
||||
}
|
||||
|
||||
/*
|
||||
* Get pattern
|
||||
*/
|
||||
Backend.prototype.getPattern = async function (id) {
|
||||
return responseHandler(await api.get(`/patterns/${id}/jwt`, this.auth))
|
||||
}
|
||||
|
||||
/*
|
||||
* Get public pattern
|
||||
*/
|
||||
Backend.prototype.getPublicPattern = async function (id) {
|
||||
return responseHandler(await api.get(`/patterns/${id}.json`))
|
||||
}
|
||||
|
||||
/*
|
||||
* Get patterns
|
||||
*/
|
||||
Backend.prototype.getPatterns = async function () {
|
||||
return responseHandler(await api.get(`/patterns/jwt`, this.auth))
|
||||
}
|
||||
|
||||
/*
|
||||
* Create pattern
|
||||
*/
|
||||
Backend.prototype.createPattern = async function (data) {
|
||||
return responseHandler(await api.post(`/patterns/jwt`, data, this.auth), 201)
|
||||
}
|
||||
|
||||
/*
|
||||
* Remove pattern
|
||||
*/
|
||||
Backend.prototype.removePattern = async function (id) {
|
||||
const response = await api.delete(`/patterns/${id}/jwt`, this.auth)
|
||||
|
||||
return response && response.status === 204 ? true : false
|
||||
}
|
||||
|
||||
/*
|
||||
* Generic update pattern set method
|
||||
*/
|
||||
Backend.prototype.updatePattern = async function (id, data) {
|
||||
return responseHandler(await api.patch(`/patterns/${id}/jwt`, data, this.auth))
|
||||
}
|
||||
|
||||
/*
|
||||
* Create GitHub issue
|
||||
*/
|
||||
Backend.prototype.createIssue = async function (data) {
|
||||
return responseHandler(await api.post(`/issues`, data), 201)
|
||||
}
|
||||
|
||||
/*
|
||||
* Create GitHub discussion
|
||||
*/
|
||||
Backend.prototype.createDiscussion = async function (data) {
|
||||
return responseHandler(await api.post(`/discussions`, data), 201)
|
||||
}
|
||||
|
||||
/*
|
||||
* Check whether a slug is available
|
||||
*/
|
||||
Backend.prototype.isSlugAvailable = async function ({ slug, type }) {
|
||||
const response = await api.get(`/slugs/${type}/${slug}/jwt`, this.auth)
|
||||
|
||||
// 404 means username is available, which is success in this case
|
||||
return response.status === 200
|
||||
? { success: false, available: false, response }
|
||||
: { success: true, data: false, available: true, response }
|
||||
}
|
||||
|
||||
/*
|
||||
* Create showcase/blog post (pull request)
|
||||
*/
|
||||
Backend.prototype.createPost = async function (type, data) {
|
||||
return responseHandler(await api.post(`/flows/pr/${type}/jwt`, data, this.auth), 201)
|
||||
}
|
||||
|
||||
/*
|
||||
* Send translation invite
|
||||
*/
|
||||
Backend.prototype.sendTranslatorInvite = async function (language) {
|
||||
return responseHandler(await api.post(`/flows/translator-invite/jwt`, { language }, this.auth))
|
||||
}
|
||||
|
||||
/*
|
||||
* Send language suggestion
|
||||
*/
|
||||
Backend.prototype.sendLanguageSuggestion = async function (data) {
|
||||
return responseHandler(await api.post(`/flows/language-suggestion/jwt`, data, this.auth))
|
||||
}
|
||||
|
||||
/*
|
||||
* Subscribe to newsletter
|
||||
*/
|
||||
Backend.prototype.newsletterSubscribe = async function ({ email, language }) {
|
||||
return responseHandler(await api.post('/subscriber', { email, language }))
|
||||
}
|
||||
|
||||
/*
|
||||
* Confirm newsletter subscribe
|
||||
*/
|
||||
Backend.prototype.confirmNewsletterSubscribe = async function ({ id, ehash }) {
|
||||
return responseHandler(await api.put('/subscriber', { id, ehash }))
|
||||
}
|
||||
|
||||
/*
|
||||
* Newsletter unsubscribe
|
||||
*/
|
||||
Backend.prototype.newsletterUnsubscribe = async function (ehash) {
|
||||
return responseHandler(await api.delete(`/subscriber/${ehash}`))
|
||||
}
|
||||
|
||||
/*
|
||||
* Upload an image
|
||||
*/
|
||||
Backend.prototype.uploadImage = async function (body) {
|
||||
return responseHandler(await api.post('/images/jwt', body, this.auth))
|
||||
}
|
||||
|
||||
/*
|
||||
* Upload an image anonymously
|
||||
*/
|
||||
Backend.prototype.uploadAnonImage = async function (body) {
|
||||
return responseHandler(await api.post('/images', body))
|
||||
}
|
||||
|
||||
/*
|
||||
* Remove an (uploaded) image
|
||||
*/
|
||||
Backend.prototype.removeImage = async function (id) {
|
||||
return responseHandler(await api.delete(`/images/${id}/jwt`, this.auth))
|
||||
}
|
||||
|
||||
/*
|
||||
* Suggest a measurements set for curation
|
||||
*/
|
||||
Backend.prototype.suggestCset = async function (data) {
|
||||
return responseHandler(await api.post(`/curated-sets/suggest/jwt`, data, this.auth))
|
||||
}
|
||||
|
||||
/*
|
||||
* Suggest an option pack
|
||||
*/
|
||||
Backend.prototype.suggestOpack = async function (data) {
|
||||
return responseHandler(await api.post(`/option-packs/suggest/jwt`, data, this.auth))
|
||||
}
|
||||
|
||||
/*
|
||||
* Create a curated set from a suggested set
|
||||
*/
|
||||
Backend.prototype.csetFromSuggestedSet = async function (id) {
|
||||
return responseHandler(await api.post(`/curated-sets/from/${id}/jwt`, {}, this.auth))
|
||||
}
|
||||
|
||||
/*
|
||||
* Ping backend to see if current token is still valid
|
||||
*/
|
||||
Backend.prototype.ping = async function () {
|
||||
return responseHandler(await api.get(`/whoami/jwt`, this.auth))
|
||||
}
|
||||
|
||||
/*
|
||||
* Search user (admin method)
|
||||
*/
|
||||
Backend.prototype.adminSearchUsers = async function (q) {
|
||||
return responseHandler(await api.post('/admin/search/users/jwt', { q }, this.auth))
|
||||
}
|
||||
|
||||
/*
|
||||
* Load user (admin method)
|
||||
*/
|
||||
Backend.prototype.adminLoadUser = async function (id) {
|
||||
return responseHandler(await api.get(`/admin/user/${id}/jwt`, this.auth))
|
||||
}
|
||||
|
||||
/*
|
||||
* Update user (admin method)
|
||||
*/
|
||||
Backend.prototype.adminUpdateUser = async function ({ id, data }) {
|
||||
return responseHandler(await api.patch(`/admin/user/${id}/jwt`, data, this.auth))
|
||||
}
|
||||
|
||||
/*
|
||||
* Impersonate user (admin method)
|
||||
*/
|
||||
Backend.prototype.adminImpersonateUser = async function (id) {
|
||||
return responseHandler(await api.get(`/admin/impersonate/${id}/jwt`, this.auth))
|
||||
}
|
||||
|
||||
/*
|
||||
* Load newsletter subscribers (admin method)
|
||||
*/
|
||||
Backend.prototype.adminLoadSubscribers = async function () {
|
||||
return responseHandler(await api.get(`/admin/subscribers/jwt`, this.auth))
|
||||
}
|
||||
|
||||
/*
|
||||
* Verify an admin account while impersonating another user
|
||||
*/
|
||||
Backend.prototype.adminPing = async function (token) {
|
||||
return responseHandler(await api.get(`/whoami/jwt`, auth(token)))
|
||||
}
|
||||
|
||||
/*
|
||||
* backend.img: Generate a social media image
|
||||
*/
|
||||
Backend.prototype.img = async function (data) {
|
||||
return responseHandler(await api.post('/img', data, { responseType: 'arraybuffer' }))
|
||||
}
|
||||
|
||||
export function useBackend(Swizzled) {
|
||||
/*
|
||||
* Load swizzled useAccount hook and use it
|
||||
*/
|
||||
const { token } = Swizzled.hooks.useAccount()
|
||||
|
||||
/*
|
||||
* This backend object is what we'll end up returning
|
||||
*/
|
||||
const backend = useMemo(() => new Backend(auth(token)), [token])
|
||||
|
||||
return backend
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
import { useMemo, useState, useEffect } from 'react'
|
||||
import useLocalStorageState from 'use-local-storage-state'
|
||||
import useSessionStorageState from 'use-session-storage-state'
|
||||
import { useQueryState, createParser } from 'nuqs'
|
||||
|
||||
/**
|
||||
* react
|
||||
* This holds the editor state, using React state.
|
||||
* It also provides helper methods to manipulate state.
|
||||
*
|
||||
* @params {object} init - Initial pattern settings
|
||||
* @return {array} return - And array with get, set, and update methods
|
||||
*/
|
||||
export const useReactEditorState = (Swizzled, init = {}, setEphemeralState) => {
|
||||
const [state, setState] = useState(init)
|
||||
const update = useMemo(
|
||||
() => Swizzled.methods.stateUpdateFactory(setState, setEphemeralState),
|
||||
[setState]
|
||||
)
|
||||
|
||||
return [state, setState, update]
|
||||
}
|
||||
|
||||
/**
|
||||
* storage
|
||||
* This holds the editor state, using local storage.
|
||||
* It also provides helper methods to manipulate state.
|
||||
*
|
||||
* @params {object} init - Initial pattern settings
|
||||
* @return {array} return - And array with get, set, and update methods
|
||||
*/
|
||||
export const useStorageEditorState = (Swizzled, init = {}, setEphemeralState) => {
|
||||
const [state, setState] = useLocalStorageState('fs-editor', { defaultValue: init })
|
||||
const update = useMemo(
|
||||
() => Swizzled.methods.stateUpdateFactory(setState, setEphemeralState),
|
||||
[setState]
|
||||
)
|
||||
|
||||
return [state, setState, update]
|
||||
}
|
||||
|
||||
/**
|
||||
* session
|
||||
* This holds the editor state, using session storage.
|
||||
* It also provides helper methods to manipulate state.
|
||||
*
|
||||
* @params {object} init - Initial pattern settings
|
||||
* @return {array} return - And array with get, set, and update methods
|
||||
*/
|
||||
export const useSessionEditorState = (Swizzled, init = {}, setEphemeralState) => {
|
||||
const [state, setState] = useSessionStorageState('fs-editor', { defaultValue: init })
|
||||
const update = useMemo(
|
||||
() => Swizzled.methods.stateUpdateFactory(setState, setEphemeralState),
|
||||
[setState]
|
||||
)
|
||||
|
||||
return [state, setState, update]
|
||||
}
|
||||
|
||||
/**
|
||||
* url
|
||||
* This holds the editor state, using session storage.
|
||||
* It also provides helper methods to manipulate state.
|
||||
*
|
||||
* @params {object} init - Initial pattern settings
|
||||
* @return {array} return - And array with get, set, and update methods
|
||||
*/
|
||||
export const useUrlEditorState = (Swizzled, init = {}, setEphemeralState) => {
|
||||
const [state, setState] = useQueryState('s', pojoParser)
|
||||
const update = useMemo(
|
||||
() => Swizzled.methods.stateUpdateFactory(setState, setEphemeralState),
|
||||
[setState]
|
||||
)
|
||||
|
||||
/*
|
||||
* Set the initial state
|
||||
*/
|
||||
useEffect(() => {
|
||||
// Handle state on a hard reload or cold start
|
||||
if (typeof URLSearchParams !== 'undefined') {
|
||||
let urlState = false
|
||||
try {
|
||||
const params = new URLSearchParams(document.location.search)
|
||||
const s = params.get('s')
|
||||
if (typeof s === 'string' && s.length > 0) urlState = JSON.parse(s)
|
||||
if (urlState) setState(urlState)
|
||||
else setState(init)
|
||||
} catch (err) {
|
||||
setState(init)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
return [state, setState, update]
|
||||
}
|
||||
|
||||
/*
|
||||
* Our URL state library does not support storing Javascript objects out of the box.
|
||||
* But it allows us to pass a customer parser to handle them, so this is that parser
|
||||
*/
|
||||
const pojoParser = createParser({
|
||||
parse: (v) => {
|
||||
let val
|
||||
try {
|
||||
val = JSON.parse(v)
|
||||
} catch (err) {
|
||||
val = null
|
||||
}
|
||||
return val
|
||||
},
|
||||
serialize: (v) => {
|
||||
let val
|
||||
try {
|
||||
val = JSON.stringify(v)
|
||||
} catch (err) {
|
||||
val = null
|
||||
}
|
||||
return val
|
||||
},
|
||||
})
|
|
@ -0,0 +1,149 @@
|
|||
export function defaultSa(Swizzled, units, inMm = true) {
|
||||
const dflt = units === 'imperial' ? 0.5 : 1
|
||||
return inMm ? Swizzled.methods.measurementAsMm(dflt, units) : dflt
|
||||
}
|
||||
export function defaultSamm(Swizzled, units, inMm = true) {
|
||||
const dflt = units === 'imperial' ? 0.5 : 1
|
||||
return inMm ? Swizzled.methods.measurementAsMm(dflt, units) : dflt
|
||||
}
|
||||
/** custom event handlers for inputs that need them */
|
||||
export function menuCoreSettingsOnlyHandler(Swizzled, { updateHandler, current }) {
|
||||
return function (path, part) {
|
||||
// Is this a reset?
|
||||
if (part === undefined || part === '__UNSET__') return updateHandler(path, part)
|
||||
|
||||
// add or remove the part from the set
|
||||
let newParts = new Set(current || [])
|
||||
if (newParts.has(part)) newParts.delete(part)
|
||||
else newParts.add(part)
|
||||
|
||||
// if the set is now empty, reset
|
||||
if (newParts.size < 1) newParts = undefined
|
||||
// otherwise use the new set
|
||||
else newParts = [...newParts]
|
||||
|
||||
updateHandler(path, newParts)
|
||||
}
|
||||
}
|
||||
|
||||
export function menuCoreSettingsSammHandler(Swizzled, { updateHandler, config }) {
|
||||
return function (_path, newCurrent) {
|
||||
// convert to millimeters if there's a value
|
||||
newCurrent = newCurrent === undefined ? config.dflt : newCurrent
|
||||
// update both values to match
|
||||
updateHandler(['samm'], newCurrent)
|
||||
updateHandler(['sa'], newCurrent)
|
||||
}
|
||||
}
|
||||
|
||||
export function menuCoreSettingsSaboolHandler(Swizzled, { toggleSa }) {
|
||||
return toggleSa
|
||||
}
|
||||
export function menuCoreSettingsStructure(
|
||||
Swizzled,
|
||||
{ units = 'metric', sabool = false, parts = [] }
|
||||
) {
|
||||
return {
|
||||
sabool: {
|
||||
ux: Swizzled.config.uxLevels.core.sa,
|
||||
list: [0, 1],
|
||||
choiceTitles: {
|
||||
0: 'saNo',
|
||||
1: 'saYes',
|
||||
},
|
||||
valueTitles: {
|
||||
0: 'no',
|
||||
1: 'yes',
|
||||
},
|
||||
dflt: 0,
|
||||
icon: Swizzled.components.SaIcon,
|
||||
},
|
||||
samm: sabool
|
||||
? {
|
||||
ux: Swizzled.config.uxLevels.core.sa,
|
||||
min: 0,
|
||||
max: units === 'imperial' ? 2 : 2.5,
|
||||
dflt: Swizzled.methods.defaultSamm(units),
|
||||
icon: Swizzled.components.SaIcon,
|
||||
}
|
||||
: false,
|
||||
paperless: {
|
||||
ux: Swizzled.config.uxLevels.core.paperless,
|
||||
list: [0, 1],
|
||||
choiceTitles: {
|
||||
0: 'paperlessNo',
|
||||
1: 'paperlessYes',
|
||||
},
|
||||
valueTitles: {
|
||||
0: 'no',
|
||||
1: 'yes',
|
||||
},
|
||||
dflt: 0,
|
||||
icon: Swizzled.components.PaperlessIcon,
|
||||
},
|
||||
units: {
|
||||
ux: Swizzled.config.uxLevels.core.units,
|
||||
list: ['metric', 'imperial'],
|
||||
dflt: 'metric',
|
||||
choiceTitles: {
|
||||
metric: 'metric',
|
||||
imperial: 'imperial',
|
||||
},
|
||||
valueTitles: {
|
||||
metric: 'metric',
|
||||
imperial: 'imperial',
|
||||
},
|
||||
icon: Swizzled.components.UnitsIcon,
|
||||
},
|
||||
complete: {
|
||||
ux: Swizzled.config.uxLevels.core.complete,
|
||||
list: [1, 0],
|
||||
dflt: 1,
|
||||
choiceTitles: {
|
||||
0: 'completeNo',
|
||||
1: 'completeYes',
|
||||
},
|
||||
valueTitles: {
|
||||
0: 'no',
|
||||
1: 'yes',
|
||||
},
|
||||
icon: Swizzled.components.DetailIcon,
|
||||
},
|
||||
expand: {
|
||||
ux: Swizzled.config.uxLevels.core.expand,
|
||||
list: [1, 0],
|
||||
dflt: 1,
|
||||
choiceTitles: {
|
||||
0: 'expandNo',
|
||||
1: 'expandYes',
|
||||
},
|
||||
valueTitles: {
|
||||
0: 'no',
|
||||
1: 'yes',
|
||||
},
|
||||
icon: Swizzled.components.ExpandIcon,
|
||||
},
|
||||
only: {
|
||||
ux: Swizzled.config.uxLevels.core.only,
|
||||
dflt: false,
|
||||
list: parts,
|
||||
parts,
|
||||
icon: Swizzled.components.IncludeIcon,
|
||||
},
|
||||
scale: {
|
||||
ux: Swizzled.config.uxLevels.core.scale,
|
||||
min: 0.1,
|
||||
max: 5,
|
||||
dflt: 1,
|
||||
step: 0.1,
|
||||
icon: Swizzled.components.ScaleIcon,
|
||||
},
|
||||
margin: {
|
||||
ux: Swizzled.config.uxLevels.core.margin,
|
||||
min: 0,
|
||||
max: 2.5,
|
||||
dflt: Swizzled.methods.measurementAsMm(units === 'imperial' ? 0.125 : 0.2, units),
|
||||
icon: Swizzled.components.MarginIcon,
|
||||
},
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
export function designOptionType(Swizzled, 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'
|
||||
|
||||
export function menuDesignOptionsStructure(Swizzled, options, settings, asFullList = false) {
|
||||
if (!options) return options
|
||||
const sorted = {}
|
||||
for (const [name, option] of Object.entries(options)) {
|
||||
if (typeof option === 'object') sorted[name] = { ...option, name }
|
||||
}
|
||||
|
||||
const menu = {}
|
||||
// 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)
|
||||
option.dflt = option.dflt || option[oType]
|
||||
if (oType === 'pct') option.dflt /= 100
|
||||
if (typeof option.menu === 'function')
|
||||
option.menu = asFullList
|
||||
? 'conditional'
|
||||
: option.menu(settings, mergeOptions(settings, options))
|
||||
if (option.menu) {
|
||||
// Handle nested groups that don't have any direct children
|
||||
if (option.menu.includes('.')) {
|
||||
let menuPath = []
|
||||
for (const chunk of option.menu.split('.')) {
|
||||
menuPath.push(chunk)
|
||||
set(menu, `${menuPath.join('.')}.isGroup`, true)
|
||||
}
|
||||
}
|
||||
set(menu, `${option.menu}.isGroup`, true)
|
||||
set(menu, `${option.menu}.${option.name}`, option)
|
||||
} else if (typeof option.menu === 'undefined') {
|
||||
console.log(
|
||||
`Warning: Option ${option.name} does not have a menu config. ` +
|
||||
'Either configure it, or set it to false to hide this option.'
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Always put advanced at the end
|
||||
if (menu.advanced) {
|
||||
const adv = menu.advanced
|
||||
delete menu.advanced
|
||||
menu.advanced = adv
|
||||
}
|
||||
|
||||
return menu
|
||||
}
|
||||
/*
|
||||
* Helper method to grab an option from an Design options structure
|
||||
*
|
||||
* 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
|
||||
)
|
||||
console.log({ structure })
|
||||
|
||||
return Swizzled.methods.findOption(structure, option)
|
||||
}
|
||||
|
||||
export function findOption(Swizzled, structure, option) {
|
||||
for (const [key, val] of Object.entries(structure)) {
|
||||
if (key === option) return val
|
||||
if (val.isGroup) {
|
||||
const sub = findOption(val, option)
|
||||
if (sub) return sub
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
716
packages/react-components/src/editor/swizzle/methods/editor.mjs
Normal file
716
packages/react-components/src/editor/swizzle/methods/editor.mjs
Normal file
|
@ -0,0 +1,716 @@
|
|||
/*
|
||||
* This method drafts the pattern
|
||||
*
|
||||
* @param {object} Swizzled - Swizzled code, not used here
|
||||
* @param {function} Design - The Design constructor
|
||||
* @param {object} settings - The settings for the pattern
|
||||
* @return {object} data - The drafted pattern, along with errors and failure data
|
||||
*/
|
||||
export function draft(Swizzled, Design, settings) {
|
||||
const data = {
|
||||
// The pattern
|
||||
pattern: new Design(settings),
|
||||
// Any errors logged by the pattern
|
||||
errors: [],
|
||||
// If the pattern fails to draft, this will hold the error
|
||||
failure: false,
|
||||
}
|
||||
// Draft the pattern or die trying
|
||||
try {
|
||||
data.pattern.draft()
|
||||
data.errors.push(...data.pattern.store.logs.error)
|
||||
for (const store of data.pattern.setStores) data.errors.push(...store.logs.error)
|
||||
} catch (error) {
|
||||
data.failure = error
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
export function flattenFlags(Swizzled, flags) {
|
||||
const all = {}
|
||||
for (const type of Swizzled.config.flagTypes) {
|
||||
let i = 0
|
||||
if (flags[type]) {
|
||||
for (const flag of Object.values(flags[type])) {
|
||||
i++
|
||||
all[`${type}-${i}`] = { ...flag, type }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return all
|
||||
}
|
||||
|
||||
export function getUiPreferenceUndoStepData(Swizzled, { step }) {
|
||||
/*
|
||||
* We'll need these
|
||||
*/
|
||||
const field = step.name === 'ui' ? step.path[1] : step.path[2]
|
||||
const structure = Swizzled.methods.menuUiPreferencesStructure()[field]
|
||||
|
||||
/*
|
||||
* This we'll end up returning
|
||||
*/
|
||||
const data = {
|
||||
icon: <Swizzled.components.UiIcon />,
|
||||
field,
|
||||
optCode: `${field}.t`,
|
||||
titleCode: 'uiPreferences.t',
|
||||
structure: Swizzled.methods.menuUiPreferencesStructure()[field],
|
||||
}
|
||||
const FieldIcon = data.structure.icon
|
||||
data.fieldIcon = <FieldIcon />
|
||||
|
||||
/*
|
||||
* Add oldval and newVal if they exist, or fall back to default
|
||||
*/
|
||||
for (const key of ['old', 'new'])
|
||||
data[key + 'Val'] = Swizzled.methods.t(
|
||||
structure.choiceTitles[
|
||||
structure.choiceTitles[String(step[key])] ? String(step[key]) : String(structure.dflt)
|
||||
] + '.t'
|
||||
)
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
export function getCoreSettingUndoStepData(Swizzled, { step, state, Design }) {
|
||||
const field = step.path[1]
|
||||
const structure = Swizzled.methods.menuCoreSettingsStructure({
|
||||
language: state.language,
|
||||
units: state.settings.units,
|
||||
sabool: state.settings.sabool,
|
||||
parts: Design.patternConfig.draftOrder,
|
||||
})
|
||||
|
||||
const data = {
|
||||
field,
|
||||
titleCode: 'coreSettings.t',
|
||||
optCode: `${field}.t`,
|
||||
icon: <Swizzled.components.SettingsIcon />,
|
||||
structure: structure[field],
|
||||
}
|
||||
if (!data.structure && field === 'sa') data.structure = structure.samm
|
||||
const FieldIcon = data.structure?.icon || Swizzled.components.FixmeIcon
|
||||
data.fieldIcon = <FieldIcon />
|
||||
|
||||
/*
|
||||
* Save us some typing
|
||||
*/
|
||||
const cord = Swizzled.methods.settingsValueCustomOrDefault
|
||||
const formatMm = Swizzled.methods.formatMm
|
||||
const Html = Swizzled.components.HtmlSpan
|
||||
|
||||
/*
|
||||
* Need to allow HTML in some of these in case this is
|
||||
* formated as imperial which uses <sub> and <sup>
|
||||
*/
|
||||
switch (data.field) {
|
||||
case 'margin':
|
||||
case 'sa':
|
||||
case 'samm':
|
||||
if (data.field !== 'margin') {
|
||||
data.optCode = `samm.t`
|
||||
}
|
||||
data.oldVal = <Html html={formatMm(cord(step.old, data.structure.dflt))} />
|
||||
data.newVal = <Html html={formatMm(cord(step.new, data.structure.dflt))} />
|
||||
return data
|
||||
case 'scale':
|
||||
data.oldVal = cord(step.old, data.structure.dflt)
|
||||
data.newVal = cord(step.new, data.structure.dflt)
|
||||
return data
|
||||
case 'units':
|
||||
data.oldVal = Swizzled.methods.t(
|
||||
step.new === 'imperial' ? 'pe:metricUnits' : 'pe:imperialUnits'
|
||||
)
|
||||
data.newVal = Swizzled.methods.t(
|
||||
step.new === 'imperial' ? 'pe:imperialUnits' : 'pe:metricUnits'
|
||||
)
|
||||
return data
|
||||
case 'only':
|
||||
data.oldVal = cord(step.old, data.structure.dflt) || Swizzled.methods.t('pe:includeAllParts')
|
||||
data.newVal = cord(step.new, data.structure.dflt) || Swizzled.methods.t('pe:includeAllParts')
|
||||
return data
|
||||
default:
|
||||
data.oldVal = Swizzled.methods.t(
|
||||
(data.structure.choiceTitles[String(step.old)]
|
||||
? data.structure.choiceTitles[String(step.old)]
|
||||
: data.structure.choiceTitles[String(data.structure.dflt)]) + '.t'
|
||||
)
|
||||
data.newVal = Swizzled.methods.t(
|
||||
(data.structure.choiceTitles[String(step.new)]
|
||||
? data.structure.choiceTitles[String(step.new)]
|
||||
: data.structure.choiceTitles[String(data.structure.dflt)]) + '.t'
|
||||
)
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
export function getDesignOptionUndoStepData(Swizzled, { step, state, Design }) {
|
||||
const option = Design.patternConfig.options[step.path[2]]
|
||||
const data = {
|
||||
icon: <Swizzled.components.OptionsIcon />,
|
||||
field: step.path[2],
|
||||
optCode: `${state.design}:${step.path[2]}.t`,
|
||||
titleCode: `designOptions.t`,
|
||||
oldVal: Swizzled.methods.formatDesignOptionValue(option, step.old, state.units === 'imperial'),
|
||||
newVal: Swizzled.methods.formatDesignOptionValue(option, step.new, state.units === 'imperial'),
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
export function getUndoStepData(Swizzled, props) {
|
||||
/*
|
||||
* UI Preferences
|
||||
*/
|
||||
if ((props.step.name === 'settings' && props.step.path[1] === 'ui') || props.step.name === 'ui')
|
||||
return Swizzled.methods.getUiPreferenceUndoStepData(props)
|
||||
|
||||
/*
|
||||
* Design options
|
||||
*/
|
||||
if (props.step.name === 'settings' && props.step.path[1] === 'options')
|
||||
return Swizzled.methods.getDesignOptionUndoStepData(props)
|
||||
|
||||
/*
|
||||
* Core Settings
|
||||
*/
|
||||
if (
|
||||
props.step.name === 'settings' &&
|
||||
[
|
||||
'sa',
|
||||
'samm',
|
||||
'margin',
|
||||
'scale',
|
||||
'only',
|
||||
'complete',
|
||||
'paperless',
|
||||
'sabool',
|
||||
'units',
|
||||
'expand',
|
||||
].includes(props.step.path[1])
|
||||
)
|
||||
return Swizzled.methods.getCoreSettingUndoStepData(props)
|
||||
|
||||
/*
|
||||
* Measurements
|
||||
*/
|
||||
if (props.step.name === 'settings' && props.step.path[1] === 'measurements') {
|
||||
const data = {
|
||||
icon: <Swizzled.components.MeasurementsIcon />,
|
||||
field: 'measurements',
|
||||
optCode: `measurements`,
|
||||
titleCode: 'measurements',
|
||||
}
|
||||
/*
|
||||
* Single measurements change?
|
||||
*/
|
||||
if (props.step.path[2])
|
||||
return {
|
||||
...data,
|
||||
field: props.step.path[2],
|
||||
oldVal: Swizzled.methods.formatMm(props.step.old, props.imperial),
|
||||
newVal: Swizzled.methods.formatMm(props.step.new, props.imperial),
|
||||
}
|
||||
let count = 0
|
||||
for (const m of Object.keys(props.step.new)) {
|
||||
if (props.step.new[m] !== props.step.old?.[m]) count++
|
||||
}
|
||||
return { ...data, msg: Swizzled.methods.t('pe:xMeasurementsChanged', { count }) }
|
||||
}
|
||||
|
||||
/*
|
||||
* Bail out of the step fell throug
|
||||
*/
|
||||
return false
|
||||
}
|
||||
/*
|
||||
* This helper method constructs the initial state object.
|
||||
*
|
||||
* If they are not present, it will fall back to the relevant defaults
|
||||
* @param {object} Swizzled - The swizzled data
|
||||
*/
|
||||
export function initialEditorState(Swizzled) {
|
||||
/*
|
||||
* Create initial state object
|
||||
*/
|
||||
const initial = { ...Swizzled.config.initialState }
|
||||
|
||||
/*
|
||||
* FIXME: Add preload support, from URL or other sources, rather than just passing in an object
|
||||
*/
|
||||
|
||||
return initial
|
||||
}
|
||||
/**
|
||||
* round a value to the correct number of decimal places to display all supplied digits after multiplication
|
||||
* this is a workaround for floating point errors
|
||||
* examples:
|
||||
* roundPct(0.72, 100) === 72
|
||||
* roundPct(7.5, 0.01) === 0.075
|
||||
* roundPct(7.50, 0.01) === 0.0750
|
||||
* @param {object} Swizzled - Swizzled code, not used here
|
||||
* @param {Number} num the number to be operated on
|
||||
* @param {Number} factor the number to multiply by
|
||||
* @return {Number} the given num multiplied by the factor, rounded appropriately
|
||||
*/
|
||||
export function menuRoundPct(Swizzled, num, factor) {
|
||||
const { round } = Swizzled.methods
|
||||
// stringify
|
||||
const str = '' + num
|
||||
// get the index of the decimal point in the number
|
||||
const decimalIndex = str.indexOf('.')
|
||||
// get the number of places the factor moves the decimal point
|
||||
const factorPlaces = factor > 0 ? Math.ceil(Math.log10(factor)) : Math.floor(Math.log10(factor))
|
||||
// the number of places needed is the number of digits that exist after the decimal minus the number of places the decimal point is being moved
|
||||
const numPlaces = Math.max(0, str.length - (decimalIndex + factorPlaces))
|
||||
return round(num * factor, numPlaces)
|
||||
}
|
||||
|
||||
const menuNumericInputMatcher = /^-?[0-9]*[.,eE]?[0-9]+$/ // match a single decimal separator
|
||||
const menuFractionInputMatcher = /^-?[0-9]*(\s?[0-9]+\/|[.,eE])?[0-9]+$/ // match a single decimal separator or fraction
|
||||
|
||||
/**
|
||||
* Validate and parse a value that should be a number
|
||||
* @param {object} Swizzled - Swizzled code, not used here
|
||||
* @param {any} val the value to validate
|
||||
* @param {Boolean} allowFractions should fractions be considered valid input?
|
||||
* @param {Number} min the minimum allowable value
|
||||
* @param {Number} max the maximum allowable value
|
||||
* @return {null|false|Number} null if the value is empty,
|
||||
* false if the value is invalid,
|
||||
* or the value parsed to a number if it is valid
|
||||
*/
|
||||
export function menuValidateNumericValue(
|
||||
Swizzled,
|
||||
val,
|
||||
allowFractions = true,
|
||||
min = -Infinity,
|
||||
max = Infinity
|
||||
) {
|
||||
// if it's empty, we're neutral
|
||||
if (typeof val === 'undefined' || val === '') return null
|
||||
|
||||
// make sure it's a string
|
||||
val = ('' + val).trim()
|
||||
|
||||
// get the appropriate match pattern and check for a match
|
||||
const matchPattern = allowFractions ? menuFractionInputMatcher : menuNumericInputMatcher
|
||||
if (!val.match(matchPattern)) return false
|
||||
|
||||
// replace comma with period
|
||||
const parsedVal = val.replace(',', '.')
|
||||
// if fractions are allowed, parse for fractions, otherwise use the number as a value
|
||||
const useVal = allowFractions ? Swizzled.methods.fractionToDecimal(parsedVal) : parsedVal
|
||||
|
||||
// check that it's a number and it's in the range
|
||||
if (isNaN(useVal) || useVal > max || useVal < min) return false
|
||||
|
||||
// all checks passed. return the parsed value
|
||||
return useVal
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if a value is different from its default
|
||||
* @param {object} Swizzled - Swizzled code, not used here
|
||||
* @param {Number|String|Boolean} current the current value
|
||||
* @param {Object} config configuration containing a dflt key
|
||||
* @return {Boolean} was the value changed?
|
||||
*/
|
||||
export function menuValueWasChanged(Swizzled, current, config) {
|
||||
if (typeof current === 'undefined') return false
|
||||
if (current == config.dflt) return false
|
||||
|
||||
return true
|
||||
}
|
||||
import get from 'lodash.get'
|
||||
import set from 'lodash.set'
|
||||
import unset from 'lodash.unset'
|
||||
|
||||
const UNSET = '__UNSET__'
|
||||
/*
|
||||
* Helper method to handle object updates
|
||||
*
|
||||
* @param {object} methods - An object holding possibly swizzled methods (unused here)
|
||||
* @param {object} obj - The object to update
|
||||
* @param {string|array} path - The path to the key to update, either as array or dot notation
|
||||
* @param {mixed} val - The new value to set or 'unset' to unset the value
|
||||
* @return {object} result - The updated object
|
||||
*/
|
||||
export function objUpdate(Swizzled, obj = {}, path, val = '__UNSET__') {
|
||||
if (val === UNSET) unset(obj, path)
|
||||
else set(obj, path, val)
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
/*
|
||||
* Helper method to handle object updates that also updates the undo history in ephemeral state
|
||||
*
|
||||
* @param {object} methods - An object holding possibly swizzled methods (unused here)
|
||||
* @param {object} obj - The object to update
|
||||
* @param {string|array} path - The path to the key to update, either as array or dot notation
|
||||
* @param {mixed} val - The new value to set or 'unset' to unset the value
|
||||
* @param {function} setEphemeralState - The ephemeral state setter
|
||||
* @return {object} result - The updated object
|
||||
*/
|
||||
export function undoableObjUpdate(
|
||||
Swizzled,
|
||||
name,
|
||||
obj = {},
|
||||
path,
|
||||
val = '__UNSET__',
|
||||
setEphemeralState
|
||||
) {
|
||||
const current = get(obj, path)
|
||||
setEphemeralState((cur) => {
|
||||
if (!Array.isArray(cur.undos)) cur.undos = []
|
||||
return {
|
||||
...cur,
|
||||
undos: [
|
||||
{
|
||||
name,
|
||||
time: Date.now(),
|
||||
path,
|
||||
old: current,
|
||||
new: val,
|
||||
restore: Swizzled.methods.cloneObject(obj),
|
||||
},
|
||||
...cur.undos,
|
||||
],
|
||||
}
|
||||
})
|
||||
|
||||
return Swizzled.methods.objUpdate(obj, path, val)
|
||||
}
|
||||
|
||||
/*
|
||||
* Helper method to add an undo step for which state updates are handles in another way
|
||||
*
|
||||
* This is typically used for SA changes as it requires changing 3 fields:
|
||||
* - sabool: Is sa on or off?
|
||||
* - sa: sa value for core
|
||||
* - samm: Holds the sa value in mm even when sa is off
|
||||
*
|
||||
* @param {object} Swizzled - An object holding possibly swizzled code (unused here)
|
||||
* @param {object} undo - The undo step to add
|
||||
* @param {object} restore - The state to restore for this step
|
||||
* @param {function} setEphemeralState - The ephemeral state setter
|
||||
*/
|
||||
export function addUndoStep(Swizzled, undo, restore, setEphemeralState) {
|
||||
setEphemeralState((cur) => {
|
||||
if (!Array.isArray(cur.undos)) cur.undos = []
|
||||
return {
|
||||
...cur,
|
||||
undos: [
|
||||
{ time: Date.now(), ...undo, restore: Swizzled.methods.cloneObject(restore) },
|
||||
...cur.undos,
|
||||
],
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* Helper method to clone an object
|
||||
*/
|
||||
export function cloneObject(Swizzled, obj) {
|
||||
return JSON.parse(JSON.stringify(obj))
|
||||
}
|
||||
/**
|
||||
* Helper method to push a prefix to a set path
|
||||
*
|
||||
* By 'set path' we mean a path to be passed to the
|
||||
* objUpdate method, which uses lodash's set under the hood.
|
||||
*
|
||||
* @param {string} prefix - The prefix path to add
|
||||
* @param {string|array} path - The path to prefix either as array or a string in dot notation
|
||||
* @return {array} newPath - The prefixed path
|
||||
*/
|
||||
export function statePrefixPath(Swizzled, prefix, path) {
|
||||
if (Array.isArray(path)) return [prefix, ...path]
|
||||
else return [prefix, ...path.split('.')]
|
||||
}
|
||||
/*
|
||||
* This creates the helper object for state updates
|
||||
*/
|
||||
export function stateUpdateFactory(Swizzled, setState, setEphemeralState) {
|
||||
return {
|
||||
/*
|
||||
* This allows raw access to the entire state object
|
||||
*/
|
||||
state: (path, val) => setState((cur) => Swizzled.methods.objUpdate({ ...cur }, path, val)),
|
||||
/*
|
||||
* These hold an object, so we take a path
|
||||
*/
|
||||
settings: (path = null, val = null) => {
|
||||
/*
|
||||
* This check can be removed once all code is migrated to the new editor
|
||||
*/
|
||||
if (Array.isArray(path) && Array.isArray(path[0]) && val === null) {
|
||||
throw new Error(
|
||||
'Update.settings was called with an array of operations. This is no longer supported.'
|
||||
)
|
||||
}
|
||||
return setState((cur) =>
|
||||
Swizzled.methods.undoableObjUpdate(
|
||||
'settings',
|
||||
{ ...cur },
|
||||
Swizzled.methods.statePrefixPath('settings', path),
|
||||
val,
|
||||
setEphemeralState
|
||||
)
|
||||
)
|
||||
},
|
||||
/*
|
||||
* Helper to restore from undo state
|
||||
* Takes the index of the undo step in the array in ephemeral state
|
||||
*/
|
||||
restore: async (i, ephemeralState) => {
|
||||
setState(ephemeralState.undos[i].restore)
|
||||
setEphemeralState((cur) => {
|
||||
cur.undos = cur.undos.slice(i + 1)
|
||||
return cur
|
||||
})
|
||||
},
|
||||
/*
|
||||
* Helper to toggle SA on or off as that requires managing sa, samm, and sabool
|
||||
*/
|
||||
toggleSa: () =>
|
||||
setState((cur) => {
|
||||
const sa = cur.settings?.samm || (cur.settings?.units === 'imperial' ? 15.3125 : 10)
|
||||
const restore = Swizzled.methods.cloneObject(cur)
|
||||
// This requires 3 changes
|
||||
const update = cur.settings.sabool
|
||||
? [
|
||||
['sabool', 0],
|
||||
['sa', 0],
|
||||
['samm', sa],
|
||||
]
|
||||
: [
|
||||
['sabool', 1],
|
||||
['sa', sa],
|
||||
['samm', sa],
|
||||
]
|
||||
for (const [key, val] of update) Swizzled.methods.objUpdate(cur, `settings.${key}`, val)
|
||||
// Which we'll group as 1 undo action
|
||||
Swizzled.methods.addUndoStep(
|
||||
{
|
||||
name: 'settings',
|
||||
path: ['settings', 'sa'],
|
||||
new: cur.settings.sabool ? 0 : sa,
|
||||
old: cur.settings.sabool ? sa : 0,
|
||||
},
|
||||
restore,
|
||||
setEphemeralState
|
||||
)
|
||||
|
||||
return cur
|
||||
}),
|
||||
ui: (path, val) =>
|
||||
setState((cur) =>
|
||||
Swizzled.methods.undoableObjUpdate(
|
||||
'ui',
|
||||
{ ...cur },
|
||||
Swizzled.methods.statePrefixPath('ui', path),
|
||||
val,
|
||||
setEphemeralState
|
||||
)
|
||||
),
|
||||
/*
|
||||
* These only hold a string, so we only take a value
|
||||
*/
|
||||
design: (val) => setState((cur) => Swizzled.methods.objUpdate({ ...cur }, 'design', val)),
|
||||
view: (val) => {
|
||||
// Only take valid view names
|
||||
if (!Swizzled.config.views.includes(val)) return console.log('not a valid view:', val)
|
||||
setState((cur) => ({ ...cur, view: val }))
|
||||
// Also add it onto the views (history)
|
||||
setEphemeralState((cur) => {
|
||||
if (!Array.isArray(cur.views)) cur.views = []
|
||||
return { ...cur, views: [val, ...cur.views] }
|
||||
})
|
||||
},
|
||||
viewBack: () => {
|
||||
setEphemeralState((eph) => {
|
||||
if (Array.isArray(eph.views) && Swizzled.config.views.includes(eph.views[1])) {
|
||||
// Load view at the 1 position of the history
|
||||
setState((cur) => ({ ...cur, view: eph.views[1] }))
|
||||
return { ...eph, views: eph.views.slice(1) }
|
||||
}
|
||||
|
||||
return eph
|
||||
})
|
||||
},
|
||||
ux: (val) => setState((cur) => Swizzled.methods.objUpdate({ ...cur }, 'ux', val)),
|
||||
clearPattern: () =>
|
||||
setState((cur) => {
|
||||
const newState = { ...cur }
|
||||
Swizzled.methods.objUpdate(newState, 'settings', {
|
||||
measurements: cur.settings.measurements,
|
||||
})
|
||||
/*
|
||||
* Let's also reset the renderer to React as that feels a bit like a pattern setting even though it's UI
|
||||
*/
|
||||
Swizzled.methods.objUpdate(newState, 'ui', { ...newState.ui, renderer: 'react' })
|
||||
return newState
|
||||
}),
|
||||
clearAll: () => setState(Swizzled.config.initialState),
|
||||
/*
|
||||
* These are setters for the ephemeral state which is passed down as part of the
|
||||
* state object, but is not managed in the state backend because it's ephemeral
|
||||
*/
|
||||
startLoading: (id, conf = {}) =>
|
||||
setEphemeralState((cur) => {
|
||||
const newState = { ...cur }
|
||||
if (typeof newState.loading !== 'object') newState.loading = {}
|
||||
if (typeof conf.color === 'undefined') conf.color = 'info'
|
||||
newState.loading[id] = {
|
||||
msg: Swizzled.methods.t('pe:genericLoadingMsg'),
|
||||
...conf,
|
||||
}
|
||||
return newState
|
||||
}),
|
||||
stopLoading: (id) =>
|
||||
setEphemeralState((cur) => {
|
||||
const newState = { ...cur }
|
||||
if (typeof newState.loading[id] !== 'undefined') delete newState.loading[id]
|
||||
return newState
|
||||
}),
|
||||
clearLoading: () => setEphemeralState((cur) => ({ ...cur, loading: {} })),
|
||||
notify: (conf, id = false) =>
|
||||
setEphemeralState((cur) => {
|
||||
const newState = { ...cur }
|
||||
/*
|
||||
* Passing in an id allows making sure the same notification is not repeated
|
||||
* So if the id is set, and we have a loading state with that id, we just return
|
||||
*/
|
||||
if (id && cur.loading?.[id]) return newState
|
||||
if (typeof newState.loading !== 'object') newState.loading = {}
|
||||
if (id === false) id = Date.now()
|
||||
newState.loading[id] = { ...conf, id, fadeTimer: Swizzled.config.notifyTimeout }
|
||||
return newState
|
||||
}),
|
||||
notifySuccess: (msg, id = false) =>
|
||||
setEphemeralState((cur) => {
|
||||
const newState = { ...cur }
|
||||
/*
|
||||
* Passing in an id allows making sure the same notification is not repeated
|
||||
* So if the id is set, and we have a loading state with that id, we just return
|
||||
*/
|
||||
if (id && cur.loading?.[id]) return newState
|
||||
if (typeof newState.loading !== 'object') newState.loading = {}
|
||||
if (id === false) id = Date.now()
|
||||
newState.loading[id] = {
|
||||
msg,
|
||||
icon: 'success',
|
||||
color: 'success',
|
||||
id,
|
||||
fadeTimer: Swizzled.config.notifyTimeout,
|
||||
}
|
||||
return newState
|
||||
}),
|
||||
notifyFailure: (msg, id = false) =>
|
||||
setEphemeralState((cur) => {
|
||||
const newState = { ...cur }
|
||||
/*
|
||||
* Passing in an id allows making sure the same notification is not repeated
|
||||
* So if the id is set, and we have a loading state with that id, we just return
|
||||
*/
|
||||
if (id && cur.loading?.[id]) return newState
|
||||
if (typeof newState.loading !== 'object') newState.loading = {}
|
||||
if (id === false) id = Date.now()
|
||||
newState.loading[id] = {
|
||||
msg,
|
||||
icon: 'failure',
|
||||
color: 'error',
|
||||
id,
|
||||
fadeTimer: Swizzled.config.notifyTimeout,
|
||||
}
|
||||
return newState
|
||||
}),
|
||||
fadeNotify: (id) =>
|
||||
setEphemeralState((cur) => {
|
||||
const newState = { ...cur }
|
||||
newState.loading[id] = { ...newState.loading[id], clearTimer: 600, id, fading: true }
|
||||
delete newState.loading[id].fadeTimer
|
||||
return newState
|
||||
}),
|
||||
clearNotify: (id) =>
|
||||
setEphemeralState((cur) => {
|
||||
const newState = { ...cur }
|
||||
if (typeof newState.loading[id] !== 'undefined') delete newState.loading[id]
|
||||
return newState
|
||||
}),
|
||||
}
|
||||
}
|
||||
/*
|
||||
* Returns the URL of a cloud-hosted image (cloudflare in this case) based on the ID and Variant
|
||||
* @param {object} Swizzled - Swizzled code, not used here
|
||||
*/
|
||||
export function cloudImageUrl(Swizzled, { id = 'default-avatar', variant = 'public' }) {
|
||||
/*
|
||||
* Return something default so that people will actually change it
|
||||
*/
|
||||
if (!id || id === 'default-avatar') return Swizzled.config.cloudImageDflt
|
||||
|
||||
/*
|
||||
* If the variant is invalid, set it to the smallest thumbnail so
|
||||
* people don't load enourmous images by accident
|
||||
*/
|
||||
if (!Swizzled.config.cloudImageVariants.includes(variant)) variant = 'sq100'
|
||||
|
||||
return `${Swizzled.config.cloudImageUrl}${id}/${variant}`
|
||||
}
|
||||
/**
|
||||
* This method does nothing. It is used to disable certain methods
|
||||
* that need to be passed it to work
|
||||
*
|
||||
* @return {null} null - null
|
||||
*/
|
||||
export function noop() {
|
||||
return null
|
||||
}
|
||||
/*
|
||||
* A method that check that a value is not empty
|
||||
*/
|
||||
export function notEmpty(value) {
|
||||
return String(value).length > 0
|
||||
}
|
||||
/**
|
||||
* Helper method to merge arrays of translation namespaces
|
||||
*
|
||||
* Note that this method is variadic
|
||||
*
|
||||
* @param {object} methods - An object holding possibly swizzled methods (unused here)
|
||||
* @param {[string]} namespaces - A string or array of strings of namespaces
|
||||
* @return {[string]} namespaces - A merged array of all namespaces
|
||||
*/
|
||||
export function nsMerge(Swizzled, ...args) {
|
||||
const ns = new Set()
|
||||
for (const arg of args) {
|
||||
if (typeof arg === 'string') ns.add(arg)
|
||||
else if (Array.isArray(arg)) {
|
||||
for (const el of nsMerge(...arg)) ns.add(el)
|
||||
}
|
||||
}
|
||||
|
||||
return [...ns]
|
||||
}
|
||||
/*
|
||||
* A translation fallback method in case none is passed in
|
||||
*
|
||||
* @param {object} Swizzled - Swizzled code, not used here
|
||||
* @param {string} key - The input
|
||||
* @return {string} key - The input is returned
|
||||
*/
|
||||
export function t(Swizzled, key) {
|
||||
return Array.isArray(key) ? key[0] : key
|
||||
}
|
||||
export function settingsValueIsCustom(Swizzled, val, dflt) {
|
||||
return typeof val === 'undefined' || val === '__UNSET__' || val === dflt ? false : true
|
||||
}
|
||||
|
||||
export function settingsValueCustomOrDefault(Swizzled, val, dflt) {
|
||||
return typeof val === 'undefined' || val === '__UNSET__' || val === dflt ? dflt : val
|
||||
}
|
|
@ -0,0 +1,245 @@
|
|||
/*
|
||||
* 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) {
|
||||
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)
|
||||
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)
|
||||
if (oType === 'list') return typeof value === 'undefined' ? option.dflt : value
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* format a value to the nearest fraction with a denominator that is a power of 2
|
||||
* or a decimal if the value is between fractions
|
||||
* NOTE: this method does not convert mm to inches. It will turn any given value directly into its equivalent fractional representation
|
||||
*
|
||||
* 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') {
|
||||
let negative = ''
|
||||
let inches = ''
|
||||
let rest = ''
|
||||
if (fraction < 0) {
|
||||
fraction = fraction * -1
|
||||
negative = '-'
|
||||
}
|
||||
if (Math.abs(fraction) < 1) rest = fraction
|
||||
else {
|
||||
inches = Math.floor(fraction)
|
||||
rest = fraction - inches
|
||||
}
|
||||
let fraction128 = Math.round(rest * 128)
|
||||
if (fraction128 == 0)
|
||||
return Swizzled.methods.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 (
|
||||
negative +
|
||||
Math.round(fraction * 100) / 100 +
|
||||
(format === 'html' || format === 'notags' ? '"' : '')
|
||||
)
|
||||
}
|
||||
|
||||
// Formatting for imperial values
|
||||
export function formatImperial(Swizzled, neg, inch, numo = false, deno = false, format = 'html') {
|
||||
if (format === 'html') {
|
||||
if (numo) return `${neg}${inch} <sup>${numo}</sup>/<sub>${deno}</sub>"`
|
||||
else return `${neg}${inch}"`
|
||||
} else if (format === 'notags') {
|
||||
if (numo) return `${neg}${inch} ${numo}/${deno}"`
|
||||
else return `${neg}${inch}"`
|
||||
} else {
|
||||
if (numo) return `${neg}${inch} ${numo}/${deno}`
|
||||
else return `${neg}${inch}`
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
if (units === 'imperial' || units === true) {
|
||||
if (val == 0) return Swizzled.methods.formatImperial('', 0, false, false, format)
|
||||
|
||||
let fraction = val / 25.4
|
||||
return Swizzled.methods.formatFraction128(fraction, format)
|
||||
} else {
|
||||
if (format === 'html' || format === 'notags') return Swizzled.methods.roundMm(val / 10) + 'cm'
|
||||
else return Swizzled.methods.roundMm(val / 10)
|
||||
}
|
||||
}
|
||||
|
||||
// Format a percentage (as in, between 0 and 1)
|
||||
export function formatPercentage(Swizzled, 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
|
||||
*/
|
||||
export function round(methods, val, decimals = 1) {
|
||||
return Math.round(val * Math.pow(10, decimals)) / Math.pow(10, decimals)
|
||||
}
|
||||
|
||||
// Rounds a value in mm
|
||||
export function roundMm(Swizzled, val, units) {
|
||||
if (units === 'imperial') return Math.round(val * 1000000) / 1000000
|
||||
else return Math.round(val * 10) / 10
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
// if it's just a number, return it
|
||||
if (!isNaN(value)) return value
|
||||
|
||||
// keep a running total
|
||||
let total = 0
|
||||
|
||||
// split by spaces
|
||||
let chunks = String(value).split(' ')
|
||||
if (chunks.length > 2) return Number.NaN // too many spaces to parse
|
||||
|
||||
// a whole number with a fraction
|
||||
if (chunks.length === 2) {
|
||||
// shift the whole number from the array
|
||||
const whole = Number(chunks.shift())
|
||||
// if it's not a number, return NaN
|
||||
if (isNaN(whole)) return Number.NaN
|
||||
// otherwise add it to the total
|
||||
total += whole
|
||||
}
|
||||
|
||||
// now we have only one chunk to parse
|
||||
let fraction = chunks[0]
|
||||
|
||||
// split it to get numerator and denominator
|
||||
let fChunks = fraction.trim().split('/')
|
||||
// not really a fraction. return NaN
|
||||
if (fChunks.length !== 2 || fChunks[1] === '') return Number.NaN
|
||||
|
||||
// do the division
|
||||
let num = Number(fChunks[0])
|
||||
let denom = Number(fChunks[1])
|
||||
if (isNaN(num) || isNaN(denom)) return NaN
|
||||
return total + num / denom
|
||||
}
|
||||
|
||||
/**
|
||||
* 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') {
|
||||
if (typeof value === 'number') return value * (units === 'imperial' ? 25.4 : 10)
|
||||
|
||||
if (String(value).endsWith('.')) return false
|
||||
|
||||
if (units === 'metric') {
|
||||
value = Number(value)
|
||||
if (isNaN(value)) return false
|
||||
return value * 10
|
||||
} else {
|
||||
const decimal = Swizzled.methods.fractionToDecimal(value)
|
||||
if (isNaN(decimal)) return false
|
||||
return decimal * 24.5
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 shortDate(locale = 'en', timestamp = false, withTime = true) {
|
||||
const options = {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
}
|
||||
if (withTime) {
|
||||
options.hour = '2-digit'
|
||||
options.minute = '2-digit'
|
||||
options.hour12 = false
|
||||
}
|
||||
const ts = timestamp ? new Date(timestamp) : new Date()
|
||||
|
||||
return ts.toLocaleDateString(locale, options)
|
||||
}
|
||||
/*
|
||||
* 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) {
|
||||
// No input is not valid
|
||||
if (!val) return false
|
||||
|
||||
// Cast to string, and replace comma with period
|
||||
val = val.toString().trim().replace(',', '.')
|
||||
|
||||
// Regex pattern for regular numbers with decimal seperator or fractions
|
||||
const regex = imperial
|
||||
? /^-?[0-9]*(\s?[0-9]+\/|[.])?[0-9]+$/ // imperial (fractions)
|
||||
: /^-?[0-9]*[.]?[0-9]+$/ // metric (no fractions)
|
||||
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)
|
||||
|
||||
return isNaN(val) ? false : Number(val)
|
||||
}
|
174
packages/react-components/src/editor/swizzle/methods/index.mjs
Normal file
174
packages/react-components/src/editor/swizzle/methods/index.mjs
Normal file
|
@ -0,0 +1,174 @@
|
|||
/*************************************************************************
|
||||
* *
|
||||
* FreeSewing's pattern editor allows swizzling methods *
|
||||
* *
|
||||
* To 'swizzle' means to replace the default implementation of a *
|
||||
* method with a custom one. It allows one to customize *
|
||||
* the pattern editor. *
|
||||
* *
|
||||
* This file holds the 'swizzleMethods' method that will return *
|
||||
* the various methods that can be swizzled, or their default *
|
||||
* implementation. *
|
||||
* *
|
||||
* To use a custom version, simply pas it as a prop into the editor *
|
||||
* under the 'methods' key. So to pass a custom 't' method (used for *
|
||||
* translation(, you do: *
|
||||
* *
|
||||
* <PatternEditor methods={{ t: myCustomTranslationMethod }} /> *
|
||||
* *
|
||||
*************************************************************************/
|
||||
|
||||
/*
|
||||
* Import of methods that can be swizzled
|
||||
*/
|
||||
import {
|
||||
defaultSa,
|
||||
defaultSamm,
|
||||
menuCoreSettingsOnlyHandler,
|
||||
menuCoreSettingsSaboolHandler,
|
||||
menuCoreSettingsSammHandler,
|
||||
menuCoreSettingsStructure,
|
||||
} from './core-settings.mjs'
|
||||
import {
|
||||
designOptionType,
|
||||
findOption,
|
||||
getOptionStructure,
|
||||
menuDesignOptionsStructure,
|
||||
} from './design-options.mjs'
|
||||
import {
|
||||
addUndoStep,
|
||||
cloneObject,
|
||||
cloudImageUrl,
|
||||
draft,
|
||||
flattenFlags,
|
||||
getCoreSettingUndoStepData,
|
||||
getDesignOptionUndoStepData,
|
||||
getUiPreferenceUndoStepData,
|
||||
getUndoStepData,
|
||||
initialEditorState,
|
||||
menuRoundPct,
|
||||
menuValidateNumericValue,
|
||||
menuValueWasChanged,
|
||||
noop,
|
||||
notEmpty,
|
||||
nsMerge,
|
||||
objUpdate,
|
||||
settingsValueIsCustom,
|
||||
settingsValueCustomOrDefault,
|
||||
statePrefixPath,
|
||||
stateUpdateFactory,
|
||||
t,
|
||||
undoableObjUpdate,
|
||||
} from './editor.mjs'
|
||||
import {
|
||||
capitalize,
|
||||
formatDesignOptionValue,
|
||||
formatFraction128,
|
||||
formatImperial,
|
||||
formatMm,
|
||||
formatPercentage,
|
||||
round,
|
||||
roundMm,
|
||||
fractionToDecimal,
|
||||
measurementAsMm,
|
||||
measurementAsUnits,
|
||||
shortDate,
|
||||
parseDistanceInput,
|
||||
} from './formatting.mjs'
|
||||
import {
|
||||
designMeasurements,
|
||||
hasRequiredMeasurements,
|
||||
isDegreeMeasurement,
|
||||
missingMeasurements,
|
||||
structureMeasurementsAsDesign,
|
||||
} from './measurements.mjs'
|
||||
import { menuUiPreferencesStructure } from './ui-preferences.mjs'
|
||||
|
||||
/**
|
||||
* This object holds all methods that can be swizzled
|
||||
*/
|
||||
const defaultMethods = {
|
||||
// core-settings.mjs
|
||||
defaultSa,
|
||||
defaultSamm,
|
||||
menuCoreSettingsOnlyHandler,
|
||||
menuCoreSettingsSaboolHandler,
|
||||
menuCoreSettingsSammHandler,
|
||||
menuCoreSettingsStructure,
|
||||
// design-options.mjs
|
||||
designOptionType,
|
||||
findOption,
|
||||
getOptionStructure,
|
||||
menuDesignOptionsStructure,
|
||||
// editor.mjs
|
||||
addUndoStep,
|
||||
cloneObject,
|
||||
cloudImageUrl,
|
||||
draft,
|
||||
flattenFlags,
|
||||
getCoreSettingUndoStepData,
|
||||
getDesignOptionUndoStepData,
|
||||
getUiPreferenceUndoStepData,
|
||||
getUndoStepData,
|
||||
initialEditorState,
|
||||
menuRoundPct,
|
||||
menuValidateNumericValue,
|
||||
menuValueWasChanged,
|
||||
noop,
|
||||
notEmpty,
|
||||
nsMerge,
|
||||
objUpdate,
|
||||
settingsValueIsCustom,
|
||||
settingsValueCustomOrDefault,
|
||||
statePrefixPath,
|
||||
stateUpdateFactory,
|
||||
t,
|
||||
undoableObjUpdate,
|
||||
// formatting.mjs
|
||||
capitalize,
|
||||
formatDesignOptionValue,
|
||||
formatFraction128,
|
||||
formatImperial,
|
||||
formatMm,
|
||||
formatPercentage,
|
||||
round,
|
||||
roundMm,
|
||||
fractionToDecimal,
|
||||
measurementAsMm,
|
||||
measurementAsUnits,
|
||||
shortDate,
|
||||
parseDistanceInput,
|
||||
// measurements.mjs
|
||||
designMeasurements,
|
||||
hasRequiredMeasurements,
|
||||
isDegreeMeasurement,
|
||||
missingMeasurements,
|
||||
structureMeasurementsAsDesign,
|
||||
// ui-preferences.mjs
|
||||
menuUiPreferencesStructure,
|
||||
}
|
||||
|
||||
/*
|
||||
* This method returns methods that can be swizzled
|
||||
* So either the passed-in methods, or the default ones
|
||||
*/
|
||||
export const swizzleMethods = (methods, Swizzled) => {
|
||||
/*
|
||||
* We need to pass down the resulting methods, swizzled or not
|
||||
* because some methods rely on other (possibly swizzled) methods.
|
||||
* So we put this in this object so we can pass that down
|
||||
*/
|
||||
const all = {}
|
||||
for (const [name, method] of Object.entries(defaultMethods)) {
|
||||
if (typeof method !== 'function')
|
||||
console.warn(`${name} is not defined as default method in swizzleMethods`)
|
||||
all[name] = methods[name]
|
||||
? (...params) => methods[name](all, ...params)
|
||||
: (...params) => method(Swizzled, ...params)
|
||||
}
|
||||
|
||||
/*
|
||||
* Return all methods
|
||||
*/
|
||||
return all
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* Returns a list of measurements for a design
|
||||
*
|
||||
* @param {object} Swizzled - Swizzled code, not used here
|
||||
* @param {object} Design - The Design object
|
||||
* @param {object} measies - The current set of measurements
|
||||
* @return {object} measurements - Object holding measurements that are relevant for this design
|
||||
*/
|
||||
export function designMeasurements(Swizzled, Design, measies = {}) {
|
||||
const measurements = {}
|
||||
for (const m of Design.patternConfig?.measurements || []) measurements[m] = measies[m]
|
||||
for (const m of Design.patternConfig?.optionalMeasurements || []) measurements[m] = measies[m]
|
||||
|
||||
return measurements
|
||||
}
|
||||
/**
|
||||
* Helper method to determine whether all required measurements for a design are present
|
||||
*
|
||||
* @param {object} Swizzled - Swizzled code, including methods
|
||||
* @param {object} Design - The FreeSewing design (or a plain object holding measurements)
|
||||
* @param {object} measurements - An object holding the user's measurements
|
||||
* @return {array} result - An array where the first element is true when we
|
||||
* have all measurements, and false if not. The second element is a list of
|
||||
* missing measurements.
|
||||
*/
|
||||
export function hasRequiredMeasurements(Swizzled, Design, measurements = {}) {
|
||||
/*
|
||||
* If design is just a plain object holding measurements, we restructure it as a Design
|
||||
* AS it happens, this method is smart enough to test for this, so we call it always
|
||||
*/
|
||||
Design = Swizzled.methods.structureMeasurementsAsDesign(Design)
|
||||
|
||||
/*
|
||||
* 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 {object} Swizzled - Swizzled code, not used here
|
||||
* @param {string} name - The name of the measurement
|
||||
* @return {bool} isDegree - True if the measurment is a degree measurement
|
||||
*/
|
||||
export function isDegreeMeasurement(Swizzled, name) {
|
||||
return Swizzled.config.degreeMeasurements.indexOf(name) !== -1
|
||||
}
|
||||
/*
|
||||
* Helper method to check whether measururements are missing
|
||||
*
|
||||
* Note that this does not actually check the settings against
|
||||
* the chose design, but rather relies on the missing measurements
|
||||
* being set in state. That's because checking is more expensive,
|
||||
* so we do it only once in the non-Swizzled ViewWrapper components
|
||||
*
|
||||
* @param {object} Swizzled - Object holding Swizzled code
|
||||
* @param {object } state - The Editor state
|
||||
* @return {bool} missing - True if there are missing measurments, false if not
|
||||
*/
|
||||
export function missingMeasurements(Swizzled, state) {
|
||||
return (
|
||||
!Swizzled.config.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} Swizzled - Swizzled code, not used here
|
||||
* @param {object} measurements - The POJO of measurments
|
||||
* @return {object} design - The measurements structured as a design object
|
||||
*/
|
||||
export function structureMeasurementsAsDesign(Swizzled, measurements) {
|
||||
return measurements.patternConfig ? measurements : { patternConfig: { measurements } }
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
export function menuUiPreferencesStructure(Swizzled) {
|
||||
const uiUx = Swizzled.config.uxLevels.ui
|
||||
const uiPreferences = {
|
||||
ux: {
|
||||
ux: uiUx.ux,
|
||||
emoji: '🖥️',
|
||||
list: [1, 2, 3, 4, 5],
|
||||
choiceTitles: {},
|
||||
icon: Swizzled.components.UxIcon,
|
||||
dflt: Swizzled.config.defaultUx,
|
||||
},
|
||||
aside: {
|
||||
ux: uiUx.aside,
|
||||
list: [0, 1],
|
||||
choiceTitles: {
|
||||
0: 'pe:noAside',
|
||||
1: 'pe:withAside',
|
||||
},
|
||||
dflt: 1,
|
||||
icon: Swizzled.components.MenuIcon,
|
||||
},
|
||||
kiosk: {
|
||||
ux: uiUx.kiosk,
|
||||
list: [0, 1],
|
||||
choiceTitles: {
|
||||
0: 'pe:websiteMode',
|
||||
1: 'pe:kioskMode',
|
||||
},
|
||||
dflt: 0,
|
||||
icon: Swizzled.components.KioskIcon,
|
||||
},
|
||||
rotate: {
|
||||
ux: uiUx.rotate,
|
||||
list: [0, 1],
|
||||
choiceTitles: {
|
||||
0: 'pe:rotateNo',
|
||||
1: 'pe:rotateYes',
|
||||
},
|
||||
dflt: 0,
|
||||
icon: Swizzled.components.RotateIcon,
|
||||
},
|
||||
renderer: {
|
||||
ux: uiUx.renderer,
|
||||
list: ['react', 'svg'],
|
||||
choiceTitles: {
|
||||
react: 'pe:renderWithReact',
|
||||
svg: 'pe:renderWithCore',
|
||||
},
|
||||
valueTitles: {
|
||||
react: 'React',
|
||||
svg: 'SVG',
|
||||
},
|
||||
dflt: 'react',
|
||||
icon: Swizzled.components.RocketIcon,
|
||||
},
|
||||
}
|
||||
|
||||
uiPreferences.ux.list.forEach((i) => (uiPreferences.ux.choiceTitles[i] = 'pe:ux' + i))
|
||||
return uiPreferences
|
||||
}
|
4
packages/react-components/src/editor/utils.mjs
Normal file
4
packages/react-components/src/editor/utils.mjs
Normal file
|
@ -0,0 +1,4 @@
|
|||
/*
|
||||
* To spread icon + text horizontal
|
||||
*/
|
||||
export const horFlexClasses = 'flex flex-row items-center justify-between gap-4 w-full'
|
|
@ -1,45 +1,47 @@
|
|||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||
// Components
|
||||
// Pattern
|
||||
import { Pattern as PatternComponent } from './pattern/index.mjs'
|
||||
import { Svg as SvgComponent } from './pattern/svg.mjs'
|
||||
import { Defs as DefsComponent } from './pattern/defs.mjs'
|
||||
import { Group as GroupComponent } from './pattern/group.mjs'
|
||||
import { Stack as StackComponent } from './pattern/stack.mjs'
|
||||
import { Part as PartComponent } from './pattern/part.mjs'
|
||||
import { Point as PointComponent } from './pattern/point.mjs'
|
||||
import { Snippet as SnippetComponent } from './pattern/snippet.mjs'
|
||||
import { Path as PathComponent } from './pattern/path.mjs'
|
||||
import { Grid as GridComponent } from './pattern/grid.mjs'
|
||||
import { Text as TextComponent, TextOnPath as TextOnPathComponent } from './pattern/text.mjs'
|
||||
// Pattern components
|
||||
import { Defs } from './pattern/defs.mjs'
|
||||
import { Grid } from './pattern/grid.mjs'
|
||||
import { Group } from './pattern/group.mjs'
|
||||
import { Part } from './pattern/part.mjs'
|
||||
import { Path } from './pattern/path.mjs'
|
||||
import { Pattern } from './pattern/index.mjs'
|
||||
import { PatternXray } from './pattern-xray/index.mjs'
|
||||
import { Point } from './pattern/point.mjs'
|
||||
import { Stack } from './pattern/stack.mjs'
|
||||
import { Snippet } from './pattern/snippet.mjs'
|
||||
import { Svg } from './pattern/svg.mjs'
|
||||
import { Text, TextOnPath } from './pattern/text.mjs'
|
||||
// Pattern Utils
|
||||
import { getProps, withinPartBounds, getId, translateStrings } from './pattern/utils.mjs'
|
||||
// PatternXray
|
||||
import { PatternXray as PatternXrayComponent } from './pattern-xray/index.mjs'
|
||||
import { getId, getProps, translateStrings, withinPartBounds } from './pattern/utils.mjs'
|
||||
|
||||
/**
|
||||
* Translation namespaces used by these components
|
||||
*/
|
||||
const ns = ['editor']
|
||||
|
||||
/*
|
||||
* Export all components as named exports
|
||||
*/
|
||||
export const Pattern = PatternComponent
|
||||
export const Svg = SvgComponent
|
||||
export const Defs = DefsComponent
|
||||
export const Group = GroupComponent
|
||||
export const Stack = StackComponent
|
||||
export const Part = PartComponent
|
||||
export const Point = PointComponent
|
||||
export const Path = PathComponent
|
||||
export const Snippet = SnippetComponent
|
||||
export const Grid = GridComponent
|
||||
export const Text = TextComponent
|
||||
export const TextOnPath = TextOnPathComponent
|
||||
export const PatternXray = PatternXrayComponent
|
||||
|
||||
/*
|
||||
* Export pattern utils
|
||||
*/
|
||||
export const utils = {
|
||||
getProps,
|
||||
withinPartBounds,
|
||||
export {
|
||||
// Pattern components
|
||||
Pattern,
|
||||
Svg,
|
||||
Defs,
|
||||
Group,
|
||||
Stack,
|
||||
Part,
|
||||
Point,
|
||||
Path,
|
||||
Snippet,
|
||||
Grid,
|
||||
Text,
|
||||
TextOnPath,
|
||||
PatternXray,
|
||||
// These are not React components but pattern helpers
|
||||
getId,
|
||||
getProps,
|
||||
translateStrings,
|
||||
withinPartBounds,
|
||||
// These are not React components but various helpers
|
||||
ns,
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue