@makigamestudio/ui-core 0.1.7 → 0.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.
@@ -1,8 +1,8 @@
1
1
  import * as i0 from '@angular/core';
2
- import { signal, computed, Injectable, input, output, viewChild, ChangeDetectionStrategy, Component } from '@angular/core';
3
- import { IonCard, IonCardHeader, IonCardTitle, IonCardSubtitle, IonCardContent, IonButton, IonIcon } from '@ionic/angular/standalone';
2
+ import { signal, computed, Injectable, input, output, viewChild, ChangeDetectionStrategy, Component, inject } from '@angular/core';
3
+ import { IonCard, IonCardHeader, IonCardTitle, IonCardSubtitle, IonCardContent, IonButton, IonIcon, PopoverController, IonList, IonItem, IonLabel, IonSpinner } from '@ionic/angular/standalone';
4
4
  import { addIcons } from 'ionicons';
5
- import { trashOutline, createOutline } from 'ionicons/icons';
5
+ import { trashOutline, createOutline, chevronDownOutline } from 'ionicons/icons';
6
6
 
7
7
  /**
8
8
  * TestClass - Example class implementing TestInterface.
@@ -32,7 +32,7 @@ class TestClass {
32
32
  title,
33
33
  description,
34
34
  createdAt: new Date(),
35
- isActive: true,
35
+ isActive: true
36
36
  });
37
37
  }
38
38
  /**
@@ -41,7 +41,7 @@ class TestClass {
41
41
  update(changes) {
42
42
  return new TestClass({
43
43
  ...this,
44
- ...changes,
44
+ ...changes
45
45
  });
46
46
  }
47
47
  /**
@@ -60,11 +60,44 @@ class TestClass {
60
60
  description: this.description,
61
61
  createdAt: this.createdAt,
62
62
  isActive: this.isActive,
63
- metadata: this.metadata,
63
+ metadata: this.metadata
64
64
  };
65
65
  }
66
66
  }
67
67
 
68
+ /**
69
+ * @file Action button type enumeration
70
+ * @description Defines the display types for action buttons
71
+ */
72
+ /**
73
+ * Enumeration of action button display types.
74
+ *
75
+ * @example
76
+ * ```typescript
77
+ * const button: ActionButton = {
78
+ * id: 'save-btn',
79
+ * label: 'Save',
80
+ * type: ActionButtonType.Default,
81
+ * handler: () => console.log('Saved!')
82
+ * };
83
+ * ```
84
+ */
85
+ var ActionButtonType;
86
+ (function (ActionButtonType) {
87
+ /**
88
+ * Standard button with label and optional icon.
89
+ */
90
+ ActionButtonType["Default"] = "default";
91
+ /**
92
+ * Button displaying only an icon, no label.
93
+ */
94
+ ActionButtonType["IconOnly"] = "icon-only";
95
+ /**
96
+ * Button that opens a dropdown popover with child actions.
97
+ */
98
+ ActionButtonType["Dropdown"] = "dropdown";
99
+ })(ActionButtonType || (ActionButtonType = {}));
100
+
68
101
  /**
69
102
  * TestService - Example service using Angular Signals for reactive state management.
70
103
  * Provided in root, demonstrating singleton service pattern with signal-based state.
@@ -84,7 +117,7 @@ class TestService {
84
117
  /** Count of all items */
85
118
  count = computed(() => this._items().length, ...(ngDevMode ? [{ debugName: "count" }] : []));
86
119
  /** Only active items */
87
- activeItems = computed(() => this._items().filter((item) => item.isActive), ...(ngDevMode ? [{ debugName: "activeItems" }] : []));
120
+ activeItems = computed(() => this._items().filter(item => item.isActive), ...(ngDevMode ? [{ debugName: "activeItems" }] : []));
88
121
  /** Count of active items */
89
122
  activeCount = computed(() => this.activeItems().length, ...(ngDevMode ? [{ debugName: "activeCount" }] : []));
90
123
  /**
@@ -92,7 +125,7 @@ class TestService {
92
125
  */
93
126
  addItem(title, description) {
94
127
  const newItem = TestClass.create(title, description);
95
- this._items.update((items) => [...items, newItem]);
128
+ this._items.update(items => [...items, newItem]);
96
129
  this._error.set(null);
97
130
  return newItem;
98
131
  }
@@ -101,7 +134,7 @@ class TestService {
101
134
  */
102
135
  removeItem(id) {
103
136
  const currentItems = this._items();
104
- const filteredItems = currentItems.filter((item) => item.id !== id);
137
+ const filteredItems = currentItems.filter(item => item.id !== id);
105
138
  if (filteredItems.length === currentItems.length) {
106
139
  return false;
107
140
  }
@@ -113,7 +146,7 @@ class TestService {
113
146
  */
114
147
  updateItem(id, changes) {
115
148
  let updatedItem = null;
116
- this._items.update((items) => items.map((item) => {
149
+ this._items.update(items => items.map(item => {
117
150
  if (item.id === id) {
118
151
  updatedItem = item.update(changes);
119
152
  return updatedItem;
@@ -132,7 +165,7 @@ class TestService {
132
165
  * Gets an item by ID using computed lookup.
133
166
  */
134
167
  getItemById(id) {
135
- return this._items().find((item) => item.id === id);
168
+ return this._items().find(item => item.id === id);
136
169
  }
137
170
  /**
138
171
  * Clears all items from the store.
@@ -159,7 +192,7 @@ class TestService {
159
192
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TestService, decorators: [{
160
193
  type: Injectable,
161
194
  args: [{
162
- providedIn: 'root',
195
+ providedIn: 'root'
163
196
  }]
164
197
  }] });
165
198
 
@@ -196,7 +229,7 @@ class TestCardComponent {
196
229
  return dateObj.toLocaleDateString(undefined, {
197
230
  year: 'numeric',
198
231
  month: 'short',
199
- day: 'numeric',
232
+ day: 'numeric'
200
233
  });
201
234
  }, ...(ngDevMode ? [{ debugName: "formattedDate" }] : []));
202
235
  constructor() {
@@ -221,11 +254,7 @@ class TestCardComponent {
221
254
  }
222
255
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TestCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
223
256
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TestCardComponent, isStandalone: true, selector: "maki-test-card", inputs: { item: { classPropertyName: "item", publicName: "item", isSignal: true, isRequired: true, transformFunction: null }, showActions: { classPropertyName: "showActions", publicName: "showActions", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { itemClick: "itemClick", editClick: "editClick", deleteClick: "deleteClick" }, viewQueries: [{ propertyName: "cardElement", first: true, predicate: ["cardElement"], descendants: true, isSignal: true }], ngImport: i0, template: `
224
- <ion-card
225
- #cardElement
226
- [class.inactive]="!item().isActive"
227
- (click)="onCardClick()"
228
- >
257
+ <ion-card #cardElement [class.inactive]="!item().isActive" (click)="onCardClick()">
229
258
  <ion-card-header>
230
259
  <ion-card-title>{{ item().title }}</ion-card-title>
231
260
  @if (formattedDate()) {
@@ -246,20 +275,11 @@ class TestCardComponent {
246
275
 
247
276
  @if (showActions()) {
248
277
  <ion-card-content class="card-actions">
249
- <ion-button
250
- fill="outline"
251
- size="small"
252
- (click)="onEditClick($event)"
253
- >
278
+ <ion-button fill="outline" size="small" (click)="onEditClick($event)">
254
279
  <ion-icon slot="start" name="create-outline"></ion-icon>
255
280
  Edit
256
281
  </ion-button>
257
- <ion-button
258
- fill="outline"
259
- size="small"
260
- color="danger"
261
- (click)="onDeleteClick($event)"
262
- >
282
+ <ion-button fill="outline" size="small" color="danger" (click)="onDeleteClick($event)">
263
283
  <ion-icon slot="start" name="trash-outline"></ion-icon>
264
284
  Delete
265
285
  </ion-button>
@@ -277,13 +297,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
277
297
  IonCardSubtitle,
278
298
  IonCardContent,
279
299
  IonButton,
280
- IonIcon,
300
+ IonIcon
281
301
  ], changeDetection: ChangeDetectionStrategy.OnPush, template: `
282
- <ion-card
283
- #cardElement
284
- [class.inactive]="!item().isActive"
285
- (click)="onCardClick()"
286
- >
302
+ <ion-card #cardElement [class.inactive]="!item().isActive" (click)="onCardClick()">
287
303
  <ion-card-header>
288
304
  <ion-card-title>{{ item().title }}</ion-card-title>
289
305
  @if (formattedDate()) {
@@ -304,20 +320,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
304
320
 
305
321
  @if (showActions()) {
306
322
  <ion-card-content class="card-actions">
307
- <ion-button
308
- fill="outline"
309
- size="small"
310
- (click)="onEditClick($event)"
311
- >
323
+ <ion-button fill="outline" size="small" (click)="onEditClick($event)">
312
324
  <ion-icon slot="start" name="create-outline"></ion-icon>
313
325
  Edit
314
326
  </ion-button>
315
- <ion-button
316
- fill="outline"
317
- size="small"
318
- color="danger"
319
- (click)="onDeleteClick($event)"
320
- >
327
+ <ion-button fill="outline" size="small" color="danger" (click)="onDeleteClick($event)">
321
328
  <ion-icon slot="start" name="trash-outline"></ion-icon>
322
329
  Delete
323
330
  </ion-button>
@@ -327,6 +334,491 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
327
334
  `, styles: [":host{display:block}ion-card{--background: var(--maki-card-background, var(--ion-card-background, #fff));--color: var(--maki-card-color, var(--ion-text-color));margin:var(--maki-card-margin, 16px);border-radius:var(--maki-card-border-radius, 12px);transition:opacity .2s ease,transform .2s ease;cursor:pointer}ion-card:hover{transform:translateY(-2px)}ion-card.inactive{opacity:.6}ion-card-title{color:var(--maki-primary, var(--ion-color-primary));font-weight:600}ion-card-subtitle{display:flex;align-items:center;gap:8px}.status-badge{font-size:.75rem;padding:2px 8px;border-radius:4px;font-weight:500}.status-badge.inactive{background-color:var(--ion-color-medium-tint);color:var(--ion-color-medium-contrast)}.card-actions{display:flex;gap:8px;padding-top:0}.card-actions ion-button{--border-color: var(--maki-primary, var(--ion-color-primary));--color: var(--maki-primary, var(--ion-color-primary))}\n"] }]
328
335
  }], ctorParameters: () => [], propDecorators: { item: [{ type: i0.Input, args: [{ isSignal: true, alias: "item", required: true }] }], showActions: [{ type: i0.Input, args: [{ isSignal: true, alias: "showActions", required: false }] }], itemClick: [{ type: i0.Output, args: ["itemClick"] }], editClick: [{ type: i0.Output, args: ["editClick"] }], deleteClick: [{ type: i0.Output, args: ["deleteClick"] }], cardElement: [{ type: i0.ViewChild, args: ['cardElement', { isSignal: true }] }] } });
329
336
 
337
+ /**
338
+ * @file Action Button List Component
339
+ * @description Component for rendering a list of action buttons inside a popover
340
+ */
341
+ /**
342
+ * Component that renders a list of action buttons for use in popovers/dropdowns.
343
+ *
344
+ * This component is designed to be used as the content of an `ion-popover`
345
+ * created via `PopoverController`. When a button is selected, it dismisses
346
+ * the popover and returns the selected button.
347
+ *
348
+ * @example
349
+ * ```typescript
350
+ * // Used internally by maki-button for dropdowns
351
+ * // Can also be used standalone:
352
+ * const popover = await popoverCtrl.create({
353
+ * component: ActionButtonListComponent,
354
+ * componentProps: { buttons: myButtons },
355
+ * event: clickEvent
356
+ * });
357
+ * await popover.present();
358
+ *
359
+ * const { data } = await popover.onDidDismiss();
360
+ * if (data) {
361
+ * data.handler();
362
+ * }
363
+ * ```
364
+ *
365
+ * @usageNotes
366
+ * ### Inputs
367
+ * - `buttons` (required): Array of `ActionButton` objects to display
368
+ *
369
+ * ### Outputs
370
+ * - `buttonSelect`: Emits the selected `ActionButton` when clicked
371
+ */
372
+ class ActionButtonListComponent {
373
+ /**
374
+ * Reference to ActionButtonType.Dropdown for template comparison.
375
+ * @internal
376
+ */
377
+ dropdownType = ActionButtonType.Dropdown;
378
+ /**
379
+ * Popover controller for dismissing the popover when an item is selected.
380
+ */
381
+ popoverCtrl = inject(PopoverController, { optional: true });
382
+ /**
383
+ * Internal signal to store buttons passed via componentProps.
384
+ * PopoverController.create() sets properties directly, bypassing signal inputs.
385
+ */
386
+ _buttonsFromProps = signal([], ...(ngDevMode ? [{ debugName: "_buttonsFromProps" }] : []));
387
+ /**
388
+ * Internal signal to store the Set of currently loading child button IDs.
389
+ */
390
+ _loadingChildIdsFromProps = signal(null, ...(ngDevMode ? [{ debugName: "_loadingChildIdsFromProps" }] : []));
391
+ /**
392
+ * The list of action buttons to display (when used via template binding).
393
+ */
394
+ buttons = input([], ...(ngDevMode ? [{ debugName: "buttons" }] : []));
395
+ /**
396
+ * Computed button list that works with both signal input and componentProps.
397
+ * PopoverController passes buttons as a direct property, not through Angular's input system.
398
+ */
399
+ buttonList = () => {
400
+ const fromProps = this._buttonsFromProps();
401
+ const fromInput = this.buttons();
402
+ return fromProps.length > 0 ? fromProps : fromInput;
403
+ };
404
+ /**
405
+ * Setter to capture buttons passed via PopoverController.create({ componentProps }).
406
+ * This is called when Ionic sets the property directly on the component instance.
407
+ */
408
+ set buttonsFromPopover(value) {
409
+ this._buttonsFromProps.set(value);
410
+ }
411
+ /**
412
+ * Setter to capture loading child IDs signal passed via PopoverController.create({ componentProps }).
413
+ */
414
+ set loadingChildIds(value) {
415
+ this._loadingChildIdsFromProps.set(value);
416
+ }
417
+ /**
418
+ * Checks if a specific button is currently loading.
419
+ */
420
+ isButtonLoading(button) {
421
+ const loadingChildIdsSignal = this._loadingChildIdsFromProps();
422
+ const loadingChildIds = loadingChildIdsSignal ? loadingChildIdsSignal() : new Set();
423
+ return button.loading || loadingChildIds.has(button.id);
424
+ }
425
+ /**
426
+ * Emits when a button is selected from the list.
427
+ */
428
+ buttonSelect = output();
429
+ constructor() {
430
+ // Register any icons that might be used
431
+ addIcons({});
432
+ }
433
+ /**
434
+ * Handles click on a button item.
435
+ * Emits the selected button and dismisses the popover.
436
+ *
437
+ * @param button - The clicked action button
438
+ */
439
+ onButtonClick(button) {
440
+ if (this.isButtonLoading(button) || button.config?.disabled) {
441
+ return;
442
+ }
443
+ this.buttonSelect.emit(button);
444
+ this.popoverCtrl?.dismiss(button, 'select');
445
+ }
446
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ActionButtonListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
447
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: ActionButtonListComponent, isStandalone: true, selector: "maki-action-button-list", inputs: { buttons: { classPropertyName: "buttons", publicName: "buttons", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { buttonSelect: "buttonSelect" }, ngImport: i0, template: `
448
+ <ion-list lines="none">
449
+ @for (button of buttonList(); track button.id) {
450
+ <ion-item
451
+ [button]="true"
452
+ [disabled]="isButtonLoading(button) || button.config?.disabled"
453
+ [detail]="button.type === dropdownType"
454
+ (click)="onButtonClick(button)"
455
+ >
456
+ @if (isButtonLoading(button) && (button.showLoadingSpinner ?? true)) {
457
+ <ion-spinner slot="start" name="crescent" />
458
+ } @else if (button.icon) {
459
+ <ion-icon slot="start" [name]="button.icon" />
460
+ }
461
+ @if (button.label) {
462
+ <ion-label>{{ button.label }}</ion-label>
463
+ }
464
+ </ion-item>
465
+ } @empty {
466
+ <ion-item>
467
+ <ion-label color="medium">No actions available</ion-label>
468
+ </ion-item>
469
+ }
470
+ </ion-list>
471
+ `, isInline: true, styles: [":host{display:block}::ng-deep ion-popover::part(content){width:fit-content}ion-list{padding:0}ion-item{--padding-start: var(--maki-action-button-list-padding, 16px);--padding-end: var(--maki-action-button-list-padding, 16px);--min-height: var(--maki-action-button-list-item-height, 44px);cursor:pointer;width:100%}ion-item[disabled]{cursor:not-allowed}ion-label{white-space:nowrap}ion-icon{font-size:var(--maki-action-button-list-icon-size, 20px);margin-inline-end:var(--maki-action-button-list-icon-gap, 12px)}ion-spinner{width:var(--maki-action-button-list-icon-size, 20px);height:var(--maki-action-button-list-icon-size, 20px);margin-inline-end:var(--maki-action-button-list-icon-gap, 12px)}\n"], dependencies: [{ kind: "component", type: IonList, selector: "ion-list", inputs: ["inset", "lines", "mode"] }, { kind: "component", type: IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
472
+ }
473
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ActionButtonListComponent, decorators: [{
474
+ type: Component,
475
+ args: [{ selector: 'maki-action-button-list', standalone: true, imports: [IonList, IonItem, IonIcon, IonLabel, IonSpinner], changeDetection: ChangeDetectionStrategy.OnPush, template: `
476
+ <ion-list lines="none">
477
+ @for (button of buttonList(); track button.id) {
478
+ <ion-item
479
+ [button]="true"
480
+ [disabled]="isButtonLoading(button) || button.config?.disabled"
481
+ [detail]="button.type === dropdownType"
482
+ (click)="onButtonClick(button)"
483
+ >
484
+ @if (isButtonLoading(button) && (button.showLoadingSpinner ?? true)) {
485
+ <ion-spinner slot="start" name="crescent" />
486
+ } @else if (button.icon) {
487
+ <ion-icon slot="start" [name]="button.icon" />
488
+ }
489
+ @if (button.label) {
490
+ <ion-label>{{ button.label }}</ion-label>
491
+ }
492
+ </ion-item>
493
+ } @empty {
494
+ <ion-item>
495
+ <ion-label color="medium">No actions available</ion-label>
496
+ </ion-item>
497
+ }
498
+ </ion-list>
499
+ `, styles: [":host{display:block}::ng-deep ion-popover::part(content){width:fit-content}ion-list{padding:0}ion-item{--padding-start: var(--maki-action-button-list-padding, 16px);--padding-end: var(--maki-action-button-list-padding, 16px);--min-height: var(--maki-action-button-list-item-height, 44px);cursor:pointer;width:100%}ion-item[disabled]{cursor:not-allowed}ion-label{white-space:nowrap}ion-icon{font-size:var(--maki-action-button-list-icon-size, 20px);margin-inline-end:var(--maki-action-button-list-icon-gap, 12px)}ion-spinner{width:var(--maki-action-button-list-icon-size, 20px);height:var(--maki-action-button-list-icon-size, 20px);margin-inline-end:var(--maki-action-button-list-icon-gap, 12px)}\n"] }]
500
+ }], ctorParameters: () => [], propDecorators: { buttons: [{ type: i0.Input, args: [{ isSignal: true, alias: "buttons", required: false }] }], buttonSelect: [{ type: i0.Output, args: ["buttonSelect"] }] } });
501
+
502
+ /**
503
+ * @file Button Component
504
+ * @description Configurable button component that renders ion-button based on ActionButton configuration
505
+ */
506
+ /**
507
+ * A configurable button component that renders an `ion-button` based on
508
+ * an `ActionButton` configuration object.
509
+ *
510
+ * Features:
511
+ * - Three display types: Default (label + icon), IconOnly, and Dropdown
512
+ * - Automatic loading state management for async handlers
513
+ * - Dropdown support via PopoverController with child actions
514
+ * - Full Ionic button styling configuration (fill, size, color, shape, expand)
515
+ * - Automatic chevron icon for dropdown buttons
516
+ *
517
+ * @example
518
+ * ```html
519
+ * <!-- Simple button -->
520
+ * <maki-button [button]="saveButton" />
521
+ *
522
+ * <!-- Icon-only button -->
523
+ * <maki-button [button]="iconButton" />
524
+ *
525
+ * <!-- Dropdown button -->
526
+ * <maki-button [button]="menuButton" />
527
+ * ```
528
+ *
529
+ * @example
530
+ * ```typescript
531
+ * // In your component
532
+ * saveButton: ActionButton = {
533
+ * id: 'save',
534
+ * label: 'Save',
535
+ * icon: 'saveOutline',
536
+ * type: ActionButtonType.Default,
537
+ * config: { fill: 'solid', color: 'primary' },
538
+ * handler: async () => {
539
+ * await this.saveData();
540
+ * }
541
+ * };
542
+ *
543
+ * menuButton: ActionButton = {
544
+ * id: 'menu',
545
+ * label: 'Actions',
546
+ * type: ActionButtonType.Dropdown,
547
+ * handler: () => {},
548
+ * children: [
549
+ * { id: 'edit', label: 'Edit', icon: 'createOutline', type: ActionButtonType.Default, handler: () => this.edit() },
550
+ * { id: 'delete', label: 'Delete', icon: 'trashOutline', type: ActionButtonType.Default, handler: () => this.delete() }
551
+ * ]
552
+ * };
553
+ * ```
554
+ *
555
+ * @usageNotes
556
+ * ### Inputs
557
+ * - `button` (required): The `ActionButton` configuration object
558
+ *
559
+ * ### Outputs
560
+ * - `buttonClick`: Emits the button configuration when clicked (for non-dropdown buttons)
561
+ * - `childSelect`: Emits the selected child button (for dropdown buttons)
562
+ *
563
+ * ### Loading State
564
+ * The component automatically manages loading state for async handlers.
565
+ * When a handler returns a Promise, the button shows a spinner until it resolves/rejects.
566
+ * You can also manually control loading via the `button.loading` property.
567
+ *
568
+ * ### Dropdown Behavior
569
+ * For `ActionButtonType.Dropdown`, clicking the button opens an `ion-popover`
570
+ * containing an `ActionButtonListComponent` with the child buttons.
571
+ * When a child is selected, its handler is executed and `childSelect` is emitted.
572
+ */
573
+ class ButtonComponent {
574
+ /**
575
+ * Popover controller for creating dropdown popovers.
576
+ */
577
+ popoverCtrl = inject(PopoverController);
578
+ /**
579
+ * Internal loading state for async handler execution.
580
+ */
581
+ _isLoading = signal(false, ...(ngDevMode ? [{ debugName: "_isLoading" }] : []));
582
+ /**
583
+ * Set of child button IDs that are currently loading (for dropdowns).
584
+ * Tracks multiple concurrent loading operations.
585
+ */
586
+ _loadingChildIds = signal(new Set(), ...(ngDevMode ? [{ debugName: "_loadingChildIds" }] : []));
587
+ /**
588
+ * The action button configuration.
589
+ */
590
+ button = input.required(...(ngDevMode ? [{ debugName: "button" }] : []));
591
+ /**
592
+ * Emits when the button is clicked (for non-dropdown buttons).
593
+ * Emits the button configuration that was clicked.
594
+ */
595
+ buttonClick = output();
596
+ /**
597
+ * Emits when a child button is selected from a dropdown.
598
+ * Only emits for `ActionButtonType.Dropdown` buttons.
599
+ */
600
+ childSelect = output();
601
+ /**
602
+ * Checks if any child button is in a loading state and has showLoadingSpinner enabled.
603
+ */
604
+ hasLoadingChild = computed(() => {
605
+ const btn = this.button();
606
+ if (btn.type !== ActionButtonType.Dropdown || !btn.children) {
607
+ return false;
608
+ }
609
+ const loadingChildIds = this._loadingChildIds();
610
+ return btn.children.some((child) => {
611
+ const isLoadingFromProp = child.loading && (child.showLoadingSpinner ?? true);
612
+ const isLoadingFromExecution = loadingChildIds.has(child.id) && (child.showLoadingSpinner ?? true);
613
+ return isLoadingFromProp || isLoadingFromExecution;
614
+ });
615
+ }, ...(ngDevMode ? [{ debugName: "hasLoadingChild" }] : []));
616
+ /**
617
+ * Combined loading state from external prop, internal async state, and child loading.
618
+ * Returns true if:
619
+ * - Internal loading state is active, OR
620
+ * - External loading prop is true, OR
621
+ * - Any child is loading AND parent has showLoadingSpinner !== false
622
+ */
623
+ isLoading = computed(() => {
624
+ const btn = this.button();
625
+ const parentLoading = this._isLoading() || (btn.loading ?? false);
626
+ const childLoading = this.hasLoadingChild() && (btn.showLoadingSpinner ?? true);
627
+ return parentLoading || childLoading;
628
+ }, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
629
+ /**
630
+ * Whether to display the loading spinner.
631
+ * Shows spinner only if button is loading AND showLoadingSpinner is not false.
632
+ */
633
+ showLoadingSpinner = computed(() => {
634
+ return this.isLoading() && (this.button().showLoadingSpinner ?? true);
635
+ }, ...(ngDevMode ? [{ debugName: "showLoadingSpinner" }] : []));
636
+ /**
637
+ * Whether the button is disabled.
638
+ * - Non-dropdown buttons: Disabled when loading or explicitly disabled in config
639
+ * - Dropdown buttons: Only disabled when explicitly disabled in config or when parent itself is loading
640
+ * (not disabled by inherited child loading - user can still open dropdown to see loading children)
641
+ */
642
+ isDisabled = computed(() => {
643
+ const btn = this.button();
644
+ const configDisabled = btn.config?.disabled ?? false;
645
+ // For dropdown buttons, only disable if config says so or if parent itself is loading
646
+ // Don't disable for inherited child loading (user should be able to open dropdown)
647
+ if (btn.type === ActionButtonType.Dropdown) {
648
+ const parentSelfLoading = this._isLoading() || (btn.loading ?? false);
649
+ return configDisabled || parentSelfLoading;
650
+ }
651
+ // For non-dropdown buttons, disable when loading
652
+ return this.isLoading() || configDisabled;
653
+ }, ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
654
+ /**
655
+ * Determines the slot for the icon based on button type and label presence.
656
+ */
657
+ iconSlot = computed(() => {
658
+ const btn = this.button();
659
+ if (btn.type === ActionButtonType.IconOnly) {
660
+ return 'icon-only';
661
+ }
662
+ return btn.label ? 'start' : 'icon-only';
663
+ }, ...(ngDevMode ? [{ debugName: "iconSlot" }] : []));
664
+ /**
665
+ * Whether to show the label text.
666
+ */
667
+ showLabel = computed(() => {
668
+ const btn = this.button();
669
+ return btn.type !== ActionButtonType.IconOnly && btn.label;
670
+ }, ...(ngDevMode ? [{ debugName: "showLabel" }] : []));
671
+ /**
672
+ * Whether to show the dropdown chevron icon.
673
+ */
674
+ showDropdownIcon = computed(() => {
675
+ const btn = this.button();
676
+ return btn.type === ActionButtonType.Dropdown && !btn.config?.hideDropdownIcon;
677
+ }, ...(ngDevMode ? [{ debugName: "showDropdownIcon" }] : []));
678
+ constructor() {
679
+ addIcons({ chevronDownOutline });
680
+ }
681
+ /**
682
+ * Handles button click events.
683
+ * For dropdown buttons, opens a popover with child actions.
684
+ * For other buttons, executes the handler with auto-loading management.
685
+ *
686
+ * @param event - The click event
687
+ */
688
+ async onClick(event) {
689
+ const btn = this.button();
690
+ if (this.isDisabled()) {
691
+ return;
692
+ }
693
+ if (btn.type === ActionButtonType.Dropdown) {
694
+ await this.openDropdown(event);
695
+ }
696
+ else {
697
+ await this.executeHandler(btn);
698
+ this.buttonClick.emit(btn);
699
+ }
700
+ }
701
+ /**
702
+ * Opens a dropdown popover with child action buttons.
703
+ *
704
+ * @param event - The triggering click event for popover positioning
705
+ */
706
+ async openDropdown(event) {
707
+ const btn = this.button();
708
+ const children = btn.children ?? [];
709
+ if (children.length === 0) {
710
+ return;
711
+ }
712
+ const popover = await this.popoverCtrl.create({
713
+ component: ActionButtonListComponent,
714
+ componentProps: {
715
+ buttonsFromPopover: children,
716
+ loadingChildIds: this._loadingChildIds,
717
+ },
718
+ event,
719
+ translucent: true,
720
+ dismissOnSelect: true,
721
+ side: 'bottom',
722
+ alignment: 'end',
723
+ arrow: false,
724
+ });
725
+ await popover.present();
726
+ const { data, role } = await popover.onDidDismiss();
727
+ if (role === 'select' && data) {
728
+ // Add child to loading set and execute its handler
729
+ this._loadingChildIds.update((ids) => new Set([...ids, data.id]));
730
+ try {
731
+ const result = data.handler();
732
+ if (result instanceof Promise) {
733
+ await result;
734
+ }
735
+ }
736
+ finally {
737
+ // Remove child from loading set
738
+ this._loadingChildIds.update((ids) => {
739
+ const newIds = new Set(ids);
740
+ newIds.delete(data.id);
741
+ return newIds;
742
+ });
743
+ }
744
+ this.childSelect.emit(data);
745
+ }
746
+ }
747
+ /**
748
+ * Executes a button's handler with automatic loading state management.
749
+ *
750
+ * @param btn - The button whose handler to execute
751
+ */
752
+ async executeHandler(btn) {
753
+ const result = btn.handler();
754
+ if (result instanceof Promise) {
755
+ this._isLoading.set(true);
756
+ try {
757
+ await result;
758
+ }
759
+ finally {
760
+ this._isLoading.set(false);
761
+ }
762
+ }
763
+ }
764
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
765
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: ButtonComponent, isStandalone: true, selector: "maki-button", inputs: { button: { classPropertyName: "button", publicName: "button", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { buttonClick: "buttonClick", childSelect: "childSelect" }, ngImport: i0, template: `
766
+ <ion-button
767
+ [fill]="button().config?.fill"
768
+ [size]="button().config?.size"
769
+ [color]="button().config?.color"
770
+ [shape]="button().config?.shape"
771
+ [expand]="button().config?.expand"
772
+ [strong]="button().config?.strong"
773
+ [disabled]="isDisabled()"
774
+ [attr.aria-label]="button().ariaLabel"
775
+ [title]="button().tooltip ?? ''"
776
+ (click)="onClick($event)"
777
+ >
778
+ @if (showLoadingSpinner()) {
779
+ <ion-spinner [slot]="iconSlot()" name="crescent" />
780
+ } @else if (button().icon) {
781
+ <ion-icon [name]="button().icon" [slot]="iconSlot()" class="button-icon" />
782
+ }
783
+ @if (showLabel()) {
784
+ {{ button().label }}
785
+ }
786
+ @if (showDropdownIcon()) {
787
+ <ion-icon name="chevron-down-outline" slot="end" class="dropdown-icon" />
788
+ }
789
+ </ion-button>
790
+ `, isInline: true, styles: [":host{display:inline-block}ion-button{--padding-start: var(--maki-button-padding-start);--padding-end: var(--maki-button-padding-end)}ion-spinner{width:var(--maki-button-spinner-size, 16px);height:var(--maki-button-spinner-size, 16px);margin:0 4px 0 0}.button-icon{font-size:var(--maki-button-icon-size, 16px);margin:0 4px 0 0}.dropdown-icon{font-size:var(--maki-button-dropdown-icon-size, 16px);margin-inline-start:var(--maki-button-dropdown-icon-gap, 4px)}\n"], dependencies: [{ kind: "component", type: IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
791
+ }
792
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ButtonComponent, decorators: [{
793
+ type: Component,
794
+ args: [{ selector: 'maki-button', standalone: true, imports: [IonButton, IonIcon, IonSpinner], changeDetection: ChangeDetectionStrategy.OnPush, template: `
795
+ <ion-button
796
+ [fill]="button().config?.fill"
797
+ [size]="button().config?.size"
798
+ [color]="button().config?.color"
799
+ [shape]="button().config?.shape"
800
+ [expand]="button().config?.expand"
801
+ [strong]="button().config?.strong"
802
+ [disabled]="isDisabled()"
803
+ [attr.aria-label]="button().ariaLabel"
804
+ [title]="button().tooltip ?? ''"
805
+ (click)="onClick($event)"
806
+ >
807
+ @if (showLoadingSpinner()) {
808
+ <ion-spinner [slot]="iconSlot()" name="crescent" />
809
+ } @else if (button().icon) {
810
+ <ion-icon [name]="button().icon" [slot]="iconSlot()" class="button-icon" />
811
+ }
812
+ @if (showLabel()) {
813
+ {{ button().label }}
814
+ }
815
+ @if (showDropdownIcon()) {
816
+ <ion-icon name="chevron-down-outline" slot="end" class="dropdown-icon" />
817
+ }
818
+ </ion-button>
819
+ `, styles: [":host{display:inline-block}ion-button{--padding-start: var(--maki-button-padding-start);--padding-end: var(--maki-button-padding-end)}ion-spinner{width:var(--maki-button-spinner-size, 16px);height:var(--maki-button-spinner-size, 16px);margin:0 4px 0 0}.button-icon{font-size:var(--maki-button-icon-size, 16px);margin:0 4px 0 0}.dropdown-icon{font-size:var(--maki-button-dropdown-icon-size, 16px);margin-inline-start:var(--maki-button-dropdown-icon-gap, 4px)}\n"] }]
820
+ }], ctorParameters: () => [], propDecorators: { button: [{ type: i0.Input, args: [{ isSignal: true, alias: "button", required: true }] }], buttonClick: [{ type: i0.Output, args: ["buttonClick"] }], childSelect: [{ type: i0.Output, args: ["childSelect"] }] } });
821
+
330
822
  /*
331
823
  * Public API Surface of @makigamestudio/ui-core
332
824
  */
@@ -336,5 +828,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
336
828
  * Generated bundle index. Do not edit.
337
829
  */
338
830
 
339
- export { TestCardComponent, TestClass, TestService };
831
+ export { ActionButtonListComponent, ActionButtonType, ButtonComponent, TestCardComponent, TestClass, TestService };
340
832
  //# sourceMappingURL=makigamestudio-ui-core.mjs.map