e031643cd5
PROBLEM:
- Reset/auto-layout placed userInfo at 1x1, mood at [1,0] blocking expansion
- Expansion pass couldn't grow userInfo to 2x1 because mood already occupied column 1
- Result: 1x1 userInfo, 1x1 mood, empty space at [2,0] in 3-col layout
ROOT CAUSE:
- Static defaultSize 1x1 → placement happens first
- Expansion happens second, but mood blocks userInfo horizontal growth
SOLUTION - Column-aware defaultSize:
1. userInfoWidget.js: defaultSize now function of columns
- Mobile (≤2 col): { w: 1, h: 1 } (compact, mood beside it)
- Desktop (3-4 col): { w: 2, h: 1 } (starts at target size)
2. dashboardManager.js: resetWidgetSizesToDefault() supports function defaultSize
- Calls defaultSize(columns) if function, otherwise uses static object
- Same pattern as maxAutoSize support
3. widgetRegistry.js: Updated validation to accept function defaultSize
- Skip validation for functions (can't validate until runtime)
4. dashboardManager.js: Reordered userWidgetOrder
- mood(2) before stats(3) so mood sits beside userInfo in top row
RESULT (3-4 columns):
- userInfo starts at 2x1, placed at [0,0]
- mood placed at [2,0] (beside 2-wide userInfo)
- stats placed at [0,1] and expands to 3x? (full width below)
- No expansion blocking, no wasted space
MOBILE FIXES (from previous commits):
- Stats widget: padding-bottom 0.5rem (was 0.3rem, prevent Arousal clipping)
- Refresh button: Show with Dashboard v2 (#rpg-dashboard-container selector)
- Mood text: 0.6rem font-size (improve readability)
AFFECTED:
- userInfoWidget.js: defaultSize + maxAutoSize column-aware functions
- dashboardManager.js: resetWidgetSizesToDefault, userWidgetOrder
- widgetRegistry.js: Validation allows function defaultSize
- userStatsWidget.js: maxAutoSize column-aware (previous commit)
- style.css: Stats padding fix 0.5rem
256 lines
8.3 KiB
JavaScript
256 lines
8.3 KiB
JavaScript
/**
|
||
* Widget Definition Type
|
||
* @typedef {Object} WidgetDefinition
|
||
* @property {string} name - Display name of the widget
|
||
* @property {string} icon - Emoji or icon for the widget
|
||
* @property {string} description - Brief description of widget functionality
|
||
* @property {{w: number, h: number}} minSize - Minimum grid size (width × height)
|
||
* @property {{w: number, h: number}} defaultSize - Default grid size when added
|
||
* @property {boolean} requiresSchema - Whether widget requires active schema to function
|
||
* @property {Function} render - Render function: (container, config) => void
|
||
* @property {Function} [getConfig] - Optional: Returns configurable options
|
||
* @property {Function} [onConfigChange] - Optional: Called when config changes
|
||
* @property {Function} [onRemove] - Optional: Cleanup when widget removed
|
||
* @property {Function} [onResize] - Optional: Called when widget resized
|
||
*/
|
||
|
||
/**
|
||
* Widget Configuration Type
|
||
* @typedef {Object} WidgetConfig
|
||
* @property {string} type - Type of config (text, number, boolean, select, color)
|
||
* @property {string} label - Display label for the config option
|
||
* @property {*} default - Default value
|
||
* @property {Array<*>} [options] - Options for select type
|
||
* @property {number} [min] - Min value for number type
|
||
* @property {number} [max] - Max value for number type
|
||
*/
|
||
|
||
/**
|
||
* WidgetRegistry - Central registry for all widget types
|
||
*
|
||
* Manages widget definitions and provides methods to register, retrieve,
|
||
* and filter available widgets based on schema requirements.
|
||
*
|
||
* @class WidgetRegistry
|
||
*/
|
||
export class WidgetRegistry {
|
||
/**
|
||
* Initialize widget registry
|
||
*/
|
||
constructor() {
|
||
/** @type {Map<string, WidgetDefinition>} */
|
||
this.widgets = new Map();
|
||
|
||
console.log('[WidgetRegistry] Initialized');
|
||
}
|
||
|
||
/**
|
||
* Register a new widget type
|
||
*
|
||
* @param {string} type - Unique identifier for the widget type
|
||
* @param {WidgetDefinition} definition - Widget definition object
|
||
* @throws {Error} If widget type already registered
|
||
*
|
||
* @example
|
||
* registry.register('userStats', {
|
||
* name: 'User Stats',
|
||
* icon: '❤️',
|
||
* description: 'Health, energy, satiety bars',
|
||
* minSize: { w: 2, h: 2 },
|
||
* defaultSize: { w: 4, h: 3 },
|
||
* requiresSchema: false,
|
||
* render: (container, config) => {
|
||
* container.innerHTML = '<div>User stats here</div>';
|
||
* }
|
||
* });
|
||
*/
|
||
register(type, definition) {
|
||
// Validate type
|
||
if (!type || typeof type !== 'string') {
|
||
throw new Error('[WidgetRegistry] Widget type must be a non-empty string');
|
||
}
|
||
|
||
// Check for duplicate
|
||
if (this.widgets.has(type)) {
|
||
console.warn(`[WidgetRegistry] Widget type "${type}" already registered, overwriting`);
|
||
}
|
||
|
||
// Validate required fields
|
||
const required = ['name', 'icon', 'description', 'minSize', 'defaultSize', 'requiresSchema', 'render'];
|
||
for (const field of required) {
|
||
if (!(field in definition)) {
|
||
throw new Error(`[WidgetRegistry] Widget definition missing required field: ${field}`);
|
||
}
|
||
}
|
||
|
||
// Validate minSize and defaultSize
|
||
if (!definition.minSize.w || !definition.minSize.h) {
|
||
throw new Error('[WidgetRegistry] Widget minSize must have w and h properties');
|
||
}
|
||
// defaultSize can be a function (column-aware) or static object
|
||
if (typeof definition.defaultSize === 'function') {
|
||
// If function, we can't validate until runtime, skip validation
|
||
} else if (!definition.defaultSize.w || !definition.defaultSize.h) {
|
||
throw new Error('[WidgetRegistry] Widget defaultSize must have w and h properties');
|
||
}
|
||
|
||
// Validate render function
|
||
if (typeof definition.render !== 'function') {
|
||
throw new Error('[WidgetRegistry] Widget render must be a function');
|
||
}
|
||
|
||
// Store widget definition
|
||
this.widgets.set(type, {
|
||
...definition,
|
||
// Bind render function to maintain 'this' context
|
||
render: definition.render.bind(definition),
|
||
// Bind optional lifecycle functions
|
||
getConfig: definition.getConfig?.bind(definition),
|
||
onConfigChange: definition.onConfigChange?.bind(definition),
|
||
onRemove: definition.onRemove?.bind(definition),
|
||
onResize: definition.onResize?.bind(definition)
|
||
});
|
||
|
||
console.log(`[WidgetRegistry] Registered widget: ${type} (${definition.name})`);
|
||
}
|
||
|
||
/**
|
||
* Get widget definition by type
|
||
*
|
||
* @param {string} type - Widget type identifier
|
||
* @returns {WidgetDefinition|undefined} Widget definition or undefined if not found
|
||
*
|
||
* @example
|
||
* const userStatsWidget = registry.get('userStats');
|
||
* if (userStatsWidget) {
|
||
* userStatsWidget.render(container, config);
|
||
* }
|
||
*/
|
||
get(type) {
|
||
const widget = this.widgets.get(type);
|
||
if (!widget) {
|
||
console.warn(`[WidgetRegistry] Widget type "${type}" not found`);
|
||
}
|
||
return widget;
|
||
}
|
||
|
||
/**
|
||
* Get all available widgets, optionally filtered by schema requirement
|
||
*
|
||
* @param {boolean} [hasSchema=false] - Whether an active schema is present
|
||
* @returns {Array<{type: string, definition: WidgetDefinition}>} Array of available widgets
|
||
*
|
||
* @example
|
||
* // Get widgets that work without schema
|
||
* const coreWidgets = registry.getAvailable(false);
|
||
*
|
||
* // Get all widgets (schema active)
|
||
* const allWidgets = registry.getAvailable(true);
|
||
*/
|
||
getAvailable(hasSchema = false) {
|
||
const available = [];
|
||
|
||
for (const [type, definition] of this.widgets.entries()) {
|
||
// If widget requires schema and we don't have one, skip it
|
||
if (definition.requiresSchema && !hasSchema) {
|
||
continue;
|
||
}
|
||
|
||
available.push({
|
||
type,
|
||
definition
|
||
});
|
||
}
|
||
|
||
console.log(`[WidgetRegistry] Found ${available.length} available widgets (hasSchema: ${hasSchema})`);
|
||
return available;
|
||
}
|
||
|
||
/**
|
||
* Get all registered widget types (regardless of schema requirement)
|
||
*
|
||
* @returns {Array<{type: string, definition: WidgetDefinition}>} All registered widgets
|
||
*/
|
||
getAll() {
|
||
const all = [];
|
||
for (const [type, definition] of this.widgets.entries()) {
|
||
all.push({ type, definition });
|
||
}
|
||
return all;
|
||
}
|
||
|
||
/**
|
||
* Check if widget type is registered
|
||
*
|
||
* @param {string} type - Widget type identifier
|
||
* @returns {boolean} True if widget type is registered
|
||
*/
|
||
has(type) {
|
||
return this.widgets.has(type);
|
||
}
|
||
|
||
/**
|
||
* Unregister a widget type
|
||
*
|
||
* @param {string} type - Widget type identifier
|
||
* @returns {boolean} True if widget was removed, false if not found
|
||
*
|
||
* @example
|
||
* registry.unregister('oldWidget');
|
||
*/
|
||
unregister(type) {
|
||
const existed = this.widgets.delete(type);
|
||
if (existed) {
|
||
console.log(`[WidgetRegistry] Unregistered widget: ${type}`);
|
||
} else {
|
||
console.warn(`[WidgetRegistry] Cannot unregister "${type}" - not found`);
|
||
}
|
||
return existed;
|
||
}
|
||
|
||
/**
|
||
* Get count of registered widgets
|
||
*
|
||
* @returns {number} Number of registered widgets
|
||
*/
|
||
count() {
|
||
return this.widgets.size;
|
||
}
|
||
|
||
/**
|
||
* Clear all registered widgets
|
||
*
|
||
* @returns {number} Number of widgets cleared
|
||
*/
|
||
clear() {
|
||
const count = this.widgets.size;
|
||
this.widgets.clear();
|
||
console.log(`[WidgetRegistry] Cleared ${count} widgets`);
|
||
return count;
|
||
}
|
||
|
||
/**
|
||
* Get statistics about registered widgets
|
||
*
|
||
* @returns {Object} Registry statistics
|
||
*/
|
||
getStats() {
|
||
const all = this.getAll();
|
||
const schemaRequired = all.filter(w => w.definition.requiresSchema).length;
|
||
const noSchema = all.length - schemaRequired;
|
||
|
||
return {
|
||
total: all.length,
|
||
requiresSchema: schemaRequired,
|
||
noSchema: noSchema,
|
||
types: all.map(w => w.type)
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Global widget registry instance
|
||
* @type {WidgetRegistry}
|
||
*/
|
||
export const widgetRegistry = new WidgetRegistry();
|