diff --git a/README.md b/README.md
index a50b360..dbd2a19 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@ after:
```tsx
const element = new UIElement();
-element.widget = ;
+render(, element);
refObject.addUI(element);
```
@@ -34,11 +34,12 @@ Add the "jsx" and "jsxFactory" fields to compilerOptions, and be sure to include
```json
{
"compilerOptions" {
- ...,
+ /* ... other compilerOptions ... */,
"jsx": "react",
"jsxFactory": "jsxInTTPG",
+ "jsxFragmentFactory": "jsxFrag"
},
- "include": [..., "./src/**/*.tsx"]
+ "include": [/* other includes */, "./src/**/*.tsx"]
},
```
@@ -47,6 +48,54 @@ additionally, you'll need to add this import at the top of any file that uses JS
`import { jsxInTTPG } from "jsx-in-ttpg";`
+## adding JSX to a UIElement or ScreenUIElement
+
+use the provider "render" function to add JSX to the UIElement/ScreenUIElement. The top-level JSX tag must be a vanilla widget (or a string or array string, since jsxInTTPG will wrap a lone string in a `` widget automagically). This could also be a custom component, so long as any nested components resolving to a top-level widget.
+
+```tsx
+`import { render, jsxInTTPG, jsxFrag } from "jsx-in-ttpg";`;
+
+const ui = new UIElement();
+/* do stuff to set up ui element */
+
+render(Hello There, ui);
+```
+
+## Fragments
+
+a custom component should always return a single JSXNode (or a primitive, null, etc, etc). If it turns out that you need to return multiple elements, wrap the returned elements in a fragment. You will need to import {jsxFrag} from 'jsx-in-ttpg' to do so;
+
+```tsx
+`import { render, jsxInTTPG, jsxFrag } from "jsx-in-ttpg";`;
+
+const MyComponent = () => {
+ return (
+ <>
+
+ Hello
+ World
+
+
+ Some fancy [b]bolded[/b] text
+
+ >
+ );
+};
+
+const ui = new UIElement();
+
+render(
+
+
+
+
+ ,
+ ui
+);
+
+refObject.addUI(ui);
+```
+
# Syntax
Every Tabletop Playground widget is resprestented in a JSX intrinsic element with certain attributes that function as function calls or property setters for that widget.
@@ -496,7 +545,7 @@ const RobPanel = (props: { children?: SingleNode; title: TextNode; onClose?: ()
};
const element = new UIElement();
-element.widget = Hi There;
+render(Hi There, element);
refObject.addUI(element);
```
@@ -524,7 +573,7 @@ const checkRef = () => {
}
};
-element.widget = (
+render(
@@ -534,7 +583,8 @@ element.widget = (
-
+ ,
+ element
);
refObject.addUI(element);
@@ -552,7 +602,7 @@ const checkRef = () => {
console.log(imageElement.getTintColor());
};
-element.widget = (
+render(
{imageElement}
@@ -560,7 +610,8 @@ element.widget = (
-
+ ,
+ element
);
refObject.addUI(element);
diff --git a/package.json b/package.json
index ea6435f..0498d5f 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "jsx-in-ttpg",
"license": "UNLICENSE",
- "version": "1.0.0",
+ "version": "1.1.0",
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts --no-splitting",
"clean": "rm -rf ./dist",
diff --git a/src/index.ts b/src/index.ts
index 8e9d5ba..0966e14 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,5 +1,3 @@
-import "./jsx.d";
-
import {
Text,
Border,
@@ -22,6 +20,12 @@ import {
SelectionBox,
Slider,
TextBox,
+ HorizontalAlignment,
+ Player,
+ TextJustification,
+ VerticalAlignment,
+ UIElement,
+ ScreenUIElement,
} from "@tabletop-playground/api";
type CanvasChild = {
@@ -48,18 +52,30 @@ export const useRef = (initial: T | null = null): RefHandle
return ref;
};
+export const render = (widget: JSX.Element, element: UIElement | ScreenUIElement) => {
+ if (!(widget instanceof Widget)) {
+ throw Error("Top-level JSX.Element must be a widget");
+ }
+ element.widget = widget;
+};
+
+export const asTextNode = (children: JSXNode): TextNode => {
+ if (!(children instanceof Widget)) {
+ return children;
+ }
+ return undefined;
+};
+
type ArrayOr = T | T[];
+export type JSXNode = JSX.Element;
export type RefHandle = { current: T | null; clear: () => void };
export type RefObject = { current: T | null };
-export type SingleNode = Widget | ArrayOr;
-export type MultiNode = ArrayOr;
+type PossibleChildren = JSX.Element | ArrayOr | ArrayOr> | ArrayOr> | TextNode;
export type TextNode = ArrayOr;
-export type BoxNode = ArrayOr>;
-export type CanvasNode = ArrayOr>;
-export const boxChild = (weight: number, element: SingleNode): BoxChild => {
+export const boxChild = (weight: number, element: JSX.Element): BoxChild => {
return {
tag: "boxchild",
weight,
@@ -67,7 +83,7 @@ export const boxChild = (weight: number, element: SingleNode): BoxChild => {
+export const canvasChild = ({ x, y, width, height }: { x: number; y: number; width: number; height: number }, element: JSX.Element): CanvasChild => {
return {
tag: "canvaschild",
x,
@@ -121,8 +137,6 @@ const ensureWidgets = (...children: PossibleChildren[]): Widget[] => {
}, []);
};
-type PossibleChildren = SingleNode | MultiNode | TextNode | BoxNode | CanvasNode;
-
const ensureCanvasChildren = (...children: PossibleChildren[]): CanvasChild[] => {
return children.reduce[]>((acc, child) => {
if (child === null || child === undefined || typeof child === "boolean" || typeof child === "string" || typeof child === "number") {
@@ -193,6 +207,13 @@ export const jsxInTTPG = (tag: ((props: any) => Widget) | keyof JSX.IntrinsicEle
return element;
};
+export const jsxFrag = (props?: { [key: string]: any }, ...children: PossibleChildren[]): PossibleChildren | PossibleChildren[] => {
+ if (children.length === 1) {
+ return children[0];
+ }
+ return children;
+};
+
const createElement = (tag: T, attrs: { [key: string]: any }, children: PossibleChildren[]) => {
switch (tag) {
case "canvas":
@@ -610,3 +631,305 @@ const INPUT_TYPES = {
integer: 3,
"positive-integer": 4,
};
+
+declare global {
+ namespace JSX {
+ type Element = Widget | ArrayOr;
+ interface ElementChildrenAttribute {
+ children: {};
+ }
+
+ interface IntrinsicElements {
+ image: {
+ ref?: { current: ImageWidget | null };
+ disabled?: boolean;
+ hidden?: boolean;
+ onLoad?: (image: ImageWidget, filename: string, packageId: string) => void;
+ color?: Color | [number, number, number, number];
+ width?: number;
+ height?: number;
+ children?: never;
+ } & (
+ | { url: string }
+ | {
+ card: Card;
+ }
+ | {
+ src: string;
+ srcPackage?: string;
+ }
+ );
+ imagebutton: {
+ ref?: { current: ImageButton | null };
+ disabled?: boolean;
+ hidden?: boolean;
+ onLoad?: (image: ImageButton, filename: string, packageId: string) => void;
+ color?: Color | [number, number, number, number];
+ width?: number;
+ height?: number;
+ onClick?: (image: ImageButton, player: Player) => void;
+ children?: never;
+ } & (
+ | { url: string }
+ | {
+ card: Card;
+ }
+ | {
+ src: string;
+ srcPackage?: string;
+ }
+ );
+ contentbutton: {
+ ref?: { current: ContentButton | null };
+ disabled?: boolean;
+ hidden?: boolean;
+ onClick?: (image: ContentButton, player: Player) => void;
+ children?: JSX.Element;
+ };
+ border: {
+ ref?: { current: Border | null };
+ disabled?: boolean;
+ hidden?: boolean;
+ color?: Color | [number, number, number, number];
+ children?: JSX.Element;
+ };
+ canvas: {
+ ref?: { current: Canvas | null };
+ disabled?: boolean;
+ hidden?: boolean;
+ children?: ArrayOr>;
+ };
+ horizontalbox: {
+ ref?: { current: HorizontalBox | null };
+ disabled?: boolean;
+ hidden?: boolean;
+ gap?: number;
+ valign?: VerticalAlignment;
+ halign?: HorizontalAlignment;
+ children?: ArrayOr>;
+ };
+ verticalbox: {
+ ref?: { current: VerticalBox | null };
+ disabled?: boolean;
+ hidden?: boolean;
+ gap?: number;
+ valign?: VerticalAlignment;
+ halign?: HorizontalAlignment;
+ children?: ArrayOr>;
+ };
+ layout: {
+ ref?: { current: LayoutBox | null };
+ disabled?: boolean;
+ hidden?: boolean;
+ valign?: VerticalAlignment;
+ halign?: HorizontalAlignment;
+ padding?: {
+ left?: number;
+ right?: number;
+ top?: number;
+ bottom?: number;
+ };
+ maxHeight?: number;
+ minHeight?: number;
+ height?: number;
+ minWidth?: number;
+ maxWidth?: number;
+ width?: number;
+ children?: JSX.Element;
+ };
+ text: {
+ ref?: { current: Text | null };
+ disabled?: boolean;
+ hidden?: boolean;
+ bold?: boolean;
+ italic?: boolean;
+ size?: number;
+ color?: Color | [number, number, number, number];
+ wrap?: boolean;
+ justify?: TextJustification;
+ children?: TextNode;
+ } & (
+ | {
+ font?: string;
+ }
+ | {
+ font: string;
+ fontPackage?: string;
+ }
+ );
+ button: {
+ ref?: { current: Button | null };
+ disabled?: boolean;
+ hidden?: boolean;
+ bold?: boolean;
+ italic?: boolean;
+ size?: number;
+ color?: Color | [number, number, number, number];
+ onClick?: (button: Button, player: Player) => void;
+ children?: TextNode;
+ } & (
+ | {
+ font?: string;
+ }
+ | {
+ font: string;
+ fontPackage?: string;
+ }
+ );
+ checkbox: {
+ ref?: { current: CheckBox | null };
+ disabled?: boolean;
+ hidden?: boolean;
+ bold?: boolean;
+ italic?: boolean;
+ size?: number;
+ color?: Color | [number, number, number, number];
+ onChange?: (checkbox: CheckBox, player: Player | undefined, state: boolean) => void;
+ checked?: boolean;
+ label?: string | string[];
+ children?: never;
+ } & (
+ | {
+ font?: string;
+ }
+ | {
+ font: string;
+ fontPackage?: string;
+ }
+ );
+ textarea: {
+ ref?: { current: MultilineTextBox | null };
+ disabled?: boolean;
+ hidden?: boolean;
+ bold?: boolean;
+ italic?: boolean;
+ size?: number;
+ color?: Color | [number, number, number, number];
+ onChange?: (element: MultilineTextBox, player: Player | undefined, text: string) => void;
+ onCommit?: (element: MultilineTextBox, player: Player | undefined, text: string) => void;
+ maxLength?: number;
+ transparent?: boolean;
+ children?: TextNode;
+ } & (
+ | {
+ font?: string;
+ }
+ | {
+ font: string;
+ fontPackage?: string;
+ }
+ );
+ progressbar: {
+ ref?: { current: ProgressBar | null };
+ disabled?: boolean;
+ hidden?: boolean;
+ bold?: boolean;
+ italic?: boolean;
+ wrap?: boolean;
+ size?: number;
+ color?: Color | [number, number, number, number];
+ value?: number;
+ label?: string | string[];
+ children?: never;
+ } & (
+ | {
+ font?: string;
+ }
+ | {
+ font: string;
+ fontPackage?: string;
+ }
+ );
+ richtext: {
+ ref?: { current: RichText | null };
+ disabled?: boolean;
+ hidden?: boolean;
+ bold?: boolean;
+ italic?: boolean;
+ size?: number;
+ color?: Color | [number, number, number, number];
+ wrap?: boolean;
+ justify?: TextJustification;
+ children?: TextNode;
+ } & (
+ | {
+ font?: string;
+ }
+ | {
+ font: string;
+ fontPackage?: string;
+ }
+ );
+ select: {
+ ref?: { current: SelectionBox | null };
+ disabled?: boolean;
+ hidden?: boolean;
+ bold?: boolean;
+ italic?: boolean;
+ size?: number;
+ color?: Color | [number, number, number, number];
+ onChange?: (element: SelectionBox, player: Player | undefined, index: number, option: string) => void;
+ value?: string;
+ options: string[];
+ children?: never;
+ } & (
+ | {
+ font?: string;
+ }
+ | {
+ font: string;
+ fontPackage?: string;
+ }
+ );
+ slider: {
+ ref?: { current: Slider | null };
+ disabled?: boolean;
+ hidden?: boolean;
+ bold?: boolean;
+ italic?: boolean;
+ size?: number;
+ color?: Color | [number, number, number, number];
+ min?: number;
+ value?: number;
+ max?: number;
+ step?: number;
+ onChange?: (element: Slider, player: Player | undefined, value: number) => void;
+ inputWidth?: number;
+ children?: never;
+ } & (
+ | {
+ font?: string;
+ }
+ | {
+ font: string;
+ fontPackage?: string;
+ }
+ );
+ input: {
+ ref?: { current: TextBox | null };
+ disabled?: boolean;
+ hidden?: boolean;
+ bold?: boolean;
+ italic?: boolean;
+ size?: number;
+ color?: Color | [number, number, number, number];
+ onChange?: (element: TextBox, player: Player | undefined, text: string) => void;
+ onCommit?: (element: TextBox, player: Player | undefined, text: string, hardCommit: boolean) => void;
+ maxLength?: number;
+ transparent?: boolean;
+ selectOnFocus?: boolean;
+ value?: string;
+ type?: "string" | "float" | "positive-float" | "integer" | "positive-integer";
+ children?: never;
+ } & (
+ | {
+ font?: string;
+ }
+ | {
+ font: string;
+ fontPackage?: string;
+ }
+ );
+ }
+ }
+}
diff --git a/src/jsx.d.ts b/src/jsx.d.ts
deleted file mode 100644
index f14d745..0000000
--- a/src/jsx.d.ts
+++ /dev/null
@@ -1,303 +0,0 @@
-export {};
-
-declare global {
- namespace JSX {
- type Element = import("@tabletop-playground/api").Widget;
- interface ElementChildrenAttribute {
- children: {};
- }
-
- interface IntrinsicElements {
- image: {
- ref?: { current: import("@tabletop-playground/api").ImageWidget | null };
- disabled?: boolean;
- hidden?: boolean;
- onLoad?: (image: import("@tabletop-playground/api").ImageWidget, filename: string, packageId: string) => void;
- color?: import("@tabletop-playground/api").Color | [number, number, number, number];
- width?: number;
- height?: number;
- children?: never;
- } & (
- | { url: string }
- | {
- card: import("@tabletop-playground/api").Card;
- }
- | {
- src: string;
- srcPackage?: string;
- }
- );
- imagebutton: {
- ref?: { current: import("@tabletop-playground/api").ImageButton | null };
- disabled?: boolean;
- hidden?: boolean;
- onLoad?: (image: import("@tabletop-playground/api").ImageButton, filename: string, packageId: string) => void;
- color?: import("@tabletop-playground/api").Color | [number, number, number, number];
- width?: number;
- height?: number;
- onClick?: (image: import("@tabletop-playground/api").ImageButton, player: import("@tabletop-playground/api").Player) => void;
- children?: never;
- } & (
- | { url: string }
- | {
- card: import("@tabletop-playground/api").Card;
- }
- | {
- src: string;
- srcPackage?: string;
- }
- );
- contentbutton: {
- ref?: { current: import("@tabletop-playground/api").ContentButton | null };
- disabled?: boolean;
- hidden?: boolean;
- onClick?: (image: import("@tabletop-playground/api").ContentButton, player: import("@tabletop-playground/api").Player) => void;
- children?: import(".").SingleNode;
- };
- border: {
- ref?: { current: import("@tabletop-playground/api").Border | null };
- disabled?: boolean;
- hidden?: boolean;
- color?: import("@tabletop-playground/api").Color | [number, number, number, number];
- children?: import(".").SingleNode;
- };
- canvas: {
- ref?: { current: import("@tabletop-playground/api").Canvas | null };
- disabled?: boolean;
- hidden?: boolean;
- children?: import(".").CanvasNode;
- };
- horizontalbox: {
- ref?: { current: import("@tabletop-playground/api").HorizontalBox | null };
- disabled?: boolean;
- hidden?: boolean;
- gap?: number;
- valign?: import("@tabletop-playground/api").VerticalAlignment;
- halign?: import("@tabletop-playground/api").HorizontalAlignment;
- children?: import(".").BoxNode;
- };
- verticalbox: {
- ref?: { current: import("@tabletop-playground/api").VerticalBox | null };
- disabled?: boolean;
- hidden?: boolean;
- gap?: number;
- valign?: import("@tabletop-playground/api").VerticalAlignment;
- halign?: import("@tabletop-playground/api").HorizontalAlignment;
- children?: import(".").BoxNode;
- };
- layout: {
- ref?: { current: import("@tabletop-playground/api").LayoutBox | null };
- disabled?: boolean;
- hidden?: boolean;
- valign?: import("@tabletop-playground/api").VerticalAlignment;
- halign?: import("@tabletop-playground/api").HorizontalAlignment;
- padding?: {
- left?: number;
- right?: number;
- top?: number;
- bottom?: number;
- };
- maxHeight?: number;
- minHeight?: number;
- height?: number;
- minWidth?: number;
- maxWidth?: number;
- width?: number;
- children?: import(".").SingleNode;
- };
- text: {
- ref?: { current: import("@tabletop-playground/api").Text | null };
- disabled?: boolean;
- hidden?: boolean;
- bold?: boolean;
- italic?: boolean;
- size?: number;
- color?: import("@tabletop-playground/api").Color | [number, number, number, number];
- wrap?: boolean;
- justify?: import("@tabletop-playground/api").TextJustification;
- children?: import(".").TextNode;
- } & (
- | {
- font?: string;
- }
- | {
- font: string;
- fontPackage?: string;
- }
- );
- button: {
- ref?: { current: import("@tabletop-playground/api").Button | null };
- disabled?: boolean;
- hidden?: boolean;
- bold?: boolean;
- italic?: boolean;
- size?: number;
- color?: import("@tabletop-playground/api").Color | [number, number, number, number];
- onClick?: (button: import("@tabletop-playground/api").Button, player: import("@tabletop-playground/api").Player) => void;
- children?: import(".").TextNode;
- } & (
- | {
- font?: string;
- }
- | {
- font: string;
- fontPackage?: string;
- }
- );
- checkbox: {
- ref?: { current: import("@tabletop-playground/api").CheckBox | null };
- disabled?: boolean;
- hidden?: boolean;
- bold?: boolean;
- italic?: boolean;
- size?: number;
- color?: import("@tabletop-playground/api").Color | [number, number, number, number];
- onChange?: (checkbox: import("@tabletop-playground/api").CheckBox, player: import("@tabletop-playground/api").Player | undefined, state: boolean) => void;
- checked?: boolean;
- label?: string | string[];
- children?: never;
- } & (
- | {
- font?: string;
- }
- | {
- font: string;
- fontPackage?: string;
- }
- );
- textarea: {
- ref?: { current: import("@tabletop-playground/api").MultilineTextBox | null };
- disabled?: boolean;
- hidden?: boolean;
- bold?: boolean;
- italic?: boolean;
- size?: number;
- color?: import("@tabletop-playground/api").Color | [number, number, number, number];
- onChange?: (element: import("@tabletop-playground/api").MultilineTextBox, player: import("@tabletop-playground/api").Player | undefined, text: string) => void;
- onCommit?: (element: import("@tabletop-playground/api").MultilineTextBox, player: import("@tabletop-playground/api").Player | undefined, text: string) => void;
- maxLength?: number;
- transparent?: boolean;
- children?: import(".").TextNode;
- } & (
- | {
- font?: string;
- }
- | {
- font: string;
- fontPackage?: string;
- }
- );
- progressbar: {
- ref?: { current: import("@tabletop-playground/api").ProgressBar | null };
- disabled?: boolean;
- hidden?: boolean;
- bold?: boolean;
- italic?: boolean;
- wrap?: boolean;
- size?: number;
- color?: import("@tabletop-playground/api").Color | [number, number, number, number];
- value?: number;
- label?: string | string[];
- children?: never;
- } & (
- | {
- font?: string;
- }
- | {
- font: string;
- fontPackage?: string;
- }
- );
- richtext: {
- ref?: { current: import("@tabletop-playground/api").RichText | null };
- disabled?: boolean;
- hidden?: boolean;
- bold?: boolean;
- italic?: boolean;
- size?: number;
- color?: import("@tabletop-playground/api").Color | [number, number, number, number];
- wrap?: boolean;
- justify?: import("@tabletop-playground/api").TextJustification;
- children?: import(".").TextNode;
- } & (
- | {
- font?: string;
- }
- | {
- font: string;
- fontPackage?: string;
- }
- );
- select: {
- ref?: { current: import("@tabletop-playground/api").SelectionBox | null };
- disabled?: boolean;
- hidden?: boolean;
- bold?: boolean;
- italic?: boolean;
- size?: number;
- color?: import("@tabletop-playground/api").Color | [number, number, number, number];
- onChange?: (element: import("@tabletop-playground/api").SelectionBox, player: import("@tabletop-playground/api").Player | undefined, index: number, option: string) => void;
- value?: string;
- options: string[];
- children?: never;
- } & (
- | {
- font?: string;
- }
- | {
- font: string;
- fontPackage?: string;
- }
- );
- slider: {
- ref?: { current: import("@tabletop-playground/api").Slider | null };
- disabled?: boolean;
- hidden?: boolean;
- bold?: boolean;
- italic?: boolean;
- size?: number;
- color?: import("@tabletop-playground/api").Color | [number, number, number, number];
- min?: number;
- value?: number;
- max?: number;
- step?: number;
- onChange?: (element: import("@tabletop-playground/api").Slider, player: import("@tabletop-playground/api").Player | undefined, value: number) => void;
- inputWidth?: number;
- children?: never;
- } & (
- | {
- font?: string;
- }
- | {
- font: string;
- fontPackage?: string;
- }
- );
- input: {
- ref?: { current: import("@tabletop-playground/api").TextBox | null };
- disabled?: boolean;
- hidden?: boolean;
- bold?: boolean;
- italic?: boolean;
- size?: number;
- color?: import("@tabletop-playground/api").Color | [number, number, number, number];
- onChange?: (element: import("@tabletop-playground/api").TextBox, player: import("@tabletop-playground/api").Player | undefined, text: string) => void;
- onCommit?: (element: import("@tabletop-playground/api").TextBox, player: import("@tabletop-playground/api").Player | undefined, text: string, hardCommit: boolean) => void;
- maxLength?: number;
- transparent?: boolean;
- selectOnFocus?: boolean;
- value?: string;
- type?: "string" | "float" | "positive-float" | "integer" | "positive-integer";
- children?: never;
- } & (
- | {
- font?: string;
- }
- | {
- font: string;
- fontPackage?: string;
- }
- );
- }
- }
-}