39948-vm/documentation/project-transition-settings.md
2026-07-03 16:11:24 +02:00

25 KiB

Project Transition Settings

Documentation for the environment-aware project-level transition settings system that cascades with global and element-level settings.

Overview

Project transition settings control the default CSS-based transition behavior for page navigation when no video transition is configured on an element. Settings are stored per-project and per-environment, following the same publishing pattern as tour_pages and project_audio_tracks.

┌─────────────────────────────────────────────────────────────────────────┐
│                    Transition Settings Cascade                           │
│                                                                          │
│  ┌─────────────────────────────────────────────────────────────────────┐│
│  │  Element Level (highest priority)                                   ││
│  │  └── ui_schema_json.elements[].transitionSettings                  ││
│  └─────────────────────────────────────────────────────────────────────┘│
│                              ↓ fallback                                  │
│  ┌─────────────────────────────────────────────────────────────────────┐│
│  │  Project Level (per environment)                                    ││
│  │  └── project_transition_settings WHERE projectId AND environment   ││
│  └─────────────────────────────────────────────────────────────────────┘│
│                              ↓ fallback                                  │
│  ┌─────────────────────────────────────────────────────────────────────┐│
│  │  Global Level (platform-wide defaults)                              ││
│  │  └── global_transition_defaults (single record)                    ││
│  └─────────────────────────────────────────────────────────────────────┘│
│                              ↓ fallback                                  │
│  ┌─────────────────────────────────────────────────────────────────────┐│
│  │  Hardcoded Fallback                                                 ││
│  │  └── type: 'fade', durationMs: 700, easing: 'ease-in-out'         ││
│  └─────────────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────────────┘

Environment-Aware Architecture

Project transition settings follow the same environment isolation pattern as tour pages:

Publishing Flow:
┌─────────────────────────────────────────────────────────────────┐
│  Constructor (dev)                                              │
│  └── project_transition_settings WHERE environment='dev'        │
│                           ↓                                     │
│                    [Save to Stage]                              │
│                           ↓                                     │
│  Stage (Preview)                                                │
│  └── project_transition_settings WHERE environment='stage'      │
│                           ↓                                     │
│                     [Publish]                                   │
│                           ↓                                     │
│  Production (Public)                                            │
│  └── project_transition_settings WHERE environment='production' │
└─────────────────────────────────────────────────────────────────┘

Database Schema

Table: project_transition_settings

Column Type Nullable Default Description
id UUID No UUIDV4 Primary key
projectId UUID No - FK to projects (CASCADE delete)
environment ENUM No - 'dev', 'stage', 'production'
source_key TEXT Yes null Tracks source record during publishing
transition_type TEXT No 'fade' CSS transition type ('fade', 'none')
duration_ms INTEGER No 700 Transition duration in milliseconds
easing TEXT No 'ease-in-out' CSS easing function
overlay_color TEXT No '#000000' Color of transition overlay
createdById UUID Yes - FK to users
updatedById UUID Yes - FK to users
createdAt TIMESTAMP No NOW() Creation timestamp
updatedAt TIMESTAMP No NOW() Last update timestamp
deletedAt TIMESTAMP Yes null Soft delete timestamp

Indexes:

  • project_transition_settings_project_env_unique - UNIQUE on (projectId, environment) WHERE deletedAt IS NULL

Constraints:

  • Only one active record per project/environment combination
  • Soft delete (paranoid: true)

API Endpoints

Authentication Model

The API uses URL-path-based public access for runtime presentations:

Endpoint Method Environment Auth Required
/project/:id/env/production GET production No (public)
/project/:id/env/dev GET dev Yes
/project/:id/env/stage GET stage Yes
/project/:id/env/* PUT/DELETE any Yes
Standard CRUD (/, /:id) all n/a Yes

This allows public presentations (/p/[slug]) to fetch production transition settings without authentication, while protecting dev/stage environments and write operations.

Write operations use the PAGE_ELEMENTS permission family because transition defaults are authored as part of the tour page/element editing surface. Global defaults and project environment upserts require UPDATE_PAGE_ELEMENTS. Environment-level reset uses DELETE to remove the project override row, but authorization treats it as an update and also requires UPDATE_PAGE_ELEMENTS rather than DELETE_PAGE_ELEMENTS.

Get Settings by Project and Environment

GET /api/project-transition-settings/project/:projectId/env/:environment
# Production: No auth required (public)
# Dev/Stage: Authorization: Bearer {token}

Response (200):

{
  "id": "uuid",
  "projectId": "project-uuid",
  "environment": "dev",
  "transition_type": "fade",
  "duration_ms": 700,
  "easing": "ease-in-out",
  "overlay_color": "#000000",
  "createdAt": "2026-05-01T12:00:00Z",
  "updatedAt": "2026-05-01T12:00:00Z"
}

Response (200) when no settings exist: null (use global defaults)

Response (401): Authentication required (for dev/stage without JWT)

Create or Update Settings

PUT /api/project-transition-settings/project/:projectId/env/:environment
Authorization: Bearer {token}
Content-Type: application/json

{
  "transition_type": "fade",
  "duration_ms": 1000,
  "easing": "ease-out",
  "overlay_color": "#1a1a1a"
}

Response (200): Created or updated settings object

Delete Settings

DELETE /api/project-transition-settings/project/:projectId/env/:environment
Authorization: Bearer {token}

Response (200): { "success": true }

Standard CRUD Endpoints (All Require Auth)

GET    /api/project-transition-settings           - List all
GET    /api/project-transition-settings/:id       - Get by ID
POST   /api/project-transition-settings           - Create
PUT    /api/project-transition-settings/:id       - Update
DELETE /api/project-transition-settings/:id       - Delete

Publishing Integration

Copy During Save to Stage / Publish

Project transition settings are copied along with tour pages and audio tracks:

// In PublishService.copyEnvironment()
const [sourcePages, sourceAudioTracks, sourceTransitionSettings] = await Promise.all([
  db.tour_pages.findAll({ where: { projectId, environment: fromEnv }, transaction }),
  db.project_audio_tracks.findAll({ where: { projectId, environment: fromEnv }, transaction }),
  db.project_transition_settings.findOne({ where: { projectId, environment: fromEnv }, transaction }),
]);

// Clean up target environment
await Promise.all([
  db.tour_pages.destroy({ where: { projectId, environment: toEnv }, transaction, force: true }),
  db.project_audio_tracks.destroy({ where: { projectId, environment: toEnv }, transaction, force: true }),
  db.project_transition_settings.destroy({ where: { projectId, environment: toEnv }, transaction, force: true }),
]);

// Copy transition settings if exists
if (sourceTransitionSettings) {
  await db.project_transition_settings.create({
    ...sanitizedData,
    projectId,
    environment: toEnv,
    source_key: sourceTransitionSettings.id,
    createdById: actorId,
    updatedById: actorId,
  }, { transaction });
}

Source Key Tracking

The source_key field tracks the lineage of copied records:

  • When copying dev → stage: stage record's source_key = dev record's id
  • When copying stage → production: production record's source_key = stage record's id

Frontend Integration

Redux Store

File: frontend/src/stores/project_transition_settings/projectTransitionSettingsSlice.ts

interface ProjectTransitionSettingsState {
  byProjectEnv: Record<string, ProjectTransitionSettingsEntity | null>;
  loading: boolean;
  loadingKeys: Record<string, boolean>;
}

// Actions - no special headers needed, backend determines public access from URL path
fetchByProjectAndEnv({ projectId, environment })
upsertByProjectAndEnv({ projectId, environment, data })
deleteByProjectAndEnv({ projectId, environment })

// Selectors
selectByProjectAndEnv(state, projectId, environment)
selectTransitionSettingsLoading(state, projectId, environment)

Global Transition Defaults Store

File: frontend/src/stores/global_transition_defaults/globalTransitionDefaultsSlice.ts

// Simple fetch - backend GET is always public
export const fetch = createAsyncThunk<GlobalTransitionDefaults, void>(
  'global_transition_defaults/fetch',
  async () => {
    const result = await axios.get('global-transition-defaults');
    return result.data;
  }
);

Type Definitions

File: frontend/src/types/transition.ts

// Database entity (snake_case)
interface ProjectTransitionSettingsEntity extends BaseEntity {
  projectId: string;
  environment: 'dev' | 'stage' | 'production';
  source_key?: string;
  transition_type: TransitionType;
  duration_ms: number;
  easing: EasingFunction;
  overlay_color: string;
}

// Cascade resolution format (camelCase)
interface ProjectTransitionSettings {
  transitionType?: TransitionType;
  durationMs?: number;
  easing?: EasingFunction;
  overlayColor?: string;
}

// Convert entity to cascade format
function entityToProjectSettings(entity): ProjectTransitionSettings | null

Hook: useTransitionSettings

File: frontend/src/hooks/useTransitionSettings.ts

Resolves final transition settings through the cascade:

const transitionSettings = useTransitionSettings({
  globalDefaults,      // From global_transition_defaults table
  projectSettings,     // From project_transition_settings (converted via entityToProjectSettings)
  elementSettings,     // From element's ui_schema_json
});

// Returns: ResolvedTransitionSettings
// { type, durationMs, easing, overlayColor, videoUrl?, reverseVideoUrl? }

Constructor Usage

File: frontend/src/pages/constructor.tsx

// Fetch dev environment settings
useEffect(() => {
  if (projectId) {
    dispatch(fetchProjectTransitionSettings({ projectId, environment: 'dev' }));
  }
}, [dispatch, projectId]);

// Select from store
const projectTransitionSettingsEntity = useAppSelector((state) =>
  projectId ? selectProjectTransitionSettings(state, projectId, 'dev') : undefined
);

// Convert for cascade resolution
const projectTransitionSettings = useMemo(
  () => entityToProjectSettings(projectTransitionSettingsEntity),
  [projectTransitionSettingsEntity]
);

Runtime Presentation Usage

File: frontend/src/components/RuntimePresentation.tsx

// Fetch environment-specific settings
useEffect(() => {
  if (project?.id) {
    dispatch(fetchProjectTransitionSettings({ projectId: project.id, environment }));
  }
}, [dispatch, project?.id, environment]);

// Use in transition settings resolution
const transitionSettings = useTransitionSettings({
  globalDefaults,
  projectSettings,  // Environment-specific
  elementSettings: currentElementTransitionSettings,  // From React state
});

Element Settings State Tracking

Both constructor.tsx and RuntimePresentation.tsx track element-level transition settings via React state:

// State for current element's transition settings
const [currentElementTransitionSettings, setCurrentElementTransitionSettings] =
  useState<ElementTransitionSettings | null>(null);

Extracting settings from clicked elements:

import { extractElementTransitionSettings } from '../types/transition';

// In click handler, extract settings from the navigation element
const elementSettings = extractElementTransitionSettings(clickedElement);

The extractElementTransitionSettings() function converts element fields to ElementTransitionSettings format:

  • Only includes fields with actual values (not empty strings)
  • Allows cascade to fall through when element uses "Use Project Default"
  • Handles type coercion for transitionType and transitionEasing

Critical: React Timing Fix with flushSync

Problem: React's async state batching can cause transitions to start with OLD settings values.

When a user clicks a navigation button, this sequence happens:

  1. setCurrentElementTransitionSettings(elementSettings) is called
  2. switchToPage() starts the transition
  3. useTransitionSettings hook resolves final settings

Without synchronization, step 2 can execute before React processes step 1, causing the transition to use stale settings.

Solution: Use flushSync from react-dom to force synchronous state updates:

import { flushSync } from 'react-dom';

// In handleElementClick:
const elementSettings = extractElementTransitionSettings(clickedElement);

// Force synchronous state update BEFORE navigation starts
flushSync(() => {
  setCurrentElementTransitionSettings(elementSettings);
});

// Now transition will use the correct settings
switchToPage(targetPageId, transitionConfig);

Files affected:

  • frontend/src/pages/constructor.tsx
  • frontend/src/components/RuntimePresentation.tsx

TourFlowManager (Settings UI)

File: frontend/src/components/TourFlowManager.tsx

The TourFlowManager component provides a UI for editing project transition settings in the dev environment:

// Fetch and display current settings
const projectTransitionSettingsEntity = useAppSelector((state) =>
  selectedProjectId ? selectByProjectAndEnv(state, selectedProjectId, 'dev') : undefined
);

// Save changes
const handleSaveTransitionSettings = async () => {
  await dispatch(upsertByProjectAndEnv({
    projectId: selectedProjectId,
    environment: 'dev',
    data: {
      transition_type: localTransitionType,
      duration_ms: localDurationMs,
      easing: localEasing,
      overlay_color: localOverlayColor,
    },
  }));
};

Transition Types

Type CSS Behavior Description
fade opacity 0→1 Smooth crossfade between pages
none No animation Instant page switch

Easing Functions

Value CSS equivalent Description
linear linear Constant speed
ease ease Standard easing
ease-in ease-in Slow start
ease-out ease-out Slow end
ease-in-out ease-in-out Slow start and end

Video Transition Priority

When an element has a transitionVideoUrl, the video transition always takes precedence over CSS-based transitions:

if (elementSettings?.transitionVideoUrl) {
  return {
    type: 'video',
    durationMs: elementSettings.transitionDurationMs ?? FALLBACK_DEFAULTS.durationMs,
    easing: elementSettings.transitionEasing ?? FALLBACK_DEFAULTS.easing,
    overlayColor: /* cascade through element → project → global */,
    videoUrl: elementSettings.transitionVideoUrl,
    reverseVideoUrl: elementSettings.reverseVideoUrl,
  };
}

File References

File Purpose
backend/src/db/migrations/20260501000002-create-project-transition-settings.js Database migration
backend/src/db/models/project_transition_settings.js Sequelize model
backend/src/db/api/project_transition_settings.ts Database API layer
backend/src/services/project_transition_settings.ts Service layer
backend/src/routes/project_transition_settings.ts REST endpoints
backend/src/services/publish.ts Publishing integration
frontend/src/stores/project_transition_settings/projectTransitionSettingsSlice.ts Redux store
frontend/src/types/transition.ts TypeScript definitions
frontend/src/hooks/useTransitionSettings.ts Cascade resolution hook
frontend/src/components/TourFlowManager.tsx Settings UI
frontend/src/pages/constructor.tsx Constructor integration
frontend/src/components/RuntimePresentation.tsx Runtime integration

Troubleshooting

Settings Not Applying

  1. Verify settings exist: GET /api/project-transition-settings/project/:id/env/:environment
  2. Check cascade order - element settings override project settings
  3. Ensure correct environment is being queried (dev/stage/production)

Settings Not Publishing

  1. Check publish event completed successfully
  2. Verify source environment has settings before publish
  3. Check source_key in target environment record

Missing Settings After Project Creation

New projects don't automatically get project-level settings. They use:

  1. Global defaults (if configured)
  2. Hardcoded fallback (fade, 700ms, ease-in-out)

To set project defaults, use TourFlowManager or API.


Slide Transitions (Gallery/Carousel)

Slide transitions for Gallery and Carousel elements cascade from page transition settings, with optional element-level overrides.

Cascade Flow

┌─────────────────────────────────────────────────────────────────────────┐
│                    Slide Transition Cascade                              │
│                                                                          │
│  ┌──────────────────────────────────────────────────────────────────┐  │
│  │  Element Override (highest priority)                              │  │
│  │  └── gallerySlideTransition* / carouselSlideTransition* fields   │  │
│  └────────────────────────────┬─────────────────────────────────────┘  │
│                               ↓ fallback                                │
│  ┌──────────────────────────────────────────────────────────────────┐  │
│  │  Page Transition Settings (resolved via useTransitionSettings)    │  │
│  │  └── Global → Project cascade (see above)                        │  │
│  └────────────────────────────┬─────────────────────────────────────┘  │
│                               ↓ fallback                                │
│  ┌──────────────────────────────────────────────────────────────────┐  │
│  │  Hardcoded Fallback                                               │  │
│  │  └── type: 'fade', durationMs: 700, easing: 'ease-in-out',       │  │
│  │      overlayColor: '#000000'                                      │  │
│  └──────────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────────┘

Element Override Fields

Stored in tour_pages.ui_schema_json.elements[]:

Carousel:

Field Type Description
carouselSlideTransitionType 'fade' | 'none' | '' Transition type ('' = use default)
carouselSlideTransitionDurationMs number | '' Duration in ms
carouselSlideTransitionEasing EasingFunction | '' CSS easing
carouselSlideTransitionOverlayColor string Overlay color (hex)

Gallery:

Field Type Description
gallerySlideTransitionType 'fade' | 'none' | '' Transition type ('' = use default)
gallerySlideTransitionDurationMs number | '' Duration in ms
gallerySlideTransitionEasing EasingFunction | '' CSS easing
gallerySlideTransitionOverlayColor string Overlay color (hex)

Slide vs Page Transition Differences

Aspect Page Transitions Slide Transitions
Video type Supported Maps to 'fade' (no video between slides)
Overlay color Yes (fade through overlay) Yes (same behavior)
Duration Configurable Inherits from page or element override
Element override Navigation element Gallery/Carousel element

Implementation Files

File Purpose
frontend/src/lib/resolveSlideTransition.ts Cascade resolver + extractors
frontend/src/hooks/useSlideTransition.ts Animation state management hook
frontend/src/components/UiElements/elements/CarouselElement.tsx Carousel with crossfade
frontend/src/components/UiElements/GalleryCarouselOverlay.tsx Gallery/Info Panel fullscreen media overlay with crossfade
frontend/src/components/ElementSettings/EffectsSettingsSectionCompact.tsx Element editor UI

Animation Mechanism

The useSlideTransition hook implements fade-through-overlay animation:

Phase 1: Fade Out (half duration)
├── overlayOpacity: 0 → 1
└── slideOpacity: 1 → 0

Phase 2: Swap (at midpoint)
└── displayIndex switches to new slide

Phase 3: Fade In (half duration)
├── overlayOpacity: 1 → 0
└── slideOpacity: 0 → 1

Backwards Compatibility

Existing elements without slide transition fields automatically cascade to page transition settings or hardcoded fallbacks. No database migration required - fields are stored in JSON column.

Behavior for existing projects:

  • Elements with page transitions configured → inherits those settings
  • Elements with no page transitions → uses hardcoded fallback (fade, 700ms)