# Global UI Controls
## Overview
Global UI controls are system-owned presentation buttons for fullscreen, global
sound mute, and offline mode. They are not regular canvas elements and are not
stored in `tour_pages.ui_schema_json.elements`.
They behave like runtime chrome:
- button dimensions use canvas-relative percentages
- button positions are relative to the visible canvas using `xPercent/yPercent`
- constructor edit mode renders controls even when hidden
- controls can be disabled or hidden, but cannot be deleted
- each control can use custom default and active icons from Assets
## Implementation Files
Backend:
- `backend/src/db/models/global_ui_control_defaults.js`
- `backend/src/db/models/project_ui_control_settings.js`
- `backend/src/db/api/global_ui_control_defaults.ts`
- `backend/src/db/api/project_ui_control_settings.ts`
- `backend/src/routes/global_ui_control_defaults.ts`
- `backend/src/routes/project_ui_control_settings.ts`
- `backend/src/services/global_ui_control_defaults.ts`
- `backend/src/services/project_ui_control_settings.ts`
Frontend:
- `frontend/src/types/uiControls.ts`
- `frontend/src/components/UiControls/UiControlsSettingsForm.tsx`
- `frontend/src/components/Runtime/RuntimeControls.tsx`
- `frontend/src/components/Constructor/ElementEditorPanel.tsx`
- `frontend/src/stores/global_ui_control_defaults/globalUiControlDefaultsSlice.ts`
- `frontend/src/stores/project_ui_control_settings/projectUiControlSettingsSlice.ts`
- `frontend/src/components/RuntimePresentation.tsx`
- `frontend/src/pages/constructor.tsx`
- `frontend/src/pages/global-ui-control-defaults.tsx`
- `frontend/src/pages/global-ui-control-defaults/[controlType].tsx`
- `frontend/src/pages/project-ui-control-settings.tsx`
## Cascade
Settings resolve field-by-field:
```
global_ui_control_defaults
-> project_ui_control_settings (project + environment)
-> tour_pages.global_ui_controls_settings_json
```
Missing fields inherit from the previous cascade level: page settings override
project settings, project settings override global defaults, and frontend
hardcoded defaults are only a safety fallback.
## Data Shape
Each control (`fullscreen`, `sound`, `offline`) supports:
| Field | Purpose |
|-------|---------|
| `enabled` | Disable action while keeping the control selectable in edit mode |
| `hidden` | Hide in runtime/preview; still render as ghost in constructor edit mode |
| `xPercent`, `yPercent` | Canvas-relative position |
| `anchor` | Which button point is placed at the coordinate |
| `buttonSizePercent`, `iconSizePercent`, `borderRadiusPercent` | Canvas-width-relative dimensions |
| `defaultIconUrl`, `activeIconUrl` | Optional custom asset URL/storage key per state |
| `defaultBackgroundColor`, `activeBackgroundColor` | Button background color per state |
| `defaultBorderColor`, `activeBorderColor` | Button border color per state |
| `hoverBackgroundColor`, `color` | Shared hover background and icon color |
| `opacity`, `boxShadow`, `zIndex`, `order` | Presentation and initial ordering |
Active state means downloaded/offline for the offline button, muted for the
sound button, and fullscreen for the fullscreen button.
When runtime is embedded in an iframe, the fullscreen control first tries the
browser Fullscreen API for the presentation document, then tries to fullscreen
the embedding iframe when same-origin access is available. For cross-origin
wrappers it posts `tour-builder:request-fullscreen` to `window.parent`; exit
attempts post `tour-builder:exit-fullscreen`.
```html
```
Dimensions are stored as percentages of canvas width. `buttonSizePercent` and
`iconSizePercent` resolve from the displayed canvas width. Vertical bounds use
the canvas aspect ratio, so buttons remain fully inside the visible canvas for
non-16:9 projects as well.
Default global values:
| Control | X | Y | Size | Icon | Radius | Order |
|---------|---|---|------|------|--------|-------|
| `offline` | `89.5` | `6` | `2.6` | `1.35` | `0.42` | `1` |
| `fullscreen` | `92.75` | `6` | `2.6` | `1.35` | `0.42` | `2` |
| `sound` | `96` | `6` | `2.6` | `1.35` | `0.42` | `3` |
## APIs
- `GET /api/global-ui-control-defaults`
- `PUT /api/global-ui-control-defaults/:id`
- `GET /api/project-ui-control-settings/project/:projectId/env/:environment`
- `PUT /api/project-ui-control-settings/project/:projectId/env/:environment`
- `DELETE /api/project-ui-control-settings/project/:projectId/env/:environment`
Production project settings reads follow the same public/private access rules
as runtime transition settings. Dev/stage and writes require JWT.
`GET /api/global-ui-control-defaults` is public-readable for runtime. Updates
require JWT and `UPDATE_PAGE_ELEMENTS`; these settings are authored with the
same permission family as element defaults. Project-level `production` reads
are public only for public production presentations; private production
presentations require JWT and presentation access.
Project override writes require `UPDATE_PAGE_ELEMENTS`. The project
environment reset endpoint is implemented as `DELETE` because it removes the
override row, but authorization treats it as an update operation: it also
requires `UPDATE_PAGE_ELEMENTS`, not `DELETE_PAGE_ELEMENTS`.
## Constructor Behavior
Constructor renders controls through `RuntimeControls` with `editMode=true`.
Selecting a system control opens the element editor in system-control mode.
System controls:
- can be dragged to update `xPercent/yPercent`
- can be edited with coordinate inputs
- can choose separate default and active custom icons
- can edit default/active background colors and border colors
- can edit hidden/disabled state, order, z-index, opacity, shadow, size, radius,
and anchor
- cannot be removed, copied, or pasted
Constructor system-control opacity is edited as a clamped percentage from `0`
to `100` and converted to the stored runtime opacity number from `0` to `1`.
Constructor edit mode blocks runtime actions. Clicking or dragging system
controls does not toggle fullscreen, sound, or offline mode. Hidden controls are
rendered as ghost controls so authors can reselect and unhide them.
## Admin Editing UI
Global defaults are listed at `/global-ui-control-defaults` and edited per
control:
- `/global-ui-control-defaults/offline`
- `/global-ui-control-defaults/fullscreen`
- `/global-ui-control-defaults/sound`
Project-level overrides are edited from
`/project-ui-control-settings?projectId=...`.
The per-control global pages and project page use `UiControlsSettingsForm`, a
typed editor with the same tab model used by element defaults:
- **General Settings**: enabled/hidden state, canvas-relative position, anchor,
order, and default/active icon URLs
- **CSS Styles**: button/icon sizing, radius, icon color, background colors,
and border colors
- **Effects**: opacity, z-index, and box shadow
Opacity inputs in the constructor/editor UI are percentage-based for authors;
stored `settings_json.opacity` remains a numeric CSS opacity value.
Project settings can be cleared with **Use Global Defaults**, which deletes the
project/environment override so the project inherits global defaults again.
## Publishing
`project_ui_control_settings` is environment-aware and is copied by the publish
workflow:
```
dev -> stage -> production
```
Page-level overrides live on `tour_pages` and are copied with page records.
Project clone copies project UI-control settings for each environment, and page
duplication copies page-level `global_ui_controls_settings_json`.
## Migrations
- `20260628000001-create-ui-control-settings.js` creates the two DB models,
adds `tour_pages.global_ui_controls_settings_json`, creates the project/env
unique index, and seeds the initial global defaults.
- `20260628000005-snapshot-existing-project-ui-controls.js` snapshots
project-level settings for existing projects that do not already have
project UI-control overrides.
## Existing Projects
Migration `20260628000005-snapshot-existing-project-ui-controls.js` snapshots
the previous runtime chrome layout into project-level settings for existing
projects in `dev`, `stage`, and `production` when no project UI-control override
already exists. The snapshot keeps the old top-right grouped placement offsets
and previous runtime z-index, but stores dimensions with the same
canvas-relative defaults used by new projects (`2.6%` button size, `1.35%` icon
size, `0.42%` radius).