kaplay-plugin-ui 1.0.2 → 1.1.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.
package/README.md CHANGED
@@ -8,9 +8,9 @@
8
8
 
9
9
  🦖 KAPLAY plugin UI (see [API](#api)):
10
10
 
11
- - `addButton`
12
- - `getTheme`
13
- - `setTheme`
11
+ - [addButton](#addButton)
12
+ - [getTheme](#getTheme)
13
+ - [setTheme](#setTheme)
14
14
 
15
15
  ## Prerequisites
16
16
 
@@ -86,22 +86,64 @@ k.addButton('Start', {
86
86
 
87
87
  ## API
88
88
 
89
- ### Button
89
+ ### addButton
90
90
 
91
- Add button game object:
91
+ Add text button:
92
92
 
93
93
  ```ts
94
94
  addButton(text, opt);
95
95
  ```
96
96
 
97
+ Buttons auto-size to their content. Width uses `components.button.paddingX` and `components.button.minWidth`; height uses `components.button.paddingY`. Pass `width` or `height` to override either dimension.
98
+
99
+ Add icon button with text:
100
+
101
+ ```ts
102
+ addButton('Play', {
103
+ icon: 'play',
104
+ });
105
+ ```
106
+
107
+ Add icon-only button:
108
+
109
+ ```ts
110
+ addButton('', {
111
+ icon: 'play',
112
+ hoverIcon: 'play-hover',
113
+ pressedIcon: 'play-pressed',
114
+ });
115
+ ```
116
+
117
+ Add image button with text:
118
+
119
+ ```ts
120
+ addButton('Play', {
121
+ background: 'panel',
122
+ });
123
+ ```
124
+
125
+ Add image-only button:
126
+
127
+ ```ts
128
+ addButton('', {
129
+ background: 'panel',
130
+ hoverBackground: 'panel-hover',
131
+ pressedBackground: 'panel-pressed',
132
+ });
133
+ ```
134
+
135
+ Icons use the theme's `spacing` as the gap between the icon and label. Icon-only and background-image buttons have a transparent background. Text is centered over a background image, which stretches to fit the button.
136
+
97
137
  Update button text:
98
138
 
99
139
  ```ts
100
- const button = addButton(text);
101
- button.label.text = 'Hello';
140
+ const button = addButton('Text');
141
+ button.label.text = 'New Text';
102
142
  ```
103
143
 
104
- ### Theme
144
+ The button resizes automatically when the label, icon, or background image dimensions change.
145
+
146
+ ### getTheme
105
147
 
106
148
  Retrieve theme:
107
149
 
@@ -109,6 +151,8 @@ Retrieve theme:
109
151
  getTheme();
110
152
  ```
111
153
 
154
+ ### setTheme
155
+
112
156
  Customize theme:
113
157
 
114
158
  ```ts
package/dist/plugin.cjs CHANGED
@@ -12,9 +12,8 @@ function toColor(k, color) {
12
12
  }
13
13
  // src/button.ts
14
14
  function createButton(k, theme, text, opt) {
15
- const width = opt.width ?? theme.components.button.width;
16
- const height = opt.height ?? theme.components.button.height;
17
15
  const radius = opt.radius ?? theme.shape.borderRadius;
16
+ const { minWidth, paddingX, paddingY } = theme.components.button;
18
17
  const baseColor = toColor(k, opt.color ?? opt.fill ?? theme.palette.primary);
19
18
  const hoverColor = toColor(k, opt.hoverColor ?? theme.palette.hover);
20
19
  const pressedColor = toColor(k, opt.pressedColor ?? theme.palette.pressed);
@@ -22,38 +21,123 @@ function createButton(k, theme, text, opt) {
22
21
  const size = opt.size ?? theme.typography.fontSize;
23
22
  const font = opt.font ?? theme.typography.fontFamily;
24
23
  const pos = opt.pos ?? k.vec2();
24
+ const textComp = k.text(text, {
25
+ size,
26
+ font
27
+ });
28
+ const isIconOnly = opt.icon !== void 0 && text.length === 0;
29
+ const rectOpt = !(opt.background !== void 0) && !isIconOnly ? radius > 0 ? { radius } : void 0 : { fill: false };
25
30
  const button = k.add([
26
- k.rect(width, height, radius > 0 ? { radius } : void 0),
31
+ k.rect(0, 0, rectOpt),
27
32
  k.pos(pos),
28
33
  k.area(),
29
34
  k.scale(),
30
35
  k.color(baseColor),
31
36
  k.anchor("center")
32
37
  ]);
38
+ const background = opt.background ? button.add([
39
+ k.sprite(opt.background),
40
+ k.pos(),
41
+ k.anchor("center")
42
+ ]) : void 0;
43
+ const icon = opt.icon ? button.add([
44
+ k.sprite(opt.icon),
45
+ k.pos(),
46
+ k.anchor("center")
47
+ ]) : void 0;
33
48
  const label = button.add([
34
- k.text(text, {
35
- size,
36
- font
37
- }),
49
+ textComp,
38
50
  k.color(textColor),
51
+ k.pos(),
39
52
  k.anchor("center")
40
53
  ]);
54
+ let iconWidth = 0;
55
+ let iconHeight = 0;
56
+ let iconState = "base";
57
+ let backgroundWidth = 0;
58
+ let backgroundHeight = 0;
59
+ let backgroundMeasured = false;
60
+ let backgroundState = "base";
61
+ /**
62
+ * Update button layout:
63
+ *
64
+ * - caches the base icon/background dimensions,
65
+ * - calculates content width/height,
66
+ * - updates `button.width` / `button.height`,
67
+ * - stretches the background and repositions the icon and label.
68
+ */
69
+ function layout() {
70
+ if (icon && iconState === "base") {
71
+ iconWidth = icon.width;
72
+ iconHeight = icon.height;
73
+ }
74
+ if (background && backgroundState === "base" && !backgroundMeasured && (background.width > 0 || background.height > 0)) {
75
+ backgroundWidth = background.width;
76
+ backgroundHeight = background.height;
77
+ backgroundMeasured = true;
78
+ }
79
+ const gap = icon && iconWidth > 0 && label.width > 0 ? theme.spacing : 0;
80
+ const contentWidth = iconWidth + gap + label.width;
81
+ const contentHeight = Math.max(iconHeight, label.height);
82
+ const contentBoxWidth = contentWidth + paddingX * 2;
83
+ const contentBoxHeight = contentHeight + paddingY * 2;
84
+ const nextWidth = opt.width ?? (background ? Math.max(contentBoxWidth, backgroundWidth) : Math.max(contentBoxWidth, minWidth));
85
+ const nextHeight = opt.height ?? (background ? Math.max(contentBoxHeight, backgroundHeight) : contentBoxHeight);
86
+ if (button.width !== nextWidth) button.width = nextWidth;
87
+ if (button.height !== nextHeight) button.height = nextHeight;
88
+ if (background && backgroundMeasured) {
89
+ background.width = nextWidth;
90
+ background.height = nextHeight;
91
+ background.pos = k.vec2();
92
+ }
93
+ if (icon) icon.pos = k.vec2(-contentWidth / 2 + iconWidth / 2, 0);
94
+ label.pos = k.vec2(-contentWidth / 2 + iconWidth + gap + label.width / 2, 0);
95
+ }
96
+ layout();
97
+ if (opt.width === void 0 || opt.height === void 0 || opt.icon !== void 0 || opt.background !== void 0) button.onUpdate(layout);
41
98
  button.onClick(() => {
42
99
  button.color = toColor(k, pressedColor);
100
+ if (icon && opt.pressedIcon) {
101
+ iconState = "pressed";
102
+ icon.sprite = opt.pressedIcon;
103
+ }
104
+ if (background && opt.pressedBackground) {
105
+ backgroundState = "pressed";
106
+ background.sprite = opt.pressedBackground;
107
+ }
43
108
  opt.onClick?.();
44
109
  });
45
110
  button.onHover(() => {
46
111
  button.color = toColor(k, hoverColor);
47
112
  button.scale = k.vec2(1.05, 1.05);
48
113
  k.setCursor("pointer");
114
+ if (icon && opt.hoverIcon) {
115
+ iconState = "hover";
116
+ icon.sprite = opt.hoverIcon;
117
+ }
118
+ if (background && opt.hoverBackground) {
119
+ backgroundState = "hover";
120
+ background.sprite = opt.hoverBackground;
121
+ }
49
122
  });
50
123
  button.onHoverEnd(() => {
51
124
  button.color = baseColor;
52
125
  button.scale = k.vec2(1, 1);
53
126
  k.setCursor("default");
127
+ if (icon && opt.icon) {
128
+ iconState = "base";
129
+ icon.sprite = opt.icon;
130
+ }
131
+ if (background && opt.background) {
132
+ backgroundState = "base";
133
+ background.sprite = opt.background;
134
+ }
135
+ if (icon || background) layout();
54
136
  });
55
137
  return {
56
138
  ...button,
139
+ background,
140
+ icon,
57
141
  label
58
142
  };
59
143
  }
@@ -84,8 +168,6 @@ const theme = {
84
168
  spacing: 8,
85
169
  components: {
86
170
  button: {
87
- width: 120,
88
- height: 40,
89
171
  minWidth: 120,
90
172
  paddingX: 12,
91
173
  paddingY: 8,
@@ -121,8 +203,7 @@ function resolveTheme(input) {
121
203
  components: {
122
204
  button: {
123
205
  ...theme.components.button,
124
- ...input?.components?.button,
125
- width: input?.components?.button?.width ?? input?.components?.button?.minWidth ?? theme.components.button.width
206
+ ...input?.components?.button
126
207
  },
127
208
  modal: {
128
209
  ...theme.components.modal,
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.cjs","names":[],"sources":["../src/utils.ts","../src/button.ts","../src/theme.ts","../src/plugin.ts"],"sourcesContent":["import type { Color, KAPLAYCtx } from 'kaplay';\n\n/**\n * Convert hex string to color or return color.\n *\n * @param k - The Kaplay context.\n * @param color - The hex string or color.\n * @returns The color.\n */\nexport function toColor(k: KAPLAYCtx, color: string | Color): Color {\n return typeof color === 'string' ? k.rgb(color) : color;\n}\n","import type { Color, KAPLAYCtx, Vec2 } from 'kaplay';\n\nimport type { ButtonOpt, Theme } from './types';\nimport { toColor } from './utils';\n\nexport function createButton(\n k: KAPLAYCtx,\n theme: Theme,\n text: string,\n opt: ButtonOpt & {\n pos?: Vec2;\n onClick?: () => void;\n color?: string | Color;\n hoverColor?: string | Color;\n pressedColor?: string | Color;\n textColor?: string | Color;\n },\n) {\n const width = opt.width ?? theme.components.button.width;\n const height = opt.height ?? theme.components.button.height;\n const radius = opt.radius ?? theme.shape.borderRadius;\n const baseColor = toColor(k, opt.color ?? opt.fill ?? theme.palette.primary);\n const hoverColor = toColor(k, opt.hoverColor ?? theme.palette.hover);\n const pressedColor = toColor(k, opt.pressedColor ?? theme.palette.pressed);\n const textColor = toColor(k, opt.textColor ?? theme.palette.text);\n const size = opt.size ?? theme.typography.fontSize;\n const font = opt.font ?? theme.typography.fontFamily;\n const pos = opt.pos ?? k.vec2();\n\n const button = k.add([\n k.rect(width, height, radius > 0 ? { radius } : undefined),\n k.pos(pos),\n k.area(),\n k.scale(),\n k.color(baseColor),\n k.anchor('center'),\n ]);\n\n const label = button.add([\n k.text(text, { size, font }),\n k.color(textColor as never),\n k.anchor('center'),\n ]);\n\n button.onClick(() => {\n button.color = toColor(k, pressedColor);\n opt.onClick?.();\n });\n\n button.onHover(() => {\n button.color = toColor(k, hoverColor);\n button.scale = k.vec2(1.05, 1.05);\n k.setCursor('pointer');\n });\n\n button.onHoverEnd(() => {\n button.color = baseColor;\n button.scale = k.vec2(1, 1);\n k.setCursor('default');\n });\n\n return {\n ...button,\n label,\n };\n}\n","import type { DeepPartial, Theme } from './types';\n\nconst PRIMARY = '#4f8cff';\nconst TEXT = '#ffffff';\n\n/** Default theme. */\nconst theme: Theme = {\n palette: {\n primary: PRIMARY,\n secondary: PRIMARY,\n background: '#111827',\n surface: '#1f2937',\n text: TEXT,\n textSecondary: TEXT,\n hover: PRIMARY,\n pressed: PRIMARY,\n disabled: '#6b7280',\n error: '#ef4444',\n },\n typography: {\n fontFamily: 'sans-serif',\n fontSize: 18,\n fontWeight: 400,\n lineHeight: 1.2,\n },\n shape: {\n borderRadius: 0,\n },\n spacing: 8,\n components: {\n button: {\n width: 120,\n height: 40,\n minWidth: 120,\n paddingX: 12,\n paddingY: 8,\n textTransform: 'none',\n },\n modal: {\n backdropOpacity: 0.5,\n borderRadius: 8,\n },\n tooltip: {\n background: '#111827',\n textColor: TEXT,\n delay: 300,\n },\n },\n};\n\n/** Resolve a partial theme into a fully resolved theme. */\nexport function resolveTheme(input?: DeepPartial<Theme>): Theme {\n return {\n palette: { ...theme.palette, ...input?.palette },\n typography: { ...theme.typography, ...input?.typography },\n shape: { ...theme.shape, ...input?.shape },\n spacing: input?.spacing ?? theme.spacing,\n components: {\n button: {\n ...theme.components.button,\n ...input?.components?.button,\n width:\n input?.components?.button?.width ??\n input?.components?.button?.minWidth ??\n theme.components.button.width,\n },\n modal: {\n ...theme.components.modal,\n ...input?.components?.modal,\n },\n tooltip: {\n ...theme.components.tooltip,\n ...input?.components?.tooltip,\n },\n },\n };\n}\n","import type { KAPLAYCtx } from 'kaplay';\n\nimport { createButton } from './button';\nimport { resolveTheme } from './theme';\nimport type {\n AddButton,\n DeepPartial,\n GetTheme,\n SetTheme,\n Theme,\n} from './types';\n\n/** KAPLAY UI plugin. */\nexport function uiPlugin(k: KAPLAYCtx, theme: DeepPartial<Theme> = {}) {\n let userTheme: DeepPartial<Theme> = theme;\n let currentTheme: Theme = resolveTheme(userTheme);\n\n const getTheme: GetTheme = () => currentTheme;\n\n const setTheme: SetTheme = (next) => {\n userTheme = { ...userTheme, ...next };\n currentTheme = resolveTheme(userTheme);\n };\n\n const addButton: AddButton = (text = '', opt = {}) => {\n return createButton(k, currentTheme, text, opt);\n };\n\n return {\n addButton,\n setTheme,\n getTheme,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"plugin.cjs","names":[],"sources":["../src/utils.ts","../src/button.ts","../src/theme.ts","../src/plugin.ts"],"sourcesContent":["import type { Color, KAPLAYCtx } from 'kaplay';\n\n/**\n * Convert hex string to color or return color.\n *\n * @param k - The Kaplay context.\n * @param color - The hex string or color.\n * @returns The color.\n */\nexport function toColor(k: KAPLAYCtx, color: string | Color): Color {\n return typeof color === 'string' ? k.rgb(color) : color;\n}\n","import type { Color, KAPLAYCtx, Vec2 } from 'kaplay';\n\nimport type { ButtonOpt, Theme } from './types';\nimport { toColor } from './utils';\n\nexport function createButton(\n k: KAPLAYCtx,\n theme: Theme,\n text: string,\n opt: ButtonOpt & {\n pos?: Vec2;\n onClick?: () => void;\n color?: string | Color;\n hoverColor?: string | Color;\n pressedColor?: string | Color;\n textColor?: string | Color;\n },\n) {\n const radius = opt.radius ?? theme.shape.borderRadius;\n const { minWidth, paddingX, paddingY } = theme.components.button;\n const baseColor = toColor(k, opt.color ?? opt.fill ?? theme.palette.primary);\n const hoverColor = toColor(k, opt.hoverColor ?? theme.palette.hover);\n const pressedColor = toColor(k, opt.pressedColor ?? theme.palette.pressed);\n const textColor = toColor(k, opt.textColor ?? theme.palette.text);\n const size = opt.size ?? theme.typography.fontSize;\n const font = opt.font ?? theme.typography.fontFamily;\n const pos = opt.pos ?? k.vec2();\n\n const textComp = k.text(text, { size, font });\n const isIconOnly = opt.icon !== undefined && text.length === 0;\n const hasBackground = opt.background !== undefined;\n const showFill = !hasBackground && !isIconOnly;\n const rectOpt = showFill\n ? radius > 0\n ? { radius }\n : undefined\n : { fill: false };\n\n const button = k.add([\n k.rect(0, 0, rectOpt),\n k.pos(pos),\n k.area(),\n k.scale(),\n k.color(baseColor),\n k.anchor('center'),\n ]);\n\n const background = opt.background\n ? button.add([k.sprite(opt.background), k.pos(), k.anchor('center')])\n : undefined;\n\n const icon = opt.icon\n ? button.add([k.sprite(opt.icon), k.pos(), k.anchor('center')])\n : undefined;\n\n const label = button.add([\n textComp,\n k.color(textColor as never),\n k.pos(),\n k.anchor('center'),\n ]);\n\n let iconWidth = 0;\n let iconHeight = 0;\n let iconState: 'base' | 'hover' | 'pressed' = 'base';\n let backgroundWidth = 0;\n let backgroundHeight = 0;\n let backgroundMeasured = false;\n let backgroundState: 'base' | 'hover' | 'pressed' = 'base';\n\n /**\n * Update button layout:\n *\n * - caches the base icon/background dimensions,\n * - calculates content width/height,\n * - updates `button.width` / `button.height`,\n * - stretches the background and repositions the icon and label.\n */\n function layout() {\n if (icon && iconState === 'base') {\n iconWidth = icon.width;\n iconHeight = icon.height;\n }\n\n if (\n background &&\n backgroundState === 'base' &&\n !backgroundMeasured &&\n (background.width > 0 || background.height > 0)\n ) {\n backgroundWidth = background.width;\n backgroundHeight = background.height;\n backgroundMeasured = true;\n }\n\n const gap = icon && iconWidth > 0 && label.width > 0 ? theme.spacing : 0;\n const contentWidth = iconWidth + gap + label.width;\n const contentHeight = Math.max(iconHeight, label.height);\n const contentBoxWidth = contentWidth + paddingX * 2;\n const contentBoxHeight = contentHeight + paddingY * 2;\n\n const nextWidth =\n opt.width ??\n (background\n ? Math.max(contentBoxWidth, backgroundWidth)\n : Math.max(contentBoxWidth, minWidth));\n const nextHeight =\n opt.height ??\n (background\n ? Math.max(contentBoxHeight, backgroundHeight)\n : contentBoxHeight);\n\n if (button.width !== nextWidth) {\n button.width = nextWidth;\n }\n\n if (button.height !== nextHeight) {\n button.height = nextHeight;\n }\n\n if (background && backgroundMeasured) {\n background.width = nextWidth;\n background.height = nextHeight;\n background.pos = k.vec2();\n }\n\n if (icon) {\n icon.pos = k.vec2(-contentWidth / 2 + iconWidth / 2, 0);\n }\n\n label.pos = k.vec2(\n -contentWidth / 2 + iconWidth + gap + label.width / 2,\n 0,\n );\n }\n\n layout();\n\n if (\n opt.width === undefined ||\n opt.height === undefined ||\n opt.icon !== undefined ||\n opt.background !== undefined\n ) {\n button.onUpdate(layout);\n }\n\n button.onClick(() => {\n button.color = toColor(k, pressedColor);\n\n if (icon && opt.pressedIcon) {\n iconState = 'pressed';\n icon.sprite = opt.pressedIcon as string;\n }\n\n if (background && opt.pressedBackground) {\n backgroundState = 'pressed';\n background.sprite = opt.pressedBackground as string;\n }\n\n opt.onClick?.();\n });\n\n button.onHover(() => {\n button.color = toColor(k, hoverColor);\n button.scale = k.vec2(1.05, 1.05);\n k.setCursor('pointer');\n\n if (icon && opt.hoverIcon) {\n iconState = 'hover';\n icon.sprite = opt.hoverIcon as string;\n }\n\n if (background && opt.hoverBackground) {\n backgroundState = 'hover';\n background.sprite = opt.hoverBackground as string;\n }\n });\n\n button.onHoverEnd(() => {\n button.color = baseColor;\n button.scale = k.vec2(1, 1);\n k.setCursor('default');\n\n if (icon && opt.icon) {\n iconState = 'base';\n icon.sprite = opt.icon as string;\n }\n\n if (background && opt.background) {\n backgroundState = 'base';\n background.sprite = opt.background as string;\n }\n\n if (icon || background) {\n layout();\n }\n });\n\n return {\n ...button,\n background,\n icon,\n label,\n };\n}\n","import type { DeepPartial, Theme } from './types';\n\nconst PRIMARY = '#4f8cff';\nconst TEXT = '#ffffff';\n\n/** Default theme. */\nconst theme: Theme = {\n palette: {\n primary: PRIMARY,\n secondary: PRIMARY,\n background: '#111827',\n surface: '#1f2937',\n text: TEXT,\n textSecondary: TEXT,\n hover: PRIMARY,\n pressed: PRIMARY,\n disabled: '#6b7280',\n error: '#ef4444',\n },\n typography: {\n fontFamily: 'sans-serif',\n fontSize: 18,\n fontWeight: 400,\n lineHeight: 1.2,\n },\n shape: {\n borderRadius: 0,\n },\n spacing: 8,\n components: {\n button: {\n minWidth: 120,\n paddingX: 12,\n paddingY: 8,\n textTransform: 'none',\n },\n modal: {\n backdropOpacity: 0.5,\n borderRadius: 8,\n },\n tooltip: {\n background: '#111827',\n textColor: TEXT,\n delay: 300,\n },\n },\n};\n\n/** Resolve a partial theme into a fully resolved theme. */\nexport function resolveTheme(input?: DeepPartial<Theme>): Theme {\n return {\n palette: { ...theme.palette, ...input?.palette },\n typography: { ...theme.typography, ...input?.typography },\n shape: { ...theme.shape, ...input?.shape },\n spacing: input?.spacing ?? theme.spacing,\n components: {\n button: {\n ...theme.components.button,\n ...input?.components?.button,\n },\n modal: {\n ...theme.components.modal,\n ...input?.components?.modal,\n },\n tooltip: {\n ...theme.components.tooltip,\n ...input?.components?.tooltip,\n },\n },\n };\n}\n","import type { KAPLAYCtx } from 'kaplay';\n\nimport { createButton } from './button';\nimport { resolveTheme } from './theme';\nimport type {\n AddButton,\n DeepPartial,\n GetTheme,\n SetTheme,\n Theme,\n} from './types';\n\n/** KAPLAY UI plugin. */\nexport function uiPlugin(k: KAPLAYCtx, theme: DeepPartial<Theme> = {}) {\n let userTheme: DeepPartial<Theme> = theme;\n let currentTheme: Theme = resolveTheme(userTheme);\n\n const getTheme: GetTheme = () => currentTheme;\n\n const setTheme: SetTheme = (next) => {\n userTheme = { ...userTheme, ...next };\n currentTheme = resolveTheme(userTheme);\n };\n\n const addButton: AddButton = (text = '', opt = {}) => {\n return createButton(k, currentTheme, text, opt);\n };\n\n return {\n addButton,\n setTheme,\n getTheme,\n };\n}\n"],"mappings":"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"}
package/dist/plugin.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { Color, KAPLAYCtx, Vec2 } from "kaplay";
1
+ import { Asset, Color, KAPLAYCtx, SpriteData, Vec2 } from "kaplay";
2
2
  declare function createButton(k: KAPLAYCtx, theme: Theme, text: string, opt: ButtonOpt & {
3
3
  pos?: Vec2;
4
4
  onClick?: () => void;
@@ -7,7 +7,9 @@ declare function createButton(k: KAPLAYCtx, theme: Theme, text: string, opt: But
7
7
  pressedColor?: string | Color;
8
8
  textColor?: string | Color;
9
9
  }): {
10
- label: import("kaplay").GameObj<import("kaplay").ColorComp | import("kaplay").AnchorComp | import("kaplay").TextComp>;
10
+ background: import("kaplay").GameObj<import("kaplay").PosComp | import("kaplay").AnchorComp | import("kaplay").SpriteComp> | undefined;
11
+ icon: import("kaplay").GameObj<import("kaplay").PosComp | import("kaplay").AnchorComp | import("kaplay").SpriteComp> | undefined;
12
+ label: import("kaplay").GameObj<import("kaplay").PosComp | import("kaplay").ColorComp | import("kaplay").AnchorComp | import("kaplay").TextComp>;
11
13
  add<T>(comps?: import("kaplay").CompList<T> | import("kaplay").GameObj<T>): import("kaplay").GameObj<T>;
12
14
  readd<T>(obj: import("kaplay").GameObj<T>): import("kaplay").GameObj<T>;
13
15
  remove(obj: import("kaplay").GameObj): void;
@@ -143,6 +145,7 @@ declare function createButton(k: KAPLAYCtx, theme: Theme, text: string, opt: But
143
145
  color: Color;
144
146
  anchor: import("kaplay").Anchor | Vec2;
145
147
  };
148
+ type SpriteSource = string | SpriteData | Asset<SpriteData>;
146
149
  declare module 'kaplay' {
147
150
  interface KAPLAYCtx {
148
151
  addButton: AddButton;
@@ -188,8 +191,6 @@ interface Theme {
188
191
  spacing: number;
189
192
  components: {
190
193
  button: {
191
- width: number;
192
- height: number;
193
194
  minWidth: number;
194
195
  paddingX: number;
195
196
  paddingY: number;
@@ -217,6 +218,18 @@ interface ButtonOpt {
217
218
  radius?: number;
218
219
  hoverFill?: string | Color;
219
220
  pressedFill?: string | Color;
221
+ /** Sprite drawn beside the label. */
222
+ icon?: SpriteSource;
223
+ /** Sprite drawn beside the label on hover. */
224
+ hoverIcon?: SpriteSource;
225
+ /** Sprite drawn beside the label when pressed. */
226
+ pressedIcon?: SpriteSource;
227
+ /** Sprite drawn behind the label. */
228
+ background?: SpriteSource;
229
+ /** Sprite drawn behind the label on hover. */
230
+ hoverBackground?: SpriteSource;
231
+ /** Sprite drawn behind the label when pressed. */
232
+ pressedBackground?: SpriteSource;
220
233
  }
221
234
  /** KAPLAY UI plugin. */
222
235
  export declare function uiPlugin(k: KAPLAYCtx, theme?: DeepPartial<Theme>): {
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.cts","names":[],"sources":["../src/button.ts","../src/types.ts","../src/plugin.ts"],"mappings":";iBAKgB,aACd,GAAG,WACH,OAAO,OACP,cACA,KAAK;EACH,MAAM;EACN;EACA,iBAAiB;EACjB,sBAAsB;EACtB,wBAAwB;EACxB,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;EAmDuj0M,GAAA,wBAAA,mCAAA,YAAG,sCAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAA34hJ,sBAAA,KAAA,IAAA,sBAAA,SAAA,uBAAC,sCAAA;KAA8K,IAAA,sBAAA,SAAA,uBAAC,sCAAA;;;KAAsN,sBAAA,KAAA,IAAA,sBAAA,SAAA,uBAAC,+BAAA,oCAAA;KAAuM,IAAA,sBAAA,SAAA,uBAAC,sCAAA;;;;;;;;;;;;;;;;;;;;;;;;;;YC7D3wzD;IACR,WAAW;IACX,UAAU;IACV,UAAU;;;KAIF,aACV,eACA,MAAM;EACJ,MAAM;EACN;EACA,iBAAiB;EACjB,sBAAsB;EACtB,wBAAwB;EACxB,qBAAqB;MAEpB,kBAAkB;KAEX,iBAAiB;KAEjB,YAAY,OAAO,YAAY;;KAG/B,YAAY,QACrB,WAAW,KAAK,EAAE,oBAAoB,YAAY,EAAE,MAAM,EAAE;;UAI9C;EACf;IACE,kBAAkB;IAClB,oBAAoB;IACpB,qBAAqB;IACrB,kBAAkB;IAClB,eAAe;IACf,wBAAwB;IACxB,gBAAgB;IAChB,kBAAkB;IAClB,mBAAmB;IACnB,gBAAgB;;EAElB;IACE;IACA;IACA;IACA;;EAEF;IACE;;EAEF;EACA;IACE;MACE;MACA;MACA;MACA;MACA;MACA;;IAEF;MACE;MACA;;IAEF;MACE,qBAAqB;MACrB,oBAAoB;MACpB;;;;;UAMW;EACf;EACA;EACA;EACA;EACA,gBAAgB;EAChB,qBAAqB;EACrB;EACA,qBAAqB;EACrB,uBAAuB;;;wBC3ET,SAAS,GAAG,WAAW,QAAO,YAAY"}
1
+ {"version":3,"file":"plugin.d.cts","names":[],"sources":["../src/button.ts","../src/types.ts","../src/plugin.ts"],"mappings":";iBAKgB,aACd,GAAG,WACH,OAAO,OACP,cACA,KAAK;EACH,MAAM;EACN;EACA,iBAAiB;EACjB,sBAAsB;EACtB,wBAAwB;EACxB,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;EA+LmntM,GAAA,wBAAA,mCAAA,YAAG,sCAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAA34hJ,sBAAA,KAAA,IAAA,sBAAA,SAAA,uBAAC,sCAAA;KAA8K,IAAA,sBAAA,SAAA,uBAAC,sCAAA;;;KAAsN,sBAAA,KAAA,IAAA,sBAAA,SAAA,uBAAC,+BAAA,oCAAA;KAAuM,IAAA,sBAAA,SAAA,uBAAC,sCAAA;;;;;;;;;;;;;;;;;;;;;;;;;KC1M90sD,wBAAwB,aAAa,MAAM;;YAGpC;IACR,WAAW;IACX,UAAU;IACV,UAAU;;;KAIF,aACV,eACA,MAAM;EACJ,MAAM;EACN;EACA,iBAAiB;EACjB,sBAAsB;EACtB,wBAAwB;EACxB,qBAAqB;MAEpB,kBAAkB;KAEX,iBAAiB;KAEjB,YAAY,OAAO,YAAY;;KAG/B,YAAY,QACrB,WAAW,KAAK,EAAE,oBAAoB,YAAY,EAAE,MAAM,EAAE;;UAI9C;EACf;IACE,kBAAkB;IAClB,oBAAoB;IACpB,qBAAqB;IACrB,kBAAkB;IAClB,eAAe;IACf,wBAAwB;IACxB,gBAAgB;IAChB,kBAAkB;IAClB,mBAAmB;IACnB,gBAAgB;;EAElB;IACE;IACA;IACA;IACA;;EAEF;IACE;;EAEF;EACA;IACE;MACE;MACA;MACA;MACA;;IAEF;MACE;MACA;;IAEF;MACE,qBAAqB;MACrB,oBAAoB;MACpB;;;;;UAMW;EACf;EACA;EACA;EACA;EACA,gBAAgB;EAChB,qBAAqB;EACrB;EACA,qBAAqB;EACrB,uBAAuB;;EAEvB,OAAO;;EAEP,YAAY;;EAEZ,cAAc;;EAEd,aAAa;;EAEb,kBAAkB;;EAElB,oBAAoB;;;wBCvFN,SAAS,GAAG,WAAW,QAAO,YAAY"}
package/dist/plugin.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { Color, KAPLAYCtx, Vec2 } from "kaplay";
1
+ import { Asset, Color, KAPLAYCtx, SpriteData, Vec2 } from "kaplay";
2
2
  declare function createButton(k: KAPLAYCtx, theme: Theme, text: string, opt: ButtonOpt & {
3
3
  pos?: Vec2;
4
4
  onClick?: () => void;
@@ -7,7 +7,9 @@ declare function createButton(k: KAPLAYCtx, theme: Theme, text: string, opt: But
7
7
  pressedColor?: string | Color;
8
8
  textColor?: string | Color;
9
9
  }): {
10
- label: import("kaplay").GameObj<import("kaplay").ColorComp | import("kaplay").AnchorComp | import("kaplay").TextComp>;
10
+ background: import("kaplay").GameObj<import("kaplay").PosComp | import("kaplay").AnchorComp | import("kaplay").SpriteComp> | undefined;
11
+ icon: import("kaplay").GameObj<import("kaplay").PosComp | import("kaplay").AnchorComp | import("kaplay").SpriteComp> | undefined;
12
+ label: import("kaplay").GameObj<import("kaplay").PosComp | import("kaplay").ColorComp | import("kaplay").AnchorComp | import("kaplay").TextComp>;
11
13
  add<T>(comps?: import("kaplay").CompList<T> | import("kaplay").GameObj<T>): import("kaplay").GameObj<T>;
12
14
  readd<T>(obj: import("kaplay").GameObj<T>): import("kaplay").GameObj<T>;
13
15
  remove(obj: import("kaplay").GameObj): void;
@@ -143,6 +145,7 @@ declare function createButton(k: KAPLAYCtx, theme: Theme, text: string, opt: But
143
145
  color: Color;
144
146
  anchor: import("kaplay").Anchor | Vec2;
145
147
  };
148
+ type SpriteSource = string | SpriteData | Asset<SpriteData>;
146
149
  declare module 'kaplay' {
147
150
  interface KAPLAYCtx {
148
151
  addButton: AddButton;
@@ -188,8 +191,6 @@ interface Theme {
188
191
  spacing: number;
189
192
  components: {
190
193
  button: {
191
- width: number;
192
- height: number;
193
194
  minWidth: number;
194
195
  paddingX: number;
195
196
  paddingY: number;
@@ -217,6 +218,18 @@ interface ButtonOpt {
217
218
  radius?: number;
218
219
  hoverFill?: string | Color;
219
220
  pressedFill?: string | Color;
221
+ /** Sprite drawn beside the label. */
222
+ icon?: SpriteSource;
223
+ /** Sprite drawn beside the label on hover. */
224
+ hoverIcon?: SpriteSource;
225
+ /** Sprite drawn beside the label when pressed. */
226
+ pressedIcon?: SpriteSource;
227
+ /** Sprite drawn behind the label. */
228
+ background?: SpriteSource;
229
+ /** Sprite drawn behind the label on hover. */
230
+ hoverBackground?: SpriteSource;
231
+ /** Sprite drawn behind the label when pressed. */
232
+ pressedBackground?: SpriteSource;
220
233
  }
221
234
  /** KAPLAY UI plugin. */
222
235
  export declare function uiPlugin(k: KAPLAYCtx, theme?: DeepPartial<Theme>): {
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.mts","names":[],"sources":["../src/button.ts","../src/types.ts","../src/plugin.ts"],"mappings":";iBAKgB,aACd,GAAG,WACH,OAAO,OACP,cACA,KAAK;EACH,MAAM;EACN;EACA,iBAAiB;EACjB,sBAAsB;EACtB,wBAAwB;EACxB,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;EAmDuj0M,GAAA,wBAAA,mCAAA,YAAG,sCAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAA34hJ,sBAAA,KAAA,IAAA,sBAAA,SAAA,uBAAC,sCAAA;KAA8K,IAAA,sBAAA,SAAA,uBAAC,sCAAA;;;KAAsN,sBAAA,KAAA,IAAA,sBAAA,SAAA,uBAAC,+BAAA,oCAAA;KAAuM,IAAA,sBAAA,SAAA,uBAAC,sCAAA;;;;;;;;;;;;;;;;;;;;;;;;;;YC7D3wzD;IACR,WAAW;IACX,UAAU;IACV,UAAU;;;KAIF,aACV,eACA,MAAM;EACJ,MAAM;EACN;EACA,iBAAiB;EACjB,sBAAsB;EACtB,wBAAwB;EACxB,qBAAqB;MAEpB,kBAAkB;KAEX,iBAAiB;KAEjB,YAAY,OAAO,YAAY;;KAG/B,YAAY,QACrB,WAAW,KAAK,EAAE,oBAAoB,YAAY,EAAE,MAAM,EAAE;;UAI9C;EACf;IACE,kBAAkB;IAClB,oBAAoB;IACpB,qBAAqB;IACrB,kBAAkB;IAClB,eAAe;IACf,wBAAwB;IACxB,gBAAgB;IAChB,kBAAkB;IAClB,mBAAmB;IACnB,gBAAgB;;EAElB;IACE;IACA;IACA;IACA;;EAEF;IACE;;EAEF;EACA;IACE;MACE;MACA;MACA;MACA;MACA;MACA;;IAEF;MACE;MACA;;IAEF;MACE,qBAAqB;MACrB,oBAAoB;MACpB;;;;;UAMW;EACf;EACA;EACA;EACA;EACA,gBAAgB;EAChB,qBAAqB;EACrB;EACA,qBAAqB;EACrB,uBAAuB;;;wBC3ET,SAAS,GAAG,WAAW,QAAO,YAAY"}
1
+ {"version":3,"file":"plugin.d.mts","names":[],"sources":["../src/button.ts","../src/types.ts","../src/plugin.ts"],"mappings":";iBAKgB,aACd,GAAG,WACH,OAAO,OACP,cACA,KAAK;EACH,MAAM;EACN;EACA,iBAAiB;EACjB,sBAAsB;EACtB,wBAAwB;EACxB,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;EA+LmntM,GAAA,wBAAA,mCAAA,YAAG,sCAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAA34hJ,sBAAA,KAAA,IAAA,sBAAA,SAAA,uBAAC,sCAAA;KAA8K,IAAA,sBAAA,SAAA,uBAAC,sCAAA;;;KAAsN,sBAAA,KAAA,IAAA,sBAAA,SAAA,uBAAC,+BAAA,oCAAA;KAAuM,IAAA,sBAAA,SAAA,uBAAC,sCAAA;;;;;;;;;;;;;;;;;;;;;;;;;KC1M90sD,wBAAwB,aAAa,MAAM;;YAGpC;IACR,WAAW;IACX,UAAU;IACV,UAAU;;;KAIF,aACV,eACA,MAAM;EACJ,MAAM;EACN;EACA,iBAAiB;EACjB,sBAAsB;EACtB,wBAAwB;EACxB,qBAAqB;MAEpB,kBAAkB;KAEX,iBAAiB;KAEjB,YAAY,OAAO,YAAY;;KAG/B,YAAY,QACrB,WAAW,KAAK,EAAE,oBAAoB,YAAY,EAAE,MAAM,EAAE;;UAI9C;EACf;IACE,kBAAkB;IAClB,oBAAoB;IACpB,qBAAqB;IACrB,kBAAkB;IAClB,eAAe;IACf,wBAAwB;IACxB,gBAAgB;IAChB,kBAAkB;IAClB,mBAAmB;IACnB,gBAAgB;;EAElB;IACE;IACA;IACA;IACA;;EAEF;IACE;;EAEF;EACA;IACE;MACE;MACA;MACA;MACA;;IAEF;MACE;MACA;;IAEF;MACE,qBAAqB;MACrB,oBAAoB;MACpB;;;;;UAMW;EACf;EACA;EACA;EACA;EACA,gBAAgB;EAChB,qBAAqB;EACrB;EACA,qBAAqB;EACrB,uBAAuB;;EAEvB,OAAO;;EAEP,YAAY;;EAEZ,cAAc;;EAEd,aAAa;;EAEb,kBAAkB;;EAElB,oBAAoB;;;wBCvFN,SAAS,GAAG,WAAW,QAAO,YAAY"}
package/dist/plugin.mjs CHANGED
@@ -11,9 +11,8 @@ function toColor(k, color) {
11
11
  }
12
12
  // src/button.ts
13
13
  function createButton(k, theme, text, opt) {
14
- const width = opt.width ?? theme.components.button.width;
15
- const height = opt.height ?? theme.components.button.height;
16
14
  const radius = opt.radius ?? theme.shape.borderRadius;
15
+ const { minWidth, paddingX, paddingY } = theme.components.button;
17
16
  const baseColor = toColor(k, opt.color ?? opt.fill ?? theme.palette.primary);
18
17
  const hoverColor = toColor(k, opt.hoverColor ?? theme.palette.hover);
19
18
  const pressedColor = toColor(k, opt.pressedColor ?? theme.palette.pressed);
@@ -21,38 +20,123 @@ function createButton(k, theme, text, opt) {
21
20
  const size = opt.size ?? theme.typography.fontSize;
22
21
  const font = opt.font ?? theme.typography.fontFamily;
23
22
  const pos = opt.pos ?? k.vec2();
23
+ const textComp = k.text(text, {
24
+ size,
25
+ font
26
+ });
27
+ const isIconOnly = opt.icon !== void 0 && text.length === 0;
28
+ const rectOpt = !(opt.background !== void 0) && !isIconOnly ? radius > 0 ? { radius } : void 0 : { fill: false };
24
29
  const button = k.add([
25
- k.rect(width, height, radius > 0 ? { radius } : void 0),
30
+ k.rect(0, 0, rectOpt),
26
31
  k.pos(pos),
27
32
  k.area(),
28
33
  k.scale(),
29
34
  k.color(baseColor),
30
35
  k.anchor("center")
31
36
  ]);
37
+ const background = opt.background ? button.add([
38
+ k.sprite(opt.background),
39
+ k.pos(),
40
+ k.anchor("center")
41
+ ]) : void 0;
42
+ const icon = opt.icon ? button.add([
43
+ k.sprite(opt.icon),
44
+ k.pos(),
45
+ k.anchor("center")
46
+ ]) : void 0;
32
47
  const label = button.add([
33
- k.text(text, {
34
- size,
35
- font
36
- }),
48
+ textComp,
37
49
  k.color(textColor),
50
+ k.pos(),
38
51
  k.anchor("center")
39
52
  ]);
53
+ let iconWidth = 0;
54
+ let iconHeight = 0;
55
+ let iconState = "base";
56
+ let backgroundWidth = 0;
57
+ let backgroundHeight = 0;
58
+ let backgroundMeasured = false;
59
+ let backgroundState = "base";
60
+ /**
61
+ * Update button layout:
62
+ *
63
+ * - caches the base icon/background dimensions,
64
+ * - calculates content width/height,
65
+ * - updates `button.width` / `button.height`,
66
+ * - stretches the background and repositions the icon and label.
67
+ */
68
+ function layout() {
69
+ if (icon && iconState === "base") {
70
+ iconWidth = icon.width;
71
+ iconHeight = icon.height;
72
+ }
73
+ if (background && backgroundState === "base" && !backgroundMeasured && (background.width > 0 || background.height > 0)) {
74
+ backgroundWidth = background.width;
75
+ backgroundHeight = background.height;
76
+ backgroundMeasured = true;
77
+ }
78
+ const gap = icon && iconWidth > 0 && label.width > 0 ? theme.spacing : 0;
79
+ const contentWidth = iconWidth + gap + label.width;
80
+ const contentHeight = Math.max(iconHeight, label.height);
81
+ const contentBoxWidth = contentWidth + paddingX * 2;
82
+ const contentBoxHeight = contentHeight + paddingY * 2;
83
+ const nextWidth = opt.width ?? (background ? Math.max(contentBoxWidth, backgroundWidth) : Math.max(contentBoxWidth, minWidth));
84
+ const nextHeight = opt.height ?? (background ? Math.max(contentBoxHeight, backgroundHeight) : contentBoxHeight);
85
+ if (button.width !== nextWidth) button.width = nextWidth;
86
+ if (button.height !== nextHeight) button.height = nextHeight;
87
+ if (background && backgroundMeasured) {
88
+ background.width = nextWidth;
89
+ background.height = nextHeight;
90
+ background.pos = k.vec2();
91
+ }
92
+ if (icon) icon.pos = k.vec2(-contentWidth / 2 + iconWidth / 2, 0);
93
+ label.pos = k.vec2(-contentWidth / 2 + iconWidth + gap + label.width / 2, 0);
94
+ }
95
+ layout();
96
+ if (opt.width === void 0 || opt.height === void 0 || opt.icon !== void 0 || opt.background !== void 0) button.onUpdate(layout);
40
97
  button.onClick(() => {
41
98
  button.color = toColor(k, pressedColor);
99
+ if (icon && opt.pressedIcon) {
100
+ iconState = "pressed";
101
+ icon.sprite = opt.pressedIcon;
102
+ }
103
+ if (background && opt.pressedBackground) {
104
+ backgroundState = "pressed";
105
+ background.sprite = opt.pressedBackground;
106
+ }
42
107
  opt.onClick?.();
43
108
  });
44
109
  button.onHover(() => {
45
110
  button.color = toColor(k, hoverColor);
46
111
  button.scale = k.vec2(1.05, 1.05);
47
112
  k.setCursor("pointer");
113
+ if (icon && opt.hoverIcon) {
114
+ iconState = "hover";
115
+ icon.sprite = opt.hoverIcon;
116
+ }
117
+ if (background && opt.hoverBackground) {
118
+ backgroundState = "hover";
119
+ background.sprite = opt.hoverBackground;
120
+ }
48
121
  });
49
122
  button.onHoverEnd(() => {
50
123
  button.color = baseColor;
51
124
  button.scale = k.vec2(1, 1);
52
125
  k.setCursor("default");
126
+ if (icon && opt.icon) {
127
+ iconState = "base";
128
+ icon.sprite = opt.icon;
129
+ }
130
+ if (background && opt.background) {
131
+ backgroundState = "base";
132
+ background.sprite = opt.background;
133
+ }
134
+ if (icon || background) layout();
53
135
  });
54
136
  return {
55
137
  ...button,
138
+ background,
139
+ icon,
56
140
  label
57
141
  };
58
142
  }
@@ -83,8 +167,6 @@ const theme = {
83
167
  spacing: 8,
84
168
  components: {
85
169
  button: {
86
- width: 120,
87
- height: 40,
88
170
  minWidth: 120,
89
171
  paddingX: 12,
90
172
  paddingY: 8,
@@ -120,8 +202,7 @@ function resolveTheme(input) {
120
202
  components: {
121
203
  button: {
122
204
  ...theme.components.button,
123
- ...input?.components?.button,
124
- width: input?.components?.button?.width ?? input?.components?.button?.minWidth ?? theme.components.button.width
205
+ ...input?.components?.button
125
206
  },
126
207
  modal: {
127
208
  ...theme.components.modal,
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.mjs","names":[],"sources":["../src/utils.ts","../src/button.ts","../src/theme.ts","../src/plugin.ts"],"sourcesContent":["import type { Color, KAPLAYCtx } from 'kaplay';\n\n/**\n * Convert hex string to color or return color.\n *\n * @param k - The Kaplay context.\n * @param color - The hex string or color.\n * @returns The color.\n */\nexport function toColor(k: KAPLAYCtx, color: string | Color): Color {\n return typeof color === 'string' ? k.rgb(color) : color;\n}\n","import type { Color, KAPLAYCtx, Vec2 } from 'kaplay';\n\nimport type { ButtonOpt, Theme } from './types';\nimport { toColor } from './utils';\n\nexport function createButton(\n k: KAPLAYCtx,\n theme: Theme,\n text: string,\n opt: ButtonOpt & {\n pos?: Vec2;\n onClick?: () => void;\n color?: string | Color;\n hoverColor?: string | Color;\n pressedColor?: string | Color;\n textColor?: string | Color;\n },\n) {\n const width = opt.width ?? theme.components.button.width;\n const height = opt.height ?? theme.components.button.height;\n const radius = opt.radius ?? theme.shape.borderRadius;\n const baseColor = toColor(k, opt.color ?? opt.fill ?? theme.palette.primary);\n const hoverColor = toColor(k, opt.hoverColor ?? theme.palette.hover);\n const pressedColor = toColor(k, opt.pressedColor ?? theme.palette.pressed);\n const textColor = toColor(k, opt.textColor ?? theme.palette.text);\n const size = opt.size ?? theme.typography.fontSize;\n const font = opt.font ?? theme.typography.fontFamily;\n const pos = opt.pos ?? k.vec2();\n\n const button = k.add([\n k.rect(width, height, radius > 0 ? { radius } : undefined),\n k.pos(pos),\n k.area(),\n k.scale(),\n k.color(baseColor),\n k.anchor('center'),\n ]);\n\n const label = button.add([\n k.text(text, { size, font }),\n k.color(textColor as never),\n k.anchor('center'),\n ]);\n\n button.onClick(() => {\n button.color = toColor(k, pressedColor);\n opt.onClick?.();\n });\n\n button.onHover(() => {\n button.color = toColor(k, hoverColor);\n button.scale = k.vec2(1.05, 1.05);\n k.setCursor('pointer');\n });\n\n button.onHoverEnd(() => {\n button.color = baseColor;\n button.scale = k.vec2(1, 1);\n k.setCursor('default');\n });\n\n return {\n ...button,\n label,\n };\n}\n","import type { DeepPartial, Theme } from './types';\n\nconst PRIMARY = '#4f8cff';\nconst TEXT = '#ffffff';\n\n/** Default theme. */\nconst theme: Theme = {\n palette: {\n primary: PRIMARY,\n secondary: PRIMARY,\n background: '#111827',\n surface: '#1f2937',\n text: TEXT,\n textSecondary: TEXT,\n hover: PRIMARY,\n pressed: PRIMARY,\n disabled: '#6b7280',\n error: '#ef4444',\n },\n typography: {\n fontFamily: 'sans-serif',\n fontSize: 18,\n fontWeight: 400,\n lineHeight: 1.2,\n },\n shape: {\n borderRadius: 0,\n },\n spacing: 8,\n components: {\n button: {\n width: 120,\n height: 40,\n minWidth: 120,\n paddingX: 12,\n paddingY: 8,\n textTransform: 'none',\n },\n modal: {\n backdropOpacity: 0.5,\n borderRadius: 8,\n },\n tooltip: {\n background: '#111827',\n textColor: TEXT,\n delay: 300,\n },\n },\n};\n\n/** Resolve a partial theme into a fully resolved theme. */\nexport function resolveTheme(input?: DeepPartial<Theme>): Theme {\n return {\n palette: { ...theme.palette, ...input?.palette },\n typography: { ...theme.typography, ...input?.typography },\n shape: { ...theme.shape, ...input?.shape },\n spacing: input?.spacing ?? theme.spacing,\n components: {\n button: {\n ...theme.components.button,\n ...input?.components?.button,\n width:\n input?.components?.button?.width ??\n input?.components?.button?.minWidth ??\n theme.components.button.width,\n },\n modal: {\n ...theme.components.modal,\n ...input?.components?.modal,\n },\n tooltip: {\n ...theme.components.tooltip,\n ...input?.components?.tooltip,\n },\n },\n };\n}\n","import type { KAPLAYCtx } from 'kaplay';\n\nimport { createButton } from './button';\nimport { resolveTheme } from './theme';\nimport type {\n AddButton,\n DeepPartial,\n GetTheme,\n SetTheme,\n Theme,\n} from './types';\n\n/** KAPLAY UI plugin. */\nexport function uiPlugin(k: KAPLAYCtx, theme: DeepPartial<Theme> = {}) {\n let userTheme: DeepPartial<Theme> = theme;\n let currentTheme: Theme = resolveTheme(userTheme);\n\n const getTheme: GetTheme = () => currentTheme;\n\n const setTheme: SetTheme = (next) => {\n userTheme = { ...userTheme, ...next };\n currentTheme = resolveTheme(userTheme);\n };\n\n const addButton: AddButton = (text = '', opt = {}) => {\n return createButton(k, currentTheme, text, opt);\n };\n\n return {\n addButton,\n setTheme,\n getTheme,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"plugin.mjs","names":[],"sources":["../src/utils.ts","../src/button.ts","../src/theme.ts","../src/plugin.ts"],"sourcesContent":["import type { Color, KAPLAYCtx } from 'kaplay';\n\n/**\n * Convert hex string to color or return color.\n *\n * @param k - The Kaplay context.\n * @param color - The hex string or color.\n * @returns The color.\n */\nexport function toColor(k: KAPLAYCtx, color: string | Color): Color {\n return typeof color === 'string' ? k.rgb(color) : color;\n}\n","import type { Color, KAPLAYCtx, Vec2 } from 'kaplay';\n\nimport type { ButtonOpt, Theme } from './types';\nimport { toColor } from './utils';\n\nexport function createButton(\n k: KAPLAYCtx,\n theme: Theme,\n text: string,\n opt: ButtonOpt & {\n pos?: Vec2;\n onClick?: () => void;\n color?: string | Color;\n hoverColor?: string | Color;\n pressedColor?: string | Color;\n textColor?: string | Color;\n },\n) {\n const radius = opt.radius ?? theme.shape.borderRadius;\n const { minWidth, paddingX, paddingY } = theme.components.button;\n const baseColor = toColor(k, opt.color ?? opt.fill ?? theme.palette.primary);\n const hoverColor = toColor(k, opt.hoverColor ?? theme.palette.hover);\n const pressedColor = toColor(k, opt.pressedColor ?? theme.palette.pressed);\n const textColor = toColor(k, opt.textColor ?? theme.palette.text);\n const size = opt.size ?? theme.typography.fontSize;\n const font = opt.font ?? theme.typography.fontFamily;\n const pos = opt.pos ?? k.vec2();\n\n const textComp = k.text(text, { size, font });\n const isIconOnly = opt.icon !== undefined && text.length === 0;\n const hasBackground = opt.background !== undefined;\n const showFill = !hasBackground && !isIconOnly;\n const rectOpt = showFill\n ? radius > 0\n ? { radius }\n : undefined\n : { fill: false };\n\n const button = k.add([\n k.rect(0, 0, rectOpt),\n k.pos(pos),\n k.area(),\n k.scale(),\n k.color(baseColor),\n k.anchor('center'),\n ]);\n\n const background = opt.background\n ? button.add([k.sprite(opt.background), k.pos(), k.anchor('center')])\n : undefined;\n\n const icon = opt.icon\n ? button.add([k.sprite(opt.icon), k.pos(), k.anchor('center')])\n : undefined;\n\n const label = button.add([\n textComp,\n k.color(textColor as never),\n k.pos(),\n k.anchor('center'),\n ]);\n\n let iconWidth = 0;\n let iconHeight = 0;\n let iconState: 'base' | 'hover' | 'pressed' = 'base';\n let backgroundWidth = 0;\n let backgroundHeight = 0;\n let backgroundMeasured = false;\n let backgroundState: 'base' | 'hover' | 'pressed' = 'base';\n\n /**\n * Update button layout:\n *\n * - caches the base icon/background dimensions,\n * - calculates content width/height,\n * - updates `button.width` / `button.height`,\n * - stretches the background and repositions the icon and label.\n */\n function layout() {\n if (icon && iconState === 'base') {\n iconWidth = icon.width;\n iconHeight = icon.height;\n }\n\n if (\n background &&\n backgroundState === 'base' &&\n !backgroundMeasured &&\n (background.width > 0 || background.height > 0)\n ) {\n backgroundWidth = background.width;\n backgroundHeight = background.height;\n backgroundMeasured = true;\n }\n\n const gap = icon && iconWidth > 0 && label.width > 0 ? theme.spacing : 0;\n const contentWidth = iconWidth + gap + label.width;\n const contentHeight = Math.max(iconHeight, label.height);\n const contentBoxWidth = contentWidth + paddingX * 2;\n const contentBoxHeight = contentHeight + paddingY * 2;\n\n const nextWidth =\n opt.width ??\n (background\n ? Math.max(contentBoxWidth, backgroundWidth)\n : Math.max(contentBoxWidth, minWidth));\n const nextHeight =\n opt.height ??\n (background\n ? Math.max(contentBoxHeight, backgroundHeight)\n : contentBoxHeight);\n\n if (button.width !== nextWidth) {\n button.width = nextWidth;\n }\n\n if (button.height !== nextHeight) {\n button.height = nextHeight;\n }\n\n if (background && backgroundMeasured) {\n background.width = nextWidth;\n background.height = nextHeight;\n background.pos = k.vec2();\n }\n\n if (icon) {\n icon.pos = k.vec2(-contentWidth / 2 + iconWidth / 2, 0);\n }\n\n label.pos = k.vec2(\n -contentWidth / 2 + iconWidth + gap + label.width / 2,\n 0,\n );\n }\n\n layout();\n\n if (\n opt.width === undefined ||\n opt.height === undefined ||\n opt.icon !== undefined ||\n opt.background !== undefined\n ) {\n button.onUpdate(layout);\n }\n\n button.onClick(() => {\n button.color = toColor(k, pressedColor);\n\n if (icon && opt.pressedIcon) {\n iconState = 'pressed';\n icon.sprite = opt.pressedIcon as string;\n }\n\n if (background && opt.pressedBackground) {\n backgroundState = 'pressed';\n background.sprite = opt.pressedBackground as string;\n }\n\n opt.onClick?.();\n });\n\n button.onHover(() => {\n button.color = toColor(k, hoverColor);\n button.scale = k.vec2(1.05, 1.05);\n k.setCursor('pointer');\n\n if (icon && opt.hoverIcon) {\n iconState = 'hover';\n icon.sprite = opt.hoverIcon as string;\n }\n\n if (background && opt.hoverBackground) {\n backgroundState = 'hover';\n background.sprite = opt.hoverBackground as string;\n }\n });\n\n button.onHoverEnd(() => {\n button.color = baseColor;\n button.scale = k.vec2(1, 1);\n k.setCursor('default');\n\n if (icon && opt.icon) {\n iconState = 'base';\n icon.sprite = opt.icon as string;\n }\n\n if (background && opt.background) {\n backgroundState = 'base';\n background.sprite = opt.background as string;\n }\n\n if (icon || background) {\n layout();\n }\n });\n\n return {\n ...button,\n background,\n icon,\n label,\n };\n}\n","import type { DeepPartial, Theme } from './types';\n\nconst PRIMARY = '#4f8cff';\nconst TEXT = '#ffffff';\n\n/** Default theme. */\nconst theme: Theme = {\n palette: {\n primary: PRIMARY,\n secondary: PRIMARY,\n background: '#111827',\n surface: '#1f2937',\n text: TEXT,\n textSecondary: TEXT,\n hover: PRIMARY,\n pressed: PRIMARY,\n disabled: '#6b7280',\n error: '#ef4444',\n },\n typography: {\n fontFamily: 'sans-serif',\n fontSize: 18,\n fontWeight: 400,\n lineHeight: 1.2,\n },\n shape: {\n borderRadius: 0,\n },\n spacing: 8,\n components: {\n button: {\n minWidth: 120,\n paddingX: 12,\n paddingY: 8,\n textTransform: 'none',\n },\n modal: {\n backdropOpacity: 0.5,\n borderRadius: 8,\n },\n tooltip: {\n background: '#111827',\n textColor: TEXT,\n delay: 300,\n },\n },\n};\n\n/** Resolve a partial theme into a fully resolved theme. */\nexport function resolveTheme(input?: DeepPartial<Theme>): Theme {\n return {\n palette: { ...theme.palette, ...input?.palette },\n typography: { ...theme.typography, ...input?.typography },\n shape: { ...theme.shape, ...input?.shape },\n spacing: input?.spacing ?? theme.spacing,\n components: {\n button: {\n ...theme.components.button,\n ...input?.components?.button,\n },\n modal: {\n ...theme.components.modal,\n ...input?.components?.modal,\n },\n tooltip: {\n ...theme.components.tooltip,\n ...input?.components?.tooltip,\n },\n },\n };\n}\n","import type { KAPLAYCtx } from 'kaplay';\n\nimport { createButton } from './button';\nimport { resolveTheme } from './theme';\nimport type {\n AddButton,\n DeepPartial,\n GetTheme,\n SetTheme,\n Theme,\n} from './types';\n\n/** KAPLAY UI plugin. */\nexport function uiPlugin(k: KAPLAYCtx, theme: DeepPartial<Theme> = {}) {\n let userTheme: DeepPartial<Theme> = theme;\n let currentTheme: Theme = resolveTheme(userTheme);\n\n const getTheme: GetTheme = () => currentTheme;\n\n const setTheme: SetTheme = (next) => {\n userTheme = { ...userTheme, ...next };\n currentTheme = resolveTheme(userTheme);\n };\n\n const addButton: AddButton = (text = '', opt = {}) => {\n return createButton(k, currentTheme, text, opt);\n };\n\n return {\n addButton,\n setTheme,\n getTheme,\n };\n}\n"],"mappings":"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"}
@@ -15,9 +15,8 @@
15
15
  }
16
16
  // src/button.ts
17
17
  function createButton(k, theme, text, opt) {
18
- const width = opt.width ?? theme.components.button.width;
19
- const height = opt.height ?? theme.components.button.height;
20
18
  const radius = opt.radius ?? theme.shape.borderRadius;
19
+ const { minWidth, paddingX, paddingY } = theme.components.button;
21
20
  const baseColor = toColor(k, opt.color ?? opt.fill ?? theme.palette.primary);
22
21
  const hoverColor = toColor(k, opt.hoverColor ?? theme.palette.hover);
23
22
  const pressedColor = toColor(k, opt.pressedColor ?? theme.palette.pressed);
@@ -25,38 +24,123 @@
25
24
  const size = opt.size ?? theme.typography.fontSize;
26
25
  const font = opt.font ?? theme.typography.fontFamily;
27
26
  const pos = opt.pos ?? k.vec2();
27
+ const textComp = k.text(text, {
28
+ size,
29
+ font
30
+ });
31
+ const isIconOnly = opt.icon !== void 0 && text.length === 0;
32
+ const rectOpt = !(opt.background !== void 0) && !isIconOnly ? radius > 0 ? { radius } : void 0 : { fill: false };
28
33
  const button = k.add([
29
- k.rect(width, height, radius > 0 ? { radius } : void 0),
34
+ k.rect(0, 0, rectOpt),
30
35
  k.pos(pos),
31
36
  k.area(),
32
37
  k.scale(),
33
38
  k.color(baseColor),
34
39
  k.anchor("center")
35
40
  ]);
41
+ const background = opt.background ? button.add([
42
+ k.sprite(opt.background),
43
+ k.pos(),
44
+ k.anchor("center")
45
+ ]) : void 0;
46
+ const icon = opt.icon ? button.add([
47
+ k.sprite(opt.icon),
48
+ k.pos(),
49
+ k.anchor("center")
50
+ ]) : void 0;
36
51
  const label = button.add([
37
- k.text(text, {
38
- size,
39
- font
40
- }),
52
+ textComp,
41
53
  k.color(textColor),
54
+ k.pos(),
42
55
  k.anchor("center")
43
56
  ]);
57
+ let iconWidth = 0;
58
+ let iconHeight = 0;
59
+ let iconState = "base";
60
+ let backgroundWidth = 0;
61
+ let backgroundHeight = 0;
62
+ let backgroundMeasured = false;
63
+ let backgroundState = "base";
64
+ /**
65
+ * Update button layout:
66
+ *
67
+ * - caches the base icon/background dimensions,
68
+ * - calculates content width/height,
69
+ * - updates `button.width` / `button.height`,
70
+ * - stretches the background and repositions the icon and label.
71
+ */
72
+ function layout() {
73
+ if (icon && iconState === "base") {
74
+ iconWidth = icon.width;
75
+ iconHeight = icon.height;
76
+ }
77
+ if (background && backgroundState === "base" && !backgroundMeasured && (background.width > 0 || background.height > 0)) {
78
+ backgroundWidth = background.width;
79
+ backgroundHeight = background.height;
80
+ backgroundMeasured = true;
81
+ }
82
+ const gap = icon && iconWidth > 0 && label.width > 0 ? theme.spacing : 0;
83
+ const contentWidth = iconWidth + gap + label.width;
84
+ const contentHeight = Math.max(iconHeight, label.height);
85
+ const contentBoxWidth = contentWidth + paddingX * 2;
86
+ const contentBoxHeight = contentHeight + paddingY * 2;
87
+ const nextWidth = opt.width ?? (background ? Math.max(contentBoxWidth, backgroundWidth) : Math.max(contentBoxWidth, minWidth));
88
+ const nextHeight = opt.height ?? (background ? Math.max(contentBoxHeight, backgroundHeight) : contentBoxHeight);
89
+ if (button.width !== nextWidth) button.width = nextWidth;
90
+ if (button.height !== nextHeight) button.height = nextHeight;
91
+ if (background && backgroundMeasured) {
92
+ background.width = nextWidth;
93
+ background.height = nextHeight;
94
+ background.pos = k.vec2();
95
+ }
96
+ if (icon) icon.pos = k.vec2(-contentWidth / 2 + iconWidth / 2, 0);
97
+ label.pos = k.vec2(-contentWidth / 2 + iconWidth + gap + label.width / 2, 0);
98
+ }
99
+ layout();
100
+ if (opt.width === void 0 || opt.height === void 0 || opt.icon !== void 0 || opt.background !== void 0) button.onUpdate(layout);
44
101
  button.onClick(() => {
45
102
  button.color = toColor(k, pressedColor);
103
+ if (icon && opt.pressedIcon) {
104
+ iconState = "pressed";
105
+ icon.sprite = opt.pressedIcon;
106
+ }
107
+ if (background && opt.pressedBackground) {
108
+ backgroundState = "pressed";
109
+ background.sprite = opt.pressedBackground;
110
+ }
46
111
  opt.onClick?.();
47
112
  });
48
113
  button.onHover(() => {
49
114
  button.color = toColor(k, hoverColor);
50
115
  button.scale = k.vec2(1.05, 1.05);
51
116
  k.setCursor("pointer");
117
+ if (icon && opt.hoverIcon) {
118
+ iconState = "hover";
119
+ icon.sprite = opt.hoverIcon;
120
+ }
121
+ if (background && opt.hoverBackground) {
122
+ backgroundState = "hover";
123
+ background.sprite = opt.hoverBackground;
124
+ }
52
125
  });
53
126
  button.onHoverEnd(() => {
54
127
  button.color = baseColor;
55
128
  button.scale = k.vec2(1, 1);
56
129
  k.setCursor("default");
130
+ if (icon && opt.icon) {
131
+ iconState = "base";
132
+ icon.sprite = opt.icon;
133
+ }
134
+ if (background && opt.background) {
135
+ backgroundState = "base";
136
+ background.sprite = opt.background;
137
+ }
138
+ if (icon || background) layout();
57
139
  });
58
140
  return {
59
141
  ...button,
142
+ background,
143
+ icon,
60
144
  label
61
145
  };
62
146
  }
@@ -87,8 +171,6 @@
87
171
  spacing: 8,
88
172
  components: {
89
173
  button: {
90
- width: 120,
91
- height: 40,
92
174
  minWidth: 120,
93
175
  paddingX: 12,
94
176
  paddingY: 8,
@@ -124,8 +206,7 @@
124
206
  components: {
125
207
  button: {
126
208
  ...theme.components.button,
127
- ...input?.components?.button,
128
- width: input?.components?.button?.width ?? input?.components?.button?.minWidth ?? theme.components.button.width
209
+ ...input?.components?.button
129
210
  },
130
211
  modal: {
131
212
  ...theme.components.modal,
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.umd.js","names":[],"sources":["../src/utils.ts","../src/button.ts","../src/theme.ts","../src/plugin.ts"],"sourcesContent":["import type { Color, KAPLAYCtx } from 'kaplay';\n\n/**\n * Convert hex string to color or return color.\n *\n * @param k - The Kaplay context.\n * @param color - The hex string or color.\n * @returns The color.\n */\nexport function toColor(k: KAPLAYCtx, color: string | Color): Color {\n return typeof color === 'string' ? k.rgb(color) : color;\n}\n","import type { Color, KAPLAYCtx, Vec2 } from 'kaplay';\n\nimport type { ButtonOpt, Theme } from './types';\nimport { toColor } from './utils';\n\nexport function createButton(\n k: KAPLAYCtx,\n theme: Theme,\n text: string,\n opt: ButtonOpt & {\n pos?: Vec2;\n onClick?: () => void;\n color?: string | Color;\n hoverColor?: string | Color;\n pressedColor?: string | Color;\n textColor?: string | Color;\n },\n) {\n const width = opt.width ?? theme.components.button.width;\n const height = opt.height ?? theme.components.button.height;\n const radius = opt.radius ?? theme.shape.borderRadius;\n const baseColor = toColor(k, opt.color ?? opt.fill ?? theme.palette.primary);\n const hoverColor = toColor(k, opt.hoverColor ?? theme.palette.hover);\n const pressedColor = toColor(k, opt.pressedColor ?? theme.palette.pressed);\n const textColor = toColor(k, opt.textColor ?? theme.palette.text);\n const size = opt.size ?? theme.typography.fontSize;\n const font = opt.font ?? theme.typography.fontFamily;\n const pos = opt.pos ?? k.vec2();\n\n const button = k.add([\n k.rect(width, height, radius > 0 ? { radius } : undefined),\n k.pos(pos),\n k.area(),\n k.scale(),\n k.color(baseColor),\n k.anchor('center'),\n ]);\n\n const label = button.add([\n k.text(text, { size, font }),\n k.color(textColor as never),\n k.anchor('center'),\n ]);\n\n button.onClick(() => {\n button.color = toColor(k, pressedColor);\n opt.onClick?.();\n });\n\n button.onHover(() => {\n button.color = toColor(k, hoverColor);\n button.scale = k.vec2(1.05, 1.05);\n k.setCursor('pointer');\n });\n\n button.onHoverEnd(() => {\n button.color = baseColor;\n button.scale = k.vec2(1, 1);\n k.setCursor('default');\n });\n\n return {\n ...button,\n label,\n };\n}\n","import type { DeepPartial, Theme } from './types';\n\nconst PRIMARY = '#4f8cff';\nconst TEXT = '#ffffff';\n\n/** Default theme. */\nconst theme: Theme = {\n palette: {\n primary: PRIMARY,\n secondary: PRIMARY,\n background: '#111827',\n surface: '#1f2937',\n text: TEXT,\n textSecondary: TEXT,\n hover: PRIMARY,\n pressed: PRIMARY,\n disabled: '#6b7280',\n error: '#ef4444',\n },\n typography: {\n fontFamily: 'sans-serif',\n fontSize: 18,\n fontWeight: 400,\n lineHeight: 1.2,\n },\n shape: {\n borderRadius: 0,\n },\n spacing: 8,\n components: {\n button: {\n width: 120,\n height: 40,\n minWidth: 120,\n paddingX: 12,\n paddingY: 8,\n textTransform: 'none',\n },\n modal: {\n backdropOpacity: 0.5,\n borderRadius: 8,\n },\n tooltip: {\n background: '#111827',\n textColor: TEXT,\n delay: 300,\n },\n },\n};\n\n/** Resolve a partial theme into a fully resolved theme. */\nexport function resolveTheme(input?: DeepPartial<Theme>): Theme {\n return {\n palette: { ...theme.palette, ...input?.palette },\n typography: { ...theme.typography, ...input?.typography },\n shape: { ...theme.shape, ...input?.shape },\n spacing: input?.spacing ?? theme.spacing,\n components: {\n button: {\n ...theme.components.button,\n ...input?.components?.button,\n width:\n input?.components?.button?.width ??\n input?.components?.button?.minWidth ??\n theme.components.button.width,\n },\n modal: {\n ...theme.components.modal,\n ...input?.components?.modal,\n },\n tooltip: {\n ...theme.components.tooltip,\n ...input?.components?.tooltip,\n },\n },\n };\n}\n","import type { KAPLAYCtx } from 'kaplay';\n\nimport { createButton } from './button';\nimport { resolveTheme } from './theme';\nimport type {\n AddButton,\n DeepPartial,\n GetTheme,\n SetTheme,\n Theme,\n} from './types';\n\n/** KAPLAY UI plugin. */\nexport function uiPlugin(k: KAPLAYCtx, theme: DeepPartial<Theme> = {}) {\n let userTheme: DeepPartial<Theme> = theme;\n let currentTheme: Theme = resolveTheme(userTheme);\n\n const getTheme: GetTheme = () => currentTheme;\n\n const setTheme: SetTheme = (next) => {\n userTheme = { ...userTheme, ...next };\n currentTheme = resolveTheme(userTheme);\n };\n\n const addButton: AddButton = (text = '', opt = {}) => {\n return createButton(k, currentTheme, text, opt);\n };\n\n return {\n addButton,\n setTheme,\n getTheme,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"plugin.umd.js","names":[],"sources":["../src/utils.ts","../src/button.ts","../src/theme.ts","../src/plugin.ts"],"sourcesContent":["import type { Color, KAPLAYCtx } from 'kaplay';\n\n/**\n * Convert hex string to color or return color.\n *\n * @param k - The Kaplay context.\n * @param color - The hex string or color.\n * @returns The color.\n */\nexport function toColor(k: KAPLAYCtx, color: string | Color): Color {\n return typeof color === 'string' ? k.rgb(color) : color;\n}\n","import type { Color, KAPLAYCtx, Vec2 } from 'kaplay';\n\nimport type { ButtonOpt, Theme } from './types';\nimport { toColor } from './utils';\n\nexport function createButton(\n k: KAPLAYCtx,\n theme: Theme,\n text: string,\n opt: ButtonOpt & {\n pos?: Vec2;\n onClick?: () => void;\n color?: string | Color;\n hoverColor?: string | Color;\n pressedColor?: string | Color;\n textColor?: string | Color;\n },\n) {\n const radius = opt.radius ?? theme.shape.borderRadius;\n const { minWidth, paddingX, paddingY } = theme.components.button;\n const baseColor = toColor(k, opt.color ?? opt.fill ?? theme.palette.primary);\n const hoverColor = toColor(k, opt.hoverColor ?? theme.palette.hover);\n const pressedColor = toColor(k, opt.pressedColor ?? theme.palette.pressed);\n const textColor = toColor(k, opt.textColor ?? theme.palette.text);\n const size = opt.size ?? theme.typography.fontSize;\n const font = opt.font ?? theme.typography.fontFamily;\n const pos = opt.pos ?? k.vec2();\n\n const textComp = k.text(text, { size, font });\n const isIconOnly = opt.icon !== undefined && text.length === 0;\n const hasBackground = opt.background !== undefined;\n const showFill = !hasBackground && !isIconOnly;\n const rectOpt = showFill\n ? radius > 0\n ? { radius }\n : undefined\n : { fill: false };\n\n const button = k.add([\n k.rect(0, 0, rectOpt),\n k.pos(pos),\n k.area(),\n k.scale(),\n k.color(baseColor),\n k.anchor('center'),\n ]);\n\n const background = opt.background\n ? button.add([k.sprite(opt.background), k.pos(), k.anchor('center')])\n : undefined;\n\n const icon = opt.icon\n ? button.add([k.sprite(opt.icon), k.pos(), k.anchor('center')])\n : undefined;\n\n const label = button.add([\n textComp,\n k.color(textColor as never),\n k.pos(),\n k.anchor('center'),\n ]);\n\n let iconWidth = 0;\n let iconHeight = 0;\n let iconState: 'base' | 'hover' | 'pressed' = 'base';\n let backgroundWidth = 0;\n let backgroundHeight = 0;\n let backgroundMeasured = false;\n let backgroundState: 'base' | 'hover' | 'pressed' = 'base';\n\n /**\n * Update button layout:\n *\n * - caches the base icon/background dimensions,\n * - calculates content width/height,\n * - updates `button.width` / `button.height`,\n * - stretches the background and repositions the icon and label.\n */\n function layout() {\n if (icon && iconState === 'base') {\n iconWidth = icon.width;\n iconHeight = icon.height;\n }\n\n if (\n background &&\n backgroundState === 'base' &&\n !backgroundMeasured &&\n (background.width > 0 || background.height > 0)\n ) {\n backgroundWidth = background.width;\n backgroundHeight = background.height;\n backgroundMeasured = true;\n }\n\n const gap = icon && iconWidth > 0 && label.width > 0 ? theme.spacing : 0;\n const contentWidth = iconWidth + gap + label.width;\n const contentHeight = Math.max(iconHeight, label.height);\n const contentBoxWidth = contentWidth + paddingX * 2;\n const contentBoxHeight = contentHeight + paddingY * 2;\n\n const nextWidth =\n opt.width ??\n (background\n ? Math.max(contentBoxWidth, backgroundWidth)\n : Math.max(contentBoxWidth, minWidth));\n const nextHeight =\n opt.height ??\n (background\n ? Math.max(contentBoxHeight, backgroundHeight)\n : contentBoxHeight);\n\n if (button.width !== nextWidth) {\n button.width = nextWidth;\n }\n\n if (button.height !== nextHeight) {\n button.height = nextHeight;\n }\n\n if (background && backgroundMeasured) {\n background.width = nextWidth;\n background.height = nextHeight;\n background.pos = k.vec2();\n }\n\n if (icon) {\n icon.pos = k.vec2(-contentWidth / 2 + iconWidth / 2, 0);\n }\n\n label.pos = k.vec2(\n -contentWidth / 2 + iconWidth + gap + label.width / 2,\n 0,\n );\n }\n\n layout();\n\n if (\n opt.width === undefined ||\n opt.height === undefined ||\n opt.icon !== undefined ||\n opt.background !== undefined\n ) {\n button.onUpdate(layout);\n }\n\n button.onClick(() => {\n button.color = toColor(k, pressedColor);\n\n if (icon && opt.pressedIcon) {\n iconState = 'pressed';\n icon.sprite = opt.pressedIcon as string;\n }\n\n if (background && opt.pressedBackground) {\n backgroundState = 'pressed';\n background.sprite = opt.pressedBackground as string;\n }\n\n opt.onClick?.();\n });\n\n button.onHover(() => {\n button.color = toColor(k, hoverColor);\n button.scale = k.vec2(1.05, 1.05);\n k.setCursor('pointer');\n\n if (icon && opt.hoverIcon) {\n iconState = 'hover';\n icon.sprite = opt.hoverIcon as string;\n }\n\n if (background && opt.hoverBackground) {\n backgroundState = 'hover';\n background.sprite = opt.hoverBackground as string;\n }\n });\n\n button.onHoverEnd(() => {\n button.color = baseColor;\n button.scale = k.vec2(1, 1);\n k.setCursor('default');\n\n if (icon && opt.icon) {\n iconState = 'base';\n icon.sprite = opt.icon as string;\n }\n\n if (background && opt.background) {\n backgroundState = 'base';\n background.sprite = opt.background as string;\n }\n\n if (icon || background) {\n layout();\n }\n });\n\n return {\n ...button,\n background,\n icon,\n label,\n };\n}\n","import type { DeepPartial, Theme } from './types';\n\nconst PRIMARY = '#4f8cff';\nconst TEXT = '#ffffff';\n\n/** Default theme. */\nconst theme: Theme = {\n palette: {\n primary: PRIMARY,\n secondary: PRIMARY,\n background: '#111827',\n surface: '#1f2937',\n text: TEXT,\n textSecondary: TEXT,\n hover: PRIMARY,\n pressed: PRIMARY,\n disabled: '#6b7280',\n error: '#ef4444',\n },\n typography: {\n fontFamily: 'sans-serif',\n fontSize: 18,\n fontWeight: 400,\n lineHeight: 1.2,\n },\n shape: {\n borderRadius: 0,\n },\n spacing: 8,\n components: {\n button: {\n minWidth: 120,\n paddingX: 12,\n paddingY: 8,\n textTransform: 'none',\n },\n modal: {\n backdropOpacity: 0.5,\n borderRadius: 8,\n },\n tooltip: {\n background: '#111827',\n textColor: TEXT,\n delay: 300,\n },\n },\n};\n\n/** Resolve a partial theme into a fully resolved theme. */\nexport function resolveTheme(input?: DeepPartial<Theme>): Theme {\n return {\n palette: { ...theme.palette, ...input?.palette },\n typography: { ...theme.typography, ...input?.typography },\n shape: { ...theme.shape, ...input?.shape },\n spacing: input?.spacing ?? theme.spacing,\n components: {\n button: {\n ...theme.components.button,\n ...input?.components?.button,\n },\n modal: {\n ...theme.components.modal,\n ...input?.components?.modal,\n },\n tooltip: {\n ...theme.components.tooltip,\n ...input?.components?.tooltip,\n },\n },\n };\n}\n","import type { KAPLAYCtx } from 'kaplay';\n\nimport { createButton } from './button';\nimport { resolveTheme } from './theme';\nimport type {\n AddButton,\n DeepPartial,\n GetTheme,\n SetTheme,\n Theme,\n} from './types';\n\n/** KAPLAY UI plugin. */\nexport function uiPlugin(k: KAPLAYCtx, theme: DeepPartial<Theme> = {}) {\n let userTheme: DeepPartial<Theme> = theme;\n let currentTheme: Theme = resolveTheme(userTheme);\n\n const getTheme: GetTheme = () => currentTheme;\n\n const setTheme: SetTheme = (next) => {\n userTheme = { ...userTheme, ...next };\n currentTheme = resolveTheme(userTheme);\n };\n\n const addButton: AddButton = (text = '', opt = {}) => {\n return createButton(k, currentTheme, text, opt);\n };\n\n return {\n addButton,\n setTheme,\n getTheme,\n };\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- (function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports):typeof define==`function`&&define.amd?define([`exports`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.KaplayPluginUI={}))})(this,function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});function t(e,t){return typeof t==`string`?e.rgb(t):t}function n(e,n,r,i){let a=i.width??n.components.button.width,o=i.height??n.components.button.height,s=i.radius??n.shape.borderRadius,c=t(e,i.color??i.fill??n.palette.primary),l=t(e,i.hoverColor??n.palette.hover),u=t(e,i.pressedColor??n.palette.pressed),d=t(e,i.textColor??n.palette.text),f=i.size??n.typography.fontSize,p=i.font??n.typography.fontFamily,m=i.pos??e.vec2(),h=e.add([e.rect(a,o,s>0?{radius:s}:void 0),e.pos(m),e.area(),e.scale(),e.color(c),e.anchor(`center`)]),g=h.add([e.text(r,{size:f,font:p}),e.color(d),e.anchor(`center`)]);return h.onClick(()=>{h.color=t(e,u),i.onClick?.()}),h.onHover(()=>{h.color=t(e,l),h.scale=e.vec2(1.05,1.05),e.setCursor(`pointer`)}),h.onHoverEnd(()=>{h.color=c,h.scale=e.vec2(1,1),e.setCursor(`default`)}),{...h,label:g}}let r=`#4f8cff`,i=`#ffffff`,a={palette:{primary:r,secondary:r,background:`#111827`,surface:`#1f2937`,text:i,textSecondary:i,hover:r,pressed:r,disabled:`#6b7280`,error:`#ef4444`},typography:{fontFamily:`sans-serif`,fontSize:18,fontWeight:400,lineHeight:1.2},shape:{borderRadius:0},spacing:8,components:{button:{width:120,height:40,minWidth:120,paddingX:12,paddingY:8,textTransform:`none`},modal:{backdropOpacity:.5,borderRadius:8},tooltip:{background:`#111827`,textColor:i,delay:300}}};function o(e){return{palette:{...a.palette,...e?.palette},typography:{...a.typography,...e?.typography},shape:{...a.shape,...e?.shape},spacing:e?.spacing??a.spacing,components:{button:{...a.components.button,...e?.components?.button,width:e?.components?.button?.width??e?.components?.button?.minWidth??a.components.button.width},modal:{...a.components.modal,...e?.components?.modal},tooltip:{...a.components.tooltip,...e?.components?.tooltip}}}}function s(e,t={}){let r=t,i=o(r);return{addButton:(t=``,r={})=>n(e,i,t,r),setTheme:e=>{r={...r,...e},i=o(r)},getTheme:()=>i}}e.uiPlugin=s});
1
+ (function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports):typeof define==`function`&&define.amd?define([`exports`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.KaplayPluginUI={}))})(this,function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});function t(e,t){return typeof t==`string`?e.rgb(t):t}function n(e,n,r,i){let a=i.radius??n.shape.borderRadius,{minWidth:o,paddingX:s,paddingY:c}=n.components.button,l=t(e,i.color??i.fill??n.palette.primary),u=t(e,i.hoverColor??n.palette.hover),d=t(e,i.pressedColor??n.palette.pressed),f=t(e,i.textColor??n.palette.text),p=i.size??n.typography.fontSize,m=i.font??n.typography.fontFamily,h=i.pos??e.vec2(),g=e.text(r,{size:p,font:m}),_=i.icon!==void 0&&r.length===0,v=i.background===void 0&&!_?a>0?{radius:a}:void 0:{fill:!1},y=e.add([e.rect(0,0,v),e.pos(h),e.area(),e.scale(),e.color(l),e.anchor(`center`)]),b=i.background?y.add([e.sprite(i.background),e.pos(),e.anchor(`center`)]):void 0,x=i.icon?y.add([e.sprite(i.icon),e.pos(),e.anchor(`center`)]):void 0,S=y.add([g,e.color(f),e.pos(),e.anchor(`center`)]),C=0,w=0,T=`base`,E=0,D=0,O=!1,k=`base`;function A(){x&&T===`base`&&(C=x.width,w=x.height),b&&k===`base`&&!O&&(b.width>0||b.height>0)&&(E=b.width,D=b.height,O=!0);let t=x&&C>0&&S.width>0?n.spacing:0,r=C+t+S.width,a=Math.max(w,S.height),l=r+s*2,u=a+c*2,d=i.width??(b?Math.max(l,E):Math.max(l,o)),f=i.height??(b?Math.max(u,D):u);y.width!==d&&(y.width=d),y.height!==f&&(y.height=f),b&&O&&(b.width=d,b.height=f,b.pos=e.vec2()),x&&(x.pos=e.vec2(-r/2+C/2,0)),S.pos=e.vec2(-r/2+C+t+S.width/2,0)}return A(),(i.width===void 0||i.height===void 0||i.icon!==void 0||i.background!==void 0)&&y.onUpdate(A),y.onClick(()=>{y.color=t(e,d),x&&i.pressedIcon&&(T=`pressed`,x.sprite=i.pressedIcon),b&&i.pressedBackground&&(k=`pressed`,b.sprite=i.pressedBackground),i.onClick?.()}),y.onHover(()=>{y.color=t(e,u),y.scale=e.vec2(1.05,1.05),e.setCursor(`pointer`),x&&i.hoverIcon&&(T=`hover`,x.sprite=i.hoverIcon),b&&i.hoverBackground&&(k=`hover`,b.sprite=i.hoverBackground)}),y.onHoverEnd(()=>{y.color=l,y.scale=e.vec2(1,1),e.setCursor(`default`),x&&i.icon&&(T=`base`,x.sprite=i.icon),b&&i.background&&(k=`base`,b.sprite=i.background),(x||b)&&A()}),{...y,background:b,icon:x,label:S}}let r=`#4f8cff`,i=`#ffffff`,a={palette:{primary:r,secondary:r,background:`#111827`,surface:`#1f2937`,text:i,textSecondary:i,hover:r,pressed:r,disabled:`#6b7280`,error:`#ef4444`},typography:{fontFamily:`sans-serif`,fontSize:18,fontWeight:400,lineHeight:1.2},shape:{borderRadius:0},spacing:8,components:{button:{minWidth:120,paddingX:12,paddingY:8,textTransform:`none`},modal:{backdropOpacity:.5,borderRadius:8},tooltip:{background:`#111827`,textColor:i,delay:300}}};function o(e){return{palette:{...a.palette,...e?.palette},typography:{...a.typography,...e?.typography},shape:{...a.shape,...e?.shape},spacing:e?.spacing??a.spacing,components:{button:{...a.components.button,...e?.components?.button},modal:{...a.components.modal,...e?.components?.modal},tooltip:{...a.components.tooltip,...e?.components?.tooltip}}}}function s(e,t={}){let r=t,i=o(r);return{addButton:(t=``,r={})=>n(e,i,t,r),setTheme:e=>{r={...r,...e},i=o(r)},getTheme:()=>i}}e.uiPlugin=s});
2
2
  //# sourceMappingURL=plugin.umd.min.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.umd.min.js","names":["factory","global","exports","toColor","color","k","createButton","width","opt","theme","height","radius","baseColor","hoverColor","pressedColor","textColor","size","font","pos","button","label","text","PRIMARY","TEXT","resolveTheme","input","uiPlugin","userTheme","currentTheme","next"],"sources":["../src/utils.ts","../src/button.ts","../src/theme.ts","../src/plugin.ts"],"sourcesContent":["import type { Color, KAPLAYCtx } from 'kaplay';\n\n/**\n * Convert hex string to color or return color.\n *\n * @param k - The Kaplay context.\n * @param color - The hex string or color.\n * @returns The color.\n */\nexport function toColor(k: KAPLAYCtx, color: string | Color): Color {\n return typeof color === 'string' ? k.rgb(color) : color;\n}\n","import type { Color, KAPLAYCtx, Vec2 } from 'kaplay';\n\nimport type { ButtonOpt, Theme } from './types';\nimport { toColor } from './utils';\n\nexport function createButton(\n k: KAPLAYCtx,\n theme: Theme,\n text: string,\n opt: ButtonOpt & {\n pos?: Vec2;\n onClick?: () => void;\n color?: string | Color;\n hoverColor?: string | Color;\n pressedColor?: string | Color;\n textColor?: string | Color;\n },\n) {\n const width = opt.width ?? theme.components.button.width;\n const height = opt.height ?? theme.components.button.height;\n const radius = opt.radius ?? theme.shape.borderRadius;\n const baseColor = toColor(k, opt.color ?? opt.fill ?? theme.palette.primary);\n const hoverColor = toColor(k, opt.hoverColor ?? theme.palette.hover);\n const pressedColor = toColor(k, opt.pressedColor ?? theme.palette.pressed);\n const textColor = toColor(k, opt.textColor ?? theme.palette.text);\n const size = opt.size ?? theme.typography.fontSize;\n const font = opt.font ?? theme.typography.fontFamily;\n const pos = opt.pos ?? k.vec2();\n\n const button = k.add([\n k.rect(width, height, radius > 0 ? { radius } : undefined),\n k.pos(pos),\n k.area(),\n k.scale(),\n k.color(baseColor),\n k.anchor('center'),\n ]);\n\n const label = button.add([\n k.text(text, { size, font }),\n k.color(textColor as never),\n k.anchor('center'),\n ]);\n\n button.onClick(() => {\n button.color = toColor(k, pressedColor);\n opt.onClick?.();\n });\n\n button.onHover(() => {\n button.color = toColor(k, hoverColor);\n button.scale = k.vec2(1.05, 1.05);\n k.setCursor('pointer');\n });\n\n button.onHoverEnd(() => {\n button.color = baseColor;\n button.scale = k.vec2(1, 1);\n k.setCursor('default');\n });\n\n return {\n ...button,\n label,\n };\n}\n","import type { DeepPartial, Theme } from './types';\n\nconst PRIMARY = '#4f8cff';\nconst TEXT = '#ffffff';\n\n/** Default theme. */\nconst theme: Theme = {\n palette: {\n primary: PRIMARY,\n secondary: PRIMARY,\n background: '#111827',\n surface: '#1f2937',\n text: TEXT,\n textSecondary: TEXT,\n hover: PRIMARY,\n pressed: PRIMARY,\n disabled: '#6b7280',\n error: '#ef4444',\n },\n typography: {\n fontFamily: 'sans-serif',\n fontSize: 18,\n fontWeight: 400,\n lineHeight: 1.2,\n },\n shape: {\n borderRadius: 0,\n },\n spacing: 8,\n components: {\n button: {\n width: 120,\n height: 40,\n minWidth: 120,\n paddingX: 12,\n paddingY: 8,\n textTransform: 'none',\n },\n modal: {\n backdropOpacity: 0.5,\n borderRadius: 8,\n },\n tooltip: {\n background: '#111827',\n textColor: TEXT,\n delay: 300,\n },\n },\n};\n\n/** Resolve a partial theme into a fully resolved theme. */\nexport function resolveTheme(input?: DeepPartial<Theme>): Theme {\n return {\n palette: { ...theme.palette, ...input?.palette },\n typography: { ...theme.typography, ...input?.typography },\n shape: { ...theme.shape, ...input?.shape },\n spacing: input?.spacing ?? theme.spacing,\n components: {\n button: {\n ...theme.components.button,\n ...input?.components?.button,\n width:\n input?.components?.button?.width ??\n input?.components?.button?.minWidth ??\n theme.components.button.width,\n },\n modal: {\n ...theme.components.modal,\n ...input?.components?.modal,\n },\n tooltip: {\n ...theme.components.tooltip,\n ...input?.components?.tooltip,\n },\n },\n };\n}\n","import type { KAPLAYCtx } from 'kaplay';\n\nimport { createButton } from './button';\nimport { resolveTheme } from './theme';\nimport type {\n AddButton,\n DeepPartial,\n GetTheme,\n SetTheme,\n Theme,\n} from './types';\n\n/** KAPLAY UI plugin. */\nexport function uiPlugin(k: KAPLAYCtx, theme: DeepPartial<Theme> = {}) {\n let userTheme: DeepPartial<Theme> = theme;\n let currentTheme: Theme = resolveTheme(userTheme);\n\n const getTheme: GetTheme = () => currentTheme;\n\n const setTheme: SetTheme = (next) => {\n userTheme = { ...userTheme, ...next };\n currentTheme = resolveTheme(userTheme);\n };\n\n const addButton: AddButton = (text = '', opt = {}) => {\n return createButton(k, currentTheme, text, opt);\n };\n\n return {\n addButton,\n setTheme,\n getTheme,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"plugin.umd.min.js","names":["factory","global","exports","toColor","color","k","createButton","radius","opt","theme","baseColor","hoverColor","pressedColor","textColor","size","font","pos","textComp","text","isIconOnly","rectOpt","button","background","icon","label","iconWidth","iconHeight","iconState","backgroundWidth","backgroundHeight","backgroundMeasured","backgroundState","layout","gap","contentWidth","contentHeight","contentBoxWidth","paddingX","contentBoxHeight","paddingY","nextWidth","minWidth","nextHeight","PRIMARY","TEXT","resolveTheme","input","uiPlugin","userTheme","currentTheme","next"],"sources":["../src/utils.ts","../src/button.ts","../src/theme.ts","../src/plugin.ts"],"sourcesContent":["import type { Color, KAPLAYCtx } from 'kaplay';\n\n/**\n * Convert hex string to color or return color.\n *\n * @param k - The Kaplay context.\n * @param color - The hex string or color.\n * @returns The color.\n */\nexport function toColor(k: KAPLAYCtx, color: string | Color): Color {\n return typeof color === 'string' ? k.rgb(color) : color;\n}\n","import type { Color, KAPLAYCtx, Vec2 } from 'kaplay';\n\nimport type { ButtonOpt, Theme } from './types';\nimport { toColor } from './utils';\n\nexport function createButton(\n k: KAPLAYCtx,\n theme: Theme,\n text: string,\n opt: ButtonOpt & {\n pos?: Vec2;\n onClick?: () => void;\n color?: string | Color;\n hoverColor?: string | Color;\n pressedColor?: string | Color;\n textColor?: string | Color;\n },\n) {\n const radius = opt.radius ?? theme.shape.borderRadius;\n const { minWidth, paddingX, paddingY } = theme.components.button;\n const baseColor = toColor(k, opt.color ?? opt.fill ?? theme.palette.primary);\n const hoverColor = toColor(k, opt.hoverColor ?? theme.palette.hover);\n const pressedColor = toColor(k, opt.pressedColor ?? theme.palette.pressed);\n const textColor = toColor(k, opt.textColor ?? theme.palette.text);\n const size = opt.size ?? theme.typography.fontSize;\n const font = opt.font ?? theme.typography.fontFamily;\n const pos = opt.pos ?? k.vec2();\n\n const textComp = k.text(text, { size, font });\n const isIconOnly = opt.icon !== undefined && text.length === 0;\n const hasBackground = opt.background !== undefined;\n const showFill = !hasBackground && !isIconOnly;\n const rectOpt = showFill\n ? radius > 0\n ? { radius }\n : undefined\n : { fill: false };\n\n const button = k.add([\n k.rect(0, 0, rectOpt),\n k.pos(pos),\n k.area(),\n k.scale(),\n k.color(baseColor),\n k.anchor('center'),\n ]);\n\n const background = opt.background\n ? button.add([k.sprite(opt.background), k.pos(), k.anchor('center')])\n : undefined;\n\n const icon = opt.icon\n ? button.add([k.sprite(opt.icon), k.pos(), k.anchor('center')])\n : undefined;\n\n const label = button.add([\n textComp,\n k.color(textColor as never),\n k.pos(),\n k.anchor('center'),\n ]);\n\n let iconWidth = 0;\n let iconHeight = 0;\n let iconState: 'base' | 'hover' | 'pressed' = 'base';\n let backgroundWidth = 0;\n let backgroundHeight = 0;\n let backgroundMeasured = false;\n let backgroundState: 'base' | 'hover' | 'pressed' = 'base';\n\n /**\n * Update button layout:\n *\n * - caches the base icon/background dimensions,\n * - calculates content width/height,\n * - updates `button.width` / `button.height`,\n * - stretches the background and repositions the icon and label.\n */\n function layout() {\n if (icon && iconState === 'base') {\n iconWidth = icon.width;\n iconHeight = icon.height;\n }\n\n if (\n background &&\n backgroundState === 'base' &&\n !backgroundMeasured &&\n (background.width > 0 || background.height > 0)\n ) {\n backgroundWidth = background.width;\n backgroundHeight = background.height;\n backgroundMeasured = true;\n }\n\n const gap = icon && iconWidth > 0 && label.width > 0 ? theme.spacing : 0;\n const contentWidth = iconWidth + gap + label.width;\n const contentHeight = Math.max(iconHeight, label.height);\n const contentBoxWidth = contentWidth + paddingX * 2;\n const contentBoxHeight = contentHeight + paddingY * 2;\n\n const nextWidth =\n opt.width ??\n (background\n ? Math.max(contentBoxWidth, backgroundWidth)\n : Math.max(contentBoxWidth, minWidth));\n const nextHeight =\n opt.height ??\n (background\n ? Math.max(contentBoxHeight, backgroundHeight)\n : contentBoxHeight);\n\n if (button.width !== nextWidth) {\n button.width = nextWidth;\n }\n\n if (button.height !== nextHeight) {\n button.height = nextHeight;\n }\n\n if (background && backgroundMeasured) {\n background.width = nextWidth;\n background.height = nextHeight;\n background.pos = k.vec2();\n }\n\n if (icon) {\n icon.pos = k.vec2(-contentWidth / 2 + iconWidth / 2, 0);\n }\n\n label.pos = k.vec2(\n -contentWidth / 2 + iconWidth + gap + label.width / 2,\n 0,\n );\n }\n\n layout();\n\n if (\n opt.width === undefined ||\n opt.height === undefined ||\n opt.icon !== undefined ||\n opt.background !== undefined\n ) {\n button.onUpdate(layout);\n }\n\n button.onClick(() => {\n button.color = toColor(k, pressedColor);\n\n if (icon && opt.pressedIcon) {\n iconState = 'pressed';\n icon.sprite = opt.pressedIcon as string;\n }\n\n if (background && opt.pressedBackground) {\n backgroundState = 'pressed';\n background.sprite = opt.pressedBackground as string;\n }\n\n opt.onClick?.();\n });\n\n button.onHover(() => {\n button.color = toColor(k, hoverColor);\n button.scale = k.vec2(1.05, 1.05);\n k.setCursor('pointer');\n\n if (icon && opt.hoverIcon) {\n iconState = 'hover';\n icon.sprite = opt.hoverIcon as string;\n }\n\n if (background && opt.hoverBackground) {\n backgroundState = 'hover';\n background.sprite = opt.hoverBackground as string;\n }\n });\n\n button.onHoverEnd(() => {\n button.color = baseColor;\n button.scale = k.vec2(1, 1);\n k.setCursor('default');\n\n if (icon && opt.icon) {\n iconState = 'base';\n icon.sprite = opt.icon as string;\n }\n\n if (background && opt.background) {\n backgroundState = 'base';\n background.sprite = opt.background as string;\n }\n\n if (icon || background) {\n layout();\n }\n });\n\n return {\n ...button,\n background,\n icon,\n label,\n };\n}\n","import type { DeepPartial, Theme } from './types';\n\nconst PRIMARY = '#4f8cff';\nconst TEXT = '#ffffff';\n\n/** Default theme. */\nconst theme: Theme = {\n palette: {\n primary: PRIMARY,\n secondary: PRIMARY,\n background: '#111827',\n surface: '#1f2937',\n text: TEXT,\n textSecondary: TEXT,\n hover: PRIMARY,\n pressed: PRIMARY,\n disabled: '#6b7280',\n error: '#ef4444',\n },\n typography: {\n fontFamily: 'sans-serif',\n fontSize: 18,\n fontWeight: 400,\n lineHeight: 1.2,\n },\n shape: {\n borderRadius: 0,\n },\n spacing: 8,\n components: {\n button: {\n minWidth: 120,\n paddingX: 12,\n paddingY: 8,\n textTransform: 'none',\n },\n modal: {\n backdropOpacity: 0.5,\n borderRadius: 8,\n },\n tooltip: {\n background: '#111827',\n textColor: TEXT,\n delay: 300,\n },\n },\n};\n\n/** Resolve a partial theme into a fully resolved theme. */\nexport function resolveTheme(input?: DeepPartial<Theme>): Theme {\n return {\n palette: { ...theme.palette, ...input?.palette },\n typography: { ...theme.typography, ...input?.typography },\n shape: { ...theme.shape, ...input?.shape },\n spacing: input?.spacing ?? theme.spacing,\n components: {\n button: {\n ...theme.components.button,\n ...input?.components?.button,\n },\n modal: {\n ...theme.components.modal,\n ...input?.components?.modal,\n },\n tooltip: {\n ...theme.components.tooltip,\n ...input?.components?.tooltip,\n },\n },\n };\n}\n","import type { KAPLAYCtx } from 'kaplay';\n\nimport { createButton } from './button';\nimport { resolveTheme } from './theme';\nimport type {\n AddButton,\n DeepPartial,\n GetTheme,\n SetTheme,\n Theme,\n} from './types';\n\n/** KAPLAY UI plugin. */\nexport function uiPlugin(k: KAPLAYCtx, theme: DeepPartial<Theme> = {}) {\n let userTheme: DeepPartial<Theme> = theme;\n let currentTheme: Theme = resolveTheme(userTheme);\n\n const getTheme: GetTheme = () => currentTheme;\n\n const setTheme: SetTheme = (next) => {\n userTheme = { ...userTheme, ...next };\n currentTheme = resolveTheme(userTheme);\n };\n\n const addButton: AddButton = (text = '', opt = {}) => {\n return createButton(k, currentTheme, text, opt);\n };\n\n return {\n addButton,\n setTheme,\n getTheme,\n };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kaplay-plugin-ui",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "Kaplay plugin UI",
5
5
  "author": "Mark <mark@remarkablemark.org> (https://remarkablemark.org)",
6
6
  "main": "./dist/plugin.cjs",