Files
uberwald 7a0d935239 Story 5.2: Video Widget Width Customization
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>
2026-05-25 12:57:24 +02:00

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
  • 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 ApplicationV2
  • src/ui/RoleRenderer.js - Enhanced with rerenderStrip capability
  • module.js - Updated to open strip for all users, not just GM
  • templates/roster-strip.hbs - Custom template for participant tiles
  • styles/scrying-pool.css - Main stylesheet
  • styles/components/_roster-strip.less - Strip-specific styles

Technical Notes:

  • Strip is created and opened during ready hook
  • 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 only
  • vertical-md: Expanded vertical list, large tiles, shows names
  • horizontal-sm: Compact horizontal row, small tiles
  • horizontal-md: Expanded horizontal row, large tiles with names
  • mosaic-sm: Grid layout, small tiles
  • mosaic-md: Grid layout, large tiles with names

Files Modified:

  • src/ui/gm/DirectorsBoard.js - Added dock layout selector to UI
  • src/ui/gm/ScryingPoolStrip.js - Layout resolution and rendering logic
  • module.js - Setting registrations with onChange callbacks
  • lang/en.json - Added localization strings for layout options
  • templates/directors-board.hbs - Added layout selector UI
  • styles/components/_directors-board.less - Layout selector styling
  • styles/components/_roster-strip.less - Per-layout styling

Migration Notes:

  • Legacy boolean values for dockLayoutExpanded are automatically migrated to string format
  • Migration code in ready hook 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:

  1. Strip constructed with default position
  2. _loadPosition() called in constructor
  3. If saved position exists, applied to this.options.position
  4. Strip rendered at saved position
  5. On drag/move, new position saved to flags

Files Modified:

  • src/ui/gm/ScryingPoolStrip.js - Added _loadPosition() method
  • module.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:

  1. Custom fallback portrait (from Player Privacy Panel)
  2. User avatar (user.avatar)
  3. Character portrait (user.character?.img)
  4. 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 strip
  • src/ui/gm/ScryingPoolStrip.js - Accepts handler in constructor
  • src/ui/gm/ScryingPoolStrip.js - Uses handler in buildParticipantList
  • module.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_OPTIONS static property instead of defaultOptions getter
  • Uses PARTS static 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 ApplicationV2
  • src/ui/gm/DirectorsBoard.js - Verified ApplicationV2 compatibility
  • Fixed jQuery parameter handling in _onRender methods

Bug Fixes:

  • Fixed DirectorsBoard position 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 .gitignore to allow dist/styles/ for distribution
  • Updated module.json styles reference
  • Added build comments to scrying-pool.less

Git Changes:

  • styles/scrying-pool.css - Generated, not tracked in git
  • styles/scrying-pool.less - Source file, tracked
  • .gitignore - Added dist/styles/ exception
  • package.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 registration
  • ready - Component construction, AV integration
  • setup - Early initialization
  • renderAVConfig - AV configuration rendering
  • updateScene - 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 strings
  • module.js - Added dockLayout, dockLayoutExpanded settings with onChange handlers; added legacy value migration; updated strip initialization
  • src/ui/RoleRenderer.js - Added rerenderStrip() method; passes portraitFallbackHandler to strip
  • src/ui/gm/DirectorsBoard.js - Added dock layout selector UI and handler
  • src/ui/gm/ScryingPoolStrip.js - Complete ApplicationV2 migration; added _loadPosition(); layout resolution logic; position persistence; PortraitFallbackHandler integration
  • src/ui/shared/ScryingPoolCameraViews.js - Minor updates
  • styles/components/_directors-board.less - Layout selector styling
  • styles/components/_roster-strip.less - Per-layout styling for all 6 options
  • styles/scrying-pool.css - Built from LESS source
  • templates/directors-board.hbs - Dock layout selector UI
  • templates/roster-strip.hbs - Updated for layout support
  • tests/unit/ui/gm/ScryingPoolStrip.test.js - Updated tests for new functionality

Lines Changed: +755, -141 across 12 files


Future Considerations

Potential v1.1 Features

  1. Compatibility Testing: Thorough testing with other popular FoundryVTT modules
  2. Performance Optimization: Strip rendering with large numbers of participants (20+)
  3. Additional Layout Options: More mosaic variants, custom grid configurations
  4. Accessibility Enhancements: Screen reader testing, high contrast modes
  5. Mobile Support: Better touch interaction for Director's Board

Architecture Improvements

  1. TypeScript Migration: Consider migrating from JavaScript to TypeScript for better type safety
  2. Component Testing: Expand unit test coverage for all components
  3. Integration Tests: Add end-to-end testing for critical user journeys
  4. 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.