# UI Elements Complete documentation for the Tour Builder Platform's UI Elements system including configuration, settings, and the three-tier scope hierarchy. ## Overview The platform implements a three-tier UI Elements defaults hierarchy: 1. **Global Scope (element_type_defaults)** - Platform-wide default configurations for element types 2. **Project Scope (project_element_defaults)** - Project-specific settings snapshots/overrides 3. **Instance Scope (tour_pages.ui_schema_json)** - Page-specific element instances with custom settings **Note:** Page-specific element instances are stored directly in `tour_pages.ui_schema_json`, not in a separate table. **Write contract:** `tour_pages` create/update requests validate `ui_schema_json` as a top-level JSON object. Valid object payloads must either contain `elements` as an array or omit it; omitted `elements` is normalized to `elements: []` before saving. JSON strings are accepted for legacy callers, parsed by the backend service, and rejected if the parsed value is not an object or if `elements` is not an array. The project does not currently use a schema/version field, derived metadata tables, or normalized page-element tables; asset usage and preload discovery continue to use existing extraction helpers. ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ UI Elements Architecture │ │ │ │ ┌──────────────────────────────────────────────────────────────────┐ │ │ │ Global Scope (element_type_defaults) │ │ │ │ System-wide default configurations │ │ │ │ │ │ │ │ 12 predefined element types with default settings: │ │ │ │ • navigation_next • navigation_prev • spot • description │ │ │ │ • gallery • carousel • logo • video_player │ │ │ │ • audio_player • popup • info_panel │ │ │ └───────────────────────────┬──────────────────────────────────────┘ │ │ │ │ │ (auto-snapshot on project creation) │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────────────┐ │ │ │ Project Scope (project_element_defaults) │ │ │ │ Project-specific overrides │ │ │ │ │ │ │ │ • Auto-snapshotted from global defaults on project creation │ │ │ │ • Can be customized per project │ │ │ │ • "Reset to Global" restores original values │ │ │ │ • Tracks source_element_id for "Check for Updates" │ │ │ └───────────────────────────┬──────────────────────────────────────┘ │ │ │ │ │ (applies defaults to new elements) │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────────────┐ │ │ │ Page Elements (tour_pages.ui_schema_json) │ │ │ │ Page-specific element instances │ │ │ │ │ │ │ │ • Stored inline in tour_pages.ui_schema_json.elements[] │ │ │ │ • Unlimited instances per page │ │ │ │ • Custom position, styling, content │ │ │ │ • Navigation uses targetPageSlug (not IDs) │ │ │ └──────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ┌───────────────┴───────────────┐ │ │ ▼ ▼ │ │ ┌────────────────┐ ┌────────────────┐ │ │ │ Constructor │ │ Runtime │ │ │ │ (Edit mode) │ │ (Playback) │ │ │ └────────────────┘ └────────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘ ``` ## Global Scope (element_type_defaults) ### Database Model **File:** `backend/src/db/models/element_type_defaults.js` | Field | Type | Required | Default | Description | |-------|------|----------|---------|-------------| | id | UUID | Yes | UUIDv4 | Primary key | | element_type | TEXT | Yes | - | Unique identifier (e.g., 'navigation_next') | | name | TEXT | Yes | - | Human-readable name (1-255 chars) | | sort_order | INTEGER | Yes | 0 | Display order in admin UI | | is_active | VIRTUAL | - | true | Always returns true (convenience field) | | default_settings_json | TEXT | No | null | JSON-serialized default configuration | | importHash | VARCHAR(255) | No | null | Unique hash for imports | | createdAt | TIMESTAMP | Yes | Auto | Creation timestamp | | updatedAt | TIMESTAMP | Yes | Auto | Update timestamp | | deletedAt | TIMESTAMP | No | null | Soft delete timestamp | **Note:** The `default_settings_json` field is stored as `settings_json` in the database (field alias). **Indexes:** - `element_type` - Unique constraint (via column definition, creates implicit unique index) - `element_type` - Additional non-unique index (explicitly defined) - `sort_order` - For ordered queries - `deletedAt` - Soft delete queries **Relationships:** ``` element_type_defaults ├── hasMany project_element_defaults (as snapshots) ─── SET NULL on delete ├── belongsTo users (as createdBy) ─── Audit trail └── belongsTo users (as updatedBy) ─── Audit trail ``` ### Predefined Element Types **File:** `backend/src/db/api/element_type_defaults.ts` The system auto-seeds 12 default element types on first database access via `DEFAULT_ROWS`: | Element Type | Name | Sort Order | Purpose | |--------------|------|------------|---------| | navigation_next | Navigation Forward Button | 1 | Forward navigation | | navigation_prev | Navigation Back Button | 2 | Back navigation | | tooltip | Tooltip | 3 | Hover information | | description | Description | 4 | Text content block | | gallery | Gallery | 5 | Image gallery cards | | carousel | Carousel | 6 | Image slideshow | | video_player | Video Player | 7 | Embedded video | | audio_player | Audio Player | 8 | Embedded audio | | spot | Hotspot | 9 | Interactive clickable area | | logo | Logo | 10 | Brand logo display | | popup | Popup | 11 | Modal dialog | | info_panel | Info Panel | 12 | Interactive panel with images/embeds | ### Default Settings by Type #### Navigation (navigation_next, navigation_prev) ```javascript { label: 'Navigation: Forward', // or 'Navigation: Back' navLabel: 'Forward', // or 'Back' navLabelFontFamily: '', // Font key for label text (e.g., 'instrument-sans-condensed') navType: 'forward', // 'forward' | 'back' navDisabled: false, transitionReverseMode: 'auto_reverse', // 'auto_reverse' | 'separate_video' transitionDurationSec: 0.7, appearDelaySec: 0, appearDurationSec: null } ``` #### Tooltip (Deprecated) > **Note:** The Tooltip element type has been deprecated and removed from the frontend constructor. The backend defaults remain for backward compatibility with existing projects. ```javascript { label: 'Tooltip', tooltipTitle: 'Tooltip title', tooltipText: 'Tooltip text', appearDelaySec: 0, appearDurationSec: null } ``` #### Description ```javascript { label: 'Description', descriptionTitle: 'TITLE', descriptionText: '', descriptionTitleFontSize: '48px', descriptionTextFontSize: '36px', descriptionTitleFontFamily: 'inherit', descriptionTextFontFamily: 'inherit', descriptionTitleColor: '#000000', descriptionTextColor: '#4B5563', appearDelaySec: 0, appearDurationSec: null } ``` **Note:** Background color is controlled via the CSS Styles tab (`backgroundColor` property) rather than a separate description-specific field. #### Gallery ```javascript { label: 'Gallery', galleryCards: [ { imageUrl: '', title: 'Card 1', description: '' } ], appearDelaySec: 0, appearDurationSec: null } ``` #### Carousel ```javascript { label: 'Carousel', carouselSlides: [ { imageUrl: '', caption: 'Slide 1' } ], carouselPrevIconUrl: '', carouselNextIconUrl: '', carouselCaptionFontFamily: '', carouselFullWidth: false, // Button positions (percentage 0-100, for full-width mode) carouselPrevX: 5, carouselPrevY: 50, carouselNextX: 95, carouselNextY: 50, // Button dimensions (CSS values, for navigation-style rendering) carouselPrevWidth: '', carouselPrevHeight: '', carouselNextWidth: '', carouselNextHeight: '', appearDelaySec: 0, appearDurationSec: null } ``` #### Video Player ```javascript { label: 'Video Player', mediaUrl: '', mediaAutoplay: true, mediaLoop: true, mediaMuted: true, appearDelaySec: 0, appearDurationSec: null } ``` #### Audio Player ```javascript { label: 'Audio Player', mediaUrl: '', mediaAutoplay: true, mediaLoop: true, mediaMuted: false, // Note: Audio is NOT muted by default appearDelaySec: 0, appearDurationSec: null } ``` #### Spot (Hotspot) ```javascript { label: 'Hotspot', iconUrl: '', appearDelaySec: 0, appearDurationSec: null } ``` #### Logo ```javascript { label: 'Logo', iconUrl: '', backgroundImageUrl: '', appearDelaySec: 0, appearDurationSec: null } ``` #### Popup ```javascript { label: 'Popup', iconUrl: '', popupTitle: '', popupContent: '', appearDelaySec: 0, appearDurationSec: null } ``` #### Info Panel ```javascript { label: 'Info Panel', // Trigger position (width/height intentionally not set - trigger sizes based on content) // Users can explicitly set width/height if fixed dimensions are needed xPercent: 5, yPercent: 90, infoPanelTriggerFontFamily: '', infoPanelDisabled: false, infoPanelOpenByDefault: false, // Hover reveal (disabled by default) hoverReveal: false, hoverRevealInitialOpacity: '1', hoverRevealTargetOpacity: '1', hoverRevealDuration: '0.3s', // Header section infoPanelHeaderImageUrl: '', infoPanelHeaderText: '', infoPanelHeaderBackgroundColor: '', infoPanelHeaderColor: '#ffffff', infoPanelHeaderFontFamily: '', infoPanelHeaderFontSize: '24', infoPanelHeaderFontWeight: '700', infoPanelHeaderPadding: '8', infoPanelHeaderBorderRadius: '8', infoPanelHeaderTextAlign: 'center', infoPanelHeaderMinHeight: '', // Panel content panelTitle: 'Information', panelText: '', // Layout infoPanelSectionGap: '12', // Panel position & wrapper styling panelXPercent: 30, panelYPercent: 50, panelWidth: '400', panelHeight: 'auto', panelBackgroundColor: 'rgba(0, 0, 0, 0.85)', panelBorderRadius: '12', panelPadding: '20', panelBackdropBlur: '10px', panelOverlayColor: 'rgba(0, 0, 0, 0.3)', panelBorderWidth: '0', panelBorderColor: '#ffffff', panelBorderStyle: 'solid', // Title section styles panelTitleColor: '#ffffff', panelTitleFontSize: '18', panelTitleFontFamily: '', infoPanelTitleBackgroundColor: '', infoPanelTitlePadding: '4 8', infoPanelTitleFontWeight: '600', infoPanelTitleTextAlign: 'left', // Text section styles panelTextColor: '#cccccc', panelTextFontSize: '14', panelTextFontFamily: '', // Span section styles infoPanelSpanBackgroundColor: 'rgba(255, 255, 255, 0.1)', infoPanelSpanColor: '#ffffff', infoPanelSpanFontFamily: '', infoPanelSpanFontSize: '12', infoPanelSpanPadding: '4 8', infoPanelSpanBorderRadius: '6', infoPanelSpanGap: '8', // Card section styles infoPanelCardBackgroundColor: 'rgba(0, 0, 0, 0.3)', infoPanelCardBorderRadius: '8', infoPanelCardAspectRatio: '16/9', infoPanelCardMinHeight: '', infoPanelCardGap: '8', // Card title overlay styles infoPanelCardTitleBackgroundColor: 'rgba(0, 0, 0, 0.6)', infoPanelCardTitleColor: '#ffffff', infoPanelCardTitleFontFamily: '', infoPanelCardTitleFontSize: '12', infoPanelCardTitlePadding: '4 8', // Detail panel detailXPercent: 70, detailYPercent: 50, detailWidth: '500', detailHeight: '400', detailBackgroundColor: 'rgba(0, 0, 0, 0.9)', detailBorderRadius: '12', detailPadding: '12', detailCaptionFontFamily: '', detailBorderWidth: '0', detailBorderColor: '#ffffff', detailBorderStyle: 'solid', // Section instances with per-section data/settings infoPanelSections: [ { id: 'default-header', type: 'header' }, { id: 'default-title', type: 'title' }, { id: 'default-text', type: 'text' }, { id: 'default-spans', type: 'spans', columns: 3, gap: '8', spans: [] }, { id: 'default-images', type: 'images', images: [] }, ], // Media section settings infoPanelImagesPreviewHeight: '300', infoPanelImagesThumbnailSize: '80', appearDelaySec: 0, appearDurationSec: null } ``` ### Auto-Initialization **File:** `backend/src/db/api/element_type_defaults.ts` The `ensureInitialized()` method guarantees defaults exist: ```javascript static async ensureInitialized() { // Singleton pattern with promise caching if (!this.initializationPromise) { this.initializationPromise = (async () => { let count = await this.MODEL.count(); // Handle table not existing (code 42P01) // Sync table if needed if (count > 0) return; // Already initialized // Seed DEFAULT_ROWS await this.MODEL.bulkCreate( this.DEFAULT_ROWS.map(item => ({ ...this.getFieldMapping(item), createdAt: new Date(), updatedAt: new Date() })) ); })(); } await this.initializationPromise; } ``` Every API method calls `ensureInitialized()` first, making the seeding idempotent. ## Project Scope (project_element_defaults) ### Database Model **File:** `backend/src/db/models/project_element_defaults.js` | Field | Type | Required | Default | Description | |-------|------|----------|---------|-------------| | id | UUID | Yes | UUIDv4 | Primary key | | projectId | UUID (FK) | Yes | - | Reference to projects | | element_type | TEXT | Yes | - | Element type identifier | | name | TEXT | No | null | Custom display name | | sort_order | INTEGER | Yes | 0 | Display order | | settings_json | TEXT | No | null | JSON-serialized settings | | source_element_id | UUID (FK) | No | null | Reference to global default (for tracking) | | snapshot_version | INTEGER | Yes | 1 | Version for update tracking | | importHash | VARCHAR(255) | No | null | Unique import hash | | createdAt | TIMESTAMP | Yes | Auto | Creation timestamp | | updatedAt | TIMESTAMP | Yes | Auto | Update timestamp | | deletedAt | TIMESTAMP | No | null | Soft delete timestamp | **Unique Constraint:** `(projectId, element_type)` - One override per type per project **Indexes:** - `projectId` - FK lookup - `projectId, element_type` - Unique composite - `element_type` - Type filtering - `source_element_id` - Global default tracking - `deletedAt` - Soft delete queries **Relationships:** ``` project_element_defaults ├── belongsTo projects (as project) ─── CASCADE on delete ├── belongsTo element_type_defaults (as source_element) ─── SET NULL on delete ├── belongsTo users (as createdBy) ─── Audit trail └── belongsTo users (as updatedBy) ─── Audit trail ``` ### Auto-Snapshot on Project Creation When a new project is created, all global element defaults are automatically copied to the project: **File:** `backend/src/db/api/projects.ts` ```javascript static async create(options) { const project = await super.create(options); // Auto-snapshot global element defaults to project try { const Project_element_defaultsDBApi = require('./project_element_defaults.ts').default; await Project_element_defaultsDBApi.snapshotGlobalDefaults(project.id, options); } catch (error) { console.error('Failed to snapshot global element defaults:', error); // Non-fatal - project is still created } return project; } ``` **File:** `backend/src/db/api/project_element_defaults.ts` ```javascript static async snapshotGlobalDefaults(projectId, options = {}) { const globalDefaults = await Element_type_defaultsDBApi.findAll({}); const projectDefaults = await this.MODEL.bulkCreate( globalDefaults.rows.map((globalDefault) => ({ projectId, element_type: globalDefault.element_type, name: globalDefault.name, sort_order: globalDefault.sort_order, settings_json: globalDefault.default_settings_json, source_element_id: globalDefault.id, // Track source for updates snapshot_version: 1, createdById: options.currentUser?.id, updatedById: options.currentUser?.id, })), { transaction: options.transaction } ); return projectDefaults; } ``` ### Reset and Diff Operations **Reset to Global:** Restore a project's element default to the current global value ```javascript static async resetToGlobalDefault(projectId, element_type, options = {}) { // Find the global default const globalDefault = await Element_type_defaultsDBApi.findByType(element_type); if (!globalDefault) throw new ValidationError('Global default not found'); // Update project default return this.update( projectDefault.id, { settings_json: globalDefault.default_settings_json, source_element_id: globalDefault.id, snapshot_version: projectDefault.snapshot_version + 1, }, options ); } ``` **Diff from Global:** Check if project settings differ from current global ```javascript static async diffFromGlobal(projectId, element_type) { const projectDefault = await this.findByProjectAndType(projectId, element_type); const globalDefault = await Element_type_defaultsDBApi.findByType(element_type); const isDifferent = JSON.stringify(projectDefault?.settings_json) !== JSON.stringify(globalDefault?.default_settings_json); return { isDifferent, hasGlobalDefault: !!globalDefault, projectDefault, globalDefault, }; } ``` ## Page Elements (Inline Storage) ### Data Structure Page elements are stored directly in `tour_pages.ui_schema_json.elements[]`, not in a separate table. This simplifies the data model and eliminates ID remapping during publishing. **Storage Location:** `tour_pages.ui_schema_json` Constructor copy/paste is handled within this inline element array. Copy stores the selected element in a constructor-local clipboard, and Paste appends a deep clone to the active page. All settings are preserved, including position, dimensions, CSS styles, effects, media URLs, links, navigation `targetPageSlug`, and transition fields. The pasted element receives fresh instance IDs for the element itself and nested gallery, carousel, and info-panel items so the clone is independent from the source. ```typescript interface UISchemaJSON { elements: UIElement[]; } interface UIElement { id: string; // Generated UUID for this element type: string; // Element type (navigation_next, spot, etc.) name?: string; // Instance name sortOrder: number; // Z-order on page isVisible: boolean; // Visibility toggle xPercent?: number; // X position (0-100%) yPercent?: number; // Y position (0-100%) widthPercent?: number; // Width as percentage heightPercent?: number; // Height as percentage rotationDeg?: number; // Rotation in degrees // ... additional type-specific fields (iconUrl, targetPageSlug, etc.) } ``` **Standard Element Types:** ```javascript const ELEMENT_TYPES = [ 'navigation_next', // Forward navigation 'navigation_prev', // Back navigation 'spot', // Hotspot/clickable area 'description', // Text description 'gallery', // Image gallery 'carousel', // Image carousel 'logo', // Logo element 'video_player', // Video player 'audio_player', // Audio player 'popup', // Modal popup 'info_panel', // Info panel with images/embeds ]; ``` **Benefits of Inline Storage:** - No separate table to manage - Elements copied automatically with pages during publishing - Navigation uses `targetPageSlug` (consistent across environments) - No ID remapping needed ### Style JSON Structure The `style_json` field stores CSS properties: ```typescript interface StyleJson { width?: string; // e.g., '24vw' height?: string; // e.g., '8vh' minWidth?: string; maxWidth?: string; minHeight?: string; maxHeight?: string; margin?: string; // e.g., '0 auto' padding?: string; // e.g., '0.5rem 0.75rem' gap?: string; fontSize?: string; // e.g., '0.875rem' lineHeight?: string; fontWeight?: string; // e.g., '500' border?: string; // e.g., '2px solid currentColor' borderRadius?: string; // e.g., '8px' opacity?: string; // CSS opacity stored as 0..1, e.g., '0', '0.5', '0.95', '1' boxShadow?: string; // e.g., '0 4px 12px rgba(0,0,0,0.1)' display?: string; // 'block' | 'flex' | 'grid' | etc. position?: string; // 'static' | 'relative' | 'absolute' | etc. justifyContent?: string; // 'flex-start' | 'center' | etc. alignItems?: string; // 'stretch' | 'center' | etc. textAlign?: string; // 'left' | 'center' | 'right' zIndex?: string; // e.g., '10' } ``` Constructor/defaults editors display opacity as a clamped percentage from 0 to 100 and convert it back to CSS opacity from 0 to 1 before saving. Opacity belongs to the shared style defaults list, so values configured in `element_type_defaults.default_settings_json` or `project_element_defaults.settings_json` are applied when new page elements are created unless the page element overrides opacity locally. Element `zIndex` values are normalized to numbers and applied to the outer absolutely-positioned wrapper used by constructor and runtime rendering. This lets overlapping page elements stack by CSS panel value instead of only by creation/render order. ### CSS Unit Normalization (Canvas Units) Style values are automatically normalized to **Canvas Units (--cu)** for responsive scaling across all viewport sizes. The `--cu` CSS custom property represents "1 design pixel" and scales proportionally with the viewport. > **See:** [UI Adaptivity System](../frontend/docs/ui-adaptivity-system.md) for complete documentation. **Canvas Unit Scaling:** ``` At design resolution (1920×1080 on 1920×1080 viewport): --cu = 1px At 4K (3840×2160 viewport): --cu = 2px (elements render 2x larger) At half-resolution (960×540 viewport): --cu = 0.5px (elements render at half size) ``` **General Element Styles (via `elementStyles.ts`):** | Property | Conversion | Example | |----------|------------|---------| | width, minWidth, maxWidth | Canvas units | `"200"` → `"calc(200 * var(--cu, 1px))"` | | height, minHeight, maxHeight | Canvas units | `"100"` → `"calc(100 * var(--cu, 1px))"` | | borderRadius | Canvas units | `"12"` → `"calc(12 * var(--cu, 1px))"` | | fontSize | Canvas units | `"16"` → `"calc(16 * var(--cu, 1px))"` | | border | none | Complex value, preserved as-is | **Legacy Unit Conversion:** | Input | Output | |-------|--------| | `"24px"` | `"calc(24 * var(--cu, 1px))"` | | `"50vw"` | `"calc(960 * var(--cu, 1px))"` (50% of 1920) | | `"25vh"` | `"calc(270 * var(--cu, 1px))"` (25% of 1080) | | `"1.5rem"` | `"calc(24 * var(--cu, 1px))"` (1.5 × 16) | **Edge Cases Handled:** - Values already using `var(--cu)` → preserved - Complex values with spaces (e.g., `"10px 20px"`) → preserved - CSS functions (e.g., `"calc(100% - 20px)"`) → preserved - CSS variables (e.g., `"var(--spacing)"`) → preserved - Zero values → `"0"` (no unit needed) - Negative values → converted (e.g., `"-10"` → `"calc(-10 * var(--cu, 1px))"`) **JavaScript Usage:** ```typescript import { toCU } from '../lib/canvasScale'; const style = { fontSize: toCU(16), // "calc(16 * var(--cu, 1px))" padding: toCU(12), // "calc(12 * var(--cu, 1px))" borderRadius: toCU(8), // "calc(8 * var(--cu, 1px))" }; ``` ### CSS Inheritance Inheritable CSS properties (color, fontSize, fontWeight, lineHeight) cascade from parent elements to children via CSS inheritance. This enables setting styles once on the wrapper and having them apply to all child sections. **Wrapper → Child Inheritance:** ``` ┌─────────────────────────────────────────────────────────────────┐ │ Element Wrapper (General Element Styles) │ │ • color: #ffffff │ │ • fontSize: 1rem │ │ • fontWeight: 500 │ │ • lineHeight: 1.5 │ ├─────────────────────────────────────────────────────────────────┤ │ Child Section (e.g., Gallery Header) │ │ • If galleryHeaderColor is NOT set → inherits #ffffff │ │ • If galleryHeaderColor IS set → uses explicit value │ └─────────────────────────────────────────────────────────────────┘ ``` **Implementation Pattern:** ```typescript // applyIfSet - only applies value if explicitly set (allows CSS inheritance) applyIfSet(style, 'color', element.galleryHeaderColor); applyIfSet(style, 'fontSize', element.galleryHeaderFontSize, normalizeRemValue); // applyWithDefault - applies value or falls back to default (blocks inheritance) applyWithDefault(style, 'padding', element.galleryHeaderPadding, defaults.padding); ``` **Elements Supporting Inheritance:** | Element | Inheritable From Wrapper | |---------|--------------------------| | NavigationElement | color, fontSize, fontWeight | | PopupElement | color, fontSize, fontWeight | | DescriptionElement | color, fontSize, fontWeight (title & text sections) | | GalleryElement | color, fontSize, fontWeight (all sections: header, title, spans, cards) | | InfoPanelElement | color, fontSize, fontWeight (all sections: header, title, text, spans, cards) | ### Content JSON Structure The `content_json` field stores type-specific content: **Navigation:** ```typescript { iconUrl?: string; navLabel?: string; navLabelFontFamily?: string; // Font key for label text navType?: 'forward' | 'back'; navDisabled?: boolean; targetPageSlug?: string; // Slug-based navigation (consistent across environments) transitionVideoUrl?: string; transitionReverseMode?: 'auto_reverse' | 'separate_video'; reverseVideoUrl?: string; transitionDurationSec?: number; } ``` **Tooltip:** ```typescript { iconUrl?: string; tooltipTitle?: string; tooltipText?: string; tooltipTitleFontFamily?: string; tooltipTextFontFamily?: string; } ``` **Description:** ```typescript { descriptionTitle?: string; descriptionText?: string; descriptionTitleFontSize?: string; descriptionTextFontSize?: string; descriptionTitleFontFamily?: string; descriptionTextFontFamily?: string; descriptionTitleColor?: string; descriptionTextColor?: string; // Background color is controlled via CSS Styles tab (backgroundColor property) } ``` **Gallery:** ```typescript { galleryCards?: Array<{ id: string; imageUrl: string; title: string; description: string; }>; galleryHeaderImageUrl?: string; galleryTitle?: string; galleryInfoSpans?: Array<{ id: string; text: string; }>; galleryColumns?: number; galleryTitleFontFamily?: string; galleryTextFontFamily?: string; // Font for card titles, descriptions, and info spans // Gallery header dimension properties galleryHeaderWidth?: string; // e.g., "100%", "80%", "300px" galleryHeaderHeight?: string; // e.g., "200px", "20vh", "auto" galleryHeaderMinHeight?: string; // e.g., "150px" galleryHeaderMaxHeight?: string; // e.g., "400px", "50vh" // Gallery card dimension properties galleryCardWidth?: string; // e.g., "auto", "100%", "250px" galleryCardHeight?: string; // e.g., "auto", "200px" galleryCardMinHeight?: string; // e.g., "150px" (default: "40px") galleryCardAspectRatio?: string; // e.g., "4/3", "1/1", "16/9", "auto" (default: "4/3") // Gallery carousel navigation icons/positions galleryCarouselPrevIconUrl?: string; galleryCarouselNextIconUrl?: string; galleryCarouselBackIconUrl?: string; galleryCarouselBackLabel?: string; galleryCarouselPrevX?: number; galleryCarouselPrevY?: number; galleryCarouselNextX?: number; galleryCarouselNextY?: number; galleryCarouselBackX?: number; galleryCarouselBackY?: number; galleryCarouselPrevWidth?: string; galleryCarouselPrevHeight?: string; galleryCarouselNextWidth?: string; galleryCarouselNextHeight?: string; galleryCarouselBackWidth?: string; galleryCarouselBackHeight?: string; // Gallery section text alignment (default: 'center') galleryHeaderTextAlign?: 'left' | 'center' | 'right'; galleryTitleTextAlign?: 'left' | 'center' | 'right'; gallerySpanTextAlign?: 'left' | 'center' | 'right'; } ``` **Carousel:** ```typescript { carouselSlides?: Array<{ id: string; imageUrl: string; caption: string; }>; carouselPrevIconUrl?: string; carouselNextIconUrl?: string; carouselCaptionFontFamily?: string; carouselFullWidth?: boolean; // Full-width background mode // Button positions (percentage 0-100, for full-width mode) carouselPrevX?: number; carouselPrevY?: number; carouselNextX?: number; carouselNextY?: number; // Button dimensions (CSS values like '3vw', '5vh') carouselPrevWidth?: string; carouselPrevHeight?: string; carouselNextWidth?: string; carouselNextHeight?: string; } ``` **Media (Video/Audio):** ```typescript { mediaUrl?: string; mediaAutoplay?: boolean; mediaLoop?: boolean; mediaMuted?: boolean; } ``` **Info Panel:** ```typescript { // Trigger button positioning and optional sizing // xPercent, yPercent: position on canvas // width, height: optional - if not set, trigger sizes based on content (icon or text) // iconUrl: optional trigger icon infoPanelTriggerLabel?: string; infoPanelTriggerFontFamily?: string; infoPanelDisabled?: boolean; infoPanelOpenByDefault?: boolean; // Panel content panelTitle?: string; panelText?: string; // Header section infoPanelHeaderImageUrl?: string; infoPanelHeaderText?: string; // Panel position & styling panelXPercent?: number; // 0-100 panelYPercent?: number; // 0-100 panelWidth?: string; // e.g., '400' panelHeight?: string; // e.g., 'auto' panelBackgroundColor?: string; // e.g., 'rgba(0, 0, 0, 0.85)' panelBorderRadius?: string; panelPadding?: string; panelBackdropBlur?: string; panelOverlayColor?: string; // Backdrop overlay panelBorderWidth?: string; panelBorderColor?: string; panelBorderStyle?: 'none' | 'solid' | 'dashed' | 'dotted'; infoPanelSectionGap?: string; // Gap between sections // Section instances (NEW: allows multiple instances of same type) infoPanelSections?: Array<{ id: string; // Unique section ID type: 'header' | 'title' | 'text' | 'spans' | 'cards' | 'images'; columns?: number; // Grid columns (spans, cards, images) gap?: string; // Gap between items mediaOpenMode?: 'panel' | 'fullscreen'; spans?: InfoPanelInfoSpan[]; // For 'spans' sections images?: InfoPanelImage[]; // For 'cards' or 'images' sections text?: string; // For 'text' sections title?: string; // For 'title' sections headerImageUrl?: string; // For 'header' sections headerText?: string; // For 'header' sections clickAction?: 'target_page' | 'external_url'; targetPageSlug?: string; externalUrl?: string; }>; // Media section settings infoPanelSelectedImageId?: string; // Restored as selected preview image when present infoPanelImagesPreviewHeight?: string; infoPanelImagesThumbnailSize?: string; // Image Detail Panel detailXPercent?: number; detailYPercent?: number; detailWidth?: string; detailHeight?: string; detailBackgroundColor?: string; detailBorderRadius?: string; detailPadding?: string; detailCaptionFontFamily?: string; detailBorderWidth?: string; detailBorderColor?: string; detailBorderStyle?: 'none' | 'solid' | 'dashed' | 'dotted'; } ``` **Normalization:** Info Panel section instances are normalized when defaults are parsed/merged into canvas elements. Missing section IDs are regenerated, invalid section types fall back to `text`, and nested spans/images keep supported fields including `iconUrl` for 360 trigger buttons. **InfoPanelInfoSpan Structure:** ```typescript { id: string; text: string; iconUrl?: string; // Renders icon instead of text when set clickAction?: 'target_page' | 'external_url'; targetPageSlug?: string; externalUrl?: string; } ``` **InfoPanelImage Structure:** ```typescript { id: string; imageUrl?: string; // Regular image URL (storage key) videoUrl?: string; // Video URL/storage key embedUrl?: string; // 360/3D embed URL (direct URL) caption?: string; itemType?: 'image' | 'video' | '360'; iconUrl?: string; // Custom icon for 360° trigger clickAction?: 'target_page' | 'external_url'; targetPageSlug?: string; externalUrl?: string; } ``` ## Element Types Reference ### Navigation Buttons (navigation_next, navigation_prev) **Purpose:** Navigate between tour pages with optional video transitions. **Interactive Behavior:** - **Click:** Navigates to page matching `targetPageSlug` if set - **Disabled:** `navDisabled: true` suppresses navigation in constructor interact mode and runtime presentations; runtime hover/focus/active visuals are preserved - **Transition:** Plays `transitionVideoUrl` during navigation - **Reverse Mode:** Back navigation can reverse the transition video automatically (`auto_reverse`) or use a separate `reverseVideoUrl` **Runtime Rendering:** ```tsx // With icon Navigation // Without icon (text label)
{element.navLabel || (element.type === 'navigation_next' ? 'Next' : 'Back')}
``` **Key Properties:** | Property | Type | Description | |----------|------|-------------| | `iconUrl` | string | Custom button icon image | | `navLabel` | string | Text label (default: "Next"/"Back") | | `navLabelFontFamily` | string | Font key for label text (e.g., 'instrument-sans-condensed') | | `navType` | 'forward' \| 'back' | Direction for transition logic | | `navDisabled` | boolean | Disable navigation | | `targetPageSlug` | string | Destination page slug (consistent across environments) | | `transitionVideoUrl` | string | Video to play during transition | | `transitionReverseMode` | 'auto_reverse' \| 'separate_video' | Reverse playback mode | | `reverseVideoUrl` | string | Separate video for back navigation | | `transitionDurationSec` | number | Transition duration (default: 0.7) | ### Hotspot (spot) **Purpose:** Interactive clickable area for triggering actions. **Interactive Behavior:** - **Click:** Triggers navigation or custom action - **Hover:** Can display tooltip or highlight (CSS-based) **Runtime Rendering:** Uses `iconUrl` or `imageUrl` if provided, otherwise transparent clickable area. ### Description **Purpose:** Display formatted text content with title and body. **Interactive Behavior:** - **Static:** No default interactivity - **Click:** Can trigger navigation if `targetPageSlug` set **Runtime Rendering:** ```tsx // With icon Description // Without icon (text block) // Note: Background color is applied to the wrapper via CSS Styles (backgroundColor)

{descriptionTitle}

{descriptionText}

``` **Key Properties:** | Property | Type | Default | Description | |----------|------|---------|-------------| | `descriptionTitle` | string | "TITLE" | Title text | | `descriptionText` | string | "" | Body text | | `descriptionTitleFontSize` | string | "48px" | Title font size | | `descriptionTextFontSize` | string | "36px" | Body font size | | `descriptionTitleFontFamily` | string | "inherit" | Title font family | | `descriptionTextFontFamily` | string | "inherit" | Body font family | | `descriptionTitleColor` | string | "#000000" | Title color | | `descriptionTextColor` | string | "#4B5563" | Body color | **Note:** Background color is controlled via the CSS Styles tab (`backgroundColor` property) rather than a separate description-specific field. ### Tooltip (Deprecated) > **Note:** The Tooltip element type has been deprecated and removed from the frontend constructor. Existing tooltip elements may still render in runtime, but new ones cannot be created. Backend defaults remain for backward compatibility. **Purpose:** Display contextual information on hover/click. **Interactive Behavior:** - **Hover/Click:** Typically shows expanded content (implementation-dependent) - **Display:** Compact card with title and text **Key Properties:** | Property | Type | Description | |----------|------|-------------| | `iconUrl` | string | Trigger icon image | | `tooltipTitle` | string | Tooltip header text | | `tooltipText` | string | Tooltip body text | ### Gallery **Purpose:** Display a grid of images with optional titles and descriptions. **Interactive Behavior:** - **Click on card:** Can open lightbox or navigate (implementation-dependent) - **Layout:** 3-column grid by default **Runtime Rendering:** ```tsx
{galleryCards.map((card) => (
{card.title}
))}
``` **Key Properties:** | Property | Type | Description | |----------|------|-------------| | `galleryCards` | GalleryCard[] | Array of gallery items | **GalleryCard Structure:** ```typescript { id: string; imageUrl: string; title: string; description: string; } ``` **Gallery Section Dimension Properties:** The CSS tab provides dimension controls for gallery sections: | Section | Property | Default | Description | |---------|----------|---------|-------------| | Header | `galleryHeaderWidth` | (none) | Header container width (e.g., "100%", "300px") | | Header | `galleryHeaderHeight` | `auto` | Header/image height | | Header | `galleryHeaderMinHeight` | (none) | Minimum header height | | Header | `galleryHeaderMaxHeight` | (none) | Maximum header height | | Cards | `galleryCardAspectRatio` | `4/3` | Card aspect ratio (4:3, 16:9, 1:1, 3:2, auto) | | Cards | `galleryCardMinHeight` | `40px` | Minimum card height | | Cards | `galleryCardWidth` | (none) | Card width (grid controls by default) | | Cards | `galleryCardHeight` | (none) | Card height (aspect ratio controls by default) | **Gallery Section Text Alignment:** Text alignment can be configured for each gallery section (header, title, spans): | Section | Property | Default | Values | |---------|----------|---------|--------| | Header | `galleryHeaderTextAlign` | `center` | `left`, `center`, `right` | | Title | `galleryTitleTextAlign` | `center` | `left`, `center`, `right` | | Info Spans | `gallerySpanTextAlign` | `center` | `left`, `center`, `right` | **Slide Transition Override Properties (Fullscreen Carousel Overlay):** When a gallery card is clicked, a fullscreen carousel overlay opens. These properties control slide transitions within that overlay: | Property | Type | Default | Description | |----------|------|---------|-------------| | `gallerySlideTransitionType` | `'fade' \| 'none' \| ''` | `''` | Transition type ('' = inherit from page transitions) | | `gallerySlideTransitionDurationMs` | `number \| ''` | `''` | Duration in ms ('' = inherit) | | `gallerySlideTransitionEasing` | `EasingFunction \| ''` | `''` | CSS easing function | | `gallerySlideTransitionOverlayColor` | `string` | `''` | Overlay color for fade ('' = inherit) | > **See:** [Project Transition Settings - Slide Transitions](project-transition-settings.md#slide-transitions-gallerycarousel) for cascade behavior. ### Carousel **Purpose:** Image slideshow with navigation controls. Supports two modes: - **Normal mode:** Inline carousel within element dimensions - **Full-width mode:** Covers full viewport as background layer (z-10), other elements positioned above **Interactive Behavior:** - **Prev/Next buttons:** Navigate between slides (click) - **Arrow keys:** ArrowLeft/ArrowRight navigation (full-width mode, runtime only) - **Touch swipe:** Swipe left/right to navigate (full-width mode, runtime only) - **Draggable buttons:** Drag to reposition nav buttons (full-width mode, constructor only) **Runtime Rendering:** ```tsx // Normal mode: inline carousel
{currentSlide.caption} {/* Navigation buttons at fixed positions */}
// Full-width mode: portal-based layers // Background layer (z-10): Full-screen image // Controls layer (z-30): Navigation buttons at configurable positions ``` **Key Properties:** | Property | Type | Default | Description | |----------|------|---------|-------------| | `carouselSlides` | CarouselSlide[] | [] | Array of slide items | | `carouselPrevIconUrl` | string | '' | Previous button custom icon | | `carouselNextIconUrl` | string | '' | Next button custom icon | | `carouselCaptionFontFamily` | string | '' | Font key for captions | | `carouselFullWidth` | boolean | false | Enable full-width background mode | **Full-Width Mode Button Properties:** | Property | Type | Default | Description | |----------|------|---------|-------------| | `carouselPrevX` | number | 5 | Prev button X position (% from left) | | `carouselPrevY` | number | 50 | Prev button Y position (% from top) | | `carouselNextX` | number | 95 | Next button X position (% from left) | | `carouselNextY` | number | 50 | Next button Y position (% from top) | | `carouselPrevWidth` | string | '' | Prev button width (vw units) | | `carouselPrevHeight` | string | '' | Prev button height (vh units) | | `carouselNextWidth` | string | '' | Next button width (vw units) | | `carouselNextHeight` | string | '' | Next button height (vh units) | **Button Rendering Modes:** | Condition | Rendering Style | |-----------|-----------------| | No custom icon | Default MDI chevron icon with backdrop blur | | Custom icon, no dimensions | Fixed 40x40 icon | | Custom icon + dimensions | Navigation-style: icon fills button, no backdrop | **CarouselSlide Structure:** ```typescript { id: string; imageUrl: string; caption: string; } ``` **Slide Transition Override Properties:** | Property | Type | Default | Description | |----------|------|---------|-------------| | `carouselSlideTransitionType` | `'fade' \| 'none' \| ''` | `''` | Transition type ('' = inherit from page transitions) | | `carouselSlideTransitionDurationMs` | `number \| ''` | `''` | Duration in ms ('' = inherit) | | `carouselSlideTransitionEasing` | `EasingFunction \| ''` | `''` | CSS easing function | | `carouselSlideTransitionOverlayColor` | `string` | `''` | Overlay color for fade ('' = inherit) | > **See:** [Project Transition Settings - Slide Transitions](project-transition-settings.md#slide-transitions-gallerycarousel) for cascade behavior. **Full-Width Mode Architecture:** ``` CarouselElement (full-width mode) ├── Background layer (z-10): Fixed inset-0, image display ├── Controls layer (z-30): Navigation buttons, caption │ └── Buttons have: │ - Configurable position (percentage-based) │ - Configurable dimensions (vw/vh units) │ - Draggable in constructor edit mode │ - Navigation-style rendering when icon+dimensions set └── Edit mode placeholder: Clickable element for selection ``` ### Video Player **Purpose:** Embedded video playback with HTML5 controls. **Interactive Behavior:** - **Controls:** Play, pause, seek, volume, fullscreen - **Autoplay:** Starts automatically if `mediaAutoplay` is true - **Loop:** Repeats continuously if `mediaLoop` is true **Runtime Rendering:** ```tsx