Glyph Persistence: Visual Sync State System

Status: Planning Priority: Critical for mobile/offline usage Scope: Canvas workspace visual feedback for connectivity and sync state

Problem Statement

Users working with glyphs on mobile devices with spotty network connectivity need real-time visual feedback about:

  1. Current connectivity state (online/offline)
  2. Which glyphs are synced to backend vs local-only
  3. What's happening during drag/modification operations
  4. Whether data is at risk of loss

Current implementation syncs glyphs to backend but provides no visual feedback. This creates uncertainty, especially on mobile where network state is unpredictable.

Design Philosophy Alignment

This proposal aligns with QNTX design philosophy:

Visual Language System

Two Distinct Modes

Offline/Local-Only Mode (Optimized for mobile/small screens):

Online/Connected Mode (Optimized for desktop/reliable connection):

Glyph Visual States

Offline Mode States

  1. Unmodified offline glyph

  2. Being modified offline (during drag)

  3. Modified but not synced (after drop, still offline)

Online Mode States

  1. Unsynced glyph (online but not yet synced)

  2. Syncing glyph

  3. Synced glyph (successfully persisted to backend)

Transition Behavior

When connectivity changes offline → online:

When connectivity changes online → offline:

Technical Implementation

1. Connectivity Detection

File: web/ts/connectivity.ts (new)

export type ConnectivityState = 'online' | 'offline';

export interface ConnectivityManager {
    state: ConnectivityState;
    subscribe(callback: (state: ConnectivityState) => void): () => void;
}

Detection strategy:

Why both sources: navigator.onLine can report false positives. WebSocket state is ground truth for QNTX backend connectivity.

Why equal debounce: Consistent 300ms delay prevents UI flapping during unstable connections, applies uniformly to both online→offline and offline→online transitions.

2. Sync State Tracking

File: web/ts/state/sync-state.ts (new)

export type GlyphSyncState =
    | 'unsynced'    // Never sent to backend
    | 'syncing'     // Request in flight
    | 'synced'      // Confirmed by backend
    | 'failed';     // Sync attempt failed

export interface SyncStateManager {
    getState(glyphId: string): GlyphSyncState;
    setState(glyphId: string, state: GlyphSyncState): void;
    subscribe(glyphId: string, callback: (state: GlyphSyncState) => void): () => void;
}

Integration points:

3. Visual Mode System

File: web/ts/visual-mode.ts (new)

Manages CSS custom property updates for mode switching.

CSS Custom Properties (add to web/css/variables.css):

:root {
    /* Online mode (default) */
    --mode-bg-lightness: 15%;
    --mode-glyph-saturation: 100%;
    --mode-glyph-boost: 1.0;

    /* Transition timing */
    --mode-transition-duration: 1.5s;
    --mode-transition-timing: cubic-bezier(0.4, 0.0, 0.2, 1);
}

:root[data-connectivity-mode="offline"] {
    --mode-bg-lightness: 25%;
    --mode-glyph-saturation: 20%;
    --mode-glyph-boost: 0.8;
}

Glyph state classes:

.canvas-glyph {
    filter: saturate(var(--mode-glyph-saturation));
    transition: filter var(--mode-transition-duration) var(--mode-transition-timing);
}

.canvas-glyph[data-sync-state="synced"] {
    filter: saturate(calc(var(--mode-glyph-saturation) * var(--mode-glyph-boost)));
}

.canvas-glyph.is-dragging[data-connectivity-mode="offline"] {
    filter: saturate(10%); /* Extra desaturated while dragging offline */
}

4. Glyph Component Integration

Files to modify:

Changes needed:

  1. Subscribe to connectivity state
  2. Apply data-connectivity-mode attribute to glyph elements
  3. Apply data-sync-state attribute based on sync state
  4. Add/remove .is-dragging class during drag operations
  5. Update sync state after successful/failed API calls

5. Offline Queue System

File: web/ts/offline-queue.ts (new)

export interface QueuedOperation {
    id: string;
    type: 'upsert_glyph' | 'delete_glyph' | 'upsert_composition' | 'delete_composition';
    payload: any;
    timestamp: number;
    retryCount: number;
}

export interface OfflineQueue {
    enqueue(operation: QueuedOperation): void;
    processQueue(): Promise<void>;
    getQueueSize(): number;
}

Behavior:

6. Server-Side Result Visibility

Files to modify:

Behavior:

Implementation Phases

Phase 1: Foundation (Current PR)

Phase 2: Visual System

Phase 3: Offline Support (#431)

Phase 4: Polish & Testing

Success Criteria

  1. Functional: User can work offline and changes sync when online
  2. Perceptible: Mode change is immediately obvious without conscious analysis
  3. Performant: No jank during transitions or mode switches
  4. Reliable: No data loss even with unstable connections
  5. Mobile-optimized: Readable and usable on small screens in poor connectivity

Open Questions for Implementation

Question 1: Diagonal Stripe Pattern Implementation

Should the diagonal stripe pattern be:

Question 2: Connectivity State Debouncing

For the 300ms debounce on connectivity state changes:

Question 3: Failed Sync Visual Treatment

When a glyph fails to sync (after retries exhausted):

Technical Constraints

  1. No new dependencies: Use native browser APIs and existing libraries only
  2. Performance budget: Mode transitions must complete in <16ms (60fps)
  3. Mobile-first: Test on real devices, not just simulators
  4. Accessibility: Color is not the only indicator - consider screen readers

References