5a21a5aece
Complete redesign of Scene Info widget following UX best practices: BEFORE: - Tab-based interface with 5 separate views - Only 1 data point visible at a time (poor scannability) - Size: 2×3 (oversized, wasted vertical space) - Didn't fit in desktop side panel - Poor information density AFTER: - Grid-based layout showing all 5 data points simultaneously - High information density and scannability - Compact size: 2×2 (reduced from 2×3) - Inspired by Apple Widgets / Material Design patterns - Mobile-responsive with breakpoints at 1000px and 340px - Zero interaction needed - all data visible at once Changes: - sceneInfoWidget.js: Complete rewrite (390→309 lines) - Removed tab logic and state management - Added data formatting helpers (formatDate, formatTime, etc.) - Grid HTML structure with semantic CSS classes - Maintained inline editing for all fields - Simplified configuration - style.css: Added comprehensive grid styling (lines 2647-2811) - CSS Grid layout with named areas - Responsive typography and spacing - Hover states and focus styles - 2 mobile breakpoints for optimal scaling - defaultLayout.js: Updated Scene Info widget - Changed height: 3→2 rows - Adjusted Y positions for widgets below - Simplified config (removed view selection) Design Principles: - All information visible simultaneously (zero interaction) - High scannability for quick information gathering - Proper information density for simple data points - Grid structure: 2 columns, 3 rows (location full-width header) - Mobile-first responsive design Layout: ┌─────────────────────────────────┐ │ 📍 Location │ ├──────────────────┬──────────────┤ │ 📅 Date │ 🕐 Time │ ├──────────────────┼──────────────┤ │ 🌤️ Weather │ 🌡️ Temp │ └──────────────────┴──────────────┘
351 lines
12 KiB
JavaScript
351 lines
12 KiB
JavaScript
/**
|
|
* Default Dashboard Layout Generator
|
|
*
|
|
* Generates the default dashboard configuration for new users or when resetting layout.
|
|
* Maps existing v1.x panel structure to v2.0 widget dashboard.
|
|
*/
|
|
|
|
/**
|
|
* Generate default dashboard configuration
|
|
*
|
|
* Creates a two-tab layout optimized for 2-column side panel:
|
|
* - "Status" tab: User stats, modular info widgets (calendar, weather, temp, clock, location), present characters
|
|
* - "Inventory" tab: Full inventory widget
|
|
*
|
|
* All positions sized for 2-column grid (w: 1-2, full width = 2).
|
|
* Layout will adapt if panel width increases to 3-4 columns.
|
|
*
|
|
* @returns {Object} Default dashboard configuration
|
|
*/
|
|
export function generateDefaultDashboard() {
|
|
const dashboard = {
|
|
version: 2,
|
|
|
|
gridConfig: {
|
|
// Columns calculated dynamically by GridEngine (2-4 based on panel width)
|
|
// Mobile: always 2, Desktop: 2-4 based on width
|
|
columns: 2, // Default to 2 columns (will be recalculated on init)
|
|
rowHeight: 5, // rem units for responsive scaling (1080p → 4K → mobile)
|
|
gap: 0.75, // rem units (scales with screen DPI)
|
|
snapToGrid: true,
|
|
showGrid: true
|
|
},
|
|
|
|
tabs: [
|
|
// Tab 1: Status (User widgets only - compact and focused)
|
|
{
|
|
id: 'tab-status',
|
|
name: 'Status',
|
|
icon: 'fa-solid fa-user',
|
|
order: 0,
|
|
widgets: [
|
|
// Row 0: User Info (left) + User Mood (top right in 3-col)
|
|
{
|
|
id: 'widget-userinfo',
|
|
type: 'userInfo',
|
|
x: 0,
|
|
y: 0,
|
|
w: 2,
|
|
h: 1,
|
|
config: {}
|
|
},
|
|
{
|
|
id: 'widget-usermood',
|
|
type: 'userMood',
|
|
x: 2,
|
|
y: 0,
|
|
w: 1,
|
|
h: 1,
|
|
config: {}
|
|
},
|
|
// Row 1-2: User Stats (health/energy bars)
|
|
{
|
|
id: 'widget-userstats',
|
|
type: 'userStats',
|
|
x: 0,
|
|
y: 1,
|
|
w: 2,
|
|
h: 2,
|
|
config: {
|
|
statBarGradient: true
|
|
}
|
|
},
|
|
// Row 3-4: User Attributes
|
|
{
|
|
id: 'widget-userattributes',
|
|
type: 'userAttributes',
|
|
x: 0,
|
|
y: 3,
|
|
w: 2,
|
|
h: 2,
|
|
config: {}
|
|
}
|
|
]
|
|
},
|
|
// Tab 2: Scene (Combined scene info widget + events + characters)
|
|
{
|
|
id: 'tab-scene',
|
|
name: 'Scene',
|
|
icon: 'fa-solid fa-map',
|
|
order: 1,
|
|
widgets: [
|
|
// Row 0-1: Scene Info (combined: calendar, weather, temp, clock, location)
|
|
{
|
|
id: 'widget-sceneinfo',
|
|
type: 'sceneInfo',
|
|
x: 0,
|
|
y: 0,
|
|
w: 2,
|
|
h: 2,
|
|
config: {}
|
|
},
|
|
// Row 2-3: Recent Events (notebook style, full width)
|
|
{
|
|
id: 'widget-recentevents',
|
|
type: 'recentEvents',
|
|
x: 0,
|
|
y: 2,
|
|
w: 2,
|
|
h: 2,
|
|
config: {
|
|
maxEvents: 3
|
|
}
|
|
},
|
|
// Row 4-7: Present Characters (full width, will expand with auto-layout)
|
|
{
|
|
id: 'widget-presentchars',
|
|
type: 'presentCharacters',
|
|
x: 0,
|
|
y: 4,
|
|
w: 2,
|
|
h: 4,
|
|
config: {
|
|
cardLayout: 'grid',
|
|
showThoughtBubbles: true
|
|
}
|
|
}
|
|
]
|
|
},
|
|
// Tab 3: Inventory (Full tab for inventory system)
|
|
{
|
|
id: 'tab-inventory',
|
|
name: 'Inventory',
|
|
icon: 'fa-solid fa-bag-shopping',
|
|
order: 2,
|
|
widgets: [
|
|
{
|
|
id: 'widget-inventory',
|
|
type: 'inventory',
|
|
x: 0,
|
|
y: 0,
|
|
w: 2,
|
|
h: 6,
|
|
config: {
|
|
defaultSubTab: 'onPerson',
|
|
defaultViewMode: 'list'
|
|
}
|
|
}
|
|
]
|
|
},
|
|
// Tab 4: Quests (Full tab for quest system)
|
|
{
|
|
id: 'tab-quests',
|
|
name: 'Quests',
|
|
icon: 'fa-solid fa-scroll',
|
|
order: 3,
|
|
widgets: [
|
|
{
|
|
id: 'widget-quests',
|
|
type: 'quests',
|
|
x: 0,
|
|
y: 0,
|
|
w: 2,
|
|
h: 5,
|
|
config: {
|
|
defaultSubTab: 'main'
|
|
}
|
|
}
|
|
]
|
|
}
|
|
],
|
|
|
|
defaultTab: 'tab-status'
|
|
};
|
|
|
|
console.log('[DefaultLayout] Generated default dashboard configuration');
|
|
return dashboard;
|
|
}
|
|
|
|
/**
|
|
* Migrate v1.x settings to v2.0 dashboard
|
|
*
|
|
* Converts existing hardcoded panel structure to widget-based layout.
|
|
* Preserves user's visibility preferences and data.
|
|
*
|
|
* @param {Object} oldSettings - v1.x extension settings
|
|
* @returns {Object} Migrated dashboard configuration
|
|
*/
|
|
export function migrateV1ToV2Dashboard(oldSettings) {
|
|
console.log('[DefaultLayout] Migrating v1.x settings to v2.0 dashboard');
|
|
|
|
const dashboard = generateDefaultDashboard();
|
|
|
|
// Respect user's visibility preferences from v1.x
|
|
const statusTab = dashboard.tabs[0];
|
|
|
|
// Check trackerConfig for field-level disabling
|
|
const trackerConfig = oldSettings.trackerConfig;
|
|
|
|
// Remove userStats widget if hidden in v1.x OR all stats disabled in trackerConfig
|
|
const allStatsDisabled = trackerConfig?.userStats?.customStats
|
|
?.every(stat => !stat.enabled) ?? false;
|
|
|
|
if (!oldSettings.showUserStats || allStatsDisabled) {
|
|
statusTab.widgets = statusTab.widgets.filter(w => w.type !== 'userStats');
|
|
console.log('[DefaultLayout] Removed userStats widget', allStatsDisabled ? '(all stats disabled in trackerConfig)' : '(was hidden in v1.x)');
|
|
}
|
|
|
|
// Remove infoBox widget if hidden in v1.x
|
|
// Note: We keep individual info widgets (calendar, weather, etc.) even if fields are disabled
|
|
// because widgets will show disabled state with link to Tracker Settings
|
|
if (!oldSettings.showInfoBox) {
|
|
statusTab.widgets = statusTab.widgets.filter(w => w.type !== 'infoBox');
|
|
console.log('[DefaultLayout] Removed infoBox widget (was hidden in v1.x)');
|
|
}
|
|
|
|
// Remove presentCharacters widget if hidden in v1.x OR thoughts disabled in trackerConfig
|
|
const thoughtsDisabled = trackerConfig?.presentCharacters?.thoughts?.enabled === false;
|
|
|
|
if (!oldSettings.showCharacterThoughts || thoughtsDisabled) {
|
|
statusTab.widgets = statusTab.widgets.filter(w => w.type !== 'presentCharacters');
|
|
console.log('[DefaultLayout] Removed presentCharacters widget', thoughtsDisabled ? '(thoughts disabled in trackerConfig)' : '(was hidden in v1.x)');
|
|
}
|
|
|
|
// Remove inventory tab if it was hidden in v1.x
|
|
if (!oldSettings.showInventory) {
|
|
dashboard.tabs = dashboard.tabs.filter(t => t.id !== 'tab-inventory');
|
|
console.log('[DefaultLayout] Removed inventory tab (was hidden in v1.x)');
|
|
}
|
|
|
|
// If all widgets were hidden on status tab, remove it too
|
|
if (statusTab.widgets.length === 0) {
|
|
dashboard.tabs = dashboard.tabs.filter(t => t.id !== 'tab-status');
|
|
console.log('[DefaultLayout] Removed status tab (all widgets were hidden)');
|
|
|
|
// If we still have inventory tab, make it default
|
|
if (dashboard.tabs.length > 0) {
|
|
dashboard.defaultTab = dashboard.tabs[0].id;
|
|
}
|
|
}
|
|
|
|
console.log(`[DefaultLayout] Migration complete - ${dashboard.tabs.length} tabs, ${dashboard.tabs.reduce((sum, t) => sum + t.widgets.length, 0)} widgets`);
|
|
|
|
return dashboard;
|
|
}
|
|
|
|
/**
|
|
* Validate dashboard configuration
|
|
*
|
|
* Ensures dashboard config has all required fields and valid structure.
|
|
*
|
|
* @param {Object} dashboard - Dashboard configuration to validate
|
|
* @returns {boolean} True if valid, false otherwise
|
|
*/
|
|
export function validateDashboardConfig(dashboard) {
|
|
if (!dashboard) {
|
|
console.error('[DefaultLayout] Dashboard config is null or undefined');
|
|
return false;
|
|
}
|
|
|
|
if (!dashboard.version) {
|
|
console.error('[DefaultLayout] Dashboard config missing version');
|
|
return false;
|
|
}
|
|
|
|
if (!dashboard.gridConfig) {
|
|
console.error('[DefaultLayout] Dashboard config missing gridConfig');
|
|
return false;
|
|
}
|
|
|
|
if (!Array.isArray(dashboard.tabs)) {
|
|
console.error('[DefaultLayout] Dashboard tabs is not an array');
|
|
return false;
|
|
}
|
|
|
|
// Validate each tab
|
|
for (const tab of dashboard.tabs) {
|
|
if (!tab.id || !tab.name) {
|
|
console.error('[DefaultLayout] Tab missing id or name:', tab);
|
|
return false;
|
|
}
|
|
|
|
if (!Array.isArray(tab.widgets)) {
|
|
console.error('[DefaultLayout] Tab widgets is not an array:', tab);
|
|
return false;
|
|
}
|
|
|
|
// Validate each widget
|
|
for (const widget of tab.widgets) {
|
|
if (!widget.id || !widget.type) {
|
|
console.error('[DefaultLayout] Widget missing id or type:', widget);
|
|
return false;
|
|
}
|
|
|
|
if (typeof widget.x !== 'number' || typeof widget.y !== 'number') {
|
|
console.error('[DefaultLayout] Widget position invalid:', widget);
|
|
return false;
|
|
}
|
|
|
|
if (typeof widget.w !== 'number' || typeof widget.h !== 'number') {
|
|
console.error('[DefaultLayout] Widget size invalid:', widget);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get widget count in dashboard
|
|
*
|
|
* @param {Object} dashboard - Dashboard configuration
|
|
* @returns {number} Total number of widgets across all tabs
|
|
*/
|
|
export function getWidgetCount(dashboard) {
|
|
if (!dashboard || !Array.isArray(dashboard.tabs)) {
|
|
return 0;
|
|
}
|
|
|
|
return dashboard.tabs.reduce((sum, tab) => {
|
|
return sum + (Array.isArray(tab.widgets) ? tab.widgets.length : 0);
|
|
}, 0);
|
|
}
|
|
|
|
/**
|
|
* Find widget by ID across all tabs
|
|
*
|
|
* @param {Object} dashboard - Dashboard configuration
|
|
* @param {string} widgetId - Widget ID to find
|
|
* @returns {{tabIndex: number, widgetIndex: number, widget: Object}|null}
|
|
*/
|
|
export function findWidget(dashboard, widgetId) {
|
|
if (!dashboard || !Array.isArray(dashboard.tabs)) {
|
|
return null;
|
|
}
|
|
|
|
for (let tabIndex = 0; tabIndex < dashboard.tabs.length; tabIndex++) {
|
|
const tab = dashboard.tabs[tabIndex];
|
|
if (!Array.isArray(tab.widgets)) continue;
|
|
|
|
for (let widgetIndex = 0; widgetIndex < tab.widgets.length; widgetIndex++) {
|
|
const widget = tab.widgets[widgetIndex];
|
|
if (widget.id === widgetId) {
|
|
return { tabIndex, widgetIndex, widget };
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|