diff --git a/backend/README.md b/backend/README.md index c9aa366..7714caf 100644 --- a/backend/README.md +++ b/backend/README.md @@ -199,6 +199,32 @@ DELETE /api/{entity}/:id # Soft delete record | `permissions` | Granular permissions | | `project_memberships` | Team access per project | +### Element Defaults Hierarchy + +UI elements use a three-tier defaults system: + +``` +element_type_defaults (Global) + │ + │ auto-snapshot on project creation + ▼ +project_element_defaults (Project) + │ + │ applied when creating elements + ▼ +tour_pages.ui_schema_json (Instance) +``` + +1. **Global** (`element_type_defaults`) - Platform-wide defaults for 11 element types (navigation, tooltip, gallery, etc.). Auto-seeded on first API access. + +2. **Project** (`project_element_defaults`) - Per-project overrides. Automatically snapshotted from global when a project is created. Can be customized independently. + +3. **Instance** (`tour_pages.ui_schema_json`) - Page-specific elements with their settings stored inline. Created in constructor with project defaults applied. + +**Additional Endpoints:** +- `POST /api/project-element-defaults/:id/reset` - Reset to current global default +- `GET /api/project-element-defaults/:id/diff` - Compare with global default + ### Publishing Workflow Three-tier environment model for content: `dev` → `stage` → `production` diff --git a/frontend/README.md b/frontend/README.md index 3dba6b0..dd61ca0 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -70,6 +70,7 @@ frontend/src/ ├── components/ # React components (PascalCase) │ ├── Assets/ # Asset management components │ ├── Constructor/ # Tour builder components +│ ├── ElementSettings/ # Shared element settings form components │ ├── UiElements/ # Element type components │ ├── Generic/ # Generic CRUD components │ ├── CardBox.tsx # Card container @@ -214,6 +215,7 @@ dispatch(create({ data: newProject })); | `useOfflineMode` | Detect offline/online status | | `usePWAPreload` | Preload assets for offline | | `useStorageQuota` | Monitor IndexedDB usage | +| `useElementSettingsForm` | Element settings form state (~60 fields) | ## Element Types @@ -243,7 +245,13 @@ Element types follow a three-tier defaults system: The constructor fetches project defaults via `/api/project-element-defaults?projectId=xxx` and merges them when creating new elements. Existing element values in `ui_schema_json` take precedence over defaults. +**Admin Pages:** +- `/element-type-defaults` - Edit global defaults (platform-wide) +- `/project-element-defaults/[id]` - Edit project defaults (with Reset to Global, Diff indicator) + **Key files:** +- `components/ElementSettings/` - Shared form components (tabs, CSS styling, type-specific sections) +- `components/ElementSettings/useElementSettingsForm.ts` - Form state hook (~60 fields) - `types/constructor.ts` - `normalizeElementDefault()`, `buildElementDefaultsMap()` utilities - `pages/constructor.tsx` - Loads project defaults, creates elements with merged settings diff --git a/frontend/src/components/ElementSettings/CarouselSettingsSection.tsx b/frontend/src/components/ElementSettings/CarouselSettingsSection.tsx new file mode 100644 index 0000000..691e34f --- /dev/null +++ b/frontend/src/components/ElementSettings/CarouselSettingsSection.tsx @@ -0,0 +1,163 @@ +/** + * CarouselSettingsSection + * + * Settings for carousel element type. + * Manages carousel slides with images and captions. + */ + +import React from 'react'; +import { mdiPlus, mdiTrashCan } from '@mdi/js'; +import BaseButton from '../BaseButton'; +import CardBox from '../CardBox'; +import FormField from '../FormField'; +import type { CarouselSettingsSectionProps } from './types'; + +const CarouselSettingsSection: React.FC = ({ + carouselPrevIconUrl, + carouselNextIconUrl, + carouselSlides, + onAddSlide, + onRemoveSlide, + onUpdateSlide, + onChange, + context, + iconAssetOptions = [], + imageAssetOptions = [], +}) => { + const isConstructor = context === 'constructor'; + + return ( + +

Carousel settings

+ +
+ {isConstructor ? ( + <> + + + + + + + + ) : ( + <> + + + onChange('carouselPrevIconUrl', event.target.value) + } + /> + + + + onChange('carouselNextIconUrl', event.target.value) + } + /> + + + )} +
+ +
+

Carousel slides

+ +
+ +
+ {carouselSlides.length === 0 ? ( +

No slides yet.

+ ) : ( + carouselSlides.map((slide, index) => ( + +
+

Slide {index + 1}

+ onRemoveSlide(slide.id)} + /> +
+
+ {isConstructor ? ( + + + + ) : ( + + + onUpdateSlide(slide.id, 'imageUrl', event.target.value) + } + /> + + )} + + + onUpdateSlide(slide.id, 'caption', event.target.value) + } + /> + +
+
+ )) + )} +
+
+ ); +}; + +export default CarouselSettingsSection; diff --git a/frontend/src/components/ElementSettings/CommonSettingsSection.tsx b/frontend/src/components/ElementSettings/CommonSettingsSection.tsx new file mode 100644 index 0000000..f774333 --- /dev/null +++ b/frontend/src/components/ElementSettings/CommonSettingsSection.tsx @@ -0,0 +1,68 @@ +/** + * CommonSettingsSection + * + * Common settings fields shared by all element types. + * Label, position, and appear timing. + */ + +import React from 'react'; +import FormField from '../FormField'; +import type { CommonSettingsSectionProps } from './types'; + +const CommonSettingsSection: React.FC = ({ + label, + xPercent, + yPercent, + appearDelaySec, + appearDurationSec, + onChange, + showPosition = true, +}) => { + return ( +
+ + onChange('label', event.target.value)} + /> + + + {showPosition && ( +
+ + onChange('xPercent', event.target.value)} + /> + + + onChange('yPercent', event.target.value)} + /> + +
+ )} + +
+ + onChange('appearDelaySec', event.target.value)} + /> + + + + onChange('appearDurationSec', event.target.value) + } + placeholder='Leave empty for none' + /> + +
+
+ ); +}; + +export default CommonSettingsSection; diff --git a/frontend/src/components/ElementSettings/DescriptionSettingsSection.tsx b/frontend/src/components/ElementSettings/DescriptionSettingsSection.tsx new file mode 100644 index 0000000..385c762 --- /dev/null +++ b/frontend/src/components/ElementSettings/DescriptionSettingsSection.tsx @@ -0,0 +1,139 @@ +/** + * DescriptionSettingsSection + * + * Settings for description element type. + */ + +import React from 'react'; +import FormField from '../FormField'; +import type { DescriptionSettingsSectionProps } from './types'; + +const DescriptionSettingsSection: React.FC = ({ + iconUrl, + descriptionTitle, + descriptionText, + descriptionTitleFontSize, + descriptionTextFontSize, + descriptionTitleFontFamily, + descriptionTextFontFamily, + descriptionTitleColor, + descriptionTextColor, + descriptionBackgroundColor, + onChange, + context, + iconAssetOptions = [], +}) => { + const isConstructor = context === 'constructor'; + + return ( +
+ {isConstructor ? ( + + + + ) : ( + + onChange('iconUrl', event.target.value)} + /> + + )} + + + onChange('descriptionTitle', event.target.value)} + /> + + + +