kaplay-plugin-ui 1.2.0 → 1.3.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
@@ -9,6 +9,7 @@
9
9
  🦖 KAPLAY plugin UI (see [API](#api)):
10
10
 
11
11
  - [addButton](#addButton)
12
+ - [addModal](#addModal)
12
13
  - [getTheme](#getTheme)
13
14
  - [setTheme](#setTheme)
14
15
 
@@ -151,6 +152,72 @@ addButton('Extra', {
151
152
  });
152
153
  ```
153
154
 
155
+ ### addModal
156
+
157
+ Add a modal dialog:
158
+
159
+ ```ts
160
+ addModal(opt);
161
+ ```
162
+
163
+ ```ts
164
+ addModal({
165
+ title: 'Pause',
166
+ text: 'Take a break?',
167
+ actions: [{ text: 'Resume' }, { text: 'Quit', onClick: () => go('menu') }],
168
+ });
169
+ ```
170
+
171
+ The modal opens immediately. It blocks the scene by pausing every object outside the modal while it's open, and restores each object's previous paused state when the modal closes. Opening another modal pauses the one below it, so only the topmost modal responds to dismissal.
172
+
173
+ Modals auto-size to their title, text, and actions. Text wraps at `components.modal.maxWidth` minus horizontal padding. Pass `width` or `height` to override either dimension.
174
+
175
+ Actions close the modal by default before calling `onClick`. Set `close: false` to keep it open:
176
+
177
+ ```ts
178
+ addModal({
179
+ text: 'Saved.',
180
+ actions: [{ text: 'OK', close: false }],
181
+ });
182
+ ```
183
+
184
+ Dismiss with the backdrop or <kbd>Esc</kbd> by default. Disable dismissal with `dismissible: false`:
185
+
186
+ ```ts
187
+ addModal({
188
+ text: 'Choose an option.',
189
+ dismissible: false,
190
+ actions: [{ text: 'Continue' }],
191
+ });
192
+ ```
193
+
194
+ Pass `z` to change the stacking order. The default is `1000`.
195
+
196
+ Add custom content to the returned `content` container:
197
+
198
+ ```ts
199
+ const modal = addModal({ title: 'Options' });
200
+
201
+ modal.content.add([text('Custom content', { size: 16 }), pos(0, 40)]);
202
+ ```
203
+
204
+ Attach extra buttons to `modal.actions`:
205
+
206
+ ```ts
207
+ addButton('Help', { parent: modal.actions });
208
+ ```
209
+
210
+ Close a modal by destroying it:
211
+
212
+ ```ts
213
+ modal.destroy();
214
+ ```
215
+
216
+ Use `modal.exists()` to check whether it is still open.
217
+
218
+ > [!NOTE]
219
+ > Pausing stops updates and object-level input events on scene objects. Global handlers registered with `onUpdate`, `onKeyPress`, or `onMousePress` still run while a modal is open. Guard those handlers if you don't want them to fire behind a modal.
220
+
154
221
  ### getTheme
155
222
 
156
223
  Retrieve theme:
package/dist/plugin.cjs CHANGED
@@ -1,7 +1,229 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ // src/modal.ts
3
+ /** Tag applied to modal game objects. */
4
+ const MODAL_TAG = uniqueTag("modal");
5
+ function isSized(obj) {
6
+ const sized = obj;
7
+ return typeof sized.width === "number" && typeof sized.height === "number";
8
+ }
9
+ function createModal(k, theme, opt, hooks) {
10
+ const { backdropOpacity, borderRadius, gap, maxWidth, minWidth, paddingX, paddingY } = theme.components.modal;
11
+ const fontSize = theme.typography.fontSize;
12
+ const titleSize = Math.round(fontSize * 1.25);
13
+ const font = theme.typography.fontFamily;
14
+ const dismissible = opt.dismissible ?? true;
15
+ const modal = k.add([
16
+ k.pos(k.center()),
17
+ k.fixed(),
18
+ k.z(opt.z ?? 1e3),
19
+ MODAL_TAG
20
+ ]);
21
+ modal.onDestroy(() => {
22
+ hooks.onClose(modal);
23
+ });
24
+ hooks.onOpen(modal);
25
+ const backdrop = modal.add([
26
+ k.rect(k.width(), k.height()),
27
+ k.pos(),
28
+ k.color(0, 0, 0),
29
+ k.opacity(backdropOpacity),
30
+ k.anchor("center"),
31
+ k.area()
32
+ ]);
33
+ const content = modal.add([
34
+ k.rect(0, 0, borderRadius > 0 ? { radius: borderRadius } : void 0),
35
+ k.pos(),
36
+ k.color(toColor(k, theme.palette.surface)),
37
+ k.anchor("center"),
38
+ k.area()
39
+ ]);
40
+ const actions = content.add([k.pos()]);
41
+ for (const action of opt.actions ?? []) createButton(k, theme, action.text, {
42
+ parent: actions,
43
+ onClick: () => {
44
+ if (action.close !== false) modal.destroy();
45
+ action.onClick?.();
46
+ }
47
+ });
48
+ const titleMeasure = opt.title ? k.text(opt.title, {
49
+ size: titleSize,
50
+ font
51
+ }) : void 0;
52
+ const bodyMeasure = opt.text ? k.text(opt.text, {
53
+ size: fontSize,
54
+ font
55
+ }) : void 0;
56
+ const textWrapWidth = Math.max(0, (opt.width ?? maxWidth) - paddingX * 2);
57
+ const title = opt.title ? content.add([
58
+ k.text(opt.title, {
59
+ size: titleSize,
60
+ font,
61
+ width: textWrapWidth > 0 ? textWrapWidth : void 0,
62
+ align: "center"
63
+ }),
64
+ k.pos(),
65
+ k.color(toColor(k, theme.palette.text)),
66
+ k.anchor("center")
67
+ ]) : void 0;
68
+ const body = opt.text ? content.add([
69
+ k.text(opt.text, {
70
+ size: fontSize,
71
+ font,
72
+ width: textWrapWidth > 0 ? textWrapWidth : void 0,
73
+ align: "center"
74
+ }),
75
+ k.pos(),
76
+ k.color(toColor(k, theme.palette.textSecondary)),
77
+ k.anchor("center")
78
+ ]) : void 0;
79
+ /**
80
+ * Update modal layout:
81
+ *
82
+ * - fits the backdrop to the screen,
83
+ * - sizes the content to its title, text, and actions,
84
+ * - repositions the title, text, and actions.
85
+ */
86
+ function layout() {
87
+ const screenWidth = k.width();
88
+ const screenHeight = k.height();
89
+ if (backdrop.width !== screenWidth) backdrop.width = screenWidth;
90
+ if (backdrop.height !== screenHeight) backdrop.height = screenHeight;
91
+ modal.pos = k.center();
92
+ const buttons = actions.children.filter(isSized);
93
+ const actionsWidth = buttons.reduce((width, button) => width + button.width, 0) + gap * Math.max(0, buttons.length - 1);
94
+ const actionsHeight = Math.max(0, ...buttons.map((button) => button.height));
95
+ const textContentWidth = Math.max(titleMeasure?.width ?? 0, bodyMeasure?.width ?? 0);
96
+ const nextWidth = opt.width ?? Math.max(Math.min(textContentWidth, maxWidth - paddingX * 2) + paddingX * 2, actionsWidth + paddingX * 2, minWidth);
97
+ const titleHeight = title?.height ?? 0;
98
+ const bodyHeight = body?.height ?? 0;
99
+ const hasTitle = title !== void 0;
100
+ const hasBody = body !== void 0;
101
+ const hasActions = buttons.length > 0;
102
+ const blockCount = Number(hasTitle) + Number(hasBody) + Number(hasActions);
103
+ const contentHeight = titleHeight + bodyHeight + actionsHeight + gap * Math.max(0, blockCount - 1);
104
+ const nextHeight = opt.height ?? contentHeight + paddingY * 2;
105
+ if (content.width !== nextWidth) content.width = nextWidth;
106
+ if (content.height !== nextHeight) content.height = nextHeight;
107
+ let y = -nextHeight / 2 + (nextHeight - contentHeight) / 2;
108
+ if (title) {
109
+ title.pos = k.vec2(0, y + title.height / 2);
110
+ y += title.height + gap;
111
+ }
112
+ if (body) {
113
+ body.pos = k.vec2(0, y + body.height / 2);
114
+ y += body.height + gap;
115
+ }
116
+ if (hasActions) {
117
+ let x = -actionsWidth / 2;
118
+ for (const button of buttons) {
119
+ button.pos = k.vec2(x + button.width / 2, y + actionsHeight / 2);
120
+ x += button.width + gap;
121
+ }
122
+ }
123
+ }
124
+ layout();
125
+ modal.onUpdate(layout);
126
+ if (dismissible) {
127
+ k.wait(0, () => {
128
+ backdrop.onClick(() => {
129
+ if (!hooks.isTopmost(modal)) return;
130
+ if (content.hasPoint(k.mousePos())) return;
131
+ modal.destroy();
132
+ });
133
+ });
134
+ modal.onKeyPress("escape", () => {
135
+ if (hooks.isTopmost(modal)) modal.destroy();
136
+ });
137
+ }
138
+ return Object.assign(modal, {
139
+ actions,
140
+ content
141
+ });
142
+ }
143
+ function createModalManager(k) {
144
+ const modals = [];
145
+ let pausedScene;
146
+ let cancelPauseOnAdd;
147
+ const isInsideModal = (gameObj) => modals.some((modal) => modal === gameObj || modal.isAncestorOf(gameObj));
148
+ function pauseSceneTree(gameObj) {
149
+ if (isModal(gameObj) || isInsideModal(gameObj)) return;
150
+ pausedScene ??= /* @__PURE__ */ new Map();
151
+ // v8 ignore next
152
+ if (!pausedScene.has(gameObj)) pausedScene.set(gameObj, gameObj.paused);
153
+ setPaused(gameObj);
154
+ gameObj.children.forEach(pauseSceneTree);
155
+ }
156
+ function openModal(modal) {
157
+ if (modals.length > 0) setPaused(getLast(modals), true, true);
158
+ modals.push(modal);
159
+ if (modals.length === 1) {
160
+ pausedScene = /* @__PURE__ */ new Map();
161
+ k.getTreeRoot().children.forEach(pauseSceneTree);
162
+ const controller = k.onAdd((gameObj) => {
163
+ if (!isModal(gameObj) && !isInsideModal(gameObj)) pauseSceneTree(gameObj);
164
+ });
165
+ cancelPauseOnAdd = () => {
166
+ controller.cancel();
167
+ };
168
+ }
169
+ }
170
+ function closeModal(modal) {
171
+ const index = modals.indexOf(modal);
172
+ if (index === -1) return;
173
+ const wasTopmost = index === modals.length - 1;
174
+ modals.splice(index, 1);
175
+ if (!modals.length) {
176
+ cancelPauseOnAdd?.();
177
+ cancelPauseOnAdd = void 0;
178
+ // v8 ignore next
179
+ if (pausedScene) {
180
+ for (const [gameObj, paused] of pausedScene) if (gameObj.exists()) setPaused(gameObj, paused);
181
+ pausedScene = void 0;
182
+ }
183
+ } else if (wasTopmost) setPaused(getLast(modals), false, true);
184
+ }
185
+ const isTopmost = (modal) => getLast(modals) === modal;
186
+ return {
187
+ openModal,
188
+ closeModal,
189
+ isTopmost,
190
+ isInsideModal
191
+ };
192
+ }
2
193
  // src/utils.ts
3
194
  /**
4
- * Convert hex string to color or return color.
195
+ * Get the last item in an array.
196
+ *
197
+ * @param items - The items.
198
+ * @returns The last item.
199
+ */
200
+ function getLast(items) {
201
+ return items[items.length - 1];
202
+ }
203
+ /**
204
+ * Check if the game object is a modal.
205
+ *
206
+ * @param gameObj - The game object.
207
+ * @returns Whether game object is a modal.
208
+ */
209
+ function isModal(gameObj) {
210
+ return gameObj.is(MODAL_TAG);
211
+ }
212
+ /**
213
+ * Pause or unpause game object(s).
214
+ *
215
+ * @param gameObj - The root game object.
216
+ * @param isPaused - Whether it should be paused.
217
+ * @param includeTree - Whether to pause all game objects in tree.
218
+ */
219
+ function setPaused(gameObj, isPaused = true, includeTree = false) {
220
+ gameObj.paused = isPaused;
221
+ if (includeTree) gameObj.children.forEach((childGameObj) => {
222
+ setPaused(childGameObj, isPaused, includeTree);
223
+ });
224
+ }
225
+ /**
226
+ * Convert a hex string or color to color.
5
227
  *
6
228
  * @param k - The Kaplay context.
7
229
  * @param color - The hex string or color.
@@ -10,6 +232,19 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
10
232
  function toColor(k, color) {
11
233
  return typeof color === "string" ? k.rgb(color) : color;
12
234
  }
235
+ /**
236
+ * Generate a unique tag.
237
+ *
238
+ * @param id - The identifier.
239
+ * @returns The tag.
240
+ */
241
+ function uniqueTag(id) {
242
+ return [
243
+ "kaplay-plugin-ui",
244
+ id,
245
+ crypto?.randomUUID?.()
246
+ ].join(":");
247
+ }
13
248
  // src/button.ts
14
249
  function createButton(k, theme, text, opt) {
15
250
  const radius = opt.radius ?? theme.shape.borderRadius;
@@ -25,7 +260,7 @@ function createButton(k, theme, text, opt) {
25
260
  size,
26
261
  font
27
262
  });
28
- const isIconOnly = opt.icon !== void 0 && text.length === 0;
263
+ const isIconOnly = opt.icon !== void 0 && !text.length;
29
264
  const rectOpt = !(opt.background !== void 0) && !isIconOnly ? radius > 0 ? { radius } : void 0 : { fill: false };
30
265
  const components = [
31
266
  k.rect(0, 0, rectOpt),
@@ -97,16 +332,18 @@ function createButton(k, theme, text, opt) {
97
332
  layout();
98
333
  if (opt.width === void 0 || opt.height === void 0 || opt.icon !== void 0 || opt.background !== void 0) button.onUpdate(layout);
99
334
  button.onClick(() => {
100
- button.color = toColor(k, pressedColor);
101
- if (icon && opt.pressedIcon) {
102
- iconState = "pressed";
103
- icon.sprite = opt.pressedIcon;
104
- }
105
- if (background && opt.pressedBackground) {
106
- backgroundState = "pressed";
107
- background.sprite = opt.pressedBackground;
108
- }
109
- opt.onClick?.();
335
+ k.wait(0, () => {
336
+ button.color = toColor(k, pressedColor);
337
+ if (icon && opt.pressedIcon) {
338
+ iconState = "pressed";
339
+ icon.sprite = opt.pressedIcon;
340
+ }
341
+ if (background && opt.pressedBackground) {
342
+ backgroundState = "pressed";
343
+ background.sprite = opt.pressedBackground;
344
+ }
345
+ opt.onClick?.();
346
+ });
110
347
  });
111
348
  button.onHover(() => {
112
349
  button.color = toColor(k, hoverColor);
@@ -175,7 +412,12 @@ const theme = {
175
412
  },
176
413
  modal: {
177
414
  backdropOpacity: .5,
178
- borderRadius: 8
415
+ borderRadius: 8,
416
+ gap: 16,
417
+ maxWidth: 480,
418
+ minWidth: 280,
419
+ paddingX: 24,
420
+ paddingY: 20
179
421
  },
180
422
  tooltip: {
181
423
  background: "#111827",
@@ -217,7 +459,13 @@ function resolveTheme(input) {
217
459
  };
218
460
  }
219
461
  // src/plugin.ts
220
- /** KAPLAY UI plugin. */
462
+ /**
463
+ * Kaplay UI plugin.
464
+ *
465
+ * @param k - The Kaplay context.
466
+ * @param theme - The theme config.
467
+ * @returns The plugin methods.
468
+ */
221
469
  function uiPlugin(k, theme = {}) {
222
470
  let userTheme = theme;
223
471
  let currentTheme = resolveTheme(userTheme);
@@ -232,8 +480,17 @@ function uiPlugin(k, theme = {}) {
232
480
  const addButton = (text = "", opt = {}) => {
233
481
  return createButton(k, currentTheme, text, opt);
234
482
  };
483
+ const modalManager = createModalManager(k);
484
+ const addModal = (opt = {}) => {
485
+ return createModal(k, currentTheme, opt, {
486
+ isTopmost: modalManager.isTopmost,
487
+ onClose: modalManager.closeModal,
488
+ onOpen: modalManager.openModal
489
+ });
490
+ };
235
491
  return {
236
492
  addButton,
493
+ addModal,
237
494
  setTheme,
238
495
  getTheme
239
496
  };
@@ -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 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 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 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.cjs","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 next\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 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 next\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 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(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 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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"}
package/dist/plugin.d.cts CHANGED
@@ -6,15 +6,25 @@ declare function createButton(k: KAPLAYCtx, theme: Theme, text: string, opt: But
6
6
  hoverColor?: string | Color;
7
7
  pressedColor?: string | Color;
8
8
  textColor?: string | Color;
9
- }): import("kaplay").GameObjRaw & import("kaplay").MergeComps<import("kaplay").RectComp | import("kaplay").PosComp | import("kaplay").AreaComp | import("kaplay").ScaleComp | import("kaplay").ColorComp | import("kaplay").AnchorComp> & {
9
+ }): import("kaplay").GameObjRaw & import("kaplay").MergeComps<import("kaplay").PosComp | import("kaplay").RectComp | import("kaplay").ColorComp | import("kaplay").AnchorComp | import("kaplay").AreaComp | import("kaplay").ScaleComp> & {
10
10
  background: import("kaplay").GameObj<import("kaplay").PosComp | import("kaplay").AnchorComp | import("kaplay").SpriteComp> | undefined;
11
11
  icon: import("kaplay").GameObj<import("kaplay").PosComp | import("kaplay").AnchorComp | import("kaplay").SpriteComp> | undefined;
12
12
  label: import("kaplay").GameObj<import("kaplay").PosComp | import("kaplay").ColorComp | import("kaplay").AnchorComp | import("kaplay").TextComp>;
13
13
  };
14
+ interface ModalHooks {
15
+ isTopmost: (modal: GameObj) => boolean;
16
+ onClose: (modal: GameObj) => void;
17
+ onOpen: (modal: GameObj) => void;
18
+ }
19
+ declare function createModal(k: KAPLAYCtx, theme: Theme, opt: ModalOpt, hooks: ModalHooks): import("kaplay").GameObjRaw & import("kaplay").MergeComps<import("kaplay").PosComp | import("kaplay").FixedComp | import("kaplay").ZComp> & {
20
+ actions: GameObj<import("kaplay").PosComp>;
21
+ content: GameObj<import("kaplay").PosComp | import("kaplay").RectComp | import("kaplay").ColorComp | import("kaplay").AnchorComp | import("kaplay").AreaComp>;
22
+ };
14
23
  type SpriteSource = string | SpriteData | Asset<SpriteData>;
15
24
  declare module 'kaplay' {
16
25
  interface KAPLAYCtx {
17
26
  addButton: AddButton;
27
+ addModal: AddModal;
18
28
  getTheme: GetTheme;
19
29
  setTheme: SetTheme;
20
30
  }
@@ -27,6 +37,7 @@ type AddButton = (text?: string, opt?: ButtonOpt & {
27
37
  pressedColor?: string | Color;
28
38
  textColor?: string | Color;
29
39
  }) => ReturnType<typeof createButton>;
40
+ type AddModal = (opt?: ModalOpt) => ReturnType<typeof createModal>;
30
41
  type GetTheme = () => Theme;
31
42
  type SetTheme = (theme: DeepPartial<Theme>) => void;
32
43
  /** Recursively partial version of a type. */
@@ -65,6 +76,11 @@ interface Theme {
65
76
  modal: {
66
77
  backdropOpacity: number;
67
78
  borderRadius: number;
79
+ gap: number;
80
+ maxWidth: number;
81
+ minWidth: number;
82
+ paddingX: number;
83
+ paddingY: number;
68
84
  };
69
85
  tooltip: {
70
86
  background: string | Color;
@@ -73,6 +89,26 @@ interface Theme {
73
89
  };
74
90
  };
75
91
  }
92
+ /** Modal options. */
93
+ interface ModalOpt {
94
+ title?: string;
95
+ text?: string;
96
+ width?: number;
97
+ height?: number;
98
+ actions?: ModalAction[];
99
+ /** Allow closing with the backdrop or Escape. Defaults to true. */
100
+ dismissible?: boolean;
101
+ /** Z index of the modal. Defaults to 1000. */
102
+ z?: number;
103
+ }
104
+ /** Modal action. */
105
+ interface ModalAction {
106
+ text: string;
107
+ /** Runs after the modal closes, unless `close` is false. */
108
+ onClick?: () => void;
109
+ /** Close the modal after the action. Defaults to true. */
110
+ close?: boolean;
111
+ }
76
112
  /** Button options. */
77
113
  interface ButtonOpt {
78
114
  width?: number;
@@ -99,9 +135,16 @@ interface ButtonOpt {
99
135
  /** Sprite drawn behind the label when pressed. */
100
136
  pressedBackground?: SpriteSource;
101
137
  }
102
- /** KAPLAY UI plugin. */
138
+ /**
139
+ * Kaplay UI plugin.
140
+ *
141
+ * @param k - The Kaplay context.
142
+ * @param theme - The theme config.
143
+ * @returns The plugin methods.
144
+ */
103
145
  export declare function uiPlugin(k: KAPLAYCtx, theme?: DeepPartial<Theme>): {
104
146
  addButton: AddButton;
147
+ addModal: AddModal;
105
148
  setTheme: SetTheme;
106
149
  getTheme: GetTheme;
107
150
  };
@@ -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;qBACtB,8BAAA,4BAAA,4BAAA,2BAAA,4BAAA,6BAAA,6BAAA;;;;;KCZE,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;;EAEA,SAAS;EACT;EACA;EACA,gBAAgB;EAChB,qBAAqB;EACrB;EACA,qBAAqB;EACrB,uBAAuB;;EAEvB,OAAO;;EAEP,YAAY;;EAEZ,cAAc;;EAEd,aAAa;;EAEb,kBAAkB;;EAElB,oBAAoB;;;wBCzFN,SAAS,GAAG,WAAW,QAAO,YAAY"}
1
+ {"version":3,"file":"plugin.d.cts","names":[],"sources":["../src/button.ts","../src/modal.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;qBACtB,8BAAA,4BAAA,2BAAA,4BAAA,6BAAA,8BAAA,4BAAA;;;;;UCFc;EACf,YAAY,OAAO;EACnB,UAAU,OAAO;EACjB,SAAS,OAAO;;iBAQF,YACd,GAAG,WACH,OAAO,OACP,KAAK,UACL,OAAO,8BAAU,8BAAA,4BAAA,2BAAA,6BAAA;;;;KCxBd,wBAAwB,aAAa,MAAM;;YAGpC;IACR,WAAW;IACX,UAAU;IACV,UAAU;IACV,UAAU;;;KAIF,aACV,eACA,MAAM;EACJ,MAAM;EACN;EACA,iBAAiB;EACjB,sBAAsB;EACtB,wBAAwB;EACxB,qBAAqB;MAEpB,kBAAkB;KAEX,YAAY,MAAM,aAAa,kBAAkB;KAEjD,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;MACA;MACA;MACA;MACA;MACA;;IAEF;MACE,qBAAqB;MACrB,oBAAoB;MACpB;;;;;UAMW;EACf;EACA;EACA;EACA;EACA,UAAU;;EAEV;;EAEA;;;UAIe;EACf;;EAEA;;EAEA;;;UAIe;EACf;EACA;;EAEA,SAAS;EACT;EACA;EACA,gBAAgB;EAChB,qBAAqB;EACrB;EACA,qBAAqB;EACrB,uBAAuB;;EAEvB,OAAO;;EAEP,YAAY;;EAEZ,cAAc;;EAEd,aAAa;;EAEb,kBAAkB;;EAElB,oBAAoB;;;;;;;;;wBChHN,SAAS,GAAG,WAAW,QAAO,YAAY"}