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 (
|
return (
|
||||||
<RecoilRoot>
|
<RecoilRoot>
|
||||||
<RecoilizeDebugger />
|
<RecoilizeDebugger />
|
||||||
{/* <SchemaBuilder /> */}
|
<SchemaBuilder />
|
||||||
<GameSystemEditor />
|
{/* <GameSystemEditor /> */}
|
||||||
</RecoilRoot>
|
</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 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>
|
||||||
);
|
);
|
||||||
|
@ -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 {
|
||||||
|
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 { 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>
|
||||||
|
<p className="subheader mb-2">Add a template</p>
|
||||||
|
<div className="mb-2">
|
||||||
<input type="text" {...bindTemplateName} />
|
<input type="text" {...bindTemplateName} />
|
||||||
<button onClick={addTemplate} disabled={!templateName}>Add</button>
|
<button onClick={addTemplate} disabled={!templateName}>Add</button>
|
||||||
<ul>
|
</div>
|
||||||
|
<ul className="rounded-lg overflow-hidden">
|
||||||
{Object.entries(schema.templates).map(([templateKey, template]) => (
|
{Object.entries(schema.templates).map(([templateKey, template]) => (
|
||||||
<TemplateEditor templateKey={templateKey} template={template} update={updateTemplate} />
|
<TemplateEditor templateKey={templateKey} template={template} update={updateTemplate} />
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<div>
|
||||||
<AnimatedPageContainer currentPage={pageNumber}>
|
<AnimatedPageContainer currentPage={pageNumber}>
|
||||||
<div>
|
<div>
|
||||||
<p className="subheader">Add a type</p>
|
<p className="subheader mb-2">Add a type</p>
|
||||||
<input type="text" {...bindTypeName} />
|
<input type="text" {...bindTypeName} />
|
||||||
<button className="interactive" disabled={!typeName} onClick={() => setPageNumber(1)}>Configure</button>
|
<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} />
|
<TypeEditor name={selectedType || typeName} saveType={saveType} type={selectedType ? schema.types[selectedType as keyof typeof schema.types] : undefined} />
|
||||||
</AnimatedPageContainer>
|
</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} />
|
||||||
|
@ -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>
|
||||||
|
@ -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,34 +13,52 @@ 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">
|
||||||
|
<div className="flex items-center gap-4 pl-2">
|
||||||
<label className="w-min">
|
<label className="w-min">
|
||||||
Type
|
Type:
|
||||||
<input type="text" {...bindProperty('type')} list="type-editor-type-list" />
|
<input type="text" {...bindProperty('type', { disallowSpaces: true })} list="type-editor-type-list" />
|
||||||
<datalist id="type-editor-type-list">
|
<datalist id="type-editor-type-list">
|
||||||
{Object.keys(TEMPLATE_TYPES).map(k => (
|
{Object.keys(TEMPLATE_TYPES).map(k => (
|
||||||
<option className="capitalize" value={k}>{k}</option>
|
<option value={k}>{k}</option>
|
||||||
))}
|
))}
|
||||||
{Object.keys(schema.types).map(k => (
|
{Object.keys(schema.types).map(k => (
|
||||||
<option className="capitalize" value={k}>{k}</option>
|
<option value={k}>{k}</option>
|
||||||
))}
|
))}
|
||||||
</datalist>
|
</datalist>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" {...bindPropertyCheck('publishable')} />
|
<input type="checkbox" {...bindPropertyCheck('publishable')} />
|
||||||
Can create publications
|
Can create publications
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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>
|
</li>
|
||||||
)
|
)
|
||||||
}
|
}
|
@ -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);
|
||||||
|
@ -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 <></>;
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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,7 +15,7 @@ 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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])
|
||||||
|
@ -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 {}
|
||||||
|
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',
|
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];
|
||||||
|
Loading…
x
Reference in New Issue
Block a user