1
0
Fork 0

wip(lab): Work on porting lab/workbenck to named exports

This commit is contained in:
joostdecock 2023-01-29 18:57:24 +01:00
parent db789180b6
commit 59708f534d
85 changed files with 1339 additions and 1149 deletions

View file

@ -1,28 +0,0 @@
import Popout from 'shared/components/popout.js'
const About = () => (
<Popout tip>
<h2>What to expect at lab.freesewing.org</h2>
<p>
The FreeSewing lab is an online environment to road-test our various patterns/designs.
</p>
<p>
This website runs the bleeding edge of our code base.
Some patterns here may be unreleased or at various states of being worked on.
As such, this website is intended for FreeSewing contributors or people interested
in what happens under the hood.
</p>
<p>
If you want sewing patterns to actually start making something,
please visit <a
href="https://freesewing.org/"
className="text-secondary font-bold hover-text-secondary-focus hover:underline"
>freesewing.org</a>, our flagship website for makers.
</p>
<Popout fixme>
This lacks translation
</Popout>
</Popout>
)
export default About

View file

@ -0,0 +1,24 @@
import { Popout } from 'shared/components/popout.mjs'
export const About = () => (
<Popout tip>
<h2>What to expect at lab.freesewing.org</h2>
<p>The FreeSewing lab is an online environment to road-test our various patterns/designs.</p>
<p>
This website runs the bleeding edge of our code base. Some patterns here may be unreleased or
at various states of being worked on. As such, this website is intended for FreeSewing
contributors or people interested in what happens under the hood.
</p>
<p>
If you want sewing patterns to actually start making something, please visit{' '}
<a
href="https://freesewing.org/"
className="text-secondary font-bold hover-text-secondary-focus hover:underline"
>
freesewing.org
</a>
, our flagship website for makers.
</p>
<Popout fixme>This lacks translation</Popout>
</Popout>
)

View file

@ -1,11 +1,10 @@
import React, { Fragment } from 'react'
import DesignIcon from 'shared/components/icons/design'
import { useTranslation } from 'next-i18next'
import { Popover, Transition } from '@headlessui/react'
import DownIcon from 'shared/components/icons/down'
import Link from 'next/link'
import { Popover, Transition } from '@headlessui/react'
import { DesignIcon, DownIcon } from 'shared/components/icons.mjs'
const PatternPicker = ({ app }) => {
export const DesignPicker = ({ app }) => {
const { t } = useTranslation(['common'])
const sectionPatterns = (section) =>
@ -68,5 +67,3 @@ const PatternPicker = ({ app }) => {
</Popover>
)
}
export default PatternPicker

View file

@ -1,18 +1,21 @@
import Logo from 'shared/components/logos/freesewing.js'
import OsiLogo from 'shared/components/logos/osi.js'
import CreativeCommonsLogo from 'shared/components/logos/cc.js'
import CcByLogo from 'shared/components/logos/cc-by.js'
// Hooks
import { useTranslation } from 'next-i18next'
import Ribbon from 'shared/components/ribbon.js'
// Components
import Link from 'next/link'
import { WordMark } from 'shared/components/wordmark.js'
import DiscordIcon from 'shared/components/icons/discord.js'
import FacebookIcon from 'shared/components/icons/facebook.js'
import GithubIcon from 'shared/components/icons/github.js'
import InstagramIcon from 'shared/components/icons/instagram.js'
import RedditIcon from 'shared/components/icons/reddit.js'
import TwitterIcon from 'shared/components/icons/twitter.js'
import { FreeSewingLogo } from 'shared/components/logos/freesewing.mjs'
import { OsiLogo } from 'shared/components/logos/osi.mjs'
import { CCLogo } from 'shared/components/logos/cc.mjs'
import { CCByLogo } from 'shared/components/logos/cc-by.mjs'
import { Ribbon } from 'shared/components/ribbon.mjs'
import { WordMark } from 'shared/components/wordmark.mjs'
import {
DiscordIcon,
FacebookIcon,
GithubIcon,
InstagramIcon,
RedditIcon,
TwitterIcon,
} from 'shared/components/icons.mjs'
// Classes
const link = 'text-secondary font-bold hover:pointer hover:underline px-1'
@ -77,7 +80,7 @@ const social = {
},
}
const Footer = ({ app }) => {
export const Footer = ({ app }) => {
const { t } = useTranslation(['common', 'patrons'])
return (
@ -88,10 +91,10 @@ const Footer = ({ app }) => {
<div className="mb-20 order-1 mt-20 2xl:mt-0 2xl:mb-0">
<div className="max-w-md m-auto">
<div>
<CreativeCommonsLogo className="w-64 m-auto" />
<CCLogo className="w-64 m-auto" />
</div>
<div className="flex flex-row gap-2 justify-center items-center mt-8">
<CcByLogo className="w-8 lg:w-12" />
<CCByLogo className="w-8 lg:w-12" />
<p className="text-neutral-content text-right basis-4/5 lg:basis-3/4 leading-5">
{translations.cc}
</p>
@ -157,7 +160,7 @@ const Footer = ({ app }) => {
{/* Col 3 - Logo & Slogan */}
<div className="w-full 4xl:w-auto xl:max-w-md mb-8 text-center order-3 mt-0 lg:mt-20 2xl:mt-0 2xl:mb-0">
<div className="max-w-md m-auto">
<Logo stroke="none" size={164} className="w-40 lg:w-64 m-auto m-auto" />
<FreeSewingLogo stroke="none" size={164} className="w-40 lg:w-64 m-auto m-auto" />
<h5 className="lg:text-3xl mt-4">
<WordMark />
</h5>
@ -172,5 +175,3 @@ const Footer = ({ app }) => {
</footer>
)
}
export default Footer

View file

@ -1,13 +1,14 @@
// Hooks
import { useState, useEffect } from 'react'
import ThemePicker from 'shared/components/theme-picker.js'
import LocalePicker from 'shared/components/locale-picker.js'
import DesignPicker from 'site/components/design-picker.js'
import CloseIcon from 'shared/components/icons/close.js'
import MenuIcon from 'shared/components/icons/menu.js'
import Ribbon from 'shared/components/ribbon.js'
import { WordMark } from 'shared/components/wordmark.js'
// Components
import { ThemePicker } from 'shared/components/theme-picker/index.mjs'
import { LocalePicker } from 'shared/components/locale-picker/index.mjs'
import { DesignPicker } from 'site/components/design-picker.mjs'
import { CloseIcon, MenuIcon } from 'shared/components/icons.mjs'
import { Ribbon } from 'shared/components/ribbon.mjs'
import { WordMark } from 'shared/components/wordmark.mjs'
const Header = ({ app }) => {
export const Header = ({ app }) => {
const [prevScrollPos, setPrevScrollPos] = useState(0)
const [show, setShow] = useState(true)
@ -67,5 +68,3 @@ const Header = ({ app }) => {
</header>
)
}
export default Header

View file

@ -1,49 +1,53 @@
import Popout from 'shared/components/popout.js'
import { Popout } from 'shared/components/popout.mjs'
const HelpUs = ({ mdx=false, slug='/' }) => (
export const HelpUs = ({ mdx = false, slug = '/' }) => (
<details className="mt-4">
<summary>Click here to learn how you can help us improve this page</summary>
{mdx && (
<Popout tip className='max-w-prose'>
<Popout tip className="max-w-prose">
<h6>Found a mistake?</h6>
You can <a
You can{' '}
<a
href={`https://github.com/freesewing/freesewing/edit/develop/markdown/dev/${slug}/en.md`}
className="text-secondary font-bold"
>edit this page on Github</a> and help us improve our documentation.
>
edit this page on Github
</a>{' '}
and help us improve our documentation.
</Popout>
)}
<Popout comment by="joost" className='max-w-prose'>
<Popout comment by="joost" className="max-w-prose">
<h6>Does this look ok?</h6>
<img
className="my-4 rounded"
src={`https://canary.backend.freesewing.org/og-img/en/dev${slug}`}
/>
<p>
If it looks ok, great! But if not, please let me know about it.
Either by <a href="https://discord.freesewing.org/" className="text-secondary">
If it looks ok, great! But if not, please let me know about it. Either by{' '}
<a href="https://discord.freesewing.org/" className="text-secondary">
reaching out on Discord
</a> or feel free to <a
</a>{' '}
or feel free to{' '}
<a
href="https://github.com/freesewing/freesewing/issues/new/choose"
className="text-secondary"
>create
an issue on Github</a>.
>
create an issue on Github
</a>
.
</p>
<h6>Why do you ask?</h6>
<p className="text-base">
I recently added a backend endpoint to auto-generate pretty (I hope) Open Graph images.
They are those little preview images you see when you paste a link in Discord (for example).
I recently added a backend endpoint to auto-generate pretty (I hope) Open Graph images. They
are those little preview images you see when you paste a link in Discord (for example).
</p>
<p className="text-base">
This idea is that it will auto-generate an image, but I am certain there are some edge cases
where that will not work.
There are hundreds of pages on this website and checking them all one by one is not something
I see myself doing. But since you are here on this page, perhaps you could see if the image
above looks ok.
where that will not work. There are hundreds of pages on this website and checking them all
one by one is not something I see myself doing. But since you are here on this page, perhaps
you could see if the image above looks ok.
</p>
<p className="text-base">Thank you, I really appreciate your help with this.</p>
</Popout>
</details>
)
export default HelpUs

View file

@ -1,17 +0,0 @@
import { useRouter } from 'next/router'
import Aside from 'shared/components/navigation/aside'
import { BeforeNav } from './lab'
const DefaultLayout = ({ app, children = [] }) => {
const router = useRouter()
const slug = router.asPath.slice(1)
return (
<>
<Aside app={app} slug={slug} before={<BeforeNav app={app} />} mobileOnly />
{children}
</>
)
}
export default DefaultLayout

View file

@ -0,0 +1,15 @@
import { useRouter } from 'next/router'
import { AsideNavigation } from 'shared/components/navigation/aside.mjs'
import { BeforeNav } from './lab.mjs'
export const BareLayout = ({ app, children = [] }) => {
const router = useRouter()
const slug = router.asPath.slice(1)
return (
<>
<AsideNavigation app={app} slug={slug} before={<BeforeNav app={app} />} mobileOnly />
{children}
</>
)
}

View file

@ -1,5 +1,5 @@
import ThemePicker from 'shared/components/theme-picker.js'
import LocalePicker from 'shared/components/locale-picker.js'
import { ThemePicker } from 'shared/components/theme-picker/index.mjs'
import { LocalePicker } from 'shared/components/locale-picker/index.mjs'
export const BeforeNav = ({ app }) => (
<>
@ -11,7 +11,7 @@ export const BeforeNav = ({ app }) => (
</>
)
const LabLayout = ({ app, AltMenu, children = [] }) => (
export const LabLayout = ({ app, AltMenu, children = [] }) => (
<div className="py-24 lg:py-36 flex flex-row">
<div className="w-full xl:w-3/4 px-8">{children}</div>
<aside
@ -42,5 +42,3 @@ const LabLayout = ({ app, AltMenu, children = [] }) => (
</aside>
</div>
)
export default LabLayout

View file

@ -1,3 +0,0 @@
const noSearch = () => null
export default noSearch

View file

@ -0,0 +1 @@
export const Search = () => null

View file

@ -1,9 +1,10 @@
// Hooks
import { useRouter } from 'next/router'
// Site components
import Header from 'site/components/header'
import Footer from 'site/components/footer'
// Components
import { Header } from 'site/components/header.mjs'
import { Footer } from 'site/components/footer.mjs'
const LayoutWrapper = ({ app, children = [] }) => {
export const LayoutWrapper = ({ app, children = [] }) => {
const startNavigation = () => {
app.startLoading()
// Force close of menu on mobile if it is open
@ -28,5 +29,3 @@ const LayoutWrapper = ({ app, children = [] }) => {
</div>
)
}
export default LayoutWrapper

View file

@ -1,11 +1,12 @@
// Hooks
import { useEffect } from 'react'
import { useSwipeable } from 'react-swipeable'
import { useRouter } from 'next/router'
// Layouts components
import LayoutWrapper from 'site/components/wrappers/layout'
// Components
import { LayoutWrapper } from 'site/components/wrappers/layout.mjs'
/* This component should wrap all page content */
const PageWrapper = ({
export const PageWrapper = ({
title = 'FIXME: No title set',
app = false,
layout = false,
@ -42,5 +43,3 @@ const PageWrapper = ({
</div>
)
}
export default PageWrapper

View file

@ -1,13 +1,13 @@
// Hooks
import { useState } from 'react'
// Stores state in local storage
import useLocalStorage from 'shared/hooks/useLocalStorage.js'
// Designs
import { designsByType } from 'prebuild/designs-by-type.mjs'
// Locale and translation
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import { useLocalStorage } from 'shared/hooks/useLocalStorage.mjs'
import { useTheme } from 'shared/hooks/useTheme.mjs'
// Dependenices
import { designsByType } from 'prebuild/designs-by-type.mjs'
// Components
import { capitalize } from 'shared/utils.mjs'
import useTheme from 'shared/hooks/useTheme'
// Initial navigation
const initialNavigation = (t) => {

View file

@ -1,10 +1,12 @@
import Page from 'site/components/wrappers/page.js'
import useApp from 'site/hooks/useApp.js'
import Link from 'next/link'
// Hooks
import { useApp } from 'site/hooks/useApp.mjs'
import { useTranslation } from 'next-i18next'
import Layout from 'site/components/layouts/bare'
import { PageTitle } from 'shared/components/layouts/default'
// Components
import Head from 'next/head'
import Link from 'next/link'
import { PageWrapper } from 'site/components/wrappers/page.mjs'
import { BareLayout } from 'site/components/layouts/bare.mjs'
import { PageTitle } from 'shared/components/layouts/default.mjs'
const DesignLinks = ({ list, prefix = '' }) => {
const { t } = useTranslation(['patterns'])
@ -29,7 +31,7 @@ const DesignLinks = ({ list, prefix = '' }) => {
)
}
const PatternListPageTemplate = ({ section = false }) => {
export const PatternListPageTemplate = ({ section = false }) => {
const app = useApp()
const { t } = useTranslation(['app'])
@ -47,7 +49,7 @@ const PatternListPageTemplate = ({ section = false }) => {
}
return (
<Page app={app} title={`FreeSewing Lab: ${title}`} layout={Layout}>
<PageWrapper app={app} title={`FreeSewing Lab: ${title}`} layout={BareLayout}>
<Head>
<title>{fullTitle}</title>
</Head>
@ -61,8 +63,6 @@ const PatternListPageTemplate = ({ section = false }) => {
<DesignLinks list={sectionDesigns(section)} />
</section>
</div>
</Page>
</PageWrapper>
)
}
export default PatternListPageTemplate

View file

@ -1,12 +1,15 @@
import Page from 'site/components/wrappers/page.js'
import useApp from 'site/hooks/useApp.js'
import WorkbenchWrapper from 'shared/components/wrappers/workbench.js'
// Hooks
import { useApp } from 'site/hooks/useApp.mjs'
import { useRouter } from 'next/router'
import Layout from 'site/components/layouts/lab'
import Head from 'next/head'
// Dependencies
import { capitalize } from 'shared/utils.mjs'
// Components
import Head from 'next/head'
import { PageWrapper } from 'site/components/wrappers/page.mjs'
import { WorkbenchWrapper } from 'shared/components/wrappers/workbench.mjs'
import { LabLayout } from 'site/components/layouts/lab.mjs'
const WorkbenchPage = ({ design }) => {
export const WorkbenchPage = ({ design }) => {
const app = useApp()
const router = useRouter()
const { preload, from } = router.query
@ -16,13 +19,11 @@ const WorkbenchPage = ({ design }) => {
const fullTitle = title + ' - FreeSewing Lab'
return (
<Page app={app}>
<PageWrapper app={app}>
<Head>
<title>{fullTitle}</title>
</Head>
<WorkbenchWrapper {...{ app, design, preload, from }} layout={Layout} />
</Page>
<WorkbenchWrapper {...{ app, design, preload, from }} layout={LabLayout} />
</PageWrapper>
)
}
export default WorkbenchPage

View file

@ -1,10 +1,13 @@
import Page from 'site/components/wrappers/page.js'
import useApp from 'site/hooks/useApp.js'
import Head from 'next/head'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import Layout from 'site/components/layouts/bare'
// Hooks
import useApp from 'site/hooks/useApp.mjs'
import { useTranslation } from 'next-i18next'
import { Icons } from 'shared/components/navigation/primary'
// Dependencies
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
// Components
import Head from 'next/head'
import { PageWrapper } from 'site/components/wrappers/page.mjs'
import { BareLayout } from 'site/components/layouts/bare.mjs'
import { Icons } from 'shared/components/navigation/primary.mjs'
const title = 'Welcome to the FreeSewing Lab'
@ -12,7 +15,7 @@ const HomePage = () => {
const app = useApp()
const { t } = useTranslation(['lab'])
return (
<Page app={app} title="{title}" layout={Layout}>
<PageWrapper app={app} title="{title}" layout={BareLayout}>
<Head>
<meta property="og:title" content="FreeSewing.dev" key="title" />
<meta property="og:type" content="article" key="type" />
@ -148,7 +151,7 @@ const HomePage = () => {
</div>
</div>
</div>
</Page>
</PageWrapper>
)
}

View file

@ -96,7 +96,7 @@ export const CopyIcon = (props) => (
)
export const DesignIcon = (props) => (
<IconWrapper {...props} stroke={1}>
<IconWrapper {...props} stroke={0} fill>
<path d="m11.975 2.9104c-1.5285 0-2.7845 1.2563-2.7845 2.7848 0 0.7494 0.30048 1.4389 0.78637 1.9394a0.79437 0.79437 0 0 0 0.0084 0.00839c0.38087 0.38087 0.74541 0.62517 0.94538 0.82483 0.19998 0.19966 0.25013 0.2645 0.25013 0.51907v0.65964l-9.1217 5.2665c-0.28478 0.16442-0.83603 0.46612-1.3165 0.9611-0.48047 0.49498-0.92451 1.3399-0.66684 2.2585 0.22026 0.78524 0.7746 1.3486 1.3416 1.5878 0.56697 0.23928 1.0982 0.23415 1.4685 0.23415h18.041c0.37033 0 0.90158 0.0051 1.4686-0.23415 0.56697-0.23928 1.1215-0.80261 1.3418-1.5878 0.25767-0.91859-0.18662-1.7636-0.66709-2.2585-0.48046-0.49498-1.0315-0.79669-1.3162-0.9611l-8.9844-5.1873v-0.73889c0-0.70372-0.35623-1.2837-0.71653-1.6435-0.35778-0.3572-0.70316-0.58503-0.93768-0.81789-0.20864-0.21601-0.33607-0.50298-0.33607-0.83033 0-0.67 0.52595-1.1962 1.1959-1.1962 0.67001 0 1.1962 0.5262 1.1962 1.1962a0.79429 0.79429 0 0 0 0.79434 0.79427 0.79429 0.79429 0 0 0 0.79427-0.79427c0-1.5285-1.2563-2.7848-2.7848-2.7848zm-0.06859 8.2927 8.9919 5.1914c0.28947 0.16712 0.69347 0.41336 0.94393 0.67138 0.25046 0.25803 0.31301 0.3714 0.24754 0.60483-0.10289 0.36677-0.19003 0.40213-0.35969 0.47373-0.16967 0.07161-0.47013 0.09952-0.80336 0.09952h-18.041c-0.33323 0-0.6337-0.02792-0.80336-0.09952-0.16967-0.07161-0.25675-0.10696-0.35963-0.47373-0.06548-0.23342-0.00303-0.3468 0.24748-0.60483 0.25046-0.25803 0.65471-0.50426 0.94418-0.67138z" />
</IconWrapper>
)

View file

@ -1,16 +0,0 @@
const defaultSettings = {
sa: 0,
saBool: false,
saMm: 10,
scale: 1,
complete: true,
paperless: false,
units: 'metric',
locale: 'en',
margin: 2,
renderer: 'react',
embed: true,
debug: true,
}
export default defaultSettings

View file

@ -1,4 +1,4 @@
const Circle = (props) =>
export const Circle = (props) =>
props.point.attributes.getAsArray('data-circle').map((r, i) => {
const circleProps = props.point.attributes.asPropsIfPrefixIs('data-circle-')
const extraProps = {}
@ -12,5 +12,3 @@ const Circle = (props) =>
return <circle key={r} cx={props.point.x} cy={props.point.y} r={r} {...extraProps} />
})
export default Circle

View file

@ -48,7 +48,7 @@ const grids = {
`,
}
const Defs = (props) => {
export const Defs = (props) => {
let defs = props.svg.defs
if (props.settings[0].paperless) {
defs += grids[props.settings[0].units || 'metric']
@ -65,5 +65,3 @@ const Defs = (props) => {
return <defs dangerouslySetInnerHTML={{ __html: defs }} />
}
export default Defs

View file

@ -1,6 +1,6 @@
import DefaultErrorView from 'shared/components/error/view'
import { ErrorView } from 'shared/components/error/view.mjs'
const Error = (props) => {
export const Error = (props) => {
const errors = {
pattern: 0,
sets: 0,
@ -58,10 +58,8 @@ const Error = (props) => {
)
return (
<DefaultErrorView inspectChildren={ic} {...props}>
<ErrorView inspectChildren={ic} {...props}>
<p>No need to be alarmed, but we ran into some trouble while drafting this pattern.</p>
</DefaultErrorView>
</ErrorView>
)
}
export default Error

View file

@ -1,7 +1,7 @@
import SvgWrapper from './svg-wrapper'
import Error from './error.js'
import { SvgWrapper } from './svg.mjs'
import { Error } from './error.mjs'
const LabDraft = (props) => {
export const LabDraft = (props) => {
const { app, draft, gist, updateGist, unsetGist, showInfo, feedback, hasRequiredMeasurements } =
props
@ -60,5 +60,3 @@ const LabDraft = (props) => {
</>
)
}
export default LabDraft

View file

@ -1,10 +1,9 @@
import { forwardRef } from 'react'
import Path from '../path'
import Point from '../point'
import Snippet from '../snippet'
import { getProps } from '../utils'
import { round } from 'shared/utils'
import { Tr, KeyTd, ValTd, Attributes, pointCoords } from '../path/index'
import { Point } from './point.mjs'
import { Snippet } from './snippet.mjs'
import { getProps } from './utils.mjs'
import { round } from 'shared/utils.mjs'
import { Path, Tr, KeyTd, ValTd, Attributes, pointCoords } from './path.mjs'
const partInfo = (props) => (
<div className="p-4 border bg-neutral bg-opacity-40 shadow rounded-lg">
@ -142,7 +141,7 @@ export const PartInner = forwardRef((props, ref) => {
PartInner.displayName = 'PartInner'
const Part = (props) => {
export const Part = (props) => {
const { partName, part } = props
return (
@ -155,14 +154,3 @@ const Part = (props) => {
</g>
)
}
/*
<ErrorBoundary
x={part.topLeft.x}
y={part.topLeft.y}
width={part.width}
height={part.height}
>
</ErrorBoundary>
*/
export default Part

View file

@ -1,7 +1,7 @@
import TextOnPath from '../text-on-path'
import { getProps } from '../utils'
import { round, formatMm } from 'shared/utils'
import RawSpan from 'shared/components/raw-span'
import { TextOnPath } from './text.mjs'
import { getProps } from './utils.mjs'
import { round, formatMm } from 'shared/utils.mjs'
import { RawSpan } from 'shared/components/raw-span.mjs'
export const pointCoords = (point) =>
point ? `[ ${round(point.x, 2)}, ${round(point.y, 2)} ]` : null
@ -620,7 +620,7 @@ const XrayPath = (props) => {
)
}
const Path = (props) => {
export const Path = (props) => {
const { path, partName, pathName } = props
if (path.hidden) return null
const output = []
@ -641,5 +641,3 @@ const Path = (props) => {
return output
}
export default Path

View file

@ -1,7 +1,7 @@
import Text from '../text'
import Circle from '../circle'
import { Tr, KeyTd, ValTd, Attributes, pointCoords } from '../path/index'
import { withinPartBounds } from '../utils.js'
import { Text } from './text.mjs'
import { Circle } from './circle.mjs'
import { Tr, KeyTd, ValTd, Attributes, pointCoords } from './path.mjs'
import { withinPartBounds } from './utils.mjs'
const RevealPoint = (props) => {
const r = 15 * props.gist.scale
@ -82,7 +82,7 @@ const XrayPoint = (props) => (
</g>
)
const Point = (props) => {
export const Point = (props) => {
const { point, pointName, partName, gist, part } = props
// Don't include parts outside the part bounding box
if (!withinPartBounds(point, part)) return null
@ -103,5 +103,3 @@ const Point = (props) => {
return output.length < 1 ? null : output
}
export default Point

View file

@ -1,9 +1,8 @@
import React from 'react'
import { getProps } from '../utils'
import { Tr, KeyTd, ValTd, Attributes, pointCoords } from '../path/index'
import { getProps } from './utils.mjs'
import { Tr, KeyTd, ValTd, Attributes, pointCoords } from './path.mjs'
const snippetInfo = (props) => {
return (
<div className="p-4 border bg-neutral bg-opacity-40 shadow rounded-lg">
<h5 className="text-neutral-content text-center pb-4">Snippet info</h5>
@ -27,30 +26,30 @@ const snippetInfo = (props) => {
</Tr>
<Tr>
<KeyTd>Attributes</KeyTd>
<ValTd><Attributes list={props.snippet.attributes.list}/></ValTd>
<ValTd>
<Attributes list={props.snippet.attributes.list} />
</ValTd>
</Tr>
</tbody>
</table>
<div className="flex flex-col flex-wrap gap-2 mt-4">
<button
className="btn btn-success"
onClick={() => console.log(props.snippet)}
>console.log(snippet)</button>
<button
className="btn btn-success"
onClick={() => console.table(props.snippet)}
>console.table(snippet)</button>
<button className="btn btn-success" onClick={() => console.log(props.snippet)}>
console.log(snippet)
</button>
<button className="btn btn-success" onClick={() => console.table(props.snippet)}>
console.table(snippet)
</button>
</div>
</div>
)
}
const Snippet = (props) => {
export const Snippet = (props) => {
if (!props.snippet?.anchor) return null
const snippetProps = {
xlinkHref: '#' + props.snippet.def,
x: props.snippet.anchor.x,
y: props.snippet.anchor.y
y: props.snippet.anchor.y,
}
let scale = props.snippet.attributes.get('data-scale')
let rotate = props.snippet.attributes.get('data-rotate')
@ -66,13 +65,16 @@ const Snippet = (props) => {
}
}
return <use
return (
<use
{...snippetProps}
{...getProps(props.snippet)}
color="currentColor"
className="hover:cursor-pointer hover:fill-primary hover:text-secondary"
onClick={(evt) => { evt.stopPropagation(); props.showInfo(snippetInfo(props)) }}
onClick={(evt) => {
evt.stopPropagation()
props.showInfo(snippetInfo(props))
}}
/>
)
}
export default Snippet

View file

@ -1,7 +1,7 @@
import Part from './part'
import { getProps } from './utils'
import { Part } from './part.mjs'
import { getProps } from './utils.mjs'
const Stack = (props) => {
export const Stack = (props) => {
const { stack, gist, updateGist, unsetGist, showInfo } = props
return (
@ -17,5 +17,3 @@ const Stack = (props) => {
</g>
)
}
export default Stack

View file

@ -1,9 +1,49 @@
import { forwardRef } from 'react'
import { SizeMe } from 'react-sizeme'
import { TransformWrapper, TransformComponent } from 'react-zoom-pan-pinch'
import Svg from './svg'
import Defs from './defs'
import Stack from './stack'
import { forwardRef } from 'react'
import { Defs } from './defs.mjs'
import { Stack } from './stack.mjs'
export const Svg = forwardRef(
(
{
embed = true,
develop = false,
locale = 'en',
className = 'freesewing pattern',
style = {},
viewBox = false,
width,
height,
children,
},
ref
) => {
if (width < 1) width = 1000
if (height < 1) height = 1000
let attributes = {
xmlns: 'http://www.w3.org/2000/svg',
'xmlns:svg': 'http://www.w3.org/2000/svg',
xmlnsXlink: 'http://www.w3.org/1999/xlink',
xmlLang: locale,
viewBox: viewBox || `0 0 ${width} ${height}`,
className,
style,
}
if (!embed) {
attributes.width = width + 'mm'
attributes.height = height + 'mm'
}
if (develop) attributes.className += ' develop'
return (
<svg {...attributes} ref={ref}>
{children}
</svg>
)
}
)
/* What's with all the wrapping?
*
@ -26,7 +66,7 @@ import { forwardRef } from 'react'
* Also still to see how this will work with SSR
*/
const SvgWrapper = forwardRef((props, ref) => {
export const SvgWrapper = forwardRef((props, ref) => {
const { patternProps = false, gist, updateGist, unsetGist, showInfo, viewBox } = props
if (!patternProps) return null
@ -65,7 +105,3 @@ const SvgWrapper = forwardRef((props, ref) => {
</SizeMe>
)
})
SvgWrapper.displayName = 'SvgWrapper'
export default SvgWrapper

View file

@ -1,35 +0,0 @@
import {forwardRef} from 'react'
const Svg = forwardRef(({
embed = true,
develop = false,
locale = 'en',
className = 'freesewing pattern',
style = {},
viewBox = false,
width,
height,
children
}, ref) => {
if (width < 1) width = 1000
if (height < 1) height = 1000
let attributes = {
xmlns: 'http://www.w3.org/2000/svg',
'xmlns:svg': 'http://www.w3.org/2000/svg',
xmlnsXlink: 'http://www.w3.org/1999/xlink',
xmlLang: locale,
viewBox: viewBox || `0 0 ${width} ${height}`,
className,
style
}
if (!embed) {
attributes.width = width + 'mm'
attributes.height = height + 'mm'
}
if (develop) attributes.className += ' develop'
return <svg {...attributes} ref={ref}>{children}</svg>
})
export default Svg

View file

@ -1,45 +0,0 @@
import { useTranslation } from 'next-i18next'
import { pathInfo } from '../path/index'
const XrayTextOnPath = props => (
<tspan
{...props.attr}
dangerouslySetInnerHTML={{__html: props.translated}}
className={`${props.attr.className} no-fill stroke-transparent stroke-4xl opacity-10 hover:cursor-pointer hover:stroke-secondary`}
style={{strokeOpacity: 0.25}}
onClick={(evt) => { evt.stopPropagation(); props.showInfo(pathInfo(props)) }}
/>
)
const TextOnPath = (props) => {
const { t } = useTranslation(['app'])
// Handle translation (and spaces)
let translated = ''
for (let string of props.path.attributes.getAsArray('data-text')) {
translated += t(string).replace(/&quot;/g, '"') + ' '
}
const textPathProps = {
xlinkHref: '#' + props.pathId,
startOffset: '0%'
}
const align = props.path.attributes.get('data-text-class')
if (align && align.indexOf('center') > -1) textPathProps.startOffset = '50%'
else if (align && align.indexOf('right') > -1) textPathProps.startOffset = '100%'
const attr = props.path.attributes.asPropsIfPrefixIs('data-text-')
return (
<text>
<textPath {...textPathProps}>
<tspan {...attr} dangerouslySetInnerHTML={{__html: translated}} />
</textPath>
{props.gist._state?.xray?.enabled && (
<textPath {...textPathProps}>
<XrayTextOnPath {...props} attr={attr} translated={translated}/>
</textPath>
)}
</text>
)
}
export default TextOnPath

View file

@ -0,0 +1,148 @@
import { useTranslation } from 'next-i18next'
import { Tr, KeyTd, ValTd, Attributes, pointCoords, pathInfo } from './path.mjs'
const textInfo = (props) =>
props.point ? (
<div className="p-4 border bg-neutral bg-opacity-60 shadow rounded-lg">
<h5 className="text-neutral-content text-center pb-4">Text info</h5>
<table className="border-collapse h-fit">
<tbody>
<Tr>
<KeyTd>Coordinates</KeyTd>
<ValTd>{pointCoords(props.point)}</ValTd>
</Tr>
<Tr>
<KeyTd>Name</KeyTd>
<ValTd>{props.pointName}</ValTd>
</Tr>
<Tr>
<KeyTd>Part</KeyTd>
<ValTd>{props.partName}</ValTd>
</Tr>
<Tr>
<KeyTd>Attributes</KeyTd>
<ValTd>
<Attributes list={props.point.attributes.list} />
</ValTd>
</Tr>
</tbody>
</table>
<div className="flex flex-col flex-wrap gap-2 mt-4">
<button className="btn btn-success" onClick={() => console.log(props.point)}>
console.log(point)
</button>
<button className="btn btn-success" onClick={() => console.table(props.point)}>
console.table(point)
</button>
</div>
</div>
) : null
const XrayText = (props) => (
<text x={props.point.x} y={props.point.y} {...props.attr}>
<TextSpans
{...props}
className="stroke-transparent stroke-4xl opacity-10 hover:cursor-pointer hover:stroke-secondary"
style={{ strokeOpacity: 0.25 }}
onClick={(evt) => {
evt.stopPropagation()
props.showInfo(textInfo(props))
}}
/>
</text>
)
const TextSpans = ({ point, className = '', style = {}, onClick = null }) => {
const { t } = useTranslation(['app'])
let text = []
// Handle translation
let translated = ''
for (const string of point.attributes.getAsArray('data-text')) {
if (string) translated += t(string.toString()).replace(/&quot;/g, '"') + ' '
}
// Handle muti-line text
if (translated.indexOf('\n') !== -1) {
let key = 0
let lines = translated.split('\n')
text.push(<tspan key={'tspan-' + key}>{lines.shift()}</tspan>)
for (let line of lines) {
key++
text.push(
<tspan
key={'tspan-' + key}
x={point.x}
dy={point.attributes.get('data-text-lineheight') || 12}
className={className}
style={style}
onClick={onClick}
>
{line.toString().replace(/&quot;/g, '"')}
</tspan>
)
}
} else
text.push(
<tspan key="tspan-1" className={className} style={style} onClick={onClick}>
{translated}
</tspan>
)
return text
}
export const Text = (props) => {
const attr = props.point.attributes.asPropsIfPrefixIs('data-text-')
return (
<>
<text x={props.point.x} y={props.point.y} {...attr}>
<TextSpans {...props} />
</text>
{props.gist._state?.xray?.enabled && <XrayText {...props} attr={attr} />}
</>
)
}
const XrayTextOnPath = (props) => (
<tspan
{...props.attr}
dangerouslySetInnerHTML={{ __html: props.translated }}
className={`${props.attr.className} no-fill stroke-transparent stroke-4xl opacity-10 hover:cursor-pointer hover:stroke-secondary`}
style={{ strokeOpacity: 0.25 }}
onClick={(evt) => {
evt.stopPropagation()
props.showInfo(pathInfo(props))
}}
/>
)
export const TextOnPath = (props) => {
const { t } = useTranslation(['app'])
// Handle translation (and spaces)
let translated = ''
for (let string of props.path.attributes.getAsArray('data-text')) {
translated += t(string).replace(/&quot;/g, '"') + ' '
}
const textPathProps = {
xlinkHref: '#' + props.pathId,
startOffset: '0%',
}
const align = props.path.attributes.get('data-text-class')
if (align && align.indexOf('center') > -1) textPathProps.startOffset = '50%'
else if (align && align.indexOf('right') > -1) textPathProps.startOffset = '100%'
const attr = props.path.attributes.asPropsIfPrefixIs('data-text-')
return (
<text>
<textPath {...textPathProps}>
<tspan {...attr} dangerouslySetInnerHTML={{ __html: translated }} />
</textPath>
{props.gist._state?.xray?.enabled && (
<textPath {...textPathProps}>
<XrayTextOnPath {...props} attr={attr} translated={translated} />
</textPath>
)}
</text>
)
}

View file

@ -1,109 +0,0 @@
import { useTranslation } from 'next-i18next'
import { Tr, KeyTd, ValTd, Attributes, pointCoords } from '../path/index'
const textInfo = (props) => props.point
? (
<div className="p-4 border bg-neutral bg-opacity-60 shadow rounded-lg">
<h5 className="text-neutral-content text-center pb-4">Text info</h5>
<table className="border-collapse h-fit">
<tbody>
<Tr>
<KeyTd>Coordinates</KeyTd>
<ValTd>{pointCoords(props.point)}</ValTd>
</Tr>
<Tr>
<KeyTd>Name</KeyTd>
<ValTd>{props.pointName}</ValTd>
</Tr>
<Tr>
<KeyTd>Part</KeyTd>
<ValTd>{props.partName}</ValTd>
</Tr>
<Tr>
<KeyTd>Attributes</KeyTd>
<ValTd><Attributes list={props.point.attributes.list} /></ValTd>
</Tr>
</tbody>
</table>
<div className="flex flex-col flex-wrap gap-2 mt-4">
<button
className="btn btn-success"
onClick={() => console.log(props.point)}
>console.log(point)</button>
<button
className="btn btn-success"
onClick={() => console.table(props.point)}
>console.table(point)</button>
</div>
</div>
) : null
const XrayText = props => (
<text
x={props.point.x}
y={props.point.y}
{...props.attr}
>
<TextSpans {...props}
className="stroke-transparent stroke-4xl opacity-10 hover:cursor-pointer hover:stroke-secondary"
style={{strokeOpacity: 0.25}}
onClick={(evt) => { evt.stopPropagation(); props.showInfo(textInfo(props)) }}
/>
</text>
)
const TextSpans = ({ point, className='', style={}, onClick=null }) => {
const { t } = useTranslation(['app'])
let text = []
// Handle translation
let translated = ''
for (const string of point.attributes.getAsArray('data-text')) {
if (string) translated += t(string.toString()).replace(/&quot;/g, '"') + ' '
}
// Handle muti-line text
if (translated.indexOf('\n') !== -1) {
let key = 0
let lines = translated.split('\n')
text.push(<tspan key={'tspan-' + key}>{lines.shift()}</tspan>)
for (let line of lines) {
key++
text.push(
<tspan
key={'tspan-' + key}
x={point.x}
dy={point.attributes.get('data-text-lineheight') || 12}
className={className} style={style}
onClick={onClick}
>
{line.toString().replace(/&quot;/g, '"')}
</tspan>
)
}
} else text.push(<tspan key="tspan-1"
className={className}
style={style}
onClick={onClick}
>{translated}</tspan>)
return text
}
const Text = (props) => {
const attr = props.point.attributes.asPropsIfPrefixIs('data-text-')
return (
<>
<text
x={props.point.x}
y={props.point.y}
{...attr}
>
<TextSpans {...props}/>
</text>
{props.gist._state?.xray?.enabled && <XrayText {...props} attr={attr}/>}
</>
)
}
export default Text

View file

@ -1,8 +1,8 @@
import Worker from 'web-worker'
import fileSaver from 'file-saver'
import { themePlugin } from '@freesewing/plugin-theme'
import { pagesPlugin } from '../layout/print/plugin'
import { capitalize } from 'shared/utils'
import { pagesPlugin } from '../layout/print/plugin.mjs'
import { capitalize } from 'shared/utils.mjs'
export const exportTypes = {
exportForPrinting: ['a4', 'a3', 'a2', 'a1', 'a0', 'letter', 'tabloid'],

View file

@ -3,7 +3,7 @@
* */
import yaml from 'js-yaml'
import axios from 'axios'
import PdfMaker from './pdf-maker'
import { PdfMaker } from './pdf-maker'
/** when the worker receives data from the page, do the appropriate export */
addEventListener('message', async (e) => {
@ -32,7 +32,7 @@ const postSuccess = (blob) => {
/** export a blob */
const exportBlob = (blobContent, type) => {
const blob = new Blob([blobContent], {
type: `${type};charset=utf-8`
type: `${type};charset=utf-8`,
})
postSuccess(blob)
}
@ -50,14 +50,15 @@ const exportPdf = async (data) => {
}
const exportGithubGist = (data) => {
axios.post('https://backend.freesewing.org/github/gist', {
axios
.post('https://backend.freesewing.org/github/gist', {
design: data.design,
data: yaml.dump(data)
data: yaml.dump(data),
})
.then(res => {
.then((res) => {
postMessage({
success: true,
link: 'https://gist.github.com/' + res.data.id
link: 'https://gist.github.com/' + res.data.id,
})
})
}

View file

@ -1,10 +1,10 @@
import { useState } from 'react'
import { useTranslation } from 'next-i18next'
import Popout from 'shared/components/popout'
import WebLink from 'shared/components/web-link'
import { exportTypes, handleExport } from './export-handler'
import { Popout } from 'shared/components/popout.mjs'
import { WebLink } from 'shared/components/web-link.mjs'
import { exportTypes, handleExport } from './export-handler.mjs'
const ExportDraft = ({ gist, design, app }) => {
export const ExportDraft = ({ gist, design, app }) => {
const [link, setLink] = useState(false)
const [error, setError] = useState(false)
const [format, setFormat] = useState(false)
@ -64,5 +64,3 @@ const ExportDraft = ({ gist, design, app }) => {
</div>
)
}
export default ExportDraft

View file

@ -1,6 +1,6 @@
import PDFDocument from 'pdfkit/js/pdfkit.standalone'
import SVGtoPDF from 'svg-to-pdfkit'
import { logoPath } from 'shared/components/icons.mjs'
import { logoPath } from 'shared/logos/freesewing.mjs'
/** an svg of the logo to put on the cover page */
const logoSvg = `<svg viewBox="0 0 25 25">
@ -18,7 +18,7 @@ const mmToPoints = 2.834645669291339
* Freesewing's first explicit class?
* handles pdf exporting
*/
export default class PdfMaker {
export class PdfMaker {
/** the svg as text to embed in the pdf */
svg
/** the document configuration */

View file

@ -1,9 +0,0 @@
import Json from 'shared/components/json-highlight.js'
const GistAsJson = props => (
<div className="max-w-screen-xl m-auto">
<Json>{JSON.stringify(props.gist, null, 2)}</Json>
</div>
)
export default GistAsJson

View file

@ -0,0 +1,58 @@
import yaml from 'js-yaml'
import axios from 'axios'
import { Json } from 'shared/components/json.mjs'
import { Yaml } from 'shared/components/yaml.mjs'
export const GistAsJson = (props) => (
<div className="max-w-screen-xl m-auto">
<Json>{JSON.stringify(props.gist, null, 2)}</Json>
</div>
)
export const GistAsYaml = (props) => (
<div className="max-w-screen-xl m-auto">
<Yaml json={JSON.stringify(props.gist, null, 2)} />
</div>
)
export const defaultGist = {
sa: 0,
saBool: false,
saMm: 10,
scale: 1,
complete: true,
paperless: false,
units: 'metric',
locale: 'en',
margin: 2,
renderer: 'react',
embed: true,
debug: true,
}
export const preloadGist = {
github: async (id, design) => {
let result
try {
result = await axios.get(`https://api.github.com/gists/${id}`)
} catch (err) {
console.log(err)
return [false, 'An unexpected error occured']
}
if (result.data.files['pattern.yaml'].content) {
let g = yaml.load(result.data.files['pattern.yaml'].content)
if (g.design !== undefined && g.design !== design.designConfig.data.name)
return [
false,
`You tried loading a configuration for ${g.design} into a ${design.designConfig.data.name} development environment`,
]
return g
}
// TODO notify people of these errors
else return [false, 'This gist does not seem to be a valid pattern configuration']
},
}

View file

@ -23,7 +23,7 @@ const EditCount = (props) => (
</div>
)
const DesignOptionCount = (props) => {
export const DesignOptionCount = (props) => {
const { t } = useTranslation(['app'])
const { count, max, min } = props.design.patternConfig.options[props.option]
const val =
@ -104,5 +104,3 @@ const DesignOptionCount = (props) => {
</div>
)
}
export default DesignOptionCount

View file

@ -2,7 +2,7 @@ import { useState } from 'react'
import { ClearIcon } from 'shared/components/icons.mjs'
import { useTranslation } from 'next-i18next'
const DesignOptionList = (props) => {
export const DesignOptionList = (props) => {
const { t } = useTranslation([`o_${props.design.designConfig.data.name}`])
const { dflt, list, doNotTranslate = false } = props.design.patternConfig.options[props.option]
const val =

View file

@ -33,7 +33,7 @@ const EditOption = (props) => (
</div>
)
const DesignOptionPctDeg = (props) => {
export const DesignOptionPctDeg = (props) => {
const { t } = useTranslation(['app'])
const suffix = props.type === 'deg' ? '°' : '%'
const factor = props.type === 'deg' ? 1 : 100
@ -134,5 +134,3 @@ const DesignOptionPctDeg = (props) => {
</div>
)
}
export default DesignOptionPctDeg

View file

@ -1,7 +1,7 @@
import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react'
import { useTranslation } from 'next-i18next'
import { isDegreeMeasurement } from '../../../config/measurements'
import { measurementAsMm } from 'shared/utils'
import { measurementAsMm } from 'shared/utils.mjs'
/*
* This is a single input for a measurements
@ -11,60 +11,65 @@ import { measurementAsMm } from 'shared/utils'
* m holds the measurement name. It's just so long to type
* measurement and I always have some typo in it because dyslexia.
*/
const MeasurementInput = ({ m, gist, app, updateMeasurements, focus }) => {
export const MeasurementInput = ({ m, gist, app, updateMeasurements, focus }) => {
const { t } = useTranslation(['app', 'measurements'])
const prefix = (app.site === 'org') ? '' : 'https://freesewing.org'
const prefix = app.site === 'org' ? '' : 'https://freesewing.org'
const title = t(`measurements:${m}`)
const isDegree = isDegreeMeasurement(m);
const factor = useMemo(() => (isDegree ? 1 : (gist.units == 'imperial' ? 25.4 : 10)), [gist.units])
const isDegree = isDegreeMeasurement(m)
const factor = useMemo(() => (isDegree ? 1 : gist.units == 'imperial' ? 25.4 : 10), [gist.units])
const isValValid = val => (typeof val === 'undefined' || val === '')
? null
: val != false && !isNaN(val)
const isValid = (newVal) => (typeof newVal === 'undefined')
? isValValid(val)
: isValValid(newVal)
const isValValid = (val) =>
typeof val === 'undefined' || val === '' ? null : val != false && !isNaN(val)
const isValid = (newVal) => (typeof newVal === 'undefined' ? isValValid(val) : isValValid(newVal))
const [val, setVal] = useState(gist.measurements?.[m] / factor || '')
// keep a single reference to a debounce timer
const debounceTimeout = useRef(null);
const input = useRef(null);
const debounceTimeout = useRef(null)
const input = useRef(null)
// onChange
const update = useCallback((evt) => {
evt.stopPropagation();
let evtVal = evt.target.value;
const update = useCallback(
(evt) => {
evt.stopPropagation()
let evtVal = evt.target.value
// set Val immediately so that the input reflects it
setVal(evtVal)
let useVal = isDegree ? evtVal : measurementAsMm(evtVal, gist.units);
let useVal = isDegree ? evtVal : measurementAsMm(evtVal, gist.units)
const ok = isValid(useVal)
// only set to the gist if it's valid
if (ok) {
// debounce in case it's still changing
if (debounceTimeout.current !== null) { clearTimeout(debounceTimeout.current); }
if (debounceTimeout.current !== null) {
clearTimeout(debounceTimeout.current)
}
debounceTimeout.current = setTimeout(() => {
// clear the timeout reference
debounceTimeout.current = null;
debounceTimeout.current = null
updateMeasurements(useVal, m)
}, 500);
}, 500)
}
}, [gist.units])
},
[gist.units]
)
// use this for better update efficiency
// FIXME: This breaks gist updates.
// See: https://github.com/freesewing/freesewing/issues/2281
const memoVal = useMemo(() => gist.measurements?.[m], [gist])
// track validity against the value and the units
const valid = useMemo(() => isValid(isDegree ? val : measurementAsMm(val, gist.units)), [val, gist.units])
const valid = useMemo(
() => isValid(isDegree ? val : measurementAsMm(val, gist.units)),
[val, gist.units]
)
// hook to update the value or format when the gist changes
useEffect(() => {
// set the value to the proper value and format
if (memoVal) {
let gistVal = +(memoVal / factor).toFixed(2);
let gistVal = +(memoVal / factor).toFixed(2)
setVal(gistVal)
}
}, [memoVal, factor])
@ -72,7 +77,7 @@ const MeasurementInput = ({ m, gist, app, updateMeasurements, focus }) => {
// focus when prompted by parent
useEffect(() => {
if (focus) {
input.current.focus();
input.current.focus()
}
}, [focus])
@ -110,25 +115,27 @@ const MeasurementInput = ({ m, gist, app, updateMeasurements, focus }) => {
value={val}
onChange={update}
/>
<span role="img" className={`bg-transparent border-y
<span
role="img"
className={`bg-transparent border-y
${valid === false && 'border-error text-neutral-content'}
${valid === true && 'border-success text-neutral-content'}
${valid === null && 'border-base-200 text-base-content'}
`}>
{(valid === true) && '👍'}
{(valid === false) && '🤔'}
`}
>
{valid === true && '👍'}
{valid === false && '🤔'}
</span>
<span className={`
<span
className={`
${valid === false && 'bg-error text-neutral-content'}
${valid === true && 'bg-success text-neutral-content'}
${valid === null && 'bg-base-200 text-base-content'}
`}>
`}
>
{isDegree ? '° ' : gist.units == 'metric' ? 'cm' : 'in'}
</span>
</label>
</div>
)
}
export default MeasurementInput

View file

@ -1,23 +1,15 @@
import { useTranslation } from 'next-i18next'
import Settings from './settings'
import { CutLayoutSettings } from './settings.mjs'
const CutLayout = props => {
export const CutLayout = (props) => {
const { t } = useTranslation(['workbench'])
let name = props.design.designConfig.data.name
name = name.replace('@freesewing/', '')
return (
<div>
<h2 className="capitalize">
{
t('layoutThing', { thing: name })
+ ': '
+ t('forCutting')
}
</h2>
<h2 className="capitalize">{t('layoutThing', { thing: name }) + ': ' + t('forCutting')}</h2>
<Settings {...props} />
</div>
)
}
export default CutLayout

View file

@ -1,10 +1,7 @@
const CutLayoutSettings = props => {
export const CutLayoutSettings = (props) => {
return (
<div>
<p>Fixme: Cut layout settings here</p>
</div>
)
}
export default CutLayoutSettings

View file

@ -0,0 +1,222 @@
// Hooks
import { useState, Fragment } from 'react'
import { useRouter } from 'next/router'
// Depenencies
import get from 'lodash.get'
// Components
import Link from 'next/link'
import { FreeSewingLogo } from 'shared/components/logos/freesewing.mjs'
import { PrimaryNavigation } from 'shared/components/navigation/primary.mjs'
import { RightIcon, LeftIcon } from 'shared/components/icons.mjs'
import { Header } from 'site/components/header.mjs'
import { Footer } from 'site/components/footer.mjs'
import { Search } from 'site/components/search.mjs'
export const PageTitle = ({ app, slug, title }) => {
if (title) return <h1>{title}</h1>
if (slug) return <h1>{get(app.navigation, slug.split('/')).__title}</h1>
return <h1>FIXME: This page has no title</h1>
}
const Breadcrumbs = ({ app, slug = false, title }) => {
if (!slug) return null
const crumbs = []
const chunks = slug.split('/')
for (const i in chunks) {
const j = parseInt(i) + parseInt(1)
const page = get(app.navigation, chunks.slice(0, j))
if (page) crumbs.push([page.__linktitle, '/' + chunks.slice(0, j).join('/'), j < chunks.length])
}
return (
<ul className="flex flex-row flex-wrap gap-2 font-bold">
<li>
<Link href="/" title="To the homepage" className="text-base-content">
<Logo size={24} fill="currentColor" stroke={false} />
</Link>
</li>
{crumbs.map((crumb) => (
<Fragment key={crumb[1]}>
<li className="text-base-content">&raquo;</li>
<li>
{crumb[2] ? (
<Link
href={crumb[1]}
title={crumb[0]}
className="text-secondary hover:text-secondary-focus"
>
{crumb[0]}
</Link>
) : (
<span className="text-base-content">{crumb[0]}</span>
)}
</li>
</Fragment>
))}
</ul>
)
}
const asideClasses = `
fixed top-0 right-0
pt-28
sm:pt-8 sm:mt-16
pb-4 px-2
sm:relative sm:transform-none
h-screen w-screen
bg-base-100
sm:bg-base-50
sm:flex
sm:sticky
overflow-y-scroll
z-20
bg-base-100 text-base-content
transition-all
xl:w-1/4
`
export const DefaultLayout = ({
app,
title = false,
children = [],
search,
setSearch,
noSearch = false,
workbench = false,
AltMenu = null,
}) => {
const startNavigation = () => {
app.startLoading()
// Force close of menu on mobile if it is open
if (app.primaryNavigation) app.setPrimaryNavigation(false)
// Force close of search modal if it is open
if (search) setSearch(false)
}
const router = useRouter()
router.events?.on('routeChangeStart', startNavigation)
router.events?.on('routeChangeComplete', () => app.stopLoading())
const slug = router.asPath.slice(1)
const [collapsePrimaryNav, setCollapsePrimaryNav] = useState(workbench || false)
const [collapseAltMenu, setCollapseAltMenu] = useState(false)
return (
<div
className={`
flex flex-col justify-between
min-h-screen
bg-base-100
`}
>
<Header app={app} setSearch={setSearch} />
<main className="grow bg-base-100">
<div className="m-auto flex flex-row justify-center">
<aside
className={`
${asideClasses}
${app.primaryMenu ? '' : 'translate-x-[-100%]'} transition-transform
sm:flex-row-reverse
${workbench && collapsePrimaryNav ? 'sm:px-0 sm:w-16' : 'sm:px-1 md:px-4 lg:px-8'}
w-96
`}
>
{workbench && (
<div className={`hidden sm:flex`}>
<button
className="text-secondary-focus h-full px-2 pl-4 hover:animate-pulse"
onClick={() => setCollapsePrimaryNav(!collapsePrimaryNav)}
>
{collapsePrimaryNav ? (
<>
<RightIcon />
<RightIcon />
<RightIcon />
</>
) : (
<>
<LeftIcon />
<LeftIcon />
<LeftIcon />
</>
)}
</button>
</div>
)}
<PrimaryNavigation app={app} active={slug} />
</aside>
<div className="p-0 m-0 bg-base-100">
<section
className={`
p-4 pt-24 sm:pt-28
sm:px-1 md:px-4 lg:px-8
${workbench && collapsePrimaryNav ? '' : 'max-w-7xl'}
`}
>
<div>
{title && (
<>
<Breadcrumbs app={app} slug={slug} title={title} />
<PageTitle app={app} slug={slug} title={title} />
</>
)}
{children}
</div>
</section>
</div>
</div>
{workbench && AltMenu && (
<aside
className={`
${asideClasses}
${app.primaryMenu ? '' : 'translate-x-[-100%]'} transition-transform
sm:flex-row
${collapseAltMenu ? 'sm:px-0 sm:w-16' : 'sm:px-1 md:px-4 lg:px-8 sm:w-[38.2%]'}
`}
>
<div className={`hidden sm:flex`}>
<button
className="text-secondary-focus h-full px-2 pr-4 hover:animate-pulse"
onClick={() => setCollapseAltMenu(!collapseAltMenu)}
>
{collapseAltMenu ? (
<>
<Left />
<Left />
<Left />
</>
) : (
<>
<Right />
<Right />
<Right />
</>
)}
</button>
</div>
{AltMenu}
</aside>
)}
</main>
{!noSearch && search && (
<>
<div
className={`
fixed w-full max-h-screen bg-base-100 top-0 z-30 pt-0 pb-16 px-8
sm:rounded-lg sm:top-24
sm:max-w-xl sm:m-auto sm:inset-x-12
md:max-w-2xl
lg:max-w-4xl
`}
>
<Search app={app} search={search} setSearch={setSearch} />
</div>
<div className="fixed top-0 left-0 w-full min-h-screen bg-neutral z-20 bg-opacity-70"></div>
</>
)}
<Footer app={app} />
</div>
)
}

View file

@ -1,66 +0,0 @@
import {FlipIconInner} from 'shared/components/icons/flip'
import {RotateIconInner} from 'shared/components/icons/rotate'
import {ClearIconInner} from 'shared/components/icons/clear'
import { useTranslation } from 'next-i18next'
const rectSize = 24
const Button = ({onClickCb, transform, Icon, children}) => {
const _onClick = (event) => {
event.stopPropagation();
onClickCb(event);
}
return <g className="svg-layout-button group" transform={transform}>
<rect width={rectSize} height={rectSize} className="button"/>
<Icon />
<text className="invisible group-hover:visible text-xl">{children}</text>
<rect width={rectSize} height={rectSize} onClick={_onClick} className="fill-transparent"/>
</g>}
/** buttons for manipulating the part */
const Buttons = ({ transform, flip, rotate, setRotate, resetPart, rotate90}) => {
const {t} = useTranslation('workbench')
return (
<g transform={transform} >
{rotate
? <circle cx="0" cy="0" r="50" className='stroke-2xl muted' />
: <path d="M -50, 0 l 100,0 M 0,-50 l 0,100" className="stroke-2xl muted" />
}
<Button
onClickCb={resetPart}
transform={`translate(${rectSize/-2}, ${rectSize/-2})`}
Icon={ClearIconInner}>
{t('toolbar.resetPart')}
</Button>
<Button
onClickCb={() => rotate90(-1)}
transform={`translate(${rectSize* -2.7}, ${rectSize/-2})`}
Icon={RotateIconInner}
>
{t('toolbar.rotateCCW')}
</Button>
<Button
onClickCb={() => flip('y')}
transform={`translate(${rectSize* 0.6}, ${rectSize/-2})`}
Icon={() => <FlipIconInner rotate="270" />}
>
{t('toolbar.flipY')}
</Button>
<Button
onClickCb={() => flip('x')}
transform={`translate(${rectSize* -1.6}, ${rectSize/-2})`}
Icon={FlipIconInner}>
{t('toolbar.flipX')}
</Button>
<Button
onClickCb={() => rotate90()}
transform={`translate(${rectSize* 1.7}, ${rectSize/-2})`}
Icon={() => <RotateIconInner flipX={true}/>}>
{t('toolbar.rotateCW')}
</Button>
</g>
)
}
export default Buttons

View file

@ -0,0 +1,98 @@
import { useTranslation } from 'next-i18next'
const Triangle = ({ transform = 'translate(0,0)', fill = 'currentColor' }) => (
<path
strokeLinecap="round"
strokeLinejoin="round"
transform={transform}
style={{ fill }}
transform-origin="12 12"
d="M1 12m9 3m-6 4h2c3 0 3 -3 3-3L9 3c-0-1.732 -2.25-2.6125 -3.325 -.77L2 16c-.77 1.333.192 3 1.732 3z"
/>
)
const FlipIconInner = ({ x = 0, y = 0, rotate = 0, ...style }) => (
<g transform={`translate(${x},${y}) rotate(${rotate})`} transform-origin="12 12" style={style}>
<Triangle fill="none" transform="translate(0, 2.5)" />
<path strokeLinecap="round" strokeLinejoin="round" transform="translate(12, 2)" d="M0 0L0 20" />
<Line />
<Triangle transform="scale(-1,1) translate(0,2.5)" />
</g>
)
const RotateIconInner = ({ flipX = false }) => (
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"
transform={flipX ? 'scale(-1,1)' : ''}
transform-origin="12 12"
/>
)
const rectSize = 24
const Button = ({ onClickCb, transform, Icon, children }) => {
const _onClick = (event) => {
event.stopPropagation()
onClickCb(event)
}
return (
<g className="svg-layout-button group" transform={transform}>
<rect width={rectSize} height={rectSize} className="button" />
<Icon />
<text className="invisible group-hover:visible text-xl">{children}</text>
<rect width={rectSize} height={rectSize} onClick={_onClick} className="fill-transparent" />
</g>
)
}
/** buttons for manipulating the part */
export const Buttons = ({ transform, flip, rotate, setRotate, resetPart, rotate90 }) => {
const { t } = useTranslation('workbench')
return (
<g transform={transform}>
{rotate ? (
<circle cx="0" cy="0" r="50" className="stroke-2xl muted" />
) : (
<path d="M -50, 0 l 100,0 M 0,-50 l 0,100" className="stroke-2xl muted" />
)}
<Button
onClickCb={resetPart}
transform={`translate(${rectSize / -2}, ${rectSize / -2})`}
Icon={ClearIconInner}
>
{t('toolbar.resetPart')}
</Button>
<Button
onClickCb={() => rotate90(-1)}
transform={`translate(${rectSize * -2.7}, ${rectSize / -2})`}
Icon={RotateIconInner}
>
{t('toolbar.rotateCCW')}
</Button>
<Button
onClickCb={() => flip('y')}
transform={`translate(${rectSize * 0.6}, ${rectSize / -2})`}
Icon={() => <FlipIconInner rotate="270" />}
>
{t('toolbar.flipY')}
</Button>
<Button
onClickCb={() => flip('x')}
transform={`translate(${rectSize * -1.6}, ${rectSize / -2})`}
Icon={FlipIconInner}
>
{t('toolbar.flipX')}
</Button>
<Button
onClickCb={() => rotate90()}
transform={`translate(${rectSize * 1.7}, ${rectSize / -2})`}
Icon={() => <RotateIconInner flipX={true} />}
>
{t('toolbar.rotateCW')}
</Button>
</g>
)
}

View file

@ -1,9 +1,9 @@
import { useRef } from 'react'
import Stack from './stack'
import SvgWrapper from '../../draft/svg-wrapper'
import { PartInner } from '../../draft/part'
import { Stack } from './stack.mjs'
import { SvgWrapper } from '../../draft/svg.mjs'
import { PartInner } from '../../draft/part.mjs'
const Draft = (props) => {
export const Draft = (props) => {
const {
draft,
patternProps,
@ -111,5 +111,3 @@ const Draft = (props) => {
</SvgWrapper>
)
}
export default Draft

View file

@ -43,15 +43,15 @@
* I've sort of left it at this because I'm starting to wonder if we should perhaps re-think
* how custom layouts are supported in the core. And I would like to discuss this with the core team.
*/
import Part from '../../draft/part'
import { useRef, useState, useEffect } from 'react'
import { generateStackTransform } from '@freesewing/core'
import { getProps, angle } from '../../draft/utils'
import { Part } from '../../draft/part.mjs'
import { getProps, angle } from '../../draft/utils.mjs'
import { drag } from 'd3-drag'
import { select } from 'd3-selection'
import { useRef, useState, useEffect } from 'react'
import Buttons from './buttons'
import { Buttons } from './buttons.mjs'
const Stack = (props) => {
export const Stack = (props) => {
const { layout, stack, stackName, gist } = props
const stackLayout = layout.stacks?.[stackName]
@ -265,5 +265,3 @@ const Stack = (props) => {
</g>
)
}
export default Stack

View file

@ -1,15 +1,15 @@
import { useEffect, useState } from 'react'
import { useTranslation } from 'next-i18next'
import Settings from './settings'
import Draft from '../draft/index'
import { pagesPlugin } from './plugin'
import { Settings } from './settings.mjs'
import { Draft } from '../draft/index.mjs'
import { pagesPlugin } from './plugin.mjs'
import {
handleExport,
defaultPdfSettings,
} from 'shared/components/workbench/exporting/export-handler'
import Popout from 'shared/components/popout'
} from 'shared/components/workbench/exporting/export-handler.mjs'
import { Popout } from 'shared/components/popout.mjs'
const PrintLayout = (props) => {
export const PrintLayout = (props) => {
// disable xray
useEffect(() => {
if (props.gist?._state?.xray?.enabled) props.updateGist(['_state', 'xray', 'enabled'], false)
@ -76,5 +76,3 @@ const PrintLayout = (props) => {
</div>
)
}
export default PrintLayout

View file

@ -1,7 +1,7 @@
import { PageIcon } from 'shared/components/icons.mjs'
import { useTranslation } from 'next-i18next'
const PageOrientationPicker = ({ gist, updateGist }) => {
export const PageOrientationPicker = ({ gist, updateGist }) => {
const { t } = useTranslation(['workbench'])
return (
@ -30,5 +30,3 @@ const PageOrientationPicker = ({ gist, updateGist }) => {
</button>
)
}
export default PageOrientationPicker

View file

@ -1,10 +1,10 @@
import { PageSizeIcon } from 'shared/components/icons.mjs'
import { useTranslation } from 'next-i18next'
import Popout from 'shared/components/popout'
import { Popout } from 'shared/components/popout.mjs'
const sizes = ['a4', 'a3', 'a2', 'a1', 'a0', 'letter', 'tabloid']
const PageSizePicker = ({ gist, updateGist }) => {
export const PageSizePicker = ({ gist, updateGist }) => {
const { t } = useTranslation(['workbench'])
const setSize = (size) => {
updateGist(['_state', 'layout', 'forPrinting', 'page', 'size'], size)
@ -56,5 +56,3 @@ const PageSizePicker = ({ gist, updateGist }) => {
</div>
)
}
export default PageSizePicker

View file

@ -1,9 +1,9 @@
import PageSizePicker from './pagesize-picker'
import OrientationPicker from './orientation-picker'
import { PageSizePicker } from './pagesize-picker.mjs'
import { OrientationPicker } from './orientation-picker.mjs'
import { PrintIcon, RightIcon, ClearIcon, ExportIcon } from 'shared/components/icons.mjs'
import { useTranslation } from 'next-i18next'
const PrintLayoutSettings = (props) => {
export const PrintLayoutSettings = (props) => {
const { t } = useTranslation(['workbench'])
let pages = props.draft?.setStores[0].get('pages')
if (!pages) return null
@ -106,5 +106,3 @@ const PrintLayoutSettings = (props) => {
</div>
)
}
export default PrintLayoutSettings

View file

@ -1,9 +1,8 @@
import { useState } from 'react'
import { SecText, SumButton, Li, SumDiv, Deg } from 'shared/components/workbench/menu/index.js'
import { useTranslation } from 'next-i18next'
import { SecText, SumButton, Li, SumDiv, Deg } from 'shared/components/workbench/menu/index.mjs'
const CoreSettingBool = props => {
export const CoreSettingBool = (props) => {
const { t } = useTranslation(['app'])
const [value, setValue] = useState(props.gist[props.setting])
@ -24,5 +23,3 @@ const CoreSettingBool = props => {
</Li>
)
}
export default CoreSettingBool

View file

@ -1,8 +1,8 @@
import { useState } from 'react'
import { Deg } from 'shared/components/workbench/menu/index.js'
import { useTranslation } from 'next-i18next'
import { Deg } from 'shared/components/workbench/menu/index.mjs'
const CoreSettingList = props => {
export const CoreSettingList = (props) => {
const { t } = useTranslation(['settings'])
const { dflt } = props
const val = props.gist?.[props.setting]
@ -29,7 +29,7 @@ const CoreSettingList = props => {
</p>
<div className="flex flex-row">
<div className="grow">
{props.list.map(entry => (
{props.list.map((entry) => (
<button
key={entry.key}
onClick={() => handleChange(entry.key)}
@ -47,5 +47,3 @@ const CoreSettingList = props => {
</div>
)
}
export default CoreSettingList

View file

@ -1,9 +1,9 @@
import { useState } from 'react'
import { formatMm } from 'shared/utils'
import { ClearIcon } from 'shared/components/icons.mjs'
import { useTranslation } from 'next-i18next'
import { formatMm } from 'shared/utils.mjs'
import { ClearIcon } from 'shared/components/icons.mjs'
const CoreSettingMm = (props) => {
export const CoreSettingMm = (props) => {
const { t } = useTranslation(['app', 'settings'])
const { dflt, min, max } = props
const val = props.gist?.[props.setting]
@ -69,5 +69,3 @@ const CoreSettingMm = (props) => {
</div>
)
}
export default CoreSettingMm

View file

@ -2,7 +2,7 @@ import { useState } from 'react'
import { ClearIcon } from 'shared/components/icons.mjs'
import { useTranslation } from 'next-i18next'
const CoreSettingNr = (props) => {
export const CoreSettingNr = (props) => {
const { t } = useTranslation(['app', 'settings'])
const { dflt, min, max } = props
const val = props.gist?.[props.setting]
@ -61,5 +61,3 @@ const CoreSettingNr = (props) => {
</div>
)
}
export default CoreSettingNr

View file

@ -2,7 +2,7 @@ import { ClearIcon } from 'shared/components/icons.mjs'
import orderBy from 'lodash.orderby'
import { useTranslation } from 'next-i18next'
const CoreSettingOnly = (props) => {
export const CoreSettingOnly = (props) => {
const { t } = useTranslation(['app', 'parts', 'settings'])
const list = props.draft.config.draftOrder
const partNames = list.map((part) => ({ id: part, name: t(`parts:${part}`) }))
@ -66,5 +66,3 @@ const CoreSettingOnly = (props) => {
</div>
)
}
export default CoreSettingOnly

View file

@ -1,9 +1,8 @@
import { useState } from 'react'
import { SecText, SumButton, Li, SumDiv, Deg } from 'shared/components/workbench/menu/index.js'
import { useTranslation } from 'next-i18next'
import { SecText, SumButton, Li, SumDiv, Deg } from 'shared/components/workbench/menu/index.mjs'
const CoreSettingSaBool = props => {
export const CoreSettingSaBool = (props) => {
const { t } = useTranslation(['app', 'settings'])
const [value, setValue] = useState(props.gist.saBool || false)
@ -11,7 +10,7 @@ const CoreSettingSaBool = props => {
props.setGist({
...props.gist,
saBool: !value,
sa: value ? 0 : props.gist.saMm
sa: value ? 0 : props.gist.saMm,
})
setValue(!value)
}
@ -28,5 +27,3 @@ const CoreSettingSaBool = props => {
</Li>
)
}
export default CoreSettingSaBool

View file

@ -1,9 +1,9 @@
import { useState } from 'react'
import { formatMm } from 'shared/utils'
import { formatMm } from 'shared/utils.mjs'
import { ClearIcon } from 'shared/components/icons.mjs'
import { useTranslation } from 'next-i18next'
const CoreSettingMm = (props) => {
export const CoreSettingMm = (props) => {
const { t } = useTranslation(['app', 'settings'])
const { dflt, min, max } = props
const val = props.gist?.[props.setting]
@ -70,5 +70,3 @@ const CoreSettingMm = (props) => {
</div>
)
}
export default CoreSettingMm

View file

@ -1,116 +0,0 @@
import { Chevron } from 'shared/components/navigation/primary'
import ListSetting from './core-setting-list'
import OnlySetting from './core-setting-only'
import MmSetting from './core-setting-mm'
import NrSetting from './core-setting-nr'
import BoolSetting from './core-setting-bool'
import SaBoolSetting from './core-setting-sa-bool'
import SaMmSetting from './core-setting-sa-mm'
import { formatMm } from 'shared/utils'
import { SecText, Li, Details, Summary, SumDiv, Deg } from 'shared/components/workbench/menu/index'
import { useTranslation } from 'next-i18next'
const settings = {
paperless: props => (
<SecText>
{props.t(props.gist.paperless ? 'yes' : 'no')}
</SecText>
),
complete: props => (
<SecText>
{props.t(props.gist.complete ? 'yes' : 'no')}
</SecText>
),
debug: props => (
<SecText>
{props.t(props.gist.debug ? 'yes' : 'no')}
</SecText>
),
locale: props => (
<SecText>
{props.t(`i18n:${props.gist.locale}`)}
</SecText>
),
units: props => (
<SecText>
{props.t(`${props.gist.units}Units`)}
</SecText>
),
margin: props => <SecText raw={formatMm(props.gist.margin, props.gist.units)} />,
scale: props => props.gist.scale === 1
? <SecText>{props.gist.scale}</SecText>
: <span className="text-accent">{props.gist.scale}</span>,
saMm: props => <SecText raw={formatMm(props.gist.saMm, props.gist.units)} />,
renderer: props => (
<SecText>
{props.config.titles[props.gist.renderer]}
</SecText>
),
only: props => (props.gist?.only && props.gist.only.length > 0)
? <SecText>{props.gist.only.length}</SecText>
: <span className="text-secondary-focus">{props.t('default')}</span>
}
const inputs = {
locale: props => <ListSetting
{...props}
list={props.config.list.map(key => ({
key,
title: props.t(`i18n:${key}`)
}))}
/>,
units: props => <ListSetting
{...props}
list={props.config.list.map(key => ({
key,
title: props.t(`${key}Units`)
}))}
/>,
margin: props => <MmSetting {...props} {...props.config} />,
scale: props => <NrSetting {...props} {...props.config} />,
saMm: props => <SaMmSetting {...props} {...props.config} />,
renderer: props => <ListSetting
{...props}
list={props.config.list.map(key => ({
key,
title: props.config.titles[key]
}))}
/>,
only: props => <OnlySetting {...props} />
}
const Setting = props => {
const { t } = useTranslation(['app', 'i18n', 'settings'])
if (props.setting === 'saBool')
return <SaBoolSetting {...props} {...props.config} />
if (['paperless', 'complete', 'debug', 'xray'].indexOf(props.setting) !== -1)
return <BoolSetting {...props} {...props.config} />
const Input = inputs[props.setting]
const Value = settings[props.setting]
return (
<Li>
<Details>
<Summary>
<SumDiv>
<Deg />
{props.setting === 'saMm'
? (
<>
<span>{t(`settings:sa.t`)}</span>
</>
)
: <span>{t(`settings:${props.setting}.t`)}</span>
}
</SumDiv>
<Value setting={props.setting} {...props} t={t}/>
<Chevron />
</Summary>
<Input {...props} t={t} />
</Details>
</Li>
)
}
export default Setting

View file

@ -0,0 +1,107 @@
import { Chevron } from 'shared/components/navigation/primary.mjs'
import { CoreSettingList as ListSetting } from './core-setting-list.mjs'
import { CoreSettingOnly as OnlySetting } from './core-setting-only.mjs'
import { CoreSettingMm as MmSetting } from './core-setting-mm.mjs'
import { CoreSettingNr as NrSetting } from './core-setting-nr.mjs'
import { CoreSettingBool as BoolSetting } from './core-setting-bool.mjs'
import { CoreSettingSaBool as SaBoolSetting } from './core-setting-sa-bool.mjs'
import { CoreSettingSaMm as SaMmSetting } from './core-setting-sa-mm.mjs'
import { formatMm } from 'shared/utils.mjs'
import {
SecText,
Li,
Details,
Summary,
SumDiv,
Deg,
} from 'shared/components/workbench/menu/index.mjs'
import { useTranslation } from 'next-i18next'
const settings = {
paperless: (props) => <SecText>{props.t(props.gist.paperless ? 'yes' : 'no')}</SecText>,
complete: (props) => <SecText>{props.t(props.gist.complete ? 'yes' : 'no')}</SecText>,
debug: (props) => <SecText>{props.t(props.gist.debug ? 'yes' : 'no')}</SecText>,
locale: (props) => <SecText>{props.t(`i18n:${props.gist.locale}`)}</SecText>,
units: (props) => <SecText>{props.t(`${props.gist.units}Units`)}</SecText>,
margin: (props) => <SecText raw={formatMm(props.gist.margin, props.gist.units)} />,
scale: (props) =>
props.gist.scale === 1 ? (
<SecText>{props.gist.scale}</SecText>
) : (
<span className="text-accent">{props.gist.scale}</span>
),
saMm: (props) => <SecText raw={formatMm(props.gist.saMm, props.gist.units)} />,
renderer: (props) => <SecText>{props.config.titles[props.gist.renderer]}</SecText>,
only: (props) =>
props.gist?.only && props.gist.only.length > 0 ? (
<SecText>{props.gist.only.length}</SecText>
) : (
<span className="text-secondary-focus">{props.t('default')}</span>
),
}
const inputs = {
locale: (props) => (
<ListSetting
{...props}
list={props.config.list.map((key) => ({
key,
title: props.t(`i18n:${key}`),
}))}
/>
),
units: (props) => (
<ListSetting
{...props}
list={props.config.list.map((key) => ({
key,
title: props.t(`${key}Units`),
}))}
/>
),
margin: (props) => <MmSetting {...props} {...props.config} />,
scale: (props) => <NrSetting {...props} {...props.config} />,
saMm: (props) => <SaMmSetting {...props} {...props.config} />,
renderer: (props) => (
<ListSetting
{...props}
list={props.config.list.map((key) => ({
key,
title: props.config.titles[key],
}))}
/>
),
only: (props) => <OnlySetting {...props} />,
}
export const Setting = (props) => {
const { t } = useTranslation(['app', 'i18n', 'settings'])
if (props.setting === 'saBool') return <SaBoolSetting {...props} {...props.config} />
if (['paperless', 'complete', 'debug', 'xray'].indexOf(props.setting) !== -1)
return <BoolSetting {...props} {...props.config} />
const Input = inputs[props.setting]
const Value = settings[props.setting]
return (
<Li>
<Details>
<Summary>
<SumDiv>
<Deg />
{props.setting === 'saMm' ? (
<>
<span>{t(`settings:sa.t`)}</span>
</>
) : (
<span>{t(`settings:${props.setting}.t`)}</span>
)}
</SumDiv>
<Value setting={props.setting} {...props} t={t} />
<Chevron />
</Summary>
<Input {...props} t={t} />
</Details>
</Li>
)
}

View file

@ -1,12 +1,12 @@
import { OptionsIcon } from 'shared/components/icons.mjs'
import { Chevron } from 'shared/components/navigation/primary.js'
import OptionGroup from './option-group'
import OptionComponent from './option'
import { Ul, Details, TopSummary, TopSumTitle } from 'shared/components/workbench/menu'
import { OptionGroup } from './option-group.mjs'
import { OptionComponent } from './option.mjs'
import { Ul, Details, TopSummary, TopSumTitle } from 'shared/components/workbench/menu.mjs'
import { useTranslation } from 'next-i18next'
import { optionsMenuStructure } from 'shared/utils.mjs'
const DesignOptions = (props) => {
export const DesignOptions = (props) => {
const { t } = useTranslation(['app'])
const Option = props.Option ? props.Option : OptionComponent
const optionsMenu = optionsMenuStructure(props.design.patternConfig.options)
@ -29,5 +29,3 @@ const DesignOptions = (props) => {
</Details>
)
}
export default DesignOptions

View file

@ -1,8 +1,8 @@
import { Chevron } from 'shared/components/navigation/primary.js'
import { Li, Ul, Details, Summary, SumDiv, Deg } from 'shared/components/workbench/menu'
import { Chevron } from 'shared/components/navigation/primary.mjs'
import { Li, Ul, Details, Summary, SumDiv, Deg } from 'shared/components/workbench/menu.mjs'
import { useTranslation } from 'next-i18next'
const OptionGroup = props => {
export const OptionGroup = (props) => {
const { t } = useTranslation(['optiongroups'])
const Option = props.Option
@ -12,21 +12,20 @@ const OptionGroup = props => {
<Summary>
<SumDiv>
<Deg />
<span className="font-bold">
{ t(props.group) }
</span>
<span className="font-bold">{t(props.group)}</span>
</SumDiv>
<Chevron />
</Summary>
<Ul>
{Object.entries(props.options).map(([option, type]) => typeof type === "string"
? <Option {...props} type={type} option={option} key={option} />
: <OptionGroup {...props} group={option} options={type} key={option} Option={Option}/>)
}
{Object.entries(props.options).map(([option, type]) =>
typeof type === 'string' ? (
<Option {...props} type={type} option={option} key={option} />
) : (
<OptionGroup {...props} group={option} options={type} key={option} Option={Option} />
)
)}
</Ul>
</Details>
</Li>
)
}
export default OptionGroup

View file

@ -1,15 +0,0 @@
import PctDegOption from 'shared/components/workbench/inputs/design-option-pct-deg'
import CountOption from 'shared/components/workbench/inputs/design-option-count'
import ListOption from 'shared/components/workbench/inputs/design-option-list'
import Popout from 'shared/components/popout'
export const Tmp = props => <p>not yet</p>
export const inputs = {
pct: PctDegOption,
count: CountOption,
deg: props => (<PctDegOption {...props} type='deg' />),
list: ListOption,
mm: () => <Popout fixme compact>Mm options are deprecated. Please report this</Popout>,
constant: Tmp,
}

View file

@ -0,0 +1,19 @@
import { PctDegOption } from 'shared/components/workbench/inputs/design-option-pct-deg.mjs'
import { CountOption } from 'shared/components/workbench/inputs/design-option-count.mjs'
import { ListOption } from 'shared/components/workbench/inputs/design-option-list.mjs'
import { Popout } from 'shared/components/popout.mjs'
export const Tmp = (props) => <p>not yet</p>
export const inputs = {
pct: PctDegOption,
count: CountOption,
deg: (props) => <PctDegOption {...props} type="deg" />,
list: ListOption,
mm: () => (
<Popout fixme compact>
Mm options are deprecated. Please report this
</Popout>
),
constant: Tmp,
}

View file

@ -1,72 +0,0 @@
import { useTranslation } from 'next-i18next'
import { formatMm, formatPercentage} from 'shared/utils'
export const values = {
pct: props => {
const val = (typeof props.gist?.options?.[props.option] === 'undefined')
? props.design.patternConfig.options[props.option].pct/100
: props.gist.options[props.option]
return (
<span className={
val=== props.design.patternConfig.options[props.option].pct/100
? 'text-secondary-focus'
: 'text-accent'
}>
{formatPercentage(val)}
{props.design.patternConfig.options[props.option]?.toAbs && props.gist.measurements
? ' | ' +formatMm(props.design.patternConfig.options[props.option]?.toAbs(val, props.gist))
: null
}
</span>
)
},
bool: props => {
const { t } = useTranslation(['app'])
const dflt = props.design.patternConfig.options[props.option].bool
let current = props.gist?.options?.[props.option]
current = current === undefined ? dflt : current;
return (
<span className={
(dflt==current || typeof current === 'undefined')
? 'text-secondary-focus'
: 'text-accent'
}>
{current
? t('yes')
: t('no')
}
</span>
)
},
count: props => {
const dflt = props.design.patternConfig.options[props.option].count
const current = props.gist?.options?.[props.option]
return (dflt==current || typeof current === 'undefined')
? (<span className="text-secondary-focus">{dflt}</span>)
: (<span className="text-accent">{current}</span>)
},
list: props => {
const dflt = props.design.patternConfig.options[props.option].dflt
const current = props.gist?.options?.[props.option]
const prefix = `${props.option}.o.`
const translate = props.design.patternConfig.options[props.option]?.doNotTranslate
? (input) => input
: (input) => props.t(prefix+input)
return (dflt==current || typeof current === 'undefined')
? (<span className="text-secondary-focus">{translate(dflt)}</span>)
: (<span className="text-accent">{translate(current)}</span>)
},
deg: props => {
const dflt = props.design.patternConfig.options[props.option].deg
const current = props.gist?.options?.[props.option]
return (dflt==current || typeof current === 'undefined')
? (<span className="text-secondary-focus">{dflt}&deg;</span>)
: (<span className="text-accent">{current}&deg;</span>)
},
mm: props => {
return (<p>No mm val yet</p>)
},
constant: props => {
return (<p>No constant val yet</p>)
}
}

View file

@ -0,0 +1,78 @@
import { useTranslation } from 'next-i18next'
import { formatMm, formatPercentage } from 'shared/utils.mjs'
export const values = {
pct: (props) => {
const val =
typeof props.gist?.options?.[props.option] === 'undefined'
? props.design.patternConfig.options[props.option].pct / 100
: props.gist.options[props.option]
return (
<span
className={
val === props.design.patternConfig.options[props.option].pct / 100
? 'text-secondary-focus'
: 'text-accent'
}
>
{formatPercentage(val)}
{props.design.patternConfig.options[props.option]?.toAbs && props.gist.measurements
? ' | ' +
formatMm(props.design.patternConfig.options[props.option]?.toAbs(val, props.gist))
: null}
</span>
)
},
bool: (props) => {
const { t } = useTranslation(['app'])
const dflt = props.design.patternConfig.options[props.option].bool
let current = props.gist?.options?.[props.option]
current = current === undefined ? dflt : current
return (
<span
className={
dflt == current || typeof current === 'undefined' ? 'text-secondary-focus' : 'text-accent'
}
>
{current ? t('yes') : t('no')}
</span>
)
},
count: (props) => {
const dflt = props.design.patternConfig.options[props.option].count
const current = props.gist?.options?.[props.option]
return dflt == current || typeof current === 'undefined' ? (
<span className="text-secondary-focus">{dflt}</span>
) : (
<span className="text-accent">{current}</span>
)
},
list: (props) => {
const dflt = props.design.patternConfig.options[props.option].dflt
const current = props.gist?.options?.[props.option]
const prefix = `${props.option}.o.`
const translate = props.design.patternConfig.options[props.option]?.doNotTranslate
? (input) => input
: (input) => props.t(prefix + input)
return dflt == current || typeof current === 'undefined' ? (
<span className="text-secondary-focus">{translate(dflt)}</span>
) : (
<span className="text-accent">{translate(current)}</span>
)
},
deg: (props) => {
const dflt = props.design.patternConfig.options[props.option].deg
const current = props.gist?.options?.[props.option]
return dflt == current || typeof current === 'undefined' ? (
<span className="text-secondary-focus">{dflt}&deg;</span>
) : (
<span className="text-accent">{current}&deg;</span>
)
},
mm: (props) => {
return <p>No mm val yet</p>
},
constant: (props) => {
return <p>No constant val yet</p>
},
}

View file

@ -1,11 +1,11 @@
import { Chevron } from 'shared/components/navigation/primary'
import { optionType } from 'shared/utils'
import { Li, Details, Summary, SumButton, SumDiv, Deg } from 'shared/components/workbench/menu'
import { Chevron } from 'shared/components/navigation/primary.mjs'
import { optionType } from 'shared/utils.mjs'
import { Li, Details, Summary, SumButton, SumDiv, Deg } from 'shared/components/workbench/menu.mjs'
import { useTranslation } from 'next-i18next'
import { values } from 'shared/components/workbench/menu/design-options/option-value'
import { inputs } from 'shared/components/workbench/menu/design-options/option-input'
import { values } from 'shared/components/workbench/menu/design-options/option-value.mjs'
import { inputs } from 'shared/components/workbench/menu/design-options/option-input.mjs'
const Option = (props) => {
export const Option = (props) => {
const { t } = useTranslation([`o_${props.design.designConfig.data.name}`])
const opt = props.design.patternConfig.options[props.option]
const type = optionType(opt)
@ -57,5 +57,3 @@ const Option = (props) => {
</Li>
)
}
export default Option

View file

@ -1,67 +1,82 @@
import { linkClasses } from 'shared/components/navigation/primary.js'
import ViewMenu from './view.js'
import DesignOptions from './design-options'
import CoreSettings from './core-settings'
import Xray from './xray'
import TestDesignOptions from './test-design-options'
import { linkClasses } from 'shared/components/navigation/primary.mjs'
import { ViewMenu } from './view.mjs'
import { DesignOptions } from './design-options.mjs'
import { CoreSettings } from './core-settings.mjs'
import { Xray } from './xray'
import { TestDesignOptions } from './test-design-options.mjs'
export const Ul = props => <ul className="pl-5 list-inside">{props.children}</ul>
export const Li = props => (
<li className="flex flex-row hover:border-r-2 hover:border-r-secondary">
{props.children}
</li>
export const Ul = (props) => <ul className="pl-5 list-inside">{props.children}</ul>
export const Li = (props) => (
<li className="flex flex-row hover:border-r-2 hover:border-r-secondary">{props.children}</li>
)
export const Details = props => (
export const Details = (props) => (
<details className="grow" open={props.open || false}>
{props.children}
</details>
)
export const Deg = props => <span className="text-3xl inline-block p-0 leading-3 px-2 translate-y-3">&deg;</span>
export const NoSumDiv = props => (
<div className={`
export const Deg = (props) => (
<span className="text-3xl inline-block p-0 leading-3 px-2 translate-y-3">&deg;</span>
)
export const NoSumDiv = (props) => (
<div
className={`
grow px-2 ml-2 border-l-2
${linkClasses}
hover:cursor-resize
hover:border-secondary
sm:hover:border-secondary-focus
text-base-content sm:text-base-content
`}>{props.children}</div>
`}
>
{props.children}
</div>
)
export const SumDiv = props => (
<div className={`
export const SumDiv = (props) => (
<div
className={`
grow pl-2 border-l-2
${linkClasses}
hover:cursor-resize
hover:border-secondary
sm:hover:border-secondary-focus
text-base-content sm:text-base-content
`}>{props.children}</div>
`}
>
{props.children}
</div>
)
export const Summary = props => (
<summary className={`
export const Summary = (props) => (
<summary
className={`
flex flex-row
px-2
text-base-content
sm:text-base-content
hover:cursor-row-resize
items-center
`}>{props.children}</summary>
`}
>
{props.children}
</summary>
)
export const TopSummary = props => (
<summary className={`
export const TopSummary = (props) => (
<summary
className={`
flex flex-row gap-4 text-lg
hover:cursor-row-resize
p-2
text-base-content
sm:text-base-content
items-center
`}>
`}
>
<span className="text-secondary-focus mr-4">{props.icon || null}</span>
{props.children}
</summary>
)
export const SumButton = props => (
<button className={`
export const SumButton = (props) => (
<button
className={`
flex flex-row
px-2
w-full justify-between
@ -71,18 +86,25 @@ export const SumButton = props => (
hover:cursor-pointer
items-center
mr-4
`} onClick={props.onClick}>{props.children}</button>
`}
onClick={props.onClick}
>
{props.children}
</button>
)
export const TopSumTitle = props => (
export const TopSumTitle = (props) => (
<span className={`grow ${linkClasses} hover:cursor-resize font-bold uppercase`}>
{props.children}
</span>
)
export const SecText = props => props.raw
? <span className="text-secondary-focus" dangerouslySetInnerHTML={{__html: props.raw}} />
: <span className="text-secondary-focus">{props.children}</span>
export const SecText = (props) =>
props.raw ? (
<span className="text-secondary-focus" dangerouslySetInnerHTML={{ __html: props.raw }} />
) : (
<span className="text-secondary-focus">{props.children}</span>
)
const WorkbenchMenu = props => {
export const WorkbenchMenu = (props) => {
return (
<nav className="grow mb-12">
<ViewMenu {...props} />
@ -97,5 +119,3 @@ const WorkbenchMenu = props => {
</nav>
)
}
export default WorkbenchMenu

View file

@ -1,9 +1,9 @@
import { MenuIcon } from 'shared/components/icons.mjs'
import { linkClasses, Chevron } from 'shared/components/navigation/primary.js'
import { linkClasses, Chevron } from 'shared/components/navigation/primary.mjs'
import { useTranslation } from 'next-i18next'
import { defaultGist } from 'shared/hooks/useGist'
import { defaultGist } from 'shared/hooks/useGist.mjs'
const View = (props) => {
export const View = (props) => {
const { t } = useTranslation(['app'])
const entries = [
{
@ -126,5 +126,3 @@ const View = (props) => {
</details>
)
}
export default View

View file

@ -1,31 +0,0 @@
import yaml from 'js-yaml'
import axios from 'axios'
const preload = {
github: async (id, design) => {
let result
try {
result = await axios.get(`https://api.github.com/gists/${id}`)
}
catch (err) {
console.log(err)
return [false, 'An unexpected error occured']
}
if (result.data.files['pattern.yaml'].content) {
let g = yaml.load(result.data.files['pattern.yaml'].content)
if (g.design !== undefined && g.design !== design.designConfig.data.name) return [
false, `You tried loading a configuration for ${g.design} into a ${design.designConfig.data.name} development environment`
]
return g
}
// TODO notify people of these errors
else return [false, 'This gist does not seem to be a valid pattern configuration']
}
}
export default preload

View file

@ -1,10 +1,9 @@
import SvgWrapper from './draft/svg-wrapper'
import Error from './draft/error.js'
import { svgattrPlugin } from '@freesewing/plugin-svgattr'
import { useTranslation } from 'next-i18next'
import { svgattrPlugin } from '@freesewing/plugin-svgattr'
import { SvgWrapper } from './draft/svg-wrapper.mjs'
import { Error } from './draft/error.mjs'
const LabSample = ({ gist, draft, updateGist, unsetGist, showInfo, app, feedback }) => {
export const LabSample = ({ gist, draft, updateGist, unsetGist, showInfo, app, feedback }) => {
const { t } = useTranslation(['workbench'])
let svg
let title = ''
@ -13,19 +12,18 @@ const LabSample = ({ gist, draft, updateGist, unsetGist, showInfo, app, feedback
if (gist.sample) {
try {
draft.use(svgattrPlugin, {
class: 'freesewing pattern max-h-screen'
class: 'freesewing pattern max-h-screen',
})
draft = draft.sample()
// Render as React
patternProps = draft.getRenderProps()
for (const logs of patternProps.logs) errors.push(...logs.error)
}
catch(err) {
} catch (err) {
console.log(err)
}
if (gist.sample.type === 'option') {
title = t('testThing', {
thing: ' : ' + t('option') + ' : ' + gist.sample.option
thing: ' : ' + t('option') + ' : ' + gist.sample.option,
})
}
}
@ -43,5 +41,3 @@ const LabSample = ({ gist, draft, updateGist, unsetGist, showInfo, app, feedback
</>
)
}
export default LabSample

View file

@ -1,9 +0,0 @@
import Yaml from 'shared/components/yaml.js'
const GistAsYaml = props => (
<div className="max-w-screen-xl m-auto">
<Yaml json={JSON.stringify(props.gist, null, 2)} />
</div>
)
export default GistAsYaml

View file

@ -1,24 +1,25 @@
// Hooks
import { useEffect, useState, useMemo } from 'react'
import { useGist } from 'shared/hooks/useGist'
import Layout from 'shared/components/layouts/default'
import Menu from 'shared/components/workbench/menu/index.js'
import DraftError from 'shared/components/workbench/draft/error.js'
// Dependencies
import { pluginTheme } from '@freesewing/plugin-theme'
import preloaders from 'shared/components/workbench/preload.js'
import Modal from 'shared/components/modal'
// Components
import { DefaultLayout } from 'shared/components/workbench/layout/default.mjs'
import { Menu } from 'shared/components/workbench/menu/index.mjs'
import { DraftError } from 'shared/components/workbench/draft/error.mjs'
import { Modal } from 'shared/components/modal.mjs'
import { ErrorBoundary } from 'shared/components/error/error-boundary.mjs'
// Views
import Measurements from 'shared/components/workbench/measurements/index.js'
import LabDraft from 'shared/components/workbench/draft/index.js'
import LabSample from 'shared/components/workbench/sample.js'
import ExportDraft from 'shared/components/workbench/exporting/index.js'
import GistAsJson from 'shared/components/workbench/gist-as-json.js'
import GistAsYaml from 'shared/components/workbench/yaml.js'
import DraftLogs from 'shared/components/workbench/logs.js'
import CutLayout from 'shared/components/workbench/layout/cut'
import PrintingLayout from 'shared/components/workbench/layout/print'
import ErrorBoundary from 'shared/components/error/error-boundary'
import { Measurements } from 'shared/components/workbench/measurements/index.js'
import { LabDraft } from 'shared/components/workbench/draft/index.js'
import { LabSample } from 'shared/components/workbench/sample.js'
import { ExportDraft } from 'shared/components/workbench/exporting/index.js'
import { GistAsJson } from 'shared/components/workbench/gist-as-json.js'
import { GistAsYaml } from 'shared/components/workbench/yaml.js'
import { DraftLogs } from 'shared/components/workbench/logs.js'
import { CutLayout } from 'shared/components/workbench/layout/cut'
import { PrintingLayout } from 'shared/components/workbench/layout/print'
const views = {
measurements: Measurements,
@ -54,7 +55,13 @@ const doPreload = async (preload, from, design, gist, setGist, setPreloaded) =>
* keeping the gist state, which will trickle down
* to all workbench subcomponents
*/
const WorkbenchWrapper = ({ app, design, preload = false, from = false, layout = false }) => {
export const WorkbenchWrapper = ({
app,
design,
preload = false,
from = false,
layout = false,
}) => {
// State for gist
const { gist, setGist, unsetGist, updateGist, gistReady, undoGist, resetGist } = useGist(
design.designConfig?.data?.name,
@ -175,5 +182,3 @@ const WorkbenchWrapper = ({ app, design, preload = false, from = false, layout =
</LayoutComponent>
)
}
export default WorkbenchWrapper

View file

@ -1,88 +0,0 @@
import useLocalStorage from './useLocalStorage';
import set from 'lodash.set'
import unset from 'lodash.unset'
import cloneDeep from 'lodash.clonedeep'
import defaultSettings from 'shared/components/workbench/default-settings.js'
import {useState} from 'react'
// Generates a default design gist to start from
export const defaultGist = (design, locale='en') => {
const gist = {
design,
...defaultSettings,
_state: {view: 'draft'}
}
if (locale) gist.locale = locale
return gist
}
// generate the gist state and its handlers
export function useGist(design, app) {
// get the localstorage state and setter
const [gist, _setGist, gistReady] = useLocalStorage(`${design}_gist`, defaultGist(design, app.locale));
const [gistHistory, setGistHistory] = useState([]);
const [gistFuture, setGistFuture] = useState([]);
const setGist = (newGist, addToHistory=true) => {
let oldGist
_setGist((gistState) => {
// have to clone it or nested objects will be referenced instead of copied, which defeats the purpose
if (addToHistory) oldGist = cloneDeep(gistState)
return typeof newGist === 'function' ? newGist(cloneDeep(gistState)) : newGist
})
if (addToHistory) {
setGistHistory((history) => {
return [...history, oldGist]
})
setGistFuture([])
}
}
/** update a single gist value */
const updateGist = (path, value, addToHistory=true) => {
setGist((gistState) => {
const newGist = {...gistState};
set(newGist, path, value);
return newGist;
}, addToHistory)
}
/** unset a single gist value */
const unsetGist = (path, addToHistory=true) => {
setGist((gistState) => {
const newGist = {...gistState};
unset(newGist, path);
return newGist;
}, addToHistory)
}
const undoGist = () => {
_setGist((gistState) => {
let prevGist;
setGistHistory((history) => {
const newHistory = [...history]
prevGist = newHistory.pop() || defaultGist(design, app.locale);
return newHistory;
})
setGistFuture((future) => [gistState, ...future]);
return {...prevGist}
})
}
const redoGist = () => {
const newHistory = [...gistHistory, gist]
const newFuture = [...gistFuture]
const newGist = newFuture.shift()
setGistHistory(newHistory)
setGistFuture(newFuture)
_setGist(newGist)
}
const resetGist = () => setGist(defaultGist(design, app.locale))
return {gist, setGist, unsetGist, gistReady, updateGist, undoGist, redoGist, resetGist};
}

View file

@ -0,0 +1,91 @@
import { useState } from 'react'
import set from 'lodash.set'
import unset from 'lodash.unset'
import cloneDeep from 'lodash.clonedeep'
import { useLocalStorage } from './useLocalStorage'
import { defaultSettings } from 'shared/components/workbench/default-settings.mjs'
// Generates a default design gist to start from
export const defaultGist = (design, locale = 'en') => {
const gist = {
design,
...defaultSettings,
_state: { view: 'draft' },
}
if (locale) gist.locale = locale
return gist
}
// generate the gist state and its handlers
export function useGist(design, app) {
// get the localstorage state and setter
const [gist, _setGist, gistReady] = useLocalStorage(
`${design}_gist`,
defaultGist(design, app.locale)
)
const [gistHistory, setGistHistory] = useState([])
const [gistFuture, setGistFuture] = useState([])
const setGist = (newGist, addToHistory = true) => {
let oldGist
_setGist((gistState) => {
// have to clone it or nested objects will be referenced instead of copied, which defeats the purpose
if (addToHistory) oldGist = cloneDeep(gistState)
return typeof newGist === 'function' ? newGist(cloneDeep(gistState)) : newGist
})
if (addToHistory) {
setGistHistory((history) => {
return [...history, oldGist]
})
setGistFuture([])
}
}
/** update a single gist value */
const updateGist = (path, value, addToHistory = true) => {
setGist((gistState) => {
const newGist = { ...gistState }
set(newGist, path, value)
return newGist
}, addToHistory)
}
/** unset a single gist value */
const unsetGist = (path, addToHistory = true) => {
setGist((gistState) => {
const newGist = { ...gistState }
unset(newGist, path)
return newGist
}, addToHistory)
}
const undoGist = () => {
_setGist((gistState) => {
let prevGist
setGistHistory((history) => {
const newHistory = [...history]
prevGist = newHistory.pop() || defaultGist(design, app.locale)
return newHistory
})
setGistFuture((future) => [gistState, ...future])
return { ...prevGist }
})
}
const redoGist = () => {
const newHistory = [...gistHistory, gist]
const newFuture = [...gistFuture]
const newGist = newFuture.shift()
setGistHistory(newHistory)
setGistFuture(newFuture)
_setGist(newGist)
}
const resetGist = () => setGist(defaultGist(design, app.locale))
return { gist, setGist, unsetGist, gistReady, updateGist, undoGist, redoGist, resetGist }
}

View file

@ -1,12 +1,13 @@
import { useState, useEffect, useRef, useReducer } from 'react'
// See: https://usehooks.com/useLocalStorage/
function useLocalStorage(key, initialValue, reducer) {
export function useLocalStorage(key, initialValue, reducer) {
const prefix = 'fs_'
const [storedValue, setStoredValue] = typeof reducer == 'function' ? useReducer(reducer, initialValue) : useState(initialValue);
const [storedValue, setStoredValue] =
typeof reducer == 'function' ? useReducer(reducer, initialValue) : useState(initialValue)
// use this to track whether it's mounted. useful for doing other effects outside this hook
const [ready, setReady] = useState(false);
const readyInternal = useRef(false);
const [ready, setReady] = useState(false)
const readyInternal = useRef(false)
const setValue = setStoredValue
// set to localstorage every time the storedValue changes
@ -18,9 +19,9 @@ function useLocalStorage(key, initialValue, reducer) {
// get the item from localstorage after the component has mounted. empty brackets mean it runs one time
useEffect(() => {
readyInternal.current = true;
readyInternal.current = true
const item = window.localStorage.getItem(prefix + key)
let valueToSet = storedValue;
let valueToSet = storedValue
if (item) {
valueToSet = JSON.parse(item)
}
@ -30,10 +31,8 @@ function useLocalStorage(key, initialValue, reducer) {
}
setValue(valueToSet)
setReady(true);
setReady(true)
}, [])
return [storedValue, setValue, ready]
}
export default useLocalStorage

View file

@ -1,22 +0,0 @@
import useLocalStorage from 'shared/hooks/useLocalStorage';
import {useEffect } from 'react'
function useTheme() {
// make a local storage item for the theme
const [storedTheme, setStoredTheme, ready] = useLocalStorage('theme', undefined);
useEffect(() => {
// set the default theme based on user prefence after mounting
if (ready && storedTheme === undefined) {
const prefersDarkMode = (typeof window !== 'undefined' && typeof window.matchMedia === 'function')
? window.matchMedia(`(prefers-color-scheme: dark`).matches
: undefined
setStoredTheme(prefersDarkMode ? 'dark' : 'light')
}
}, [ready])
return [storedTheme, setStoredTheme];
}
export default useTheme;

View file

@ -0,0 +1,21 @@
import { useLocalStorage } from 'shared/hooks/useLocalStorage'
import { useEffect } from 'react'
export function useTheme() {
// make a local storage item for the theme
const [storedTheme, setStoredTheme, ready] = useLocalStorage('theme', undefined)
useEffect(() => {
// set the default theme based on user prefence after mounting
if (ready && storedTheme === undefined) {
const prefersDarkMode =
typeof window !== 'undefined' && typeof window.matchMedia === 'function'
? window.matchMedia(`(prefers-color-scheme: dark`).matches
: undefined
setStoredTheme(prefersDarkMode ? 'dark' : 'light')
}
}, [ready])
return [storedTheme, setStoredTheme]
}