1
0
Fork 0

wip: Undo view

This commit is contained in:
joostdecock 2025-02-02 17:36:50 +01:00
parent 3ec1cbd01b
commit 987ba5419c
12 changed files with 158 additions and 82 deletions

View file

@ -93,6 +93,7 @@ packageJson:
"./components/LineDrawing": "./components/LineDrawing/index.mjs"
"./components/Link": "./components/Link/index.mjs"
"./components/Logo": "./components/Logo/index.mjs"
"./components/Mini": "./components/Mini/index.mjs"
"./components/Modal": "./components/Modal/index.mjs"
"./components/New": "./components/New/index.mjs"
"./components/Number": "./components/Number/index.mjs"

View file

@ -82,7 +82,7 @@ export const HeaderMenuDraftView = (props) => {
}
export const HeaderMenuDropdown = (props) => {
const { tooltip, toggle, open, setOpen, id } = props
const { tooltip, toggle, open, setOpen, id, end = false } = props
/*
* We need to use both !fixed and md:!absolute here to override DaisyUI's
* classes on dropdown-content to force the dropdown to use the available
@ -95,14 +95,16 @@ export const HeaderMenuDropdown = (props) => {
disabled
tabIndex={0}
role="button"
className="tw-daisy-btn tw-daisy-btn-ghost hover:tw-bg-secondary hover:tw-bg-opacity-20 hover:tw-border-solid hover:tw-border-2 hover:tw-border-secondary tw-border tw-border-secondary tw-border-2 tw-border-dotted tw-daisy-btn-sm tw-px-2 tw-z-20 tw-relative"
className={`tw-daisy-btn tw-daisy-btn-ghost hover:tw-bg-secondary hover:tw-bg-opacity-20 hover:tw-border-solid hover:tw-border-2 hover:tw-border-secondary tw-border tw-border-secondary tw-border-2 tw-border-dotted tw-daisy-btn-sm tw-px-2 tw-z-20 tw-relative`}
>
{toggle}
</button>
</Tooltip>
) : (
<Tooltip tip={tooltip}>
<div className={`tw-daisy-dropdown ${open === id ? 'tw-daisy-dropdown-open tw-z-20' : ''}`}>
<div
className={`tw-daisy-dropdown ${open === id ? 'tw-daisy-dropdown-open tw-z-20' : ''} ${end ? ' tw-daisy-dropdown-end' : ''}`}
>
<div
tabIndex={0}
role="button"
@ -113,7 +115,7 @@ export const HeaderMenuDropdown = (props) => {
</div>
<div
tabIndex={0}
className="tw-daisy-dropdown-content tw-bg-base-100 tw-bg-opacity-90 tw-z-20 tw-shadow tw-left-0 !tw-fixed md:!tw-absolute tw-top-12 tw-w-screen md:tw-max-w-lg tw-overflow-y-scroll tw-mb-12 tw-h-fit"
className="tw-daisy-dropdown-content tw-bg-base-100 tw-bg-opacity-90 tw-z-20 tw-shadow tw-left-0 !tw-fixed md:!tw-absolute tw-top-12 tw-w-screen md:tw-max-w-md tw-overflow-y-scroll tw-mb-12 tw-h-fit"
style={{ maxHeight: 'calc(100vh - 12rem)' }}
>
{props.children}
@ -329,6 +331,7 @@ export const HeaderMenuUndoIcons = (props) => {
<UndoIcon className={`${size} ${undos ? 'tw-text-secondary' : ''}`} text="A" />
</Button>
<HeaderMenuDropdown
end
{...props}
tooltip={viewLabels.undos.t}
id="undos"
@ -341,19 +344,14 @@ export const HeaderMenuUndoIcons = (props) => {
}
>
{undos ? (
<ul className="tw-daisy-dropdown-content tw-bg-base-100 tw-bg-opacity-90 tw-z-20 tw-shadow tw-left-0 !tw-fixed md:!tw-absolute tw-w-screen md:tw-w-96 tw-px-4 md:tw-p-2 md:tw-pt-0">
<ul className="tw-daisy-dropdown-content tw-bg-base-100 tw-bg-opacity-90 tw-z-20 tw-shadow tw-left-0 !tw-fixed md:!tw-absolute tw-w-screen md:tw-w-96 tw-px-4 md:tw-p-2 md:tw-pt-0 tw-contents">
{undos.slice(0, 9).map((step, index) => (
<li key={index}>
<UndoStep {...{ step, update, state, Design, index }} compact />
</li>
))}
<li key="view">
<ButtonFrame
dense
onClick={() => {
return null /*update.state(index, state._) */
}}
>
<ButtonFrame dense onClick={() => update.view('undos')}>
<div className="tw-flex tw-flex-row tw-items-center tw-align-center tw-justify-between tw-gap-2 tw-w-full">
<div className="tw-flex tw-flex-row tw-items-center tw-align-start tw-gap-2 tw-grow">
<UndoIcon className="tw-w-5 tw-h-5 tw-text-secondary" />
@ -502,7 +500,7 @@ export const HeaderMenuViewMenu = (props) => {
>
<ul
tabIndex={i}
className="tw-dropdown-content tw-bg-base-100 tw-bg-opacity-95 tw-z-20 tw-shadow tw-left-0 !tw-fixed md:!tw-absolute tw-w-screen md:tw-max-w-lg md:tw-pt-0 tw-mt-14 md:tw-mt-0 tw-contents"
className="tw-daisy-dropdown-content tw-bg-base-100 tw-bg-opacity-95 tw-z-20 tw-shadow tw-left-0 !tw-fixed md:!tw-absolute tw-w-screen md:tw-max-w-lg md:tw-pt-0 tw-mt-14 md:tw-mt-0 tw-contents"
>
{output}
</ul>

View file

@ -4,7 +4,7 @@ import { TipIcon, OkIcon } from '@freesewing/react/components/Icon'
import { Null } from './Null.mjs'
const config = {
timeout: 2,
timeout: 2.5,
defaults: {
color: 'secondary',
icon: 'Spinner',

View file

@ -111,7 +111,13 @@ export const UserSetPicker = ({
)
}
export const BookmarkedSetPicker = ({ Design, clickHandler, missingClickHandler, size = 'lg' }) => {
export const BookmarkedSetPicker = ({
Design,
config,
clickHandler,
missingClickHandler,
size = 'lg',
}) => {
// Hooks
const backend = useBackend()

View file

@ -5,15 +5,10 @@ import { designOptionType } from '@freesewing/utils'
import React, { useState, useMemo } from 'react'
// Components
import { SubAccordion } from '../Accordion.mjs'
import {
EditIcon,
GroupIcon,
OptionsIcon,
ResetIcon,
TipIcon,
} from '@freesewing/react/components/Icon'
import { EditIcon, GroupIcon, OptionsIcon, ResetIcon } from '@freesewing/react/components/Icon'
import { CoreSettingsMenu } from './CoreSettingsMenu.mjs'
import { FormControl } from '@freesewing/react/components/Input'
import { MiniTip } from '@freesewing/react/components/Mini'
/** @type {String} class to apply to buttons on open menu items */
const iconButtonClass = 'tw-daisy-btn tw-daisy-btn-xs tw-daisy-btn-ghost tw-px-0 tw-text-accent'
@ -129,16 +124,7 @@ export const MenuItem = ({
>
<Input {...drillProps} />
</FormControl>
{config.about ? (
<div className="tw-flex tw-flex-row tw-border tw-border-success tw-rounded">
<div className="tw-bg-success tw-text-success-content tw-p-1 tw-rounded-l tw-flex tw-flex-row tw-items-center">
<TipIcon className="tw-w-6 tw-h-6 tw-text-success-content" />
</div>
<div className="tw-p-1 tw-text-sm tw-font-medium tw-bg-success/10 tw-grow tw-rounded-r">
{config.about}
</div>
</div>
) : null}
{config.about ? <MiniTip>{config.about}</MiniTip> : null}
</>
)
}

View file

@ -3,19 +3,23 @@ import { t, designMeasurements } from '../../lib/index.mjs'
import { capitalize, horFlexClasses as horFlexClasses } from '@freesewing/utils'
import { measurements as measurementsTranslations } from '@freesewing/i18n'
// Hooks
import React, { Fragment, useEffect } from 'react'
import React, { Fragment, useState, useEffect } from 'react'
import { useBackend } from '@freesewing/react/hooks/useBackend'
// Components
import { Popout } from '@freesewing/react/components/Popout'
import { NumberInput } from '@freesewing/react/components/Input'
import {
BookmarkIcon,
CuratedMeasurementsSetIcon,
EditIcon,
MeasurementsSetIcon,
FingerprintIcon,
} from '@freesewing/react/components/Icon'
import { Accordion } from '../Accordion.mjs'
import { MeasurementsEditor } from '../MeasurementsEditor.mjs'
import { SetPicker, BookmarkedSetPicker, CuratedSetPicker, UserSetPicker } from '../Set.mjs'
import { HeaderMenu } from '../HeaderMenu.mjs'
import { H1, H5 } from '@freesewing/react/components/Heading'
const iconClasses = {
className: 'tw-w-8 tw-h-8 md:tw-w-10 md:tw-h-10 lg:tw-w-12 lg:tw-h-12 tw-shrink-0',
@ -35,7 +39,14 @@ const iconClasses = {
* @param {Object} props.update - Helper object for updating the editor state
* @return {Function} MeasurementsView - React component
*/
export const MeasurementsView = ({ config, Design, missingMeasurements, state, update }) => {
export const MeasurementsView = ({
config,
Design,
missingMeasurements,
state,
update,
design,
}) => {
/*
* If there is no view set, completing measurements will switch to the view picker
* Which is a bit confusing. So in this case, set the view to measurements.
@ -50,7 +61,7 @@ export const MeasurementsView = ({ config, Design, missingMeasurements, state, u
},
'missingMeasurements'
)
else update.notifySuccess(t('pe:measurementsAreOk'))
else update.notifySuccess(`We have all measurements to draft ${capitalize(design)}`)
}, [state.view, update])
const loadMeasurements = (set) => {
@ -72,7 +83,7 @@ export const MeasurementsView = ({ config, Design, missingMeasurements, state, u
[
<Fragment key={1}>
<div className={`${horFlexClasses} tw-w-full`}>
<h4 id="ownsets">Choose one of your own measurements sets</h4>
<H5 id="ownsets">Choose one of your own measurements sets</H5>
<MeasurementsSetIcon {...iconClasses} />
</div>
<p className="tw-text-left">
@ -92,7 +103,7 @@ export const MeasurementsView = ({ config, Design, missingMeasurements, state, u
[
<Fragment key={1}>
<div className={`${horFlexClasses} tw-w-full`}>
<h4 id="bookmarkedsets">Choose one of the measurements sets you have bookmarked</h4>
<H5 id="bookmarkedsets">Choose one of the measurements sets you have bookmarked</H5>
<BookmarkIcon {...iconClasses} />
</div>
<p className="tw-text-left">
@ -111,7 +122,7 @@ export const MeasurementsView = ({ config, Design, missingMeasurements, state, u
[
<Fragment key={1}>
<div className={`${horFlexClasses} tw-w-full`}>
<h4 id="curatedsets">Choose one of FreeSewing&apos;s curated measurements sets</h4>
<H5 id="curatedsets">Choose one of FreeSewing&apos;s curated measurements sets</H5>
<CuratedMeasurementsSetIcon {...iconClasses} />
</div>
<p className="tw-text-left">
@ -121,13 +132,27 @@ export const MeasurementsView = ({ config, Design, missingMeasurements, state, u
</Fragment>,
<CuratedSetPicker key={2} clickHandler={loadMeasurements} {...{ config, Design }} />,
'csets',
],
[
<Fragment key={1}>
<div className={`${horFlexClasses} tw-w-full`}>
<H5 id="loadid">Load a measurements set by ID</H5>
<FingerprintIcon {...iconClasses} />
</div>
<p className="tw-text-left">
If you know the ID of a measurements set either one of your own or a public set we
can load it for you.
</p>
</Fragment>,
<LoadMeasurementsSetById key={2} {...{ loadMeasurements, update }} />,
'setid',
]
)
// Manual editing is always an option
items.push([
<Fragment key={1}>
<div className={`${horFlexClasses} tw-w-full`}>
<h4 id="editmeasurements">Edit Measurements</h4>
<H5 id="editmeasurements">Edit measurements by hand</H5>
<EditIcon {...iconClasses} />
</div>
<p className="tw-text-left">You can manually set or override measurements below.</p>
@ -140,7 +165,7 @@ export const MeasurementsView = ({ config, Design, missingMeasurements, state, u
<>
<HeaderMenu state={state} {...{ config, update }} />
<div className="tw-max-w-7xl tw-mt-8 tw-mx-auto tw-px-4 tw-mb-4">
<h1 className="tw-text-center">Measurements</h1>
<H1>Measurements</H1>
{missingMeasurements && missingMeasurements.length > 0 ? (
<Popout note dense noP>
<h3>
@ -158,16 +183,16 @@ export const MeasurementsView = ({ config, Design, missingMeasurements, state, u
</Popout>
) : (
<Popout tip dense noP>
<h5>We have all required measurements to draft this pattern</h5>
<H5>We have all required measurements to draft this pattern</H5>
<div className="tw-flex tw-flex-row tw-flex-wrap tw-gap-2 tw-mt-2">
<button
className="tw-daisy-btn tw-daisy-btn-primary lg:tw-daisy-btn-lg"
className="tw-daisy-btn tw-daisy-btn-primary"
onClick={() => update.view('draft')}
>
{viewLabels.draft.t}
Draft Pattern
</button>
<button
className="tw-daisy-btn tw-daisy-btn-primary tw-daisy-btn-outline lg:tw-daisy-btn-lg"
className="tw-daisy-btn tw-daisy-btn-primary tw-daisy-btn-outline"
onClick={() => update.view('picker')}
>
Choose a different view
@ -180,3 +205,43 @@ export const MeasurementsView = ({ config, Design, missingMeasurements, state, u
</>
)
}
const LoadMeasurementsSetById = ({ loadMeasurements, update }) => {
const backend = useBackend()
const [id, setId] = useState('')
return (
<div>
<div className="tw-flex tw-flex-row tw-gap-2 tw-items-end">
<NumberInput
label="Measurements Set ID"
update={setId}
current={id}
valid={(val) => Number(val) == val}
/>
<button
className="tw-daisy-btn tw-daisy-btn-primary"
onClick={() => loadMeasurementsSet(id, backend, loadMeasurements, update)}
>
Load set
</button>
</div>
</div>
)
}
async function loadMeasurementsSet(id, backend, loadMeasurements, update) {
update.startLoading('getset', {
msg: 'Loading measurements set from the FreeSewing backend',
icon: 'spinner',
})
const result = await backend.getSet(id)
if (result[0] === 200 && result[1].set) {
loadMeasurements(result[1].set)
update.clearLoading()
update.notifySuccess('Measurements set loaded', 'getsetok')
} else {
update.clearLoading()
update.notifySuccess('Measurements set loaded', 'getsetko')
}
}

View file

@ -1,7 +1,11 @@
import React from 'react'
import { orderBy } from '@freesewing/utils'
import { ButtonFrame } from '@freesewing/react/components/Input'
import { UndoIcon } from '@freesewing/react/components/Icon'
import { Popout } from '@freesewing/react/components/Popout'
import { UndoIcon, TipIcon, LeftIcon } from '@freesewing/react/components/Icon'
import { HeaderMenu } from '../HeaderMenu.mjs'
import { H1 } from '@freesewing/react/components/Heading'
import { MiniTip } from '@freesewing/react/components/Mini'
import { getUndoStepData } from '../../lib/index.mjs'
@ -12,18 +16,15 @@ import { getUndoStepData } from '../../lib/index.mjs'
* @param {object} designs - Object holding all designs
* @param {object} update - ViewWrapper state update object
*/
export const UndosView = ({ Design, update, state }) => {
export const UndosView = ({ Design, update, state, config }) => {
const steps = orderBy(state._.undos, 'time', 'desc')
return (
<>
<HeaderMenu state={state} {...{ update, Design }} />
<HeaderMenu {...{ update, Design, config, state }} />
<div className="tw-text-left tw-mt-8 tw-mb-24 tw-px-4 tw-max-w-xl tw-mx-auto">
<h2>Undo History</h2>
<p>Time-travel through your recent pattern changes</p>
<small>
<b>Tip:</b> Click on any change to undo all changes up to, and including, that change.
</small>
<H1>Undo History</H1>
<p className="tw-mb-4">Time-travel through your recent pattern changes.</p>
{steps.length < 1 ? (
<Popout note>
<h4>Your undo history is currently empty</h4>
@ -38,11 +39,16 @@ export const UndosView = ({ Design, update, state }) => {
<p>As soon as you do, the change will show up here, and you can undo it.</p>
</Popout>
) : (
<>
<MiniTip>
Click on any change to undo all changes up to, and including, that change.
</MiniTip>
<div className="tw-flex tw-flex-col tw-gap-2 tw-mt-4">
{steps.map((step, index) => (
<UndoStep key={step.time} {...{ step, update, state, Design, index }} />
))}
</div>
</>
)}
</div>
</>
@ -93,7 +99,7 @@ export const UndoStep = ({ update, state, step, Design, compact = false, index =
<ButtonFrame dense onClick={() => update.restore(index, state._)}>
<div className="tw-flex tw-flex-row tw-items-center tw-align-start tw-gap-2 tw-w-full">
<UndoIcon text={index} className="tw-w-5 tw-h-5 tw-text-secondary" />
{data.msg ? data.msg : `pe:${data.optCode}`}
{data.msg ? data.msg : data.title}
</div>
</ButtonFrame>
)
@ -107,10 +113,10 @@ export const UndoStep = ({ update, state, step, Design, compact = false, index =
<div className="tw-flex tw-flex-row tw-items-center tw-justify-between tw-gap-2 tw-w-full tw-m-0 tw-p-0 tw--mt-2 tw-text-lg">
<span className="tw-flex tw-flex-row tw-gap-2 tw-items-center">
{data.fieldIcon || null}
{`pe:${data.optCode}`}
{data.title}
</span>
<span className="tw-opacity-70 tw-flex tw-flex-row tw-gap-1 tw-items-center tw-text-base">
{data.icon || null} {`pe:${data.titleCode}`}
{data.icon || null} {data.menu}
</span>
</div>
<div className="tw-flex tw-flex-row tw-gap-1 tw-items-center tw-align-start tw-w-full">

View file

@ -5,6 +5,7 @@ import { MeasurementsView } from './MeasurementsView.mjs'
import { DraftView } from './DraftView.mjs'
import { SaveView } from './SaveView.mjs'
import { ExportView } from './ExportView.mjs'
import { UndosView } from './UndosView.mjs'
import { ErrorIcon } from '@freesewing/react/components/Icon'
import {
OptionsIcon,
@ -55,6 +56,7 @@ export const View = (props) => {
if (view === 'draft') return <DraftView {...props} />
if (view === 'save') return <SaveView {...props} />
if (view === 'export') return <ExportView {...props} />
if (view === 'undos') return <UndosView {...props} />
/*
viewComponents: {
draft: 'DraftView',

View file

@ -138,7 +138,7 @@ export const defaultConfig = {
base: 'user',
},
// Ms before to fade out a notification
notifyTimeout: 6660,
notifyTimeout: 2500,
degreeMeasurements: ['shoulderSlope'],
}

View file

@ -71,8 +71,8 @@ export function getUiPreferenceUndoStepData({ step }) {
const data = {
icon: <UiIcon />,
field,
optCode: `${field}.t`,
titleCode: 'uiPreferences.t',
title: structure.title,
menu: 'UI Preferences',
structure: menuUiPreferencesStructure()[field],
}
const FieldIcon = data.structure.icon
@ -85,7 +85,7 @@ export function getUiPreferenceUndoStepData({ step }) {
data[key + 'Val'] = t(
structure.choiceTitles[
structure.choiceTitles[String(step[key])] ? String(step[key]) : String(structure.dflt)
] + '.t'
]
)
return data
@ -102,8 +102,8 @@ export function getCoreSettingUndoStepData({ step, state, Design }) {
const data = {
field,
titleCode: 'coreSettings.t',
optCode: `${field}.t`,
menu: 'Core Settings',
title: structure?.[field] ? structure[field].title : '',
icon: <SettingsIcon />,
structure: structure[field],
}
@ -125,9 +125,7 @@ export function getCoreSettingUndoStepData({ step, state, Design }) {
case 'margin':
case 'sa':
case 'samm':
if (data.field !== 'margin') {
data.optCode = `samm.t`
}
if (data.field !== 'margin') data.title = 'Seam Allowance'
data.oldVal = <Html html={formatMm(cord(step.old, data.structure.dflt))} />
data.newVal = <Html html={formatMm(cord(step.new, data.structure.dflt))} />
return data
@ -136,23 +134,23 @@ export function getCoreSettingUndoStepData({ step, state, Design }) {
data.newVal = cord(step.new, data.structure.dflt)
return data
case 'units':
data.oldVal = t(step.new === 'imperial' ? 'pe:metricUnits' : 'pe:imperialUnits')
data.newVal = t(step.new === 'imperial' ? 'pe:imperialUnits' : 'pe:metricUnits')
data.oldVal = t(step.new === 'imperial' ? 'Metric Units' : 'Imperial Units')
data.newVal = t(step.new === 'imperial' ? 'Imperial Units' : 'Metric Units')
return data
case 'only':
data.oldVal = cord(step.old, data.structure.dflt) || t('pe:includeAllParts')
data.newVal = cord(step.new, data.structure.dflt) || t('pe:includeAllParts')
data.oldVal = cord(step.old, data.structure.dflt) || 'Include all parts'
data.newVal = cord(step.new, data.structure.dflt) || 'Include all parts'
return data
default:
data.oldVal = t(
(data.structure.choiceTitles[String(step.old)]
data.structure.choiceTitles[String(step.old)]
? data.structure.choiceTitles[String(step.old)]
: data.structure.choiceTitles[String(data.structure.dflt)]) + '.t'
: data.structure.choiceTitles[String(data.structure.dflt)]
)
data.newVal = t(
(data.structure.choiceTitles[String(step.new)]
data.structure.choiceTitles[String(step.new)]
? data.structure.choiceTitles[String(step.new)]
: data.structure.choiceTitles[String(data.structure.dflt)]) + '.t'
: data.structure.choiceTitles[String(data.structure.dflt)]
)
return data
}
@ -163,8 +161,8 @@ export function getDesignOptionUndoStepData({ step, state, Design }) {
const data = {
icon: <OptionsIcon />,
field: step.path[2],
optCode: `${state.design}:${step.path[2]}.t`,
titleCode: `designOptions.t`,
title: `${state.design}:${step.path[2]}`,
menu: `Design Options`,
oldVal: formatDesignOptionValue(option, step.old, state.units === 'imperial'),
newVal: formatDesignOptionValue(option, step.new, state.units === 'imperial'),
}
@ -212,8 +210,8 @@ export function getUndoStepData(props) {
const data = {
icon: <MeasurementsIcon />,
field: 'measurements',
optCode: `measurements`,
titleCode: 'measurements',
title: `measurements`,
menu: 'measurements',
}
/*
* Single measurements change?
@ -229,7 +227,7 @@ export function getUndoStepData(props) {
for (const m of Object.keys(props.step.new)) {
if (props.step.new[m] !== props.step.old?.[m]) count++
}
return { ...data, msg: t('pe:xMeasurementsChanged', { count }) }
return { ...data, msg: `${count} measurements updated` }
}
/*

View file

@ -0,0 +1,13 @@
import React from 'react'
import { TipIcon } from '@freesewing/react/components/Icon'
export const MiniTip = ({ children }) => (
<div className="tw-flex tw-flex-row tw-border tw-border-success tw-rounded">
<div className="tw-bg-success tw-text-success-content tw-p-1 tw-rounded-l tw-flex tw-flex-row tw-items-center">
<TipIcon className="tw-w-6 tw-h-6 tw-text-success-content" />
</div>
<div className="tw-p-1 tw-px-2 tw-text-sm tw-font-medium tw-bg-success/10 tw-grow tw-rounded-r">
{children}
</div>
</div>
)

View file

@ -46,6 +46,7 @@
"./components/LineDrawing": "./components/LineDrawing/index.mjs",
"./components/Link": "./components/Link/index.mjs",
"./components/Logo": "./components/Logo/index.mjs",
"./components/Mini": "./components/Mini/index.mjs",
"./components/Modal": "./components/Modal/index.mjs",
"./components/New": "./components/New/index.mjs",
"./components/Number": "./components/Number/index.mjs",
@ -86,12 +87,12 @@
"html-react-parser": "^5.0.7",
"luxon": "^3.5.0",
"nuqs": "^1.17.6",
"pdfkit": "^0.16.0",
"react-markdown": "^9.0.1",
"tlds": "^1.255.0",
"use-local-storage-state": "19.1.0",
"use-session-storage-state": "^19.0.0"
},
"devDependencies": {},
"files": [
"components/**",
"hooks/**",