New icons, better page transition, truncated popper, adds disallowSpaces option to binders, delete and edit buttons
This commit is contained in:
parent
e78e4304a4
commit
f84ef2ee19
@ -7,8 +7,8 @@ function App() {
|
||||
return (
|
||||
<RecoilRoot>
|
||||
<RecoilizeDebugger />
|
||||
{/* <SchemaBuilder /> */}
|
||||
<GameSystemEditor />
|
||||
<SchemaBuilder />
|
||||
{/* <GameSystemEditor /> */}
|
||||
</RecoilRoot>
|
||||
)
|
||||
}
|
||||
|
6
project-warstone/src/assets/icons/Anvil Icon.svg
Normal file
6
project-warstone/src/assets/icons/Anvil Icon.svg
Normal 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 |
8
project-warstone/src/assets/icons/Trash Icon Open.svg
Normal file
8
project-warstone/src/assets/icons/Trash Icon Open.svg
Normal 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 |
12
project-warstone/src/assets/icons/Trash Icon.svg
Normal file
12
project-warstone/src/assets/icons/Trash Icon.svg
Normal 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 |
@ -0,0 +1,6 @@
|
||||
.page {
|
||||
transition:
|
||||
transform 500ms,
|
||||
opacity 300ms,
|
||||
z-index 0ms 500ms
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect, ReactNode, useRef, useLayoutEffect, useCallback } from 'react';
|
||||
import { FCC } from '../../types';
|
||||
import './index.css';
|
||||
|
||||
interface IProps {
|
||||
currentPage: number;
|
||||
@ -10,12 +11,24 @@ const AnimatedPageContainer: FCC<IProps> = ({ children, currentPage }) => {
|
||||
|
||||
const renderChild = (child: ReactNode, index: number) => {
|
||||
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 (
|
||||
<div
|
||||
key={`page container ${uuid}: ${index}`}
|
||||
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}
|
||||
</div>
|
||||
@ -23,7 +36,7 @@ const AnimatedPageContainer: FCC<IProps> = ({ children, currentPage }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="relative overflow-hidden">
|
||||
{React.Children.map(children, renderChild)}
|
||||
</div>
|
||||
);
|
||||
|
@ -1,8 +1,14 @@
|
||||
import { FC } from 'react'
|
||||
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 = {
|
||||
help
|
||||
help,
|
||||
trash,
|
||||
trash_hover,
|
||||
anvil
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
|
15
project-warstone/src/components/Poppables/truncation.tsx
Normal file
15
project-warstone/src/components/Poppables/truncation.tsx
Normal 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>
|
||||
);
|
||||
}
|
19
project-warstone/src/components/PublicationEditor/index.tsx
Normal file
19
project-warstone/src/components/PublicationEditor/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -9,6 +9,7 @@ import { SchemaEditAtom } from '../../recoil/atoms/schema';
|
||||
import { GameSystemsService } from '../../services/game-systems';
|
||||
import { SchemaViewer } from './schema-viewer';
|
||||
import { TemplateEditor } from './template-editor';
|
||||
import { Icon } from '../Icon';
|
||||
|
||||
|
||||
export const SchemaBuilder: FC = () => {
|
||||
@ -60,7 +61,7 @@ export const SchemaBuilder: FC = () => {
|
||||
setPageNumber(1);
|
||||
}, [])
|
||||
|
||||
const { value: templateName, bind: bindTemplateName, reset: resetTemplateName } = useInput('');
|
||||
const { value: templateName, bind: bindTemplateName, reset: resetTemplateName } = useInput('', { disallowSpaces: true });
|
||||
const addTemplate = useCallback(() => {
|
||||
updateSchema(s => ({
|
||||
templates: {
|
||||
@ -83,25 +84,55 @@ export const SchemaBuilder: FC = () => {
|
||||
}))
|
||||
}, [updateSchema])
|
||||
|
||||
const deleteType = useCallback((key: string) => {
|
||||
updateSchema(s => {
|
||||
const types = { ...s.types };
|
||||
delete types[key];
|
||||
return { types }
|
||||
})
|
||||
}, [updateSchema])
|
||||
|
||||
return (
|
||||
<div className="container flex gap-4 p-8">
|
||||
<div className="panel w-2/3 h-full">
|
||||
<p className="subheader">Add a template</p>
|
||||
<input type="text" {...bindTemplateName} />
|
||||
<button onClick={addTemplate} disabled={!templateName}>Add</button>
|
||||
<ul>
|
||||
{Object.entries(schema.templates).map(([templateKey, template]) => (
|
||||
<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 className="panel w-2/3 h-full flex flex-col gap-4">
|
||||
<div>
|
||||
<p className="subheader mb-2">Add a template</p>
|
||||
<div className="mb-2">
|
||||
<input type="text" {...bindTemplateName} />
|
||||
<button onClick={addTemplate} disabled={!templateName}>Add</button>
|
||||
</div>
|
||||
<TypeEditor name={selectedType || typeName} saveType={saveType} type={selectedType ? schema.types[selectedType as keyof typeof schema.types] : undefined} />
|
||||
</AnimatedPageContainer>
|
||||
<ul className="rounded-lg overflow-hidden">
|
||||
{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 className="panel basis-1/3">
|
||||
<SchemaViewer schema={schema} onTypeClick={selectTypeForEdit} />
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { FC, useCallback } from 'react'
|
||||
import { FieldType, FieldTypes, Schema, TypeType, fieldTypesWithValues } from '../../types/schema'
|
||||
import { Truncate } from '../Poppables/truncation';
|
||||
|
||||
interface IProps {
|
||||
schema: Schema;
|
||||
@ -16,6 +17,7 @@ export const SchemaViewer: FC<IProps> = ({ schema, onTypeClick }) => {
|
||||
switch (field.type) {
|
||||
case FieldTypes.type: return 'Type:'
|
||||
case FieldTypes.dice: return 'Dice:'
|
||||
case FieldTypes.select: return 'Options:'
|
||||
default: return '';
|
||||
}
|
||||
}, [])
|
||||
@ -53,7 +55,7 @@ export const SchemaViewer: FC<IProps> = ({ schema, onTypeClick }) => {
|
||||
<p className="font-bold">{fieldKey}</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>
|
||||
{(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>
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { FC, useCallback } from 'react'
|
||||
import { FC, useCallback, useState } from 'react'
|
||||
import { Template } from '../../types/schema';
|
||||
import { useObjectStateWrapper } from '../../hooks/useObjectState';
|
||||
import { TEMPLATE_TYPES } from '../../constants/TemplateTypes';
|
||||
import { SchemaEditAtom } from '../../recoil/atoms/schema';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { Icon } from '../Icon';
|
||||
|
||||
interface IProps {
|
||||
templateKey: string;
|
||||
@ -12,33 +13,51 @@ interface IProps {
|
||||
}
|
||||
|
||||
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)) => {
|
||||
update(templateKey, typeof t === 'function' ? t(template) : t)
|
||||
}, [templateKey, update, template])
|
||||
|
||||
const { bindProperty, bindPropertyCheck } = useObjectStateWrapper(template, updateTemplate)
|
||||
|
||||
const deleteTemplate = useCallback(() => {
|
||||
setSchema(s => {
|
||||
const templates = { ...s.templates };
|
||||
delete templates[templateKey]
|
||||
return {
|
||||
...s,
|
||||
templates
|
||||
}
|
||||
})
|
||||
}, [setSchema, templateKey])
|
||||
return (
|
||||
<li>
|
||||
<li className="odd:bg-black/50 p-2">
|
||||
<p className="font-bold">{templateKey}</p>
|
||||
<div className="flex gap-4">
|
||||
<label className="w-min">
|
||||
Type
|
||||
<input type="text" {...bindProperty('type')} list="type-editor-type-list" />
|
||||
<datalist id="type-editor-type-list">
|
||||
{Object.keys(TEMPLATE_TYPES).map(k => (
|
||||
<option className="capitalize" value={k}>{k}</option>
|
||||
))}
|
||||
{Object.keys(schema.types).map(k => (
|
||||
<option className="capitalize" value={k}>{k}</option>
|
||||
))}
|
||||
</datalist>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" {...bindPropertyCheck('publishable')} />
|
||||
Can create publications
|
||||
</label>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4 pl-2">
|
||||
<label className="w-min">
|
||||
Type:
|
||||
<input type="text" {...bindProperty('type', { disallowSpaces: true })} list="type-editor-type-list" />
|
||||
<datalist id="type-editor-type-list">
|
||||
{Object.keys(TEMPLATE_TYPES).map(k => (
|
||||
<option value={k}>{k}</option>
|
||||
))}
|
||||
{Object.keys(schema.types).map(k => (
|
||||
<option value={k}>{k}</option>
|
||||
))}
|
||||
</datalist>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" {...bindPropertyCheck('publishable')} />
|
||||
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>
|
||||
</li>
|
||||
)
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { FC, useCallback, useEffect } from 'react'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { FCC } from '../../types'
|
||||
import { FieldType, FieldTypes, TypeType } from '../../types/schema'
|
||||
import { useObjectState } from '../../hooks/useObjectState';
|
||||
@ -16,7 +16,7 @@ const constantProperties = ['metadata'];
|
||||
export const TypeEditor: FCC<IProps> = ({ saveType, name, type: passedType }) => {
|
||||
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 = () => {
|
||||
saveType(name, type);
|
||||
|
@ -3,6 +3,7 @@ import { FieldTypes } from '../../types/schema';
|
||||
import { InputBinder } from '../../types/inputBinder';
|
||||
import { FieldTypeInput } from './field-type-input';
|
||||
import { useInput } from '../../hooks/useInput';
|
||||
import { HelpPopper } from '../Poppables/help';
|
||||
|
||||
interface IValueProps {
|
||||
type: FieldTypes;
|
||||
@ -62,6 +63,38 @@ export const ValueField: FC<IValueProps> = ({ type, bind }) => {
|
||||
return (
|
||||
<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:
|
||||
return <></>;
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ export const TEMPLATE_TYPES: Record<string, TypeType> = {
|
||||
value: ''
|
||||
}
|
||||
},
|
||||
tableRow: {
|
||||
table_row: {
|
||||
columns: {
|
||||
isConstant: false,
|
||||
limit: 0,
|
||||
|
@ -1,6 +1,11 @@
|
||||
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);
|
||||
|
||||
return {
|
||||
@ -10,11 +15,11 @@ export const useInput = <T extends string | number>(initialValue: T) => {
|
||||
bind: {
|
||||
value: value,
|
||||
onChange: (event: ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
|
||||
const changed: string | number = typeof initialValue === 'number' ? parseInt(event.target.value) : event.target.value;
|
||||
setValue(changed as T);
|
||||
}
|
||||
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);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const useCheckbox = (initial: boolean) => {
|
||||
|
@ -1,10 +1,15 @@
|
||||
import React, { ChangeEvent, useCallback, useState } from 'react';
|
||||
import { InputBinder } from '../types/inputBinder';
|
||||
|
||||
type ObjectStateHookConfig = {
|
||||
disallowSpaces?: boolean;
|
||||
spaceReplacer?: string;
|
||||
}
|
||||
|
||||
export const useObjectState = <T extends object>(initial: 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] ?? '',
|
||||
name: property,
|
||||
onChange: (event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) =>
|
||||
@ -14,7 +19,10 @@ export const useObjectState = <T extends object>(initial: T) => {
|
||||
[event.target.name]: (
|
||||
(typeof value[property] === 'number') ?
|
||||
Number(event.target.value) || 0 :
|
||||
event.target.value)
|
||||
config?.disallowSpaces ?
|
||||
event.target.value.replace(' ', config.spaceReplacer || '_') :
|
||||
event.target.value
|
||||
)
|
||||
}
|
||||
))
|
||||
}), [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>>) => {
|
||||
|
||||
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] ?? '',
|
||||
name: property.toString(),
|
||||
onChange: (event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) =>
|
||||
@ -56,7 +64,10 @@ export const useObjectStateWrapper = <T extends object>(state: T, setState: Reac
|
||||
[event.target.name]: (
|
||||
(typeof value[property] === 'number') ?
|
||||
Number(event.target.value) || 0 :
|
||||
event.target.value)
|
||||
config?.disallowSpaces ?
|
||||
event.target.value.replace(' ', config.spaceReplacer || '_') :
|
||||
event.target.value
|
||||
)
|
||||
}
|
||||
))
|
||||
}), [setState, state])
|
||||
|
@ -41,7 +41,7 @@
|
||||
@apply text-xl font-bold
|
||||
}
|
||||
|
||||
button {
|
||||
button:not(.no-default) {
|
||||
@apply interactive bg-olive-drab p-1
|
||||
}
|
||||
|
||||
@ -65,6 +65,26 @@
|
||||
.fade-out {
|
||||
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 {}
|
||||
|
1
project-warstone/src/types/gemerics.ts
Normal file
1
project-warstone/src/types/gemerics.ts
Normal file
@ -0,0 +1 @@
|
||||
export type WithId<T> = {id: string} & T;
|
9
project-warstone/src/types/publication.ts
Normal file
9
project-warstone/src/types/publication.ts
Normal 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>[]>;
|
||||
}
|
@ -30,8 +30,9 @@ export enum FieldTypes {
|
||||
checkbox = 'checkbox',
|
||||
type = '@type',
|
||||
dice = 'dice',
|
||||
any = '@select'
|
||||
any = '@select',
|
||||
select = 'select'
|
||||
}
|
||||
|
||||
export const fieldTypeOptions: (keyof typeof FieldTypes)[] = ['number', 'text', 'long text', 'checkbox', 'type', 'dice']
|
||||
export const fieldTypesWithValues = [FieldTypes.dice, FieldTypes.type];
|
||||
export const fieldTypeOptions: (keyof typeof FieldTypes)[] = ['number', 'text', 'long text', 'checkbox', 'type', 'dice', 'select', 'any']
|
||||
export const fieldTypesWithValues = [FieldTypes.dice, FieldTypes.type, FieldTypes.select, FieldTypes.any];
|
||||
|
Loading…
x
Reference in New Issue
Block a user