KS
Killer-Skills

angular-capacitor-patterns — how to use angular-capacitor-patterns how to use angular-capacitor-patterns, angular-capacitor-patterns tutorial, angular-capacitor-patterns vs ionic, angular-capacitor-patterns install, angular-capacitor-patterns setup guide, what is angular-capacitor-patterns, angular-capacitor-patterns alternative, angular-capacitor-patterns mobile app development

v1.0.0
GitHub

About this Skill

Perfect for Mobile App Agents needing specialized architectural patterns for hybrid mobile apps using Angular Standalone Components and Capacitor 6. angular-capacitor-patterns is a set of specialized architectural patterns for building hybrid mobile applications using Angular, Capacitor, and offline-first strategies.

Features

Implements offline-first synchronization with Supabase
Integrates Capacitor plugins such as Notifications, Storage, and Camera
Configures GSAP animations for gamification
Structures features with NgRx Signals
Optimizes mobile components for touch using Angular Standalone Components

# Core Topics

kizzz kizzz
[0]
[0]
Updated: 3/6/2026

Quality Score

Top 5%
42
Excellent
Based on code quality & docs
Installation
SYS Universal Install (Auto-Detect)
Cursor IDE Windsurf IDE VS Code IDE
> npx killer-skills add kizzz/eleon-client/angular-capacitor-patterns

Agent Capability Analysis

The angular-capacitor-patterns MCP Server by kizzz is an open-source Categories.community integration for Claude and other AI agents, enabling seamless task automation and capability expansion. Optimized for how to use angular-capacitor-patterns, angular-capacitor-patterns tutorial, angular-capacitor-patterns vs ionic.

Ideal Agent Persona

Perfect for Mobile App Agents needing specialized architectural patterns for hybrid mobile apps using Angular Standalone Components and Capacitor 6.

Core Value

Empowers agents to implement offline-first strategies, integrate Capacitor plugins like Notifications and Storage, and configure GSAP animations for gamification, all while structuring features with NgRx Signals and utilizing Supabase for synchronization.

Capabilities Granted for angular-capacitor-patterns MCP Server

Implementing offline-first synchronization with Supabase
Integrating Capacitor plugins for enhanced mobile functionality
Configuring GSAP animations for interactive gamification elements
Structuring mobile app features with NgRx Signals for improved state management

! Prerequisites & Limits

  • Requires Angular Standalone Components and Capacitor 6
  • Offline-first strategies may require additional setup and configuration
  • Supabase integration necessary for offline synchronization
Project
SKILL.md
22.0 KB
.cursorrules
1.2 KB
package.json
240 B
Ready
UTF-8

# Tags

[No tags]
SKILL.md
Readonly

Angular + Capacitor Mobile Patterns

Patrones arquitectónicos especializados para aplicaciones móviles híbridas usando Angular Standalone Components, Capacitor 6, y estrategias offline-first.

Cuándo Usar Esta Skill

  • Crear componentes móviles optimizados para touch
  • Implementar sincronización offline-first con Supabase
  • Integrar plugins de Capacitor (Notifications, Storage, Camera)
  • Configurar animaciones GSAP para gamificación
  • Estructurar features con NgRx Signals

Principios Fundamentales

1. Mobile-First Architecture

Todos los componentes deben:

  • Usar ChangeDetectionStrategy.OnPush obligatorio
  • Implementar gestos touch-friendly (min 44x44px)
  • Considerar keyboard virtual (bottom padding dinámico)
  • Optimizar para 3G/4G (lazy loading agresivo)
typescript
1@Component({ 2 selector: 'app-block-card', 3 standalone: true, 4 changeDetection: ChangeDetectionStrategy.OnPush, 5 host: { 6 '[style.min-height.px]': '88', // 2x touch target 7 '[class.ios]': 'platform.is("ios")', 8 '[class.android]': 'platform.is("android")' 9 } 10}) 11export class BlockCardComponent {}

2. Offline-First Data Flow

┌─────────────────────────────────────────────┐
│ User Action (Complete Block)               │
└───────────────┬─────────────────────────────┘
                │
                ▼
┌───────────────────────────────────────────┐
│ 1. Write to Local SQLite FIRST            │
│    (Instant UI update via signals)        │
└───────────────┬───────────────────────────┘
                │
                ▼
┌───────────────────────────────────────────┐
│ 2. Queue Sync Operation                   │
│    (Background service)                   │
└───────────────┬───────────────────────────┘
                │
                ▼
         ┌──────┴───────┐
         │ Online?      │
         └──┬────────┬──┘
           YES      NO
            │        │
            │        └─> Store in sync_queue
            │            (retry on reconnect)
            ▼
┌───────────────────────────────────────────┐
│ 3. Push to Supabase                       │
│    (Last-write-wins conflict resolution)  │
└───────────────┬───────────────────────────┘
                │
                ▼
┌───────────────────────────────────────────┐
│ 4. Update Local with server response      │
│    (Reconcile timestamps)                 │
└───────────────────────────────────────────┘

3. NgRx Signals State Management

NUNCA usar NgRx Store clásico. Siempre Signals:

typescript
1// ✅ CORRECTO: Signal Store con computeds 2import { signalStore, withState, withComputed, withMethods } from '@ngrx/signals'; 3 4interface BlocksState { 5 blocks: DailyBlock[]; 6 selectedDate: string; 7 loading: boolean; 8} 9 10export const BlocksStore = signalStore( 11 { providedIn: 'root' }, 12 withState<BlocksState>({ 13 blocks: [], 14 selectedDate: new Date().toISOString().split('T')[0], 15 loading: false 16 }), 17 18 withComputed(({ blocks, selectedDate }) => ({ 19 todayBlocks: computed(() => 20 blocks().filter(b => b.date === selectedDate()) 21 ), 22 completedCount: computed(() => 23 blocks().filter(b => b.status === 'completed').length 24 ), 25 completionRate: computed(() => { 26 const total = blocks().length; 27 const completed = blocks().filter(b => b.status === 'completed').length; 28 return total > 0 ? (completed / total) * 100 : 0; 29 }) 30 })), 31 32 withMethods((store, blocksRepo = inject(BlocksRepository)) => ({ 33 async loadBlocks(userId: string, date: string) { 34 patchState(store, { loading: true }); 35 try { 36 const blocks = await blocksRepo.getDailyBlocks(userId, date); 37 patchState(store, { blocks, selectedDate: date, loading: false }); 38 } catch (error) { 39 console.error('Failed to load blocks', error); 40 patchState(store, { loading: false }); 41 } 42 }, 43 44 async completeBlock(blockId: string, note?: string) { 45 // Optimistic update 46 const updatedBlocks = store.blocks().map(b => 47 b.id === blockId 48 ? { ...b, status: 'completed' as const, completion_note: note } 49 : b 50 ); 51 patchState(store, { blocks: updatedBlocks }); 52 53 // Background sync 54 try { 55 await blocksRepo.completeBlock(blockId, note); 56 } catch (error) { 57 // Rollback on failure 58 patchState(store, { blocks: store.blocks() }); 59 throw error; 60 } 61 } 62 })) 63);

Capacitor Integration Patterns

Plugin Initialization

typescript
1// src/app/core/capacitor/capacitor.service.ts 2import { Injectable, inject } from '@angular/core'; 3import { Platform } from '@angular/cdk/platform'; 4import { App } from '@capacitor/app'; 5import { StatusBar, Style } from '@capacitor/status-bar'; 6import { SplashScreen } from '@capacitor/splash-screen'; 7 8@Injectable({ providedIn: 'root' }) 9export class CapacitorService { 10 private platform = inject(Platform); 11 12 async initialize() { 13 if (!this.platform.isBrowser) { 14 await this.setupStatusBar(); 15 await this.setupAppListeners(); 16 await SplashScreen.hide(); 17 } 18 } 19 20 private async setupStatusBar() { 21 if (this.platform.IOS) { 22 await StatusBar.setStyle({ style: Style.Dark }); 23 } else if (this.platform.ANDROID) { 24 await StatusBar.setBackgroundColor({ color: '#1a202c' }); 25 } 26 } 27 28 private async setupAppListeners() { 29 App.addListener('appStateChange', ({ isActive }) => { 30 if (isActive) { 31 // Resume sync cuando app vuelve a foreground 32 inject(SyncService).resumeSync(); 33 } 34 }); 35 36 App.addListener('backButton', ({ canGoBack }) => { 37 if (!canGoBack) { 38 App.exitApp(); 39 } 40 }); 41 } 42}

Local Notifications Pattern

typescript
1// src/app/core/notifications/notification.service.ts 2import { LocalNotifications, ScheduleOptions } from '@capacitor/local-notifications'; 3import { Haptics, ImpactStyle } from '@capacitor/haptics'; 4 5@Injectable({ providedIn: 'root' }) 6export class NotificationService { 7 private hasPermissions = signal(false); 8 9 async init() { 10 const result = await LocalNotifications.requestPermissions(); 11 this.hasPermissions.set(result.display === 'granted'); 12 } 13 14 async scheduleBlockReminder(block: DailyBlock, minutesBefore: number = 5) { 15 if (!this.hasPermissions()) return; 16 17 const reminderTime = new Date(block.start_time); 18 reminderTime.setMinutes(reminderTime.getMinutes() - minutesBefore); 19 20 await LocalNotifications.schedule({ 21 notifications: [{ 22 id: this.generateNotificationId(block.id), 23 title: `Próximo: ${block.name}`, 24 body: `Comienza en ${minutesBefore} minutos`, 25 schedule: { at: reminderTime }, 26 actionTypeId: 'BLOCK_REMINDER', 27 extra: { blockId: block.id, blockName: block.name } 28 }] 29 }); 30 } 31 32 // Cancelar todas las notificaciones de un día específico 33 async cancelDayNotifications(date: string) { 34 const pending = await LocalNotifications.getPending(); 35 const idsToCancel = pending.notifications 36 .filter(n => n.extra?.date === date) 37 .map(n => n.id); 38 39 if (idsToCancel.length > 0) { 40 await LocalNotifications.cancel({ notifications: idsToCancel.map(id => ({ id })) }); 41 } 42 } 43 44 // Haptic feedback para confirmaciones 45 async triggerSuccess() { 46 await Haptics.impact({ style: ImpactStyle.Medium }); 47 } 48 49 async triggerError() { 50 await Haptics.notification({ type: NotificationType.Error }); 51 } 52 53 private generateNotificationId(blockId: string): number { 54 // Convertir UUID a número único (primeros 8 chars en hex) 55 return parseInt(blockId.slice(0, 8), 16); 56 } 57}

PrimeNG Mobile Optimizations

Touch-Optimized Dialog

typescript
1// Wrapper component para dialogs móviles 2@Component({ 3 selector: 'app-mobile-dialog', 4 standalone: true, 5 imports: [DialogModule], 6 template: ` 7 <p-dialog 8 [(visible)]="visible" 9 [modal]="true" 10 [dismissableMask]="true" 11 [blockScroll]="true" 12 [styleClass]="'mobile-dialog'" 13 [position]="position()" 14 [breakpoints]="{ '960px': '90vw', '640px': '100vw' }" 15 > 16 <ng-content></ng-content> 17 </p-dialog> 18 `, 19 styles: [` 20 :host ::ng-deep .mobile-dialog { 21 .p-dialog-content { 22 padding: var(--spacing-lg); 23 max-height: 70vh; 24 overflow-y: auto; 25 -webkit-overflow-scrolling: touch; 26 } 27 28 // iOS safe area 29 @supports (padding: env(safe-area-inset-bottom)) { 30 .p-dialog-footer { 31 padding-bottom: calc(var(--spacing-md) + env(safe-area-inset-bottom)); 32 } 33 } 34 } 35 `] 36}) 37export class MobileDialogComponent { 38 visible = model<boolean>(false); 39 40 private platform = inject(Platform); 41 42 position = computed(() => 43 this.platform.IOS ? 'bottom' : 'center' 44 ); 45}

Infinite Scroll con DataView

typescript
1@Component({ 2 selector: 'app-habit-history', 3 standalone: true, 4 imports: [DataViewModule, ScrollerModule], 5 template: ` 6 <p-dataView 7 [value]="visibleItems()" 8 [layout]="'list'" 9 [lazy]="true" 10 (onLazyLoad)="loadMore($event)" 11 > 12 <ng-template let-item pTemplate="listItem"> 13 <app-habit-card [habit]="item" /> 14 </ng-template> 15 </p-dataView> 16 ` 17}) 18export class HabitHistoryComponent { 19 private habitsStore = inject(HabitsStore); 20 21 visibleItems = computed(() => 22 this.habitsStore.habits().slice(0, this.pageSize() * this.currentPage()) 23 ); 24 25 private pageSize = signal(20); 26 private currentPage = signal(1); 27 28 loadMore(event: any) { 29 this.currentPage.update(p => p + 1); 30 } 31}

GSAP Animation Patterns

Streak Celebration Animation

typescript
1// src/app/shared/animations/streak.animations.ts 2import gsap from 'gsap'; 3 4export class StreakAnimations { 5 static playStreakComplete(element: HTMLElement, days: number) { 6 const tl = gsap.timeline(); 7 8 // Scale + Rotation 9 tl.to(element, { 10 scale: 1.3, 11 rotation: 360, 12 duration: 0.6, 13 ease: 'back.out(2)', 14 onStart: () => { 15 element.classList.add('celebrating'); 16 } 17 }); 18 19 // Bounce back 20 tl.to(element, { 21 scale: 1, 22 duration: 0.3, 23 ease: 'elastic.out(1, 0.5)' 24 }); 25 26 // Milestone confetti 27 if ([7, 30, 100].includes(days)) { 28 this.playConfetti(element.parentElement!); 29 } 30 } 31 32 static playStreakLost(element: HTMLElement) { 33 const tl = gsap.timeline(); 34 35 // Shake violently 36 tl.to(element, { 37 x: -10, 38 duration: 0.05, 39 repeat: 5, 40 yoyo: true 41 }); 42 43 // Fade out and shrink 44 tl.to(element, { 45 opacity: 0, 46 scale: 0.5, 47 duration: 0.4, 48 ease: 'power2.in', 49 onComplete: () => { 50 gsap.set(element, { opacity: 1, scale: 1, x: 0 }); 51 } 52 }); 53 } 54 55 private static playConfetti(container: HTMLElement) { 56 // Crear partículas 57 const colors = ['#6366f1', '#10b981', '#f59e0b', '#ef4444']; 58 const particleCount = 30; 59 60 for (let i = 0; i < particleCount; i++) { 61 const particle = document.createElement('div'); 62 particle.className = 'confetti-particle'; 63 particle.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)]; 64 container.appendChild(particle); 65 66 gsap.fromTo(particle, 67 { 68 x: 0, 69 y: 0, 70 opacity: 1, 71 scale: 1 72 }, 73 { 74 x: (Math.random() - 0.5) * 300, 75 y: -200 + Math.random() * 100, 76 opacity: 0, 77 scale: 0, 78 duration: 1.5, 79 ease: 'power2.out', 80 onComplete: () => particle.remove() 81 } 82 ); 83 } 84 } 85}

Page Transition Animation

typescript
1// src/app/core/animations/route-animations.ts 2import { trigger, transition, style, animate, query } from '@angular/animations'; 3 4export const slideInAnimation = trigger('routeAnimations', [ 5 transition('* <=> *', [ 6 query(':enter, :leave', [ 7 style({ 8 position: 'absolute', 9 width: '100%', 10 transform: 'translateX(0)', 11 opacity: 1 12 }) 13 ], { optional: true }), 14 15 query(':enter', [ 16 style({ transform: 'translateX(100%)', opacity: 0 }) 17 ], { optional: true }), 18 19 query(':leave', [ 20 animate('300ms ease-out', style({ 21 transform: 'translateX(-100%)', 22 opacity: 0 23 })) 24 ], { optional: true }), 25 26 query(':enter', [ 27 animate('300ms 150ms ease-out', style({ 28 transform: 'translateX(0)', 29 opacity: 1 30 })) 31 ], { optional: true }) 32 ]) 33]);

Offline Sync Patterns

SQLite Repository

typescript
1// src/app/core/database/sqlite.service.ts 2import { CapacitorSQLite, SQLiteConnection, SQLiteDBConnection } from '@capacitor-community/sqlite'; 3 4@Injectable({ providedIn: 'root' }) 5export class SQLiteService { 6 private sqlite: SQLiteConnection; 7 private db!: SQLiteDBConnection; 8 9 async init() { 10 this.sqlite = new SQLiteConnection(CapacitorSQLite); 11 12 this.db = await this.sqlite.createConnection( 13 'lifeblocks', 14 false, 15 'no-encryption', 16 1, 17 false 18 ); 19 20 await this.db.open(); 21 await this.createTables(); 22 } 23 24 private async createTables() { 25 const schema = ` 26 CREATE TABLE IF NOT EXISTS daily_blocks ( 27 id TEXT PRIMARY KEY, 28 user_id TEXT NOT NULL, 29 date TEXT NOT NULL, 30 name TEXT NOT NULL, 31 start_time TEXT NOT NULL, 32 end_time TEXT NOT NULL, 33 status TEXT DEFAULT 'pending', 34 completion_note TEXT, 35 completed_at TEXT, 36 synced INTEGER DEFAULT 0, 37 updated_at TEXT DEFAULT CURRENT_TIMESTAMP 38 ); 39 40 CREATE INDEX IF NOT EXISTS idx_blocks_date ON daily_blocks(date); 41 CREATE INDEX IF NOT EXISTS idx_blocks_sync ON daily_blocks(synced); 42 43 CREATE TABLE IF NOT EXISTS sync_queue ( 44 id INTEGER PRIMARY KEY AUTOINCREMENT, 45 operation TEXT NOT NULL, 46 table_name TEXT NOT NULL, 47 record_id TEXT NOT NULL, 48 payload TEXT NOT NULL, 49 created_at TEXT DEFAULT CURRENT_TIMESTAMP 50 ); 51 `; 52 53 await this.db.execute(schema); 54 } 55 56 async getUnsyncedBlocks(): Promise<DailyBlock[]> { 57 const result = await this.db.query( 58 'SELECT * FROM daily_blocks WHERE synced = 0' 59 ); 60 return result.values || []; 61 } 62 63 async markAsSynced(blockId: string) { 64 await this.db.run( 65 'UPDATE daily_blocks SET synced = 1 WHERE id = ?', 66 [blockId] 67 ); 68 } 69}

Background Sync Service

typescript
1// src/app/core/sync/background-sync.service.ts 2@Injectable({ providedIn: 'root' }) 3export class BackgroundSyncService { 4 private sqlite = inject(SQLiteService); 5 private supabase = inject(SupabaseService); 6 private network = inject(NetworkService); 7 8 private syncInterval$ = interval(30000); // 30 segundos 9 10 startAutoSync() { 11 this.syncInterval$ 12 .pipe( 13 filter(() => this.network.isOnline()), 14 switchMap(() => this.syncPendingChanges()), 15 catchError(error => { 16 console.error('Sync failed', error); 17 return of(null); 18 }) 19 ) 20 .subscribe(); 21 } 22 23 async syncPendingChanges() { 24 const unsynced = await this.sqlite.getUnsyncedBlocks(); 25 26 for (const block of unsynced) { 27 try { 28 await this.supabase 29 .from('daily_blocks') 30 .upsert(block, { onConflict: 'id' }); 31 32 await this.sqlite.markAsSynced(block.id); 33 } catch (error) { 34 console.error(`Failed to sync block ${block.id}`, error); 35 // Continuar con el siguiente 36 } 37 } 38 } 39}

Performance Optimization

Virtual Scrolling for Long Lists

typescript
1@Component({ 2 selector: 'app-inventory-list', 3 standalone: true, 4 imports: [ScrollingModule, VirtualScrollerModule], 5 template: ` 6 <cdk-virtual-scroll-viewport itemSize="72" class="viewport"> 7 <app-inventory-item 8 *cdkVirtualFor="let item of items(); trackBy: trackById" 9 [item]="item" 10 /> 11 </cdk-virtual-scroll-viewport> 12 ` 13}) 14export class InventoryListComponent { 15 items = input.required<InventoryItem[]>(); 16 17 trackById(index: number, item: InventoryItem) { 18 return item.id; 19 } 20}

Image Loading Strategy

typescript
1// Directiva para lazy loading de imágenes 2@Directive({ 3 selector: 'img[appLazyLoad]', 4 standalone: true 5}) 6export class LazyLoadDirective implements OnInit { 7 @Input() src!: string; 8 private el = inject(ElementRef<HTMLImageElement>); 9 10 ngOnInit() { 11 if ('IntersectionObserver' in window) { 12 const observer = new IntersectionObserver((entries) => { 13 entries.forEach(entry => { 14 if (entry.isIntersecting) { 15 this.loadImage(); 16 observer.disconnect(); 17 } 18 }); 19 }); 20 21 observer.observe(this.el.nativeElement); 22 } else { 23 this.loadImage(); 24 } 25 } 26 27 private loadImage() { 28 this.el.nativeElement.src = this.src; 29 } 30}

Testing Patterns

Component Testing with Signals

typescript
1import { ComponentFixture, TestBed } from '@angular/core/testing'; 2import { signal } from '@angular/core'; 3 4describe('BlockCardComponent', () => { 5 let component: BlockCardComponent; 6 let fixture: ComponentFixture<BlockCardComponent>; 7 8 beforeEach(async () => { 9 await TestBed.configureTestingModule({ 10 imports: [BlockCardComponent] 11 }).compileComponents(); 12 13 fixture = TestBed.createComponent(BlockCardComponent); 14 component = fixture.componentInstance; 15 }); 16 17 it('should update completion status when confirmed', async () => { 18 const mockBlock = { 19 id: '1', 20 name: 'Deep Work', 21 status: 'pending' as const 22 }; 23 24 fixture.componentRef.setInput('block', mockBlock); 25 fixture.detectChanges(); 26 27 // Simular confirmación 28 await component.confirmCompletion('Terminé el módulo de auth'); 29 30 expect(component.block().status).toBe('completed'); 31 }); 32});

Service Testing with Mocks

typescript
1describe('BlocksRepository', () => { 2 let repo: BlocksRepository; 3 let mockSupabase: jasmine.SpyObj<SupabaseClient>; 4 5 beforeEach(() => { 6 mockSupabase = jasmine.createSpyObj('SupabaseClient', ['from']); 7 8 TestBed.configureTestingModule({ 9 providers: [ 10 BlocksRepository, 11 { provide: SupabaseService, useValue: { client: mockSupabase } } 12 ] 13 }); 14 15 repo = TestBed.inject(BlocksRepository); 16 }); 17 18 it('should fetch daily blocks', async () => { 19 const mockData = [{ id: '1', name: 'Gym' }]; 20 mockSupabase.from.and.returnValue({ 21 select: () => ({ 22 eq: () => ({ 23 eq: () => Promise.resolve({ data: mockData, error: null }) 24 }) 25 }) 26 } as any); 27 28 const result = await repo.getDailyBlocks('user-1', '2025-01-30'); 29 30 expect(result).toEqual(mockData); 31 }); 32});

Troubleshooting Common Issues

Issue: Notificaciones no aparecen en iOS

Causa: Permisos no solicitados correctamente

Solución:

typescript
1// Solicitar permisos DESPUÉS de que el usuario interactúe 2async requestNotificationPermissions() { 3 const result = await LocalNotifications.requestPermissions(); 4 5 if (result.display !== 'granted') { 6 // Mostrar dialog explicando por qué son necesarias 7 this.showPermissionsExplanation(); 8 } 9}

Issue: App se congela en sincronización

Causa: Operaciones síncronas en main thread

Solución: Usar Web Workers

typescript
1// sync.worker.ts 2addEventListener('message', async (e) => { 3 const { blocks } = e.data; 4 5 // Procesamiento pesado aquí 6 const processed = await heavySync(blocks); 7 8 postMessage({ result: processed }); 9});

Issue: Memoria creciente en listas largas

Causa: No usar virtual scrolling

Solución: Implementar cdk-virtual-scroll-viewport (ver sección Performance)


Scripts de Automatización

Generar Componente Mobile-Ready

bash
1#!/bin/bash 2# scripts/generate-mobile-component.sh 3 4COMPONENT_NAME=$1 5 6ng generate component "features/${COMPONENT_NAME}" \ 7 --standalone=true \ 8 --change-detection=OnPush \ 9 --skip-tests=false \ 10 --style=scss 11 12echo "✅ Componente mobile-ready creado en features/${COMPONENT_NAME}"

Sincronizar Tipos de Supabase

bash
1#!/bin/bash 2# scripts/sync-supabase-types.sh 3 4npx supabase gen types typescript \ 5 --project-id $SUPABASE_PROJECT_ID \ 6 > src/app/core/supabase/database.types.ts 7 8echo "✅ Tipos de Supabase actualizados"

Checklist de Code Review

Antes de merge, verificar:

  • Todos los componentes usan ChangeDetectionStrategy.OnPush
  • State management usa NgRx Signals (no NgRx Store)
  • Sincronización escribe primero a SQLite, luego a Supabase
  • Notificaciones tienen haptic feedback
  • Touch targets mínimo 44x44px
  • Animaciones GSAP optimizadas (willChange usado correctamente)
  • Tests cubren casos offline y online
  • Safe area insets considerados para iOS

Related skills: angular-primeng, angular-best-practices-primeng (PrimeNG); angular (Signals, Standalone).


Versión: 1.0.0
Última actualización: 2025-01-30
Compatibilidad: Angular 18+, Capacitor 6+, PrimeNG 18+

Related Skills

Looking for an alternative to angular-capacitor-patterns or building a Categories.community AI Agent? Explore these related open-source MCP Servers.

View All

widget-generator

Logo of f
f

widget-generator is an open-source AI agent skill for creating widget plugins that are injected into prompt feeds on prompts.chat. It supports two rendering modes: standard prompt widgets using default PromptCard styling and custom render widgets built as full React components.

149.6k
0
Design

chat-sdk

Logo of lobehub
lobehub

chat-sdk is a unified TypeScript SDK for building chat bots across multiple platforms, providing a single interface for deploying bot logic.

73.0k
0
Communication

zustand

Logo of lobehub
lobehub

The ultimate space for work and life — to find, build, and collaborate with agent teammates that grow with you. We are taking agent harness to the next level — enabling multi-agent collaboration, effortless agent team design, and introducing agents as the unit of work interaction.

72.8k
0
Communication

data-fetching

Logo of lobehub
lobehub

The ultimate space for work and life — to find, build, and collaborate with agent teammates that grow with you. We are taking agent harness to the next level — enabling multi-agent collaboration, effortless agent team design, and introducing agents as the unit of work interaction.

72.8k
0
Communication