@rdlabo/ionic-angular-kit 0.0.12 → 0.0.14

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
@@ -26,10 +26,18 @@ npm install @rdlabo/ionic-angular-kit
26
26
  | `@ionic/angular` | `^8.0.0` |
27
27
  | `@ionic/storage-angular` | `^4.0.0` |
28
28
  | `@capacitor/core` | `>=6.0.0 <9.0.0` |
29
+ | `@capacitor/haptics` | `>=6.0.0 <9.0.0` |
29
30
  | `@capacitor/keyboard` | `>=6.0.0 <9.0.0` |
30
31
  | `@capacitor/network` | `>=6.0.0 <9.0.0` |
32
+ | `@capacitor/preferences` | `>=6.0.0 <9.0.0` |
33
+ | `@capacitor/status-bar` | `>=6.0.0 <9.0.0` |
34
+ | `@capacitor-community/in-app-review` | `>=6.0.0 <9.0.0` |
35
+ | `@rdlabo/capacitor-brotherprint` | `>=6.0.0 <9.0.0` |
36
+ | `dom-to-image-more` | `^3.0.0` |
31
37
  | `rxjs` | `^7.8.0` |
32
38
 
39
+ Feature-scoped peers are only needed by the features that use them (`status-bar` → `KitThemeController`; `preferences` + `in-app-review` → `kitRequestReview`; `capacitor-brotherprint` + `dom-to-image-more` → the printer helpers); an app that doesn't use a feature can ignore its unmet-peer warning.
40
+
33
41
  ---
34
42
 
35
43
  ## Features
@@ -119,8 +127,8 @@ export class MyPage {
119
127
  readonly #overlay = inject(KitOverlayController);
120
128
 
121
129
  async openDetail(): Promise<void> {
122
- const result = await this.#overlay.presentModal<{ id: number }>(DetailPage, { item });
123
- // result is the data passed to modal.dismiss()
130
+ const result = await this.#overlay.presentModal(DetailPage, { item });
131
+ // result type is inferred from `declare static modalReturn` on DetailPage
124
132
  }
125
133
 
126
134
  async confirm(): Promise<void> {
@@ -141,11 +149,12 @@ export class MyPage {
141
149
  **API**
142
150
 
143
151
  ```typescript
144
- presentModal<O>(
145
- component: ModalOptions['component'],
146
- componentProps?: ModalOptions['componentProps'],
147
- options?: KitModalPresentOptions, // Omit<ModalOptions, 'component'|'componentProps'> + watchKeyboard?
148
- ): Promise<O | undefined>
152
+ presentModal<C extends ModalOptions['component']>(
153
+ component: C,
154
+ ...args: ModalPresentArgs<C>, // props inferred from input() fields; options?: KitModalPresentOptions
155
+ ): Promise<ModalReturnOf<C> | undefined>
156
+ // Props inferred from the component's input() fields (required/optional).
157
+ // Return type inferred from `declare static modalReturn: T` on the component (void if absent).
149
158
 
150
159
  presentPopover<O>(
151
160
  component: PopoverOptions['component'],
@@ -174,9 +183,14 @@ alertConfirm(options: {
174
183
  **Best practice — the modal launcher pattern.** Never call `modalController.create(...)` inline in a component. Instead, each modal/popover page exports a typed launcher next to itself and every call site goes through `KitOverlayController`:
175
184
 
176
185
  ```typescript
177
- // detail.page.ts
178
- export const launchDetailPage = (overlay: KitOverlayController, props: DetailProps): Promise<DetailResult | undefined> =>
179
- overlay.presentModal<DetailResult>(DetailPage, props, { backdropDismiss: false });
186
+ // detail.page.ts — component declares its return type:
187
+ export class DetailPage {
188
+ declare static modalReturn: DetailResult;
189
+ readonly item = input.required<Item>();
190
+ }
191
+
192
+ export const launchDetailPage = (overlay: KitOverlayController, props: { item: Item }): Promise<DetailResult | undefined> =>
193
+ overlay.presentModal(DetailPage, props, { backdropDismiss: false });
180
194
  ```
181
195
 
182
196
  This centralizes presentation options, keeps component props and dismiss data type-safe, and makes every modal discoverable. A well-disciplined app has **zero** inline `controller.create()` calls.
@@ -409,6 +423,43 @@ export class ComposePage {
409
423
 
410
424
  ---
411
425
 
426
+ ### KitThemeController + provideKitTheme
427
+
428
+ Light/dark theme controller that unifies the theme logic that had drifted across the fleet: it persists the user's choice, follows the OS `prefers-color-scheme` until the user overrides it, toggles the configured palette classes, and syncs the native Android status bar. It also fixes a latent leak in one variant where the system-theme listener stayed registered after a manual toggle — `changeTheme()` always detaches the listener first, so a later OS change can't silently flip an app the user pinned.
429
+
430
+ Per-app CSS differences are absorbed by config: `darkClasses` are toggled on when dark, `lightClasses` on when light. The kit ships no class names of its own. Subscribe to `themeSubject` (a `BehaviorSubject`) to reflect the current mode in the UI. It is a controller (not a plain function) because the subject and OS-listener are shared state across the app lifetime.
431
+
432
+ ```typescript
433
+ // app.config.ts
434
+ provideKitTheme({
435
+ storageKey: StorageKeyEnum.theme,
436
+ darkClasses: ['ion-palette-dark', 'a2ui-dark'],
437
+ lightClasses: ['a2ui-light'],
438
+ });
439
+
440
+ // app.component.ts — apply on boot
441
+ inject(KitThemeController).setDefaultThemeMode();
442
+
443
+ // settings page — bind a toggle
444
+ const theme = inject(KitThemeController);
445
+ theme.themeSubject.subscribe((mode) => this.isDark.set(mode === 'dark'));
446
+ theme.changeTheme(true); // force dark, stop following the OS
447
+ ```
448
+
449
+ ---
450
+
451
+ ### kitRequestReview
452
+
453
+ A plain function (no DI — `@capacitor/preferences`, `@capacitor-community/in-app-review` and `Capacitor` are all static) that requests the native in-app review dialog, throttled so the user is prompted at most once per window. A no-op on web. The wait/throttle/record sequence was previously copy-pasted verbatim across the fleet; centralizing it means a single place to tune the prompt cadence. The storage key and throttle window are passed as arguments, so the kit ships no config of its own.
454
+
455
+ ```typescript
456
+ import { kitRequestReview } from '@rdlabo/ionic-angular-kit';
457
+
458
+ await kitRequestReview({ storageKey: StorageEnum.lastRequestRate, throttleMonths: 3 });
459
+ ```
460
+
461
+ ---
462
+
412
463
  ### Utilities
413
464
 
414
465
  Framework-agnostic helpers (no DI required unless noted):
@@ -432,6 +483,61 @@ async onSubmit(event: Event) {
432
483
  }
433
484
  ```
434
485
 
486
+ Ionic-event / lifecycle helpers:
487
+
488
+ ```typescript
489
+ import { kitChangeEventDisabled, kitCreateDidEnter } from '@rdlabo/ionic-angular-kit';
490
+
491
+ // Toggle a signal-held ion-infinite-scroll / ion-refresher's `disabled` (no-op when empty).
492
+ kitChangeEventDisabled(infiniteScrollSignal, true);
493
+
494
+ // Observe an Ionic page's "is entered" state from its lifecycle DOM events (true on didEnter).
495
+ readonly isEntered = toSignal(kitCreateDidEnter(inject(ElementRef)), { initialValue: false });
496
+ ```
497
+
498
+ ---
499
+
500
+ ### kitPresentLanguageActionSheet
501
+
502
+ A plain function (the `ActionSheetController` is passed in — nothing injected) that presents a language picker and, on a new selection, reloads the app at that locale's entry point. Unifies the language-switch flow duplicated across apps: it stashes the current path in `sessionStorage` (to restore after reload), records the chosen locale in `localStorage`, and calls `window.location.replace()` with the app-provided URL. Being a navigation helper, it stays standalone rather than part of a controller. All text, the locale list, and the per-locale URL mapping are injected, so the kit stays free of i18n strings.
503
+
504
+ ```typescript
505
+ import { kitPresentLanguageActionSheet } from '@rdlabo/ionic-angular-kit';
506
+
507
+ await kitPresentLanguageActionSheet(inject(ActionSheetController), {
508
+ header: $localize`言語設定`,
509
+ locales: [{ text: 'English', data: 'en-US' }, { text: '日本語', data: 'ja' }],
510
+ cancelText: $localize`キャンセル`,
511
+ currentLocale: normalizedLocale,
512
+ currentPath: this.#router.url,
513
+ pathnameStorageKey: StorageKeyEnum.pathnameBeforeRedirect,
514
+ buildRedirectUrl: (locale) => location.origin + (localePath[locale.toLowerCase()] ?? '/index.html'),
515
+ enabled: environment.production,
516
+ });
517
+ ```
518
+
519
+ ---
520
+
521
+ ### Printer (Brother label plumbing)
522
+
523
+ Three pure functions (no DI) that extract the i18n-free core of the fleet's Brother label printing, so a device-quirk or print-setting fix lands in every app at once. The UI orchestration — search/channel-selection alerts, loading overlays, and the app-specific paper list — stays in each app, since those diverge (labels, paper options, copies policy).
524
+
525
+ - `kitDomToPng(element, { rotate?, scale? })` — render a DOM element to a base64 PNG with the fleet's device fixes (iOS +2px to avoid bottom clipping, none on Android to avoid a black line; retries up to 10×). The caller presents its own loading UI.
526
+ - `kitRotationImage(base64)` — rotate a base64 image 90° via canvas.
527
+ - `kitBuildBrotherPrintSettings({ modelName, printBase64, label, numberOfCopies, halftoneThreshold })` — assemble the canonical `BRLMPrintOptions` (fit-page, centered, best quality, threshold halftone, standard margins, tape size parsed from the label's `W<w>H<h>` code). Merge `{ port, channelInfo }` from the selected channel before calling `BrotherPrint.printImage()`.
528
+
529
+ ```typescript
530
+ import { kitDomToPng, kitBuildBrotherPrintSettings } from '@rdlabo/ionic-angular-kit';
531
+
532
+ const png = await kitDomToPng(this.preview().nativeElement, { rotate: true });
533
+ const settings = kitBuildBrotherPrintSettings({
534
+ modelName, printBase64: png, label,
535
+ numberOfCopies: printOptions.printNum,
536
+ halftoneThreshold: printOptions.halftoneThreshold,
537
+ });
538
+ await BrotherPrint.printImage({ ...settings, port: channel.port, channelInfo: channel.channelInfo });
539
+ ```
540
+
435
541
  ---
436
542
 
437
543
  ## Consumer Vitest setup notes