@rdlabo/ionic-angular-kit 0.0.3 → 0.0.4

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,53 +0,0 @@
1
- import type { EnvironmentProviders } from '@angular/core';
2
- import { InjectionToken, makeEnvironmentProviders } from '@angular/core';
3
-
4
- /**
5
- * User-visible button labels consumed by `KitOverlayController`.
6
- *
7
- * @remarks
8
- * The kit deliberately ships no i18n strings of its own and has no hard dependency on
9
- * `@angular/localize`. The consuming application always provides these labels: multilingual apps
10
- * pass `$localize`-resolved strings, single-language apps pass plain literals.
11
- */
12
- export interface KitLabels {
13
- /** Text for the "close" button used by alerts and toasts. */
14
- readonly close: string;
15
- /** Text for the "cancel" button used by confirmation alerts. */
16
- readonly cancel: string;
17
- }
18
-
19
- /**
20
- * Overlay configuration injected via `provideKitOverlay()`.
21
- *
22
- * @remarks
23
- * All fields are required; the consuming application must supply a complete configuration.
24
- */
25
- export interface KitOverlayConfig {
26
- /** Button labels used across the overlays presented by `KitOverlayController`. */
27
- readonly labels: KitLabels;
28
- }
29
-
30
- /**
31
- * Injection token carrying the {@link KitOverlayConfig} for `KitOverlayController`.
32
- *
33
- * @remarks
34
- * Provide it through {@link provideKitOverlay} rather than registering it directly.
35
- */
36
- export const KIT_OVERLAY_CONFIG = new InjectionToken<KitOverlayConfig>('@rdlabo/ionic-angular-kit:overlay');
37
-
38
- /**
39
- * Wire `KitOverlayController` into the application by providing its button labels.
40
- *
41
- * @param config - overlay configuration, including the button labels to inject
42
- * @returns environment providers to add to the application's provider list
43
- * @example
44
- * ```ts
45
- * bootstrapApplication(AppComponent, {
46
- * providers: [
47
- * provideKitOverlay({ labels: { close: $localize`Close`, cancel: $localize`Cancel` } }),
48
- * ],
49
- * });
50
- * ```
51
- */
52
- export const provideKitOverlay = (config: KitOverlayConfig): EnvironmentProviders =>
53
- makeEnvironmentProviders([{ provide: KIT_OVERLAY_CONFIG, useValue: config }]);
@@ -1,127 +0,0 @@
1
- import { provideZonelessChangeDetection } from '@angular/core';
2
- import { TestBed } from '@angular/core/testing';
3
-
4
- import { KitStorageService } from './kit-storage.service';
5
-
6
- // ---------------------------------------------------------------------------
7
- // Fake storage engine (in-memory Map)
8
- // ---------------------------------------------------------------------------
9
- class FakeStorageEngine {
10
- private readonly store = new Map<string, unknown>();
11
- async set(key: string, value: unknown): Promise<void> {
12
- this.store.set(key, value);
13
- }
14
- async get(key: string): Promise<unknown | null> {
15
- return this.store.has(key) ? this.store.get(key) : null;
16
- }
17
- async remove(key: string): Promise<void> {
18
- this.store.delete(key);
19
- }
20
- async clear(): Promise<void> {
21
- this.store.clear();
22
- }
23
- async keys(): Promise<string[]> {
24
- return [...this.store.keys()];
25
- }
26
- }
27
-
28
- // Fake @ionic/storage-angular Storage class — create() resolves to FakeStorageEngine.
29
- // We do NOT vi.mock the module; instead we provide this value directly as the DI token
30
- // to avoid ESM/transform issues with the real package.
31
- class FakeStorage {
32
- create() {
33
- return Promise.resolve(new FakeStorageEngine());
34
- }
35
- }
36
-
37
- // ---------------------------------------------------------------------------
38
- // Tests
39
- // ---------------------------------------------------------------------------
40
- describe('KitStorageService', () => {
41
- let service: KitStorageService;
42
-
43
- // We need the real Storage class as the DI token. Import it lazily to
44
- // avoid hard-wiring an ESM import at module-top level where Vitest might
45
- // not have transformed it yet. Using vi.mock avoids the issue entirely.
46
- beforeEach(async () => {
47
- // Dynamically import so the vi.mock below (hoisted) takes effect first.
48
- const { Storage } = await import('@ionic/storage-angular');
49
-
50
- TestBed.configureTestingModule({
51
- providers: [provideZonelessChangeDetection(), KitStorageService, { provide: Storage, useValue: new FakeStorage() }],
52
- });
53
- service = TestBed.inject(KitStorageService);
54
- });
55
-
56
- it('set → get round-trip for string', async () => {
57
- await service.set<string>('key', 'hello');
58
- const result = await service.get<string>('key');
59
- expect(result).toBe('hello');
60
- });
61
-
62
- it('set → get round-trip for object', async () => {
63
- const obj = { a: 1, b: true };
64
- await service.set('obj', obj);
65
- const result = await service.get<typeof obj>('obj');
66
- expect(result).toEqual(obj);
67
- });
68
-
69
- it('get returns null for a missing key', async () => {
70
- const result = await service.get<string>('does-not-exist');
71
- expect(result).toBeNull();
72
- });
73
-
74
- it('remove deletes the key so get returns null', async () => {
75
- await service.set('x', 42);
76
- await service.remove('x');
77
- const result = await service.get<number>('x');
78
- expect(result).toBeNull();
79
- });
80
-
81
- it('clear removes all keys', async () => {
82
- await service.set('a', 1);
83
- await service.set('b', 2);
84
- await service.clear();
85
- expect(await service.get('a')).toBeNull();
86
- expect(await service.get('b')).toBeNull();
87
- expect(await service.keys()).toEqual([]);
88
- });
89
-
90
- it('keys returns every stored key', async () => {
91
- await service.set('p', 1);
92
- await service.set('q', 2);
93
- const result = await service.keys();
94
- expect(result).toContain('p');
95
- expect(result).toContain('q');
96
- expect(result).toHaveLength(2);
97
- });
98
-
99
- it('set called without separate init is not lost (readiness guarantee)', async () => {
100
- // Create a fresh service in a fresh TestBed for this isolation test.
101
- // The key contract: the service never requires the caller to separately
102
- // "await storage.create()" before using it — #ready handles that internally.
103
- const { Storage } = await import('@ionic/storage-angular');
104
- let resolveCreate!: (engine: FakeStorageEngine) => void;
105
- const engine = new FakeStorageEngine();
106
- const delayedStorage = {
107
- // create() returns a promise that we control manually
108
- create: () => new Promise<FakeStorageEngine>((r) => (resolveCreate = r)),
109
- };
110
-
111
- TestBed.resetTestingModule();
112
- TestBed.configureTestingModule({
113
- providers: [provideZonelessChangeDetection(), KitStorageService, { provide: Storage, useValue: delayedStorage }],
114
- });
115
- const svc = TestBed.inject(KitStorageService);
116
-
117
- // Issue a set before storage is ready
118
- const setPromise = svc.set('early', 'value');
119
- // Now resolve the storage creation
120
- resolveCreate(engine);
121
- await setPromise;
122
-
123
- // Verify the set was not lost
124
- const result = await svc.get<string>('early');
125
- expect(result).toBe('value');
126
- });
127
- });
@@ -1,91 +0,0 @@
1
- import { inject, Injectable } from '@angular/core';
2
- import { Storage } from '@ionic/storage-angular';
3
-
4
- /**
5
- * Thin, typed wrapper around `@ionic/storage-angular`.
6
- *
7
- * Starts `create()` exactly once and stores the resulting ready Promise. Every operation awaits
8
- * that Promise before touching the underlying store, so calls made before initialization completes
9
- * are queued rather than dropped.
10
- *
11
- * @remarks
12
- * A naive wrapper that reads the store synchronously would silently no-op (or throw) when invoked
13
- * before `create()` resolves, losing early writes. Awaiting the one-time ready Promise on every
14
- * operation removes that race without forcing callers to coordinate initialization themselves.
15
- *
16
- * @example
17
- * ```ts
18
- * constructor(private readonly storage: KitStorageService) {}
19
- *
20
- * async ngOnInit(): Promise<void> {
21
- * await this.storage.set('token', 'abc123');
22
- * const token = await this.storage.get<string>('token');
23
- * }
24
- * ```
25
- */
26
- @Injectable({
27
- providedIn: 'root',
28
- })
29
- export class KitStorageService {
30
- /** One-time `create()` ready Promise; awaited before every operation so early calls are not lost. */
31
- readonly #ready: Promise<Storage> = inject(Storage).create();
32
-
33
- /**
34
- * Persist a value under the given key.
35
- *
36
- * @typeParam T - type of the value being stored
37
- * @param key - key to store the value under
38
- * @param value - value to persist; overwrites any existing value for the key
39
- * @returns a Promise that resolves once the value has been written
40
- * @example
41
- * ```ts
42
- * await storage.set('user', { id: 1, name: 'Ada' });
43
- * ```
44
- */
45
- async set<T>(key: string, value: T): Promise<void> {
46
- await (await this.#ready).set(key, value);
47
- }
48
-
49
- /**
50
- * Read the value stored under the given key.
51
- *
52
- * @typeParam T - expected type of the stored value
53
- * @param key - key to read
54
- * @returns the stored value, or `null` when the key is absent
55
- * @example
56
- * ```ts
57
- * const user = await storage.get<{ id: number }>('user');
58
- * ```
59
- */
60
- async get<T>(key: string): Promise<T | null> {
61
- return (await (await this.#ready).get(key)) ?? null;
62
- }
63
-
64
- /**
65
- * Remove the value stored under the given key.
66
- *
67
- * @param key - key to remove; a no-op when the key is absent
68
- * @returns a Promise that resolves once the key has been removed
69
- */
70
- async remove(key: string): Promise<void> {
71
- await (await this.#ready).remove(key);
72
- }
73
-
74
- /**
75
- * Remove every key/value pair from the store.
76
- *
77
- * @returns a Promise that resolves once the store has been emptied
78
- */
79
- async clear(): Promise<void> {
80
- await (await this.#ready).clear();
81
- }
82
-
83
- /**
84
- * List every key currently present in the store.
85
- *
86
- * @returns an array of all stored keys
87
- */
88
- async keys(): Promise<string[]> {
89
- return (await this.#ready).keys();
90
- }
91
- }
@@ -1,33 +0,0 @@
1
- import { arrayConcatById } from './array';
2
-
3
- interface Row {
4
- id: number;
5
- label: string;
6
- }
7
-
8
- describe('arrayConcatById', () => {
9
- it('returns [] when both inputs are empty', () => {
10
- expect(arrayConcatById<Row>([], [], 'id')).toEqual([]);
11
- });
12
-
13
- it('merges new rows over old by key and sorts DESC by default', () => {
14
- const old: Row[] = [
15
- { id: 3, label: 'old-3' },
16
- { id: 2, label: 'old-2' },
17
- { id: 1, label: 'old-1' },
18
- ];
19
- const fresh: Row[] = [
20
- { id: 3, label: 'new-3' },
21
- { id: 2, label: 'new-2' },
22
- ];
23
- const result = arrayConcatById<Row>(old, fresh, 'id');
24
- expect(result.map((r) => r.id)).toEqual([3, 2, 1]);
25
- expect(result.find((r) => r.id === 3)?.label).toBe('new-3');
26
- expect(result.find((r) => r.id === 1)?.label).toBe('old-1');
27
- });
28
-
29
- it('sorts ASC when requested', () => {
30
- const result = arrayConcatById<Row>([{ id: 1, label: 'a' }], [{ id: 2, label: 'b' }], 'id', 'ASC');
31
- expect(result.map((r) => r.id)).toEqual([1, 2]);
32
- });
33
- });
@@ -1,82 +0,0 @@
1
- /**
2
- * Merge a newly fetched page of items into an existing list by id, keeping the result sorted.
3
- *
4
- * Items are merged by the numeric `key` field. On a duplicate `key` the item from `arrayNew` wins
5
- * and replaces the one in `arrayOld`. Old items whose `key` falls *inside* the window spanned by
6
- * `arrayNew` (between its first and last `key`) are dropped, since the freshly fetched page is the
7
- * authoritative copy of that window; old items *outside* the window are kept. The merged items are
8
- * finally sorted by `key`, ascending or descending according to `order`.
9
- *
10
- * The algorithm is:
11
- * 1. If both inputs are empty, return an empty array.
12
- * 2. Read the first (`lead`) and last (`last`) `key` values of `arrayNew` as the bounds of the
13
- * page's window. Keep the old items whose `key` lies *outside* that window (at or beyond the
14
- * extremes) and drop those strictly inside it, handling both a high-to-low page (`lead > last`)
15
- * and a low-to-high page (`lead < last`). A single-value page (`lead === last`) keeps all old items.
16
- * 3. Remove old items whose `key` already exists in `arrayNew` (new wins on duplicates).
17
- * 4. Concatenate `arrayNew` with the surviving old items and sort by `key` in the requested
18
- * direction.
19
- *
20
- * @remarks
21
- * Designed for infinite-scroll / paginated list merging, where each fetched page may overlap the
22
- * previously held items and the server returns a contiguous, ordered window of records. The `key`
23
- * field is treated as a number for range comparison and sorting.
24
- *
25
- * @typeParam T - Element type of both arrays. Its `key` property must be a numeric value.
26
- * @param arrayOld - The previously accumulated list. May be empty or nullish-safe (empty when falsy).
27
- * @param arrayNew - The newly fetched page of items; its `key` range defines the window that its own
28
- * items replace, and its items take precedence on duplicates.
29
- * @param key - The property of `T` used as the unique, numeric id for matching, range filtering, and sorting.
30
- * @param order - Sort direction by `key`: `'ASC'` for ascending or `'DESC'` for descending. Defaults to `'DESC'`.
31
- * @returns A new array containing `arrayNew` merged with the out-of-window, non-duplicate old items, sorted by `key`.
32
- * @example
33
- * ```ts
34
- * interface Post {
35
- * id: number;
36
- * title: string;
37
- * }
38
- *
39
- * const loaded: Post[] = [
40
- * { id: 30, title: 'c' },
41
- * { id: 20, title: 'b' },
42
- * { id: 10, title: 'a' },
43
- * ];
44
- * const nextPage: Post[] = [
45
- * { id: 20, title: 'b (updated)' },
46
- * { id: 15, title: 'a.5' },
47
- * ];
48
- *
49
- * // Descending merge: id 30 lies above the new page's [15, 20] window so it is kept; id 20 is
50
- * // inside the window and is replaced by the new value; id 10 lies below the window and is kept.
51
- * const merged = arrayConcatById(loaded, nextPage, 'id', 'DESC');
52
- * // => [{ id: 30, title: 'c' }, { id: 20, title: 'b (updated)' }, { id: 15, title: 'a.5' }, { id: 10, title: 'a' }]
53
- * ```
54
- */
55
- export const arrayConcatById = <T>(arrayOld: T[], arrayNew: T[], key: keyof T, order: 'ASC' | 'DESC' = 'DESC'): T[] => {
56
- if (!arrayNew.length && !arrayOld.length) {
57
- return [];
58
- }
59
- const lead = arrayNew[0][key] as number;
60
- const last = arrayNew[arrayNew.length - 1][key] as number;
61
-
62
- const filteredOld = (arrayOld || []).filter((vol) => {
63
- const value = vol[key] as number;
64
- return (lead > last && (value >= lead || value <= last)) || (lead < last && (value <= lead || value >= last)) || lead === last;
65
- });
66
-
67
- const oldData = filteredOld.filter((vol) => !arrayNew.some((element) => element[key] === vol[key]));
68
- const data = arrayNew.concat(oldData);
69
-
70
- const direction = order === 'ASC' ? 1 : -1;
71
- return data.sort((a, b) => {
72
- const x = a[key] as number;
73
- const y = b[key] as number;
74
- if (x > y) {
75
- return direction;
76
- }
77
- if (x < y) {
78
- return direction * -1;
79
- }
80
- return 0;
81
- });
82
- };
@@ -1,32 +0,0 @@
1
- import { Capacitor } from '@capacitor/core';
2
- import { Haptics, ImpactStyle } from '@capacitor/haptics';
3
-
4
- /**
5
- * Trigger native haptic impact feedback.
6
- *
7
- * Delegates to `@capacitor/haptics` `Haptics.impact()` when running on a native platform. On the
8
- * web (or any non-native platform) it is a no-op and resolves without doing anything.
9
- *
10
- * @remarks
11
- * This haptic side effect was previously bundled implicitly into toast/modal presentation. It has
12
- * been decoupled into this explicit function so callers opt in to the feedback deliberately, rather
13
- * than receiving it as a hidden side effect of presenting UI.
14
- *
15
- * @param style - The impact intensity to play. Defaults to {@link ImpactStyle.Light}.
16
- * @returns A promise that resolves once the feedback has been requested (immediately on the web).
17
- * @example
18
- * ```ts
19
- * import { ImpactStyle } from '@capacitor/haptics';
20
- *
21
- * // Light tap (default)
22
- * await kitImpact();
23
- *
24
- * // Stronger feedback, e.g. on a confirming action
25
- * await kitImpact(ImpactStyle.Heavy);
26
- * ```
27
- */
28
- export const kitImpact = async (style: ImpactStyle = ImpactStyle.Light): Promise<void> => {
29
- if (Capacitor.isNativePlatform()) {
30
- await Haptics.impact({ style });
31
- }
32
- };
package/src/public-api.ts DELETED
@@ -1,24 +0,0 @@
1
- /*
2
- * Public API Surface of @rdlabo/ionic-angular-kit
3
- */
4
-
5
- // Storage: typed wrapper around the platform key/value store.
6
- export * from './lib/storage/kit-storage.service';
7
-
8
- // Overlay: wrapper around the Ionic Modal / Toast / Alert controllers.
9
- export * from './lib/overlay/overlay-config';
10
- export * from './lib/overlay/kit-overlay.controller';
11
- export * from './lib/overlay/kit-reload-alert.controller';
12
-
13
- // Directives.
14
- export * from './lib/directives/autofill.directive';
15
-
16
- // Auth: functional route guards.
17
- export * from './lib/auth/auth-guards';
18
-
19
- // HTTP: functional interceptor.
20
- export * from './lib/http/kit-http.interceptor';
21
-
22
- // Utils: framework-agnostic pure helpers.
23
- export * from './lib/utils/haptics';
24
- export * from './lib/utils/array';
package/tsconfig.lib.json DELETED
@@ -1,14 +0,0 @@
1
- /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
2
- /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
3
- {
4
- "extends": "../../tsconfig.json",
5
- "compilerOptions": {
6
- "outDir": "../../out-tsc/lib",
7
- "declaration": true,
8
- "declarationMap": true,
9
- "inlineSources": true,
10
- "types": []
11
- },
12
- "include": ["src/**/*.ts"],
13
- "exclude": ["**/*.spec.ts"]
14
- }
@@ -1,11 +0,0 @@
1
- /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
2
- /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
3
- {
4
- "extends": "./tsconfig.lib.json",
5
- "compilerOptions": {
6
- "declarationMap": false
7
- },
8
- "angularCompilerOptions": {
9
- "compilationMode": "partial"
10
- }
11
- }
@@ -1,10 +0,0 @@
1
- /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
2
- /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
3
- {
4
- "extends": "../../tsconfig.json",
5
- "compilerOptions": {
6
- "outDir": "../../out-tsc/spec",
7
- "types": ["vitest/globals"]
8
- },
9
- "include": ["src/**/*.d.ts", "src/**/*.spec.ts"]
10
- }