Implements configurable video widget widths for small and large dock layouts: - Add widgetWidthSm and widgetWidthMd world settings (defaults: 83px, 150px) - Add width dropdown selectors in Director's Board UI - Apply width settings to participant avatars via CSS custom properties - Update strip width/height calculations to use custom widths - Add localization strings for widget width settings - Add CSS styling for widget width dropdown controls - Fix Handlebars template syntax for select element selected values Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
17 KiB
Addendum — Video View Manager PRD
Workspace: _bmad-output/planning-artifacts/prds/prd-video-view-manager-2026-05-19/
Created: 2026-05-25
Author: Morr
Purpose: Capture implementation decisions, technical notes, and downstream documentation that earned a place but does not fit the PRD's main narrative.
Implementation Notes from Live Testing (May 20-25, 2026)
Full AV Dock Replacement (Story 5-1)
Decision: Implement complete replacement of Foundry's native AV dock rather than hooking into it.
Rationale:
- Hooking approach proved unreliable during live testing
- Full replacement provides consistent behavior across all AV states
- Enables advanced features like dock layouts and position persistence
- Better control over rendering pipeline
Implementation Details:
-
ScryingPoolStrip: Custom ApplicationV2-based component that serves as the main AV dock
- Replaces
game.webrtc.render()output - Opens automatically for all users when AV is active
- Uses Handlebars templates (roster-strip.hbs)
- Integrates with RoleRenderer for participant management
- Replaces
-
RoleRenderer: Orchestrator component that:
- Manages ScryingPoolStrip lifecycle
- Coordinates between StateStore, ScryingPoolController, AVTileAdapter
- Handles participant list building with proper fallback priorities
- Provides
rerenderStrip()method for on-demand refreshes
Files Modified:
src/ui/gm/ScryingPoolStrip.js- Complete rewrite to ApplicationV2src/ui/RoleRenderer.js- Enhanced with rerenderStrip capabilitymodule.js- Updated to open strip for all users, not just GMtemplates/roster-strip.hbs- Custom template for participant tilesstyles/scrying-pool.css- Main stylesheetstyles/components/_roster-strip.less- Strip-specific styles
Technical Notes:
- Strip is created and opened during
readyhook - Previous implementation only opened for GM; now opens for all users
- Position loading happens in
_loadPosition()method - Strip is automatically closed and reopened when layout changes
Compatibility Considerations:
- Must coexist with other modules that patch AV-related hooks
- Proper hook chaining required to avoid conflicts
- Native Foundry AV controls remain functional
Dock Layout System
Decision: Implement 6 configurable layout options with GM control and per-user override.
Implementation Details:
World-Scoped Setting: dockLayout
- Type: String
- Default:
"vertical-sm" - Options:
vertical-sm,vertical-md,horizontal-sm,horizontal-md,mosaic-sm,mosaic-md - OnChange: Triggers
roleRenderer?.rerenderStrip() - UI: Configured via Director's Board with visual icon selector
Client-Scoped Setting: dockLayoutExpanded
- Type: String
- Default:
""(empty string = inherit from world) - Options:
""(inherit),"sm"(force small),"md"(force large) - OnChange: Triggers
roleRenderer?.rerenderStrip() - Purpose: Allows individual users to override GM's size preference
Layout Resolution Logic:
const rawLayout = settings.get('dockLayout'); // e.g., "vertical-sm"
const baseLayout = typeof rawLayout === 'string' ? rawLayout : 'vertical-sm';
const sizeOverride = settings.get('dockLayoutExpanded'); // '' | 'sm' | 'md'
const parts = baseLayout.split('-');
const dir = parts.slice(0, -1).join('-'); // e.g., "vertical"
const canonicalSize = parts[parts.length - 1]; // e.g., "sm"
const effectiveSize = (sizeOverride === 'sm' || sizeOverride === 'md')
? sizeOverride
: canonicalSize;
const dockLayout = `${dir}-${effectiveSize}`; // e.g., "vertical-md"
Layout Characteristics:
vertical-sm: Compact vertical list, small tiles, icons onlyvertical-md: Expanded vertical list, large tiles, shows nameshorizontal-sm: Compact horizontal row, small tileshorizontal-md: Expanded horizontal row, large tiles with namesmosaic-sm: Grid layout, small tilesmosaic-md: Grid layout, large tiles with names
Files Modified:
src/ui/gm/DirectorsBoard.js- Added dock layout selector to UIsrc/ui/gm/ScryingPoolStrip.js- Layout resolution and rendering logicmodule.js- Setting registrations with onChange callbackslang/en.json- Added localization strings for layout optionstemplates/directors-board.hbs- Added layout selector UIstyles/components/_directors-board.less- Layout selector stylingstyles/components/_roster-strip.less- Per-layout styling
Migration Notes:
- Legacy boolean values for
dockLayoutExpandedare automatically migrated to string format - Migration code in
readyhook checks for boolean values and resets to""
Position Persistence
Decision: Save and restore ScryingPoolStrip window position.
Implementation Details:
Storage Mechanism:
- Position saved to GM user flags:
game.user.setFlag('scrying-pool', 'stripState', { left, top }) - Loaded during strip construction via
_loadPosition()method - Applied to options.position before initial render
Position Object:
{
left: number, // CSS pixel value
top: number // CSS pixel value
}
Lifecycle:
- Strip constructed with default position
_loadPosition()called in constructor- If saved position exists, applied to
this.options.position - Strip rendered at saved position
- On drag/move, new position saved to flags
Files Modified:
src/ui/gm/ScryingPoolStrip.js- Added_loadPosition()methodmodule.js- No changes needed; handled internally
Edge Cases:
- First-time users: No saved position, uses default
- Invalid position values: Caught and ignored, uses default
- Corrupted flags: Handled gracefully with try/catch
PortraitFallbackHandler Integration
Decision: Enhance portrait fallback with dedicated handler component.
Implementation Details:
Priority Chain:
- Custom fallback portrait (from Player Privacy Panel)
- User avatar (
user.avatar) - Character portrait (
user.character?.img) - Mystery-man placeholder (
icons/svg/mystery-man.svg)
Integration Points:
- Passed to
buildParticipantList()function - Used in RoleRenderer construction
- Applied when building participant objects for strip rendering
Code Example:
const avatarSrc = portraitFallbackHandler?.getFallbackImageURL(userId)
|| user.avatar
|| user.character?.img
|| 'icons/svg/mystery-man.svg';
Files Modified:
src/ui/RoleRenderer.js- Passes handler to stripsrc/ui/gm/ScryingPoolStrip.js- Accepts handler in constructorsrc/ui/gm/ScryingPoolStrip.js- Uses handler inbuildParticipantListmodule.js- Handler created and passed to RoleRenderer
ApplicationV2 Migration
Decision: Migrate all UI components to ApplicationV2 API.
Changes from Previous Implementation:
Before (Application base class):
static get defaultOptions() {
return {
id: 'scrying-pool-strip',
popOut: true,
resizable: false,
title: 'Scrying Pool',
classes: ['scrying-pool-strip'],
};
}
After (ApplicationV2 with HandlebarsApplicationMixin):
static DEFAULT_OPTIONS = {
id: 'scrying-pool-strip',
classes: ['scrying-pool-strip'],
window: { title: 'Scrying Pool', resizable: false },
position: { width: 240 },
};
static PARTS = {
strip: {
template: 'modules/scrying-pool/templates/roster-strip.hbs',
},
};
constructor(options) {
super(options);
// ApplicationV2 lifecycle methods
}
async _prepareContext(options) {
// Return context for template
}
_onRender(context, options) {
// Called after render
}
_onClose() {
// Called on close
}
Key Differences:
- Uses
DEFAULT_OPTIONSstatic property instead ofdefaultOptionsgetter - Uses
PARTSstatic property for template configuration - Lifecycle methods:
_prepareContext,_onRender,_onClose - Proper position management via
setPosition() - Better separation of concerns
Files Modified:
src/ui/gm/ScryingPoolStrip.js- Complete migration to ApplicationV2src/ui/gm/DirectorsBoard.js- Verified ApplicationV2 compatibility- Fixed jQuery parameter handling in
_onRendermethods
Bug Fixes:
- Fixed
DirectorsBoardposition loading error (D-19) - Fixed ApplicationV2 jQuery parameter handling (
a05d3ca)
CSS Build Pipeline
Decision: Automate CSS compilation from LESS source files.
Implementation Details:
Build Script:
- Added to
package.json:
"scripts": {
"build:css": "lessc styles/scrying-pool.less > styles/scrying-pool.css",
"postinstall": "npm run build:css"
}
File Structure:
styles/
├── scrying-pool.less # Main LESS file (imports components)
├── scrying-pool.css # Compiled output (gitignored, generated)
└── components/
├── _directors-board.less # Director's Board styles
├── _roster-strip.less # Strip styles
└── ...
Changes:
- Moved CSS to root
styles/folder for FoundryVTT compatibility - Added component LESS files for better organization
- Updated
.gitignoreto allowdist/styles/for distribution - Updated
module.jsonstyles reference - Added build comments to scrying-pool.less
Git Changes:
styles/scrying-pool.css- Generated, not tracked in gitstyles/scrying-pool.less- Source file, tracked.gitignore- Addeddist/styles/exceptionpackage.json- Added build scripts
Module Settings Reference
World-Scoped Settings
| Setting Key | Type | Default | Description | onChange |
|---|---|---|---|---|
visibilityMatrix |
Object | { _version: 1, matrix: {} } |
Visibility state for all participants | Triggers stateStore.init() and strip rerender |
dockLayout |
String | "vertical-sm" |
Dock layout configuration | Triggers roleRenderer.rerenderStrip() |
autoApplyEnabled |
Boolean | true |
Enable Scene Preset auto-apply | None |
showGMSelfFeed |
Boolean | true |
Show GM's own feed in their view | None |
Client-Scoped Settings
| Setting Key | Type | Default | Description | onChange |
|---|---|---|---|---|
dockLayoutExpanded |
String | "" |
Per-user layout size override | Triggers roleRenderer.rerenderStrip() |
notificationVerbosity |
String | "all" |
Notification output mode | None |
Technical Architecture Notes
Component Hierarchy
FoundryVTT Core
├── game.webrtc (native AV)
│
└── Module: scrying-pool
├── module.js (entry point)
│ ├── Hooks registration
│ ├── Settings registration
│ └── Component initialization
│
├── Data Layer
│ ├── FoundryAdapter (settings, users, hooks wrapper)
│ ├── StateStore (visibility matrix persistence)
│ └── SocketHandler (broadcast and receive)
│
├── Core Logic
│ ├── ScryingPoolController (visibility management)
│ └── VisibilityManager (state mutations)
│
├── UI Layer
│ ├── RoleRenderer (AV dock orchestrator)
│ │ └── ScryingPoolStrip (custom AV dock)
│ │ ├── templates/roster-strip.hbs
│ │ └── styles/components/_roster-strip.less
│ │
│ ├── GM Controls
│ │ ├── DirectorsBoard (bulk management)
│ │ │ ├── templates/directors-board.hbs
│ │ │ └── styles/components/_directors-board.less
│ │ ├── ConfirmationBar (preset feedback)
│ │ └── ActionPopover (right-click menu)
│ │
│ ├── Player Controls
│ │ └── PlayerPrivacyPanel (opt-in management)
│ │
│ └── Shared
│ ├── ScryingPoolCameraViews (AV integration)
│ ├── StripOverlayLayer (UI overlays)
│ └── PortraitFallbackHandler (image resolution)
│
└── Utilities
├── uuid.js (ID generation)
└── ...
Hook Registration
Hooks Used:
init- Module initialization, settings registrationready- Component construction, AV integrationsetup- Early initializationrenderAVConfig- AV configuration renderingupdateScene- Scene activation (for preset auto-apply)- Various AV-related hooks for full replacement
Hook Pattern:
Hooks.on('hookName', () => {
// Handler code
});
Testing Notes
Unit Tests
- Tests located in
tests/unit/ - Primarily test ScryingPoolStrip functionality
- Mock FoundryVTT globals for test environment
- Test files:
ScryingPoolStrip.test.js
Test Coverage (from live testing)
- ✅ Full AV dock replacement
- ✅ Dock layout switching (all 6 options)
- ✅ Position persistence across sessions
- ✅ Per-user size override
- ✅ ApplicationV2 compatibility
- ✅ Portrait fallback with all priority levels
- ✅ PortraitFallbackHandler integration
- ✅ CSS build pipeline
Known Issues Resolved
- DirectorsBoard position loading error - Fixed (
5b421d6) - ApplicationV2 jQuery parameter handling - Fixed (
a05d3ca) - CSS class selector in StripOverlayLayer - Fixed (
ea4462e) - StripOverlayLayer initialization timing - Fixed (
6f07e48) - RegisterMenu method missing - Fixed (
0cb046b) - GMPlayerPrivacySelectorMenu ApplicationV2 - Fixed (
e2da477)
File Changes Summary (May 20-25, 2026)
Major Enhancements
| Commit | Date | Description | Files Changed |
|---|---|---|---|
6d7a0b5 |
2026-05-20 | Story 4.2: Implement full AV replacement | 5 files |
c4a375f |
2026-05-20 | Story 4.2: WebRTC stream access | 3 files |
20d13fc |
2026-05-20 | Story 4.2: Fix lint errors | 2 files |
25dd427 |
2026-05-20 | Update tests for Story 5-1 | 1 file |
f8cbb75 |
2026-05-20 | Code review patches | 4 files |
7b56d62 |
2026-05-25 | Finalize deck strip and management | 12 files |
Commit 7b56d62 Details (Final Live Test Enhancements)
Title: Finalize deck strip and management Author: LeRatierBretonnier Date: Mon May 25 00:51:46 2026 +0200
Changes:
lang/en.json- Added dock layout localization stringsmodule.js- Added dockLayout, dockLayoutExpanded settings with onChange handlers; added legacy value migration; updated strip initializationsrc/ui/RoleRenderer.js- Added rerenderStrip() method; passes portraitFallbackHandler to stripsrc/ui/gm/DirectorsBoard.js- Added dock layout selector UI and handlersrc/ui/gm/ScryingPoolStrip.js- Complete ApplicationV2 migration; added _loadPosition(); layout resolution logic; position persistence; PortraitFallbackHandler integrationsrc/ui/shared/ScryingPoolCameraViews.js- Minor updatesstyles/components/_directors-board.less- Layout selector stylingstyles/components/_roster-strip.less- Per-layout styling for all 6 optionsstyles/scrying-pool.css- Built from LESS sourcetemplates/directors-board.hbs- Dock layout selector UItemplates/roster-strip.hbs- Updated for layout supporttests/unit/ui/gm/ScryingPoolStrip.test.js- Updated tests for new functionality
Lines Changed: +755, -141 across 12 files
Future Considerations
Potential v1.1 Features
- Compatibility Testing: Thorough testing with other popular FoundryVTT modules
- Performance Optimization: Strip rendering with large numbers of participants (20+)
- Additional Layout Options: More mosaic variants, custom grid configurations
- Accessibility Enhancements: Screen reader testing, high contrast modes
- Mobile Support: Better touch interaction for Director's Board
Architecture Improvements
- TypeScript Migration: Consider migrating from JavaScript to TypeScript for better type safety
- Component Testing: Expand unit test coverage for all components
- Integration Tests: Add end-to-end testing for critical user journeys
- Documentation: Expand API documentation for module developers
Roadmap Alignment
The following Later-roadmap features from §10 are now more feasible with the full AV replacement architecture:
- Token-Anchored Floating Cams - Can leverage ScryingPoolStrip infrastructure
- Spectator View - Can use similar dock replacement pattern
- Dual Layout System - Can extend current layout system
- Browser Source API - Can expose strip layouts for OBS integration
References
- Module ID:
scrying-pool - Module Title: Scrying Pool
- Version: 0.1.0
- FoundryVTT Compatibility: v14+
- Repository: video-view-manager
- PRD Location:
_bmad-output/planning-artifacts/prds/prd-video-view-manager-2026-05-19/ - Implementation Artifacts:
_bmad-output/implementation-artifacts/ - Tests:
tests/unit/
This addendum captures technical implementation details that support the PRD but do not belong in its main narrative. For authoritative requirements, see prd.md. For decision rationale, see .decision-log.md.