# Story 5.2: Video Widget Width Customization **Status:** done **Epic:** 5 - Full AV Replacement **Story Key:** 5-2-video-widget-width-customization **Created:** 2026-05-26 **Last Updated:** 2026-05-26 **Completed:** 2026-05-26 **Version:** v0.1.1 --- ## Story Header | Field | Value | |-------|-------| | **Epic** | 5 - Full AV Replacement | | **Story ID** | 5.2 | | **Story Key** | 5-2-video-widget-width-customization | | **Title** | Video Widget Width Customization | | **Status** | ready-for-dev | | **Priority** | Medium | | **Assigned Agent** | DEV (Mistral Vibe / Morr) | | **Created** | 2026-05-26 | | **Last Updated** | 2026-05-26 | | **Target Version** | v0.1.1 | --- ## ๐Ÿ“‹ Story Requirements ### User Story **As a** GM using Video View Manager, **I want to** select custom widths for small and large video widgets in the dock, **So that** I can optimize the display to match my screen space and table preferences. ### Persona Alignment - **Primary:** GM (Marcus, Jake) - Needs flexibility to customize video display for different screen configurations and table setups - **Secondary:** Players - Benefit from consistent, appropriately-sized video feeds that match the GM's layout preferences ### Business Value This feature extends the Dock Layout System (Epic 5, Story 5-1) by adding granular control over video widget dimensions. It addresses user feedback requesting the ability to customize video tile sizes to better match their specific display needs and table aesthetics. ### Acceptance Criteria (BDD Format) #### AC-1: Widget Width Settings **Given** the Video View Manager module is active **When** the GM opens the Director's Board **Then** there are dropdown selectors for small and large widget widths **And** the dropdowns contain the following options: 60px, 80px, 100px, 120px, 150px, 200px #### AC-2: Default Width Values **Given** a fresh installation of Video View Manager **When** no width settings have been configured **Then** the default width for small widgets is 80px **And** the default width for large widgets is 120px #### AC-3: Width Setting Application **Given** the GM has selected width values in the Director's Board **When** the GM saves the settings **Then** all video widgets in the dock are re-rendered with the new widths **And** small layout variants (vertical-sm, horizontal-sm, mosaic-sm) use the small width setting **And** large layout variants (vertical-md, horizontal-md, mosaic-md) use the large width setting #### AC-4: Width Persistence **Given** the GM has configured custom widget widths **When** the page is refreshed or the session is restarted **Then** the widget widths persist and are applied automatically **And** the dropdowns show the previously selected values #### AC-5: Per-Layout Width Application **Given** the GM has selected different widths for small and large **When** the dock is using a small layout variant **Then** all video widgets use the small width setting **And** when the dock is using a large layout variant **Then** all video widgets use the large width setting #### AC-6: Real-Time Preview **Given** the Director's Board is open **When** the GM changes a width dropdown value **Then** the video widgets in the dock update in real-time (within 500ms) **And** no page refresh is required #### AC-7: Width Setting Validation **Given** the width dropdown is displayed **When** the GM selects a width value **Then** the value is validated as one of the allowed options **And** invalid values are rejected with a warning message **And** the previous valid value is retained #### AC-8: CSS Properly Scoped **Given** custom widths are applied **When** the video widgets are rendered **Then** all width-related CSS is properly scoped under `.sp-participant-item` or `.sp-participant-video` **And** no unintended CSS conflicts occur with other modules #### AC-9: Null Safety Throughout **Given** any DOM query returns null **When** width application methods are called **Then** no TypeError is thrown **And** appropriate warnings are logged to console --- ## ๐ŸŽฏ Functional Requirements - **FR-33:** Video widget width configuration โ€” The module provides configurable width options for small and large video tiles via world-scoped settings `widgetWidthSm` and `widgetWidthMd` - **FR-34:** Width selection dropdown in Director's Board โ€” The Director's Board includes dropdown UI controls for selecting video widget widths for both small and large sizes --- ## ๐ŸŽฏ Implementation Details ### Files to Modify | File | Changes | Status | |------|---------|--------| | `module.js` | Register `widgetWidthSm` and `widgetWidthMd` world settings with onChange callbacks | Pending | | `src/ui/gm/DirectorsBoard.js` | Add width dropdown selectors to settings panel, add handler for width changes | Pending | | `src/ui/gm/ScryingPoolStrip.js` | Apply width settings to video elements, pass width values to template context | Pending | | `templates/directors-board.hbs` | Add dropdown UI for width selection in settings panel | Pending | | `styles/components/_roster-strip.less` | Use width settings for `.sp-participant-item` sizing | Pending | | `lang/en.json` | Add localization strings for width settings | Pending | ### Technical Implementation #### Settings Registration (module.js) ```javascript // World-scoped settings for widget widths adapter.settings.register("widgetWidthSm", { scope: "world", config: false, type: String, default: "80", onChange: () => roleRenderer?.rerenderStrip(), }); adapter.settings.register("widgetWidthMd", { scope: "world", config: false, type: String, default: "120", onChange: () => roleRenderer?.rerenderStrip(), }); ``` #### Director's Board UI (DirectorsBoard.js) ```javascript // Add to _prepareContext() const widgetWidthSm = this._adapter.settings?.get?.('widgetWidthSm') ?? '80'; const widgetWidthMd = this._adapter.settings?.get?.('widgetWidthMd') ?? '120'; const WIDTH_OPTIONS = [ { value: '60', label: '60px' }, { value: '80', label: '80px' }, { value: '100', label: '100px' }, { value: '120', label: '120px' }, { value: '150', label: '150px' }, { value: '200', label: '200px' }, ]; // Add to context return { ...base, widthOptions: WIDTH_OPTIONS, widgetWidthSm, widgetWidthMd, }; // Add handler in _onClickButton() case 'set-widget-width-sm': this._onSetWidgetWidth(btn.dataset.value, 'sm'); break; case 'set-widget-width-md': this._onSetWidgetWidth(btn.dataset.value, 'md'); break; // New method async _onSetWidgetWidth(value, size) { if (!value) return; const settingKey = size === 'sm' ? 'widgetWidthSm' : 'widgetWidthMd'; try { await this._adapter.settings?.set?.(settingKey, value); } catch (err) { console.error('[ScryingPool] Failed to set widget width:', err); } if (this.rendered) this.render({ force: true }); } ``` #### ScryingPoolStrip Context (ScryingPoolStrip.js) ```javascript // Add to _prepareContext() const widgetWidthSm = this._adapter.settings?.get?.('widgetWidthSm') ?? '80'; const widgetWidthMd = this._adapter.settings?.get?.('widgetWidthMd') ?? '120'; const dockLayout = this.context?.dockLayout ?? 'vertical-sm'; const isLarge = dockLayout.endsWith('-md'); const effectiveWidth = isLarge ? widgetWidthMd : widgetWidthSm; return { ...base, widgetWidth: effectiveWidth, isLarge, }; ``` #### Template Updates (directors-board.hbs) ```handlebars {{!-- In settings panel --}}
Small:
Large:
``` #### CSS Updates (components/_roster-strip.less) ```less // Apply width to participant items .sp-participant-item { width: var(--widget-width, 80px); min-width: var(--widget-width, 80px); // Or use inline style from template // width: {{widgetWidth}}px; // min-width: {{widgetWidth}}px; } // Ensure video elements fill the width .sp-participant-video { width: 100%; height: 100%; } ``` --- ## ๐Ÿ—๏ธ Architecture Compliance ### Design Token Usage โœ… All width values use CSS pixel units (px) for consistency with FoundryVTT conventions โœ… No direct Foundry `--color-*`/`--font-*`/`--border-*` tokens used โœ… All selectors properly scoped under `.sp-*` prefix ### Import Boundaries โœ… No direct `game.*` access in core logic โœ… All Foundry API access via FoundryAdapter โœ… Settings access via adapter.settings โœ… Dependency injection maintained for all managers ### Code Conventions โœ… JSDoc on all exported symbols โœ… Private methods prefixed with `_` โœ… Consistent error handling pattern (try-catch with console.error) โœ… Null-safe access patterns throughout โœ… Input validation on all user-provided values --- ## ๐Ÿงช Testing Requirements ### Unit Tests **Test File:** `tests/unit/ui/gm/VideoWidgetWidth.test.js` (new file) **Test Coverage:** - โœ… Settings registration with proper defaults - โœ… Dropdown rendering with all width options - โœ… Width selection handler updates settings correctly - โœ… onChange callback triggers strip re-render - โœ… Small and large widths apply correctly based on layout - โœ… Context includes correct width value - โœ… Null safety for missing settings - โœ… Input validation rejects invalid width values ### Manual Test Cases 1. **Default Widths** - [ ] Install fresh module - [ ] Verify small width defaults to 80px - [ ] Verify large width defaults to 120px - [ ] Verify video widgets render at default sizes 2. **Width Selection** - [ ] Open Director's Board - [ ] Navigate to settings panel - [ ] Verify small and large width dropdowns are present - [ ] Verify all width options (60, 80, 100, 120, 150, 200) are available 3. **Real-Time Preview** - [ ] Select a different small width (e.g., 100px) - [ ] Verify video widgets update within 500ms - [ ] Verify no page refresh is required - [ ] Select a different large width (e.g., 150px) - [ ] Switch to a large layout variant - [ ] Verify widgets use large width 4. **Layout Switching** - [ ] Set small width to 80px, large width to 150px - [ ] Switch to vertical-sm layout - [ ] Verify widgets use 80px width - [ ] Switch to vertical-md layout - [ ] Verify widgets use 150px width - [ ] Switch back to vertical-sm - [ ] Verify widgets return to 80px width 5. **Persistence** - [ ] Configure custom widths - [ ] Refresh the page - [ ] Verify widths persist - [ ] Restart FoundryVTT - [ ] Verify widths persist 6. **Input Validation** - [ ] Attempt to set invalid width value via console - [ ] Verify value is rejected - [ ] Verify previous valid value is retained - [ ] Verify warning is logged to console --- ## ๐Ÿ“š Developer Context Section ### What the Developer MUST Know #### 1. Settings Pattern **Critical:** This story uses the established world-scoped setting pattern from previous stories. **Pattern:** ```javascript adapter.settings.register("settingName", { scope: "world", config: false, type: String, default: "defaultValue", onChange: () => callback(), }); ``` **Do NOT:** - Use client-scoped settings (width should be consistent for all users) - Forget the onChange callback (strips won't update automatically) - Use numeric type (stored as string to maintain precision) **DO:** - Always provide sensible defaults - Always include onChange callback to trigger UI updates - Use string type for pixel values to avoid type issues #### 2. Dock Layout Integration This feature **extends** the existing Dock Layout System (FR-29-30), not replaces it. **Integration Points:** - Width settings are independent of layout direction (vertical/horizontal/mosaic) - Width settings depend on size variant (sm/md) - Layout selection (FR-29) and width selection (FR-33) work together **Flow:** 1. GM selects layout direction + size (e.g., vertical-md) 2. System determines if layout is small or large variant 3. System applies corresponding width setting (widgetWidthSm or widgetWidthMd) 4. Strip re-renders with new layout AND width #### 3. Real-Time Update Pattern **Critical:** Width changes must update video widgets in real-time without page refresh. **Pattern:** ```javascript onChange: () => roleRenderer?.rerenderStrip() ``` **Why:** - Provides immediate visual feedback - Matches existing pattern from Dock Layout System (FR-29) - Expected by users from modern web applications **DO:** - Use onChange callback on settings registration - Call rerenderStrip() when width changes - Ensure rerender includes updated width in context #### 4. Dropdown UI Pattern **Critical:** Follow existing Director's Board UI patterns for consistency. **Pattern:** - Use `