kaplay-plugin-ui 1.2.1 → 1.4.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.
@@ -2,9 +2,232 @@
2
2
  typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define(["exports"], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, factory(global.KaplayPluginUI = {}));
3
3
  })(this, function(exports) {
4
4
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
5
+ // src/modal.ts
6
+ /** Tag applied to modal game objects. */
7
+ const MODAL_TAG = uniqueTag("modal");
8
+ function isSized(obj) {
9
+ const sized = obj;
10
+ return typeof sized.width === "number" && typeof sized.height === "number";
11
+ }
12
+ function createModal(k, theme, opt, hooks) {
13
+ const { backdropOpacity, borderRadius, gap, maxWidth, minWidth, paddingX, paddingY } = theme.components.modal;
14
+ const fontSize = theme.typography.fontSize;
15
+ const titleSize = Math.round(fontSize * 1.25);
16
+ const font = theme.typography.fontFamily;
17
+ const dismissible = opt.dismissible ?? true;
18
+ const modal = k.add([
19
+ k.pos(k.center()),
20
+ k.fixed(),
21
+ k.z(opt.z ?? 1e3),
22
+ MODAL_TAG
23
+ ]);
24
+ modal.onDestroy(() => {
25
+ hooks.onClose(modal);
26
+ });
27
+ hooks.onOpen(modal);
28
+ const backdrop = modal.add([
29
+ k.rect(k.width(), k.height()),
30
+ k.pos(),
31
+ k.color(0, 0, 0),
32
+ k.opacity(backdropOpacity),
33
+ k.anchor("center"),
34
+ k.area()
35
+ ]);
36
+ const content = modal.add([
37
+ k.rect(0, 0, borderRadius > 0 ? { radius: borderRadius } : void 0),
38
+ k.pos(),
39
+ k.color(toColor(k, theme.palette.surface)),
40
+ k.anchor("center"),
41
+ k.area()
42
+ ]);
43
+ const actions = content.add([k.pos()]);
44
+ for (const action of opt.actions ?? []) createButton(k, theme, action.text, {
45
+ parent: actions,
46
+ onClick: () => {
47
+ if (action.close !== false) modal.destroy();
48
+ action.onClick?.();
49
+ }
50
+ });
51
+ const titleMeasure = opt.title ? k.text(opt.title, {
52
+ size: titleSize,
53
+ font
54
+ }) : void 0;
55
+ const bodyMeasure = opt.text ? k.text(opt.text, {
56
+ size: fontSize,
57
+ font
58
+ }) : void 0;
59
+ const textWrapWidth = Math.max(0, (opt.width ?? maxWidth) - paddingX * 2);
60
+ const title = opt.title ? content.add([
61
+ k.text(opt.title, {
62
+ size: titleSize,
63
+ font,
64
+ width: textWrapWidth > 0 ? textWrapWidth : void 0,
65
+ align: "center"
66
+ }),
67
+ k.pos(),
68
+ k.color(toColor(k, theme.palette.text)),
69
+ k.anchor("center")
70
+ ]) : void 0;
71
+ const body = opt.text ? content.add([
72
+ k.text(opt.text, {
73
+ size: fontSize,
74
+ font,
75
+ width: textWrapWidth > 0 ? textWrapWidth : void 0,
76
+ align: "center"
77
+ }),
78
+ k.pos(),
79
+ k.color(toColor(k, theme.palette.textSecondary)),
80
+ k.anchor("center")
81
+ ]) : void 0;
82
+ /**
83
+ * Update modal layout:
84
+ *
85
+ * - fits the backdrop to the screen,
86
+ * - sizes the content to its title, text, and actions,
87
+ * - repositions the title, text, and actions.
88
+ */
89
+ function layout() {
90
+ const screenWidth = k.width();
91
+ const screenHeight = k.height();
92
+ if (backdrop.width !== screenWidth) backdrop.width = screenWidth;
93
+ if (backdrop.height !== screenHeight) backdrop.height = screenHeight;
94
+ modal.pos = k.center();
95
+ const buttons = actions.children.filter(isSized);
96
+ const actionsWidth = buttons.reduce((width, button) => width + button.width, 0) + gap * Math.max(0, buttons.length - 1);
97
+ const actionsHeight = Math.max(0, ...buttons.map((button) => button.height));
98
+ const textContentWidth = Math.max(titleMeasure?.width ?? 0, bodyMeasure?.width ?? 0);
99
+ const nextWidth = opt.width ?? Math.max(Math.min(textContentWidth, maxWidth - paddingX * 2) + paddingX * 2, actionsWidth + paddingX * 2, minWidth);
100
+ const titleHeight = title?.height ?? 0;
101
+ const bodyHeight = body?.height ?? 0;
102
+ const hasTitle = title !== void 0;
103
+ const hasBody = body !== void 0;
104
+ const hasActions = buttons.length > 0;
105
+ const blockCount = Number(hasTitle) + Number(hasBody) + Number(hasActions);
106
+ const contentHeight = titleHeight + bodyHeight + actionsHeight + gap * Math.max(0, blockCount - 1);
107
+ const nextHeight = opt.height ?? contentHeight + paddingY * 2;
108
+ if (content.width !== nextWidth) content.width = nextWidth;
109
+ if (content.height !== nextHeight) content.height = nextHeight;
110
+ let y = -nextHeight / 2 + (nextHeight - contentHeight) / 2;
111
+ if (title) {
112
+ title.pos = k.vec2(0, y + title.height / 2);
113
+ y += title.height + gap;
114
+ }
115
+ if (body) {
116
+ body.pos = k.vec2(0, y + body.height / 2);
117
+ y += body.height + gap;
118
+ }
119
+ if (hasActions) {
120
+ let x = -actionsWidth / 2;
121
+ for (const button of buttons) {
122
+ button.pos = k.vec2(x + button.width / 2, y + actionsHeight / 2);
123
+ x += button.width + gap;
124
+ }
125
+ }
126
+ }
127
+ layout();
128
+ modal.onUpdate(layout);
129
+ if (dismissible) {
130
+ k.wait(0, () => {
131
+ backdrop.onClick(() => {
132
+ if (!hooks.isTopmost(modal)) return;
133
+ if (content.hasPoint(k.mousePos())) return;
134
+ modal.destroy();
135
+ });
136
+ });
137
+ modal.onKeyPress("escape", () => {
138
+ if (hooks.isTopmost(modal)) modal.destroy();
139
+ });
140
+ }
141
+ return Object.assign(modal, {
142
+ actions,
143
+ content
144
+ });
145
+ }
146
+ function createModalManager(k) {
147
+ const modals = [];
148
+ let pausedScene;
149
+ let cancelPauseOnAdd;
150
+ const isInsideModal = (gameObj) => modals.some((modal) => modal === gameObj || modal.isAncestorOf(gameObj));
151
+ function pauseSceneTree(gameObj) {
152
+ if (isModal(gameObj) || isInsideModal(gameObj)) return;
153
+ pausedScene ??= /* @__PURE__ */ new Map();
154
+ // v8 ignore if
155
+ if (!pausedScene.has(gameObj)) pausedScene.set(gameObj, gameObj.paused);
156
+ setPaused(gameObj);
157
+ gameObj.children.forEach(pauseSceneTree);
158
+ }
159
+ function openModal(modal) {
160
+ k.setCursor("default");
161
+ if (modals.length > 0) setPaused(getLast(modals), true, true);
162
+ modals.push(modal);
163
+ if (modals.length === 1) {
164
+ pausedScene = /* @__PURE__ */ new Map();
165
+ k.getTreeRoot().children.forEach(pauseSceneTree);
166
+ const controller = k.onAdd((gameObj) => {
167
+ if (!isModal(gameObj) && !isInsideModal(gameObj)) pauseSceneTree(gameObj);
168
+ });
169
+ cancelPauseOnAdd = () => {
170
+ controller.cancel();
171
+ };
172
+ }
173
+ }
174
+ function closeModal(modal) {
175
+ const index = modals.indexOf(modal);
176
+ if (index === -1) return;
177
+ const wasTopmost = index === modals.length - 1;
178
+ modals.splice(index, 1);
179
+ if (!modals.length) {
180
+ cancelPauseOnAdd?.();
181
+ cancelPauseOnAdd = void 0;
182
+ // v8 ignore if
183
+ if (pausedScene) {
184
+ for (const [gameObj, paused] of pausedScene) if (gameObj.exists()) setPaused(gameObj, paused);
185
+ pausedScene = void 0;
186
+ }
187
+ } else if (wasTopmost) setPaused(getLast(modals), false, true);
188
+ }
189
+ const isTopmost = (modal) => getLast(modals) === modal;
190
+ return {
191
+ openModal,
192
+ closeModal,
193
+ isTopmost,
194
+ isInsideModal
195
+ };
196
+ }
5
197
  // src/utils.ts
6
198
  /**
7
- * Convert hex string to color or return color.
199
+ * Get the last item in an array.
200
+ *
201
+ * @param items - The items.
202
+ * @returns The last item.
203
+ */
204
+ function getLast(items) {
205
+ return items[items.length - 1];
206
+ }
207
+ /**
208
+ * Check if the game object is a modal.
209
+ *
210
+ * @param gameObj - The game object.
211
+ * @returns Whether game object is a modal.
212
+ */
213
+ function isModal(gameObj) {
214
+ return gameObj.is(MODAL_TAG);
215
+ }
216
+ /**
217
+ * Pause or unpause game object(s).
218
+ *
219
+ * @param gameObj - The root game object.
220
+ * @param isPaused - Whether it should be paused.
221
+ * @param includeTree - Whether to pause all game objects in tree.
222
+ */
223
+ function setPaused(gameObj, isPaused = true, includeTree = false) {
224
+ gameObj.paused = isPaused;
225
+ if (includeTree) gameObj.children.forEach((childGameObj) => {
226
+ setPaused(childGameObj, isPaused, includeTree);
227
+ });
228
+ }
229
+ /**
230
+ * Convert a hex string or color to color.
8
231
  *
9
232
  * @param k - The Kaplay context.
10
233
  * @param color - The hex string or color.
@@ -13,11 +236,25 @@
13
236
  function toColor(k, color) {
14
237
  return typeof color === "string" ? k.rgb(color) : color;
15
238
  }
239
+ /**
240
+ * Generate a unique tag.
241
+ *
242
+ * @param id - The identifier.
243
+ * @returns The tag.
244
+ */
245
+ function uniqueTag(id) {
246
+ return [
247
+ "kaplay-plugin-ui",
248
+ id,
249
+ crypto?.randomUUID?.()
250
+ ].join(":");
251
+ }
16
252
  // src/button.ts
17
253
  function createButton(k, theme, text, opt) {
18
254
  const radius = opt.radius ?? theme.shape.borderRadius;
19
255
  const { minWidth, paddingX, paddingY } = theme.components.button;
20
256
  const baseColor = toColor(k, opt.color ?? opt.fill ?? theme.palette.primary);
257
+ let disabled = opt.disabled ?? false;
21
258
  const hoverColor = toColor(k, opt.hoverColor ?? theme.palette.hover);
22
259
  const pressedColor = toColor(k, opt.pressedColor ?? theme.palette.pressed);
23
260
  const textColor = toColor(k, opt.textColor ?? theme.palette.text);
@@ -28,14 +265,14 @@
28
265
  size,
29
266
  font
30
267
  });
31
- const isIconOnly = opt.icon !== void 0 && text.length === 0;
268
+ const isIconOnly = opt.icon !== void 0 && !text.length;
32
269
  const rectOpt = !(opt.background !== void 0) && !isIconOnly ? radius > 0 ? { radius } : void 0 : { fill: false };
33
270
  const components = [
34
271
  k.rect(0, 0, rectOpt),
35
272
  k.pos(pos),
36
273
  k.area(),
37
274
  k.scale(),
38
- k.color(baseColor),
275
+ k.color(disabled ? toColor(k, theme.palette.disabled) : baseColor),
39
276
  k.anchor("center")
40
277
  ];
41
278
  const button = opt.parent ? opt.parent.add(components) : k.add(components);
@@ -100,6 +337,7 @@
100
337
  layout();
101
338
  if (opt.width === void 0 || opt.height === void 0 || opt.icon !== void 0 || opt.background !== void 0) button.onUpdate(layout);
102
339
  button.onClick(() => {
340
+ if (disabled) return;
103
341
  k.wait(0, () => {
104
342
  button.color = toColor(k, pressedColor);
105
343
  if (icon && opt.pressedIcon) {
@@ -111,12 +349,18 @@
111
349
  background.sprite = opt.pressedBackground;
112
350
  }
113
351
  opt.onClick?.();
352
+ // v8 ignore if
353
+ if (disabled) k.setCursor("not-allowed");
354
+ else if (!button.isHovering()) k.setCursor("default");
114
355
  });
115
356
  });
116
357
  button.onHover(() => {
358
+ if (disabled) {
359
+ k.setCursor("not-allowed");
360
+ return;
361
+ } else k.setCursor("pointer");
117
362
  button.color = toColor(k, hoverColor);
118
363
  button.scale = k.vec2(1.05, 1.05);
119
- k.setCursor("pointer");
120
364
  if (icon && opt.hoverIcon) {
121
365
  iconState = "hover";
122
366
  icon.sprite = opt.hoverIcon;
@@ -127,9 +371,10 @@
127
371
  }
128
372
  });
129
373
  button.onHoverEnd(() => {
374
+ k.setCursor("default");
375
+ if (disabled) return;
130
376
  button.color = baseColor;
131
377
  button.scale = k.vec2(1, 1);
132
- k.setCursor("default");
133
378
  if (icon && opt.icon) {
134
379
  iconState = "base";
135
380
  icon.sprite = opt.icon;
@@ -140,11 +385,29 @@
140
385
  }
141
386
  if (icon || background) layout();
142
387
  });
143
- return Object.assign(button, {
388
+ button.onDestroy(() => {
389
+ if (button.isHovering()) k.setCursor("default");
390
+ });
391
+ function applyDisabledState() {
392
+ button.color = disabled ? toColor(k, theme.palette.disabled) : baseColor;
393
+ }
394
+ applyDisabledState();
395
+ const assignedButton = Object.assign(button, {
144
396
  background,
145
397
  icon,
146
398
  label
147
399
  });
400
+ return Object.defineProperty(assignedButton, "disabled", {
401
+ get() {
402
+ return disabled;
403
+ },
404
+ set(value) {
405
+ disabled = value;
406
+ applyDisabledState();
407
+ },
408
+ enumerable: true,
409
+ configurable: true
410
+ });
148
411
  }
149
412
  // src/theme.ts
150
413
  const PRIMARY = "#4f8cff";
@@ -180,7 +443,12 @@
180
443
  },
181
444
  modal: {
182
445
  backdropOpacity: .5,
183
- borderRadius: 8
446
+ borderRadius: 8,
447
+ gap: 16,
448
+ maxWidth: 480,
449
+ minWidth: 280,
450
+ paddingX: 24,
451
+ paddingY: 20
184
452
  },
185
453
  tooltip: {
186
454
  background: "#111827",
@@ -222,7 +490,13 @@
222
490
  };
223
491
  }
224
492
  // src/plugin.ts
225
- /** KAPLAY UI plugin. */
493
+ /**
494
+ * Kaplay UI plugin.
495
+ *
496
+ * @param k - The Kaplay context.
497
+ * @param theme - The theme config.
498
+ * @returns The plugin methods.
499
+ */
226
500
  function uiPlugin(k, theme = {}) {
227
501
  let userTheme = theme;
228
502
  let currentTheme = resolveTheme(userTheme);
@@ -237,8 +511,17 @@
237
511
  const addButton = (text = "", opt = {}) => {
238
512
  return createButton(k, currentTheme, text, opt);
239
513
  };
514
+ const modalManager = createModalManager(k);
515
+ const addModal = (opt = {}) => {
516
+ return createModal(k, currentTheme, opt, {
517
+ isTopmost: modalManager.isTopmost,
518
+ onClose: modalManager.closeModal,
519
+ onOpen: modalManager.openModal
520
+ });
521
+ };
240
522
  return {
241
523
  addButton,
524
+ addModal,
242
525
  setTheme,
243
526
  getTheme
244
527
  };
@@ -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 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 components = [\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 const button = opt.parent ? opt.parent.add(components) : k.add(components);\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 // prevent stray click from click-through\n k.wait(0, () => {\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\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 Object.assign(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
+ {"version":3,"file":"plugin.umd.js","names":[],"sources":["../src/modal.ts","../src/utils.ts","../src/button.ts","../src/theme.ts","../src/plugin.ts"],"sourcesContent":["import type { GameObj, KAPLAYCtx } from 'kaplay';\n\nimport { createButton } from './button';\nimport type { ModalOpt, Theme } from './types';\nimport { getLast, isModal, setPaused, toColor, uniqueTag } from './utils';\n\n/** Tag applied to modal game objects. */\nexport const MODAL_TAG = uniqueTag('modal');\n\ntype SizedGameObj = GameObj & {\n height: number;\n width: number;\n};\n\nexport interface ModalHooks {\n isTopmost: (modal: GameObj) => boolean;\n onClose: (modal: GameObj) => void;\n onOpen: (modal: GameObj) => void;\n}\n\nfunction isSized(obj: GameObj): obj is SizedGameObj {\n const sized = obj as Partial<SizedGameObj>;\n return typeof sized.width === 'number' && typeof sized.height === 'number';\n}\n\nexport function createModal(\n k: KAPLAYCtx,\n theme: Theme,\n opt: ModalOpt,\n hooks: ModalHooks,\n) {\n const {\n backdropOpacity,\n borderRadius,\n gap,\n maxWidth,\n minWidth,\n paddingX,\n paddingY,\n } = theme.components.modal;\n const fontSize = theme.typography.fontSize;\n const titleSize = Math.round(fontSize * 1.25);\n const font = theme.typography.fontFamily;\n const dismissible = opt.dismissible ?? true;\n\n const modal = k.add([\n k.pos(k.center()),\n k.fixed(),\n k.z(opt.z ?? 1000),\n MODAL_TAG,\n ]);\n\n modal.onDestroy(() => {\n hooks.onClose(modal);\n });\n\n hooks.onOpen(modal);\n\n const backdrop = modal.add([\n k.rect(k.width(), k.height()),\n k.pos(),\n k.color(0, 0, 0),\n k.opacity(backdropOpacity),\n k.anchor('center'),\n k.area(),\n ]);\n\n const content = modal.add([\n k.rect(0, 0, borderRadius > 0 ? { radius: borderRadius } : undefined),\n k.pos(),\n k.color(toColor(k, theme.palette.surface)),\n k.anchor('center'),\n k.area(),\n ]);\n\n const actions = content.add([k.pos()]);\n\n for (const action of opt.actions ?? []) {\n createButton(k, theme, action.text, {\n parent: actions,\n onClick: () => {\n if (action.close !== false) {\n modal.destroy();\n }\n\n action.onClick?.();\n },\n });\n }\n\n const titleMeasure = opt.title\n ? k.text(opt.title, { size: titleSize, font })\n : undefined;\n const bodyMeasure = opt.text\n ? k.text(opt.text, { size: fontSize, font })\n : undefined;\n const textWrapWidth = Math.max(0, (opt.width ?? maxWidth) - paddingX * 2);\n\n const title = opt.title\n ? content.add([\n k.text(opt.title, {\n size: titleSize,\n font,\n width: textWrapWidth > 0 ? textWrapWidth : undefined,\n align: 'center',\n }),\n k.pos(),\n k.color(toColor(k, theme.palette.text)),\n k.anchor('center'),\n ])\n : undefined;\n\n const body = opt.text\n ? content.add([\n k.text(opt.text, {\n size: fontSize,\n font,\n width: textWrapWidth > 0 ? textWrapWidth : undefined,\n align: 'center',\n }),\n k.pos(),\n k.color(toColor(k, theme.palette.textSecondary)),\n k.anchor('center'),\n ])\n : undefined;\n\n /**\n * Update modal layout:\n *\n * - fits the backdrop to the screen,\n * - sizes the content to its title, text, and actions,\n * - repositions the title, text, and actions.\n */\n function layout() {\n const screenWidth = k.width();\n const screenHeight = k.height();\n\n if (backdrop.width !== screenWidth) {\n backdrop.width = screenWidth;\n }\n\n if (backdrop.height !== screenHeight) {\n backdrop.height = screenHeight;\n }\n\n modal.pos = k.center();\n\n const buttons = actions.children.filter(isSized);\n const actionsWidth =\n buttons.reduce((width, button) => width + button.width, 0) +\n gap * Math.max(0, buttons.length - 1);\n const actionsHeight = Math.max(\n 0,\n ...buttons.map((button) => button.height),\n );\n\n const textContentWidth = Math.max(\n titleMeasure?.width ?? 0,\n bodyMeasure?.width ?? 0,\n );\n const nextWidth =\n opt.width ??\n Math.max(\n Math.min(textContentWidth, maxWidth - paddingX * 2) + paddingX * 2,\n actionsWidth + paddingX * 2,\n minWidth,\n );\n\n const titleHeight = title?.height ?? 0;\n const bodyHeight = body?.height ?? 0;\n const hasTitle = title !== undefined;\n const hasBody = body !== undefined;\n const hasActions = buttons.length > 0;\n const blockCount = Number(hasTitle) + Number(hasBody) + Number(hasActions);\n const contentHeight =\n titleHeight +\n bodyHeight +\n actionsHeight +\n gap * Math.max(0, blockCount - 1);\n const nextHeight = opt.height ?? contentHeight + paddingY * 2;\n\n if (content.width !== nextWidth) {\n content.width = nextWidth;\n }\n\n if (content.height !== nextHeight) {\n content.height = nextHeight;\n }\n\n let y = -nextHeight / 2 + (nextHeight - contentHeight) / 2;\n\n if (title) {\n title.pos = k.vec2(0, y + title.height / 2);\n y += title.height + gap;\n }\n\n if (body) {\n body.pos = k.vec2(0, y + body.height / 2);\n y += body.height + gap;\n }\n\n if (hasActions) {\n let x = -actionsWidth / 2;\n\n for (const button of buttons) {\n button.pos = k.vec2(x + button.width / 2, y + actionsHeight / 2);\n x += button.width + gap;\n }\n }\n }\n\n layout();\n modal.onUpdate(layout);\n\n if (dismissible) {\n // prevent click-through so modal isn't dismissed right after creation\n k.wait(0, () => {\n backdrop.onClick(() => {\n if (!hooks.isTopmost(modal)) {\n return;\n }\n\n if (content.hasPoint(k.mousePos())) {\n return;\n }\n\n modal.destroy();\n });\n });\n\n modal.onKeyPress('escape', () => {\n if (hooks.isTopmost(modal)) {\n modal.destroy();\n }\n });\n }\n\n return Object.assign(modal, {\n actions,\n content,\n });\n}\n\nexport interface ModalManager {\n openModal: ModalHooks['onOpen'];\n closeModal: ModalHooks['onClose'];\n isTopmost: ModalHooks['isTopmost'];\n isInsideModal: (gameObj: GameObj) => boolean;\n}\n\nexport function createModalManager(k: KAPLAYCtx): ModalManager {\n const modals: GameObj[] = [];\n let pausedScene: Map<GameObj, boolean> | undefined;\n let cancelPauseOnAdd: (() => void) | undefined;\n\n const isInsideModal = (gameObj: GameObj) =>\n modals.some((modal) => modal === gameObj || modal.isAncestorOf(gameObj));\n\n function pauseSceneTree(gameObj: GameObj) {\n if (isModal(gameObj) || isInsideModal(gameObj)) {\n return;\n }\n\n pausedScene ??= new Map();\n\n // v8 ignore if\n if (!pausedScene.has(gameObj)) {\n pausedScene.set(gameObj, gameObj.paused);\n }\n\n setPaused(gameObj);\n gameObj.children.forEach(pauseSceneTree);\n }\n\n function openModal(modal: GameObj) {\n k.setCursor('default');\n\n if (modals.length > 0) {\n setPaused(getLast(modals), true, true);\n }\n\n modals.push(modal);\n\n if (modals.length === 1) {\n pausedScene = new Map();\n k.getTreeRoot().children.forEach(pauseSceneTree);\n\n const controller = k.onAdd((gameObj) => {\n if (!isModal(gameObj) && !isInsideModal(gameObj)) {\n pauseSceneTree(gameObj);\n }\n });\n\n cancelPauseOnAdd = () => {\n controller.cancel();\n };\n }\n }\n\n function closeModal(modal: GameObj) {\n const index = modals.indexOf(modal);\n\n if (index === -1) {\n return;\n }\n\n const wasTopmost = index === modals.length - 1;\n\n modals.splice(index, 1);\n\n if (!modals.length) {\n cancelPauseOnAdd?.();\n cancelPauseOnAdd = undefined;\n\n // v8 ignore if\n if (pausedScene) {\n for (const [gameObj, paused] of pausedScene) {\n if (gameObj.exists()) {\n setPaused(gameObj, paused);\n }\n }\n\n pausedScene = undefined;\n }\n } else if (wasTopmost) {\n setPaused(getLast(modals), false, true);\n }\n }\n\n const isTopmost = (modal: GameObj) => getLast(modals) === modal;\n\n return { openModal, closeModal, isTopmost, isInsideModal };\n}\n","import type { Color, GameObj, KAPLAYCtx } from 'kaplay';\n\nimport { MODAL_TAG } from './modal';\n\n/**\n * Get the last item in an array.\n *\n * @param items - The items.\n * @returns The last item.\n */\nexport function getLast<T>(items: T[]): T {\n return items[items.length - 1];\n}\n\n/**\n * Check if the game object is a modal.\n *\n * @param gameObj - The game object.\n * @returns Whether game object is a modal.\n */\nexport function isModal(gameObj: GameObj): boolean {\n return gameObj.is(MODAL_TAG);\n}\n\n/**\n * Pause or unpause game object(s).\n *\n * @param gameObj - The root game object.\n * @param isPaused - Whether it should be paused.\n * @param includeTree - Whether to pause all game objects in tree.\n */\nexport function setPaused(\n gameObj: GameObj,\n isPaused = true,\n includeTree = false,\n): void {\n gameObj.paused = isPaused;\n\n if (includeTree) {\n gameObj.children.forEach((childGameObj) => {\n setPaused(childGameObj, isPaused, includeTree);\n });\n }\n}\n\n/**\n * Convert a hex string or color to 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\n/**\n * Generate a unique tag.\n *\n * @param id - The identifier.\n * @returns The tag.\n */\nexport function uniqueTag(id: string): string {\n return [\n 'kaplay-plugin-ui',\n id,\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n crypto?.randomUUID?.(),\n ].join(':');\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 let disabled = opt.disabled ?? false;\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;\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 components = [\n k.rect(0, 0, rectOpt),\n k.pos(pos),\n k.area(),\n k.scale(),\n k.color(disabled ? toColor(k, theme.palette.disabled) : baseColor),\n k.anchor('center'),\n ];\n const button = opt.parent ? opt.parent.add(components) : k.add(components);\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 if (disabled) {\n return;\n }\n\n // prevent click-through\n k.wait(0, () => {\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 // v8 ignore if\n if (disabled) {\n k.setCursor('not-allowed');\n } else if (!button.isHovering()) {\n k.setCursor('default');\n }\n });\n });\n\n button.onHover(() => {\n if (disabled) {\n k.setCursor('not-allowed');\n return;\n } else {\n k.setCursor('pointer');\n }\n\n button.color = toColor(k, hoverColor);\n button.scale = k.vec2(1.05, 1.05);\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 k.setCursor('default');\n\n if (disabled) {\n return;\n }\n\n button.color = baseColor;\n button.scale = k.vec2(1, 1);\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 button.onDestroy(() => {\n if (button.isHovering()) {\n k.setCursor('default');\n }\n });\n\n function applyDisabledState() {\n button.color = disabled ? toColor(k, theme.palette.disabled) : baseColor;\n }\n\n applyDisabledState();\n\n const assignedButton = Object.assign(button, {\n background,\n icon,\n label,\n });\n\n const definedButton = Object.defineProperty(assignedButton, 'disabled', {\n get() {\n return disabled;\n },\n set(value: boolean) {\n disabled = value;\n applyDisabledState();\n },\n enumerable: true,\n configurable: true,\n }) as typeof assignedButton & { disabled: boolean };\n\n return definedButton;\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 gap: 16,\n maxWidth: 480,\n minWidth: 280,\n paddingX: 24,\n paddingY: 20,\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 { createModal, createModalManager } from './modal';\nimport { resolveTheme } from './theme';\nimport type {\n AddButton,\n AddModal,\n DeepPartial,\n GetTheme,\n SetTheme,\n Theme,\n} from './types';\n\n/**\n * Kaplay UI plugin.\n *\n * @param k - The Kaplay context.\n * @param theme - The theme config.\n * @returns The plugin methods.\n */\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 const modalManager = createModalManager(k);\n\n const addModal: AddModal = (opt = {}) => {\n return createModal(k, currentTheme, opt, {\n isTopmost: modalManager.isTopmost,\n onClose: modalManager.closeModal,\n onOpen: modalManager.openModal,\n });\n };\n\n return {\n addButton,\n addModal,\n setTheme,\n getTheme,\n };\n}\n"],"mappings":"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"}
@@ -1,2 +1,4 @@
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.rect(0,0,v),e.pos(h),e.area(),e.scale(),e.color(l),e.anchor(`center`)],b=i.parent?i.parent.add(y):e.add(y),x=i.background?b.add([e.sprite(i.background),e.pos(),e.anchor(`center`)]):void 0,S=i.icon?b.add([e.sprite(i.icon),e.pos(),e.anchor(`center`)]):void 0,C=b.add([g,e.color(f),e.pos(),e.anchor(`center`)]),w=0,T=0,E=`base`,D=0,O=0,k=!1,A=`base`;function j(){S&&E===`base`&&(w=S.width,T=S.height),x&&A===`base`&&!k&&(x.width>0||x.height>0)&&(D=x.width,O=x.height,k=!0);let t=S&&w>0&&C.width>0?n.spacing:0,r=w+t+C.width,a=Math.max(T,C.height),l=r+s*2,u=a+c*2,d=i.width??(x?Math.max(l,D):Math.max(l,o)),f=i.height??(x?Math.max(u,O):u);b.width!==d&&(b.width=d),b.height!==f&&(b.height=f),x&&k&&(x.width=d,x.height=f,x.pos=e.vec2()),S&&(S.pos=e.vec2(-r/2+w/2,0)),C.pos=e.vec2(-r/2+w+t+C.width/2,0)}return j(),(i.width===void 0||i.height===void 0||i.icon!==void 0||i.background!==void 0)&&b.onUpdate(j),b.onClick(()=>{e.wait(0,()=>{b.color=t(e,d),S&&i.pressedIcon&&(E=`pressed`,S.sprite=i.pressedIcon),x&&i.pressedBackground&&(A=`pressed`,x.sprite=i.pressedBackground),i.onClick?.()})}),b.onHover(()=>{b.color=t(e,u),b.scale=e.vec2(1.05,1.05),e.setCursor(`pointer`),S&&i.hoverIcon&&(E=`hover`,S.sprite=i.hoverIcon),x&&i.hoverBackground&&(A=`hover`,x.sprite=i.hoverBackground)}),b.onHoverEnd(()=>{b.color=l,b.scale=e.vec2(1,1),e.setCursor(`default`),S&&i.icon&&(E=`base`,S.sprite=i.icon),x&&i.background&&(A=`base`,x.sprite=i.background),(S||x)&&j()}),Object.assign(b,{background:x,icon:S,label:C})}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});
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`});let t=l(`modal`);function n(e){let t=e;return typeof t.width==`number`&&typeof t.height==`number`}function r(e,r,i,a){let{backdropOpacity:o,borderRadius:s,gap:l,maxWidth:d,minWidth:f,paddingX:p,paddingY:m}=r.components.modal,h=r.typography.fontSize,g=Math.round(h*1.25),_=r.typography.fontFamily,v=i.dismissible??!0,y=e.add([e.pos(e.center()),e.fixed(),e.z(i.z??1e3),t]);y.onDestroy(()=>{a.onClose(y)}),a.onOpen(y);let b=y.add([e.rect(e.width(),e.height()),e.pos(),e.color(0,0,0),e.opacity(o),e.anchor(`center`),e.area()]),x=y.add([e.rect(0,0,s>0?{radius:s}:void 0),e.pos(),e.color(c(e,r.palette.surface)),e.anchor(`center`),e.area()]),S=x.add([e.pos()]);for(let t of i.actions??[])u(e,r,t.text,{parent:S,onClick:()=>{t.close!==!1&&y.destroy(),t.onClick?.()}});let C=i.title?e.text(i.title,{size:g,font:_}):void 0,w=i.text?e.text(i.text,{size:h,font:_}):void 0,T=Math.max(0,(i.width??d)-p*2),E=i.title?x.add([e.text(i.title,{size:g,font:_,width:T>0?T:void 0,align:`center`}),e.pos(),e.color(c(e,r.palette.text)),e.anchor(`center`)]):void 0,D=i.text?x.add([e.text(i.text,{size:h,font:_,width:T>0?T:void 0,align:`center`}),e.pos(),e.color(c(e,r.palette.textSecondary)),e.anchor(`center`)]):void 0;function O(){let t=e.width(),r=e.height();b.width!==t&&(b.width=t),b.height!==r&&(b.height=r),y.pos=e.center();let a=S.children.filter(n),o=a.reduce((e,t)=>e+t.width,0)+l*Math.max(0,a.length-1),s=Math.max(0,...a.map(e=>e.height)),c=Math.max(C?.width??0,w?.width??0),u=i.width??Math.max(Math.min(c,d-p*2)+p*2,o+p*2,f),h=E?.height??0,g=D?.height??0,_=E!==void 0,v=D!==void 0,T=a.length>0,O=Number(_)+Number(v)+Number(T),k=h+g+s+l*Math.max(0,O-1),A=i.height??k+m*2;x.width!==u&&(x.width=u),x.height!==A&&(x.height=A);let j=-A/2+(A-k)/2;if(E&&(E.pos=e.vec2(0,j+E.height/2),j+=E.height+l),D&&(D.pos=e.vec2(0,j+D.height/2),j+=D.height+l),T){let t=-o/2;for(let n of a)n.pos=e.vec2(t+n.width/2,j+s/2),t+=n.width+l}}return O(),y.onUpdate(O),v&&(e.wait(0,()=>{b.onClick(()=>{a.isTopmost(y)&&(x.hasPoint(e.mousePos())||y.destroy())})}),y.onKeyPress(`escape`,()=>{a.isTopmost(y)&&y.destroy()})),Object.assign(y,{actions:S,content:x})}function i(e){let t=[],n,r,i=e=>t.some(t=>t===e||t.isAncestorOf(e));function c(e){o(e)||i(e)||(n??=/* @__PURE__ */ new Map,n.has(e)||n.set(e,e.paused),s(e),e.children.forEach(c))}function l(l){if(e.setCursor(`default`),t.length>0&&s(a(t),!0,!0),t.push(l),t.length===1){n=/* @__PURE__ */ new Map,e.getTreeRoot().children.forEach(c);let t=e.onAdd(e=>{!o(e)&&!i(e)&&c(e)});r=()=>{t.cancel()}}}function u(e){let i=t.indexOf(e);if(i===-1)return;let o=i===t.length-1;if(t.splice(i,1),t.length)o&&s(a(t),!1,!0);else if(r?.(),r=void 0,n){for(let[e,t]of n)e.exists()&&s(e,t);n=void 0}}return{openModal:l,closeModal:u,isTopmost:e=>a(t)===e,isInsideModal:i}}function a(e){return e[e.length-1]}function o(e){return e.is(t)}function s(e,t=!0,n=!1){e.paused=t,n&&e.children.forEach(e=>{s(e,t,n)})}function c(e,t){return typeof t==`string`?e.rgb(t):t}function l(e){return[`kaplay-plugin-ui`,e,crypto?.randomUUID?.()].join(`:`)}function u(e,t,n,r){let i=r.radius??t.shape.borderRadius,{minWidth:a,paddingX:o,paddingY:s}=t.components.button,l=c(e,r.color??r.fill??t.palette.primary),u=r.disabled??!1,d=c(e,r.hoverColor??t.palette.hover),f=c(e,r.pressedColor??t.palette.pressed),p=c(e,r.textColor??t.palette.text),m=r.size??t.typography.fontSize,h=r.font??t.typography.fontFamily,g=r.pos??e.vec2(),_=e.text(n,{size:m,font:h}),v=r.icon!==void 0&&!n.length,y=r.background===void 0&&!v?i>0?{radius:i}:void 0:{fill:!1},b=[e.rect(0,0,y),e.pos(g),e.area(),e.scale(),e.color(u?c(e,t.palette.disabled):l),e.anchor(`center`)],x=r.parent?r.parent.add(b):e.add(b),S=r.background?x.add([e.sprite(r.background),e.pos(),e.anchor(`center`)]):void 0,C=r.icon?x.add([e.sprite(r.icon),e.pos(),e.anchor(`center`)]):void 0,w=x.add([_,e.color(p),e.pos(),e.anchor(`center`)]),T=0,E=0,D=`base`,O=0,k=0,A=!1,j=`base`;function M(){C&&D===`base`&&(T=C.width,E=C.height),S&&j===`base`&&!A&&(S.width>0||S.height>0)&&(O=S.width,k=S.height,A=!0);let n=C&&T>0&&w.width>0?t.spacing:0,i=T+n+w.width,c=Math.max(E,w.height),l=i+o*2,u=c+s*2,d=r.width??(S?Math.max(l,O):Math.max(l,a)),f=r.height??(S?Math.max(u,k):u);x.width!==d&&(x.width=d),x.height!==f&&(x.height=f),S&&A&&(S.width=d,S.height=f,S.pos=e.vec2()),C&&(C.pos=e.vec2(-i/2+T/2,0)),w.pos=e.vec2(-i/2+T+n+w.width/2,0)}M(),(r.width===void 0||r.height===void 0||r.icon!==void 0||r.background!==void 0)&&x.onUpdate(M),x.onClick(()=>{u||e.wait(0,()=>{
2
+ // v8 ignore if
3
+ x.color=c(e,f),C&&r.pressedIcon&&(D=`pressed`,C.sprite=r.pressedIcon),S&&r.pressedBackground&&(j=`pressed`,S.sprite=r.pressedBackground),r.onClick?.(),u?e.setCursor(`not-allowed`):x.isHovering()||e.setCursor(`default`)})}),x.onHover(()=>{u?e.setCursor(`not-allowed`):(e.setCursor(`pointer`),x.color=c(e,d),x.scale=e.vec2(1.05,1.05),C&&r.hoverIcon&&(D=`hover`,C.sprite=r.hoverIcon),S&&r.hoverBackground&&(j=`hover`,S.sprite=r.hoverBackground))}),x.onHoverEnd(()=>{e.setCursor(`default`),!u&&(x.color=l,x.scale=e.vec2(1,1),C&&r.icon&&(D=`base`,C.sprite=r.icon),S&&r.background&&(j=`base`,S.sprite=r.background),(C||S)&&M())}),x.onDestroy(()=>{x.isHovering()&&e.setCursor(`default`)});function N(){x.color=u?c(e,t.palette.disabled):l}N();let P=Object.assign(x,{background:S,icon:C,label:w});return Object.defineProperty(P,"disabled",{get(){return u},set(e){u=e,N()},enumerable:!0,configurable:!0})}let d=`#4f8cff`,f=`#ffffff`,p={palette:{primary:d,secondary:d,background:`#111827`,surface:`#1f2937`,text:f,textSecondary:f,hover:d,pressed:d,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,gap:16,maxWidth:480,minWidth:280,paddingX:24,paddingY:20},tooltip:{background:`#111827`,textColor:f,delay:300}}};function m(e){return{palette:{...p.palette,...e?.palette},typography:{...p.typography,...e?.typography},shape:{...p.shape,...e?.shape},spacing:e?.spacing??p.spacing,components:{button:{...p.components.button,...e?.components?.button},modal:{...p.components.modal,...e?.components?.modal},tooltip:{...p.components.tooltip,...e?.components?.tooltip}}}}function h(e,t={}){let n=t,a=m(n),o=()=>a,s=e=>{n={...n,...e},a=m(n)},c=(t=``,n={})=>u(e,a,t,n),l=i(e);return{addButton:c,addModal:(t={})=>r(e,a,t,{isTopmost:l.isTopmost,onClose:l.closeModal,onOpen:l.openModal}),setTheme:s,getTheme:o}}e.uiPlugin=h});
2
4
  //# sourceMappingURL=plugin.umd.min.js.map
@@ -1 +1 @@
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","components","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 components = [\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 const button = opt.parent ? opt.parent.add(components) : k.add(components);\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 // prevent stray click from click-through\n k.wait(0, () => {\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\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 Object.assign(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
+ {"version":3,"file":"plugin.umd.min.js","names":["factory","global","exports","MODAL_TAG","uniqueTag","isSized","sized","obj","createModal","theme","fontSize","titleSize","font","dismissible","opt","modal","k","hooks","backdrop","backdropOpacity","content","borderRadius","toColor","actions","action","createButton","titleMeasure","bodyMeasure","textWrapWidth","maxWidth","paddingX","title","body","layout","screenWidth","screenHeight","buttons","actionsWidth","width","button","gap","actionsHeight","textContentWidth","nextWidth","minWidth","titleHeight","bodyHeight","hasTitle","hasBody","hasActions","blockCount","contentHeight","nextHeight","paddingY","y","x","createModalManager","modals","pausedScene","cancelPauseOnAdd","isInsideModal","gameObj","pauseSceneTree","isModal","setPaused","openModal","getLast","controller","closeModal","index","wasTopmost","paused","items","isPaused","includeTree","childGameObj","color","id","radius","baseColor","disabled","hoverColor","pressedColor","textColor","size","pos","textComp","text","isIconOnly","rectOpt","components","background","icon","label","iconWidth","iconHeight","iconState","backgroundWidth","backgroundHeight","backgroundMeasured","backgroundState","contentWidth","contentBoxWidth","contentBoxHeight","applyDisabledState","assignedButton","value","PRIMARY","TEXT","resolveTheme","input","uiPlugin","userTheme","currentTheme","getTheme","setTheme","next","addButton","modalManager"],"sources":["../src/modal.ts","../src/utils.ts","../src/button.ts","../src/theme.ts","../src/plugin.ts"],"sourcesContent":["import type { GameObj, KAPLAYCtx } from 'kaplay';\n\nimport { createButton } from './button';\nimport type { ModalOpt, Theme } from './types';\nimport { getLast, isModal, setPaused, toColor, uniqueTag } from './utils';\n\n/** Tag applied to modal game objects. */\nexport const MODAL_TAG = uniqueTag('modal');\n\ntype SizedGameObj = GameObj & {\n height: number;\n width: number;\n};\n\nexport interface ModalHooks {\n isTopmost: (modal: GameObj) => boolean;\n onClose: (modal: GameObj) => void;\n onOpen: (modal: GameObj) => void;\n}\n\nfunction isSized(obj: GameObj): obj is SizedGameObj {\n const sized = obj as Partial<SizedGameObj>;\n return typeof sized.width === 'number' && typeof sized.height === 'number';\n}\n\nexport function createModal(\n k: KAPLAYCtx,\n theme: Theme,\n opt: ModalOpt,\n hooks: ModalHooks,\n) {\n const {\n backdropOpacity,\n borderRadius,\n gap,\n maxWidth,\n minWidth,\n paddingX,\n paddingY,\n } = theme.components.modal;\n const fontSize = theme.typography.fontSize;\n const titleSize = Math.round(fontSize * 1.25);\n const font = theme.typography.fontFamily;\n const dismissible = opt.dismissible ?? true;\n\n const modal = k.add([\n k.pos(k.center()),\n k.fixed(),\n k.z(opt.z ?? 1000),\n MODAL_TAG,\n ]);\n\n modal.onDestroy(() => {\n hooks.onClose(modal);\n });\n\n hooks.onOpen(modal);\n\n const backdrop = modal.add([\n k.rect(k.width(), k.height()),\n k.pos(),\n k.color(0, 0, 0),\n k.opacity(backdropOpacity),\n k.anchor('center'),\n k.area(),\n ]);\n\n const content = modal.add([\n k.rect(0, 0, borderRadius > 0 ? { radius: borderRadius } : undefined),\n k.pos(),\n k.color(toColor(k, theme.palette.surface)),\n k.anchor('center'),\n k.area(),\n ]);\n\n const actions = content.add([k.pos()]);\n\n for (const action of opt.actions ?? []) {\n createButton(k, theme, action.text, {\n parent: actions,\n onClick: () => {\n if (action.close !== false) {\n modal.destroy();\n }\n\n action.onClick?.();\n },\n });\n }\n\n const titleMeasure = opt.title\n ? k.text(opt.title, { size: titleSize, font })\n : undefined;\n const bodyMeasure = opt.text\n ? k.text(opt.text, { size: fontSize, font })\n : undefined;\n const textWrapWidth = Math.max(0, (opt.width ?? maxWidth) - paddingX * 2);\n\n const title = opt.title\n ? content.add([\n k.text(opt.title, {\n size: titleSize,\n font,\n width: textWrapWidth > 0 ? textWrapWidth : undefined,\n align: 'center',\n }),\n k.pos(),\n k.color(toColor(k, theme.palette.text)),\n k.anchor('center'),\n ])\n : undefined;\n\n const body = opt.text\n ? content.add([\n k.text(opt.text, {\n size: fontSize,\n font,\n width: textWrapWidth > 0 ? textWrapWidth : undefined,\n align: 'center',\n }),\n k.pos(),\n k.color(toColor(k, theme.palette.textSecondary)),\n k.anchor('center'),\n ])\n : undefined;\n\n /**\n * Update modal layout:\n *\n * - fits the backdrop to the screen,\n * - sizes the content to its title, text, and actions,\n * - repositions the title, text, and actions.\n */\n function layout() {\n const screenWidth = k.width();\n const screenHeight = k.height();\n\n if (backdrop.width !== screenWidth) {\n backdrop.width = screenWidth;\n }\n\n if (backdrop.height !== screenHeight) {\n backdrop.height = screenHeight;\n }\n\n modal.pos = k.center();\n\n const buttons = actions.children.filter(isSized);\n const actionsWidth =\n buttons.reduce((width, button) => width + button.width, 0) +\n gap * Math.max(0, buttons.length - 1);\n const actionsHeight = Math.max(\n 0,\n ...buttons.map((button) => button.height),\n );\n\n const textContentWidth = Math.max(\n titleMeasure?.width ?? 0,\n bodyMeasure?.width ?? 0,\n );\n const nextWidth =\n opt.width ??\n Math.max(\n Math.min(textContentWidth, maxWidth - paddingX * 2) + paddingX * 2,\n actionsWidth + paddingX * 2,\n minWidth,\n );\n\n const titleHeight = title?.height ?? 0;\n const bodyHeight = body?.height ?? 0;\n const hasTitle = title !== undefined;\n const hasBody = body !== undefined;\n const hasActions = buttons.length > 0;\n const blockCount = Number(hasTitle) + Number(hasBody) + Number(hasActions);\n const contentHeight =\n titleHeight +\n bodyHeight +\n actionsHeight +\n gap * Math.max(0, blockCount - 1);\n const nextHeight = opt.height ?? contentHeight + paddingY * 2;\n\n if (content.width !== nextWidth) {\n content.width = nextWidth;\n }\n\n if (content.height !== nextHeight) {\n content.height = nextHeight;\n }\n\n let y = -nextHeight / 2 + (nextHeight - contentHeight) / 2;\n\n if (title) {\n title.pos = k.vec2(0, y + title.height / 2);\n y += title.height + gap;\n }\n\n if (body) {\n body.pos = k.vec2(0, y + body.height / 2);\n y += body.height + gap;\n }\n\n if (hasActions) {\n let x = -actionsWidth / 2;\n\n for (const button of buttons) {\n button.pos = k.vec2(x + button.width / 2, y + actionsHeight / 2);\n x += button.width + gap;\n }\n }\n }\n\n layout();\n modal.onUpdate(layout);\n\n if (dismissible) {\n // prevent click-through so modal isn't dismissed right after creation\n k.wait(0, () => {\n backdrop.onClick(() => {\n if (!hooks.isTopmost(modal)) {\n return;\n }\n\n if (content.hasPoint(k.mousePos())) {\n return;\n }\n\n modal.destroy();\n });\n });\n\n modal.onKeyPress('escape', () => {\n if (hooks.isTopmost(modal)) {\n modal.destroy();\n }\n });\n }\n\n return Object.assign(modal, {\n actions,\n content,\n });\n}\n\nexport interface ModalManager {\n openModal: ModalHooks['onOpen'];\n closeModal: ModalHooks['onClose'];\n isTopmost: ModalHooks['isTopmost'];\n isInsideModal: (gameObj: GameObj) => boolean;\n}\n\nexport function createModalManager(k: KAPLAYCtx): ModalManager {\n const modals: GameObj[] = [];\n let pausedScene: Map<GameObj, boolean> | undefined;\n let cancelPauseOnAdd: (() => void) | undefined;\n\n const isInsideModal = (gameObj: GameObj) =>\n modals.some((modal) => modal === gameObj || modal.isAncestorOf(gameObj));\n\n function pauseSceneTree(gameObj: GameObj) {\n if (isModal(gameObj) || isInsideModal(gameObj)) {\n return;\n }\n\n pausedScene ??= new Map();\n\n // v8 ignore if\n if (!pausedScene.has(gameObj)) {\n pausedScene.set(gameObj, gameObj.paused);\n }\n\n setPaused(gameObj);\n gameObj.children.forEach(pauseSceneTree);\n }\n\n function openModal(modal: GameObj) {\n k.setCursor('default');\n\n if (modals.length > 0) {\n setPaused(getLast(modals), true, true);\n }\n\n modals.push(modal);\n\n if (modals.length === 1) {\n pausedScene = new Map();\n k.getTreeRoot().children.forEach(pauseSceneTree);\n\n const controller = k.onAdd((gameObj) => {\n if (!isModal(gameObj) && !isInsideModal(gameObj)) {\n pauseSceneTree(gameObj);\n }\n });\n\n cancelPauseOnAdd = () => {\n controller.cancel();\n };\n }\n }\n\n function closeModal(modal: GameObj) {\n const index = modals.indexOf(modal);\n\n if (index === -1) {\n return;\n }\n\n const wasTopmost = index === modals.length - 1;\n\n modals.splice(index, 1);\n\n if (!modals.length) {\n cancelPauseOnAdd?.();\n cancelPauseOnAdd = undefined;\n\n // v8 ignore if\n if (pausedScene) {\n for (const [gameObj, paused] of pausedScene) {\n if (gameObj.exists()) {\n setPaused(gameObj, paused);\n }\n }\n\n pausedScene = undefined;\n }\n } else if (wasTopmost) {\n setPaused(getLast(modals), false, true);\n }\n }\n\n const isTopmost = (modal: GameObj) => getLast(modals) === modal;\n\n return { openModal, closeModal, isTopmost, isInsideModal };\n}\n","import type { Color, GameObj, KAPLAYCtx } from 'kaplay';\n\nimport { MODAL_TAG } from './modal';\n\n/**\n * Get the last item in an array.\n *\n * @param items - The items.\n * @returns The last item.\n */\nexport function getLast<T>(items: T[]): T {\n return items[items.length - 1];\n}\n\n/**\n * Check if the game object is a modal.\n *\n * @param gameObj - The game object.\n * @returns Whether game object is a modal.\n */\nexport function isModal(gameObj: GameObj): boolean {\n return gameObj.is(MODAL_TAG);\n}\n\n/**\n * Pause or unpause game object(s).\n *\n * @param gameObj - The root game object.\n * @param isPaused - Whether it should be paused.\n * @param includeTree - Whether to pause all game objects in tree.\n */\nexport function setPaused(\n gameObj: GameObj,\n isPaused = true,\n includeTree = false,\n): void {\n gameObj.paused = isPaused;\n\n if (includeTree) {\n gameObj.children.forEach((childGameObj) => {\n setPaused(childGameObj, isPaused, includeTree);\n });\n }\n}\n\n/**\n * Convert a hex string or color to 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\n/**\n * Generate a unique tag.\n *\n * @param id - The identifier.\n * @returns The tag.\n */\nexport function uniqueTag(id: string): string {\n return [\n 'kaplay-plugin-ui',\n id,\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n crypto?.randomUUID?.(),\n ].join(':');\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 let disabled = opt.disabled ?? false;\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;\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 components = [\n k.rect(0, 0, rectOpt),\n k.pos(pos),\n k.area(),\n k.scale(),\n k.color(disabled ? toColor(k, theme.palette.disabled) : baseColor),\n k.anchor('center'),\n ];\n const button = opt.parent ? opt.parent.add(components) : k.add(components);\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 if (disabled) {\n return;\n }\n\n // prevent click-through\n k.wait(0, () => {\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 // v8 ignore if\n if (disabled) {\n k.setCursor('not-allowed');\n } else if (!button.isHovering()) {\n k.setCursor('default');\n }\n });\n });\n\n button.onHover(() => {\n if (disabled) {\n k.setCursor('not-allowed');\n return;\n } else {\n k.setCursor('pointer');\n }\n\n button.color = toColor(k, hoverColor);\n button.scale = k.vec2(1.05, 1.05);\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 k.setCursor('default');\n\n if (disabled) {\n return;\n }\n\n button.color = baseColor;\n button.scale = k.vec2(1, 1);\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 button.onDestroy(() => {\n if (button.isHovering()) {\n k.setCursor('default');\n }\n });\n\n function applyDisabledState() {\n button.color = disabled ? toColor(k, theme.palette.disabled) : baseColor;\n }\n\n applyDisabledState();\n\n const assignedButton = Object.assign(button, {\n background,\n icon,\n label,\n });\n\n const definedButton = Object.defineProperty(assignedButton, 'disabled', {\n get() {\n return disabled;\n },\n set(value: boolean) {\n disabled = value;\n applyDisabledState();\n },\n enumerable: true,\n configurable: true,\n }) as typeof assignedButton & { disabled: boolean };\n\n return definedButton;\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 gap: 16,\n maxWidth: 480,\n minWidth: 280,\n paddingX: 24,\n paddingY: 20,\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 { createModal, createModalManager } from './modal';\nimport { resolveTheme } from './theme';\nimport type {\n AddButton,\n AddModal,\n DeepPartial,\n GetTheme,\n SetTheme,\n Theme,\n} from './types';\n\n/**\n * Kaplay UI plugin.\n *\n * @param k - The Kaplay context.\n * @param theme - The theme config.\n * @returns The plugin methods.\n */\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 const modalManager = createModalManager(k);\n\n const addModal: AddModal = (opt = {}) => {\n return createModal(k, currentTheme, opt, {\n isTopmost: modalManager.isTopmost,\n onClose: modalManager.closeModal,\n onOpen: modalManager.openModal,\n });\n };\n\n return {\n addButton,\n addModal,\n setTheme,\n getTheme,\n };\n}\n"],"mappings":"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"}