@kustomizer/visual-editor 0.0.1 → 0.1.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,16 +1,12 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, inject, Injectable, signal, computed, makeEnvironmentProviders, ViewContainerRef, DestroyRef, input, effect, reflectComponentType, ChangeDetectionStrategy, Component, viewChild, output } from '@angular/core';
2
+ import { InjectionToken, inject, Injectable, makeEnvironmentProviders, ViewContainerRef, DestroyRef, input, effect, reflectComponentType, ChangeDetectionStrategy, Component, computed, NgZone, signal, viewChild, output } from '@angular/core';
3
3
  import { Router, ActivatedRoute } from '@angular/router';
4
- import { of, startWith, map, delay, throwError, firstValueFrom, forkJoin, BehaviorSubject, tap as tap$1, Subject, debounceTime, switchMap as switchMap$1, takeUntil, filter } from 'rxjs';
5
- import { tap, catchError, shareReplay, take, map as map$1, switchMap } from 'rxjs/operators';
4
+ import { of, startWith, map, delay, throwError, isObservable, Observable, BehaviorSubject, tap, Subject, filter, debounceTime, switchMap as switchMap$1, takeUntil } from 'rxjs';
5
+ import { switchMap, map as map$1, catchError } from 'rxjs/operators';
6
6
  import { HttpClient } from '@angular/common/http';
7
7
  import { createActionGroup, emptyProps, props, createReducer, on, createFeatureSelector, createSelector, provideState, Store } from '@ngrx/store';
8
8
  import { DomSanitizer } from '@angular/platform-browser';
9
9
  import { toSignal } from '@angular/core/rxjs-interop';
10
- import * as i1$1 from '@angular/common';
11
- import { CommonModule } from '@angular/common';
12
- import * as i1 from '@angular/forms';
13
- import { FormsModule } from '@angular/forms';
14
10
 
15
11
  /**
16
12
  * Abstract navigation service that consumers can implement
@@ -57,6 +53,9 @@ class DefaultRouterNavigationService extends VisualEditorNavigation {
57
53
  this.router.navigate([previewPath]);
58
54
  }
59
55
  break;
56
+ case 'setup':
57
+ this.router.navigate(['/setup']);
58
+ break;
60
59
  }
61
60
  }
62
61
  confirm(options) {
@@ -82,10 +81,10 @@ class DefaultRouterNavigationService extends VisualEditorNavigation {
82
81
  return this.router.events.pipe(startWith(null), // Trigger initial emission
83
82
  map(() => extractPageIdFromUrl()));
84
83
  }
85
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: DefaultRouterNavigationService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
86
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: DefaultRouterNavigationService });
84
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DefaultRouterNavigationService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
85
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DefaultRouterNavigationService });
87
86
  }
88
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: DefaultRouterNavigationService, decorators: [{
87
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DefaultRouterNavigationService, decorators: [{
89
88
  type: Injectable
90
89
  }] });
91
90
 
@@ -218,27 +217,27 @@ class PageMemoryRepository {
218
217
  publishedAt: page.publishedAt,
219
218
  };
220
219
  }
221
- getPages(environment) {
220
+ getPages() {
222
221
  const summaries = Array.from(this.pages.values())
223
222
  .map((p) => this.toSummary(p))
224
223
  .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
225
224
  return of(summaries).pipe(delay(this.DELAY_MS));
226
225
  }
227
- getPage(id, environment) {
226
+ getPage(id) {
228
227
  const page = this.pages.get(id);
229
228
  if (!page) {
230
229
  return throwError(() => new Error(`Page not found: ${id}`)).pipe(delay(this.DELAY_MS));
231
230
  }
232
231
  return of(structuredClone(page)).pipe(delay(this.DELAY_MS));
233
232
  }
234
- getPublishedPageBySlug(slug, environment) {
233
+ getPublishedPageBySlug(slug) {
235
234
  const page = Array.from(this.pages.values()).find((p) => p.slug === slug && p.status === 'published');
236
235
  if (!page) {
237
236
  return throwError(() => new Error(`Published page not found: ${slug}`)).pipe(delay(this.DELAY_MS));
238
237
  }
239
238
  return of(structuredClone(page)).pipe(delay(this.DELAY_MS));
240
239
  }
241
- createPage(request, environment) {
240
+ createPage(request) {
242
241
  // Check for duplicate slug
243
242
  const existingSlug = Array.from(this.pages.values()).find((p) => p.slug === request.slug);
244
243
  if (existingSlug) {
@@ -260,7 +259,7 @@ class PageMemoryRepository {
260
259
  console.log('[PageMemoryRepository] Created page:', page.id, page.title);
261
260
  return of(structuredClone(page)).pipe(delay(this.DELAY_MS));
262
261
  }
263
- updatePage(id, request, environment) {
262
+ updatePage(id, request) {
264
263
  const page = this.pages.get(id);
265
264
  if (!page) {
266
265
  return throwError(() => new Error(`Page not found: ${id}`)).pipe(delay(this.DELAY_MS));
@@ -290,7 +289,7 @@ class PageMemoryRepository {
290
289
  console.log('[PageMemoryRepository] Sections JSON:', JSON.stringify(updatedPage.sections, null, 2));
291
290
  return of(structuredClone(updatedPage)).pipe(delay(this.DELAY_MS));
292
291
  }
293
- deletePage(id, environment) {
292
+ deletePage(id) {
294
293
  if (!this.pages.has(id)) {
295
294
  return throwError(() => new Error(`Page not found: ${id}`)).pipe(delay(this.DELAY_MS));
296
295
  }
@@ -298,7 +297,7 @@ class PageMemoryRepository {
298
297
  console.log('[PageMemoryRepository] Deleted page:', id);
299
298
  return of(undefined).pipe(delay(this.DELAY_MS));
300
299
  }
301
- publishPage(id, request, environment) {
300
+ publishPage(id, request) {
302
301
  const page = this.pages.get(id);
303
302
  if (!page) {
304
303
  return throwError(() => new Error(`Page not found: ${id}`)).pipe(delay(this.DELAY_MS));
@@ -318,7 +317,7 @@ class PageMemoryRepository {
318
317
  console.log('[PageMemoryRepository] Published page:', id);
319
318
  return of(structuredClone(updatedPage)).pipe(delay(this.DELAY_MS));
320
319
  }
321
- unpublishPage(id, request, environment) {
320
+ unpublishPage(id, request) {
322
321
  const page = this.pages.get(id);
323
322
  if (!page) {
324
323
  return throwError(() => new Error(`Page not found: ${id}`)).pipe(delay(this.DELAY_MS));
@@ -336,7 +335,7 @@ class PageMemoryRepository {
336
335
  console.log('[PageMemoryRepository] Unpublished page:', id);
337
336
  return of(structuredClone(updatedPage)).pipe(delay(this.DELAY_MS));
338
337
  }
339
- duplicatePage(id, environment) {
338
+ duplicatePage(id) {
340
339
  const page = this.pages.get(id);
341
340
  if (!page) {
342
341
  return throwError(() => new Error(`Page not found: ${id}`)).pipe(delay(this.DELAY_MS));
@@ -363,573 +362,50 @@ class PageMemoryRepository {
363
362
  this.seedData();
364
363
  console.log('[PageMemoryRepository] Reset to initial state');
365
364
  }
366
- deletePagesByEnvironment(environment) {
367
- this.pages.clear();
368
- console.log('[PageMemoryRepository] Deleted all pages for environment:', environment ?? 'production');
369
- return of(undefined).pipe(delay(this.DELAY_MS));
370
- }
371
365
  // Utility method to get all pages (useful for debugging)
372
366
  getAllPages() {
373
367
  return Array.from(this.pages.values());
374
368
  }
375
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: PageMemoryRepository, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
376
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: PageMemoryRepository, providedIn: 'root' });
369
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: PageMemoryRepository, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
370
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: PageMemoryRepository, providedIn: 'root' });
377
371
  }
378
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: PageMemoryRepository, decorators: [{
372
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: PageMemoryRepository, decorators: [{
379
373
  type: Injectable,
380
374
  args: [{ providedIn: 'root' }]
381
375
  }], ctorParameters: () => [] });
382
376
 
383
- const SUPABASE_CONFIG = new InjectionToken('SUPABASE_CONFIG');
384
- class SupabaseAuthService {
385
- config = inject(SUPABASE_CONFIG, { optional: true });
386
- supabase = null;
387
- supabasePromise = null;
388
- authSubscription = null;
389
- _session = signal(null, ...(ngDevMode ? [{ debugName: "_session" }] : []));
390
- _user = signal(null, ...(ngDevMode ? [{ debugName: "_user" }] : []));
391
- _loading = signal(true, ...(ngDevMode ? [{ debugName: "_loading" }] : []));
392
- _error = signal(null, ...(ngDevMode ? [{ debugName: "_error" }] : []));
393
- _initialized = signal(false, ...(ngDevMode ? [{ debugName: "_initialized" }] : []));
394
- session = this._session.asReadonly();
395
- user = this._user.asReadonly();
396
- loading = this._loading.asReadonly();
397
- error = this._error.asReadonly();
398
- initialized = this._initialized.asReadonly();
399
- isAuthenticated = computed(() => this._initialized() && !!this._session(), ...(ngDevMode ? [{ debugName: "isAuthenticated" }] : []));
400
- accessToken = computed(() => this._session()?.access_token ?? null, ...(ngDevMode ? [{ debugName: "accessToken" }] : []));
401
- userEmail = computed(() => this._user()?.email ?? null, ...(ngDevMode ? [{ debugName: "userEmail" }] : []));
402
- async getSupabaseClient() {
403
- if (this.supabase)
404
- return this.supabase;
405
- if (this.supabasePromise)
406
- return this.supabasePromise;
407
- if (!this.config || typeof window === 'undefined') {
408
- this._loading.set(false);
409
- this._initialized.set(true);
410
- return null;
411
- }
412
- this.supabasePromise = this.initializeClient();
413
- return this.supabasePromise;
414
- }
415
- async initializeClient() {
416
- try {
417
- const { createClient } = await import('@supabase/supabase-js');
418
- this.supabase = createClient(this.config.supabaseUrl, this.config.supabaseAnonKey, {
419
- auth: {
420
- autoRefreshToken: true,
421
- persistSession: true,
422
- detectSessionInUrl: true,
423
- },
424
- });
425
- const { data: { subscription } } = this.supabase.auth.onAuthStateChange((event, session) => {
426
- this._session.set(session);
427
- this._user.set(session?.user ?? null);
428
- this._loading.set(false);
429
- this._initialized.set(true);
430
- if (event === 'SIGNED_OUT') {
431
- this._error.set(null);
432
- }
433
- });
434
- this.authSubscription = subscription;
435
- const { data: { session }, error } = await this.supabase.auth.getSession();
436
- if (!error) {
437
- this._session.set(session);
438
- this._user.set(session?.user ?? null);
439
- }
440
- this._loading.set(false);
441
- this._initialized.set(true);
442
- return this.supabase;
443
- }
444
- catch (err) {
445
- this._error.set(err instanceof Error ? err.message : 'Failed to initialize');
446
- this._loading.set(false);
447
- this._initialized.set(true);
448
- return null;
449
- }
450
- }
451
- async ensureInitialized() {
452
- await this.getSupabaseClient();
453
- }
454
- async signInWithPassword(email, password) {
455
- const client = await this.getSupabaseClient();
456
- if (!client) {
457
- return { success: false, error: 'Supabase not initialized' };
458
- }
459
- this._loading.set(true);
460
- this._error.set(null);
461
- try {
462
- const { data, error } = await client.auth.signInWithPassword({
463
- email,
464
- password,
465
- });
466
- if (error)
467
- throw error;
468
- this._session.set(data.session);
469
- this._user.set(data.user);
470
- return { success: true };
471
- }
472
- catch (err) {
473
- const message = err instanceof Error ? err.message : 'Login failed';
474
- this._error.set(message);
475
- return { success: false, error: message };
476
- }
477
- finally {
478
- this._loading.set(false);
479
- }
480
- }
481
- async signUp(email, password) {
482
- const client = await this.getSupabaseClient();
483
- if (!client) {
484
- return { success: false, error: 'Supabase not initialized' };
485
- }
486
- this._loading.set(true);
487
- this._error.set(null);
488
- try {
489
- const { data, error } = await client.auth.signUp({
490
- email,
491
- password,
492
- });
493
- if (error)
494
- throw error;
495
- if (data.session) {
496
- this._session.set(data.session);
497
- this._user.set(data.user);
498
- }
499
- return { success: true };
500
- }
501
- catch (err) {
502
- const message = err instanceof Error ? err.message : 'Sign up failed';
503
- this._error.set(message);
504
- return { success: false, error: message };
505
- }
506
- finally {
507
- this._loading.set(false);
508
- }
509
- }
510
- async signOut() {
511
- const client = await this.getSupabaseClient();
512
- if (!client)
513
- return;
514
- this._loading.set(true);
515
- try {
516
- await client.auth.signOut();
517
- this._session.set(null);
518
- this._user.set(null);
519
- }
520
- catch (err) {
521
- this._error.set(err instanceof Error ? err.message : 'Sign out failed');
522
- }
523
- finally {
524
- this._loading.set(false);
525
- }
526
- }
527
- async resetPassword(email) {
528
- const client = await this.getSupabaseClient();
529
- if (!client) {
530
- return { success: false, error: 'Supabase not initialized' };
531
- }
532
- try {
533
- const { error } = await client.auth.resetPasswordForEmail(email);
534
- if (error)
535
- throw error;
536
- return { success: true };
537
- }
538
- catch (err) {
539
- return { success: false, error: err instanceof Error ? err.message : 'Reset failed' };
540
- }
541
- }
542
- async getClient() {
543
- return this.getSupabaseClient();
544
- }
545
- ngOnDestroy() {
546
- this.authSubscription?.unsubscribe();
547
- }
548
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SupabaseAuthService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
549
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SupabaseAuthService, providedIn: 'root' });
550
- }
551
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SupabaseAuthService, decorators: [{
552
- type: Injectable,
553
- args: [{ providedIn: 'root' }]
554
- }] });
555
-
556
377
  /**
557
- * Token para inyectar el dominio de la tienda.
558
- * Debe ser configurado por la aplicación que instala el editor.
559
- *
560
- * Ejemplo de uso:
561
- * ```typescript
562
- * providers: [
563
- * { provide: STORE_DOMAIN, useValue: 'nike.com' }
564
- * ]
565
- * ```
566
- *
567
- * Si no se provee, el servicio intentará detectar el dominio desde window.location.hostname
378
+ * Injection token for Shopify configuration
379
+ * Can be provided as a static object or as an Observable for dynamic config
568
380
  */
569
- const STORE_DOMAIN = new InjectionToken('STORE_DOMAIN');
570
- class ShopAuthService {
571
- http = inject(HttpClient);
572
- config = inject(SUPABASE_CONFIG, { optional: true });
573
- authService = inject(SupabaseAuthService);
574
- storeDomainConfig = inject(STORE_DOMAIN, { optional: true });
575
- // Estado interno
576
- _shop = signal(null, ...(ngDevMode ? [{ debugName: "_shop" }] : []));
577
- _user = signal(null, ...(ngDevMode ? [{ debugName: "_user" }] : []));
578
- _license = signal(null, ...(ngDevMode ? [{ debugName: "_license" }] : []));
579
- _loading = signal(false, ...(ngDevMode ? [{ debugName: "_loading" }] : []));
580
- _error = signal(null, ...(ngDevMode ? [{ debugName: "_error" }] : []));
581
- _initialized = signal(false, ...(ngDevMode ? [{ debugName: "_initialized" }] : []));
582
- // Exponer estado como readonly
583
- shop = this._shop.asReadonly();
584
- user = this._user.asReadonly();
585
- license = this._license.asReadonly();
586
- loading = this._loading.asReadonly();
587
- error = this._error.asReadonly();
588
- initialized = this._initialized.asReadonly();
589
- // Computed helpers
590
- shopId = computed(() => this._shop()?.id ?? null, ...(ngDevMode ? [{ debugName: "shopId" }] : []));
591
- shopName = computed(() => this._shop()?.name ?? null, ...(ngDevMode ? [{ debugName: "shopName" }] : []));
592
- shopifyDomain = computed(() => this._shop()?.shopify_domain ?? null, ...(ngDevMode ? [{ debugName: "shopifyDomain" }] : []));
593
- userEmail = computed(() => this._user()?.email ?? null, ...(ngDevMode ? [{ debugName: "userEmail" }] : []));
594
- userRole = computed(() => this._user()?.role ?? null, ...(ngDevMode ? [{ debugName: "userRole" }] : []));
595
- isOwner = computed(() => this._user()?.role === 'owner', ...(ngDevMode ? [{ debugName: "isOwner" }] : []));
596
- isAdmin = computed(() => ['owner', 'admin'].includes(this._user()?.role ?? ''), ...(ngDevMode ? [{ debugName: "isAdmin" }] : []));
597
- canEdit = computed(() => ['owner', 'admin'].includes(this._user()?.role ?? ''), ...(ngDevMode ? [{ debugName: "canEdit" }] : []));
598
- licenseTier = computed(() => this._license()?.tier ?? null, ...(ngDevMode ? [{ debugName: "licenseTier" }] : []));
599
- licenseActive = computed(() => this._license()?.active ?? false, ...(ngDevMode ? [{ debugName: "licenseActive" }] : []));
600
- licenseExpiresAt = computed(() => this._license()?.expires_at ?? null, ...(ngDevMode ? [{ debugName: "licenseExpiresAt" }] : []));
601
- hasAccess = computed(() => {
602
- return this._shop() !== null && this._user() !== null && this._license()?.active === true;
603
- }, ...(ngDevMode ? [{ debugName: "hasAccess" }] : []));
604
- daysRemaining = computed(() => {
605
- const expiresAt = this._license()?.expires_at;
606
- if (!expiresAt)
607
- return null;
608
- const days = Math.ceil((new Date(expiresAt).getTime() - Date.now()) / (1000 * 60 * 60 * 24));
609
- return Math.max(0, days);
610
- }, ...(ngDevMode ? [{ debugName: "daysRemaining" }] : []));
611
- isLicenseExpiringSoon = computed(() => {
612
- const days = this.daysRemaining();
613
- return days !== null && days <= 7;
614
- }, ...(ngDevMode ? [{ debugName: "isLicenseExpiringSoon" }] : []));
615
- /**
616
- * Obtiene el dominio actual.
617
- * Prioridad:
618
- * 1. STORE_DOMAIN token (inyectado)
619
- * 2. Query param ?shop= (solo en localhost)
620
- * 3. window.location.hostname
621
- */
622
- getCurrentDomain() {
623
- if (this.storeDomainConfig) {
624
- return this.storeDomainConfig;
625
- }
626
- if (typeof window !== 'undefined') {
627
- const hostname = window.location.hostname;
628
- if (hostname === 'localhost' || hostname === '127.0.0.1') {
629
- const params = new URLSearchParams(window.location.search);
630
- const shopParam = params.get('shop');
631
- if (shopParam) {
632
- localStorage.setItem('kustomizer_shop_domain', shopParam);
633
- return shopParam;
634
- }
635
- const stored = localStorage.getItem('kustomizer_shop_domain');
636
- if (stored) {
637
- return stored;
638
- }
639
- }
640
- return hostname;
641
- }
642
- return '';
643
- }
644
- /**
645
- * Autentica al usuario para el dominio actual.
646
- * Esta es la función principal que debe llamarse al cargar la app.
647
- */
648
- async authenticate() {
649
- // Si ya está inicializado y tiene datos, retornar cache
650
- if (this._initialized() && this._shop()) {
651
- return {
652
- shop: this._shop(),
653
- user: this._user(),
654
- license: this._license(),
655
- };
656
- }
657
- // Esperar a que el auth service esté listo
658
- await this.authService.ensureInitialized();
659
- const token = this.authService.accessToken();
660
- const email = this.authService.userEmail();
661
- if (!token || !email) {
662
- this._error.set({ code: 'NOT_AUTHENTICATED', message: 'User is not authenticated' });
663
- this._initialized.set(true);
664
- return { shop: null, user: null, license: null };
665
- }
666
- const domain = this.getCurrentDomain();
667
- if (!domain) {
668
- this._error.set({ code: 'NO_DOMAIN', message: 'Could not determine current domain' });
669
- this._initialized.set(true);
670
- return { shop: null, user: null, license: null };
671
- }
672
- if (domain === 'localhost' || domain === '127.0.0.1') {
673
- this._error.set({
674
- code: 'LOCALHOST_NO_SHOP',
675
- message: 'Running on localhost. Add ?shop=your-store.myshopify.com to the URL'
676
- });
677
- this._initialized.set(true);
678
- return { shop: null, user: null, license: null };
679
- }
680
- this._loading.set(true);
681
- this._error.set(null);
682
- try {
683
- const url = this.config
684
- ? `${this.config.supabaseUrl}/functions/v1/shop_auth`
685
- : '/functions/v1/shop_auth';
686
- const anonKey = this.config?.supabaseAnonKey ?? '';
687
- const response = await firstValueFrom(this.http.post(url, { domain, email }, {
688
- headers: {
689
- 'Authorization': `Bearer ${anonKey}`,
690
- 'apikey': anonKey,
691
- 'Content-Type': 'application/json',
692
- },
693
- }));
694
- this._shop.set(response.shop);
695
- this._user.set(response.user);
696
- this._license.set(response.license);
697
- this._initialized.set(true);
698
- this._loading.set(false);
699
- return {
700
- shop: response.shop,
701
- user: response.user,
702
- license: response.license,
703
- };
704
- }
705
- catch (err) {
706
- console.error('[ShopAuthService] Authentication failed:', err);
707
- const httpError = err;
708
- let errorCode = 'UNKNOWN_ERROR';
709
- let errorMessage = 'Authentication failed';
710
- if (httpError.error?.code) {
711
- errorCode = httpError.error.code;
712
- errorMessage = httpError.error.error || httpError.error.message || errorMessage;
713
- }
714
- else if (httpError.status === 404) {
715
- errorCode = 'SHOP_NOT_FOUND';
716
- errorMessage = 'This domain is not registered';
717
- }
718
- else if (httpError.status === 403) {
719
- errorCode = 'NO_ACCESS';
720
- errorMessage = 'You do not have access to this shop';
721
- }
722
- this._error.set({ code: errorCode, message: errorMessage });
723
- this._shop.set(null);
724
- this._user.set(null);
725
- this._license.set(null);
726
- this._initialized.set(true);
727
- this._loading.set(false);
728
- return { shop: null, user: null, license: null };
729
- }
730
- }
731
- /**
732
- * Limpia el estado (para logout)
733
- */
734
- clear() {
735
- this._shop.set(null);
736
- this._user.set(null);
737
- this._license.set(null);
738
- this._error.set(null);
739
- this._initialized.set(false);
740
- if (typeof window !== 'undefined') {
741
- localStorage.removeItem('kustomizer_shop_domain');
742
- }
743
- }
744
- /**
745
- * Refresca la autenticación
746
- */
747
- async refresh() {
748
- this._initialized.set(false);
749
- return this.authenticate();
750
- }
751
- /**
752
- * Verifica si el error actual indica que el usuario no tiene acceso
753
- */
754
- isNoAccessError() {
755
- const code = this._error()?.code;
756
- return code === 'NO_ACCESS' || code === 'SHOP_NOT_FOUND';
757
- }
758
- /**
759
- * Verifica si el error actual indica que la licencia expiró
760
- */
761
- isLicenseExpiredError() {
762
- return this._error()?.code === 'LICENSE_EXPIRED';
763
- }
764
- /**
765
- * Verifica si el usuario está inactivo
766
- */
767
- isUserInactiveError() {
768
- return this._error()?.code === 'USER_INACTIVE';
769
- }
770
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ShopAuthService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
771
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ShopAuthService, providedIn: 'root' });
772
- }
773
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ShopAuthService, decorators: [{
774
- type: Injectable,
775
- args: [{ providedIn: 'root' }]
776
- }] });
777
-
778
- const SHOPIFY_APP_CONFIG = new InjectionToken('SHOPIFY_APP_CONFIG', {
381
+ const SHOPIFY_CONFIG = new InjectionToken('SHOPIFY_CONFIG', {
779
382
  providedIn: 'root',
780
383
  factory: () => ({
781
- appUrl: 'https://kustomizer.xyz'
782
- })
384
+ shopDomain: '',
385
+ accessToken: '',
386
+ apiVersion: '2026-01',
387
+ proxyUrl: '/shopify-api/graphql.json',
388
+ }),
389
+ });
390
+ /**
391
+ * Injection token for Storefront API configuration.
392
+ * Used by the public-storefront to read metafields via the Storefront API.
393
+ */
394
+ const STOREFRONT_CONFIG = new InjectionToken('STOREFRONT_CONFIG');
395
+ /**
396
+ * Injection token for the client storefront URL.
397
+ * The editor uses this to:
398
+ * 1. Fetch the component manifest (`{url}/kustomizer/manifest.json`)
399
+ * 2. Embed the live preview iframe (`{url}/kustomizer/editor`)
400
+ */
401
+ const STOREFRONT_URL = new InjectionToken('STOREFRONT_URL', {
402
+ providedIn: 'root',
403
+ factory: () => '',
783
404
  });
784
- class ShopifyTokenService {
785
- http = inject(HttpClient);
786
- config = inject(SHOPIFY_APP_CONFIG);
787
- authService = inject(SupabaseAuthService);
788
- shopAuthService = inject(ShopAuthService);
789
- CACHE_TTL_MS = 5 * 60 * 1000;
790
- tokenCache = new Map();
791
- _currentToken = signal(null, ...(ngDevMode ? [{ debugName: "_currentToken" }] : []));
792
- _currentDomain = signal(null, ...(ngDevMode ? [{ debugName: "_currentDomain" }] : []));
793
- _loading = signal(false, ...(ngDevMode ? [{ debugName: "_loading" }] : []));
794
- _error = signal(null, ...(ngDevMode ? [{ debugName: "_error" }] : []));
795
- pendingRequest = null;
796
- currentToken = this._currentToken.asReadonly();
797
- currentDomain = this._currentDomain.asReadonly();
798
- loading = this._loading.asReadonly();
799
- error = this._error.asReadonly();
800
- hasToken = computed(() => !!this._currentToken(), ...(ngDevMode ? [{ debugName: "hasToken" }] : []));
801
- isReady = computed(() => this.authService.isAuthenticated() && !!this.shopAuthService.shop(), ...(ngDevMode ? [{ debugName: "isReady" }] : []));
802
- getToken() {
803
- if (!this.authService.isAuthenticated()) {
804
- return throwError(() => this.createError('NOT_AUTHENTICATED', 'User is not authenticated'));
805
- }
806
- const shop = this.shopAuthService.shop();
807
- if (!shop?.shopify_domain) {
808
- return throwError(() => this.createError('NO_ACTIVE_STORE', 'No active store selected'));
809
- }
810
- return this.getTokenForDomain(shop.shopify_domain);
811
- }
812
- getTokenForDomain(shopifyDomain) {
813
- const cached = this.getCachedToken(shopifyDomain);
814
- if (cached) {
815
- this._currentToken.set(cached.accessToken);
816
- this._currentDomain.set(cached.shopifyDomain);
817
- return of({
818
- accessToken: cached.accessToken,
819
- shopifyDomain: cached.shopifyDomain
820
- });
821
- }
822
- if (this.pendingRequest) {
823
- return this.pendingRequest;
824
- }
825
- return this.fetchToken(shopifyDomain);
826
- }
827
- refreshToken() {
828
- const shop = this.shopAuthService.shop();
829
- if (!shop?.shopify_domain) {
830
- return throwError(() => this.createError('NO_ACTIVE_STORE', 'No active store selected'));
831
- }
832
- this.invalidateCache(shop.shopify_domain);
833
- return this.fetchToken(shop.shopify_domain);
834
- }
835
- invalidateCache(shopifyDomain) {
836
- if (shopifyDomain) {
837
- this.tokenCache.delete(shopifyDomain);
838
- if (this._currentDomain() === shopifyDomain) {
839
- this._currentToken.set(null);
840
- this._currentDomain.set(null);
841
- }
842
- }
843
- else {
844
- this.tokenCache.clear();
845
- this._currentToken.set(null);
846
- this._currentDomain.set(null);
847
- }
848
- }
849
- clear() {
850
- this.invalidateCache();
851
- this._error.set(null);
852
- this._loading.set(false);
853
- this.pendingRequest = null;
854
- }
855
- fetchToken(shopifyDomain) {
856
- this._loading.set(true);
857
- this._error.set(null);
858
- const supabaseToken = this.authService.accessToken();
859
- if (!supabaseToken) {
860
- this._loading.set(false);
861
- return throwError(() => this.createError('NOT_AUTHENTICATED', 'No authentication token available'));
862
- }
863
- const url = `${this.config.appUrl}/api/get-token`;
864
- this.pendingRequest = this.http.post(url, { shopifyDomain }, {
865
- headers: {
866
- 'Authorization': `Bearer ${supabaseToken}`,
867
- 'Content-Type': 'application/json'
868
- }
869
- }).pipe(tap(response => {
870
- this.setCachedToken(shopifyDomain, response);
871
- this._currentToken.set(response.accessToken);
872
- this._currentDomain.set(response.shopifyDomain);
873
- this._loading.set(false);
874
- this.pendingRequest = null;
875
- }), catchError((error) => {
876
- this._loading.set(false);
877
- this.pendingRequest = null;
878
- const tokenError = this.handleHttpError(error);
879
- this._error.set(tokenError);
880
- return throwError(() => tokenError);
881
- }), shareReplay(1), take(1));
882
- return this.pendingRequest;
883
- }
884
- getCachedToken(shopifyDomain) {
885
- const entry = this.tokenCache.get(shopifyDomain);
886
- if (entry && Date.now() < entry.expiresAt) {
887
- return entry;
888
- }
889
- if (entry) {
890
- this.tokenCache.delete(shopifyDomain);
891
- }
892
- return null;
893
- }
894
- setCachedToken(shopifyDomain, response) {
895
- this.tokenCache.set(shopifyDomain, {
896
- accessToken: response.accessToken,
897
- shopifyDomain: response.shopifyDomain,
898
- expiresAt: Date.now() + this.CACHE_TTL_MS
899
- });
900
- }
901
- handleHttpError(error) {
902
- if (error.error instanceof ErrorEvent) {
903
- return this.createError('NETWORK_ERROR', 'Network error occurred', error.status);
904
- }
905
- switch (error.status) {
906
- case 401:
907
- return this.createError('SESSION_EXPIRED', 'Session expired. Please login again.', 401);
908
- case 403:
909
- return this.createError('NO_STORE_ACCESS', 'You do not have access to this store.', 403);
910
- case 404:
911
- return this.createError('NO_STORE_ACCESS', 'Store not found or not linked.', 404);
912
- default:
913
- const message = error.error?.error || error.error?.message || 'Failed to get token';
914
- return this.createError('TOKEN_FETCH_FAILED', message, error.status);
915
- }
916
- }
917
- createError(code, message, status) {
918
- return { code, message, status };
919
- }
920
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ShopifyTokenService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
921
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ShopifyTokenService, providedIn: 'root' });
922
- }
923
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ShopifyTokenService, decorators: [{
924
- type: Injectable,
925
- args: [{ providedIn: 'root' }]
926
- }] });
927
405
 
928
406
  class ShopifyGraphQLClient {
929
407
  http = inject(HttpClient);
930
- authService = inject(SupabaseAuthService);
931
- shopAuthService = inject(ShopAuthService);
932
- config = inject(SHOPIFY_APP_CONFIG);
408
+ config = inject(SHOPIFY_CONFIG);
933
409
  query(query, variables) {
934
410
  return this.executeOperation(query, variables);
935
411
  }
@@ -937,96 +413,100 @@ class ShopifyGraphQLClient {
937
413
  return this.executeOperation(mutation, variables);
938
414
  }
939
415
  executeOperation(operation, variables) {
940
- const shop = this.shopAuthService.shop();
941
- if (!shop?.shopify_domain) {
942
- return throwError(() => this.createError('TOKEN_ERROR', 'No active store selected'));
943
- }
944
- const supabaseToken = this.authService.accessToken();
945
- if (!supabaseToken) {
946
- return throwError(() => this.createError('UNAUTHORIZED', 'Not authenticated'));
947
- }
948
- const shopifyDomain = shop.shopify_domain;
949
- const proxyUrl = `${this.config.appUrl}/api/graphql-proxy`;
950
- return this.http.post(proxyUrl, {
951
- shopifyDomain,
952
- query: operation,
953
- variables,
954
- }, {
955
- headers: {
416
+ const config$ = isObservable(this.config)
417
+ ? this.config
418
+ : new Observable(subscriber => {
419
+ subscriber.next(this.config);
420
+ subscriber.complete();
421
+ });
422
+ return config$.pipe(switchMap((config) => {
423
+ // Use proxy URL if available (for development), otherwise direct Shopify URL
424
+ const url = config.proxyUrl
425
+ ? config.proxyUrl
426
+ : `https://${config.shopDomain}/admin/api/${config.apiVersion}/graphql.json`;
427
+ const request = {
428
+ query: operation,
429
+ variables,
430
+ };
431
+ // When using proxy, headers are set by the proxy config
432
+ const headers = {
956
433
  'Content-Type': 'application/json',
957
- 'Authorization': `Bearer ${supabaseToken}`,
958
- },
959
- }).pipe(map$1(response => this.handleResponse(response)), catchError(error => this.handleHttpError(error)));
434
+ };
435
+ // Only add access token header when not using proxy
436
+ if (!config.proxyUrl) {
437
+ headers['X-Shopify-Access-Token'] = config.accessToken;
438
+ }
439
+ return this.http.post(url, request, { headers }).pipe(map$1(response => this.handleResponse(response)), catchError(error => this.handleError(error)));
440
+ }));
960
441
  }
961
442
  handleResponse(response) {
962
443
  if (response.errors && response.errors.length > 0) {
963
- const error = {
444
+ throw {
964
445
  code: 'GRAPHQL_ERROR',
965
446
  message: response.errors[0].message,
966
- errors: response.errors.map(e => ({ message: e.message }))
447
+ errors: response.errors,
967
448
  };
968
- throw error;
969
449
  }
970
450
  if (!response.data) {
971
- const error = {
451
+ throw {
972
452
  code: 'EMPTY_RESPONSE',
973
- message: 'GraphQL response contains no data'
453
+ message: 'GraphQL response contains no data',
974
454
  };
975
- throw error;
976
455
  }
977
456
  return response.data;
978
457
  }
979
- handleHttpError(error) {
980
- if (error.error instanceof ErrorEvent) {
981
- return throwError(() => this.createError('NETWORK_ERROR', error.error.message));
458
+ handleError(error) {
459
+ let errorCode = 'UNKNOWN_ERROR';
460
+ let errorMessage = 'An unknown error occurred';
461
+ if (typeof ErrorEvent !== 'undefined' && error.error instanceof ErrorEvent) {
462
+ errorCode = 'NETWORK_ERROR';
463
+ errorMessage = error.error.message;
982
464
  }
983
- let code;
984
- let message;
985
- switch (error.status) {
986
- case 401:
987
- code = 'UNAUTHORIZED';
988
- message = 'Session expired. Please login again.';
989
- break;
990
- case 403:
991
- code = 'FORBIDDEN';
992
- message = 'Access denied to this store.';
993
- break;
994
- case 404:
995
- code = 'TOKEN_ERROR';
996
- message = 'Store not connected. Please install the Shopify app.';
997
- break;
998
- case 429:
999
- code = 'RATE_LIMIT';
1000
- message = 'Rate limit exceeded. Please try again later.';
1001
- break;
1002
- case 500:
1003
- case 502:
1004
- case 503:
1005
- case 504:
1006
- code = 'SERVER_ERROR';
1007
- message = 'Server error. Please try again.';
1008
- break;
1009
- default:
1010
- code = 'HTTP_ERROR';
1011
- message = error.error?.error || `HTTP ${error.status}: ${error.statusText || 'Unknown error'}`;
465
+ else {
466
+ switch (error.status) {
467
+ case 401:
468
+ errorCode = 'UNAUTHORIZED';
469
+ errorMessage = 'Invalid or expired access token';
470
+ break;
471
+ case 403:
472
+ errorCode = 'FORBIDDEN';
473
+ errorMessage = 'Insufficient permissions';
474
+ break;
475
+ case 429:
476
+ errorCode = 'RATE_LIMIT';
477
+ errorMessage = 'Rate limit exceeded';
478
+ break;
479
+ case 500:
480
+ case 502:
481
+ case 503:
482
+ case 504:
483
+ errorCode = 'SERVER_ERROR';
484
+ errorMessage = 'Shopify server error';
485
+ break;
486
+ default:
487
+ errorCode = 'HTTP_ERROR';
488
+ errorMessage = `HTTP ${error.status}: ${error.statusText}`;
489
+ }
1012
490
  }
1013
- return throwError(() => this.createError(code, message, error.status, error));
1014
- }
1015
- createError(code, message, status, original) {
1016
- return { code, message, status, original };
491
+ return throwError(() => ({
492
+ code: errorCode,
493
+ message: errorMessage,
494
+ status: error.status,
495
+ original: error,
496
+ }));
1017
497
  }
1018
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ShopifyGraphQLClient, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1019
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ShopifyGraphQLClient, providedIn: 'root' });
498
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ShopifyGraphQLClient, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
499
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ShopifyGraphQLClient, providedIn: 'root' });
1020
500
  }
1021
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ShopifyGraphQLClient, decorators: [{
501
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ShopifyGraphQLClient, decorators: [{
1022
502
  type: Injectable,
1023
503
  args: [{ providedIn: 'root' }]
1024
504
  }] });
1025
505
 
1026
- const NAMESPACE = 'kustomizer';
1027
- const INDEX_KEY = 'pages_index';
506
+ const NAMESPACE$1 = 'kustomizer';
507
+ const INDEX_KEY$1 = 'pages_index';
1028
508
  const MAX_METAFIELD_SIZE = 64 * 1024;
1029
- const GET_SHOP_METAFIELD_QUERY = `
509
+ const GET_SHOP_METAFIELD_QUERY$1 = `
1030
510
  query GetShopMetafield($namespace: String!, $key: String!) {
1031
511
  shop {
1032
512
  id
@@ -1069,6 +549,39 @@ const DELETE_METAFIELDS_MUTATION = `
1069
549
  }
1070
550
  }
1071
551
  `;
552
+ const CREATE_METAFIELD_DEFINITION_MUTATION = `
553
+ mutation CreateMetafieldDefinition($definition: MetafieldDefinitionInput!) {
554
+ metafieldDefinitionCreate(definition: $definition) {
555
+ createdDefinition {
556
+ id
557
+ }
558
+ userErrors {
559
+ field
560
+ message
561
+ code
562
+ }
563
+ }
564
+ }
565
+ `;
566
+ const DELETE_METAFIELD_DEFINITION_MUTATION = `
567
+ mutation DeleteMetafieldDefinition($id: ID!) {
568
+ metafieldDefinitionDelete(id: $id) {
569
+ deletedDefinitionId
570
+ userErrors {
571
+ field
572
+ message
573
+ code
574
+ }
575
+ }
576
+ }
577
+ `;
578
+ const GET_METAFIELD_DEFINITION_QUERY = `
579
+ query GetMetafieldDefinition($ownerType: MetafieldOwnerType!, $namespace: String, $key: String!) {
580
+ metafieldDefinition(identifier: { ownerType: $ownerType, namespace: $namespace, key: $key }) {
581
+ id
582
+ }
583
+ }
584
+ `;
1072
585
  const GET_SHOP_ID_QUERY = `
1073
586
  query GetShopId {
1074
587
  shop {
@@ -1078,20 +591,8 @@ const GET_SHOP_ID_QUERY = `
1078
591
  `;
1079
592
  class ShopifyMetafieldRepository {
1080
593
  client = inject(ShopifyGraphQLClient);
1081
- resolveEnvironment(environment) {
1082
- const trimmed = environment?.trim();
1083
- return trimmed ? trimmed : 'production';
1084
- }
1085
- buildIndexKey(environment) {
1086
- const env = this.resolveEnvironment(environment);
1087
- return `${env}.${INDEX_KEY}`;
1088
- }
1089
- buildPageKey(pageId, environment) {
1090
- const env = this.resolveEnvironment(environment);
1091
- return `${env}.page_${pageId}`;
1092
- }
1093
594
  getMetafield(namespace, key) {
1094
- return this.client.query(GET_SHOP_METAFIELD_QUERY, { namespace, key }).pipe(map$1(response => {
595
+ return this.client.query(GET_SHOP_METAFIELD_QUERY$1, { namespace, key }).pipe(map$1(response => {
1095
596
  if (!response.shop.metafield) {
1096
597
  return null;
1097
598
  }
@@ -1164,9 +665,8 @@ class ShopifyMetafieldRepository {
1164
665
  getShopId() {
1165
666
  return this.client.query(GET_SHOP_ID_QUERY).pipe(map$1(response => response.shop.id));
1166
667
  }
1167
- getPageIndex(environment) {
1168
- const key = this.buildIndexKey(environment);
1169
- return this.getMetafield(NAMESPACE, key).pipe(map$1(metafield => {
668
+ getPageIndex() {
669
+ return this.getMetafield(NAMESPACE$1, INDEX_KEY$1).pipe(map$1(metafield => {
1170
670
  if (!metafield) {
1171
671
  return this.getDefaultIndex();
1172
672
  }
@@ -1186,20 +686,19 @@ class ShopifyMetafieldRepository {
1186
686
  }
1187
687
  }));
1188
688
  }
1189
- updatePageIndex(index, environment) {
689
+ updatePageIndex(index) {
1190
690
  return this.getShopId().pipe(switchMap(shopId => {
1191
691
  const updatedIndex = {
1192
692
  ...index,
1193
693
  version: index.version + 1,
1194
694
  lastUpdated: new Date().toISOString(),
1195
695
  };
1196
- const key = this.buildIndexKey(environment);
1197
- return this.setMetafield(NAMESPACE, key, updatedIndex, shopId).pipe(map$1(() => updatedIndex));
696
+ return this.setMetafield(NAMESPACE$1, INDEX_KEY$1, updatedIndex, shopId).pipe(map$1(() => updatedIndex));
1198
697
  }));
1199
698
  }
1200
- getPageMetafield(pageId, environment) {
1201
- const key = this.buildPageKey(pageId, environment);
1202
- return this.getMetafield(NAMESPACE, key).pipe(map$1(metafield => {
699
+ getPageMetafield(pageId) {
700
+ const key = `page_${pageId}`;
701
+ return this.getMetafield(NAMESPACE$1, key).pipe(map$1(metafield => {
1203
702
  if (!metafield) {
1204
703
  return null;
1205
704
  }
@@ -1215,21 +714,81 @@ class ShopifyMetafieldRepository {
1215
714
  }
1216
715
  }));
1217
716
  }
1218
- setPageMetafield(pageId, page, environment) {
1219
- const key = this.buildPageKey(pageId, environment);
717
+ setPageMetafield(pageId, page) {
718
+ const key = `page_${pageId}`;
1220
719
  return this.getShopId().pipe(switchMap(shopId => {
1221
- return this.setMetafield(NAMESPACE, key, page, shopId).pipe(map$1(() => page));
720
+ return this.setMetafield(NAMESPACE$1, key, page, shopId).pipe(map$1(() => page));
1222
721
  }));
1223
722
  }
1224
- deletePageMetafield(pageId, environment) {
1225
- const key = this.buildPageKey(pageId, environment);
1226
- return this.deleteMetafield(NAMESPACE, key);
723
+ deletePageMetafield(pageId) {
724
+ const key = `page_${pageId}`;
725
+ return this.deleteMetafield(NAMESPACE$1, key);
1227
726
  }
1228
- resetPageIndex(environment) {
1229
- const key = this.buildIndexKey(environment);
1230
- const resetIndex = this.getDefaultIndex();
1231
- return this.getShopId().pipe(switchMap(shopId => {
1232
- return this.setMetafield(NAMESPACE, key, resetIndex, shopId).pipe(map$1(() => resetIndex));
727
+ /**
728
+ * Create a MetafieldDefinition with PUBLIC_READ storefront access.
729
+ * Idempotent: if definition already exists, returns the existing ID.
730
+ */
731
+ createMetafieldDefinition(namespace, key, name) {
732
+ return this.client.mutate(CREATE_METAFIELD_DEFINITION_MUTATION, {
733
+ definition: {
734
+ namespace,
735
+ key,
736
+ name,
737
+ ownerType: 'SHOP',
738
+ type: 'json',
739
+ access: {
740
+ storefront: 'PUBLIC_READ',
741
+ },
742
+ },
743
+ }).pipe(switchMap(response => {
744
+ const { createdDefinition, userErrors } = response.metafieldDefinitionCreate;
745
+ if (createdDefinition) {
746
+ return of(createdDefinition.id);
747
+ }
748
+ // If definition already exists, look it up instead of failing
749
+ const alreadyExists = userErrors.some(e => e.code === 'TAKEN' || e.code === 'ALREADY_EXISTS');
750
+ if (alreadyExists) {
751
+ return this.getMetafieldDefinitionId(namespace, key);
752
+ }
753
+ throw {
754
+ code: 'METAFIELD_DEFINITION_ERROR',
755
+ message: userErrors[0]?.message || 'Failed to create metafield definition',
756
+ errors: userErrors,
757
+ };
758
+ }));
759
+ }
760
+ /**
761
+ * Delete a MetafieldDefinition by ID. Does not delete associated metafields.
762
+ */
763
+ deleteMetafieldDefinition(definitionId) {
764
+ return this.client.mutate(DELETE_METAFIELD_DEFINITION_MUTATION, {
765
+ id: definitionId,
766
+ }).pipe(map$1(response => {
767
+ if (response.metafieldDefinitionDelete.userErrors.length > 0) {
768
+ throw {
769
+ code: 'METAFIELD_DEFINITION_ERROR',
770
+ message: response.metafieldDefinitionDelete.userErrors[0].message,
771
+ errors: response.metafieldDefinitionDelete.userErrors,
772
+ };
773
+ }
774
+ }));
775
+ }
776
+ /**
777
+ * Look up a MetafieldDefinition ID by namespace and key.
778
+ */
779
+ getMetafieldDefinitionId(namespace, key) {
780
+ return this.client.query(GET_METAFIELD_DEFINITION_QUERY, {
781
+ ownerType: 'SHOP',
782
+ namespace,
783
+ key,
784
+ }).pipe(map$1(response => {
785
+ if (!response.metafieldDefinition) {
786
+ throw {
787
+ code: 'DEFINITION_NOT_FOUND',
788
+ message: `MetafieldDefinition not found for ${namespace}.${key}`,
789
+ };
790
+ }
791
+ return response.metafieldDefinition.id;
1233
792
  }));
1234
793
  }
1235
794
  validateSize(data) {
@@ -1249,31 +808,25 @@ class ShopifyMetafieldRepository {
1249
808
  pages: [],
1250
809
  };
1251
810
  }
1252
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ShopifyMetafieldRepository, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1253
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ShopifyMetafieldRepository, providedIn: 'root' });
811
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ShopifyMetafieldRepository, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
812
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ShopifyMetafieldRepository, providedIn: 'root' });
1254
813
  }
1255
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ShopifyMetafieldRepository, decorators: [{
814
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ShopifyMetafieldRepository, decorators: [{
1256
815
  type: Injectable,
1257
816
  args: [{ providedIn: 'root' }]
1258
817
  }] });
1259
818
 
1260
819
  class PageShopifyRepository {
1261
820
  metafieldRepo = inject(ShopifyMetafieldRepository);
1262
- resolveEnvironment(environment) {
1263
- const trimmed = environment?.trim();
1264
- return trimmed ? trimmed : 'production';
1265
- }
1266
- getPages(environment) {
1267
- const env = this.resolveEnvironment(environment);
1268
- return this.metafieldRepo.getPageIndex(env).pipe(map$1(index => {
821
+ getPages() {
822
+ return this.metafieldRepo.getPageIndex().pipe(map$1(index => {
1269
823
  return index.pages
1270
824
  .map(entry => this.mapIndexEntryToSummary(entry))
1271
825
  .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
1272
826
  }));
1273
827
  }
1274
- getPage(id, environment) {
1275
- const env = this.resolveEnvironment(environment);
1276
- return this.metafieldRepo.getPageMetafield(id, env).pipe(map$1(page => {
828
+ getPage(id) {
829
+ return this.metafieldRepo.getPageMetafield(id).pipe(map$1(page => {
1277
830
  if (!page) {
1278
831
  throw {
1279
832
  code: 'PAGE_NOT_FOUND',
@@ -1283,9 +836,8 @@ class PageShopifyRepository {
1283
836
  return page;
1284
837
  }));
1285
838
  }
1286
- getPublishedPageBySlug(slug, environment) {
1287
- const env = this.resolveEnvironment(environment);
1288
- return this.metafieldRepo.getPageIndex(env).pipe(switchMap(index => {
839
+ getPublishedPageBySlug(slug) {
840
+ return this.metafieldRepo.getPageIndex().pipe(switchMap(index => {
1289
841
  const entry = index.pages.find(p => p.slug === slug && p.status === 'published');
1290
842
  if (!entry) {
1291
843
  return throwError(() => ({
@@ -1293,12 +845,11 @@ class PageShopifyRepository {
1293
845
  message: `Published page with slug '${slug}' not found`,
1294
846
  }));
1295
847
  }
1296
- return this.getPage(entry.id, env);
848
+ return this.getPage(entry.id);
1297
849
  }));
1298
850
  }
1299
- createPage(request, environment) {
1300
- const env = this.resolveEnvironment(environment);
1301
- return this.metafieldRepo.getPageIndex(env).pipe(switchMap(index => {
851
+ createPage(request) {
852
+ return this.metafieldRepo.getPageIndex().pipe(switchMap(index => {
1302
853
  this.validateUniqueSlug(request.slug, index);
1303
854
  const newPage = {
1304
855
  id: crypto.randomUUID(),
@@ -1321,15 +872,17 @@ class PageShopifyRepository {
1321
872
  percentUsed: validation.percentUsed,
1322
873
  }));
1323
874
  }
1324
- return this.metafieldRepo.setPageMetafield(newPage.id, newPage, env).pipe(switchMap(() => {
1325
- const updatedIndex = this.addPageToIndex(index, newPage, validation.size);
1326
- return this.metafieldRepo.updatePageIndex(updatedIndex, env);
875
+ return this.metafieldRepo.setPageMetafield(newPage.id, newPage).pipe(switchMap(() => {
876
+ // Create MetafieldDefinition with PUBLIC_READ for Storefront API access
877
+ return this.metafieldRepo.createMetafieldDefinition('kustomizer', `page_${newPage.id}`, `Kustomizer Page: ${newPage.title}`).pipe(catchError(() => of(null)));
878
+ }), switchMap((definitionId) => {
879
+ const updatedIndex = this.addPageToIndex(index, newPage, validation.size, definitionId ?? undefined);
880
+ return this.metafieldRepo.updatePageIndex(updatedIndex);
1327
881
  }), map$1(() => newPage));
1328
882
  }));
1329
883
  }
1330
- updatePage(id, request, environment) {
1331
- const env = this.resolveEnvironment(environment);
1332
- return this.getPage(id, env).pipe(switchMap(currentPage => {
884
+ updatePage(id, request) {
885
+ return this.getPage(id).pipe(switchMap(currentPage => {
1333
886
  if (currentPage.version !== request.version) {
1334
887
  return throwError(() => ({
1335
888
  code: 'VERSION_CONFLICT',
@@ -1337,7 +890,7 @@ class PageShopifyRepository {
1337
890
  currentVersion: currentPage.version,
1338
891
  }));
1339
892
  }
1340
- return this.metafieldRepo.getPageIndex(env).pipe(switchMap(index => {
893
+ return this.metafieldRepo.getPageIndex().pipe(switchMap(index => {
1341
894
  if (request.slug && request.slug !== currentPage.slug) {
1342
895
  this.validateUniqueSlug(request.slug, index, id);
1343
896
  }
@@ -1360,23 +913,30 @@ class PageShopifyRepository {
1360
913
  percentUsed: validation.percentUsed,
1361
914
  }));
1362
915
  }
1363
- return this.metafieldRepo.setPageMetafield(id, updatedPage, env).pipe(switchMap(() => {
916
+ return this.metafieldRepo.setPageMetafield(id, updatedPage).pipe(switchMap(() => {
1364
917
  const updatedIndex = this.updatePageInIndex(index, updatedPage, validation.size);
1365
- return this.metafieldRepo.updatePageIndex(updatedIndex, env);
918
+ return this.metafieldRepo.updatePageIndex(updatedIndex);
1366
919
  }), map$1(() => updatedPage));
1367
920
  }));
1368
921
  }));
1369
922
  }
1370
- deletePage(id, environment) {
1371
- const env = this.resolveEnvironment(environment);
1372
- return this.metafieldRepo.deletePageMetafield(id, env).pipe(switchMap(() => this.metafieldRepo.getPageIndex(env)), switchMap(index => {
1373
- const updatedIndex = this.removePageFromIndex(index, id);
1374
- return this.metafieldRepo.updatePageIndex(updatedIndex, env);
1375
- }), map$1(() => undefined));
923
+ deletePage(id) {
924
+ return this.metafieldRepo.getPageIndex().pipe(switchMap(index => {
925
+ const entry = index.pages.find(p => p.id === id);
926
+ return this.metafieldRepo.deletePageMetafield(id).pipe(switchMap(() => {
927
+ // Delete the MetafieldDefinition if we have its ID
928
+ if (entry?.definitionId) {
929
+ return this.metafieldRepo.deleteMetafieldDefinition(entry.definitionId).pipe(catchError(() => of(undefined)));
930
+ }
931
+ return of(undefined);
932
+ }), switchMap(() => {
933
+ const updatedIndex = this.removePageFromIndex(index, id);
934
+ return this.metafieldRepo.updatePageIndex(updatedIndex);
935
+ }), map$1(() => undefined));
936
+ }));
1376
937
  }
1377
- publishPage(id, request, environment) {
1378
- const env = this.resolveEnvironment(environment);
1379
- return this.getPage(id, env).pipe(switchMap(currentPage => {
938
+ publishPage(id, request) {
939
+ return this.getPage(id).pipe(switchMap(currentPage => {
1380
940
  if (currentPage.version !== request.version) {
1381
941
  return throwError(() => ({
1382
942
  code: 'VERSION_CONFLICT',
@@ -1392,15 +952,14 @@ class PageShopifyRepository {
1392
952
  updatedAt: new Date().toISOString(),
1393
953
  };
1394
954
  const validation = this.metafieldRepo.validateSize(publishedPage);
1395
- return this.metafieldRepo.setPageMetafield(id, publishedPage, env).pipe(switchMap(() => this.metafieldRepo.getPageIndex(env)), switchMap(index => {
955
+ return this.metafieldRepo.setPageMetafield(id, publishedPage).pipe(switchMap(() => this.metafieldRepo.getPageIndex()), switchMap(index => {
1396
956
  const updatedIndex = this.updatePageInIndex(index, publishedPage, validation.size);
1397
- return this.metafieldRepo.updatePageIndex(updatedIndex, env);
957
+ return this.metafieldRepo.updatePageIndex(updatedIndex);
1398
958
  }), map$1(() => publishedPage));
1399
959
  }));
1400
960
  }
1401
- unpublishPage(id, request, environment) {
1402
- const env = this.resolveEnvironment(environment);
1403
- return this.getPage(id, env).pipe(switchMap(currentPage => {
961
+ unpublishPage(id, request) {
962
+ return this.getPage(id).pipe(switchMap(currentPage => {
1404
963
  if (currentPage.version !== request.version) {
1405
964
  return throwError(() => ({
1406
965
  code: 'VERSION_CONFLICT',
@@ -1422,16 +981,15 @@ class PageShopifyRepository {
1422
981
  updatedAt: new Date().toISOString(),
1423
982
  };
1424
983
  const validation = this.metafieldRepo.validateSize(unpublishedPage);
1425
- return this.metafieldRepo.setPageMetafield(id, unpublishedPage, env).pipe(switchMap(() => this.metafieldRepo.getPageIndex(env)), switchMap(index => {
984
+ return this.metafieldRepo.setPageMetafield(id, unpublishedPage).pipe(switchMap(() => this.metafieldRepo.getPageIndex()), switchMap(index => {
1426
985
  const updatedIndex = this.updatePageInIndex(index, unpublishedPage, validation.size);
1427
- return this.metafieldRepo.updatePageIndex(updatedIndex, env);
986
+ return this.metafieldRepo.updatePageIndex(updatedIndex);
1428
987
  }), map$1(() => unpublishedPage));
1429
988
  }));
1430
989
  }
1431
- duplicatePage(id, environment) {
1432
- const env = this.resolveEnvironment(environment);
1433
- return this.getPage(id, env).pipe(switchMap(sourcePage => {
1434
- return this.metafieldRepo.getPageIndex(env).pipe(switchMap(index => {
990
+ duplicatePage(id) {
991
+ return this.getPage(id).pipe(switchMap(sourcePage => {
992
+ return this.metafieldRepo.getPageIndex().pipe(switchMap(index => {
1435
993
  const newSlug = this.generateUniqueSlug(sourcePage.slug, index);
1436
994
  const duplicatedPage = {
1437
995
  ...sourcePage,
@@ -1454,21 +1012,43 @@ class PageShopifyRepository {
1454
1012
  percentUsed: validation.percentUsed,
1455
1013
  }));
1456
1014
  }
1457
- return this.metafieldRepo.setPageMetafield(duplicatedPage.id, duplicatedPage, env).pipe(switchMap(() => {
1458
- const updatedIndex = this.addPageToIndex(index, duplicatedPage, validation.size);
1459
- return this.metafieldRepo.updatePageIndex(updatedIndex, env);
1015
+ return this.metafieldRepo.setPageMetafield(duplicatedPage.id, duplicatedPage).pipe(switchMap(() => {
1016
+ return this.metafieldRepo.createMetafieldDefinition('kustomizer', `page_${duplicatedPage.id}`, `Kustomizer Page: ${duplicatedPage.title}`).pipe(catchError(() => of(null)));
1017
+ }), switchMap((definitionId) => {
1018
+ const updatedIndex = this.addPageToIndex(index, duplicatedPage, validation.size, definitionId ?? undefined);
1019
+ return this.metafieldRepo.updatePageIndex(updatedIndex);
1460
1020
  }), map$1(() => duplicatedPage));
1461
1021
  }));
1462
1022
  }));
1463
1023
  }
1464
- deletePagesByEnvironment(environment) {
1465
- const env = this.resolveEnvironment(environment);
1466
- return this.metafieldRepo.getPageIndex(env).pipe(switchMap(index => {
1467
- const deletions = index.pages.map(entry => this.metafieldRepo.deletePageMetafield(entry.id, env));
1468
- if (deletions.length === 0) {
1469
- return this.metafieldRepo.resetPageIndex(env).pipe(map$1(() => undefined));
1024
+ /**
1025
+ * Backfill MetafieldDefinitions for existing pages that don't have one.
1026
+ * Creates definitions with PUBLIC_READ storefront access.
1027
+ */
1028
+ ensureDefinitionsExist() {
1029
+ // Ensure the pages_index definition exists with PUBLIC_READ
1030
+ return this.metafieldRepo.createMetafieldDefinition('kustomizer', 'pages_index', 'Kustomizer Pages Index').pipe(catchError(() => of(null)), switchMap(() => this.metafieldRepo.getPageIndex()), switchMap(index => {
1031
+ const pagesWithoutDefinition = index.pages.filter(p => !p.definitionId);
1032
+ if (pagesWithoutDefinition.length === 0) {
1033
+ return of({ created: 0, skipped: index.pages.length, index: null });
1034
+ }
1035
+ // Create definitions sequentially to avoid rate limits
1036
+ let chain$ = of({ created: 0, skipped: 0, updatedPages: [...index.pages] });
1037
+ for (const page of pagesWithoutDefinition) {
1038
+ chain$ = chain$.pipe(switchMap(acc => {
1039
+ return this.metafieldRepo.createMetafieldDefinition('kustomizer', `page_${page.id}`, `Kustomizer Page: ${page.title}`).pipe(map$1(definitionId => {
1040
+ const updatedPages = acc.updatedPages.map(p => p.id === page.id ? { ...p, definitionId } : p);
1041
+ return { created: acc.created + 1, skipped: acc.skipped, updatedPages };
1042
+ }), catchError(() => of({ ...acc, skipped: acc.skipped + 1 })));
1043
+ }));
1470
1044
  }
1471
- return forkJoin(deletions).pipe(switchMap(() => this.metafieldRepo.resetPageIndex(env)), map$1(() => undefined));
1045
+ return chain$.pipe(switchMap(result => {
1046
+ const updatedIndex = { ...index, pages: result.updatedPages };
1047
+ if (result.created > 0) {
1048
+ return this.metafieldRepo.updatePageIndex(updatedIndex).pipe(map$1(() => ({ created: result.created, skipped: result.skipped })));
1049
+ }
1050
+ return of({ created: result.created, skipped: result.skipped });
1051
+ }));
1472
1052
  }));
1473
1053
  }
1474
1054
  validateUniqueSlug(slug, index, excludeId) {
@@ -1489,7 +1069,7 @@ class PageShopifyRepository {
1489
1069
  }
1490
1070
  return newSlug;
1491
1071
  }
1492
- addPageToIndex(index, page, size) {
1072
+ addPageToIndex(index, page, size, definitionId) {
1493
1073
  const entry = {
1494
1074
  id: page.id,
1495
1075
  slug: page.slug,
@@ -1498,6 +1078,7 @@ class PageShopifyRepository {
1498
1078
  updatedAt: page.updatedAt,
1499
1079
  publishedAt: page.publishedAt,
1500
1080
  size,
1081
+ ...(definitionId ? { definitionId } : {}),
1501
1082
  };
1502
1083
  return {
1503
1084
  ...index,
@@ -1540,10 +1121,10 @@ class PageShopifyRepository {
1540
1121
  publishedAt: entry.publishedAt,
1541
1122
  };
1542
1123
  }
1543
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: PageShopifyRepository, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1544
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: PageShopifyRepository, providedIn: 'root' });
1124
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: PageShopifyRepository, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1125
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: PageShopifyRepository, providedIn: 'root' });
1545
1126
  }
1546
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: PageShopifyRepository, decorators: [{
1127
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: PageShopifyRepository, decorators: [{
1547
1128
  type: Injectable,
1548
1129
  args: [{ providedIn: 'root' }]
1549
1130
  }] });
@@ -1556,127 +1137,95 @@ const USE_IN_MEMORY_PAGES = new InjectionToken('USE_IN_MEMORY_PAGES', {
1556
1137
  providedIn: 'root',
1557
1138
  factory: () => false,
1558
1139
  });
1559
- /**
1560
- * Injection token for page environment (metafield key prefix).
1561
- * Default is 'production'.
1562
- */
1563
- const PAGE_ENVIRONMENT = new InjectionToken('PAGE_ENVIRONMENT', {
1564
- providedIn: 'root',
1565
- factory: () => 'production',
1566
- });
1567
1140
  class PageService {
1568
1141
  useInMemory = inject(USE_IN_MEMORY_PAGES);
1569
- pageEnvironment = inject(PAGE_ENVIRONMENT);
1570
1142
  memoryRepo = inject(PageMemoryRepository);
1571
1143
  shopifyRepo = inject(PageShopifyRepository);
1572
- resolveEnvironment(environment) {
1573
- const trimmed = environment?.trim();
1574
- return trimmed ? trimmed : this.pageEnvironment;
1575
- }
1576
1144
  /**
1577
1145
  * Get all pages (summaries only for listing)
1578
1146
  */
1579
- getPages(environment) {
1580
- const env = this.resolveEnvironment(environment);
1147
+ getPages() {
1581
1148
  if (this.useInMemory) {
1582
- return this.memoryRepo.getPages(env);
1149
+ return this.memoryRepo.getPages();
1583
1150
  }
1584
- return this.shopifyRepo.getPages(env);
1151
+ return this.shopifyRepo.getPages();
1585
1152
  }
1586
1153
  /**
1587
1154
  * Get a single page by ID (full content)
1588
1155
  */
1589
- getPage(id, environment) {
1590
- const env = this.resolveEnvironment(environment);
1156
+ getPage(id) {
1591
1157
  if (this.useInMemory) {
1592
- return this.memoryRepo.getPage(id, env);
1158
+ return this.memoryRepo.getPage(id);
1593
1159
  }
1594
- return this.shopifyRepo.getPage(id, env);
1160
+ return this.shopifyRepo.getPage(id);
1595
1161
  }
1596
1162
  /**
1597
1163
  * Get a published page by slug (for public rendering)
1598
1164
  */
1599
- getPublishedPageBySlug(slug, environment) {
1600
- const env = this.resolveEnvironment(environment);
1165
+ getPublishedPageBySlug(slug) {
1601
1166
  if (this.useInMemory) {
1602
- return this.memoryRepo.getPublishedPageBySlug(slug, env);
1167
+ return this.memoryRepo.getPublishedPageBySlug(slug);
1603
1168
  }
1604
- return this.shopifyRepo.getPublishedPageBySlug(slug, env);
1169
+ return this.shopifyRepo.getPublishedPageBySlug(slug);
1605
1170
  }
1606
1171
  /**
1607
1172
  * Create a new page
1608
1173
  */
1609
- createPage(request, environment) {
1610
- const env = this.resolveEnvironment(environment);
1174
+ createPage(request) {
1611
1175
  if (this.useInMemory) {
1612
- return this.memoryRepo.createPage(request, env);
1176
+ return this.memoryRepo.createPage(request);
1613
1177
  }
1614
- return this.shopifyRepo.createPage(request, env);
1178
+ return this.shopifyRepo.createPage(request);
1615
1179
  }
1616
1180
  /**
1617
1181
  * Update an existing page
1618
1182
  */
1619
- updatePage(id, request, environment) {
1620
- const env = this.resolveEnvironment(environment);
1183
+ updatePage(id, request) {
1621
1184
  if (this.useInMemory) {
1622
- return this.memoryRepo.updatePage(id, request, env);
1185
+ return this.memoryRepo.updatePage(id, request);
1623
1186
  }
1624
- return this.shopifyRepo.updatePage(id, request, env);
1187
+ return this.shopifyRepo.updatePage(id, request);
1625
1188
  }
1626
1189
  /**
1627
1190
  * Delete a page
1628
1191
  */
1629
- deletePage(id, environment) {
1630
- const env = this.resolveEnvironment(environment);
1192
+ deletePage(id) {
1631
1193
  if (this.useInMemory) {
1632
- return this.memoryRepo.deletePage(id, env);
1194
+ return this.memoryRepo.deletePage(id);
1633
1195
  }
1634
- return this.shopifyRepo.deletePage(id, env);
1196
+ return this.shopifyRepo.deletePage(id);
1635
1197
  }
1636
1198
  /**
1637
1199
  * Publish a page (changes status to 'published')
1638
1200
  */
1639
- publishPage(id, request, environment) {
1640
- const env = this.resolveEnvironment(environment);
1201
+ publishPage(id, request) {
1641
1202
  if (this.useInMemory) {
1642
- return this.memoryRepo.publishPage(id, request, env);
1203
+ return this.memoryRepo.publishPage(id, request);
1643
1204
  }
1644
- return this.shopifyRepo.publishPage(id, request, env);
1205
+ return this.shopifyRepo.publishPage(id, request);
1645
1206
  }
1646
1207
  /**
1647
1208
  * Unpublish a page (changes status to 'draft')
1648
1209
  */
1649
- unpublishPage(id, request, environment) {
1650
- const env = this.resolveEnvironment(environment);
1210
+ unpublishPage(id, request) {
1651
1211
  if (this.useInMemory) {
1652
- return this.memoryRepo.unpublishPage(id, request, env);
1212
+ return this.memoryRepo.unpublishPage(id, request);
1653
1213
  }
1654
- return this.shopifyRepo.unpublishPage(id, request, env);
1214
+ return this.shopifyRepo.unpublishPage(id, request);
1655
1215
  }
1656
1216
  /**
1657
1217
  * Duplicate an existing page
1658
1218
  */
1659
- duplicatePage(id, environment) {
1660
- const env = this.resolveEnvironment(environment);
1661
- if (this.useInMemory) {
1662
- return this.memoryRepo.duplicatePage(id, env);
1663
- }
1664
- return this.shopifyRepo.duplicatePage(id, env);
1665
- }
1666
- /**
1667
- * Delete all pages for a given environment
1668
- */
1669
- deletePagesByEnvironment(environment) {
1670
- const env = this.resolveEnvironment(environment);
1219
+ duplicatePage(id) {
1671
1220
  if (this.useInMemory) {
1672
- return this.memoryRepo.deletePagesByEnvironment(env);
1221
+ return this.memoryRepo.duplicatePage(id);
1673
1222
  }
1674
- return this.shopifyRepo.deletePagesByEnvironment(env);
1223
+ return this.shopifyRepo.duplicatePage(id);
1675
1224
  }
1676
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: PageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1677
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: PageService, providedIn: 'root' });
1225
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: PageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1226
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: PageService, providedIn: 'root' });
1678
1227
  }
1679
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: PageService, decorators: [{
1228
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: PageService, decorators: [{
1680
1229
  type: Injectable,
1681
1230
  args: [{ providedIn: 'root' }]
1682
1231
  }] });
@@ -1699,10 +1248,10 @@ class RoutePageLoadingStrategy extends PageLoadingStrategy {
1699
1248
  getPageIdToLoad() {
1700
1249
  return this.navigation.getCurrentPageId();
1701
1250
  }
1702
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: RoutePageLoadingStrategy, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
1703
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: RoutePageLoadingStrategy });
1251
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: RoutePageLoadingStrategy, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
1252
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: RoutePageLoadingStrategy });
1704
1253
  }
1705
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: RoutePageLoadingStrategy, decorators: [{
1254
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: RoutePageLoadingStrategy, decorators: [{
1706
1255
  type: Injectable
1707
1256
  }] });
1708
1257
  /**
@@ -1725,10 +1274,10 @@ class InputPageLoadingStrategy extends PageLoadingStrategy {
1725
1274
  getPageIdToLoad() {
1726
1275
  return this.pageId$.asObservable();
1727
1276
  }
1728
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: InputPageLoadingStrategy, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
1729
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: InputPageLoadingStrategy });
1277
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: InputPageLoadingStrategy, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
1278
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: InputPageLoadingStrategy });
1730
1279
  }
1731
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: InputPageLoadingStrategy, decorators: [{
1280
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: InputPageLoadingStrategy, decorators: [{
1732
1281
  type: Injectable
1733
1282
  }] });
1734
1283
 
@@ -2795,10 +2344,10 @@ class ComponentRegistryService {
2795
2344
  }
2796
2345
  return errors;
2797
2346
  }
2798
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ComponentRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2799
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ComponentRegistryService, providedIn: 'root' });
2347
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComponentRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2348
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComponentRegistryService, providedIn: 'root' });
2800
2349
  }
2801
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ComponentRegistryService, decorators: [{
2350
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComponentRegistryService, decorators: [{
2802
2351
  type: Injectable,
2803
2352
  args: [{ providedIn: 'root' }]
2804
2353
  }], ctorParameters: () => [] });
@@ -2904,8 +2453,8 @@ class DynamicRendererComponent {
2904
2453
  }
2905
2454
  this.viewContainerRef.clear();
2906
2455
  }
2907
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: DynamicRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2908
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: DynamicRendererComponent, isStandalone: true, selector: "lib-dynamic-renderer", inputs: { element: { classPropertyName: "element", publicName: "element", isSignal: true, isRequired: true, transformFunction: null }, context: { classPropertyName: "context", publicName: "context", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
2456
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DynamicRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2457
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: DynamicRendererComponent, isStandalone: true, selector: "lib-dynamic-renderer", inputs: { element: { classPropertyName: "element", publicName: "element", isSignal: true, isRequired: true, transformFunction: null }, context: { classPropertyName: "context", publicName: "context", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
2909
2458
  @if (error()) {
2910
2459
  <div class="dynamic-renderer-error">
2911
2460
  <span>Unknown component type: {{ element().type }}</span>
@@ -2913,7 +2462,7 @@ class DynamicRendererComponent {
2913
2462
  }
2914
2463
  `, isInline: true, styles: [":host{display:contents}.dynamic-renderer-error{padding:1rem;background:#fee2e2;border:1px dashed #ef4444;color:#dc2626;border-radius:4px;font-size:.875rem}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2915
2464
  }
2916
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: DynamicRendererComponent, decorators: [{
2465
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DynamicRendererComponent, decorators: [{
2917
2466
  type: Component,
2918
2467
  args: [{ selector: 'lib-dynamic-renderer', template: `
2919
2468
  @if (error()) {
@@ -2961,8 +2510,8 @@ class SlotRendererComponent {
2961
2510
  return true;
2962
2511
  });
2963
2512
  }, ...(ngDevMode ? [{ debugName: "filteredChildren" }] : []));
2964
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SlotRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2965
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: SlotRendererComponent, isStandalone: true, selector: "lib-slot-renderer", inputs: { slot: { classPropertyName: "slot", publicName: "slot", isSignal: true, isRequired: true, transformFunction: null }, children: { classPropertyName: "children", publicName: "children", isSignal: true, isRequired: true, transformFunction: null }, parentElementId: { classPropertyName: "parentElementId", publicName: "parentElementId", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: `
2513
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: SlotRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2514
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: SlotRendererComponent, isStandalone: true, selector: "lib-slot-renderer", inputs: { slot: { classPropertyName: "slot", publicName: "slot", isSignal: true, isRequired: true, transformFunction: null }, children: { classPropertyName: "children", publicName: "children", isSignal: true, isRequired: true, transformFunction: null }, parentElementId: { classPropertyName: "parentElementId", publicName: "parentElementId", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: `
2966
2515
  @if (filteredChildren().length === 0 && slot().emptyPlaceholder) {
2967
2516
  <div class="slot-empty" [attr.data-slot]="slot().name">
2968
2517
  {{ slot().emptyPlaceholder }}
@@ -2977,7 +2526,7 @@ class SlotRendererComponent {
2977
2526
  }
2978
2527
  `, isInline: true, styles: [":host{display:contents}.slot-empty{padding:2rem;border:2px dashed #d1d5db;color:#9ca3af;text-align:center;border-radius:4px;font-size:.875rem}\n"], dependencies: [{ kind: "component", type: DynamicRendererComponent, selector: "lib-dynamic-renderer", inputs: ["element", "context"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2979
2528
  }
2980
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SlotRendererComponent, decorators: [{
2529
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: SlotRendererComponent, decorators: [{
2981
2530
  type: Component,
2982
2531
  args: [{ selector: 'lib-slot-renderer', imports: [DynamicRendererComponent], template: `
2983
2532
  @if (filteredChildren().length === 0 && slot().emptyPlaceholder) {
@@ -3533,7 +3082,7 @@ class VisualEditorFacade {
3533
3082
  version: currentPage.version,
3534
3083
  };
3535
3084
  console.log('Saving page:', request);
3536
- return this.pageService.updatePage(pageId, request).pipe(tap$1((page) => {
3085
+ return this.pageService.updatePage(pageId, request).pipe(tap((page) => {
3537
3086
  this.store.dispatch(VisualEditorActions.updatePageContext({
3538
3087
  context: { version: page.version },
3539
3088
  }));
@@ -3552,7 +3101,7 @@ class VisualEditorFacade {
3552
3101
  ...metadata,
3553
3102
  version: currentPage.version,
3554
3103
  };
3555
- return this.pageService.updatePage(pageId, request).pipe(tap$1((page) => {
3104
+ return this.pageService.updatePage(pageId, request).pipe(tap((page) => {
3556
3105
  this.store.dispatch(VisualEditorActions.updatePageContext({
3557
3106
  context: {
3558
3107
  title: page.title,
@@ -3576,7 +3125,7 @@ class VisualEditorFacade {
3576
3125
  if (!currentPage) {
3577
3126
  throw new Error('No page loaded in editor');
3578
3127
  }
3579
- return this.pageService.publishPage(pageId, { version: currentPage.version }).pipe(tap$1((page) => {
3128
+ return this.pageService.publishPage(pageId, { version: currentPage.version }).pipe(tap((page) => {
3580
3129
  this.store.dispatch(VisualEditorActions.updatePageContext({
3581
3130
  context: { status: page.status, version: page.version },
3582
3131
  }));
@@ -3590,7 +3139,7 @@ class VisualEditorFacade {
3590
3139
  if (!currentPage) {
3591
3140
  throw new Error('No page loaded in editor');
3592
3141
  }
3593
- return this.pageService.unpublishPage(pageId, { version: currentPage.version }).pipe(tap$1((page) => {
3142
+ return this.pageService.unpublishPage(pageId, { version: currentPage.version }).pipe(tap((page) => {
3594
3143
  this.store.dispatch(VisualEditorActions.updatePageContext({
3595
3144
  context: { status: page.status, version: page.version },
3596
3145
  }));
@@ -3620,25 +3169,108 @@ class VisualEditorFacade {
3620
3169
  markClean() {
3621
3170
  this.store.dispatch(VisualEditorActions.markClean());
3622
3171
  }
3623
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: VisualEditorFacade, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
3624
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: VisualEditorFacade, providedIn: 'root' });
3172
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: VisualEditorFacade, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
3173
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: VisualEditorFacade, providedIn: 'root' });
3625
3174
  }
3626
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: VisualEditorFacade, decorators: [{
3175
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: VisualEditorFacade, decorators: [{
3627
3176
  type: Injectable,
3628
3177
  args: [{ providedIn: 'root' }]
3629
3178
  }] });
3630
3179
 
3631
- const MAX_DEPTH = 12;
3632
- class DragDropService {
3633
- store = inject(Store);
3634
- registry = inject(ComponentRegistryService);
3635
- sections = toSignal(this.store.select(selectSections), { initialValue: [] });
3636
- dragItem = signal(null, ...(ngDevMode ? [{ debugName: "dragItem" }] : []));
3637
- dropTarget = signal(null, ...(ngDevMode ? [{ debugName: "dropTarget" }] : []));
3638
- isDragging = computed(() => this.dragItem() !== null, ...(ngDevMode ? [{ debugName: "isDragging" }] : []));
3639
- startDrag(item, event) {
3640
- this.dragItem.set(item);
3641
- this.store.dispatch(VisualEditorActions.startDrag({ elementId: item.elementId }));
3180
+ /**
3181
+ * Manages postMessage communication between the editor (parent) and the
3182
+ * storefront preview (iframe).
3183
+ */
3184
+ class IframeBridgeService {
3185
+ zone = inject(NgZone);
3186
+ iframe = null;
3187
+ origin = '*';
3188
+ messages$ = new Subject();
3189
+ messageHandler = (event) => this.handleMessage(event);
3190
+ constructor() {
3191
+ this.zone.runOutsideAngular(() => {
3192
+ window.addEventListener('message', this.messageHandler);
3193
+ });
3194
+ }
3195
+ ngOnDestroy() {
3196
+ window.removeEventListener('message', this.messageHandler);
3197
+ this.messages$.complete();
3198
+ }
3199
+ /**
3200
+ * Connect to an iframe element. Must be called after the iframe loads.
3201
+ */
3202
+ connect(iframe, origin) {
3203
+ this.iframe = iframe;
3204
+ this.origin = origin || '*';
3205
+ }
3206
+ /**
3207
+ * Disconnect from the current iframe.
3208
+ */
3209
+ disconnect() {
3210
+ this.iframe = null;
3211
+ }
3212
+ // ─── Outbound (Parent → Iframe) ──────────────────────────────────────────
3213
+ sendPageUpdate(sections) {
3214
+ this.send({ type: 'kustomizer:page-update', sections });
3215
+ }
3216
+ sendSelectElement(elementId) {
3217
+ this.send({ type: 'kustomizer:select-element', elementId });
3218
+ }
3219
+ sendDeselect() {
3220
+ this.send({ type: 'kustomizer:deselect' });
3221
+ }
3222
+ // ─── Inbound (Iframe → Parent) ───────────────────────────────────────────
3223
+ onReady() {
3224
+ return this.messages$.pipe(filter((msg) => msg.type === 'kustomizer:ready'), map(() => undefined));
3225
+ }
3226
+ onElementClicked() {
3227
+ return this.messages$.pipe(filter((msg) => msg.type === 'kustomizer:element-clicked'), map(({ elementId, sectionId }) => ({ elementId, sectionId })));
3228
+ }
3229
+ onElementHover() {
3230
+ return this.messages$.pipe(filter((msg) => msg.type === 'kustomizer:element-hover'), map(({ elementId }) => elementId));
3231
+ }
3232
+ onElementMoved() {
3233
+ return this.messages$.pipe(filter((msg) => msg.type === 'kustomizer:element-moved'), map(({ elementId, newIndex, targetSectionId }) => ({
3234
+ elementId,
3235
+ newIndex,
3236
+ targetSectionId,
3237
+ })));
3238
+ }
3239
+ // ─── Private ─────────────────────────────────────────────────────────────
3240
+ send(message) {
3241
+ if (!this.iframe?.contentWindow)
3242
+ return;
3243
+ this.iframe.contentWindow.postMessage(message, this.origin);
3244
+ }
3245
+ handleMessage(event) {
3246
+ const data = event.data;
3247
+ if (!data?.type?.startsWith('kustomizer:'))
3248
+ return;
3249
+ // Only accept messages from the connected iframe's origin
3250
+ if (this.origin !== '*' && event.origin !== this.origin)
3251
+ return;
3252
+ this.zone.run(() => {
3253
+ this.messages$.next(data);
3254
+ });
3255
+ }
3256
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: IframeBridgeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
3257
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: IframeBridgeService });
3258
+ }
3259
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: IframeBridgeService, decorators: [{
3260
+ type: Injectable
3261
+ }], ctorParameters: () => [] });
3262
+
3263
+ const MAX_DEPTH = 12;
3264
+ class DragDropService {
3265
+ store = inject(Store);
3266
+ registry = inject(ComponentRegistryService);
3267
+ sections = toSignal(this.store.select(selectSections), { initialValue: [] });
3268
+ dragItem = signal(null, ...(ngDevMode ? [{ debugName: "dragItem" }] : []));
3269
+ dropTarget = signal(null, ...(ngDevMode ? [{ debugName: "dropTarget" }] : []));
3270
+ isDragging = computed(() => this.dragItem() !== null, ...(ngDevMode ? [{ debugName: "isDragging" }] : []));
3271
+ startDrag(item, event) {
3272
+ this.dragItem.set(item);
3273
+ this.store.dispatch(VisualEditorActions.startDrag({ elementId: item.elementId }));
3642
3274
  if (event.dataTransfer) {
3643
3275
  event.dataTransfer.effectAllowed = 'move';
3644
3276
  event.dataTransfer.setData('text/plain', item.elementId);
@@ -3870,10 +3502,10 @@ class DragDropService {
3870
3502
  return false;
3871
3503
  return a.every((id, i) => id === b[i]);
3872
3504
  }
3873
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: DragDropService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
3874
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: DragDropService, providedIn: 'root' });
3505
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DragDropService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
3506
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DragDropService, providedIn: 'root' });
3875
3507
  }
3876
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: DragDropService, decorators: [{
3508
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: DragDropService, decorators: [{
3877
3509
  type: Injectable,
3878
3510
  args: [{ providedIn: 'root' }]
3879
3511
  }] });
@@ -3885,7 +3517,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
3885
3517
  class BlockTreeItemComponent {
3886
3518
  facade = inject(VisualEditorFacade);
3887
3519
  dndService = inject(DragDropService);
3888
- sanitizer = inject(DomSanitizer);
3889
3520
  treeBlock = viewChild('treeBlock', ...(ngDevMode ? [{ debugName: "treeBlock" }] : []));
3890
3521
  block = input.required(...(ngDevMode ? [{ debugName: "block" }] : []));
3891
3522
  context = input.required(...(ngDevMode ? [{ debugName: "context" }] : []));
@@ -3913,20 +3544,14 @@ class BlockTreeItemComponent {
3913
3544
  }), ...(ngDevMode ? [{ debugName: "childContext" }] : []));
3914
3545
  blockIcon = computed(() => {
3915
3546
  const iconMap = {
3916
- 'text-block': '<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"><path d="M4 3h12a1 1 0 011 1v1a1 1 0 01-1 1h-5v10a1 1 0 01-2 0V6H4a1 1 0 01-1-1V4a1 1 0 011-1z"/></svg>',
3917
- 'button-block': '<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"><path d="M3 6a2 2 0 012-2h10a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2V6zm4 4a1 1 0 100 2h6a1 1 0 100-2H7z"/></svg>',
3918
- 'image-block': '<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"><path d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6zM6 7a1 1 0 110-2 1 1 0 010 2z"/></svg>',
3919
- 'icon-block': '<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"><path d="M10 2l2.39 4.84L18 7.77l-4 3.9.94 5.5L10 14.27l-4.94 2.9.94-5.5-4-3.9 5.61-.93L10 2z"/></svg>',
3920
- 'feature-item': '<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"><path d="M10 2l2.39 4.84L18 7.77l-4 3.9.94 5.5L10 14.27l-4.94 2.9.94-5.5-4-3.9 5.61-.93L10 2z"/></svg>',
3921
- 'card-block': '<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"><path d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm0 2h12v4H4V5zm0 6h8v4H4v-4z"/></svg>',
3922
- 'video-block': '<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"><path d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm4 3.5l5 3.5-5 3.5v-7z"/></svg>',
3923
- 'picture-block': '<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"><path d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6zM6 7a1 1 0 110-2 1 1 0 010 2z"/></svg>',
3924
- 'flex-layout-block': '<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"><path d="M3 3h4v14H3V3zm5 0h4v14H8V3zm5 0h4v14h-4V3z"/></svg>',
3547
+ 'text-block': '📝',
3548
+ 'button-block': '🔘',
3549
+ 'image-block': '🖼️',
3550
+ 'icon-block': '',
3551
+ 'feature-item': '',
3552
+ 'card-block': '🃏',
3925
3553
  };
3926
- const def = this.facade.getDefinition(this.block().type);
3927
- const raw = iconMap[this.block().type] ?? def?.icon ?? '<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"><path d="M10 2l1.5 3h3.5l-2.5 2.5 1 3.5-3.5-2-3.5 2 1-3.5L4 5h3.5L10 2zm-6 10h12v2H4v-2zm2 4h8v2H6v-2z"/></svg>';
3928
- const html = raw.trim().startsWith('<') ? raw : `<span style="font-family:'Material Icons',sans-serif;font-weight:normal;font-style:normal;line-height:1;letter-spacing:normal;text-transform:none;white-space:nowrap;direction:ltr;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility">${raw}</span>`;
3929
- return this.sanitizer.bypassSecurityTrustHtml(html);
3554
+ return iconMap[this.block().type] ?? '🧩';
3930
3555
  }, ...(ngDevMode ? [{ debugName: "blockIcon" }] : []));
3931
3556
  blockName = computed(() => {
3932
3557
  const block = this.block();
@@ -4088,8 +3713,8 @@ class BlockTreeItemComponent {
4088
3713
  zone,
4089
3714
  };
4090
3715
  }
4091
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: BlockTreeItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4092
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: BlockTreeItemComponent, isStandalone: true, selector: "lib-block-tree-item", inputs: { block: { classPropertyName: "block", publicName: "block", isSignal: true, isRequired: true, transformFunction: null }, context: { classPropertyName: "context", publicName: "context", isSignal: true, isRequired: true, transformFunction: null }, index: { classPropertyName: "index", publicName: "index", isSignal: true, isRequired: true, transformFunction: null }, totalSiblings: { classPropertyName: "totalSiblings", publicName: "totalSiblings", isSignal: true, isRequired: true, transformFunction: null }, expandAll: { classPropertyName: "expandAll", publicName: "expandAll", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectBlock: "selectBlock", deleteBlock: "deleteBlock", duplicateBlock: "duplicateBlock", moveBlock: "moveBlock", openBlockPicker: "openBlockPicker" }, viewQueries: [{ propertyName: "treeBlock", first: true, predicate: ["treeBlock"], descendants: true, isSignal: true }], ngImport: i0, template: `
3716
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: BlockTreeItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3717
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: BlockTreeItemComponent, isStandalone: true, selector: "lib-block-tree-item", inputs: { block: { classPropertyName: "block", publicName: "block", isSignal: true, isRequired: true, transformFunction: null }, context: { classPropertyName: "context", publicName: "context", isSignal: true, isRequired: true, transformFunction: null }, index: { classPropertyName: "index", publicName: "index", isSignal: true, isRequired: true, transformFunction: null }, totalSiblings: { classPropertyName: "totalSiblings", publicName: "totalSiblings", isSignal: true, isRequired: true, transformFunction: null }, expandAll: { classPropertyName: "expandAll", publicName: "expandAll", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectBlock: "selectBlock", deleteBlock: "deleteBlock", duplicateBlock: "duplicateBlock", moveBlock: "moveBlock", openBlockPicker: "openBlockPicker" }, viewQueries: [{ propertyName: "treeBlock", first: true, predicate: ["treeBlock"], descendants: true, isSignal: true }], ngImport: i0, template: `
4093
3718
  <div
4094
3719
  class="tree-block"
4095
3720
  #treeBlock
@@ -4123,7 +3748,7 @@ class BlockTreeItemComponent {
4123
3748
  <span class="block-indent"></span>
4124
3749
  }
4125
3750
  <span class="icon-drag-wrapper">
4126
- <span class="block-icon" [innerHTML]="blockIcon()"></span>
3751
+ <span class="block-icon">{{ blockIcon() }}</span>
4127
3752
  <span
4128
3753
  class="drag-handle"
4129
3754
  role="button"
@@ -4192,9 +3817,9 @@ class BlockTreeItemComponent {
4192
3817
  }
4193
3818
  </div>
4194
3819
  }
4195
- `, isInline: true, styles: [":host{display:block;font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif}.tree-block{display:flex;align-items:center;gap:.375rem;padding:.375rem .75rem .375rem .5rem;cursor:pointer;transition:background .15s;position:relative}.tree-block:hover{background:#f6f6f7;border-radius:8px;margin-right:.375rem}.tree-block.selected{background:#005bd3;color:#fff;border-radius:8px;margin-right:.375rem}.tree-block.selected .block-icon,.tree-block.selected .block-name,.tree-block.selected .expand-btn,.tree-block.selected .drag-handle,.tree-block.selected .tree-btn{color:inherit}.tree-block.drag-over-before{box-shadow:inset 0 2px #005bd3}.tree-block.drag-over-inside{background:#005bd314;box-shadow:inset 0 0 0 2px #005bd3}.tree-block.drag-over-after{box-shadow:inset 0 -2px #005bd3}.tree-block.is-dragging{opacity:.4;cursor:grabbing}.drag-handle{cursor:grab;color:#8c9196;opacity:0;transition:opacity .15s;display:flex;align-items:center;justify-content:center;flex-shrink:0;padding:.125rem;border:none;background:none;border-radius:4px}.drag-handle:hover{color:#303030;background:#e3e3e3}.drag-handle:focus-visible{opacity:1;outline:2px solid #005bd3;outline-offset:1px}.tree-block:hover .drag-handle{opacity:1}@media(prefers-reduced-motion:reduce){.tree-block,.drag-handle,.tree-actions,.expand-icon{transition:none}}.expand-btn{width:16px;height:16px;padding:0;border:none;background:none;cursor:pointer;display:flex;align-items:center;justify-content:center;color:#8c9196;flex-shrink:0}.expand-btn.expanded .expand-icon{transform:rotate(90deg)}.expand-icon{font-size:.5rem;transition:transform .15s}.block-indent{width:18px;height:1px;background:#e3e3e3;flex-shrink:0}.icon-drag-wrapper{position:relative;display:flex;align-items:center;justify-content:center;flex-shrink:0}.icon-drag-wrapper .drag-handle{position:absolute;inset:0;display:flex;align-items:center;justify-content:center}.tree-block:hover .icon-drag-wrapper .block-icon,.icon-drag-wrapper:has(.drag-handle:focus-visible) .block-icon{visibility:hidden}.block-icon{font-size:.875rem;color:#8c9196;flex-shrink:0;display:flex;align-items:center}.block-name{flex:1;font-size:.8125rem;color:#303030;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tree-actions{display:flex;gap:.125rem;opacity:0;transition:opacity .15s}.tree-block:hover .tree-actions{opacity:1}.tree-btn{padding:.125rem .375rem;border:none;border-radius:4px;background:transparent;cursor:pointer;font-size:.6875rem;color:#8c9196;transition:all .15s}.tree-btn:hover{background:#e3e3e3;color:#303030}.tree-btn.delete:hover{background:#fef2f0;color:#d72c0d}.material-icon{font-family:Material Icons,sans-serif;font-size:inherit;font-weight:400;font-style:normal;line-height:1;letter-spacing:normal;text-transform:none;white-space:nowrap;word-wrap:normal;direction:ltr;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}.nested-content{border-left:1px solid #e3e3e3;margin-left:1.25rem}.add-block-btn{display:flex;align-items:center;gap:.375rem;width:calc(100% - 1rem);margin:.25rem .5rem;padding:.375rem .5rem;border:none;border-radius:8px;background:transparent;cursor:pointer;font-size:.8125rem;font-family:inherit;color:#005bd3;transition:all .15s}.add-block-btn:hover{background:#f0f5ff;color:#004299}.add-icon{font-size:.8125rem;font-weight:600}.add-icon-circle{display:flex;align-items:center;justify-content:center}\n"], dependencies: [{ kind: "component", type: BlockTreeItemComponent, selector: "lib-block-tree-item", inputs: ["block", "context", "index", "totalSiblings", "expandAll"], outputs: ["selectBlock", "deleteBlock", "duplicateBlock", "moveBlock", "openBlockPicker"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3820
+ `, isInline: true, styles: [":host{display:block}.tree-block{display:flex;align-items:center;gap:.375rem;padding:.5rem .75rem .5rem .5rem;cursor:pointer;transition:background .2s;position:relative}.tree-block:hover{background:#f5f5f5;border-radius:8px;margin-right:.375rem}.tree-block.selected{background:#005bd3;color:#fff;border-radius:8px;margin-right:.375rem}.tree-block.selected .block-icon,.tree-block.selected .block-name,.tree-block.selected .expand-btn,.tree-block.selected .drag-handle,.tree-block.selected .tree-btn{color:inherit}.tree-block.drag-over-before{box-shadow:inset 0 2px #4a90d9}.tree-block.drag-over-inside{background:#4a90d91a;box-shadow:inset 0 0 0 2px #4a90d9}.tree-block.drag-over-after{box-shadow:inset 0 -2px #4a90d9}.tree-block.is-dragging{opacity:.4;cursor:grabbing}.drag-handle{cursor:grab;color:#999;opacity:0;transition:opacity .2s;display:flex;align-items:center;justify-content:center;flex-shrink:0;padding:.125rem;border:none;background:none;border-radius:2px}.drag-handle:hover{color:#666;background:#e0e0e0}.drag-handle:focus-visible{opacity:1;outline:2px solid #4a90d9;outline-offset:1px}.tree-block:hover .drag-handle{opacity:1}@media(prefers-reduced-motion:reduce){.tree-block,.drag-handle,.tree-actions,.expand-icon{transition:none}}.expand-btn{width:16px;height:16px;padding:0;border:none;background:none;cursor:pointer;display:flex;align-items:center;justify-content:center;color:#666;flex-shrink:0}.expand-btn.expanded .expand-icon{transform:rotate(90deg)}.expand-icon{font-size:.5rem;transition:transform .2s}.block-indent{width:18px;height:1px;background:#ddd;flex-shrink:0}.icon-drag-wrapper{position:relative;display:flex;align-items:center;justify-content:center;flex-shrink:0}.icon-drag-wrapper .drag-handle{position:absolute;inset:0;display:flex;align-items:center;justify-content:center}.tree-block:hover .icon-drag-wrapper .block-icon,.icon-drag-wrapper:has(.drag-handle:focus-visible) .block-icon{visibility:hidden}.block-icon{font-size:.875rem;flex-shrink:0}.block-name{flex:1;font-size:.75rem;color:#555;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tree-actions{display:flex;gap:.125rem;opacity:0;transition:opacity .2s}.tree-block:hover .tree-actions{opacity:1}.tree-btn{padding:.125rem .375rem;border:none;border-radius:3px;background:#e0e0e0;cursor:pointer;font-size:.6875rem;color:#666}.tree-btn:hover{background:#d0d0d0}.tree-btn.delete:hover{background:#e74c3c;color:#fff}.material-icon{font-family:Material Icons,sans-serif;font-size:.75rem;font-weight:400;font-style:normal;line-height:1;letter-spacing:normal;text-transform:none;white-space:nowrap;word-wrap:normal;direction:ltr;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}.nested-content{border-left:1px solid #e0e0e0;margin-left:1.25rem}.add-block-btn{display:flex;align-items:center;gap:.375rem;width:calc(100% - 1rem);margin:.375rem .5rem;padding:.375rem .5rem;border:none;border-radius:4px;background:transparent;cursor:pointer;font-size:.6875rem;color:#005bd3;transition:all .2s}.add-block-btn:hover{background:#f0f5ff;color:#004299}.add-icon{font-size:.75rem;font-weight:600}.add-icon-circle{display:flex;align-items:center;justify-content:center}\n"], dependencies: [{ kind: "component", type: BlockTreeItemComponent, selector: "lib-block-tree-item", inputs: ["block", "context", "index", "totalSiblings", "expandAll"], outputs: ["selectBlock", "deleteBlock", "duplicateBlock", "moveBlock", "openBlockPicker"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4196
3821
  }
4197
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: BlockTreeItemComponent, decorators: [{
3822
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: BlockTreeItemComponent, decorators: [{
4198
3823
  type: Component,
4199
3824
  args: [{ selector: 'lib-block-tree-item', imports: [BlockTreeItemComponent], template: `
4200
3825
  <div
@@ -4230,7 +3855,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
4230
3855
  <span class="block-indent"></span>
4231
3856
  }
4232
3857
  <span class="icon-drag-wrapper">
4233
- <span class="block-icon" [innerHTML]="blockIcon()"></span>
3858
+ <span class="block-icon">{{ blockIcon() }}</span>
4234
3859
  <span
4235
3860
  class="drag-handle"
4236
3861
  role="button"
@@ -4299,7 +3924,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
4299
3924
  }
4300
3925
  </div>
4301
3926
  }
4302
- `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block;font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif}.tree-block{display:flex;align-items:center;gap:.375rem;padding:.375rem .75rem .375rem .5rem;cursor:pointer;transition:background .15s;position:relative}.tree-block:hover{background:#f6f6f7;border-radius:8px;margin-right:.375rem}.tree-block.selected{background:#005bd3;color:#fff;border-radius:8px;margin-right:.375rem}.tree-block.selected .block-icon,.tree-block.selected .block-name,.tree-block.selected .expand-btn,.tree-block.selected .drag-handle,.tree-block.selected .tree-btn{color:inherit}.tree-block.drag-over-before{box-shadow:inset 0 2px #005bd3}.tree-block.drag-over-inside{background:#005bd314;box-shadow:inset 0 0 0 2px #005bd3}.tree-block.drag-over-after{box-shadow:inset 0 -2px #005bd3}.tree-block.is-dragging{opacity:.4;cursor:grabbing}.drag-handle{cursor:grab;color:#8c9196;opacity:0;transition:opacity .15s;display:flex;align-items:center;justify-content:center;flex-shrink:0;padding:.125rem;border:none;background:none;border-radius:4px}.drag-handle:hover{color:#303030;background:#e3e3e3}.drag-handle:focus-visible{opacity:1;outline:2px solid #005bd3;outline-offset:1px}.tree-block:hover .drag-handle{opacity:1}@media(prefers-reduced-motion:reduce){.tree-block,.drag-handle,.tree-actions,.expand-icon{transition:none}}.expand-btn{width:16px;height:16px;padding:0;border:none;background:none;cursor:pointer;display:flex;align-items:center;justify-content:center;color:#8c9196;flex-shrink:0}.expand-btn.expanded .expand-icon{transform:rotate(90deg)}.expand-icon{font-size:.5rem;transition:transform .15s}.block-indent{width:18px;height:1px;background:#e3e3e3;flex-shrink:0}.icon-drag-wrapper{position:relative;display:flex;align-items:center;justify-content:center;flex-shrink:0}.icon-drag-wrapper .drag-handle{position:absolute;inset:0;display:flex;align-items:center;justify-content:center}.tree-block:hover .icon-drag-wrapper .block-icon,.icon-drag-wrapper:has(.drag-handle:focus-visible) .block-icon{visibility:hidden}.block-icon{font-size:.875rem;color:#8c9196;flex-shrink:0;display:flex;align-items:center}.block-name{flex:1;font-size:.8125rem;color:#303030;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tree-actions{display:flex;gap:.125rem;opacity:0;transition:opacity .15s}.tree-block:hover .tree-actions{opacity:1}.tree-btn{padding:.125rem .375rem;border:none;border-radius:4px;background:transparent;cursor:pointer;font-size:.6875rem;color:#8c9196;transition:all .15s}.tree-btn:hover{background:#e3e3e3;color:#303030}.tree-btn.delete:hover{background:#fef2f0;color:#d72c0d}.material-icon{font-family:Material Icons,sans-serif;font-size:inherit;font-weight:400;font-style:normal;line-height:1;letter-spacing:normal;text-transform:none;white-space:nowrap;word-wrap:normal;direction:ltr;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}.nested-content{border-left:1px solid #e3e3e3;margin-left:1.25rem}.add-block-btn{display:flex;align-items:center;gap:.375rem;width:calc(100% - 1rem);margin:.25rem .5rem;padding:.375rem .5rem;border:none;border-radius:8px;background:transparent;cursor:pointer;font-size:.8125rem;font-family:inherit;color:#005bd3;transition:all .15s}.add-block-btn:hover{background:#f0f5ff;color:#004299}.add-icon{font-size:.8125rem;font-weight:600}.add-icon-circle{display:flex;align-items:center;justify-content:center}\n"] }]
3927
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block}.tree-block{display:flex;align-items:center;gap:.375rem;padding:.5rem .75rem .5rem .5rem;cursor:pointer;transition:background .2s;position:relative}.tree-block:hover{background:#f5f5f5;border-radius:8px;margin-right:.375rem}.tree-block.selected{background:#005bd3;color:#fff;border-radius:8px;margin-right:.375rem}.tree-block.selected .block-icon,.tree-block.selected .block-name,.tree-block.selected .expand-btn,.tree-block.selected .drag-handle,.tree-block.selected .tree-btn{color:inherit}.tree-block.drag-over-before{box-shadow:inset 0 2px #4a90d9}.tree-block.drag-over-inside{background:#4a90d91a;box-shadow:inset 0 0 0 2px #4a90d9}.tree-block.drag-over-after{box-shadow:inset 0 -2px #4a90d9}.tree-block.is-dragging{opacity:.4;cursor:grabbing}.drag-handle{cursor:grab;color:#999;opacity:0;transition:opacity .2s;display:flex;align-items:center;justify-content:center;flex-shrink:0;padding:.125rem;border:none;background:none;border-radius:2px}.drag-handle:hover{color:#666;background:#e0e0e0}.drag-handle:focus-visible{opacity:1;outline:2px solid #4a90d9;outline-offset:1px}.tree-block:hover .drag-handle{opacity:1}@media(prefers-reduced-motion:reduce){.tree-block,.drag-handle,.tree-actions,.expand-icon{transition:none}}.expand-btn{width:16px;height:16px;padding:0;border:none;background:none;cursor:pointer;display:flex;align-items:center;justify-content:center;color:#666;flex-shrink:0}.expand-btn.expanded .expand-icon{transform:rotate(90deg)}.expand-icon{font-size:.5rem;transition:transform .2s}.block-indent{width:18px;height:1px;background:#ddd;flex-shrink:0}.icon-drag-wrapper{position:relative;display:flex;align-items:center;justify-content:center;flex-shrink:0}.icon-drag-wrapper .drag-handle{position:absolute;inset:0;display:flex;align-items:center;justify-content:center}.tree-block:hover .icon-drag-wrapper .block-icon,.icon-drag-wrapper:has(.drag-handle:focus-visible) .block-icon{visibility:hidden}.block-icon{font-size:.875rem;flex-shrink:0}.block-name{flex:1;font-size:.75rem;color:#555;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tree-actions{display:flex;gap:.125rem;opacity:0;transition:opacity .2s}.tree-block:hover .tree-actions{opacity:1}.tree-btn{padding:.125rem .375rem;border:none;border-radius:3px;background:#e0e0e0;cursor:pointer;font-size:.6875rem;color:#666}.tree-btn:hover{background:#d0d0d0}.tree-btn.delete:hover{background:#e74c3c;color:#fff}.material-icon{font-family:Material Icons,sans-serif;font-size:.75rem;font-weight:400;font-style:normal;line-height:1;letter-spacing:normal;text-transform:none;white-space:nowrap;word-wrap:normal;direction:ltr;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}.nested-content{border-left:1px solid #e0e0e0;margin-left:1.25rem}.add-block-btn{display:flex;align-items:center;gap:.375rem;width:calc(100% - 1rem);margin:.375rem .5rem;padding:.375rem .5rem;border:none;border-radius:4px;background:transparent;cursor:pointer;font-size:.6875rem;color:#005bd3;transition:all .2s}.add-block-btn:hover{background:#f0f5ff;color:#004299}.add-icon{font-size:.75rem;font-weight:600}.add-icon-circle{display:flex;align-items:center;justify-content:center}\n"] }]
4303
3928
  }], propDecorators: { treeBlock: [{ type: i0.ViewChild, args: ['treeBlock', { isSignal: true }] }], block: [{ type: i0.Input, args: [{ isSignal: true, alias: "block", required: true }] }], context: [{ type: i0.Input, args: [{ isSignal: true, alias: "context", required: true }] }], index: [{ type: i0.Input, args: [{ isSignal: true, alias: "index", required: true }] }], totalSiblings: [{ type: i0.Input, args: [{ isSignal: true, alias: "totalSiblings", required: true }] }], expandAll: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandAll", required: false }] }], selectBlock: [{ type: i0.Output, args: ["selectBlock"] }], deleteBlock: [{ type: i0.Output, args: ["deleteBlock"] }], duplicateBlock: [{ type: i0.Output, args: ["duplicateBlock"] }], moveBlock: [{ type: i0.Output, args: ["moveBlock"] }], openBlockPicker: [{ type: i0.Output, args: ["openBlockPicker"] }] } });
4304
3929
 
4305
3930
  const FILES_QUERY = `
@@ -4414,10 +4039,10 @@ class ShopifyFilesService {
4414
4039
  return 'unknown';
4415
4040
  }
4416
4041
  }
4417
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ShopifyFilesService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
4418
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ShopifyFilesService, providedIn: 'root' });
4042
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ShopifyFilesService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
4043
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ShopifyFilesService, providedIn: 'root' });
4419
4044
  }
4420
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ShopifyFilesService, decorators: [{
4045
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ShopifyFilesService, decorators: [{
4421
4046
  type: Injectable,
4422
4047
  args: [{ providedIn: 'root' }]
4423
4048
  }] });
@@ -4440,7 +4065,7 @@ class ShopifyFilePickerComponent {
4440
4065
  endCursor = null;
4441
4066
  hasNextPage = false;
4442
4067
  ngOnInit() {
4443
- this.searchSubject.pipe(debounceTime(300), tap$1(() => {
4068
+ this.searchSubject.pipe(debounceTime(300), tap(() => {
4444
4069
  this.files.set([]);
4445
4070
  this.endCursor = null;
4446
4071
  this.hasNextPage = false;
@@ -4553,8 +4178,8 @@ class ShopifyFilePickerComponent {
4553
4178
  },
4554
4179
  });
4555
4180
  }
4556
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ShopifyFilePickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4557
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: ShopifyFilePickerComponent, isStandalone: true, selector: "lib-shopify-file-picker", inputs: { mediaType: { classPropertyName: "mediaType", publicName: "mediaType", isSignal: true, isRequired: true, transformFunction: null }, currentValue: { classPropertyName: "currentValue", publicName: "currentValue", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { fileSelected: "fileSelected", closed: "closed" }, viewQueries: [{ propertyName: "searchInput", first: true, predicate: ["searchInput"], descendants: true, isSignal: true }, { propertyName: "scrollContainer", first: true, predicate: ["scrollContainer"], descendants: true, isSignal: true }], ngImport: i0, template: `
4181
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ShopifyFilePickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4182
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: ShopifyFilePickerComponent, isStandalone: true, selector: "lib-shopify-file-picker", inputs: { mediaType: { classPropertyName: "mediaType", publicName: "mediaType", isSignal: true, isRequired: true, transformFunction: null }, currentValue: { classPropertyName: "currentValue", publicName: "currentValue", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { fileSelected: "fileSelected", closed: "closed" }, viewQueries: [{ propertyName: "searchInput", first: true, predicate: ["searchInput"], descendants: true, isSignal: true }, { propertyName: "scrollContainer", first: true, predicate: ["scrollContainer"], descendants: true, isSignal: true }], ngImport: i0, template: `
4558
4183
  <div
4559
4184
  class="file-picker-overlay"
4560
4185
  role="dialog"
@@ -4649,9 +4274,9 @@ class ShopifyFilePickerComponent {
4649
4274
  </div>
4650
4275
  </div>
4651
4276
  </div>
4652
- `, isInline: true, styles: [".file-picker-overlay{position:fixed;inset:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:1100;font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif}.file-picker{background:#fff;border-radius:12px;width:720px;max-width:95vw;height:80vh;max-height:80vh;display:flex;flex-direction:column;box-shadow:0 8px 32px #0003}.picker-header{display:flex;justify-content:space-between;align-items:center;padding:1rem 1.25rem;border-bottom:1px solid #e3e3e3}.picker-header h3{margin:0;font-size:.9375rem;font-weight:600;color:#303030}.close-btn{border:none;background:none;font-size:1.25rem;cursor:pointer;color:#8c9196;line-height:1;padding:.25rem;border-radius:8px;transition:all .15s}.close-btn:hover{background:#f6f6f7;color:#303030}.picker-search{padding:.75rem 1.25rem;border-bottom:1px solid #e3e3e3}.picker-search-input{width:100%;padding:.5rem .75rem;border:1px solid #c9cccf;border-radius:8px;font-size:.8125rem;font-family:inherit;color:#303030;outline:none;transition:border-color .15s,box-shadow .15s;box-sizing:border-box}.picker-search-input:focus{border-color:#005bd3;box-shadow:0 0 0 1px #005bd3}.picker-search-input::placeholder{color:#8c9196}.picker-content{flex:1;overflow-y:auto;padding:1rem 1.25rem;min-height:0}.file-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:.75rem;align-content:start}.file-card{display:flex;flex-direction:column;align-items:center;padding:.375rem;border:2px solid #e3e3e3;border-radius:8px;background:#fff;cursor:pointer;transition:all .15s;position:relative;text-align:center;font-family:inherit;overflow:hidden}.file-card:hover{border-color:#005bd3;background:#f8fafe}.file-card:focus-visible{outline:2px solid #005bd3;outline-offset:2px}.file-card.selected{border-color:#005bd3;background:#f0f5ff;box-shadow:0 0 0 1px #005bd3}.file-card.current{border-color:#008060}.file-thumbnail{width:100%;height:80px;object-fit:cover;border-radius:6px;background:#f6f6f7}.video-thumbnail{width:100%;height:80px;display:flex;align-items:center;justify-content:center;background:#1a1a1a;border-radius:6px;color:#fff}.video-icon{opacity:.8}.file-name{display:block;margin-top:.375rem;font-size:.6875rem;color:#8c9196;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}.current-badge{position:absolute;top:.375rem;right:.375rem;padding:.125rem .375rem;background:#008060;color:#fff;font-size:.625rem;font-weight:600;border-radius:6px}.picker-loading{padding:2rem;text-align:center;color:#8c9196;font-size:.8125rem}.picker-error{padding:2rem;text-align:center;color:#d72c0d}.picker-error p{margin:0 0 .75rem;font-size:.8125rem}.retry-btn{padding:.375rem .75rem;border:1px solid #d72c0d;border-radius:8px;background:#fff;color:#d72c0d;cursor:pointer;font-size:.8125rem;font-family:inherit;transition:background .15s}.retry-btn:hover{background:#fef2f0}.picker-empty{padding:2rem;text-align:center;color:#8c9196;font-size:.8125rem}.picker-footer{display:flex;justify-content:flex-end;gap:.5rem;padding:.75rem 1.25rem;border-top:1px solid #e3e3e3}.cancel-btn{padding:.5rem 1rem;border:1px solid #c9cccf;border-radius:8px;background:#fff;cursor:pointer;font-size:.8125rem;font-family:inherit;color:#303030;transition:all .15s}.cancel-btn:hover{background:#f6f6f7}.select-btn{padding:.5rem 1rem;border:none;border-radius:8px;background:#005bd3;color:#fff;cursor:pointer;font-size:.8125rem;font-weight:500;font-family:inherit;transition:background .15s}.select-btn:hover:not(:disabled){background:#004299}.select-btn:disabled{opacity:.5;cursor:not-allowed}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4277
+ `, isInline: true, styles: [".file-picker-overlay{position:fixed;inset:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:1100}.file-picker{background:#fff;border-radius:12px;width:560px;max-height:70vh;display:flex;flex-direction:column;box-shadow:0 12px 40px #00000040}.picker-header{display:flex;justify-content:space-between;align-items:center;padding:1rem 1.25rem;border-bottom:1px solid #e0e0e0}.picker-header h3{margin:0;font-size:1rem;font-weight:600;color:#303030}.close-btn{border:none;background:none;font-size:1.5rem;cursor:pointer;color:#666;line-height:1;padding:.25rem;border-radius:4px}.close-btn:hover{background:#f0f2f5;color:#303030}.picker-search{padding:.75rem 1.25rem;border-bottom:1px solid #e0e0e0}.picker-search-input{width:100%;padding:.5rem .75rem;border:1px solid #c9cccf;border-radius:8px;font-size:.875rem;outline:none;transition:border-color .2s;box-sizing:border-box}.picker-search-input:focus{border-color:#005bd3;box-shadow:0 0 0 1px #005bd3}.picker-content{flex:1;overflow-y:auto;padding:1rem 1.25rem;min-height:200px}.file-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:.75rem}.file-card{display:flex;flex-direction:column;align-items:center;padding:.5rem;border:2px solid #e0e0e0;border-radius:8px;background:#fff;cursor:pointer;transition:all .15s;position:relative;text-align:center}.file-card:hover{border-color:#005bd3;background:#f8fafe}.file-card:focus-visible{outline:2px solid #005bd3;outline-offset:2px}.file-card.selected{border-color:#005bd3;background:#f0f5ff;box-shadow:0 0 0 1px #005bd3}.file-card.current{border-color:#008060}.file-thumbnail{width:100%;aspect-ratio:1;object-fit:cover;border-radius:4px;background:#f0f2f5}.video-thumbnail{width:100%;aspect-ratio:1;display:flex;align-items:center;justify-content:center;background:#1a1a1a;border-radius:4px;color:#fff}.video-icon{opacity:.8}.file-name{display:block;margin-top:.375rem;font-size:.6875rem;color:#616161;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}.current-badge{position:absolute;top:.25rem;right:.25rem;padding:.125rem .375rem;background:#008060;color:#fff;font-size:.625rem;font-weight:600;border-radius:4px}.picker-loading{padding:2rem;text-align:center;color:#616161;font-size:.875rem}.picker-error{padding:2rem;text-align:center;color:#d72c0d}.picker-error p{margin:0 0 .75rem;font-size:.875rem}.retry-btn{padding:.375rem .75rem;border:1px solid #d72c0d;border-radius:6px;background:#fff;color:#d72c0d;cursor:pointer;font-size:.8125rem}.retry-btn:hover{background:#fef2f0}.picker-empty{padding:2rem;text-align:center;color:#616161;font-size:.875rem}.picker-footer{display:flex;justify-content:flex-end;gap:.5rem;padding:.75rem 1.25rem;border-top:1px solid #e0e0e0}.cancel-btn{padding:.5rem 1rem;border:1px solid #c9cccf;border-radius:8px;background:#fff;cursor:pointer;font-size:.8125rem;color:#303030}.cancel-btn:hover{background:#f0f2f5}.select-btn{padding:.5rem 1rem;border:none;border-radius:8px;background:#005bd3;color:#fff;cursor:pointer;font-size:.8125rem;font-weight:500}.select-btn:hover:not(:disabled){background:#004299}.select-btn:disabled{opacity:.5;cursor:not-allowed}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4653
4278
  }
4654
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ShopifyFilePickerComponent, decorators: [{
4279
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ShopifyFilePickerComponent, decorators: [{
4655
4280
  type: Component,
4656
4281
  args: [{ selector: 'lib-shopify-file-picker', template: `
4657
4282
  <div
@@ -4748,7 +4373,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
4748
4373
  </div>
4749
4374
  </div>
4750
4375
  </div>
4751
- `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".file-picker-overlay{position:fixed;inset:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:1100;font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif}.file-picker{background:#fff;border-radius:12px;width:720px;max-width:95vw;height:80vh;max-height:80vh;display:flex;flex-direction:column;box-shadow:0 8px 32px #0003}.picker-header{display:flex;justify-content:space-between;align-items:center;padding:1rem 1.25rem;border-bottom:1px solid #e3e3e3}.picker-header h3{margin:0;font-size:.9375rem;font-weight:600;color:#303030}.close-btn{border:none;background:none;font-size:1.25rem;cursor:pointer;color:#8c9196;line-height:1;padding:.25rem;border-radius:8px;transition:all .15s}.close-btn:hover{background:#f6f6f7;color:#303030}.picker-search{padding:.75rem 1.25rem;border-bottom:1px solid #e3e3e3}.picker-search-input{width:100%;padding:.5rem .75rem;border:1px solid #c9cccf;border-radius:8px;font-size:.8125rem;font-family:inherit;color:#303030;outline:none;transition:border-color .15s,box-shadow .15s;box-sizing:border-box}.picker-search-input:focus{border-color:#005bd3;box-shadow:0 0 0 1px #005bd3}.picker-search-input::placeholder{color:#8c9196}.picker-content{flex:1;overflow-y:auto;padding:1rem 1.25rem;min-height:0}.file-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:.75rem;align-content:start}.file-card{display:flex;flex-direction:column;align-items:center;padding:.375rem;border:2px solid #e3e3e3;border-radius:8px;background:#fff;cursor:pointer;transition:all .15s;position:relative;text-align:center;font-family:inherit;overflow:hidden}.file-card:hover{border-color:#005bd3;background:#f8fafe}.file-card:focus-visible{outline:2px solid #005bd3;outline-offset:2px}.file-card.selected{border-color:#005bd3;background:#f0f5ff;box-shadow:0 0 0 1px #005bd3}.file-card.current{border-color:#008060}.file-thumbnail{width:100%;height:80px;object-fit:cover;border-radius:6px;background:#f6f6f7}.video-thumbnail{width:100%;height:80px;display:flex;align-items:center;justify-content:center;background:#1a1a1a;border-radius:6px;color:#fff}.video-icon{opacity:.8}.file-name{display:block;margin-top:.375rem;font-size:.6875rem;color:#8c9196;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}.current-badge{position:absolute;top:.375rem;right:.375rem;padding:.125rem .375rem;background:#008060;color:#fff;font-size:.625rem;font-weight:600;border-radius:6px}.picker-loading{padding:2rem;text-align:center;color:#8c9196;font-size:.8125rem}.picker-error{padding:2rem;text-align:center;color:#d72c0d}.picker-error p{margin:0 0 .75rem;font-size:.8125rem}.retry-btn{padding:.375rem .75rem;border:1px solid #d72c0d;border-radius:8px;background:#fff;color:#d72c0d;cursor:pointer;font-size:.8125rem;font-family:inherit;transition:background .15s}.retry-btn:hover{background:#fef2f0}.picker-empty{padding:2rem;text-align:center;color:#8c9196;font-size:.8125rem}.picker-footer{display:flex;justify-content:flex-end;gap:.5rem;padding:.75rem 1.25rem;border-top:1px solid #e3e3e3}.cancel-btn{padding:.5rem 1rem;border:1px solid #c9cccf;border-radius:8px;background:#fff;cursor:pointer;font-size:.8125rem;font-family:inherit;color:#303030;transition:all .15s}.cancel-btn:hover{background:#f6f6f7}.select-btn{padding:.5rem 1rem;border:none;border-radius:8px;background:#005bd3;color:#fff;cursor:pointer;font-size:.8125rem;font-weight:500;font-family:inherit;transition:background .15s}.select-btn:hover:not(:disabled){background:#004299}.select-btn:disabled{opacity:.5;cursor:not-allowed}\n"] }]
4376
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".file-picker-overlay{position:fixed;inset:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:1100}.file-picker{background:#fff;border-radius:12px;width:560px;max-height:70vh;display:flex;flex-direction:column;box-shadow:0 12px 40px #00000040}.picker-header{display:flex;justify-content:space-between;align-items:center;padding:1rem 1.25rem;border-bottom:1px solid #e0e0e0}.picker-header h3{margin:0;font-size:1rem;font-weight:600;color:#303030}.close-btn{border:none;background:none;font-size:1.5rem;cursor:pointer;color:#666;line-height:1;padding:.25rem;border-radius:4px}.close-btn:hover{background:#f0f2f5;color:#303030}.picker-search{padding:.75rem 1.25rem;border-bottom:1px solid #e0e0e0}.picker-search-input{width:100%;padding:.5rem .75rem;border:1px solid #c9cccf;border-radius:8px;font-size:.875rem;outline:none;transition:border-color .2s;box-sizing:border-box}.picker-search-input:focus{border-color:#005bd3;box-shadow:0 0 0 1px #005bd3}.picker-content{flex:1;overflow-y:auto;padding:1rem 1.25rem;min-height:200px}.file-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:.75rem}.file-card{display:flex;flex-direction:column;align-items:center;padding:.5rem;border:2px solid #e0e0e0;border-radius:8px;background:#fff;cursor:pointer;transition:all .15s;position:relative;text-align:center}.file-card:hover{border-color:#005bd3;background:#f8fafe}.file-card:focus-visible{outline:2px solid #005bd3;outline-offset:2px}.file-card.selected{border-color:#005bd3;background:#f0f5ff;box-shadow:0 0 0 1px #005bd3}.file-card.current{border-color:#008060}.file-thumbnail{width:100%;aspect-ratio:1;object-fit:cover;border-radius:4px;background:#f0f2f5}.video-thumbnail{width:100%;aspect-ratio:1;display:flex;align-items:center;justify-content:center;background:#1a1a1a;border-radius:4px;color:#fff}.video-icon{opacity:.8}.file-name{display:block;margin-top:.375rem;font-size:.6875rem;color:#616161;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}.current-badge{position:absolute;top:.25rem;right:.25rem;padding:.125rem .375rem;background:#008060;color:#fff;font-size:.625rem;font-weight:600;border-radius:4px}.picker-loading{padding:2rem;text-align:center;color:#616161;font-size:.875rem}.picker-error{padding:2rem;text-align:center;color:#d72c0d}.picker-error p{margin:0 0 .75rem;font-size:.875rem}.retry-btn{padding:.375rem .75rem;border:1px solid #d72c0d;border-radius:6px;background:#fff;color:#d72c0d;cursor:pointer;font-size:.8125rem}.retry-btn:hover{background:#fef2f0}.picker-empty{padding:2rem;text-align:center;color:#616161;font-size:.875rem}.picker-footer{display:flex;justify-content:flex-end;gap:.5rem;padding:.75rem 1.25rem;border-top:1px solid #e0e0e0}.cancel-btn{padding:.5rem 1rem;border:1px solid #c9cccf;border-radius:8px;background:#fff;cursor:pointer;font-size:.8125rem;color:#303030}.cancel-btn:hover{background:#f0f2f5}.select-btn{padding:.5rem 1rem;border:none;border-radius:8px;background:#005bd3;color:#fff;cursor:pointer;font-size:.8125rem;font-weight:500}.select-btn:hover:not(:disabled){background:#004299}.select-btn:disabled{opacity:.5;cursor:not-allowed}\n"] }]
4752
4377
  }], propDecorators: { mediaType: [{ type: i0.Input, args: [{ isSignal: true, alias: "mediaType", required: true }] }], currentValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentValue", required: false }] }], fileSelected: [{ type: i0.Output, args: ["fileSelected"] }], closed: [{ type: i0.Output, args: ["closed"] }], searchInput: [{ type: i0.ViewChild, args: ['searchInput', { isSignal: true }] }], scrollContainer: [{ type: i0.ViewChild, args: ['scrollContainer', { isSignal: true }] }] } });
4753
4378
 
4754
4379
  /**
@@ -4765,6 +4390,8 @@ class VisualEditorComponent {
4765
4390
  loadingStrategy = inject(PageLoadingStrategy);
4766
4391
  config = inject(VISUAL_EDITOR_CONFIG);
4767
4392
  dndService = inject(DragDropService);
4393
+ iframeBridge = inject(IframeBridgeService);
4394
+ storefrontUrl = inject(STOREFRONT_URL);
4768
4395
  sanitizer = inject(DomSanitizer);
4769
4396
  destroy$ = new Subject();
4770
4397
  // Configuration-driven UI options
@@ -4774,6 +4401,14 @@ class VisualEditorComponent {
4774
4401
  isPreviewMode = signal(false, ...(ngDevMode ? [{ debugName: "isPreviewMode" }] : []));
4775
4402
  editorContext = computed(() => ({ isEditor: !this.isPreviewMode() }), ...(ngDevMode ? [{ debugName: "editorContext" }] : []));
4776
4403
  canvasEl = viewChild('canvasEl', ...(ngDevMode ? [{ debugName: "canvasEl" }] : []));
4404
+ // Iframe preview
4405
+ previewFrame = viewChild('previewFrame', ...(ngDevMode ? [{ debugName: "previewFrame" }] : []));
4406
+ previewUrl = computed(() => {
4407
+ if (!this.storefrontUrl)
4408
+ return null;
4409
+ return this.sanitizer.bypassSecurityTrustResourceUrl(`${this.storefrontUrl}/kustomizer/editor`);
4410
+ }, ...(ngDevMode ? [{ debugName: "previewUrl" }] : []));
4411
+ iframeReady = false;
4777
4412
  propertiesTab = signal('props', ...(ngDevMode ? [{ debugName: "propertiesTab" }] : []));
4778
4413
  expandedGroups = signal(new Set(), ...(ngDevMode ? [{ debugName: "expandedGroups" }] : []));
4779
4414
  expandedSections = signal(new Set(), ...(ngDevMode ? [{ debugName: "expandedSections" }] : []));
@@ -4898,23 +4533,19 @@ class VisualEditorComponent {
4898
4533
  });
4899
4534
  }, ...(ngDevMode ? [{ debugName: "filteredBlockPresetsForNestedSlot" }] : []));
4900
4535
  sectionIconMap = {
4901
- 'hero-section': '<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"><path d="M2 4a2 2 0 012-2h12a2 2 0 012 2v4H2V4zm0 6h16v6a2 2 0 01-2 2H4a2 2 0 01-2-2v-6z"/></svg>',
4902
- 'features-grid': '<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"><path d="M3 3h6v6H3V3zm8 0h6v6h-6V3zm-8 8h6v6H3v-6zm8 0h6v6h-6v-6z"/></svg>',
4903
- 'testimonials': '<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"><path d="M2 5a2 2 0 012-2h12a2 2 0 012 2v7a2 2 0 01-2 2H6l-4 4V5z"/></svg>',
4904
- 'cta-section': '<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"><path d="M10 2a8 8 0 100 16 8 8 0 000-16zm1 11H9v-2h2v2zm0-4H9V5h2v4z"/></svg>',
4905
- 'footer-section': '<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"><path d="M2 4a2 2 0 012-2h12a2 2 0 012 2v12a2 2 0 01-2 2H4a2 2 0 01-2-2V4zm2 10v2h12v-2H4z"/></svg>',
4906
- 'layout-section': '<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"><path d="M3 3h14a1 1 0 011 1v12a1 1 0 01-1 1H3a1 1 0 01-1-1V4a1 1 0 011-1zm1 2v10h5V5H4zm7 0v10h5V5h-5z"/></svg>',
4536
+ 'hero-section': '🎯',
4537
+ 'features-grid': '',
4538
+ 'testimonials': '💬',
4539
+ 'cta-section': '📢',
4540
+ 'footer-section': '📋',
4907
4541
  };
4908
4542
  blockIconMap = {
4909
- 'text-block': '<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"><path d="M4 3h12a1 1 0 011 1v1a1 1 0 01-1 1h-5v10a1 1 0 01-2 0V6H4a1 1 0 01-1-1V4a1 1 0 011-1z"/></svg>',
4910
- 'button-block': '<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"><path d="M3 6a2 2 0 012-2h10a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2V6zm4 4a1 1 0 100 2h6a1 1 0 100-2H7z"/></svg>',
4911
- 'image-block': '<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"><path d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6zM6 7a1 1 0 110-2 1 1 0 010 2z"/></svg>',
4912
- 'icon-block': '<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"><path d="M10 2l2.39 4.84L18 7.77l-4 3.9.94 5.5L10 14.27l-4.94 2.9.94-5.5-4-3.9 5.61-.93L10 2z"/></svg>',
4913
- 'feature-item': '<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"><path d="M10 2l2.39 4.84L18 7.77l-4 3.9.94 5.5L10 14.27l-4.94 2.9.94-5.5-4-3.9 5.61-.93L10 2z"/></svg>',
4914
- 'card-block': '<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"><path d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm0 2h12v4H4V5zm0 6h8v4H4v-4z"/></svg>',
4915
- 'video-block': '<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"><path d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm4 3.5l5 3.5-5 3.5v-7z"/></svg>',
4916
- 'picture-block': '<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"><path d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6zM6 7a1 1 0 110-2 1 1 0 010 2z"/></svg>',
4917
- 'flex-layout-block': '<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"><path d="M3 3h4v14H3V3zm5 0h4v14H8V3zm5 0h4v14h-4V3z"/></svg>',
4543
+ 'text-block': '📝',
4544
+ 'button-block': '🔘',
4545
+ 'image-block': '🖼️',
4546
+ 'icon-block': '',
4547
+ 'feature-item': '',
4548
+ 'card-block': '🃏',
4918
4549
  };
4919
4550
  selectedBlockDefinition = computed(() => this.facade.selectedBlockDefinition(), ...(ngDevMode ? [{ debugName: "selectedBlockDefinition" }] : []));
4920
4551
  // Inline name editing
@@ -4974,6 +4605,27 @@ class VisualEditorComponent {
4974
4605
  cancelEditingName() {
4975
4606
  this.isEditingName.set(false);
4976
4607
  }
4608
+ constructor() {
4609
+ // Sync sections to iframe whenever they change
4610
+ effect(() => {
4611
+ const sections = this.facade.sections();
4612
+ if (this.iframeReady && this.previewUrl()) {
4613
+ this.iframeBridge.sendPageUpdate(sections);
4614
+ }
4615
+ });
4616
+ // Sync selection to iframe
4617
+ effect(() => {
4618
+ const selected = this.facade.selectedElement()?.id ?? null;
4619
+ if (!this.iframeReady || !this.previewUrl())
4620
+ return;
4621
+ if (selected) {
4622
+ this.iframeBridge.sendSelectElement(selected);
4623
+ }
4624
+ else {
4625
+ this.iframeBridge.sendDeselect();
4626
+ }
4627
+ });
4628
+ }
4977
4629
  ngOnInit() {
4978
4630
  // Load page using the configured loading strategy
4979
4631
  this.loadingStrategy
@@ -4993,13 +4645,38 @@ class VisualEditorComponent {
4993
4645
  this.navigation.navigate({ type: 'page-list' });
4994
4646
  },
4995
4647
  });
4648
+ // Listen for iframe events
4649
+ if (this.previewUrl()) {
4650
+ this.iframeBridge
4651
+ .onReady()
4652
+ .pipe(takeUntil(this.destroy$))
4653
+ .subscribe(() => {
4654
+ this.iframeReady = true;
4655
+ // Send initial page state
4656
+ this.iframeBridge.sendPageUpdate(this.facade.sections());
4657
+ });
4658
+ this.iframeBridge
4659
+ .onElementClicked()
4660
+ .pipe(takeUntil(this.destroy$))
4661
+ .subscribe(({ elementId, sectionId }) => {
4662
+ this.facade.selectElement(sectionId, elementId);
4663
+ });
4664
+ }
4996
4665
  }
4997
4666
  ngOnDestroy() {
4998
4667
  this.destroy$.next();
4999
4668
  this.destroy$.complete();
4669
+ this.iframeBridge.disconnect();
5000
4670
  // Clear page from store when leaving editor
5001
4671
  this.facade.clearPage();
5002
4672
  }
4673
+ onIframeLoad() {
4674
+ const iframe = this.previewFrame()?.nativeElement;
4675
+ if (iframe) {
4676
+ const origin = this.storefrontUrl ? new URL(this.storefrontUrl).origin : '*';
4677
+ this.iframeBridge.connect(iframe, origin);
4678
+ }
4679
+ }
5003
4680
  goBack() {
5004
4681
  if (this.facade.isDirty()) {
5005
4682
  this.navigation
@@ -5064,25 +4741,17 @@ class VisualEditorComponent {
5064
4741
  },
5065
4742
  });
5066
4743
  }
5067
- defaultSectionIcon = '<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"><path d="M3 3h14a1 1 0 011 1v12a1 1 0 01-1 1H3a1 1 0 01-1-1V4a1 1 0 011-1zm1 2v10h12V5H4z"/></svg>';
5068
- defaultBlockIcon = '<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"><path d="M10 2l1.5 3h3.5l-2.5 2.5 1 3.5-3.5-2-3.5 2 1-3.5L4 5h3.5L10 2zm-6 10h12v2H4v-2zm2 4h8v2H6v-2z"/></svg>';
5069
- toSafeIcon(icon) {
5070
- if (icon.trim().startsWith('<')) {
5071
- return this.sanitizer.bypassSecurityTrustHtml(icon);
5072
- }
5073
- return this.sanitizer.bypassSecurityTrustHtml(`<span style="font-family:'Material Icons',sans-serif;font-weight:normal;font-style:normal;line-height:1;letter-spacing:normal;text-transform:none;white-space:nowrap;direction:ltr;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility">${icon}</span>`);
5074
- }
5075
4744
  getIcon(def) {
5076
- return this.toSafeIcon(this.sectionIconMap[def.type] ?? def.icon ?? this.defaultSectionIcon);
4745
+ return this.sectionIconMap[def.type] ?? '📦';
5077
4746
  }
5078
4747
  getBlockIcon(def) {
5079
- return this.toSafeIcon(this.blockIconMap[def.type] ?? def.icon ?? this.defaultBlockIcon);
4748
+ return this.blockIconMap[def.type] ?? '🧩';
5080
4749
  }
5081
4750
  getBlockIconByType(type) {
5082
- return this.toSafeIcon(this.blockIconMap[type] ?? this.defaultBlockIcon);
4751
+ return this.blockIconMap[type] ?? '🧩';
5083
4752
  }
5084
4753
  getSectionIcon(type) {
5085
- return this.toSafeIcon(this.sectionIconMap[type] ?? this.defaultSectionIcon);
4754
+ return this.sectionIconMap[type] ?? '📦';
5086
4755
  }
5087
4756
  // Tree expansion
5088
4757
  isSectionExpanded(sectionId) {
@@ -5508,10 +5177,10 @@ class VisualEditorComponent {
5508
5177
  this.closeNestedBlockPicker();
5509
5178
  }
5510
5179
  getSectionPresetIcon(rp) {
5511
- return this.toSafeIcon(rp.displayIcon ?? this.sectionIconMap[rp.definition.type] ?? this.defaultSectionIcon);
5180
+ return rp.displayIcon ?? this.sectionIconMap[rp.definition.type] ?? '📦';
5512
5181
  }
5513
5182
  getBlockPresetIcon(rp) {
5514
- return this.toSafeIcon(rp.displayIcon ?? this.blockIconMap[rp.definition.type] ?? this.defaultBlockIcon);
5183
+ return rp.displayIcon ?? this.blockIconMap[rp.definition.type] ?? '🧩';
5515
5184
  }
5516
5185
  selectSection(section, event) {
5517
5186
  event.stopPropagation();
@@ -5755,15 +5424,15 @@ class VisualEditorComponent {
5755
5424
  return;
5756
5425
  this.facade.updateSectionProps(section.id, { [key]: '' });
5757
5426
  }
5758
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: VisualEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5759
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: VisualEditorComponent, isStandalone: true, selector: "lib-visual-editor", viewQueries: [{ propertyName: "canvasEl", first: true, predicate: ["canvasEl"], descendants: true, isSignal: true }, { propertyName: "nameInput", first: true, predicate: ["nameInput"], descendants: true, isSignal: true }], ngImport: i0, template: `
5427
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: VisualEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5428
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: VisualEditorComponent, isStandalone: true, selector: "lib-visual-editor", providers: [IframeBridgeService], viewQueries: [{ propertyName: "canvasEl", first: true, predicate: ["canvasEl"], descendants: true, isSignal: true }, { propertyName: "previewFrame", first: true, predicate: ["previewFrame"], descendants: true, isSignal: true }, { propertyName: "nameInput", first: true, predicate: ["nameInput"], descendants: true, isSignal: true }], ngImport: i0, template: `
5760
5429
  <div class="editor-layout">
5761
5430
  <!-- Sidebar: Hierarchical Tree -->
5762
5431
  <aside class="sidebar">
5763
5432
  <div class="sidebar-header">
5764
5433
  <h2>Page Structure</h2>
5765
5434
  <div class="search-box">
5766
- <span class="search-icon"><svg width="14" height="14" viewBox="0 0 20 20" fill="currentColor"><path d="M8 16a8 8 0 116.32-3.1l5.39 5.38a1 1 0 01-1.42 1.42l-5.38-5.39A8 8 0 018 16zm0-2A6 6 0 108 2a6 6 0 000 12z"/></svg></span>
5435
+ <span class="search-icon">🔍</span>
5767
5436
  <input
5768
5437
  type="text"
5769
5438
  class="search-input"
@@ -5811,7 +5480,7 @@ class VisualEditorComponent {
5811
5480
  <span class="expand-icon">&#9654;</span>
5812
5481
  </button>
5813
5482
  <span class="icon-drag-wrapper">
5814
- <span class="section-icon" [innerHTML]="getSectionIcon(section.type)"></span>
5483
+ <span class="section-icon">{{ getSectionIcon(section.type) }}</span>
5815
5484
  <span
5816
5485
  class="drag-handle section-drag-handle"
5817
5486
  role="button"
@@ -5913,7 +5582,7 @@ class VisualEditorComponent {
5913
5582
  }
5914
5583
  @for (rp of group.presets; track rp.displayName + '-' + rp.definition.type) {
5915
5584
  <button class="picker-item" (click)="addSectionFromPreset(rp)">
5916
- <span class="picker-icon" [innerHTML]="getSectionPresetIcon(rp)"></span>
5585
+ <span class="picker-icon">{{ getSectionPresetIcon(rp) }}</span>
5917
5586
  <div class="picker-info">
5918
5587
  <span class="picker-name">{{ rp.displayName }}</span>
5919
5588
  @if (rp.displayDescription) {
@@ -5951,7 +5620,7 @@ class VisualEditorComponent {
5951
5620
  <div class="picker-content">
5952
5621
  @for (rp of filteredBlockPresetsForSection(); track rp.displayName + '-' + rp.definition.type) {
5953
5622
  <button class="picker-item" (click)="addBlockToSectionFromPreset(blockPickerSection()!, rp)">
5954
- <span class="picker-icon" [innerHTML]="getBlockPresetIcon(rp)"></span>
5623
+ <span class="picker-icon">{{ getBlockPresetIcon(rp) }}</span>
5955
5624
  <div class="picker-info">
5956
5625
  <span class="picker-name">{{ rp.displayName }}</span>
5957
5626
  @if (rp.displayDescription) {
@@ -5994,7 +5663,7 @@ class VisualEditorComponent {
5994
5663
  <div class="picker-content">
5995
5664
  @for (rp of filteredBlockPresetsForNestedSlot(); track rp.displayName + '-' + rp.definition.type) {
5996
5665
  <button class="picker-item" (click)="addNestedBlockFromPreset(rp)">
5997
- <span class="picker-icon" [innerHTML]="getBlockPresetIcon(rp)"></span>
5666
+ <span class="picker-icon">{{ getBlockPresetIcon(rp) }}</span>
5998
5667
  <div class="picker-info">
5999
5668
  <span class="picker-name">{{ rp.displayName }}</span>
6000
5669
  @if (rp.displayDescription) {
@@ -6024,7 +5693,7 @@ class VisualEditorComponent {
6024
5693
  <div class="toolbar">
6025
5694
  <div class="toolbar-left">
6026
5695
  <button class="toolbar-btn back-btn" (click)="goBack()" title="Back to pages">
6027
- <svg width="14" height="14" viewBox="0 0 20 20" fill="currentColor" style="vertical-align: middle"><path d="M17 9H5.414l3.293-3.293a1 1 0 00-1.414-1.414l-5 5a1 1 0 000 1.414l5 5a1 1 0 001.414-1.414L5.414 11H17a1 1 0 100-2z"/></svg> Back
5696
+ Back
6028
5697
  </button>
6029
5698
  <button
6030
5699
  class="toolbar-btn"
@@ -6032,7 +5701,7 @@ class VisualEditorComponent {
6032
5701
  (click)="facade.undo()"
6033
5702
  title="Undo"
6034
5703
  >
6035
- <svg width="14" height="14" viewBox="0 0 20 20" fill="currentColor" style="vertical-align: middle"><path d="M7.5 4.5a1 1 0 00-1.7-.7l-4 4a1 1 0 000 1.4l4 4a1 1 0 001.7-.7V11h5a4 4 0 010 8H9a1 1 0 110-2h3.5a2 2 0 000-4h-5v1.5a1 1 0 01-1.7.7l-4-4a1 1 0 010-1.4l4-4a1 1 0 011.7.7V6h5a6 6 0 010 12H9a3 3 0 010-6h-1.5V4.5z"/></svg> Undo
5704
+ Undo
6036
5705
  </button>
6037
5706
  <button
6038
5707
  class="toolbar-btn"
@@ -6040,7 +5709,7 @@ class VisualEditorComponent {
6040
5709
  (click)="facade.redo()"
6041
5710
  title="Redo"
6042
5711
  >
6043
- <svg width="14" height="14" viewBox="0 0 20 20" fill="currentColor" style="vertical-align: middle; transform: scaleX(-1)"><path d="M7.5 4.5a1 1 0 00-1.7-.7l-4 4a1 1 0 000 1.4l4 4a1 1 0 001.7-.7V11h5a4 4 0 010 8H9a1 1 0 110-2h3.5a2 2 0 000-4h-5v1.5a1 1 0 01-1.7.7l-4-4a1 1 0 010-1.4l4-4a1 1 0 011.7.7V6h5a6 6 0 010 12H9a3 3 0 010-6h-1.5V4.5z"/></svg> Redo
5712
+ Redo
6044
5713
  </button>
6045
5714
  </div>
6046
5715
  <div class="toolbar-center">
@@ -6073,7 +5742,7 @@ class VisualEditorComponent {
6073
5742
  (click)="savePage()"
6074
5743
  title="Save changes"
6075
5744
  >
6076
- {{ isSaving() ? 'Saving...' : 'Save' }}
5745
+ {{ isSaving() ? 'Saving...' : '💾 Save' }}
6077
5746
  </button>
6078
5747
  }
6079
5748
  @if (facade.currentPage() && showPublishButtons()) {
@@ -6084,7 +5753,7 @@ class VisualEditorComponent {
6084
5753
  (click)="publishPage()"
6085
5754
  title="Publish page"
6086
5755
  >
6087
- {{ isPublishing() ? 'Publishing...' : 'Publish' }}
5756
+ {{ isPublishing() ? 'Publishing...' : '🚀 Publish' }}
6088
5757
  </button>
6089
5758
  } @else {
6090
5759
  <button
@@ -6093,57 +5762,68 @@ class VisualEditorComponent {
6093
5762
  (click)="unpublishPage()"
6094
5763
  title="Unpublish page"
6095
5764
  >
6096
- {{ isPublishing() ? 'Unpublishing...' : 'Unpublish' }}
5765
+ {{ isPublishing() ? 'Unpublishing...' : '📥 Unpublish' }}
6097
5766
  </button>
6098
5767
  }
6099
5768
  }
6100
5769
  <button class="toolbar-btn preview-btn" (click)="togglePreview()">
6101
- {{ isPreviewMode() ? 'Edit' : 'Preview' }}
5770
+ {{ isPreviewMode() ? '✏️ Edit' : '👁 Preview' }}
6102
5771
  </button>
6103
5772
  </div>
6104
5773
  </div>
6105
5774
 
6106
5775
  <!-- Canvas Content -->
6107
5776
  <div class="canvas" #canvasEl [class.preview-mode]="isPreviewMode()">
6108
- @if (facade.sections().length === 0) {
6109
- <div class="empty-state">
6110
- <div class="empty-icon"><svg width="48" height="48" viewBox="0 0 20 20" fill="currentColor"><path d="M6 2a2 2 0 00-2 2v12a2 2 0 002 2h8a2 2 0 002-2V7.414a2 2 0 00-.586-1.414l-3.414-3.414A2 2 0 0010.586 2H6zm0 2h4v3a1 1 0 001 1h3v8H6V4z"/></svg></div>
6111
- <h3>Start Building</h3>
6112
- <p>Click on a component from the sidebar to add it to your page</p>
6113
- </div>
5777
+ @if (previewUrl()) {
5778
+ <iframe
5779
+ #previewFrame
5780
+ class="preview-iframe"
5781
+ [src]="previewUrl()!"
5782
+ (load)="onIframeLoad()"
5783
+ allow="clipboard-read; clipboard-write"
5784
+ ></iframe>
6114
5785
  } @else {
6115
- @for (section of facade.sections(); track section.id; let idx = $index) {
6116
- <div
6117
- class="section-outer"
6118
- [class.selected]="facade.selectedSection()?.id === section.id"
6119
- >
6120
- @if (!isPreviewMode()) {
6121
- <div class="section-controls">
6122
- <span class="section-label">{{ getSectionName(section) }}</span>
6123
- <div class="section-actions">
6124
- @if (idx > 0) {
6125
- <button class="action-btn" (click)="moveUp(section.id, idx, $event)" title="Move up">↑</button>
6126
- }
6127
- @if (idx < facade.sections().length - 1) {
6128
- <button class="action-btn" (click)="moveDown(section.id, idx, $event)" title="Move down">↓</button>
6129
- }
6130
- @if (facade.isSectionDuplicable(section.type)) {
6131
- <button class="action-btn" (click)="duplicateSection(section.id, $event)" title="Duplicate">
6132
- <span class="material-icon">content_copy</span>
6133
- </button>
6134
- }
6135
- <button class="action-btn delete" (click)="deleteSection(section.id, $event)" title="Delete">×</button>
6136
- </div>
6137
- </div>
6138
- }
5786
+ <!-- Fallback: direct rendering when no storefront URL configured -->
5787
+ @if (facade.sections().length === 0) {
5788
+ <div class="empty-state">
5789
+ <div class="empty-icon">📄</div>
5790
+ <h3>Start Building</h3>
5791
+ <p>Click on a component from the sidebar to add it to your page</p>
5792
+ </div>
5793
+ } @else {
5794
+ @for (section of facade.sections(); track section.id; let idx = $index) {
6139
5795
  <div
6140
- class="section-wrapper"
6141
- [attr.data-section-id]="section.id"
6142
- (click)="selectSection(section, $event)"
5796
+ class="section-outer"
5797
+ [class.selected]="facade.selectedSection()?.id === section.id"
6143
5798
  >
6144
- <lib-dynamic-renderer [element]="section" [context]="editorContext()" />
5799
+ @if (!isPreviewMode()) {
5800
+ <div class="section-controls">
5801
+ <span class="section-label">{{ getSectionName(section) }}</span>
5802
+ <div class="section-actions">
5803
+ @if (idx > 0) {
5804
+ <button class="action-btn" (click)="moveUp(section.id, idx, $event)" title="Move up">↑</button>
5805
+ }
5806
+ @if (idx < facade.sections().length - 1) {
5807
+ <button class="action-btn" (click)="moveDown(section.id, idx, $event)" title="Move down">↓</button>
5808
+ }
5809
+ @if (facade.isSectionDuplicable(section.type)) {
5810
+ <button class="action-btn" (click)="duplicateSection(section.id, $event)" title="Duplicate">
5811
+ <span class="material-icon">content_copy</span>
5812
+ </button>
5813
+ }
5814
+ <button class="action-btn delete" (click)="deleteSection(section.id, $event)" title="Delete">×</button>
5815
+ </div>
5816
+ </div>
5817
+ }
5818
+ <div
5819
+ class="section-wrapper"
5820
+ [attr.data-section-id]="section.id"
5821
+ (click)="selectSection(section, $event)"
5822
+ >
5823
+ <lib-dynamic-renderer [element]="section" [context]="editorContext()" />
5824
+ </div>
6145
5825
  </div>
6146
- </div>
5826
+ }
6147
5827
  }
6148
5828
  }
6149
5829
  </div>
@@ -6533,7 +6213,7 @@ class VisualEditorComponent {
6533
6213
  [class.selected]="facade.selectedBlock()?.id === block.id"
6534
6214
  (click)="selectBlock(block)"
6535
6215
  >
6536
- <span class="block-icon" [innerHTML]="getBlockIconByType(block.type)"></span>
6216
+ <span class="block-icon">{{ getBlockIconByType(block.type) }}</span>
6537
6217
  <span class="block-name">{{ getBlockName(block) }}</span>
6538
6218
  <div class="block-actions">
6539
6219
  @if (idx > 0) {
@@ -6580,18 +6260,18 @@ class VisualEditorComponent {
6580
6260
  />
6581
6261
  }
6582
6262
  </div>
6583
- `, isInline: true, styles: [":host{display:block;height:calc(100vh - 56px);overflow:hidden;font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif}.editor-layout{display:grid;grid-template-columns:280px 1fr 300px;height:100%;background:#f6f6f7}.sidebar{background:#fff;border-right:1px solid #e3e3e3;display:flex;flex-direction:column}.sidebar-header{padding:1rem;border-bottom:1px solid #e3e3e3}.sidebar-header h2{margin:0;font-size:.8125rem;font-weight:600;color:#303030}.search-box{display:none}.search-box:focus-within{border-color:#005bd3;background:#fff}.search-icon{font-size:.8125rem;opacity:.6}.search-input{flex:1;border:none;background:transparent;font-size:.8125rem;color:#303030;font-family:inherit;outline:none}.search-input::placeholder{color:#8c9196}.search-clear{padding:.125rem .375rem;border:none;background:#e3e3e3;border-radius:4px;cursor:pointer;font-size:.75rem;color:#616161;line-height:1}.search-clear:hover{background:#c9cccf}.search-no-results{padding:2rem 1rem;text-align:center;color:#8c9196}.search-no-results p{margin:0;font-size:.8125rem}.picker-search{padding:.5rem .75rem;border-bottom:1px solid #e3e3e3}.picker-search-input{width:100%;padding:.5rem .75rem;border:1px solid #c9cccf;border-radius:8px;font-size:.8125rem;font-family:inherit;outline:none;box-sizing:border-box;transition:border-color .2s,box-shadow .2s}.picker-search-input:focus{border-color:#005bd3;box-shadow:0 0 0 1px #005bd3}.picker-search-input::placeholder{color:#8c9196}.section-tree{flex:1;overflow-y:auto;padding:.25rem 0}.tree-section{border-bottom:none}.tree-section.selected>.tree-section-header{background:#005bd3;color:#fff;border-radius:8px;margin:0 .375rem}.tree-section.selected>.tree-section-header .section-name,.tree-section.selected>.tree-section-header .expand-btn,.tree-section.selected>.tree-section-header .section-icon,.tree-section.selected>.tree-section-header .drag-handle,.tree-section.selected>.tree-section-header .tree-btn{color:inherit}.tree-section-header{display:flex;align-items:center;gap:.375rem;padding:.375rem .5rem;cursor:pointer;transition:background .15s}.tree-section-header:hover{background:#f6f6f7;border-radius:8px;margin:0 .375rem}.expand-btn{width:16px;height:16px;padding:0;border:none;background:none;cursor:pointer;display:flex;align-items:center;justify-content:center;color:#8c9196;transition:transform .15s;flex-shrink:0}.expand-btn.expanded .expand-icon{transform:rotate(90deg)}.expand-icon{font-size:.5rem;transition:transform .15s}.icon-drag-wrapper{position:relative;display:flex;align-items:center;justify-content:center;flex-shrink:0}.icon-drag-wrapper .drag-handle{position:absolute;inset:0;display:flex;align-items:center;justify-content:center}.tree-section-header:hover .icon-drag-wrapper .section-icon,.icon-drag-wrapper:has(.drag-handle:focus-visible) .section-icon{visibility:hidden}.section-icon{font-size:.875rem;color:#8c9196;display:flex;align-items:center}.section-name{flex:1;font-size:.8125rem;font-weight:500;color:#303030;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tree-actions{display:flex;gap:.125rem;opacity:0;transition:opacity .15s}.tree-section-header:hover .tree-actions,.tree-block:hover .tree-actions{opacity:1}.tree-btn{padding:.125rem .375rem;border:none;border-radius:4px;background:transparent;cursor:pointer;font-size:.6875rem;color:#8c9196;transition:all .15s}.tree-btn:hover{background:#e3e3e3;color:#303030}.tree-btn.delete:hover{background:#fef2f0;color:#d72c0d}.tree-section.drag-over-before>.tree-section-header{box-shadow:inset 0 2px #005bd3}.tree-section.drag-over-after>.tree-section-header{box-shadow:inset 0 -2px #005bd3}.tree-section.is-section-dragging{opacity:.4}.tree-section.is-section-dragging>.tree-section-header{cursor:grabbing}.drag-handle{cursor:grab;color:#8c9196;opacity:0;transition:opacity .15s;display:flex;align-items:center;justify-content:center;flex-shrink:0;padding:.125rem;border:none;background:none;border-radius:4px}.drag-handle:hover{color:#303030;background:#e3e3e3}.drag-handle:focus-visible{opacity:1;outline:2px solid #005bd3;outline-offset:1px}.tree-section-header:hover .drag-handle{opacity:1}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}@media(prefers-reduced-motion:reduce){.tree-section,.drag-handle,.tree-actions{transition:none}}.tree-section-content{padding-left:.5rem}.tree-block{display:flex;align-items:center;gap:.375rem;padding:.375rem .75rem;cursor:pointer;transition:background .15s}.tree-block:hover{background:#f6f6f7;border-radius:8px;margin:0 .375rem}.tree-block.selected{background:#005bd3;color:#fff;border-radius:8px;margin:0 .375rem}.tree-block.selected .block-icon,.tree-block.selected .block-name{color:inherit}.block-indent{width:18px;height:1px;background:#e3e3e3;margin-left:10px}.block-name{flex:1;font-size:.8125rem;color:#303030;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.add-block-btn{display:flex;align-items:center;gap:.375rem;width:calc(100% - 1.5rem);margin:.25rem .75rem .375rem 2rem;padding:.375rem .5rem;border:none;border-radius:8px;background:transparent;cursor:pointer;font-size:.8125rem;font-family:inherit;color:#005bd3;transition:all .15s}.add-block-btn:hover{background:#f0f5ff;color:#004299}.add-icon{font-size:.8125rem;font-weight:600}.add-icon-circle{display:flex;align-items:center;justify-content:center}.add-section-area{padding:.75rem;border-top:1px solid #e3e3e3;position:relative}.add-section-btn{display:flex;align-items:center;justify-content:center;gap:.375rem;width:100%;padding:.625rem;border:none;border-radius:8px;background:transparent;cursor:pointer;font-size:.8125rem;font-weight:500;font-family:inherit;color:#005bd3;transition:all .15s}.add-section-btn:hover{background:#f0f5ff}.section-picker{position:absolute;bottom:100%;left:.75rem;right:.75rem;background:#fff;border:1px solid #e3e3e3;border-radius:12px;box-shadow:0 -4px 12px #0000001a;max-height:300px;overflow-y:auto;z-index:100}.picker-item{display:flex;align-items:center;gap:.75rem;width:100%;padding:.625rem .75rem;border:none;border-bottom:1px solid #f6f6f7;background:#fff;cursor:pointer;text-align:left;font-family:inherit;transition:background .15s}.picker-item:last-child{border-bottom:none}.picker-item:hover{background:#f6f6f7}.picker-icon{font-size:1.25rem;color:#8c9196;display:flex;align-items:center;flex-shrink:0}.picker-info{display:flex;flex-direction:column;gap:.125rem}.picker-name{font-size:.8125rem;font-weight:500;color:#303030}.picker-desc{font-size:.75rem;color:#8c9196}.block-picker-overlay{position:fixed;inset:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:1000}.block-picker{background:#fff;border-radius:12px;width:340px;max-height:420px;display:flex;flex-direction:column;box-shadow:0 8px 32px #0003}.picker-header{display:flex;justify-content:space-between;align-items:center;padding:1rem 1.25rem;border-bottom:1px solid #e3e3e3}.picker-header h3{margin:0;font-size:.9375rem;font-weight:600;color:#303030}.picker-content{flex:1;overflow-y:auto;padding:.5rem}.picker-content .picker-item{border:1px solid #e3e3e3;border-radius:8px;margin-bottom:.5rem}.picker-content .picker-item:hover{border-color:#005bd3;background:#f8fafe}.picker-category{border-bottom:1px solid #f6f6f7}.picker-category:last-child{border-bottom:none}.picker-category-label{padding:.5rem .75rem .25rem;font-size:.6875rem;font-weight:600;color:#8c9196;text-transform:uppercase;letter-spacing:.3px}.picker-empty{padding:2rem;text-align:center;color:#8c9196;font-size:.8125rem}.canvas-area{display:flex;flex-direction:column;overflow:hidden}.toolbar{display:flex;align-items:center;justify-content:space-between;padding:0 1rem;height:56px;background:#fff;border-bottom:1px solid #e3e3e3}.toolbar-left,.toolbar-right{display:flex;gap:.5rem;align-items:center}.toolbar-center{display:flex;align-items:center}.toolbar-btn{padding:.375rem .75rem;border:1px solid #c9cccf;border-radius:8px;background:#fff;cursor:pointer;font-size:.8125rem;font-family:inherit;font-weight:500;color:#303030;transition:all .15s}.toolbar-btn:hover:not(:disabled){background:#f6f6f7}.toolbar-btn:disabled{opacity:.5;cursor:not-allowed}.preview-btn,.publish-btn{background:#303030;color:#fff;border-color:#303030;border-radius:8px}.preview-btn:hover,.publish-btn:hover:not(:disabled){background:#1a1a1a!important}.back-btn{border-color:transparent;background:transparent;color:#303030}.back-btn:hover:not(:disabled){background:#f6f6f7}.section-count{font-size:.8125rem;color:#616161}.page-title{font-size:.875rem;font-weight:600;color:#303030;margin-right:.75rem}.page-title-link{text-decoration:none;color:inherit}.page-title-link:hover{text-decoration:underline}.status-badge{display:inline-block;padding:.125rem .5rem;border-radius:10px;font-size:.6875rem;font-weight:500;text-transform:capitalize;background:#ffd6a4;color:#b98900}.status-badge.published{background:#aee9d1;color:#0d542b}.dirty-indicator{margin-left:.75rem;font-size:.75rem;color:#b98900;font-weight:500}.canvas{flex:1;overflow-y:auto;padding:2rem;background:#e8e8e8}.canvas.preview-mode{padding:0;background:#fff}.canvas.preview-mode .section-outer{margin:0}.canvas.preview-mode .section-wrapper{border:none;border-radius:0}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:#616161;text-align:center}.empty-icon{font-size:3rem;margin-bottom:1rem;color:#8c9196}.empty-state h3{margin:0 0 .5rem;color:#303030;font-size:1rem}.empty-state p{margin:0;font-size:.8125rem;color:#8c9196}.section-outer{position:relative;margin-bottom:0}.section-wrapper{border:1px solid transparent;border-radius:0;overflow:hidden;transition:border-color .15s;background:#fff}.section-outer:hover .section-wrapper{border-color:#005bd34d}.section-outer.selected .section-wrapper{border-color:#005bd366}.section-controls{position:absolute;top:-22px;left:0;right:0;display:flex;justify-content:space-between;align-items:center;padding:.1875rem .75rem;background:#005bd3;color:#fff;font-size:.75rem;border-radius:8px 8px 0 0;opacity:0;pointer-events:none;transition:opacity .15s;z-index:10}.section-outer:hover .section-controls,.section-outer.selected .section-controls{opacity:1;pointer-events:auto}.section-outer:hover .section-wrapper,.section-outer.selected .section-wrapper{border-top-left-radius:0;border-top-right-radius:0}.section-label{font-weight:500}.section-actions{display:flex;gap:.25rem}.action-btn{padding:.1875rem .5rem;border:none;border-radius:4px;background:#ffffff26;color:#fff;cursor:pointer;font-size:.625rem;font-family:inherit;transition:background .15s}.action-btn:hover{background:#ffffff4d}.action-btn.delete:hover{background:#d72c0d}.material-icon{font-family:Material Icons,sans-serif;font-size:inherit;font-weight:400;font-style:normal;line-height:1;letter-spacing:normal;text-transform:none;white-space:nowrap;word-wrap:normal;direction:ltr;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}.properties-panel{background:#fff;border-left:1px solid #e3e3e3;display:flex;flex-direction:column;overflow:hidden}.properties-header{display:flex;justify-content:space-between;align-items:center;padding:1rem;border-bottom:1px solid #e3e3e3}.properties-header h3{margin:0;font-size:.9375rem;font-weight:600;color:#303030}.editable-name{cursor:pointer;display:flex;align-items:center;gap:.375rem;border-radius:8px;padding:.25rem .375rem;margin:-.25rem -.375rem;transition:background .15s}.editable-name:hover{background:#f6f6f7}.edit-icon{opacity:0;color:#8c9196;transition:opacity .15s;display:flex;flex-shrink:0}.editable-name:hover .edit-icon{opacity:1}.name-edit-input{flex:1;padding:.375rem .5rem;border:1px solid #005bd3;border-radius:8px;font-size:.9375rem;font-weight:600;font-family:inherit;color:#303030;outline:none;box-shadow:0 0 0 1px #005bd3}.close-btn{border:none;background:none;font-size:1.25rem;cursor:pointer;color:#8c9196;transition:color .15s}.close-btn:hover{color:#303030}.more-menu-btn{border:none;background:none;cursor:pointer;color:#8c9196;padding:.25rem;border-radius:8px;display:flex;align-items:center;justify-content:center;transition:all .15s}.more-menu-btn:hover{background:#f6f6f7;color:#303030}.properties-content{flex:1;overflow-y:auto;padding:1rem}.property-group{margin-bottom:0;padding:1rem 0;border-top:1px solid #e3e3e3}.property-group:first-child{border-top:none}.group-title{display:flex;align-items:center;gap:.375rem;width:100%;padding:0;margin:0 0 .75rem;border:none;background:none;font-size:.8125rem;font-weight:600;font-family:inherit;color:#303030;cursor:pointer}.group-title:hover{color:#1a1a1a}.group-title:focus-visible{outline:2px solid #005bd3;outline-offset:2px;border-radius:4px}.group-title-text{flex:1;text-align:left}.group-toggle-icon{font-size:.5rem;color:#8c9196;transition:transform .15s;display:inline-block}.group-toggle-icon.collapsed{transform:rotate(-90deg)}.property-field{margin-bottom:1rem}.property-label{display:block;font-size:.8125rem;font-weight:500;margin-bottom:.375rem;color:#303030}.property-input,.property-select,.property-textarea{width:100%;padding:.5rem .75rem;border:1px solid #c9cccf;border-radius:8px;font-size:.8125rem;font-family:inherit;color:#303030;box-sizing:border-box;transition:border-color .15s,box-shadow .15s}.property-input:focus,.property-select:focus,.property-textarea:focus{outline:none;border-color:#005bd3;box-shadow:0 0 0 1px #005bd3}.property-input::placeholder,.property-textarea::placeholder{color:#8c9196}.property-textarea{resize:vertical;font-family:SF Mono,SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:.75rem}.color-field{display:flex;gap:.5rem;align-items:center}.color-picker{width:36px;height:36px;padding:2px;border:1px solid #e3e3e3;border-radius:6px;cursor:pointer}.color-text{flex:1}.no-selection{display:flex;align-items:center;justify-content:center;height:100%;color:#8c9196;text-align:center;padding:2rem}.no-selection p{margin:0;font-size:.8125rem}.panel-tabs{display:flex;border-bottom:1px solid #e3e3e3}.panel-tab{flex:1;padding:.75rem;border:none;border-bottom:2px solid transparent;background:#fff;cursor:pointer;font-size:.8125rem;font-weight:500;font-family:inherit;color:#616161;transition:all .15s}.panel-tab:hover{background:#f6f6f7;color:#303030}.panel-tab.active{background:#fff;color:#303030;font-weight:600;border-bottom:2px solid #005bd3}.blocks-list{flex:1;overflow-y:auto;padding:1rem}.slot-group{margin-bottom:1.5rem}.slot-title{font-size:.75rem;font-weight:600;color:#8c9196;margin:0 0 .5rem;text-transform:uppercase;letter-spacing:.3px}.block-item{display:flex;align-items:center;gap:.5rem;padding:.5rem .625rem;border:1px solid #e3e3e3;border-radius:8px;margin-bottom:.375rem;cursor:pointer;transition:all .15s}.block-item:hover{border-color:#005bd3;background:#f8fafe}.block-item.selected{border-color:#005bd3;background:#005bd3;color:#fff}.block-item.selected .block-icon,.block-item.selected .block-name{color:inherit}.block-icon{font-size:.875rem;color:#8c9196;display:flex;align-items:center}.block-name{flex:1;font-size:.8125rem;font-weight:500;color:#303030;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.block-actions{display:flex;gap:.125rem;opacity:0;transition:opacity .15s}.block-item:hover .block-actions{opacity:1}.mini-btn{padding:.125rem .375rem;border:none;border-radius:4px;background:transparent;cursor:pointer;font-size:.75rem;color:#8c9196;transition:all .15s}.mini-btn:hover{background:#e3e3e3;color:#303030}.mini-btn.delete:hover{background:#fef2f0;color:#d72c0d}.empty-slot{padding:.75rem;text-align:center;color:#8c9196;font-size:.8125rem;background:#f6f6f7;border:1px dashed #e3e3e3;border-radius:8px}.no-slots{padding:2rem;text-align:center;color:#8c9196}.no-slots p{margin:0;font-size:.8125rem}.delete-block-btn{width:auto;padding:0;margin-top:1rem;border:none;border-radius:0;background:transparent;color:#d72c0d;cursor:pointer;font-size:.8125rem;font-weight:500;font-family:inherit;transition:color .15s}.delete-block-btn:hover{background:transparent;color:#bc2200;text-decoration:underline}.toggle-field{display:flex;align-items:center;justify-content:space-between}.toggle-label{font-size:.8125rem;color:#303030}.toggle-switch{position:relative;display:inline-flex;cursor:pointer}.toggle-input{position:absolute;opacity:0;width:0;height:0}.toggle-track{width:36px;height:20px;background:#8c9196;border-radius:10px;position:relative;transition:background .2s}.toggle-thumb{position:absolute;top:2px;left:2px;width:16px;height:16px;background:#fff;border-radius:50%;transition:transform .2s;box-shadow:0 1px 3px #0003}.toggle-input:checked+.toggle-track{background:#008060}.toggle-input:checked+.toggle-track .toggle-thumb{transform:translate(16px)}.toggle-input:focus-visible+.toggle-track{outline:2px solid #005bd3;outline-offset:2px}.toggle-description{display:block;font-size:.75rem;color:#8c9196;margin-top:.25rem}.media-field{display:flex;flex-direction:column;gap:.5rem}.media-preview{position:relative;border:1px solid #e3e3e3;border-radius:8px;overflow:hidden}.media-thumbnail{display:block;width:100%;max-height:160px;object-fit:cover;background:#f6f6f7}.media-remove-btn{position:absolute;top:.375rem;right:.375rem;width:22px;height:22px;border:none;border-radius:50%;background:#0009;color:#fff;cursor:pointer;font-size:.875rem;line-height:1;display:flex;align-items:center;justify-content:center;transition:background .15s}.media-remove-btn:hover{background:#d72c0d}.media-browse-btn{padding:.5rem .75rem;border:1px solid #c9cccf;border-radius:8px;background:#fff;cursor:pointer;font-size:.8125rem;font-family:inherit;color:#303030;transition:all .15s}.media-browse-btn:hover{background:#f6f6f7;border-color:#8c9196}.media-url-input{font-size:.75rem;color:#616161}.video-preview{display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;background:#1a1a1a}.video-indicator{color:#fff;display:flex;align-items:center;flex-shrink:0}.video-url-text{flex:1;color:#8c9196;font-size:.6875rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.video-preview .media-remove-btn{position:static;flex-shrink:0}\n"], dependencies: [{ kind: "component", type: DynamicRendererComponent, selector: "lib-dynamic-renderer", inputs: ["element", "context"] }, { kind: "component", type: BlockTreeItemComponent, selector: "lib-block-tree-item", inputs: ["block", "context", "index", "totalSiblings", "expandAll"], outputs: ["selectBlock", "deleteBlock", "duplicateBlock", "moveBlock", "openBlockPicker"] }, { kind: "component", type: ShopifyFilePickerComponent, selector: "lib-shopify-file-picker", inputs: ["mediaType", "currentValue"], outputs: ["fileSelected", "closed"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6263
+ `, isInline: true, styles: [":host{display:block;height:100vh;overflow:hidden}.editor-layout{display:grid;grid-template-columns:280px 1fr 300px;height:100%;background:#f0f2f5}.sidebar{background:#fff;border-right:1px solid #e0e0e0;display:flex;flex-direction:column}.sidebar-header{padding:1rem;border-bottom:1px solid #e0e0e0}.sidebar-header h2{margin:0;font-size:.875rem;font-weight:600;color:#333}.search-box{display:none}.search-box:focus-within{border-color:#005bd3;background:#fff}.search-icon{font-size:.875rem;opacity:.6}.search-input{flex:1;border:none;background:transparent;font-size:.8125rem;color:#333;outline:none}.search-input::placeholder{color:#999}.search-clear{padding:.125rem .375rem;border:none;background:#ddd;border-radius:3px;cursor:pointer;font-size:.75rem;color:#666;line-height:1}.search-clear:hover{background:#ccc}.search-no-results{padding:2rem 1rem;text-align:center;color:#888}.search-no-results p{margin:0;font-size:.875rem}.picker-search{padding:.5rem;border-bottom:1px solid #e0e0e0}.picker-search-input{width:87%;padding:.5rem .75rem;border:1px solid #e0e0e0;border-radius:4px;font-size:.8125rem;outline:none;transition:border-color .2s}.picker-search-input:focus{border-color:#005bd3}.picker-search-input::placeholder{color:#999}.section-tree{flex:1;overflow-y:auto;padding:.25rem 0}.tree-section{border-bottom:none}.tree-section.selected>.tree-section-header{background:#005bd3;color:#fff;border-radius:8px;margin:0 .375rem}.tree-section.selected>.tree-section-header .section-name,.tree-section.selected>.tree-section-header .expand-btn,.tree-section.selected>.tree-section-header .section-icon,.tree-section.selected>.tree-section-header .drag-handle,.tree-section.selected>.tree-section-header .tree-btn{color:inherit}.tree-section-header{display:flex;align-items:center;gap:.5rem;padding:.5rem;cursor:pointer;transition:background .2s}.tree-section-header:hover{background:#f5f5f5;border-radius:8px;margin:0 .375rem}.expand-btn{width:16px;height:16px;padding:0;border:none;background:none;cursor:pointer;display:flex;align-items:center;justify-content:center;color:#666;transition:transform .2s;flex-shrink:0}.expand-btn.expanded .expand-icon{transform:rotate(90deg)}.expand-icon{font-size:.625rem;transition:transform .2s}.icon-drag-wrapper{position:relative;display:flex;align-items:center;justify-content:center;flex-shrink:0}.icon-drag-wrapper .drag-handle{position:absolute;inset:0;display:flex;align-items:center;justify-content:center}.tree-section-header:hover .icon-drag-wrapper .section-icon,.icon-drag-wrapper:has(.drag-handle:focus-visible) .section-icon{visibility:hidden}.section-icon{font-size:1rem}.section-name{flex:1;font-size:.8125rem;font-weight:500;color:#333;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tree-actions{display:flex;gap:.125rem;opacity:0;transition:opacity .2s}.tree-section-header:hover .tree-actions,.tree-block:hover .tree-actions{opacity:1}.tree-btn{padding:.125rem .375rem;border:none;border-radius:3px;background:#e0e0e0;cursor:pointer;font-size:.6875rem;color:#666}.tree-btn:hover{background:#d0d0d0}.tree-btn.delete:hover{background:#e74c3c;color:#fff}.tree-section.drag-over-before>.tree-section-header{box-shadow:inset 0 2px #4a90d9}.tree-section.drag-over-after>.tree-section-header{box-shadow:inset 0 -2px #4a90d9}.tree-section.is-section-dragging{opacity:.4}.tree-section.is-section-dragging>.tree-section-header{cursor:grabbing}.drag-handle{cursor:grab;color:#999;opacity:0;transition:opacity .2s;display:flex;align-items:center;justify-content:center;flex-shrink:0;padding:.125rem;border:none;background:none;border-radius:2px}.drag-handle:hover{color:#666;background:#e0e0e0}.drag-handle:focus-visible{opacity:1;outline:2px solid #4a90d9;outline-offset:1px}.tree-section-header:hover .drag-handle{opacity:1}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}@media(prefers-reduced-motion:reduce){.tree-section,.drag-handle,.tree-actions{transition:none}}.tree-section-content{padding-left:.5rem}.tree-block{display:flex;align-items:center;gap:.375rem;padding:.5rem .75rem;cursor:pointer;transition:background .2s}.tree-block:hover{background:#f5f5f5;border-radius:8px;margin:0 .375rem}.tree-block.selected{background:#005bd3;color:#fff;border-radius:8px;margin:0 .375rem}.tree-block.selected .block-icon,.tree-block.selected .block-name{color:inherit}.block-indent{width:20px;height:1px;background:#ddd;margin-left:10px}.block-icon{font-size:.875rem}.block-name{flex:1;font-size:.75rem;color:#555;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.add-block-btn{display:flex;align-items:center;gap:.375rem;width:calc(100% - 1.5rem);margin:.375rem .75rem .5rem 2rem;padding:.375rem .5rem;border:none;border-radius:4px;background:transparent;cursor:pointer;font-size:.75rem;color:#005bd3;transition:all .2s}.add-block-btn:hover{background:#f0f5ff;color:#004299}.add-icon{font-size:.875rem;font-weight:600}.add-icon-circle{display:flex;align-items:center;justify-content:center}.add-section-area{padding:.75rem;border-top:1px solid #e0e0e0;position:relative}.add-section-btn{display:flex;align-items:center;justify-content:center;gap:.375rem;width:100%;padding:.625rem;border:none;border-radius:6px;background:transparent;cursor:pointer;font-size:.8125rem;font-weight:500;color:#005bd3;transition:all .2s}.add-section-btn:hover{background:#f0f5ff}.section-picker{position:absolute;bottom:100%;left:.75rem;right:.75rem;background:#fff;border:1px solid #e0e0e0;border-radius:6px;box-shadow:0 -4px 12px #0000001a;max-height:300px;overflow-y:auto;z-index:100}.picker-item{display:flex;align-items:center;gap:.75rem;width:100%;padding:.75rem;border:none;border-bottom:1px solid #f0f0f0;background:#fff;cursor:pointer;text-align:left;transition:background .2s}.picker-item:last-child{border-bottom:none}.picker-item:hover{background:#f5f5f5}.picker-icon{font-family:Material Icons,sans-serif;font-size:1.5rem;font-weight:400;font-style:normal;line-height:1;letter-spacing:normal;text-transform:none;white-space:nowrap;word-wrap:normal;direction:ltr;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}.picker-info{display:flex;flex-direction:column;gap:.125rem}.picker-name{font-size:.8125rem;font-weight:500;color:#333}.picker-desc{font-size:.6875rem;color:#888}.block-picker-overlay{position:fixed;inset:0;background:#0006;display:flex;align-items:center;justify-content:center;z-index:1000}.block-picker{background:#fff;border-radius:8px;width:320px;max-height:400px;display:flex;flex-direction:column;box-shadow:0 8px 32px #0003}.picker-header{display:flex;justify-content:space-between;align-items:center;padding:1rem;border-bottom:1px solid #e0e0e0}.picker-header h3{margin:0;font-size:.9375rem;font-weight:600}.picker-content{flex:1;overflow-y:auto;padding:.5rem}.picker-content .picker-item{border:1px solid #e0e0e0;border-radius:6px;margin-bottom:.5rem}.picker-content .picker-item:hover{border-color:#005bd3}.picker-category{border-bottom:1px solid #f0f0f0}.picker-category:last-child{border-bottom:none}.picker-category-label{padding:.5rem .75rem .25rem;font-size:.6875rem;font-weight:600;color:#888}.picker-empty{padding:2rem;text-align:center;color:#888;font-size:.875rem}.canvas-area{display:flex;flex-direction:column;overflow:hidden}.toolbar{display:flex;align-items:center;justify-content:space-between;padding:.5rem 1rem;background:#fff;border-bottom:1px solid #e0e0e0}.toolbar-left,.toolbar-right{display:flex;gap:.5rem}.toolbar-btn{padding:.375rem .75rem;border:1px solid #e0e0e0;border-radius:8px;background:#fff;cursor:pointer;font-size:.8125rem;transition:all .2s}.toolbar-btn:hover:not(:disabled){background:#f0f2f5}.toolbar-btn:disabled{opacity:.5;cursor:not-allowed}.preview-btn,.publish-btn{background:#303030;color:#fff;border-color:#303030;border-radius:8px}.preview-btn:hover,.publish-btn:hover:not(:disabled){background:#1a1a1a!important}.back-btn{border-color:transparent;background:transparent}.section-count{font-size:.875rem;color:#666}.page-title{font-size:.9375rem;font-weight:600;color:#333;margin-right:.75rem}.page-title-link{text-decoration:none;color:inherit}.page-title-link:hover{text-decoration:underline}.status-badge{display:inline-block;padding:.125rem .5rem;border-radius:10px;font-size:.6875rem;font-weight:500;text-transform:capitalize;background:#ffeaa7;color:#856404}.status-badge.published{background:#b4fed3;color:#0d542b}.dirty-indicator{margin-left:.75rem;font-size:.8125rem;color:#e67e22;font-weight:500}.canvas{flex:1;overflow-y:auto;padding:2rem;position:relative}.canvas.preview-mode{padding:0;background:#fff}.preview-iframe{width:100%;height:100%;border:none;position:absolute;inset:0}.canvas.preview-mode .section-outer{margin:0}.canvas.preview-mode .section-wrapper{border:none;border-radius:0}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:#666;text-align:center}.empty-icon{font-size:4rem;margin-bottom:1rem}.empty-state h3{margin:0 0 .5rem;color:#333}.empty-state p{margin:0;font-size:.875rem}.section-outer{position:relative;margin-bottom:0}.section-wrapper{border:1px solid transparent;border-radius:0;overflow:hidden;transition:border-color .2s;background:#fff}.section-outer:hover .section-wrapper,.section-outer.selected .section-wrapper{border-color:#005bd366}.section-controls{position:absolute;top:-20px;left:0;right:0;display:flex;justify-content:space-between;align-items:center;padding:.14rem .75rem;background:#005bd3;color:#fff;font-size:.75rem;border-radius:8px 8px 0 0;opacity:0;pointer-events:none;transition:opacity .2s;z-index:10}.section-outer:hover .section-controls,.section-outer.selected .section-controls{opacity:1;pointer-events:auto}.section-outer:hover .section-wrapper,.section-outer.selected .section-wrapper{border-top-left-radius:0;border-top-right-radius:0}.section-label{font-weight:500}.section-actions{display:flex;gap:.25rem}.action-btn{padding:.15rem .5rem;border:none;border-radius:4px;background:#ffffff26;color:#fff;cursor:pointer;font-size:.6rem}.action-btn:hover{background:#ffffff4d}.action-btn.delete:hover{background:#e74c3c}.material-icon{font-family:Material Icons,sans-serif;font-size:.75rem;font-weight:400;font-style:normal;line-height:1;letter-spacing:normal;text-transform:none;white-space:nowrap;word-wrap:normal;direction:ltr;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}.properties-panel{background:#fff;border-left:1px solid #e0e0e0;display:flex;flex-direction:column;overflow:hidden}.properties-header{display:flex;justify-content:space-between;align-items:center;padding:1rem;border-bottom:1px solid #e0e0e0}.properties-header h3{margin:0;font-size:1rem;font-weight:600}.editable-name{cursor:pointer;display:flex;align-items:center;gap:.375rem;border-radius:4px;padding:.125rem .25rem;margin:-.125rem -.25rem;transition:background .2s}.editable-name:hover{background:#f0f2f5}.edit-icon{opacity:0;color:#999;transition:opacity .2s;display:flex;flex-shrink:0}.editable-name:hover .edit-icon{opacity:1}.name-edit-input{flex:1;padding:.375rem .5rem;border:1px solid #005bd3;border-radius:8px;font-size:1rem;font-weight:600;outline:none}.close-btn{border:none;background:none;font-size:1.25rem;cursor:pointer;color:#666}.more-menu-btn{border:none;background:none;cursor:pointer;color:#616161;padding:.25rem;border-radius:6px;display:flex;align-items:center;justify-content:center;transition:background .2s}.more-menu-btn:hover{background:#f0f2f5;color:#303030}.properties-content{flex:1;overflow-y:auto;padding:1rem}.property-group{margin-bottom:0;padding:1rem 0;border-top:1px solid #e0e0e0}.property-group:first-child{border-top:none}.group-title{display:flex;align-items:center;gap:.375rem;width:100%;padding:0;margin:0 0 .75rem;border:none;background:none;font-size:.8125rem;font-weight:600;color:#303030;cursor:pointer}.group-title:hover{color:#333}.group-title:focus-visible{outline:2px solid #4a90d9;outline-offset:2px;border-radius:2px}.group-title-text{flex:1;text-align:left}.group-toggle-icon{font-size:.5rem;transition:transform .2s;display:inline-block}.group-toggle-icon.collapsed{transform:rotate(-90deg)}.property-field{margin-bottom:1rem}.property-label{display:block;font-size:.875rem;font-weight:500;margin-bottom:.375rem;color:#616161}.property-input,.property-select,.property-textarea{width:100%;padding:.5rem .75rem;border:1px solid #c9cccf;border-radius:8px;font-size:.875rem;transition:border-color .2s,box-shadow .2s}.property-input:focus,.property-select:focus,.property-textarea:focus{outline:none;border-color:#005bd3;box-shadow:0 0 0 1px #005bd3}.property-textarea{resize:vertical;font-family:monospace;font-size:.75rem}.color-field{display:flex;gap:.5rem}.color-picker{width:40px;height:36px;padding:2px;border:1px solid #e0e0e0;border-radius:4px;cursor:pointer}.color-text{flex:1}.no-selection{display:flex;align-items:center;justify-content:center;height:100%;color:#666;text-align:center;padding:2rem}.no-selection p{margin:0;font-size:.875rem}.panel-tabs{display:flex;border-bottom:1px solid #e0e0e0}.panel-tab{flex:1;padding:.75rem;border:none;border-bottom:2px solid transparent;background:#fff;cursor:pointer;font-size:.75rem;font-weight:500;color:#666;transition:all .2s}.panel-tab:hover{background:#f8f9fa;color:#303030}.panel-tab.active{background:#fff;color:#303030;border-bottom:2px solid #005bd3}.blocks-list{flex:1;overflow-y:auto;padding:1rem}.slot-group{margin-bottom:1.5rem}.slot-title{font-size:.75rem;font-weight:600;color:#616161;margin:0 0 .5rem}.block-item{display:flex;align-items:center;gap:.5rem;padding:.5rem;border:1px solid #e0e0e0;border-radius:4px;margin-bottom:.375rem;cursor:pointer;transition:all .2s}.block-item:hover{border-color:#005bd3;background:#f8f9fa}.block-item.selected{border-color:#005bd3;background:#005bd3;color:#fff}.block-item.selected .block-icon,.block-item.selected .block-name{color:inherit}.block-icon{font-size:1rem}.block-name{flex:1;font-size:.8rem;font-weight:500;color:#333;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.block-actions{display:flex;gap:.125rem;opacity:0;transition:opacity .2s}.block-item:hover .block-actions{opacity:1}.mini-btn{padding:.125rem .375rem;border:none;border-radius:3px;background:#e0e0e0;cursor:pointer;font-size:.75rem;color:#666}.mini-btn:hover{background:#d0d0d0}.mini-btn.delete:hover{background:#e74c3c;color:#fff}.empty-slot{padding:.75rem;text-align:center;color:#999;font-size:.75rem;background:#f8f9fa;border:1px dashed #ddd;border-radius:4px}.no-slots{padding:2rem;text-align:center;color:#666}.no-slots p{margin:0;font-size:.875rem}.delete-block-btn{width:auto;padding:0;margin-top:1rem;border:none;border-radius:0;background:transparent;color:#d72c0d;cursor:pointer;font-size:.875rem;font-weight:500;transition:color .2s}.delete-block-btn:hover{background:transparent;color:#bc2200;text-decoration:underline}.toggle-field{display:flex;align-items:center;justify-content:space-between}.toggle-label{font-size:.875rem;color:#303030}.toggle-switch{position:relative;display:inline-flex;cursor:pointer}.toggle-input{position:absolute;opacity:0;width:0;height:0}.toggle-track{width:36px;height:20px;background:#b5b5b5;border-radius:10px;position:relative;transition:background .2s}.toggle-thumb{position:absolute;top:2px;left:2px;width:16px;height:16px;background:#fff;border-radius:50%;transition:transform .2s;box-shadow:0 1px 3px #0003}.toggle-input:checked+.toggle-track{background:#008060}.toggle-input:checked+.toggle-track .toggle-thumb{transform:translate(16px)}.toggle-input:focus-visible+.toggle-track{outline:2px solid #005bd3;outline-offset:2px}.toggle-description{display:block;font-size:.75rem;color:#616161;margin-top:.25rem}.media-field{display:flex;flex-direction:column;gap:.5rem}.media-preview{position:relative;border:1px solid #e0e0e0;border-radius:8px;overflow:hidden}.media-thumbnail{display:block;width:100%;max-height:160px;object-fit:cover;background:#f0f2f5}.media-remove-btn{position:absolute;top:.25rem;right:.25rem;width:22px;height:22px;border:none;border-radius:50%;background:#0009;color:#fff;cursor:pointer;font-size:.875rem;line-height:1;display:flex;align-items:center;justify-content:center}.media-remove-btn:hover{background:#d72c0d}.media-browse-btn{padding:.5rem .75rem;border:1px solid #c9cccf;border-radius:8px;background:#fff;cursor:pointer;font-size:.8125rem;color:#303030;transition:all .15s}.media-browse-btn:hover{background:#f0f2f5;border-color:#999}.media-url-input{font-size:.75rem;color:#616161}.video-preview{display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;background:#1a1a1a}.video-indicator{color:#fff;display:flex;align-items:center;flex-shrink:0}.video-url-text{flex:1;color:#ccc;font-size:.6875rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.video-preview .media-remove-btn{position:static;flex-shrink:0}\n"], dependencies: [{ kind: "component", type: DynamicRendererComponent, selector: "lib-dynamic-renderer", inputs: ["element", "context"] }, { kind: "component", type: BlockTreeItemComponent, selector: "lib-block-tree-item", inputs: ["block", "context", "index", "totalSiblings", "expandAll"], outputs: ["selectBlock", "deleteBlock", "duplicateBlock", "moveBlock", "openBlockPicker"] }, { kind: "component", type: ShopifyFilePickerComponent, selector: "lib-shopify-file-picker", inputs: ["mediaType", "currentValue"], outputs: ["fileSelected", "closed"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6584
6264
  }
6585
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: VisualEditorComponent, decorators: [{
6265
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: VisualEditorComponent, decorators: [{
6586
6266
  type: Component,
6587
- args: [{ selector: 'lib-visual-editor', imports: [DynamicRendererComponent, BlockTreeItemComponent, ShopifyFilePickerComponent], template: `
6267
+ args: [{ selector: 'lib-visual-editor', imports: [DynamicRendererComponent, BlockTreeItemComponent, ShopifyFilePickerComponent], providers: [IframeBridgeService], template: `
6588
6268
  <div class="editor-layout">
6589
6269
  <!-- Sidebar: Hierarchical Tree -->
6590
6270
  <aside class="sidebar">
6591
6271
  <div class="sidebar-header">
6592
6272
  <h2>Page Structure</h2>
6593
6273
  <div class="search-box">
6594
- <span class="search-icon"><svg width="14" height="14" viewBox="0 0 20 20" fill="currentColor"><path d="M8 16a8 8 0 116.32-3.1l5.39 5.38a1 1 0 01-1.42 1.42l-5.38-5.39A8 8 0 018 16zm0-2A6 6 0 108 2a6 6 0 000 12z"/></svg></span>
6274
+ <span class="search-icon">🔍</span>
6595
6275
  <input
6596
6276
  type="text"
6597
6277
  class="search-input"
@@ -6639,7 +6319,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
6639
6319
  <span class="expand-icon">&#9654;</span>
6640
6320
  </button>
6641
6321
  <span class="icon-drag-wrapper">
6642
- <span class="section-icon" [innerHTML]="getSectionIcon(section.type)"></span>
6322
+ <span class="section-icon">{{ getSectionIcon(section.type) }}</span>
6643
6323
  <span
6644
6324
  class="drag-handle section-drag-handle"
6645
6325
  role="button"
@@ -6741,7 +6421,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
6741
6421
  }
6742
6422
  @for (rp of group.presets; track rp.displayName + '-' + rp.definition.type) {
6743
6423
  <button class="picker-item" (click)="addSectionFromPreset(rp)">
6744
- <span class="picker-icon" [innerHTML]="getSectionPresetIcon(rp)"></span>
6424
+ <span class="picker-icon">{{ getSectionPresetIcon(rp) }}</span>
6745
6425
  <div class="picker-info">
6746
6426
  <span class="picker-name">{{ rp.displayName }}</span>
6747
6427
  @if (rp.displayDescription) {
@@ -6779,7 +6459,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
6779
6459
  <div class="picker-content">
6780
6460
  @for (rp of filteredBlockPresetsForSection(); track rp.displayName + '-' + rp.definition.type) {
6781
6461
  <button class="picker-item" (click)="addBlockToSectionFromPreset(blockPickerSection()!, rp)">
6782
- <span class="picker-icon" [innerHTML]="getBlockPresetIcon(rp)"></span>
6462
+ <span class="picker-icon">{{ getBlockPresetIcon(rp) }}</span>
6783
6463
  <div class="picker-info">
6784
6464
  <span class="picker-name">{{ rp.displayName }}</span>
6785
6465
  @if (rp.displayDescription) {
@@ -6822,7 +6502,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
6822
6502
  <div class="picker-content">
6823
6503
  @for (rp of filteredBlockPresetsForNestedSlot(); track rp.displayName + '-' + rp.definition.type) {
6824
6504
  <button class="picker-item" (click)="addNestedBlockFromPreset(rp)">
6825
- <span class="picker-icon" [innerHTML]="getBlockPresetIcon(rp)"></span>
6505
+ <span class="picker-icon">{{ getBlockPresetIcon(rp) }}</span>
6826
6506
  <div class="picker-info">
6827
6507
  <span class="picker-name">{{ rp.displayName }}</span>
6828
6508
  @if (rp.displayDescription) {
@@ -6852,7 +6532,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
6852
6532
  <div class="toolbar">
6853
6533
  <div class="toolbar-left">
6854
6534
  <button class="toolbar-btn back-btn" (click)="goBack()" title="Back to pages">
6855
- <svg width="14" height="14" viewBox="0 0 20 20" fill="currentColor" style="vertical-align: middle"><path d="M17 9H5.414l3.293-3.293a1 1 0 00-1.414-1.414l-5 5a1 1 0 000 1.414l5 5a1 1 0 001.414-1.414L5.414 11H17a1 1 0 100-2z"/></svg> Back
6535
+ Back
6856
6536
  </button>
6857
6537
  <button
6858
6538
  class="toolbar-btn"
@@ -6860,7 +6540,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
6860
6540
  (click)="facade.undo()"
6861
6541
  title="Undo"
6862
6542
  >
6863
- <svg width="14" height="14" viewBox="0 0 20 20" fill="currentColor" style="vertical-align: middle"><path d="M7.5 4.5a1 1 0 00-1.7-.7l-4 4a1 1 0 000 1.4l4 4a1 1 0 001.7-.7V11h5a4 4 0 010 8H9a1 1 0 110-2h3.5a2 2 0 000-4h-5v1.5a1 1 0 01-1.7.7l-4-4a1 1 0 010-1.4l4-4a1 1 0 011.7.7V6h5a6 6 0 010 12H9a3 3 0 010-6h-1.5V4.5z"/></svg> Undo
6543
+ Undo
6864
6544
  </button>
6865
6545
  <button
6866
6546
  class="toolbar-btn"
@@ -6868,7 +6548,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
6868
6548
  (click)="facade.redo()"
6869
6549
  title="Redo"
6870
6550
  >
6871
- <svg width="14" height="14" viewBox="0 0 20 20" fill="currentColor" style="vertical-align: middle; transform: scaleX(-1)"><path d="M7.5 4.5a1 1 0 00-1.7-.7l-4 4a1 1 0 000 1.4l4 4a1 1 0 001.7-.7V11h5a4 4 0 010 8H9a1 1 0 110-2h3.5a2 2 0 000-4h-5v1.5a1 1 0 01-1.7.7l-4-4a1 1 0 010-1.4l4-4a1 1 0 011.7.7V6h5a6 6 0 010 12H9a3 3 0 010-6h-1.5V4.5z"/></svg> Redo
6551
+ Redo
6872
6552
  </button>
6873
6553
  </div>
6874
6554
  <div class="toolbar-center">
@@ -6901,7 +6581,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
6901
6581
  (click)="savePage()"
6902
6582
  title="Save changes"
6903
6583
  >
6904
- {{ isSaving() ? 'Saving...' : 'Save' }}
6584
+ {{ isSaving() ? 'Saving...' : '💾 Save' }}
6905
6585
  </button>
6906
6586
  }
6907
6587
  @if (facade.currentPage() && showPublishButtons()) {
@@ -6912,7 +6592,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
6912
6592
  (click)="publishPage()"
6913
6593
  title="Publish page"
6914
6594
  >
6915
- {{ isPublishing() ? 'Publishing...' : 'Publish' }}
6595
+ {{ isPublishing() ? 'Publishing...' : '🚀 Publish' }}
6916
6596
  </button>
6917
6597
  } @else {
6918
6598
  <button
@@ -6921,57 +6601,68 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
6921
6601
  (click)="unpublishPage()"
6922
6602
  title="Unpublish page"
6923
6603
  >
6924
- {{ isPublishing() ? 'Unpublishing...' : 'Unpublish' }}
6604
+ {{ isPublishing() ? 'Unpublishing...' : '📥 Unpublish' }}
6925
6605
  </button>
6926
6606
  }
6927
6607
  }
6928
6608
  <button class="toolbar-btn preview-btn" (click)="togglePreview()">
6929
- {{ isPreviewMode() ? 'Edit' : 'Preview' }}
6609
+ {{ isPreviewMode() ? '✏️ Edit' : '👁 Preview' }}
6930
6610
  </button>
6931
6611
  </div>
6932
6612
  </div>
6933
6613
 
6934
6614
  <!-- Canvas Content -->
6935
6615
  <div class="canvas" #canvasEl [class.preview-mode]="isPreviewMode()">
6936
- @if (facade.sections().length === 0) {
6937
- <div class="empty-state">
6938
- <div class="empty-icon"><svg width="48" height="48" viewBox="0 0 20 20" fill="currentColor"><path d="M6 2a2 2 0 00-2 2v12a2 2 0 002 2h8a2 2 0 002-2V7.414a2 2 0 00-.586-1.414l-3.414-3.414A2 2 0 0010.586 2H6zm0 2h4v3a1 1 0 001 1h3v8H6V4z"/></svg></div>
6939
- <h3>Start Building</h3>
6940
- <p>Click on a component from the sidebar to add it to your page</p>
6941
- </div>
6616
+ @if (previewUrl()) {
6617
+ <iframe
6618
+ #previewFrame
6619
+ class="preview-iframe"
6620
+ [src]="previewUrl()!"
6621
+ (load)="onIframeLoad()"
6622
+ allow="clipboard-read; clipboard-write"
6623
+ ></iframe>
6942
6624
  } @else {
6943
- @for (section of facade.sections(); track section.id; let idx = $index) {
6944
- <div
6945
- class="section-outer"
6946
- [class.selected]="facade.selectedSection()?.id === section.id"
6947
- >
6948
- @if (!isPreviewMode()) {
6949
- <div class="section-controls">
6950
- <span class="section-label">{{ getSectionName(section) }}</span>
6951
- <div class="section-actions">
6952
- @if (idx > 0) {
6953
- <button class="action-btn" (click)="moveUp(section.id, idx, $event)" title="Move up">↑</button>
6954
- }
6955
- @if (idx < facade.sections().length - 1) {
6956
- <button class="action-btn" (click)="moveDown(section.id, idx, $event)" title="Move down">↓</button>
6957
- }
6958
- @if (facade.isSectionDuplicable(section.type)) {
6959
- <button class="action-btn" (click)="duplicateSection(section.id, $event)" title="Duplicate">
6960
- <span class="material-icon">content_copy</span>
6961
- </button>
6962
- }
6963
- <button class="action-btn delete" (click)="deleteSection(section.id, $event)" title="Delete">×</button>
6964
- </div>
6965
- </div>
6966
- }
6625
+ <!-- Fallback: direct rendering when no storefront URL configured -->
6626
+ @if (facade.sections().length === 0) {
6627
+ <div class="empty-state">
6628
+ <div class="empty-icon">📄</div>
6629
+ <h3>Start Building</h3>
6630
+ <p>Click on a component from the sidebar to add it to your page</p>
6631
+ </div>
6632
+ } @else {
6633
+ @for (section of facade.sections(); track section.id; let idx = $index) {
6967
6634
  <div
6968
- class="section-wrapper"
6969
- [attr.data-section-id]="section.id"
6970
- (click)="selectSection(section, $event)"
6635
+ class="section-outer"
6636
+ [class.selected]="facade.selectedSection()?.id === section.id"
6971
6637
  >
6972
- <lib-dynamic-renderer [element]="section" [context]="editorContext()" />
6638
+ @if (!isPreviewMode()) {
6639
+ <div class="section-controls">
6640
+ <span class="section-label">{{ getSectionName(section) }}</span>
6641
+ <div class="section-actions">
6642
+ @if (idx > 0) {
6643
+ <button class="action-btn" (click)="moveUp(section.id, idx, $event)" title="Move up">↑</button>
6644
+ }
6645
+ @if (idx < facade.sections().length - 1) {
6646
+ <button class="action-btn" (click)="moveDown(section.id, idx, $event)" title="Move down">↓</button>
6647
+ }
6648
+ @if (facade.isSectionDuplicable(section.type)) {
6649
+ <button class="action-btn" (click)="duplicateSection(section.id, $event)" title="Duplicate">
6650
+ <span class="material-icon">content_copy</span>
6651
+ </button>
6652
+ }
6653
+ <button class="action-btn delete" (click)="deleteSection(section.id, $event)" title="Delete">×</button>
6654
+ </div>
6655
+ </div>
6656
+ }
6657
+ <div
6658
+ class="section-wrapper"
6659
+ [attr.data-section-id]="section.id"
6660
+ (click)="selectSection(section, $event)"
6661
+ >
6662
+ <lib-dynamic-renderer [element]="section" [context]="editorContext()" />
6663
+ </div>
6973
6664
  </div>
6974
- </div>
6665
+ }
6975
6666
  }
6976
6667
  }
6977
6668
  </div>
@@ -7361,7 +7052,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
7361
7052
  [class.selected]="facade.selectedBlock()?.id === block.id"
7362
7053
  (click)="selectBlock(block)"
7363
7054
  >
7364
- <span class="block-icon" [innerHTML]="getBlockIconByType(block.type)"></span>
7055
+ <span class="block-icon">{{ getBlockIconByType(block.type) }}</span>
7365
7056
  <span class="block-name">{{ getBlockName(block) }}</span>
7366
7057
  <div class="block-actions">
7367
7058
  @if (idx > 0) {
@@ -7408,8 +7099,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
7408
7099
  />
7409
7100
  }
7410
7101
  </div>
7411
- `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block;height:calc(100vh - 56px);overflow:hidden;font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif}.editor-layout{display:grid;grid-template-columns:280px 1fr 300px;height:100%;background:#f6f6f7}.sidebar{background:#fff;border-right:1px solid #e3e3e3;display:flex;flex-direction:column}.sidebar-header{padding:1rem;border-bottom:1px solid #e3e3e3}.sidebar-header h2{margin:0;font-size:.8125rem;font-weight:600;color:#303030}.search-box{display:none}.search-box:focus-within{border-color:#005bd3;background:#fff}.search-icon{font-size:.8125rem;opacity:.6}.search-input{flex:1;border:none;background:transparent;font-size:.8125rem;color:#303030;font-family:inherit;outline:none}.search-input::placeholder{color:#8c9196}.search-clear{padding:.125rem .375rem;border:none;background:#e3e3e3;border-radius:4px;cursor:pointer;font-size:.75rem;color:#616161;line-height:1}.search-clear:hover{background:#c9cccf}.search-no-results{padding:2rem 1rem;text-align:center;color:#8c9196}.search-no-results p{margin:0;font-size:.8125rem}.picker-search{padding:.5rem .75rem;border-bottom:1px solid #e3e3e3}.picker-search-input{width:100%;padding:.5rem .75rem;border:1px solid #c9cccf;border-radius:8px;font-size:.8125rem;font-family:inherit;outline:none;box-sizing:border-box;transition:border-color .2s,box-shadow .2s}.picker-search-input:focus{border-color:#005bd3;box-shadow:0 0 0 1px #005bd3}.picker-search-input::placeholder{color:#8c9196}.section-tree{flex:1;overflow-y:auto;padding:.25rem 0}.tree-section{border-bottom:none}.tree-section.selected>.tree-section-header{background:#005bd3;color:#fff;border-radius:8px;margin:0 .375rem}.tree-section.selected>.tree-section-header .section-name,.tree-section.selected>.tree-section-header .expand-btn,.tree-section.selected>.tree-section-header .section-icon,.tree-section.selected>.tree-section-header .drag-handle,.tree-section.selected>.tree-section-header .tree-btn{color:inherit}.tree-section-header{display:flex;align-items:center;gap:.375rem;padding:.375rem .5rem;cursor:pointer;transition:background .15s}.tree-section-header:hover{background:#f6f6f7;border-radius:8px;margin:0 .375rem}.expand-btn{width:16px;height:16px;padding:0;border:none;background:none;cursor:pointer;display:flex;align-items:center;justify-content:center;color:#8c9196;transition:transform .15s;flex-shrink:0}.expand-btn.expanded .expand-icon{transform:rotate(90deg)}.expand-icon{font-size:.5rem;transition:transform .15s}.icon-drag-wrapper{position:relative;display:flex;align-items:center;justify-content:center;flex-shrink:0}.icon-drag-wrapper .drag-handle{position:absolute;inset:0;display:flex;align-items:center;justify-content:center}.tree-section-header:hover .icon-drag-wrapper .section-icon,.icon-drag-wrapper:has(.drag-handle:focus-visible) .section-icon{visibility:hidden}.section-icon{font-size:.875rem;color:#8c9196;display:flex;align-items:center}.section-name{flex:1;font-size:.8125rem;font-weight:500;color:#303030;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tree-actions{display:flex;gap:.125rem;opacity:0;transition:opacity .15s}.tree-section-header:hover .tree-actions,.tree-block:hover .tree-actions{opacity:1}.tree-btn{padding:.125rem .375rem;border:none;border-radius:4px;background:transparent;cursor:pointer;font-size:.6875rem;color:#8c9196;transition:all .15s}.tree-btn:hover{background:#e3e3e3;color:#303030}.tree-btn.delete:hover{background:#fef2f0;color:#d72c0d}.tree-section.drag-over-before>.tree-section-header{box-shadow:inset 0 2px #005bd3}.tree-section.drag-over-after>.tree-section-header{box-shadow:inset 0 -2px #005bd3}.tree-section.is-section-dragging{opacity:.4}.tree-section.is-section-dragging>.tree-section-header{cursor:grabbing}.drag-handle{cursor:grab;color:#8c9196;opacity:0;transition:opacity .15s;display:flex;align-items:center;justify-content:center;flex-shrink:0;padding:.125rem;border:none;background:none;border-radius:4px}.drag-handle:hover{color:#303030;background:#e3e3e3}.drag-handle:focus-visible{opacity:1;outline:2px solid #005bd3;outline-offset:1px}.tree-section-header:hover .drag-handle{opacity:1}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}@media(prefers-reduced-motion:reduce){.tree-section,.drag-handle,.tree-actions{transition:none}}.tree-section-content{padding-left:.5rem}.tree-block{display:flex;align-items:center;gap:.375rem;padding:.375rem .75rem;cursor:pointer;transition:background .15s}.tree-block:hover{background:#f6f6f7;border-radius:8px;margin:0 .375rem}.tree-block.selected{background:#005bd3;color:#fff;border-radius:8px;margin:0 .375rem}.tree-block.selected .block-icon,.tree-block.selected .block-name{color:inherit}.block-indent{width:18px;height:1px;background:#e3e3e3;margin-left:10px}.block-name{flex:1;font-size:.8125rem;color:#303030;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.add-block-btn{display:flex;align-items:center;gap:.375rem;width:calc(100% - 1.5rem);margin:.25rem .75rem .375rem 2rem;padding:.375rem .5rem;border:none;border-radius:8px;background:transparent;cursor:pointer;font-size:.8125rem;font-family:inherit;color:#005bd3;transition:all .15s}.add-block-btn:hover{background:#f0f5ff;color:#004299}.add-icon{font-size:.8125rem;font-weight:600}.add-icon-circle{display:flex;align-items:center;justify-content:center}.add-section-area{padding:.75rem;border-top:1px solid #e3e3e3;position:relative}.add-section-btn{display:flex;align-items:center;justify-content:center;gap:.375rem;width:100%;padding:.625rem;border:none;border-radius:8px;background:transparent;cursor:pointer;font-size:.8125rem;font-weight:500;font-family:inherit;color:#005bd3;transition:all .15s}.add-section-btn:hover{background:#f0f5ff}.section-picker{position:absolute;bottom:100%;left:.75rem;right:.75rem;background:#fff;border:1px solid #e3e3e3;border-radius:12px;box-shadow:0 -4px 12px #0000001a;max-height:300px;overflow-y:auto;z-index:100}.picker-item{display:flex;align-items:center;gap:.75rem;width:100%;padding:.625rem .75rem;border:none;border-bottom:1px solid #f6f6f7;background:#fff;cursor:pointer;text-align:left;font-family:inherit;transition:background .15s}.picker-item:last-child{border-bottom:none}.picker-item:hover{background:#f6f6f7}.picker-icon{font-size:1.25rem;color:#8c9196;display:flex;align-items:center;flex-shrink:0}.picker-info{display:flex;flex-direction:column;gap:.125rem}.picker-name{font-size:.8125rem;font-weight:500;color:#303030}.picker-desc{font-size:.75rem;color:#8c9196}.block-picker-overlay{position:fixed;inset:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:1000}.block-picker{background:#fff;border-radius:12px;width:340px;max-height:420px;display:flex;flex-direction:column;box-shadow:0 8px 32px #0003}.picker-header{display:flex;justify-content:space-between;align-items:center;padding:1rem 1.25rem;border-bottom:1px solid #e3e3e3}.picker-header h3{margin:0;font-size:.9375rem;font-weight:600;color:#303030}.picker-content{flex:1;overflow-y:auto;padding:.5rem}.picker-content .picker-item{border:1px solid #e3e3e3;border-radius:8px;margin-bottom:.5rem}.picker-content .picker-item:hover{border-color:#005bd3;background:#f8fafe}.picker-category{border-bottom:1px solid #f6f6f7}.picker-category:last-child{border-bottom:none}.picker-category-label{padding:.5rem .75rem .25rem;font-size:.6875rem;font-weight:600;color:#8c9196;text-transform:uppercase;letter-spacing:.3px}.picker-empty{padding:2rem;text-align:center;color:#8c9196;font-size:.8125rem}.canvas-area{display:flex;flex-direction:column;overflow:hidden}.toolbar{display:flex;align-items:center;justify-content:space-between;padding:0 1rem;height:56px;background:#fff;border-bottom:1px solid #e3e3e3}.toolbar-left,.toolbar-right{display:flex;gap:.5rem;align-items:center}.toolbar-center{display:flex;align-items:center}.toolbar-btn{padding:.375rem .75rem;border:1px solid #c9cccf;border-radius:8px;background:#fff;cursor:pointer;font-size:.8125rem;font-family:inherit;font-weight:500;color:#303030;transition:all .15s}.toolbar-btn:hover:not(:disabled){background:#f6f6f7}.toolbar-btn:disabled{opacity:.5;cursor:not-allowed}.preview-btn,.publish-btn{background:#303030;color:#fff;border-color:#303030;border-radius:8px}.preview-btn:hover,.publish-btn:hover:not(:disabled){background:#1a1a1a!important}.back-btn{border-color:transparent;background:transparent;color:#303030}.back-btn:hover:not(:disabled){background:#f6f6f7}.section-count{font-size:.8125rem;color:#616161}.page-title{font-size:.875rem;font-weight:600;color:#303030;margin-right:.75rem}.page-title-link{text-decoration:none;color:inherit}.page-title-link:hover{text-decoration:underline}.status-badge{display:inline-block;padding:.125rem .5rem;border-radius:10px;font-size:.6875rem;font-weight:500;text-transform:capitalize;background:#ffd6a4;color:#b98900}.status-badge.published{background:#aee9d1;color:#0d542b}.dirty-indicator{margin-left:.75rem;font-size:.75rem;color:#b98900;font-weight:500}.canvas{flex:1;overflow-y:auto;padding:2rem;background:#e8e8e8}.canvas.preview-mode{padding:0;background:#fff}.canvas.preview-mode .section-outer{margin:0}.canvas.preview-mode .section-wrapper{border:none;border-radius:0}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:#616161;text-align:center}.empty-icon{font-size:3rem;margin-bottom:1rem;color:#8c9196}.empty-state h3{margin:0 0 .5rem;color:#303030;font-size:1rem}.empty-state p{margin:0;font-size:.8125rem;color:#8c9196}.section-outer{position:relative;margin-bottom:0}.section-wrapper{border:1px solid transparent;border-radius:0;overflow:hidden;transition:border-color .15s;background:#fff}.section-outer:hover .section-wrapper{border-color:#005bd34d}.section-outer.selected .section-wrapper{border-color:#005bd366}.section-controls{position:absolute;top:-22px;left:0;right:0;display:flex;justify-content:space-between;align-items:center;padding:.1875rem .75rem;background:#005bd3;color:#fff;font-size:.75rem;border-radius:8px 8px 0 0;opacity:0;pointer-events:none;transition:opacity .15s;z-index:10}.section-outer:hover .section-controls,.section-outer.selected .section-controls{opacity:1;pointer-events:auto}.section-outer:hover .section-wrapper,.section-outer.selected .section-wrapper{border-top-left-radius:0;border-top-right-radius:0}.section-label{font-weight:500}.section-actions{display:flex;gap:.25rem}.action-btn{padding:.1875rem .5rem;border:none;border-radius:4px;background:#ffffff26;color:#fff;cursor:pointer;font-size:.625rem;font-family:inherit;transition:background .15s}.action-btn:hover{background:#ffffff4d}.action-btn.delete:hover{background:#d72c0d}.material-icon{font-family:Material Icons,sans-serif;font-size:inherit;font-weight:400;font-style:normal;line-height:1;letter-spacing:normal;text-transform:none;white-space:nowrap;word-wrap:normal;direction:ltr;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}.properties-panel{background:#fff;border-left:1px solid #e3e3e3;display:flex;flex-direction:column;overflow:hidden}.properties-header{display:flex;justify-content:space-between;align-items:center;padding:1rem;border-bottom:1px solid #e3e3e3}.properties-header h3{margin:0;font-size:.9375rem;font-weight:600;color:#303030}.editable-name{cursor:pointer;display:flex;align-items:center;gap:.375rem;border-radius:8px;padding:.25rem .375rem;margin:-.25rem -.375rem;transition:background .15s}.editable-name:hover{background:#f6f6f7}.edit-icon{opacity:0;color:#8c9196;transition:opacity .15s;display:flex;flex-shrink:0}.editable-name:hover .edit-icon{opacity:1}.name-edit-input{flex:1;padding:.375rem .5rem;border:1px solid #005bd3;border-radius:8px;font-size:.9375rem;font-weight:600;font-family:inherit;color:#303030;outline:none;box-shadow:0 0 0 1px #005bd3}.close-btn{border:none;background:none;font-size:1.25rem;cursor:pointer;color:#8c9196;transition:color .15s}.close-btn:hover{color:#303030}.more-menu-btn{border:none;background:none;cursor:pointer;color:#8c9196;padding:.25rem;border-radius:8px;display:flex;align-items:center;justify-content:center;transition:all .15s}.more-menu-btn:hover{background:#f6f6f7;color:#303030}.properties-content{flex:1;overflow-y:auto;padding:1rem}.property-group{margin-bottom:0;padding:1rem 0;border-top:1px solid #e3e3e3}.property-group:first-child{border-top:none}.group-title{display:flex;align-items:center;gap:.375rem;width:100%;padding:0;margin:0 0 .75rem;border:none;background:none;font-size:.8125rem;font-weight:600;font-family:inherit;color:#303030;cursor:pointer}.group-title:hover{color:#1a1a1a}.group-title:focus-visible{outline:2px solid #005bd3;outline-offset:2px;border-radius:4px}.group-title-text{flex:1;text-align:left}.group-toggle-icon{font-size:.5rem;color:#8c9196;transition:transform .15s;display:inline-block}.group-toggle-icon.collapsed{transform:rotate(-90deg)}.property-field{margin-bottom:1rem}.property-label{display:block;font-size:.8125rem;font-weight:500;margin-bottom:.375rem;color:#303030}.property-input,.property-select,.property-textarea{width:100%;padding:.5rem .75rem;border:1px solid #c9cccf;border-radius:8px;font-size:.8125rem;font-family:inherit;color:#303030;box-sizing:border-box;transition:border-color .15s,box-shadow .15s}.property-input:focus,.property-select:focus,.property-textarea:focus{outline:none;border-color:#005bd3;box-shadow:0 0 0 1px #005bd3}.property-input::placeholder,.property-textarea::placeholder{color:#8c9196}.property-textarea{resize:vertical;font-family:SF Mono,SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:.75rem}.color-field{display:flex;gap:.5rem;align-items:center}.color-picker{width:36px;height:36px;padding:2px;border:1px solid #e3e3e3;border-radius:6px;cursor:pointer}.color-text{flex:1}.no-selection{display:flex;align-items:center;justify-content:center;height:100%;color:#8c9196;text-align:center;padding:2rem}.no-selection p{margin:0;font-size:.8125rem}.panel-tabs{display:flex;border-bottom:1px solid #e3e3e3}.panel-tab{flex:1;padding:.75rem;border:none;border-bottom:2px solid transparent;background:#fff;cursor:pointer;font-size:.8125rem;font-weight:500;font-family:inherit;color:#616161;transition:all .15s}.panel-tab:hover{background:#f6f6f7;color:#303030}.panel-tab.active{background:#fff;color:#303030;font-weight:600;border-bottom:2px solid #005bd3}.blocks-list{flex:1;overflow-y:auto;padding:1rem}.slot-group{margin-bottom:1.5rem}.slot-title{font-size:.75rem;font-weight:600;color:#8c9196;margin:0 0 .5rem;text-transform:uppercase;letter-spacing:.3px}.block-item{display:flex;align-items:center;gap:.5rem;padding:.5rem .625rem;border:1px solid #e3e3e3;border-radius:8px;margin-bottom:.375rem;cursor:pointer;transition:all .15s}.block-item:hover{border-color:#005bd3;background:#f8fafe}.block-item.selected{border-color:#005bd3;background:#005bd3;color:#fff}.block-item.selected .block-icon,.block-item.selected .block-name{color:inherit}.block-icon{font-size:.875rem;color:#8c9196;display:flex;align-items:center}.block-name{flex:1;font-size:.8125rem;font-weight:500;color:#303030;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.block-actions{display:flex;gap:.125rem;opacity:0;transition:opacity .15s}.block-item:hover .block-actions{opacity:1}.mini-btn{padding:.125rem .375rem;border:none;border-radius:4px;background:transparent;cursor:pointer;font-size:.75rem;color:#8c9196;transition:all .15s}.mini-btn:hover{background:#e3e3e3;color:#303030}.mini-btn.delete:hover{background:#fef2f0;color:#d72c0d}.empty-slot{padding:.75rem;text-align:center;color:#8c9196;font-size:.8125rem;background:#f6f6f7;border:1px dashed #e3e3e3;border-radius:8px}.no-slots{padding:2rem;text-align:center;color:#8c9196}.no-slots p{margin:0;font-size:.8125rem}.delete-block-btn{width:auto;padding:0;margin-top:1rem;border:none;border-radius:0;background:transparent;color:#d72c0d;cursor:pointer;font-size:.8125rem;font-weight:500;font-family:inherit;transition:color .15s}.delete-block-btn:hover{background:transparent;color:#bc2200;text-decoration:underline}.toggle-field{display:flex;align-items:center;justify-content:space-between}.toggle-label{font-size:.8125rem;color:#303030}.toggle-switch{position:relative;display:inline-flex;cursor:pointer}.toggle-input{position:absolute;opacity:0;width:0;height:0}.toggle-track{width:36px;height:20px;background:#8c9196;border-radius:10px;position:relative;transition:background .2s}.toggle-thumb{position:absolute;top:2px;left:2px;width:16px;height:16px;background:#fff;border-radius:50%;transition:transform .2s;box-shadow:0 1px 3px #0003}.toggle-input:checked+.toggle-track{background:#008060}.toggle-input:checked+.toggle-track .toggle-thumb{transform:translate(16px)}.toggle-input:focus-visible+.toggle-track{outline:2px solid #005bd3;outline-offset:2px}.toggle-description{display:block;font-size:.75rem;color:#8c9196;margin-top:.25rem}.media-field{display:flex;flex-direction:column;gap:.5rem}.media-preview{position:relative;border:1px solid #e3e3e3;border-radius:8px;overflow:hidden}.media-thumbnail{display:block;width:100%;max-height:160px;object-fit:cover;background:#f6f6f7}.media-remove-btn{position:absolute;top:.375rem;right:.375rem;width:22px;height:22px;border:none;border-radius:50%;background:#0009;color:#fff;cursor:pointer;font-size:.875rem;line-height:1;display:flex;align-items:center;justify-content:center;transition:background .15s}.media-remove-btn:hover{background:#d72c0d}.media-browse-btn{padding:.5rem .75rem;border:1px solid #c9cccf;border-radius:8px;background:#fff;cursor:pointer;font-size:.8125rem;font-family:inherit;color:#303030;transition:all .15s}.media-browse-btn:hover{background:#f6f6f7;border-color:#8c9196}.media-url-input{font-size:.75rem;color:#616161}.video-preview{display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;background:#1a1a1a}.video-indicator{color:#fff;display:flex;align-items:center;flex-shrink:0}.video-url-text{flex:1;color:#8c9196;font-size:.6875rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.video-preview .media-remove-btn{position:static;flex-shrink:0}\n"] }]
7412
- }], propDecorators: { canvasEl: [{ type: i0.ViewChild, args: ['canvasEl', { isSignal: true }] }], nameInput: [{ type: i0.ViewChild, args: ['nameInput', { isSignal: true }] }] } });
7102
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block;height:100vh;overflow:hidden}.editor-layout{display:grid;grid-template-columns:280px 1fr 300px;height:100%;background:#f0f2f5}.sidebar{background:#fff;border-right:1px solid #e0e0e0;display:flex;flex-direction:column}.sidebar-header{padding:1rem;border-bottom:1px solid #e0e0e0}.sidebar-header h2{margin:0;font-size:.875rem;font-weight:600;color:#333}.search-box{display:none}.search-box:focus-within{border-color:#005bd3;background:#fff}.search-icon{font-size:.875rem;opacity:.6}.search-input{flex:1;border:none;background:transparent;font-size:.8125rem;color:#333;outline:none}.search-input::placeholder{color:#999}.search-clear{padding:.125rem .375rem;border:none;background:#ddd;border-radius:3px;cursor:pointer;font-size:.75rem;color:#666;line-height:1}.search-clear:hover{background:#ccc}.search-no-results{padding:2rem 1rem;text-align:center;color:#888}.search-no-results p{margin:0;font-size:.875rem}.picker-search{padding:.5rem;border-bottom:1px solid #e0e0e0}.picker-search-input{width:87%;padding:.5rem .75rem;border:1px solid #e0e0e0;border-radius:4px;font-size:.8125rem;outline:none;transition:border-color .2s}.picker-search-input:focus{border-color:#005bd3}.picker-search-input::placeholder{color:#999}.section-tree{flex:1;overflow-y:auto;padding:.25rem 0}.tree-section{border-bottom:none}.tree-section.selected>.tree-section-header{background:#005bd3;color:#fff;border-radius:8px;margin:0 .375rem}.tree-section.selected>.tree-section-header .section-name,.tree-section.selected>.tree-section-header .expand-btn,.tree-section.selected>.tree-section-header .section-icon,.tree-section.selected>.tree-section-header .drag-handle,.tree-section.selected>.tree-section-header .tree-btn{color:inherit}.tree-section-header{display:flex;align-items:center;gap:.5rem;padding:.5rem;cursor:pointer;transition:background .2s}.tree-section-header:hover{background:#f5f5f5;border-radius:8px;margin:0 .375rem}.expand-btn{width:16px;height:16px;padding:0;border:none;background:none;cursor:pointer;display:flex;align-items:center;justify-content:center;color:#666;transition:transform .2s;flex-shrink:0}.expand-btn.expanded .expand-icon{transform:rotate(90deg)}.expand-icon{font-size:.625rem;transition:transform .2s}.icon-drag-wrapper{position:relative;display:flex;align-items:center;justify-content:center;flex-shrink:0}.icon-drag-wrapper .drag-handle{position:absolute;inset:0;display:flex;align-items:center;justify-content:center}.tree-section-header:hover .icon-drag-wrapper .section-icon,.icon-drag-wrapper:has(.drag-handle:focus-visible) .section-icon{visibility:hidden}.section-icon{font-size:1rem}.section-name{flex:1;font-size:.8125rem;font-weight:500;color:#333;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tree-actions{display:flex;gap:.125rem;opacity:0;transition:opacity .2s}.tree-section-header:hover .tree-actions,.tree-block:hover .tree-actions{opacity:1}.tree-btn{padding:.125rem .375rem;border:none;border-radius:3px;background:#e0e0e0;cursor:pointer;font-size:.6875rem;color:#666}.tree-btn:hover{background:#d0d0d0}.tree-btn.delete:hover{background:#e74c3c;color:#fff}.tree-section.drag-over-before>.tree-section-header{box-shadow:inset 0 2px #4a90d9}.tree-section.drag-over-after>.tree-section-header{box-shadow:inset 0 -2px #4a90d9}.tree-section.is-section-dragging{opacity:.4}.tree-section.is-section-dragging>.tree-section-header{cursor:grabbing}.drag-handle{cursor:grab;color:#999;opacity:0;transition:opacity .2s;display:flex;align-items:center;justify-content:center;flex-shrink:0;padding:.125rem;border:none;background:none;border-radius:2px}.drag-handle:hover{color:#666;background:#e0e0e0}.drag-handle:focus-visible{opacity:1;outline:2px solid #4a90d9;outline-offset:1px}.tree-section-header:hover .drag-handle{opacity:1}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}@media(prefers-reduced-motion:reduce){.tree-section,.drag-handle,.tree-actions{transition:none}}.tree-section-content{padding-left:.5rem}.tree-block{display:flex;align-items:center;gap:.375rem;padding:.5rem .75rem;cursor:pointer;transition:background .2s}.tree-block:hover{background:#f5f5f5;border-radius:8px;margin:0 .375rem}.tree-block.selected{background:#005bd3;color:#fff;border-radius:8px;margin:0 .375rem}.tree-block.selected .block-icon,.tree-block.selected .block-name{color:inherit}.block-indent{width:20px;height:1px;background:#ddd;margin-left:10px}.block-icon{font-size:.875rem}.block-name{flex:1;font-size:.75rem;color:#555;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.add-block-btn{display:flex;align-items:center;gap:.375rem;width:calc(100% - 1.5rem);margin:.375rem .75rem .5rem 2rem;padding:.375rem .5rem;border:none;border-radius:4px;background:transparent;cursor:pointer;font-size:.75rem;color:#005bd3;transition:all .2s}.add-block-btn:hover{background:#f0f5ff;color:#004299}.add-icon{font-size:.875rem;font-weight:600}.add-icon-circle{display:flex;align-items:center;justify-content:center}.add-section-area{padding:.75rem;border-top:1px solid #e0e0e0;position:relative}.add-section-btn{display:flex;align-items:center;justify-content:center;gap:.375rem;width:100%;padding:.625rem;border:none;border-radius:6px;background:transparent;cursor:pointer;font-size:.8125rem;font-weight:500;color:#005bd3;transition:all .2s}.add-section-btn:hover{background:#f0f5ff}.section-picker{position:absolute;bottom:100%;left:.75rem;right:.75rem;background:#fff;border:1px solid #e0e0e0;border-radius:6px;box-shadow:0 -4px 12px #0000001a;max-height:300px;overflow-y:auto;z-index:100}.picker-item{display:flex;align-items:center;gap:.75rem;width:100%;padding:.75rem;border:none;border-bottom:1px solid #f0f0f0;background:#fff;cursor:pointer;text-align:left;transition:background .2s}.picker-item:last-child{border-bottom:none}.picker-item:hover{background:#f5f5f5}.picker-icon{font-family:Material Icons,sans-serif;font-size:1.5rem;font-weight:400;font-style:normal;line-height:1;letter-spacing:normal;text-transform:none;white-space:nowrap;word-wrap:normal;direction:ltr;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}.picker-info{display:flex;flex-direction:column;gap:.125rem}.picker-name{font-size:.8125rem;font-weight:500;color:#333}.picker-desc{font-size:.6875rem;color:#888}.block-picker-overlay{position:fixed;inset:0;background:#0006;display:flex;align-items:center;justify-content:center;z-index:1000}.block-picker{background:#fff;border-radius:8px;width:320px;max-height:400px;display:flex;flex-direction:column;box-shadow:0 8px 32px #0003}.picker-header{display:flex;justify-content:space-between;align-items:center;padding:1rem;border-bottom:1px solid #e0e0e0}.picker-header h3{margin:0;font-size:.9375rem;font-weight:600}.picker-content{flex:1;overflow-y:auto;padding:.5rem}.picker-content .picker-item{border:1px solid #e0e0e0;border-radius:6px;margin-bottom:.5rem}.picker-content .picker-item:hover{border-color:#005bd3}.picker-category{border-bottom:1px solid #f0f0f0}.picker-category:last-child{border-bottom:none}.picker-category-label{padding:.5rem .75rem .25rem;font-size:.6875rem;font-weight:600;color:#888}.picker-empty{padding:2rem;text-align:center;color:#888;font-size:.875rem}.canvas-area{display:flex;flex-direction:column;overflow:hidden}.toolbar{display:flex;align-items:center;justify-content:space-between;padding:.5rem 1rem;background:#fff;border-bottom:1px solid #e0e0e0}.toolbar-left,.toolbar-right{display:flex;gap:.5rem}.toolbar-btn{padding:.375rem .75rem;border:1px solid #e0e0e0;border-radius:8px;background:#fff;cursor:pointer;font-size:.8125rem;transition:all .2s}.toolbar-btn:hover:not(:disabled){background:#f0f2f5}.toolbar-btn:disabled{opacity:.5;cursor:not-allowed}.preview-btn,.publish-btn{background:#303030;color:#fff;border-color:#303030;border-radius:8px}.preview-btn:hover,.publish-btn:hover:not(:disabled){background:#1a1a1a!important}.back-btn{border-color:transparent;background:transparent}.section-count{font-size:.875rem;color:#666}.page-title{font-size:.9375rem;font-weight:600;color:#333;margin-right:.75rem}.page-title-link{text-decoration:none;color:inherit}.page-title-link:hover{text-decoration:underline}.status-badge{display:inline-block;padding:.125rem .5rem;border-radius:10px;font-size:.6875rem;font-weight:500;text-transform:capitalize;background:#ffeaa7;color:#856404}.status-badge.published{background:#b4fed3;color:#0d542b}.dirty-indicator{margin-left:.75rem;font-size:.8125rem;color:#e67e22;font-weight:500}.canvas{flex:1;overflow-y:auto;padding:2rem;position:relative}.canvas.preview-mode{padding:0;background:#fff}.preview-iframe{width:100%;height:100%;border:none;position:absolute;inset:0}.canvas.preview-mode .section-outer{margin:0}.canvas.preview-mode .section-wrapper{border:none;border-radius:0}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:#666;text-align:center}.empty-icon{font-size:4rem;margin-bottom:1rem}.empty-state h3{margin:0 0 .5rem;color:#333}.empty-state p{margin:0;font-size:.875rem}.section-outer{position:relative;margin-bottom:0}.section-wrapper{border:1px solid transparent;border-radius:0;overflow:hidden;transition:border-color .2s;background:#fff}.section-outer:hover .section-wrapper,.section-outer.selected .section-wrapper{border-color:#005bd366}.section-controls{position:absolute;top:-20px;left:0;right:0;display:flex;justify-content:space-between;align-items:center;padding:.14rem .75rem;background:#005bd3;color:#fff;font-size:.75rem;border-radius:8px 8px 0 0;opacity:0;pointer-events:none;transition:opacity .2s;z-index:10}.section-outer:hover .section-controls,.section-outer.selected .section-controls{opacity:1;pointer-events:auto}.section-outer:hover .section-wrapper,.section-outer.selected .section-wrapper{border-top-left-radius:0;border-top-right-radius:0}.section-label{font-weight:500}.section-actions{display:flex;gap:.25rem}.action-btn{padding:.15rem .5rem;border:none;border-radius:4px;background:#ffffff26;color:#fff;cursor:pointer;font-size:.6rem}.action-btn:hover{background:#ffffff4d}.action-btn.delete:hover{background:#e74c3c}.material-icon{font-family:Material Icons,sans-serif;font-size:.75rem;font-weight:400;font-style:normal;line-height:1;letter-spacing:normal;text-transform:none;white-space:nowrap;word-wrap:normal;direction:ltr;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}.properties-panel{background:#fff;border-left:1px solid #e0e0e0;display:flex;flex-direction:column;overflow:hidden}.properties-header{display:flex;justify-content:space-between;align-items:center;padding:1rem;border-bottom:1px solid #e0e0e0}.properties-header h3{margin:0;font-size:1rem;font-weight:600}.editable-name{cursor:pointer;display:flex;align-items:center;gap:.375rem;border-radius:4px;padding:.125rem .25rem;margin:-.125rem -.25rem;transition:background .2s}.editable-name:hover{background:#f0f2f5}.edit-icon{opacity:0;color:#999;transition:opacity .2s;display:flex;flex-shrink:0}.editable-name:hover .edit-icon{opacity:1}.name-edit-input{flex:1;padding:.375rem .5rem;border:1px solid #005bd3;border-radius:8px;font-size:1rem;font-weight:600;outline:none}.close-btn{border:none;background:none;font-size:1.25rem;cursor:pointer;color:#666}.more-menu-btn{border:none;background:none;cursor:pointer;color:#616161;padding:.25rem;border-radius:6px;display:flex;align-items:center;justify-content:center;transition:background .2s}.more-menu-btn:hover{background:#f0f2f5;color:#303030}.properties-content{flex:1;overflow-y:auto;padding:1rem}.property-group{margin-bottom:0;padding:1rem 0;border-top:1px solid #e0e0e0}.property-group:first-child{border-top:none}.group-title{display:flex;align-items:center;gap:.375rem;width:100%;padding:0;margin:0 0 .75rem;border:none;background:none;font-size:.8125rem;font-weight:600;color:#303030;cursor:pointer}.group-title:hover{color:#333}.group-title:focus-visible{outline:2px solid #4a90d9;outline-offset:2px;border-radius:2px}.group-title-text{flex:1;text-align:left}.group-toggle-icon{font-size:.5rem;transition:transform .2s;display:inline-block}.group-toggle-icon.collapsed{transform:rotate(-90deg)}.property-field{margin-bottom:1rem}.property-label{display:block;font-size:.875rem;font-weight:500;margin-bottom:.375rem;color:#616161}.property-input,.property-select,.property-textarea{width:100%;padding:.5rem .75rem;border:1px solid #c9cccf;border-radius:8px;font-size:.875rem;transition:border-color .2s,box-shadow .2s}.property-input:focus,.property-select:focus,.property-textarea:focus{outline:none;border-color:#005bd3;box-shadow:0 0 0 1px #005bd3}.property-textarea{resize:vertical;font-family:monospace;font-size:.75rem}.color-field{display:flex;gap:.5rem}.color-picker{width:40px;height:36px;padding:2px;border:1px solid #e0e0e0;border-radius:4px;cursor:pointer}.color-text{flex:1}.no-selection{display:flex;align-items:center;justify-content:center;height:100%;color:#666;text-align:center;padding:2rem}.no-selection p{margin:0;font-size:.875rem}.panel-tabs{display:flex;border-bottom:1px solid #e0e0e0}.panel-tab{flex:1;padding:.75rem;border:none;border-bottom:2px solid transparent;background:#fff;cursor:pointer;font-size:.75rem;font-weight:500;color:#666;transition:all .2s}.panel-tab:hover{background:#f8f9fa;color:#303030}.panel-tab.active{background:#fff;color:#303030;border-bottom:2px solid #005bd3}.blocks-list{flex:1;overflow-y:auto;padding:1rem}.slot-group{margin-bottom:1.5rem}.slot-title{font-size:.75rem;font-weight:600;color:#616161;margin:0 0 .5rem}.block-item{display:flex;align-items:center;gap:.5rem;padding:.5rem;border:1px solid #e0e0e0;border-radius:4px;margin-bottom:.375rem;cursor:pointer;transition:all .2s}.block-item:hover{border-color:#005bd3;background:#f8f9fa}.block-item.selected{border-color:#005bd3;background:#005bd3;color:#fff}.block-item.selected .block-icon,.block-item.selected .block-name{color:inherit}.block-icon{font-size:1rem}.block-name{flex:1;font-size:.8rem;font-weight:500;color:#333;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.block-actions{display:flex;gap:.125rem;opacity:0;transition:opacity .2s}.block-item:hover .block-actions{opacity:1}.mini-btn{padding:.125rem .375rem;border:none;border-radius:3px;background:#e0e0e0;cursor:pointer;font-size:.75rem;color:#666}.mini-btn:hover{background:#d0d0d0}.mini-btn.delete:hover{background:#e74c3c;color:#fff}.empty-slot{padding:.75rem;text-align:center;color:#999;font-size:.75rem;background:#f8f9fa;border:1px dashed #ddd;border-radius:4px}.no-slots{padding:2rem;text-align:center;color:#666}.no-slots p{margin:0;font-size:.875rem}.delete-block-btn{width:auto;padding:0;margin-top:1rem;border:none;border-radius:0;background:transparent;color:#d72c0d;cursor:pointer;font-size:.875rem;font-weight:500;transition:color .2s}.delete-block-btn:hover{background:transparent;color:#bc2200;text-decoration:underline}.toggle-field{display:flex;align-items:center;justify-content:space-between}.toggle-label{font-size:.875rem;color:#303030}.toggle-switch{position:relative;display:inline-flex;cursor:pointer}.toggle-input{position:absolute;opacity:0;width:0;height:0}.toggle-track{width:36px;height:20px;background:#b5b5b5;border-radius:10px;position:relative;transition:background .2s}.toggle-thumb{position:absolute;top:2px;left:2px;width:16px;height:16px;background:#fff;border-radius:50%;transition:transform .2s;box-shadow:0 1px 3px #0003}.toggle-input:checked+.toggle-track{background:#008060}.toggle-input:checked+.toggle-track .toggle-thumb{transform:translate(16px)}.toggle-input:focus-visible+.toggle-track{outline:2px solid #005bd3;outline-offset:2px}.toggle-description{display:block;font-size:.75rem;color:#616161;margin-top:.25rem}.media-field{display:flex;flex-direction:column;gap:.5rem}.media-preview{position:relative;border:1px solid #e0e0e0;border-radius:8px;overflow:hidden}.media-thumbnail{display:block;width:100%;max-height:160px;object-fit:cover;background:#f0f2f5}.media-remove-btn{position:absolute;top:.25rem;right:.25rem;width:22px;height:22px;border:none;border-radius:50%;background:#0009;color:#fff;cursor:pointer;font-size:.875rem;line-height:1;display:flex;align-items:center;justify-content:center}.media-remove-btn:hover{background:#d72c0d}.media-browse-btn{padding:.5rem .75rem;border:1px solid #c9cccf;border-radius:8px;background:#fff;cursor:pointer;font-size:.8125rem;color:#303030;transition:all .15s}.media-browse-btn:hover{background:#f0f2f5;border-color:#999}.media-url-input{font-size:.75rem;color:#616161}.video-preview{display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;background:#1a1a1a}.video-indicator{color:#fff;display:flex;align-items:center;flex-shrink:0}.video-url-text{flex:1;color:#ccc;font-size:.6875rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.video-preview .media-remove-btn{position:static;flex-shrink:0}\n"] }]
7103
+ }], ctorParameters: () => [], propDecorators: { canvasEl: [{ type: i0.ViewChild, args: ['canvasEl', { isSignal: true }] }], previewFrame: [{ type: i0.ViewChild, args: ['previewFrame', { isSignal: true }] }], nameInput: [{ type: i0.ViewChild, args: ['nameInput', { isSignal: true }] }] } });
7413
7104
 
7414
7105
  /**
7415
7106
  * Page manager component for listing, creating, and managing pages
@@ -7447,6 +7138,9 @@ class PageManagerComponent {
7447
7138
  },
7448
7139
  });
7449
7140
  }
7141
+ openSetup() {
7142
+ this.navigation.navigate({ type: 'setup' });
7143
+ }
7450
7144
  openCreateDialog() {
7451
7145
  this.showCreateDialog.set(true);
7452
7146
  this.newPageTitle.set('');
@@ -7538,14 +7232,19 @@ class PageManagerComponent {
7538
7232
  year: 'numeric',
7539
7233
  });
7540
7234
  }
7541
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: PageManagerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
7542
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: PageManagerComponent, isStandalone: true, selector: "lib-page-manager", ngImport: i0, template: `
7235
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: PageManagerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
7236
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: PageManagerComponent, isStandalone: true, selector: "lib-page-manager", ngImport: i0, template: `
7543
7237
  <div class="page-manager">
7544
7238
  <header class="manager-header">
7545
7239
  <h1>Pages</h1>
7546
- <button class="btn-primary" (click)="openCreateDialog()">
7547
- + New Page
7548
- </button>
7240
+ <div class="header-actions">
7241
+ <button class="btn-secondary" (click)="openSetup()">
7242
+ Setup
7243
+ </button>
7244
+ <button class="btn-primary" (click)="openCreateDialog()">
7245
+ + New Page
7246
+ </button>
7247
+ </div>
7549
7248
  </header>
7550
7249
 
7551
7250
  @if (isLoading()) {
@@ -7559,7 +7258,7 @@ class PageManagerComponent {
7559
7258
  </div>
7560
7259
  } @else if (pages().length === 0) {
7561
7260
  <div class="empty-state">
7562
- <div class="empty-icon"><svg width="40" height="40" viewBox="0 0 20 20" fill="currentColor"><path d="M6 2a2 2 0 00-2 2v12a2 2 0 002 2h8a2 2 0 002-2V7.414A2 2 0 0015.414 6L12 2.586A2 2 0 0010.586 2H6zm0 2h4v3a1 1 0 001 1h3v8H6V4zm6 .414L14.586 7H12V4.414z"/></svg></div>
7261
+ <div class="empty-icon">📄</div>
7563
7262
  <h2>No pages yet</h2>
7564
7263
  <p>Create your first page to get started</p>
7565
7264
  <button class="btn-primary" (click)="openCreateDialog()">
@@ -7591,21 +7290,21 @@ class PageManagerComponent {
7591
7290
  (click)="editPage(page.id); $event.stopPropagation()"
7592
7291
  title="Edit"
7593
7292
  >
7594
- <svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"><path d="M14.846 1.403a2.45 2.45 0 013.464 3.464l-1.241 1.24-3.464-3.463 1.241-1.241zm-2.654 2.654L2 14.25V17.5h3.464L15.656 7.308l-3.464-3.25z"/></svg>
7293
+ ✏️
7595
7294
  </button>
7596
7295
  <button
7597
7296
  class="action-btn"
7598
7297
  (click)="duplicatePage(page.id, $event)"
7599
7298
  title="Duplicate"
7600
7299
  >
7601
- <svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"><path d="M8 2a2 2 0 00-2 2v10a2 2 0 002 2h6a2 2 0 002-2V4a2 2 0 00-2-2H8zm0 2h6v10H8V4zM4 6a2 2 0 00-2 2v8a2 2 0 002 2h6a2 2 0 002-2h-2a4 4 0 01-4-4V6H4z"/></svg>
7300
+ 📋
7602
7301
  </button>
7603
7302
  <button
7604
7303
  class="action-btn delete"
7605
7304
  (click)="confirmDelete(page, $event)"
7606
7305
  title="Delete"
7607
7306
  >
7608
- <svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"><path d="M8 3a1 1 0 00-1 1H5a1 1 0 000 2h10a1 1 0 100-2h-2a1 1 0 00-1-1H8zm-2 4h8l-.5 8.5a1 1 0 01-1 .5h-5a1 1 0 01-1-.5L6 7z"/></svg>
7307
+ 🗑️
7609
7308
  </button>
7610
7309
  </span>
7611
7310
  </div>
@@ -7686,17 +7385,22 @@ class PageManagerComponent {
7686
7385
  </div>
7687
7386
  }
7688
7387
  </div>
7689
- `, isInline: true, styles: [":host{display:block;min-height:100vh;background:#f6f6f7;font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif}.page-manager{max-width:1200px;margin:0 auto;padding:2rem}.manager-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:2rem}.manager-header h1{margin:0;font-size:1.5rem;font-weight:600;color:#303030}.btn-primary{padding:.625rem 1.25rem;border:none;border-radius:8px;background:#303030;color:#fff;font-size:.8125rem;font-weight:500;font-family:inherit;cursor:pointer;transition:background .15s}.btn-primary:hover{background:#1a1a1a}.btn-primary:disabled{opacity:.5;cursor:not-allowed}.btn-secondary{padding:.625rem 1.25rem;border:1px solid #c9cccf;border-radius:8px;background:#fff;color:#303030;font-size:.8125rem;font-weight:500;font-family:inherit;cursor:pointer;transition:all .15s}.btn-secondary:hover{background:#f6f6f7}.btn-danger{padding:.625rem 1.25rem;border:none;border-radius:8px;background:#d72c0d;color:#fff;font-size:.8125rem;font-weight:500;font-family:inherit;cursor:pointer;transition:background .15s}.btn-danger:hover{background:#bc2200}.btn-danger:disabled{opacity:.5;cursor:not-allowed}.loading-state,.error-state,.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:4rem 2rem;background:#fff;border-radius:12px;text-align:center;box-shadow:0 1px #0000000d;border:1px solid #e3e3e3}.empty-icon{margin-bottom:1rem;color:#8c9196;display:flex;align-items:center;justify-content:center}.empty-state h2{margin:0 0 .5rem;color:#303030;font-size:1.125rem}.empty-state p{margin:0 0 1.5rem;color:#8c9196;font-size:.8125rem}.error-state{color:#d72c0d}.pages-table{background:#fff;border-radius:12px;overflow:hidden;box-shadow:0 1px #0000000d;border:1px solid #e3e3e3}.table-header,.table-row{display:grid;grid-template-columns:1fr 150px 100px 150px 120px;gap:1rem;padding:.875rem 1.5rem;align-items:center}.table-header{background:#f6f6f7;font-size:.75rem;font-weight:600;text-transform:uppercase;color:#8c9196;letter-spacing:.3px;border-bottom:1px solid #e3e3e3}.table-row{border-top:1px solid #f6f6f7;cursor:pointer;transition:background .15s}.table-row:first-of-type{border-top:none}.table-row:hover{background:#f6f6f7}.col-title{font-weight:500;color:#303030;font-size:.8125rem}.col-slug{font-size:.75rem;color:#8c9196;font-family:SF Mono,SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace}.status-badge{display:inline-block;padding:.125rem .5rem;border-radius:10px;font-size:.75rem;font-weight:500;text-transform:capitalize;background:#ffd6a4;color:#b98900}.status-badge.published{background:#aee9d1;color:#0d542b}.col-updated{font-size:.8125rem;color:#8c9196}.col-actions{display:flex;gap:.375rem}.action-btn{padding:.375rem;border:none;border-radius:8px;background:transparent;cursor:pointer;color:#8c9196;display:inline-flex;align-items:center;justify-content:center;transition:all .15s}.action-btn:hover{background:#f6f6f7;color:#303030}.action-btn.delete:hover{background:#fef2f0;color:#d72c0d}.dialog-overlay{position:fixed;inset:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:1000}.dialog{background:#fff;border-radius:12px;width:100%;max-width:480px;box-shadow:0 8px 32px #0003}.dialog-small{max-width:400px}.dialog-header{display:flex;justify-content:space-between;align-items:center;padding:1.25rem 1.5rem;border-bottom:1px solid #e3e3e3}.dialog-header h2{margin:0;font-size:1.125rem;font-weight:600;color:#303030}.close-btn{border:none;background:none;font-size:1.25rem;cursor:pointer;color:#8c9196;padding:.25rem;line-height:1;border-radius:8px;transition:all .15s}.close-btn:hover{color:#303030;background:#f6f6f7}.dialog-content{padding:1.5rem}.dialog-content p{margin:0 0 .5rem;color:#303030;font-size:.8125rem}.dialog-content .warning{color:#d72c0d;font-size:.8125rem}.form-field{margin-bottom:1.25rem}.form-field label{display:block;margin-bottom:.375rem;font-size:.8125rem;font-weight:500;color:#303030}.form-field input{width:100%;padding:.5rem .75rem;border:1px solid #c9cccf;border-radius:8px;font-size:.8125rem;font-family:inherit;color:#303030;box-sizing:border-box;transition:border-color .15s,box-shadow .15s}.form-field input:focus{outline:none;border-color:#005bd3;box-shadow:0 0 0 1px #005bd3}.form-field input::placeholder{color:#8c9196}.slug-field{display:flex;align-items:center;border:1px solid #c9cccf;border-radius:8px;overflow:hidden;transition:border-color .15s,box-shadow .15s}.slug-field:focus-within{border-color:#005bd3;box-shadow:0 0 0 1px #005bd3}.slug-prefix{padding:.5rem .75rem;background:#f6f6f7;color:#8c9196;font-size:.8125rem;font-family:SF Mono,SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;border-right:1px solid #e3e3e3}.slug-field input{border:none;border-radius:0}.slug-field input:focus{border-color:transparent;box-shadow:none}.form-error{margin-bottom:1rem;padding:.75rem;background:#fef2f0;border-radius:8px;color:#d72c0d;font-size:.8125rem}.dialog-actions{display:flex;justify-content:flex-end;gap:.75rem;padding:1.25rem 1.5rem;border-top:1px solid #e3e3e3}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
7388
+ `, isInline: true, styles: [":host{display:block;min-height:100vh;background:#f0f2f5}.page-manager{max-width:1200px;margin:0 auto;padding:2rem}.manager-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:2rem}.header-actions{display:flex;gap:.75rem;align-items:center}.manager-header h1{margin:0;font-size:1.75rem;font-weight:600;color:#1a1a2e}.btn-primary{padding:.75rem 1.5rem;border:none;border-radius:6px;background:#1a1a2e;color:#fff;font-size:.875rem;font-weight:500;cursor:pointer;transition:background .2s}.btn-primary:hover{background:#2a2a3e}.btn-primary:disabled{opacity:.6;cursor:not-allowed}.btn-secondary{padding:.75rem 1.5rem;border:1px solid #e0e0e0;border-radius:6px;background:#fff;color:#333;font-size:.875rem;font-weight:500;cursor:pointer;transition:all .2s}.btn-secondary:hover{background:#f5f5f5}.btn-danger{padding:.75rem 1.5rem;border:none;border-radius:6px;background:#e74c3c;color:#fff;font-size:.875rem;font-weight:500;cursor:pointer;transition:background .2s}.btn-danger:hover{background:#c0392b}.btn-danger:disabled{opacity:.6;cursor:not-allowed}.loading-state,.error-state,.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:4rem 2rem;background:#fff;border-radius:8px;text-align:center}.empty-icon{font-size:4rem;margin-bottom:1rem}.empty-state h2{margin:0 0 .5rem;color:#333}.empty-state p{margin:0 0 1.5rem;color:#666}.error-state{color:#e74c3c}.pages-table{background:#fff;border-radius:8px;overflow:hidden;box-shadow:0 1px 3px #0000001a}.table-header,.table-row{display:grid;grid-template-columns:1fr 150px 100px 150px 120px;gap:1rem;padding:1rem 1.5rem;align-items:center}.table-header{background:#f8f9fa;font-size:.75rem;font-weight:600;text-transform:uppercase;color:#666;letter-spacing:.5px}.table-row{border-top:1px solid #e0e0e0;cursor:pointer;transition:background .2s}.table-row:hover{background:#f8f9fa}.col-title{font-weight:500;color:#333}.col-slug{font-size:.8125rem;color:#666;font-family:monospace}.status-badge{display:inline-block;padding:.25rem .5rem;border-radius:4px;font-size:.75rem;font-weight:500;text-transform:capitalize;background:#ffeaa7;color:#856404}.status-badge.published{background:#d4edda;color:#155724}.col-updated{font-size:.8125rem;color:#666}.col-actions{display:flex;gap:.5rem}.action-btn{padding:.375rem;border:none;border-radius:4px;background:transparent;cursor:pointer;font-size:1rem;transition:background .2s}.action-btn:hover{background:#e0e0e0}.action-btn.delete:hover{background:#fde2e2}.dialog-overlay{position:fixed;inset:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:1000}.dialog{background:#fff;border-radius:8px;width:100%;max-width:480px;box-shadow:0 8px 32px #0003}.dialog-small{max-width:400px}.dialog-header{display:flex;justify-content:space-between;align-items:center;padding:1.5rem;border-bottom:1px solid #e0e0e0}.dialog-header h2{margin:0;font-size:1.25rem;font-weight:600}.close-btn{border:none;background:none;font-size:1.5rem;cursor:pointer;color:#666;padding:0;line-height:1}.close-btn:hover{color:#333}.dialog-content{padding:1.5rem}.dialog-content p{margin:0 0 .5rem;color:#333}.dialog-content .warning{color:#e74c3c;font-size:.875rem}.form-field{margin-bottom:1.25rem}.form-field label{display:block;margin-bottom:.5rem;font-size:.875rem;font-weight:500;color:#333}.form-field input{width:100%;padding:.75rem;border:1px solid #e0e0e0;border-radius:6px;font-size:.875rem;transition:border-color .2s}.form-field input:focus{outline:none;border-color:#1a1a2e}.slug-field{display:flex;align-items:center;border:1px solid #e0e0e0;border-radius:6px;overflow:hidden}.slug-field:focus-within{border-color:#1a1a2e}.slug-prefix{padding:.75rem;background:#f5f5f5;color:#666;font-size:.875rem;font-family:monospace}.slug-field input{border:none;border-radius:0}.slug-field input:focus{border-color:transparent}.form-error{margin-bottom:1rem;padding:.75rem;background:#fde2e2;border-radius:6px;color:#e74c3c;font-size:.875rem}.dialog-actions{display:flex;justify-content:flex-end;gap:.75rem;padding:1.5rem;border-top:1px solid #e0e0e0}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
7690
7389
  }
7691
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: PageManagerComponent, decorators: [{
7390
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: PageManagerComponent, decorators: [{
7692
7391
  type: Component,
7693
7392
  args: [{ selector: 'lib-page-manager', template: `
7694
7393
  <div class="page-manager">
7695
7394
  <header class="manager-header">
7696
7395
  <h1>Pages</h1>
7697
- <button class="btn-primary" (click)="openCreateDialog()">
7698
- + New Page
7699
- </button>
7396
+ <div class="header-actions">
7397
+ <button class="btn-secondary" (click)="openSetup()">
7398
+ Setup
7399
+ </button>
7400
+ <button class="btn-primary" (click)="openCreateDialog()">
7401
+ + New Page
7402
+ </button>
7403
+ </div>
7700
7404
  </header>
7701
7405
 
7702
7406
  @if (isLoading()) {
@@ -7710,7 +7414,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
7710
7414
  </div>
7711
7415
  } @else if (pages().length === 0) {
7712
7416
  <div class="empty-state">
7713
- <div class="empty-icon"><svg width="40" height="40" viewBox="0 0 20 20" fill="currentColor"><path d="M6 2a2 2 0 00-2 2v12a2 2 0 002 2h8a2 2 0 002-2V7.414A2 2 0 0015.414 6L12 2.586A2 2 0 0010.586 2H6zm0 2h4v3a1 1 0 001 1h3v8H6V4zm6 .414L14.586 7H12V4.414z"/></svg></div>
7417
+ <div class="empty-icon">📄</div>
7714
7418
  <h2>No pages yet</h2>
7715
7419
  <p>Create your first page to get started</p>
7716
7420
  <button class="btn-primary" (click)="openCreateDialog()">
@@ -7742,21 +7446,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
7742
7446
  (click)="editPage(page.id); $event.stopPropagation()"
7743
7447
  title="Edit"
7744
7448
  >
7745
- <svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"><path d="M14.846 1.403a2.45 2.45 0 013.464 3.464l-1.241 1.24-3.464-3.463 1.241-1.241zm-2.654 2.654L2 14.25V17.5h3.464L15.656 7.308l-3.464-3.25z"/></svg>
7449
+ ✏️
7746
7450
  </button>
7747
7451
  <button
7748
7452
  class="action-btn"
7749
7453
  (click)="duplicatePage(page.id, $event)"
7750
7454
  title="Duplicate"
7751
7455
  >
7752
- <svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"><path d="M8 2a2 2 0 00-2 2v10a2 2 0 002 2h6a2 2 0 002-2V4a2 2 0 00-2-2H8zm0 2h6v10H8V4zM4 6a2 2 0 00-2 2v8a2 2 0 002 2h6a2 2 0 002-2h-2a4 4 0 01-4-4V6H4z"/></svg>
7456
+ 📋
7753
7457
  </button>
7754
7458
  <button
7755
7459
  class="action-btn delete"
7756
7460
  (click)="confirmDelete(page, $event)"
7757
7461
  title="Delete"
7758
7462
  >
7759
- <svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"><path d="M8 3a1 1 0 00-1 1H5a1 1 0 000 2h10a1 1 0 100-2h-2a1 1 0 00-1-1H8zm-2 4h8l-.5 8.5a1 1 0 01-1 .5h-5a1 1 0 01-1-.5L6 7z"/></svg>
7463
+ 🗑️
7760
7464
  </button>
7761
7465
  </span>
7762
7466
  </div>
@@ -7837,1111 +7541,287 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
7837
7541
  </div>
7838
7542
  }
7839
7543
  </div>
7840
- `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block;min-height:100vh;background:#f6f6f7;font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif}.page-manager{max-width:1200px;margin:0 auto;padding:2rem}.manager-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:2rem}.manager-header h1{margin:0;font-size:1.5rem;font-weight:600;color:#303030}.btn-primary{padding:.625rem 1.25rem;border:none;border-radius:8px;background:#303030;color:#fff;font-size:.8125rem;font-weight:500;font-family:inherit;cursor:pointer;transition:background .15s}.btn-primary:hover{background:#1a1a1a}.btn-primary:disabled{opacity:.5;cursor:not-allowed}.btn-secondary{padding:.625rem 1.25rem;border:1px solid #c9cccf;border-radius:8px;background:#fff;color:#303030;font-size:.8125rem;font-weight:500;font-family:inherit;cursor:pointer;transition:all .15s}.btn-secondary:hover{background:#f6f6f7}.btn-danger{padding:.625rem 1.25rem;border:none;border-radius:8px;background:#d72c0d;color:#fff;font-size:.8125rem;font-weight:500;font-family:inherit;cursor:pointer;transition:background .15s}.btn-danger:hover{background:#bc2200}.btn-danger:disabled{opacity:.5;cursor:not-allowed}.loading-state,.error-state,.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:4rem 2rem;background:#fff;border-radius:12px;text-align:center;box-shadow:0 1px #0000000d;border:1px solid #e3e3e3}.empty-icon{margin-bottom:1rem;color:#8c9196;display:flex;align-items:center;justify-content:center}.empty-state h2{margin:0 0 .5rem;color:#303030;font-size:1.125rem}.empty-state p{margin:0 0 1.5rem;color:#8c9196;font-size:.8125rem}.error-state{color:#d72c0d}.pages-table{background:#fff;border-radius:12px;overflow:hidden;box-shadow:0 1px #0000000d;border:1px solid #e3e3e3}.table-header,.table-row{display:grid;grid-template-columns:1fr 150px 100px 150px 120px;gap:1rem;padding:.875rem 1.5rem;align-items:center}.table-header{background:#f6f6f7;font-size:.75rem;font-weight:600;text-transform:uppercase;color:#8c9196;letter-spacing:.3px;border-bottom:1px solid #e3e3e3}.table-row{border-top:1px solid #f6f6f7;cursor:pointer;transition:background .15s}.table-row:first-of-type{border-top:none}.table-row:hover{background:#f6f6f7}.col-title{font-weight:500;color:#303030;font-size:.8125rem}.col-slug{font-size:.75rem;color:#8c9196;font-family:SF Mono,SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace}.status-badge{display:inline-block;padding:.125rem .5rem;border-radius:10px;font-size:.75rem;font-weight:500;text-transform:capitalize;background:#ffd6a4;color:#b98900}.status-badge.published{background:#aee9d1;color:#0d542b}.col-updated{font-size:.8125rem;color:#8c9196}.col-actions{display:flex;gap:.375rem}.action-btn{padding:.375rem;border:none;border-radius:8px;background:transparent;cursor:pointer;color:#8c9196;display:inline-flex;align-items:center;justify-content:center;transition:all .15s}.action-btn:hover{background:#f6f6f7;color:#303030}.action-btn.delete:hover{background:#fef2f0;color:#d72c0d}.dialog-overlay{position:fixed;inset:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:1000}.dialog{background:#fff;border-radius:12px;width:100%;max-width:480px;box-shadow:0 8px 32px #0003}.dialog-small{max-width:400px}.dialog-header{display:flex;justify-content:space-between;align-items:center;padding:1.25rem 1.5rem;border-bottom:1px solid #e3e3e3}.dialog-header h2{margin:0;font-size:1.125rem;font-weight:600;color:#303030}.close-btn{border:none;background:none;font-size:1.25rem;cursor:pointer;color:#8c9196;padding:.25rem;line-height:1;border-radius:8px;transition:all .15s}.close-btn:hover{color:#303030;background:#f6f6f7}.dialog-content{padding:1.5rem}.dialog-content p{margin:0 0 .5rem;color:#303030;font-size:.8125rem}.dialog-content .warning{color:#d72c0d;font-size:.8125rem}.form-field{margin-bottom:1.25rem}.form-field label{display:block;margin-bottom:.375rem;font-size:.8125rem;font-weight:500;color:#303030}.form-field input{width:100%;padding:.5rem .75rem;border:1px solid #c9cccf;border-radius:8px;font-size:.8125rem;font-family:inherit;color:#303030;box-sizing:border-box;transition:border-color .15s,box-shadow .15s}.form-field input:focus{outline:none;border-color:#005bd3;box-shadow:0 0 0 1px #005bd3}.form-field input::placeholder{color:#8c9196}.slug-field{display:flex;align-items:center;border:1px solid #c9cccf;border-radius:8px;overflow:hidden;transition:border-color .15s,box-shadow .15s}.slug-field:focus-within{border-color:#005bd3;box-shadow:0 0 0 1px #005bd3}.slug-prefix{padding:.5rem .75rem;background:#f6f6f7;color:#8c9196;font-size:.8125rem;font-family:SF Mono,SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;border-right:1px solid #e3e3e3}.slug-field input{border:none;border-radius:0}.slug-field input:focus{border-color:transparent;box-shadow:none}.form-error{margin-bottom:1rem;padding:.75rem;background:#fef2f0;border-radius:8px;color:#d72c0d;font-size:.8125rem}.dialog-actions{display:flex;justify-content:flex-end;gap:.75rem;padding:1.25rem 1.5rem;border-top:1px solid #e3e3e3}\n"] }]
7544
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block;min-height:100vh;background:#f0f2f5}.page-manager{max-width:1200px;margin:0 auto;padding:2rem}.manager-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:2rem}.header-actions{display:flex;gap:.75rem;align-items:center}.manager-header h1{margin:0;font-size:1.75rem;font-weight:600;color:#1a1a2e}.btn-primary{padding:.75rem 1.5rem;border:none;border-radius:6px;background:#1a1a2e;color:#fff;font-size:.875rem;font-weight:500;cursor:pointer;transition:background .2s}.btn-primary:hover{background:#2a2a3e}.btn-primary:disabled{opacity:.6;cursor:not-allowed}.btn-secondary{padding:.75rem 1.5rem;border:1px solid #e0e0e0;border-radius:6px;background:#fff;color:#333;font-size:.875rem;font-weight:500;cursor:pointer;transition:all .2s}.btn-secondary:hover{background:#f5f5f5}.btn-danger{padding:.75rem 1.5rem;border:none;border-radius:6px;background:#e74c3c;color:#fff;font-size:.875rem;font-weight:500;cursor:pointer;transition:background .2s}.btn-danger:hover{background:#c0392b}.btn-danger:disabled{opacity:.6;cursor:not-allowed}.loading-state,.error-state,.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:4rem 2rem;background:#fff;border-radius:8px;text-align:center}.empty-icon{font-size:4rem;margin-bottom:1rem}.empty-state h2{margin:0 0 .5rem;color:#333}.empty-state p{margin:0 0 1.5rem;color:#666}.error-state{color:#e74c3c}.pages-table{background:#fff;border-radius:8px;overflow:hidden;box-shadow:0 1px 3px #0000001a}.table-header,.table-row{display:grid;grid-template-columns:1fr 150px 100px 150px 120px;gap:1rem;padding:1rem 1.5rem;align-items:center}.table-header{background:#f8f9fa;font-size:.75rem;font-weight:600;text-transform:uppercase;color:#666;letter-spacing:.5px}.table-row{border-top:1px solid #e0e0e0;cursor:pointer;transition:background .2s}.table-row:hover{background:#f8f9fa}.col-title{font-weight:500;color:#333}.col-slug{font-size:.8125rem;color:#666;font-family:monospace}.status-badge{display:inline-block;padding:.25rem .5rem;border-radius:4px;font-size:.75rem;font-weight:500;text-transform:capitalize;background:#ffeaa7;color:#856404}.status-badge.published{background:#d4edda;color:#155724}.col-updated{font-size:.8125rem;color:#666}.col-actions{display:flex;gap:.5rem}.action-btn{padding:.375rem;border:none;border-radius:4px;background:transparent;cursor:pointer;font-size:1rem;transition:background .2s}.action-btn:hover{background:#e0e0e0}.action-btn.delete:hover{background:#fde2e2}.dialog-overlay{position:fixed;inset:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:1000}.dialog{background:#fff;border-radius:8px;width:100%;max-width:480px;box-shadow:0 8px 32px #0003}.dialog-small{max-width:400px}.dialog-header{display:flex;justify-content:space-between;align-items:center;padding:1.5rem;border-bottom:1px solid #e0e0e0}.dialog-header h2{margin:0;font-size:1.25rem;font-weight:600}.close-btn{border:none;background:none;font-size:1.5rem;cursor:pointer;color:#666;padding:0;line-height:1}.close-btn:hover{color:#333}.dialog-content{padding:1.5rem}.dialog-content p{margin:0 0 .5rem;color:#333}.dialog-content .warning{color:#e74c3c;font-size:.875rem}.form-field{margin-bottom:1.25rem}.form-field label{display:block;margin-bottom:.5rem;font-size:.875rem;font-weight:500;color:#333}.form-field input{width:100%;padding:.75rem;border:1px solid #e0e0e0;border-radius:6px;font-size:.875rem;transition:border-color .2s}.form-field input:focus{outline:none;border-color:#1a1a2e}.slug-field{display:flex;align-items:center;border:1px solid #e0e0e0;border-radius:6px;overflow:hidden}.slug-field:focus-within{border-color:#1a1a2e}.slug-prefix{padding:.75rem;background:#f5f5f5;color:#666;font-size:.875rem;font-family:monospace}.slug-field input{border:none;border-radius:0}.slug-field input:focus{border-color:transparent}.form-error{margin-bottom:1rem;padding:.75rem;background:#fde2e2;border-radius:6px;color:#e74c3c;font-size:.875rem}.dialog-actions{display:flex;justify-content:flex-end;gap:.75rem;padding:1.5rem;border-top:1px solid #e0e0e0}\n"] }]
7841
7545
  }] });
7842
7546
 
7843
- class LicenseGuardComponent {
7844
- shopAuthService = inject(ShopAuthService);
7845
- showExpirationWarning = input(true, ...(ngDevMode ? [{ debugName: "showExpirationWarning" }] : []));
7846
- subscribe = output();
7847
- renew = output();
7848
- reauth = output();
7849
- retry = output();
7850
- onSubscribe() {
7851
- this.subscribe.emit();
7852
- }
7853
- onRenew() {
7854
- this.renew.emit();
7855
- }
7856
- onReauth() {
7857
- this.reauth.emit();
7858
- }
7859
- onRetry() {
7860
- this.retry.emit();
7861
- }
7862
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: LicenseGuardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
7863
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: LicenseGuardComponent, isStandalone: true, selector: "lib-license-guard", inputs: { showExpirationWarning: { classPropertyName: "showExpirationWarning", publicName: "showExpirationWarning", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { subscribe: "subscribe", renew: "renew", reauth: "reauth", retry: "retry" }, ngImport: i0, template: `
7864
- @if (shopAuthService.loading()) {
7865
- <div class="license-loading">
7866
- <div class="spinner"></div>
7867
- <span>Verificando acceso...</span>
7868
- </div>
7869
- } @else if (!shopAuthService.hasAccess()) {
7870
- <div class="license-paywall">
7871
- <div class="paywall-content">
7872
- <h2>Acceso restringido</h2>
7873
-
7874
- @switch (shopAuthService.error()?.code) {
7875
- @case ('LICENSE_EXPIRED') {
7876
- <p>Tu licencia ha expirado.</p>
7877
- <button class="btn-primary" (click)="onRenew()">Renovar licencia</button>
7878
- }
7879
- @case ('NO_ACCESS') {
7880
- <p>No tienes una licencia activa.</p>
7881
- <button class="btn-primary" (click)="onSubscribe()">Obtener licencia</button>
7882
- }
7883
- @case ('SHOP_NOT_FOUND') {
7884
- <p>Tienda no encontrada.</p>
7885
- <button class="btn-secondary" (click)="onRetry()">Reintentar</button>
7886
- }
7887
- @case ('USER_INACTIVE') {
7888
- <p>No tienes permisos para acceder.</p>
7889
- }
7890
- @case ('NOT_AUTHENTICATED') {
7891
- <p>Sesion no valida.</p>
7892
- <button class="btn-primary" (click)="onReauth()">Iniciar sesion</button>
7893
- }
7894
- @case ('UNKNOWN_ERROR') {
7895
- <p>Error de conexion.</p>
7896
- <button class="btn-secondary" (click)="onRetry()">Reintentar</button>
7897
- }
7898
- @default {
7899
- <p>No tienes acceso a esta funcionalidad.</p>
7900
- <button class="btn-primary" (click)="onSubscribe()">Ver planes</button>
7547
+ /**
7548
+ * Read-only GraphQL client for the Shopify Storefront API.
7549
+ * Uses a Storefront Access Token (public, non-secret) instead of Admin API credentials.
7550
+ */
7551
+ class StorefrontGraphQLClient {
7552
+ http = inject(HttpClient);
7553
+ config = inject(STOREFRONT_CONFIG);
7554
+ query(query, variables) {
7555
+ const url = this.config.proxyUrl
7556
+ ? this.config.proxyUrl
7557
+ : `https://${this.config.shopDomain}/api/${this.config.apiVersion}/graphql.json`;
7558
+ const request = { query, variables };
7559
+ const headers = {
7560
+ 'Content-Type': 'application/json',
7561
+ };
7562
+ if (!this.config.proxyUrl) {
7563
+ headers['X-Shopify-Storefront-Access-Token'] = this.config.storefrontAccessToken;
7564
+ }
7565
+ return this.http.post(url, request, { headers }).pipe(map$1(response => this.handleResponse(response)), catchError(error => this.handleError(error)));
7566
+ }
7567
+ handleResponse(response) {
7568
+ if (response.errors && response.errors.length > 0) {
7569
+ throw {
7570
+ code: 'GRAPHQL_ERROR',
7571
+ message: response.errors[0].message,
7572
+ errors: response.errors,
7573
+ };
7574
+ }
7575
+ if (!response.data) {
7576
+ throw {
7577
+ code: 'EMPTY_RESPONSE',
7578
+ message: 'GraphQL response contains no data',
7579
+ };
7580
+ }
7581
+ return response.data;
7582
+ }
7583
+ handleError(error) {
7584
+ let errorCode = 'UNKNOWN_ERROR';
7585
+ let errorMessage = 'An unknown error occurred';
7586
+ if (typeof ErrorEvent !== 'undefined' && error.error instanceof ErrorEvent) {
7587
+ errorCode = 'NETWORK_ERROR';
7588
+ errorMessage = error.error.message;
7589
+ }
7590
+ else {
7591
+ switch (error.status) {
7592
+ case 401:
7593
+ errorCode = 'UNAUTHORIZED';
7594
+ errorMessage = 'Invalid or expired storefront access token';
7595
+ break;
7596
+ case 402:
7597
+ errorCode = 'PAYMENT_REQUIRED';
7598
+ errorMessage = 'Storefront API access requires a Shopify plan';
7599
+ break;
7600
+ case 429:
7601
+ errorCode = 'RATE_LIMIT';
7602
+ errorMessage = 'Rate limit exceeded';
7603
+ break;
7604
+ case 500:
7605
+ case 502:
7606
+ case 503:
7607
+ case 504:
7608
+ errorCode = 'SERVER_ERROR';
7609
+ errorMessage = 'Shopify server error';
7610
+ break;
7611
+ default:
7612
+ errorCode = 'HTTP_ERROR';
7613
+ errorMessage = `HTTP ${error.status}: ${error.statusText}`;
7901
7614
  }
7902
- }
7903
- </div>
7904
- </div>
7905
- } @else {
7906
- <ng-content></ng-content>
7907
-
7908
- @if (shopAuthService.isLicenseExpiringSoon() && showExpirationWarning()) {
7909
- <div class="expiration-warning">
7910
- <span class="warning-icon">!</span>
7911
- <span>Tu licencia expira en {{ shopAuthService.daysRemaining() }} dias.</span>
7912
- <button class="btn-small" (click)="onRenew()">Renovar</button>
7913
- </div>
7914
- }
7615
+ }
7616
+ return throwError(() => ({
7617
+ code: errorCode,
7618
+ message: errorMessage,
7619
+ status: error.status,
7620
+ original: error,
7621
+ }));
7915
7622
  }
7916
- `, isInline: true, styles: [".license-loading{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:48px;gap:16px}.spinner{width:32px;height:32px;border:3px solid #e5e7eb;border-top-color:#3b82f6;border-radius:50%;animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.license-paywall{display:flex;align-items:center;justify-content:center;min-height:300px;padding:24px;background:#f9fafb;border-radius:8px}.paywall-content{text-align:center;max-width:400px}.paywall-content h2{margin:0 0 12px;font-size:24px;font-weight:600;color:#111827}.paywall-content p{margin:0 0 24px;color:#6b7280}.btn-primary{padding:12px 24px;background:#3b82f6;color:#fff;border:none;border-radius:6px;font-size:14px;font-weight:500;cursor:pointer;transition:background .2s}.btn-primary:hover{background:#2563eb}.btn-secondary{padding:12px 24px;background:#6b7280;color:#fff;border:none;border-radius:6px;font-size:14px;font-weight:500;cursor:pointer;transition:background .2s}.btn-secondary:hover{background:#4b5563}.btn-small{padding:6px 12px;background:transparent;color:inherit;border:1px solid currentColor;border-radius:4px;font-size:12px;cursor:pointer;transition:background .2s}.btn-small:hover{background:#0000000d}.expiration-warning{position:fixed;bottom:0;left:0;right:0;display:flex;align-items:center;justify-content:center;gap:12px;padding:12px 24px;background:#fef3c7;color:#92400e;font-size:14px;z-index:1000}.warning-icon{display:flex;align-items:center;justify-content:center;width:20px;height:20px;background:#f59e0b;color:#fff;border-radius:50%;font-size:12px;font-weight:700}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }] });
7623
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: StorefrontGraphQLClient, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
7624
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: StorefrontGraphQLClient, providedIn: 'root' });
7917
7625
  }
7918
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: LicenseGuardComponent, decorators: [{
7919
- type: Component,
7920
- args: [{ selector: 'lib-license-guard', standalone: true, imports: [CommonModule], template: `
7921
- @if (shopAuthService.loading()) {
7922
- <div class="license-loading">
7923
- <div class="spinner"></div>
7924
- <span>Verificando acceso...</span>
7925
- </div>
7926
- } @else if (!shopAuthService.hasAccess()) {
7927
- <div class="license-paywall">
7928
- <div class="paywall-content">
7929
- <h2>Acceso restringido</h2>
7626
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: StorefrontGraphQLClient, decorators: [{
7627
+ type: Injectable,
7628
+ args: [{ providedIn: 'root' }]
7629
+ }] });
7930
7630
 
7931
- @switch (shopAuthService.error()?.code) {
7932
- @case ('LICENSE_EXPIRED') {
7933
- <p>Tu licencia ha expirado.</p>
7934
- <button class="btn-primary" (click)="onRenew()">Renovar licencia</button>
7935
- }
7936
- @case ('NO_ACCESS') {
7937
- <p>No tienes una licencia activa.</p>
7938
- <button class="btn-primary" (click)="onSubscribe()">Obtener licencia</button>
7631
+ const NAMESPACE = 'kustomizer';
7632
+ const INDEX_KEY = 'pages_index';
7633
+ /**
7634
+ * Storefront API query for reading shop metafields.
7635
+ * Uses `shop.metafield(namespace, key)` — the Storefront API equivalent
7636
+ * of the Admin API's shop metafield query.
7637
+ */
7638
+ const GET_SHOP_METAFIELD_QUERY = `
7639
+ query GetShopMetafield($namespace: String!, $key: String!) {
7640
+ shop {
7641
+ metafield(namespace: $namespace, key: $key) {
7642
+ value
7643
+ type
7644
+ }
7645
+ }
7646
+ }
7647
+ `;
7648
+ /**
7649
+ * Read-only metafield repository using the Shopify Storefront API.
7650
+ * Only works for metafields with MetafieldDefinitions that have `access.storefront: PUBLIC_READ`.
7651
+ */
7652
+ class StorefrontMetafieldRepository {
7653
+ client = inject(StorefrontGraphQLClient);
7654
+ getMetafield(namespace, key) {
7655
+ return this.client.query(GET_SHOP_METAFIELD_QUERY, { namespace, key }).pipe(map$1(response => response.shop.metafield));
7656
+ }
7657
+ getPageIndex() {
7658
+ return this.getMetafield(NAMESPACE, INDEX_KEY).pipe(map$1(metafield => {
7659
+ if (!metafield) {
7660
+ return this.getDefaultIndex();
7939
7661
  }
7940
- @case ('SHOP_NOT_FOUND') {
7941
- <p>Tienda no encontrada.</p>
7942
- <button class="btn-secondary" (click)="onRetry()">Reintentar</button>
7662
+ try {
7663
+ const parsed = JSON.parse(metafield.value);
7664
+ if (!parsed.pages || !Array.isArray(parsed.pages)) {
7665
+ throw new Error('Invalid index structure');
7666
+ }
7667
+ return parsed;
7943
7668
  }
7944
- @case ('USER_INACTIVE') {
7945
- <p>No tienes permisos para acceder.</p>
7669
+ catch (error) {
7670
+ throw {
7671
+ code: 'INDEX_PARSE_ERROR',
7672
+ message: 'Stored index data is corrupted',
7673
+ original: error,
7674
+ };
7946
7675
  }
7947
- @case ('NOT_AUTHENTICATED') {
7948
- <p>Sesion no valida.</p>
7949
- <button class="btn-primary" (click)="onReauth()">Iniciar sesion</button>
7676
+ }));
7677
+ }
7678
+ getPageMetafield(pageId) {
7679
+ const key = `page_${pageId}`;
7680
+ return this.getMetafield(NAMESPACE, key).pipe(map$1(metafield => {
7681
+ if (!metafield) {
7682
+ return null;
7950
7683
  }
7951
- @case ('UNKNOWN_ERROR') {
7952
- <p>Error de conexion.</p>
7953
- <button class="btn-secondary" (click)="onRetry()">Reintentar</button>
7684
+ try {
7685
+ return JSON.parse(metafield.value);
7954
7686
  }
7955
- @default {
7956
- <p>No tienes acceso a esta funcionalidad.</p>
7957
- <button class="btn-primary" (click)="onSubscribe()">Ver planes</button>
7687
+ catch (error) {
7688
+ throw {
7689
+ code: 'PAGE_PARSE_ERROR',
7690
+ message: 'Stored page data is corrupted',
7691
+ original: error,
7692
+ };
7958
7693
  }
7959
- }
7960
- </div>
7961
- </div>
7962
- } @else {
7963
- <ng-content></ng-content>
7964
-
7965
- @if (shopAuthService.isLicenseExpiringSoon() && showExpirationWarning()) {
7966
- <div class="expiration-warning">
7967
- <span class="warning-icon">!</span>
7968
- <span>Tu licencia expira en {{ shopAuthService.daysRemaining() }} dias.</span>
7969
- <button class="btn-small" (click)="onRenew()">Renovar</button>
7970
- </div>
7971
- }
7694
+ }));
7695
+ }
7696
+ getDefaultIndex() {
7697
+ return {
7698
+ version: 0,
7699
+ lastUpdated: new Date().toISOString(),
7700
+ pages: [],
7701
+ };
7972
7702
  }
7973
- `, styles: [".license-loading{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:48px;gap:16px}.spinner{width:32px;height:32px;border:3px solid #e5e7eb;border-top-color:#3b82f6;border-radius:50%;animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.license-paywall{display:flex;align-items:center;justify-content:center;min-height:300px;padding:24px;background:#f9fafb;border-radius:8px}.paywall-content{text-align:center;max-width:400px}.paywall-content h2{margin:0 0 12px;font-size:24px;font-weight:600;color:#111827}.paywall-content p{margin:0 0 24px;color:#6b7280}.btn-primary{padding:12px 24px;background:#3b82f6;color:#fff;border:none;border-radius:6px;font-size:14px;font-weight:500;cursor:pointer;transition:background .2s}.btn-primary:hover{background:#2563eb}.btn-secondary{padding:12px 24px;background:#6b7280;color:#fff;border:none;border-radius:6px;font-size:14px;font-weight:500;cursor:pointer;transition:background .2s}.btn-secondary:hover{background:#4b5563}.btn-small{padding:6px 12px;background:transparent;color:inherit;border:1px solid currentColor;border-radius:4px;font-size:12px;cursor:pointer;transition:background .2s}.btn-small:hover{background:#0000000d}.expiration-warning{position:fixed;bottom:0;left:0;right:0;display:flex;align-items:center;justify-content:center;gap:12px;padding:12px 24px;background:#fef3c7;color:#92400e;font-size:14px;z-index:1000}.warning-icon{display:flex;align-items:center;justify-content:center;width:20px;height:20px;background:#f59e0b;color:#fff;border-radius:50%;font-size:12px;font-weight:700}\n"] }]
7974
- }], propDecorators: { showExpirationWarning: [{ type: i0.Input, args: [{ isSignal: true, alias: "showExpirationWarning", required: false }] }], subscribe: [{ type: i0.Output, args: ["subscribe"] }], renew: [{ type: i0.Output, args: ["renew"] }], reauth: [{ type: i0.Output, args: ["reauth"] }], retry: [{ type: i0.Output, args: ["retry"] }] } });
7703
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: StorefrontMetafieldRepository, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
7704
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: StorefrontMetafieldRepository, providedIn: 'root' });
7705
+ }
7706
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: StorefrontMetafieldRepository, decorators: [{
7707
+ type: Injectable,
7708
+ args: [{ providedIn: 'root' }]
7709
+ }] });
7975
7710
 
7976
- class LoginComponent {
7977
- authService = inject(SupabaseAuthService);
7978
- router = inject(Router);
7979
- loginSuccess = output();
7980
- email = signal('', ...(ngDevMode ? [{ debugName: "email" }] : []));
7981
- password = signal('', ...(ngDevMode ? [{ debugName: "password" }] : []));
7982
- error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : []));
7983
- loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : []));
7984
- isSignUp = signal(false, ...(ngDevMode ? [{ debugName: "isSignUp" }] : []));
7985
- showPassword = signal(false, ...(ngDevMode ? [{ debugName: "showPassword" }] : []));
7986
- toggleMode() {
7987
- this.isSignUp.update(v => !v);
7988
- this.error.set(null);
7711
+ /**
7712
+ * Read-only page repository using the Shopify Storefront API.
7713
+ * Used by the public-storefront to read published pages without Admin API access.
7714
+ */
7715
+ class PageStorefrontRepository {
7716
+ metafieldRepo = inject(StorefrontMetafieldRepository);
7717
+ getPages() {
7718
+ return this.metafieldRepo.getPageIndex().pipe(map$1(index => index.pages
7719
+ .map(entry => ({
7720
+ id: entry.id,
7721
+ slug: entry.slug,
7722
+ title: entry.title,
7723
+ status: entry.status,
7724
+ updatedAt: entry.updatedAt,
7725
+ publishedAt: entry.publishedAt,
7726
+ }))
7727
+ .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))));
7989
7728
  }
7990
- togglePassword() {
7991
- this.showPassword.update(v => !v);
7729
+ getPublishedPages() {
7730
+ return this.getPages().pipe(map$1(pages => pages.filter(p => p.status === 'published')));
7992
7731
  }
7993
- async onSubmit() {
7994
- if (!this.email() || !this.password()) {
7995
- this.error.set('Please fill in all fields');
7996
- return;
7997
- }
7998
- this.loading.set(true);
7999
- this.error.set(null);
8000
- try {
8001
- let result;
8002
- if (this.isSignUp()) {
8003
- result = await this.authService.signUp(this.email(), this.password());
8004
- if (result.success) {
8005
- this.loginSuccess.emit();
8006
- return;
8007
- }
8008
- }
8009
- else {
8010
- result = await this.authService.signInWithPassword(this.email(), this.password());
8011
- if (result.success) {
8012
- this.loginSuccess.emit();
8013
- return;
8014
- }
7732
+ getPage(id) {
7733
+ return this.metafieldRepo.getPageMetafield(id).pipe(map$1(page => {
7734
+ if (!page) {
7735
+ throw {
7736
+ code: 'PAGE_NOT_FOUND',
7737
+ message: `Page with id ${id} not found`,
7738
+ };
8015
7739
  }
8016
- this.error.set(result.error ?? 'Authentication error');
8017
- }
8018
- finally {
8019
- this.loading.set(false);
8020
- }
7740
+ return page;
7741
+ }));
8021
7742
  }
8022
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: LoginComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
8023
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: LoginComponent, isStandalone: true, selector: "lib-login", outputs: { loginSuccess: "loginSuccess" }, ngImport: i0, template: `
8024
- <div class="login-page">
8025
- <div class="login-bg">
8026
- <div class="login-gradient"></div>
8027
- <div class="login-pattern"></div>
8028
- <div class="login-glow login-glow--1"></div>
8029
- <div class="login-glow login-glow--2"></div>
8030
- </div>
8031
-
8032
- <div class="login-container">
8033
- <div class="login-card">
8034
- <header class="login-header">
8035
- <div class="login-logo-wrap">
8036
- <img src="/img/kustomizerlogo.png" alt="Kustomizer" class="login-logo" />
8037
- </div>
8038
- <h1 class="login-title">{{ isSignUp() ? 'Create account' : 'Sign in' }}</h1>
8039
- <p class="login-subtitle">{{ isSignUp() ? 'Create your account to get started' : 'Sign in to your Kustomizer account' }}</p>
8040
- </header>
8041
-
8042
- @if (error()) {
8043
- <div class="login-alert">
8044
- <svg viewBox="0 0 20 20" fill="currentColor" class="login-alert-icon">
8045
- <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z" clip-rule="evenodd" />
8046
- </svg>
8047
- <span>{{ error() }}</span>
8048
- </div>
8049
- }
8050
-
8051
- <form (ngSubmit)="onSubmit()" class="login-form">
8052
- <div class="login-field">
8053
- <label for="email" class="login-label">Email</label>
8054
- <input
8055
- id="email"
8056
- type="email"
8057
- [(ngModel)]="email"
8058
- name="email"
8059
- placeholder=""
8060
- class="login-input"
8061
- required
8062
- [disabled]="loading()"
8063
- />
8064
- </div>
8065
-
8066
- <div class="login-field">
8067
- <label for="password" class="login-label">Password</label>
8068
- <div class="login-input-wrap">
8069
- <input
8070
- id="password"
8071
- [type]="showPassword() ? 'text' : 'password'"
8072
- [(ngModel)]="password"
8073
- name="password"
8074
- placeholder="••••••••"
8075
- class="login-input"
8076
- required
8077
- [disabled]="loading()"
8078
- />
8079
- <button type="button" class="login-password-toggle" (click)="togglePassword()" tabindex="-1">
8080
- @if (showPassword()) {
8081
- <svg viewBox="0 0 20 20" fill="currentColor">
8082
- <path fill-rule="evenodd" d="M3.28 2.22a.75.75 0 00-1.06 1.06l14.5 14.5a.75.75 0 101.06-1.06l-1.745-1.745a10.029 10.029 0 003.3-4.38 1.651 1.651 0 000-1.185A10.004 10.004 0 009.999 3a9.956 9.956 0 00-4.744 1.194L3.28 2.22zM7.752 6.69l1.092 1.092a2.5 2.5 0 013.374 3.373l1.091 1.092a4 4 0 00-5.557-5.557z" clip-rule="evenodd" />
8083
- <path d="M10.748 13.93l2.523 2.523a9.987 9.987 0 01-3.27.547c-4.258 0-7.894-2.66-9.337-6.41a1.651 1.651 0 010-1.186A10.007 10.007 0 012.839 6.02L6.07 9.252a4 4 0 004.678 4.678z" />
8084
- </svg>
8085
- } @else {
8086
- <svg viewBox="0 0 20 20" fill="currentColor">
8087
- <path d="M10 12.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5z" />
8088
- <path fill-rule="evenodd" d="M.664 10.59a1.651 1.651 0 010-1.186A10.004 10.004 0 0110 3c4.257 0 7.893 2.66 9.336 6.41.147.381.146.804 0 1.186A10.004 10.004 0 0110 17c-4.257 0-7.893-2.66-9.336-6.41zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd" />
8089
- </svg>
8090
- }
8091
- </button>
8092
- </div>
8093
- </div>
8094
-
8095
- <button type="submit" class="login-submit" [disabled]="loading()">
8096
- @if (loading()) {
8097
- <span class="login-spinner"></span>
8098
- {{ isSignUp() ? 'Creating...' : 'Signing in...' }}
8099
- } @else {
8100
- {{ isSignUp() ? 'Create account' : 'Sign in' }}
8101
- }
8102
- </button>
8103
- </form>
8104
-
8105
- <div class="login-divider"><span>or</span></div>
8106
-
8107
- <button type="button" class="login-switch" (click)="toggleMode()">
8108
- {{ isSignUp() ? 'Already have an account? Sign in' : "Don't have an account? Sign up" }}
8109
- </button>
8110
- </div>
8111
-
8112
- <footer class="login-footer">
8113
- <span class="login-footer-text">Kustomizer</span>
8114
- <span class="login-footer-dot">·</span>
8115
- <a href="https://kustomizer.net" target="_blank" rel="noopener noreferrer" class="login-footer-link">kustomizer.net</a>
8116
- </footer>
8117
- </div>
8118
- </div>
8119
- `, isInline: true, styles: [":host{display:block}.login-page{--k-primary: #bdf366;--k-primary-hover: #a8e050;--k-primary-text: #1a1f0e;--k-bg: #0c0f08;--k-card: #ffffff;--k-border: #e5e7eb;--k-border-focus: #bdf366;--k-text: #1f2937;--k-text-secondary: #6b7280;--k-text-muted: #9ca3af;--k-error: #dc2626;--k-error-bg: #fef2f2;--k-error-border: #fecaca;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,sans-serif;min-height:100vh;display:flex;align-items:center;justify-content:center;background:var(--k-bg);color:var(--k-text);padding:20px;position:relative;overflow:hidden}.login-bg{position:fixed;inset:0;pointer-events:none;z-index:0}.login-gradient{position:absolute;inset:0;background:radial-gradient(ellipse 80% 50% at 50% -20%,rgba(189,243,102,.15),transparent),radial-gradient(ellipse 60% 40% at 100% 100%,rgba(189,243,102,.08),transparent),linear-gradient(180deg,#0c0f08,#111a0a)}.login-pattern{position:absolute;inset:0;background-image:radial-gradient(rgba(189,243,102,.07) 1px,transparent 1px);background-size:32px 32px;mask-image:radial-gradient(ellipse 100% 80% at 50% 20%,black,transparent);-webkit-mask-image:radial-gradient(ellipse 100% 80% at 50% 20%,black,transparent)}.login-glow{position:absolute;border-radius:50%;filter:blur(80px);opacity:.6}.login-glow--1{width:400px;height:400px;background:#bdf3661f;top:-150px;left:50%;transform:translate(-50%);animation:glowPulse 8s ease-in-out infinite}.login-glow--2{width:300px;height:300px;background:#bdf3660f;bottom:-100px;right:-50px;animation:glowPulse2 10s ease-in-out infinite}@keyframes glowPulse{0%,to{opacity:.4;transform:translate(-50%) scale(1)}50%{opacity:.7;transform:translate(-50%) scale(1.1)}}@keyframes glowPulse2{0%,to{opacity:.3;transform:scale(1)}50%{opacity:.5;transform:scale(1.15)}}.login-container{position:relative;z-index:1;width:100%;max-width:400px;animation:fadeIn .5s ease}@keyframes fadeIn{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.login-card{background:var(--k-card);border-radius:20px;padding:40px 32px;box-shadow:0 0 0 1px #ffffff1a,0 25px 50px -12px #00000080,0 0 100px -20px #bdf36633}.login-header{text-align:center;margin-bottom:32px}.login-logo-wrap{margin-bottom:20px}.login-logo{width:56px;height:56px;border-radius:12px}.login-title{font-size:24px;font-weight:600;color:var(--k-text);margin:0 0 8px;letter-spacing:-.3px}.login-subtitle{font-size:14px;color:var(--k-text-secondary);margin:0;font-weight:400}.login-alert{display:flex;align-items:flex-start;gap:10px;padding:12px 14px;border-radius:10px;font-size:14px;margin-bottom:20px;background:var(--k-error-bg);border:1px solid var(--k-error-border);color:var(--k-error)}.login-alert-icon{width:18px;height:18px;flex-shrink:0;margin-top:1px}.login-form{display:flex;flex-direction:column;gap:20px}.login-field{display:flex;flex-direction:column;gap:6px}.login-label{font-size:14px;font-weight:500;color:var(--k-text)}.login-input-wrap{position:relative;display:flex;align-items:center}.login-input{width:100%;background:var(--k-card);border:1px solid var(--k-border);border-radius:10px;padding:12px 14px;font-size:15px;font-family:inherit;color:var(--k-text);outline:none;transition:border-color .2s ease,box-shadow .2s ease;box-sizing:border-box}.login-input-wrap .login-input{padding-right:44px}.login-input::placeholder{color:var(--k-text-muted)}.login-input:hover{border-color:#d1d5db}.login-input:focus{border-color:var(--k-border-focus);box-shadow:0 0 0 3px #bdf36626}.login-input:disabled{background:#f3f4f6;cursor:not-allowed;opacity:.7}.login-password-toggle{position:absolute;right:10px;background:none;border:none;padding:6px;cursor:pointer;color:var(--k-text-muted);transition:color .2s ease;display:flex;align-items:center;justify-content:center;border-radius:6px}.login-password-toggle:hover{color:var(--k-primary-text);background:#bdf36626}.login-password-toggle svg{width:18px;height:18px}.login-submit{width:100%;background:var(--k-primary);color:var(--k-primary-text);border:none;border-radius:10px;padding:14px 20px;font-size:15px;font-weight:600;font-family:inherit;cursor:pointer;transition:background .2s ease,transform .1s ease;margin-top:4px;display:flex;align-items:center;justify-content:center;gap:8px}.login-submit:hover:not(:disabled){background:var(--k-primary-hover)}.login-submit:active:not(:disabled){transform:scale(.98)}.login-submit:disabled{opacity:.6;cursor:not-allowed}.login-spinner{width:16px;height:16px;border:2px solid transparent;border-top-color:var(--k-primary-text);border-radius:50%;animation:spin .7s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.login-divider{display:flex;align-items:center;gap:14px;margin:24px 0}.login-divider:before,.login-divider:after{content:\"\";flex:1;height:1px;background:var(--k-border)}.login-divider span{font-size:13px;color:var(--k-text-muted);text-transform:lowercase}.login-switch{width:100%;background:transparent;border:1px solid var(--k-border);border-radius:10px;padding:12px 16px;font-size:14px;font-weight:500;font-family:inherit;color:var(--k-text-secondary);cursor:pointer;transition:all .2s ease}.login-switch:hover{background:#bdf36614;border-color:#bdf3664d;color:var(--k-primary-text)}.login-footer{display:flex;align-items:center;justify-content:center;gap:8px;margin-top:24px}.login-footer-text{font-size:13px;color:#fff6;font-weight:500}.login-footer-link{font-size:13px;color:#ffffff80;text-decoration:none;transition:color .2s ease}.login-footer-link:hover{color:var(--k-primary)}.login-footer-dot{color:#ffffff4d;font-size:12px}@media(max-width:440px){.login-page{padding:16px}.login-card{padding:32px 24px}.login-title{font-size:22px}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }] });
8120
- }
8121
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: LoginComponent, decorators: [{
8122
- type: Component,
8123
- args: [{ selector: 'lib-login', standalone: true, imports: [CommonModule, FormsModule], template: `
8124
- <div class="login-page">
8125
- <div class="login-bg">
8126
- <div class="login-gradient"></div>
8127
- <div class="login-pattern"></div>
8128
- <div class="login-glow login-glow--1"></div>
8129
- <div class="login-glow login-glow--2"></div>
8130
- </div>
8131
-
8132
- <div class="login-container">
8133
- <div class="login-card">
8134
- <header class="login-header">
8135
- <div class="login-logo-wrap">
8136
- <img src="/img/kustomizerlogo.png" alt="Kustomizer" class="login-logo" />
8137
- </div>
8138
- <h1 class="login-title">{{ isSignUp() ? 'Create account' : 'Sign in' }}</h1>
8139
- <p class="login-subtitle">{{ isSignUp() ? 'Create your account to get started' : 'Sign in to your Kustomizer account' }}</p>
8140
- </header>
8141
-
8142
- @if (error()) {
8143
- <div class="login-alert">
8144
- <svg viewBox="0 0 20 20" fill="currentColor" class="login-alert-icon">
8145
- <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z" clip-rule="evenodd" />
8146
- </svg>
8147
- <span>{{ error() }}</span>
8148
- </div>
8149
- }
8150
-
8151
- <form (ngSubmit)="onSubmit()" class="login-form">
8152
- <div class="login-field">
8153
- <label for="email" class="login-label">Email</label>
8154
- <input
8155
- id="email"
8156
- type="email"
8157
- [(ngModel)]="email"
8158
- name="email"
8159
- placeholder=""
8160
- class="login-input"
8161
- required
8162
- [disabled]="loading()"
8163
- />
8164
- </div>
8165
-
8166
- <div class="login-field">
8167
- <label for="password" class="login-label">Password</label>
8168
- <div class="login-input-wrap">
8169
- <input
8170
- id="password"
8171
- [type]="showPassword() ? 'text' : 'password'"
8172
- [(ngModel)]="password"
8173
- name="password"
8174
- placeholder="••••••••"
8175
- class="login-input"
8176
- required
8177
- [disabled]="loading()"
8178
- />
8179
- <button type="button" class="login-password-toggle" (click)="togglePassword()" tabindex="-1">
8180
- @if (showPassword()) {
8181
- <svg viewBox="0 0 20 20" fill="currentColor">
8182
- <path fill-rule="evenodd" d="M3.28 2.22a.75.75 0 00-1.06 1.06l14.5 14.5a.75.75 0 101.06-1.06l-1.745-1.745a10.029 10.029 0 003.3-4.38 1.651 1.651 0 000-1.185A10.004 10.004 0 009.999 3a9.956 9.956 0 00-4.744 1.194L3.28 2.22zM7.752 6.69l1.092 1.092a2.5 2.5 0 013.374 3.373l1.091 1.092a4 4 0 00-5.557-5.557z" clip-rule="evenodd" />
8183
- <path d="M10.748 13.93l2.523 2.523a9.987 9.987 0 01-3.27.547c-4.258 0-7.894-2.66-9.337-6.41a1.651 1.651 0 010-1.186A10.007 10.007 0 012.839 6.02L6.07 9.252a4 4 0 004.678 4.678z" />
8184
- </svg>
8185
- } @else {
8186
- <svg viewBox="0 0 20 20" fill="currentColor">
8187
- <path d="M10 12.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5z" />
8188
- <path fill-rule="evenodd" d="M.664 10.59a1.651 1.651 0 010-1.186A10.004 10.004 0 0110 3c4.257 0 7.893 2.66 9.336 6.41.147.381.146.804 0 1.186A10.004 10.004 0 0110 17c-4.257 0-7.893-2.66-9.336-6.41zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd" />
8189
- </svg>
8190
- }
8191
- </button>
8192
- </div>
8193
- </div>
8194
-
8195
- <button type="submit" class="login-submit" [disabled]="loading()">
8196
- @if (loading()) {
8197
- <span class="login-spinner"></span>
8198
- {{ isSignUp() ? 'Creating...' : 'Signing in...' }}
8199
- } @else {
8200
- {{ isSignUp() ? 'Create account' : 'Sign in' }}
8201
- }
8202
- </button>
8203
- </form>
8204
-
8205
- <div class="login-divider"><span>or</span></div>
8206
-
8207
- <button type="button" class="login-switch" (click)="toggleMode()">
8208
- {{ isSignUp() ? 'Already have an account? Sign in' : "Don't have an account? Sign up" }}
8209
- </button>
8210
- </div>
8211
-
8212
- <footer class="login-footer">
8213
- <span class="login-footer-text">Kustomizer</span>
8214
- <span class="login-footer-dot">·</span>
8215
- <a href="https://kustomizer.net" target="_blank" rel="noopener noreferrer" class="login-footer-link">kustomizer.net</a>
8216
- </footer>
8217
- </div>
8218
- </div>
8219
- `, styles: [":host{display:block}.login-page{--k-primary: #bdf366;--k-primary-hover: #a8e050;--k-primary-text: #1a1f0e;--k-bg: #0c0f08;--k-card: #ffffff;--k-border: #e5e7eb;--k-border-focus: #bdf366;--k-text: #1f2937;--k-text-secondary: #6b7280;--k-text-muted: #9ca3af;--k-error: #dc2626;--k-error-bg: #fef2f2;--k-error-border: #fecaca;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,sans-serif;min-height:100vh;display:flex;align-items:center;justify-content:center;background:var(--k-bg);color:var(--k-text);padding:20px;position:relative;overflow:hidden}.login-bg{position:fixed;inset:0;pointer-events:none;z-index:0}.login-gradient{position:absolute;inset:0;background:radial-gradient(ellipse 80% 50% at 50% -20%,rgba(189,243,102,.15),transparent),radial-gradient(ellipse 60% 40% at 100% 100%,rgba(189,243,102,.08),transparent),linear-gradient(180deg,#0c0f08,#111a0a)}.login-pattern{position:absolute;inset:0;background-image:radial-gradient(rgba(189,243,102,.07) 1px,transparent 1px);background-size:32px 32px;mask-image:radial-gradient(ellipse 100% 80% at 50% 20%,black,transparent);-webkit-mask-image:radial-gradient(ellipse 100% 80% at 50% 20%,black,transparent)}.login-glow{position:absolute;border-radius:50%;filter:blur(80px);opacity:.6}.login-glow--1{width:400px;height:400px;background:#bdf3661f;top:-150px;left:50%;transform:translate(-50%);animation:glowPulse 8s ease-in-out infinite}.login-glow--2{width:300px;height:300px;background:#bdf3660f;bottom:-100px;right:-50px;animation:glowPulse2 10s ease-in-out infinite}@keyframes glowPulse{0%,to{opacity:.4;transform:translate(-50%) scale(1)}50%{opacity:.7;transform:translate(-50%) scale(1.1)}}@keyframes glowPulse2{0%,to{opacity:.3;transform:scale(1)}50%{opacity:.5;transform:scale(1.15)}}.login-container{position:relative;z-index:1;width:100%;max-width:400px;animation:fadeIn .5s ease}@keyframes fadeIn{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.login-card{background:var(--k-card);border-radius:20px;padding:40px 32px;box-shadow:0 0 0 1px #ffffff1a,0 25px 50px -12px #00000080,0 0 100px -20px #bdf36633}.login-header{text-align:center;margin-bottom:32px}.login-logo-wrap{margin-bottom:20px}.login-logo{width:56px;height:56px;border-radius:12px}.login-title{font-size:24px;font-weight:600;color:var(--k-text);margin:0 0 8px;letter-spacing:-.3px}.login-subtitle{font-size:14px;color:var(--k-text-secondary);margin:0;font-weight:400}.login-alert{display:flex;align-items:flex-start;gap:10px;padding:12px 14px;border-radius:10px;font-size:14px;margin-bottom:20px;background:var(--k-error-bg);border:1px solid var(--k-error-border);color:var(--k-error)}.login-alert-icon{width:18px;height:18px;flex-shrink:0;margin-top:1px}.login-form{display:flex;flex-direction:column;gap:20px}.login-field{display:flex;flex-direction:column;gap:6px}.login-label{font-size:14px;font-weight:500;color:var(--k-text)}.login-input-wrap{position:relative;display:flex;align-items:center}.login-input{width:100%;background:var(--k-card);border:1px solid var(--k-border);border-radius:10px;padding:12px 14px;font-size:15px;font-family:inherit;color:var(--k-text);outline:none;transition:border-color .2s ease,box-shadow .2s ease;box-sizing:border-box}.login-input-wrap .login-input{padding-right:44px}.login-input::placeholder{color:var(--k-text-muted)}.login-input:hover{border-color:#d1d5db}.login-input:focus{border-color:var(--k-border-focus);box-shadow:0 0 0 3px #bdf36626}.login-input:disabled{background:#f3f4f6;cursor:not-allowed;opacity:.7}.login-password-toggle{position:absolute;right:10px;background:none;border:none;padding:6px;cursor:pointer;color:var(--k-text-muted);transition:color .2s ease;display:flex;align-items:center;justify-content:center;border-radius:6px}.login-password-toggle:hover{color:var(--k-primary-text);background:#bdf36626}.login-password-toggle svg{width:18px;height:18px}.login-submit{width:100%;background:var(--k-primary);color:var(--k-primary-text);border:none;border-radius:10px;padding:14px 20px;font-size:15px;font-weight:600;font-family:inherit;cursor:pointer;transition:background .2s ease,transform .1s ease;margin-top:4px;display:flex;align-items:center;justify-content:center;gap:8px}.login-submit:hover:not(:disabled){background:var(--k-primary-hover)}.login-submit:active:not(:disabled){transform:scale(.98)}.login-submit:disabled{opacity:.6;cursor:not-allowed}.login-spinner{width:16px;height:16px;border:2px solid transparent;border-top-color:var(--k-primary-text);border-radius:50%;animation:spin .7s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.login-divider{display:flex;align-items:center;gap:14px;margin:24px 0}.login-divider:before,.login-divider:after{content:\"\";flex:1;height:1px;background:var(--k-border)}.login-divider span{font-size:13px;color:var(--k-text-muted);text-transform:lowercase}.login-switch{width:100%;background:transparent;border:1px solid var(--k-border);border-radius:10px;padding:12px 16px;font-size:14px;font-weight:500;font-family:inherit;color:var(--k-text-secondary);cursor:pointer;transition:all .2s ease}.login-switch:hover{background:#bdf36614;border-color:#bdf3664d;color:var(--k-primary-text)}.login-footer{display:flex;align-items:center;justify-content:center;gap:8px;margin-top:24px}.login-footer-text{font-size:13px;color:#fff6;font-weight:500}.login-footer-link{font-size:13px;color:#ffffff80;text-decoration:none;transition:color .2s ease}.login-footer-link:hover{color:var(--k-primary)}.login-footer-dot{color:#ffffff4d;font-size:12px}@media(max-width:440px){.login-page{padding:16px}.login-card{padding:32px 24px}.login-title{font-size:22px}}\n"] }]
8220
- }], propDecorators: { loginSuccess: [{ type: i0.Output, args: ["loginSuccess"] }] } });
8221
-
8222
- class ActivateLicenseComponent {
8223
- shopAuthService = inject(ShopAuthService);
8224
- viewState = computed(() => {
8225
- if (this.shopAuthService.loading()) {
8226
- return 'loading';
8227
- }
8228
- if (!this.shopAuthService.shop()) {
8229
- return 'no-store';
8230
- }
8231
- return 'license-expired';
8232
- }, ...(ngDevMode ? [{ debugName: "viewState" }] : []));
8233
- ngOnInit() {
8234
- this.shopAuthService.authenticate();
8235
- }
8236
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ActivateLicenseComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
8237
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: ActivateLicenseComponent, isStandalone: true, selector: "lib-activate-license", ngImport: i0, template: `
8238
- <div class="activate-page">
8239
- <div class="activate-bg">
8240
- <div class="activate-glow"></div>
8241
- <div class="activate-grid"></div>
8242
- </div>
8243
-
8244
- <div class="activate-card">
8245
- @switch (viewState()) {
8246
- @case ('loading') {
8247
- <div class="activate-loading">
8248
- <span class="pulse-dot"></span>
8249
- <p>Verificando cuenta...</p>
8250
- </div>
8251
- }
8252
-
8253
- @case ('no-store') {
8254
- <div class="activate-content">
8255
- <p class="store-name">Tienda no encontrada</p>
8256
- <p class="license-status">Licencia expirada</p>
8257
- <a class="renew-button" href="https://kustomizer.net" target="_blank" rel="noopener noreferrer">
8258
- Renovar en kustomizer.net
8259
- </a>
8260
- </div>
8261
- }
8262
-
8263
- @case ('license-expired') {
8264
- <div class="activate-content">
8265
- <p class="store-name">{{ shopAuthService.shopName() }}</p>
8266
- <p class="license-status">Licencia expirada</p>
8267
- <a class="renew-button" href="https://kustomizer.net" target="_blank" rel="noopener noreferrer">
8268
- Renovar en kustomizer.net
8269
- </a>
8270
- </div>
8271
- }
8272
- }
8273
- </div>
8274
- </div>
8275
- `, isInline: true, styles: ["@import\"https://fonts.googleapis.com/css2?family=Newsreader:wght@400;500;600&family=Spline+Sans:wght@400;500;600&display=swap\";:host{display:block}.activate-page{--k-accent: #bdf366;--k-accent-strong: #a8e050;--k-ink: #11140c;--k-ink-muted: rgba(17, 20, 12, .6);--k-ink-faint: rgba(17, 20, 12, .45);--k-ink-ghost: rgba(17, 20, 12, .3);--k-surface: #f7f8f1;--k-surface-strong: #ffffff;--k-outline: rgba(17, 20, 12, .12);--k-shadow: 0 40px 100px -60px rgba(12, 15, 8, .6);min-height:100vh;display:flex;align-items:center;justify-content:center;background:#0c0f08;padding:24px;position:relative;overflow:hidden;font-family:Spline Sans,Segoe UI,sans-serif;color:var(--k-ink)}.activate-bg{position:absolute;inset:0;pointer-events:none}.activate-glow{position:absolute;width:520px;height:520px;left:50%;top:-200px;transform:translate(-50%);background:radial-gradient(circle,rgba(189,243,102,.22),transparent 70%);filter:blur(10px);opacity:.8}.activate-grid{position:absolute;inset:0;background-image:linear-gradient(rgba(189,243,102,.08) 1px,transparent 1px),linear-gradient(90deg,rgba(189,243,102,.08) 1px,transparent 1px);background-size:40px 40px;mask-image:radial-gradient(circle at 50% 10%,black,transparent 70%);-webkit-mask-image:radial-gradient(circle at 50% 10%,black,transparent 70%);opacity:.4}.activate-card{position:relative;z-index:1;width:min(480px,92vw);background:var(--k-surface-strong);border-radius:24px;padding:48px 40px;box-shadow:var(--k-shadow);border:1px solid rgba(255,255,255,.2);animation:rise .5s ease both}@keyframes rise{0%{opacity:0;transform:translateY(18px)}to{opacity:1;transform:translateY(0)}}.activate-loading{text-align:center;color:#ffffffb3;font-size:.9rem}.activate-loading p{margin:12px 0 0}.pulse-dot{width:10px;height:10px;border-radius:999px;background:var(--k-accent);display:inline-block;animation:pulse 1.4s ease-in-out infinite}@keyframes pulse{0%,to{transform:scale(1);opacity:.6}50%{transform:scale(1.5);opacity:1}}.activate-content{display:flex;flex-direction:column;gap:16px;align-items:center;text-align:center}.store-name{margin:0;font-family:Newsreader,Times New Roman,serif;font-size:1.6rem;letter-spacing:-.4px;color:var(--k-ink)}.license-status{margin:0;font-size:.95rem;text-transform:uppercase;letter-spacing:.2em;color:var(--k-ink-ghost)}.renew-button{margin-top:16px;padding:12px 22px;border-radius:999px;border:1px solid var(--k-outline);color:var(--k-ink);text-decoration:none;font-size:.9rem;font-weight:600;letter-spacing:.02em;background:var(--k-surface);transition:all .2s ease}.renew-button:hover{border-color:#11140c47;background:#fff;transform:translateY(-1px)}@media(max-width:480px){.activate-card{padding:36px 26px}.store-name{font-size:1.4rem}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }] });
7743
+ getPublishedPageBySlug(slug) {
7744
+ return this.metafieldRepo.getPageIndex().pipe(switchMap(index => {
7745
+ const entry = index.pages.find(p => p.slug === slug && p.status === 'published');
7746
+ if (!entry) {
7747
+ return throwError(() => ({
7748
+ code: 'PAGE_NOT_FOUND',
7749
+ message: `Published page with slug '${slug}' not found`,
7750
+ }));
7751
+ }
7752
+ return this.getPage(entry.id);
7753
+ }));
7754
+ }
7755
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: PageStorefrontRepository, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
7756
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: PageStorefrontRepository, providedIn: 'root' });
8276
7757
  }
8277
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ActivateLicenseComponent, decorators: [{
8278
- type: Component,
8279
- args: [{ selector: 'lib-activate-license', standalone: true, imports: [CommonModule], template: `
8280
- <div class="activate-page">
8281
- <div class="activate-bg">
8282
- <div class="activate-glow"></div>
8283
- <div class="activate-grid"></div>
8284
- </div>
8285
-
8286
- <div class="activate-card">
8287
- @switch (viewState()) {
8288
- @case ('loading') {
8289
- <div class="activate-loading">
8290
- <span class="pulse-dot"></span>
8291
- <p>Verificando cuenta...</p>
8292
- </div>
8293
- }
8294
-
8295
- @case ('no-store') {
8296
- <div class="activate-content">
8297
- <p class="store-name">Tienda no encontrada</p>
8298
- <p class="license-status">Licencia expirada</p>
8299
- <a class="renew-button" href="https://kustomizer.net" target="_blank" rel="noopener noreferrer">
8300
- Renovar en kustomizer.net
8301
- </a>
8302
- </div>
8303
- }
8304
-
8305
- @case ('license-expired') {
8306
- <div class="activate-content">
8307
- <p class="store-name">{{ shopAuthService.shopName() }}</p>
8308
- <p class="license-status">Licencia expirada</p>
8309
- <a class="renew-button" href="https://kustomizer.net" target="_blank" rel="noopener noreferrer">
8310
- Renovar en kustomizer.net
8311
- </a>
8312
- </div>
8313
- }
8314
- }
8315
- </div>
8316
- </div>
8317
- `, styles: ["@import\"https://fonts.googleapis.com/css2?family=Newsreader:wght@400;500;600&family=Spline+Sans:wght@400;500;600&display=swap\";:host{display:block}.activate-page{--k-accent: #bdf366;--k-accent-strong: #a8e050;--k-ink: #11140c;--k-ink-muted: rgba(17, 20, 12, .6);--k-ink-faint: rgba(17, 20, 12, .45);--k-ink-ghost: rgba(17, 20, 12, .3);--k-surface: #f7f8f1;--k-surface-strong: #ffffff;--k-outline: rgba(17, 20, 12, .12);--k-shadow: 0 40px 100px -60px rgba(12, 15, 8, .6);min-height:100vh;display:flex;align-items:center;justify-content:center;background:#0c0f08;padding:24px;position:relative;overflow:hidden;font-family:Spline Sans,Segoe UI,sans-serif;color:var(--k-ink)}.activate-bg{position:absolute;inset:0;pointer-events:none}.activate-glow{position:absolute;width:520px;height:520px;left:50%;top:-200px;transform:translate(-50%);background:radial-gradient(circle,rgba(189,243,102,.22),transparent 70%);filter:blur(10px);opacity:.8}.activate-grid{position:absolute;inset:0;background-image:linear-gradient(rgba(189,243,102,.08) 1px,transparent 1px),linear-gradient(90deg,rgba(189,243,102,.08) 1px,transparent 1px);background-size:40px 40px;mask-image:radial-gradient(circle at 50% 10%,black,transparent 70%);-webkit-mask-image:radial-gradient(circle at 50% 10%,black,transparent 70%);opacity:.4}.activate-card{position:relative;z-index:1;width:min(480px,92vw);background:var(--k-surface-strong);border-radius:24px;padding:48px 40px;box-shadow:var(--k-shadow);border:1px solid rgba(255,255,255,.2);animation:rise .5s ease both}@keyframes rise{0%{opacity:0;transform:translateY(18px)}to{opacity:1;transform:translateY(0)}}.activate-loading{text-align:center;color:#ffffffb3;font-size:.9rem}.activate-loading p{margin:12px 0 0}.pulse-dot{width:10px;height:10px;border-radius:999px;background:var(--k-accent);display:inline-block;animation:pulse 1.4s ease-in-out infinite}@keyframes pulse{0%,to{transform:scale(1);opacity:.6}50%{transform:scale(1.5);opacity:1}}.activate-content{display:flex;flex-direction:column;gap:16px;align-items:center;text-align:center}.store-name{margin:0;font-family:Newsreader,Times New Roman,serif;font-size:1.6rem;letter-spacing:-.4px;color:var(--k-ink)}.license-status{margin:0;font-size:.95rem;text-transform:uppercase;letter-spacing:.2em;color:var(--k-ink-ghost)}.renew-button{margin-top:16px;padding:12px 22px;border-radius:999px;border:1px solid var(--k-outline);color:var(--k-ink);text-decoration:none;font-size:.9rem;font-weight:600;letter-spacing:.02em;background:var(--k-surface);transition:all .2s ease}.renew-button:hover{border-color:#11140c47;background:#fff;transform:translateY(-1px)}@media(max-width:480px){.activate-card{padding:36px 26px}.store-name{font-size:1.4rem}}\n"] }]
7758
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: PageStorefrontRepository, decorators: [{
7759
+ type: Injectable,
7760
+ args: [{ providedIn: 'root' }]
8318
7761
  }] });
8319
7762
 
8320
- class AppShellComponent {
8321
- authService = inject(SupabaseAuthService);
8322
- shopAuthService = inject(ShopAuthService);
8323
- loggedOut = output();
8324
- async logout() {
8325
- await this.authService.signOut();
8326
- this.shopAuthService.clear();
8327
- this.loggedOut.emit();
8328
- }
8329
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: AppShellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
8330
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: AppShellComponent, isStandalone: true, selector: "lib-app-shell", outputs: { loggedOut: "loggedOut" }, ngImport: i0, template: `
8331
- <div class="app-shell">
8332
- <header class="app-header">
8333
- <div class="header-left">
8334
- <span class="logo">Kustomizer</span>
8335
- @if (shopAuthService.shopName()) {
8336
- <span class="store-name">{{ shopAuthService.shopName() }}</span>
8337
- }
8338
- </div>
8339
- <div class="header-right">
8340
- @if (shopAuthService.licenseTier()) {
8341
- <span class="license-badge" [class.expiring]="shopAuthService.daysRemaining() !== null && shopAuthService.daysRemaining()! <= 7">
8342
- {{ shopAuthService.licenseTier() | uppercase }}
8343
- @if (shopAuthService.daysRemaining() !== null && shopAuthService.daysRemaining()! <= 7) {
8344
- <span class="days-left">({{ shopAuthService.daysRemaining() }}d)</span>
8345
- }
8346
- </span>
8347
- }
8348
- <div class="user-menu">
8349
- <span class="user-email">{{ authService.userEmail() }}</span>
8350
- <button class="btn-logout" (click)="logout()">Salir</button>
8351
- </div>
8352
- </div>
8353
- </header>
8354
- <main class="app-content">
8355
- <ng-content></ng-content>
8356
- </main>
8357
- </div>
8358
- `, isInline: true, styles: [".app-shell{min-height:100vh;display:flex;flex-direction:column;background:#f6f6f7;font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif}.app-header{display:flex;justify-content:space-between;align-items:center;padding:0 1rem;height:56px;background:#1a1a1a;color:#e3e3e3;box-shadow:0 1px #00000014}.header-left{display:flex;align-items:center;gap:.75rem}.logo{font-size:.9375rem;font-weight:600;letter-spacing:-.3px;color:#fff}.store-name{font-size:.8125rem;color:#8c9196;padding-left:.75rem;border-left:1px solid rgba(255,255,255,.15)}.header-right{display:flex;align-items:center;gap:.75rem}.license-badge{padding:.1875rem .5rem;background:#ffffff1f;border-radius:10px;font-size:.6875rem;font-weight:500;letter-spacing:.3px;color:#b5b5b5}.license-badge.expiring{background:#ffd6a4;color:#b98900}.days-left{font-weight:400;opacity:.8}.user-menu{display:flex;align-items:center;gap:.5rem}.user-email{font-size:.8125rem;color:#8c9196}.btn-logout{padding:.375rem .75rem;background:transparent;border:1px solid rgba(255,255,255,.15);border-radius:8px;color:#e3e3e3;font-size:.8125rem;font-family:inherit;cursor:pointer;transition:all .2s}.btn-logout:hover{background:#ffffff1a;border-color:#ffffff40}.app-content{flex:1}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "pipe", type: i1$1.UpperCasePipe, name: "uppercase" }] });
8359
- }
8360
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: AppShellComponent, decorators: [{
8361
- type: Component,
8362
- args: [{ selector: 'lib-app-shell', standalone: true, imports: [CommonModule], template: `
8363
- <div class="app-shell">
8364
- <header class="app-header">
8365
- <div class="header-left">
8366
- <span class="logo">Kustomizer</span>
8367
- @if (shopAuthService.shopName()) {
8368
- <span class="store-name">{{ shopAuthService.shopName() }}</span>
8369
- }
8370
- </div>
8371
- <div class="header-right">
8372
- @if (shopAuthService.licenseTier()) {
8373
- <span class="license-badge" [class.expiring]="shopAuthService.daysRemaining() !== null && shopAuthService.daysRemaining()! <= 7">
8374
- {{ shopAuthService.licenseTier() | uppercase }}
8375
- @if (shopAuthService.daysRemaining() !== null && shopAuthService.daysRemaining()! <= 7) {
8376
- <span class="days-left">({{ shopAuthService.daysRemaining() }}d)</span>
8377
- }
8378
- </span>
8379
- }
8380
- <div class="user-menu">
8381
- <span class="user-email">{{ authService.userEmail() }}</span>
8382
- <button class="btn-logout" (click)="logout()">Salir</button>
8383
- </div>
8384
- </div>
8385
- </header>
8386
- <main class="app-content">
8387
- <ng-content></ng-content>
8388
- </main>
8389
- </div>
8390
- `, styles: [".app-shell{min-height:100vh;display:flex;flex-direction:column;background:#f6f6f7;font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif}.app-header{display:flex;justify-content:space-between;align-items:center;padding:0 1rem;height:56px;background:#1a1a1a;color:#e3e3e3;box-shadow:0 1px #00000014}.header-left{display:flex;align-items:center;gap:.75rem}.logo{font-size:.9375rem;font-weight:600;letter-spacing:-.3px;color:#fff}.store-name{font-size:.8125rem;color:#8c9196;padding-left:.75rem;border-left:1px solid rgba(255,255,255,.15)}.header-right{display:flex;align-items:center;gap:.75rem}.license-badge{padding:.1875rem .5rem;background:#ffffff1f;border-radius:10px;font-size:.6875rem;font-weight:500;letter-spacing:.3px;color:#b5b5b5}.license-badge.expiring{background:#ffd6a4;color:#b98900}.days-left{font-weight:400;opacity:.8}.user-menu{display:flex;align-items:center;gap:.5rem}.user-email{font-size:.8125rem;color:#8c9196}.btn-logout{padding:.375rem .75rem;background:transparent;border:1px solid rgba(255,255,255,.15);border-radius:8px;color:#e3e3e3;font-size:.8125rem;font-family:inherit;cursor:pointer;transition:all .2s}.btn-logout:hover{background:#ffffff1a;border-color:#ffffff40}.app-content{flex:1}\n"] }]
8391
- }], propDecorators: { loggedOut: [{ type: i0.Output, args: ["loggedOut"] }] } });
8392
-
8393
- class NoAccessComponent {
8394
- backToLogin = output();
8395
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NoAccessComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
8396
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.0.6", type: NoAccessComponent, isStandalone: true, selector: "ke-no-access", outputs: { backToLogin: "backToLogin" }, ngImport: i0, template: `
8397
- <div class="noaccess-page">
8398
- <div class="noaccess-bg">
8399
- <div class="noaccess-gradient"></div>
8400
- <div class="noaccess-pattern"></div>
8401
- <div class="noaccess-glow noaccess-glow--1"></div>
8402
- <div class="noaccess-glow noaccess-glow--2"></div>
8403
- </div>
8404
-
8405
- <div class="noaccess-container">
8406
- <div class="noaccess-card">
8407
- <header class="noaccess-header">
8408
- <div class="noaccess-logo-wrap">
8409
- <img src="/img/kustomizerlogo.png" alt="Kustomizer" class="noaccess-logo" />
8410
- </div>
8411
-
8412
- <div class="noaccess-icon-wrap">
8413
- <div class="noaccess-icon-ring"></div>
8414
- <svg class="noaccess-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
8415
- <rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
8416
- <path d="M7 11V7a5 5 0 0 1 10 0v4" />
8417
- </svg>
8418
- </div>
8419
-
8420
- <h1 class="noaccess-title">No access</h1>
8421
- <p class="noaccess-subtitle">You don't have permissions to access this store. Contact the store owner to request an invitation.</p>
8422
- </header>
8423
-
8424
- <div class="noaccess-info">
8425
- <div class="noaccess-info-row">
8426
- <svg viewBox="0 0 20 20" fill="currentColor" class="noaccess-info-icon">
8427
- <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" clip-rule="evenodd" />
8428
- </svg>
8429
- <span>If you believe this is an error, try signing in with a different account.</span>
8430
- </div>
8431
- </div>
8432
-
8433
- <button type="button" class="noaccess-btn" (click)="backToLogin.emit()">
8434
- <svg viewBox="0 0 20 20" fill="currentColor" class="noaccess-btn-icon">
8435
- <path fill-rule="evenodd" d="M17 10a.75.75 0 01-.75.75H5.612l4.158 3.96a.75.75 0 11-1.04 1.08l-5.5-5.25a.75.75 0 010-1.08l5.5-5.25a.75.75 0 111.04 1.08L5.612 9.25H16.25A.75.75 0 0117 10z" clip-rule="evenodd" />
8436
- </svg>
8437
- Back to sign in
8438
- </button>
8439
- </div>
8440
-
8441
- <footer class="noaccess-footer">
8442
- <span class="noaccess-footer-text">Kustomizer</span>
8443
- <span class="noaccess-footer-dot">&middot;</span>
8444
- <a href="https://kustomizer.net" target="_blank" rel="noopener noreferrer" class="noaccess-footer-link">kustomizer.net</a>
8445
- </footer>
8446
- </div>
8447
- </div>
8448
- `, isInline: true, styles: [":host{display:block}.noaccess-page{--k-primary: #bdf366;--k-primary-hover: #a8e050;--k-primary-text: #1a1f0e;--k-bg: #0c0f08;--k-card: #ffffff;--k-border: #e5e7eb;--k-text: #1f2937;--k-text-secondary: #6b7280;--k-text-muted: #9ca3af;--k-red: #dc2626;--k-red-light: #fef2f2;--k-red-border: #fecaca;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,sans-serif;min-height:100vh;display:flex;align-items:center;justify-content:center;background:var(--k-bg);color:var(--k-text);padding:20px;position:relative;overflow:hidden}.noaccess-bg{position:fixed;inset:0;pointer-events:none;z-index:0}.noaccess-gradient{position:absolute;inset:0;background:radial-gradient(ellipse 80% 50% at 50% -20%,rgba(220,38,38,.1),transparent),radial-gradient(ellipse 60% 40% at 100% 100%,rgba(189,243,102,.06),transparent),linear-gradient(180deg,#0c0f08,#111a0a)}.noaccess-pattern{position:absolute;inset:0;background-image:radial-gradient(rgba(189,243,102,.07) 1px,transparent 1px);background-size:32px 32px;mask-image:radial-gradient(ellipse 100% 80% at 50% 20%,black,transparent);-webkit-mask-image:radial-gradient(ellipse 100% 80% at 50% 20%,black,transparent)}.noaccess-glow{position:absolute;border-radius:50%;filter:blur(80px);opacity:.6}.noaccess-glow--1{width:400px;height:400px;background:#dc262614;top:-150px;left:50%;transform:translate(-50%);animation:glowPulse 8s ease-in-out infinite}.noaccess-glow--2{width:300px;height:300px;background:#bdf3660f;bottom:-100px;right:-50px;animation:glowPulse2 10s ease-in-out infinite}@keyframes glowPulse{0%,to{opacity:.4;transform:translate(-50%) scale(1)}50%{opacity:.7;transform:translate(-50%) scale(1.1)}}@keyframes glowPulse2{0%,to{opacity:.3;transform:scale(1)}50%{opacity:.5;transform:scale(1.15)}}.noaccess-container{position:relative;z-index:1;width:100%;max-width:400px;animation:fadeIn .5s ease}@keyframes fadeIn{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.noaccess-card{background:var(--k-card);border-radius:20px;padding:40px 32px;box-shadow:0 0 0 1px #ffffff1a,0 25px 50px -12px #00000080,0 0 100px -20px #dc26261f}.noaccess-header{text-align:center;margin-bottom:24px}.noaccess-logo-wrap{margin-bottom:24px}.noaccess-logo{width:56px;height:56px;border-radius:12px}.noaccess-icon-wrap{position:relative;display:inline-flex;align-items:center;justify-content:center;width:56px;height:56px;margin-bottom:20px}.noaccess-icon-ring{position:absolute;inset:0;border-radius:50%;background:var(--k-red-light);border:1px solid var(--k-red-border)}.noaccess-icon{position:relative;width:28px;height:28px;color:var(--k-red)}.noaccess-title{font-size:24px;font-weight:600;color:var(--k-text);margin:0 0 8px;letter-spacing:-.3px}.noaccess-subtitle{font-size:14px;color:var(--k-text-secondary);margin:0;font-weight:400;line-height:1.5}.noaccess-info{background:var(--k-red-light);border:1px solid var(--k-red-border);border-radius:10px;padding:12px 14px;margin-bottom:24px}.noaccess-info-row{display:flex;align-items:flex-start;gap:10px;font-size:13px;color:#991b1b;line-height:1.4}.noaccess-info-icon{width:16px;height:16px;flex-shrink:0;margin-top:1px;color:var(--k-red)}.noaccess-btn{display:flex;align-items:center;justify-content:center;gap:8px;width:100%;background:var(--k-primary);color:var(--k-primary-text);border:none;border-radius:10px;padding:14px 20px;font-size:15px;font-weight:600;font-family:inherit;cursor:pointer;transition:background .2s ease,transform .1s ease;text-decoration:none;box-sizing:border-box}.noaccess-btn:hover{background:var(--k-primary-hover)}.noaccess-btn:active{transform:scale(.98)}.noaccess-btn-icon{width:16px;height:16px}.noaccess-footer{display:flex;align-items:center;justify-content:center;gap:8px;margin-top:24px}.noaccess-footer-text{font-size:13px;color:#fff6;font-weight:500}.noaccess-footer-link{font-size:13px;color:#ffffff80;text-decoration:none;transition:color .2s ease}.noaccess-footer-link:hover{color:var(--k-primary)}.noaccess-footer-dot{color:#ffffff4d;font-size:12px}@media(max-width:440px){.noaccess-page{padding:16px}.noaccess-card{padding:32px 24px}.noaccess-title{font-size:22px}}\n"] });
8449
- }
8450
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NoAccessComponent, decorators: [{
8451
- type: Component,
8452
- args: [{ selector: 'ke-no-access', standalone: true, template: `
8453
- <div class="noaccess-page">
8454
- <div class="noaccess-bg">
8455
- <div class="noaccess-gradient"></div>
8456
- <div class="noaccess-pattern"></div>
8457
- <div class="noaccess-glow noaccess-glow--1"></div>
8458
- <div class="noaccess-glow noaccess-glow--2"></div>
8459
- </div>
8460
-
8461
- <div class="noaccess-container">
8462
- <div class="noaccess-card">
8463
- <header class="noaccess-header">
8464
- <div class="noaccess-logo-wrap">
8465
- <img src="/img/kustomizerlogo.png" alt="Kustomizer" class="noaccess-logo" />
8466
- </div>
8467
-
8468
- <div class="noaccess-icon-wrap">
8469
- <div class="noaccess-icon-ring"></div>
8470
- <svg class="noaccess-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
8471
- <rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
8472
- <path d="M7 11V7a5 5 0 0 1 10 0v4" />
8473
- </svg>
8474
- </div>
8475
-
8476
- <h1 class="noaccess-title">No access</h1>
8477
- <p class="noaccess-subtitle">You don't have permissions to access this store. Contact the store owner to request an invitation.</p>
8478
- </header>
8479
-
8480
- <div class="noaccess-info">
8481
- <div class="noaccess-info-row">
8482
- <svg viewBox="0 0 20 20" fill="currentColor" class="noaccess-info-icon">
8483
- <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" clip-rule="evenodd" />
8484
- </svg>
8485
- <span>If you believe this is an error, try signing in with a different account.</span>
8486
- </div>
8487
- </div>
8488
-
8489
- <button type="button" class="noaccess-btn" (click)="backToLogin.emit()">
8490
- <svg viewBox="0 0 20 20" fill="currentColor" class="noaccess-btn-icon">
8491
- <path fill-rule="evenodd" d="M17 10a.75.75 0 01-.75.75H5.612l4.158 3.96a.75.75 0 11-1.04 1.08l-5.5-5.25a.75.75 0 010-1.08l5.5-5.25a.75.75 0 111.04 1.08L5.612 9.25H16.25A.75.75 0 0117 10z" clip-rule="evenodd" />
8492
- </svg>
8493
- Back to sign in
8494
- </button>
8495
- </div>
8496
-
8497
- <footer class="noaccess-footer">
8498
- <span class="noaccess-footer-text">Kustomizer</span>
8499
- <span class="noaccess-footer-dot">&middot;</span>
8500
- <a href="https://kustomizer.net" target="_blank" rel="noopener noreferrer" class="noaccess-footer-link">kustomizer.net</a>
8501
- </footer>
8502
- </div>
8503
- </div>
8504
- `, styles: [":host{display:block}.noaccess-page{--k-primary: #bdf366;--k-primary-hover: #a8e050;--k-primary-text: #1a1f0e;--k-bg: #0c0f08;--k-card: #ffffff;--k-border: #e5e7eb;--k-text: #1f2937;--k-text-secondary: #6b7280;--k-text-muted: #9ca3af;--k-red: #dc2626;--k-red-light: #fef2f2;--k-red-border: #fecaca;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,sans-serif;min-height:100vh;display:flex;align-items:center;justify-content:center;background:var(--k-bg);color:var(--k-text);padding:20px;position:relative;overflow:hidden}.noaccess-bg{position:fixed;inset:0;pointer-events:none;z-index:0}.noaccess-gradient{position:absolute;inset:0;background:radial-gradient(ellipse 80% 50% at 50% -20%,rgba(220,38,38,.1),transparent),radial-gradient(ellipse 60% 40% at 100% 100%,rgba(189,243,102,.06),transparent),linear-gradient(180deg,#0c0f08,#111a0a)}.noaccess-pattern{position:absolute;inset:0;background-image:radial-gradient(rgba(189,243,102,.07) 1px,transparent 1px);background-size:32px 32px;mask-image:radial-gradient(ellipse 100% 80% at 50% 20%,black,transparent);-webkit-mask-image:radial-gradient(ellipse 100% 80% at 50% 20%,black,transparent)}.noaccess-glow{position:absolute;border-radius:50%;filter:blur(80px);opacity:.6}.noaccess-glow--1{width:400px;height:400px;background:#dc262614;top:-150px;left:50%;transform:translate(-50%);animation:glowPulse 8s ease-in-out infinite}.noaccess-glow--2{width:300px;height:300px;background:#bdf3660f;bottom:-100px;right:-50px;animation:glowPulse2 10s ease-in-out infinite}@keyframes glowPulse{0%,to{opacity:.4;transform:translate(-50%) scale(1)}50%{opacity:.7;transform:translate(-50%) scale(1.1)}}@keyframes glowPulse2{0%,to{opacity:.3;transform:scale(1)}50%{opacity:.5;transform:scale(1.15)}}.noaccess-container{position:relative;z-index:1;width:100%;max-width:400px;animation:fadeIn .5s ease}@keyframes fadeIn{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.noaccess-card{background:var(--k-card);border-radius:20px;padding:40px 32px;box-shadow:0 0 0 1px #ffffff1a,0 25px 50px -12px #00000080,0 0 100px -20px #dc26261f}.noaccess-header{text-align:center;margin-bottom:24px}.noaccess-logo-wrap{margin-bottom:24px}.noaccess-logo{width:56px;height:56px;border-radius:12px}.noaccess-icon-wrap{position:relative;display:inline-flex;align-items:center;justify-content:center;width:56px;height:56px;margin-bottom:20px}.noaccess-icon-ring{position:absolute;inset:0;border-radius:50%;background:var(--k-red-light);border:1px solid var(--k-red-border)}.noaccess-icon{position:relative;width:28px;height:28px;color:var(--k-red)}.noaccess-title{font-size:24px;font-weight:600;color:var(--k-text);margin:0 0 8px;letter-spacing:-.3px}.noaccess-subtitle{font-size:14px;color:var(--k-text-secondary);margin:0;font-weight:400;line-height:1.5}.noaccess-info{background:var(--k-red-light);border:1px solid var(--k-red-border);border-radius:10px;padding:12px 14px;margin-bottom:24px}.noaccess-info-row{display:flex;align-items:flex-start;gap:10px;font-size:13px;color:#991b1b;line-height:1.4}.noaccess-info-icon{width:16px;height:16px;flex-shrink:0;margin-top:1px;color:var(--k-red)}.noaccess-btn{display:flex;align-items:center;justify-content:center;gap:8px;width:100%;background:var(--k-primary);color:var(--k-primary-text);border:none;border-radius:10px;padding:14px 20px;font-size:15px;font-weight:600;font-family:inherit;cursor:pointer;transition:background .2s ease,transform .1s ease;text-decoration:none;box-sizing:border-box}.noaccess-btn:hover{background:var(--k-primary-hover)}.noaccess-btn:active{transform:scale(.98)}.noaccess-btn-icon{width:16px;height:16px}.noaccess-footer{display:flex;align-items:center;justify-content:center;gap:8px;margin-top:24px}.noaccess-footer-text{font-size:13px;color:#fff6;font-weight:500}.noaccess-footer-link{font-size:13px;color:#ffffff80;text-decoration:none;transition:color .2s ease}.noaccess-footer-link:hover{color:var(--k-primary)}.noaccess-footer-dot{color:#ffffff4d;font-size:12px}@media(max-width:440px){.noaccess-page{padding:16px}.noaccess-card{padding:32px 24px}.noaccess-title{font-size:22px}}\n"] }]
8505
- }], propDecorators: { backToLogin: [{ type: i0.Output, args: ["backToLogin"] }] } });
8506
-
8507
- class LicenseExpiredComponent {
8508
- backToLogin = output();
8509
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: LicenseExpiredComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
8510
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.0.6", type: LicenseExpiredComponent, isStandalone: true, selector: "ke-license-expired", outputs: { backToLogin: "backToLogin" }, ngImport: i0, template: `
8511
- <div class="expired-page">
8512
- <div class="expired-bg">
8513
- <div class="expired-gradient"></div>
8514
- <div class="expired-pattern"></div>
8515
- <div class="expired-glow expired-glow--1"></div>
8516
- <div class="expired-glow expired-glow--2"></div>
8517
- </div>
8518
-
8519
- <div class="expired-container">
8520
- <div class="expired-card">
8521
- <header class="expired-header">
8522
- <div class="expired-logo-wrap">
8523
- <img src="/img/kustomizerlogo.png" alt="Kustomizer" class="expired-logo" />
8524
- </div>
8525
-
8526
- <div class="expired-icon-wrap">
8527
- <div class="expired-icon-ring"></div>
8528
- <svg class="expired-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
8529
- <circle cx="12" cy="12" r="10" />
8530
- <polyline points="12 6 12 12 16 14" />
8531
- </svg>
8532
- </div>
8533
-
8534
- <h1 class="expired-title">License expired</h1>
8535
- <p class="expired-subtitle">The license for this store's owner has expired. Contact the owner to renew the subscription.</p>
8536
- </header>
8537
-
8538
- <div class="expired-info">
8539
- <div class="expired-info-row">
8540
- <svg viewBox="0 0 20 20" fill="currentColor" class="expired-info-icon">
8541
- <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" clip-rule="evenodd" />
8542
- </svg>
8543
- <span>All editing features are disabled until the license is renewed.</span>
8544
- </div>
8545
- </div>
8546
-
8547
- <button type="button" class="expired-btn" (click)="backToLogin.emit()">
8548
- <svg viewBox="0 0 20 20" fill="currentColor" class="expired-btn-icon">
8549
- <path fill-rule="evenodd" d="M17 10a.75.75 0 01-.75.75H5.612l4.158 3.96a.75.75 0 11-1.04 1.08l-5.5-5.25a.75.75 0 010-1.08l5.5-5.25a.75.75 0 111.04 1.08L5.612 9.25H16.25A.75.75 0 0117 10z" clip-rule="evenodd" />
8550
- </svg>
8551
- Back to sign in
8552
- </button>
8553
- </div>
8554
-
8555
- <footer class="expired-footer">
8556
- <span class="expired-footer-text">Kustomizer</span>
8557
- <span class="expired-footer-dot">&middot;</span>
8558
- <a href="https://kustomizer.net" target="_blank" rel="noopener noreferrer" class="expired-footer-link">kustomizer.net</a>
8559
- </footer>
8560
- </div>
8561
- </div>
8562
- `, isInline: true, styles: [":host{display:block}.expired-page{--k-primary: #bdf366;--k-primary-hover: #a8e050;--k-primary-text: #1a1f0e;--k-bg: #0c0f08;--k-card: #ffffff;--k-border: #e5e7eb;--k-text: #1f2937;--k-text-secondary: #6b7280;--k-text-muted: #9ca3af;--k-amber: #f59e0b;--k-amber-light: #fef3c7;--k-amber-border: #fde68a;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,sans-serif;min-height:100vh;display:flex;align-items:center;justify-content:center;background:var(--k-bg);color:var(--k-text);padding:20px;position:relative;overflow:hidden}.expired-bg{position:fixed;inset:0;pointer-events:none;z-index:0}.expired-gradient{position:absolute;inset:0;background:radial-gradient(ellipse 80% 50% at 50% -20%,rgba(245,158,11,.12),transparent),radial-gradient(ellipse 60% 40% at 100% 100%,rgba(189,243,102,.06),transparent),linear-gradient(180deg,#0c0f08,#111a0a)}.expired-pattern{position:absolute;inset:0;background-image:radial-gradient(rgba(189,243,102,.07) 1px,transparent 1px);background-size:32px 32px;mask-image:radial-gradient(ellipse 100% 80% at 50% 20%,black,transparent);-webkit-mask-image:radial-gradient(ellipse 100% 80% at 50% 20%,black,transparent)}.expired-glow{position:absolute;border-radius:50%;filter:blur(80px);opacity:.6}.expired-glow--1{width:400px;height:400px;background:#f59e0b1a;top:-150px;left:50%;transform:translate(-50%);animation:glowPulse 8s ease-in-out infinite}.expired-glow--2{width:300px;height:300px;background:#bdf3660f;bottom:-100px;right:-50px;animation:glowPulse2 10s ease-in-out infinite}@keyframes glowPulse{0%,to{opacity:.4;transform:translate(-50%) scale(1)}50%{opacity:.7;transform:translate(-50%) scale(1.1)}}@keyframes glowPulse2{0%,to{opacity:.3;transform:scale(1)}50%{opacity:.5;transform:scale(1.15)}}.expired-container{position:relative;z-index:1;width:100%;max-width:400px;animation:fadeIn .5s ease}@keyframes fadeIn{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.expired-card{background:var(--k-card);border-radius:20px;padding:40px 32px;box-shadow:0 0 0 1px #ffffff1a,0 25px 50px -12px #00000080,0 0 100px -20px #f59e0b26}.expired-header{text-align:center;margin-bottom:24px}.expired-logo-wrap{margin-bottom:24px}.expired-logo{width:56px;height:56px;border-radius:12px}.expired-icon-wrap{position:relative;display:inline-flex;align-items:center;justify-content:center;width:56px;height:56px;margin-bottom:20px}.expired-icon-ring{position:absolute;inset:0;border-radius:50%;background:var(--k-amber-light);border:1px solid var(--k-amber-border);animation:ringPulse 3s ease-in-out infinite}@keyframes ringPulse{0%,to{transform:scale(1);opacity:1}50%{transform:scale(1.08);opacity:.8}}.expired-icon{position:relative;width:28px;height:28px;color:var(--k-amber)}.expired-title{font-size:24px;font-weight:600;color:var(--k-text);margin:0 0 8px;letter-spacing:-.3px}.expired-subtitle{font-size:14px;color:var(--k-text-secondary);margin:0;font-weight:400;line-height:1.5}.expired-info{background:var(--k-amber-light);border:1px solid var(--k-amber-border);border-radius:10px;padding:12px 14px;margin-bottom:24px}.expired-info-row{display:flex;align-items:flex-start;gap:10px;font-size:13px;color:#92400e;line-height:1.4}.expired-info-icon{width:16px;height:16px;flex-shrink:0;margin-top:1px;color:var(--k-amber)}.expired-btn{display:flex;align-items:center;justify-content:center;gap:8px;width:100%;background:var(--k-primary);color:var(--k-primary-text);border:none;border-radius:10px;padding:14px 20px;font-size:15px;font-weight:600;font-family:inherit;cursor:pointer;transition:background .2s ease,transform .1s ease;text-decoration:none;box-sizing:border-box}.expired-btn:hover{background:var(--k-primary-hover)}.expired-btn:active{transform:scale(.98)}.expired-btn-icon{width:16px;height:16px}.expired-footer{display:flex;align-items:center;justify-content:center;gap:8px;margin-top:24px}.expired-footer-text{font-size:13px;color:#fff6;font-weight:500}.expired-footer-link{font-size:13px;color:#ffffff80;text-decoration:none;transition:color .2s ease}.expired-footer-link:hover{color:var(--k-primary)}.expired-footer-dot{color:#ffffff4d;font-size:12px}@media(max-width:440px){.expired-page{padding:16px}.expired-card{padding:32px 24px}.expired-title{font-size:22px}}\n"] });
8563
- }
8564
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: LicenseExpiredComponent, decorators: [{
8565
- type: Component,
8566
- args: [{ selector: 'ke-license-expired', standalone: true, template: `
8567
- <div class="expired-page">
8568
- <div class="expired-bg">
8569
- <div class="expired-gradient"></div>
8570
- <div class="expired-pattern"></div>
8571
- <div class="expired-glow expired-glow--1"></div>
8572
- <div class="expired-glow expired-glow--2"></div>
8573
- </div>
8574
-
8575
- <div class="expired-container">
8576
- <div class="expired-card">
8577
- <header class="expired-header">
8578
- <div class="expired-logo-wrap">
8579
- <img src="/img/kustomizerlogo.png" alt="Kustomizer" class="expired-logo" />
8580
- </div>
8581
-
8582
- <div class="expired-icon-wrap">
8583
- <div class="expired-icon-ring"></div>
8584
- <svg class="expired-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
8585
- <circle cx="12" cy="12" r="10" />
8586
- <polyline points="12 6 12 12 16 14" />
8587
- </svg>
8588
- </div>
8589
-
8590
- <h1 class="expired-title">License expired</h1>
8591
- <p class="expired-subtitle">The license for this store's owner has expired. Contact the owner to renew the subscription.</p>
8592
- </header>
8593
-
8594
- <div class="expired-info">
8595
- <div class="expired-info-row">
8596
- <svg viewBox="0 0 20 20" fill="currentColor" class="expired-info-icon">
8597
- <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" clip-rule="evenodd" />
8598
- </svg>
8599
- <span>All editing features are disabled until the license is renewed.</span>
8600
- </div>
8601
- </div>
8602
-
8603
- <button type="button" class="expired-btn" (click)="backToLogin.emit()">
8604
- <svg viewBox="0 0 20 20" fill="currentColor" class="expired-btn-icon">
8605
- <path fill-rule="evenodd" d="M17 10a.75.75 0 01-.75.75H5.612l4.158 3.96a.75.75 0 11-1.04 1.08l-5.5-5.25a.75.75 0 010-1.08l5.5-5.25a.75.75 0 111.04 1.08L5.612 9.25H16.25A.75.75 0 0117 10z" clip-rule="evenodd" />
8606
- </svg>
8607
- Back to sign in
8608
- </button>
8609
- </div>
8610
-
8611
- <footer class="expired-footer">
8612
- <span class="expired-footer-text">Kustomizer</span>
8613
- <span class="expired-footer-dot">&middot;</span>
8614
- <a href="https://kustomizer.net" target="_blank" rel="noopener noreferrer" class="expired-footer-link">kustomizer.net</a>
8615
- </footer>
8616
- </div>
8617
- </div>
8618
- `, styles: [":host{display:block}.expired-page{--k-primary: #bdf366;--k-primary-hover: #a8e050;--k-primary-text: #1a1f0e;--k-bg: #0c0f08;--k-card: #ffffff;--k-border: #e5e7eb;--k-text: #1f2937;--k-text-secondary: #6b7280;--k-text-muted: #9ca3af;--k-amber: #f59e0b;--k-amber-light: #fef3c7;--k-amber-border: #fde68a;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,sans-serif;min-height:100vh;display:flex;align-items:center;justify-content:center;background:var(--k-bg);color:var(--k-text);padding:20px;position:relative;overflow:hidden}.expired-bg{position:fixed;inset:0;pointer-events:none;z-index:0}.expired-gradient{position:absolute;inset:0;background:radial-gradient(ellipse 80% 50% at 50% -20%,rgba(245,158,11,.12),transparent),radial-gradient(ellipse 60% 40% at 100% 100%,rgba(189,243,102,.06),transparent),linear-gradient(180deg,#0c0f08,#111a0a)}.expired-pattern{position:absolute;inset:0;background-image:radial-gradient(rgba(189,243,102,.07) 1px,transparent 1px);background-size:32px 32px;mask-image:radial-gradient(ellipse 100% 80% at 50% 20%,black,transparent);-webkit-mask-image:radial-gradient(ellipse 100% 80% at 50% 20%,black,transparent)}.expired-glow{position:absolute;border-radius:50%;filter:blur(80px);opacity:.6}.expired-glow--1{width:400px;height:400px;background:#f59e0b1a;top:-150px;left:50%;transform:translate(-50%);animation:glowPulse 8s ease-in-out infinite}.expired-glow--2{width:300px;height:300px;background:#bdf3660f;bottom:-100px;right:-50px;animation:glowPulse2 10s ease-in-out infinite}@keyframes glowPulse{0%,to{opacity:.4;transform:translate(-50%) scale(1)}50%{opacity:.7;transform:translate(-50%) scale(1.1)}}@keyframes glowPulse2{0%,to{opacity:.3;transform:scale(1)}50%{opacity:.5;transform:scale(1.15)}}.expired-container{position:relative;z-index:1;width:100%;max-width:400px;animation:fadeIn .5s ease}@keyframes fadeIn{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.expired-card{background:var(--k-card);border-radius:20px;padding:40px 32px;box-shadow:0 0 0 1px #ffffff1a,0 25px 50px -12px #00000080,0 0 100px -20px #f59e0b26}.expired-header{text-align:center;margin-bottom:24px}.expired-logo-wrap{margin-bottom:24px}.expired-logo{width:56px;height:56px;border-radius:12px}.expired-icon-wrap{position:relative;display:inline-flex;align-items:center;justify-content:center;width:56px;height:56px;margin-bottom:20px}.expired-icon-ring{position:absolute;inset:0;border-radius:50%;background:var(--k-amber-light);border:1px solid var(--k-amber-border);animation:ringPulse 3s ease-in-out infinite}@keyframes ringPulse{0%,to{transform:scale(1);opacity:1}50%{transform:scale(1.08);opacity:.8}}.expired-icon{position:relative;width:28px;height:28px;color:var(--k-amber)}.expired-title{font-size:24px;font-weight:600;color:var(--k-text);margin:0 0 8px;letter-spacing:-.3px}.expired-subtitle{font-size:14px;color:var(--k-text-secondary);margin:0;font-weight:400;line-height:1.5}.expired-info{background:var(--k-amber-light);border:1px solid var(--k-amber-border);border-radius:10px;padding:12px 14px;margin-bottom:24px}.expired-info-row{display:flex;align-items:flex-start;gap:10px;font-size:13px;color:#92400e;line-height:1.4}.expired-info-icon{width:16px;height:16px;flex-shrink:0;margin-top:1px;color:var(--k-amber)}.expired-btn{display:flex;align-items:center;justify-content:center;gap:8px;width:100%;background:var(--k-primary);color:var(--k-primary-text);border:none;border-radius:10px;padding:14px 20px;font-size:15px;font-weight:600;font-family:inherit;cursor:pointer;transition:background .2s ease,transform .1s ease;text-decoration:none;box-sizing:border-box}.expired-btn:hover{background:var(--k-primary-hover)}.expired-btn:active{transform:scale(.98)}.expired-btn-icon{width:16px;height:16px}.expired-footer{display:flex;align-items:center;justify-content:center;gap:8px;margin-top:24px}.expired-footer-text{font-size:13px;color:#fff6;font-weight:500}.expired-footer-link{font-size:13px;color:#ffffff80;text-decoration:none;transition:color .2s ease}.expired-footer-link:hover{color:var(--k-primary)}.expired-footer-dot{color:#ffffff4d;font-size:12px}@media(max-width:440px){.expired-page{padding:16px}.expired-card{padding:32px 24px}.expired-title{font-size:22px}}\n"] }]
8619
- }], propDecorators: { backToLogin: [{ type: i0.Output, args: ["backToLogin"] }] } });
8620
-
8621
- class ShopTeamService {
7763
+ /**
7764
+ * Loads a component manifest from a remote storefront URL and registers
7765
+ * the entries in the ComponentRegistryService as "virtual" definitions
7766
+ * (no Angular component class — only metadata for palette and property panel).
7767
+ */
7768
+ class ManifestLoaderService {
8622
7769
  http = inject(HttpClient);
8623
- config = inject(SUPABASE_CONFIG, { optional: true });
8624
- authService = inject(SupabaseAuthService);
8625
- shopAuthService = inject(ShopAuthService);
8626
- // Estado
8627
- _team = signal(null, ...(ngDevMode ? [{ debugName: "_team" }] : []));
8628
- _loading = signal(false, ...(ngDevMode ? [{ debugName: "_loading" }] : []));
8629
- _error = signal(null, ...(ngDevMode ? [{ debugName: "_error" }] : []));
8630
- team = this._team.asReadonly();
8631
- loading = this._loading.asReadonly();
8632
- error = this._error.asReadonly();
8633
- // Computed
8634
- members = computed(() => this._team()?.members ?? [], ...(ngDevMode ? [{ debugName: "members" }] : []));
8635
- invitations = computed(() => this._team()?.invitations ?? [], ...(ngDevMode ? [{ debugName: "invitations" }] : []));
8636
- currentUser = computed(() => this._team()?.current_user ?? null, ...(ngDevMode ? [{ debugName: "currentUser" }] : []));
8637
- activeMembers = computed(() => this.members().filter((m) => m.status === 'active'), ...(ngDevMode ? [{ debugName: "activeMembers" }] : []));
8638
- pendingInvitations = computed(() => this.invitations().filter((i) => !i.is_expired), ...(ngDevMode ? [{ debugName: "pendingInvitations" }] : []));
8639
- canInvite = computed(() => this._team()?.current_user?.can_invite ?? false, ...(ngDevMode ? [{ debugName: "canInvite" }] : []));
7770
+ registry = inject(ComponentRegistryService);
8640
7771
  /**
8641
- * Carga el equipo de la shop actual
7772
+ * Fetch the manifest from the given URL and register all components.
8642
7773
  */
8643
- async loadTeam() {
8644
- const shopId = this.shopAuthService.shopId();
8645
- if (!shopId) {
8646
- this._error.set('No shop selected');
8647
- return null;
8648
- }
8649
- const token = this.authService.accessToken();
8650
- if (!token) {
8651
- this._error.set('Not authenticated');
8652
- return null;
8653
- }
8654
- this._loading.set(true);
8655
- this._error.set(null);
8656
- try {
8657
- const url = this.config
8658
- ? `${this.config.supabaseUrl}/functions/v1/shop_team`
8659
- : '/functions/v1/shop_team';
8660
- const response = await firstValueFrom(this.http.post(url, { shop_id: shopId }, {
8661
- headers: {
8662
- 'Authorization': `Bearer ${token}`,
8663
- 'Content-Type': 'application/json',
8664
- },
8665
- }));
8666
- this._team.set(response);
8667
- this._loading.set(false);
8668
- return response;
8669
- }
8670
- catch (err) {
8671
- console.error('[ShopTeamService] Load team failed:', err);
8672
- const httpError = err;
8673
- this._error.set(httpError.error?.error || 'Failed to load team');
8674
- this._loading.set(false);
8675
- return null;
8676
- }
7774
+ loadManifest(url) {
7775
+ return this.http.get(url).pipe(tap((manifest) => this.registerManifestComponents(manifest)));
8677
7776
  }
8678
7777
  /**
8679
- * Invita a un usuario a la shop
7778
+ * Register manifest entries in the ComponentRegistryService.
7779
+ * Each entry becomes a ComponentDefinition without a `component` field.
8680
7780
  */
8681
- async inviteUser(email, role, message) {
8682
- const shopId = this.shopAuthService.shopId();
8683
- if (!shopId) {
8684
- this._error.set('No shop selected');
8685
- return null;
8686
- }
8687
- const token = this.authService.accessToken();
8688
- if (!token) {
8689
- this._error.set('Not authenticated');
8690
- return null;
8691
- }
8692
- this._loading.set(true);
8693
- this._error.set(null);
8694
- try {
8695
- const url = this.config
8696
- ? `${this.config.supabaseUrl}/functions/v1/invite_user`
8697
- : '/functions/v1/invite_user';
8698
- const response = await firstValueFrom(this.http.post(url, {
8699
- shop_id: shopId,
8700
- email: email.toLowerCase().trim(),
8701
- role,
8702
- message: message?.trim() || undefined,
8703
- }, {
8704
- headers: {
8705
- 'Authorization': `Bearer ${token}`,
8706
- 'Content-Type': 'application/json',
8707
- },
8708
- }));
8709
- // Recargar equipo para mostrar la invitación
8710
- await this.loadTeam();
8711
- this._loading.set(false);
8712
- return response;
8713
- }
8714
- catch (err) {
8715
- console.error('[ShopTeamService] Invite user failed:', err);
8716
- const httpError = err;
8717
- this._error.set(httpError.error?.error || 'Failed to invite user');
8718
- this._loading.set(false);
8719
- return null;
8720
- }
8721
- }
8722
- /**
8723
- * Acepta una invitación (usado cuando un usuario hace click en el link de invitación)
8724
- */
8725
- async acceptInvitation(invitationToken) {
8726
- const authToken = this.authService.accessToken();
8727
- if (!authToken) {
8728
- this._error.set('Not authenticated');
8729
- return null;
8730
- }
8731
- this._loading.set(true);
8732
- this._error.set(null);
8733
- try {
8734
- const url = this.config
8735
- ? `${this.config.supabaseUrl}/functions/v1/accept_invitation`
8736
- : '/functions/v1/accept_invitation';
8737
- const response = await firstValueFrom(this.http.post(url, {
8738
- token: invitationToken,
8739
- }, {
8740
- headers: {
8741
- 'Authorization': `Bearer ${authToken}`,
8742
- 'Content-Type': 'application/json',
8743
- },
8744
- }));
8745
- this._loading.set(false);
8746
- return response;
8747
- }
8748
- catch (err) {
8749
- console.error('[ShopTeamService] Accept invitation failed:', err);
8750
- const httpError = err;
8751
- this._error.set(httpError.error?.error || 'Failed to accept invitation');
8752
- this._loading.set(false);
8753
- return null;
7781
+ registerManifestComponents(manifest) {
7782
+ for (const entry of manifest.components) {
7783
+ const definition = this.manifestEntryToDefinition(entry);
7784
+ this.registry.register(definition);
8754
7785
  }
8755
7786
  }
8756
- /**
8757
- * Genera el URL de invitación (para copiar/compartir)
8758
- */
8759
- getInvitationUrl(token, baseUrl) {
8760
- const base = baseUrl || (typeof window !== 'undefined' ? window.location.origin : '');
8761
- return `${base}/accept-invitation?token=${token}`;
8762
- }
8763
- /**
8764
- * Limpia el estado
8765
- */
8766
- clear() {
8767
- this._team.set(null);
8768
- this._error.set(null);
7787
+ manifestEntryToDefinition(entry) {
7788
+ return {
7789
+ type: entry.type,
7790
+ name: entry.name,
7791
+ description: entry.description,
7792
+ category: entry.category,
7793
+ icon: entry.icon,
7794
+ props: entry.props,
7795
+ slots: entry.slots,
7796
+ isSection: entry.isSection,
7797
+ isBlock: entry.isBlock,
7798
+ blockScope: entry.blockScope,
7799
+ sectionTypes: entry.sectionTypes,
7800
+ draggable: entry.draggable,
7801
+ deletable: entry.deletable,
7802
+ duplicable: entry.duplicable,
7803
+ tags: entry.tags,
7804
+ order: entry.order,
7805
+ // No `component` — rendering happens in the iframe
7806
+ };
8769
7807
  }
8770
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ShopTeamService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
8771
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ShopTeamService, providedIn: 'root' });
7808
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ManifestLoaderService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
7809
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ManifestLoaderService, providedIn: 'root' });
8772
7810
  }
8773
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ShopTeamService, decorators: [{
7811
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ManifestLoaderService, decorators: [{
8774
7812
  type: Injectable,
8775
7813
  args: [{ providedIn: 'root' }]
8776
7814
  }] });
8777
7815
 
8778
- const SHOPIFY_API_VERSION = '2026-01';
8779
- const SHOPIFY_CONFIG = new InjectionToken('SHOPIFY_CONFIG');
8780
-
8781
- const authGuard = async () => {
8782
- const authService = inject(SupabaseAuthService);
8783
- const router = inject(Router);
8784
- await authService.ensureInitialized();
8785
- if (authService.isAuthenticated()) {
8786
- return true;
8787
- }
8788
- router.navigate(['/login']);
8789
- return false;
8790
- };
8791
- const noAuthGuard = async () => {
8792
- const authService = inject(SupabaseAuthService);
8793
- const router = inject(Router);
8794
- await authService.ensureInitialized();
8795
- if (!authService.isAuthenticated()) {
8796
- return true;
8797
- }
8798
- router.navigate(['/admin']);
8799
- return false;
8800
- };
8801
-
8802
- const licenseGuard = async () => {
8803
- const shopAuthService = inject(ShopAuthService);
8804
- const router = inject(Router);
8805
- await shopAuthService.authenticate();
8806
- if (shopAuthService.hasAccess()) {
8807
- return true;
8808
- }
8809
- router.navigate(['/activate']);
8810
- return false;
8811
- };
8812
- const noLicenseGuard = async () => {
8813
- const shopAuthService = inject(ShopAuthService);
8814
- const router = inject(Router);
8815
- await shopAuthService.authenticate();
8816
- if (!shopAuthService.hasAccess()) {
8817
- return true;
8818
- }
8819
- router.navigate(['/admin']);
8820
- return false;
8821
- };
8822
-
8823
- /**
8824
- * Guard que verifica:
8825
- * 1. Usuario autenticado
8826
- * 2. Usuario tiene acceso a la shop del dominio actual
8827
- * 3. Licencia activa
8828
- *
8829
- * Redirige a:
8830
- * - /login si no está autenticado
8831
- * - /no-access si no tiene acceso a la shop
8832
- * - /license-expired si la licencia expiró
8833
- */
8834
- const shopAuthGuard = async () => {
8835
- const authService = inject(SupabaseAuthService);
8836
- const shopAuthService = inject(ShopAuthService);
8837
- const router = inject(Router);
8838
- // Esperar inicialización del auth
8839
- await authService.ensureInitialized();
8840
- // Verificar autenticación básica
8841
- if (!authService.isAuthenticated()) {
8842
- router.navigate(['/login']);
8843
- return false;
8844
- }
8845
- // Autenticar contra la shop
8846
- await shopAuthService.authenticate();
8847
- // Verificar acceso
8848
- if (shopAuthService.hasAccess()) {
8849
- return true;
8850
- }
8851
- // Manejar diferentes tipos de error
8852
- const error = shopAuthService.error();
8853
- if (error?.code === 'NOT_AUTHENTICATED') {
8854
- router.navigate(['/login']);
8855
- return false;
8856
- }
8857
- if (error?.code === 'LICENSE_EXPIRED') {
8858
- router.navigate(['/license-expired']);
8859
- return false;
8860
- }
8861
- if (error?.code === 'NO_ACCESS' || error?.code === 'SHOP_NOT_FOUND') {
8862
- router.navigate(['/no-access']);
8863
- return false;
8864
- }
8865
- if (error?.code === 'LOCALHOST_NO_SHOP') {
8866
- alert('Add ?shop=your-store.myshopify.com to the URL');
8867
- return false;
8868
- }
8869
- if (error?.code === 'USER_INACTIVE') {
8870
- router.navigate(['/account-inactive']);
8871
- return false;
8872
- }
8873
- // Error desconocido, redirigir a no-access
8874
- router.navigate(['/no-access']);
8875
- return false;
8876
- };
8877
- /**
8878
- * Guard que solo permite acceso si el usuario NO tiene acceso a una shop.
8879
- * Útil para páginas como /login que no deberían ser accesibles si ya tienes acceso.
8880
- */
8881
- const noShopAuthGuard = async () => {
8882
- const authService = inject(SupabaseAuthService);
8883
- const shopAuthService = inject(ShopAuthService);
8884
- const router = inject(Router);
8885
- await authService.ensureInitialized();
8886
- // Si no está autenticado, permitir (puede ver login)
8887
- if (!authService.isAuthenticated()) {
8888
- return true;
8889
- }
8890
- // Verificar acceso a shop
8891
- await shopAuthService.authenticate();
8892
- // Si tiene acceso completo, redirigir al admin
8893
- if (shopAuthService.hasAccess()) {
8894
- router.navigate(['/admin']);
8895
- return false;
8896
- }
8897
- // No tiene acceso completo, permitir ver la página
8898
- return true;
8899
- };
8900
- /**
8901
- * Guard que verifica que el usuario tenga rol de admin o owner.
8902
- * Requiere que shopAuthGuard se haya ejecutado primero.
8903
- */
8904
- const shopAdminGuard = async () => {
8905
- const shopAuthService = inject(ShopAuthService);
8906
- const router = inject(Router);
8907
- // Asegurar que ya se autenticó
8908
- if (!shopAuthService.initialized()) {
8909
- await shopAuthService.authenticate();
8910
- }
8911
- if (shopAuthService.isAdmin()) {
8912
- return true;
8913
- }
8914
- // No es admin, redirigir a admin (sin permisos de gestión)
8915
- router.navigate(['/admin']);
8916
- return false;
8917
- };
8918
- /**
8919
- * Guard que verifica que el usuario pueda editar (owner o admin).
8920
- * Los readers solo pueden ver, no editar.
8921
- */
8922
- const shopEditorGuard = async () => {
8923
- const shopAuthService = inject(ShopAuthService);
8924
- const router = inject(Router);
8925
- if (!shopAuthService.initialized()) {
8926
- await shopAuthService.authenticate();
8927
- }
8928
- if (shopAuthService.canEdit()) {
8929
- return true;
8930
- }
8931
- // Es reader, mostrar página de solo lectura o redirigir
8932
- router.navigate(['/admin']);
8933
- return false;
8934
- };
8935
-
8936
7816
  class VisualEditor {
8937
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: VisualEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
8938
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.0.6", type: VisualEditor, isStandalone: true, selector: "lib-visual-editor", ngImport: i0, template: `
7817
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: VisualEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
7818
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.0", type: VisualEditor, isStandalone: true, selector: "lib-visual-editor", ngImport: i0, template: `
8939
7819
  <p>
8940
7820
  visual-editor works!
8941
7821
  </p>
8942
7822
  `, isInline: true, styles: [""] });
8943
7823
  }
8944
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: VisualEditor, decorators: [{
7824
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: VisualEditor, decorators: [{
8945
7825
  type: Component,
8946
7826
  args: [{ selector: 'lib-visual-editor', imports: [], template: `
8947
7827
  <p>
@@ -8959,5 +7839,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
8959
7839
  * Generated bundle index. Do not edit.
8960
7840
  */
8961
7841
 
8962
- export { ActivateLicenseComponent, AppShellComponent, BlockTreeItemComponent, ComponentRegistryService, DEFAULT_ROUTER_NAVIGATION_CONFIG, DEFAULT_VISUAL_EDITOR_CONFIG, DELETE_METAFIELDS_MUTATION, DefaultRouterNavigationService, DragDropService, DynamicRendererComponent, EDITOR_COMPONENT_DEFINITIONS, FILES_QUERY, GET_SHOP_ID_QUERY, GET_SHOP_METAFIELD_QUERY, INDEX_KEY, InputPageLoadingStrategy, LicenseExpiredComponent, LicenseGuardComponent, LoginComponent, MAX_METAFIELD_SIZE, NAMESPACE, NoAccessComponent, PAGE_ENVIRONMENT, PageLoadingStrategy, PageManagerComponent, PageService, PageShopifyRepository, ROUTER_NAVIGATION_CONFIG, RoutePageLoadingStrategy, SET_METAFIELD_MUTATION, SHOPIFY_API_VERSION, SHOPIFY_APP_CONFIG, SHOPIFY_CONFIG, STORE_DOMAIN, SUPABASE_CONFIG, ShopAuthService, ShopTeamService, ShopifyFilePickerComponent, ShopifyFilesService, ShopifyGraphQLClient, ShopifyMetafieldRepository, ShopifyTokenService, SlotRendererComponent, SupabaseAuthService, USE_IN_MEMORY_PAGES, VISUAL_EDITOR_CONFIG, VISUAL_EDITOR_FEATURE_KEY, VisualEditor, VisualEditorActions, VisualEditorComponent, VisualEditorFacade, VisualEditorNavigation, authGuard, initialVisualEditorState, licenseGuard, noAuthGuard, noLicenseGuard, noShopAuthGuard, provideEditorComponents, provideVisualEditor, provideVisualEditorStore, selectBlocksForSection, selectBlocksForSlot, selectCanRedo, selectCanUndo, selectCurrentPage, selectCurrentPageId, selectCurrentPageSlug, selectCurrentPageStatus, selectCurrentPageTitle, selectCurrentPageVersion, selectDraggedElementId, selectElementById, selectHistory, selectHistoryIndex, selectHistoryLength, selectIsDirty, selectIsDragging, selectIsPageLoaded, selectLastAction, selectSectionById, selectSections, selectSelectedBlock, selectSelectedBlockSlotName, selectSelectedElement, selectSelectedElementId, selectSelectedElementType, selectSelectedSection, selectSelectedSectionId, selectSelectedSectionType, selectVisualEditorState, shopAdminGuard, shopAuthGuard, shopEditorGuard, visualEditorReducer };
7842
+ export { BlockTreeItemComponent, CREATE_METAFIELD_DEFINITION_MUTATION, ComponentRegistryService, DEFAULT_ROUTER_NAVIGATION_CONFIG, DEFAULT_VISUAL_EDITOR_CONFIG, DELETE_METAFIELDS_MUTATION, DELETE_METAFIELD_DEFINITION_MUTATION, DefaultRouterNavigationService, DragDropService, DynamicRendererComponent, EDITOR_COMPONENT_DEFINITIONS, FILES_QUERY, GET_METAFIELD_DEFINITION_QUERY, GET_SHOP_ID_QUERY, GET_SHOP_METAFIELD_QUERY$1 as GET_SHOP_METAFIELD_QUERY, INDEX_KEY$1 as INDEX_KEY, IframeBridgeService, InputPageLoadingStrategy, MAX_METAFIELD_SIZE, ManifestLoaderService, NAMESPACE$1 as NAMESPACE, PageLoadingStrategy, PageManagerComponent, PageService, PageShopifyRepository, PageStorefrontRepository, ROUTER_NAVIGATION_CONFIG, RoutePageLoadingStrategy, SET_METAFIELD_MUTATION, SHOPIFY_CONFIG, STOREFRONT_CONFIG, STOREFRONT_URL, ShopifyFilePickerComponent, ShopifyFilesService, ShopifyGraphQLClient, ShopifyMetafieldRepository, SlotRendererComponent, StorefrontGraphQLClient, StorefrontMetafieldRepository, USE_IN_MEMORY_PAGES, VISUAL_EDITOR_CONFIG, VISUAL_EDITOR_FEATURE_KEY, VisualEditor, VisualEditorActions, VisualEditorComponent, VisualEditorFacade, VisualEditorNavigation, initialVisualEditorState, provideEditorComponents, provideVisualEditor, provideVisualEditorStore, selectBlocksForSection, selectBlocksForSlot, selectCanRedo, selectCanUndo, selectCurrentPage, selectCurrentPageId, selectCurrentPageSlug, selectCurrentPageStatus, selectCurrentPageTitle, selectCurrentPageVersion, selectDraggedElementId, selectElementById, selectHistory, selectHistoryIndex, selectHistoryLength, selectIsDirty, selectIsDragging, selectIsPageLoaded, selectLastAction, selectSectionById, selectSections, selectSelectedBlock, selectSelectedBlockSlotName, selectSelectedElement, selectSelectedElementId, selectSelectedElementType, selectSelectedSection, selectSelectedSectionId, selectSelectedSectionType, selectVisualEditorState, visualEditorReducer };
8963
7843
  //# sourceMappingURL=kustomizer-visual-editor.mjs.map