1
0
Fork 0

Merge pull request 'feat: Docs for @freesewing/react, the new editor, and other improvements' (#348) from cdocs into develop

Reviewed-on: https://codeberg.org/freesewing/freesewing/pulls/348
This commit is contained in:
Joost De Cock 2025-06-01 17:45:13 +02:00
commit 94d3f4c09f
457 changed files with 17818 additions and 8053 deletions

View file

@ -83,6 +83,7 @@ packageJson:
"./components/Echart": "./components/Echart/index.mjs" "./components/Echart": "./components/Echart/index.mjs"
"./components/Editor": "./components/Editor/index.mjs" "./components/Editor": "./components/Editor/index.mjs"
"./components/Heading": "./components/Heading/index.mjs" "./components/Heading": "./components/Heading/index.mjs"
"./components/Help": "./components/Help/index.mjs"
"./components/Highlight": "./components/Highlight/index.mjs" "./components/Highlight": "./components/Highlight/index.mjs"
"./components/Icon": "./components/Icon/index.mjs" "./components/Icon": "./components/Icon/index.mjs"
"./components/Input": "./components/Input/index.mjs" "./components/Input": "./components/Input/index.mjs"
@ -124,6 +125,7 @@ packageJson:
"./hooks/useControl": "./hooks/useControl/index.mjs" "./hooks/useControl": "./hooks/useControl/index.mjs"
"./hooks/useDesign": "./hooks/useDesign/index.mjs" "./hooks/useDesign": "./hooks/useDesign/index.mjs"
"./hooks/useDesignTranslation": "./hooks/useDesignTranslation/index.mjs" "./hooks/useDesignTranslation": "./hooks/useDesignTranslation/index.mjs"
"./hooks/useFilter": "./hooks/useFilter/index.mjs"
"./hooks/useSelection": "./hooks/useSelection/index.mjs" "./hooks/useSelection": "./hooks/useSelection/index.mjs"
"./hooks/useStateObject": "./hooks/useStateObject/index.mjs" "./hooks/useStateObject": "./hooks/useStateObject/index.mjs"
# Lib # Lib

24
eslint.config.mjs Normal file
View file

@ -0,0 +1,24 @@
import js from '@eslint/js'
import globals from 'globals'
import pluginReact from 'eslint-plugin-react'
import json from '@eslint/json'
import markdown from '@eslint/markdown'
import css from '@eslint/css'
import { defineConfig } from 'eslint/config'
export default defineConfig([
{ files: ['**/*.{js,mjs,cjs,jsx}'], plugins: { js }, extends: ['js/recommended'] },
{
files: ['**/*.{js,mjs,cjs,jsx}'],
languageOptions: { globals: { ...globals.browser, ...globals.node } },
},
pluginReact.configs.flat.recommended,
{ files: ['**/*.json'], plugins: { json }, language: 'json/json', extends: ['json/recommended'] },
{
files: ['**/*.md'],
plugins: { markdown },
language: 'markdown/commonmark',
extends: ['markdown/recommended'],
},
{ files: ['**/*.css'], plugins: { css }, language: 'css/css', extends: ['css/recommended'] },
])

4277
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -61,17 +61,23 @@
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^19.0.3", "@commitlint/cli": "^19.0.3",
"@commitlint/config-conventional": "^19.0.3", "@commitlint/config-conventional": "^19.0.3",
"@eslint/css": "^0.8.1",
"@eslint/js": "^9.27.0",
"@eslint/json": "^0.12.0",
"@eslint/markdown": "^6.4.0",
"@nx/eslint": "20.2.1", "@nx/eslint": "20.2.1",
"all-contributors-cli": "^6.26.1", "all-contributors-cli": "^6.26.1",
"axios": "^1.5.1", "axios": "^1.5.1",
"chalk": "^4.1.0", "chalk": "^4.1.0",
"cross-env": "^7.0.2", "cross-env": "^7.0.2",
"eslint": "^8.23.1", "eslint": "^9.27.0",
"eslint-plugin-jsonc": "^2.4.0", "eslint-plugin-jsonc": "^2.4.0",
"eslint-plugin-markdown": "^5.0.0", "eslint-plugin-markdown": "^5.0.0",
"eslint-plugin-mongo": "^1.0.5", "eslint-plugin-mongo": "^1.0.5",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-yaml": "^0.5.0", "eslint-plugin-yaml": "^0.5.0",
"execa": "^9.3.1", "execa": "^9.3.1",
"globals": "^16.2.0",
"husky": "^9.0.10", "husky": "^9.0.10",
"js-yaml": "^4.0.0", "js-yaml": "^4.0.0",
"lerna": "^8.0.0", "lerna": "^8.0.0",
@ -97,7 +103,6 @@
"@babel/plugin-syntax-import-assertions": "^7.22.5", "@babel/plugin-syntax-import-assertions": "^7.22.5",
"@babel/preset-react": "^7.22.15", "@babel/preset-react": "^7.22.15",
"c8": "^10.1.2", "c8": "^10.1.2",
"eslint-config-next": "^14.0.1",
"glob": "^11.0.1", "glob": "^11.0.1",
"rehype-format": "^5.0.0", "rehype-format": "^5.0.0",
"rehype-stringify": "^10.0.1", "rehype-stringify": "^10.0.1",

View file

@ -1,58 +1,45 @@
// Dependencies // Dependencies
import { DateTime } from 'luxon' import { DateTime } from 'luxon'
import orderBy from 'lodash/orderBy.js' import orderBy from 'lodash/orderBy.js'
import { capitalize, shortDate } from '@freesewing/utils' import { shortDate } from '@freesewing/utils'
import { apikeyLevels } from '@freesewing/config' import { apikeyLevels } from '@freesewing/config'
// Context // Context
import { ModalContext } from '@freesewing/react/context/Modal' import { ModalContext } from '@freesewing/react/context/Modal'
import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus' import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus'
// Hooks // Hooks
import React, { useState, useEffect, useContext } from 'react' import React, { useState, useEffect, useContext } from 'react'
import { useAccount } from '@freesewing/react/hooks/useAccount' import { useAccount } from '@freesewing/react/hooks/useAccount'
import { useBackend } from '@freesewing/react/hooks/useBackend' import { useBackend } from '@freesewing/react/hooks/useBackend'
import { useSelection } from '@freesewing/react/hooks/useSelection' import { useSelection } from '@freesewing/react/hooks/useSelection'
// Components // Components
import { TableWrapper } from '@freesewing/react/components/Table' import { TableWrapper } from '@freesewing/react/components/Table'
import { Link as WebLink } from '@freesewing/react/components/Link' import { Link as WebLink } from '@freesewing/react/components/Link'
import { import { PlusIcon, RightIcon, TrashIcon } from '@freesewing/react/components/Icon'
BoolNoIcon,
BoolYesIcon,
PlusIcon,
RightIcon,
TrashIcon,
} from '@freesewing/react/components/Icon'
import { Uuid } from '@freesewing/react/components/Uuid' import { Uuid } from '@freesewing/react/components/Uuid'
import { Popout } from '@freesewing/react/components/Popout' import { Popout } from '@freesewing/react/components/Popout'
import { ModalWrapper } from '@freesewing/react/components/Modal' import { ModalWrapper } from '@freesewing/react/components/Modal'
import { NumberCircle } from '@freesewing/react/components/Number' import { NumberCircle } from '@freesewing/react/components/Number'
import { StringInput, FormControl, ListInput } from '@freesewing/react/components/Input' import { StringInput, Fieldset, ListInput } from '@freesewing/react/components/Input'
import { DisplayRow } from './shared.mjs' import { CopyToClipboardButton } from '@freesewing/react/components/Button'
import { CopyToClipboardButton } from '@freesewing/react/components/CopyToClipboardButton'
import { TimeAgo, TimeToGo } from '@freesewing/react/components/Time' import { TimeAgo, TimeToGo } from '@freesewing/react/components/Time'
import { KeyVal } from '@freesewing/react/components/KeyVal' import { KeyVal } from '@freesewing/react/components/KeyVal'
const t = (input) => {
console.log('t called', input)
return input
}
const fields = { const fields = {
id: 'Key', id: 'Key',
name: 'Name', name: 'Name',
calls: 'Calls', calls: 'Calls',
level: 'Level', level: 'Level',
level: 'Level',
createdAt: 'Created', createdAt: 'Created',
expiresAt: 'Expires', expiresAt: 'Expires',
} }
/* /**
* Component for the account/apikeys page * A component to mange the user's API keys
* *
* @params {object} props - All React props * @component
* @params {function} Link - A framework specific Link component for client-side routing * @param {object} props - All component props
* @param {JSX.Element} [props.Link] - An optional framework-specific Link component
* @returns {JSX.Element}
*/ */
export const Apikeys = ({ Link = false }) => { export const Apikeys = ({ Link = false }) => {
if (!Link) Link = WebLink if (!Link) Link = WebLink
@ -240,13 +227,6 @@ const NewApikey = ({ onCreate = false }) => {
} else setLoadingStatus([true, 'An error occured. Please report this', true, false]) } else setLoadingStatus([true, 'An error occured. Please report this', true, false])
} }
const clear = () => {
setApikey(false)
setGenerate(false)
setName('')
setLevel(1)
}
return ( return (
<div className="tw:w-full"> <div className="tw:w-full">
<h2>New API key {apikey ? `: ${apikey.name}` : ''}</h2> <h2>New API key {apikey ? `: ${apikey.name}` : ''}</h2>
@ -311,7 +291,7 @@ const ShowNewApikey = ({ apikey }) => (
<CopyToClipboardButton sup content={apikey.secret} label="API key secret" /> <CopyToClipboardButton sup content={apikey.secret} label="API key secret" />
</h6> </h6>
<pre>{apikey.secret}</pre> <pre>{apikey.secret}</pre>
<Popout warning compact> <Popout type="warning" compact>
This is the only time you can see the key secret, make sure to copy it. This is the only time you can see the key secret, make sure to copy it.
</Popout> </Popout>
</div> </div>
@ -331,7 +311,7 @@ const ExpiryPicker = ({ expires, setExpires }) => {
return ( return (
<div className="tw:flex tw:flex-row tw:gap-2 tw:items-center"> <div className="tw:flex tw:flex-row tw:gap-2 tw:items-center">
<FormControl <Fieldset
label="Key Expiry" label="Key Expiry"
labelBL={shortDate(expires)} labelBL={shortDate(expires)}
labelBR={<TimeToGo iso={expires} />} labelBR={<TimeToGo iso={expires} />}
@ -344,7 +324,7 @@ const ExpiryPicker = ({ expires, setExpires }) => {
className="tw:daisy-range tw:daisy-range-secondary tw:w-full" className="tw:daisy-range tw:daisy-range-secondary tw:w-full"
onChange={update} onChange={update}
/> />
</FormControl> </Fieldset>
</div> </div>
) )
} }

View file

@ -17,12 +17,14 @@ import { PassiveImageInput } from '@freesewing/react/components/Input'
import { IconButton } from '@freesewing/react/components/Button' import { IconButton } from '@freesewing/react/components/Button'
import { WelcomeIcons } from './shared.mjs' import { WelcomeIcons } from './shared.mjs'
/* /**
* Component for the account/bio page * Component to manage the user's Avatar
* *
* @params {object} props - All React props * @component
* @params {bool} props.welcome - Set to true to use this component on the welcome page * @param {object} props - All component props
* @params {function} props.Link - A framework specific Link component for client-side routing * @param {boolean} [props.welcome = false] - Set to true to render the welcome/onboarding flow
* @param {function} props.Link - A framework specific Link component for client-side routing
* @returns {JSX.Element}
*/ */
export const Avatar = ({ welcome = false, Link = false }) => { export const Avatar = ({ welcome = false, Link = false }) => {
if (!Link) Link = WebLink if (!Link) Link = WebLink
@ -98,7 +100,7 @@ export const Avatar = ({ welcome = false, Link = false }) => {
<> <>
<p className="tw:text-right"> <p className="tw:text-right">
<button <button
className="tw:daisy-btn tw:daisy-btn-primary tw:w-full tw:lg:w-auto mt-8" className="tw:daisy-btn tw:daisy-btn-primary tw:w-full tw:lg:w-auto tw:mt-4"
onClick={save} onClick={save}
> >
<SaveIcon /> Save Avatar <SaveIcon /> Save Avatar

View file

@ -16,12 +16,14 @@ import { MarkdownInput } from '@freesewing/react/components/Input'
import { IconButton } from '@freesewing/react/components/Button' import { IconButton } from '@freesewing/react/components/Button'
import { WelcomeIcons } from './shared.mjs' import { WelcomeIcons } from './shared.mjs'
/* /**
* Component for the account/bio page * Component to manage the user's Bio
* *
* @params {object} props - All React props * @component
* @params {bool} props.welcome - Set to true to use this component on the welcome page * @param {object} props - All component props
* @params {function} props.Link - A framework specific Link component for client-side routing * @param {boolean} [props.welcome = false] - Set to true to render the welcome/onboarding flow
* @param {React.Component} props.Link - A framework specific Link component for client-side routing
* @returns {JSX.Element}
*/ */
export const Bio = ({ welcome = false, Link = false }) => { export const Bio = ({ welcome = false, Link = false }) => {
if (!Link) Link = WebLink if (!Link) Link = WebLink

View file

@ -7,7 +7,7 @@ import { useBackend } from '@freesewing/react/hooks/useBackend'
import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus' import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus'
import { ModalContext } from '@freesewing/react/context/Modal' import { ModalContext } from '@freesewing/react/context/Modal'
// Components // Components
import { BookmarkIcon, LeftIcon, PlusIcon, TrashIcon } from '@freesewing/react/components/Icon' import { BookmarkIcon, PlusIcon, TrashIcon } from '@freesewing/react/components/Icon'
import { Link as WebLink } from '@freesewing/react/components/Link' import { Link as WebLink } from '@freesewing/react/components/Link'
import { ModalWrapper } from '@freesewing/react/components/Modal' import { ModalWrapper } from '@freesewing/react/components/Modal'
import { StringInput } from '@freesewing/react/components/Input' import { StringInput } from '@freesewing/react/components/Input'
@ -25,12 +25,15 @@ const types = {
} }
/** /**
* Component for the account/bookmarks page * A component to manage the user's bookmarks
*
* @component
* @returns {JSX.Element}
*/ */
export const Bookmarks = () => { export const Bookmarks = () => {
// Hooks & Context // Hooks & Context
const backend = useBackend() const backend = useBackend()
const { setModal, clearModal } = useContext(ModalContext) const { setModal } = useContext(ModalContext)
const { setLoadingStatus, LoadingProgress } = useContext(LoadingStatusContext) const { setLoadingStatus, LoadingProgress } = useContext(LoadingStatusContext)
// State // State
@ -178,7 +181,7 @@ export const Bookmarks = () => {
* @param {object} props - All the React props * @param {object} props - All the React props
* @param {function} onCreated - An optional method to call when the bookmark is created * @param {function} onCreated - An optional method to call when the bookmark is created
*/ */
export const NewBookmark = ({ onCreated = false }) => { const NewBookmark = ({ onCreated = false }) => {
// Hooks // Hooks
const { setLoadingStatus } = useContext(LoadingStatusContext) const { setLoadingStatus } = useContext(LoadingStatusContext)
const { clearModal } = useContext(ModalContext) const { clearModal } = useContext(ModalContext)
@ -191,7 +194,7 @@ export const NewBookmark = ({ onCreated = false }) => {
// This method will create the bookmark // This method will create the bookmark
const createBookmark = async () => { const createBookmark = async () => {
setLoadingStatus([true, 'Processing update']) setLoadingStatus([true, 'Processing update'])
const [status, body] = await backend.createBookmark({ const [status] = await backend.createBookmark({
title, title,
url, url,
type: 'custom', type: 'custom',
@ -242,13 +245,15 @@ export const NewBookmark = ({ onCreated = false }) => {
) )
} }
/* /**
* A component to add a bookmark from wherever * Component to add a bookmark to the user's account
* *
* @params {object} props - All React props * @component
* @params {string} props.href - The bookmark href * @param {object} props - All component props
* @params {string} props.title - The bookmark title * @param {string} props.slug - The bookmark slug/href
* @params {string} props.type - The bookmark type * @param {string} props.title - The bookmark title
* @param {string} props.type - The bookmark type, one of design, pattern, set, cset, doc, or custom
* @returns {JSX.Element}
*/ */
export const BookmarkButton = ({ slug, type, title }) => { export const BookmarkButton = ({ slug, type, title }) => {
const { setModal } = useContext(ModalContext) const { setModal } = useContext(ModalContext)
@ -279,19 +284,20 @@ export const BookmarkButton = ({ slug, type, title }) => {
/* /*
* A component to create a bookmark, preloaded with props * A component to create a bookmark, preloaded with props
* *
* @params {object} props - All React props * @component
* @params {string} props.href - The bookmark href * @param {object} props - All component props
* @params {string} props.title - The bookmark title * @param {string} props.href - The bookmark href
* @params {string} props.type - The bookmark type * @param {string} props.title - The bookmark title
* * @param {string} props.type - The bookmark type
* @returns {JSX.Element}
*/ */
export const CreateBookmark = ({ type, title, slug }) => { const CreateBookmark = ({ type, title, slug }) => {
const backend = useBackend() const backend = useBackend()
const [name, setName] = useState(title) const [name, setName] = useState(title)
const { setLoadingStatus } = useContext(LoadingStatusContext) const { setLoadingStatus } = useContext(LoadingStatusContext)
const { setModal } = useContext(ModalContext) const { setModal } = useContext(ModalContext)
const url = `/${slug}` const url = slug.toLowerCase().slice(0, 4) === 'http' ? slug : `/${slug}`
const bookmark = async (evt) => { const bookmark = async (evt) => {
evt.stopPropagation() evt.stopPropagation()

View file

@ -1,17 +1,13 @@
// Dependencies // Dependencies
import { welcomeSteps } from './shared.mjs' import { welcomeSteps } from './shared.mjs'
// Context // Context
import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus' import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus'
// Hooks // Hooks
import React, { useState, useContext } from 'react' import React, { useState, useContext } from 'react'
import { useAccount } from '@freesewing/react/hooks/useAccount' import { useAccount } from '@freesewing/react/hooks/useAccount'
import { useBackend } from '@freesewing/react/hooks/useBackend' import { useBackend } from '@freesewing/react/hooks/useBackend'
// Components // Components
import { Link as WebLink } from '@freesewing/react/components/Link' import { NoIcon, OkIcon, RightIcon } from '@freesewing/react/components/Icon'
import { NoIcon, OkIcon, SaveIcon, RightIcon } from '@freesewing/react/components/Icon'
import { ListInput } from '@freesewing/react/components/Input' import { ListInput } from '@freesewing/react/components/Input'
import { IconButton } from '@freesewing/react/components/Button' import { IconButton } from '@freesewing/react/components/Button'
import { WelcomeIcons } from './shared.mjs' import { WelcomeIcons } from './shared.mjs'
@ -31,11 +27,12 @@ const strings = {
}, },
} }
/* /**
* Component for the account/preferences/compare page * A component to manage the user's compare setting
* *
* @params {object} props - All React props * @component
* @params {bool} props.welcome - Set to true to use this component on the welcome page * @params {bool} [props.welcome = false] - Set to true to render the welcome/onboarding view
* @returns {JSX.Element}
*/ */
export const Compare = ({ welcome = false }) => { export const Compare = ({ welcome = false }) => {
// Hooks // Hooks

View file

@ -1,42 +1,23 @@
// Dependencies // Dependencies
import { welcomeSteps } from './shared.mjs' import { navigate } from '@freesewing/utils'
import { linkClasses, navigate } from '@freesewing/utils'
// Context // Context
import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus' import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus'
// Hooks // Hooks
import React, { useState, useContext } from 'react' import React, { useState, useContext } from 'react'
import { useAccount } from '@freesewing/react/hooks/useAccount' import { useAccount } from '@freesewing/react/hooks/useAccount'
import { useBackend } from '@freesewing/react/hooks/useBackend' import { useBackend } from '@freesewing/react/hooks/useBackend'
// Components // Components
import { Link as WebLink } from '@freesewing/react/components/Link' import { Link as WebLink } from '@freesewing/react/components/Link'
import { NoIcon, OkIcon, SaveIcon } from '@freesewing/react/components/Icon'
import { ListInput } from '@freesewing/react/components/Input'
import { Popout } from '@freesewing/react/components/Popout' import { Popout } from '@freesewing/react/components/Popout'
const strings = { /**
yes: { * A component to manage the user's consent setting
title: 'Yes, in case it may help me',
desc:
'Allowing us to compare your measurments to a baseline or others measurements sets ' +
'allows us to detect potential problems in your measurements or patterns.',
},
no: {
title: 'No, never compare',
desc:
'We get it, comparison is the thief of joy. Just be aware that this limits our ability ' +
'to warn you about potential problems in your measurements sets or patterns.',
},
}
/*
* Component for the account/preferences/consent page
* *
* @params {object} props - All React props * @component
* @params {bool} props.signUp - Set to true to use this component on the initial signUp * @param {object} props - All component props
* @param {function} props.Link - An optional framework-specific Link component * @param {bool} [props.signUp = false] - Set to true to use this component on the initial signUp
* @param {function} [props.Link = false] - An optional framework-specific Link component
* @returns {JSX.Element}
*/ */
export const Consent = ({ signUp = false, Link = false, title = false }) => { export const Consent = ({ signUp = false, Link = false, title = false }) => {
if (!Link) Link = WebLink if (!Link) Link = WebLink
@ -77,7 +58,7 @@ export const Consent = ({ signUp = false, Link = false, title = false }) => {
// Helper method to remove the account // Helper method to remove the account
const removeAccount = async () => { const removeAccount = async () => {
setLoadingStatus([true, 'One moment please']) setLoadingStatus([true, 'One moment please'])
const [status, body] = await backend.removeAccount() const [status] = await backend.removeAccount()
if (status === 200) { if (status === 200) {
setLoadingStatus([true, 'All done, farewell', true, true]) setLoadingStatus([true, 'All done, farewell', true, true])
setToken(null) setToken(null)

View file

@ -1,48 +1,27 @@
// Dependencies // Dependencies
import { welcomeSteps } from './shared.mjs' import { welcomeSteps } from './shared.mjs'
import { controlDesc } from '@freesewing/config' import { controlDesc } from '@freesewing/config'
// Context
import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus'
// Hooks // Hooks
import React, { useState, useContext } from 'react' import React from 'react'
import { useAccount } from '@freesewing/react/hooks/useAccount'
import { useBackend } from '@freesewing/react/hooks/useBackend'
import { useControl } from '@freesewing/react/hooks/useControl' import { useControl } from '@freesewing/react/hooks/useControl'
// Components // Components
import { Link as WebLink } from '@freesewing/react/components/Link' import { RightIcon } from '@freesewing/react/components/Icon'
import { RightIcon, NoIcon, OkIcon, SaveIcon } from '@freesewing/react/components/Icon'
import { ListInput } from '@freesewing/react/components/Input' import { ListInput } from '@freesewing/react/components/Input'
import { ControlScore } from '@freesewing/react/components/Control' import { ControlScore } from '@freesewing/react/components/Control'
import { IconButton } from '@freesewing/react/components/Button' import { IconButton } from '@freesewing/react/components/Button'
import { WelcomeIcons } from './shared.mjs' import { WelcomeIcons } from './shared.mjs'
const strings = { /**
1: { * A component to manage the user's control/UX setting
title: 'Keep it as simple as possible',
desc:
'Allowing us to compare your measurments to a baseline or others measurements sets ' +
'allows us to detect potential problems in your measurements or patterns.',
},
2: {
title: 'No, never compare',
desc:
'We get it, comparison is the thief of joy. Just be aware that this limits our ability ' +
'to warn you about potential problems in your measurements sets or patterns.',
},
}
/*
* Component for the account/preferences/control page
* *
* @params {object} props - All React props * @component
* @params {bool} props.welcome - Set to true to use this component on the welcome page * @param {object} props - All component props
* @param {bool} [props.welcome = false] - Set to true to render the welcome/onboarding view
* @returns {JSX.Element}
*/ */
export const Control = ({ welcome = false }) => { export const Control = ({ welcome = false }) => {
// Hooks // Hooks
const { control, updateControl } = useControl() const { control, setControl } = useControl()
// Helper to get the link to the next onboarding step // Helper to get the link to the next onboarding step
const nextHref = welcome const nextHref = welcome
@ -67,7 +46,7 @@ export const Control = ({ welcome = false }) => {
desc: controlDesc[val].desc, desc: controlDesc[val].desc,
}))} }))}
current={control} current={control}
update={updateControl} update={setControl}
/> />
{welcome ? ( {welcome ? (
<> <>

View file

@ -1,15 +1,11 @@
// Dependencies // Dependencies
import { welcomeSteps } from './shared.mjs' import { validateEmail, validateTld, getSearchParam, navigate } from '@freesewing/utils'
import { validateEmail, validateTld, getSearchParam } from '@freesewing/utils'
// Context // Context
import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus' import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus'
// Hooks // Hooks
import React, { useState, useContext, useEffect } from 'react' import React, { useState, useContext, useEffect } from 'react'
import { useAccount } from '@freesewing/react/hooks/useAccount' import { useAccount } from '@freesewing/react/hooks/useAccount'
import { useBackend } from '@freesewing/react/hooks/useBackend' import { useBackend } from '@freesewing/react/hooks/useBackend'
// Components // Components
import { Link as WebLink } from '@freesewing/react/components/Link' import { Link as WebLink } from '@freesewing/react/components/Link'
import { SaveIcon } from '@freesewing/react/components/Icon' import { SaveIcon } from '@freesewing/react/components/Icon'
@ -17,14 +13,15 @@ import { EmailInput } from '@freesewing/react/components/Input'
import { Popout } from '@freesewing/react/components/Popout' import { Popout } from '@freesewing/react/components/Popout'
import { Spinner } from '@freesewing/react/components/Spinner' import { Spinner } from '@freesewing/react/components/Spinner'
/* /**
* Component for the account/bio page * A component to manage the user's email address
* *
* @params {object} props - All React props * @component
* @params {bool} props.welcome - Set to true to use this component on the welcome page * @param {object} props - All component props
* @params {function} props.Link - A framework specific Link component for client-side routing * @param {React.Component} props.Link - A framework specific Link component for client-side routing
* @returns {JSX.Element}
*/ */
export const Email = ({ welcome = false, Link = false }) => { export const Email = ({ Link = false }) => {
if (!Link) Link = WebLink if (!Link) Link = WebLink
// Hooks // Hooks
@ -53,7 +50,7 @@ export const Email = ({ welcome = false, Link = false }) => {
return ( return (
<div className="tw:w-full"> <div className="tw:w-full">
{changed ? ( {changed ? (
<Popout note> <Popout type="note">
<h3>Please confirm this change</h3> <h3>Please confirm this change</h3>
<p> <p>
We have sent an E-mail to your new address to confirm this change. Please click the link We have sent an E-mail to your new address to confirm this change. Please click the link
@ -87,6 +84,14 @@ export const Email = ({ welcome = false, Link = false }) => {
) )
} }
/**
* A component to render the confirmation after changing the user's email
*
* @component
* @param {object} props - All component props
* @param {function} [props.onSuccess = false] - A method to call after the email is changed
* @returns {JSX.Element}
*/
export const EmailChangeConfirmation = ({ onSuccess = false }) => { export const EmailChangeConfirmation = ({ onSuccess = false }) => {
// State // State
const [error, setError] = useState(false) const [error, setError] = useState(false)
@ -94,7 +99,7 @@ export const EmailChangeConfirmation = ({ onSuccess = false }) => {
const [check, setCheck] = useState() const [check, setCheck] = useState()
// Hooks // Hooks
const { setAccount, setToken } = useAccount() const { setAccount } = useAccount()
const backend = useBackend() const backend = useBackend()
// Context // Context
@ -124,7 +129,7 @@ export const EmailChangeConfirmation = ({ onSuccess = false }) => {
}) })
// If it works, store account, which runs the onSuccess handler // If it works, store account, which runs the onSuccess handler
if (body.result === 'success' && body.account) return storeAccount(body) if (status === 200 && body.result === 'success' && body.account) return storeAccount(body)
// If we get here, we're not sure what's wrong // If we get here, we're not sure what's wrong
if (body.error) return setError(body.error) if (body.error) return setError(body.error)
return setError(true) return setError(true)
@ -150,7 +155,7 @@ export const EmailChangeConfirmation = ({ onSuccess = false }) => {
if (!id || !check) if (!id || !check)
return ( return (
<> <>
<h1>One moment pleae</h1> <h1>One moment please</h1>
<Spinner className="tw:w-8 tw:h-8 tw:m-auto tw:animate-spin" /> <Spinner className="tw:w-8 tw:h-8 tw:m-auto tw:animate-spin" />
</> </>
) )

View file

@ -1,19 +1,19 @@
// Context // Context
import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus' import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus'
// Hooks // Hooks
import React, { useState, useContext } from 'react' import React, { useState, useContext } from 'react'
import { useAccount } from '@freesewing/react/hooks/useAccount'
import { useBackend } from '@freesewing/react/hooks/useBackend' import { useBackend } from '@freesewing/react/hooks/useBackend'
// Components // Components
import { Link as WebLink } from '@freesewing/react/components/Link' import { Link as WebLink } from '@freesewing/react/components/Link'
import { DownloadIcon } from '@freesewing/react/components/Icon' import { DownloadIcon } from '@freesewing/react/components/Icon'
import { Popout } from '@freesewing/react/components/Popout' import { Popout } from '@freesewing/react/components/Popout'
import { IconButton } from '@freesewing/react/components/Button' import { IconButton } from '@freesewing/react/components/Button'
/* /**
* Component for the account/actions/export page * A component to manage the user's export account data action
*
* @component
* @returns {JSX.Element}
*/ */
export const Export = () => { export const Export = () => {
// Hooks // Hooks
@ -36,7 +36,7 @@ export const Export = () => {
return ( return (
<div className="tw:max-w-xl"> <div className="tw:max-w-xl">
{link ? ( {link ? (
<Popout link> <Popout type="link">
<h5>Your data was exported and is available for download at the following location:</h5> <h5>Your data was exported and is available for download at the following location:</h5>
<p className="tw:text-lg"> <p className="tw:text-lg">
<WebLink href={link}>{link}</WebLink> <WebLink href={link}>{link}</WebLink>

View file

@ -10,8 +10,11 @@ import { useBackend } from '@freesewing/react/hooks/useBackend'
import { SaveIcon } from '@freesewing/react/components/Icon' import { SaveIcon } from '@freesewing/react/components/Icon'
import { StringInput } from '@freesewing/react/components/Input' import { StringInput } from '@freesewing/react/components/Input'
/* /**
* Component for the account/social/github page * A component to manage the user's Github handle in their account data
*
* @component
* @returns {JSX.Element}
*/ */
export const Github = () => { export const Github = () => {
// Hooks // Hooks

View file

@ -1,22 +1,16 @@
// Hooks // Hooks
import React, { useState } from 'react'
import { useAccount } from '@freesewing/react/hooks/useAccount' import { useAccount } from '@freesewing/react/hooks/useAccount'
// Components /**
import { Link as WebLink } from '@freesewing/react/components/Link' * A component to display the current user's ID
/*
* Component to display a user ID
* *
* @params {object} props - All React props * @component
* @params {function} props.Link - A framework specific Link component for client-side routing * @param {object} props - All component props
* @returns {JSX.Element}
*/ */
export const UserId = ({ Link = false }) => { export const UserId = () => {
if (!Link) Link = WebLink
// Hooks // Hooks
const { account } = useAccount() const { account } = useAccount()
const [id, setId] = useState(account.id)
return id || null return account.id || null
} }

View file

@ -1,24 +1,21 @@
// Dependencies
import yaml from 'js-yaml'
// Context // Context
import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus' import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus'
// Hooks // Hooks
import React, { useState, useContext } from 'react' import React, { useContext } from 'react'
import { useAccount } from '@freesewing/react/hooks/useAccount' import { useAccount } from '@freesewing/react/hooks/useAccount'
import { useBackend } from '@freesewing/react/hooks/useBackend' import { useBackend } from '@freesewing/react/hooks/useBackend'
// Components // Components
import { Link as WebLink } from '@freesewing/react/components/Link'
import { SaveIcon } from '@freesewing/react/components/Icon'
import { FileInput } from '@freesewing/react/components/Input' import { FileInput } from '@freesewing/react/components/Input'
import { Popout } from '@freesewing/react/components/Popout' import { Popout } from '@freesewing/react/components/Popout'
import { Yaml } from '@freesewing/react/components/Yaml' import { Yaml } from '@freesewing/react/components/Yaml'
/* /**
* Component for the account/bio page * A component to manage the importing of a measurements set into a user's account data
* *
* @params {object} props - All React props * @component
* @params {bool} props.welcome - Set to true to use this component on the welcome page * @returns {JSX.Element}
* @params {function} props.Link - A framework specific Link component for client-side routing
*/ */
export const ImportSet = () => { export const ImportSet = () => {
// Hooks // Hooks
@ -42,7 +39,7 @@ export const ImportSet = () => {
if (set.measurements || set.measies) { if (set.measurements || set.measies) {
const name = set.name || 'J. Doe' const name = set.name || 'J. Doe'
setLoadingStatus([true, `Importing ${name}`]) setLoadingStatus([true, `Importing ${name}`])
const [status, body] = await backend.createSet({ const [status] = await backend.createSet({
name: set.name || 'J. Doe', name: set.name || 'J. Doe',
units: set.units || 'metric', units: set.units || 'metric',
notes: set.notes || '', notes: set.notes || '',
@ -77,7 +74,7 @@ export const ImportSet = () => {
multiple: false, multiple: false,
}} }}
/> />
<Popout tip> <Popout type="tip">
<p> <p>
To import a measurement set, you should have a JSON or YAML file that has the following To import a measurement set, you should have a JSON or YAML file that has the following
structure: structure:

View file

@ -1,64 +0,0 @@
// Context
import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus'
// Hooks
import React, { useState, useContext } from 'react'
import { useAccount } from '@freesewing/react/hooks/useAccount'
import { useBackend } from '@freesewing/react/hooks/useBackend'
// Components
import { SaveIcon } from '@freesewing/react/components/Icon'
import { StringInput } from '@freesewing/react/components/Input'
/*
* Component for the account/social/github page
*/
export const Instagram = () => {
// Hooks
const { account, setAccount } = useAccount()
const backend = useBackend()
const { setLoadingStatus } = useContext(LoadingStatusContext)
// State
const [githubUsername, setGithubUsername] = useState(account.data.githubUsername || '')
const [githubEmail, setGithubEmail] = useState(account.data.githubEmail || '')
// Helper method to save changes
const save = async () => {
setLoadingStatus([true, 'Saving bio'])
const [status, body] = await backend.updateAccount({ data: { githubUsername, githubEmail } })
if (status === 200 && body.result === 'success') {
setAccount(body.account)
setLoadingStatus([true, 'GitHub info updated', true, true])
} else setLoadingStatus([true, 'Something went wrong. Please report this', true, true])
}
return (
<div className="tw:w-full">
<StringInput
id="account-github-email"
label="GitHub Email Address"
current={githubEmail}
update={setGithubEmail}
valid={(val) => val.length > 0}
placeholder={'joost@joost.at'}
/>
<StringInput
id="account-github-username"
label="GitHub Username"
current={githubUsername}
update={setGithubUsername}
valid={(val) => val.length > 0}
placeholder={'joostdecock'}
/>
<p className="tw:text-right">
<button
className="tw:daisy-btn tw:daisy-btn-primary tw:w-full tw:lg:w-auto tw:mt-8"
onClick={save}
>
<SaveIcon /> Save
</button>
</p>
</div>
)
}

View file

@ -6,7 +6,7 @@ import React, { useState, useEffect } from 'react'
import { useAccount } from '@freesewing/react/hooks/useAccount' import { useAccount } from '@freesewing/react/hooks/useAccount'
import { useBackend } from '@freesewing/react/hooks/useBackend' import { useBackend } from '@freesewing/react/hooks/useBackend'
// Components // Components
import { Link as DefautLink } from '@freesewing/react/components/Link' import { Link as DefaultLink } from '@freesewing/react/components/Link'
import { ControlScore } from '@freesewing/react/components/Control' import { ControlScore } from '@freesewing/react/components/Control'
import { import {
MeasurementsSetIcon, MeasurementsSetIcon,
@ -93,13 +93,13 @@ const titles = {
const YesNo = ({ check }) => (check ? <BoolYesIcon /> : <BoolNoIcon />) const YesNo = ({ check }) => (check ? <BoolYesIcon /> : <BoolNoIcon />)
const t = (input) => input
/** /**
* The Links component shows all of the links to manage your account * A component to manage the user's Instagram handle in their account data
* *
* @param {object} props - All the React props * @component
* @param {function} Link - A custom Link component, typically the Docusaurus one, but it's optional * @param {object} props - All component props
* @param {React.Component} props.Link - A framework specific Link component for client-side routing
* @returns {JSX.Element}
*/ */
export const Links = ({ Link = false }) => { export const Links = ({ Link = false }) => {
// Use custom Link component if available // Use custom Link component if available

View file

@ -1,30 +1,26 @@
// Dependencies // Dependencies
import { welcomeSteps } from './shared.mjs'
import { horFlexClasses } from '@freesewing/utils' import { horFlexClasses } from '@freesewing/utils'
// Context // Context
import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus' import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus'
// Hooks // Hooks
import React, { useState, useContext } from 'react' import React, { useState, useContext } from 'react'
import { useAccount } from '@freesewing/react/hooks/useAccount' import { useAccount } from '@freesewing/react/hooks/useAccount'
import { useBackend } from '@freesewing/react/hooks/useBackend' import { useBackend } from '@freesewing/react/hooks/useBackend'
// Components // Components
import { Link as WebLink } from '@freesewing/react/components/Link'
import { NoIcon, LockIcon } from '@freesewing/react/components/Icon' import { NoIcon, LockIcon } from '@freesewing/react/components/Icon'
import { PasswordInput } from '@freesewing/react/components/Input' import { PasswordInput } from '@freesewing/react/components/Input'
import { Popout } from '@freesewing/react/components/Popout' import { Popout } from '@freesewing/react/components/Popout'
import { NumberCircle } from '@freesewing/react/components/Number' import { CopyToClipboardButton } from '@freesewing/react/components/Button'
import { CopyToClipboardButton } from '@freesewing/react/components/CopyToClipboardButton'
/* /**
* Component for the account/security/password page * A component to manage the user's MFA settings
* *
* @params {object} props - All React props * @component
* @params {bool} props.welcome - Set to true to use this component on the welcome page * @param {object} props - All component props
* @param {string} [props.title = false] - Whether or not to display a title
* @returns {JSX.Element}
*/ */
export const Mfa = ({ welcome = false, title = true }) => { export const Mfa = ({ title = true }) => {
// Hooks // Hooks
const backend = useBackend() const backend = useBackend()
const { account, setAccount } = useAccount() const { account, setAccount } = useAccount()
@ -199,7 +195,7 @@ export const Mfa = ({ welcome = false, title = true }) => {
<LockIcon /> <LockIcon />
Set up Mult-Factor Authentication Set up Mult-Factor Authentication
</button> </button>
<Popout tip> <Popout type="tip">
<h5>Please consider enabling Two-Factor Authentication</h5> <h5>Please consider enabling Two-Factor Authentication</h5>
<p> <p>
We do not enforce a password policy, but we do recommend you enable Multi-Factor We do not enforce a password policy, but we do recommend you enable Multi-Factor

View file

@ -1,29 +1,28 @@
// Dependencies // Dependencies
import { welcomeSteps } from './shared.mjs' import { welcomeSteps } from './shared.mjs'
import { linkClasses } from '@freesewing/utils' import { linkClasses } from '@freesewing/utils'
// Context // Context
import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus' import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus'
// Hooks // Hooks
import React, { useState, useContext } from 'react' import React, { useState, useContext } from 'react'
import { useAccount } from '@freesewing/react/hooks/useAccount' import { useAccount } from '@freesewing/react/hooks/useAccount'
import { useBackend } from '@freesewing/react/hooks/useBackend' import { useBackend } from '@freesewing/react/hooks/useBackend'
// Components // Components
import { Link as WebLink } from '@freesewing/react/components/Link' import { Link as WebLink } from '@freesewing/react/components/Link'
import { NoIcon, OkIcon, SaveIcon, RightIcon } from '@freesewing/react/components/Icon' import { NoIcon, OkIcon, RightIcon } from '@freesewing/react/components/Icon'
import { ListInput } from '@freesewing/react/components/Input' import { ListInput } from '@freesewing/react/components/Input'
import { Popout } from '@freesewing/react/components/Popout' import { Popout } from '@freesewing/react/components/Popout'
import { IconButton } from '@freesewing/react/components/Button' import { IconButton } from '@freesewing/react/components/Button'
import { WelcomeIcons } from './shared.mjs' import { WelcomeIcons } from './shared.mjs'
/* /**
* Component for the account/preferences/newsletter page * A component to manage the user's newsletter subscription in their account data
* *
* @params {object} props - All React props * @component
* @params {bool} props.welcome - Set to true to use this component on the welcome page * @params {object} props - All Component props
* @param {function} props.Link - An optional framework-specific Link component * @param {bool} props.welcome - Set to true to use this component on the welcome page
* @param {React.Component} props.Link - An optional framework-specific Link component
* @returns {JSX.Element}
*/ */
export const Newsletter = ({ welcome = false, Link = false }) => { export const Newsletter = ({ welcome = false, Link = false }) => {
if (!Link) Link = WebLink if (!Link) Link = WebLink
@ -109,7 +108,7 @@ export const Newsletter = ({ welcome = false, Link = false }) => {
</> </>
) : null} ) : null}
{welcome ? null : ( {welcome ? null : (
<Popout tip> <Popout type="tip">
<h5>You can unsubscribe at any time with the link below</h5> <h5>You can unsubscribe at any time with the link below</h5>
<p> <p>
This unsubscribe link will also be included at the bottom of every newsletter we send This unsubscribe link will also be included at the bottom of every newsletter we send

View file

@ -1,28 +1,25 @@
// Dependencies
import { welcomeSteps } from './shared.mjs'
import { horFlexClasses } from '@freesewing/utils'
// Context // Context
import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus' import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus'
// Hooks // Hooks
import React, { useState, useContext } from 'react' import React, { useState, useContext } from 'react'
import { useAccount } from '@freesewing/react/hooks/useAccount' import { useAccount } from '@freesewing/react/hooks/useAccount'
import { useBackend } from '@freesewing/react/hooks/useBackend' import { useBackend } from '@freesewing/react/hooks/useBackend'
// Components // Components
import { Link as WebLink } from '@freesewing/react/components/Link' import { Link as WebLink } from '@freesewing/react/components/Link'
import { RightIcon, SaveIcon } from '@freesewing/react/components/Icon' import { RightIcon, SaveIcon } from '@freesewing/react/components/Icon'
import { PasswordInput } from '@freesewing/react/components/Input' import { PasswordInput } from '@freesewing/react/components/Input'
import { Popout } from '@freesewing/react/components/Popout' import { Popout } from '@freesewing/react/components/Popout'
/* /**
* Component for the account/security/password page * A component to manage the user's password
* *
* @params {object} props - All React props * @component
* @params {bool} props.welcome - Set to true to use this component on the welcome page * @param {object} props - All component props
* @param {bool} [props.welcome = false] - Set to true to render the welcome/onboarding view
* @param {React.Component} props.Link - A framework specific Link component for client-side routing
* @returns {JSX.Element}
*/ */
export const Password = ({ welcome = false, Link = false }) => { export const Password = ({ Link = false }) => {
if (!Link) Link = WebLink if (!Link) Link = WebLink
// Hooks // Hooks
const backend = useBackend() const backend = useBackend()
@ -60,7 +57,7 @@ export const Password = ({ welcome = false, Link = false }) => {
<SaveIcon /> Save <SaveIcon /> Save
</button> </button>
{!account.mfaEnabled && ( {!account.mfaEnabled && (
<Popout tip> <Popout type="tip">
<h5>Please consider enabling Two-Factor Authentication</h5> <h5>Please consider enabling Two-Factor Authentication</h5>
<p> <p>
We do not enforce a password policy, but we do recommend you enable Two-Factor We do not enforce a password policy, but we do recommend you enable Two-Factor

View file

@ -1,25 +1,13 @@
// Dependencies // Dependencies
import orderBy from 'lodash/orderBy.js' import { cloudflareImageUrl, horFlexClasses, patternUrlFromState } from '@freesewing/utils'
import {
cloudflareImageUrl,
capitalize,
shortDate,
horFlexClasses,
newPatternUrl,
patternUrlFromState,
} from '@freesewing/utils'
import { urls, control as controlConfig } from '@freesewing/config' import { urls, control as controlConfig } from '@freesewing/config'
// Context // Context
import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus' import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus'
import { ModalContext } from '@freesewing/react/context/Modal' import { ModalContext } from '@freesewing/react/context/Modal'
// Hooks // Hooks
import React, { useState, useEffect, useContext } from 'react' import React, { useState, useEffect, useContext } from 'react'
import { useAccount } from '@freesewing/react/hooks/useAccount' import { useAccount } from '@freesewing/react/hooks/useAccount'
import { useBackend } from '@freesewing/react/hooks/useBackend' import { useBackend } from '@freesewing/react/hooks/useBackend'
import { useSelection } from '@freesewing/react/hooks/useSelection'
// Components // Components
import Markdown from 'react-markdown' import Markdown from 'react-markdown'
import { import {
@ -28,7 +16,7 @@ import {
PassiveImageInput, PassiveImageInput,
ListInput, ListInput,
} from '@freesewing/react/components/Input' } from '@freesewing/react/components/Input'
import { Link as WebLink, AnchorLink } from '@freesewing/react/components/Link' import { Link as WebLink } from '@freesewing/react/components/Link'
import { import {
BoolNoIcon, BoolNoIcon,
BoolYesIcon, BoolYesIcon,
@ -42,12 +30,20 @@ import {
ResetIcon, ResetIcon,
UploadIcon, UploadIcon,
} from '@freesewing/react/components/Icon' } from '@freesewing/react/components/Icon'
import { DisplayRow } from './shared.mjs'
import { TimeAgo } from '@freesewing/react/components/Time' import { TimeAgo } from '@freesewing/react/components/Time'
import { Popout } from '@freesewing/react/components/Popout' import { Popout } from '@freesewing/react/components/Popout'
import { ModalWrapper } from '@freesewing/react/components/Modal' import { ModalWrapper } from '@freesewing/react/components/Modal'
import { KeyVal } from '@freesewing/react/components/KeyVal' import { KeyVal } from '@freesewing/react/components/KeyVal'
/**
* A component to manage a pattern in the user's account data
*
* @component
* @param {object} props - All component props
* @param {number} props.id - The pattern ID to load
* @param {React.Component} props.Link - A framework specific Link component for client-side routing
* @returns {JSX.Element}
*/
export const Pattern = ({ id, Link }) => { export const Pattern = ({ id, Link }) => {
if (!Link) Link = WebLink if (!Link) Link = WebLink
// Hooks // Hooks
@ -137,7 +133,7 @@ export const Pattern = ({ id, Link }) => {
return ( return (
<div className="tw:w-full"> <div className="tw:w-full">
{pattern.public ? ( {pattern.public ? (
<Popout note> <Popout type="note">
<h5>This is the private view of your pattern</h5> <h5>This is the private view of your pattern</h5>
<p> <p>
Everyone can access the public view since this is a public pattern. Everyone can access the public view since this is a public pattern.
@ -254,6 +250,21 @@ export const Pattern = ({ id, Link }) => {
) )
} }
/**
* A component to display a card representing a pattern in the user's account data.
*
* This is a pure render component, you have to pass in the data.
*
* @component
* @param {object} props - All component props
* @param {React.Component} props.Link - A framework specific Link component for client-side routing
* @param {string} [props.href = false] - An optional URL the pattern card should link to
* @param {function} [props.onClick = false] - An optional onClick handler
* @param {object} props.pattern - An object holding the pattern data
* @param {string} [props.size = 'md'] - The size, one of lg, md, sm, or xs
* @param {boolean} [props.useA = false] - Whether to use an a tag of Link component when passing in a href prop
* @returns {JSX.Element}
*/
export const PatternCard = ({ export const PatternCard = ({
pattern, pattern,
href = false, href = false,
@ -265,7 +276,7 @@ export const PatternCard = ({
if (!Link) Link = WebLink if (!Link) Link = WebLink
const sizes = { const sizes = {
lg: 96, lg: 96,
md: 52, md: 48,
sm: 36, sm: 36,
xs: 20, xs: 20,
} }
@ -315,25 +326,16 @@ export const PatternCard = ({
const BadgeLink = ({ label, href }) => ( const BadgeLink = ({ label, href }) => (
<a <a
href={href} href={href}
className="tw:daisy-badge tw:daisy-badge-secondary tw:font-bold tw:daisy-badge-lg tw:hover:text-secondary-content tw:hover:no-underline" className="tw:daisy-badge tw:daisy-badge-secondary tw:font-bold tw:daisy-badge-lg tw:hover:text-secondary-content tw:hover:cursor-pointer"
> >
{label} <span className="tw:text-secondary-content hover:tw:decoration-0">{label}</span>
</a> </a>
) )
/** /**
* Helper component to show the pattern title, image, and various buttons * Helper component to show the pattern title, image, and various buttons
*/ */
const PatternHeader = ({ const PatternHeader = ({ pattern, Link, account, setModal, setEdit, togglePublic, clone }) => (
pattern,
Link,
account,
setModal,
setEdit,
togglePublic,
save,
clone,
}) => (
<> <>
<h2>{pattern.name}</h2> <h2>{pattern.name}</h2>
<div className="tw:flex tw:flex-row tw:flex-wrap tw:gap-2 tw:text-sm tw:items-center tw:mb-2"> <div className="tw:flex tw:flex-row tw:flex-wrap tw:gap-2 tw:text-sm tw:items-center tw:mb-2">

View file

@ -1,16 +1,12 @@
// Dependencies // Dependencies
import orderBy from 'lodash/orderBy.js' import orderBy from 'lodash/orderBy.js'
import { capitalize, shortDate } from '@freesewing/utils' import { capitalize, shortDate } from '@freesewing/utils'
// Context // Context
import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus' import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus'
// Hooks // Hooks
import React, { useState, useEffect, useContext } from 'react' import React, { useState, useEffect, useContext } from 'react'
import { useAccount } from '@freesewing/react/hooks/useAccount'
import { useBackend } from '@freesewing/react/hooks/useBackend' import { useBackend } from '@freesewing/react/hooks/useBackend'
import { useSelection } from '@freesewing/react/hooks/useSelection' import { useSelection } from '@freesewing/react/hooks/useSelection'
// Components // Components
import { TableWrapper } from '@freesewing/react/components/Table' import { TableWrapper } from '@freesewing/react/components/Table'
import { PatternCard } from '@freesewing/react/components/Account' import { PatternCard } from '@freesewing/react/components/Account'
@ -23,16 +19,13 @@ import {
TrashIcon, TrashIcon,
} from '@freesewing/react/components/Icon' } from '@freesewing/react/components/Icon'
const t = (input) => { /**
console.log('t called', input) * A component to display and manage the list of patterns in the user's account
return input
}
/*
* Component for the account/patterns page
* *
* @params {object} props - All React props * @component
* @params {function} Link - A framework specific Link component for client-side routing * @param {object} props - All component props
* @param {React.Component} props.Link - A framework specific Link component for client-side routing
* @returns {JSX.Element}
*/ */
export const Patterns = ({ Link = false }) => { export const Patterns = ({ Link = false }) => {
if (!Link) Link = WebLink if (!Link) Link = WebLink
@ -95,14 +88,16 @@ export const Patterns = ({ Link = false }) => {
onClick={removeSelectedPatterns} onClick={removeSelectedPatterns}
disabled={count < 1} disabled={count < 1}
> >
<TrashIcon /> {count} {t('patterns')} <TrashIcon /> {count} Patterns
</button> </button>
<Link <Link
className="tw:daisy-btn tw:daisy-btn-primary tw:capitalize tw:w-full tw:md:w-auto tw:hover:text-primary-content" className="tw:daisy-btn tw:daisy-btn-primary tw:capitalize tw:w-full tw:md:w-auto tw:hover:text-primary-content"
href="/editor/" href="/editor/"
> >
<PlusIcon /> <span className="tw:text-primary-content">
Create a new pattern <PlusIcon />
</span>
<span className="tw:text-primary-content">Create a new pattern</span>
</Link> </Link>
</div> </div>
<TableWrapper> <TableWrapper>

View file

@ -19,11 +19,52 @@ const labels = {
website: 'Website', website: 'Website',
} }
/**
* A component to manage the user's Instagram handle in their account data
*
* @component
* @returns {JSX.Element}
*/
export const Instagram = () => <Platform platform="instagram" /> export const Instagram = () => <Platform platform="instagram" />
/**
* A component to manage the user's Mastodon handle in their account data
*
* @component
* @returns {JSX.Element}
*/
export const Mastodon = () => <Platform platform="mastodon" /> export const Mastodon = () => <Platform platform="mastodon" />
/**
* A component to manage the user's Reddit handle in their account data
*
* @component
* @returns {JSX.Element}
*/
export const Reddit = () => <Platform platform="reddit" /> export const Reddit = () => <Platform platform="reddit" />
/**
* A component to manage the user's Twitch handle in their account data
*
* @component
* @returns {JSX.Element}
*/
export const Twitch = () => <Platform platform="twitch" /> export const Twitch = () => <Platform platform="twitch" />
/**
* A component to manage the user's Tiktok handle in their account data
*
* @component
* @returns {JSX.Element}
*/
export const Tiktok = () => <Platform platform="tiktok" /> export const Tiktok = () => <Platform platform="tiktok" />
/**
* A component to manage the user's website URL in their account data
*
* @component
* @returns {JSX.Element}
*/
export const Website = () => <Platform platform="website" /> export const Website = () => <Platform platform="website" />
/* /*

View file

@ -1,19 +1,18 @@
// Context // Context
import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus' import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus'
// Hooks // Hooks
import React, { useState, useContext } from 'react' import React, { useContext } from 'react'
import { useAccount } from '@freesewing/react/hooks/useAccount' import { useAccount } from '@freesewing/react/hooks/useAccount'
import { useBackend } from '@freesewing/react/hooks/useBackend' import { useBackend } from '@freesewing/react/hooks/useBackend'
// Components // Components
import { Link as WebLink } from '@freesewing/react/components/Link'
import { ReloadIcon } from '@freesewing/react/components/Icon' import { ReloadIcon } from '@freesewing/react/components/Icon'
import { Popout } from '@freesewing/react/components/Popout'
import { IconButton } from '@freesewing/react/components/Button' import { IconButton } from '@freesewing/react/components/Button'
/* /**
* Component for the account/actions/export page * A component handle a reload of the account data
*
* @component
* @returns {JSX.Element}
*/ */
export const Reload = () => { export const Reload = () => {
// Hooks // Hooks

View file

@ -3,21 +3,20 @@ import { navigate } from '@freesewing/utils'
// Context // Context
import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus' import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus'
import { ModalContext } from '@freesewing/react/context/Modal' import { ModalContext } from '@freesewing/react/context/Modal'
// Hooks // Hooks
import React, { useState, useContext } from 'react' import React, { useContext } from 'react'
import { useAccount } from '@freesewing/react/hooks/useAccount' import { useAccount } from '@freesewing/react/hooks/useAccount'
import { useBackend } from '@freesewing/react/hooks/useBackend' import { useBackend } from '@freesewing/react/hooks/useBackend'
// Components // Components
import { Link as WebLink } from '@freesewing/react/components/Link'
import { BackIcon as ExitIcon, TrashIcon } from '@freesewing/react/components/Icon' import { BackIcon as ExitIcon, TrashIcon } from '@freesewing/react/components/Icon'
import { Popout } from '@freesewing/react/components/Popout'
import { IconButton } from '@freesewing/react/components/Button' import { IconButton } from '@freesewing/react/components/Button'
import { ModalWrapper } from '@freesewing/react/components/Modal' import { ModalWrapper } from '@freesewing/react/components/Modal'
/* /**
* Component for the account/actions/remove page * A component to handle the removal of a user's account
*
* @component
* @returns {JSX.Element}
*/ */
export const Remove = () => { export const Remove = () => {
// Hooks // Hooks

View file

@ -6,19 +6,23 @@ import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus'
import { ModalContext } from '@freesewing/react/context/Modal' import { ModalContext } from '@freesewing/react/context/Modal'
// Hooks // Hooks
import React, { useState, useContext } from 'react' import React, { useContext } from 'react'
import { useAccount } from '@freesewing/react/hooks/useAccount' import { useAccount } from '@freesewing/react/hooks/useAccount'
import { useBackend } from '@freesewing/react/hooks/useBackend' import { useBackend } from '@freesewing/react/hooks/useBackend'
// Components // Components
import { Link as WebLink } from '@freesewing/react/components/Link' import { Link as WebLink } from '@freesewing/react/components/Link'
import { BackIcon, NoIcon } from '@freesewing/react/components/Icon' import { BackIcon, NoIcon } from '@freesewing/react/components/Icon'
import { Popout } from '@freesewing/react/components/Popout'
import { IconButton } from '@freesewing/react/components/Button' import { IconButton } from '@freesewing/react/components/Button'
import { ModalWrapper } from '@freesewing/react/components/Modal' import { ModalWrapper } from '@freesewing/react/components/Modal'
/* /**
* Component for the account/actions/restrict page * A component to manage the user's options to restrict processing of their data
*
* @component
* @param {object} props - All component props
* @param {React.Component} props.Link - A framework specific Link component for client-side routing
* @returns {JSX.Element}
*/ */
export const Restrict = ({ Link = false }) => { export const Restrict = ({ Link = false }) => {
if (!Link) Link = WebLink if (!Link) Link = WebLink
@ -34,7 +38,7 @@ export const Restrict = ({ Link = false }) => {
// Helper method to restrict the account // Helper method to restrict the account
const restrictAccount = async () => { const restrictAccount = async () => {
setLoadingStatus([true, 'Talking to the backend']) setLoadingStatus([true, 'Talking to the backend'])
const [status, body] = await backend.restrictAccount() const [status] = await backend.restrictAccount()
if (status === 200) { if (status === 200) {
setLoadingStatus([true, 'Done. Consider yourself restrcited.', true, true]) setLoadingStatus([true, 'Done. Consider yourself restrcited.', true, true])
signOut() signOut()
@ -46,7 +50,7 @@ export const Restrict = ({ Link = false }) => {
<> <>
<p>This button is red for a reason.</p> <p>This button is red for a reason.</p>
<IconButton onClick={restrictAccount} color="error"> <IconButton onClick={restrictAccount} color="error">
<Nocon /> <NoIcon />
Remove your FreeSewing account Remove your FreeSewing account
</IconButton> </IconButton>
</> </>

View file

@ -1,6 +1,14 @@
import React from 'react' import React from 'react'
import { KeyVal } from '@freesewing/react/components/KeyVal' import { KeyVal } from '@freesewing/react/components/KeyVal'
/**
* A component to display the user's role
*
* @component
* @param {object} props - All component props
* @param {string} props.role - The user role, either user or admin
* @returns {JSX.Element}
*/
export const UserRole = ({ role }) => { export const UserRole = ({ role }) => {
if (role === 'user') return <RoleUser /> if (role === 'user') return <RoleUser />
if (role === 'admin') return <RoleAdmin /> if (role === 'admin') return <RoleAdmin />

View file

@ -15,6 +15,7 @@ import {
shortDate, shortDate,
timeAgo, timeAgo,
} from '@freesewing/utils' } from '@freesewing/utils'
import { modalMeasurementHelp } from '@freesewing/react/components/Help'
// Context // Context
import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus' import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus'
import { ModalContext } from '@freesewing/react/context/Modal' import { ModalContext } from '@freesewing/react/context/Modal'
@ -58,21 +59,16 @@ import { bundlePatternTranslations, draft, flattenFlags } from '../Editor/lib/in
import { Bonny } from '@freesewing/bonny' import { Bonny } from '@freesewing/bonny'
import { MiniNote, MiniTip } from '../Mini/index.mjs' import { MiniNote, MiniTip } from '../Mini/index.mjs'
const t = (input) => { /**
console.log('t called', input)
return input
}
/*
* Component to show an individual measurements set * Component to show an individual measurements set
* *
* @param {object} props - All React props * @component
* @param {number} id - The ID of the measurements set to load * @param {object} props - All Component props
* @param {bool} publicOnly - If the set should be used with the backend.getPublicSet method * @param {number} props.id - The ID of the measurements set to load
* @param {function} Link - An optional framework-specific Link component to use for client-side routing * @param {bool} [props.publicOnly = false] - If the set should be used with the backend.getPublicSet method
* @param {object} measurementHelpProvider - A function that returns a url or action to show help for a specific measurement * @param {function} [props.Link = false] - An optional framework-specific Link component to use for client-side routing
*/ */
export const Set = ({ id, publicOnly = false, Link = false, measurementHelpProvider = false }) => { export const Set = ({ id, publicOnly = false, Link = false }) => {
if (!Link) Link = WebLink if (!Link) Link = WebLink
// Hooks // Hooks
@ -384,7 +380,7 @@ export const Set = ({ id, publicOnly = false, Link = false, measurementHelpProvi
return ( return (
<div className="tw:w-full"> <div className="tw:w-full">
{heading} {heading}
<RenderedCSet {...{ mset, setLoadingStatus, backend, imperial }} /> <RenderedCset {...{ mset, setLoadingStatus, backend, imperial }} />
</div> </div>
) )
@ -490,7 +486,7 @@ export const Set = ({ id, publicOnly = false, Link = false, measurementHelpProvi
current={mset.measies[m]} current={mset.measies[m]}
original={mset.measies[m]} original={mset.measies[m]}
update={updateMeasies} update={updateMeasies}
helpProvider={measurementHelpProvider} help={() => modalMeasurementHelp(m, setModal)}
/> />
))} ))}
@ -619,36 +615,38 @@ export const Set = ({ id, publicOnly = false, Link = false, measurementHelpProvi
) )
} }
/** /*
* A (helper) component to display a measurements value * A (helper) component to display a measurements value
* *
* @component
* @param {object} props - All React props * @param {object} props - All React props
* @param {string} val - The value * @param {string} val - The value
* @param {string} m - The measurement name * @param {string} m - The measurement name
* @param {bool} imperial - True for imperial measurements, or metric by default * @param {bool} imperial - True for imperial measurements, or metric by default
*/ */
export const MeasurementValue = ({ val, m, imperial = false }) => const MeasurementValue = ({ val, m, imperial = false }) =>
isDegreeMeasurement(m) ? ( isDegreeMeasurement(m) ? (
<span>{val}°</span> <span>{val}°</span>
) : ( ) : (
<span dangerouslySetInnerHTML={{ __html: formatMm(val, imperial) }}></span> <span dangerouslySetInnerHTML={{ __html: formatMm(val, imperial) }}></span>
) )
/** /*
* React component to suggest a measurements set for curation * React component to suggest a measurements set for curation
* *
* @component
* @param {object} props - All React props * @param {object} props - All React props
* @param {string} mset - The measurements set * @param {string} mset - The measurements set
*/ */
export const SuggestCset = ({ mset, Link }) => { const SuggestCset = ({ mset, Link }) => {
// State // State
const [height, setHeight] = useState('') const [height, setHeight] = useState('')
const [img, setImg] = useState('') const [img, setImg] = useState('')
const [name, setName] = useState('') const [name, setName] = useState('')
const [notes, setNotes] = useState('') const [notes, setNotes] = useState('')
const [submission, setSubmission] = useState(false) const [submission, setSubmission] = useState(false)
// Context
console.log(mset) const { setLoadingStatus } = useContext(LoadingStatusContext)
// Hooks // Hooks
const backend = useBackend() const backend = useBackend()
@ -745,7 +743,7 @@ export const SuggestCset = ({ mset, Link }) => {
Notes Notes
</h4> </h4>
<p>If you would like to add any notes, you can do so here.</p> <p>If you would like to add any notes, you can do so here.</p>
<Popout tip compact> <Popout type="tip" compact>
This field supports markdown This field supports markdown
</Popout> </Popout>
<MarkdownInput label="Notes" current={notes} update={setNotes} valid={() => true} /> <MarkdownInput label="Notes" current={notes} update={setNotes} valid={() => true} />
@ -760,13 +758,14 @@ export const SuggestCset = ({ mset, Link }) => {
) )
} }
/** /*
* React component to render a preview of a measurement set using the bonny pattern * React component to render a preview of a measurement set using the bonny pattern
* *
* @component
* @param {object} props - All React props * @param {object} props - All React props
* @param {string} mset - The measurements set * @param {string} mset - The measurements set
*/ */
export const RenderedCSet = ({ mset, imperial }) => { const RenderedCset = ({ mset, imperial }) => {
const [previewVisible, setPreviewVisible] = useState(false) const [previewVisible, setPreviewVisible] = useState(false)
const missing = [] const missing = []
@ -799,7 +798,7 @@ export const RenderedCSet = ({ mset, imperial }) => {
<strong>{formatMm(pattern.parts[0].front.points.head.y * -1, imperial)}</strong> high. <strong>{formatMm(pattern.parts[0].front.points.head.y * -1, imperial)}</strong> high.
</p> </p>
<p>Here is what the automated analysis found:</p> <p>Here is what the automated analysis found:</p>
{Object.entries(flattenFlags(flags)).map(([key, flag], i) => { {Object.entries(flattenFlags(flags)).map(([key, flag]) => {
const desc = strings[flag.desc] || flag.desc const desc = strings[flag.desc] || flag.desc
return ( return (
@ -840,7 +839,7 @@ export const RenderedCSet = ({ mset, imperial }) => {
</li> </li>
<li> <li>
This preview is an <strong>approximation</strong>, not an exact representation. Bodies This preview is an <strong>approximation</strong>, not an exact representation. Bodies
have many variations that can't be captured with just a few measurements. We are have many variations that can&apos;t be captured with just a few measurements. We are
missing some information, like how weight is distributed or your posture. missing some information, like how weight is distributed or your posture.
</li> </li>
<li> <li>
@ -867,11 +866,17 @@ export const RenderedCSet = ({ mset, imperial }) => {
) )
} }
/**
* A component to create a new measurements set.
*
* @component
* @returns {JSX.Element}
*/
export const NewSet = () => { export const NewSet = () => {
// Hooks // Hooks
const backend = useBackend() const backend = useBackend()
const { account } = useAccount() const { account } = useAccount()
const { setLoadingStatus, LoadingProgress } = useContext(LoadingStatusContext) const { setLoadingStatus } = useContext(LoadingStatusContext)
// State // State
const [name, setName] = useState('') const [name, setName] = useState('')

View file

@ -1,8 +1,7 @@
// Dependencies // Dependencies
import { measurements } from '@freesewing/config'
import { measurements as measurementsTranslations } from '@freesewing/i18n' import { measurements as measurementsTranslations } from '@freesewing/i18n'
import { requiredMeasurements as designMeasurements } from '@freesewing/collection' import { requiredMeasurements as designMeasurements } from '@freesewing/collection'
import { cloudflareImageUrl, capitalize, hasRequiredMeasurements } from '@freesewing/utils' import { cloudflareImageUrl, hasRequiredMeasurements } from '@freesewing/utils'
// Context // Context
import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus' import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus'
import { ModalContext } from '@freesewing/react/context/Modal' import { ModalContext } from '@freesewing/react/context/Modal'
@ -16,11 +15,13 @@ import { NoIcon, OkIcon, PlusIcon, TrashIcon, UploadIcon } from '@freesewing/rea
import { ModalWrapper } from '@freesewing/react/components/Modal' import { ModalWrapper } from '@freesewing/react/components/Modal'
import { NewSet } from './Set.mjs' import { NewSet } from './Set.mjs'
/* /**
* The component for the an account/sets page * The component for the measurements sets in the user's account.
* *
* @param {object} props - All React props * @component
* @param {function} Link - An optional framework-specific Link component * @param {object} props - All component props
* @param {React.Component} props.Link - A framework specific Link component for client-side routing
* @returns {JSX.Element}
*/ */
export const Sets = ({ Link = false }) => { export const Sets = ({ Link = false }) => {
if (!Link) Link = WebLink if (!Link) Link = WebLink
@ -160,16 +161,20 @@ export const Sets = ({ Link = false }) => {
} }
/** /**
* React component to display a (card of a) single measurements set * A component to render a card of a single measurements set.
* *
* @param {object} props - All React props * This is a pure render component, you need to pass in the data.
* @param {function} Link - An optional framework-specific Link component *
* @param {string} design - The designs for which to check required measurements * @component
* @param {test} href - Where the set should link to * @param {object} props - All component props
* @param {function} onClick - What to do when clicking on a set * @param {React.Component} [props.Link = false] - An optional framework-specific Link component
* @param {object} set - The (data of the) measurements set * @param {string} [props.design = false] - An optional design name to check for required measurements in this set
* @param {string} size - Size of the card * @param {string} [props.href = false] - On optional href for the card to link to
* @param {bool} useA - Whether to use an A tag or not * @param {function} [props.onClick = false] - On optional onClick handler method
* @param {object} props.set - The data of the measurements set
* @param {string} [props.size = 'lg'] - Size of the card, one of lg, md, or sm
* @param {bool} [props.useA = false] - Whether to use an A tag or a Link component for href
* @returns {JSX.Element}
*/ */
export const MsetCard = ({ export const MsetCard = ({
Link = false, Link = false,
@ -183,7 +188,7 @@ export const MsetCard = ({
if (!Link) Link = WebLink if (!Link) Link = WebLink
const sizes = { const sizes = {
lg: 96, lg: 96,
md: 52, md: 48,
sm: 36, sm: 36,
} }
const s = sizes[size] const s = sizes[size]

View file

@ -1,6 +1,14 @@
import React from 'react' import React from 'react'
import { KeyVal } from '@freesewing/react/components/KeyVal' import { KeyVal } from '@freesewing/react/components/KeyVal'
/**
* Displays a badge for the account status you pass it
*
* @component
* @param {object} props - All component props
* @param {number} props.status - The account status. One of -2, -1, 0, or 1
* @returns {JSX.Element}
*/
export const AccountStatus = ({ status }) => { export const AccountStatus = ({ status }) => {
if (status === 0) return <AccountInactive /> if (status === 0) return <AccountInactive />
if (status === 1) return <AccountActive /> if (status === 1) return <AccountActive />

View file

@ -1,27 +1,24 @@
// Dependencies // Dependencies
import { welcomeSteps } from './shared.mjs' import { welcomeSteps } from './shared.mjs'
// Context // Context
import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus' import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus'
// Hooks // Hooks
import React, { useState, useContext } from 'react' import React, { useState, useContext } from 'react'
import { useAccount } from '@freesewing/react/hooks/useAccount' import { useAccount } from '@freesewing/react/hooks/useAccount'
import { useBackend } from '@freesewing/react/hooks/useBackend' import { useBackend } from '@freesewing/react/hooks/useBackend'
// Components // Components
import { Link as WebLink } from '@freesewing/react/components/Link' import { RightIcon } from '@freesewing/react/components/Icon'
import { SaveIcon, RightIcon } from '@freesewing/react/components/Icon'
import { ListInput } from '@freesewing/react/components/Input' import { ListInput } from '@freesewing/react/components/Input'
import { NumberCircle } from '@freesewing/react/components/Number' import { NumberCircle } from '@freesewing/react/components/Number'
import { IconButton } from '@freesewing/react/components/Button' import { IconButton } from '@freesewing/react/components/Button'
import { WelcomeIcons } from './shared.mjs' import { WelcomeIcons } from './shared.mjs'
/* /**
* Component for the account/preferences/units page * A component to manage the user's units
* *
* @params {object} props - All React props * @component
* @params {bool} props.welcome - Set to true to use this component on the welcome page * @param {bool} [props.welcome = false] - Set to true to render the welcome/onboarding view
* @returns {JSX.Element}
*/ */
export const Units = ({ welcome = false }) => { export const Units = ({ welcome = false }) => {
// Hooks // Hooks

View file

@ -16,12 +16,14 @@ import { StringInput } from '@freesewing/react/components/Input'
import { IconButton } from '@freesewing/react/components/Button' import { IconButton } from '@freesewing/react/components/Button'
import { WelcomeIcons } from './shared.mjs' import { WelcomeIcons } from './shared.mjs'
/* /**
* Component for the account/username page * A component to manage the user's username
* *
* @params {object} props - All React props * @component
* @params {bool} props.welcome - Set to true to use this component on the welcome page * @param {object} props - All component props
* @params {function} props.Link - A framework specific Link component for client-side routing * @param {bool} [props.welcome = false] - Set to true to render the welcome/onboarding view
* @param {React.Component} [props.Link = false] - A framework specific Link component for client-side routing
* @returns {JSX.Element}
*/ */
export const Username = ({ welcome = false, Link = false }) => { export const Username = ({ welcome = false, Link = false }) => {
if (!Link) Link = WebLink if (!Link) Link = WebLink
@ -55,10 +57,6 @@ export const Username = ({ welcome = false, Link = false }) => {
? '/welcome/' + welcomeSteps[account.control][5] ? '/welcome/' + welcomeSteps[account.control][5]
: '/docs/about/guide' : '/docs/about/guide'
let btnClasses = 'daisy-btn mt-4 capitalize '
if (welcome) btnClasses += 'w-64 daisy-btn-secondary'
else btnClasses += 'w-full daisy-btn-primary'
return ( return (
<div className="tw:w-full"> <div className="tw:w-full">
<StringInput <StringInput

View file

@ -1,71 +1,69 @@
import React from 'react'
import { AccountStatus } from './Status.mjs' import { AccountStatus } from './Status.mjs'
import { Bookmarks, BookmarkButton } from './Bookmarks.mjs'
import { Links } from './Links.mjs'
import { Set, NewSet } from './Set.mjs'
import { Sets, MsetCard } from './Sets.mjs'
import { Patterns } from './Patterns.mjs'
import { Pattern, PatternCard } from './Pattern.mjs'
import { Apikeys } from './Apikeys.mjs' import { Apikeys } from './Apikeys.mjs'
import { Username } from './Username.mjs'
import { Bio } from './Bio.mjs'
import { Avatar } from './Avatar.mjs' import { Avatar } from './Avatar.mjs'
import { Email, EmailChangeConfirmation } from './Email.mjs' import { Bio } from './Bio.mjs'
import { Github } from './Github.mjs' import { BookmarkButton, Bookmarks } from './Bookmarks.mjs'
import { Instagram, Mastodon, Reddit, Twitch, Tiktok, Website } from './Platform.mjs'
import { Compare } from './Compare.mjs' import { Compare } from './Compare.mjs'
import { Control } from './Control.mjs'
import { Units } from './Units.mjs'
import { Newsletter } from './Newsletter.mjs'
import { Consent } from './Consent.mjs' import { Consent } from './Consent.mjs'
import { Password } from './Password.mjs' import { Control } from './Control.mjs'
import { Mfa } from './Mfa.mjs' import { Email, EmailChangeConfirmation } from './Email.mjs'
import { ImportSet } from './Import.mjs'
import { Export } from './Export.mjs' import { Export } from './Export.mjs'
import { Github } from './Github.mjs'
import { ImportSet } from './Import.mjs'
import { Instagram, Mastodon, Reddit, Twitch, Tiktok, Website } from './Platform.mjs'
import { Links } from './Links.mjs'
import { Mfa } from './Mfa.mjs'
import { MsetCard, Sets } from './Sets.mjs'
import { Newsletter } from './Newsletter.mjs'
import { Password } from './Password.mjs'
import { Pattern, PatternCard } from './Pattern.mjs'
import { Patterns } from './Patterns.mjs'
import { Reload } from './Reload.mjs' import { Reload } from './Reload.mjs'
import { Remove } from './Remove.mjs' import { Remove } from './Remove.mjs'
import { Restrict } from './Restrict.mjs' import { Restrict } from './Restrict.mjs'
import { Set, NewSet } from './Set.mjs'
import { Units } from './Units.mjs'
import { UserId } from './Id.mjs' import { UserId } from './Id.mjs'
import { Username } from './Username.mjs'
import { UserRole } from './Role.mjs' import { UserRole } from './Role.mjs'
export { export {
AccountStatus, AccountStatus,
Bookmarks,
BookmarkButton,
Links,
Set,
NewSet,
Sets,
MsetCard,
Patterns,
Pattern,
PatternCard,
Apikeys, Apikeys,
Username,
Bio,
Avatar, Avatar,
Bio,
BookmarkButton,
Bookmarks,
Compare,
Consent,
Control,
Email, Email,
EmailChangeConfirmation, EmailChangeConfirmation,
Github,
Instagram,
Mastodon,
Reddit,
Twitch,
Tiktok,
Website,
Compare,
Control,
Units,
Newsletter,
Consent,
Password,
Mfa,
ImportSet,
Export, Export,
Github,
ImportSet,
Instagram,
Links,
Mastodon,
Mfa,
MsetCard,
NewSet,
Newsletter,
Password,
Pattern,
PatternCard,
Patterns,
Reddit,
Reload, Reload,
Remove, Remove,
Restrict, Restrict,
Set,
Sets,
Tiktok,
Twitch,
Units,
UserId, UserId,
Username,
UserRole, UserRole,
Website,
} }

View file

@ -8,9 +8,6 @@ import {
CompareIcon, CompareIcon,
DocsIcon, DocsIcon,
UserIcon, UserIcon,
LeftIcon,
OkIcon,
NoIcon,
ShowcaseIcon, ShowcaseIcon,
} from '@freesewing/react/components/Icon' } from '@freesewing/react/components/Icon'

View file

@ -1,8 +1,7 @@
// Dependencies // Dependencies
import { uiRoles as roles } from '@freesewing/config'
import { userAvatarUrl } from '@freesewing/utils' import { userAvatarUrl } from '@freesewing/utils'
// Hooks // Hooks
import React, { useState, useContext, useEffect } from 'react' import React, { useState, useContext } from 'react'
import { useAccount } from '@freesewing/react/hooks/useAccount' import { useAccount } from '@freesewing/react/hooks/useAccount'
import { useBackend } from '@freesewing/react/hooks/useBackend' import { useBackend } from '@freesewing/react/hooks/useBackend'
// Context // Context
@ -14,11 +13,16 @@ import { Spinner } from '@freesewing/react/components/Spinner'
import { Link as WebLink } from '@freesewing/react/components/Link' import { Link as WebLink } from '@freesewing/react/components/Link'
import { SearchIcon } from '@freesewing/react/components/Icon' import { SearchIcon } from '@freesewing/react/components/Icon'
import { KeyVal } from '@freesewing/react/components/KeyVal' import { KeyVal } from '@freesewing/react/components/KeyVal'
import { Markdown } from '@freesewing/react/components/Markdown'
import { ModalWrapper } from '@freesewing/react/components/Modal' import { ModalWrapper } from '@freesewing/react/components/Modal'
import { AccountStatus, UserRole } from '@freesewing/react/components/Account' import { AccountStatus, UserRole } from '@freesewing/react/components/Account'
export const SubscriberAdministration = ({ page }) => { /**
* A component to manage FreeSewing newsletter subscribers (requires admin role)
*
* @component
* @returns {JSX.Element}
*/
export const SubscriberAdministration = () => {
const [subscribers, setSubscribers] = useState() const [subscribers, setSubscribers] = useState()
const [q, setQ] = useState() const [q, setQ] = useState()
const [hits, setHits] = useState([]) const [hits, setHits] = useState([])
@ -106,6 +110,14 @@ export const SubscriberAdministration = ({ page }) => {
) )
} }
/**
* A component to manage FreeSewing users (requires the admin role)
*
* @component
* @param {object} props - All component props
* @param {React.Component} props.Link - A framework specific Link component for client-side routing
* @returns {JSX.Element}
*/
export const UserAdministration = ({ Link = false }) => { export const UserAdministration = ({ Link = false }) => {
const backend = useBackend() const backend = useBackend()
@ -154,7 +166,7 @@ export const UserAdministration = ({ Link = false }) => {
) )
} }
export const Hits = ({ results, Link = false }) => { const Hits = ({ results, Link = false }) => {
if (!Link) Link = WebLink if (!Link) Link = WebLink
return ( return (
@ -179,7 +191,7 @@ export const Hits = ({ results, Link = false }) => {
) )
} }
export const User = ({ user, Link }) => { const User = ({ user, Link }) => {
const { setModal } = useContext(ModalContext) const { setModal } = useContext(ModalContext)
const { setLoadingStatus } = useContext(LoadingStatusContext) const { setLoadingStatus } = useContext(LoadingStatusContext)
const backend = useBackend() const backend = useBackend()
@ -274,7 +286,7 @@ export const User = ({ user, Link }) => {
) )
} }
export const ImpersonateButton = ({ userId }) => { const ImpersonateButton = ({ userId }) => {
const backend = useBackend() const backend = useBackend()
const { setLoadingStatus } = useContext(LoadingStatusContext) const { setLoadingStatus } = useContext(LoadingStatusContext)
const { impersonate } = useAccount() const { impersonate } = useAccount()
@ -299,95 +311,3 @@ export const ImpersonateButton = ({ userId }) => {
</button> </button>
) )
} }
export const Row = ({ title, val }) => (
<tr className="py-1">
<td className="text-sm px-2 text-right font-bold">{title}</td>
<td className="text-sm">{val}</td>
</tr>
)
export const ManageUser = ({ userId }) => {
// Hooks
const backend = useBackend()
const { setLoadingStatus } = useContext(LoadingStatusContext)
const { account } = useAccount()
const { role } = account
// State
const [user, setUser] = useState({})
const [patterns, setPatterns] = useState({})
const [sets, setSets] = useState({})
// Effect
useEffect(() => {
const loadUser = async () => {
const result = await backend.adminLoadUser(userId)
if (result.success) {
setUser(result.data.user)
setPatterns(result.data.patterns)
setSets(result.data.sets)
}
}
loadUser()
}, [userId])
const updateUser = async (data) => {
setLoadingStatus([true, 'status:contactingBackend'])
const result = await backend.adminUpdateUser({ id: userId, data })
if (result.success) {
setLoadingStatus([true, 'status:settingsSaved', true, true])
setUser(result.data.user)
} else setLoadingStatus([true, 'status:backendError', true, false])
}
return user.id ? (
<div className="my-8">
<ShowUser
user={user}
button={role === 'admin' ? <ImpersonateButton userId={user.id} /> : null}
/>
{role === 'admin' ? (
<div className="flex flex-row flex-wrap gap-2 my-2">
{roles.map((role) => (
<button
key={role}
className="btn btn-primary btn-outline btn-sm"
onClick={() => updateUser({ role })}
disabled={role === user.role}
>
Assign {role} role
</button>
))}
</div>
) : null}
<div className="flex flex-row flex-wrap gap-2 my-2 mb-4">
{user.mfaEnabled && (
<button
className="btn btn-warning btn-outline btn-sm"
onClick={() => updateUser({ mfaEnabled: false })}
>
Disable MFA
</button>
)}
{Object.keys(freeSewingConfig.statuses).map((status) => (
<button
key={status}
className="btn btn-warning btn-outline btn-sm"
onClick={() => updateUser({ status })}
disabled={Number(status) === user.status}
>
Set {freeSewingConfig.statuses[status].name.toUpperCase()} status
</button>
))}
</div>
<Tabs tabs="Account, Patterns, Sets">
<Tab tabId="Account">{user.id ? <Json js={user} /> : null}</Tab>
<Tab tabId="Patterns">{patterns ? <Json js={patterns} /> : null}</Tab>
<Tab id="Sets">{sets ? <Json js={sets} /> : null}</Tab>
</Tabs>
</div>
) : (
<Loading />
)
}

View file

@ -1,12 +1,16 @@
import React from 'react' import React from 'react'
/* /**
* The actual Breadcrumbs component * A component to render breadcrumbs
* *
* @param {object} props - All the React props * This is a pure render component, you need to pass in the data.
* @param {array} props.crumbs - The crumbs, an array with objects with href, label keys *
* @param {function} Link - An optional custom component to use to render the Link * @component
* @param {text} title - The title of the current page * @param {object} props - All component props
* @param {React.Component} props.Link - A framework specific Link component for client-side routing
* @param {array} [props.crumbs = []] - The crumbs, an array with objects with href & label properties
* @param {text} title - The title of the current (final) page the breadcrumbs lead to
* @returns {JSX.Element}
*/ */
export const Breadcrumbs = ({ crumbs = [], title, Link = false }) => { export const Breadcrumbs = ({ crumbs = [], title, Link = false }) => {
if (Link === false) Link = RegularLink if (Link === false) Link = RegularLink
@ -18,13 +22,20 @@ export const Breadcrumbs = ({ crumbs = [], title, Link = false }) => {
style={{ paddingLeft: 0 }} style={{ paddingLeft: 0 }}
> >
<li className="tw:inline"> <li className="tw:inline">
<Link href="/">Home</Link> <Link href="/">
<b>Home</b>
</Link>
</li> </li>
<Spacer /> <Spacer />
{crumbs.map((crumb, i) => ( {crumbs.map((crumb, i) => (
<li key={i} className="tw:inline"> <React.Fragment key={i}>
<Link href={crumb.href}>{crumb.label}</Link> <li key={i} className="tw:inline">
</li> <Link href={crumb.href}>{crumb.label}</Link>
</li>
<li key={i} className="tw:inline">
<Spacer />
</li>
</React.Fragment>
))} ))}
<li className="tw:inline">{title}</li> <li className="tw:inline">{title}</li>
</ul> </ul>

View file

@ -1,4 +1,69 @@
import React from 'react' import React, { useContext, useState } from 'react'
import { copyToClipboard } from '@freesewing/utils'
import { CopyIcon, OkIcon } from '@freesewing/react/components/Icon'
import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus'
const handleCopied = (content, setCopied, setLoadingStatus, label, handler = false) => {
copyToClipboard(content)
setCopied(true)
setLoadingStatus([
true,
label ? `${label} copied to clipboard` : 'Copied to clipboard',
true,
true,
])
setTimeout(() => setCopied(false), 1000)
if (typeof handler === 'function') handler(content, label)
}
/**
* A component to copy something to the clipboard
*
* @component
* @param {object} props - All component props
* @param {JSX.element} props.children - The component children
* @param {string} [props.btnClasses = 'tw:daisy-btn tw:daisy-btn-ghost tw:hover:border-transparent w:hover:border-transparent tw:hover:shadow-none'] - The content that should be copied to the clipboard
* @param {string} props.content - The content that should be copied to the clipboard
* @param {string} props.label - The label to show when the content is copied
* @param {function} [props.onCopy=false] - An optional handler to call after copying to the clipboard, receives content, label as parameters
* @param {boolean} props.sup - Set this to true to render as superscript (above the line)
* @returns {JSX.Element}
*/
export const CopyToClipboardButton = ({
children,
content,
label = false,
sup = false,
btnClasses = 'tw:daisy-btn tw:daisy-btn-ghost tw:hover:border-transparent w:hover:border-transparent tw:hover:shadow-none',
onCopy = false,
}) => {
const [copied, setCopied] = useState(false)
const { setLoadingStatus } = useContext(LoadingStatusContext)
const style = sup ? 'tw:w-4 tw:h-4 tw:-mt-4 tw:-ml-1' : 'tw:w-5 tw:h-5'
return (
<button
className={
(copied ? 'tw:text-success ' : '') +
btnClasses +
' tw:w-full tw:lg:w-auto tw:group tw:flex tw:flex-row tw:justify-between'
}
onClick={() => handleCopied(content, setCopied, setLoadingStatus, label, onCopy)}
>
{sup ? children : null}
{copied ? (
<OkIcon
className={`${style} tw:text-success-content tw:bg-success tw:rounded-full tw:p-1`}
stroke={4}
/>
) : (
<CopyIcon className={`${style} tw:text-inherit tw:group-hover:text-secondary`} />
)}
{sup ? null : children}
</button>
)
}
/** /**
* A button with an icon and a label, something which we commonly use across our UI. * A button with an icon and a label, something which we commonly use across our UI.

View file

@ -1,36 +1,25 @@
// Dependencies // Dependencies
import { atomWithHash } from 'jotai-location'
import { import {
about, about,
collection, collection,
tags, tags,
techniques, techniques,
designers,
developers,
examples, examples,
measurements,
requiredMeasurements, requiredMeasurements,
optionalMeasurements, optionalMeasurements,
} from '@freesewing/collection' } from '@freesewing/collection'
import { capitalize, linkClasses, mutateObject } from '@freesewing/utils' import { capitalize, linkClasses, mutateObject } from '@freesewing/utils'
import { measurements as measurementsTranslations } from '@freesewing/i18n' import { measurements as measurementsTranslations } from '@freesewing/i18n'
// Context
import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus'
import { ModalContext } from '@freesewing/react/context/Modal'
// Hooks // Hooks
import React, { useState, useContext, Fragment } from 'react' import React, { useState, Fragment } from 'react'
import { useAtom } from 'jotai' import { useFilter } from '@freesewing/react/hooks/useFilter'
// Components // Components
import { Link as WebLink, AnchorLink } from '@freesewing/react/components/Link' import { Link as WebLink } from '@freesewing/react/components/Link'
import { import {
CircleIcon, CircleIcon,
CisFemaleIcon, CisFemaleIcon,
DocsIcon, DocsIcon,
FilterIcon, FilterIcon,
HeartIcon,
NewPatternIcon, NewPatternIcon,
ResetIcon, ResetIcon,
ShowcaseIcon, ShowcaseIcon,
@ -40,23 +29,19 @@ import {
lineDrawingsBack, lineDrawingsBack,
} from '@freesewing/react/components/LineDrawing' } from '@freesewing/react/components/LineDrawing'
import { IconButton } from '@freesewing/react/components/Button' import { IconButton } from '@freesewing/react/components/Button'
import { ModalWrapper } from '@freesewing/react/components/Modal'
import { KeyVal } from '@freesewing/react/components/KeyVal' import { KeyVal } from '@freesewing/react/components/KeyVal'
import { MissingLinedrawing } from '../LineDrawing/missing.mjs' import { MissingLinedrawing } from '../LineDrawing/missing.mjs'
const filterAtom = atomWithHash('filter', { example: true })
export const useFilter = () => {
return useAtom(filterAtom)
}
/** /**
* React component to show the FreeSewing collection and pick a design * A component to show the FreeSewing collection and pick a design.
* *
* @param {object} props - All React props * @component
* @param {function} Link - An optional framework specific Link component for client-side routing * @param {object} props - All component props
* @param {bool} editor - Set this to when loaded in the editor (this will make the display more dense) * @param {React.Component} [props.Link = false] - A framework specific Link component for client-side routing
* @param {bool} onClick - Set this to trigger an onClick event, rather than using links * @param {boolean} [props.editor = false] - Set this to true when rendering inside the FreeSewing editor
* @param {string} [props.linkTo = 'about'] - This controls where to link the design to. One of 'new', 'docs', or 'about'.
* @param {functino} [props.onClick = false] - You can pass in an onClick handler rather than using links
* @returns {JSX.Element}
*/ */
export const Collection = ({ Link = false, linkTo = 'about', editor = false, onClick = false }) => { export const Collection = ({ Link = false, linkTo = 'about', editor = false, onClick = false }) => {
if (!Link) Link = WebLink if (!Link) Link = WebLink
@ -184,17 +169,17 @@ export const Collection = ({ Link = false, linkTo = 'about', editor = false, onC
<div className="tw:flex tw:flex-row tw:gap-4 tw:items-center tw:justify-center tw:flex-wrap tw:my-2"> <div className="tw:flex tw:flex-row tw:gap-4 tw:items-center tw:justify-center tw:flex-wrap tw:my-2">
<button <button
className="tw:daisy-btn tw:daisy-btn-secondary tw:daisy-btn-outline" className="tw:daisy-btn tw:daisy-btn-secondary tw:daisy-btn-outline"
onClick={() => updateFilter('example', !filter.example)} onClick={() => updateFilter('ld', !filter.ld)}
> >
{filter.example ? <CisFemaleIcon /> : <ShowcaseIcon />} {filter.ld ? <CisFemaleIcon /> : <ShowcaseIcon />}
{filter.example ? 'Show Line Drawings' : 'Show Examples'} {filter.ld ? 'Show Examples' : 'Show Line Drawings'}
</button> </button>
<button <button
className="tw:daisy-btn tw:daisy-btn-secondary tw:daisy-btn-outline" className="tw:daisy-btn tw:daisy-btn-secondary tw:daisy-btn-outline"
onClick={() => setFilter({ example: 1 })} onClick={() => setFilter({ ld: 1 })}
> >
<ResetIcon /> <ResetIcon />
Clear Filter Clear Filters
</button> </button>
<button <button
className="tw:daisy-btn tw:daisy-btn-secondary tw:daisy-btn-outline" className="tw:daisy-btn tw:daisy-btn-secondary tw:daisy-btn-outline"
@ -209,10 +194,10 @@ export const Collection = ({ Link = false, linkTo = 'about', editor = false, onC
<div className="tw:flex tw:flex-row tw:gap-4 tw:items-center tw:justify-center tw:flex-wrap tw:my-2"> <div className="tw:flex tw:flex-row tw:gap-4 tw:items-center tw:justify-center tw:flex-wrap tw:my-2">
<button <button
className="tw:daisy-btn tw:daisy-btn-secondary tw:daisy-btn-outline" className="tw:daisy-btn tw:daisy-btn-secondary tw:daisy-btn-outline"
onClick={() => updateFilter('example', !filter.example)} onClick={() => updateFilter('ld', !filter.ld)}
> >
{filter.example ? <CisFemaleIcon /> : <ShowcaseIcon />} {filter.ld ? <ShowcaseIcon /> : <CisFemaleIcon />}
{filter.example ? 'Show Line Drawings' : 'Show Examples'} {filter.ld ? 'Show Examples' : 'Show Line Drawings'}
</button> </button>
<button <button
className="tw:daisy-btn tw:daisy-btn-secondary tw:daisy-btn-outline" className="tw:daisy-btn tw:daisy-btn-secondary tw:daisy-btn-outline"
@ -235,7 +220,7 @@ export const Collection = ({ Link = false, linkTo = 'about', editor = false, onC
key={d} key={d}
linkTo={linkTo} linkTo={linkTo}
onClick={onClick} onClick={onClick}
lineDrawing={filter.example ? false : true} lineDrawing={filter.ld ? true : false}
/> />
))} ))}
</div> </div>
@ -243,38 +228,6 @@ export const Collection = ({ Link = false, linkTo = 'about', editor = false, onC
) )
} }
/*
* A helper component to show a design technique
*
* @param {object} props - All React props
* @param {function} props.Link - A Link component, typically specific to the framework for client-side routing
* @param {string} props.technique - The technique name/id
*/
const Technique = ({ Link = WebLink, technique }) => (
<Link
href={`/designs/techniques/${technique}`}
className="tw:daisy-badge tw:daisy-badge-accent hover:tw:daisy-badge-secondary tw:hover:shadow tw:font-medium"
>
{technique}
</Link>
)
/*
* A helper component to show a design tag
*
* @param {object} props - All React props
* @param {function} props.Link - A Link component, typically specific to the framework for client-side routing
* @param {string} props.tag - The tag name/id
*/
const Tag = ({ Link = WebLink, technique }) => (
<Link
href={`/designs/tags/${tag}`}
className="tw:daisy-badge tw:daisy-badge-primary hover:tw:daisy-badge-secondary tw:hover:shadow tw:font-medium"
>
{tag}
</Link>
)
const DesignCard = ({ name, lineDrawing = false, linkTo, Link, onClick }) => { const DesignCard = ({ name, lineDrawing = false, linkTo, Link, onClick }) => {
if (!Link) Link = WebLink if (!Link) Link = WebLink
@ -363,11 +316,14 @@ const noExample =
'https://images.pexels.com/photos/5626595/pexels-photo-5626595.jpeg?cs=srgb&fm=jpg&w=640&h=427' 'https://images.pexels.com/photos/5626595/pexels-photo-5626595.jpeg?cs=srgb&fm=jpg&w=640&h=427'
/** /**
* React component to show info about a FreeSewing design * A component to show info about a FreeSewing design
* *
* @param {object} props - All React props * @component
* @param {string} design - The name/id of the design * @param {object} props - All component props
* @param {function} Link - An optional framework specific Link component for client-side routing * @param {React.Component} props.Link - A framework specific Link component for client-side routing
* @param {string} props.design - The name/id of the design
* @param {boolean} props.noDocsLink - Set this to true to not render a link to the documentation
* @returns {JSX.Element}
*/ */
export const DesignInfo = ({ Link = false, design = false, noDocsLink = false }) => { export const DesignInfo = ({ Link = false, design = false, noDocsLink = false }) => {
if (!Link) Link = WebLink if (!Link) Link = WebLink
@ -375,10 +331,6 @@ export const DesignInfo = ({ Link = false, design = false, noDocsLink = false })
// State // State
const [back, setBack] = useState(false) const [back, setBack] = useState(false)
// Context
const { setModal, clearModal } = useContext(ModalContext)
const { setLoadingStatus } = useContext(LoadingStatusContext)
if (!design) return null if (!design) return null
// Line drawings // Line drawings
@ -392,13 +344,6 @@ export const DesignInfo = ({ Link = false, design = false, noDocsLink = false })
: [about[design].design] : [about[design].design]
const tags = about[design].tags || [] const tags = about[design].tags || []
const techniques = about[design].techniques || [] const techniques = about[design].techniques || []
const colors = {
1: 'success',
2: 'success',
3: 'warning',
4: 'warning',
5: 'error',
}
const makeButton = ( const makeButton = (
<div className={`tw:grid tw:grid-cols-1 tw:gap-2 tw:mb-4`}> <div className={`tw:grid tw:grid-cols-1 tw:gap-2 tw:mb-4`}>
@ -506,7 +451,7 @@ export const DesignInfo = ({ Link = false, design = false, noDocsLink = false })
className="tw:daisy-badge tw:daisy-badge-primary tw:font-medium tw:hover:shadow tw:hover:cursor-pointer" className="tw:daisy-badge tw:daisy-badge-primary tw:font-medium tw:hover:shadow tw:hover:cursor-pointer"
href={`/designs/#filter={"example"%3Atrue%2C"tag"%3A["${tag}"]}`} href={`/designs/#filter={"example"%3Atrue%2C"tag"%3A["${tag}"]}`}
> >
{tag} <span className="tw:text-primary-content">{tag}</span>
</Link> </Link>
))} ))}
</div> </div>
@ -518,7 +463,7 @@ export const DesignInfo = ({ Link = false, design = false, noDocsLink = false })
className="tw:daisy-badge tw:daisy-badge-accent tw:font-medium tw:hover:shadow tw:hover:cursor-pointer" className="tw:daisy-badge tw:daisy-badge-accent tw:font-medium tw:hover:shadow tw:hover:cursor-pointer"
href={`/designs/#filter={"example"%3Atrue%2C"tag"%3A["${tech}"]}`} href={`/designs/#filter={"example"%3Atrue%2C"tag"%3A["${tech}"]}`}
> >
{tech} <span className="tw:text-accent-content">{tech}</span>
</Link> </Link>
))} ))}
</div> </div>
@ -555,21 +500,3 @@ export const DesignInfo = ({ Link = false, design = false, noDocsLink = false })
</> </>
) )
} }
const SharingIsCaring = ({ design }) => (
<>
<h2>
Use <b>#FreeSewing{capitalize(design)}</b> to facilitate discovery
</h2>
<p>
Please use the{' '}
<b>
<code>#FreeSewing{capitalize(design)}</code>
</b>{' '}
hashtag when discussing FreeSewing&apos;s <b>{capitalize(design)}</b> pattern online.
<br />
Doing so can help others discover your post, which really is a win-win.
</p>
<p>If you like, you can copy the hashtag below:</p>
</>
)

View file

@ -2,9 +2,17 @@ import React from 'react'
import { controlDesc } from '@freesewing/config' import { controlDesc } from '@freesewing/config'
import { BulletIcon } from '@freesewing/react/components/Icon' import { BulletIcon } from '@freesewing/react/components/Icon'
export const ControlScore = ({ control, color = 'base-content' }) => /**
* A component to render a visualisation of the user's control/UX setting
*
* @component
* @param {object} props - All component props
* @param {number} props.control - The user's control setting (a number)
* @returns {JSX.Element}
*/
export const ControlScore = ({ control }) =>
control ? ( control ? (
<div className={`tw:flex tw:flex-row tw:items-center tw:text-${color}`}> <div className={`tw:flex tw:flex-row tw:items-center tw:text-base-content`}>
{Object.keys(controlDesc).map((score) => ( {Object.keys(controlDesc).map((score) => (
<BulletIcon <BulletIcon
fill={control >= score ? true : false} fill={control >= score ? true : false}

View file

@ -1,49 +0,0 @@
import React, { useContext, useState } from 'react'
import { copyToClipboard } from '@freesewing/utils'
import ReactDOMServer from 'react-dom/server'
import { CopyIcon, OkIcon } from '@freesewing/react/components/Icon'
import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus'
const strip = (html) =>
typeof DOMParser === 'undefined'
? html
: new DOMParser().parseFromString(html, 'text/html').body.textContent || ''
const handleCopied = (content, setCopied, setLoadingStatus, label) => {
copyToClipboard(content)
setCopied(true)
setLoadingStatus([
true,
label ? `${label} copied to clipboard` : 'Copied to clipboard',
true,
true,
])
setTimeout(() => setCopied(false), 1000)
}
export const CopyToClipboardButton = ({ children, content, label = false, sup = false }) => {
const [copied, setCopied] = useState(false)
const { setLoadingStatus } = useContext(LoadingStatusContext)
const style = sup ? 'tw:w-4 tw:h-4 tw:-mt-4' : 'tw:w-5 tw:h-5'
return (
<button
className={
(copied ? 'tw:text-success ' : '') +
'tw:daisy-btn tw:w-full tw:daisy-btn-ghost tw:lg:w-auto'
}
onClick={() => handleCopied(content, setCopied, setLoadingStatus, label)}
>
{copied ? (
<OkIcon
className={`${style} tw:text-success-content tw:bg-success tw:rounded-full tw:p-1`}
stroke={4}
/>
) : (
<CopyIcon className={`${style} tw:text-inherit`} />
)}
{children}
</button>
)
}

View file

@ -12,6 +12,18 @@ import { Spinner } from '@freesewing/react/components/Spinner'
import { Markdown } from '@freesewing/react/components/Markdown' import { Markdown } from '@freesewing/react/components/Markdown'
import { KeyVal } from '@freesewing/react/components/KeyVal' import { KeyVal } from '@freesewing/react/components/KeyVal'
/**
* A component to render a lineup of curated measurements sets.
*
* You need to provide either a clickHandler or a method to resolve the URL to link to as the href prop.
*
* @component
* @param {object} props - All component props
* @param {React.Component} props.Link - A framework specific Link component for client-side routing
* @param {function} [props.clickHandler = false] - An optional function to call when a set is clicked
* @param {function} [props.href = false] - An optional function that should return the URL to be used for a given set
* @returns {JSX.Element}
*/
export const CuratedSetLineup = ({ href = false, clickHandler = false, Link = false }) => { export const CuratedSetLineup = ({ href = false, clickHandler = false, Link = false }) => {
if (!Link) Link = WebLink if (!Link) Link = WebLink
// Hooks // Hooks
@ -86,6 +98,15 @@ export const CuratedSetLineup = ({ href = false, clickHandler = false, Link = fa
) )
} }
/**
* A component to render a curated measurements set
*
* @component
* @param {object} props - All component props
* @param {React.Component} props.Link - A framework specific Link component for client-side routing
* @param {number} props.id - The ID of the curated set
* @returns {JSX.Element}
*/
export const CuratedSet = ({ Link = false, id = false }) => { export const CuratedSet = ({ Link = false, id = false }) => {
if (!Link) Link = WebLink if (!Link) Link = WebLink
// Hooks // Hooks

View file

@ -1,105 +0,0 @@
// Dependencies
import { collection } from '@freesewing/collection'
import { capitalize } from '@freesewing/utils'
// Hooks
import React, { useState } from 'react'
// Components
import { Link as WebLink, AnchorLink } from '@freesewing/react/components/Link'
import { ShowcaseIcon } from '@freesewing/react/components/Icon'
const linkBuilders = {
new: (design) => `/-/?d=${design.toLowerCase()}`,
docs: (design) => `/docs/designs/${design.toLowerCase()}`,
}
export const DesignTechnique = ({ technique }) => (
<Link
className="badge badge-accent hover:badge-secondary hover:shadow font-medium"
href={`/designs/techniques/${technique}`}
>
{technique}
</Link>
)
export const DesignTag = ({ tag }) => {
const { t } = useTranslation(['tags'])
return (
<Link
className="badge badge-primary hover:badge-secondary hover:shadow font-medium"
href={`/designs/tags/${tag}`}
>
{t(tag)}
</Link>
)
}
export const DesignLink = ({ name, linkTo = 'new', className = linkClasses }) => (
<Link href={linkBuilders[linkTo](name)} className={className}>
{name}
</Link>
)
export const DesignCard = ({ name, lineDrawing = false }) => {
const { t } = useTranslation(ns)
// Context
const { setModal } = useContext(ModalContext)
const LineDrawing =
lineDrawing && lineDrawings[name]
? lineDrawings[name]
: ({ className }) => <div className={className}></div>
const exampleImageUrl = designImages[name]
? designImages[name]
: 'https://images.pexels.com/photos/5626595/pexels-photo-5626595.jpeg?cs=srgb&dl=pexels-frida-toth-5626595.jpg&fm=jpg&w=640&h=427&_gl=1*vmxq7y*_ga*MTM0OTk5OTY4NS4xNjYxMjUyMjc0*_ga_8JE65Q40S6*MTY5NTU1NDc0Mi4yNS4xLjE2OTU1NTU1NjIuMC4wLjA.'
const bg = lineDrawing
? {}
: {
backgroundImage: `url(${exampleImageUrl}`,
backgroundSize: 'cover',
backgroundPosition: 'center center',
}
return (
<button
onClick={() =>
setModal(
<ModalWrapper
flex="col"
justify="top lg:justify-center"
slideFrom="right"
keepOpenOnClick
>
<h1>{t(`designs:${name}.t`)}</h1>
<DesignInfo design={name} modal />
</ModalWrapper>
)
}
>
<div
className={`flex flex-col flex-nowrap items-start justify-start gap-2 h-80 w-full
btn btn-ghost border border-neutral p-0 border-b-none
hover:border hover:border-secondary
relative`}
style={bg}
>
<h5
className={`text-center py-2 px-4 rounded-t-lg m-0 w-full
${lineDrawing ? '' : 'bg-neutral/70 text-neutral-content'}`}
>
{t(`designs:${name}.t`)}
</h5>
<div className={lineDrawing ? 'p-4 grow w-full' : 'py-8'}>
<LineDrawing className="h-64 max-w-full m-auto my-4 text-base-content" />
</div>
<div
className={`pt-0 m-0 -mt-2 text-center w-full
${lineDrawing ? 'bg-transparent text-base-content' : 'bg-neutral/70 text-neutral-content'}`}
></div>
</div>
</button>
)
}

View file

@ -2,7 +2,24 @@ import React from 'react'
import { diffWords, diffJson } from 'diff' import { diffWords, diffJson } from 'diff'
import ReactDiffViewer from 'react-diff-viewer-continued' import ReactDiffViewer from 'react-diff-viewer-continued'
/**
* A method to diff JSON content
*
* @public
* @param {object} from - Once side of the diff
* @param {object} to - Other side of the diff
* @returns {object}
*/
export const diffJSON = (from, to) => diffJson(from, to) export const diffJSON = (from, to) => diffJson(from, to)
/**
* A method to diff string content
*
* @public
* @param {string} from - Once side of the diff
* @param {string} to - Other side of the diff
* @returns {object}
*/
export const diffCheck = (from, to) => diffWords(from, to) export const diffCheck = (from, to) => diffWords(from, to)
export const DiffViewer = (props) => <ReactDiffViewer {...props} /> export const DiffViewer = (props) => <ReactDiffViewer {...props} />

View file

@ -9,6 +9,7 @@ import {
ChatIcon, ChatIcon,
DesignIcon, DesignIcon,
DocsIcon, DocsIcon,
HelpIcon,
ShowcaseIcon, ShowcaseIcon,
RssIcon, RssIcon,
LockIcon, LockIcon,
@ -16,16 +17,25 @@ import {
} from '@freesewing/react/components/Icon' } from '@freesewing/react/components/Icon'
import { Layout as DefaultLayout } from '@freesewing/react/components/Layout' import { Layout as DefaultLayout } from '@freesewing/react/components/Layout'
/* /**
* This component should be the top level of a Docusaurus page * This component should be the top level of a Docusaurus page where you want access to context.
* *
* This sets up the various context providers before * This sets up the various context providers before
* passing all props down to the InnerPageWrapper. * passing all props down to the InnerPageWrapper.
* This is required because the context providers need to * This is required because the context providers need to
* be setup for the modal and loading state work we do in the InnerPageWrapper * be set up for the modal and loading state work.
*
* We also re-use the Docusaurus Layout component here, which needs to be at * We also re-use the Docusaurus Layout component here, which needs to be at
* the top level of the page * the top level of the page.
*
* @component
* @param {object} props - All component props
* @param {React.Component} props.DocusaurusLayout - The docusaurus layout to apply
* @param {React.Component} props.Layout - The layout to apply inside docusaurus
* @param {JSX.Element} props.children - The component children
* @param {array} props.crumbs - The page breadcrumbs
* @param {string} props.description - The page description
* @param {string} props.title - The page title
* @returns {JSX.Element}
*/ */
export const DocusaurusPage = (props) => { export const DocusaurusPage = (props) => {
const DocusaurusLayout = props.DocusaurusLayout const DocusaurusLayout = props.DocusaurusLayout
@ -42,14 +52,23 @@ export const DocusaurusPage = (props) => {
) )
} }
/* /**
* This component should be the top level of any Docusaurus content that's not * This component should be the top level of any Docusaurus content that's not
* a full page where you want access to context (typically account pages and so on) * a full page where you want access to context (typically account pages and so on).
* *
* This sets up the various context providers before * This sets up the various context providers before
* passing all props down to the InnerPageWrapper. * passing all props down to the InnerPageWrapper.
* This is required because the context providers need to * This is required because the context providers need to
* be setup for the modal and loading state work we do in the InnerPageWrapper * be setup for the modal and loading state work we do in the InnerPageWrapper
*
* @component
* @param {object} props - All component props
* @param {React.Component} props.Layout - The layout to apply inside docusaurus
* @param {JSX.Element} props.children - The component children
* @param {array} props.crumbs - The page breadcrumbs
* @param {string} props.description - The page description
* @param {string} props.title - The page title
* @returns {JSX.Element}
*/ */
export const DocusaurusDoc = (props) => ( export const DocusaurusDoc = (props) => (
<ModalContextProvider> <ModalContextProvider>
@ -94,6 +113,19 @@ const InnerDocusaurusPage = ({
) )
} }
/**
* A component to render (some) custom navbar items in Docusaurus
*
* This can be used to swizzle the default NavbarItem in Docusaurus.
* You should pass it in the default NavbarItem and it will use that
* for all but the following: account, designs, docs, blog, showcase, forum
*
* @component
* @param {object} props - All component props
* @param {string} props.id - The navbar item ID
* @param {React.Component} props.Default - The default NavbarItem component to use
* @returns {JSX.Element}
*/
export const NavbarItem = (props) => { export const NavbarItem = (props) => {
const { id, Default } = props const { id, Default } = props
@ -153,4 +185,7 @@ const navbarItems = {
Link={props.Link} Link={props.Link}
/> />
), ),
support: (props) => (
<SimpleNavbarItem label="Support" href="/support/" Icon={HelpIcon} Link={props.Link} />
),
} }

View file

@ -3,6 +3,12 @@ import * as _echarts from 'echarts'
import ReactECharts from 'echarts-for-react' import ReactECharts from 'echarts-for-react'
import { Popout } from '@freesewing/react/components/Popout' import { Popout } from '@freesewing/react/components/Popout'
/**
* Re-export of Apache Echarts
*
* @public
* @constant
*/
export const echarts = _echarts export const echarts = _echarts
echarts.registerTheme('light', { echarts.registerTheme('light', {
@ -12,6 +18,25 @@ echarts.registerTheme('dark', {
backgroundColor: 'transparent', backgroundColor: 'transparent',
}) })
/**
* A component to provide Echart functionality.
*
* This is a wrapper around Apache Echarts. The option prop is for echarts.
*
* @component
* @param {object} props - All component props
* @param {object} [props.option = false] - The Echarts option object. This is
* marked as optional because this component will show a loading message when
* option is not an object. However, that is intended for use-cases where
* option relies on async code. This component is pointless if you do not
* (eventually) pass it an option prop.
* @param {string} [props.theme = 'light'] - The theme to use for echarts. Supports 'light' and 'dark'.
* @param {number} [props.h = 400] - The height of the chart, in pixels. Charts
* are rendered as SVG, we need to set a height because without a height, some
* browsers will not properly render the SVG element. This is an Echart
* limitation.
* @returns {JSX.Element}
*/
export const ChartWrapper = ({ option = false, theme = 'light', h = 400 }) => { export const ChartWrapper = ({ option = false, theme = 'light', h = 400 }) => {
return option ? ( return option ? (
<ReactECharts option={option} className="class_2" theme={theme} style={{ height: h }} /> <ReactECharts option={option} className="class_2" theme={theme} style={{ height: h }} />

View file

@ -4,12 +4,12 @@ import React, { useState } from 'react'
* DaisyUI's accordion seems rather unreliable. * DaisyUI's accordion seems rather unreliable.
* So instead, we handle this in React state * So instead, we handle this in React state
*/ */
const getProps = (isActive = false) => ({ const getProps = () => ({
className: `tw:p-0 tw:rounded-lg tw:bg-transparent tw:hover:cursor-pointer className: `tw:p-0 tw:rounded-lg tw:bg-transparent tw:hover:cursor-pointer
tw:w-full tw:h-auto tw:content-start tw:text-left tw:list-none`, tw:w-full tw:h-auto tw:content-start tw:text-left tw:list-none`,
}) })
const getSubProps = (isActive) => ({ const getSubProps = () => ({
className: `tw:p-0 tw:rounded-none tw:bg-transparent tw:w-full tw:h-auto className: `tw:p-0 tw:rounded-none tw:bg-transparent tw:w-full tw:h-auto
tw:content-start tw:text-left tw:list-none`, tw:content-start tw:text-left tw:list-none`,
}) })

View file

@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import mustache from 'mustache' import mustache from 'mustache'
import { flattenFlags, stripNamespace, bundlePatternTranslations } from '../lib/index.mjs' import { flattenFlags } from '../lib/index.mjs'
import { import {
ChatIcon, ChatIcon,
ErrorIcon, ErrorIcon,
@ -115,7 +115,7 @@ export const FlagsAccordionTitle = ({ flags }) => {
) )
} }
export const FlagsAccordionEntries = ({ flags, update, pattern, strings }) => { export const FlagsAccordionEntries = ({ flags, update, strings }) => {
const flagList = flattenFlags(flags) const flagList = flattenFlags(flags)
if (Object.keys(flagList).length < 1) return null if (Object.keys(flagList).length < 1) return null

View file

@ -17,6 +17,7 @@ import {
ExportIcon, ExportIcon,
FixmeIcon, FixmeIcon,
FlagIcon, FlagIcon,
MeasurementsIcon,
OptionsIcon, OptionsIcon,
PaperlessIcon, PaperlessIcon,
PrintIcon, PrintIcon,
@ -24,7 +25,6 @@ import {
ResetIcon, ResetIcon,
RightIcon, RightIcon,
RocketIcon, RocketIcon,
RotateIcon,
SaIcon, SaIcon,
SaveAsIcon, SaveAsIcon,
SaveIcon, SaveIcon,
@ -53,6 +53,7 @@ const headerMenuIcons = {
settings: SettingsIcon, settings: SettingsIcon,
ui: UiIcon, ui: UiIcon,
layout: PrintIcon, layout: PrintIcon,
measurements: MeasurementsIcon,
} }
export const HeaderMenuIcon = (props) => { export const HeaderMenuIcon = (props) => {
@ -122,7 +123,7 @@ export const HeaderMenuTestViewDesignMeasurements = (props) => {
tooltip="See how changes to a measurement influence the pattern being generated." tooltip="See how changes to a measurement influence the pattern being generated."
toggle={ toggle={
<> <>
<HeaderMenuIcon name="options" extraClasses="tw:text-secondary" /> <HeaderMenuIcon name="measurements" extraClasses="tw:text-secondary" />
<span className="tw:hidden tw:lg:inline">Test Measurements</span> <span className="tw:hidden tw:lg:inline">Test Measurements</span>
</> </>
} }
@ -133,7 +134,7 @@ export const HeaderMenuTestViewDesignMeasurements = (props) => {
} }
export const HeaderMenuDropdown = (props) => { export const HeaderMenuDropdown = (props) => {
const { tooltip, toggle, open, setOpen, id, end = false } = props const { toggle, open, setOpen, id } = props
const [localOpen, setLocalOpen] = useState(false) const [localOpen, setLocalOpen] = useState(false)
useEffect(() => { useEffect(() => {
@ -460,18 +461,16 @@ export const HeaderMenuUndoIcons = (props) => {
} }
export const HeaderMenuTestIcons = (props) => { export const HeaderMenuTestIcons = (props) => {
const { update, state, Design } = props const { update } = props
const Button = HeaderMenuButton const Button = HeaderMenuButton
const size = 'tw:w-5 tw:h-5'
const undos = state._?.undos && state._.undos.length > 0 ? state._.undos : false
return ( return (
<div className="tw:flex tw:flex-row tw:flex-wrap tw:items-center tw:justify-center tw:px-0.5 tw:lg:px-1"> <div className="tw:flex tw:flex-row tw:flex-wrap tw:items-center tw:justify-center tw:px-0.5 tw:lg:px-1 tw:items-center">
<Button <Button
updateHandler={() => update.settings('sample', undefined)} updateHandler={() => update.settings('sample', undefined)}
tooltip="Clear the test so you can select another" tooltip="Clear the test so you can select another"
> >
Clear Test <ResetIcon /> <span className="tw:hidden tw:lg:inline">Clear Test</span>
</Button> </Button>
</div> </div>
) )
@ -531,7 +530,7 @@ export const HeaderMenuButton = ({
}) => ( }) => (
<Tooltip tip={tooltip}> <Tooltip tip={tooltip}>
<button <button
className={`${lgOnly ? 'tw:hidden tw:lg:inline' : ''} tw:daisy-btn tw:daisy-btn-ghost tw:daisy-btn-sm tw:px-1 tw:disabled:bg-transparent`} className={`${lgOnly ? 'tw:hidden tw:lg:inline' : ''} tw:daisy-btn tw:daisy-btn-ghost tw:my-1 tw:px-1 tw:disabled:bg-transparent`}
onClick={updateHandler} onClick={updateHandler}
disabled={disabled} disabled={disabled}
> >
@ -607,7 +606,7 @@ export const HeaderMenuViewMenu = (props) => {
} }
export const HeaderMenuLayoutView = (props) => ( export const HeaderMenuLayoutView = (props) => (
<> <div className="tw:flex tw:flex-row tw:items-center">
<HeaderMenuDropdown <HeaderMenuDropdown
{...props} {...props}
id="layoutOptions" id="layoutOptions"
@ -622,7 +621,7 @@ export const HeaderMenuLayoutView = (props) => (
<LayoutSettingsMenu {...props} /> <LayoutSettingsMenu {...props} />
</HeaderMenuDropdown> </HeaderMenuDropdown>
<HeaderMenuLayoutViewIcons {...props} /> <HeaderMenuLayoutViewIcons {...props} />
</> </div>
) )
export const HeaderMenuLayoutViewIcons = (props) => { export const HeaderMenuLayoutViewIcons = (props) => {
@ -660,11 +659,6 @@ export const HeaderMenuLayoutViewIcons = (props) => {
} }
const pages = pattern.setStores[0].get('pages', {}) const pages = pattern.setStores[0].get('pages', {})
const format = state.ui.print?.pages?.size
? state.ui.print.pages.size
: settings.units === 'imperial'
? 'letter'
: 'a4'
const { cols, rows, count } = pages const { cols, rows, count } = pages
const blank = cols * rows - count const blank = cols * rows - count

View file

@ -35,11 +35,6 @@ export const LoadingStatus = ({ state, update }) => {
if (!state._.loading || Object.keys(state._.loading).length < 1) return null if (!state._.loading || Object.keys(state._.loading).length < 1) return null
const colorClasses = {
info: 'tw:bg-info tw:text-info-content',
primary: 'tw:bg-primary tw:text-primary-content',
}
return ( return (
<div className="tw:fixed tw:bottom-4 md:tw:buttom-28 tw:left-0 tw:w-full tw:z-30 tw:md:px-4 tw:md:mx-auto mb-4"> <div className="tw:fixed tw:bottom-4 md:tw:buttom-28 tw:left-0 tw:w-full tw:z-30 tw:md:px-4 tw:md:mx-auto mb-4">
<div className="tw:flex tw:flex-col tw:gap-2"> <div className="tw:flex tw:flex-col tw:gap-2">

View file

@ -1,5 +1,10 @@
import React from 'react' import React, { useContext } from 'react'
import { measurements as measurementTranslations } from '@freesewing/i18n'
// Context
import { ModalContext } from '@freesewing/react/context/Modal'
// Components
import { MeasurementInput } from '@freesewing/react/components/Input' import { MeasurementInput } from '@freesewing/react/components/Input'
import { modalMeasurementHelp } from '@freesewing/react/components/Help'
/** /**
* This MeasurementsEditor component allows inline-editing of the measurements * This MeasurementsEditor component allows inline-editing of the measurements
@ -9,10 +14,13 @@ import { MeasurementInput } from '@freesewing/react/components/Input'
* @param {object} props.state - The ViewWrapper state object * @param {object} props.state - The ViewWrapper state object
* @param {object} props.state.settings - The current settings * @param {object} props.state.settings - The current settings
* @param {object} props.update - Helper object for updating the ViewWrapper state * @param {object} props.update - Helper object for updating the ViewWrapper state
* @param {object} props.helpProvider - A function that takes a measurement and returns a url or action to show help for that measurement
* @return {function} MeasurementsEditor - React component * @return {function} MeasurementsEditor - React component
*/ */
export const MeasurementsEditor = ({ Design, update, state, helpProvider = false }) => { export const MeasurementsEditor = ({ Design, update, state }) => {
// Context
const { setModal, modalContent } = useContext(ModalContext)
console.log({ modalContent })
/* /*
* Helper method to handle state updates for measurements * Helper method to handle state updates for measurements
*/ */
@ -26,7 +34,7 @@ export const MeasurementsEditor = ({ Design, update, state, helpProvider = false
const { settings = {} } = state const { settings = {} } = state
return ( return (
<div className="tw:max-w-2xl tw:mx-auto"> <div className="tw:max-w-xl tw:w-full tw:mx-auto">
<h4>Required Measurements</h4> <h4>Required Measurements</h4>
{Object.keys(Design.patternConfig.measurements).length === 0 ? ( {Object.keys(Design.patternConfig.measurements).length === 0 ? (
<p>This design does not require any measurements.</p> <p>This design does not require any measurements.</p>
@ -40,7 +48,8 @@ export const MeasurementsEditor = ({ Design, update, state, helpProvider = false
original={settings.measurements?.[m]} original={settings.measurements?.[m]}
update={(m, newVal) => onUpdate(m, newVal)} update={(m, newVal) => onUpdate(m, newVal)}
id={`edit-${m}`} id={`edit-${m}`}
helpProvider={helpProvider} label={measurementTranslations[m]}
help={() => modalMeasurementHelp(m, setModal)}
/> />
))} ))}
<br /> <br />
@ -58,6 +67,7 @@ export const MeasurementsEditor = ({ Design, update, state, helpProvider = false
original={settings.measurements?.[m]} original={settings.measurements?.[m]}
update={(m, newVal) => onUpdate(m, newVal)} update={(m, newVal) => onUpdate(m, newVal)}
id={`edit-${m}`} id={`edit-${m}`}
help={() => modalMeasurementHelp(m, setModal)}
/> />
)) ))
)} )}

View file

@ -2,7 +2,6 @@ import React, { useRef, useState, useEffect, useCallback } from 'react'
import { ZoomablePattern } from './ZoomablePattern.mjs' import { ZoomablePattern } from './ZoomablePattern.mjs'
import { generateStackTransform, getTransformedBounds } from '@freesewing/core' import { generateStackTransform, getTransformedBounds } from '@freesewing/core'
import { getProps } from '@freesewing/react/components/Pattern' import { getProps } from '@freesewing/react/components/Pattern'
import { FlipIcon, RotateIcon, ResetIcon } from '@freesewing/react/components/Icon'
import { drag } from 'd3-drag' import { drag } from 'd3-drag'
import { select } from 'd3-selection' import { select } from 'd3-selection'
//import { Buttons } from './transform-buttons.mjs' //import { Buttons } from './transform-buttons.mjs'
@ -85,7 +84,7 @@ export const MovablePattern = ({
const sortedRenderProps = { ...renderProps, stacks: sortedStacks } const sortedRenderProps = { ...renderProps, stacks: sortedStacks }
const Stack = ({ stackName, stack, settings, components, t }) => ( const Stack = ({ stackName, stack, settings, components }) => (
<MovableStack <MovableStack
{...{ {...{
stackName, stackName,
@ -397,7 +396,7 @@ function angle(pointA, pointB) {
const rectSize = 24 const rectSize = 24
const Button = ({ onClickCb, transform, Icon, children, title = '' }) => { const Button = ({ onClickCb, transform, Icon, title = '' }) => {
const _onClick = (event) => { const _onClick = (event) => {
event.stopPropagation() event.stopPropagation()
onClickCb(event) onClickCb(event)
@ -413,22 +412,24 @@ const Button = ({ onClickCb, transform, Icon, children, title = '' }) => {
) )
} }
export const ShowButtonsToggle = ({ ui, update }) => { const InnerFlipIcon = () => (
const hideButtons = (evt) => { <path
update.ui('hideMovableButtons', !evt.target.checked) strokeLinecap="round"
} strokeLinejoin="round"
return ( d="M7.5 21 3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5"
<label className="label cursor-pointer"> />
<span className="label-text text-lg mr-2">{t('showMovableButtons')}</span> )
<input const InnerRotateIcon = ({ props }) => (
type="checkbox" <path
className="toggle toggle-primary" strokeLinecap="round"
checked={!ui.hideMovableButtons} strokeLinejoin="round"
onChange={hideButtons} d="M 19.5,12 C 19.5,10.768 19.454,9.547 19.362,8.338 19.21576,6.3582806 17.641719,4.7842398 15.662,4.638 14.504476,4.5506731 13.344609,4.5048098 12.184624,4.5004103 M 19.5,12 l 3,-3 m -3,3 -3,-3 m -12,3 c 0,1.232 0.046,2.453 0.138,3.662 0.1462398,1.979719 1.7202806,3.55376 3.7,3.7 1.295324,0.09777 2.593584,0.143587 3.891661,0.13746 M 4.5,12 l 3,3 m -3,-3 -3,3"
/> {...props}
</label> />
) )
} const InnerResetIcon = () => (
<path d="M12 9.75 14.25 12m0 0 2.25 2.25M14.25 12l2.25-2.25M14.25 12 12 14.25m-2.58 4.92-6.374-6.375a1.125 1.125 0 0 1 0-1.59L9.42 4.83c.21-.211.497-.33.795-.33H19.5a2.25 2.25 0 0 1 2.25 2.25v10.5a2.25 2.25 0 0 1-2.25 2.25h-9.284c-.298 0-.585-.119-.795-.33Z" />
)
/** buttons for manipulating the part */ /** buttons for manipulating the part */
export const Buttons = ({ transform, flip, rotate, resetPart, rotate90, iconSize }) => { export const Buttons = ({ transform, flip, rotate, resetPart, rotate90, iconSize }) => {
@ -455,31 +456,31 @@ export const Buttons = ({ transform, flip, rotate, resetPart, rotate90, iconSize
<Button <Button
onClickCb={resetPart} onClickCb={resetPart}
transform={`translate(${rectSize / -2}, ${rectSize / -2})`} transform={`translate(${rectSize / -2}, ${rectSize / -2})`}
Icon={() => <ResetIcon wrapped={0} />} Icon={() => <InnerResetIcon />}
title="Reset part orientation" title="Reset part orientation"
/> />
<Button <Button
onClickCb={() => rotate90()} onClickCb={() => rotate90()}
transform={`translate(${rectSize * -2.7}, ${rectSize / -2})`} transform={`translate(${rectSize * -2.7}, ${rectSize / -2})`}
Icon={() => <RotateIcon wrapped={0} style={{}} />} Icon={() => <InnerRotateIcon />}
title="Rotate part clockwise" title="Rotate part clockwise"
/> />
<Button <Button
onClickCb={() => flip('y')} onClickCb={() => flip('y')}
transform={`rotate(90) translate(${rectSize / -2}, ${rectSize * -1.6})`} transform={`rotate(90) translate(${rectSize / -2}, ${rectSize * -1.6})`}
Icon={() => <FlipIcon wrapped={0} />} Icon={() => <InnerFlipIcon />}
title="Flip part top/bottom" title="Flip part top/bottom"
/> />
<Button <Button
onClickCb={() => flip('x')} onClickCb={() => flip('x')}
transform={`translate(${rectSize * -1.6}, ${rectSize / -2})`} transform={`translate(${rectSize * -1.6}, ${rectSize / -2})`}
Icon={() => <FlipIcon style={{}} wrapped={0} />} Icon={() => <InnerFlipIcon />}
title="Flip part left/right" title="Flip part left/right"
/> />
<Button <Button
onClickCb={() => rotate90(-1)} onClickCb={() => rotate90(-1)}
transform={`translate(${rectSize * 1.7}, ${rectSize / -2})`} transform={`translate(${rectSize * 1.7}, ${rectSize / -2})`}
Icon={() => <RotateIcon transform="scale(-1,1), translate(-24,0)" wrapped={0} />} Icon={() => <InnerRotateIcon transform="scale(-1,1), translate(-24,0)" />}
title="Rotate part counter-clockwise" title="Rotate part counter-clockwise"
/> />
</g> </g>

View file

@ -1,17 +1,12 @@
import React from 'react' import React from 'react'
import { useDesignTranslation } from '@freesewing/react/hooks/useDesignTranslation'
import { ZoomContextProvider } from './ZoomablePattern.mjs' import { ZoomContextProvider } from './ZoomablePattern.mjs'
import { import { HeaderMenu } from './HeaderMenu.mjs'
HeaderMenu,
HeaderMenuDraftViewDesignOptions,
HeaderMenuDraftViewCoreSettings,
HeaderMenuDraftViewUiPreferences,
HeaderMenuDraftViewFlags,
} from './HeaderMenu.mjs'
import { DesignOptionsMenu } from './menus/DesignOptionsMenu.mjs' import { DesignOptionsMenu } from './menus/DesignOptionsMenu.mjs'
import { CoreSettingsMenu } from './menus/CoreSettingsMenu.mjs' import { CoreSettingsMenu } from './menus/CoreSettingsMenu.mjs'
import { UiPreferencesMenu } from './menus/UiPreferencesMenu.mjs' import { UiPreferencesMenu } from './menus/UiPreferencesMenu.mjs'
import { Accordion } from './Accordion.mjs' import { LayoutSettingsMenu } from './menus/LayoutMenu.mjs'
import { TestOptionsMenu, TestMeasurementsMenu } from './menus/TestMenu.mjs'
import { useDesignTranslation } from '@freesewing/react/hooks/useDesignTranslation'
/** /**
* A layout for views that include a drafted pattern * A layout for views that include a drafted pattern
@ -24,9 +19,8 @@ import { Accordion } from './Accordion.mjs'
* @param {object] pattern - The drafted pattern * @param {object] pattern - The drafted pattern
*/ */
export const PatternLayout = (props) => { export const PatternLayout = (props) => {
const { menu = null, Design, pattern, update, config, state } = props const { Design, pattern, update, config } = props
const i18n = useDesignTranslation(Design.designConfig.data.id) const i18n = useDesignTranslation(Design.designConfig.data.id)
const flags = props.pattern?.setStores?.[0]?.plugins?.['plugin-annotations']?.flags
return ( return (
<ZoomContextProvider> <ZoomContextProvider>
@ -39,30 +33,66 @@ export const PatternLayout = (props) => {
<div className="tw:lg:w-2/3 tw:flex tw:flex-col tw:h-full tw:grow tw:p-2 tw:shadow tw:mx-2"> <div className="tw:lg:w-2/3 tw:flex tw:flex-col tw:h-full tw:grow tw:p-2 tw:shadow tw:mx-2">
{props.output} {props.output}
</div> </div>
{state.ui?.aside ? ( <PatternAsideMenu {...props} i18n={i18n} />
<div
className={`tw:hidden tw:md:block tw:w-1/3 tw:shrink tw:grow-0 tw:lg:p-4 tw:max-w-2xl tw:h-full tw:overflow-scroll`}
>
<h5 className="tw:capitalize">{pattern.designConfig.data.id} Options</h5>
<SideMenuUl>
<DesignOptionsMenu {...props} />
</SideMenuUl>
<h5>Core Settings</h5>
<SideMenuUl>
<CoreSettingsMenu {...props} />
</SideMenuUl>
<h5>UI Preferences</h5>
<SideMenuUl>
<UiPreferencesMenu {...props} />
</SideMenuUl>
</div>
) : null}
</div> </div>
</div> </div>
</ZoomContextProvider> </ZoomContextProvider>
) )
} }
const PatternAsideMenu = (props) => {
if (!props.state.ui?.aside) return null
if (props.state.view === 'draft')
return (
<PatternAsideWrapper>
<h5 className="tw:capitalize">{props.pattern.designConfig.data.id} Options</h5>
<SideMenuUl>
<DesignOptionsMenu {...props} />
</SideMenuUl>
<h5>Core Settings</h5>
<SideMenuUl>
<CoreSettingsMenu {...props} />
</SideMenuUl>
<h5>UI Preferences</h5>
<SideMenuUl>
<UiPreferencesMenu {...props} />
</SideMenuUl>
</PatternAsideWrapper>
)
if (props.state.view === 'layout')
return (
<PatternAsideWrapper>
<h5>Layout Settings</h5>
<SideMenuUl>
<LayoutSettingsMenu {...props} />
</SideMenuUl>
</PatternAsideWrapper>
)
if (props.state.view === 'test')
return (
<PatternAsideWrapper>
<h5>Test Design Options</h5>
<SideMenuUl>
<TestOptionsMenu {...props} />
</SideMenuUl>
<h5>Test Measurements</h5>
<SideMenuUl>
<TestMeasurementsMenu {...props} />
</SideMenuUl>
</PatternAsideWrapper>
)
return null
}
const PatternAsideWrapper = ({ children }) => (
<div
className={`tw:hidden tw:md:block tw:w-1/3 tw:shrink tw:grow-0 tw:lg:p-4 tw:max-w-2xl tw:h-full tw:overflow-scroll`}
>
{children}
</div>
)
export const SideMenuUl = ({ children }) => ( export const SideMenuUl = ({ children }) => (
<ul <ul
className="tw:daisy-menu tw:daisy-dropdown-content tw:flex-nowrap tw:bg-base-200 tw:rounded-box tw:z-1 tw:w-full tw:p-0 tw:pl-0" className="tw:daisy-menu tw:daisy-dropdown-content tw:flex-nowrap tw:bg-base-200 tw:rounded-box tw:z-1 tw:w-full tw:p-0 tw:pl-0"

View file

@ -47,7 +47,7 @@ export const UserSetPicker = ({
if (!hasSets) if (!hasSets)
return ( return (
<div className="tw:w-full tw:max-w-3xl tw:mx-auto"> <div className="tw:w-full tw:max-w-3xl tw:mx-auto">
<Popout tip> <Popout type="tip">
<h5> You do not (yet) have any of your own measurements sets</h5> <h5> You do not (yet) have any of your own measurements sets</h5>
<p> <p>
You can store your measurements as a measurements set, after which you can generate as You can store your measurements as a measurements set, after which you can generate as
@ -58,7 +58,7 @@ export const UserSetPicker = ({
href={config.hrefNewSet} href={config.hrefNewSet}
className="tw:daisy-btn tw:daisy-btn-accent tw:capitalize" className="tw:daisy-btn tw:daisy-btn-accent tw:capitalize"
target="_BLANK" target="_BLANK"
rel="nofollow" rel="noreferrer"
> >
Create a new measurements set Create a new measurements set
</a> </a>
@ -88,7 +88,7 @@ export const UserSetPicker = ({
)} )}
{lackingSets.length > 0 ? ( {lackingSets.length > 0 ? (
<div className="tw:my-4"> <div className="tw:my-4">
<Popout note> <Popout type="note">
<h5> <h5>
Some of your measurements sets lack the measurements required to generate this pattern Some of your measurements sets lack the measurements required to generate this pattern
</h5> </h5>
@ -174,7 +174,7 @@ export const BookmarkedSetPicker = ({
)} )}
{lackingSets.length > 0 && ( {lackingSets.length > 0 && (
<div className="tw:my-4"> <div className="tw:my-4">
<Popout note> <Popout type="note">
<h5> <h5>
Some of these measurements sets lack the measurements required to generate this Some of these measurements sets lack the measurements required to generate this
pattern pattern

View file

@ -7,7 +7,7 @@ import { ZoomInIcon, ZoomOutIcon, RotateIcon } from '@freesewing/react/component
* A pattern you can pan and zoom * A pattern you can pan and zoom
*/ */
export const ZoomablePattern = forwardRef(function ZoomablePatternRef(props, ref) { export const ZoomablePattern = forwardRef(function ZoomablePatternRef(props, ref) {
const { renderProps, rotate, update, components = {}, strings = {} } = props const { renderProps, rotate, components = {}, strings = {} } = props
const { onTransformed, zoomFunctions, setZoomFunctions } = useContext(ZoomContext) const { onTransformed, zoomFunctions, setZoomFunctions } = useContext(ZoomContext)
return ( return (

View file

@ -1,15 +1,27 @@
// Dependencies // Dependencies
import { menuValueWasChanged } from '../../lib/index.mjs' import { menuValueWasChanged } from '../../lib/index.mjs'
import { designOptionType } from '@freesewing/utils' import { designOptionType } from '@freesewing/utils'
import {
modalDesignOptionHelp,
modalCoreSettingHelp,
modalUiPreferenceHelp,
} from '@freesewing/react/components/Help'
// Context
import { ModalContext } from '@freesewing/react/context/Modal'
// Hooks // Hooks
import React, { useState, useMemo } from 'react' import React, { useState, useMemo, useContext } from 'react'
// Components // Components
import { SubAccordion } from '../Accordion.mjs' import { SubAccordion } from '../Accordion.mjs'
import { EditIcon, GroupIcon, OptionsIcon, ResetIcon } from '@freesewing/react/components/Icon' import {
import { FormControl } from '@freesewing/react/components/Input' HelpIcon,
EditIcon,
GroupIcon,
OptionsIcon,
ResetIcon,
} from '@freesewing/react/components/Icon'
import { Fieldset } from '@freesewing/react/components/Input'
import { MiniTip } from '@freesewing/react/components/Mini' import { MiniTip } from '@freesewing/react/components/Mini'
/** @type {String} class to apply to buttons on open menu items */
const iconButtonClass = 'tw:daisy-btn tw:daisy-btn-xs tw:daisy-btn-ghost tw:px-0 tw:text-accent' const iconButtonClass = 'tw:daisy-btn tw:daisy-btn-xs tw:daisy-btn-ghost tw:px-0 tw:text-accent'
/** /**
@ -26,6 +38,7 @@ const iconButtonClass = 'tw:daisy-btn tw:daisy-btn-xs tw:daisy-btn-ghost tw:px-0
* @param {React.Component} Value a value display 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 {Boolean} allowOverride all a text input to be used to override the given input component
* @param {Number} ux the user-defined ux level * @param {Number} ux the user-defined ux level
* @param {strign} type one of designOption, coreSetting, or uiPreference
*/ */
export const MenuItem = ({ export const MenuItem = ({
name, name,
@ -37,11 +50,11 @@ export const MenuItem = ({
allowOverride = false, allowOverride = false,
ux = 5, ux = 5,
state, state,
docs,
config, config,
Design, Design,
i18n, type,
}) => { }) => {
const { setModal } = useContext(ModalContext)
// Local state - whether the override input should be shown // Local state - whether the override input should be shown
const [override, setOverride] = useState(false) const [override, setOverride] = useState(false)
@ -72,6 +85,39 @@ export const MenuItem = ({
// get buttons for open and closed states // get buttons for open and closed states
const buttons = [] const buttons = []
if (type === 'designOption')
buttons.push(
<button
key="help"
className="tw:daisy-btn tw:daisy-btn-xs tw:daisy-btn-ghost tw:px-0 tw:text-success"
onClick={() => modalDesignOptionHelp(Design.designConfig.data.id, name, setModal)}
title="Show help for this design option"
>
<HelpIcon />
</button>
)
else if (type === 'coreSetting')
buttons.push(
<button
key="help"
className="tw:daisy-btn tw:daisy-btn-xs tw:daisy-btn-ghost tw:px-0 tw:text-success"
onClick={() => modalCoreSettingHelp(name, setModal)}
title="Show help for this core setting"
>
<HelpIcon />
</button>
)
else if (type === 'uiPreference')
buttons.push(
<button
key="help"
className="tw:daisy-btn tw:daisy-btn-xs tw:daisy-btn-ghost tw:px-0 tw:text-success"
onClick={() => modalUiPreferenceHelp(name, setModal)}
title="Show help for this UI preference"
>
<HelpIcon />
</button>
)
if (allowOverride) if (allowOverride)
buttons.push( buttons.push(
<button <button
@ -106,7 +152,7 @@ export const MenuItem = ({
return ( return (
<> <>
<FormControl <Fieldset
label={false} label={false}
id={config.name} id={config.name}
labelBR={<div className="tw:flex tw:flex-row tw:items-center tw:gap-2">{buttons}</div>} labelBR={<div className="tw:flex tw:flex-row tw:items-center tw:gap-2">{buttons}</div>}
@ -119,7 +165,7 @@ export const MenuItem = ({
} }
> >
<Input {...drillProps} /> <Input {...drillProps} />
</FormControl> </Fieldset>
{config.about ? <MiniTip>{config.about}</MiniTip> : null} {config.about ? <MiniTip>{config.about}</MiniTip> : null}
</> </>
) )

View file

@ -21,7 +21,7 @@ import {
MenuScaleSettingValue, MenuScaleSettingValue,
} from './Value.mjs' } from './Value.mjs'
import { MenuItemGroup, MenuItem } from './Container.mjs' import { MenuItemGroup, MenuItem } from './Container.mjs'
import { SettingsIcon } from '@freesewing/react/components/Icon' import { SettingsIcon, TrashIcon } from '@freesewing/react/components/Icon'
/** /**
* The core settings menu * The core settings menu
@ -124,6 +124,7 @@ export const CoreSetting = ({ name, config, ux, updateHandler, current, passProp
return ( return (
<MenuItem <MenuItem
type="coreSetting"
{...{ {...{
name, name,
config, config,

View file

@ -121,6 +121,7 @@ export const DesignOption = ({ config, settings, ux, inputs, values, ...rest })
return ( return (
<MenuItem <MenuItem
type="designOption"
{...{ {...{
config, config,
ux, ux,

View file

@ -3,7 +3,7 @@ import { OptionsIcon, SettingsIcon, UiIcon } from '@freesewing/react/components/
import { DesignOptionsMenu } from './DesignOptionsMenu.mjs' import { DesignOptionsMenu } from './DesignOptionsMenu.mjs'
import { CoreSettingsMenu } from './CoreSettingsMenu.mjs' import { CoreSettingsMenu } from './CoreSettingsMenu.mjs'
import { UiPreferencesMenu } from './UiPreferencesMenu.mjs' import { UiPreferencesMenu } from './UiPreferencesMenu.mjs'
import { FlagsAccordionEntries } from '../Flag.mjs' import { FlagsAccordionEntries, FlagsAccordionTitle } from '../Flag.mjs'
import { Accordion } from '../Accordion.mjs' import { Accordion } from '../Accordion.mjs'
export const DraftMenu = ({ Design, pattern, state, update, i18n }) => { export const DraftMenu = ({ Design, pattern, state, update, i18n }) => {

View file

@ -16,12 +16,7 @@ import { mergeOptions } from '@freesewing/core'
import { KeyVal } from '@freesewing/react/components/KeyVal' import { KeyVal } from '@freesewing/react/components/KeyVal'
/** A boolean version of {@see MenuListInput} that sets up the necessary configuration */ /** A boolean version of {@see MenuListInput} that sets up the necessary configuration */
export const MenuBoolInput = (props) => { export const MenuBoolInput = (props) => <MenuListInput {...props} />
const { name, config } = props
//const boolConfig = useBoolConfig(name, config)
return <MenuListInput {...props} />
}
/** A placeholder for an input to handle constant values */ /** A placeholder for an input to handle constant values */
export const MenuConstantInput = ({ export const MenuConstantInput = ({
@ -59,41 +54,6 @@ export const MenuDegInput = (props) => {
) )
} }
const getTitleAndDesc = (config = {}, i18n = {}, isDesignOption = false) => {
if (config.choiceTitles && config.choiceDescriptions) {
const current = typeof config.current === 'undefined' ? config.dflt : config.current
return {
title: config.choiceTitles[current],
desc: config.choiceDescriptions[current],
}
}
let titleKey = config.choiceTitles
? 'fixme' //config.choiceTitles[entry]
: isDesignOption
? i18n?.en?.o?.[name] || name
: `${name}.o.${entry}`
if (!config.choiceTitles && i18n && i18n.en.o[`${name}.${entry}`])
titleKey = i18n.en.o[`${name}.${entry}`]
const title = config.title
? config.title
: config.titleMethod
? config.titleMethod(entry)
: typeof titleKey === 'string'
? i18n.en.o[titleKey]?.t
: titleKey.t
const desc = config.valueMethod
? config.valueMethod(entry)
: typeof titleKey === 'string'
? i18n.en.o[titleKey]?.d
: titleKey.d
return {
title: 'fixmeTitle',
desc: 'fixmeDesc',
}
}
/** /**
* An input for selecting and item from a list * An input for selecting and item from a list
* @param {String} options.name the name of the property this input changes * @param {String} options.name the name of the property this input changes
@ -111,11 +71,9 @@ export const MenuListInput = ({
current, current,
updateHandler, updateHandler,
compact = false, compact = false,
t,
changed, changed,
design, design,
isDesignOption = false, isDesignOption = false,
i18n,
}) => { }) => {
const handleChange = useSharedHandlers({ const handleChange = useSharedHandlers({
dflt: config.dflt, dflt: config.dflt,
@ -125,7 +83,7 @@ export const MenuListInput = ({
}) })
return config.list.map((entry) => { return config.list.map((entry) => {
const { title = false, about = false } = config //getTitleAndDesc(config, i18n, isDesignOption) const { title = false, about = false } = config
if (!title || !about) console.log('No title or about in', { name, config, design }) if (!title || !about) console.log('No title or about in', { name, config, design })
const sideBySide = config.sideBySide || about.length + title.length < 42 const sideBySide = config.sideBySide || about.length + title.length < 42
@ -286,7 +244,6 @@ export const MenuSliderInput = ({
setReset, setReset,
children, children,
changed, changed,
i18n,
state, state,
Design, Design,
}) => { }) => {

View file

@ -61,63 +61,3 @@ export const LayoutSettingsMenu = ({ update, state, Design }) => {
/> />
) )
} }
/*
const PrintActions = ({ state, update }) => (
<SubAccordion
items={[
[
<div className="tw:w-full tw:flex tw:flex-row tw:gap2 tw:justify-between" key={1}>
<div className="tw:flex tw:flex-row tw:items-center tw:gap-2">
<LeftRightIcon />
<span>{'workbench:partTransfo'}</span>
</div>
{state.ui.hideMovableButtons ? <BoolNoIcon /> : <BoolYesIcon />}
</div>,
<ListInput
key={2}
update={() => update.state.ui('hideMovableButtons', state.ui.hideMovableButtons ? false : true)}
label={
<span className="tw:text-base tw:font-normal">{'workbench:partTransfoDesc'}</span>
}
list={[
{
val: true,
label: 'workbench:partTransfoNo',
desc: 'workbench:partTransfoNoDesc',
},
{
val: false,
label: 'workbench:partTransfoYes',
desc: 'workbench:partTransfoYesDesc',
},
]}
current={state.ui.hideMovableButtons ? true : false}
/>,
'partTransfo',
],
[
<div className="tw:w-full tw:flex tw:flex-row tw:gap2 tw:justify-between" key={1}>
<div className="tw:flex tw:flex-row tw:items-center tw:gap-2">
<ResetIcon />
<span>{'workbench:resetPrintLayout'}</span>
</div>
<WarningIcon />
</div>,
<Fragment key={2}>
<p>{'workbench:resetPrintLayoutDesc'}</p>
<button
className={`${horFlexClasses} tw:btn tw:btn-warning tw:btn-outline tw:w-full`}
onClick={() => update.ui(['layouts', 'print'])}
>
<ResetIcon />
<span>{'workbench:resetPrintLayout'}</span>
</button>
</Fragment>,
'reset',
],
]}
/>
)
*/

View file

@ -5,7 +5,7 @@ import { measurements as measurementsTranslations } from '@freesewing/i18n'
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
// Components // Components
import { MenuButtonGroup } from './Container.mjs' import { MenuButtonGroup } from './Container.mjs'
import { BeakerIcon, OptionsIcon } from '@freesewing/react/components/Icon' import { MeasurementsIcon, OptionsIcon } from '@freesewing/react/components/Icon'
/** /**
* The test design options menu * The test design options menu
@ -87,12 +87,12 @@ const SampleOptionButton = ({ name, i18n, update }) => (
} }
onClick={() => update.settings('sample', { type: 'option', option: name })} onClick={() => update.settings('sample', { type: 'option', option: name })}
> >
<BeakerIcon className="tw:w-5 tw:h-5" /> <OptionsIcon className="tw:w-5 tw:h-5" />
<span>{i18n.en?.o[name]?.t ?? name}</span> <span>{i18n.en?.o[name]?.t ?? name}</span>
</button> </button>
) )
const SampleMeasurementButton = ({ name, i18n, update }) => ( const SampleMeasurementButton = ({ name, update }) => (
<button <button
className={ className={
'tw:daisy-btn tw:daisy-btn-outline tw:daisy-btn-sm tw:mx-2 ' + 'tw:daisy-btn tw:daisy-btn-outline tw:daisy-btn-sm tw:mx-2 ' +
@ -100,7 +100,7 @@ const SampleMeasurementButton = ({ name, i18n, update }) => (
} }
onClick={() => update.settings('sample', { type: 'measurement', measurement: name })} onClick={() => update.settings('sample', { type: 'measurement', measurement: name })}
> >
<BeakerIcon className="tw:w-5 tw:h-5" /> <MeasurementsIcon className="tw:w-5 tw:h-5" />
<span>{measurementsTranslations[name]}</span> <span>{measurementsTranslations[name]}</span>
</button> </button>
) )

View file

@ -19,7 +19,7 @@ export const UiPreferencesMenu = ({ update, state, Design }) => {
} }
const values = { const values = {
aside: MenuListValue, aside: MenuListValue,
ux: (props) => <span>{state.ui.ux}/5</span>, ux: () => <span>{state.ui.ux}/5</span>,
rotate: MenuListValue, rotate: MenuListValue,
renderer: MenuListValue, renderer: MenuListValue,
} }
@ -53,5 +53,11 @@ export const UiPreferencesMenu = ({ update, state, Design }) => {
} }
export const UiPreference = ({ name, ux, ...rest }) => ( export const UiPreference = ({ name, ux, ...rest }) => (
<MenuItem {...rest} name={name} allowToggle={!['ux', 'view'].includes(name) && ux > 3} ux={ux} /> <MenuItem
type="uiPreference"
{...rest}
name={name}
allowToggle={!['ux', 'view'].includes(name) && ux > 3}
ux={ux}
/>
) )

View file

@ -160,11 +160,10 @@ export const MenuScaleSettingValue = ({ current, config, changed }) => (
/** /**
* Displays the value for core's only setting * Displays the value for core's only setting
* *
* @param {object} config - The option config
* @param {number} current - The current (count) value * @param {number} current - The current (count) value
* @param {bool} changed - Whether or not the value is non-default * @param {bool} changed - Whether or not the value is non-default
*/ */
export const MenuOnlySettingValue = ({ current, config }) => ( export const MenuOnlySettingValue = ({ current }) => (
<MenuHighlightValue changed={current !== undefined}> <MenuHighlightValue changed={current !== undefined}>
{current === undefined ? '-' : current.length} {current === undefined ? '-' : current.length}
</MenuHighlightValue> </MenuHighlightValue>

View file

@ -5,10 +5,9 @@ import { Collection } from '@freesewing/react/components/Collection'
* The designs view is loaded if and only if no design name is passed to the editor * The designs view is loaded if and only if no design name is passed to the editor
* *
* @param {Object} props - All the props * @param {Object} props - All the props
* @param {Object} designs - Object holding all designs
* @param {Object} update - ViewWrapper state update object * @param {Object} update - ViewWrapper state update object
*/ */
export const DesignsView = ({ designs = {}, update }) => ( export const DesignsView = ({ update }) => (
<div className="tw:text-center tw:mt-8 tw:mb-24 tw:p-2 lg: tw:p-8"> <div className="tw:text-center tw:mt-8 tw:mb-24 tw:p-2 lg: tw:p-8">
<h1>Choose a design from the FreeSewing collection</h1> <h1>Choose a design from the FreeSewing collection</h1>
<Collection <Collection

View file

@ -1,11 +1,14 @@
// Dependencies // Dependencies
import { linkClasses, capitalize } from '@freesewing/utils' import { linkClasses } from '@freesewing/utils'
// Context
import { ModalContext } from '@freesewing/react/context/Modal'
// Hooks // Hooks
import React from 'react' import React, { useContext } from 'react'
// Components // Components
import { H1, H5 } from '@freesewing/react/components/Heading' import { H1, H2 } from '@freesewing/react/components/Heading'
import { Popout } from '@freesewing/react/components/Popout' import { modalDocsHelp } from '@freesewing/react/components/Help'
import { HeaderMenu } from '../HeaderMenu.mjs' import { HeaderMenu } from '../HeaderMenu.mjs'
import { Popout } from '@freesewing/react/components/Popout'
/** /**
* This is the docs view, it just shows content * This is the docs view, it just shows content
@ -16,51 +19,55 @@ import { HeaderMenu } from '../HeaderMenu.mjs'
* @param {Object} props.update - Helper object for updating the editor state * @param {Object} props.update - Helper object for updating the editor state
*/ */
export const DocsView = ({ state, config, update }) => { export const DocsView = ({ state, config, update }) => {
const { setModal, modalContent } = useContext(ModalContext)
return ( return (
<> <>
<HeaderMenu state={state} {...{ config, update }} /> <HeaderMenu state={state} {...{ config, update }} />
<div className="tw:m-auto tw:mt-8 tw:max-w-2xl tw:px-4 tw:mb-8"> <div className="tw:m-auto tw:mt-8 tw:max-w-2xl tw:px-4 tw:mb-8">
<H1>Documentation</H1> <H1>Documentation</H1>
{state?.design ? ( {state?.design ? (
<Popout link> <>
<H5>Design Documentation</H5> <H2>Design Documentation</H2>
<p className="tw:text-lg"> <Popout type="link" compact dense>
You can find documentation for the {capitalize(state.design)} design at: <div className="tw:font-bold tw:py-1">
<br />
<b>
<a <a
className={linkClasses} className={linkClasses}
href={`https://freesewing.eu/docs/designs/${state.design}`} href={`https://freesewing.eu/docs/designs/${state.design}`}
>{`FreeSewing.eu/docs/designs/${state.design}`}</a> >{`FreeSewing.eu/docs/designs/${state.design}`}</a>
</b> </div>
</p> </Popout>
</Popout> <button
className="tw:daisy-btn tw:daisy-btn-secondary tw:daisy-btn-outline tw:mt-4"
onClick={() => modalDocsHelp(`docs/designs/${state.design}`, setModal)}
>
Open without leaving the Editor
</button>
</>
) : null} ) : null}
<Popout link> <H2>Editor Documentation</H2>
<H5>Understanding the FreeSewing Pattern Editor</H5> <Popout type="link" compact dense>
<p className="tw:text-lg"> <div className="tw:font-bold tw:py-1">
Please refer to the pattern editor documentation at: <a
<br /> className={linkClasses}
<b> href="https://freesewing.eu/docs/editor"
<a >{`FreeSewing.eu/docs/editor`}</a>
className={linkClasses} </div>
href="https://freesewing.eu/docs/about/editor"
>{`FreeSewing.eu/docs/about/editor`}</a>
</b>
</p>
</Popout> </Popout>
<Popout tip> <button
<H5> className="tw:daisy-btn tw:daisy-btn-secondary tw:daisy-btn-outline tw:mt-4"
Looking for info on how it <em>really</em> works? onClick={() => modalDocsHelp(`docs/editor`, setModal)}
</H5> >
<p> Open without leaving the Editor
Documentation for developers and contributors is available at{' '} </button>
<b> <H2>Developer Documentation</H2>
<a className={linkClasses} href="https://freesewing.dev/">{`FreeSewing.dev`}</a> <Popout type="link" compact>
</b> <b>
</p> <a className={linkClasses} href="https://freesewing.dev/">{`FreeSewing.dev`}</a>
</b>
</Popout> </Popout>
</div> </div>
{modalContent}
</> </>
) )
} }

View file

@ -14,7 +14,7 @@ export const DraftErrorHandler = ({ failure, errors }) => {
} }
return ( return (
<Popout error> <Popout type="error">
<p> <p>
Sorry, there were problems drafting your pattern with the given measurements and options. Sorry, there were problems drafting your pattern with the given measurements and options.
</p> </p>
@ -27,8 +27,8 @@ export const DraftErrorHandler = ({ failure, errors }) => {
. .
</p> </p>
<p> <p>
If you believe your measurements are correct and/or if you'd like further assistance, you If you believe your measurements are correct and/or if you&apos;d like further assistance,
can ask for help <Link href="https://forum.freesewing.eu">on our forum</Link>,{' '} you can ask for help <Link href="https://forum.freesewing.eu">on our forum</Link>,{' '}
<Link href="https://discord.freesewing.org">our Discord server</Link>, or{' '} <Link href="https://discord.freesewing.org">our Discord server</Link>, or{' '}
<Link href="https://codeberg.org/freesewing/freesewing/issues">report an issue</Link>. <Link href="https://codeberg.org/freesewing/freesewing/issues">report an issue</Link>.
</p> </p>

View file

@ -1,7 +1,9 @@
// Dependencies // Dependencies
import React from 'react' import React, { useContext } from 'react'
import { bundlePatternTranslations, draft, missingMeasurements } from '../../lib/index.mjs' import { bundlePatternTranslations, draft, missingMeasurements } from '../../lib/index.mjs'
import { colors, darkColors } from '@freesewing/plugin-theme' import { colors, darkColors } from '@freesewing/plugin-theme'
// Context
import { ModalContext } from '@freesewing/react/context/Modal'
// Hooks // Hooks
import { useColorMode } from '@docusaurus/theme-common' import { useColorMode } from '@docusaurus/theme-common'
// Components // Components
@ -29,6 +31,7 @@ import { translateStrings } from '../../../Pattern/index.mjs'
* @return {function} DraftView - React component * @return {function} DraftView - React component
*/ */
export const DraftView = ({ Design, state, update, config, plugins = [], PluginOutput = Null }) => { export const DraftView = ({ Design, state, update, config, plugins = [], PluginOutput = Null }) => {
const { modalContent } = useContext(ModalContext)
/* /*
* We need to manipulate the theme for SVG in the browser * We need to manipulate the theme for SVG in the browser
*/ */
@ -103,6 +106,7 @@ export const DraftView = ({ Design, state, update, config, plugins = [], PluginO
rotate={state.ui.rotate} rotate={state.ui.rotate}
update={update} update={update}
/> />
{modalContent}
</> </>
) )
} }

View file

@ -23,7 +23,6 @@ import { yaml as yamlLang } from '@codemirror/lang-yaml'
* @param {Object} props.update - Helper object for updating the editor state * @param {Object} props.update - Helper object for updating the editor state
*/ */
export const EditSettingsView = (props) => { export const EditSettingsView = (props) => {
const [settings, setSettings] = useState(props.state?.settings || {})
const { state, config, update } = props const { state, config, update } = props
return ( return (
@ -96,7 +95,7 @@ export const PrimedSettingsEditor = (props) => {
setSettings(newSettings) setSettings(newSettings)
} }
} catch (err) { } catch (err) {
// This is fine console.log(err)
} }
} }

View file

@ -8,7 +8,7 @@ import React, { useState } from 'react'
// Components // Components
import { H1, H2, H3, H5 } from '@freesewing/react/components/Heading' import { H1, H2, H3, H5 } from '@freesewing/react/components/Heading'
import { HeaderMenu } from '../HeaderMenu.mjs' import { HeaderMenu } from '../HeaderMenu.mjs'
import { CopyToClipboardButton } from '@freesewing/react/components/CopyToClipboardButton' import { CopyToClipboardButton } from '@freesewing/react/components/Button'
import { Highlight } from '@freesewing/react/components/Highlight' import { Highlight } from '@freesewing/react/components/Highlight'
import { EditIcon, CodeIcon, TipIcon, PrintIcon } from '@freesewing/react/components/Icon' import { EditIcon, CodeIcon, TipIcon, PrintIcon } from '@freesewing/react/components/Icon'
@ -23,10 +23,10 @@ import { EditIcon, CodeIcon, TipIcon, PrintIcon } from '@freesewing/react/compon
export const ExportView = (props) => { export const ExportView = (props) => {
const { config, state, update } = props const { config, state, update } = props
const { settings = {} } = state // Guard against undefined settings const { settings = {} } = state // Guard against undefined settings
const [link, setLink] = useState(false) const setLink = useState(false)[1]
const [format, setFormat] = useState(false) const setFormat = useState(false)[1]
const { protocol, hostname, port } = window.location const { protocol, port } = window.location
const site = const site =
(protocol === 'https:' && port === 443) || (protocol === 'http:' && port === 80) (protocol === 'https:' && port === 443) || (protocol === 'http:' && port === 80)
? `${window.location.protocol}//${window.location.hostname}` ? `${window.location.protocol}//${window.location.hostname}`
@ -58,10 +58,26 @@ export const ExportView = (props) => {
<H2>Share your pattern</H2> <H2>Share your pattern</H2>
<p>If you merely want to share your pattern with others, you can copy these URLs:</p> <p>If you merely want to share your pattern with others, you can copy these URLs:</p>
<div className="tw:grid tw:grid-cols-1 tw:lg:grid-cols-2 tw:gap-2 tw:mt-2 "> <div className="tw:grid tw:grid-cols-1 tw:lg:grid-cols-2 tw:gap-2 tw:mt-2 ">
<CopyToClipboardButton content={urls.a} update={update}> <CopyToClipboardButton
content={urls.a}
btnClasses="tw:daisy-btn tw-daisy-btn-neutral tw:daisy-btn-outline"
label="Pattern and Measurements URL"
onCopy={() => {
console.log('handler called')
update.notifySuccess('Pattern and Measurements URL copied to clipboard')
}}
>
Pattern and Measurements Pattern and Measurements
</CopyToClipboardButton> </CopyToClipboardButton>
<CopyToClipboardButton content={urls.b} update={update}> <CopyToClipboardButton
content={urls.b}
btnClasses="tw:daisy-btn tw-daisy-btn-neutral tw:daisy-btn-outline"
label="Pattern URL"
onCopy={() => {
console.log('handler called')
update.notifySuccess('Pattern URL copied to clipboard')
}}
>
Pattern only Pattern only
</CopyToClipboardButton> </CopyToClipboardButton>
</div> </div>
@ -102,6 +118,7 @@ export const ExportView = (props) => {
<H3>ISO paper sizes</H3> <H3>ISO paper sizes</H3>
{['a4', 'a3', 'a2', 'a1', 'a0'].map((format) => ( {['a4', 'a3', 'a2', 'a1', 'a0'].map((format) => (
<button <button
key={format}
className={`${horFlexClasses} tw:daisy-btn tw:daisy-btn-primary tw:uppercase`} className={`${horFlexClasses} tw:daisy-btn tw:daisy-btn-primary tw:uppercase`}
onClick={() => exportPattern({ ...exportProps, format })} onClick={() => exportPattern({ ...exportProps, format })}
> >
@ -114,6 +131,7 @@ export const ExportView = (props) => {
<H3>Other paper sizes</H3> <H3>Other paper sizes</H3>
{['letter', 'legal', 'tabloid'].map((format) => ( {['letter', 'legal', 'tabloid'].map((format) => (
<button <button
key={format}
className={`${horFlexClasses} tw:daisy-btn tw:daisy-btn-primary tw:uppercase`} className={`${horFlexClasses} tw:daisy-btn tw:daisy-btn-primary tw:uppercase`}
onClick={() => exportPattern({ ...exportProps, format })} onClick={() => exportPattern({ ...exportProps, format })}
> >
@ -130,6 +148,7 @@ export const ExportView = (props) => {
<div className="tw:grid tw:grid-cols-1 tw:lg:grid-cols-2 tw:gap-2 tw:mt-2"> <div className="tw:grid tw:grid-cols-1 tw:lg:grid-cols-2 tw:gap-2 tw:mt-2">
{['svg', 'pdf'].map((format) => ( {['svg', 'pdf'].map((format) => (
<button <button
key={format}
className={`${horFlexClasses} tw:daisy-btn tw:daisy-btn-primary tw:uppercase`} className={`${horFlexClasses} tw:daisy-btn tw:daisy-btn-primary tw:uppercase`}
onClick={() => exportPattern({ ...exportProps, format })} onClick={() => exportPattern({ ...exportProps, format })}
> >
@ -143,6 +162,7 @@ export const ExportView = (props) => {
<div className="tw:grid tw:grid-cols-1 tw:lg:grid-cols-2 tw:gap-2 tw:mt-2"> <div className="tw:grid tw:grid-cols-1 tw:lg:grid-cols-2 tw:gap-2 tw:mt-2">
{['json', 'yaml'].map((format) => ( {['json', 'yaml'].map((format) => (
<button <button
key={format}
className={`${horFlexClasses} tw:daisy-btn tw:daisy-btn-primary tw:uppercase`} className={`${horFlexClasses} tw:daisy-btn tw:daisy-btn-primary tw:uppercase`}
onClick={() => exportPattern({ ...exportProps, format })} onClick={() => exportPattern({ ...exportProps, format })}
> >

View file

@ -11,7 +11,7 @@ import { DraftErrorHandler } from './DraftErrorHandler.mjs'
export const LayoutView = (props) => { export const LayoutView = (props) => {
const { config, state, update, Design } = props const { config, state, update, Design } = props
const { ui, settings } = state const { settings } = state
const defaultSettings = defaultPrintSettings(settings?.units) const defaultSettings = defaultPrintSettings(settings?.units)
// Settings for the tiler plugin // Settings for the tiler plugin

View file

@ -2,8 +2,10 @@
import { designMeasurements } from '../../lib/index.mjs' import { designMeasurements } from '../../lib/index.mjs'
import { capitalize, horFlexClasses as horFlexClasses } from '@freesewing/utils' import { capitalize, horFlexClasses as horFlexClasses } from '@freesewing/utils'
import { measurements as measurementsTranslations } from '@freesewing/i18n' import { measurements as measurementsTranslations } from '@freesewing/i18n'
// Context
import { ModalContext } from '@freesewing/react/context/Modal'
// Hooks // Hooks
import React, { Fragment, useState, useEffect } from 'react' import React, { Fragment, useState, useEffect, useContext } from 'react'
import { useBackend } from '@freesewing/react/hooks/useBackend' import { useBackend } from '@freesewing/react/hooks/useBackend'
// Components // Components
import { Popout } from '@freesewing/react/components/Popout' import { Popout } from '@freesewing/react/components/Popout'
@ -37,7 +39,6 @@ const iconClasses = {
* @param {Array} props.missingMeasurements - List of missing measurements for the current design * @param {Array} props.missingMeasurements - List of missing measurements for the current design
* @param {Object} props.state - The editor state object * @param {Object} props.state - The editor state object
* @param {Object} props.update - Helper object for updating the editor state * @param {Object} props.update - Helper object for updating the editor state
* @param {object} props.helpProvider - A function that takes a measurement and returns a url or action to show help for that measurement
* @return {Function} MeasurementsView - React component * @return {Function} MeasurementsView - React component
*/ */
export const MeasurementsView = ({ export const MeasurementsView = ({
@ -47,8 +48,8 @@ export const MeasurementsView = ({
state, state,
update, update,
design, design,
measurementHelpProvider = false,
}) => { }) => {
const { modalContent } = useContext(ModalContext)
/* /*
* If there is no view set, completing measurements will switch to the view picker * 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. * Which is a bit confusing. So in this case, set the view to measurements.
@ -170,10 +171,7 @@ export const MeasurementsView = ({
</div> </div>
<p className="tw:text-left">You can manually set or override measurements below.</p> <p className="tw:text-left">You can manually set or override measurements below.</p>
</Fragment>, </Fragment>,
<MeasurementsEditor <MeasurementsEditor key={2} {...{ Design, config, update, state }} />,
key={2}
{...{ Design, config, update, state, helpProvider: measurementHelpProvider }}
/>,
'edit', 'edit',
]) ])
@ -183,7 +181,7 @@ export const MeasurementsView = ({
<div className="tw:max-w-7xl tw:mt-8 tw:mx-auto tw:px-4 tw:mb-4"> <div className="tw:max-w-7xl tw:mt-8 tw:mx-auto tw:px-4 tw:mb-4">
<H1>Measurements</H1> <H1>Measurements</H1>
{missingMeasurements && missingMeasurements.length > 0 ? ( {missingMeasurements && missingMeasurements.length > 0 ? (
<Popout note dense noP> <Popout type="note" dense>
<h3> <h3>
To generate this pattern, we need {missingMeasurements.length} additional measurement To generate this pattern, we need {missingMeasurements.length} additional measurement
{missingMeasurements.length === 1 ? '' : 's'}: {missingMeasurements.length === 1 ? '' : 's'}:
@ -198,7 +196,7 @@ export const MeasurementsView = ({
</ol> </ol>
</Popout> </Popout>
) : ( ) : (
<Popout tip dense noP> <Popout type="tip" dense>
<H5>We have all required measurements to draft this pattern</H5> <H5>We have all required measurements to draft this pattern</H5>
<div className="tw:flex tw:flex-row tw:flex-wrap tw:gap-2 tw:mt-2"> <div className="tw:flex tw:flex-row tw:flex-wrap tw:gap-2 tw:mt-2">
<button <button
@ -218,6 +216,7 @@ export const MeasurementsView = ({
)} )}
{items.length > 1 ? <Accordion items={items} /> : items} {items.length > 1 ? <Accordion items={items} /> : items}
</div> </div>
{modalContent}
</> </>
) )
} }

View file

@ -8,8 +8,8 @@ import { useBackend } from '@freesewing/react/hooks/useBackend'
// Components // Components
import { RoleBlock } from '@freesewing/react/components/Role' import { RoleBlock } from '@freesewing/react/components/Role'
import { Popout } from '@freesewing/react/components/Popout' import { Popout } from '@freesewing/react/components/Popout'
import { StringInput } from '@freesewing/react/components/Input' import { StringInput, MarkdownInput } from '@freesewing/react/components/Input'
import { SaveAsIcon } from '@freesewing/react/components/Icon' import { SaveAsIcon, SaveIcon } from '@freesewing/react/components/Icon'
import { H1 } from '@freesewing/react/components/Heading' import { H1 } from '@freesewing/react/components/Heading'
import { Link, SuccessLink } from '@freesewing/react/components/Link' import { Link, SuccessLink } from '@freesewing/react/components/Link'
import { HeaderMenu } from '../HeaderMenu.mjs' import { HeaderMenu } from '../HeaderMenu.mjs'
@ -67,7 +67,8 @@ export const SaveView = ({ config, state, update }) => {
} }
const savePattern = async () => { const savePattern = async () => {
setLoadingStatus([true, 'Saving pattern...']) const loadingId = 'savePattern'
update.startLoading(loadingId)
const patternData = { const patternData = {
design: state.design, design: state.design,
settings, settings,
@ -77,17 +78,10 @@ export const SaveView = ({ config, state, update }) => {
} }
const result = await backend.updatePattern(saveAs.pattern, patternData) const result = await backend.updatePattern(saveAs.pattern, patternData)
if (result.success) { if (result.success) {
//setLoadingStatus([ update.stopLoading(loadingId)
// true,
// <>
// {t('status:patternSaved')} <small>[#{saveAs.pattern}]</small>
// </>,
// true,
// true,
//])
setSavedId(saveAs.pattern) setSavedId(saveAs.pattern)
update.notify({ color: 'success', msg: 'boom' }, saveAs.pattern) update.notifySuccess('Pattern saved', loadingId)
} //else setLoadingStatus([true, 'backendError', true, false]) }
} }
return ( return (
@ -98,17 +92,17 @@ export const SaveView = ({ config, state, update }) => {
<> <>
<h2>Save Pattern</h2> <h2>Save Pattern</h2>
{savedId && ( {savedId && (
<Popout link> <Popout type="link">
<h5>Pattern Saved</h5> <h5>Pattern Saved</h5>
See: <Link href={`/account/patterns/${savedId}`}>/account/patterns/{savedId}</Link> See: <Link href={`/account/patterns/${savedId}`}>/account/patterns/{savedId}</Link>
</Popout> </Popout>
)} )}
<button <button
className={`${classeshorFlexNoSm} tw:btn tw:btn-primary tw:btn-lg tw:w-full tw:mt-2 tw:my-8`} className={`tw:flex tw:flex-row tw:items-center tw:gap-2 tw:btn tw:btn-primary tw:btn-lg tw:w-full tw:mt-2 tw:my-8`}
onClick={savePattern} onClick={savePattern}
> >
<SaveIcon className="tw:h-8 tw:w-8" /> <SaveIcon className="tw:h-8 tw:w-8" />
Save Patter #{saveAs.pattern} Save Pattern #{saveAs.pattern}
</button> </button>
</> </>
) : null} ) : null}
@ -145,11 +139,7 @@ export const SaveView = ({ config, state, update }) => {
} }
/> />
{withNotes ? ( {withNotes ? (
<Swizzled.components.MarkdownInput <MarkdownInput label="Pattern notes" current={notes} update={setNotes} />
label="Pattern notes"
current={notes}
update={setNotes}
/>
) : null} ) : null}
<div className="tw:flex tw:flex-row tw:gap-2 tw:mt-8"> <div className="tw:flex tw:flex-row tw:gap-2 tw:mt-8">
<button <button
@ -172,7 +162,7 @@ export const SaveView = ({ config, state, update }) => {
To access your saved patterns, go to: To access your saved patterns, go to:
<b> <b>
{' '} {' '}
<Link href="/account/patterns">/account/patterns</Link> <Link href="/account/data/patterns/">/account/data/patterns</Link>
</b> </b>
</p> </p>
</div> </div>

View file

@ -14,6 +14,7 @@ import { ZoomablePattern } from '../ZoomablePattern.mjs'
import { PatternLayout } from '../PatternLayout.mjs' import { PatternLayout } from '../PatternLayout.mjs'
import { HeaderMenu } from '../HeaderMenu.mjs' import { HeaderMenu } from '../HeaderMenu.mjs'
import { H1, H3, H4, H5 } from '@freesewing/react/components/Heading' import { H1, H3, H4, H5 } from '@freesewing/react/components/Heading'
import { OptionsIcon, MeasurementsIcon } from '@freesewing/react/components/Icon'
/** /**
* The test view allows users to test options and measurements * The test view allows users to test options and measurements
@ -69,7 +70,6 @@ export const TestView = ({ Design, state, update, config }) => {
patternLocale={state.locale || 'en'} patternLocale={state.locale || 'en'}
rotate={state.ui.rotate} rotate={state.ui.rotate}
strings={strings} strings={strings}
rotate={state.ui.rotate}
/> />
) )
@ -88,6 +88,8 @@ export const TestView = ({ Design, state, update, config }) => {
't', 't',
'ASC' 'ASC'
) )
const btnClasses =
'tw:my-0.5 tw:block tw:daisy-btn tw:daisy-btn-primary tw:daisy-btn-outline tw:daisy-btn-xs tw:flex tw:flex-row tw:items-center tw:justify-between tw:w-full tw:max-w-64'
return ( return (
<> <>
@ -107,12 +109,13 @@ export const TestView = ({ Design, state, update, config }) => {
{trm.map(({ t, m }) => ( {trm.map(({ t, m }) => (
<button <button
key={m} key={m}
className="tw:my-0.5 tw:block tw:daisy-btn tw:daisy-btn-primary tw:daisy-btn-outline tw:daisy-btn-xs" className={btnClasses}
onClick={() => onClick={() =>
update.settings(['sample'], { type: 'measurement', measurement: m }) update.settings(['sample'], { type: 'measurement', measurement: m })
} }
> >
{t} <MeasurementsIcon className="tw:w-4 tw:h-4" />
<span>{t}</span>
</button> </button>
))} ))}
</div> </div>
@ -123,12 +126,13 @@ export const TestView = ({ Design, state, update, config }) => {
{tom.map(({ t, m }) => ( {tom.map(({ t, m }) => (
<button <button
key={m} key={m}
className="tw:my-0.5 tw:block tw:daisy-btn tw:daisy-btn-primary tw:daisy-btn-outline tw:daisy-btn-xs" className={btnClasses}
onClick={() => onClick={() =>
update.settings(['sample'], { type: 'measurement', measurement: m }) update.settings(['sample'], { type: 'measurement', measurement: m })
} }
> >
{t} <MeasurementsIcon className="tw:w-4 tw:h-4" />
<span>{t}</span>
</button> </button>
))} ))}
</div> </div>
@ -159,6 +163,8 @@ const SampleOptionsMenu = ({ Design, state, update }) => {
const SampleOptionsSubMenu = ({ structure, update, level = 1 }) => { const SampleOptionsSubMenu = ({ structure, update, level = 1 }) => {
const output = [] const output = []
const btnClasses =
'tw:my-0.5 tw:block tw:daisy-btn tw:daisy-btn-primary tw:daisy-btn-outline tw:daisy-btn-xs tw:flex tw:flex-row tw:items-center tw:justify-between tw:w-full tw:max-w-64'
/* /*
* Show entries alphabetic, but force groups last, and advanced last among them * Show entries alphabetic, but force groups last, and advanced last among them
*/ */
@ -176,10 +182,11 @@ const SampleOptionsSubMenu = ({ structure, update, level = 1 }) => {
output.push( output.push(
<button <button
key={name} key={name}
className="tw:my-0.5 tw:block tw:daisy-btn tw:daisy-btn-primary tw:daisy-btn-outline tw:daisy-btn-xs" className={btnClasses}
onClick={() => update.settings(['sample'], { type: 'option', option: name })} onClick={() => update.settings(['sample'], { type: 'option', option: name })}
> >
{struct.title} <OptionsIcon className="tw:w-4 tw:h-4" />
<span>{struct.title}</span>
</button> </button>
) )
} }

View file

@ -1,6 +1,5 @@
// Dependencies // Dependencies
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import * as echarts from 'echarts'
import { timingPlugin } from '@freesewing/plugin-timing' import { timingPlugin } from '@freesewing/plugin-timing'
// Components // Components
import { ChartWrapper } from '@freesewing/react/components/Echart' import { ChartWrapper } from '@freesewing/react/components/Echart'
@ -36,18 +35,6 @@ const TimingHeader = ({ timing, parts }) => {
) : null ) : null
} }
const resolveColor = (color) => {
const [c, i] = color.split('-')
return tailwindColors[c][i]
}
const getColor = (i, colors) =>
new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: resolveColor(colors[i % colors.length]) + 'dd' },
{ offset: 1, color: resolveColor(colors[i % colors.length]) },
])
const timeScore = (took) => { const timeScore = (took) => {
if (took < 25) return 'Very Fast' if (took < 25) return 'Very Fast'
if (took < 50) return 'Fast' if (took < 50) return 'Fast'
@ -58,7 +45,6 @@ const timeScore = (took) => {
} }
const option = (parts, data, took, setData) => ({ const option = (parts, data, took, setData) => ({
//color: colors.map((color) => resolveColor(color)),
title: { title: {
text: `Timing of most recent draft: ${timeScore(took)}`, text: `Timing of most recent draft: ${timeScore(took)}`,
left: 'center', left: 'center',
@ -127,7 +113,7 @@ const option = (parts, data, took, setData) => ({
}, },
], ],
yAxis: [{ type: 'value' }], yAxis: [{ type: 'value' }],
series: parts.map((name, i) => ({ series: parts.map((name) => ({
name, name,
type: 'line', type: 'line',
stack: 'Total', stack: 'Total',

View file

@ -27,7 +27,7 @@ export const UndosView = ({ Design, update, state, config }) => {
<H1>Undo History</H1> <H1>Undo History</H1>
<p className="tw:mb-4">Time-travel through your recent pattern changes.</p> <p className="tw:mb-4">Time-travel through your recent pattern changes.</p>
{steps.length < 1 ? ( {steps.length < 1 ? (
<Popout note> <Popout type="note">
<h4>Your undo history is currently empty</h4> <h4>Your undo history is currently empty</h4>
<p>When you make changes to your pattern, they will show up here.</p> <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> <p>For example, you can click the button below to change the pattern rotation:</p>
@ -108,33 +108,33 @@ export const UndoStep = ({ update, state, step, Design, compact = false, index =
return ( return (
<> <>
<p className="tw:text-sm tw:italic tw:font-medium tw:opacity-70 tw:text-right tw:p-0 tw:tw:m-0 tw:-mb-2 tw:pr-2">
<UndoStepTimeAgo step={step} />
</p>
<ButtonFrame onClick={() => update.restore(index, state._)}> <ButtonFrame onClick={() => update.restore(index, state._)}>
<div className="tw:flex tw:flex-row tw:items-center tw:justify-between tw:gap-2 tw:w-full tw:m-0 tw:p-0 tw:-mt-2 tw:text-lg"> <div className="tw:flex tw:flex-col tw:font-medium tw:items-end tw:w-full tw:-mb-2">
<span className="tw:flex tw:flex-row tw:gap-2 tw:items-center"> <div className="tw:text-sm tw:-mt-2 tw:italic">
{data.fieldIcon || null} <UndoStepTimeAgo step={step} />
{data.title} </div>
</span> <div className="tw:flex tw:flex-row tw:items-center tw:justify-start tw:gap-2 tw:w-full tw:text-lg tw:-mt-2">
<span className="tw:opacity-70 tw:flex tw:flex-row tw:gap-1 tw:items-center tw:text-base"> <span className="tw:opacity-70 tw:flex tw:flex-row tw:gap-1 tw:items-center tw:text-base">
{data.icon || null} {data.menu} {data.menu}
</span> </span>
</div> <span>&raquo;</span>
<div className="tw:flex tw:flex-row tw:gap-1 tw:items-center tw:align-start tw:w-full"> <span className="tw:flex tw:flex-row tw:gap-2 tw:items-center">{data.title}</span>
{data.msg ? ( </div>
data.msg <div className="tw:flex tw:flex-row tw:gap-1 tw:items-center tw:align-start tw:w-full">
) : ( {data.msg ? (
<> data.msg
<span className=""> ) : (
{Array.isArray(data.newVal) ? data.newVal.join(', ') : data.newVal} <>
</span> <span className="">
<LeftIcon className="tw:w-4 tw:h-4 tw:text-secondary tw:shrink-0" stroke={4} /> {Array.isArray(data.newVal) ? data.newVal.join(', ') : data.newVal}
<span className="tw:line-through tw:decoration-1 tw:opacity-70"> </span>
{Array.isArray(data.oldVal) ? data.oldVal.join(', ') : data.oldVal} <LeftIcon className="tw:w-4 tw:h-4 tw:text-secondary tw:shrink-0" stroke={4} />
</span> <span className="tw:line-through tw:decoration-1 tw:opacity-70">
</> {Array.isArray(data.oldVal) ? data.oldVal.join(', ') : data.oldVal}
)} </span>
</>
)}
</div>
</div> </div>
</ButtonFrame> </ButtonFrame>
</> </>

View file

@ -28,7 +28,7 @@ export const ViewPicker = ({ Design, update, state }) => {
.map((view) => ( .map((view) => (
<MainCard key={view} {...{ view, update, Design }} /> <MainCard key={view} {...{ view, update, Design }} />
))} ))}
<Popout note> <Popout type="note">
<div className="tw:text-left"> <div className="tw:text-left">
<H5>pe:measurementsFreeViewsOnly.t:</H5> <H5>pe:measurementsFreeViewsOnly.t:</H5>
<p>pe:measurementsFreeViewsOnly.d</p> <p>pe:measurementsFreeViewsOnly.d</p>
@ -70,7 +70,7 @@ export const ViewPicker = ({ Design, update, state }) => {
) )
} }
const MainCard = ({ view, update, Design }) => { const MainCard = ({ view, update }) => {
const Icon = viewIcons[view] const Icon = viewIcons[view]
return ( return (

View file

@ -37,6 +37,7 @@ export const useEditorState = (init = {}, setEphemeralState, config) => {
if (typeof data.s === 'object') setState(data.s) if (typeof data.s === 'object') setState(data.s)
else setState(init) else setState(init)
} catch (err) { } catch (err) {
console.log(err)
setState(init) setState(init)
} }
} }
@ -45,31 +46,6 @@ export const useEditorState = (init = {}, setEphemeralState, config) => {
return [state, setState, update] return [state, setState, update]
} }
/*
* Our URL state library does not support storing Javascript objects out of the box.
* But it allows us to pass a customer parser to handle them, so this is that parser
*/
const pojoParser = {
parse: (v) => {
let val
try {
val = JSON.parse(v)
} catch (err) {
val = null
}
return val
},
serialize: (v) => {
let val
try {
val = JSON.stringify(v)
} catch (err) {
val = null
}
return val
},
}
function getHashData() { function getHashData() {
if (!window) return false if (!window) return false

View file

@ -19,16 +19,18 @@ import { useAccount } from '../../hooks/useAccount/index.mjs'
* FreeSewing's pattern editor * FreeSewing's pattern editor
* *
* Editor is the high-level FreeSewing component * Editor is the high-level FreeSewing component
* that provides the entire pattern editing environment * that provides the entire pattern editing environment.
* This is a high-level wrapper that figures out what view to load initially, * This is a high-level wrapper that figures out what view to load initially,
* and handles state for the pattern, including the view * and handles state for the pattern, including the view.
* *
* @component
* @param {object} props - All React props * @param {object} props - All React props
* @param {object} props.config - A configuration object for the editor * @param {object} [props.config = {}] - A configuration object for the editor
* @param {object} props.design - A design name to force the editor to use this design * @param {object} [props.design = false] - A design name to preset the editor to use this design
* @param {object} props.preload - Any state to preload * @param {object} [props.preload = {}] - Any state to preload
* @param {function} props.setTitle - A way to set the page title (optional) * @param {function} [props.setTitle = false] - A way to set the page title
* @param {object} props.localDesigns - A way to add local designs to the editor (optional) * @param {object} [props.localDesigns = {}] - A way to add local designs to the editor
* @param {function} [props.measurementsHelpProvider = false] - A function that should return to a URL for measurements help
*/ */
export const Editor = ({ export const Editor = ({
config = {}, config = {},
@ -161,7 +163,7 @@ export const Editor = ({
) )
} }
/** /*
* Helper method to figure out what view to load * Helper method to figure out what view to load
* based on the props passed in, and destructure * based on the props passed in, and destructure
* the props we need for it. * the props we need for it.

View file

@ -2,7 +2,7 @@ import React from 'react'
// Dependencies // Dependencies
import { defaultConfig as config } from '../config/index.mjs' import { defaultConfig as config } from '../config/index.mjs'
import { measurementAsMm } from '@freesewing/utils' import { measurementAsMm } from '@freesewing/utils'
import { linkClasses } from '@freesewing/utils'
/* /*
* Components * Components
* Note that these are only used as returns values * Note that these are only used as returns values
@ -61,12 +61,6 @@ export function menuCoreSettingsSaboolHandler({ toggleSa }) {
return toggleSa return toggleSa
} }
const CoreDocsLink = ({ item }) => (
<a href={`/docs/about/site/draft/#${item.toLowerCase()}`} className={`${linkClasses} tw:px-2`}>
Learn more
</a>
)
export function menuCoreSettingsStructure({ export function menuCoreSettingsStructure({
units = 'metric', units = 'metric',
sabool = false, sabool = false,
@ -77,12 +71,7 @@ export function menuCoreSettingsStructure({
sabool: { sabool: {
dense: true, dense: true,
title: 'Include seam allowance', title: 'Include seam allowance',
about: ( about: <>Controls whether or not you want to include seam allowance on your pattern.</>,
<>
Controls whether or not you want to include seam allowance on your pattern.
<CoreDocsLink item="sabool" />
</>
),
ux: config.uxLevels.core.sa, ux: config.uxLevels.core.sa,
list: [0, 1], list: [0, 1],
choiceTitles: { choiceTitles: {
@ -99,12 +88,7 @@ export function menuCoreSettingsStructure({
samm: sabool samm: sabool
? { ? {
title: 'Seam Allowance Size', title: 'Seam Allowance Size',
about: ( about: <>Controls the size of the pattern&apos;s seam allowance.</>,
<>
Controls the size of the pattern's seam allowance.
<CoreDocsLink item="sa" />
</>
),
ux: config.uxLevels.core.sa, ux: config.uxLevels.core.sa,
min: 0, min: 0,
max: units === 'imperial' ? 25.4 : 25, // values are in mm max: units === 'imperial' ? 25.4 : 25, // values are in mm
@ -115,12 +99,7 @@ export function menuCoreSettingsStructure({
units: { units: {
dense: true, dense: true,
title: 'Pattern units', title: 'Pattern units',
about: ( about: <>Allows you to switch between metric and imperial units on the pattern.</>,
<>
Allows you to switch between metric and imperial units on the pattern.
<CoreDocsLink item="units" />
</>
),
ux: config.uxLevels.core.units, ux: config.uxLevels.core.units,
list: ['metric', 'imperial'], list: ['metric', 'imperial'],
dflt: accountUnits, dflt: accountUnits,
@ -141,7 +120,6 @@ export function menuCoreSettingsStructure({
<> <>
Trees are awesome, and taping together sewing patterns is not much fun. Try our paperless 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. mode to avoid the need to print out your pattern altogether.
<CoreDocsLink item="paperless" />
</> </>
), ),
ux: config.uxLevels.core.paperless, ux: config.uxLevels.core.paperless,
@ -164,7 +142,6 @@ export function menuCoreSettingsStructure({
<> <>
Controls how detailed the pattern is; Either a complete pattern with all details, or a Controls how detailed the pattern is; Either a complete pattern with all details, or a
basic outline of the pattern parts. basic outline of the pattern parts.
<CoreDocsLink item="complete" />
</> </>
), ),
ux: config.uxLevels.core.complete, ux: config.uxLevels.core.complete,
@ -187,7 +164,6 @@ export function menuCoreSettingsStructure({
<> <>
Controls efforts to save paper. Disable this to expand all pattern parts at the cost of Controls efforts to save paper. Disable this to expand all pattern parts at the cost of
using more space & paper. using more space & paper.
<CoreDocsLink item="expand" />
</> </>
), ),
ux: config.uxLevels.core.expand, ux: config.uxLevels.core.expand,
@ -206,12 +182,7 @@ export function menuCoreSettingsStructure({
only: { only: {
dense: true, dense: true,
title: 'Only included selected pattern parts', title: 'Only included selected pattern parts',
about: ( about: <>Allows you to control what parts to include in your pattern.</>,
<>
Allows you to control what parts to include in your pattern.
<CoreDocsLink item="only" />
</>
),
ux: config.uxLevels.core.only, ux: config.uxLevels.core.only,
dflt: false, dflt: false,
list: parts, list: parts,
@ -224,7 +195,6 @@ export function menuCoreSettingsStructure({
<> <>
Allows you to control the scale of annotations on the pattern. This is most useful when Allows you to control the scale of annotations on the pattern. This is most useful when
generating very small patterns, like for doll outfits. generating very small patterns, like for doll outfits.
<CoreDocsLink item="scale" />
</> </>
), ),
ux: config.uxLevels.core.scale, ux: config.uxLevels.core.scale,
@ -240,7 +210,6 @@ export function menuCoreSettingsStructure({
<> <>
Controls the gap between pattern parts, as well as the gap between the parts and the page Controls the gap between pattern parts, as well as the gap between the parts and the page
edge. edge.
<CoreDocsLink item="margin" />
</> </>
), ),
ux: config.uxLevels.core.margin, ux: config.uxLevels.core.margin,

View file

@ -2,17 +2,6 @@ import React from 'react'
import { mergeOptions } from '@freesewing/core' import { mergeOptions } from '@freesewing/core'
import { designOptionType, set, orderBy } from '@freesewing/utils' import { designOptionType, set, orderBy } from '@freesewing/utils'
import { i18n } from '@freesewing/collection' import { i18n } from '@freesewing/collection'
import { linkClasses } from '@freesewing/utils'
const DesignDocsLink = ({ design, item }) => (
<a
href={`/docs/designs/${design}/options/#${item.toLowerCase()}`}
className={`${linkClasses} tw:px-2`}
target="_BLANK"
>
Learn more
</a>
)
export function menuDesignOptionsStructure(design, options, settings, asFullList = false) { export function menuDesignOptionsStructure(design, options, settings, asFullList = false) {
if (!options) return options if (!options) return options
@ -23,12 +12,7 @@ export function menuDesignOptionsStructure(design, options, settings, asFullList
...option, ...option,
name, name,
title: i18n[design]?.en?.o?.[name]?.t || name, title: i18n[design]?.en?.o?.[name]?.t || name,
about: ( about: <span>{i18n[design]?.en?.o?.[name]?.d || name}</span>,
<span>
{i18n[design]?.en?.o?.[name]?.d || name}
<DesignDocsLink item={name} design={design} />
</span>
),
dense: true, dense: true,
sideBySide: true, sideBySide: true,
} }
@ -62,14 +46,14 @@ export function menuDesignOptionsStructure(design, options, settings, asFullList
option.valueTitles = {} option.valueTitles = {}
option.choiceTitles = {} option.choiceTitles = {}
option.choiceDescriptions = {} option.choiceDescriptions = {}
for (const entry of option.list) { option.list.forEach(() => {
option.choiceTitles.false = eno[`${option.name}No`]?.t || option.name option.choiceTitles.false = eno[`${option.name}No`]?.t || option.name
option.choiceDescriptions.false = eno[`${option.name}No`]?.d || 'No' option.choiceDescriptions.false = eno[`${option.name}No`]?.d || 'No'
option.valueTitles.false = 'No' option.valueTitles.false = 'No'
option.choiceTitles.true = eno[`${option.name}Yes`]?.t || 'Yes' option.choiceTitles.true = eno[`${option.name}Yes`]?.t || 'Yes'
option.choiceDescriptions.true = eno[`${option.name}Yes`]?.d || 'No' option.choiceDescriptions.true = eno[`${option.name}Yes`]?.d || 'No'
option.valueTitles.true = 'Yes' option.valueTitles.true = 'Yes'
} })
} }
if (typeof option.menu === 'function') if (typeof option.menu === 'function')
option.menu = asFullList option.menu = asFullList

View file

@ -2,7 +2,7 @@
import React from 'react' import React from 'react'
import { defaultConfig } from '../config/index.mjs' import { defaultConfig } from '../config/index.mjs'
import { round, formatMm, randomLoadingMessage } from '@freesewing/utils' import { round, formatMm, randomLoadingMessage } from '@freesewing/utils'
import { formatDesignOptionValue, menuCoreSettingsStructure } from './index.mjs' import { formatDesignOptionValue, menuCoreSettingsStructure, fractionToDecimal } from './index.mjs'
import { menuUiPreferencesStructure } from './ui-preferences.mjs' import { menuUiPreferencesStructure } from './ui-preferences.mjs'
import { i18n } from '@freesewing/collection' import { i18n } from '@freesewing/collection'
import { i18n as pluginI18n } from '@freesewing/core-plugins' import { i18n as pluginI18n } from '@freesewing/core-plugins'
@ -320,7 +320,7 @@ export function initialEditorState(preload = {}, config) {
return initial return initial
} }
/** /*
* round a value to the correct number of decimal places to display all supplied digits after multiplication * 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 * this is a workaround for floating point errors
* examples: * examples:
@ -721,15 +721,15 @@ export function cloudImageUrl({ id = 'default-avatar', variant = 'public' }) {
/* /*
* Return something default so that people will actually change it * Return something default so that people will actually change it
*/ */
if (!id || id === 'default-avatar') return config.cloudImageDflt if (!id || id === 'default-avatar') return defaultConfig.cloudImageDflt
/* /*
* If the variant is invalid, set it to the smallest thumbnail so * If the variant is invalid, set it to the smallest thumbnail so
* people don't load enourmous images by accident * people don't load enourmous images by accident
*/ */
if (!config.cloudImageVariants.includes(variant)) variant = 'sq100' if (!defaultConfig.cloudImageVariants.includes(variant)) variant = 'sq100'
return `${config.cloudImageUrl}${id}/${variant}` return `${defaultConfig.cloudImageUrl}${id}/${variant}`
} }
/** /**
* This method does nothing. It is used to disable certain methods * This method does nothing. It is used to disable certain methods
@ -746,25 +746,6 @@ export function noop() {
export function notEmpty(value) { export function notEmpty(value) {
return String(value).length > 0 return String(value).length > 0
} }
/**
* Helper method to merge arrays of translation namespaces
*
* Note that this method is variadic
*
* @param {[string]} namespaces - A string or array of strings of namespaces
* @return {[string]} namespaces - A merged array of all namespaces
*/
export function nsMerge(...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 * A translation fallback method in case none is passed in

View file

@ -3,7 +3,7 @@ import fileSaver from 'file-saver'
import { themePlugin } from '@freesewing/plugin-theme' import { themePlugin } from '@freesewing/plugin-theme'
import { pluginI18n } from '@freesewing/plugin-i18n' import { pluginI18n } from '@freesewing/plugin-i18n'
import { tilerPlugin } from './plugin-tiler.mjs' import { tilerPlugin } from './plugin-tiler.mjs'
import { capitalize, escapeSvgText, formatMm, get } from '@freesewing/utils' import { capitalize, escapeSvgText, get } from '@freesewing/utils'
import mustache from 'mustache' import mustache from 'mustache'
import he from 'he' import he from 'he'
import yaml from 'js-yaml' import yaml from 'js-yaml'

View file

@ -28,7 +28,6 @@ import {
menuValueWasChanged, menuValueWasChanged,
noop, noop,
notEmpty, notEmpty,
nsMerge,
objUpdate, objUpdate,
sample, sample,
settingsValueIsCustom, settingsValueIsCustom,
@ -91,7 +90,6 @@ export {
menuValueWasChanged, menuValueWasChanged,
noop, noop,
notEmpty, notEmpty,
nsMerge,
objUpdate, objUpdate,
sample, sample,
settingsValueIsCustom, settingsValueIsCustom,

View file

@ -1,5 +1,4 @@
import React from 'react' import React from 'react'
import { linkClasses } from '@freesewing/utils'
import { import {
CoverPageIcon, CoverPageIcon,
PageMarginIcon, PageMarginIcon,
@ -9,13 +8,6 @@ import {
ScaleIcon, ScaleIcon,
} from '@freesewing/react/components/Icon' } from '@freesewing/react/components/Icon'
const UiDocsLink = ({ item }) => (
<a href={`/docs/about/site/draft/#${item.toLowerCase()}`} className={`${linkClasses} tw:px-2`}>
Learn more
</a>
)
const sizes = ['a4', 'a3', 'a2', 'a1', 'a0', 'letter', 'legal', 'tabloid']
const defaultPrintSettings = (units) => ({ const defaultPrintSettings = (units) => ({
size: units === 'imperial' ? 'letter' : 'a4', size: units === 'imperial' ? 'letter' : 'a4',
orientation: 'portrait', orientation: 'portrait',

View file

@ -1,14 +1,7 @@
import React from 'react' import React from 'react'
import { defaultConfig } from '../config/index.mjs' import { defaultConfig } from '../config/index.mjs'
import { linkClasses } from '@freesewing/utils'
import { AsideIcon, RotateIcon, RocketIcon, UxIcon } from '@freesewing/react/components/Icon' import { AsideIcon, RotateIcon, RocketIcon, UxIcon } from '@freesewing/react/components/Icon'
const UiDocsLink = ({ item }) => (
<a href={`/docs/about/site/draft/#${item.toLowerCase()}`} className={`${linkClasses} tw:px-2`}>
Learn more
</a>
)
export function menuUiPreferencesStructure() { export function menuUiPreferencesStructure() {
const uiUx = defaultConfig.uxLevels.ui const uiUx = defaultConfig.uxLevels.ui
const uiPreferences = { const uiPreferences = {
@ -19,7 +12,6 @@ export function menuUiPreferencesStructure() {
<span> <span>
Uses the right side of the screen for the Design Options, Core Settings, and UI Uses the right side of the screen for the Design Options, Core Settings, and UI
Preferences menus. Preferences menus.
<UiDocsLink item="aside" />
</span> </span>
), ),
ux: uiUx.aside, ux: uiUx.aside,
@ -35,10 +27,7 @@ export function menuUiPreferencesStructure() {
dense: true, dense: true,
title: 'User Experience', title: 'User Experience',
about: ( about: (
<span> <span>Controls the user experience, from keep it simple, to give me all the powers.</span>
Controls the user experience, from keep it simple, to give me all the powers.
<UiDocsLink item="control" />
</span>
), ),
ux: uiUx.ux, ux: uiUx.ux,
emoji: '🖥️', emoji: '🖥️',
@ -63,12 +52,7 @@ export function menuUiPreferencesStructure() {
rotate: { rotate: {
dense: true, dense: true,
title: 'Rotate Pattern', title: 'Rotate Pattern',
about: ( about: <span>Allows you to rotate your pattern 90 degrees, handy for tall patterns.</span>,
<span>
Allows you to rotate your pattern 90 degrees, handy for tall patterns.
<UiDocsLink item="rotate" />
</span>
),
ux: uiUx.rotate, ux: uiUx.rotate,
list: [0, 1], list: [0, 1],
choiceTitles: { choiceTitles: {
@ -81,12 +65,7 @@ export function menuUiPreferencesStructure() {
renderer: { renderer: {
dense: true, dense: true,
title: 'Pattern render engine', title: 'Pattern render engine',
about: ( about: <span>Change the underlying method for rendering the pattern on screen.</span>,
<span>
Change the underlying method for rendering the pattern on screen.
<UiDocsLink item="renderer" />
</span>
),
ux: uiUx.renderer, ux: uiUx.renderer,
list: ['react', 'svg'], list: ['react', 'svg'],
choiceTitles: { choiceTitles: {

View file

@ -1,35 +1,119 @@
import React from 'react' import React from 'react'
/**
* A component to renderd a styled H1 tag.
*
* Because of the TailwindCSS reset, common tags are unstyled.
* When you need to render a H1 tag outside of a context where it is
* automatically styled (such as inside markdown) you can use this.
* Alternatively, you can wrap your content in div.prose which will apply the
* styles in CSS.
*
* @component
* @param {object} props - All component props
* @param {JSX.element} props.children - The component children
* @returns {JSX.Element}
*/
export const H1 = ({ children }) => ( export const H1 = ({ children }) => (
<h1 className="tw:text-5xl tw:pt-5 tw:pb-4 tw:font-thin tw:tracking-tighter tw:lg:text-6xl"> <h1 className="tw:text-5xl tw:pt-5 tw:pb-4 tw:font-thin tw:tracking-tighter tw:lg:text-6xl">
{children} {children}
</h1> </h1>
) )
/**
* A component to renderd a styled H2 tag.
*
* Because of the TailwindCSS reset, common tags are unstyled.
* When you need to render a H2 tag outside of a context where it is
* automatically styled (such as inside markdown) you can use this.
* Alternatively, you can wrap your content in div.prose which will apply the
* styles in CSS.
*
* @component
* @param {object} props - All component props
* @param {JSX.element} props.children - The component children
* @returns {JSX.Element}
*/
export const H2 = ({ children }) => ( export const H2 = ({ children }) => (
<h2 className="tw:text-3xl tw:pt-4 tw:pb-3 tw:font-black tw:tracking-tighter tw:m-0 tw:lg:text-4xl"> <h2 className="tw:text-3xl tw:pt-4 tw:pb-3 tw:font-black tw:tracking-tighter tw:m-0 tw:lg:text-4xl">
{children} {children}
</h2> </h2>
) )
/**
* A component to renderd a styled H3 tag.
*
* Because of the TailwindCSS reset, common tags are unstyled.
* When you need to render a H3 tag outside of a context where it is
* automatically styled (such as inside markdown) you can use this.
* Alternatively, you can wrap your content in div.prose which will apply the
* styles in CSS.
*
* @component
* @param {object} props - All component props
* @param {JSX.element} props.children - The component children
* @returns {JSX.Element}
*/
export const H3 = ({ children }) => ( export const H3 = ({ children }) => (
<h3 className="tw:text-2xl tw:pt-3 tw:pb-2 tw:font-extrabold tw:m-0 tw:tracking-tighter tw:lg:text-3xl"> <h3 className="tw:text-2xl tw:pt-3 tw:pb-2 tw:font-extrabold tw:m-0 tw:tracking-tighter tw:lg:text-3xl">
{children} {children}
</h3> </h3>
) )
/**
* A component to renderd a styled H4 tag.
*
* Because of the TailwindCSS reset, common tags are unstyled.
* When you need to render a H4 tag outside of a context where it is
* automatically styled (such as inside markdown) you can use this.
* Alternatively, you can wrap your content in div.prose which will apply the
* styles in CSS.
*
* @component
* @param {object} props - All component props
* @param {JSX.element} props.children - The component children
* @returns {JSX.Element}
*/
export const H4 = ({ children }) => ( export const H4 = ({ children }) => (
<h4 className="tw:text-xl tw:pt-2 tw:pb-1 tw:font-bold tw:m-0 tw:tracking-tighter tw:lg:text-2xl"> <h4 className="tw:text-xl tw:pt-2 tw:pb-1 tw:font-bold tw:m-0 tw:tracking-tighter tw:lg:text-2xl">
{children} {children}
</h4> </h4>
) )
/**
* A component to renderd a styled H5 tag.
*
* Because of the TailwindCSS reset, common tags are unstyled.
* When you need to render a H5 tag outside of a context where it is
* automatically styled (such as inside markdown) you can use this.
* Alternatively, you can wrap your content in div.prose which will apply the
* styles in CSS.
*
* @component
* @param {object} props - All component props
* @param {JSX.element} props.children - The component children
* @returns {JSX.Element}
*/
export const H5 = ({ children }) => ( export const H5 = ({ children }) => (
<h5 className="tw:text-lg tw:py-1 tw:font-semibold tw:m-0 tw:tracking-tight tw:lg:text-xl"> <h5 className="tw:text-lg tw:py-1 tw:font-semibold tw:m-0 tw:tracking-tight tw:lg:text-xl">
{children} {children}
</h5> </h5>
) )
/**
* A component to renderd a styled H6 tag.
*
* Because of the TailwindCSS reset, common tags are unstyled.
* When you need to render a H6 tag outside of a context where it is
* automatically styled (such as inside markdown) you can use this.
* Alternatively, you can wrap your content in div.prose which will apply the
* styles in CSS.
*
* @component
* @param {object} props - All component props
* @param {JSX.element} props.children - The component children
* @returns {JSX.Element}
*/
export const H6 = ({ children }) => ( export const H6 = ({ children }) => (
<h6 className="tw:text-base tw:py-1 tw:font-medium tw:italic tw:m-0 tw:tracking-tight tw:lg:text-lg"> <h6 className="tw:text-base tw:py-1 tw:font-medium tw:italic tw:m-0 tw:tracking-tight tw:lg:text-lg">
{children} {children}

View file

@ -0,0 +1,154 @@
import React from 'react'
// Components
import { ModalWrapper } from '@freesewing/react/components/Modal'
/*
* A component to display an iframe intended for a modal window.
*
* All props are passed down to the iframe tag.
*
* @component
* @param {object} props - All component props
* @returns {JSX.Element}
*/
const Iframe = (props) => (
<iframe
{...props}
style={{ height: '90vh', width: '90vw' }}
className="tw:w-full tw:mx-auto tw:max-w-4xl"
/>
)
/*
* A component to display an iframe with FreeSewing.eu docs content intended for a modal window.
*
* @component
* @param {object} props - All component props
* @param {string} props.path - The (relative) URL path of the page to load
* @returns {JSX.Element}
*/
const DocsHelp = ({ path }) => (
<Iframe src={`https://freesewing.eu/${path}/?docusaurus-data-fs-embed=true`} />
)
/*
* A component to display inline help for a design option
*
* This is typically loaded as modal content as it returns an iframe.
*
* @component
* @param {object} props - All component props
* @param {string} props.design - The design name
* @param {string} props.o - The option ID
* @returns {JSX.Element}
*/
const DesignOptionHelp = ({ design, o }) =>
design && o ? (
<Iframe
src={`https://freesewing.eu/docs/designs/${design.toLowerCase()}/options/${o.toLowerCase()}/index.html?docusaurus-data-fs-embed=true`}
title="Design Options Help"
/>
) : (
<p>Invalid props provided to DesignOptionHelp.</p>
)
/*
* A component to display inline help for a core setting
*
* This is typically loaded as modal content as it returns an iframe.
*
* @component
* @param {object} props - All component props
* @param {string} props.name - The core setting name/id
* @returns {JSX.Element}
*/
const CoreSettingHelp = ({ name }) =>
name ? (
<Iframe
src={`https://freesewing.eu/docs/editor/menus/settings/${name.toLowerCase()}/index.html?docusaurus-data-fs-embed=true`}
title="Core Setting Help"
/>
) : (
<p>Invalid props provided to CoreSettingsHelp.</p>
)
/*
* A component to display inline help for a UI preference
*
* This is typically loaded as modal content as it returns an iframe.
*
* @component
* @param {object} props - All component props
* @param {string} props.name - The core setting name/id
* @returns {JSX.Element}
*/
const UiPreferenceHelp = ({ name }) =>
name ? (
<Iframe
src={`https://freesewing.eu/docs/editor/menus/preferences/${name.toLowerCase()}/index.html?docusaurus-data-fs-embed=true`}
title="UI Preferences Help"
/>
) : (
<p>Invalid props provided to UiPreferenceHelp.</p>
)
/*
* A component to display inline help for a measurement.
*
* This is typically loaded as modal content as it returns an iframe
*
* @component
* @param {object} props - All component props
* @param {string} [props.m = undefined] - The measurment name (id)
* @returns {JSX.Element}
*/
const MeasurementHelp = ({ m }) =>
m ? (
<iframe
src={`https://freesewing.eu/docs/measurements/${m.toLowerCase()}/index.html?docusaurus-data-fs-embed=true`}
title="Measurement Help"
style={{ height: '90vh', width: '90vw' }}
/>
) : (
<p>No measurement name provided in the m prop.</p>
)
export function modalCoreSettingHelp(name, setModal) {
setModal(
<ModalWrapper fullWidth keepOpenOnClick>
<CoreSettingHelp name={name} />
</ModalWrapper>
)
}
export function modalUiPreferenceHelp(name, setModal) {
setModal(
<ModalWrapper fullWidth keepOpenOnClick>
<UiPreferenceHelp name={name} />
</ModalWrapper>
)
}
export function modalDesignOptionHelp(design, o, setModal) {
setModal(
<ModalWrapper fullWidth keepOpenOnClick>
<DesignOptionHelp {...{ design, o }} />
</ModalWrapper>
)
}
export function modalMeasurementHelp(m, setModal) {
setModal(
<ModalWrapper fullWidth keepOpenOnClick>
<MeasurementHelp m={m} />
</ModalWrapper>
)
}
export function modalDocsHelp(path, setModal) {
setModal(
<ModalWrapper wide keepOpenOnClick>
<DocsHelp path={path} />
</ModalWrapper>
)
}

View file

@ -1,5 +1,5 @@
import React from 'react' import React from 'react'
import { CopyToClipboardButton } from '@freesewing/react/components/CopyToClipboardButton' import { CopyToClipboardButton } from '@freesewing/react/components/Button'
const defaultTitles = { const defaultTitles = {
js: 'Javascript', js: 'Javascript',
@ -12,13 +12,17 @@ const defaultTitles = {
/** /**
* A React component to highlight code * A React component to highlight code
* *
* @params {object} props - All React props * @component
* @params {string} language - The language to highlight * @param {object} props - All React props
* @params {object} children - The React children * @param {string} [props.language = 'txt'] - The language to highlight
* @params {bool} raw - Set this to true to not escape tags * @param {JSX.Element} props.children - The React children (this is the code to highlight)
* @params {string} title - Title for the highlight * @param {bool} [props.raw = false] - Set this to true to not escape tags
* @params {string} copy - Content to copy to clipboard * @param {string} [props.title = false] - Title for the highlight. When
* @params {bool} noCopy - Do not add copy to clipboard * language is js, bash, sh, json, or yaml the title will be set accordingly
* (but you can still pass in a custom one)
* @param {string} [props.copy = props.children] - Content for the copy to clipboard button, by default this will use props.children
* @param {bool} [props.noCopy = false] - Set this to true to not add the copy to clipboard button
* @returns {JSX.Element}
*/ */
export const Highlight = ({ export const Highlight = ({
language = 'txt', language = 'txt',

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -2,6 +2,15 @@ import React from 'react'
import { Highlight } from '@freesewing/react/components/Highlight' import { Highlight } from '@freesewing/react/components/Highlight'
import hljs from 'highlight.js/lib/common' import hljs from 'highlight.js/lib/common'
/**
* A component to code-highlight JSON data
*
* @component
* @param {object} props - All component props
* @param {object} [props.js = undefined] - An optional Javascript Object to highlight
* @param {JSX.Element} props.children - The component children, will be rendered if props.js is not set
* @returns {JSX.Element}
*/
export const Json = (props) => { export const Json = (props) => {
const code = props.js ? JSON.stringify(props.js, null, 2) : props.children const code = props.js ? JSON.stringify(props.js, null, 2) : props.children

View file

@ -2,6 +2,19 @@ import React, { useState, useContext } from 'react'
import { copyToClipboard } from '@freesewing/utils' import { copyToClipboard } from '@freesewing/utils'
import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus' import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus'
/**
* A component to display key/value pairs
*
* @component
* @param {object} props - All component props
* @param {boolean} [props.small = false] - Set this to render a small version
* @param {string} [props.color = primary] - The DaisyUI color to apply
* @param {boolean} [props.href = false] - Set this to make this a link
* @param {string} props.k - The key to display (key is a reserved react prop, so we use k)
* @param {boolean} [props.onClick = false] - Set this to make this a button
* @param {string} props.val - The value to display
* @returns {JSX.Element}
*/
export const KeyVal = ({ export const KeyVal = ({
k, k,
val, val,
@ -10,7 +23,7 @@ export const KeyVal = ({
href = false, href = false,
onClick = false, onClick = false,
}) => { }) => {
const [copied, setCopied] = useState(false) const setCopied = useState(false)[1]
const { setLoadingStatus } = useContext(LoadingStatusContext) const { setLoadingStatus } = useContext(LoadingStatusContext)
let colorClasses1 = primaryClasses1 let colorClasses1 = primaryClasses1
@ -25,6 +38,7 @@ export const KeyVal = ({
if (color === 'secondary') colorClasses2 = secondaryClasses2 if (color === 'secondary') colorClasses2 = secondaryClasses2
else if (color === 'neutral') colorClasses2 = neutralClasses2 else if (color === 'neutral') colorClasses2 = neutralClasses2
else if (color === 'accent') colorClasses2 = accentClasses2 else if (color === 'accent') colorClasses2 = accentClasses2
else if (color === 'info') colorClasses2 = infoClasses2
else if (color === 'warning') colorClasses2 = warningClasses2 else if (color === 'warning') colorClasses2 = warningClasses2
else if (color === 'success') colorClasses2 = successClasses2 else if (color === 'success') colorClasses2 = successClasses2
else if (color === 'error') colorClasses2 = errorClasses2 else if (color === 'error') colorClasses2 = errorClasses2
@ -69,7 +83,6 @@ export const KeyVal = ({
const LinkKeyVal = ({ const LinkKeyVal = ({
k, k,
val, val,
color = 'primary',
small = false, small = false,
href = false, href = false,
colorClasses1, colorClasses1,
@ -120,21 +133,6 @@ const successClasses2 = `tw:text-success tw:border-success`
const errorClasses1 = `tw:text-error-content tw:bg-error tw:border-error` const errorClasses1 = `tw:text-error-content tw:bg-error tw:border-error`
const errorClasses2 = `tw:text-error tw:border-error` const errorClasses2 = `tw:text-error tw:border-error`
const PrimarySpans = ({ small, k, val }) => (
<>
<span
className={`${sharedClasses} ${small ? 'tw:rounded-l' : 'tw:rounded-l-lg'} ${primaryClasses} ${small ? 'tw:text-xs' : ''}`}
>
{k}
</span>
<span
className={`${sharedClasses} ${small ? 'tw:rounded-r' : 'tw:rounded-r-lg'} ${primaryClasses} ${small ? 'tw:text-xs' : ''}`}
>
{val}
</span>
</>
)
const handleCopied = (setCopied, setLoadingStatus, label) => { const handleCopied = (setCopied, setLoadingStatus, label) => {
setCopied(true) setCopied(true)
setLoadingStatus([ setLoadingStatus([

View file

@ -2,17 +2,18 @@ import React from 'react'
import { Breadcrumbs } from '@freesewing/react/components/Breadcrumbs' import { Breadcrumbs } from '@freesewing/react/components/Breadcrumbs'
import { Link as DefaultLink } from '@freesewing/react/components/Link' import { Link as DefaultLink } from '@freesewing/react/components/Link'
/* /**
* This is the default layout, including title and breadcrumbs * This is the default layout, including title and breadcrumbs
* *
* @param {object} props - All React props * @component
* @param {array} props.children - The content to go in the layout * @param {object} props - All component props
* @param {array} props.crumbs - Data for the breadcrumbs * @param {function} [props.Link = false] - An optional framework specific Link component
* @param {string} props.description - The page description * @param {JSX.Element} [props.children = []] - The component children to render inside the layout
* @param {function} props.Link - An optional framework specific Link component * @param {array} [props.crumbs = []] - Data for the breadcrumbs, see Breadcrumbs
* @param {string} props.title - The page title * @param {string} props.title - The page title
* @returns {JSX.Element}
*/ */
export const Layout = ({ children = [], crumbs = [], description, Link = false, title }) => { export const Layout = ({ children = [], crumbs = [], Link = false, title }) => {
if (!Link) Link = DefaultLink if (!Link) Link = DefaultLink
return ( return (
@ -26,11 +27,13 @@ export const Layout = ({ children = [], crumbs = [], description, Link = false,
) )
} }
/* /**
* This is the base layout * This is the base layout
* *
* @param {object} props - All React props * @component
* @param {array} props.children - The content to go in the layout * @param {object} props - All component props
* @param {JSX.Element} [props.children = []] - The component children to render inside the layout
* @returns {JSX.Element}
*/ */
export const BaseLayout = ({ children }) => ( export const BaseLayout = ({ children }) => (
<div className="tw:flex tw:flex-row tw:items-start tw:w-full tw:justify-between tw:2xl:px-36 tw:xl:px-12 tw:px-4 tw:gap-0 tw:lg:gap-4 tw:xl:gap-8 3xl:tw:gap-12"> <div className="tw:flex tw:flex-row tw:items-start tw:w-full tw:justify-between tw:2xl:px-36 tw:xl:px-12 tw:px-4 tw:gap-0 tw:lg:gap-4 tw:xl:gap-8 3xl:tw:gap-12">
@ -38,11 +41,13 @@ export const BaseLayout = ({ children }) => (
</div> </div>
) )
/* /**
* The left column of the base layout * The left column of the base layout
* *
* @param {object} props - All React props * @component
* @param {array} props.children - The content to go in the layout * @param {object} props - All component props
* @param {JSX.Element} [props.children = []] - The component children to render inside the layout
* @returns {JSX.Element}
*/ */
export const BaseLayoutLeft = ({ children = [] }) => ( export const BaseLayoutLeft = ({ children = [] }) => (
<div className="tw:max-w-96 tw:w-1/4 tw:hidden tw:lg:block tw:shrink-0 tw:my-8 tw:sticky tw:top-4 tw:max-h-screen tw:overflow-scroll"> <div className="tw:max-w-96 tw:w-1/4 tw:hidden tw:lg:block tw:shrink-0 tw:my-8 tw:sticky tw:top-4 tw:max-h-screen tw:overflow-scroll">
@ -50,11 +55,13 @@ export const BaseLayoutLeft = ({ children = [] }) => (
</div> </div>
) )
/* /**
* The right column of the base layout * The right column of the base layout
* *
* @param {object} props - All React props * @component
* @param {array} props.children - The content to go in the layout * @param {object} props - All component props
* @param {JSX.Element} [props.children = []] - The component children to render inside the layout
* @returns {JSX.Element}
*/ */
export const BaseLayoutRight = ({ children = [] }) => ( export const BaseLayoutRight = ({ children = [] }) => (
<div className="tw:max-w-96 tw:w-1/4 tw:hidden tw:xl:block tw:my-8 tw:sticky tw:top-2"> <div className="tw:max-w-96 tw:w-1/4 tw:hidden tw:xl:block tw:my-8 tw:sticky tw:top-2">
@ -62,12 +69,14 @@ export const BaseLayoutRight = ({ children = [] }) => (
</div> </div>
) )
/* /**
* The main column for prose (text like docs and so on) * The main column for prose (text like docs and so on)
* *
* @param {object} props - All React props * @component
* @param {array} props.children - The content to go in the layout * @param {object} props - All component props
* @param {array} props.wide - Whether or not to use the wide view * @param {JSX.Element} [props.children = []] - The component children to render inside the layout
* @param {boolean} [props.wide false] - Set this to true to render a full-width prose layout
* @returns {JSX.Element}
*/ */
export const BaseLayoutProse = ({ children = [], wide = false }) => ( export const BaseLayoutProse = ({ children = [], wide = false }) => (
<div className={`tw:grow tw:w-full tw:m-auto tw:max-w-${wide ? 'full' : 'prose'} tw:my-8`}> <div className={`tw:grow tw:w-full tw:m-auto tw:max-w-${wide ? 'full' : 'prose'} tw:my-8`}>
@ -75,21 +84,25 @@ export const BaseLayoutProse = ({ children = [], wide = false }) => (
</div> </div>
) )
/* /**
* The central column for wide content (no max-width) * The central column for wide content (no max-width)
* *
* @param {object} props - All React props * @component
* @param {array} props.children - The content to go in the layout * @param {object} props - All component props
* @param {JSX.Element} [props.children = []] - The component children to render inside the layout
* @returns {JSX.Element}
*/ */
export const BaseLayoutWide = ({ children = [] }) => ( export const BaseLayoutWide = ({ children = [] }) => (
<div className="tw:grow tw:w-full tw:m-auto tw:my-8 tw:grow">{children}</div> <div className="tw:grow tw:w-full tw:m-auto tw:my-8 tw:grow">{children}</div>
) )
/* /**
* A layout for pages that do their own title/layout, like the sign in page * A layout for pages that do their own title/layout, like the sign in page
* *
* @param {object} props - All React props * @component
* @param {array} props.children - The content to go in the layout * @param {object} props - All component props
* @param {JSX.Element} [props.children = []] - The component children to render inside the layout
* @returns {JSX.Element}
*/ */
export const NoTitleLayout = ({ children }) => { export const NoTitleLayout = ({ children }) => {
return ( return (

View file

@ -10,9 +10,11 @@ const strokeScale = 0.5
/** /**
* A linedrawing component for Aaron * A linedrawing component for Aaron
* *
* @param {object} props - All React props * @component
* @param {object} props - All component props
* @param {string} props.className - Any CSS classes to apply * @param {string} props.className - Any CSS classes to apply
* @param {number} props.stroke - The stroke width to apply * @param {number} props.stroke - The stroke width to apply
* @returns {JSX.Element}
*/ */
export const Aaron = ({ className, stroke = 1 }) => ( export const Aaron = ({ className, stroke = 1 }) => (
<LineDrawingWrapper viewBox="-2 -15 152 152" {...{ stroke, className }}> <LineDrawingWrapper viewBox="-2 -15 152 152" {...{ stroke, className }}>
@ -24,9 +26,11 @@ export const Aaron = ({ className, stroke = 1 }) => (
/** /**
* A linedrawing component for the front of Aaron * A linedrawing component for the front of Aaron
* *
* @param {object} props - All React props * @component
* @param {object} props - All component props
* @param {string} props.className - Any CSS classes to apply * @param {string} props.className - Any CSS classes to apply
* @param {number} props.stroke - The stroke width to apply * @param {number} props.stroke - The stroke width to apply
* @returns {JSX.Element}
*/ */
export const AaronFront = ({ className, stroke = 1 }) => ( export const AaronFront = ({ className, stroke = 1 }) => (
<LineDrawingWrapper viewBox="-23 0 119 119" {...{ className }}> <LineDrawingWrapper viewBox="-23 0 119 119" {...{ className }}>
@ -37,9 +41,11 @@ export const AaronFront = ({ className, stroke = 1 }) => (
/** /**
* A linedrawing component for the front of Aaron * A linedrawing component for the front of Aaron
* *
* @param {object} props - All React props * @component
* @param {object} props - All component props
* @param {string} props.className - Any CSS classes to apply * @param {string} props.className - Any CSS classes to apply
* @param {number} props.stroke - The stroke width to apply * @param {number} props.stroke - The stroke width to apply
* @returns {JSX.Element}
*/ */
export const AaronBack = ({ className, stroke = 1 }) => ( export const AaronBack = ({ className, stroke = 1 }) => (
<LineDrawingWrapper viewBox="51 0 119 119" {...{ className }}> <LineDrawingWrapper viewBox="51 0 119 119" {...{ className }}>
@ -50,7 +56,7 @@ export const AaronBack = ({ className, stroke = 1 }) => (
/* /*
* SVG elements for the front * SVG elements for the front
*/ */
export const Front = ({ stroke }) => ( const Front = ({ stroke }) => (
<> <>
<path <path
key="stitches" key="stitches"

View file

@ -10,9 +10,11 @@ const strokeScale = 0.5
/** /**
* A linedrawing component for Albert * A linedrawing component for Albert
* *
* @param {object} props - All React props * @component
* @param {object} props - All component props
* @param {string} props.className - Any CSS classes to apply * @param {string} props.className - Any CSS classes to apply
* @param {number} props.stroke - The stroke width to apply * @param {number} props.stroke - The stroke width to apply
* @returns {JSX.Element}
*/ */
export const Albert = ({ className, stroke = 1 }) => ( export const Albert = ({ className, stroke = 1 }) => (
<LineDrawingWrapper viewBox="-10 -2 146 146" {...{ className }}> <LineDrawingWrapper viewBox="-10 -2 146 146" {...{ className }}>
@ -24,9 +26,11 @@ export const Albert = ({ className, stroke = 1 }) => (
/** /**
* A linedrawing component for the front of Albert * A linedrawing component for the front of Albert
* *
* @param {object} props - All React props * @component
* @param {object} props - All component props
* @param {string} props.className - Any CSS classes to apply * @param {string} props.className - Any CSS classes to apply
* @param {number} props.stroke - The stroke width to apply * @param {number} props.stroke - The stroke width to apply
* @returns {JSX.Element}
*/ */
export const AlbertFront = ({ className, stroke = 1 }) => ( export const AlbertFront = ({ className, stroke = 1 }) => (
<LineDrawingWrapper viewBox="-41 -2 145 145" {...{ className }}> <LineDrawingWrapper viewBox="-41 -2 145 145" {...{ className }}>
@ -37,12 +41,14 @@ export const AlbertFront = ({ className, stroke = 1 }) => (
/** /**
* A linedrawing component for the front of Albert * A linedrawing component for the front of Albert
* *
* @param {object} props - All React props * @component
* @param {object} props - All component props
* @param {string} props.className - Any CSS classes to apply * @param {string} props.className - Any CSS classes to apply
* @param {number} props.stroke - The stroke width to apply * @param {number} props.stroke - The stroke width to apply
* @returns {JSX.Element}
*/ */
export const AlbertBack = ({ className, stroke = 1 }) => ( export const AlbertBack = ({ className, stroke = 1 }) => (
<LineDrawingWrapper viewBox="74 0 74 119" {...props}> <LineDrawingWrapper viewBox="74 0 74 119" {...{ className, stroke }}>
<Back stroke={stroke * strokeScale} /> <Back stroke={stroke * strokeScale} />
</LineDrawingWrapper> </LineDrawingWrapper>
) )
@ -50,7 +56,7 @@ export const AlbertBack = ({ className, stroke = 1 }) => (
/* /*
* SVG elements for the front * SVG elements for the front
*/ */
export const Front = ({ stroke }) => ( const Front = ({ stroke }) => (
<> <>
<path <path
key="stitches" key="stitches"

View file

@ -10,9 +10,11 @@ const strokeScale = 0.4
/** /**
* A linedrawing component for Bee * A linedrawing component for Bee
* *
* @param {object} props - All React props * @component
* @param {object} props - All component props
* @param {string} props.className - Any CSS classes to apply * @param {string} props.className - Any CSS classes to apply
* @param {number} props.stroke - The stroke width to apply * @param {number} props.stroke - The stroke width to apply
* @returns {JSX.Element}
*/ */
export const Bee = ({ className, stroke = 1 }) => ( export const Bee = ({ className, stroke = 1 }) => (
<LineDrawingWrapper viewBox="-20 0 120 120" {...{ stroke, className }}> <LineDrawingWrapper viewBox="-20 0 120 120" {...{ stroke, className }}>
@ -21,14 +23,20 @@ export const Bee = ({ className, stroke = 1 }) => (
) )
/** /**
* Front is the same * A linedrawing component for the front of Bee
*
* @component
* @param {object} props - All component props
* @param {string} props.className - Any CSS classes to apply
* @param {number} props.stroke - The stroke width to apply
* @returns {JSX.Element}
*/ */
export const BeeFront = Bee export const BeeFront = Bee
/* /*
* SVG elements for the front * SVG elements for the front
*/ */
export const Front = ({ stroke }) => ( const Front = ({ stroke }) => (
<> <>
<path <path
key="stitches" key="stitches"

View file

@ -10,9 +10,11 @@ const strokeScale = 0.5
/** /**
* A linedrawing component for Bella * A linedrawing component for Bella
* *
* @param {object} props - All React props * @component
* @param {object} props - All component props
* @param {string} props.className - Any CSS classes to apply * @param {string} props.className - Any CSS classes to apply
* @param {number} props.stroke - The stroke width to apply * @param {number} props.stroke - The stroke width to apply
* @returns {JSX.Element}
*/ */
export const Bella = ({ className, stroke = 1 }) => ( export const Bella = ({ className, stroke = 1 }) => (
<LineDrawingWrapper viewBox="2 -30 155 155" {...{ stroke, className }}> <LineDrawingWrapper viewBox="2 -30 155 155" {...{ stroke, className }}>
@ -24,9 +26,11 @@ export const Bella = ({ className, stroke = 1 }) => (
/** /**
* A linedrawing component for the front of Bella * A linedrawing component for the front of Bella
* *
* @param {object} props - All React props * @component
* @param {object} props - All component props
* @param {string} props.className - Any CSS classes to apply * @param {string} props.className - Any CSS classes to apply
* @param {number} props.stroke - The stroke width to apply * @param {number} props.stroke - The stroke width to apply
* @returns {JSX.Element}
*/ */
export const BellaFront = ({ className, stroke = 1 }) => ( export const BellaFront = ({ className, stroke = 1 }) => (
<LineDrawingWrapper viewBox="-4 2 89 86" {...{ className, stroke }}> <LineDrawingWrapper viewBox="-4 2 89 86" {...{ className, stroke }}>
@ -34,8 +38,14 @@ export const BellaFront = ({ className, stroke = 1 }) => (
</LineDrawingWrapper> </LineDrawingWrapper>
) )
/* /**
* React component for the back * A linedrawing component for the back of Bella
*
* @component
* @param {object} props - All component props
* @param {string} props.className - Any CSS classes to apply
* @param {number} props.stroke - The stroke width to apply
* @returns {JSX.Element}
*/ */
export const BellaBack = ({ export const BellaBack = ({
className = 'tw:w-full', // CSS classes to apply className = 'tw:w-full', // CSS classes to apply
@ -54,7 +64,7 @@ export const BellaBack = ({
/* /*
* SVG elements for the front * SVG elements for the front
*/ */
export const Front = ({ stroke }) => ( const Front = ({ stroke }) => (
<> <>
<path <path
key="stitches" key="stitches"

View file

@ -10,9 +10,11 @@ const strokeScale = 1.2
/** /**
* A linedrawing component for Benjamin * A linedrawing component for Benjamin
* *
* @param {object} props - All React props * @component
* @param {object} props - All component props
* @param {string} props.className - Any CSS classes to apply * @param {string} props.className - Any CSS classes to apply
* @param {number} props.stroke - The stroke width to apply * @param {number} props.stroke - The stroke width to apply
* @returns {JSX.Element}
*/ */
export const Benjamin = ({ className, stroke = 1 }) => ( export const Benjamin = ({ className, stroke = 1 }) => (
<LineDrawingWrapper viewBox="0 -44 138 138" {...{ className, stroke }}> <LineDrawingWrapper viewBox="0 -44 138 138" {...{ className, stroke }}>
@ -20,15 +22,21 @@ export const Benjamin = ({ className, stroke = 1 }) => (
</LineDrawingWrapper> </LineDrawingWrapper>
) )
/* /**
* Front is the same * A linedrawing component for the front of Benjamin
*
* @component
* @param {object} props - All component props
* @param {string} props.className - Any CSS classes to apply
* @param {number} props.stroke - The stroke width to apply
* @returns {JSX.Element}
*/ */
export const BenjaminFront = Benjamin export const BenjaminFront = Benjamin
/* /*
* SVG elements for the front * SVG elements for the front
*/ */
export const Front = ({ stroke }) => ( const Front = ({ stroke }) => (
<> <>
<path <path
key="folds" key="folds"

View file

@ -10,12 +10,14 @@ const strokeScale = 0.7
/** /**
* A linedrawing component for Bent * A linedrawing component for Bent
* *
* @param {object} props - All React props * @component
* @param {object} props - All component props
* @param {string} props.className - Any CSS classes to apply * @param {string} props.className - Any CSS classes to apply
* @param {number} props.stroke - The stroke width to apply * @param {number} props.stroke - The stroke width to apply
* @returns {JSX.Element}
*/ */
export const Bent = ({ className, stroke = 1 }) => ( export const Bent = ({ className, stroke = 1 }) => (
<LineDrawingWrapper viewBox="0 -70 210 210" {...{ stroke }}> <LineDrawingWrapper viewBox="0 -70 210 210" {...{ className, stroke }}>
<Front stroke={strokeScale * stroke} /> <Front stroke={strokeScale * stroke} />
<Back stroke={strokeScale * stroke} /> <Back stroke={strokeScale * stroke} />
</LineDrawingWrapper> </LineDrawingWrapper>
@ -24,9 +26,11 @@ export const Bent = ({ className, stroke = 1 }) => (
/** /**
* A linedrawing component for the front of Bent * A linedrawing component for the front of Bent
* *
* @param {object} props - All React props * @component
* @param {object} props - All component props
* @param {string} props.className - Any CSS classes to apply * @param {string} props.className - Any CSS classes to apply
* @param {number} props.stroke - The stroke width to apply * @param {number} props.stroke - The stroke width to apply
* @returns {JSX.Element}
*/ */
export const BentFront = ({ className, stroke = 1 }) => ( export const BentFront = ({ className, stroke = 1 }) => (
<LineDrawingWrapper viewBox="0 -1 103 103" {...{ className, stroke }}> <LineDrawingWrapper viewBox="0 -1 103 103" {...{ className, stroke }}>
@ -34,8 +38,14 @@ export const BentFront = ({ className, stroke = 1 }) => (
</LineDrawingWrapper> </LineDrawingWrapper>
) )
/* /**
* React component for the back * A linedrawing component for the back of Bent
*
* @component
* @param {object} props - All component props
* @param {string} props.className - Any CSS classes to apply
* @param {number} props.stroke - The stroke width to apply
* @returns {JSX.Element}
*/ */
export const BentBack = ({ export const BentBack = ({
className = 'tw:w-full', // CSS classes to apply className = 'tw:w-full', // CSS classes to apply
@ -54,7 +64,7 @@ export const BentBack = ({
/* /*
* SVG elements for the front * SVG elements for the front
*/ */
export const Front = ({ stroke }) => ( const Front = ({ stroke }) => (
<> <>
<path <path
key="stitches" key="stitches"

Some files were not shown because too many files have changed in this diff Show more