adds accordion, details query syntax, various small tweaks

This commit is contained in:
Emma 2023-06-15 05:18:45 -06:00
parent f84ef2ee19
commit 4d08f91921
12 changed files with 291 additions and 23 deletions

View File

@ -0,0 +1,50 @@
Start query: ?
- by default, queries search the publication
Query current object: ^
Access child: .
- this also maps over all items in a list and returns a list of the relevant children
- if the parent is a list, it will return a list of just the specified child
Select: []
- Selects any of the items that matches the provided selector
- Providing a number selects the ordinal item in the list
- Providing a comparator selects items that match the comparator
- Selectors can be separated by either commas for "and" or slashes for "or", but not both
Comparators:
= is similar to
== is exactly the same
> greater than
< less than
>= greater than or equal to
<= less than or equal to
!! is true/exists
! is not
/ or, allows matching one or more comparators at the same time
Combiner: ()
- Will only select items that match all of the selectors
- Selectors can be separated by either commas for "and" or slashes for "or", but not both
--Templating--
insertion: {{}}
- Allows for queries to be wrapped in a templated string
- Can be either a direct query or used in combination with the "_" character to reference a single query
single query: <<?
- Used the same as start query, but appended to the end of a templated string
- Used in conjunction with "_" to reference the queried value
query reference: _
- used in conjunction with single query to reference the queried value
separator: ::;;
- used at the end of a template to indicate how to separate results when the results are more than one.
- Whatever text is placed between in the middle is treated as the full separator, or in other words, if the separator doesn't have any spaces, then there will be no spaces between items in the final text
- If no separator is provided, it will default to comma and space separation
- placed at the end of the template but before the single query
- should support any standard escaped characters such as newline
Examples:
?core.weapon_abilities[name=Rapid Fire].body
- this searches the publication 'core' object for weapon_abilities that have the name
Sustained Hits {{?^sustained_hits}}
- This formats the string with the queried values, which start the query within the same object that the field exists in. This would result in the string 'Sustained Hits 1'
Anti-{{_.keyword}} {{_.value}}<<?^anti_abilities
- This formats the string with the queried values, which start the query within the same object that the field exists in. This would result in the string 'Anti-Infantry 2+' or 'Anti-Infantry 2+, Anti-Monster 4+' if there are more than one matching values
Anti-{{_.keyword}} {{_.value}}:://;;<<?^anti_abilities
- This formats the string with the queried values, which start the query within the same object that the field exists in. This would result in the string 'Anti-Infantry 2+' or 'Anti-Infantry 2+//Anti-Monster 4+' if there are more than one matching values

View File

@ -3,14 +3,16 @@ import { FieldType, FieldTypes, fieldTypeOptions, fieldTypesWithValues } from '.
import { useObjectStateWrapper } from '../../hooks/useObjectState';
import { ValueField } from './value-field';
import { HelpPopper } from '../Poppables/help';
import { Icon } from '../Icon';
interface IProps {
update: (arg: FieldType) => void;
field: FieldType;
fieldName: string;
deleteField: (arg: string) => void;
}
export const FieldEditor: FC<IProps> = ({ update, field, fieldName }) => {
export const FieldEditor: FC<IProps> = ({ update, field, fieldName, deleteField }) => {
const { bindProperty, bindPropertyCheck } = useObjectStateWrapper(field, (e) => update(typeof e === 'function' ? e(field) : e))
const shouldShowValueField = useCallback(() => fieldTypesWithValues.includes(field.type) || field.isConstant, [field.isConstant, field.type]);
@ -22,7 +24,7 @@ export const FieldEditor: FC<IProps> = ({ update, field, fieldName }) => {
return (
<li className="odd:bg-black/50">
<p>{fieldName}</p>
<div className=" flex gap-x-4 items-center p-2">
<div className=" flex gap-x-4 items-center p-2 w-full">
<label className="w-min">
Field Type:&nbsp;
<select className="capitalize" {...bindProperty('type')}>
@ -40,10 +42,20 @@ export const FieldEditor: FC<IProps> = ({ update, field, fieldName }) => {
<p className="text-sm">Constant values can't be overwritten in publications. When a dice field is set to a constant value, it instead rolls a dice of that value whenever this field is displayed (unless exported). This could be useful for a randomly generated scenario or for cards being drawn as the dice value will automatically be determined by the dice roll.</p>
</HelpPopper>
</span>
<label className="w-min">
Minimum:
<input className="w-12" type="number" {...bindProperty('minimum')} />
</label>
<label className="w-min">
Limit:
<input className="w-12" type="number" {...bindProperty('limit')} />
</label>
<HelpPopper>
<p className="text-sm">Minimum and Limit apply to the number of entries allowed for this field, not the maximum and minimum value. Set the minimum to 0 to make a field optional. Set the limit to 0 to allow for unlimited entries.</p>
</HelpPopper>
<button className="no-default self-end ml-auto" onClick={() => deleteField(fieldName)}>
<Icon className="svg-red-700 hover:svg-red-500 trash-can w-6 h-6" icon="trash"></Icon>
</button>
</div>
</li>
)

View File

@ -135,8 +135,7 @@ export const SchemaBuilder: FC = () => {
</div>
</div>
<div className="panel basis-1/3">
<SchemaViewer schema={schema} onTypeClick={selectTypeForEdit} />
<div className="flex gap-2 mt-2">
<div className="flex gap-2 mb-2">
<button
onClick={saveSchema}
disabled={lastSaved === schema}
@ -151,6 +150,7 @@ export const SchemaBuilder: FC = () => {
Discard Changes
</button>
</div>
<SchemaViewer schema={schema} onTypeClick={selectTypeForEdit} />
</div>
</div>
)

View File

@ -1,6 +1,7 @@
import { FC, useCallback } from 'react'
import { FieldType, FieldTypes, Schema, TypeType, fieldTypesWithValues } from '../../types/schema'
import { Truncate } from '../Poppables/truncation';
import { Accordion, AccordionContent } from '../../lib/accordion';
interface IProps {
schema: Schema;
@ -40,25 +41,30 @@ export const SchemaViewer: FC<IProps> = ({ schema, onTypeClick }) => {
</ul>
<hr />
<p className="font-bold italic">Types</p>
<ul className="rounded-lg overflow-hidden">
<ul className="rounded-lg overflow-hidden grid">
{Object.entries(schema.types).map(([typeKey, type]) => (
<li
key={'type viewer' + typeKey}
onClick={() => onTypeClick && onTypeClick(typeKey, type)}
// onClick={() => onTypeClick && onTypeClick(typeKey, type)}
data-clickable={!!onTypeClick}
className="odd:bg-black/50 p-2 data-[clickable=true]:cursor-pointer"
className="odd:bg-black/50 p-2 group overflow-hidden"
>
<p className="mb-2 font-bold">{typeKey}</p>
<div className="grid grid-cols-3 gap-2">
{Object.entries(type).map(([fieldKey, field]) => (
<div key={'field viewer' + fieldKey} className="rounded-lg border border-olive-drab p-2">
<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)} <Truncate>{field.value}</Truncate></p>}
<Accordion
title={<p className="group-data-[expanded]/controlled:mb-2 transition-all font-bold">{typeKey}</p>}
>
<AccordionContent>
<div className="grid grid-cols-2 gap-2">
{Object.entries(type).map(([fieldKey, field]) => (
<div key={'field viewer' + fieldKey} className="rounded-lg border border-olive-drab p-2">
<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)} <Truncate>{field.value}</Truncate></p>}
</div>
))}
</div>
))}
</div>
</AccordionContent>
</Accordion>
</li>
))}
</ul>

View File

@ -30,6 +30,7 @@ export const TypeEditor: FCC<IProps> = ({ saveType, name, type: passedType }) =>
value: '',
isConstant: false,
limit: 1,
minimum: 1,
}
});
resetPropertyName();
@ -43,14 +44,22 @@ export const TypeEditor: FCC<IProps> = ({ saveType, name, type: passedType }) =>
passedType && setType(passedType);
}, [passedType, setType]);
const deleteField = useCallback((name: string) => {
setType(t => {
const fields = {...t};
delete fields[name];
return fields;
})
}, [setType])
return (
<div>
<p className="subheader">{passedType ? 'Editing' : 'Creating'} type "{name}"</p>
<input type="text" {...bindPropertyName} />
<button disabled={!propertyName} onClick={addField}>Add Field</button>
<ul className="rounded-lg overflow-hidden">
{Object.entries(type).filter(([k]) => !constantProperties.includes(k)).map(([key, value]) => (
<FieldEditor field={value} update={updateField(key)} fieldName={key} />
{Object.entries(type).reverse().filter(([k]) => !constantProperties.includes(k)).map(([key, value]) => (
<FieldEditor field={value} update={updateField(key)} fieldName={key} deleteField={deleteField} />
))}
</ul>
<div>

View File

@ -61,7 +61,7 @@ export const ValueField: FC<IValueProps> = ({ type, bind }) => {
)
case FieldTypes.text:
return (
<label className="w-min">Value:<input type="number" {...bind} /></label>
<label className="w-min">Value:<input type="text" {...bind} /></label>
)
case FieldTypes.select:
return (

View File

@ -5,12 +5,14 @@ export const TEMPLATE_TYPES: Record<string, TypeType> = {
name: {
isConstant: false,
limit: 1,
minimum: 1,
type: FieldTypes.text,
value: ''
},
body: {
isConstant: false,
limit: 0,
minimum: 1,
type: FieldTypes['long text'],
value: ''
},
@ -19,6 +21,7 @@ export const TEMPLATE_TYPES: Record<string, TypeType> = {
steps: {
isConstant: false,
limit: 0,
minimum: 1,
type: FieldTypes.type,
value: 'section'
}
@ -27,12 +30,14 @@ export const TEMPLATE_TYPES: Record<string, TypeType> = {
name: {
isConstant: false,
limit: 1,
minimum: 1,
type: FieldTypes.text,
value: ''
},
link: {
isConstant: false,
limit: 1,
minimum: 1,
type: FieldTypes.text,
value: ''
},
@ -41,6 +46,7 @@ export const TEMPLATE_TYPES: Record<string, TypeType> = {
items: {
isConstant: false,
limit: 0,
minimum: 1,
type: FieldTypes['long text'],
value: ''
}
@ -49,6 +55,7 @@ export const TEMPLATE_TYPES: Record<string, TypeType> = {
columns: {
isConstant: false,
limit: 0,
minimum: 1,
type: FieldTypes.any,
value: ''
}
@ -57,12 +64,14 @@ export const TEMPLATE_TYPES: Record<string, TypeType> = {
rows: {
isConstant: false,
limit: 0,
minimum: 1,
type: FieldTypes.type,
value: 'tableRow'
},
header: {
isConstant: false,
limit: 1,
minimum: 0,
type: FieldTypes.type,
value: 'tableRow'
}

View File

@ -42,7 +42,11 @@
}
button:not(.no-default) {
@apply interactive bg-olive-drab p-1
@apply interactive p-1
}
button:not([class*="bg-"]):not(.no-default) {
@apply bg-olive-drab
}
.interactive {
@ -66,11 +70,14 @@
animation: fade 300ms forwards ease-in reverse;
}
.trash-can, .anvil {
.trash-can,
.anvil {
overflow: visible;
}
.trash-can path.trash-lid, .anvil .anvil-base, .anvil .anvil-body {
.trash-can path.trash-lid,
.anvil .anvil-base,
.anvil .anvil-body {
transition: 300ms transform, 300ms rotate, 300ms fill, 300ms stroke;
}
@ -82,6 +89,7 @@
.anvil:hover .anvil-base {
transform: translate(0px, 5%);
}
.anvil:hover .anvil-body {
transform: translate(0px, -5%);
}

View File

@ -0,0 +1,68 @@
import { FC, PropsWithChildren, ReactNode, useCallback, useState } from 'react'
import { FCC } from '../../types';
interface IProps {
expandOnHover?: boolean;
expanded?: boolean;
title?: ReactNode;
}
export const Accordion: FCC<IProps> = ({ children, expandOnHover, expanded, title }) => {
const [open, setOpen] = useState(false);
return (
<div
data-expanded={open || expanded}
data-expandOnHover={expandOnHover}
className={(expandOnHover ? 'group/hover' : 'group/controlled') + ' group'}
onClick={() => !title && !expandOnHover && setOpen(!open)}
>
{!!title && (
<div className="flex justify-between cursor-pointer" onClick={() => !expandOnHover && setOpen(!open)}>
{title}
<div
className={`
group-hover/hover:-rotate-180
group-data-[expanded]:-rotate-180
transition-transform
duration-500
grid
rounded-full
h-min
mr-2
mt-1
scale-y-50
`}
>
<span className="block w-2 h-2 rotate-45 border-r-2 border-b-2 place-self-center"></span>
<span className="block w-2 h-2 rotate-45 border-r-2 border-b-2 place-self-center"></span>
</div>
</div>
)}
{children}
</div>
)
}
export const AccordionContent: FC<PropsWithChildren> = ({ children }) => {
const [height, setHeight] = useState(0);
const updateRef = useCallback((node: HTMLDivElement | null) => {
if (node) {
setHeight(node.clientHeight)
} else {
setHeight(0)
}
}, [])
const Child = () => <div className="absolute bottom-0 w-full" ref={updateRef}>
{children}
</div>
return (
<div className="relative overflow-hidden">
{<Child />}
<span style={{ ['--v-height' as never]: height + 'px' }} className="w-0 block h-0 group-hover/hover:h-variable group-data-[expanded]/controlled:h-variable transition-all duration-700" />
</div>
);
}

View File

@ -7,6 +7,7 @@ export type FieldType = {
value: string;
isConstant: boolean;
limit: number;
minimum: number;
};
export type TypeType = Record<string, FieldType>;

View File

@ -50,6 +50,9 @@ export default {
},
container: {
center: true
},
height: {
variable: 'var(--v-height)'
}
},
},

102
project-warstone/temp.json Normal file
View File

@ -0,0 +1,102 @@
{
"id": "286f4c18-d280-444b-8d7e-9a3dd09f64ef",
"name": "Warhammer 40,000 10e v1",
"types": {
"Wargear_Profile": {
"name": {
"type": "text",
"value": "",
"isConstant": false,
"limit": 1,
"minimum": "1"
},
"abilities": {
"type": "select",
"value": "Assault,Pistol,Rapid Fire,Torrent,Ignores Cover,Lethal Hits,Twin-linked,Lance,Indirect Fire,Blast,Melta,Precision,Heavy,Hazardous,Sustained Hits:Sustained Hits {{?^sustained_hits}},Extra Attacks,Devastating Wounds,Anti:Anti-{{_.keyword}} {{_.count}}+<<?^sustained_hits",
"isConstant": false,
"limit": 0,
"minimum": "1"
},
"range": {
"type": "text",
"value": "",
"isConstant": false,
"limit": 1,
"minimum": 1
},
"attacks": {
"type": "@select",
"value": "number,dice",
"isConstant": false,
"limit": 1,
"minimum": 1
},
"bs_or_ws": {
"type": "select",
"value": "ballistic_skill,weapon_skill",
"isConstant": false,
"limit": 1,
"minimum": 1
},
"bs_ws": {
"type": "number",
"value": "",
"isConstant": false,
"limit": 1,
"minimum": 1
},
"bs_ws_formatted": {
"type": "text",
"value": "{{?^bs_ws}}+",
"isConstant": true,
"limit": 1,
"minimum": 1
},
"strength": {
"type": "@select",
"value": "number,dice",
"isConstant": false,
"limit": 1,
"minimum": 1
},
"ap": {
"type": "@select",
"value": "number,dice",
"isConstant": false,
"limit": 1,
"minimum": 1
},
"damage": {
"type": "@select",
"value": "number,dice",
"isConstant": false,
"limit": 1,
"minimum": 1
},
"additional_abilities": {
"type": "long text",
"value": "",
"isConstant": false,
"limit": 0,
"minimum": 0
}
},
"anti": {
"keyword": {
"type": "number",
"value": "",
"isConstant": false,
"limit": 1,
"minimum": 1
},
"count": {
"type": "number",
"value": "",
"isConstant": false,
"limit": 1,
"minimum": 1
}
}
},
"templates": {}
}