@stanko/kaplay-inspector 0.1.7 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +10 -4
  2. package/dist/components/anchor-control.d.ts +6 -0
  3. package/dist/components/{anchor-controls.js → anchor-control.js} +4 -4
  4. package/dist/components/blend-control.d.ts +5 -0
  5. package/dist/components/blend-control.js +12 -0
  6. package/dist/components/color-control.d.ts +6 -0
  7. package/dist/components/color-control.js +24 -0
  8. package/dist/components/game-object.js +2 -2
  9. package/dist/components/hold-button.d.ts +2 -1
  10. package/dist/components/hold-button.js +51 -28
  11. package/dist/components/hp-control.d.ts +7 -0
  12. package/dist/components/hp-control.js +8 -0
  13. package/dist/components/number-control.d.ts +7 -0
  14. package/dist/components/number-control.js +7 -0
  15. package/dist/components/number-input.d.ts +8 -0
  16. package/dist/components/number-input.js +51 -0
  17. package/dist/components/sprite-control.d.ts +6 -0
  18. package/dist/components/sprite-control.js +6 -0
  19. package/dist/components/text-control.d.ts +6 -0
  20. package/dist/components/text-control.js +8 -0
  21. package/dist/components/text-input.d.ts +7 -0
  22. package/dist/components/text-input.js +26 -0
  23. package/dist/components/vector-control.d.ts +7 -0
  24. package/dist/components/vector-control.js +10 -0
  25. package/dist/lib/inspect-comps.js +22 -10
  26. package/dist/lib/to-fixed.d.ts +1 -0
  27. package/dist/lib/to-fixed.js +3 -0
  28. package/dist/styles.css +57 -21
  29. package/package.json +4 -3
  30. package/dist/components/anchor-controls.d.ts +0 -6
  31. package/dist/components/color-controls.d.ts +0 -6
  32. package/dist/components/color-controls.js +0 -24
  33. package/dist/components/position-controls.d.ts +0 -6
  34. package/dist/components/position-controls.js +0 -8
  35. package/dist/components/sprite-controls.d.ts +0 -6
  36. package/dist/components/sprite-controls.js +0 -6
  37. package/dist/components/text-controls.d.ts +0 -6
  38. package/dist/components/text-controls.js +0 -10
  39. package/dist/components/vector-controls.d.ts +0 -8
  40. package/dist/components/vector-controls.js +0 -9
  41. package/dist/lib/round-to-decimal.d.ts +0 -1
  42. package/dist/lib/round-to-decimal.js +0 -4
package/README.md CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  A dev tool for [Kaplay](https://kaplayjs.com/) which allows you to explore and inspect the game object tree real time.
4
4
 
5
- ![Kaplay inspector in action](./public/screenshot.png)
5
+ Check the demo: [muffinman.io/kaplay-inspector/](https://muffinman.io/kaplay-inspector/).
6
+
7
+ [![Kaplay inspector in action](./public/screenshot.png)](https://muffinman.io/kaplay-inspector/)
6
8
 
7
9
  ## Features
8
10
 
@@ -11,7 +13,12 @@ A dev tool for [Kaplay](https://kaplayjs.com/) which allows you to explore and i
11
13
  - Hover an object to draw it's area, anchor and bounding box
12
14
  - Inspect object's component and custom props
13
15
  - Log an object to console
14
- - Tweak position, text or color
16
+ - Tweak object properties live
17
+ - position, scale, rotate, skew, z-index
18
+ - opacity, color, blend mode
19
+ - text
20
+ - anchor
21
+ - health
15
22
  - Pause objects
16
23
  - Hide objects
17
24
  - Search for tags or comps
@@ -132,5 +139,4 @@ Same as with colors, be sure to have a higher specificity selector if inspector'
132
139
  ## TODO
133
140
 
134
141
  * [ ] Controllable theme - system/light/dark. At the moment it is always matching the system.
135
- * [ ] Persist search in URL or local storage
136
- * [ ] Collapse/Expand all button
142
+ * [ ] Persist search/options in URL or local storage
@@ -0,0 +1,6 @@
1
+ import type { GameObj } from "kaplay";
2
+ export interface AnchorControlProps {
3
+ className?: string;
4
+ obj: GameObj;
5
+ }
6
+ export declare const AnchorControl: ({ className, obj }: AnchorControlProps) => import("preact").JSX.Element;
@@ -1,18 +1,18 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "preact/jsx-runtime";
2
2
  import { cx } from "../lib/cx";
3
- import { VectorControls } from "./vector-controls";
4
3
  import { k } from "../k";
4
+ import { VectorControl } from "./vector-control";
5
5
  const strings = [
6
6
  ["topleft", "top", "topright"],
7
7
  ["left", "center", "right"],
8
8
  ["botleft", "bot", "botright"],
9
9
  ];
10
- export const AnchorControls = ({ className = "", obj, }) => {
10
+ export const AnchorControl = ({ className = "", obj }) => {
11
11
  const object = obj;
12
12
  const isString = typeof obj.anchor === "string";
13
- return (_jsx("div", { class: cx(className, "anchor-controls"), children: isString ? (_jsxs(_Fragment, { children: [_jsxs("div", { class: "anchor-radios", children: [strings.map((row, i) => {
13
+ return (_jsx("div", { class: cx(className, "anchor-control"), children: isString ? (_jsxs(_Fragment, { children: [_jsxs("div", { class: "anchor-radios", children: [strings.map((row, i) => {
14
14
  return (_jsx("div", { class: "anchor-row", children: row.map((anchor) => {
15
15
  return (_jsx("label", { children: _jsx("input", { type: "radio", name: "anchor", value: anchor, checked: anchor === object.anchor, onChange: () => (object.anchor = anchor) }) }, anchor));
16
16
  }) }, i));
17
- }), _jsx("div", { children: isString && object.anchor })] }), _jsx("button", { class: "ki-btn", onClick: () => (object.anchor = k.vec2(0, 0)), children: "Switch to vector" })] })) : (_jsxs(_Fragment, { children: [_jsx(VectorControls, { value: object.anchor, onChange: (anchor) => (object.anchor = anchor), step: 0.05 }), _jsx("button", { class: "ki-btn", onClick: () => (object.anchor = "center"), children: "Switch to string" })] })) }));
17
+ }), _jsx("div", { children: isString && object.anchor })] }), _jsx("button", { class: "ki-btn", onClick: () => (object.anchor = k.vec2(0, 0)), children: "Use a vector" })] })) : (_jsxs(_Fragment, { children: [_jsx(VectorControl, { obj: obj, property: "anchor", step: 0.1 }), _jsx("button", { class: "ki-btn", onClick: () => (object.anchor = "center"), children: "Use a named location" })] })) }));
18
18
  };
@@ -0,0 +1,5 @@
1
+ export interface BlendControlProps {
2
+ obj: Record<string, any>;
3
+ className?: string;
4
+ }
5
+ export declare const BlendControl: ({ className, obj }: BlendControlProps) => import("preact").JSX.Element;
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
2
+ import { cx } from "../lib/cx";
3
+ const MODES = [
4
+ { label: "normal", value: 0 },
5
+ { label: "add", value: 1 },
6
+ { label: "multiply", value: 2 },
7
+ { label: "screen", value: 3 },
8
+ { label: "overlay", value: 4 },
9
+ ];
10
+ export const BlendControl = ({ className = "", obj }) => {
11
+ return (_jsx("div", { class: cx(className, "blend-control"), children: MODES.map((mode) => (_jsxs("label", { class: "ki-flex", children: [_jsx("input", { type: "radio", name: `blend-${obj.id}`, value: mode.value, checked: obj.blend === mode.value, onChange: () => (obj.blend = mode.value) }), mode.label] }, mode.value))) }));
12
+ };
@@ -0,0 +1,6 @@
1
+ import type { GameObj } from "kaplay";
2
+ export interface ColorControlProps {
3
+ className?: string;
4
+ obj: GameObj;
5
+ }
6
+ export declare const ColorControl: ({ obj }: ColorControlProps) => import("preact").JSX.Element;
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
2
+ import { useEffect, useState } from "preact/hooks";
3
+ import { cx } from "../lib/cx";
4
+ const ColorSlider = ({ onChange, value, channel }) => {
5
+ const handleChange = (e) => {
6
+ const target = e.target;
7
+ onChange(channel, parseInt(target.value));
8
+ };
9
+ return (_jsx("input", { className: cx("color-control__slider-input", `color-control__slider-input--${channel}`), type: "range", min: "0", max: "255", value: value, onInput: handleChange }));
10
+ };
11
+ export const ColorControl = ({ obj }) => {
12
+ const object = obj;
13
+ const { r, g, b } = object.color;
14
+ const [color, setColor] = useState({ r, g, b });
15
+ useEffect(() => {
16
+ setColor({ r, g, b });
17
+ }, [r, g, b]);
18
+ const updateColor = (channel, value) => {
19
+ const newColor = { ...color, [channel]: value };
20
+ setColor(newColor);
21
+ object.color[channel] = value;
22
+ };
23
+ return (_jsxs("div", { class: "color-control", children: [_jsx("div", { class: "color-control__swatch", style: { background: `rgb(${color.r} ${color.g} ${color.b})` } }), _jsxs("div", { children: [_jsx(ColorSlider, { value: color.r, onChange: updateColor, channel: "r" }), _jsx(ColorSlider, { value: color.g, onChange: updateColor, channel: "g" }), _jsx(ColorSlider, { value: color.b, onChange: updateColor, channel: "b" })] }), _jsxs("div", { children: ["rgb(", color.r, ", ", color.g, ", ", color.b, ")"] })] }));
24
+ };
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "preact/jsx-runtime";
2
2
  import { useCallback, useEffect, useRef, useState } from "preact/hooks";
3
3
  import { MinusIcon, PlusIcon } from "../components/icons";
4
4
  import { cx } from "../lib/cx";
@@ -55,5 +55,5 @@ export const GameObject = ({ obj, className = "", isExpanded: isExpandedExternal
55
55
  "game-object--no-children": !hasChildren,
56
56
  }), children: [isInspecting && _jsx(Breadcrumbs, { setRenderRoot: setRenderRoot, obj: obj }), _jsxs("div", { class: "game-object__content", onMouseEnter: handleMouseEnter, onMouseLeave: cancelUpdateControllers, children: [_jsxs("button", { class: cx("game-object__header", {
57
57
  "game-object__header--expandable": showExpandTree,
58
- }), onClick: handleToggleClick, children: [isExpanded ? (_jsx(MinusIcon, { className: "game-object__expand-icon" })) : (_jsx(PlusIcon, { className: "game-object__expand-icon" })), _jsxs("div", { class: "game-object__id", children: ["ID ", obj.id, ":"] }), tags ? (_jsx("div", { class: "game-object__tags", children: isRootObject ? "Root" : tags })) : (_jsx("div", { class: "game-object__comp-names", children: compsLabel })), obj.children.length > 0 && _jsxs("div", { children: ["(", obj.children.length, ")"] }), isObjectDestroyed && (_jsx("div", { class: "game-object__destroyed", children: "DESTROYED" }))] }), _jsxs("div", { class: "game-object__buttons", children: [!isRenderRoot && (_jsx("button", { class: "ki-btn", onClick: () => setRenderRoot(obj), children: "inspect" })), _jsx("button", { class: "ki-btn ", onClick: () => console.log(obj), children: "log" })] }), isExpanded && (_jsx("div", { class: "game-object__comps-wrapper", children: _jsxs("div", { class: "game-object__comps", children: [_jsx(BooleanComp, { obj: obj, propName: "paused" }), _jsx(BooleanComp, { obj: obj, propName: "hidden" }), hasSize && (_jsxs("div", { class: "game-object__comps-row", children: [_jsx("b", { children: "size" }), _jsxs("div", { children: [obj.width, " x ", obj.height] })] })), compsData.map((comp) => (_jsxs("div", { class: "game-object__comps-row", children: [_jsx("b", { children: comp.tag }), _jsx("div", { children: comp.value })] }, comp.tag)))] }) }))] }), isExpanded && hasChildren && (_jsx("div", { class: "game-object__children", style: { display: isExpanded ? "block" : "none" }, children: obj.children.map((child) => (_jsx(GameObject, { obj: child, setRenderRoot: setRenderRoot, shouldDrawInspect: shouldDrawInspect }, child.id))) }))] }, obj.id));
58
+ }), onClick: handleToggleClick, children: [isExpanded ? (_jsx(MinusIcon, { className: "game-object__expand-icon" })) : (_jsx(PlusIcon, { className: "game-object__expand-icon" })), _jsxs("div", { class: "game-object__id", children: ["ID ", obj.id, ":"] }), tags ? (_jsx("div", { class: "game-object__tags", children: isRootObject ? "Root" : tags })) : (_jsx("div", { class: "game-object__comp-names", children: compsLabel })), obj.children.length > 0 && _jsxs("div", { children: ["(", obj.children.length, ")"] }), isObjectDestroyed && (_jsx("div", { class: "game-object__destroyed", children: "DESTROYED" }))] }), _jsxs("div", { class: "game-object__buttons", children: [!isRenderRoot && (_jsxs(_Fragment, { children: [_jsx("button", { class: "ki-btn ki-btn--red", onClick: () => obj.destroy(), children: "destroy" }), _jsx("button", { class: "ki-btn", onClick: () => setRenderRoot(obj), children: "inspect" })] })), _jsx("button", { class: "ki-btn ", onClick: () => console.log(obj), children: "log" })] }), isExpanded && (_jsx("div", { class: "game-object__comps-wrapper", children: _jsxs("div", { class: "game-object__comps", children: [_jsx(BooleanComp, { obj: obj, propName: "paused" }), _jsx(BooleanComp, { obj: obj, propName: "hidden" }), hasSize && (_jsxs("div", { class: "game-object__comps-row", children: [_jsx("b", { children: "size" }), _jsxs("div", { children: [obj.width, " x ", obj.height] })] })), compsData.map((comp) => (_jsxs("div", { class: "game-object__comps-row", children: [_jsx("b", { children: comp.tag }), _jsx("div", { children: comp.value })] }, comp.tag)))] }) }))] }), isExpanded && hasChildren && (_jsx("div", { class: "game-object__children", style: { display: isExpanded ? "block" : "none" }, children: obj.children.map((child) => (_jsx(GameObject, { obj: child, setRenderRoot: setRenderRoot, shouldDrawInspect: shouldDrawInspect }, child.id))) }))] }, obj.id));
59
59
  };
@@ -3,7 +3,8 @@ export type HoldButtonProps = {
3
3
  children?: JSX.Element | string | number;
4
4
  className?: string;
5
5
  onClickAndHold: () => void;
6
+ onHoldEnd?: () => void;
6
7
  interval?: number;
7
8
  startRepeatingDelay?: number;
8
9
  };
9
- export declare const HoldButton: ({ children, className, onClickAndHold, interval, startRepeatingDelay, }: HoldButtonProps) => JSX.Element;
10
+ export declare const HoldButton: ({ children, className, onClickAndHold, interval, startRepeatingDelay, onHoldEnd, }: HoldButtonProps) => JSX.Element;
@@ -1,33 +1,56 @@
1
1
  import { jsx as _jsx } from "preact/jsx-runtime";
2
- import { useEffect, useRef, useState } from "preact/hooks";
3
- export const HoldButton = ({ children, className, onClickAndHold, interval = 50, startRepeatingDelay = 200, }) => {
4
- const intervalRef = useRef();
5
- const timeoutRef = useRef();
6
- const [isMouseDown, setIsMouseDown] = useState(false);
2
+ import { useCallback, useEffect, useRef } from "preact/hooks";
3
+ export const HoldButton = ({ children, className, onClickAndHold, interval = 50, startRepeatingDelay = 200, onHoldEnd, }) => {
4
+ // const [isActive, setIsActive] = useState(false);
5
+ const timerRef = useRef();
6
+ const onClickAndHoldRef = useRef(onClickAndHold);
7
+ const onHoldEndRef = useRef(onHoldEnd);
8
+ const startTimer = useCallback(() => {
9
+ stopTimer();
10
+ // setIsActive(true);
11
+ // Trigger once immediately
12
+ onClickAndHoldRef.current();
13
+ // Wait before repeating
14
+ timerRef.current = setTimeout(() => {
15
+ // Repeat
16
+ onClickAndHoldRef.current();
17
+ timerRef.current = setInterval(() => {
18
+ onClickAndHoldRef.current();
19
+ }, interval);
20
+ }, startRepeatingDelay);
21
+ }, [interval, startRepeatingDelay]);
22
+ const stopTimer = useCallback(() => {
23
+ // setIsActive(false);
24
+ clearTimeout(timerRef.current);
25
+ clearInterval(timerRef.current);
26
+ }, []);
27
+ const handleHoldEnd = useCallback(() => {
28
+ // console.log("---------end", isActive);
29
+ // if (isActive) {
30
+ stopTimer();
31
+ onHoldEndRef.current?.();
32
+ // }
33
+ }, [
34
+ stopTimer,
35
+ // , isActive
36
+ ]);
37
+ // Keep references updated to the latest callbacks
38
+ // This ensures the interval always calls the latest version
39
+ useEffect(() => {
40
+ onClickAndHoldRef.current = onClickAndHold;
41
+ }, [onClickAndHold]);
42
+ useEffect(() => {
43
+ onClickAndHoldRef.current = onClickAndHold;
44
+ }, [onClickAndHold]);
45
+ // Use document on mouseup and touchend for nicer UX
7
46
  useEffect(() => {
8
- document.addEventListener("mouseup", handleMouseUp);
47
+ document.addEventListener("mouseup", handleHoldEnd);
48
+ document.addEventListener("touchend", handleHoldEnd);
9
49
  return () => {
10
- clearTimeout(timeoutRef.current);
11
- clearInterval(intervalRef.current);
12
- document.removeEventListener("mouseup", handleMouseUp);
50
+ stopTimer();
51
+ document.removeEventListener("mouseup", handleHoldEnd);
52
+ document.removeEventListener("touchend", handleHoldEnd);
13
53
  };
14
- }, []);
15
- useEffect(() => {
16
- clearTimeout(timeoutRef.current);
17
- clearInterval(intervalRef.current);
18
- if (isMouseDown) {
19
- timeoutRef.current = setTimeout(() => {
20
- intervalRef.current = setInterval(() => {
21
- onClickAndHold();
22
- }, interval);
23
- }, startRepeatingDelay);
24
- }
25
- }, [isMouseDown]);
26
- const handleMouseDown = () => {
27
- setIsMouseDown(true);
28
- };
29
- const handleMouseUp = () => {
30
- setIsMouseDown(false);
31
- };
32
- return (_jsx("button", { className: className, onMouseDown: handleMouseDown, onClick: onClickAndHold, children: children }));
54
+ }, [handleHoldEnd]);
55
+ return (_jsx("button", { className: className, onMouseDown: startTimer, onTouchStart: startTimer, children: children }));
33
56
  };
@@ -0,0 +1,7 @@
1
+ import type { GameObj } from "kaplay";
2
+ export interface HpControlProps {
3
+ className?: string;
4
+ obj: GameObj;
5
+ step?: number;
6
+ }
7
+ export declare const HpControl: ({ className, obj, step }: HpControlProps) => import("preact").JSX.Element | null;
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
2
+ import { NumberControl } from "./number-control";
3
+ export const HpControl = ({ className = "", obj, step }) => {
4
+ if (obj.hp === undefined) {
5
+ return null;
6
+ }
7
+ return (_jsxs("div", { class: "hp-control ki-flex", children: [_jsx(NumberControl, { className: className, obj: obj, property: "hp", step: step }), " ", "(Max: ", obj.maxHP, ")"] }));
8
+ };
@@ -0,0 +1,7 @@
1
+ export interface NumberControlProps {
2
+ obj: Record<string, any>;
3
+ property: string;
4
+ className?: string;
5
+ step?: number;
6
+ }
7
+ export declare const NumberControl: ({ className, obj, property, step, }: NumberControlProps) => import("preact").JSX.Element;
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx } from "preact/jsx-runtime";
2
+ import { NumberInput } from "./number-input";
3
+ export const NumberControl = ({ className = "", obj, property, step, }) => {
4
+ return (_jsx(NumberInput, { className: className, obj: obj, property: property, onChange: (n) => {
5
+ obj[property] = n;
6
+ }, step: step }));
7
+ };
@@ -0,0 +1,8 @@
1
+ export interface NumberInputProps {
2
+ obj: Record<string, any>;
3
+ property: string;
4
+ onChange: (value: number) => void;
5
+ className?: string;
6
+ step?: number;
7
+ }
8
+ export declare const NumberInput: ({ obj, property, className, onChange, step, }: NumberInputProps) => import("preact").JSX.Element | null;
@@ -0,0 +1,51 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
2
+ import { useEffect, useState } from "preact/hooks";
3
+ import { cx } from "../lib/cx";
4
+ import { toFixed } from "../lib/to-fixed";
5
+ import { HoldButton } from "./hold-button";
6
+ export const NumberInput = ({ obj, property, className = "", onChange, step = 1, }) => {
7
+ const [localValue, setLocalValue] = useState(toFixed(obj[property]).toString());
8
+ const [isFocused, setIsFocused] = useState(false);
9
+ const [error, setError] = useState(false);
10
+ // Sync game state -> UI when input is not focused
11
+ useEffect(() => {
12
+ if (!isFocused) {
13
+ setLocalValue(toFixed(obj[property]).toString());
14
+ }
15
+ }, [obj[property], isFocused]);
16
+ // Has to be after hooks
17
+ if (obj[property] === undefined) {
18
+ return null;
19
+ }
20
+ const handleInput = (e) => {
21
+ const value = e.target.value;
22
+ setLocalValue(value);
23
+ const parsedValue = parseFloat(value);
24
+ if (!isNaN(value) && !Number.isNaN(parsedValue)) {
25
+ // Sync UI -> game state
26
+ onChange(parsedValue);
27
+ setError(false);
28
+ }
29
+ else {
30
+ setError(true);
31
+ }
32
+ };
33
+ const handleClickAndHold = (offset) => {
34
+ // Use local value so we don't wait for the inspector to re-render
35
+ const newValue = parseFloat(localValue) + offset;
36
+ // obj[property] = newValue;
37
+ onChange(newValue);
38
+ setLocalValue(toFixed(newValue).toString());
39
+ };
40
+ return (_jsxs("div", { class: cx(className, "number-input ki-flex"), children: [_jsx(HoldButton, { className: "ki-btn", onClickAndHold: () => handleClickAndHold(-step), onHoldEnd: () => {
41
+ // Final sync to ensure value is in sync
42
+ setLocalValue(toFixed(obj[property]).toString());
43
+ }, children: "-" }), _jsx("input", { class: cx("ki-input", {
44
+ "ki-input--error": error,
45
+ }), value: localValue, onInput: handleInput, onFocus: () => setIsFocused(true), onBlur: () => {
46
+ setIsFocused(false);
47
+ setError(false);
48
+ // Final sync to ensure value is in sync
49
+ setLocalValue(toFixed(obj[property]).toString());
50
+ } }), _jsx(HoldButton, { className: "ki-btn", onClickAndHold: () => handleClickAndHold(step), children: "+" })] }));
51
+ };
@@ -0,0 +1,6 @@
1
+ import type { GameObj } from "kaplay";
2
+ export interface SpriteControlProps {
3
+ className?: string;
4
+ obj: GameObj;
5
+ }
6
+ export declare const SpriteControl: ({ obj }: SpriteControlProps) => import("preact").JSX.Element;
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "preact/jsx-runtime";
2
+ export const SpriteControl = ({ obj }) => {
3
+ const object = obj;
4
+ const animation = object.getCurAnim();
5
+ return (_jsxs("div", { class: "sprite-control", children: [_jsx("b", { children: object.sprite }), _jsxs("div", { children: ["frame: ", object.frame] }), animation && (_jsxs(_Fragment, { children: [_jsxs("div", { children: ["animation: ", animation.name] }), _jsxs("div", { children: ["animation frame: ", animation?.frameIndex] })] }))] }));
6
+ };
@@ -0,0 +1,6 @@
1
+ import type { GameObj } from "kaplay";
2
+ export interface TextControlProps {
3
+ className?: string;
4
+ obj: GameObj;
5
+ }
6
+ export declare const TextControl: ({ obj }: TextControlProps) => import("preact").JSX.Element | null;
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx } from "preact/jsx-runtime";
2
+ import { TextInput } from "./text-input";
3
+ export const TextControl = ({ obj }) => {
4
+ if (typeof obj.text !== "string") {
5
+ return null;
6
+ }
7
+ return (_jsx("div", { class: "text-control", children: _jsx(TextInput, { className: "text-control__input ki-input", placeholder: "Enter text", value: obj.text, onChange: (text) => (obj.text = text) }) }));
8
+ };
@@ -0,0 +1,7 @@
1
+ export interface TextInputProps {
2
+ className?: string;
3
+ value: string;
4
+ onChange: (text: string) => void;
5
+ placeholder?: string;
6
+ }
7
+ export declare const TextInput: ({ className, value, onChange, placeholder, }: TextInputProps) => import("preact").JSX.Element | null;
@@ -0,0 +1,26 @@
1
+ import { jsx as _jsx } from "preact/jsx-runtime";
2
+ import { useEffect, useState } from "preact/hooks";
3
+ import { cx } from "../lib/cx";
4
+ export const TextInput = ({ className = "", value, onChange, placeholder = "", }) => {
5
+ const [localValue, setLocalValue] = useState(value.toString());
6
+ const [isFocused, setIsFocused] = useState(false);
7
+ // Sync game state -> UI when input is not focused
8
+ useEffect(() => {
9
+ if (!isFocused) {
10
+ setLocalValue(value.toString());
11
+ }
12
+ }, [value, isFocused]);
13
+ if (value === undefined) {
14
+ return null;
15
+ }
16
+ const handleInput = (e) => {
17
+ const value = e.target.value;
18
+ setLocalValue(value);
19
+ onChange(value);
20
+ };
21
+ return (_jsx("textarea", { class: cx(className, "text-input ki-input"), value: localValue, onInput: handleInput, placeholder: placeholder, onFocus: () => setIsFocused(true), onBlur: () => {
22
+ setIsFocused(false);
23
+ // Final sync to ensure value is in sync
24
+ setLocalValue(value);
25
+ } }));
26
+ };
@@ -0,0 +1,7 @@
1
+ export interface VectorControlProps {
2
+ className?: string;
3
+ obj: Record<string, any>;
4
+ property: string;
5
+ step?: number;
6
+ }
7
+ export declare const VectorControl: ({ className, obj, property, step, }: VectorControlProps) => import("preact").JSX.Element | null;
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
2
+ import { cx } from "../lib/cx";
3
+ import { NumberInput } from "./number-input";
4
+ import { k } from "../k";
5
+ export const VectorControl = ({ className = "", obj, property, step, }) => {
6
+ if (obj[property] === undefined) {
7
+ return null;
8
+ }
9
+ return (_jsxs("div", { className: cx(className, "vector-control ki-flex"), children: ["x:", " ", _jsx(NumberInput, { className: "vector-control__x-input", obj: obj[property], property: "x", onChange: (x) => (obj[property] = k.vec2(x, obj[property].y)), step: step }), "y:", " ", _jsx(NumberInput, { obj: obj[property], property: "y", onChange: (y) => (obj[property] = k.vec2(obj[property].x, y)), step: step })] }));
10
+ };
@@ -1,18 +1,30 @@
1
1
  import { jsx as _jsx } from "preact/jsx-runtime";
2
2
  import { stringify } from "./stringify";
3
- import { PositionControls } from "../components/position-controls";
4
- import { TextControls } from "../components/text-controls";
5
- import { SpriteControls } from "../components/sprite-controls";
6
- import { Color } from "../components/color-controls";
3
+ import { TextControl } from "../components/text-control";
4
+ import { SpriteControl } from "../components/sprite-control";
5
+ import { ColorControl } from "../components/color-control";
7
6
  import { ChildObject } from "../components/child-object";
8
7
  import { isGameObj } from "./is-game-obj";
9
- import { AnchorControls } from "../components/anchor-controls";
8
+ import { AnchorControl } from "../components/anchor-control";
9
+ import { HpControl } from "../components/hp-control";
10
+ import { NumberControl } from "../components/number-control";
11
+ import { VectorControl } from "../components/vector-control";
12
+ import { BlendControl } from "../components/blend-control";
10
13
  const componentMap = {
11
- pos: PositionControls,
12
- text: TextControls,
13
- sprite: SpriteControls,
14
- color: Color,
15
- anchor: AnchorControls,
14
+ text: TextControl,
15
+ sprite: SpriteControl,
16
+ color: ColorControl,
17
+ anchor: AnchorControl,
18
+ health: HpControl,
19
+ blend: BlendControl,
20
+ // Vectors
21
+ pos: ({ obj }) => _jsx(VectorControl, { obj: obj, property: "pos" }),
22
+ scale: ({ obj }) => _jsx(VectorControl, { obj: obj, property: "scale", step: 0.1 }),
23
+ skew: ({ obj }) => _jsx(VectorControl, { obj: obj, property: "skew", step: 5 }),
24
+ // Numbers
25
+ opacity: ({ obj }) => (_jsx(NumberControl, { obj: obj, property: "opacity", step: 0.1 })),
26
+ rotate: ({ obj }) => _jsx(NumberControl, { obj: obj, property: "angle", step: 5 }),
27
+ z: ({ obj }) => _jsx(NumberControl, { obj: obj, property: "z" }),
16
28
  };
17
29
  export const inspectComps = (obj) => {
18
30
  const object = obj;
@@ -0,0 +1 @@
1
+ export declare const toFixed: (value: number, decimalPlaces?: number) => number;
@@ -0,0 +1,3 @@
1
+ export const toFixed = (value, decimalPlaces = 3) => {
2
+ return Number(value.toFixed(decimalPlaces));
3
+ };
package/dist/styles.css CHANGED
@@ -73,6 +73,22 @@
73
73
  color: inherit;
74
74
  }
75
75
 
76
+ textarea {
77
+ field-sizing: content;
78
+ max-height: 6rem;
79
+ }
80
+
81
+ input::-webkit-outer-spin-button,
82
+ input::-webkit-inner-spin-button {
83
+ -webkit-appearance: none;
84
+ margin: 0;
85
+ }
86
+
87
+ input[type="number"] {
88
+ -moz-appearance: textfield;
89
+ appearance: textfield;
90
+ }
91
+
76
92
  button {
77
93
  border: none;
78
94
  cursor: pointer;
@@ -100,6 +116,14 @@
100
116
  border: 1px solid var(--ki-primary);
101
117
  }
102
118
 
119
+ .ki-btn--red {
120
+ color: var(--ki-red);
121
+ }
122
+
123
+ .ki-btn--red:hover {
124
+ border: 1px solid var(--ki-red);
125
+ }
126
+
103
127
  .ki-input {
104
128
  background-color: var(--ki-bg);
105
129
  border: none;
@@ -114,6 +138,11 @@
114
138
  border-color: var(--ki-primary);
115
139
  }
116
140
 
141
+ .ki-input--error,
142
+ .ki-input--error:focus-visible {
143
+ border-color: var(--ki-red);
144
+ }
145
+
117
146
  /* Main layout */
118
147
 
119
148
  .k-inspector__header {
@@ -309,35 +338,42 @@
309
338
  padding-bottom: 1rem;
310
339
  }
311
340
 
312
- /* Vector controls */
341
+ /* Flex layout for controls */
313
342
 
314
- .vector-controls {
343
+ .ki-flex {
315
344
  display: flex;
316
- align-items: center;
317
345
  gap: 0.25rem;
318
- width: fit-content;
319
- min-width: 12rem;
346
+ align-items: center;
320
347
  }
321
348
 
322
- .vector-controls .ki-btn,
323
- .vector-controls__value {
324
- flex-shrink: 0;
325
- text-align: center;
349
+ /* Number input */
350
+
351
+ .number-input input {
352
+ width: 4.5rem;
326
353
  }
327
354
 
328
- .vector-controls__value {
329
- width: 5rem;
355
+ /* Vector control */
356
+
357
+ .vector-control__x-input {
358
+ margin-right: 0.75rem; /* with gap it comes to 1rem */
359
+ }
360
+
361
+ /* Blend control */
362
+
363
+ .blend-control {
364
+ display: grid;
365
+ gap: 0.25rem;
330
366
  }
331
367
 
332
- /* Text Controls */
368
+ /* Text control */
333
369
 
334
- .text-controls__input {
370
+ .text-control__input {
335
371
  width: 100%;
336
372
  }
337
373
 
338
- /* Anchor controls */
374
+ /* Anchor control */
339
375
 
340
- .anchor-controls {
376
+ .anchor-control {
341
377
  display: flex;
342
378
  align-items: center;
343
379
  gap: 1rem;
@@ -357,7 +393,7 @@
357
393
 
358
394
  /* Color */
359
395
 
360
- .color-controls {
396
+ .color-control {
361
397
  display: flex;
362
398
  align-items: center;
363
399
  gap: 0.5rem;
@@ -365,28 +401,28 @@
365
401
  min-width: 12rem;
366
402
  }
367
403
 
368
- .color-controls__swatch {
404
+ .color-control__swatch {
369
405
  width: 2rem;
370
406
  height: 2rem;
371
407
  border-radius: 8px;
372
408
  border: 1px solid var(--ki-border-light);
373
409
  }
374
410
 
375
- .color-controls__slider {
411
+ .color-control__slider {
376
412
  display: flex;
377
413
  align-items: center;
378
414
  gap: 0.25rem;
379
415
  }
380
416
 
381
- .color-controls__slider-input--r {
417
+ .color-control__slider-input--r {
382
418
  accent-color: red;
383
419
  }
384
420
 
385
- .color-controls__slider-input--g {
421
+ .color-control__slider-input--g {
386
422
  accent-color: green;
387
423
  }
388
424
 
389
- .color-controls__slider-input--b {
425
+ .color-control__slider-input--b {
390
426
  accent-color: blue;
391
427
  }
392
428
 
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stanko/kaplay-inspector",
3
3
  "description": "A dev tool for Kaplay which allows you to explore and inspect the game object tree real time.",
4
4
  "private": false,
5
- "version": "0.1.7",
5
+ "version": "0.2.0",
6
6
  "type": "module",
7
7
  "main": "./dist/init.js",
8
8
  "types": "./dist/init.d.ts",
@@ -21,7 +21,7 @@
21
21
  "prepublish": "npm run build-lib"
22
22
  },
23
23
  "peerDependencies": {
24
- "kaplay": "^4000.0.0-alpha.20"
24
+ "kaplay": "^4000.0.0-alpha.26"
25
25
  },
26
26
  "dependencies": {
27
27
  "preact": "^10.28.2"
@@ -30,7 +30,8 @@
30
30
  "@preact/preset-vite": "^2.10.2",
31
31
  "@types/node": "^25.0.6",
32
32
  "typescript": "~5.9.3",
33
- "vite": "^7.3.1"
33
+ "vite": "^7.3.1",
34
+ "kaplay": "^4000.0.0-alpha.26"
34
35
  },
35
36
  "repository": {
36
37
  "type": "git",
@@ -1,6 +0,0 @@
1
- import type { GameObj } from "kaplay";
2
- export interface AnchorControlsProps {
3
- className?: string;
4
- obj: GameObj;
5
- }
6
- export declare const AnchorControls: ({ className, obj, }: AnchorControlsProps) => import("preact").JSX.Element;
@@ -1,6 +0,0 @@
1
- import type { GameObj } from "kaplay";
2
- export interface ColorProps {
3
- className?: string;
4
- obj: GameObj;
5
- }
6
- export declare const Color: ({ obj }: ColorProps) => import("preact").JSX.Element;
@@ -1,24 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
2
- import { useEffect, useState } from "preact/hooks";
3
- import { cx } from "../lib/cx";
4
- const ColorSlider = ({ onChange, value, channel }) => {
5
- const handleChange = (e) => {
6
- const target = e.target;
7
- onChange(channel, parseInt(target.value));
8
- };
9
- return (_jsx("input", { className: cx("color-controls__slider-input", `color-controls__slider-input--${channel}`), type: "range", min: "0", max: "255", value: value, onInput: handleChange }));
10
- };
11
- export const Color = ({ obj }) => {
12
- const object = obj;
13
- const { r, g, b } = object.color;
14
- const [color, setColor] = useState({ r, g, b });
15
- useEffect(() => {
16
- setColor({ r, g, b });
17
- }, [r, g, b]);
18
- const updateColor = (channel, value) => {
19
- const newColor = { ...color, [channel]: value };
20
- setColor(newColor);
21
- object.color[channel] = value;
22
- };
23
- return (_jsxs("div", { class: "color-controls", children: [_jsx("div", { class: "color-controls__swatch", style: { background: `rgb(${color.r} ${color.g} ${color.b})` } }), _jsxs("div", { children: [_jsx(ColorSlider, { value: color.r, onChange: updateColor, channel: "r" }), _jsx(ColorSlider, { value: color.g, onChange: updateColor, channel: "g" }), _jsx(ColorSlider, { value: color.b, onChange: updateColor, channel: "b" })] }), _jsxs("div", { children: ["rgb(", color.r, ", ", color.g, ", ", color.b, ")"] })] }));
24
- };
@@ -1,6 +0,0 @@
1
- import type { GameObj } from "kaplay";
2
- export interface PositionControlsProps {
3
- className?: string;
4
- obj: GameObj;
5
- }
6
- export declare const PositionControls: ({ className, obj, }: PositionControlsProps) => import("preact").JSX.Element | null;
@@ -1,8 +0,0 @@
1
- import { jsx as _jsx } from "preact/jsx-runtime";
2
- import { VectorControls } from "./vector-controls";
3
- export const PositionControls = ({ className = "", obj, }) => {
4
- if (!obj.pos) {
5
- return null;
6
- }
7
- return (_jsx(VectorControls, { className: className, value: obj.pos, onChange: (pos) => (obj.pos = pos) }));
8
- };
@@ -1,6 +0,0 @@
1
- import type { GameObj } from "kaplay";
2
- export interface SpriteControlsProps {
3
- className?: string;
4
- obj: GameObj;
5
- }
6
- export declare const SpriteControls: ({ obj }: SpriteControlsProps) => import("preact").JSX.Element;
@@ -1,6 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "preact/jsx-runtime";
2
- export const SpriteControls = ({ obj }) => {
3
- const object = obj;
4
- const animation = object.getCurAnim();
5
- return (_jsxs("div", { class: "sprite-controls", children: [_jsx("b", { children: object.sprite }), _jsxs("div", { children: ["frame: ", object.frame] }), animation && (_jsxs(_Fragment, { children: [_jsxs("div", { children: ["animation: ", animation.name] }), _jsxs("div", { children: ["animation frame: ", animation?.frameIndex] })] }))] }));
6
- };
@@ -1,6 +0,0 @@
1
- import type { GameObj } from "kaplay";
2
- export interface TextControlsProps {
3
- className?: string;
4
- obj: GameObj;
5
- }
6
- export declare const TextControls: ({ obj }: TextControlsProps) => import("preact").JSX.Element | null;
@@ -1,10 +0,0 @@
1
- import { jsx as _jsx } from "preact/jsx-runtime";
2
- export const TextControls = ({ obj }) => {
3
- if (typeof obj.text !== "string") {
4
- return null;
5
- }
6
- const handleInput = (e) => {
7
- obj.text = e.target.value;
8
- };
9
- return (_jsx("div", { class: "text-controls", children: _jsx("textarea", { class: "text-controls__input ki-input", placeholder: "Enter text", value: obj.text, onInput: handleInput }) }));
10
- };
@@ -1,8 +0,0 @@
1
- import type { Vec2 } from "kaplay";
2
- export interface VectorControlsProps {
3
- className?: string;
4
- value: Vec2;
5
- onChange: (v: Vec2) => void;
6
- step?: number;
7
- }
8
- export declare const VectorControls: ({ className, value, onChange, step, }: VectorControlsProps) => import("preact").JSX.Element;
@@ -1,9 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
2
- import { roundToDecimal } from "../lib/round-to-decimal";
3
- import { HoldButton } from "./hold-button";
4
- import { ArrowDownIcon, ArrowLeftIcon, ArrowRightIcon, ArrowUpIcon, } from "./icons";
5
- import { k } from "../k";
6
- import { cx } from "../lib/cx";
7
- export const VectorControls = ({ className = "", value, onChange, step = 1, }) => {
8
- return (_jsxs("div", { class: cx(className, "vector-controls"), children: [_jsx(HoldButton, { className: "ki-btn", onClickAndHold: () => onChange(k.vec2(value.x - step, value.y)), children: _jsx(ArrowLeftIcon, {}) }), _jsx(HoldButton, { className: "ki-btn", onClickAndHold: () => onChange(k.vec2(value.x + step, value.y)), children: _jsx(ArrowRightIcon, {}) }), _jsxs("div", { class: "vector-controls__value", children: ["x: ", roundToDecimal(value.x, 2)] }), _jsxs("div", { class: "vector-controls__value", children: ["y: ", roundToDecimal(value.y, 2)] }), _jsx(HoldButton, { className: "ki-btn", onClickAndHold: () => onChange(k.vec2(value.x, value.y - step)), children: _jsx(ArrowUpIcon, {}) }), _jsx(HoldButton, { className: "ki-btn", onClickAndHold: () => onChange(k.vec2(value.x, value.y + step)), children: _jsx(ArrowDownIcon, {}) })] }));
9
- };
@@ -1 +0,0 @@
1
- export declare const roundToDecimal: (value: number, decimalPlaces: number) => number;
@@ -1,4 +0,0 @@
1
- export const roundToDecimal = (value, decimalPlaces) => {
2
- const factor = Math.pow(10, decimalPlaces);
3
- return Math.round(value * factor) / factor;
4
- };