# 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 `