New icons, better page transition, truncated popper, adds disallowSpaces option to binders, delete and edit buttons

This commit is contained in:
Emma 2023-06-14 03:09:27 -06:00
parent e78e4304a4
commit f84ef2ee19
21 changed files with 277 additions and 60 deletions

View File

@ -7,8 +7,8 @@ function App() {
return ( return (
<RecoilRoot> <RecoilRoot>
<RecoilizeDebugger /> <RecoilizeDebugger />
{/* <SchemaBuilder /> */} <SchemaBuilder />
<GameSystemEditor /> {/* <GameSystemEditor /> */}
</RecoilRoot> </RecoilRoot>
) )
} }

View File

@ -0,0 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect class="anvil-base" x="4.5" y="9.5" width="10" height="2" rx="0.5" stroke="inherit" fill="none" />
<path class="anvil-body"
d="M6 1H2C3 2 5.5 2.5 7 3C8.5 3.5 8.54315 4.2918 7 6L6 7V8H13V7C13 7 11.5 6 11 5C10.5 4 11 2.5 15 2V0.5H6V1Z"
stroke="inherit" fill="none" />
</svg>

After

Width:  |  Height:  |  Size: 385 B

View File

@ -0,0 +1,8 @@
<svg width="35" height="35" viewBox="0 0 23 35" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 12H21V32C21 33.1046 20.1046 34 19 34H5C3.89543 34 3 33.1046 3 32V12Z" stroke="inherit" fill="none" stroke-width="2"/>
<path d="M7 16L7 29" stroke="inherit" fill="none" stroke-width="2" stroke-linecap="round"/>
<path d="M12 16V29" stroke="inherit" fill="none" stroke-width="2" stroke-linecap="round"/>
<path d="M17 16V29" stroke="inherit" fill="none" stroke-width="2" stroke-linecap="round"/>
<path d="M8.59244 1.36064L12.5317 0.666048C12.8036 0.618097 13.063 0.799681 13.1109 1.07163L13.3714 2.54884L8.44734 3.41708L8.18687 1.93987C8.13891 1.66792 8.3205 1.40859 8.59244 1.36064Z" stroke="inherit" fill="none"/>
<path d="M2.05644 4.54394L19.783 1.41827C20.5988 1.27442 21.3768 1.81917 21.5207 2.63501L21.9548 5.09703L1.27382 8.74365L0.8397 6.28163C0.695845 5.46578 1.2406 4.6878 2.05644 4.54394Z" stroke="inherit" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 943 B

View File

@ -0,0 +1,12 @@
<svg class="trash-can" width="35" height="30" viewBox="0 0 22 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- body -->
<path d="M2 7H20V27C20 28.1046 19.1046 29 18 29H4C2.89543 29 2 28.1046 2 27V7Z" stroke="inherit" fill="none" stroke-width="2"/>
<!-- body lines -->
<path d="M6 11L6 24" stroke="inherit" fill="none" stroke-width="2" stroke-linecap="round"/>
<path d="M11 11V24" stroke="inherit" fill="none" stroke-width="2" stroke-linecap="round"/>
<path d="M16 11V24" stroke="inherit" fill="none" stroke-width="2" stroke-linecap="round"/>
<!-- handle -->
<path class="trash-lid" d="M9 0.5H13C13.2761 0.5 13.5 0.723858 13.5 1V2.5H8.5V1C8.5 0.723858 8.72386 0.5 9 0.5Z" stroke="inherit" fill="none"/>
<!-- cap -->
<path class="trash-lid" d="M2 2.5H20C20.8284 2.5 21.5 3.17157 21.5 4V6.5H0.5V4C0.5 3.17157 1.17157 2.5 2 2.5Z" stroke="inherit" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 872 B

View File

@ -0,0 +1,6 @@
.page {
transition:
transform 500ms,
opacity 300ms,
z-index 0ms 500ms
}

View File

@ -1,5 +1,6 @@
import React, { useState, useEffect, ReactNode, useRef, useLayoutEffect, useCallback } from 'react'; import React, { useState, useEffect, ReactNode, useRef, useLayoutEffect, useCallback } from 'react';
import { FCC } from '../../types'; import { FCC } from '../../types';
import './index.css';
interface IProps { interface IProps {
currentPage: number; currentPage: number;
@ -10,12 +11,24 @@ const AnimatedPageContainer: FCC<IProps> = ({ children, currentPage }) => {
const renderChild = (child: ReactNode, index: number) => { const renderChild = (child: ReactNode, index: number) => {
const isActive = index === currentPage; const isActive = index === currentPage;
let position = 'active';
switch ((index - currentPage) / Math.abs(index - currentPage)) {
case 1:
position = 'right';
break;
case -1:
position = 'left';
break;
default:
position = 'active';
}
return ( return (
<div <div
key={`page container ${uuid}: ${index}`} key={`page container ${uuid}: ${index}`}
data-active={isActive} data-active={isActive}
className="data-[active=true]:opacity-100 data-[active=true]:static opacity-0 top-0 left-0 absolute transition-opacity duration-300 data-[active=false]:-z-10" data-position={position}
className="data-[active=true]:opacity-100 data-[active=true]:static opacity-0 top-0 left-0 absolute page data-[position=left]:-translate-x-96 data-[position=right]:translate-x-96 translate-x-0"
> >
{child} {child}
</div> </div>
@ -23,7 +36,7 @@ const AnimatedPageContainer: FCC<IProps> = ({ children, currentPage }) => {
}; };
return ( return (
<div className="relative"> <div className="relative overflow-hidden">
{React.Children.map(children, renderChild)} {React.Children.map(children, renderChild)}
</div> </div>
); );

View File

@ -1,8 +1,14 @@
import { FC } from 'react' import { FC } from 'react'
import { ReactComponent as help } from '../../assets/icons/Help Icon.svg'; import { ReactComponent as help } from '../../assets/icons/Help Icon.svg';
import { ReactComponent as trash } from '../../assets/icons/Trash Icon.svg';
import { ReactComponent as trash_hover } from '../../assets/icons/Trash Icon Open.svg';
import { ReactComponent as anvil } from '../../assets/icons/Anvil Icon.svg';
const library = { const library = {
help help,
trash,
trash_hover,
anvil
} }
interface IProps { interface IProps {

View File

@ -0,0 +1,15 @@
import { FC, PropsWithChildren } from 'react';
import { Poppable } from '../../lib/poppables/components/poppable';
export const Truncate: FC<PropsWithChildren> = ({children}) => {
return (
<Poppable
content={children}
preferredAlign="centered"
preferredEdge="top"
>
<p className="truncate max-w-full underline">{children}</p>
</Poppable>
);
}

View File

@ -0,0 +1,19 @@
import {FC} from 'react'
import { Publication } from '../../types/publication';
import { useObjectState } from '../../hooks/useObjectState';
export const PublicationEditor: FC = () => {
const {value: publication, setValue: setPublication, bindProperty: bindPublication, update: updatePublication, reset: resetPublication} = useObjectState<Publication>({
gameSystemId: '',
name: '',
status: 'private',
templates: {},
version: '1'
});
return (
<div className="container p-8">
</div>
)
}

View File

@ -9,6 +9,7 @@ import { SchemaEditAtom } from '../../recoil/atoms/schema';
import { GameSystemsService } from '../../services/game-systems'; import { GameSystemsService } from '../../services/game-systems';
import { SchemaViewer } from './schema-viewer'; import { SchemaViewer } from './schema-viewer';
import { TemplateEditor } from './template-editor'; import { TemplateEditor } from './template-editor';
import { Icon } from '../Icon';
export const SchemaBuilder: FC = () => { export const SchemaBuilder: FC = () => {
@ -60,7 +61,7 @@ export const SchemaBuilder: FC = () => {
setPageNumber(1); setPageNumber(1);
}, []) }, [])
const { value: templateName, bind: bindTemplateName, reset: resetTemplateName } = useInput(''); const { value: templateName, bind: bindTemplateName, reset: resetTemplateName } = useInput('', { disallowSpaces: true });
const addTemplate = useCallback(() => { const addTemplate = useCallback(() => {
updateSchema(s => ({ updateSchema(s => ({
templates: { templates: {
@ -83,25 +84,55 @@ export const SchemaBuilder: FC = () => {
})) }))
}, [updateSchema]) }, [updateSchema])
const deleteType = useCallback((key: string) => {
updateSchema(s => {
const types = { ...s.types };
delete types[key];
return { types }
})
}, [updateSchema])
return ( return (
<div className="container flex gap-4 p-8"> <div className="container flex gap-4 p-8">
<div className="panel w-2/3 h-full"> <div className="panel w-2/3 h-full flex flex-col gap-4">
<p className="subheader">Add a template</p> <div>
<input type="text" {...bindTemplateName} /> <p className="subheader mb-2">Add a template</p>
<button onClick={addTemplate} disabled={!templateName}>Add</button> <div className="mb-2">
<ul> <input type="text" {...bindTemplateName} />
{Object.entries(schema.templates).map(([templateKey, template]) => ( <button onClick={addTemplate} disabled={!templateName}>Add</button>
<TemplateEditor templateKey={templateKey} template={template} update={updateTemplate} />
))}
</ul>
<AnimatedPageContainer currentPage={pageNumber}>
<div>
<p className="subheader">Add a type</p>
<input type="text" {...bindTypeName} />
<button className="interactive" disabled={!typeName} onClick={() => setPageNumber(1)}>Configure</button>
</div> </div>
<TypeEditor name={selectedType || typeName} saveType={saveType} type={selectedType ? schema.types[selectedType as keyof typeof schema.types] : undefined} /> <ul className="rounded-lg overflow-hidden">
</AnimatedPageContainer> {Object.entries(schema.templates).map(([templateKey, template]) => (
<TemplateEditor templateKey={templateKey} template={template} update={updateTemplate} />
))}
</ul>
</div>
<hr />
<div>
<AnimatedPageContainer currentPage={pageNumber}>
<div>
<p className="subheader mb-2">Add a type</p>
<input type="text" {...bindTypeName} />
<button className="interactive" disabled={!typeName} onClick={() => setPageNumber(1)}>Configure</button>
</div>
<TypeEditor name={selectedType || typeName} saveType={saveType} type={selectedType ? schema.types[selectedType as keyof typeof schema.types] : undefined} />
</AnimatedPageContainer>
<ul className="mt-3 w-96">
{Object.keys(schema.types).map(t => (
<li className="odd:bg-black/50 flex justify-between p-2">
{t}
<div className="flex gap-3">
<button title="Edit" className="no-default" onClick={() => selectTypeForEdit(t)}>
<Icon icon="anvil" className="anvil svg-olive-drab hover:svg-olive-drab-100 w-6 h-6" />
</button>
<button title="Delete" className="no-default" onClick={() => deleteType(t)}>
<Icon icon="trash" className="trash-can svg-red-700 hover:svg-red-500 w-6 h-6" />
</button>
</div>
</li>
))}
</ul>
</div>
</div> </div>
<div className="panel basis-1/3"> <div className="panel basis-1/3">
<SchemaViewer schema={schema} onTypeClick={selectTypeForEdit} /> <SchemaViewer schema={schema} onTypeClick={selectTypeForEdit} />

View File

@ -1,5 +1,6 @@
import { FC, useCallback } from 'react' import { FC, useCallback } from 'react'
import { FieldType, FieldTypes, Schema, TypeType, fieldTypesWithValues } from '../../types/schema' import { FieldType, FieldTypes, Schema, TypeType, fieldTypesWithValues } from '../../types/schema'
import { Truncate } from '../Poppables/truncation';
interface IProps { interface IProps {
schema: Schema; schema: Schema;
@ -16,6 +17,7 @@ export const SchemaViewer: FC<IProps> = ({ schema, onTypeClick }) => {
switch (field.type) { switch (field.type) {
case FieldTypes.type: return 'Type:' case FieldTypes.type: return 'Type:'
case FieldTypes.dice: return 'Dice:' case FieldTypes.dice: return 'Dice:'
case FieldTypes.select: return 'Options:'
default: return ''; default: return '';
} }
}, []) }, [])
@ -53,7 +55,7 @@ export const SchemaViewer: FC<IProps> = ({ schema, onTypeClick }) => {
<p className="font-bold">{fieldKey}</p> <p className="font-bold">{fieldKey}</p>
<p className="font-thin capitalize text-xs">{field.type}</p> <p className="font-thin capitalize text-xs">{field.type}</p>
<p className="font-thin capitalize text-xs">Maximum entries: {field.limit === 0 ? 'unlimited ' : field.limit}</p> <p className="font-thin capitalize text-xs">Maximum entries: {field.limit === 0 ? 'unlimited ' : field.limit}</p>
{(field.isConstant || fieldTypesWithValues.includes(field.type)) && <p className="font-thin capitalize text-xs">{createValueLable(field)} {field.value}</p>} {(field.isConstant || fieldTypesWithValues.includes(field.type)) && <p className="font-thin capitalize text-xs">{createValueLable(field)} <Truncate>{field.value}</Truncate></p>}
</div> </div>
))} ))}
</div> </div>

View File

@ -1,9 +1,10 @@
import { FC, useCallback } from 'react' import { FC, useCallback, useState } from 'react'
import { Template } from '../../types/schema'; import { Template } from '../../types/schema';
import { useObjectStateWrapper } from '../../hooks/useObjectState'; import { useObjectStateWrapper } from '../../hooks/useObjectState';
import { TEMPLATE_TYPES } from '../../constants/TemplateTypes'; import { TEMPLATE_TYPES } from '../../constants/TemplateTypes';
import { SchemaEditAtom } from '../../recoil/atoms/schema'; import { SchemaEditAtom } from '../../recoil/atoms/schema';
import { useRecoilValue } from 'recoil'; import { useRecoilState, useRecoilValue } from 'recoil';
import { Icon } from '../Icon';
interface IProps { interface IProps {
templateKey: string; templateKey: string;
@ -12,33 +13,51 @@ interface IProps {
} }
export const TemplateEditor: FC<IProps> = ({ templateKey, update, template }) => { export const TemplateEditor: FC<IProps> = ({ templateKey, update, template }) => {
const schema = useRecoilValue(SchemaEditAtom); const [schema, setSchema] = useRecoilState(SchemaEditAtom);
const updateTemplate = useCallback((t: Template | ((arg: Template) => Template)) => { const updateTemplate = useCallback((t: Template | ((arg: Template) => Template)) => {
update(templateKey, typeof t === 'function' ? t(template) : t) update(templateKey, typeof t === 'function' ? t(template) : t)
}, [templateKey, update, template]) }, [templateKey, update, template])
const { bindProperty, bindPropertyCheck } = useObjectStateWrapper(template, updateTemplate) const { bindProperty, bindPropertyCheck } = useObjectStateWrapper(template, updateTemplate)
const deleteTemplate = useCallback(() => {
setSchema(s => {
const templates = { ...s.templates };
delete templates[templateKey]
return {
...s,
templates
}
})
}, [setSchema, templateKey])
return ( return (
<li> <li className="odd:bg-black/50 p-2">
<p className="font-bold">{templateKey}</p> <p className="font-bold">{templateKey}</p>
<div className="flex gap-4"> <div className="flex items-center justify-between">
<label className="w-min"> <div className="flex items-center gap-4 pl-2">
Type <label className="w-min">
<input type="text" {...bindProperty('type')} list="type-editor-type-list" /> Type:
<datalist id="type-editor-type-list"> <input type="text" {...bindProperty('type', { disallowSpaces: true })} list="type-editor-type-list" />
{Object.keys(TEMPLATE_TYPES).map(k => ( <datalist id="type-editor-type-list">
<option className="capitalize" value={k}>{k}</option> {Object.keys(TEMPLATE_TYPES).map(k => (
))} <option value={k}>{k}</option>
{Object.keys(schema.types).map(k => ( ))}
<option className="capitalize" value={k}>{k}</option> {Object.keys(schema.types).map(k => (
))} <option value={k}>{k}</option>
</datalist> ))}
</label> </datalist>
<label> </label>
<input type="checkbox" {...bindPropertyCheck('publishable')} /> <label>
Can create publications <input type="checkbox" {...bindPropertyCheck('publishable')} />
</label> &nbsp;Can create publications
</label>
</div>
<button
className="no-default"
onClick={deleteTemplate}
>
<Icon icon="trash" className="svg-red-700 hover:svg-red-500 trash-can w-6 h-6" />
</button>
</div> </div>
</li> </li>
) )

View File

@ -1,4 +1,4 @@
import { FC, useCallback, useEffect } from 'react' import { useCallback, useEffect } from 'react'
import { FCC } from '../../types' import { FCC } from '../../types'
import { FieldType, FieldTypes, TypeType } from '../../types/schema' import { FieldType, FieldTypes, TypeType } from '../../types/schema'
import { useObjectState } from '../../hooks/useObjectState'; import { useObjectState } from '../../hooks/useObjectState';
@ -16,7 +16,7 @@ const constantProperties = ['metadata'];
export const TypeEditor: FCC<IProps> = ({ saveType, name, type: passedType }) => { export const TypeEditor: FCC<IProps> = ({ saveType, name, type: passedType }) => {
const { update: updateType, reset: resetType, state: type, setState: setType } = useObjectState<TypeType>({}); const { update: updateType, reset: resetType, state: type, setState: setType } = useObjectState<TypeType>({});
const { value: propertyName, setValue: setPropertyName, bind: bindPropertyName, reset: resetPropertyName } = useInput(''); const { value: propertyName, bind: bindPropertyName, reset: resetPropertyName } = useInput('', { disallowSpaces: true });
const save = () => { const save = () => {
saveType(name, type); saveType(name, type);

View File

@ -3,6 +3,7 @@ import { FieldTypes } from '../../types/schema';
import { InputBinder } from '../../types/inputBinder'; import { InputBinder } from '../../types/inputBinder';
import { FieldTypeInput } from './field-type-input'; import { FieldTypeInput } from './field-type-input';
import { useInput } from '../../hooks/useInput'; import { useInput } from '../../hooks/useInput';
import { HelpPopper } from '../Poppables/help';
interface IValueProps { interface IValueProps {
type: FieldTypes; type: FieldTypes;
@ -62,6 +63,38 @@ export const ValueField: FC<IValueProps> = ({ type, bind }) => {
return ( return (
<label className="w-min">Value:<input type="number" {...bind} /></label> <label className="w-min">Value:<input type="number" {...bind} /></label>
) )
case FieldTypes.select:
return (
<>
<label className="w-min">
<div className="flex gap-2 items-center">
Values:
<HelpPopper>
<p className="text-xs">
A comma separated list (no spaces, spaces are reserved for values) of options that can be chosen while creating publications. Ex: earthquake,wind storm,fire tornado,rainbow. Alternatively, you can specify a display value and an actual value separated with a colon. This is useful for when you want to create a reference in a publication with a dropdown field. Ex: Rapid Fire:^core.weaponAbilities[name=rapid fire],Heavy:^core.weaponAbilities[name=heavy]
</p>
</HelpPopper>
</div>
<input type="text" {...bind} />
</label>
</>
)
case FieldTypes.any:
return (
<>
<label className="w-min">
<div className="flex gap-2 items-center">
Type options:
<HelpPopper>
<p className="text-xs">
A comma separated list (no spaces, spaces are reserved for values) of options that are names of types that can be selected when creating a publication, Ex: dice,number,text. Do not leave this blank, allowing for any type to be selected makes querying gross.
</p>
</HelpPopper>
</div>
<input type="text" {...bind} />
</label>
</>
)
default: default:
return <></>; return <></>;
} }

View File

@ -45,7 +45,7 @@ export const TEMPLATE_TYPES: Record<string, TypeType> = {
value: '' value: ''
} }
}, },
tableRow: { table_row: {
columns: { columns: {
isConstant: false, isConstant: false,
limit: 0, limit: 0,

View File

@ -1,6 +1,11 @@
import { useState, ChangeEvent } from 'react'; import { useState, ChangeEvent } from 'react';
export const useInput = <T extends string | number>(initialValue: T) => { type InputHookConfig = {
disallowSpaces?: boolean;
spaceReplacer?: string;
}
export const useInput = <T extends string | number>(initialValue: T, config?: InputHookConfig) => {
const [value, setValue] = useState<T>(initialValue); const [value, setValue] = useState<T>(initialValue);
return { return {
@ -10,11 +15,11 @@ export const useInput = <T extends string | number>(initialValue: T) => {
bind: { bind: {
value: value, value: value,
onChange: (event: ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => { onChange: (event: ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
const changed: string | number = typeof initialValue === 'number' ? parseInt(event.target.value) : event.target.value; const changed: string | number = typeof initialValue === 'number' ? parseInt(event.target.value) : config?.disallowSpaces ? event.target.value.replace(' ', config.spaceReplacer || '_') : event.target.value;
setValue(changed as T); setValue(changed as T);
}
} }
}; }
};
}; };
export const useCheckbox = (initial: boolean) => { export const useCheckbox = (initial: boolean) => {

View File

@ -1,10 +1,15 @@
import React, { ChangeEvent, useCallback, useState } from 'react'; import React, { ChangeEvent, useCallback, useState } from 'react';
import { InputBinder } from '../types/inputBinder'; import { InputBinder } from '../types/inputBinder';
type ObjectStateHookConfig = {
disallowSpaces?: boolean;
spaceReplacer?: string;
}
export const useObjectState = <T extends object>(initial: T) => { export const useObjectState = <T extends object>(initial: T) => {
const [state, setState] = useState<T>(initial || {} as T); const [state, setState] = useState<T>(initial || {} as T);
const bindProperty = useCallback(<K extends keyof T>(property: K) => ({ const bindProperty = useCallback(<K extends keyof T>(property: K, config: ObjectStateHookConfig) => ({
value: state[property] ?? '', value: state[property] ?? '',
name: property, name: property,
onChange: (event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => onChange: (event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) =>
@ -14,7 +19,10 @@ export const useObjectState = <T extends object>(initial: T) => {
[event.target.name]: ( [event.target.name]: (
(typeof value[property] === 'number') ? (typeof value[property] === 'number') ?
Number(event.target.value) || 0 : Number(event.target.value) || 0 :
event.target.value) config?.disallowSpaces ?
event.target.value.replace(' ', config.spaceReplacer || '_') :
event.target.value
)
} }
)) ))
}), [state]) }), [state])
@ -46,7 +54,7 @@ export const useObjectState = <T extends object>(initial: T) => {
export const useObjectStateWrapper = <T extends object>(state: T, setState: React.Dispatch<React.SetStateAction<T>>) => { export const useObjectStateWrapper = <T extends object>(state: T, setState: React.Dispatch<React.SetStateAction<T>>) => {
const bindProperty = useCallback(<K extends keyof T>(property: K): InputBinder => ({ const bindProperty = useCallback(<K extends keyof T>(property: K, config?: ObjectStateHookConfig): InputBinder => ({
value: state[property] ?? '', value: state[property] ?? '',
name: property.toString(), name: property.toString(),
onChange: (event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => onChange: (event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) =>
@ -56,7 +64,10 @@ export const useObjectStateWrapper = <T extends object>(state: T, setState: Reac
[event.target.name]: ( [event.target.name]: (
(typeof value[property] === 'number') ? (typeof value[property] === 'number') ?
Number(event.target.value) || 0 : Number(event.target.value) || 0 :
event.target.value) config?.disallowSpaces ?
event.target.value.replace(' ', config.spaceReplacer || '_') :
event.target.value
)
} }
)) ))
}), [setState, state]) }), [setState, state])

View File

@ -41,7 +41,7 @@
@apply text-xl font-bold @apply text-xl font-bold
} }
button { button:not(.no-default) {
@apply interactive bg-olive-drab p-1 @apply interactive bg-olive-drab p-1
} }
@ -65,6 +65,26 @@
.fade-out { .fade-out {
animation: fade 300ms forwards ease-in reverse; animation: fade 300ms forwards ease-in reverse;
} }
.trash-can, .anvil {
overflow: visible;
}
.trash-can path.trash-lid, .anvil .anvil-base, .anvil .anvil-body {
transition: 300ms transform, 300ms rotate, 300ms fill, 300ms stroke;
}
.trash-can:hover path.trash-lid {
transform: translate(-5%, -10%);
rotate: -10deg;
}
.anvil:hover .anvil-base {
transform: translate(0px, 5%);
}
.anvil:hover .anvil-body {
transform: translate(0px, -5%);
}
} }
@layer utilities {} @layer utilities {}

View File

@ -0,0 +1 @@
export type WithId<T> = {id: string} & T;

View File

@ -0,0 +1,9 @@
import { WithId } from './gemerics';
export type Publication = {
name: string;
version: string;
gameSystemId: string;
status: 'published' | 'private';
templates: Record<string, WithId<any>[]>;
}

View File

@ -30,8 +30,9 @@ export enum FieldTypes {
checkbox = 'checkbox', checkbox = 'checkbox',
type = '@type', type = '@type',
dice = 'dice', dice = 'dice',
any = '@select' any = '@select',
select = 'select'
} }
export const fieldTypeOptions: (keyof typeof FieldTypes)[] = ['number', 'text', 'long text', 'checkbox', 'type', 'dice'] export const fieldTypeOptions: (keyof typeof FieldTypes)[] = ['number', 'text', 'long text', 'checkbox', 'type', 'dice', 'select', 'any']
export const fieldTypesWithValues = [FieldTypes.dice, FieldTypes.type]; export const fieldTypesWithValues = [FieldTypes.dice, FieldTypes.type, FieldTypes.select, FieldTypes.any];