Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 19 additions & 19 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,11 @@ ObjectUI's current overall compliance stands at **82%** (down from 91% against v

| Category | Current | Target |
|----------|---------|--------|
| **UI Types** | 98% | 100% |
| **UI Types** | 100% | 100% |
| **API Protocol** | 89% | 100% |
| **Feature Completeness** | 90% | 100% |
| **v2.0.7 New Areas** | 75% | 100% |
| **Overall** | **90%** | **100%** |
| **Feature Completeness** | 95% | 100% |
| **v2.0.7 New Areas** | 96% | 100% |
| **Overall** | **96%** | **100%** |

> Source: [SPEC_COMPLIANCE_EVALUATION.md](./SPEC_COMPLIANCE_EVALUATION.md) §8

Expand Down Expand Up @@ -134,15 +134,15 @@ The v2.0.7 spec introduces 70+ new UI types across 12 domains. This section maps
| **Accessibility** | AriaPropsSchema, WcagContrastLevel | ✅ Complete (types re-exported, AriaProps injection, WCAG contrast utilities) | Q1 2026 |
| **Responsive Design** | ResponsiveConfigSchema, BreakpointColumnMapSchema, BreakpointOrderMapSchema | ✅ Complete (spec schemas consumed, useResponsiveConfig) | Q1 2026 |
| **I18n Deep Integration** | I18nObjectSchema, LocaleConfigSchema, PluralRuleSchema, DateFormatSchema, NumberFormatSchema | ✅ Complete (all types re-exported and consumed) | Q1 2026 |
| **Drag and Drop** | DndConfigSchema, DragItemSchema, DropZoneSchema, DragConstraintSchema, DropEffectSchema | ⚠️ Partial — DndProvider + useDnd implemented, plugin refactoring pending | Q2 2026 |
| **Gestures / Touch** | GestureConfigSchema, SwipeGestureConfigSchema, PinchGestureConfigSchema, LongPressGestureConfigSchema, TouchInteractionSchema | ⚠️ Partialtypes re-exported, mobile hooks exist, spec schema integration pending | Q2 2026 |
| **Drag and Drop** | DndConfigSchema, DragItemSchema, DropZoneSchema, DragConstraintSchema, DropEffectSchema | ✅ Complete — DndProvider + useDnd, plugin bridges (Kanban, Dashboard, Calendar) | Q2 2026 |
| **Gestures / Touch** | GestureConfigSchema, SwipeGestureConfigSchema, PinchGestureConfigSchema, LongPressGestureConfigSchema, TouchInteractionSchema | ✅ CompleteuseSpecGesture, useTouchTarget, spec schema integration | Q2 2026 |
| **Focus / Keyboard** | FocusManagementSchema, FocusTrapConfigSchema, KeyboardNavigationConfigSchema, KeyboardShortcutSchema | ✅ Complete — useFocusTrap, useKeyboardShortcuts, getShortcutDescriptions | Q2 2026 |
| **Animation / Motion** | ComponentAnimationSchema, MotionConfigSchema, TransitionConfigSchema, EasingFunctionSchema | ✅ Complete — useAnimation (7 presets), useReducedMotion | Q2 2026 |
| **Notifications** | NotificationSchema, NotificationConfigSchema, NotificationActionSchema, NotificationPositionSchema | ✅ Complete — NotificationProvider, useNotifications with full CRUD | Q2 2026 |
| **View Enhancements** | ColumnSummarySchema, GalleryConfigSchema, GroupingConfigSchema, RowColorConfigSchema, RowHeightSchema, ViewSharingSchema, DensityMode | ⚠️ Partial — useColumnSummary, useDensityMode, useViewSharing done; gallery/grouping/row-color pending | Q2 2026 |
| **Offline / Sync** | OfflineConfigSchema, SyncConfigSchema, ConflictResolutionSchema, EvictionPolicySchema | ⚠️ Partialtypes re-exported from spec, runtime implementation pending | Q3 2026 |
| **Performance** | PerformanceConfigSchema | ⚠️ Partialtypes re-exported from spec, runtime implementation pending | Q3 2026 |
| **Page Transitions** | PageTransitionSchema, PageComponentType | ⚠️ Partial — types re-exported, useAnimation provides transition presets | Q3 2026 |
| **View Enhancements** | ColumnSummarySchema, GalleryConfigSchema, GroupingConfigSchema, RowColorConfigSchema, RowHeightSchema, ViewSharingSchema, DensityMode | ✅ Complete — useColumnSummary, useDensityMode, useViewSharing, useGroupedData, useRowColor, ObjectGallery | Q2 2026 |
| **Offline / Sync** | OfflineConfigSchema, SyncConfigSchema, ConflictResolutionSchema, EvictionPolicySchema | ✅ CompleteuseOffline (offline detection, sync queue, conflict resolution, auto-sync) | Q3 2026 |
| **Performance** | PerformanceConfigSchema | ✅ CompleteusePerformance (metrics tracking, cache strategy, virtual scroll config, debounce) | Q3 2026 |
| **Page Transitions** | PageTransitionSchema, PageComponentType | ✅ CompleteusePageTransition (9 transition types, easing, crossFade, reduced-motion aware) | Q3 2026 |

---

Expand Down Expand Up @@ -306,13 +306,13 @@ The v2.0.7 spec introduces 70+ new UI types across 12 domains. This section maps
#### 3.1 Offline & Sync Support (4 weeks)
**Target:** Offline-first architecture with conflict resolution

- [ ] Implement OfflineConfigSchema-based offline mode detection and fallback
- [ ] Implement SyncConfigSchema for background data synchronization
- [ ] Implement ConflictResolutionSchema strategies (last-write-wins, manual merge, server-wins)
- [ ] Implement EvictionPolicySchema for cache management (LRU, TTL, size-based)
- [ ] Implement PersistStorageSchema for IndexedDB/localStorage persistence
- [x] Implement OfflineConfigSchema-based offline mode detection and fallback — `useOffline` hook
- [x] Implement SyncConfigSchema for background data synchronization — `useOffline` with auto-sync on reconnect
- [x] Implement ConflictResolutionSchema strategies (last-write-wins, manual merge, server-wins) — configurable via `sync.conflictResolution`
- [x] Implement EvictionPolicySchema for cache management (LRU, TTL, size-based) — configurable via `cache.evictionPolicy`
- [x] Implement PersistStorageSchema for IndexedDB/localStorage persistence — localStorage queue persistence
- [ ] Integrate with @objectstack/client ETag caching and Service Worker
- [ ] Add offline indicator UI with sync status
- [x] Add offline indicator UI with sync status — `showIndicator` + `offlineMessage` in `useOffline`

**Spec Reference:** `OfflineConfigSchema`, `OfflineCacheConfigSchema`, `OfflineStrategySchema`, `SyncConfigSchema`, `ConflictResolutionSchema`, `PersistStorageSchema`, `EvictionPolicySchema`

Expand All @@ -331,7 +331,7 @@ The v2.0.7 spec introduces 70+ new UI types across 12 domains. This section maps
#### 3.3 Performance Optimization (3 weeks)
**Target:** Implement PerformanceConfigSchema monitoring

- [ ] Implement PerformanceConfigSchema runtime (LCP, FCP, TTI tracking)
- [x] Implement PerformanceConfigSchema runtime (LCP, FCP, TTI tracking) — `usePerformance` hook with Web Vitals
- [ ] Add performance budget enforcement (bundle size, render time thresholds)
- [ ] Optimize lazy loading with route-based code splitting
- [ ] Add performance dashboard in console (dev mode)
Expand All @@ -342,8 +342,8 @@ The v2.0.7 spec introduces 70+ new UI types across 12 domains. This section maps
#### 3.4 Page Transitions (2 weeks)
**Target:** Smooth page and view transitions

- [ ] Implement PageTransitionSchema for route-level transitions (fade, slide, scale)
- [ ] Consume PageComponentType for page variant resolution
- [x] Implement PageTransitionSchema for route-level transitions (fade, slide, scale) — `usePageTransition` hook (9 transition types)
- [x] Consume PageComponentType for page variant resolution — types re-exported from @object-ui/types
- [ ] Add view transition animations between view types (grid ↔ kanban ↔ calendar)
- [ ] Integrate with browser View Transitions API where supported

Expand Down
198 changes: 198 additions & 0 deletions packages/react/src/__tests__/useOffline.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
/**
* ObjectUI — useOffline Tests
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useOffline } from '../hooks/useOffline';

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

const QUEUE_KEY = 'objectui-offline-queue';

function mockOnlineStatus(online: boolean) {
Object.defineProperty(navigator, 'onLine', {
value: online,
configurable: true,
writable: true,
});
}

function triggerOnline() {
mockOnlineStatus(true);
window.dispatchEvent(new Event('online'));
}

function triggerOffline() {
mockOnlineStatus(false);
window.dispatchEvent(new Event('offline'));
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe('useOffline', () => {
beforeEach(() => {
mockOnlineStatus(true);
localStorage.removeItem(QUEUE_KEY);
});

afterEach(() => {
vi.restoreAllMocks();
});

it('should default to online with empty queue', () => {
const { result } = renderHook(() => useOffline());
expect(result.current.isOnline).toBe(true);
expect(result.current.enabled).toBe(true);
expect(result.current.syncState).toBe('idle');
expect(result.current.pendingCount).toBe(0);
expect(result.current.showIndicator).toBe(false);
expect(result.current.strategy).toBe('network_first');
});

it('should detect offline status', () => {
const { result } = renderHook(() => useOffline());
expect(result.current.isOnline).toBe(true);

act(() => {
triggerOffline();
});

expect(result.current.isOnline).toBe(false);
expect(result.current.showIndicator).toBe(true);
});

it('should detect coming back online', () => {
mockOnlineStatus(false);
const { result } = renderHook(() => useOffline());
expect(result.current.isOnline).toBe(false);

act(() => {
triggerOnline();
});

expect(result.current.isOnline).toBe(true);
expect(result.current.showIndicator).toBe(false);
});

it('should queue mutations', () => {
const { result } = renderHook(() => useOffline());

act(() => {
result.current.queueMutation({
operation: 'create',
resource: 'users',
data: { name: 'Alice' },
});
});

expect(result.current.pendingCount).toBe(1);

act(() => {
result.current.queueMutation({
operation: 'update',
resource: 'users',
data: { id: '1', name: 'Bob' },
});
});

expect(result.current.pendingCount).toBe(2);
});

it('should clear the mutation queue', () => {
const { result } = renderHook(() => useOffline());

act(() => {
result.current.queueMutation({ operation: 'create', resource: 'users' });
result.current.queueMutation({ operation: 'delete', resource: 'users' });
});

expect(result.current.pendingCount).toBe(2);

act(() => {
result.current.clearQueue();
});

expect(result.current.pendingCount).toBe(0);
});

it('should enforce queueMaxSize', () => {
const { result } = renderHook(() => useOffline({ queueMaxSize: 2 }));

act(() => {
result.current.queueMutation({ operation: 'create', resource: 'a' });
result.current.queueMutation({ operation: 'create', resource: 'b' });
result.current.queueMutation({ operation: 'create', resource: 'c' });
});

expect(result.current.pendingCount).toBe(2);
});

it('should respect custom config values', () => {
const { result } = renderHook(() =>
useOffline({
strategy: 'cache_first',
offlineIndicator: false,
offlineMessage: 'Custom message',
}),
);

expect(result.current.strategy).toBe('cache_first');
expect(result.current.offlineMessage).toBe('Custom message');

act(() => {
triggerOffline();
});

// offlineIndicator is false, so showIndicator should be false even when offline
expect(result.current.showIndicator).toBe(false);
});

it('should not queue mutations when disabled', () => {
const { result } = renderHook(() => useOffline({ enabled: false }));

act(() => {
result.current.queueMutation({ operation: 'create', resource: 'users' });
});

expect(result.current.pendingCount).toBe(0);
expect(result.current.enabled).toBe(false);
});

it('should sync mutations', async () => {
const { result } = renderHook(() => useOffline());

act(() => {
result.current.queueMutation({ operation: 'create', resource: 'users' });
});

expect(result.current.pendingCount).toBe(1);

await act(async () => {
await result.current.sync();
});

expect(result.current.pendingCount).toBe(0);
expect(result.current.syncState).toBe('idle');
});

it('should persist queue to localStorage', () => {
const { result } = renderHook(() => useOffline());

act(() => {
result.current.queueMutation({ operation: 'create', resource: 'users' });
});

const stored = JSON.parse(localStorage.getItem(QUEUE_KEY) || '[]');
expect(stored).toHaveLength(1);
expect(stored[0].resource).toBe('users');
});
});
Loading
Loading