25 Commits
Author SHA1 Message Date
Pakobbix 03400ad5d5 Merge pull request 'Fix: comprehensive bug review — restore refactor regressions & fix bugs' (#24) from fix-comprehensive-bug-review into main
Reviewed-on: #24
2026-08-16 19:52:33 +00:00
ARIA 34553c18f7 Fix: comprehensive bug review — restore refactor regressions & fix critical/major/minor bugs
Critical:
- responseExtractor.js: fix import depth (7x ../ -> 6x) that broke the module
  graph for encounters, separate/external generation, and auto-avatars
- settingsListeners.js: restore ~25 undefined functions, ~40 lost event
  bindings, and the entire 'Initialize UI state' block dropped by the c14c141
  refactor; add initializeSettingsUIState()
- Restore all External API mode handlers (base-url, api-key, model,
  max-tokens, temperature, key-visibility, test-connection) + value init
- validator.js: convert CJS->ESM (crashed under type:module), fix
  glob.sync->globSync (v13 API), fix '!key in obj' precedence bugs

Major:
- sillytavern.js: add missing updateMessageBlock import
- infoBox.js: add missing saveSettings import
- userStats.js: fix double-escaped quantity regex

Minor:
- config.js: settingsVersion 6->7, restore showLockIcons, fix apiKey comment
- events.js: off() tracking + unregister/re-register on disable/enable
- escapeHtml: escape quotes (XSS) in all 3 copies
- template.html: data-prompt dialogue-coloring -> dialogueColoring
- trackerEditor.js: stale externalApiOnly -> sendAllEnabledOnRefresh
- suppression.js: fix '[object Object]' on empty instruct value
- persistence.js: await migrateToV3JSON (loadSettings now async)
- weatherEffects.js: JSON.parse -> repairJSON
- mobile.js: fix corrupted comment line
- Add missing debugMode setting; settings.html v3.7.2 -> v3.7.4

Dead code removed:
- Unused imports/vars (promptBuilder, jsonPromptHelpers, encounterPrompts,
  injector), withChangeDetection, trackJQueryHandler/cleanupJQueryEvents,
  renderThoughtsSidebarOnly, updateCheckpointButtonInMenu, addDiceQuickReply,
  setupRefreshButtonDrag/setupDebugButtonDrag (~430 lines)

Verified: 78/78 tests pass, style.css in sync, validator runs clean.
2026-08-16 21:42:14 +02:00
Pakobbix 6f906d0d97 Merge pull request 'Branch cleanup — remove 9 stale remote branches' (#23) from issue-22-branch-cleanup into main
Reviewed-on: #23
2026-07-13 10:17:01 +00:00
ARIA 3b78f284a1 Fixes #22: Document branch cleanup — removed 9 stale remote branches 2026-07-13 12:15:46 +02:00
Pakobbix 6e816e1436 Merge pull request 'Fixes #20: After change errors (JSON parse + equipment tab)' (#21) from issue-20-fix-json-parse-equipment into main
Reviewed-on: #21
2026-07-12 16:22:01 +00:00
ARIA 28ef82aae7 Fix: remove broken import in desktop.js that caused 404 and extension load failure 2026-07-12 18:14:59 +02:00
ARIA 59b125fd2a Fixes #20: Call updateSectionVisibility() during init to show equipment tab
The equipment section has display:none in CSS and updateSectionVisibility()
is the only function that calls .show() on it. It was never called during
initialization, only when settings toggles changed. Added the call to the
main init flow so the equipment tab becomes visible on page load.
2026-07-12 17:56:35 +02:00
ARIA 4bd29a207a Fixes #20: Fix JSON parse errors and empty equipment tab
- Replace JSON.parse() with repairJSON() in desktop.js and mobile.js
  to handle malformed JSON from infoBox/userStats data
- Add defensive initialization in renderEquipment(): ensure showEquipment
  defaults to true and equipment data structure exists
- Add explicit renderEquipment() call during initialization
2026-07-12 17:33:08 +02:00
Pakobbix e4bb48d9ea Update README.md 2026-07-12 15:09:11 +00:00
Pakobbix 8b5cf75396 Merge pull request 'Fixes #18: Update Readme & AGENTS.md' (#19) from issue-18-update-readme-agents into main
Reviewed-on: #19
2026-07-12 15:06:50 +00:00
ARIA c938916fd8 Fixes #18: Update README & AGENTS.md documentation
- README: Add Equipment System feature, Code Quality, Performance,
  Memory Management, Testing, Development Commands, and Tech Stack sections
- AGENTS.md: Update source tree with CSS modules, equipment constants,
  renderUtils, settings consolidation; add development commands,
  performance optimizations, memory management notes, validator factory;
  update file reference table

Based on PRs: #1 #3 #6 #8 #10 #12 #14 #17
2026-07-12 17:03:37 +02:00
Pakobbix f00475cb65 Merge pull request 'Fixes #16: Code quality enhancement Part 3' (#17) from issue-16-code-quality-enhancements into main
Reviewed-on: #17
2026-07-12 13:31:05 +00:00
ARIA 28802b1ad7 Fixes #16: Code quality enhancements Part 3
- Consolidate settings defaults: config.js is now single source of truth,
  state.js imports from config.js (eliminates ~300 lines of duplication)
- Validator factory: createStringValidator() unifies sanitizeLocationName/
  sanitizeItemName with configurable rules (blocked names, max length, rejects)
- Refactor promptBuilder: Extract getCharacterCardsInfo into buildNarratorCardInfo,
  buildGroupCardInfo, buildSingleCardInfo, appendCharacterFields
- Error handling: jsonRepair.js silent failures now log to console.debug
- Testing: Fix ESM support (babel.config.json + jest.config.cjs transform),
  add security.test.js with 24 tests for validator factory and security utilities
- Total: 78 tests passing (was 54)
2026-07-12 15:24:58 +02:00
Pakobbix 5b9c1ca0f1 Merge pull request 'Fixes #13: CSS performance optimizations (minification + extended custom properties)' (#14) from issue-13-css-performance into main
Reviewed-on: #14
2026-07-12 13:06:04 +00:00
ARIA df7d3b3a38 Fixes #13: CSS performance optimizations
- Add CSS minification support to build script (--minify flag)
  - Pure JS minifier: removes comments, whitespace, redundant chars
  - Reports size reduction percentage
  - Preserves CSS custom properties, data URIs, strings

- Refactor _08_themes.css: extend CSS custom properties pattern
  - Define theme-specific properties per theme (--rpg-content-bg,
    --rpg-content-box-shadow, --rpg-header-text-shadow, etc.)
  - Replace triple-theme selector duplication with single [data-theme]
    selectors — children inherit variables automatically
  - Reduces themes.css from 379 to ~220 lines
  - Reduces style.css from 305KB to 294KB (3.8% reduction)

- Add build:css:min npm script for production builds
- Add 9 tests for CSS build: minification, size reduction, CSS
  variable preservation, data URI preservation, theme refactoring
2026-07-12 14:55:36 +02:00
Pakobbix 29f7865927 Merge pull request 'Fixes #11: Memory management enhancements' (#12) from issue-11-memory-management into main
Reviewed-on: #12
2026-07-12 12:44:06 +00:00
ARIA 9c0f09619d Fixes #11: Memory management enhancements
- Add clearDomCache() for jQuery object cache invalidation on panel rebuild
- Track all event handlers in on()/once() for complete cleanup by unregisterAllEvents()
- Migrate direct eventSource.on() calls to tracked onEvent() in index.js, injector.js, sillyTavernExpressions.js
- Add clearDebugLogs() called on CHAT_CHANGED to prevent memory accumulation
- Add trackJQueryHandler()/cleanupJQueryEvents() infrastructure for jQuery event cleanup
- Add comprehensive memory management tests (14 new tests, all passing)
2026-07-12 14:31:14 +02:00
Pakobbix 3c4e632906 Merge pull request 'Enhancing code quality part 2.2: Parser Performance Optimizations' (#10) from issue-9-parser-performance into main
Reviewed-on: #10
2026-07-12 12:01:59 +00:00
ARIA 4e4b2328ba Fixes #9: Parser performance optimizations
- Pre-compile all regex patterns as module-level constants in parser.js and jsonRepair.js
- Add format detection caching (lastDetectedFormat, formatCacheHits) to skip redundant checks
- Add clearFormatCache() export for cache invalidation
- Add comprehensive test suite (33 tests) for parser and jsonRepair modules
- Fix regex escaping in dynamic patterns (statRegex, fieldRegex)
- Improve toFieldKey() regex specificity
2026-07-12 13:58:21 +02:00
Pakobbix c9d604ab68 Merge pull request 'Rendering performance optimizations (Issue #7 part 2.1)' (#8) from issue-7-rendering-performance into main
Reviewed-on: #8
2026-07-12 11:33:38 +00:00
ARIA f8894a9f3c Fix: Scope event handlers to container in userStats.js
Fixes critical bug identified by Zephyre_Review: event handlers used
global jQuery selectors instead of container-scoped selectors, causing
potential event handler leaks and scope mismatches.
2026-07-12 13:14:19 +02:00
ARIA 24c964c38d Fixes #7: Rendering performance optimizations
- requestAnimationFrame batching: rerenderRpgState() now batches all
  render calls into a single animation frame, reducing reflow/repaint costs

- Lightweight change detection: Render functions use updateIfChanged() to
  skip DOM updates when content hasn't changed, and only re-bind event
  handlers when the DOM was actually updated

- CSS content-visibility: Added content-visibility: auto with
  contain-intrinsic-size to inventory and equipment containers, deferring
  rendering of off-screen content

New file: src/systems/rendering/renderUtils.js (rendering utilities module)
Updated: All render modules + sillytavern.js integration + style.css
2026-07-12 13:10:17 +02:00
Pakobbix 1f8591d4ec Merge pull request 'Enhancing code quality part 1' (#6) from issue-5-code-quality-part1 into main
Reviewed-on: #6
2026-07-12 10:58:21 +00:00
ARIA 7e4461823f Fix: Pass required arguments to setupPlotButtons
- setupPlotButtons requires handlePlotClick and handleEncounterClick callbacks
- Pass sendPlotProgression and openEncounterModal as arguments
- removeAlternatePresentCharactersPanel is already correctly imported
2026-07-12 12:29:06 +02:00
ARIA c14c1417c1 Fixes #5: Refactor code quality - Part 1
- Split index.js from 1,567 lines to 456 lines (73% reduction)
  - Extracted settings event listeners to src/systems/ui/settingsListeners.js
  - Extracted settings panel to src/core/settingsPanel.js
  - Simplified initUI() and main initialization block

- Modularized style.css into 28 logical CSS modules in src/styles/
  - Added build script (npm run build:css) to concatenate modules
  - Modules: panel, components, user-stats, info-box, thoughts, settings,
    themes, modals, mobile, inventory, quests, equipment, encounters,
    weather, music-player, FAB widgets, strip widgets, etc.

- Updated package.json with build:css script and type: module

No functional changes - pure refactoring for maintainability.
2026-07-12 12:24:21 +02:00
81 changed files with 24204 additions and 2774 deletions
+97 -27
View File
@@ -2,18 +2,19 @@
## What this is
A **browser extension for SillyTavern** (AI roleplay frontend). It is NOT a standalone Node.js application. All JavaScript runs in the browser context inside SillyTavern. There is **no build step** — files are loaded directly by SillyTavern's extension loader.
A **browser extension for SillyTavern** (AI roleplay frontend). It is NOT a standalone Node.js application. All JavaScript runs in the browser context inside SillyTavern. There is **no bundler** JS files are loaded directly by SillyTavern's extension loader. CSS modules are concatenated via a build script.
---
## Quick start
- **Entry point**: `index.js` (loaded by SillyTavern via `manifest.json`)
- **CSS**: `style.css` (single file, ~12,300 lines)
- **CSS**: Modular — 28 files in `src/styles/`, concatenated via `npm run build:css` (or `npm run build:css:min` for production)
- **HTML templates**: `template.html` (main panel), `settings.html` (Extensions tab settings drawer)
- **Runtime**: Browser only. All code executes inside SillyTavern's page context.
- **jQuery**: Available globally as `$` / `jQuery`. Used extensively for DOM manipulation.
- **ES modules**: All `src/` files use `import`/`export` (browser ES module syntax).
- **Testing**: Jest + Babel ESM support — 78 tests (`npm test`)
---
@@ -76,11 +77,12 @@ The deeper relative imports in `src/` files (e.g., `../../../../../../script.js`
```
src/
├── core/ # Core infrastructure
│ ├── config.js # Extension name, folder path, default settings
│ ├── state.js # Centralized state variables + getters/setters
│ ├── config.js # Extension name, folder path, default settings (single source of truth)
│ ├── state.js # Centralized state variables + getters/setters (imports defaults from config.js)
│ ├── persistence.js # Save/load settings, chat data, migrations
│ ├── events.js # SillyTavern event system wrapper
── i18n.js # Translation system (fetches JSON, applies data-i18n-key)
── i18n.js # Translation system (fetches JSON, applies data-i18n-key)
│ └── settingsPanel.js # Extracted settings panel UI (from index.js refactor)
├── i18n/ # Translation JSON files
│ ├── en.json # English (reference, 513 lines)
│ ├── fr.json # French
@@ -88,10 +90,39 @@ src/
│ ├── zh-cn.json # Simplified Chinese
│ ├── zh-tw.json # Traditional Chinese
│ └── validator.js # Node.js script to validate translation consistency
├── styles/ # CSS modules (28 files, concatenated via build script)
│ ├── _01_variables.css # CSS custom properties
│ ├── _02_panel.css # Main panel layout
│ ├── _03_components.css # Buttons, inputs, common components
│ ├── _04_user-stats.css # User stats panel
│ ├── _05_info-box.css # Info box dashboard
│ ├── _06_thoughts.css # Character thoughts
│ ├── _07_settings.css # Settings modal
│ ├── _08_themes.css # Theme definitions (extended custom properties)
│ ├── _09_dice-modal.css # Dice roller modal
│ ├── _10_tracker-editor.css # Tracker configuration editor
│ ├── _11_settings-modal.css # Settings modal styles
│ ├── _12_thought-bubbles.css # Floating thought bubbles
│ ├── _13_mobile.css # Mobile responsive styles
│ ├── _14_inventory.css # Inventory views
│ ├── _15_inventory-views.css # Inventory detail views
│ ├── _16_desktop-tabs.css # Desktop tab navigation
│ ├── _17_quests.css # Quest tracking
│ ├── _18_equipment.css # Equipment grid
│ ├── _19_encounters.css # Combat encounters
│ ├── _20_plot-buttons.css # Plot progression buttons
│ ├── _21_music-player.css # Spotify music player
│ ├── _22_weather.css # Weather effects
│ ├── _23_holiday-promo.css # Holiday promotions
│ ├── _24_feature-toggles.css # Feature toggle switches
│ ├── _25_import-dialog.css # Import/export dialog
│ ├── _26_fab-widgets.css # Mobile FAB widgets
│ ├── _27_strip-widgets.css # Collapsed panel strip widgets
│ └── _28_equipment-modal.css # Equipment edit modal
├── systems/ # Feature systems (organized by concern)
│ ├── generation/ # AI prompt generation, parsing, API calls
│ │ ├── promptBuilder.js # Builds tracker prompts for AI
│ │ ├── parser.js # Parses AI responses to extract tracker data
│ │ ├── promptBuilder.js # Builds tracker prompts for AI (refactored with focused helpers)
│ │ ├── parser.js # Parses AI responses (~50 pre-compiled regex patterns)
│ │ ├── apiClient.js # Makes API calls for separate/external generation
│ │ ├── injector.js # Injects tracker prompts into generation context
│ │ ├── lockManager.js # Manages locked tracker fields
@@ -106,7 +137,8 @@ src/
│ │ ├── inventory.js # Renders inventory section
│ │ ├── equipment.js # Renders equipment section
│ │ ├── quests.js # Renders quests section
│ │ ── musicPlayer.js # Renders Spotify music player
│ │ ── musicPlayer.js # Renders Spotify music player
│ │ └── renderUtils.js # Rendering utilities (updateIfChanged cache, rAF batching)
│ ├── ui/ # UI components and helpers
│ │ ├── theme.js # Theme system (default, sci-fi, fantasy, cyberpunk, custom)
│ │ ├── modals.js # Settings modal, dice modal, deprecation modal
@@ -119,7 +151,10 @@ src/
│ │ ├── encounterUI.js # Combat encounter modal
│ │ ├── snowflakes.js # Snowflake animation effect
│ │ ├── weatherEffects.js # Dynamic weather visual effects
│ │ ── alternatePresentCharacters.js # Below-chat character panel
│ │ ── alternatePresentCharacters.js # Below-chat character panel
│ │ └── settingsListeners.js # Settings event listeners (extracted from index.js)
│ ├── equipment/ # Equipment system
│ │ └── constants.js # Equipment slot definitions, type limits, stat config
│ ├── integration/ # SillyTavern event integration
│ │ ├── sillytavern.js # Event handlers: message sent/received, swipe, etc.
│ │ └── thoughtBasedExpressions.js # Integrates with ST Character Expressions
@@ -144,11 +179,11 @@ src/
├── imageUrls.js # Image URL utilities
├── itemParser.js # Item string parsing
├── jsonMigration.js # v2 to v3 JSON format migration
├── jsonRepair.js # JSON repair for malformed AI responses
├── jsonRepair.js # JSON repair for malformed AI responses (14 pre-compiled regex)
├── migration.js # Settings migration helpers
├── presentCharacters.js # Present character data helpers
├── responseExtractor.js # Extract tracker data from AI responses
├── security.js # Input validation and sanitization
├── security.js # Input validation and sanitization (validator factory)
├── sillyTavernExpressions.js # ST expression mapping
├── thoughtBasedExpressionPortraits.js # Expression-to-portrait mapping
└── transformations.js # Data transformation utilities
@@ -163,7 +198,7 @@ src/
5. **External API mode**: Uses user-configured external API endpoint
6. **AI responds**`MESSAGE_RECEIVED` event fires
7. **Extension hooks** `onMessageReceived``parser.js` extracts tracker data
8. **Rendering**: `renderUserStats()`, `renderInfoBox()`, `renderThoughts()`, etc. update the UI
8. **Rendering**: `renderUserStats()`, `renderInfoBox()`, `renderThoughts()`, etc. update the UI (batched via rAF)
9. **Persistence**: Tracker data is saved to `chat_metadata` per chat, and per-swipe
### State management
@@ -178,6 +213,8 @@ All extension state lives in `src/core/state.js` as module-level `let` variables
- `pendingDiceRoll` — dice roll result to inject into next generation
- DOM element caches: `$panelContainer`, `$userStatsContainer`, etc.
**Settings consolidation**: `config.js` is the single source of truth for default settings. `state.js` imports defaults from `config.js` (~300 lines eliminated from duplication).
### Persistence
`src/core/persistence.js` handles:
@@ -187,20 +224,35 @@ All extension state lives in `src/core/state.js` as module-level `let` variables
- Settings versioning: automatic migration via `CURRENT_SETTINGS_VERSION` (currently 7)
- Deferred saves: batches chat data saves to avoid excessive writes
### Memory management
- `clearDomCache()`: Invalidates jQuery object cache on panel rebuild
- Event tracking: All `on()`/`once()` handlers tracked for cleanup via `unregisterAllEvents()`
- `clearDebugLogs()`: Called on `CHAT_CHANGED` to prevent unbounded debug log growth
### Performance optimizations
- **Rendering**: `requestAnimationFrame` batching in `rerenderRpgState()`, `updateIfChanged()` cache comparison in `renderUtils.js`, CSS `content-visibility: auto` on inventory/equipment containers
- **Parser**: ~64 regex patterns pre-compiled as module-level constants, format detection caching with `lastDetectedFormat` (3-hit threshold)
- **CSS**: Minification via `--minify` flag, extended CSS custom properties, theme refactoring (379 → ~220 lines)
---
## Development commands
```bash
# Validate translation files (check missing keys, type mismatches, empty values)
npm run validate_locale_once
# Build CSS from modular sources
npm run build:css # Concatenate CSS modules into style.css
npm run build:css:min # Minified production build (~50% smaller)
# Watch mode: re-validate on file changes
npm run validate_locale
# Testing
npm test # Run Jest test suite (78 tests)
# Translation validation
npm run validate_locale # Watch mode: re-validate on file changes
npm run validate_locale_once # Single validation run
```
No build, lint, test, or typecheck commands exist. The code runs directly in the browser.
---
## i18n system
@@ -256,6 +308,14 @@ All CSS classes use `rpg-` prefix to avoid conflicts with SillyTavern and other
- `.rpg-panel`, `.rpg-section`, `.rpg-stat-bar`, `.rpg-btn-primary`, etc.
- CSS custom properties: `--rpg-bg`, `--rpg-accent`, `--rpg-text`, `--rpg-highlight`, `--rpg-border`, `--rpg-shadow`
### CSS build pipeline
CSS modules in `src/styles/` are concatenated in numeric order by `scripts/build-css.js`. Modules follow the naming convention `_NN_name.css` where NN is the load order. After modifying any CSS module, run `npm run build:css` to regenerate `style.css`.
### Validator factory
Input sanitization uses `createStringValidator()` from `src/utils/security.js` — a factory pattern that unifies `sanitizeLocationName`/`sanitizeItemName` with configurable rules (max length, allowed characters, etc.).
### Panel structure
The main panel (`template.html`) has these sections in order:
@@ -290,7 +350,7 @@ The `skipInjectionsForGuided` setting controls tracker injection behavior during
2. **Relative import depth matters**: Files in `src/` use deeper relative paths (`../../../../../../script.js`) than `index.js` (`../../../../script.js`). When moving files between directories, adjust accordingly.
3. **Settings merge**: On load, saved settings are deep-merged with defaults from `src/core/state.js`. New settings fields should be added to both the state module (runtime defaults) AND `src/core/config.js` (documentation defaults).
3. **Settings merge**: On load, saved settings are deep-merged with defaults. `config.js` is the single source of truth for default settings — `state.js` imports from it. New settings fields should be added to `config.js`.
4. **Chat metadata**: Per-chat tracker data lives in `chat_metadata.rpg_companion`. Per-swipe data lives on individual chat message objects in the `swipe_store`.
@@ -302,9 +362,13 @@ The `skipInjectionsForGuided` setting controls tracker injection behavior during
8. **External API key**: Stored in `localStorage` as `rpg_companion_external_api_key`, NOT in extension settings (for security).
9. **The `package.json` has no runtime dependencies**. The `devDependencies` (chokidar, fs-extra, glob) are only for the i18n validator.
9. **Dev dependencies**: `package.json` has devDependencies (Jest, Babel, chokidar, fs-extra, glob, execa) for testing and i18n validation. No runtime dependencies — the extension runs entirely in the browser.
10. **Deprecation status**: The README states this extension is deprecated in favor of Marinara Engine. The code shows a deprecation modal that displays on first load.
10. **CSS rebuild required**: After modifying any file in `src/styles/`, run `npm run build:css` to regenerate `style.css`. SillyTavern loads `style.css`, not the individual modules.
11. **Deprecation status**: The README states this extension is deprecated in favor of Marinara Engine. The code shows a deprecation modal that displays on first load.
12. **ESM testing**: Jest uses Babel for ESM transformation (`babel.config.json` + `jest.config.cjs`). All test files use ES module syntax.
---
@@ -312,19 +376,25 @@ The `skipInjectionsForGuided` setting controls tracker injection behavior during
| File | Purpose |
|------|---------|
| `index.js` | Main entry point. All imports, UI init, event registration, startup logic. |
| `index.js` | Main entry point. Imports, UI init, event registration, startup logic (~456 lines after refactor). |
| `manifest.json` | SillyTavern extension manifest (name, loading order, JS/CSS files). |
| `style.css` | All CSS (~12,300 lines). Themes, animations, responsive breakpoints. |
| `style.css` | Built CSS output (concatenated from `src/styles/` modules). Do not edit directly. |
| `template.html` | Main panel HTML template. Rendered by SillyTavern's `renderExtensionTemplateAsync`. |
| `settings.html` | Extensions tab settings drawer. Minimal — most settings are in the panel modal. |
| `src/core/state.js` | Central state module. All extension-wide variables live here. |
| `scripts/build-css.js` | CSS build script. Concatenates modules, supports `--minify` flag. |
| `src/core/state.js` | Central state module. Imports defaults from config.js. All extension-wide variables. |
| `src/core/persistence.js` | Settings/chat data save/load, version migrations. |
| `src/core/config.js` | Extension name, path detection, default settings documentation. |
| `src/core/config.js` | Extension name, path detection, default settings (single source of truth). |
| `src/core/events.js` | SillyTavern event system wrapper with bulk register/unregister. |
| `src/core/i18n.js` | Translation loader and DOM applier. |
| `src/core/settingsPanel.js` | Extracted settings panel UI (from index.js refactor, PR #6). |
| `src/systems/integration/sillytavern.js` | All SillyTavern event handlers (message lifecycle, swipes, chat changes). |
| `src/systems/generation/promptBuilder.js` | Builds AI prompts for tracker generation. |
| `src/systems/generation/parser.js` | Parses AI responses to extract tracker JSON. |
| `src/systems/generation/promptBuilder.js` | Builds AI prompts for tracker generation (refactored with focused helpers). |
| `src/systems/generation/parser.js` | Parses AI responses to extract tracker JSON (~50 pre-compiled regex patterns). |
| `src/systems/generation/apiClient.js` | API client for separate/external generation modes. |
| `src/systems/generation/injector.js` | Injects tracker prompts into SillyTavern's generation context. |
| `src/systems/rendering/renderUtils.js` | Rendering utilities: `updateIfChanged()` cache, rAF batching. |
| `src/systems/equipment/constants.js` | Equipment slot definitions, type limits, stat configuration. |
| `src/utils/security.js` | Input validation, sanitization, validator factory (`createStringValidator()`). |
| `src/utils/jsonRepair.js` | JSON repair for malformed AI responses (14 pre-compiled regex patterns). |
| `src/i18n/validator.js` | Node.js script for validating translation consistency. |
+13
View File
@@ -0,0 +1,13 @@
# Branch Cleanup — July 13, 2026
The following stale remote branches were deleted, leaving only `main`:
- `SpicyMarinara-patch-1`
- `feature/equipment-system`
- `pr-109`
- `revert-111-main`
- `revert-116-revert-111-main`
- `revert-36-feat/v2-widget-dashboard-system`
- `revert-40-feat/responsive-dashboard-layout`
- `revert-59-main`
- `test-pr90-pr91-combined`
+47 -14
View File
@@ -2,16 +2,8 @@
An immersive RPG extension for browsers that tracks character stats, scene information, and character thoughts in a beautiful, customizable UI panel. All automated! Works with any preset. Choose between Together or Separate generation modes for context and generation control.
[![My Discord](https://img.shields.io/badge/Discord-Join%20Server-7289da)](https://discord.com/invite/KdAkTg94ME)
[![Support Me](https://img.shields.io/badge/Ko--fi-Support%20Creator-ff5e5b)](https://ko-fi.com/marinara_spaghetti)
## 🆕 What's New
### DEPRECATED
Moving on to developing the Marinara Engine frontend, the extension will now be maintained by the community!
<https://github.com/Pasta-Devs/Marinara-Engine>
[![Marinara Discord](https://img.shields.io/badge/Discord-Join%20Server-7289da)](https://discord.com/invite/KdAkTg94ME)
[![Support Original Author](https://img.shields.io/badge/Ko--fi-Support%20Creator-ff5e5b)](https://ko-fi.com/marinara_spaghetti)
## 📥 Installation
@@ -21,7 +13,7 @@ Moving on to developing the Marinara Engine frontend, the extension will now be
3. Go to Install extension
4. Copy-paste this link: <https://github.com/SpicyMarinara/rpg-companion-sillytavern>
4. Copy-paste this link: <https://gitea.zephyre.one/Pakobbix/rpg-companion-sillytavern>
5. Press Install for all users/Install just for me
@@ -41,6 +33,7 @@ Moving on to developing the Marinara Engine frontend, the extension will now be
- **🎭 Floating Thought Bubbles**: Optional thought bubbles positioned next to character avatars in chat
- **🎲 Classic RPG Stats**: STR, DEX, CON, INT, WIS, CHA attributes with dice roll support
- **📦 Advanced Inventory System**: Multi-location storage (On Person, Stored locations, Assets) with v2 format
- **⚔️ Equipment System**: 19 equipment slots (Helmet, Necklace, Body Armor, Gloves, Pants, Shoes, 10 Rings, 3 Accessories) with slot-type validation, stat bonuses (+99 max), and automatic slot assignment
- **🎯 Character Stats**: Track health, energy, or any custom stats for each present character with color interpolation
- **📜 Immersive HTML**: Enhance the immersion by including creative HTML/CSS/JS elements in your roleplay
- **➡️ Plot Progression**: Progress the plot with randomized events or natural progression with a click of a button
@@ -226,6 +219,49 @@ Choose from 6 beautiful themes:
If you ever have an awesome idea to do your own SillyTavern extension, don't.
### Code Quality
- **index.js refactored**: 1567 lines → 456 lines (73% reduction) through modular extraction
- **CSS modularized**: 28 logical modules in `src/styles/` with automated build script
- **Settings consolidation**: `config.js` as single source of truth — state.js imports from it (~300 lines eliminated)
- **Validator factory pattern**: `createStringValidator()` unifies `sanitizeLocationName`/`sanitizeItemName`
- **Prompt builder refactored**: `getCharacterCardsInfo` extracted into focused helper functions
- **Error visibility**: `jsonRepair.js` silent failures now log to `console.debug`
### Performance
- **Rendering**: `requestAnimationFrame` batching in `rerenderRpgState()`, `updateIfChanged()` cache in `renderUtils.js`, CSS `content-visibility: auto` on inventory/equipment containers
- **Parser**: Regex pre-compilation (~64 patterns in `parser.js` + `jsonRepair.js` as module-level constants), format detection caching (`lastDetectedFormat` with 3-hit threshold), `clearFormatCache()` for invalidation
- **CSS**: Minification support (`--minify` flag in build script), extended CSS custom properties, theme refactoring (379 → ~220 lines), `style.css` reduced from 305.4 KB → 293.9 KB (3.8%), minified build adds ~50%+ further reduction
### Memory Management
- **`clearDomCache()`**: Invalidates jQuery object cache on panel rebuild to prevent stale references
- **Event tracking**: All `on()`/`once()` handlers tracked for cleanup via `unregisterAllEvents()`
- **`clearDebugLogs()`**: Called on `CHAT_CHANGED` to prevent unbounded debug log growth
### Testing
- **78 tests passing** (up from 54) using Jest with Babel ESM support
- ESM compatibility via `babel.config.json` + `jest.config.cjs` transform configuration
- Security-focused test suite (`security.test.js`, 24 tests)
### Development Commands
```bash
npm run build:css # Concatenate CSS modules
npm run build:css:min # Minified production build
npm test # Run Jest test suite
npm run validate_locale # Watch mode translation validation
npm run validate_locale_once
```
### Tech Stack
- `"type": "module"` — ES modules throughout
- Jest + Babel for testing with full ESM support
- Modular CSS build pipeline with minification support
## 🐛 Troubleshooting
### Extension doesn't appear
@@ -291,6 +327,3 @@ SpicyMarinara, Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDea
---
Made with ❤️ by Marinara
PS I'm looking for a job or a sponsor to fund my custom AI frontend, contact me if interested:
[mgrabower97@gmail.com](mailto:mgrabower97@gmail.com)
+1
View File
@@ -0,0 +1 @@
export const saveSettings = jest.fn();
+147
View File
@@ -0,0 +1,147 @@
/**
* Tests for CSS build script (scripts/build-css.js)
* Tests minification logic and build output.
*/
const fs = require('fs-extra');
const path = require('path');
const { execSync } = require('child_process');
const rootDir = path.resolve(__dirname, '..');
const stylesDir = path.join(rootDir, 'src', 'styles');
const outputFile = path.join(rootDir, 'style.css');
describe('CSS Build Script', () => {
let originalContent;
beforeAll(async () => {
// Save original style.css for restoration
if (await fs.pathExists(outputFile)) {
originalContent = await fs.readFile(outputFile, 'utf-8');
}
});
afterAll(async () => {
// Restore original style.css
if (originalContent) {
await fs.writeFile(outputFile, originalContent, 'utf-8');
}
});
test('styles directory contains CSS files', async () => {
const files = await fs.readdir(stylesDir);
const cssFiles = files.filter(f => f.endsWith('.css'));
expect(cssFiles.length).toBeGreaterThan(0);
expect(cssFiles).toContain('_variables.css');
expect(cssFiles).toContain('_08_themes.css');
});
test('build:css produces readable output with comments', () => {
const result = execSync('node scripts/build-css.js', { cwd: rootDir, encoding: 'utf-8' });
expect(result).toContain('Building CSS from src/styles/');
const content = fs.readFileSync(outputFile, 'utf-8');
// Readable build should contain section comments
expect(content).toContain('/* ============================================ */');
expect(content).toContain('/* _variables.css */');
});
test('build:css --minify produces minified output', () => {
const result = execSync('node scripts/build-css.js --minify', { cwd: rootDir, encoding: 'utf-8' });
expect(result).toContain('minified');
expect(result).toContain('Minification:');
const content = fs.readFileSync(outputFile, 'utf-8');
// Minified build should NOT contain section comments
expect(content).not.toContain('/* ============================================ */');
// Should not have multiple consecutive spaces (except in strings)
expect(content).not.toMatch(/[^\S\r\n]{2,}/);
// Should not have newlines between rules
const lines = content.split('\n');
expect(lines.length).toBeLessThan(1000); // Original is ~12000 lines
});
test('minified output is smaller than readable output', () => {
// Build readable
execSync('node scripts/build-css.js', { cwd: rootDir, encoding: 'utf-8' });
const readableStats = fs.statSync(outputFile);
const readableSize = readableStats.size;
// Build minified
execSync('node scripts/build-css.js --minify', { cwd: rootDir, encoding: 'utf-8' });
const minifiedStats = fs.statSync(outputFile);
const minifiedSize = minifiedStats.size;
expect(minifiedSize).toBeLessThan(readableSize);
// Expect at least 30% reduction (comments + whitespace)
const reduction = ((readableSize - minifiedSize) / readableSize) * 100;
expect(reduction).toBeGreaterThan(30);
// Restore readable version for other tests
execSync('node scripts/build-css.js', { cwd: rootDir, encoding: 'utf-8' });
});
test('minified output preserves CSS custom properties', () => {
execSync('node scripts/build-css.js --minify', { cwd: rootDir, encoding: 'utf-8' });
const content = fs.readFileSync(outputFile, 'utf-8');
// CSS variables should be preserved
expect(content).toContain('--rpg-bg');
expect(content).toContain('--rpg-accent');
expect(content).toContain('--rpg-text');
expect(content).toContain('--rpg-highlight');
expect(content).toContain('--rpg-border');
expect(content).toContain('--rpg-shadow');
});
test('minified output preserves data URIs', () => {
execSync('node scripts/build-css.js --minify', { cwd: rootDir, encoding: 'utf-8' });
const content = fs.readFileSync(outputFile, 'utf-8');
// Fantasy theme has SVG data URI — should be preserved
expect(content).toContain('data:image/svg+xml');
});
test('minified output preserves data-theme selectors', () => {
execSync('node scripts/build-css.js --minify', { cwd: rootDir, encoding: 'utf-8' });
const content = fs.readFileSync(outputFile, 'utf-8');
expect(content).toContain('[data-theme="sci-fi"]');
expect(content).toContain('[data-theme="fantasy"]');
expect(content).toContain('[data-theme="cyberpunk"]');
});
test('themes.css uses grouped [data-theme] selectors (no duplication)', async () => {
const themesContent = await fs.readFile(path.join(stylesDir, '_08_themes.css'), 'utf-8');
// Should use [data-theme] grouped selectors instead of repeating all 3 themes
expect(themesContent).toContain('.rpg-panel[data-theme] .rpg-tabs-nav');
expect(themesContent).toContain('.rpg-panel[data-theme] .rpg-tab-btn');
// Should NOT have the old pattern of listing all 3 themes for every element
const oldPattern = /\.rpg-panel\[data-theme="sci-fi"\] \.rpg-tabs-nav,\s*\.rpg-panel\[data-theme="fantasy"\] \.rpg-tabs-nav,\s*\.rpg-panel\[data-theme="cyberpunk"\] \.rpg-tabs-nav/;
expect(themesContent).not.toMatch(oldPattern);
});
test('themes.css defines extended custom properties per theme', async () => {
const themesContent = await fs.readFile(path.join(stylesDir, '_08_themes.css'), 'utf-8');
// Extended properties for content box
expect(themesContent).toContain('--rpg-content-bg');
expect(themesContent).toContain('--rpg-content-box-shadow');
expect(themesContent).toContain('--rpg-content-border');
// Extended properties for headers
expect(themesContent).toContain('--rpg-header-text-shadow');
expect(themesContent).toContain('--rpg-header-font-family');
// Extended properties for dividers
expect(themesContent).toContain('--rpg-divider-bg');
expect(themesContent).toContain('--rpg-divider-content');
// Extended properties for thoughts
expect(themesContent).toContain('--rpg-thoughts-bg');
});
});
+136
View File
@@ -0,0 +1,136 @@
import { repairJSON, extractJSONFromText, validateJSONSchema, safeParseJSON } from '../src/utils/jsonRepair.js';
describe('repairJSON', () => {
test('parses valid JSON', () => {
const input = '{"name": "test", "value": 42}';
const result = repairJSON(input);
expect(result).toEqual({ name: 'test', value: 42 });
});
test('handles trailing commas', () => {
const input = '{"name": "test", "value": 42,}';
const result = repairJSON(input);
expect(result).toEqual({ name: 'test', value: 42 });
});
test('handles markdown code fences', () => {
const input = '```json\n{"name": "test"}\n```';
const result = repairJSON(input);
expect(result).toEqual({ name: 'test' });
});
test('removes thinking tags', () => {
const input = '```json\n{"name": "test"}\n```';
const result = repairJSON(input);
expect(result).toEqual({ name: 'test' });
});
test('returns null for invalid input', () => {
expect(repairJSON(null)).toBeNull();
expect(repairJSON(123)).toBeNull();
expect(repairJSON('')).toBeNull();
expect(repairJSON('not json at all')).toBeNull();
});
test('handles JSON arrays', () => {
const input = '[1, 2, 3]';
const result = repairJSON(input);
expect(result).toEqual([1, 2, 3]);
});
test('handles nested objects', () => {
const input = '{"outer": {"inner": {"deep": true}}}';
const result = repairJSON(input);
expect(result).toEqual({ outer: { inner: { deep: true } } });
});
test('removes JavaScript comments', () => {
const input = '{"name": "test"} // comment';
const result = repairJSON(input);
expect(result).toEqual({ name: 'test' });
});
});
describe('extractJSONFromText', () => {
test('extracts from json code fence', () => {
const input = 'Here is some text\n```json\n{"key": "value"}\n```\nMore text';
const result = extractJSONFromText(input);
expect(result).toBe('{"key": "value"}');
});
test('extracts from generic code fence', () => {
const input = '```{"key": "value"}```';
const result = extractJSONFromText(input);
expect(result).toBe('{"key": "value"}');
});
test('extracts standalone JSON object', () => {
const input = 'Some text {"key": "value"} more text';
const result = extractJSONFromText(input);
expect(result).toBe('{"key": "value"}');
});
test('extracts standalone JSON array', () => {
const input = 'Some text [1, 2, 3] more text';
const result = extractJSONFromText(input);
expect(result).toBe('[1, 2, 3]');
});
test('returns null for no JSON', () => {
expect(extractJSONFromText('no json here')).toBeNull();
expect(extractJSONFromText(null)).toBeNull();
expect(extractJSONFromText(123)).toBeNull();
});
});
describe('validateJSONSchema', () => {
test('validates userStats schema', () => {
const valid = { stats: [{ id: 'health', name: 'Health', value: 100 }] };
const invalid = { stats: [{ id: 'health', name: 'Health' }] }; // missing value
expect(validateJSONSchema(valid, 'userStats')).toBe(true);
expect(validateJSONSchema(invalid, 'userStats')).toBe(false);
});
test('validates infoBox schema', () => {
const valid = { date: '2024-01-01' };
const invalid = { unrelated: 'data' };
// Note: validateJSONSchema returns the truthy field value, not strictly true
expect(validateJSONSchema(valid, 'infoBox')).toBeTruthy();
expect(validateJSONSchema(invalid, 'infoBox')).toBeFalsy();
});
test('validates characters schema', () => {
const valid = { characters: [{ name: 'Alice' }] };
const invalid = { characters: [{ noName: true }] };
expect(validateJSONSchema(valid, 'characters')).toBe(true);
expect(validateJSONSchema(invalid, 'characters')).toBe(false);
});
test('returns false for invalid types', () => {
expect(validateJSONSchema(null, 'userStats')).toBe(false);
expect(validateJSONSchema('string', 'userStats')).toBe(false);
expect(validateJSONSchema({}, 'unknown')).toBe(false);
});
});
describe('safeParseJSON', () => {
test('successfully parses and validates', () => {
const result = safeParseJSON('{"stats": [{"id": "health", "name": "Health", "value": 100}]}', 'userStats');
expect(result.success).toBe(true);
expect(result.data).toEqual({ stats: [{ id: 'health', name: 'Health', value: 100 }] });
expect(result.error).toBeNull();
});
test('returns error when no JSON found', () => {
const result = safeParseJSON('no json here');
expect(result.success).toBe(false);
expect(result.error).toBe('No JSON found in text');
});
test('returns error when schema validation fails', () => {
const result = safeParseJSON('{"unrelated": "data"}', 'userStats');
expect(result.success).toBe(false);
expect(result.error).toContain('does not match expected schema');
expect(result.data).not.toBeNull(); // Still returns data
});
});
+240
View File
@@ -0,0 +1,240 @@
/**
* Memory Management Tests
* Tests DOM cache invalidation, event listener cleanup, and debug log management
*/
describe('DOM Cache Invalidation', () => {
test('clearDomCache sets all jQuery references to null', () => {
// Simulate the clearDomCache logic
const domCache = {
$panelContainer: { isPanel: true },
$userStatsContainer: { isStats: true },
$infoBoxContainer: { isInfoBox: true },
$thoughtsContainer: { isThoughts: true },
$inventoryContainer: { isInventory: true },
$questsContainer: { isQuests: true },
$musicPlayerContainer: { isMusic: true },
$equipmentContainer: { isEquipment: true }
};
// Simulate clearDomCache
domCache.$panelContainer = null;
domCache.$userStatsContainer = null;
domCache.$infoBoxContainer = null;
domCache.$thoughtsContainer = null;
domCache.$inventoryContainer = null;
domCache.$questsContainer = null;
domCache.$musicPlayerContainer = null;
domCache.$equipmentContainer = null;
// Verify all are null
expect(domCache.$panelContainer).toBeNull();
expect(domCache.$userStatsContainer).toBeNull();
expect(domCache.$infoBoxContainer).toBeNull();
expect(domCache.$thoughtsContainer).toBeNull();
expect(domCache.$inventoryContainer).toBeNull();
expect(domCache.$questsContainer).toBeNull();
expect(domCache.$musicPlayerContainer).toBeNull();
expect(domCache.$equipmentContainer).toBeNull();
});
test('clearDomCache is idempotent (safe to call multiple times)', () => {
const cache = { $panelContainer: null };
// Call clear twice - should not throw
cache.$panelContainer = null;
cache.$panelContainer = null;
expect(cache.$panelContainer).toBeNull();
});
});
describe('Debug Log Memory Management', () => {
test('debugLogs array is capped at 100 entries', () => {
const debugLogs = [];
const MAX_LOGS = 100;
// Add 150 entries
for (let i = 0; i < 150; i++) {
debugLogs.push({ timestamp: new Date().toISOString(), message: `Log ${i}` });
if (debugLogs.length > MAX_LOGS) {
debugLogs.shift();
}
}
expect(debugLogs.length).toBe(100);
expect(debugLogs[0].message).toBe('Log 50'); // First 50 were shifted out
expect(debugLogs[99].message).toBe('Log 149');
});
test('clearDebugLogs empties the array', () => {
const debugLogs = [
{ timestamp: '2024-01-01T00:00:00Z', message: 'Log 1' },
{ timestamp: '2024-01-01T00:00:01Z', message: 'Log 2' },
{ timestamp: '2024-01-01T00:00:02Z', message: 'Log 3' }
];
// Simulate clearDebugLogs: debugLogs.length = 0
debugLogs.length = 0;
expect(debugLogs.length).toBe(0);
});
test('debugLogs cleared on chat change prevents memory accumulation', () => {
const debugLogs = [];
const MAX_LOGS = 100;
// Simulate multiple chat sessions
for (let chat = 0; chat < 5; chat++) {
// Clear on chat change
debugLogs.length = 0;
// Add logs during this chat session
for (let i = 0; i < 30; i++) {
debugLogs.push({ timestamp: new Date().toISOString(), message: `Chat ${chat} Log ${i}` });
if (debugLogs.length > MAX_LOGS) {
debugLogs.shift();
}
}
}
// After 5 chats with clearing, we should only have logs from the last chat
expect(debugLogs.length).toBe(30);
expect(debugLogs[0].message).toBe('Chat 4 Log 0');
});
});
describe('Event Listener Cleanup', () => {
test('registered handlers are tracked for cleanup', () => {
// Simulate the registeredHandlers Map from events.js
const registeredHandlers = new Map();
function trackHandler(eventType, handler) {
if (!registeredHandlers.has(eventType)) {
registeredHandlers.set(eventType, []);
}
registeredHandlers.get(eventType).push(handler);
}
const handler1 = () => {};
const handler2 = () => {};
trackHandler('MESSAGE_SENT', handler1);
trackHandler('MESSAGE_SENT', handler2);
trackHandler('CHAT_CHANGED', handler1);
expect(registeredHandlers.get('MESSAGE_SENT').length).toBe(2);
expect(registeredHandlers.get('CHAT_CHANGED').length).toBe(1);
});
test('unregisterAllEvents clears all tracked handlers', () => {
const registeredHandlers = new Map();
function trackHandler(eventType, handler) {
if (!registeredHandlers.has(eventType)) {
registeredHandlers.set(eventType, []);
}
registeredHandlers.get(eventType).push(handler);
}
trackHandler('MESSAGE_SENT', () => {});
trackHandler('CHAT_CHANGED', () => {});
// Simulate unregisterAllEvents
registeredHandlers.clear();
expect(registeredHandlers.size).toBe(0);
});
test('on() function tracks handlers for cleanup', () => {
const registeredHandlers = new Map();
// Simulate the on() function with tracking
function on(eventType, handler) {
// eventSource.on(eventType, handler); // Would call real event source
if (!registeredHandlers.has(eventType)) {
registeredHandlers.set(eventType, []);
}
registeredHandlers.get(eventType).push(handler);
}
const handler = () => {};
on('GENERATE_BEFORE_COMBINE_PROMPTS', handler);
expect(registeredHandlers.has('GENERATE_BEFORE_COMBINE_PROMPTS')).toBe(true);
expect(registeredHandlers.get('GENERATE_BEFORE_COMBINE_PROMPTS').includes(handler)).toBe(true);
});
test('once() function tracks handlers for cleanup', () => {
const registeredHandlers = new Map();
// Simulate the once() function with tracking
function once(eventType, handler) {
// eventSource.once(eventType, handler); // Would call real event source
if (!registeredHandlers.has(eventType)) {
registeredHandlers.set(eventType, []);
}
registeredHandlers.get(eventType).push(handler);
}
const handler = () => {};
once('TEXT_COMPLETION_SETTINGS_READY', handler);
expect(registeredHandlers.has('TEXT_COMPLETION_SETTINGS_READY')).toBe(true);
expect(registeredHandlers.get('TEXT_COMPLETION_SETTINGS_READY').includes(handler)).toBe(true);
});
test('multiple handlers for same event type are all tracked', () => {
const registeredHandlers = new Map();
function on(eventType, handler) {
if (!registeredHandlers.has(eventType)) {
registeredHandlers.set(eventType, []);
}
registeredHandlers.get(eventType).push(handler);
}
const handlers = [() => {}, () => {}, () => {}];
handlers.forEach(h => on('CHAT_CHANGED', h));
expect(registeredHandlers.get('CHAT_CHANGED').length).toBe(3);
});
});
describe('jQuery Event Handler Tracking', () => {
test('tracked jQuery handlers can be cleaned up', () => {
const jqueryEventHandlers = [];
function trackJQueryHandler(event, selector, handler) {
jqueryEventHandlers.push({ event, selector, handler });
return handler;
}
const handler1 = () => {};
const handler2 = () => {};
trackJQueryHandler('click', '.rpg-item-remove', handler1);
trackJQueryHandler('click', '.rpg-equip-btn', handler2);
expect(jqueryEventHandlers.length).toBe(2);
// Simulate cleanup
jqueryEventHandlers.length = 0;
expect(jqueryEventHandlers.length).toBe(0);
});
test('trackJQueryHandler returns the handler for chaining', () => {
const jqueryEventHandlers = [];
function trackJQueryHandler(event, selector, handler) {
jqueryEventHandlers.push({ event, selector, handler });
return handler;
}
const handler = () => 'test';
const returned = trackJQueryHandler('click', '.selector', handler);
expect(returned).toBe(handler);
expect(returned()).toBe('test');
});
});
+154
View File
@@ -0,0 +1,154 @@
/**
* Parser performance tests
* Tests regex pre-compilation and format caching optimizations
*/
// Test the pre-compiled regex patterns directly by importing them from parser.js
// We use dynamic import with mocking to avoid the SillyTavern dependency chain
describe('Parser regex pre-compilation', () => {
// Test that pre-compiled regex patterns produce correct results
// These patterns are now module-level constants in parser.js
test('emoji regex pattern works correctly', () => {
const EMOJI_RE = /^[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{1F000}-\u{1F02F}\u{1F0A0}-\u{1F0FF}\u{1F100}-\u{1F64F}\u{1F680}-\u{1F6FF}\u{1F910}-\u{1F96B}\u{1F980}-\u{1F9E0}\u{FE00}-\u{FE0F}\u{200D}\u{20E3}]+/u;
expect('😊 Hello'.match(EMOJI_RE)).toBeTruthy();
expect('Hello'.match(EMOJI_RE)).toBeNull();
});
test('thinking tag regex works correctly', () => {
const THINKING_TAG_RE = /<think>[\s\S]*?<\/think>/gi;
const THINKING_TAG_ALT_RE = /<thinking>[\s\S]*?<\/thinking>/gi;
const text1 = '<think>thinking content</think>actual content';
const text2 = '<thinking>thinking content</thinking>actual content';
expect(text1.replace(THINKING_TAG_RE, '')).toBe('actual content');
expect(text2.replace(THINKING_TAG_ALT_RE, '')).toBe('actual content');
});
test('JSON block regex works correctly', () => {
const JSON_BLOCK_RE = /```json\s*\n([\s\S]*?)```/g;
const text = '```json\n{"key": "value"}\n```';
const matches = [...text.matchAll(JSON_BLOCK_RE)];
expect(matches.length).toBe(1);
expect(matches[0][1].trim()).toBe('{"key": "value"}');
});
test('code block regex works correctly', () => {
const CODE_BLOCK_RE = /```([^`]+)```/g;
const text = '```Stats\n---\nHealth: 100%\n```';
const matches = [...text.matchAll(CODE_BLOCK_RE)];
expect(matches.length).toBe(1);
expect(matches[0][1].trim()).toBe('Stats\n---\nHealth: 100%');
});
test('XML trackers regex works correctly', () => {
const XML_TRACKERS_RE = /<trackers>([\s\S]*?)<\/trackers>/i;
const text = '<trackers>content here</trackers>';
const match = text.match(XML_TRACKERS_RE);
expect(match).toBeTruthy();
expect(match[1]).toBe('content here');
});
test('stats section detection regex works', () => {
const STATS_SECTION_RE = /Stats\s*\n\s*---/i;
expect('Stats\n---\nHealth: 100%'.match(STATS_SECTION_RE)).toBeTruthy();
expect('Info Box\n---\nDate: 2024'.match(STATS_SECTION_RE)).toBeNull();
});
test('info box section detection regex works', () => {
const INFOBOX_SECTION_RE = /Info Box\s*\n\s*---/i;
expect('Info Box\n---\nDate: 2024'.match(INFOBOX_SECTION_RE)).toBeTruthy();
expect('Stats\n---\nHealth: 100%'.match(INFOBOX_SECTION_RE)).toBeNull();
});
test('characters section detection regex works', () => {
const CHARACTERS_SECTION_RE = /Present Characters\s*\n\s*---/i;
expect('Present Characters\n---\nAlice'.match(CHARACTERS_SECTION_RE)).toBeTruthy();
expect('Stats\n---\nHealth: 100%'.match(CHARACTERS_SECTION_RE)).toBeNull();
});
test('RPG attribute regexes work', () => {
const RPG_STR_RE = /STR:\s*(\d+)/i;
const RPG_DEX_RE = /DEX:\s*(\d+)/i;
const RPG_LVL_RE = /LVL:\s*(\d+)/i;
expect('STR: 15'.match(RPG_STR_RE)?.[1]).toBe('15');
expect('DEX: 20'.match(RPG_DEX_RE)?.[1]).toBe('20');
expect('LVL: 10'.match(RPG_LVL_RE)?.[1]).toBe('10');
});
test('placeholder pattern regex works', () => {
const PLACEHOLDER_PATTERN_RE = /\[([A-Za-z\s\/]+)\]/g;
const text = '[Location] some text [Mood Emoji]';
const matches = [...text.matchAll(PLACEHOLDER_PATTERN_RE)];
expect(matches.length).toBe(2);
});
test('format marker regex works', () => {
const FORMAT_MARKER_RE = /FORMAT:\s*/gi;
expect('FORMAT: some text'.replace(FORMAT_MARKER_RE, '')).toBe('some text');
});
});
describe('Format cache logic', () => {
test('format cache hit threshold logic', () => {
// Simulate the format caching logic from parser.js
let lastDetectedFormat = null;
let formatCacheHits = 0;
const FORMAT_CACHE_HIT_THRESHOLD = 3;
// Initially no cache
expect(lastDetectedFormat).toBeNull();
expect(formatCacheHits).toBe(0);
// First detection
lastDetectedFormat = 'json';
formatCacheHits = 1;
expect(formatCacheHits >= FORMAT_CACHE_HIT_THRESHOLD).toBe(false);
// Second hit
formatCacheHits++;
expect(formatCacheHits >= FORMAT_CACHE_HIT_THRESHOLD).toBe(false);
// Third hit - now cache is trusted
formatCacheHits++;
expect(formatCacheHits >= FORMAT_CACHE_HIT_THRESHOLD).toBe(true);
// Cache clear
lastDetectedFormat = null;
formatCacheHits = 0;
expect(lastDetectedFormat).toBeNull();
expect(formatCacheHits).toBe(0);
});
});
describe('Regex performance verification', () => {
test('pre-compiled regex is faster than re-compiling', () => {
// Pre-compiled constant (module-level)
const PRECOMPILED_RE = /```json\s*\n([\s\S]*?)```/g;
// Simulate per-call compilation (old behavior)
const compilePerCall = (text) => {
const re = /```json\s*\n([\s\S]*?)```/g;
return [...text.matchAll(re)];
};
// Pre-compiled usage (new behavior)
const usePrecompiled = (text) => {
// Reset lastIndex for global regex
PRECOMPILED_RE.lastIndex = 0;
return [...text.matchAll(PRECOMPILED_RE)];
};
const testText = '```json\n{"key": "value"}\n```';
// Both should produce the same result
const compiled = compilePerCall(testText);
const precompiled = usePrecompiled(testText);
expect(compiled.length).toBe(precompiled.length);
expect(compiled[0][1]).toBe(precompiled[0][1]);
});
});
+196
View File
@@ -0,0 +1,196 @@
import { createStringValidator, sanitizeLocationName, sanitizeItemName, validateStoredInventory, cleanItemString, MAX_ITEMS_PER_SECTION } from '../src/utils/security.js';
describe('createStringValidator', () => {
test('creates a validator that trims whitespace', () => {
const validator = createStringValidator({
label: 'test',
maxLength: 100,
checkBlockedNames: false
});
expect(validator(' hello ')).toBe('hello');
});
test('returns null for non-string input', () => {
const validator = createStringValidator({
label: 'test',
maxLength: 100
});
expect(validator(null)).toBeNull();
expect(validator(undefined)).toBeNull();
expect(validator(123)).toBeNull();
expect(validator('')).toBeNull();
});
test('truncates strings exceeding maxLength', () => {
const validator = createStringValidator({
label: 'test',
maxLength: 5
});
const result = validator('hello world');
expect(result).toBe('hello');
expect(result.length).toBe(5);
});
test('rejects blocked property names when checkBlockedNames is true', () => {
const validator = createStringValidator({
label: 'property',
maxLength: 100,
checkBlockedNames: true
});
expect(validator('__proto__')).toBeNull();
expect(validator('constructor')).toBeNull();
expect(validator('prototype')).toBeNull();
});
test('allows blocked property names when checkBlockedNames is false', () => {
const validator = createStringValidator({
label: 'item',
maxLength: 100,
checkBlockedNames: false
});
// __proto__ is allowed when checkBlockedNames is false
expect(validator('__proto__')).toBe('__proto__');
});
test('rejects additional values via additionalRejects', () => {
const validator = createStringValidator({
label: 'item',
maxLength: 100,
additionalRejects: ['none', 'null', 'undefined'],
checkBlockedNames: false
});
expect(validator('none')).toBeNull();
expect(validator('NONE')).toBeNull();
expect(validator('None')).toBeNull();
expect(validator('Sword')).toBe('Sword');
});
test('is case-insensitive for blocked names', () => {
const validator = createStringValidator({
label: 'test',
maxLength: 100,
checkBlockedNames: true
});
expect(validator('__PROTO__')).toBeNull();
expect(validator('__Proto__')).toBeNull();
expect(validator('CONSTRUCTOR')).toBeNull();
});
test('returns valid input unchanged', () => {
const validator = createStringValidator({
label: 'test',
maxLength: 100
});
expect(validator('ValidName')).toBe('ValidName');
expect(validator('another-valid-name')).toBe('another-valid-name');
});
});
describe('sanitizeLocationName', () => {
test('allows valid location names', () => {
expect(sanitizeLocationName('Home')).toBe('Home');
expect(sanitizeLocationName('Tavern')).toBe('Tavern');
expect(sanitizeLocationName('Forest Clearing')).toBe('Forest Clearing');
});
test('blocks dangerous property names', () => {
expect(sanitizeLocationName('__proto__')).toBeNull();
expect(sanitizeLocationName('constructor')).toBeNull();
expect(sanitizeLocationName('prototype')).toBeNull();
});
test('truncates overly long names', () => {
const longName = 'A'.repeat(250);
const result = sanitizeLocationName(longName);
expect(result.length).toBe(200);
});
test('returns null for invalid input', () => {
expect(sanitizeLocationName(null)).toBeNull();
expect(sanitizeLocationName('')).toBeNull();
expect(sanitizeLocationName(' ')).toBeNull();
});
});
describe('sanitizeItemName', () => {
test('allows valid item names', () => {
expect(sanitizeItemName('Sword')).toBe('Sword');
expect(sanitizeItemName('Health Potion')).toBe('Health Potion');
});
test('rejects "none" as an item name', () => {
expect(sanitizeItemName('none')).toBeNull();
expect(sanitizeItemName('None')).toBeNull();
expect(sanitizeItemName('NONE')).toBeNull();
});
test('truncates overly long names', () => {
const longName = 'A'.repeat(600);
const result = sanitizeItemName(longName);
expect(result.length).toBe(500);
});
test('returns null for invalid input', () => {
expect(sanitizeItemName(null)).toBeNull();
expect(sanitizeItemName('')).toBeNull();
});
});
describe('validateStoredInventory', () => {
test('returns cleaned inventory object', () => {
const input = { Home: 'Sword, Shield' };
const result = validateStoredInventory(input);
expect(result).toEqual({ Home: 'Sword, Shield' });
});
test('removes dangerous keys', () => {
const input = { Home: 'Sword' };
// __proto__ as a key is blocked by sanitizeLocationName
Object.setPrototypeOf(input, Object.create(null));
input['__proto__'] = 'malicious';
const result = validateStoredInventory(input);
// Result should not have __proto__ as an own property
expect(Object.prototype.hasOwnProperty.call(result, '__proto__')).toBe(false);
expect(result).toHaveProperty('Home');
});
test('returns empty object for invalid input', () => {
expect(validateStoredInventory(null)).toEqual({});
expect(validateStoredInventory(undefined)).toEqual({});
expect(validateStoredInventory('string')).toEqual({});
expect(validateStoredInventory([])).toEqual({});
});
test('skips non-string values', () => {
const input = { Home: 123, Barn: 'Hay' };
const result = validateStoredInventory(input);
expect(result).not.toHaveProperty('Home');
expect(result).toHaveProperty('Barn');
});
});
describe('cleanItemString', () => {
test('returns clean item string', () => {
expect(cleanItemString('Sword, Shield')).toBe('Sword, Shield');
});
test('item sanitizer does not block __proto__ (by design - checkBlockedNames is false)', () => {
// sanitizeItemName has checkBlockedNames: false, so __proto__ is allowed as item name
// Only sanitizeLocationName blocks __proto__ (for object keys)
const result = cleanItemString('Sword, Shield');
expect(result).toContain('Sword');
});
test('strips markdown formatting', () => {
const result = cleanItemString('**Sword**, *Shield*');
expect(result).not.toContain('**');
expect(result).not.toContain('*');
});
});
describe('MAX_ITEMS_PER_SECTION', () => {
test('is a positive number', () => {
expect(MAX_ITEMS_PER_SECTION).toBeGreaterThan(0);
expect(typeof MAX_ITEMS_PER_SECTION).toBe('number');
});
});
+9
View File
@@ -0,0 +1,9 @@
{
"presets": [
["@babel/preset-env", {
"targets": {
"node": "current"
}
}]
]
}
+133 -1214
View File
File diff suppressed because it is too large Load Diff
+8
View File
@@ -0,0 +1,8 @@
module.exports = {
testEnvironment: 'node',
testMatch: ['**/__tests__/**/*.test.js'],
transform: {
'^.+\\.js$': 'babel-jest',
},
transformIgnorePatterns: ['/node_modules/'],
};
+22
View File
@@ -0,0 +1,22 @@
// Jest setup file - set up mocks before tests run
const mockState = {
extensionSettings: {
debugMode: false,
userStats: {},
trackerConfig: {},
quests: { main: '', optional: [] }
},
FEATURE_FLAGS: { useNewInventory: false },
addDebugLog: () => {}
};
const mockPersistence = {
saveSettings: () => {}
};
const mockInventoryParser = {
extractInventory: () => null
};
// We can't easily mock ESM imports in Jest without transform
// Instead, we'll create mock files that Jest can use
+8226 -1
View File
File diff suppressed because it is too large Load Diff
+13 -5
View File
@@ -1,9 +1,13 @@
{
"name": "rpg-complanion-sillytavern",
"name": "rpg-companion-sillytavern",
"version": "3.7.4",
"description": "",
"description": "RPG Companion extension for SillyTavern",
"main": "index.js",
"type": "module",
"scripts": {
"build:css": "node scripts/build-css.js",
"build:css:min": "node scripts/build-css.js --minify",
"test": "jest",
"validate_locale": "node src/i18n/validator.js --watch",
"validate_locale_once": "node src/i18n/validator.js"
},
@@ -11,9 +15,13 @@
"author": "",
"license": "MIT",
"devDependencies": {
"@babel/core": "^8.0.1",
"@babel/preset-env": "^8.0.2",
"@jest/globals": "^30.4.1",
"chokidar": "^5.0.0",
"execa": "^9.6.1",
"fs-extra": "^11.3.3",
"glob": "^13.0.6"
},
"dependencies": {}
"glob": "^13.0.6",
"jest": "^30.4.2"
}
}
+141
View File
@@ -0,0 +1,141 @@
#!/usr/bin/env node
/**
* CSS Build Script
* Concatenates modular CSS files from src/styles/ into style.css
*
* Usage:
* node scripts/build-css.js # Development build (readable)
* node scripts/build-css.js --minify # Production build (minified)
*
* File order matters! Files are concatenated in alphabetical order.
* Prefix files with numbers to control order: _01_variables.css, _02_panel.css, etc.
*/
import fs from 'fs-extra';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const rootDir = path.resolve(__dirname, '..');
const stylesDir = path.join(rootDir, 'src', 'styles');
const outputFile = path.join(rootDir, 'style.css');
const shouldMinify = process.argv.includes('--minify');
/**
* Minify CSS by removing comments, unnecessary whitespace, and redundant characters.
* Safe for CSS custom properties, strings, and data URIs.
*/
function minifyCSS(css) {
let result = css;
// Remove CSS comments (/* ... */) — but not inside strings
result = result.replace(/\/\*[\s\S]*?\*\//g, '');
// Remove lines that are only whitespace after comment removal
result = result.replace(/^\s*[\r\n]/g, '\n');
// Collapse multiple newlines into one
result = result.replace(/\n{2,}/g, '\n');
// Remove spaces around structural characters outside of strings
result = result.replace(/(\{|\}|;|:|,)/g, '$1');
// Remove space before {
result = result.replace(/\s+\{/g, '{');
// Remove space after {
result = result.replace(/\{\s+/g, '{');
// Remove space before }
result = result.replace(/\s+\}/g, '}');
// Remove space after }
result = result.replace(/\}\s+/g, '}');
// Remove space around :
result = result.replace(/\s*:\s*/g, ':');
// Remove space around ;
result = result.replace(/;\s*/g, ';');
// Remove space around ,
result = result.replace(/,\s*/g, ',');
// Remove trailing semicolon before }
result = result.replace(/;\}/g, '}');
// Collapse multiple spaces into one (preserves spaces inside strings roughly)
result = result.replace(/[ \t]+/g, ' ');
// Remove leading/trailing whitespace
result = result.trim();
return result;
}
async function buildCSS() {
console.log(`Building CSS from src/styles/...${shouldMinify ? ' (minified)' : ' (readable)'}`);
// Read all CSS files from src/styles/
const files = await fs.readdir(stylesDir);
const cssFiles = files
.filter(f => f.endsWith('.css'))
.sort() // Alphabetical order (use _01_, _02_ prefix for control)
.map(f => path.join(stylesDir, f));
if (cssFiles.length === 0) {
console.error('No CSS files found in src/styles/');
process.exit(1);
}
console.log(`Found ${cssFiles.length} CSS files:`);
cssFiles.forEach(f => console.log(` - ${path.basename(f)}`));
// Read and concatenate all files
const contents = [];
for (const file of cssFiles) {
const content = await fs.readFile(file, 'utf-8');
const basename = path.basename(file);
if (shouldMinify) {
// In minified mode, skip comment separators
contents.push(content);
} else {
contents.push(`/* ============================================ */`);
contents.push(`/* ${basename} */`);
contents.push(`/* ============================================ */`);
contents.push('');
contents.push(content);
contents.push('');
}
}
let combined = contents.join('\n');
// Apply minification if requested
if (shouldMinify) {
const beforeSize = combined.length;
combined = minifyCSS(combined);
const afterSize = combined.length;
const reduction = (((beforeSize - afterSize) / beforeSize) * 100).toFixed(1);
console.log(`\n🗜️ Minification: ${beforeSize.toLocaleString()}${afterSize.toLocaleString()} bytes (${reduction}% reduction)`);
}
// Write to style.css
await fs.writeFile(outputFile, combined, 'utf-8');
const stats = await fs.stat(outputFile);
const lines = combined.split('\n').length;
const sizeKB = (stats.size / 1024).toFixed(1);
console.log(`\n✅ Built ${outputFile}`);
console.log(` Lines: ${lines}`);
console.log(` Size: ${sizeKB} KB`);
}
buildCSS().catch(err => {
console.error('Build failed:', err);
process.exit(1);
});
+1 -1
View File
@@ -58,7 +58,7 @@
</div>
<div style="margin-top: 10px; text-align: center; opacity: 0.6; font-size: 0.85em;">
v3.7.2
v3.7.4
</div>
</div>
</div>
+283 -15
View File
@@ -13,17 +13,20 @@ export const extensionName = 'third-party/rpg-companion-sillytavern';
* This supports both global (public/extensions) and user-specific (data/default-user/extensions) installations
*/
const currentScriptPath = import.meta.url;
const isUserExtension = currentScriptPath.includes('/data/') || currentScriptPath.includes('\\data\\');
const isUserExtension = currentScriptPath.includes('/data/') || currentScriptPath.includes('\\\\data\\\\');
export const extensionFolderPath = isUserExtension
? `data/default-user/extensions/${extensionName}`
: `scripts/extensions/${extensionName}`;
/**
* Default extension settings
* Default extension settings — single source of truth for all setting defaults.
* Imported by state.js to initialize extensionSettings.
*/
export const defaultSettings = {
settingsVersion: 7, // Version number for settings migrations
enabled: true,
autoUpdate: true,
debugMode: false, // Enable verbose debug logging (developer flag)
autoUpdate: false,
updateDepth: 4, // How many messages to include in the context
generationMode: 'together', // 'separate' or 'together' - whether to generate with main response or separately
showUserStats: true,
@@ -33,37 +36,94 @@ export const defaultSettings = {
enableThoughtBasedExpressions: false,
hideDefaultExpressionDisplay: false,
showInventory: true, // Show inventory section (v2 system)
showEquipment: true, // Show equipment section
showQuests: true, // Show quests section
showLockIcons: true, // Show lock/unlock icons on tracker items
showThoughtsInChat: true, // Show thoughts overlay in chat
thoughtsInChatStyle: 'corner', // 'corner' or 'inline'
narratorMode: false, // Use character card as narrator instead of fixed character references
customNarratorPrompt: '', // Custom narrator mode prompt text (empty = use default)
customContextInstructionsPrompt: '', // Custom context instructions prompt text (empty = use default)
enableHtmlPrompt: false, // Enable immersive HTML prompt injection
customHtmlPrompt: '', // Custom HTML prompt text (empty = use default)
enableDialogueColoring: false, // Enable dialogue coloring prompt injection
customDialogueColoringPrompt: '', // Custom dialogue coloring prompt text (empty = use default)
enableDeceptionSystem: false, // Enable deception tracking with <lie> tags
customDeceptionPrompt: '', // Custom deception prompt text (empty = use default)
enableOmniscienceFilter: false, // Enable omniscience filter with <ofilter> tags
customOmnisciencePrompt: '', // Custom omniscience filter prompt text (empty = use default)
enableCYOA: false, // Enable "Choose Your Own Adventure" formatting with action choices
customCYOAPrompt: '', // Custom CYOA prompt text (empty = use default)
enableSpotifyMusic: false, // Enable Spotify music integration (asks AI for Spotify URLs)
customSpotifyPrompt: '', // Custom Spotify prompt text (empty = use default)
// Controls when the extension skips injecting tracker instructions/examples/HTML
// into generations that appear to be user-injected instructions. Valid values:
// - 'none' -> never skip (legacy behavior: always inject)
// - 'guided' -> skip for any guided / instruct or quiet_prompt generation
// - 'impersonation' -> skip only for impersonation-style guided generations
// This setting helps compatibility with other extensions like GuidedGenerations.
skipInjectionsForGuided: 'none',
enablePlotButtons: true, // Show plot progression buttons above chat input
saveTrackerHistory: false, // Save tracker data in chat history for each message
enableDynamicWeather: true, // Enable dynamic weather effects based on Info Box weather field (v2: enabled by default)
weatherBackground: true, // Show weather effects in background (behind chat)
weatherForeground: false, // Show weather effects in foreground (on top of chat)
dismissedHolidayPromo: false, // User dismissed the holiday promotion banner
showHtmlToggle: true, // Show Immersive HTML toggle in main panel
showDialogueColoringToggle: true, // Show Dialogue Coloring toggle in main panel (enabled by default)
showDeceptionToggle: true, // Show Deception System toggle in main panel
showOmniscienceToggle: true, // Show Omniscience Filter toggle in main panel
showCYOAToggle: true, // Show CYOA toggle in main panel
showSpotifyToggle: true, // Show Spotify Music toggle in main panel
showDynamicWeatherToggle: true, // Show Dynamic Weather Effects toggle in main panel
showNarratorMode: true, // Show Narrator Mode toggle in main panel
showAutoAvatars: true, // Show Auto-generate Avatars toggle in main panel
skipInjectionsForGuided: 'none', // skip injections for instruct injections and quiet prompts (GuidedGenerations compatibility)
enableRandomizedPlot: true, // Show randomized plot progression button above chat input
enableNaturalPlot: true, // Show natural plot progression button above chat input
// History persistence settings - inject selected tracker data into historical messages
historyPersistence: {
enabled: false, // Master toggle for history persistence feature
messageCount: 5, // Number of messages to include (0 = all available)
injectionPosition: 'assistant_message_end', // 'user_message_end', 'assistant_message_end', 'extra_user_message', 'extra_assistant_message'
contextPreamble: '', // Optional custom preamble text (empty = use default short one)
sendAllEnabledOnRefresh: false // If true, sends all enabled stats from preset instead of only persistInHistory-enabled stats on Refresh RPG Info
},
panelPosition: 'right', // 'left', 'right', or 'top'
theme: 'default', // Theme: default, sci-fi, fantasy, cyberpunk, custom
customColors: {
bg: '#1a1a2e',
bgOpacity: 100,
accent: '#16213e',
accentOpacity: 100,
text: '#eaeaea',
highlight: '#e94560'
textOpacity: 100,
highlight: '#e94560',
highlightOpacity: 100
},
statBarColorLow: '#cc3333', // Color for low stat values (red)
statBarColorLowOpacity: 100,
statBarColorHigh: '#33cc66', // Color for high stat values (green)
statBarColorHighOpacity: 100,
enableAnimations: true, // Enable smooth animations for stats and content updates
mobileFabPosition: {
top: 'calc(var(--topBarBlockSize) + 60px)',
right: '12px'
}, // Saved position for mobile FAB button
// Mobile FAB widget display options (8-position system around the button)
mobileFabWidgets: {
enabled: true, // Master toggle for FAB widgets
weatherIcon: { enabled: true, position: 0 }, // Weather emoji (☀️, 🌧️, etc.)
weatherDesc: { enabled: true, position: 1 }, // Weather description text
clock: { enabled: true, position: 2 }, // Current time display
date: { enabled: true, position: 3 }, // Date display
location: { enabled: true, position: 4 }, // Location name
stats: { enabled: true, position: 5 }, // All stats as compact numbers
attributes: { enabled: true, position: 6 } // Compact RPG attributes display
},
// Desktop strip widget display options (shown in collapsed panel strip)
desktopStripWidgets: {
enabled: true, // Master toggle for strip widgets (enabled by default)
weatherIcon: { enabled: true }, // Weather emoji (☀️, 🌧️, etc.)
clock: { enabled: true }, // Current time display
date: { enabled: true }, // Date display
location: { enabled: true }, // Location name
stats: { enabled: true }, // All stats as compact numbers
attributes: { enabled: true } // Compact RPG attributes display
},
userStats: {
health: 100,
satiety: 100,
@@ -72,14 +132,162 @@ export const defaultSettings = {
arousal: 0,
mood: '😐',
conditions: 'None',
/** @type {InventoryV2} */
skills: [],
inventory: {
version: 2,
onPerson: "None",
clothing: "None",
stored: {},
assets: "None"
},
equipment: {
items: [], // Array of {id, name, type, slot, stats: {str: 2, dex: 1, ...}, description}
slots: {
helmet: null,
ring1: null,
ring2: null,
ring3: null,
ring4: null,
ring5: null,
ring6: null,
ring7: null,
ring8: null,
ring9: null,
ring10: null,
necklace: null,
bodyArmor: null,
pants: null,
shoes: null,
gloves: null,
accessory1: null,
accessory2: null,
accessory3: null
}
}
},
statNames: {
health: 'Health',
satiety: 'Satiety',
energy: 'Energy',
hygiene: 'Hygiene',
arousal: 'Arousal'
},
// Tracker customization configuration
trackerConfig: {
userStats: {
// Stats display mode: 'percentage' or 'number'
statsDisplayMode: 'percentage',
// Array of custom stats (allows add/remove/rename)
customStats: [
{ id: 'health', name: 'Health', enabled: true, persistInHistory: false, maxValue: 100 },
{ id: 'satiety', name: 'Satiety', enabled: true, persistInHistory: false, maxValue: 100 },
{ id: 'energy', name: 'Energy', enabled: true, persistInHistory: false, maxValue: 100 },
{ id: 'hygiene', name: 'Hygiene', enabled: true, persistInHistory: false, maxValue: 100 },
{ id: 'arousal', name: 'Arousal', enabled: true, persistInHistory: false, maxValue: 100 }
],
// RPG Attributes (customizable D&D-style attributes)
showRPGAttributes: true,
showLevel: true, // Show/hide level in UI and prompts
alwaysSendAttributes: false, // If true, always send attributes; if false, only send with dice rolls
rpgAttributes: [
{ id: 'str', name: 'STR', enabled: true, persistInHistory: false },
{ id: 'dex', name: 'DEX', enabled: true, persistInHistory: false },
{ id: 'con', name: 'CON', enabled: true, persistInHistory: false },
{ id: 'int', name: 'INT', enabled: true, persistInHistory: false },
{ id: 'wis', name: 'WIS', enabled: true, persistInHistory: false },
{ id: 'cha', name: 'CHA', enabled: true, persistInHistory: false }
],
// Status section config
statusSection: {
enabled: true,
showMoodEmoji: true,
customFields: ['Conditions'], // User can edit what to track
persistInHistory: false // Persist status in historical messages
},
// Optional skills field
skillsSection: {
enabled: false,
label: 'Skills', // User-editable
customFields: [], // Array of skill names
persistInHistory: false // Persist skills in historical messages
},
// Inventory persistence
inventoryPersistInHistory: false, // Persist inventory in historical messages
// Quests persistence
questsPersistInHistory: false // Persist quests in historical messages
},
infoBox: {
widgets: {
date: { enabled: true, format: 'Weekday, Month, Year', persistInHistory: true }, // Date enabled by default for history
weather: { enabled: true, persistInHistory: true }, // Weather enabled by default for history
temperature: { enabled: true, unit: 'C', persistInHistory: false }, // 'C' or 'F'
time: { enabled: true, persistInHistory: true }, // Time enabled by default for history
location: { enabled: true, persistInHistory: true }, // Location enabled by default for history
recentEvents: { enabled: true, persistInHistory: false }
}
},
presentCharacters: {
// Fixed fields (always shown)
showEmoji: true,
showName: true,
// Relationship fields configuration
relationships: {
enabled: true,
// Relationship to emoji mapping (shown on character portraits)
relationshipEmojis: {
'Lover': '❤️',
'Friend': '⭐',
'Ally': '🤝',
'Enemy': '⚔️',
'Neutral': '⚖️'
}
},
// Legacy fields kept for backward compatibility
relationshipFields: ['Lover', 'Friend', 'Ally', 'Enemy', 'Neutral'],
relationshipEmojis: {
'Lover': '❤️',
'Friend': '⭐',
'Ally': '🤝',
'Enemy': '⚔️',
'Neutral': '⚖️'
},
// Custom fields (appearance, demeanor, etc. - shown after relationship, separated by |)
customFields: [
{ id: 'appearance', name: 'Appearance', enabled: true, description: 'Visible physical appearance (clothing, hair, notable features)', persistInHistory: false },
{ id: 'demeanor', name: 'Demeanor', enabled: true, description: 'Observable demeanor or emotional state', persistInHistory: false }
],
// Thoughts configuration (separate line)
thoughts: {
enabled: true,
name: 'Thoughts',
description: 'Internal Monologue (in first person from character\'s POV, up to three sentences long)',
persistInHistory: false
},
// Character stats toggle (optional feature)
characterStats: {
enabled: false,
customStats: [
{ id: 'health', name: 'Health', enabled: true },
{ id: 'arousal', name: 'Arousal', enabled: true }
]
}
}
},
quests: {
main: "None", // Current main quest title
optional: [] // Array of optional quest titles
},
infoBox: JSON.stringify({
date: { value: new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }) },
weather: { emoji: '☀️', forecast: 'Clear skies' },
temperature: { value: 20, unit: 'C' },
time: { start: '00:00', end: '00:00' },
location: { value: 'Unknown Location' }
}, null, 2),
characterThoughts: JSON.stringify({
characters: []
}, null, 2),
level: 1, // User's character level
classicStats: {
str: 10,
dex: 10,
@@ -89,5 +297,65 @@ export const defaultSettings = {
cha: 10
},
lastDiceRoll: null, // Store last dice roll result
collapsedInventoryLocations: [] // Array of collapsed storage location names
showDiceDisplay: true, // Show the "Last Roll" display in the panel
collapsedInventoryLocations: [], // Array of collapsed storage location names
inventoryViewModes: {
onPerson: 'list', // 'list' or 'grid' view mode for On Person section
stored: 'list', // 'list' or 'grid' view mode for Stored section
assets: 'list' // 'list' or 'grid' view mode for Assets section
},
npcAvatars: {}, // Store custom avatar images for NPCs (key: character name, value: base64 data URI)
// Combat encounter settings
encounterSettings: {
enabled: true, // Show Start Encounter button above chat input
historyDepth: 8, // Number of recent messages to include in combat initialization
autoSaveLogs: false // Save detailed combat logs to file
},
// Auto avatar generation settings
autoGenerateAvatars: true, // Master toggle for auto-generating avatars
avatarLLMCustomInstruction: '', // Custom instruction for LLM prompt generation
// External API settings for 'external' generation mode
externalApiSettings: {
baseUrl: '', // OpenAI-compatible API base URL (e.g., "https://api.openai.com/v1")
// apiKey is NOT stored here for security. It is stored in localStorage('rpg_companion_external_api_key')
model: '', // Model identifier (e.g., "gpt-4o-mini")
maxTokens: 8192, // Maximum tokens for generation
temperature: 0.7 // Temperature setting for generation
},
// Lock state for tracker items (v3 JSON format feature)
lockedItems: {
stats: [], // Array of locked stat IDs (e.g., ["health", "satiety"])
skills: [], // Array of locked skill names (e.g., ["Cooking", "Swordsmanship"])
inventory: {
onPerson: [], // Array of locked item indices (e.g., [0, 2])
clothing: [], // Array of locked item indices
stored: {}, // Object with location keys, each containing array of locked indices (e.g., {"Home": [0, 1]})
assets: [] // Array of locked asset indices
},
quests: {
main: false, // Boolean for main quest lock
optional: [] // Array of locked optional quest indices (e.g., [0, 2])
},
infoBox: {
date: false, // Boolean for date widget lock
weather: false, // Boolean for weather widget lock
temperature: false, // Boolean for temperature widget lock
time: false, // Boolean for time widget lock
location: false, // Boolean for location widget lock
recentEvents: false // Boolean for recent events widget lock
},
characters: {} // Object mapping character names to their locked fields (e.g., {"Sarah": {relationship: true, thoughts: false}})
},
// Preset management for tracker configurations
presetManager: {
// Map of preset ID to preset data (contains name and trackerConfig)
presets: {},
// Map of character/group entity to preset ID (e.g., "char_0": "preset_123", "group_abc": "preset_456")
// Note: This is stored separately and NOT exported with presets
characterAssociations: {},
// Currently active preset ID
activePresetId: null,
// Default preset ID (used when no character association exists)
defaultPresetId: null
}
};
+24
View File
@@ -12,6 +12,12 @@ import { eventSource, event_types } from '../../../../../../script.js';
*/
export function on(eventType, handler) {
eventSource.on(eventType, handler);
// Track for cleanup
if (!registeredHandlers.has(eventType)) {
registeredHandlers.set(eventType, []);
}
registeredHandlers.get(eventType).push(handler);
}
/**
@@ -21,6 +27,12 @@ export function on(eventType, handler) {
*/
export function once(eventType, handler) {
eventSource.once(eventType, handler);
// Track for cleanup (one-time handlers should also be tracked)
if (!registeredHandlers.has(eventType)) {
registeredHandlers.set(eventType, []);
}
registeredHandlers.get(eventType).push(handler);
}
/**
@@ -30,6 +42,18 @@ export function once(eventType, handler) {
*/
export function off(eventType, handler) {
eventSource.off(eventType, handler);
// Remove from tracking so unregisterAllEvents() doesn't hold a stale reference
const handlers = registeredHandlers.get(eventType);
if (handlers) {
const index = handlers.indexOf(handler);
if (index !== -1) {
handlers.splice(index, 1);
}
if (handlers.length === 0) {
registeredHandlers.delete(eventType);
}
}
}
/**
+2 -2
View File
@@ -559,7 +559,7 @@ function validateSettings(settings) {
* Loads the extension settings from the global settings object.
* Automatically migrates v1 inventory to v2 format if needed.
*/
export function loadSettings() {
export async function loadSettings() {
try {
const context = getContext();
const extension_settings = context.extension_settings || context.extensionSettings;
@@ -601,7 +601,7 @@ export function loadSettings() {
// Migration to version 3: Convert text trackers to JSON format
if (currentVersion < 3) {
// console.log('[RPG Companion] Migrating settings to version 3 (JSON tracker format)');
migrateToV3JSON();
await migrateToV3JSON();
extensionSettings.settingsVersion = 3;
settingsChanged = true;
}
+81
View File
@@ -0,0 +1,81 @@
/**
* Settings Panel Module
* Manages the extension settings tab in SillyTavern's Extensions panel.
* Extracted from index.js to reduce main entry point size.
*/
import { extensionSettings } from './state.js';
import { saveSettings } from './persistence.js';
import { extensionName } from './config.js';
import { i18n } from './i18n.js';
/**
* Adds the extension settings to the Extensions tab.
* @param {Function} $ - jQuery function
* @param {Function} renderExtensionTemplateAsync - SillyTavern template renderer
* @param {Function} clearExtensionPrompts - Clear extension prompts from SillyTavern
* @param {Function} updateChatThoughts - Update thought bubbles in chat
* @param {Function} cleanupCheckpointUI - Remove checkpoint UI elements
* @param {Function} clearThoughtBasedExpressionsCache - Clear expression cache
* @param {Function} toggleDynamicWeather - Toggle weather effects
* @param {Function} initUI - Initialize the main UI panel
* @param {Function} loadChatData - Load chat-specific data
* @param {Function} scheduleChatStateRehydration - Schedule chat state rehydration
* @param {Function} initThoughtBasedExpressions - Initialize thought-based expressions
* @param {Function} injectCheckpointButton - Add checkpoint buttons
* @param {Function} updateAllCheckpointIndicators - Update checkpoint button states
* @param {Function} removeAlternatePresentCharactersPanel - Remove alt characters panel
* @param {Function} registerExtensionEvents - (Re)register all SillyTavern event handlers
* @param {Function} unregisterAllEvents - Unregister all tracked SillyTavern event handlers
* @param {Function} initHistoryInjection - (Re)register history injection listeners
*/
export async function addExtensionSettings($, renderExtensionTemplateAsync, clearExtensionPrompts, updateChatThoughts, cleanupCheckpointUI, clearThoughtBasedExpressionsCache, toggleDynamicWeather, initUI, loadChatData, scheduleChatStateRehydration, initThoughtBasedExpressions, injectCheckpointButton, updateAllCheckpointIndicators, removeAlternatePresentCharactersPanel, registerExtensionEvents, unregisterAllEvents, initHistoryInjection) {
const settingsHtml = await renderExtensionTemplateAsync(extensionName, 'settings');
$('#extensions_settings2').append(settingsHtml);
// Enable/disable toggle
$('#rpg-extension-enabled').prop('checked', extensionSettings.enabled).on('change', async function() {
const wasEnabled = extensionSettings.enabled;
extensionSettings.enabled = $(this).prop('checked');
saveSettings();
if (!extensionSettings.enabled && wasEnabled) {
clearExtensionPrompts();
updateChatThoughts();
cleanupCheckpointUI();
clearThoughtBasedExpressionsCache();
toggleDynamicWeather(false);
// Unregister all tracked event handlers to avoid leaks while disabled
unregisterAllEvents();
$('#rpg-companion-panel').remove();
$('#rpg-mobile-toggle').remove();
$('#rpg-collapse-toggle').remove();
$('#rpg-plot-buttons').remove();
removeAlternatePresentCharactersPanel();
} else if (extensionSettings.enabled && !wasEnabled) {
// Re-register event handlers that were unregistered on disable
registerExtensionEvents();
initHistoryInjection();
await initUI();
loadChatData();
scheduleChatStateRehydration();
initThoughtBasedExpressions();
updateChatThoughts();
injectCheckpointButton();
updateAllCheckpointIndicators();
}
});
// Language selector
const langSelect = $('#rpg-companion-language-select');
if (langSelect.length) {
langSelect.val(i18n.currentLanguage);
langSelect.on('change', async function() {
const selectedLanguage = $(this).val();
await i18n.setLanguage(selectedLanguage);
i18n.applyTranslations(document.getElementById('extensions_settings2'));
});
}
}
+29 -335
View File
@@ -3,347 +3,16 @@
* Centralizes all extension state variables
*/
import { defaultSettings } from './config.js';
// Type imports
/** @typedef {import('../types/inventory.js').InventoryV2} InventoryV2 */
/**
* Extension settings - persisted to SillyTavern settings
* Initialized from config.js defaultSettings (single source of truth)
*/
export let extensionSettings = {
settingsVersion: 6, // Version number for settings migrations
enabled: true,
autoUpdate: false,
updateDepth: 4, // How many messages to include in the context
generationMode: 'together', // 'separate' or 'together' - whether to generate with main response or separately
showUserStats: true,
showInfoBox: true,
showCharacterThoughts: true,
showAlternatePresentCharactersPanel: false,
enableThoughtBasedExpressions: false,
hideDefaultExpressionDisplay: false,
showInventory: true, // Show inventory section (v2 system)
showEquipment: true, // Show equipment section
showQuests: true, // Show quests section
showThoughtsInChat: true, // Show thoughts overlay in chat
thoughtsInChatStyle: 'corner', // 'corner' or 'inline'
narratorMode: false, // Use character card as narrator instead of fixed character references
customNarratorPrompt: '', // Custom narrator mode prompt text (empty = use default)
customContextInstructionsPrompt: '', // Custom context instructions prompt text (empty = use default)
enableHtmlPrompt: false, // Enable immersive HTML prompt injection
customHtmlPrompt: '', // Custom HTML prompt text (empty = use default)
enableDialogueColoring: false, // Enable dialogue coloring prompt injection
customDialogueColoringPrompt: '', // Custom dialogue coloring prompt text (empty = use default)
enableDeceptionSystem: false, // Enable deception tracking with <lie> tags
customDeceptionPrompt: '', // Custom deception prompt text (empty = use default)
enableOmniscienceFilter: false, // Enable omniscience filter with <ofilter> tags
customOmnisciencePrompt: '', // Custom omniscience filter prompt text (empty = use default)
enableCYOA: false, // Enable "Choose Your Own Adventure" formatting with action choices
customCYOAPrompt: '', // Custom CYOA prompt text (empty = use default)
enableSpotifyMusic: false, // Enable Spotify music integration (asks AI for Spotify URLs)
customSpotifyPrompt: '', // Custom Spotify prompt text (empty = use default)
enableDynamicWeather: true, // Enable dynamic weather effects based on Info Box weather field (v2: enabled by default)
weatherBackground: true, // Show weather effects in background (behind chat)
weatherForeground: false, // Show weather effects in foreground (on top of chat)
dismissedHolidayPromo: false, // User dismissed the holiday promotion banner
showHtmlToggle: true, // Show Immersive HTML toggle in main panel
showDialogueColoringToggle: true, // Show Dialogue Coloring toggle in main panel (enabled by default)
showDeceptionToggle: true, // Show Deception System toggle in main panel
showOmniscienceToggle: true, // Show Omniscience Filter toggle in main panel
showCYOAToggle: true, // Show CYOA toggle in main panel
showSpotifyToggle: true, // Show Spotify Music toggle in main panel
showDynamicWeatherToggle: true, // Show Dynamic Weather Effects toggle in main panel
showNarratorMode: true, // Show Narrator Mode toggle in main panel
showAutoAvatars: true, // Show Auto-generate Avatars toggle in main panel
skipInjectionsForGuided: 'none', // skip injections for instruct injections and quiet prompts (GuidedGenerations compatibility)
enableRandomizedPlot: true, // Show randomized plot progression button above chat input
enableNaturalPlot: true, // Show natural plot progression button above chat input
// History persistence settings - inject selected tracker data into historical messages
historyPersistence: {
enabled: false, // Master toggle for history persistence feature
messageCount: 5, // Number of messages to include (0 = all available)
injectionPosition: 'assistant_message_end', // 'user_message_end', 'assistant_message_end', 'extra_user_message', 'extra_assistant_message'
contextPreamble: '', // Optional custom preamble text (empty = use default short one)
sendAllEnabledOnRefresh: false // If true, sends all enabled stats from preset instead of only persistInHistory-enabled stats on Refresh RPG Info
},
panelPosition: 'right', // 'left', 'right', or 'top'
theme: 'default', // Theme: default, sci-fi, fantasy, cyberpunk, custom
customColors: {
bg: '#1a1a2e',
bgOpacity: 100,
accent: '#16213e',
accentOpacity: 100,
text: '#eaeaea',
textOpacity: 100,
highlight: '#e94560',
highlightOpacity: 100
},
statBarColorLow: '#cc3333', // Color for low stat values (red)
statBarColorLowOpacity: 100,
statBarColorHigh: '#33cc66', // Color for high stat values (green)
statBarColorHighOpacity: 100,
enableAnimations: true, // Enable smooth animations for stats and content updates
mobileFabPosition: {
top: 'calc(var(--topBarBlockSize) + 60px)',
right: '12px'
}, // Saved position for mobile FAB button
// Mobile FAB widget display options (8-position system around the button)
mobileFabWidgets: {
enabled: true, // Master toggle for FAB widgets
weatherIcon: { enabled: true, position: 0 }, // Weather emoji (☀️, 🌧️, etc.)
weatherDesc: { enabled: true, position: 1 }, // Weather description text
clock: { enabled: true, position: 2 }, // Current time display
date: { enabled: true, position: 3 }, // Date display
location: { enabled: true, position: 4 }, // Location name
stats: { enabled: true, position: 5 }, // All stats as compact numbers
attributes: { enabled: true, position: 6 } // Compact RPG attributes display
},
// Desktop strip widget display options (shown in collapsed panel strip)
desktopStripWidgets: {
enabled: true, // Master toggle for strip widgets (enabled by default)
weatherIcon: { enabled: true }, // Weather emoji (☀️, 🌧️, etc.)
clock: { enabled: true }, // Current time display
date: { enabled: true }, // Date display
location: { enabled: true }, // Location name
stats: { enabled: true }, // All stats as compact numbers
attributes: { enabled: true } // Compact RPG attributes display
},
userStats: {
health: 100,
satiety: 100,
energy: 100,
hygiene: 100,
arousal: 0,
mood: '😐',
conditions: 'None',
skills: [],
inventory: {
version: 2,
onPerson: "None",
clothing: "None",
stored: {},
assets: "None"
},
equipment: {
items: [], // Array of {id, name, type, slot, stats: {str: 2, dex: 1, ...}, description}
slots: {
helmet: null,
ring1: null,
ring2: null,
ring3: null,
ring4: null,
ring5: null,
ring6: null,
ring7: null,
ring8: null,
ring9: null,
ring10: null,
necklace: null,
bodyArmor: null,
pants: null,
shoes: null,
gloves: null,
accessory1: null,
accessory2: null,
accessory3: null
}
}
},
statNames: {
health: 'Health',
satiety: 'Satiety',
energy: 'Energy',
hygiene: 'Hygiene',
arousal: 'Arousal'
},
// Tracker customization configuration
trackerConfig: {
userStats: {
// Stats display mode: 'percentage' or 'number'
statsDisplayMode: 'percentage',
// Array of custom stats (allows add/remove/rename)
customStats: [
{ id: 'health', name: 'Health', enabled: true, persistInHistory: false, maxValue: 100 },
{ id: 'satiety', name: 'Satiety', enabled: true, persistInHistory: false, maxValue: 100 },
{ id: 'energy', name: 'Energy', enabled: true, persistInHistory: false, maxValue: 100 },
{ id: 'hygiene', name: 'Hygiene', enabled: true, persistInHistory: false, maxValue: 100 },
{ id: 'arousal', name: 'Arousal', enabled: true, persistInHistory: false, maxValue: 100 }
],
// RPG Attributes (customizable D&D-style attributes)
showRPGAttributes: true,
showLevel: true, // Show/hide level in UI and prompts
alwaysSendAttributes: false, // If true, always send attributes; if false, only send with dice rolls
rpgAttributes: [
{ id: 'str', name: 'STR', enabled: true, persistInHistory: false },
{ id: 'dex', name: 'DEX', enabled: true, persistInHistory: false },
{ id: 'con', name: 'CON', enabled: true, persistInHistory: false },
{ id: 'int', name: 'INT', enabled: true, persistInHistory: false },
{ id: 'wis', name: 'WIS', enabled: true, persistInHistory: false },
{ id: 'cha', name: 'CHA', enabled: true, persistInHistory: false }
],
// Status section config
statusSection: {
enabled: true,
showMoodEmoji: true,
customFields: ['Conditions'], // User can edit what to track
persistInHistory: false // Persist status in historical messages
},
// Optional skills field
skillsSection: {
enabled: false,
label: 'Skills', // User-editable
customFields: [], // Array of skill names
persistInHistory: false // Persist skills in historical messages
},
// Inventory persistence
inventoryPersistInHistory: false, // Persist inventory in historical messages
// Quests persistence
questsPersistInHistory: false // Persist quests in historical messages
},
infoBox: {
widgets: {
date: { enabled: true, format: 'Weekday, Month, Year', persistInHistory: true }, // Date enabled by default for history
weather: { enabled: true, persistInHistory: true }, // Weather enabled by default for history
temperature: { enabled: true, unit: 'C', persistInHistory: false }, // 'C' or 'F'
time: { enabled: true, persistInHistory: true }, // Time enabled by default for history
location: { enabled: true, persistInHistory: true }, // Location enabled by default for history
recentEvents: { enabled: true, persistInHistory: false }
}
},
presentCharacters: {
// Fixed fields (always shown)
showEmoji: true,
showName: true,
// Relationship fields configuration
relationships: {
enabled: true,
// Relationship to emoji mapping (shown on character portraits)
relationshipEmojis: {
'Lover': '❤️',
'Friend': '⭐',
'Ally': '🤝',
'Enemy': '⚔️',
'Neutral': '⚖️'
}
},
// Legacy fields kept for backward compatibility
relationshipFields: ['Lover', 'Friend', 'Ally', 'Enemy', 'Neutral'],
relationshipEmojis: {
'Lover': '❤️',
'Friend': '⭐',
'Ally': '🤝',
'Enemy': '⚔️',
'Neutral': '⚖️'
},
// Custom fields (appearance, demeanor, etc. - shown after relationship, separated by |)
customFields: [
{ id: 'appearance', name: 'Appearance', enabled: true, description: 'Visible physical appearance (clothing, hair, notable features)', persistInHistory: false },
{ id: 'demeanor', name: 'Demeanor', enabled: true, description: 'Observable demeanor or emotional state', persistInHistory: false }
],
// Thoughts configuration (separate line)
thoughts: {
enabled: true,
name: 'Thoughts',
description: 'Internal Monologue (in first person from character\'s POV, up to three sentences long)',
persistInHistory: false
},
// Character stats toggle (optional feature)
characterStats: {
enabled: false,
customStats: [
{ id: 'health', name: 'Health', enabled: true },
{ id: 'arousal', name: 'Arousal', enabled: true }
]
}
}
},
quests: {
main: "None", // Current main quest title
optional: [] // Array of optional quest titles
},
infoBox: JSON.stringify({
date: { value: new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }) },
weather: { emoji: '☀️', forecast: 'Clear skies' },
temperature: { value: 20, unit: 'C' },
time: { start: '00:00', end: '00:00' },
location: { value: 'Unknown Location' }
}, null, 2),
characterThoughts: JSON.stringify({
characters: []
}, null, 2),
level: 1, // User's character level
classicStats: {
str: 10,
dex: 10,
con: 10,
int: 10,
wis: 10,
cha: 10
},
lastDiceRoll: null, // Store last dice roll result
showDiceDisplay: true, // Show the "Last Roll" display in the panel
collapsedInventoryLocations: [], // Array of collapsed storage location names
inventoryViewModes: {
onPerson: 'list', // 'list' or 'grid' view mode for On Person section
stored: 'list', // 'list' or 'grid' view mode for Stored section
assets: 'list' // 'list' or 'grid' view mode for Assets section
},
npcAvatars: {}, // Store custom avatar images for NPCs (key: character name, value: base64 data URI)
// Combat encounter settings
encounterSettings: {
enabled: true, // Show Start Encounter button above chat input
historyDepth: 8, // Number of recent messages to include in combat initialization
autoSaveLogs: false // Save detailed combat logs to file
},
// Auto avatar generation settings
autoGenerateAvatars: true, // Master toggle for auto-generating avatars
avatarLLMCustomInstruction: '', // Custom instruction for LLM prompt generation
// External API settings for 'external' generation mode
externalApiSettings: {
baseUrl: '', // OpenAI-compatible API base URL (e.g., "https://api.openai.com/v1")
// apiKey is NOT stored here for security. It is stored in localStorage('rpg_companion_api_key')
model: '', // Model identifier (e.g., "gpt-4o-mini")
maxTokens: 8192, // Maximum tokens for generation
temperature: 0.7 // Temperature setting for generation
},
// Lock state for tracker items (v3 JSON format feature)
lockedItems: {
stats: [], // Array of locked stat IDs (e.g., ["health", "satiety"])
skills: [], // Array of locked skill names (e.g., ["Cooking", "Swordsmanship"])
inventory: {
onPerson: [], // Array of locked item indices (e.g., [0, 2])
clothing: [], // Array of locked item indices
stored: {}, // Object with location keys, each containing array of locked indices (e.g., {"Home": [0, 1]})
assets: [] // Array of locked asset indices
},
quests: {
main: false, // Boolean for main quest lock
optional: [] // Array of locked optional quest indices (e.g., [0, 2])
},
infoBox: {
date: false, // Boolean for date widget lock
weather: false, // Boolean for weather widget lock
temperature: false, // Boolean for temperature widget lock
time: false, // Boolean for time widget lock
location: false, // Boolean for location widget lock
recentEvents: false // Boolean for recent events widget lock
},
characters: {} // Object mapping character names to their locked fields (e.g., {"Sarah": {relationship: true, thoughts: false}})
},
// Preset management for tracker configurations
presetManager: {
// Map of preset ID to preset data (contains name and trackerConfig)
presets: {},
// Map of character/group entity to preset ID (e.g., "char_0": "preset_123", "group_abc": "preset_456")
// Note: This is stored separately and NOT exported with presets
characterAssociations: {},
// Currently active preset ID
activePresetId: null,
// Default preset ID (used when no character association exists)
defaultPresetId: null
}
};
export let extensionSettings = { ...defaultSettings };
/**
* Last generated data from AI response
@@ -594,3 +263,28 @@ export function setMusicPlayerContainer($element) {
export function setEquipmentContainer($element) {
$equipmentContainer = $element;
}
/**
* Clears all cached DOM element references.
* Call this when the panel is destroyed or rebuilt to prevent stale jQuery references.
*/
export function clearDomCache() {
$panelContainer = null;
$userStatsContainer = null;
$infoBoxContainer = null;
$thoughtsContainer = null;
$inventoryContainer = null;
$questsContainer = null;
$musicPlayerContainer = null;
$equipmentContainer = null;
}
/**
* Clears the debug logs array.
* Call this on chat change to prevent memory accumulation across sessions.
*/
export function clearDebugLogs() {
debugLogs.length = 0;
}
+11 -8
View File
@@ -1,7 +1,10 @@
const fs = require('fs-extra');
const path = require('path');
const chokidar = require('chokidar');
const glob = require('glob');
import fs from 'fs-extra';
import path from 'path';
import { fileURLToPath } from 'url';
import chokidar from 'chokidar';
import { globSync } from 'glob';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const COMPILED_DIR = __dirname // path.join(__dirname, 'compiled');
@@ -11,7 +14,7 @@ function findUnlocalizedText() {
console.log(`\n🔎 Scanning for unlocalized text in ${srcDir}...`);
const files = glob.sync(`${srcDir}/**/*.{html,js,jsx}`, {
const files = globSync(`${srcDir}/**/*.{html,js,jsx}`, {
ignore: ['**/node_modules/**', '**/dist/**', '**/build/**']
});
@@ -101,7 +104,7 @@ function validateTranslations() {
// Get all keys from reference locale
const referenceKeys = Object.keys(translations[referenceLocale]);
console.log(`🔢 Reference locale has ${referenceKeys.size} unique keys`);
console.log(`🔢 Reference locale has ${referenceKeys.length} unique keys`);
// Track statistics
const stats = {
@@ -127,7 +130,7 @@ function validateTranslations() {
// Check for missing keys
for (const key of referenceKeys) {
if (!key in translations[locale]) {
if (!(key in translations[locale])) {
stats.missingKeys[locale].push(key);
} else {
// Check for type mismatches
@@ -146,7 +149,7 @@ function validateTranslations() {
// Check for extra keys
for (const key of localeKeys) {
if (!key in translations[referenceLocale]) {
if (!(key in translations[referenceLocale])) {
stats.extraKeys[locale].push(key);
}
}
+265
View File
@@ -0,0 +1,265 @@
/* Ensure chat stays centered when panel is visible */
body:has(.rpg-panel.rpg-position-right) #sheld {
margin: auto !important;
}
body:has(.rpg-panel.rpg-position-left) #sheld {
margin: auto !important;
}
/* ============================================
RPG COMPANION - GAME-LIKE UI - RESPONSIVE
============================================ */
/* CSS Variables for Theming - Default uses SillyTavern's variables */
.rpg-panel,
#rpg-thought-panel,
#rpg-thought-icon,
#rpg-alt-present-characters,
.rpg-mobile-toggle {
--rpg-bg: var(--SmartThemeBlurTintColor, rgba(26, 26, 46, 0.9));
--rpg-accent: var(--black30a, rgba(22, 33, 62, 0.9));
--rpg-text: var(--SmartThemeBodyColor, #eaeaea);
--rpg-highlight: var(--SmartThemeQuoteColor, #e94560);
--rpg-border: var(--SmartThemeBorderColor, #4a7ba7);
--rpg-shadow: rgba(0, 0, 0, 0.5);
}
/* Main Panel Container - Fills available margin space */
.rpg-panel {
position: fixed;
top: var(--topBarBlockSize);
bottom: 0;
background: var(--rpg-bg);
box-shadow: 0 0 20px var(--rpg-shadow);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
box-sizing: border-box;
z-index: 1000;
color: var(--rpg-text);
display: flex;
flex-direction: column;
overflow: visible;
transition: width 0.3s ease, left 0.3s ease, right 0.3s ease, transform 0.3s ease;
}
/* Collapsed state - fixed width for collapse */
.rpg-panel.rpg-collapsed {
max-width: 2.5rem !important;
min-width: 2.5rem !important;
left: auto !important;
right: auto !important;
}
.rpg-panel.rpg-collapsed.rpg-position-right {
right: 0 !important;
}
.rpg-panel.rpg-collapsed.rpg-position-left {
left: 0 !important;
}
/* Fix positioning when collapsed - panel should stick to correct edge */
.rpg-panel.rpg-position-right.rpg-collapsed {
left: calc(100vw - 40px);
right: 0;
}
.rpg-panel.rpg-position-left.rpg-collapsed {
right: calc(100vw - 40px);
left: 0;
}
.rpg-panel.rpg-collapsed .rpg-game-container {
opacity: 0;
pointer-events: none;
}
/* Collapse/Expand Toggle Button */
.rpg-collapse-toggle {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 1.875rem;
height: 5rem;
background: var(--rpg-accent);
border: 2px solid var(--rpg-border);
color: var(--rpg-highlight);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
z-index: 10001;
transition: all 0.3s ease;
font-size: clamp(1rem, 1.5vw, 1.5rem);
box-shadow: 0 0 10px var(--rpg-shadow);
pointer-events: auto;
}
.rpg-collapse-toggle:hover {
background: var(--rpg-highlight);
color: var(--rpg-bg);
box-shadow: 0 0 15px var(--rpg-highlight);
}
/* Position collapse button on the left side for right panel */
.rpg-panel.rpg-position-right .rpg-collapse-toggle {
left: -2rem;
border-radius: 0.5em 0 0 0.5em;
}
/* Position collapse button on the right side for left panel */
.rpg-panel.rpg-position-left .rpg-collapse-toggle {
right: -2rem;
border-radius: 0 0.5em 0.5em 0;
}
/* Hide collapse button for top position */
.rpg-panel.rpg-position-top .rpg-collapse-toggle {
display: none;
}
/* Right Position (Default) - Panel fills right margin space from chat edge to screen edge */
.rpg-panel.rpg-position-right {
right: 0;
width: calc(50vw - var(--sheldWidth) / 2);
border-left: 3px solid var(--rpg-border);
box-shadow: -5px 0 20px var(--rpg-shadow);
}
/* Left Position - Panel fills left margin space from screen edge to chat edge */
.rpg-panel.rpg-position-left {
left: 0;
width: calc(50vw - var(--sheldWidth) / 2);
border-right: 3px solid var(--rpg-border);
box-shadow: 5px 0 20px var(--rpg-shadow);
}
/* Top Position */
.rpg-panel.rpg-position-top {
left: 0;
right: 0;
bottom: auto;
width: 100%;
max-width: 100%;
height: auto;
max-height: 18.75rem;
border-bottom: 3px solid var(--rpg-border);
box-shadow: 0 5px 20px var(--rpg-shadow);
}
/* Top Position Layout Adjustments */
.rpg-panel.rpg-position-top .rpg-content-box {
display: flex;
flex-direction: row;
gap: 0.938em;
}
.rpg-panel.rpg-position-top .rpg-section {
flex: 1;
min-width: 0;
}
.rpg-panel.rpg-position-top .rpg-divider {
display: none;
}
.rpg-panel.rpg-position-top .rpg-stats-grid {
gap: 0.5em;
}
.rpg-panel.rpg-position-top .rpg-stat-row {
gap: 0.5em;
}
.rpg-panel.rpg-position-top .rpg-stat-label {
min-width: 5.625rem;
}
.rpg-panel.rpg-position-top .rpg-stat-bar {
height: 1.125rem;
}
/* Mobile Responsiveness */
@media (max-width: 1024px) {
.rpg-panel {
width: 20rem;
max-width: 35vw;
min-width: 16.25rem;
}
}
@media (max-width: 768px) {
.rpg-section-title {
font-size: 1em;
}
}
@media (max-width: 768px) {
/* Left position on mobile/tablet */
body.rpg-panel-position-left #rpg-companion-panel.rpg-panel,
#rpg-companion-panel.rpg-panel.rpg-position-left {
left: 0 !important;
right: auto !important;
width: 85% !important;
height: 100dvh !important;
top: 0 !important;
bottom: 0 !important;
}
/* Right position on mobile/tablet */
body.rpg-panel-position-right #rpg-companion-panel.rpg-panel,
#rpg-companion-panel.rpg-panel.rpg-position-right {
left: auto !important;
right: 0 !important;
width: 85% !important;
height: 100dvh !important;
top: 0 !important;
bottom: 0 !important;
}
/* Top position on mobile */
#rpg-companion-panel.rpg-panel.rpg-position-top {
left: 0 !important;
right: 0 !important;
width: 100% !important;
max-height: 40vh !important;
top: 0 !important;
bottom: auto !important;
}
}
.rpg-panel.rpg-position-top .rpg-stats-title {
font-size: clamp(1rem, 1.3vw, 1.3rem);
}
.rpg-panel.rpg-position-top .rpg-mood {
font-size: clamp(0.875rem, 1vw, 1rem);
padding: 0.375em;
}
.rpg-panel.rpg-position-top .rpg-classic-stats {
margin-top: 0.5em;
padding: 0.5em;
}
.rpg-panel.rpg-position-top .rpg-classic-stats-title {
font-size: clamp(0.875rem, 1vw, 1rem);
margin-bottom: 0.5em;
}
.rpg-panel.rpg-position-top .rpg-classic-stats-grid {
gap: 0.375em;
}
.rpg-panel.rpg-position-top .rpg-classic-stat {
padding: 0.25em;
}
.rpg-panel.rpg-position-top .rpg-classic-stat-label {
font-size: clamp(0.75rem, 0.85vw, 0.875rem);
}
.rpg-panel.rpg-position-top .rpg-classic-stat-value {
font-size: clamp(1rem, 1.3vw, 1.3rem);
}
+318
View File
@@ -0,0 +1,318 @@
/* ============================================
ANIMATIONS
============================================ */
/* Only apply animations when enabled */
.rpg-panel.rpg-animations-enabled .rpg-stat-fill {
transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1);
}
.rpg-panel.rpg-animations-enabled .rpg-stat-bar {
transition: all 0.3s ease;
}
.rpg-panel.rpg-animations-enabled .rpg-info-content,
.rpg-panel.rpg-animations-enabled .rpg-thoughts-content {
transition: opacity 0.4s ease, transform 0.4s ease;
}
.rpg-panel.rpg-animations-enabled .rpg-info-line {
animation: slideInFromLeft 0.5s ease-out backwards;
}
.rpg-panel.rpg-animations-enabled .rpg-info-line:nth-child(1) { animation-delay: 0.1s; }
.rpg-panel.rpg-animations-enabled .rpg-info-line:nth-child(2) { animation-delay: 0.2s; }
.rpg-panel.rpg-animations-enabled .rpg-info-line:nth-child(3) { animation-delay: 0.3s; }
.rpg-panel.rpg-animations-enabled .rpg-info-line:nth-child(4) { animation-delay: 0.4s; }
.rpg-panel.rpg-animations-enabled .rpg-info-line:nth-child(5) { animation-delay: 0.5s; }
.rpg-panel.rpg-animations-enabled .rpg-thoughts-overlay {
animation: fadeInScale 0.6s ease-out;
}
.rpg-panel.rpg-animations-enabled .rpg-mood-value {
transition: all 0.3s ease;
}
.rpg-panel.rpg-animations-enabled .rpg-dice-animation {
animation: none; /* Control separately */
}
/* Disable dice animation when animations are off */
.rpg-panel:not(.rpg-animations-enabled) .rpg-dice-rolling i {
animation: none !important;
}
.rpg-panel:not(.rpg-animations-enabled) .rpg-dice-result-value {
animation: none !important;
}
@keyframes slideInFromLeft {
from {
opacity: 0;
transform: translateX(-1.25rem);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes fadeInScale {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
/* Add updating class for smooth content changes */
.rpg-panel.rpg-animations-enabled .rpg-content-updating {
animation: contentPulse 0.5s ease;
}
@keyframes contentPulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.6;
}
}
.rpg-panel.rpg-position-top .rpg-classic-stat-btn {
padding: 0.125em;
font-size: clamp(0.875rem, 1.1vw, 1.1rem);
}
.rpg-panel.rpg-position-top .rpg-info-content,
.rpg-panel.rpg-position-top .rpg-thoughts-content {
font-size: clamp(0.875rem, 1.1vw, 1.1rem);
max-height: 9.375rem;
}
/* Custom Scrollbar */
.rpg-panel::-webkit-scrollbar,
#rpg-panel-content::-webkit-scrollbar,
.rpg-content-box::-webkit-scrollbar {
width: 0.5rem;
}
.rpg-panel::-webkit-scrollbar-track,
#rpg-panel-content::-webkit-scrollbar-track,
.rpg-content-box::-webkit-scrollbar-track {
background: var(--rpg-accent);
}
.rpg-panel::-webkit-scrollbar-thumb,
#rpg-panel-content::-webkit-scrollbar-thumb,
.rpg-content-box::-webkit-scrollbar-thumb {
background: var(--rpg-highlight);
border-radius: 0.25em;
}
.rpg-panel::-webkit-scrollbar-thumb:hover,
#rpg-panel-content::-webkit-scrollbar-thumb:hover,
.rpg-content-box::-webkit-scrollbar-thumb:hover {
background: var(--rpg-border);
}
/* Game Container - Full height flex container */
.rpg-game-container {
padding: 0.75em;
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
transition: opacity 0.3s ease;
}
/* Panel Content - Main scrollable area */
#rpg-panel-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.375em;
overflow-y: hidden;
overflow-x: hidden;
min-height: 0;
}
/* Header - Fixed size, doesn't grow */
.rpg-panel-header {
display: flex;
justify-content: space-between;
align-items: center;
margin: 0;
margin-bottom: 0.5em;
padding: 0;
border-bottom: 2px solid var(--rpg-border);
flex-shrink: 0;
}
.rpg-panel-header h3 {
margin: 0;
font-size: clamp(1.25rem, 1.7vw, 1.75rem);
font-weight: bold;
display: flex;
align-items: center;
gap: 0.5em;
color: var(--rpg-highlight);
text-shadow: 0 0 8px var(--rpg-highlight);
letter-spacing: 0.031em;
border-left: none !important;
padding-left: 0 !important;
}
.rpg-panel-header h3 i {
border-left: none !important;
padding-left: 0 !important;
color: var(--rpg-highlight);
}
.rpg-btn-icon {
padding: 0.5em 0.75em;
background: var(--rpg-accent);
border: 2px solid var(--rpg-border);
border-radius: 0.5em;
cursor: pointer;
color: var(--rpg-text);
transition: all 0.3s ease;
}
.rpg-btn-icon:hover {
background: var(--rpg-highlight);
border-color: var(--rpg-highlight);
transform: scale(1.05);
}
/* Loading Indicator - Fixed size */
.rpg-loading {
text-align: center;
padding: 0.938em;
background: var(--rpg-accent);
border-radius: 0.75em;
color: var(--rpg-highlight);
font-size: clamp(1rem, 1.3vw, 1.3rem);
font-weight: bold;
animation: pulseGlow 1.5s ease-in-out infinite;
flex-shrink: 0;
}
@keyframes pulseGlow {
0%, 100% {
opacity: 1;
box-shadow: 0 0 10px var(--rpg-highlight);
}
50% {
opacity: 0.7;
box-shadow: 0 0 20px var(--rpg-highlight);
}
}
/* Dice Roll Display - More compact */
.rpg-dice-display {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5em;
padding: 0.5em;
background: rgba(0, 0, 0, 0.3);
border-radius: 0.5em;
border: 2px solid var(--rpg-border);
font-size: clamp(0.875rem, 1vw, 1rem);
font-weight: bold;
color: var(--rpg-text);
cursor: pointer;
transition: all 0.3s ease;
flex-shrink: 0;
}
.rpg-dice-display:hover {
background: var(--rpg-accent);
border-color: var(--rpg-highlight);
transform: scale(1.02);
}
.rpg-dice-display i {
color: var(--rpg-highlight);
font-size: clamp(1rem, 1.3vw, 1.3rem);
}
/* Clear dice roll button */
.rpg-clear-dice-btn {
background: rgba(255, 0, 0, 0.2);
border: 1px solid rgba(255, 0, 0, 0.4);
border-radius: 0.25em;
color: #ff6b6b;
font-size: clamp(1rem, 1.5vw, 1.5rem);
font-weight: bold;
width: 1.25rem;
height: 1.25rem;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
padding: 0;
line-height: 1;
margin-left: auto;
}
.rpg-clear-dice-btn:hover {
background: rgba(255, 0, 0, 0.4);
border-color: #ff6b6b;
transform: scale(1.1);
}
.rpg-clear-dice-btn:active {
transform: scale(0.95);
}
/* Unified Content Box - Contains all sections, scales to fit viewport */
.rpg-content-box {
background: linear-gradient(135deg, var(--rpg-accent) 0%, var(--rpg-bg) 100%);
border: 2px solid var(--rpg-border);
border-radius: 0.625em;
padding: 0.5em;
box-shadow: 0 4px 18px var(--rpg-shadow), inset 0 0 12px rgba(0, 0, 0, 0.3);
display: flex;
flex-direction: column;
gap: 0.375em;
flex: 1;
min-height: 0;
overflow: hidden;
}
/* Section Styling - Flexible to scale with available space */
.rpg-section {
margin: 0;
padding: 0;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow-y: auto;
overflow-x: hidden;
}
.rpg-section:empty {
display: none;
}
/* Divider - More compact */
.rpg-divider {
width: 100%;
height: 0.062rem;
background: linear-gradient(to right, transparent, var(--rpg-highlight), transparent);
margin: 0.5em 0;
position: relative;
flex-shrink: 0;
}
.rpg-divider::after {
display: none; /* Remove diamond decoration for more space */
}
+635
View File
@@ -0,0 +1,635 @@
/* ============================================
USER STATS SECTION
============================================ */
#rpg-user-stats {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
overflow: auto;
}
.rpg-stats-section {
text-align: center;
display: flex;
flex-direction: column;
min-height: 0;
flex: 1;
overflow-y: auto;
overflow-x: hidden;
position: relative; /* For absolute positioning of lock icon on mobile */
}
/* Hide the stats header - we'll integrate portrait/inventory into stats content */
.rpg-stats-header {
display: none;
}
.rpg-stats-header-left {
display: none;
}
.rpg-user-portrait {
width: clamp(24px, 4vh, 32px);
height: clamp(24px, 4vh, 32px);
border-radius: 50%;
border: 2px solid var(--rpg-highlight);
box-shadow: 0 0 8px var(--rpg-highlight);
object-fit: cover;
transition: transform 0.3s ease;
flex-shrink: 0;
}
.rpg-user-portrait:hover {
transform: scale(1.1) rotate(5deg);
}
.rpg-stats-title {
display: none;
}
/* Inventory Box - Styled like a treasure chest */
.rpg-inventory-box {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
background: linear-gradient(135deg, var(--rpg-accent) 0%, var(--rpg-bg) 100%);
border: 2px solid var(--rpg-highlight);
border-radius: clamp(4px, 0.8vh, 6px);
padding: clamp(3px, 0.6vh, 5px);
max-height: clamp(35px, 5vh, 45px);
min-height: clamp(28px, 4vh, 35px);
box-shadow:
inset 0 2px 4px rgba(255, 255, 255, 0.1),
inset 0 -2px 4px rgba(0, 0, 0, 0.3),
0 2px 8px rgba(0, 0, 0, 0.3);
overflow: hidden;
}
/* Add decorative chest details with pseudo-elements */
.rpg-inventory-box::before {
content: '';
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 60%;
height: 0.125rem;
background: var(--rpg-highlight);
opacity: 0.5;
}
.rpg-inventory-box::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 80%;
height: 0.062rem;
background: var(--rpg-highlight);
opacity: 0.3;
}
.rpg-inventory-items {
font-size: clamp(0.625rem, 0.6vw, 0.75rem);
color: var(--rpg-text);
line-height: 1.3;
overflow-y: auto;
flex: 1;
padding: clamp(1px, 0.3vh, 3px);
text-align: left;
}
.rpg-inventory-items::-webkit-scrollbar {
width: 0.188rem;
}
.rpg-inventory-items::-webkit-scrollbar-track {
background: var(--rpg-bg);
border-radius: 2px;
}
.rpg-inventory-items::-webkit-scrollbar-thumb {
background: var(--rpg-highlight);
border-radius: 2px;
}
.rpg-inventory-items::-webkit-scrollbar-thumb:hover {
background: var(--rpg-text);
}
/* Stats Content - Two-column layout */
.rpg-stats-content {
display: flex;
gap: clamp(4px, 0.8vh, 8px);
flex: 1;
min-height: 0;
overflow: hidden;
position: relative; /* For absolute positioning of lock icon */
}
/* Stats Left - Portrait, Bars and mood */
.rpg-stats-left {
flex: 1;
display: flex;
flex-direction: column;
gap: clamp(3px, 0.6vh, 6px);
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
}
.rpg-stats-left::-webkit-scrollbar {
width: 0.188rem;
}
.rpg-stats-left::-webkit-scrollbar-track {
background: var(--rpg-bg);
border-radius: 2px;
}
.rpg-stats-left::-webkit-scrollbar-thumb {
background: var(--rpg-highlight);
border-radius: 2px;
}
.rpg-stats-left::-webkit-scrollbar-thumb:hover {
background: var(--rpg-text);
}
/* User info row (portrait, name, separator, level) */
.rpg-user-info-row {
display: flex;
align-items: center;
justify-content: center;
gap: 0.375em;
font-size: clamp(0.5rem, 0.5vw, 0.625rem);
flex-shrink: 0;
}
/* User portrait */
.rpg-user-portrait {
width: clamp(12px, 1.8vw, 16px);
height: clamp(12px, 1.8vw, 16px);
border-radius: 50%;
object-fit: cover;
flex-shrink: 0;
}
/* User name and level - inline with portrait */
.rpg-user-name {
font-weight: 600;
font-size: 1em;
color: var(--rpg-text-color);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.rpg-level-label {
font-size: 1em;
font-weight: 600;
color: var(--rpg-text-color);
opacity: 0.7;
}
.rpg-level-value {
font-size: 1em;
font-weight: 700;
color: var(--rpg-highlight-color);
padding: clamp(1px, 0.2vh, 2px) 0.375em;
background: var(--rpg-accent-color);
border-radius: clamp(2px, 0.3vh, 3px);
border: 1px solid var(--rpg-highlight-color);
min-width: 1.5em;
text-align: center;
cursor: text;
transition: all 0.2s ease;
}
.rpg-level-value:hover {
background: var(--rpg-highlight-color);
color: var(--rpg-bg-color);
}
.rpg-level-value:focus {
outline: 2px solid var(--rpg-highlight-color);
outline-offset: 1px;
background: var(--rpg-bg-color);
}
/* Portrait and Inventory row at top of stats-left */
.rpg-stats-left > .rpg-user-portrait,
.rpg-stats-left > .rpg-inventory-box {
flex-shrink: 0;
}
.rpg-stats-left > .rpg-user-portrait {
align-self: flex-start;
}
.rpg-stats-left > .rpg-inventory-box {
width: 100%;
}
/* Stats Right - Attributes */
.rpg-stats-right {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.rpg-stats-grid {
display: flex;
flex-direction: column;
gap: clamp(3px, 0.5vh, 6px);
flex: 1 1 auto;
position: relative; /* For lock icon positioning */
overflow: visible;
}
.rpg-stats-grid::-webkit-scrollbar {
width: 0.188rem;
}
.rpg-stats-grid::-webkit-scrollbar-track {
background: var(--rpg-bg);
border-radius: 2px;
}
.rpg-stats-grid::-webkit-scrollbar-thumb {
background: var(--rpg-highlight);
border-radius: 2px;
}
.rpg-stats-grid::-webkit-scrollbar-thumb:hover {
background: var(--rpg-text);
}
.rpg-classic-stats-grid::-webkit-scrollbar {
width: 0.188rem;
}
.rpg-classic-stats-grid::-webkit-scrollbar-track {
background: var(--rpg-bg);
border-radius: 2px;
}
.rpg-classic-stats-grid::-webkit-scrollbar-thumb {
background: var(--rpg-highlight);
border-radius: 2px;
}
.rpg-classic-stats-grid::-webkit-scrollbar-thumb:hover {
background: var(--rpg-text);
}
/* Section-level lock icon styling - appears on hover in top-right corner */
.rpg-section-lock-icon {
position: absolute;
top: 8px;
right: 8px;
font-size: 1rem;
cursor: pointer;
opacity: 0;
transition: opacity 0.2s ease, transform 0.1s ease;
user-select: none;
z-index: 10;
pointer-events: none;
}
/* Show lock icon on hover or when section is locked */
.rpg-stats-content:hover .rpg-section-lock-icon,
.rpg-dashboard-widget:hover .rpg-section-lock-icon,
.rpg-character-card:hover .rpg-section-lock-icon,
.rpg-item-card:hover .rpg-section-lock-icon,
.rpg-item-row:hover .rpg-section-lock-icon,
.rpg-quest-item:hover .rpg-section-lock-icon,
.rpg-section-lock-icon.locked {
opacity: 0.7;
pointer-events: auto;
}
.rpg-section-lock-icon:hover {
opacity: 1;
transform: scale(1.15);
}
/* Mobile: always show lock icons since there's no hover */
@media (max-width: 1000px) {
.rpg-section-lock-icon {
opacity: 0.6 !important;
pointer-events: auto !important;
}
.rpg-section-lock-icon.locked {
opacity: 0.9 !important;
}
}
/* Make containers relative for absolute positioning of lock icon */
.rpg-stats-content,
.rpg-dashboard-widget,
.rpg-character-content,
.rpg-item-card,
.rpg-item-row,
.rpg-quest-item {
position: relative;
}
.rpg-stat-row {
display: flex;
align-items: center;
gap: 0.25em;
flex: 1 1 0;
min-width: fit-content;
max-width: 100%;
width: 100%;
min-height: clamp(12px, 1.8vh, 16px);
transition: background-color 0.2s ease;
overflow: visible;
box-sizing: border-box;
}
/* Old per-field lock styling - remove */
.rpg-lock-icon {
display: none;
}
.rpg-stat-row.rpg-locked {
background-color: transparent;
border-left: none;
padding-left: 0;
}
.rpg-stat-label {
flex: 0 0 auto;
min-width: 3rem;
max-width: 30%;
font-size: clamp(0.625rem, 0.6vw, 0.75rem);
font-weight: 600;
text-align: left;
color: var(--rpg-text);
text-transform: uppercase;
letter-spacing: 0.019em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.rpg-stat-bar {
flex: 1;
min-width: 3.75rem;
max-width: 100%;
width: 100%;
height: clamp(9px, 1.3vh, 11px);
min-height: 9px;
position: relative;
border-radius: 0.375em;
overflow: hidden;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.5);
border: 1px solid rgba(255, 255, 255, 0.1);
box-sizing: border-box;
}
.rpg-stat-fill {
position: absolute;
right: 0;
top: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
/* Transition moved to .rpg-animations-enabled */
}
.rpg-stat-value {
color: #fff;
font-size: clamp(0.625rem, 0.6vw, 0.75rem);
font-weight: bold;
min-width: 1.875rem;
text-align: right;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
}
/* Compact layout for high zoom / small viewports */
@media (max-height: 750px), (max-width: 1500px) {
.rpg-stat-row {
position: relative;
min-height: clamp(16px, 2.5vh, 20px);
}
.rpg-stat-label {
position: absolute;
left: 0.5em;
top: 50%;
transform: translateY(-50%);
z-index: 2;
min-width: auto;
max-width: calc(100% - 3rem);
font-size: min(0.7vw, 0.875rem);
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.9);
overflow: hidden;
white-space: nowrap;
}
.rpg-stat-bar {
width: 100% !important;
max-width: 100% !important;
height: 100%;
min-height: clamp(16px, 2.5vh, 20px);
box-sizing: border-box;
}
.rpg-stat-value {
position: absolute;
right: 0.5em;
top: 50%;
transform: translateY(-50%);
z-index: 2;
min-width: auto;
font-size: clamp(0.75rem, 0.7vw, 0.875rem);
}
.rpg-mood {
font-size: clamp(0.625rem, 1vw, 0.75rem);
flex-shrink: 1;
min-width: 0;
}
.rpg-mood-emoji {
font-size: clamp(0.875rem, 2vw, 1.125rem);
}
.rpg-mood-conditions {
font-size: clamp(0.625rem, 1vw, 0.75rem);
word-wrap: break-word;
overflow-wrap: break-word;
}
}
.rpg-mood {
display: flex;
align-items: center;
gap: 0.375em;
font-size: clamp(0.5rem, 0.5vw, 0.625rem);
padding: clamp(4px, 0.6vh, 6px) 0.375em;
background: rgba(0, 0, 0, 0.3);
border-radius: 0.25em;
border: 1px solid var(--rpg-border);
flex-shrink: 0;
position: relative; /* For lock icon positioning */
}
.rpg-mood-emoji {
font-size: clamp(12px, 1.8vw, 16px);
flex-shrink: 0;
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.5));
}
.rpg-mood-conditions {
flex: 1;
min-width: 0;
line-height: 1.2;
color: var(--rpg-text);
font-weight: 600;
word-wrap: break-word;
overflow-wrap: break-word;
hyphens: auto;
}
/* Skills Section */
.rpg-skills-section {
display: flex;
align-items: center;
gap: 0.375em;
font-size: clamp(0.5rem, 0.5vw, 0.625rem);
padding: clamp(4px, 0.6vh, 6px) 0.375em;
background: rgba(0, 0, 0, 0.3);
border-radius: 0.25em;
border: 1px solid var(--rpg-border);
flex-shrink: 0;
margin-top: 0.375em;
position: relative; /* For lock icon positioning */
}
.rpg-skills-label {
font-weight: 700;
color: var(--rpg-highlight);
flex-shrink: 0;
}
.rpg-skills-value {
flex: 1;
min-width: 0;
line-height: 1.2;
color: var(--rpg-text);
font-weight: 600;
word-wrap: break-word;
overflow-wrap: break-word;
hyphens: auto;
}
/* Classic RPG Stats - Will match height of stats box automatically */
.rpg-classic-stats {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
overflow: hidden;
}
.rpg-classic-stats-title {
text-align: center;
font-size: clamp(0.75rem, 0.85vw, 0.875rem);
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.037em;
color: var(--rpg-highlight);
margin-bottom: 0.25em;
flex-shrink: 0;
}
.rpg-classic-stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: clamp(2px, 0.4vh, 4px);
flex: 1;
grid-auto-rows: 1fr;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
}
.rpg-classic-stat {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: clamp(1px, 0.15vh, 2px);
padding: clamp(3px, 0.5vh, 5px);
background: rgba(0, 0, 0, 0.3);
border-radius: 0.25em;
border: 1px solid var(--rpg-border);
box-sizing: border-box;
}
.rpg-classic-stat-label {
font-size: clamp(0.5rem, 0.5vw, 0.625rem);
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.006em;
color: var(--rpg-text);
line-height: 1;
flex-shrink: 0;
}
.rpg-classic-stat-value {
font-size: clamp(0.625rem, 0.6vw, 0.75rem);
font-weight: bold;
color: var(--rpg-highlight);
text-align: center;
line-height: 1;
flex-shrink: 0;
}
.rpg-classic-stat-buttons {
display: flex;
gap: clamp(3px, 0.4vh, 5px);
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 100%;
}
.rpg-classic-stat-btn {
width: clamp(18px, 2.2vh, 24px);
height: clamp(16px, 2vh, 20px);
padding: 0;
background: var(--rpg-accent);
border: 1px solid var(--rpg-border);
border-radius: 2px;
color: var(--rpg-text);
font-size: clamp(9px, 1.1vw, 12px);
font-weight: bold;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
line-height: 1;
flex: 0 0 auto;
}
.rpg-classic-stat-btn:hover {
background: var(--rpg-highlight);
border-color: var(--rpg-highlight);
transform: scale(1.05);
}
.rpg-classic-stat-btn:active {
transform: scale(0.95);
}
+740
View File
@@ -0,0 +1,740 @@
/* ============================================
INFO BOX SECTION
============================================ */
.rpg-info-header {
font-size: clamp(1rem, 1.3vw, 1.3rem);
font-weight: bold;
margin-bottom: 0.625em;
color: var(--rpg-highlight);
text-shadow: 0 0 8px var(--rpg-highlight);
flex-shrink: 0;
}
.rpg-info-content {
font-size: clamp(0.875rem, 1.1vw, 1.1rem);
line-height: 1.5;
text-align: left;
flex: 1;
min-height: 0;
overflow-y: auto;
}
/* ============================================
INFO BOX - VISUAL DASHBOARD
============================================ */
.rpg-info-section {
background: rgba(0, 0, 0, 0.2);
border: 2px solid var(--rpg-border);
border-radius: 0.75em;
padding: 0.375em;
margin-bottom: 0;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
gap: 0.25em;
align-items: stretch;
width: 100%;
overflow-y: auto;
overflow-x: hidden;
}
/* Scrollable content wrapper inside info section */
.rpg-info-content {
display: grid;
grid-template-rows: 80px 60px minmax(min-content, auto);
gap: 0.25em;
flex: 1;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
}
.rpg-info-content::-webkit-scrollbar {
width: 0.188rem;
}
.rpg-info-content::-webkit-scrollbar-track {
background: var(--rpg-bg);
border-radius: 2px;
}
.rpg-info-content::-webkit-scrollbar-thumb {
background: var(--rpg-highlight);
border-radius: 2px;
}
.rpg-info-content::-webkit-scrollbar-thumb:hover {
background: var(--rpg-text);
}
.rpg-dashboard {
display: flex;
gap: 0.25em;
flex: 1;
min-height: 0;
overflow-x: auto;
overflow-y: hidden;
}
/* Row 1: 4 equal-width widgets - more square aspect ratio */
.rpg-dashboard-row-1 {
flex: 0 0 auto;
min-height: 0;
height: clamp(60px, 10vh, 80px);
}
.rpg-dashboard-row-1 .rpg-dashboard-widget {
flex: 1 1 0;
min-width: 0;
height: 100%;
aspect-ratio: 1 / 1;
}
/* Row 2: Full-width location */
.rpg-dashboard-row-2 {
flex: 0 0 auto;
min-height: 0;
}
.rpg-dashboard-row-2 .rpg-dashboard-widget {
flex: 1;
width: 100%;
height: 100%;
}
.rpg-dashboard-widget {
background: rgba(0, 0, 0, 0.5);
border: 2px solid var(--rpg-border);
border-radius: 0.375em;
padding: 0.25em;
transition: transform 0.2s ease, box-shadow 0.2s ease;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
}
.rpg-dashboard-widget:hover {
transform: translateY(-0.125rem);
box-shadow: 0 4px 12px var(--rpg-shadow);
}
/* Location widget - flexible height */
.rpg-location-widget {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.125em;
padding: 0.25em;
overflow: hidden;
}
/* Calendar Widget */
.rpg-calendar-widget {
padding: 0.188em;
container-type: size;
height: 100%;
overflow: hidden;
}
.rpg-calendar-top {
background: var(--rpg-highlight);
color: var(--rpg-bg);
font-size: clamp(0.5rem, 3.5cqh, 0.625rem);
font-weight: bold;
padding: 0.125em 0.375em;
border-radius: 3px 3px 0 0;
width: 100%;
text-align: center;
word-wrap: break-word;
overflow-wrap: break-word;
white-space: normal;
max-width: 100%;
line-height: 1.2;
overflow: hidden;
}
.rpg-calendar-day {
background: rgba(255, 255, 255, 0.1);
color: var(--rpg-text);
font-size: clamp(0.625rem, 5cqh, 0.875rem);
font-weight: bold;
padding: 0.25em;
width: 100%;
text-align: center;
border: 2px solid var(--rpg-highlight);
border-top: none;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
max-width: 100%;
line-height: 1.1;
min-width: 0;
overflow-y: auto;
overflow-x: hidden;
}
.rpg-calendar-day-text {
max-width: 100%;
word-wrap: break-word;
overflow-wrap: break-word;
hyphens: auto;
}
.rpg-calendar-year {
font-size: clamp(0.5rem, 3.5cqh, 0.625rem);
color: var(--rpg-text);
opacity: 0.7;
margin-top: 0.062em;
width: 100%;
text-align: center;
word-wrap: break-word;
overflow-wrap: break-word;
white-space: normal;
max-width: 100%;
line-height: 1.2;
overflow: hidden;
}
/* Minimal scrollbar styling for calendar day */
.rpg-calendar-day::-webkit-scrollbar {
width: 3px;
}
.rpg-calendar-day::-webkit-scrollbar-track {
background: transparent;
}
.rpg-calendar-day::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 2px;
}
.rpg-calendar-day::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
/* Weather Widget */
.rpg-weather-widget {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.188em;
padding: 0.25em;
max-width: 100%;
height: 100%;
overflow: hidden;
box-sizing: border-box;
container-type: size;
}
/* Weather Widget Icon */
.rpg-weather-icon {
font-size: clamp(0.875rem, 2.5vw, 1.25rem);
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.5));
flex-shrink: 0;
line-height: 1;
}
.rpg-weather-forecast {
font-size: clamp(0.5rem, 4cqh, 0.75rem);
text-align: center;
margin: 0;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.013em;
opacity: 0.85;
line-height: 1.1;
word-wrap: break-word;
overflow-wrap: break-word;
max-width: 100%;
width: 100%;
white-space: normal;
flex-shrink: 1;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
hyphens: auto;
}
.rpg-weather-forecast.rpg-editable {
margin-top: 0;
}
/* Temperature Widget - Thermometer */
.rpg-temp-widget {
gap: 0.188em;
}
.rpg-thermometer {
position: relative;
width: 1.25rem;
height: 2.5rem;
display: flex;
flex-direction: column;
align-items: center;
}
.rpg-thermometer-tube {
position: relative;
width: 0.5rem;
height: 1.75rem;
background: rgba(255, 255, 255, 0.1);
border: 2px solid var(--rpg-border);
border-radius: 0.625em 0.625em 0 0;
overflow: hidden;
display: flex;
align-items: flex-end;
}
.rpg-thermometer-fill {
width: 100%;
background: linear-gradient(to top, #e94560, #ff6b6b);
transition: height 0.5s ease;
border-radius: 0.5em 0.5em 0 0;
}
.rpg-thermometer-bulb {
position: absolute;
bottom: 0;
width: 0.875rem;
height: 0.875rem;
background: var(--rpg-highlight);
border: 2px solid var(--rpg-border);
border-radius: 50%;
z-index: 1;
}
.rpg-temp-value {
font-size: clamp(0.625rem, 0.6vw, 0.75rem);
font-weight: bold;
color: var(--rpg-text);
text-align: center;
}
/* Clock Widget */
.rpg-clock-widget {
gap: 0.188em;
}
.rpg-clock {
width: 2.625rem;
height: 2.625rem;
border-radius: 50%;
background: rgba(0, 0, 0, 0.4);
border: 3px solid var(--rpg-border);
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.5);
position: relative;
}
.rpg-clock-face {
width: 100%;
height: 100%;
position: relative;
}
.rpg-clock-hour,
.rpg-clock-minute {
position: absolute;
background: var(--rpg-highlight);
transform-origin: bottom center;
left: 50%;
bottom: 50%;
border-radius: 2px 2px 0 0;
}
.rpg-clock-hour {
width: 0.188rem;
height: 0.75rem;
margin-left: -0.094em;
opacity: 0.9;
}
.rpg-clock-minute {
width: 0.125rem;
height: 1rem;
margin-left: -0.062em;
}
.rpg-clock-center {
position: absolute;
width: 0.312rem;
height: 0.312rem;
background: var(--rpg-highlight);
border-radius: 50%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 2;
}
.rpg-time-value {
font-size: clamp(0.625rem, 0.6vw, 0.75rem);
font-weight: bold;
color: var(--rpg-text);
text-align: center;
margin-top: 0.25em;
}
.rpg-time-range {
display: flex;
align-items: center;
justify-content: center;
gap: 0.25em;
margin-top: 0.25em;
overflow-x: auto;
overflow-y: hidden;
max-width: 100%;
white-space: nowrap;
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
}
.rpg-time-range .rpg-time-value {
margin-top: 0;
min-width: 2.5em;
}
.rpg-time-separator {
font-size: clamp(0.5rem, 0.5vw, 0.625rem);
color: var(--rpg-text-muted, var(--rpg-text));
opacity: 0.7;
}
/* Minimal scrollbar styling for time range display */
.rpg-time-range::-webkit-scrollbar {
height: 3px;
}
.rpg-time-range::-webkit-scrollbar-track {
background: transparent;
}
.rpg-time-range::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 2px;
}
.rpg-time-range::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
/* Location Widget - Map */
.rpg-map-bg {
width: 100%;
height: auto;
max-height: 60%;
padding: 0.5em 0;
margin: 0;
background:
linear-gradient(45deg, rgba(255,255,255,0.05) 25%, transparent 25%),
linear-gradient(-45deg, rgba(255,255,255,0.05) 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, rgba(255,255,255,0.05) 75%),
linear-gradient(-45deg, transparent 75%, rgba(255,255,255,0.05) 75%);
background-size: 6px 6px;
background-position: 0 0, 0 3px, 3px -3px, -3px 0px;
background-color: rgba(0, 0, 0, 0.3);
border: 2px solid var(--rpg-border);
border-radius: 0.25em;
display: flex;
align-items: center;
justify-content: center;
position: relative;
flex: 0 0 auto;
}
.rpg-map-marker {
font-size: clamp(0.875rem, 2.5vw, 1.25rem);
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.8));
animation: markerPulse 2s ease-in-out infinite;
line-height: 1;
}
@keyframes markerPulse {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-2px); }
}
.rpg-location-text {
font-size: clamp(0.625rem, 0.6vw, 0.75rem);
font-weight: bold;
color: var(--rpg-text);
text-align: center;
line-height: 1.2;
padding: 0;
margin: 0;
word-wrap: break-word;
overflow-wrap: break-word;
hyphens: auto;
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
max-height: 100%;
}
/* Minimal scrollbar styling for location text */
.rpg-location-text::-webkit-scrollbar {
width: 3px;
}
.rpg-location-text::-webkit-scrollbar-track {
background: transparent;
}
.rpg-location-text::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 2px;
}
.rpg-location-text::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
/* Row 3: Recent Events */
.rpg-dashboard-row-3 {
flex: 0 0 auto;
min-height: 0;
}
.rpg-dashboard-row-3 .rpg-dashboard-widget {
flex: 1;
width: 100%;
}
/* Recent Events Widget - Notebook Style */
.rpg-events-widget {
background: linear-gradient(to bottom,
rgba(255, 248, 220, 0.08) 0%,
rgba(255, 248, 220, 0.12) 50%,
rgba(255, 248, 220, 0.08) 100%);
border: 2px solid var(--rpg-border);
border-radius: 0.375em;
padding: 0;
display: flex;
flex-direction: column;
gap: 0;
transition: transform 0.2s ease, box-shadow 0.2s ease;
min-height: 0;
overflow: hidden;
position: relative;
}
/* Notebook paper lines effect */
.rpg-events-widget::before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-image: repeating-linear-gradient(
transparent,
transparent calc(1em + 0.5em),
rgba(var(--SmartThemeBorderColor), 0.08) calc(1em + 0.5em),
rgba(var(--SmartThemeBorderColor), 0.08) calc(1em + 0.6em)
);
pointer-events: none;
z-index: 0;
}
.rpg-events-widget:hover {
transform: translateY(-0.125rem);
box-shadow: 0 4px 12px var(--rpg-shadow);
}
/* Notebook ring binding at top */
.rpg-notebook-header {
display: flex;
justify-content: center;
gap: 1.5em;
padding: 0.25em 0;
background: rgba(0, 0, 0, 0.2);
border-bottom: 1px solid var(--rpg-border);
position: relative;
z-index: 1;
}
.rpg-notebook-ring {
width: 0.5em;
height: 0.5em;
border-radius: 50%;
background: radial-gradient(circle at 30% 30%,
rgba(80, 80, 80, 0.4),
rgba(40, 40, 40, 0.6));
border: 1px solid rgba(0, 0, 0, 0.5);
box-shadow: inset 0 1px 2px rgba(255, 255, 255, 0.2),
0 1px 2px rgba(0, 0, 0, 0.3);
}
.rpg-notebook-title {
font-size: clamp(0.625rem, 0.6vw, 0.75rem);
font-weight: bold;
color: var(--rpg-highlight);
text-align: center;
padding: 0.25em 0.5em 0.25em 0.5em;
text-transform: uppercase;
letter-spacing: 0.05em;
opacity: 0.9;
position: relative;
z-index: 1;
}
.rpg-notebook-lines {
display: flex;
flex-direction: column;
gap: 0.125em;
padding: 0.25em 0.75em 0.5em 0.75em;
position: relative;
z-index: 1;
overflow-y: auto;
flex: 1 1 auto;
}
.rpg-notebook-lines::-webkit-scrollbar {
width: 0.188rem;
}
.rpg-notebook-lines::-webkit-scrollbar-track {
background: var(--rpg-bg);
border-radius: 2px;
}
.rpg-notebook-lines::-webkit-scrollbar-thumb {
background: var(--rpg-highlight);
border-radius: 2px;
}
.rpg-notebook-lines::-webkit-scrollbar-thumb:hover {
background: var(--rpg-text);
}
.rpg-notebook-line {
display: flex;
align-items: flex-start;
gap: 0.375em;
position: relative;
}
.rpg-notebook-line.rpg-event-add {
opacity: 0.5;
}
.rpg-notebook-line.rpg-event-add:hover {
opacity: 0.8;
}
.rpg-bullet {
font-size: clamp(0.625rem, 0.6vw, 0.75rem);
color: var(--rpg-highlight);
flex-shrink: 0;
line-height: 1.4;
opacity: 0.8;
}
.rpg-event-add .rpg-bullet {
color: var(--rpg-text);
opacity: 0.6;
}
.rpg-event-text {
font-size: clamp(0.563rem, 0.55vw, 0.688rem);
color: var(--rpg-text);
line-height: 1.4;
flex: 1;
word-wrap: break-word;
cursor: text;
padding: 0.125em 0.25em;
border-radius: 0.2em;
transition: background 0.2s ease;
opacity: 0.85;
}
.rpg-event-text.rpg-event-placeholder {
opacity: 0.5;
font-style: italic;
}
.rpg-event-text:hover {
background: rgba(255, 255, 255, 0.05);
opacity: 1;
}
.rpg-event-text:focus {
background: rgba(255, 255, 255, 0.1);
outline: 1px solid var(--rpg-highlight);
opacity: 1;
}
/* Character Status Cards */
.rpg-character-status {
display: flex;
flex-direction: column;
gap: 0.25em;
margin: 0;
padding: 0;
flex-shrink: 0;
}
.rpg-character-status-card {
display: flex;
align-items: center;
gap: 0.5em;
padding: 0.25em 0.375em;
background: rgba(0, 0, 0, 0.3);
border-left: 3px solid var(--rpg-highlight);
border-radius: 0.25em;
transition: all 0.2s ease;
}
.rpg-character-status-card:hover {
background: rgba(0, 0, 0, 0.5);
transform: translateX(0.188rem);
}
.rpg-char-emoji {
font-size: clamp(14px, 2.5vw, 18px);
flex-shrink: 0;
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.5));
}
.rpg-char-details {
flex: 1;
min-width: 0;
}
.rpg-char-name {
font-size: clamp(8px, 1.5vw, 10px);
font-weight: bold;
color: var(--rpg-highlight);
margin-bottom: 0.062em;
}
.rpg-char-traits {
font-size: clamp(7px, 1.3vw, 9px);
color: var(--rpg-text);
opacity: 0.8;
line-height: 1.2;
}/* Old info line styles (legacy) */
.rpg-info-line {
margin: 0.375em 0;
padding: 0.375em 0.625em;
background: rgba(0, 0, 0, 0.2);
border-left: 2px solid var(--rpg-highlight);
border-radius: 0.25em;
transition: all 0.3s ease;
}
.rpg-info-line:hover {
background: rgba(0, 0, 0, 0.4);
transform: translateX(0.312rem);
}
+679
View File
@@ -0,0 +1,679 @@
/* ============================================
CHARACTER THOUGHTS SECTION
============================================ */
.rpg-thoughts-section {
text-align: center;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow-y: auto;
overflow-x: hidden;
}
.rpg-thoughts-header {
font-size: clamp(1rem, 1.3vw, 1.3rem);
font-weight: bold;
margin-bottom: 0.625em;
color: var(--rpg-highlight);
text-shadow: 0 0 8px var(--rpg-highlight);
flex-shrink: 0;
}
.rpg-thoughts-content {
position: relative;
padding: 0.5em;
border-radius: 0.5em;
border-left: 3px solid var(--rpg-highlight);
box-shadow: inset 0 0 8px rgba(0, 0, 0, 0.3);
text-align: left;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
gap: clamp(6px, 1vh, 8px);
overflow-y: auto;
overflow-x: hidden;
/* Remove centering for multiple character cards */
}
/* Individual thought item */
.rpg-thought-item {
display: flex;
align-items: flex-start;
gap: 0.625em;
padding: 0.5em;
margin-bottom: 0.5em;
background: rgba(0, 0, 0, 0.3);
border-radius: 0.375em;
width: 100%; /* Ensure items take full width */
box-sizing: border-box; /* Include padding in width calculation */
}
.rpg-thought-item:last-child {
margin-bottom: 0;
}
/* Character avatar with thought bubbles */
.rpg-thought-avatar {
position: relative;
flex-shrink: 0;
}
.rpg-thought-avatar img {
width: clamp(30px, 5vh, 40px);
height: clamp(30px, 5vh, 40px);
border-radius: 50%;
border: 2px solid var(--rpg-highlight);
object-fit: cover;
}
/* Thought bubbles - Left side, ascending size from bottom-left to top-right */
.rpg-thought-bubbles {
position: absolute;
bottom: -0.25rem;
left: -0.5rem;
display: flex;
flex-direction: column;
gap: 0.125em;
align-items: flex-start;
}
.rpg-bubble {
background: var(--rpg-highlight);
border-radius: 50%;
opacity: 0.8;
}
.rpg-bubble-1 {
width: 0.5rem;
height: 0.5rem;
}
.rpg-bubble-2 {
width: 0.375rem;
height: 0.375rem;
margin-left: 0.125em;
}
/* Thought content */
.rpg-thought-content {
flex: 1;
min-width: 0;
}
.rpg-thought-name {
font-weight: bold;
color: var(--rpg-highlight);
font-size: clamp(8px, 1.5vw, 10px);
margin-bottom: 0.188em;
text-transform: uppercase;
letter-spacing: 0.031em;
}
.rpg-thought-name::before,
.rpg-thought-name::after {
content: none !important;
}
.rpg-thought-text {
font-size: clamp(9px, 1.6vw, 11px);
font-style: italic;
line-height: 1.3;
color: var(--rpg-text);
opacity: 0.9;
}
.rpg-thought-text::before {
content: ''; /* Explicitly ensure no emoji */
}
/* Overlay to fade the background portrait and provide contrast */
.rpg-thoughts-content::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
135deg,
rgba(0, 0, 0, 0.85) 0%,
rgba(0, 0, 0, 0.75) 50%,
rgba(0, 0, 0, 0.85) 100%
);
z-index: 1;
display: none; /* Hide background overlay for new format */
}
/* Content wrapper to sit above the background */
.rpg-thoughts-overlay {
position: relative;
z-index: 2;
display: flex;
align-items: flex-start;
gap: 0.5em;
font-size: clamp(0.875rem, 1.1vw, 1.1rem);
font-style: italic;
line-height: 1.4;
}
/* Present Characters - Character Cards */
.rpg-character-card {
display: flex;
flex-direction: column;
gap: clamp(6px, 0.8vh, 10px);
padding: clamp(6px, 1vh, 8px);
background: rgba(0, 0, 0, 0.3);
border-radius: clamp(4px, 0.5vh, 6px);
border: 1px solid rgba(255, 255, 255, 0.1);
transition: all 0.2s ease;
width: 100%; /* Ensure cards take full width */
max-height: clamp(200px, 18vh, 250px);
box-sizing: border-box; /* Include padding and border in width calculation */
flex-shrink: 0; /* Prevent cards from shrinking */
overflow: auto;
scrollbar-width: thin;
scrollbar-color: var(--rpg-border) transparent;
}
.rpg-character-card::-webkit-scrollbar {
width: 4px;
height: 4px;
}
.rpg-character-card::-webkit-scrollbar-track {
background: transparent;
}
.rpg-character-card::-webkit-scrollbar-thumb {
background: var(--rpg-border);
border-radius: 2px;
}
.rpg-character-card::-webkit-scrollbar-thumb:hover {
background: var(--rpg-highlight);
}
.rpg-character-card:hover {
background: rgba(0, 0, 0, 0.4);
border-color: var(--rpg-highlight);
}
/* Header row with avatar and name */
.rpg-character-header-row {
display: flex;
align-items: center;
gap: clamp(8px, 1vw, 12px);
width: 100%;
}
/* Character avatar container with relationship badge */
.rpg-character-avatar {
position: relative;
flex-shrink: 0;
}
.rpg-character-avatar img {
width: clamp(30px, 5vh, 40px);
height: clamp(30px, 5vh, 40px);
border-radius: 50%;
border: 2px solid var(--rpg-highlight);
object-fit: cover;
display: block; /* Prevent inline spacing issues */
}
/* Uploadable avatar - make it clickable */
.rpg-avatar-upload {
cursor: pointer;
transition: all 0.2s ease;
position: relative;
}
.rpg-avatar-upload::after {
content: '📷';
position: absolute;
bottom: -2px;
right: -2px;
font-size: clamp(10px, 1.5vh, 14px);
background: var(--rpg-bg);
border: 1px solid var(--rpg-highlight);
border-radius: 50%;
width: clamp(18px, 2.5vh, 22px);
height: clamp(18px, 2.5vh, 22px);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.2s ease;
pointer-events: none;
}
.rpg-avatar-upload:hover::after {
opacity: 1;
}
.rpg-avatar-upload:hover {
transform: scale(1.05);
}
.rpg-avatar-upload:hover img {
opacity: 0.8;
border-color: var(--rpg-text);
}
.rpg-avatar-upload:active {
transform: scale(0.98);
}
/* Relationship badge in top-right corner */
.rpg-relationship-badge {
position: absolute;
top: -0.125rem;
right: -0.125rem;
background: var(--rpg-bg);
border: 1px solid var(--rpg-highlight);
border-radius: 50%;
width: clamp(16px, 2.5vh, 20px);
height: clamp(16px, 2.5vh, 20px);
display: flex;
align-items: center;
justify-content: center;
font-size: clamp(8px, 1.2vw, 12px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
/* Character info section - now takes full width below header row */
.rpg-character-content {
width: 100%;
display: flex;
flex-direction: column;
gap: clamp(3px, 0.5vh, 5px);
overflow: hidden;
}
.rpg-character-info {
display: flex;
flex-direction: column;
gap: clamp(3px, 0.5vh, 5px);
overflow-y: auto;
overflow-x: hidden;
flex: 1;
min-height: 0;
scrollbar-width: thin;
scrollbar-color: var(--rpg-border) transparent;
}
.rpg-character-info::-webkit-scrollbar {
width: 4px;
}
.rpg-character-info::-webkit-scrollbar-track {
background: transparent;
}
.rpg-character-info::-webkit-scrollbar-thumb {
background: var(--rpg-border);
border-radius: 2px;
}
.rpg-character-info::-webkit-scrollbar-thumb:hover {
background: var(--rpg-highlight);
}
/* Character header with emoji and name - now inside header row */
.rpg-character-header {
display: flex;
align-items: center;
gap: clamp(4px, 0.5vw, 6px);
flex-wrap: nowrap; /* Prevent wrapping */
flex: 1;
min-width: 0;
}
.rpg-character-emoji {
font-size: clamp(12px, 2vw, 16px);
flex-shrink: 0;
}
.rpg-character-name {
font-weight: bold;
color: var(--rpg-highlight);
font-size: clamp(0.75rem, 0.75vw, 0.875rem);
text-transform: uppercase;
letter-spacing: 0.031em;
white-space: nowrap; /* Prevent name from wrapping */
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
}
/* Character remove button */
.rpg-character-remove {
background: transparent;
border: none;
color: var(--rpg-text);
font-size: clamp(14px, 2vw, 18px);
font-weight: bold;
cursor: pointer;
padding: 0;
margin-left: auto;
width: clamp(16px, 2.5vh, 20px);
height: clamp(16px, 2.5vh, 20px);
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
opacity: 0.5;
transition: all 0.2s ease;
flex-shrink: 0;
}
.rpg-character-remove:hover {
opacity: 1;
background: rgba(255, 0, 0, 0.2);
color: #ff4444;
transform: scale(1.1);
}
.rpg-character-remove:active {
transform: scale(0.95);
}
/* Add Character Button (inside rpg-thoughts-content, after last character) */
.rpg-add-character-btn {
background: var(--rpg-accent);
border: 1px solid var(--rpg-border);
color: var(--rpg-text);
padding: 0;
margin: 0.5rem auto 0;
font-size: clamp(0.625rem, 0.6vw, 0.75rem);
font-weight: 500;
cursor: pointer;
border-radius: 3px;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 0.25rem;
opacity: 0.6;
width: auto;
}
.rpg-add-character-btn:hover {
background: var(--rpg-highlight);
border-color: var(--rpg-highlight);
color: var(--rpg-bg);
opacity: 1;
}
.rpg-add-character-btn:active {
transform: scale(0.95);
}
.rpg-add-character-btn i {
font-size: 0.875em;
}
/* Character traits/status line and custom fields */
.rpg-character-traits,
.rpg-character-field {
font-size: clamp(0.75rem, 0.7vw, 0.875rem);
color: var(--rpg-text);
opacity: 0.8;
line-height: 1.3;
overflow-wrap: break-word; /* Allow long text to wrap */
word-wrap: break-word;
}
/* Empty field placeholder using data-placeholder attribute */
.rpg-editable.rpg-empty-field:empty::before {
content: attr(data-placeholder);
color: var(--rpg-highlight);
opacity: 0.4;
font-style: italic;
pointer-events: none;
}
/* Ensure empty fields have minimum height for clickability */
.rpg-editable.rpg-empty-field {
min-height: 1.2em;
display: inline-block;
min-width: 3em;
}
/* Character stats display */
.rpg-character-stats {
width: 100%;
max-height: clamp(50px, 7vh, 70px);
margin-top: clamp(3px, 0.5vh, 5px);
padding: clamp(3px, 0.4vh, 5px) clamp(4px, 0.5vw, 6px);
background: var(--rpg-bg);
border: 1px solid var(--rpg-border);
border-radius: clamp(2px, 0.3vh, 4px);
box-sizing: border-box;
overflow: auto;
scrollbar-width: thin;
scrollbar-color: var(--rpg-border) transparent;
}
.rpg-character-stats::-webkit-scrollbar {
width: 4px;
height: 4px;
}
.rpg-character-stats::-webkit-scrollbar-track {
background: transparent;
}
.rpg-character-stats::-webkit-scrollbar-thumb {
background: var(--rpg-border);
border-radius: 2px;
}
.rpg-character-stats::-webkit-scrollbar-thumb:hover {
background: var(--rpg-highlight);
}
.rpg-character-stats-inner {
display: flex;
flex-wrap: wrap;
gap: clamp(6px, 1vw, 12px);
align-items: center;
}
.rpg-character-stat {
flex-shrink: 0;
}
.rpg-character-stat .rpg-stat-name {
font-size: clamp(9px, 0.6vw, 0.7vw) !important;
font-weight: 600 !important;
white-space: nowrap !important;
}
/* Placeholder styles for empty sections */
.rpg-thoughts-placeholder,
.rpg-placeholder-widget {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: clamp(12px, 2vh, 20px);
text-align: center;
opacity: 0.6;
}
.rpg-placeholder-text {
font-size: clamp(10px, 1.6vw, 14px);
color: var(--rpg-text);
font-weight: bold;
margin-bottom: clamp(4px, 0.6vh, 6px);
}
.rpg-placeholder-hint {
font-size: clamp(8px, 1.2vw, 10px);
color: var(--rpg-text);
opacity: 0.7;
font-style: italic;
}
/* Editable field styles */
.rpg-editable,
.rpg-editable-stat,
.rpg-editable-stat-name {
cursor: text;
transition: all 0.2s ease;
border-radius: 2px;
padding: 0.062em 0.125em;
}
.rpg-editable:hover,
.rpg-editable-stat:hover,
.rpg-editable-stat-name:hover {
background: var(--rpg-accent);
outline: 1px solid var(--rpg-highlight);
}
.rpg-editable:focus,
.rpg-editable-stat:focus,
.rpg-editable-stat-name:focus {
background: var(--rpg-bg);
outline: 2px solid var(--rpg-highlight);
box-shadow: 0 0 8px var(--rpg-highlight);
}
/* Edit button container and styling */
.rpg-edit-button-container {
display: flex;
justify-content: center;
padding: clamp(4px, 0.8vh, 8px);
margin-top: clamp(4px, 0.8vh, 8px);
}
.rpg-edit-button {
background: var(--rpg-accent);
border: 1px solid var(--rpg-highlight);
color: var(--rpg-text);
padding: clamp(3px, 0.6vh, 6px) clamp(6px, 1.2vh, 12px);
border-radius: clamp(3px, 0.6vh, 6px);
font-size: clamp(7px, 1.2vw, 10px);
cursor: pointer;
display: flex;
align-items: center;
gap: clamp(2px, 0.4vh, 4px);
transition: all 0.2s ease;
}
.rpg-edit-button:hover {
background: var(--rpg-highlight);
color: var(--rpg-bg);
transform: translateY(-0.062rem);
box-shadow: 0 2px 8px var(--rpg-highlight);
}
.rpg-edit-button:active {
transform: translateY(0);
}
.rpg-edit-button i {
font-size: clamp(7px, 1.2vw, 10px);
}
/* Removed emoji icon styling - no longer needed */
/* Settings Styling */
.rpg-settings {
margin-top: 0.625em;
padding-top: 0.5em;
border-top: 1px solid #444;
}
.rpg-settings summary {
cursor: pointer;
font-weight: bold;
padding: 0.375em;
background: rgba(0, 0, 0, 0.2);
border-radius: 0.25em;
margin-bottom: 0.5em;
display: flex;
align-items: center;
gap: 0.375em;
font-size: clamp(0.875rem, 1.1vw, 1.1rem);
}
.rpg-settings summary:hover {
background: rgba(255, 255, 255, 0.1);
}
.rpg-settings-content {
padding: 0.5em;
}
.rpg-settings-content label {
display: block;
margin: 0.375em 0;
}
.rpg-setting-row {
margin: 0.5em 0;
}
.rpg-setting-row label {
display: block;
margin-bottom: 0.188em;
font-size: clamp(0.875rem, 1vw, 1rem);
}
.rpg-setting-row input[type="number"] {
width: 100%;
padding: 0.25em;
border: 1px solid #444;
border-radius: 3px;
background: rgba(0, 0, 0, 0.3);
color: inherit;
font-size: clamp(0.875rem, 1vw, 1rem);
}
.rpg-setting-row small {
display: block;
margin-top: 0.188em;
color: #888;
font-size: clamp(0.75rem, 0.95vw, 0.95rem);
}
#rpg-manual-update {
width: 100%;
margin-top: 0.5em;
padding: 0.375em;
display: flex;
align-items: center;
justify-content: center;
gap: 0.375em;
font-size: clamp(0.875rem, 1.1vw, 1.1rem);
}
/* Responsive adjustments */
@media (max-width: 768px) {
.rpg-panel {
max-width: 100%;
}
.rpg-stat-label {
min-width: 5rem;
font-size: clamp(0.875rem, 1.1vw, 1.1rem);
}
}
/* Animation for stats updates */
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.6;
}
}
.rpg-stat-row.updating {
animation: pulse 0.5s ease-in-out;
}
+605
View File
@@ -0,0 +1,605 @@
/* ============================================
SETTINGS SECTION
============================================ */
.rpg-settings {
margin-top: 0.938em;
padding-top: 0.938em;
border-top: 2px solid var(--rpg-border);
}
.rpg-settings summary {
cursor: pointer;
font-weight: bold;
padding: 0.75em;
background: var(--rpg-accent);
border: 2px solid var(--rpg-border);
border-radius: 0.625em;
margin-bottom: 0.938em;
display: flex;
align-items: center;
gap: 0.625em;
transition: all 0.3s ease;
}
.rpg-settings summary:hover {
background: var(--rpg-highlight);
border-color: var(--rpg-highlight);
transform: translateX(0.312rem);
}
.rpg-settings-content {
padding: 0.938em;
background: var(--rpg-accent);
border-radius: 0.625em;
}
.rpg-settings-group {
margin-bottom: 1.25em;
padding-bottom: 0.938em;
border-bottom: 1px solid var(--rpg-border);
}
.rpg-settings-group:last-child {
border-bottom: none;
}
.rpg-settings-group h4 {
margin: 0 0 0.75em 0;
font-size: clamp(1.125rem, 1.5vw, 1.5rem);
color: var(--rpg-highlight);
display: flex;
align-items: center;
gap: 0.5em;
}
.rpg-setting-row {
margin: 0.75em 0;
}
.rpg-setting-row label {
display: block;
margin-bottom: 0.375em;
font-size: clamp(0.938rem, 1.2vw, 1.2rem);
font-weight: 600;
}
.rpg-select,
.rpg-input {
width: 100%;
padding: 0.5em;
border: 2px solid var(--rpg-border);
border-radius: 0.375em;
background: var(--rpg-bg);
color: var(--rpg-text);
font-size: clamp(1rem, 1.3vw, 1.3rem);
transition: all 0.3s ease;
}
.rpg-select:focus,
.rpg-input:focus {
outline: none;
border-color: var(--rpg-highlight);
box-shadow: 0 0 10px var(--rpg-highlight);
}
.rpg-setting-row input[type="color"] {
width: 100%;
height: 2.5rem;
padding: 0.25em;
border: 2px solid var(--rpg-border);
border-radius: 0.375em;
background: var(--rpg-bg);
cursor: pointer;
transition: all 0.3s ease;
/* Ensure color picker works on all browsers */
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
/* Remove padding for better color swatch display */
.rpg-setting-row input[type="color"]::-webkit-color-swatch-wrapper {
padding: 0;
}
.rpg-setting-row input[type="color"]::-webkit-color-swatch {
border: none;
border-radius: 0.25em;
}
.rpg-setting-row input[type="color"]::-moz-color-swatch {
border: none;
border-radius: 0.25em;
}
.rpg-setting-row input[type="color"]:hover {
border-color: var(--rpg-highlight);
}
.rpg-setting-row small {
display: block;
margin-top: 0.25em;
color: #999;
font-size: clamp(0.875rem, 1vw, 1rem);
font-style: italic;
}
.rpg-custom-colors {
margin-top: 0.625em;
padding: 0.938em;
background: rgba(0, 0, 0, 0.2);
border-radius: 0.5em;
border: 1px solid var(--rpg-border);
}
.checkbox_label {
display: flex;
align-items: center;
gap: 0.5em;
margin: 0.625em 0;
cursor: pointer;
transition: all 0.3s ease;
}
.checkbox_label input[type="checkbox"] {
accent-color: #666 !important;
}
.checkbox_label input[type="checkbox"]:checked {
background-color: #666 !important;
}
.rpg-panel[data-theme="sci-fi"] .checkbox_label input[type="checkbox"],
.rpg-settings-popup[data-theme="sci-fi"] .checkbox_label input[type="checkbox"] {
accent-color: var(--rpg-highlight) !important;
}
.rpg-panel[data-theme="sci-fi"] .checkbox_label input[type="checkbox"]:checked,
.rpg-settings-popup[data-theme="sci-fi"] .checkbox_label input[type="checkbox"]:checked {
background-color: var(--rpg-highlight) !important;
}
.rpg-panel[data-theme="fantasy"] .checkbox_label input[type="checkbox"],
.rpg-settings-popup[data-theme="fantasy"] .checkbox_label input[type="checkbox"] {
accent-color: var(--rpg-highlight) !important;
}
.rpg-panel[data-theme="fantasy"] .checkbox_label input[type="checkbox"]:checked,
.rpg-settings-popup[data-theme="fantasy"] .checkbox_label input[type="checkbox"]:checked {
background-color: var(--rpg-highlight) !important;
}
.rpg-panel[data-theme="cyberpunk"] .checkbox_label input[type="checkbox"],
.rpg-settings-popup[data-theme="cyberpunk"] .checkbox_label input[type="checkbox"] {
accent-color: var(--rpg-highlight) !important;
}
.rpg-panel[data-theme="cyberpunk"] .checkbox_label input[type="checkbox"]:checked,
.rpg-settings-popup[data-theme="cyberpunk"] .checkbox_label input[type="checkbox"]:checked {
background-color: var(--rpg-highlight) !important;
}
.rpg-panel[data-theme="custom"] .checkbox_label input[type="checkbox"],
.rpg-settings-popup[data-theme="custom"] .checkbox_label input[type="checkbox"] {
accent-color: var(--rpg-highlight) !important;
}
.rpg-panel[data-theme="custom"] .checkbox_label input[type="checkbox"]:checked,
.rpg-settings-popup[data-theme="custom"] .checkbox_label input[type="checkbox"]:checked {
background-color: var(--rpg-highlight) !important;
}
/* Radio buttons - default theme */
.checkbox_label input[type="radio"] {
accent-color: #666 !important;
}
/* Radio buttons - sci-fi theme */
.rpg-panel[data-theme="sci-fi"] .checkbox_label input[type="radio"],
.rpg-settings-popup[data-theme="sci-fi"] .checkbox_label input[type="radio"] {
accent-color: var(--rpg-highlight) !important;
}
/* Radio buttons - fantasy theme */
.rpg-panel[data-theme="fantasy"] .checkbox_label input[type="radio"],
.rpg-settings-popup[data-theme="fantasy"] .checkbox_label input[type="radio"] {
accent-color: var(--rpg-highlight) !important;
}
/* Radio buttons - cyberpunk theme */
.rpg-panel[data-theme="cyberpunk"] .checkbox_label input[type="radio"],
.rpg-settings-popup[data-theme="cyberpunk"] .checkbox_label input[type="radio"] {
accent-color: var(--rpg-highlight) !important;
}
/* Radio buttons - custom theme */
.rpg-panel[data-theme="custom"] .checkbox_label input[type="radio"],
.rpg-settings-popup[data-theme="custom"] .checkbox_label input[type="radio"] {
accent-color: var(--rpg-highlight) !important;
}
/* All radio buttons (including those not in checkbox_label) - default theme */
.rpg-settings-popup input[type="radio"],
#rpg-tracker-editor-popup input[type="radio"] {
accent-color: #666 !important;
color-scheme: dark;
background-color: transparent !important;
}
.rpg-settings-popup input[type="radio"]:checked,
#rpg-tracker-editor-popup input[type="radio"]:checked {
background-color: #666 !important;
}
/* All radio buttons - sci-fi theme */
.rpg-settings-popup[data-theme="sci-fi"] input[type="radio"],
#rpg-tracker-editor-popup[data-theme="sci-fi"] input[type="radio"] {
accent-color: var(--rpg-highlight) !important;
color-scheme: dark;
background-color: transparent !important;
}
.rpg-settings-popup[data-theme="sci-fi"] input[type="radio"]:checked,
#rpg-tracker-editor-popup[data-theme="sci-fi"] input[type="radio"]:checked {
background-color: var(--rpg-highlight) !important;
}
/* All radio buttons - fantasy theme */
.rpg-settings-popup[data-theme="fantasy"] input[type="radio"],
#rpg-tracker-editor-popup[data-theme="fantasy"] input[type="radio"] {
accent-color: var(--rpg-highlight) !important;
color-scheme: dark;
background-color: transparent !important;
}
.rpg-settings-popup[data-theme="fantasy"] input[type="radio"]:checked,
#rpg-tracker-editor-popup[data-theme="fantasy"] input[type="radio"]:checked {
background-color: var(--rpg-highlight) !important;
}
/* All radio buttons - cyberpunk theme */
.rpg-settings-popup[data-theme="cyberpunk"] input[type="radio"],
#rpg-tracker-editor-popup[data-theme="cyberpunk"] input[type="radio"] {
accent-color: var(--rpg-highlight) !important;
color-scheme: dark;
background-color: transparent !important;
}
.rpg-settings-popup[data-theme="cyberpunk"] input[type="radio"]:checked,
#rpg-tracker-editor-popup[data-theme="cyberpunk"] input[type="radio"]:checked {
background-color: var(--rpg-highlight) !important;
}
/* All radio buttons - custom theme */
.rpg-settings-popup[data-theme="custom"] input[type="radio"],
#rpg-tracker-editor-popup[data-theme="custom"] input[type="radio"] {
accent-color: var(--rpg-highlight) !important;
color-scheme: dark;
background-color: transparent !important;
}
.rpg-settings-popup[data-theme="custom"] input[type="radio"]:checked,
#rpg-tracker-editor-popup[data-theme="custom"] input[type="radio"]:checked {
background-color: var(--rpg-highlight) !important;
}
/* Tracker Editor checkboxes (not wrapped in checkbox_label) */
#rpg-tracker-editor-popup input[type="checkbox"] {
accent-color: #666 !important;
}
#rpg-tracker-editor-popup input[type="checkbox"]:checked {
background-color: #666 !important;
}
#rpg-tracker-editor-popup[data-theme="sci-fi"] input[type="checkbox"] {
accent-color: var(--rpg-highlight) !important;
}
#rpg-tracker-editor-popup[data-theme="sci-fi"] input[type="checkbox"]:checked {
background-color: var(--rpg-highlight) !important;
}
#rpg-tracker-editor-popup[data-theme="fantasy"] input[type="checkbox"] {
accent-color: var(--rpg-highlight) !important;
}
#rpg-tracker-editor-popup[data-theme="fantasy"] input[type="checkbox"]:checked {
background-color: var(--rpg-highlight) !important;
}
#rpg-tracker-editor-popup[data-theme="cyberpunk"] input[type="checkbox"] {
accent-color: var(--rpg-highlight) !important;
}
#rpg-tracker-editor-popup[data-theme="cyberpunk"] input[type="checkbox"]:checked {
background-color: var(--rpg-highlight) !important;
}
#rpg-tracker-editor-popup[data-theme="custom"] input[type="checkbox"] {
accent-color: var(--rpg-highlight) !important;
}
#rpg-tracker-editor-popup[data-theme="custom"] input[type="checkbox"]:checked {
background-color: var(--rpg-highlight) !important;
}
/* Panel checkboxes (not wrapped in checkbox_label) */
.rpg-panel input[type="checkbox"]:not(.checkbox_label input[type="checkbox"]) {
accent-color: #666 !important;
}
.rpg-panel input[type="checkbox"]:not(.checkbox_label input[type="checkbox"]):checked {
background-color: #666 !important;
}
.rpg-panel[data-theme="sci-fi"] input[type="checkbox"]:not(.checkbox_label input[type="checkbox"]) {
accent-color: var(--rpg-highlight) !important;
}
.rpg-panel[data-theme="sci-fi"] input[type="checkbox"]:not(.checkbox_label input[type="checkbox"]):checked {
background-color: var(--rpg-highlight) !important;
}
.rpg-panel[data-theme="fantasy"] input[type="checkbox"]:not(.checkbox_label input[type="checkbox"]) {
accent-color: var(--rpg-highlight) !important;
}
.rpg-panel[data-theme="fantasy"] input[type="checkbox"]:not(.checkbox_label input[type="checkbox"]):checked {
background-color: var(--rpg-highlight) !important;
}
.rpg-panel[data-theme="cyberpunk"] input[type="checkbox"]:not(.checkbox_label input[type="checkbox"]) {
accent-color: var(--rpg-highlight) !important;
}
.rpg-panel[data-theme="cyberpunk"] input[type="checkbox"]:not(.checkbox_label input[type="checkbox"]):checked {
background-color: var(--rpg-highlight) !important;
}
.rpg-panel[data-theme="custom"] input[type="checkbox"]:not(.checkbox_label input[type="checkbox"]) {
accent-color: var(--rpg-highlight) !important;
}
.rpg-panel[data-theme="custom"] input[type="checkbox"]:not(.checkbox_label input[type="checkbox"]):checked {
background-color: var(--rpg-highlight) !important;
}
.checkbox_label:hover {
color: var(--rpg-highlight);
}
.rpg-btn-primary {
width: 100%;
padding: 0.75em;
background: var(--rpg-accent);
border: 2px solid var(--rpg-border);
border-radius: 0.625em;
color: var(--rpg-text);
font-size: clamp(1.063rem, 1.4vw, 1.4rem);
font-weight: bold;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 0.625em;
transition: all 0.3s ease;
box-shadow: 0 4px 15px var(--rpg-shadow);
}
.rpg-btn-primary:hover {
background: var(--rpg-highlight);
border-color: var(--rpg-highlight);
transform: translateY(-0.125rem);
box-shadow: 0 6px 20px var(--rpg-shadow);
}
.rpg-btn-primary:active {
transform: translateY(0);
}
/* Clear Cache Button - Danger style */
.rpg-btn-clear-cache {
width: 100%;
padding: 0.625em;
background: rgba(220, 53, 69, 0.2);
border: 2px solid rgba(220, 53, 69, 0.5);
border-radius: 0.5em;
color: #ff6b6b;
font-size: clamp(0.938rem, 1.2vw, 1.2rem);
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5em;
transition: all 0.3s ease;
}
.rpg-btn-clear-cache:hover {
background: rgba(220, 53, 69, 0.3);
border-color: rgba(220, 53, 69, 0.8);
color: #ff8787;
transform: translateY(-0.062rem);
}
.rpg-btn-clear-cache:active {
transform: translateY(0);
}
/* Reset FAB Positions Button - Similar to clear cache but different color */
.rpg-btn-reset-fab {
width: 100%;
padding: 0.625em;
background: rgba(52, 152, 219, 0.2);
border: 2px solid rgba(52, 152, 219, 0.5);
border-radius: 0.5em;
color: #5dade2;
font-size: clamp(0.938rem, 1.2vw, 1.2rem);
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5em;
transition: all 0.3s ease;
}
.rpg-btn-reset-fab:hover {
background: rgba(52, 152, 219, 0.3);
border-color: rgba(52, 152, 219, 0.8);
color: #85c1e9;
transform: translateY(-0.062rem);
}
.rpg-btn-reset-fab:active {
transform: translateY(0);
}
/* Customize Prompts Button - Neutral gray style */
.rpg-btn-customize-prompts {
width: 100%;
padding: 0.625em;
background: rgba(128, 128, 128, 0.2);
border: 2px solid rgba(128, 128, 128, 0.5);
border-radius: 0.5em;
color: #aaa;
font-size: clamp(0.938rem, 1.2vw, 1.2rem);
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5em;
transition: all 0.3s ease;
}
.rpg-btn-customize-prompts:hover {
background: rgba(128, 128, 128, 0.3);
border-color: rgba(128, 128, 128, 0.8);
color: #ccc;
transform: translateY(-0.062rem);
}
.rpg-btn-customize-prompts:active {
transform: translateY(0);
}
/* Memory Recollection styles removed - feature deprecated */
.rpg-memory-modal-header h3 {
margin: 0;
color: var(--SmartThemeBodyColor, #eaeaea);
font-size: 1.25em;
font-weight: 600;
}
/* Modal Body */
.rpg-memory-modal-body {
padding: 1.5em;
color: var(--SmartThemeBodyColor, #eaeaea);
}
.rpg-memory-modal-body p {
margin: 0.75em 0;
line-height: 1.6;
}
.rpg-memory-modal-info {
background: var(--rpg-border, rgba(102, 126, 234, 0.1));
padding: 1em;
border-radius: 8px;
border-left: 4px solid var(--rpg-border, var(--SmartThemeBorderColor, #4a7ba7));
margin-top: 1em;
}
.rpg-memory-modal-hint {
font-size: 0.85em;
color: #999;
}
/* Progress Elements */
.rpg-memory-progress-text {
text-align: center;
font-weight: 600;
margin-bottom: 1em;
}
.rpg-memory-progress-bar {
width: 100%;
height: 30px;
background: rgba(0, 0, 0, 0.3);
border-radius: 15px;
overflow: hidden;
position: relative;
}
.rpg-memory-progress-fill {
height: 100%;
width: 0%;
background: linear-gradient(90deg, var(--rpg-border, #4a7ba7) 0%, var(--rpg-highlight, #e94560) 100%);
transition: width 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 600;
font-size: 0.85em;
}
.rpg-memory-status {
margin-top: 1em;
padding: 0.75em;
background: rgba(0, 0, 0, 0.2);
border-radius: 6px;
font-size: 0.9em;
color: #999;
max-height: 100px;
overflow-y: auto;
}
/* Modal Footer */
.rpg-memory-modal-footer {
padding: 1em 1.25em;
border-top: 1px solid var(--rpg-border, var(--SmartThemeBorderColor, #4a7ba7));
display: flex;
gap: 0.75em;
justify-content: flex-end;
}
/* Modal Buttons */
.rpg-memory-modal-btn {
padding: 0.625em 1.25em;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
border: none;
font-size: 14px;
}
.rpg-memory-cancel {
background: rgba(220, 53, 69, 0.2);
color: #ff6b6b;
border: 2px solid rgba(220, 53, 69, 0.5);
}
.rpg-memory-cancel:hover {
background: rgba(220, 53, 69, 0.3);
border-color: rgba(220, 53, 69, 0.8);
}
.rpg-memory-proceed {
background: linear-gradient(135deg, var(--rpg-border, #4a7ba7) 0%, var(--rpg-highlight, #e94560) 100%);
color: white;
border: 2px solid var(--rpg-border-transparent, rgba(74, 123, 167, 0.5));
}
.rpg-memory-proceed:hover {
background: linear-gradient(135deg, var(--rpg-highlight, #e94560) 0%, var(--rpg-border, #4a7ba7) 100%);
border-color: var(--rpg-border-opaque, rgba(74, 123, 167, 0.8));
box-shadow: 0 4px 12px var(--rpg-border-transparent, rgba(74, 123, 167, 0.3));
}
/* Lorebook Limiter styles removed - feature deprecated */
+276
View File
@@ -0,0 +1,276 @@
/* ============================================
THEME VARIATIONS
============================================ */
/* Sci-Fi / Synthwave Theme */
.rpg-panel[data-theme="sci-fi"] {
--rpg-bg: #0a0e27;
--rpg-accent: #1a1f3a;
--rpg-text: #00fff9;
--rpg-highlight: #ff006e;
--rpg-border: #8b00ff;
--rpg-shadow: rgba(139, 0, 255, 0.5);
--rpg-content-bg: linear-gradient(135deg, #1a1f3a 0%, #0a0e27 100%);
--rpg-content-box-shadow: 0 0 40px rgba(139, 0, 255, 0.4), inset 0 0 30px rgba(0, 0, 0, 0.5);
--rpg-content-border: 2px solid #8b00ff;
--rpg-header-text-shadow: 0 0 20px #ff006e, 0 0 40px #8b00ff;
--rpg-divider-bg: linear-gradient(to right, transparent, #8b00ff, #ff006e, #8b00ff, transparent);
--rpg-thoughts-bg: linear-gradient(135deg, rgba(10, 14, 39, 0.9) 0%, rgba(26, 31, 58, 0.8) 50%, rgba(10, 14, 39, 0.9) 100%);
}
/* Apply sci-fi theme to thought panel */
#rpg-thought-panel[data-theme="sci-fi"],
#rpg-thought-icon[data-theme="sci-fi"],
#rpg-alt-present-characters[data-theme="sci-fi"],
.rpg-mobile-toggle[data-theme="sci-fi"] {
--rpg-bg: #0a0e27;
--rpg-accent: #1a1f3a;
--rpg-text: #00fff9;
--rpg-highlight: #ff006e;
--rpg-border: #8b00ff;
--rpg-shadow: rgba(139, 0, 255, 0.5);
}
/* Fantasy / Rustic Parchment Theme */
.rpg-panel[data-theme="fantasy"] {
--rpg-bg: #2b1810;
--rpg-accent: #3d2414;
--rpg-text: #f4e8d0;
--rpg-highlight: #d4af37;
--rpg-border: #8b6914;
--rpg-shadow: rgba(0, 0, 0, 0.7);
--rpg-bg-image: linear-gradient(rgba(43, 24, 16, 0.9), rgba(43, 24, 16, 0.9)), url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><filter id="noise"><feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="4" /></filter><rect width="100" height="100" filter="url(%23noise)" opacity="0.1"/></svg>');
--rpg-content-bg: linear-gradient(135deg, #3d2414 0%, #2b1810 100%);
--rpg-content-box-shadow: 0 8px 32px rgba(0, 0, 0, 0.8), inset 0 0 40px rgba(139, 105, 20, 0.2);
--rpg-content-border: 3px solid #8b6914;
--rpg-content-border-style: ridge;
--rpg-header-font-family: 'Georgia', serif;
--rpg-header-text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
--rpg-divider-content: '❦';
--rpg-divider-font-size: clamp(1rem, 1.5vw, 1.5rem);
--rpg-thoughts-bg: linear-gradient(135deg, rgba(43, 24, 16, 0.92) 0%, rgba(61, 36, 20, 0.88) 50%, rgba(43, 24, 16, 0.92) 100%);
}
/* Apply fantasy theme to thought panel */
#rpg-thought-panel[data-theme="fantasy"],
#rpg-thought-icon[data-theme="fantasy"],
#rpg-alt-present-characters[data-theme="fantasy"],
.rpg-mobile-toggle[data-theme="fantasy"] {
--rpg-bg: #2b1810;
--rpg-accent: #3d2414;
--rpg-text: #f4e8d0;
--rpg-highlight: #d4af37;
--rpg-border: #8b6914;
--rpg-shadow: rgba(0, 0, 0, 0.7);
}
/* Cyberpunk / Neon Grid Theme */
.rpg-panel[data-theme="cyberpunk"] {
--rpg-bg: #000000;
--rpg-accent: #0d0d0d;
--rpg-text: #00ff41;
--rpg-highlight: #ff2a6d;
--rpg-border: #05d9e8;
--rpg-shadow: rgba(5, 217, 232, 0.5);
--rpg-bg-image: linear-gradient(rgba(0, 0, 0, 0.95), rgba(0, 0, 0, 0.95)), repeating-linear-gradient(0deg, rgba(5, 217, 232, 0.1) 0px, transparent 1px, transparent 2px, rgba(5, 217, 232, 0.1) 3px), repeating-linear-gradient(90deg, rgba(5, 217, 232, 0.1) 0px, transparent 1px, transparent 2px, rgba(5, 217, 232, 0.1) 3px);
--rpg-bg-size: 100% 100%, 30px 30px, 30px 30px;
--rpg-content-bg: linear-gradient(135deg, rgba(13, 13, 13, 0.9) 0%, rgba(0, 0, 0, 0.9) 100%);
--rpg-content-box-shadow: 0 0 40px rgba(255, 42, 109, 0.4), inset 0 0 30px rgba(5, 217, 232, 0.2);
--rpg-content-border: 2px solid #05d9e8;
--rpg-header-font-family: 'Courier New', monospace;
--rpg-header-letter-spacing: 0.125em;
--rpg-header-text-shadow: 0 0 10px #ff2a6d, 0 0 20px #05d9e8, 0 0 30px #ff2a6d;
--rpg-divider-bg: linear-gradient(to right, transparent, #05d9e8, #ff2a6d, #05d9e8, transparent);
--rpg-divider-height: 0.188rem;
--rpg-thoughts-bg: linear-gradient(135deg, rgba(0, 0, 0, 0.92) 0%, rgba(13, 13, 13, 0.85) 50%, rgba(0, 0, 0, 0.92) 100%);
}
/* Apply cyberpunk theme to thought panel */
#rpg-thought-panel[data-theme="cyberpunk"],
#rpg-thought-icon[data-theme="cyberpunk"],
#rpg-alt-present-characters[data-theme="cyberpunk"],
.rpg-mobile-toggle[data-theme="cyberpunk"] {
--rpg-bg: #000000;
--rpg-accent: #0d0d0d;
--rpg-text: #00ff41;
--rpg-highlight: #ff2a6d;
--rpg-border: #05d9e8;
--rpg-shadow: rgba(5, 217, 232, 0.5);
}
/* ============================================
THEME SUPPORT FOR ALL PANEL ELEMENTS
Uses CSS custom properties defined above no selector duplication needed.
Any element inside .rpg-panel inherits the theme variables automatically.
============================================ */
/* Apply theme background image if defined */
.rpg-panel[data-theme] {
background-image: var(--rpg-bg-image, none);
background-size: var(--rpg-bg-size, auto);
}
/* Content box theming via custom properties */
.rpg-panel[data-theme] .rpg-content-box {
background: var(--rpg-content-bg);
box-shadow: var(--rpg-content-box-shadow);
border: var(--rpg-content-border);
border-style: var(--rpg-content-border-style, solid);
}
/* Header theming */
.rpg-panel[data-theme] .rpg-panel-header h3,
.rpg-panel[data-theme] .rpg-stats-title,
.rpg-panel[data-theme] .rpg-info-header,
.rpg-panel[data-theme] .rpg-thoughts-header {
font-family: var(--rpg-header-font-family, inherit);
text-shadow: var(--rpg-header-text-shadow, none);
letter-spacing: var(--rpg-header-letter-spacing, normal);
}
/* Divider theming */
.rpg-panel[data-theme] .rpg-divider {
background: var(--rpg-divider-bg);
height: var(--rpg-divider-height, auto);
}
.rpg-panel[data-theme] .rpg-divider::after {
content: var(--rpg-divider-content, none);
font-size: var(--rpg-divider-font-size, inherit);
}
/* Thoughts content theming */
.rpg-panel[data-theme] .rpg-thoughts-content::before {
background: var(--rpg-thoughts-bg);
}
/* Tabs navigation theming */
.rpg-panel[data-theme] .rpg-tabs-nav {
border-bottom-color: var(--rpg-border);
}
.rpg-panel[data-theme] .rpg-tab-btn {
background: var(--rpg-accent);
border-color: var(--rpg-border);
color: var(--rpg-text);
}
.rpg-panel[data-theme] .rpg-tab-btn:hover {
background: var(--rpg-highlight);
border-color: var(--rpg-highlight);
}
.rpg-panel[data-theme] .rpg-tab-btn.active {
background: var(--rpg-highlight);
border-color: var(--rpg-highlight);
color: var(--rpg-bg);
}
/* Inventory subtabs theming */
.rpg-panel[data-theme] .rpg-inventory-subtabs {
border-bottom-color: var(--rpg-border);
}
.rpg-panel[data-theme] .rpg-inventory-subtab {
border-color: var(--rpg-border);
color: var(--rpg-text);
}
.rpg-panel[data-theme] .rpg-inventory-subtab:hover {
border-color: var(--rpg-highlight);
color: var(--rpg-highlight);
}
.rpg-panel[data-theme] .rpg-inventory-subtab.active {
border-color: var(--rpg-highlight);
color: var(--rpg-highlight);
}
/* Quests subtabs theming */
.rpg-panel[data-theme] .rpg-quests-subtabs {
border-bottom-color: var(--rpg-border);
}
.rpg-panel[data-theme] .rpg-quests-subtab {
border-color: var(--rpg-border);
color: var(--rpg-text);
}
.rpg-panel[data-theme] .rpg-quests-subtab:hover {
border-color: var(--rpg-highlight);
color: var(--rpg-highlight);
}
.rpg-panel[data-theme] .rpg-quests-subtab.active {
border-color: var(--rpg-highlight);
color: var(--rpg-highlight);
}
/* Storage locations theming */
.rpg-panel[data-theme] .rpg-storage-location {
border-color: var(--rpg-border);
}
.rpg-panel[data-theme] .rpg-storage-header {
background: var(--rpg-highlight);
}
.rpg-panel[data-theme] .rpg-storage-content {
background: var(--rpg-accent);
}
/* Button theming */
.rpg-panel[data-theme] .rpg-inventory-edit-btn,
.rpg-panel[data-theme] .rpg-inventory-add-btn,
.rpg-panel[data-theme] .rpg-inventory-remove-btn,
.rpg-panel[data-theme] .rpg-quest-edit,
.rpg-panel[data-theme] .rpg-quest-remove,
.rpg-panel[data-theme] .rpg-add-quest-btn {
background: var(--rpg-accent);
border-color: var(--rpg-border);
color: var(--rpg-text);
}
.rpg-panel[data-theme] .rpg-inventory-add-btn,
.rpg-panel[data-theme] .rpg-add-quest-btn {
background: transparent;
border-color: var(--rpg-highlight);
color: var(--rpg-highlight);
}
/* Inventory/quest items theming */
.rpg-panel[data-theme] .rpg-inventory-text,
.rpg-panel[data-theme] .rpg-quest-item,
.rpg-panel[data-theme] .rpg-main-quest-display {
background: var(--rpg-accent);
border-color: var(--rpg-border);
color: var(--rpg-text);
}
.rpg-panel[data-theme] .rpg-quest-item:hover {
border-color: var(--rpg-highlight);
}
/* Hints and empty states theming */
.rpg-panel[data-theme] .rpg-inventory-hint,
.rpg-panel[data-theme] .rpg-quest-hint,
.rpg-panel[data-theme] .rpg-inventory-empty,
.rpg-panel[data-theme] .rpg-quest-empty {
border-color: var(--rpg-highlight);
color: var(--rpg-text);
}
/* Input fields theming */
.rpg-panel[data-theme] .rpg-inline-input,
.rpg-panel[data-theme] .rpg-quest-edit-form input,
.rpg-panel[data-theme] .rpg-quest-edit-form textarea {
background: var(--rpg-bg);
border-color: var(--rpg-border);
color: var(--rpg-text);
}
.rpg-panel[data-theme] .rpg-inline-input:focus,
.rpg-panel[data-theme] .rpg-quest-edit-form input:focus,
.rpg-panel[data-theme] .rpg-quest-edit-form textarea:focus {
border-color: var(--rpg-highlight);
}
+425
View File
@@ -0,0 +1,425 @@
/* ============================================
RESPONSIVE DESIGN
============================================ */
@media (max-width: 768px) {
.rpg-panel {
width: 100%;
max-width: 100%;
}
body:has(.rpg-panel) #sheld {
margin-right: 0;
}
.rpg-user-portrait {
width: 3.75rem;
height: 3.75rem;
}
.rpg-stats-title {
font-size: clamp(1.125rem, 1.5vw, 1.5rem);
}
}
/* ============================================
ANIMATIONS
============================================ */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(0.625rem);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.rpg-section {
animation: fadeIn 0.5s ease-out;
}
.rpg-stat-row {
animation: fadeIn 0.3s ease-out;
animation-fill-mode: both;
}
.rpg-stat-row:nth-child(1) { animation-delay: 0.1s; }
.rpg-stat-row:nth-child(2) { animation-delay: 0.15s; }
.rpg-stat-row:nth-child(3) { animation-delay: 0.2s; }
.rpg-stat-row:nth-child(4) { animation-delay: 0.25s; }
.rpg-stat-row:nth-child(5) { animation-delay: 0.3s; }
/* ============================================
DICE ROLL MODAL - MOBILE FIRST
============================================ */
/* CSS Custom Properties for Responsive Scaling */
.rpg-dice-popup {
/* Fluid spacing that scales with viewport */
--modal-padding: clamp(0.5rem, 2vw, 0.75rem);
--modal-gap: clamp(0.375rem, 1.5vw, 0.5rem);
--modal-border-width: 2px;
/* Fluid typography */
--modal-font-base: clamp(0.8rem, 3vw, 0.9rem);
--modal-font-small: clamp(0.7rem, 2.5vw, 0.8rem);
--modal-font-large: clamp(1.25rem, 6vw, 1.75rem);
--modal-font-huge: clamp(1.5rem, 8vw, 2.5rem);
/* Touch-friendly sizing */
--modal-button-height: 44px;
--modal-input-height: 44px;
/* Content constraints - MUCH more conservative */
--modal-max-width: min(90vw, 360px);
--modal-max-height: 70vh;
}
/* Modal Container - Hidden by default */
.rpg-dice-popup {
position: fixed;
inset: 0;
z-index: 10000;
display: none;
align-items: center;
justify-content: center;
padding: 0.5rem;
}
/* Open state - managed by JavaScript classList */
.rpg-dice-popup.is-open {
display: flex;
animation: fadeIn 0.2s ease-out;
}
/* Closing state - allows exit animation */
.rpg-dice-popup.is-closing {
display: flex;
animation: fadeOut 0.2s ease-in;
}
/* Backdrop overlay - using ::before pseudo-element */
.rpg-dice-popup::before {
content: '';
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
}
/* Modal Content Box */
.rpg-dice-popup-content {
position: relative;
width: 100%;
max-width: var(--modal-max-width);
height: auto;
max-height: var(--modal-max-height);
min-height: 0;
background: rgba(30, 30, 30, 0.8);
border: var(--modal-border-width) solid var(--rpg-border);
border-radius: 0.5rem;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.9);
color: var(--rpg-text);
display: flex;
flex-direction: column;
overflow: hidden;
animation: slideInUp 0.3s cubic-bezier(0.16, 1, 0.3, 1);
margin: auto 0;
}
/* Header */
.rpg-dice-popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--modal-padding);
background: var(--rpg-accent);
border-bottom: var(--modal-border-width) solid var(--rpg-border);
flex-shrink: 0;
}
.rpg-dice-popup-header h3 {
margin: 0;
font-size: var(--modal-font-base);
color: var(--rpg-highlight);
display: flex;
align-items: center;
gap: var(--modal-gap);
}
/* Close button - touch-friendly */
#rpg-dice-popup-close {
min-width: 44px;
min-height: 44px;
padding: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
}
/* Scrollable Body */
.rpg-dice-popup-body {
padding: var(--modal-padding);
overflow-y: auto;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
flex: 1 1 auto;
min-height: 0;
}
/* Input Container */
.rpg-dice-selector-container {
padding: var(--modal-padding);
background: rgba(0, 0, 0, 0.3);
border-radius: 0.5rem;
border: var(--modal-border-width) solid var(--rpg-border);
margin-bottom: var(--modal-gap);
}
/* Input Grid - Stacked on mobile */
.rpg-dice-selector {
display: grid;
grid-template-columns: 1fr;
gap: var(--modal-gap);
margin-bottom: var(--modal-gap);
}
.rpg-dice-input-group {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.rpg-dice-input-group label {
font-size: var(--modal-font-small);
font-weight: 600;
color: var(--rpg-text);
}
.rpg-dice-input-group input,
.rpg-dice-input-group select {
width: 100%;
min-height: var(--modal-input-height);
padding: 0.5rem;
border: var(--modal-border-width) solid var(--rpg-border);
border-radius: 0.375rem;
background: var(--rpg-accent);
color: var(--rpg-text);
font-size: var(--modal-font-base);
font-weight: 600;
text-align: center;
transition: all 0.2s ease;
}
.rpg-dice-input-group input:focus,
.rpg-dice-input-group select:focus {
outline: none;
border-color: var(--rpg-highlight);
box-shadow: 0 0 0 3px rgba(var(--rpg-highlight-rgb, 255, 0, 100), 0.2);
background: rgba(0, 0, 0, 0.5);
}
/* Roll Button - touch-friendly */
#rpg-dice-roll-btn {
width: 100%;
min-height: var(--modal-button-height);
padding: 0.75rem 1rem;
background: linear-gradient(135deg, var(--rpg-highlight), var(--rpg-accent));
border: var(--modal-border-width) solid var(--rpg-highlight);
border-radius: 0.5rem;
color: var(--rpg-text);
font-size: var(--modal-font-base);
font-weight: 700;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
#rpg-dice-roll-btn:active {
transform: scale(0.98);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
}
/* Animation Section */
.rpg-dice-animation {
text-align: center;
padding: var(--modal-padding);
}
.rpg-dice-rolling i {
font-size: var(--modal-font-large);
color: var(--rpg-highlight);
animation: diceRoll 0.8s ease-in-out infinite;
}
.rpg-dice-rolling-text {
margin-top: var(--modal-gap);
font-size: var(--modal-font-base);
font-weight: 600;
color: var(--rpg-highlight);
animation: pulseGlow 1s ease-in-out infinite;
}
/* Result Section */
.rpg-dice-result {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: var(--modal-padding);
background: rgba(0, 0, 0, 0.3);
border-radius: 0.5rem;
border: var(--modal-border-width) solid var(--rpg-border);
}
.rpg-dice-result-label {
font-size: var(--modal-font-small);
color: var(--rpg-text);
margin-bottom: 0.5rem;
text-transform: uppercase;
letter-spacing: 0.05em;
opacity: 0.8;
}
.rpg-dice-result-value {
font-size: var(--modal-font-huge);
font-weight: 700;
color: var(--rpg-highlight);
text-shadow: 0 0 20px var(--rpg-highlight);
line-height: 1;
}
.rpg-dice-result-value.is-animating {
animation: resultPop 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}
.rpg-dice-result-details {
margin-top: var(--modal-gap);
font-size: var(--modal-font-small);
color: var(--rpg-text);
opacity: 0.7;
}
/* Save Button */
.rpg-dice-save-btn {
margin-top: var(--modal-gap);
width: 100%;
min-height: var(--modal-button-height);
padding: 0.75rem 1rem;
background: linear-gradient(135deg, #28a745, #20c997);
border: var(--modal-border-width) solid #28a745;
border-radius: 0.5rem;
color: white;
font-size: var(--modal-font-base);
font-weight: 700;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 4px 12px rgba(40, 167, 69, 0.4);
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.rpg-dice-save-btn:active {
transform: scale(0.98);
box-shadow: 0 2px 8px rgba(40, 167, 69, 0.4);
}
/* Animations */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes slideInUp {
from {
opacity: 0;
transform: translateY(20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes diceRoll {
0%, 100% { transform: rotate(0deg); }
25% { transform: rotate(90deg) scale(1.1); }
50% { transform: rotate(180deg); }
75% { transform: rotate(270deg) scale(1.1); }
}
@keyframes resultPop {
0% { transform: scale(0); opacity: 0; }
50% { transform: scale(1.1); }
100% { transform: scale(1); opacity: 1; }
}
/* Theme Support - CSS Custom Properties */
.rpg-dice-popup[data-theme="sci-fi"] .rpg-dice-popup-content {
--rpg-bg: #0a0e27;
--rpg-accent: #1a1f3a;
--rpg-text: #00fff9;
--rpg-highlight: #ff006e;
--rpg-border: #00fff9;
}
.rpg-dice-popup[data-theme="fantasy"] .rpg-dice-popup-content {
--rpg-bg: #2c1810;
--rpg-accent: #3d2817;
--rpg-text: #f4e8d0;
--rpg-highlight: #d4af37;
--rpg-border: #8b7355;
}
.rpg-dice-popup[data-theme="cyberpunk"] .rpg-dice-popup-content {
--rpg-bg: #0d0221;
--rpg-accent: #1a0b2e;
--rpg-text: #00ff9f;
--rpg-highlight: #ff00ff;
--rpg-border: #ff00ff;
}
/* Desktop Enhancement (1001px+) */
@media (min-width: 1001px) {
.rpg-dice-popup {
--modal-padding: 1.5rem;
--modal-gap: 1rem;
--modal-font-base: 1rem;
--modal-font-small: 0.875rem;
--modal-font-large: 3rem;
--modal-font-huge: 3.75rem;
--modal-max-width: 500px;
}
/* Side-by-side inputs on desktop */
.rpg-dice-selector {
grid-template-columns: 1fr 1fr;
}
/* Hover effects on desktop */
#rpg-dice-roll-btn:hover,
.rpg-dice-save-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px currentColor;
}
.rpg-dice-save-btn {
max-width: 200px;
margin-left: auto;
margin-right: auto;
}
}
+739
View File
@@ -0,0 +1,739 @@
/* ============================================
HTML PROMPT TOGGLE
============================================ */
.rpg-toggle-container {
padding: 0.5em;
background: rgba(0, 0, 0, 0.2);
border-radius: 0.312em;
margin: 0;
}
.rpg-toggle-label {
display: flex;
align-items: center;
cursor: pointer;
user-select: none;
}
.rpg-toggle-label input[type="checkbox"] {
margin: 0 0.5em 0 0;
cursor: pointer;
}
.rpg-toggle-label i {
margin-right: 0.375em;
}
/* ============================================
MANUAL UPDATE BUTTON
============================================ */
.rpg-manual-update-btn {
width: 100%;
height: 2.5rem;
margin: 0 !important;
margin-top: 0 !important;
margin-bottom: 0 !important;
padding: 0;
background: linear-gradient(135deg, var(--rpg-highlight), var(--rpg-accent));
border: 2px solid var(--rpg-highlight);
border-radius: 0.5em;
color: var(--rpg-text);
font-size: clamp(0.875rem, 1.1vw, 1.1rem);
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 0.375em;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
flex-shrink: 0;
box-sizing: border-box;
}
.rpg-manual-update-btn:hover {
transform: translateY(-0.125rem);
box-shadow: 0 6px 20px var(--rpg-highlight);
background: var(--rpg-highlight);
}
.rpg-manual-update-btn:active {
transform: translateY(0);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
}
/* ============================================
SETTINGS BUTTON
============================================ */
.rpg-btn-settings {
width: 100%;
height: 2.5rem;
margin: 0;
padding: 0;
background: var(--rpg-accent);
border: 2px solid var(--rpg-border);
border-radius: 0.5em;
color: var(--rpg-text);
font-size: clamp(0.875rem, 1.1vw, 1.1rem);
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 0.375em;
flex-shrink: 0;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
box-sizing: border-box;
}
.rpg-btn-settings:hover {
background: var(--rpg-highlight);
border-color: var(--rpg-highlight);
transform: translateY(-0.125rem);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}
.rpg-btn-settings:active {
transform: translateY(0);
}
/* Settings buttons row (Edit Trackers + Settings side by side) */
.rpg-settings-buttons-row {
display: flex;
gap: 0.5em;
width: 100%;
}
.rpg-btn-half {
flex: 1;
min-width: 0;
}
/* ============================================
TRACKER EDITOR MODAL
============================================ */
/* Preset Management Section */
.rpg-preset-management {
padding: 0.75em 1em;
background: var(--rpg-accent);
border-bottom: 2px solid var(--rpg-border);
margin-bottom: 0;
}
.rpg-preset-row {
display: flex;
align-items: center;
gap: 0.5em;
flex-wrap: nowrap;
}
.rpg-preset-row label {
font-weight: 600;
font-size: 0.9em;
white-space: nowrap;
}
.rpg-preset-row .rpg-select {
flex: 1;
min-width: 120px;
max-width: 250px;
height: 2.2em;
padding: 0 0.6em;
font-size: 0.9em;
}
.rpg-preset-row .rpg-btn-icon {
height: 2.2em;
width: 2.2em;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.9em;
flex-shrink: 0;
}
.rpg-preset-row .rpg-btn-icon.rpg-btn-active {
background: var(--rpg-highlight);
border-color: var(--rpg-highlight);
color: white;
}
.rpg-preset-association-row {
margin-top: 0.5em;
padding-top: 0.5em;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.rpg-preset-association-row .checkbox_label {
margin: 0;
font-size: 0.85em;
opacity: 0.9;
}
.rpg-preset-association-row .checkbox_label strong {
color: var(--rpg-highlight);
font-weight: 600;
}
/* Editor tabs */
.rpg-editor-tabs {
display: flex;
gap: 0;
border-bottom: 2px solid var(--rpg-border);
}
.rpg-editor-tab {
flex: 1;
padding: 0.75em 1em;
background: var(--rpg-accent);
border: none;
border-bottom: 3px solid transparent;
color: var(--rpg-text);
font-size: 0.9em;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5em;
}
.rpg-editor-tab:hover {
background: var(--rpg-bg);
}
.rpg-editor-tab.active {
background: var(--rpg-bg);
border-bottom-color: var(--rpg-highlight);
}
.rpg-editor-tab-content {
max-height: none;
overflow-y: visible;
}
.rpg-editor-section {
padding: 1em 0;
}
.rpg-editor-section h4 {
color: var(--rpg-highlight);
margin: 1em 0 0.5em 0;
display: flex;
align-items: center;
gap: 0.5em;
}
.rpg-editor-section h4:first-child {
margin-top: 0;
}
.rpg-editor-hint {
font-size: 0.9em;
color: var(--rpg-text);
opacity: 0.7;
margin-bottom: 1em;
}
/* Stats list */
.rpg-editor-stats-list {
display: flex;
flex-direction: column;
gap: 0.5em;
margin-bottom: 1em;
}
.rpg-editor-stat-item {
display: flex;
align-items: center;
gap: 0.5em;
padding: 0.5em;
background: var(--rpg-accent);
border: 1px solid var(--rpg-border);
border-radius: 0.375em;
}
.rpg-stat-toggle {
flex-shrink: 0;
}
.rpg-stat-name {
flex: 1;
padding: 0.375em 0.5em;
background: var(--rpg-bg);
border: 1px solid var(--rpg-border);
border-radius: 0.25em;
color: var(--rpg-text);
font-size: 0.95em;
}
.rpg-stat-max {
width: 5em;
padding: 0.375em 0.5em;
background: var(--rpg-bg);
border: 1px solid var(--rpg-border);
border-radius: 0.25em;
color: var(--rpg-text);
font-size: 0.95em;
text-align: center;
}
.rpg-hidden {
display: none !important;
}
.rpg-radio-group {
display: flex;
gap: 1em;
align-items: center;
}
.rpg-radio-group label {
display: flex;
align-items: center;
gap: 0.375em;
cursor: pointer;
}
.rpg-stat-remove,
.rpg-attr-remove,
.rpg-remove-relationship {
flex-shrink: 0;
padding: 0.375em 0.625em;
background: var(--rpg-highlight);
border: none;
border-radius: 0.25em;
color: white;
cursor: pointer;
transition: opacity 0.2s;
}
.rpg-stat-remove:hover,
.rpg-attr-remove:hover,
.rpg-remove-relationship:hover {
opacity: 0.8;
}
/* RPG Attributes editor styles (same as custom stats) */
.rpg-attr-toggle {
flex-shrink: 0;
}
.rpg-attr-name {
flex: 1;
padding: 0.375em 0.5em;
background: var(--rpg-bg);
border: 1px solid var(--rpg-border);
border-radius: 0.25em;
color: var(--rpg-text);
font-size: 0.95em;
}
.rpg-attr-remove {
flex-shrink: 0;
padding: 0.375em 0.625em;
background: var(--rpg-highlight);
border: none;
border-radius: 0.25em;
color: white;
cursor: pointer;
transition: opacity 0.2s;
}
.rpg-attr-remove:hover {
opacity: 0.8;
}
/* Toggle rows */
.rpg-editor-toggle-row {
display: flex;
align-items: center;
gap: 0.5em;
margin: 0.75em 0;
}
.rpg-editor-toggle-row input[type="checkbox"] {
flex-shrink: 0;
}
.rpg-editor-toggle-row label {
flex: 1;
color: var(--rpg-text);
}
/* Text inputs */
.rpg-text-input {
width: 100%;
padding: 0.5em;
background: var(--rpg-bg);
border: 1px solid var(--rpg-border);
border-radius: 0.375em;
color: var(--rpg-text);
font-size: 0.95em;
margin-top: 0.25em;
}
/* Widget rows */
.rpg-editor-widget-row {
display: flex;
align-items: center;
gap: 0.75em;
padding: 0.625em;
background: var(--rpg-accent);
border: 1px solid var(--rpg-border);
border-radius: 0.375em;
margin-bottom: 0.5em;
}
.rpg-editor-widget-row input[type="checkbox"] {
flex-shrink: 0;
}
.rpg-editor-widget-row label {
flex: 1;
color: var(--rpg-text);
}
.rpg-select-mini {
padding: 0.375em 0.5em;
background: var(--rpg-bg);
border: 1px solid var(--rpg-border);
border-radius: 0.25em;
color: var(--rpg-text);
font-size: 0.9em;
}
.rpg-radio-group {
display: flex;
gap: 1em;
}
.rpg-radio-group label {
display: flex;
align-items: center;
gap: 0.375em;
color: var(--rpg-text);
}
/* Character fields list */
.rpg-editor-fields-list {
display: flex;
flex-direction: column;
gap: 0.5em;
margin-bottom: 1em;
}
/* Relationship Mapping Styles */
.rpg-relationship-mapping-list {
display: flex;
flex-direction: column;
gap: 0.5em;
margin-bottom: 1em;
}
.rpg-relationship-item {
display: grid;
grid-template-columns: 1fr auto 80px auto;
align-items: center;
gap: 0.75em;
padding: 0.5em;
background: var(--rpg-accent);
border: 1px solid var(--rpg-border);
border-radius: 0.375em;
}
.rpg-relationship-name,
.rpg-relationship-emoji {
padding: 0.375em 0.5em;
background: var(--rpg-bg);
border: 1px solid var(--rpg-border);
border-radius: 0.25em;
color: var(--rpg-text);
font-size: 0.95em;
}
.rpg-relationship-emoji {
text-align: center;
font-size: 1.2em;
}
.rpg-arrow {
color: var(--rpg-highlight);
font-weight: bold;
font-size: 1.2em;
}
/* Thoughts Configuration Input Groups */
.rpg-thoughts-config {
display: flex;
flex-direction: column;
gap: 1em;
margin-top: 0.75em;
}
.rpg-editor-input-group {
display: flex;
flex-direction: column;
gap: 0.375em;
}
.rpg-editor-input-group label {
font-size: 0.9em;
color: var(--rpg-text);
opacity: 0.9;
}
.rpg-editor-input-group input[type="text"] {
width: 100%;
padding: 0.5em;
background: var(--rpg-accent);
border: 1px solid var(--rpg-border);
border-radius: 0.25em;
color: var(--rpg-text);
font-size: 0.95em;
}
/* History Persistence inputs/selects - match tracker editor styling */
#rpg-tracker-editor-popup .rpg-input,
#rpg-tracker-editor-popup .rpg-select {
padding: 0.5em;
background: var(--rpg-accent);
border: 1px solid var(--rpg-border);
border-radius: 0.25em;
color: var(--rpg-text);
font-size: 0.95em;
text-align: left;
margin-left: 0 !important;
display: block;
}
#rpg-tracker-editor-popup .rpg-input:focus,
#rpg-tracker-editor-popup .rpg-select:focus {
outline: none;
border-color: var(--rpg-highlight);
box-shadow: 0 0 5px rgba(var(--rpg-highlight-rgb), 0.3);
}
.rpg-editor-field-item {
display: grid;
grid-template-columns: auto auto 1fr 2fr auto;
align-items: center;
gap: 0.5em;
padding: 0.5em;
background: var(--rpg-accent);
border: 1px solid var(--rpg-border);
border-radius: 0.375em;
min-width: 0; /* Allow grid items to shrink below content size */
}
.rpg-field-controls {
display: flex;
flex-direction: column;
gap: 0.25em;
}
.rpg-field-move-up,
.rpg-field-move-down {
padding: 0.125em 0.375em;
background: var(--rpg-bg);
border: 1px solid var(--rpg-border);
border-radius: 0.25em;
color: var(--rpg-text);
cursor: pointer;
font-size: 0.75em;
}
.rpg-field-move-up:disabled,
.rpg-field-move-down:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.rpg-field-toggle {
flex-shrink: 0;
}
.rpg-field-label,
.rpg-char-stat-label,
.rpg-field-placeholder {
padding: 0.375em 0.5em;
background: var(--rpg-bg);
border: 1px solid var(--rpg-border);
border-radius: 0.25em;
color: var(--rpg-text);
font-size: 0.95em;
word-wrap: break-word;
overflow-wrap: break-word;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0; /* Allow input to shrink */
width: 100%; /* Fill available grid space */
box-sizing: border-box;
}
.rpg-field-remove,
.rpg-remove-relationship {
flex-shrink: 0;
padding: 0.375em 0.625em;
background: var(--rpg-highlight);
border: none;
border-radius: 0.25em;
color: white;
cursor: pointer;
transition: opacity 0.2s;
}
.rpg-field-remove:hover,
.rpg-remove-relationship:hover {
opacity: 0.8;
}
/* Character stats checkboxes */
.rpg-char-stats-checkboxes {
display: flex;
flex-wrap: wrap;
gap: 1em;
margin-top: 0.5em;
}
.rpg-char-stats-checkboxes label {
display: flex;
align-items: center;
gap: 0.375em;
color: var(--rpg-text);
}
/* Footer buttons */
.rpg-settings-popup-footer {
padding: 1em;
border-top: 2px solid var(--rpg-border);
background: var(--rpg-accent);
}
.rpg-editor-footer-buttons {
display: flex;
flex-direction: column;
gap: 0.75em;
}
.rpg-editor-footer-row {
display: flex;
gap: 0.5em;
justify-content: center;
}
.rpg-editor-footer-row:last-child button {
flex: 1;
max-width: 400px;
}
.rpg-footer-left {
display: flex;
gap: 0.5em;
}
.rpg-footer-right {
display: flex;
gap: 0.5em;
}
/* Prompts Editor Styles */
.rpg-prompt-editor-section {
margin-bottom: 24px;
padding-bottom: 24px;
border-bottom: 1px solid var(--rpg-border);
}
.rpg-prompt-editor-section:last-child {
border-bottom: none;
}
.rpg-prompt-textarea {
width: 100%;
padding: 10px;
border-radius: 4px;
border: 1px solid var(--SmartThemeBorderColor);
background: var(--SmartThemeBlurTintColor);
color: var(--SmartThemeBodyColor);
font-family: 'Courier New', monospace;
font-size: 12px;
resize: vertical;
line-height: 1.5;
box-sizing: border-box;
}
.rpg-prompt-textarea:focus {
outline: none;
border-color: var(--rpg-highlight);
box-shadow: 0 0 0 2px rgba(233, 69, 96, 0.2);
}
.rpg-restore-prompt-btn {
font-size: 0.9em;
white-space: nowrap;
min-width: 120px;
padding: 0.5em 1em;
}
/* Editor buttons */
.rpg-btn-primary,
.rpg-btn-secondary,
.rpg-btn-cancel,
.rpg-btn-reset {
padding: 0.625em 1.25em;
border: 1px solid var(--rpg-border);
border-radius: 0.375em;
cursor: pointer;
font-size: 0.95em;
font-weight: 600;
transition: all 0.2s;
}
.rpg-btn-primary {
background: var(--rpg-highlight);
color: white;
border-color: var(--rpg-highlight);
}
.rpg-btn-primary:hover {
opacity: 0.9;
transform: translateY(-1px);
}
.rpg-btn-secondary {
background: var(--rpg-bg);
color: var(--rpg-text);
border-color: var(--rpg-border);
}
.rpg-btn-secondary:hover {
background: var(--rpg-accent);
transform: translateY(-1px);
}
.rpg-btn-cancel {
background: var(--rpg-accent);
color: var(--rpg-text);
border-color: var(--rpg-border);
}
.rpg-btn-cancel:hover {
background: var(--rpg-bg);
}
.rpg-btn-reset {
background: transparent;
color: var(--rpg-text);
border-color: var(--rpg-border);
opacity: 0.8;
}
.rpg-btn-reset:hover {
opacity: 1;
background: var(--rpg-accent);
}
+246
View File
@@ -0,0 +1,246 @@
/* ============================================
SETTINGS MODAL - MOBILE FIRST
============================================ */
/* CSS Custom Properties for Responsive Scaling */
.rpg-settings-popup {
/* Fluid spacing */
--modal-padding: clamp(0.5rem, 2vw, 0.75rem);
--modal-gap: clamp(0.375rem, 1.5vw, 0.5rem);
--modal-border-width: 2px;
/* Fluid typography */
--modal-font-base: clamp(0.8rem, 3vw, 0.9rem);
--modal-font-small: clamp(0.7rem, 2.5vw, 0.8rem);
--modal-font-heading: clamp(0.9rem, 3.5vw, 1rem);
/* Content constraints - more height than dice roller */
--modal-max-width: min(90vw, 500px);
--modal-max-height: 75vh;
}
/* Modal Container - Hidden by default */
.rpg-settings-popup {
position: fixed;
inset: 0;
z-index: 10000;
display: none;
align-items: center;
justify-content: center;
padding: 0.5rem;
}
/* Open state */
.rpg-settings-popup.is-open {
display: flex;
animation: fadeIn 0.2s ease-out;
}
/* Closing state */
.rpg-settings-popup.is-closing {
display: flex;
animation: fadeOut 0.2s ease-in;
}
/* Backdrop overlay - using ::before pseudo-element */
.rpg-settings-popup::before {
content: '';
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
}
/* Modal Content Box */
.rpg-settings-popup-content {
position: relative;
width: 100%;
max-width: var(--modal-max-width);
height: auto;
max-height: var(--modal-max-height);
min-height: 0;
background: rgba(30, 30, 30, 0.8);
border: var(--modal-border-width) solid var(--rpg-border);
border-radius: 0.5rem;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.9);
color: var(--rpg-text);
display: flex;
flex-direction: column;
overflow: hidden;
animation: slideInUp 0.3s cubic-bezier(0.16, 1, 0.3, 1);
margin: auto 0;
}
/* Header */
.rpg-settings-popup-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--modal-padding);
background: var(--rpg-accent);
border-bottom: var(--modal-border-width) solid var(--rpg-border);
flex-shrink: 0;
}
.rpg-settings-popup-header h3 {
margin: 0;
font-size: var(--modal-font-heading);
color: var(--rpg-highlight);
display: flex;
align-items: center;
gap: var(--modal-gap);
border-left: none !important;
padding-left: 0 !important;
}
.rpg-settings-popup-header h3 i {
border-left: none !important;
padding-left: 0 !important;
color: var(--rpg-highlight);
}
/* Close button - touch-friendly */
.rpg-popup-close {
background: transparent;
border: none;
color: var(--rpg-text);
font-size: clamp(1.5rem, 2.2vw, 2.2rem);
cursor: pointer;
padding: 0.5rem;
min-width: 44px;
min-height: 44px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0.25rem;
transition: all 0.2s ease;
}
.rpg-popup-close:active {
background: rgba(255, 255, 255, 0.1);
color: var(--rpg-highlight);
}
/* Scrollable Body */
.rpg-settings-popup-body {
padding: var(--modal-padding);
overflow-y: auto;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
flex: 1 1 auto;
min-height: 0;
background: transparent;
color: inherit;
}
/* Settings Groups */
.rpg-settings-group {
margin-bottom: var(--modal-padding);
padding-bottom: var(--modal-padding);
border-bottom: 1px solid var(--rpg-border);
}
.rpg-settings-group:last-child {
border-bottom: none;
}
.rpg-settings-group h4 {
margin: 0 0 var(--modal-gap) 0;
font-size: var(--modal-font-base);
color: var(--rpg-highlight);
display: flex;
align-items: center;
gap: var(--modal-gap);
font-weight: 600;
}
/* Theme Support - Default Theme (no data-theme attribute) */
.rpg-settings-popup-content {
--rpg-bg: var(--SmartThemeBlurTintColor, rgba(26, 26, 46, 0.9));
--rpg-accent: var(--black30a, rgba(22, 33, 62, 0.9));
--rpg-text: var(--SmartThemeBodyColor, #eaeaea);
--rpg-highlight: var(--SmartThemeQuoteColor, #e94560);
--rpg-border: #6b9fd4;
}
/* Theme Support - Settings Modal */
#rpg-settings-popup[data-theme="sci-fi"] .rpg-settings-popup-content {
--rpg-bg: #0a0e27;
--rpg-accent: #1a1f3a;
--rpg-text: #00ffff;
--rpg-highlight: #ff00ff;
--rpg-border: #00ffff;
background: rgba(10, 14, 39, 0.95);
color: #00ffff;
}
#rpg-settings-popup[data-theme="fantasy"] .rpg-settings-popup-content {
--rpg-bg: #2b1810;
--rpg-accent: #3d2516;
--rpg-text: #f4e4c1;
--rpg-highlight: #d4af37;
--rpg-border: #8b6914;
background: rgba(43, 24, 16, 0.95);
color: #f4e4c1;
}
#rpg-settings-popup[data-theme="cyberpunk"] .rpg-settings-popup-content {
--rpg-bg: #0d0221;
--rpg-accent: #1a0b2e;
--rpg-text: #00ff9f;
--rpg-highlight: #ff00ff;
--rpg-border: #ff00ff;
background: rgba(13, 2, 33, 0.95);
color: #00ff9f;
}
/* Theme Support - Tracker Editor Modal */
#rpg-tracker-editor-popup[data-theme="sci-fi"] .rpg-settings-popup-content {
--rpg-bg: #0a0e27;
--rpg-accent: #1a1f3a;
--rpg-text: #00ffff;
--rpg-highlight: #ff00ff;
--rpg-border: #00ffff;
background: rgba(10, 14, 39, 0.95);
color: #00ffff;
}
#rpg-tracker-editor-popup[data-theme="fantasy"] .rpg-settings-popup-content {
--rpg-bg: #2b1810;
--rpg-accent: #3d2516;
--rpg-text: #f4e4c1;
--rpg-highlight: #d4af37;
--rpg-border: #8b6914;
background: rgba(43, 24, 16, 0.95);
color: #f4e4c1;
}
#rpg-tracker-editor-popup[data-theme="cyberpunk"] .rpg-settings-popup-content {
--rpg-bg: #0d0221;
--rpg-accent: #1a0b2e;
--rpg-text: #00ff9f;
--rpg-highlight: #ff00ff;
--rpg-border: #ff00ff;
background: rgba(13, 2, 33, 0.95);
color: #00ff9f;
}
/* Desktop Enhancement (1001px+) */
@media (min-width: 1001px) {
.rpg-settings-popup {
--modal-padding: 1rem;
--modal-gap: 0.75rem;
--modal-font-base: 0.9rem;
--modal-font-small: 0.8rem;
--modal-font-heading: 1.125rem;
--modal-max-width: 600px;
}
/* Hover effects on desktop */
.rpg-popup-close:hover {
background: rgba(255, 255, 255, 0.1);
color: var(--rpg-highlight);
}
}
+498
View File
@@ -0,0 +1,498 @@
/* ============================================
BELOW-CHAT PRESENT CHARACTERS
============================================ */
#rpg-alt-present-characters {
margin: 0 0 10px;
padding: 8px 10px 8px;
border-radius: 14px;
border: 1px solid var(--rpg-border, rgba(255, 255, 255, 0.14));
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0) 100%),
linear-gradient(135deg, var(--rpg-accent, rgba(34, 40, 60, 0.94)) 0%, var(--rpg-bg, rgba(18, 21, 34, 0.96)) 100%);
box-shadow: 0 12px 28px var(--rpg-shadow, rgba(0, 0, 0, 0.24));
color: var(--rpg-text, var(--SmartThemeBodyColor, #ecf0f1));
backdrop-filter: blur(12px);
}
.rpg-alt-present-characters__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 6px;
}
.rpg-alt-present-characters__title {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 0.84rem;
font-weight: 700;
letter-spacing: 0.02em;
}
.rpg-alt-present-characters__title i {
color: var(--rpg-highlight, #e94560);
}
.rpg-alt-present-characters__count {
min-width: 24px;
height: 24px;
padding: 0 7px;
border-radius: 999px;
display: inline-flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.08);
border: 1px solid var(--rpg-border, rgba(255, 255, 255, 0.14));
color: var(--rpg-text, var(--SmartThemeBodyColor, #ecf0f1));
font-size: 0.74rem;
font-weight: 700;
}
.rpg-alt-present-characters__scroll {
overflow-x: auto;
overflow-y: hidden;
padding-bottom: 2px;
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.24) transparent;
transform: scaleY(-1);
}
.rpg-alt-present-characters__scroll::-webkit-scrollbar {
height: 6px;
}
.rpg-alt-present-characters__scroll::-webkit-scrollbar-track {
background: transparent;
}
.rpg-alt-present-characters__scroll::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.24);
border-radius: 999px;
}
.rpg-alt-present-characters__scroll::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.34);
}
.rpg-alt-present-characters__track {
display: flex;
gap: 10px;
width: max-content;
min-width: 100%;
transform: scaleY(-1);
padding-top: 8px;
padding-bottom: 2px;
}
.rpg-alt-present-character {
flex: 0 0 98px;
display: flex;
flex-direction: column;
gap: 4px;
min-width: 98px;
}
.rpg-alt-present-character__portrait {
position: relative;
aspect-ratio: 11 / 15;
border-radius: 12px;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.14);
background: var(--rpg-bg, rgba(18, 21, 34, 0.96));
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.18);
transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
}
.rpg-alt-present-character__portrait:hover {
transform: translateY(-2px);
border-color: var(--rpg-highlight, #e94560);
box-shadow: 0 12px 20px rgba(0, 0, 0, 0.22);
}
.rpg-alt-present-character__portrait img {
width: 100%;
height: 100%;
display: block;
object-fit: cover;
}
.rpg-alt-present-character__meta {
display: flex;
flex-direction: column;
min-width: 0;
}
.rpg-alt-present-character__name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.rpg-alt-present-character__name {
font-size: 0.78rem;
font-weight: 600;
line-height: 1.1;
text-align: center;
}
@media (max-width: 768px) {
#rpg-alt-present-characters {
margin-bottom: 8px;
padding: 7px 9px 7px;
border-radius: 12px;
}
.rpg-alt-present-character {
flex-basis: 84px;
min-width: 84px;
}
}
/* ============================================
CHAT THOUGHT OVERLAYS
============================================ */
/* Container for thought overlay on chat messages */
/* Floating thought panel - positioned next to character avatar */
#rpg-thought-panel {
position: fixed;
z-index: 1000; /* Lower z-index to stay below dropdown menus */
pointer-events: auto;
max-width: 350px; /* Fixed max like panels, will be constrained by viewport */
width: auto;
transform: translateY(0); /* No vertical centering - align with avatar top */
animation: thoughtPanelFadeIn 0.4s ease-out;
}
/* Collapsed thought icon */
#rpg-thought-icon {
position: fixed;
z-index: 1000; /* Lower z-index to stay below dropdown menus */
display: none; /* Hidden by default in desktop */
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
border-radius: 50%;
background: var(--rpg-bg, rgba(30, 30, 50, 0.95));
border: 2px solid var(--rpg-highlight, #e94560);
color: var(--rpg-text, #ecf0f1);
font-size: clamp(1.25rem, 1.85vw, 1.875rem);
cursor: move;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
backdrop-filter: blur(10px);
transition: transform 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease;
user-select: none;
-webkit-user-select: none;
}
/* Show icon only in mobile view */
@media (max-width: 1000px) {
#rpg-thought-icon {
display: flex;
}
}
/* Force hide class for desktop mode - overrides media query (unless collapsed) */
#rpg-thought-icon.rpg-force-hide:not(.rpg-collapsed-desktop) {
display: none !important;
}
/* When collapsed in desktop, show icon */
#rpg-thought-icon.rpg-collapsed-desktop {
display: flex !important;
}
/* Hidden state that allows transitions */
#rpg-thought-icon.rpg-hidden {
opacity: 0;
pointer-events: none;
}
#rpg-thought-icon.dragging {
transition: none !important;
cursor: grabbing;
}
#rpg-thought-icon:not(.dragging):hover {
transform: scale(1.1);
box-shadow: 0 6px 20px rgba(233, 69, 96, 0.4);
}
/* Close button */
.rpg-thought-close {
position: absolute;
top: -0.5rem;
right: -0.5rem;
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
background: var(--rpg-highlight, #e94560);
color: white;
border: 2px solid var(--rpg-bg, rgba(30, 30, 50, 0.95));
font-size: clamp(0.9rem, 1rem, 1.1rem);
line-height: 1;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
z-index: 10001;
transition: all 0.2s ease;
}
.rpg-thought-close:hover {
transform: scale(1.1);
background: var(--rpg-text, #eaeaea);
color: var(--rpg-highlight, #e94560);
}
/* Show close button in desktop view for collapsing */
@media (min-width: 1001px) {
.rpg-thought-close {
display: flex !important;
}
}
@keyframes thoughtIconPulse {
0%, 100% {
transform: scale(1);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
}
50% {
transform: scale(1.05);
box-shadow: 0 6px 20px rgba(233, 69, 96, 0.3);
}
}
/* Thought circles floating from avatar */
.rpg-thought-circles {
position: absolute;
display: flex;
flex-direction: row-reverse; /* Horizontal layout, flipped */
align-items: center; /* Center vertically */
gap: 0.5em;
inset: 0px -50px auto;
z-index: 1;
}
/* Flip thought circles to left side when panel is on left */
.rpg-thought-panel-right .rpg-thought-circles {
inset: 0px auto auto -50px;
flex-direction: row; /* Normal order for left side */
}
.rpg-thought-circle {
background: var(--rpg-bg, rgba(30, 30, 50, 0.8));
border: 2px solid var(--rpg-highlight, #e94560);
border-radius: 50%;
animation: thoughtCirclePulse 1.5s ease-in-out infinite;
}
.rpg-circle-1 {
width: 0.5rem;
height: 0.5rem;
animation-delay: 0s;
}
.rpg-circle-2 {
width: 0.75rem;
height: 0.75rem;
animation-delay: 0.2s;
}
.rpg-circle-3 {
width: 1rem;
height: 1rem;
animation-delay: 0.4s;
}
/* Thought bubble main container */
.rpg-thought-bubble {
background: var(--rpg-bg, rgba(30, 30, 50, 0.95));
border: 2px solid var(--rpg-highlight, #e94560);
border-radius: clamp(10px, 1.5vh, 14px);
padding: clamp(10px, 1.5vh, 14px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
position: relative;
backdrop-filter: blur(15px);
max-height: calc(100vh - 120px); /* Scale with viewport height */
overflow-y: auto;
overflow-x: hidden;
}
/* Custom scrollbar for thought bubble */
.rpg-thought-bubble::-webkit-scrollbar {
width: 0.375rem;
}
.rpg-thought-bubble::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.2);
border-radius: 3px;
}
.rpg-thought-bubble::-webkit-scrollbar-thumb {
background: var(--rpg-highlight, #e94560);
border-radius: 3px;
opacity: 0.5;
}
.rpg-thought-bubble::-webkit-scrollbar-thumb:hover {
background: var(--rpg-highlight, #e94560);
opacity: 0.8;
}
/* Individual thought item (for multiple characters) */
.rpg-thought-item {
display: flex;
gap: clamp(10px, 1.5vh, 12px);
align-items: flex-start;
}
/* Emoji box on the left */
.rpg-thought-emoji-box {
flex-shrink: 0;
width: clamp(32px, 4vh, 40px);
height: clamp(32px, 4vh, 40px);
display: flex;
align-items: center;
justify-content: center;
background: var(--rpg-accent, rgba(50, 50, 70, 0.8));
border: 1px solid var(--rpg-highlight, #e94560);
border-radius: clamp(6px, 1vh, 8px);
font-size: clamp(16px, 2vw, 20px);
}
/* Thought content on the right */
.rpg-thought-content {
flex: 1;
font-size: clamp(0.85rem, 0.9rem, 0.95rem);
line-height: 1.5;
color: var(--rpg-text, #eaeaea);
font-style: italic;
opacity: 0.95;
word-wrap: break-word;
overflow-wrap: break-word;
hyphens: auto;
}
/* Divider between multiple thoughts */
.rpg-thought-divider {
height: 0.062rem;
background: linear-gradient(to right, transparent, var(--rpg-highlight, #e94560), transparent);
margin: clamp(8px, 1vh, 10px) 0;
opacity: 0.5;
}
/* Arrow pointing right (when panel is on left) */
#rpg-thought-panel.rpg-thought-panel-left .rpg-thought-bubble::after {
content: '';
position: absolute;
top: 50%;
right: -0.75rem;
transform: translateY(-50%);
width: 0;
height: 0;
border-style: solid;
border-width: 10px 0 10px 12px;
border-color: transparent transparent transparent var(--rpg-highlight, #e94560);
}
#rpg-thought-panel.rpg-thought-panel-left .rpg-thought-bubble::before {
content: '';
position: absolute;
top: 50%;
right: -0.562rem;
transform: translateY(-50%);
width: 0;
height: 0;
border-style: solid;
border-width: 0.5rem 0 0.5rem 0.625rem;
border-color: transparent transparent transparent var(--rpg-bg, rgba(30, 30, 50, 0.95));
z-index: 1;
}
/* Arrow pointing left (when panel is on right) */
#rpg-thought-panel.rpg-thought-panel-right .rpg-thought-bubble::after {
content: '';
position: absolute;
top: 50%;
left: -0.75rem;
transform: translateY(-50%);
width: 0;
height: 0;
border-style: solid;
border-width: 10px 12px 10px 0;
border-color: transparent var(--rpg-highlight, #e94560) transparent transparent;
}
#rpg-thought-panel.rpg-thought-panel-right .rpg-thought-bubble::before {
content: '';
position: absolute;
top: 50%;
left: -0.562rem;
transform: translateY(-50%);
width: 0;
height: 0;
border-style: solid;
border-width: 0.5rem 0.625rem 0.5rem 0;
border-color: transparent var(--rpg-bg, rgba(30, 30, 50, 0.95)) transparent transparent;
z-index: 1;
}
/* Animations */
@keyframes thoughtPanelFadeIn {
from {
opacity: 0;
transform: translateY(-50%) scale(0.9);
}
to {
opacity: 1;
transform: translateY(-50%) scale(1);
}
}
@keyframes thoughtCirclePulse {
0%, 100% {
opacity: 0.3;
transform: scale(0.8);
}
50% {
opacity: 0.8;
transform: scale(1.1);
}
}
/* Responsive positioning for mobile/narrow screens */
@media (max-width: 1000px) {
#rpg-thought-panel {
position: fixed !important;
max-width: 90vw !important;
width: 90vw !important;
left: 50% !important;
transform: translateX(-50%) !important;
max-height: calc(100dvh - 80px) !important;
}
#rpg-thought-panel .rpg-thought-bubble {
max-height: calc(100dvh - 100px) !important;
overflow-y: auto !important;
}
.rpg-thought-circles {
display: none !important;
}
#rpg-thought-panel .rpg-thought-bubble::after,
#rpg-thought-panel .rpg-thought-bubble::before {
display: none !important;
}
.rpg-thought-content {
font-size: clamp(12px, 3.5vw, 16px);
}
}
File diff suppressed because it is too large Load Diff
+389
View File
@@ -0,0 +1,389 @@
/* ========================================
Inventory System v2 Styles
======================================== */
/* Inventory Container */
#rpg-inventory {
display: flex;
flex-direction: column;
gap: 1rem;
padding: 0.5rem;
font-size: 0.9rem;
}
.rpg-inventory-container {
display: flex;
flex-direction: column;
gap: 1rem;
padding: 0.5rem;
font-size: 0.9rem;
}
/* Sub-tabs Navigation */
.rpg-inventory-subtabs {
display: flex;
gap: 0.5rem;
border-bottom: 2px solid var(--SmartThemeBorderColor);
padding-bottom: 0.5rem;
overflow-x: auto;
overflow-y: hidden;
flex-wrap: nowrap;
scrollbar-width: thin;
scrollbar-color: var(--SmartThemeBorderColor) transparent;
}
.rpg-inventory-subtabs::-webkit-scrollbar {
height: 6px;
}
.rpg-inventory-subtabs::-webkit-scrollbar-track {
background: transparent;
}
.rpg-inventory-subtabs::-webkit-scrollbar-thumb {
background: var(--SmartThemeBorderColor);
border-radius: 3px;
}
.rpg-inventory-subtabs::-webkit-scrollbar-thumb:hover {
background: var(--rpg-accent);
}
.rpg-inventory-subtab {
flex: 1;
min-width: fit-content;
white-space: nowrap;
padding: 0.5rem 1rem;
background: transparent;
border: 2px solid var(--SmartThemeBorderColor);
border-radius: 0.25rem;
color: var(--SmartThemeBodyColor);
cursor: pointer;
transition: all 0.2s ease;
font-weight: 500;
text-align: center;
font-size: clamp(0.75rem, 1vw, 0.9rem);
}
.rpg-inventory-subtab:hover {
background: rgba(var(--rpg-highlight-rgb, 233, 69, 96), 0.1);
border-color: var(--rpg-highlight);
color: var(--rpg-highlight);
}
.rpg-inventory-subtab.active {
background: transparent;
border-color: var(--rpg-highlight);
color: var(--rpg-highlight);
font-weight: 600;
}
/* Inventory Sections */
.rpg-inventory-section {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.rpg-inventory-header {
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--SmartThemeBorderColor);
gap: 0.5rem;
overflow-x: auto;
overflow-y: hidden;
flex-wrap: nowrap;
scrollbar-width: thin;
scrollbar-color: var(--SmartThemeBorderColor) transparent;
}
.rpg-inventory-header::-webkit-scrollbar {
height: 6px;
}
.rpg-inventory-header::-webkit-scrollbar-track {
background: transparent;
}
.rpg-inventory-header::-webkit-scrollbar-thumb {
background: var(--SmartThemeBorderColor);
border-radius: 3px;
}
.rpg-inventory-header::-webkit-scrollbar-thumb:hover {
background: var(--rpg-accent);
}
.rpg-inventory-header h4 {
margin: 0;
font-size: 1.1rem;
color: var(--SmartThemeBodyColor);
white-space: nowrap;
min-width: fit-content;
}
.rpg-inventory-content {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.rpg-inventory-text {
padding: 0.75rem;
background: var(--SmartThemeBlurTintColor);
border: 1px solid var(--SmartThemeBorderColor);
border-radius: 0.25rem;
color: var(--SmartThemeBodyColor);
line-height: 1.6;
white-space: pre-wrap;
word-wrap: break-word;
}
.rpg-inventory-empty {
padding: 2rem;
text-align: center;
color: var(--SmartThemeFastUISliderColColor);
font-style: italic;
}
.rpg-inventory-hint {
padding: 0.5rem;
background: transparent;
border: 2px solid var(--rpg-highlight);
border-radius: 0.25rem;
font-size: 0.85rem;
color: var(--SmartThemeFastUISliderColColor);
display: flex;
gap: 0.5rem;
align-items: flex-start;
}
.rpg-inventory-hint i {
margin-top: 0.1rem;
}
/* Storage Locations */
.rpg-storage-location {
border: 1px solid var(--SmartThemeBorderColor);
border-radius: 0.25rem;
overflow: hidden;
}
.rpg-storage-header {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
background: var(--SmartThemeQuoteColor);
cursor: pointer;
}
.rpg-storage-toggle {
background: none;
border: none;
color: var(--SmartThemeBodyColor);
cursor: pointer;
padding: 0.25rem;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.2s ease;
}
.rpg-storage-toggle:hover {
color: var(--ac-style-color-matchedText);
}
.rpg-storage-name {
flex: 1;
margin: 0;
font-size: 1rem;
font-weight: 600;
color: #000000;
}
.rpg-storage-actions {
display: flex;
gap: 0.5rem;
}
.rpg-storage-content {
padding: 0.5rem;
background: var(--SmartThemeBlurTintColor);
}
.rpg-storage-location.collapsed .rpg-storage-content {
display: none;
}
.rpg-storage-add-item-container {
display: flex;
justify-content: center;
margin-top: 0.75rem;
}
/* Buttons */
.rpg-inventory-edit-btn,
.rpg-inventory-add-btn,
.rpg-inventory-remove-btn {
padding: 0.4rem 0.75rem;
border: 1px solid var(--SmartThemeBorderColor);
border-radius: 0.25rem;
background: var(--SmartThemeBlurTintColor);
color: var(--SmartThemeBodyColor);
cursor: pointer;
transition: all 0.2s ease;
font-size: 0.85rem;
display: flex;
align-items: center;
gap: 0.35rem;
}
.rpg-inventory-edit-btn:hover {
background: var(--ac-style-color-matchedText);
border-color: var(--ac-style-color-matchedText);
color: white;
}
.rpg-inventory-add-btn {
background: transparent;
border-color: var(--rpg-highlight);
color: var(--rpg-highlight);
white-space: nowrap;
min-width: fit-content;
}
.rpg-inventory-add-btn:hover {
background: rgba(var(--rpg-highlight-rgb, 233, 69, 96), 0.1);
border-color: var(--rpg-highlight);
}
.rpg-inventory-remove-btn {
background: transparent;
color: var(--SmartThemeFastUISliderColColor);
}
.rpg-inventory-remove-btn:hover {
background: #dc3545;
border-color: #dc3545;
color: white;
}
/* Inline Editing Styles */
.rpg-inventory-text.rpg-editable {
cursor: text;
transition: all 0.2s ease;
min-height: 2rem;
}
.rpg-inventory-text.rpg-editable:hover {
background: var(--SmartThemeQuoteColor);
border-color: var(--ac-style-color-matchedText);
}
.rpg-inventory-text.rpg-editable:focus {
outline: none;
border-color: var(--ac-style-color-matchedText);
background: var(--SmartThemeEmColor);
box-shadow: 0 0 0 2px rgba(var(--ac-style-color-matchedText-rgb, 66, 135, 245), 0.2);
}
.rpg-inventory-text.rpg-editable:empty::before {
content: 'Click to edit...';
color: var(--SmartThemeFastUISliderColColor);
font-style: italic;
}
/* Inline Forms */
.rpg-inline-form {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 0.75rem;
background: var(--SmartThemeQuoteColor);
border: 1px solid var(--ac-style-color-matchedText);
border-radius: 0.25rem;
margin-bottom: 0.75rem;
}
.rpg-inline-input {
padding: 0.5rem 0.75rem;
background: var(--SmartThemeBlurTintColor);
border: 1px solid var(--SmartThemeBorderColor);
border-radius: 0.25rem;
color: var(--SmartThemeBodyColor);
font-size: 0.9rem;
font-family: inherit;
}
.rpg-inline-input:focus {
outline: none;
border-color: var(--ac-style-color-matchedText);
box-shadow: 0 0 0 2px rgba(var(--ac-style-color-matchedText-rgb, 66, 135, 245), 0.2);
}
.rpg-inline-buttons {
display: flex;
gap: 0.5rem;
justify-content: flex-end;
}
.rpg-inline-btn {
padding: 0.4rem 0.75rem;
border: 1px solid var(--SmartThemeBorderColor);
border-radius: 0.25rem;
background: var(--SmartThemeBlurTintColor);
color: var(--SmartThemeBodyColor);
cursor: pointer;
transition: all 0.2s ease;
font-size: 0.85rem;
display: flex;
align-items: center;
gap: 0.35rem;
}
.rpg-inline-btn:hover {
opacity: 0.85;
}
.rpg-inline-cancel {
background: var(--SmartThemeBlurTintColor);
color: var(--SmartThemeFastUISliderColColor);
}
.rpg-inline-cancel:hover {
background: #6c757d;
border-color: #6c757d;
color: white;
}
.rpg-inline-save,
.rpg-inline-confirm {
background: var(--ac-style-color-matchedText);
border-color: var(--ac-style-color-matchedText);
color: white;
}
.rpg-inline-save:hover,
.rpg-inline-confirm:hover {
opacity: 0.85;
}
/* Inline Confirmation */
.rpg-inline-confirmation {
padding: 0.75rem;
background: var(--SmartThemeQuoteColor);
border: 1px solid #dc3545;
border-radius: 0.25rem;
margin-top: 0.5rem;
}
.rpg-inline-confirmation p {
margin: 0 0 0.75rem 0;
color: var(--SmartThemeBodyColor);
font-size: 0.9rem;
}
+173
View File
@@ -0,0 +1,173 @@
/* ============================================
ITEM LIST AND GRID VIEW STYLES
============================================ */
/* Item list container base styles */
.rpg-item-list {
min-height: 2rem;
padding: 0.5rem;
}
/* LIST VIEW - Full-width rows */
.rpg-item-list-view {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.rpg-item-row {
position: relative;
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
background: transparent;
border: 2px solid var(--rpg-highlight);
border-radius: 0.25rem;
color: var(--SmartThemeBodyColor);
font-size: 0.95rem;
transition: all 0.2s ease;
}
.rpg-item-row:hover {
border-color: var(--rpg-highlight);
background: rgba(var(--rpg-highlight-rgb, 233, 69, 96), 0.1);
}
.rpg-item-row .rpg-item-name {
flex: 1;
line-height: 1.5;
word-wrap: break-word;
overflow-wrap: break-word;
}
.rpg-item-row .rpg-item-remove {
flex-shrink: 0;
padding: 0.3rem 0.6rem;
background: transparent;
border: 1px solid var(--SmartThemeBorderColor);
border-radius: 0.25rem;
color: var(--SmartThemeFastUISliderColColor);
cursor: pointer;
transition: all 0.2s ease;
font-size: 0.85rem;
}
.rpg-item-row .rpg-item-remove:hover {
background: #dc3545;
border-color: #dc3545;
color: white;
}
/* GRID VIEW - Responsive card grid */
.rpg-item-grid-view {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 0.75rem;
}
.rpg-item-card {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 1rem 0.75rem;
background: transparent;
border: 2px solid var(--rpg-highlight);
border-radius: 0.25rem;
color: var(--SmartThemeBodyColor);
font-size: 0.9rem;
transition: all 0.2s ease;
min-height: 80px;
}
.rpg-item-card:hover {
border-color: var(--rpg-highlight);
background: rgba(var(--rpg-highlight-rgb, 233, 69, 96), 0.1);
}
.rpg-item-card .rpg-item-name {
text-align: center;
word-wrap: break-word;
overflow-wrap: break-word;
hyphens: auto;
max-width: 100%;
}
.rpg-item-card .rpg-item-remove {
position: absolute;
top: 0.25rem;
right: 0.25rem;
padding: 0;
width: 1.5rem;
height: 1.5rem;
background: transparent;
border: none;
border-radius: 0.25rem;
color: var(--SmartThemeFastUISliderColColor);
cursor: pointer;
transition: all 0.2s ease;
font-size: 0.85rem;
display: flex;
align-items: center;
justify-content: center;
}
.rpg-item-card .rpg-item-remove:hover {
background: #dc3545;
color: white;
}
/* Empty state message */
.rpg-inventory-empty {
padding: 1rem;
text-align: center;
color: var(--SmartThemeFastUISliderColColor);
font-style: italic;
font-size: 0.9rem;
}
/* View Toggle Buttons */
.rpg-inventory-header-actions {
display: flex;
align-items: center;
gap: 0.75rem;
flex-wrap: nowrap;
min-width: fit-content;
}
.rpg-view-toggle {
display: flex;
gap: 0.25rem;
flex-wrap: nowrap;
}
.rpg-view-btn {
padding: 0.35rem 0.6rem;
background: var(--SmartThemeBlurTintColor);
border: 2px solid var(--SmartThemeBorderColor);
border-radius: 0.25rem;
color: var(--SmartThemeFastUISliderColColor);
cursor: pointer;
white-space: nowrap;
min-width: fit-content;
transition: all 0.2s ease;
font-size: 0.9rem;
display: flex;
align-items: center;
justify-content: center;
}
.rpg-view-btn:hover {
background: rgba(var(--rpg-highlight-rgb, 233, 69, 96), 0.1);
border-color: var(--rpg-highlight);
color: var(--rpg-highlight);
}
.rpg-view-btn.active {
background: var(--SmartThemeBlurTintColor);
border-color: var(--rpg-highlight);
color: var(--rpg-highlight);
}
+138
View File
@@ -0,0 +1,138 @@
/* ============================================
DESKTOP TABS SYSTEM
============================================ */
/* Desktop tabs container */
.rpg-tabs-container {
display: flex;
flex-direction: column;
gap: 0;
height: 100%;
width: 100%;
}
/* Desktop tab navigation */
.rpg-tabs-nav {
display: flex;
gap: 0;
background: var(--SmartThemeBlurTintColor);
border-bottom: 2px solid var(--SmartThemeBorderColor);
overflow-x: auto;
overflow-y: hidden;
flex-wrap: nowrap;
scrollbar-width: thin;
scrollbar-color: var(--SmartThemeBorderColor) transparent;
}
.rpg-tabs-nav::-webkit-scrollbar {
height: 6px;
}
.rpg-tabs-nav::-webkit-scrollbar-track {
background: transparent;
}
.rpg-tabs-nav::-webkit-scrollbar-thumb {
background: var(--SmartThemeBorderColor);
border-radius: 3px;
}
.rpg-tabs-nav::-webkit-scrollbar-thumb:hover {
background: var(--rpg-highlight);
}
/* Desktop tab button */
.rpg-tab-btn {
flex: 1;
min-width: fit-content;
white-space: nowrap;
padding: 0.75rem 1rem;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
font-size: 0.95rem;
font-weight: 500;
color: var(--SmartThemeBodyColor);
background: transparent;
border: none;
border-bottom: 3px solid transparent;
cursor: pointer;
transition: all 0.2s ease;
}
.rpg-tab-btn:hover {
background: rgba(var(--rpg-highlight-rgb, 233, 69, 96), 0.1);
color: var(--rpg-highlight);
}
.rpg-tab-btn.active {
background: transparent;
border-bottom-color: var(--rpg-highlight);
color: var(--rpg-highlight);
}
.rpg-tab-btn i {
font-size: 1.1rem;
}
/* Desktop tab content */
.rpg-tab-content {
display: none;
flex-direction: column;
gap: 1rem;
overflow-y: auto;
overflow-x: hidden;
padding: 0 0.5rem;
flex: 1;
}
.rpg-tab-content.active {
display: flex;
}
/* Hide dividers when tabs are active (tabs separate content) */
.rpg-tabs-container .rpg-divider {
display: none;
}
/* Mobile Responsive Styles */
@media (max-width: 768px) {
.rpg-inventory-subtabs {
flex-direction: column;
gap: 0.35rem;
}
.rpg-inventory-subtab {
font-size: 1rem;
padding: 0.75rem;
}
.rpg-inventory-header {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
.rpg-inventory-header h4 {
font-size: 1rem;
}
.rpg-inventory-edit-btn,
.rpg-inventory-add-btn,
.rpg-inventory-remove-btn {
padding: 0.6rem 1rem;
font-size: 0.95rem;
min-height: 2.5rem;
}
.rpg-storage-header {
padding: 0.75rem;
}
.rpg-storage-toggle {
min-width: 2rem;
min-height: 2rem;
}
}
+391
View File
@@ -0,0 +1,391 @@
/* ============================================
QUESTS SYSTEM STYLING
============================================ */
/* Quest Container */
#rpg-quests {
display: flex;
flex-direction: column;
gap: 1rem;
padding: 0.5rem;
font-size: 0.9rem;
}
/* Quest Sub-tabs Navigation (Main / Optional) */
.rpg-quests-subtabs {
display: flex;
gap: 0.5rem;
border-bottom: 2px solid var(--SmartThemeBorderColor);
padding-bottom: 0.5rem;
overflow-x: auto;
overflow-y: hidden;
flex-wrap: nowrap;
scrollbar-width: thin;
scrollbar-color: var(--SmartThemeBorderColor) transparent;
}
.rpg-quests-subtabs::-webkit-scrollbar {
height: 6px;
}
.rpg-quests-subtabs::-webkit-scrollbar-track {
background: transparent;
}
.rpg-quests-subtabs::-webkit-scrollbar-thumb {
background: var(--SmartThemeBorderColor);
border-radius: 3px;
}
.rpg-quests-subtabs::-webkit-scrollbar-thumb:hover {
background: var(--rpg-accent);
}
.rpg-quests-subtab {
flex: 1;
padding: 0.5rem 1rem;
background: transparent;
border: 2px solid var(--SmartThemeBorderColor);
border-radius: 0.25rem;
color: var(--SmartThemeBodyColor);
cursor: pointer;
transition: all 0.2s ease;
font-weight: 500;
text-align: center;
font-size: clamp(0.75rem, 1vw, 0.9rem);
min-width: fit-content;
white-space: nowrap;
}
.rpg-quests-subtab:hover {
background: rgba(var(--rpg-highlight-rgb, 233, 69, 96), 0.1);
border-color: var(--rpg-highlight);
color: var(--rpg-highlight);
}
.rpg-quests-subtab.active {
background: transparent;
border-color: var(--rpg-highlight);
color: var(--rpg-highlight);
font-weight: 600;
}
/* Quest Sections */
.rpg-quest-section {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.rpg-quest-header {
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--SmartThemeBorderColor);
}
.rpg-quest-header h4 {
margin: 0;
font-size: 1.1rem;
color: var(--SmartThemeBodyColor);
}
.rpg-quest-content {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
/* Main Quest Display */
.rpg-main-quest-display {
padding: 0.75rem;
background: var(--SmartThemeBlurTintColor);
border: 2px solid var(--rpg-highlight);
border-radius: 0.25rem;
color: var(--SmartThemeBodyColor);
line-height: 1.6;
white-space: pre-wrap;
word-wrap: break-word;
font-size: 1rem;
font-weight: 500;
}
.rpg-main-quest-actions {
display: flex;
gap: 0.5rem;
justify-content: flex-end;
}
/* Optional Quests List */
.rpg-quest-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.rpg-quest-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
background: var(--SmartThemeBlurTintColor);
border: 1px solid var(--SmartThemeBorderColor);
border-radius: 0.25rem;
transition: all 0.2s ease;
}
.rpg-quest-item:hover {
border-color: var(--rpg-highlight);
background: rgba(var(--rpg-highlight-rgb, 233, 69, 96), 0.05);
}
.rpg-quest-title {
flex: 1;
color: var(--SmartThemeBodyColor);
line-height: 1.5;
word-wrap: break-word;
}
.rpg-quest-actions {
display: flex;
gap: 0.5rem;
}
/* Quest Buttons */
.rpg-quest-edit,
.rpg-quest-remove,
.rpg-add-quest-btn {
padding: 0.4rem 0.75rem;
border: 1px solid var(--SmartThemeBorderColor);
border-radius: 0.25rem;
background: var(--SmartThemeBlurTintColor);
color: var(--SmartThemeBodyColor);
cursor: pointer;
transition: all 0.2s ease;
font-size: 0.85rem;
display: flex;
align-items: center;
gap: 0.35rem;
}
.rpg-quest-edit:hover {
background: var(--ac-style-color-matchedText);
border-color: var(--ac-style-color-matchedText);
color: white;
}
.rpg-quest-remove:hover {
background: #e74c3c;
border-color: #e74c3c;
color: white;
}
.rpg-add-quest-btn {
background: transparent;
border-color: var(--rpg-highlight);
color: var(--rpg-highlight);
}
.rpg-add-quest-btn:hover {
background: rgba(var(--rpg-highlight-rgb, 233, 69, 96), 0.1);
border-color: var(--rpg-highlight);
}
/* Quest Empty State */
.rpg-quest-empty {
padding: 2rem;
text-align: center;
color: var(--SmartThemeFastUISliderColColor);
font-style: italic;
}
/* Quest Hint */
.rpg-quest-hint {
padding: 0.5rem;
background: transparent;
border: 2px solid var(--rpg-highlight);
border-radius: 0.25rem;
font-size: 0.85rem;
color: var(--SmartThemeFastUISliderColColor);
display: flex;
gap: 0.5rem;
align-items: flex-start;
}
.rpg-quest-hint i {
margin-top: 0.1rem;
}
/* Quest Inline Edit Form */
.rpg-quest-edit-form {
display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 0.75rem;
background: var(--SmartThemeBlurTintColor);
border: 2px solid var(--rpg-highlight);
border-radius: 0.25rem;
}
.rpg-quest-edit-form input,
.rpg-quest-edit-form textarea {
width: 100%;
padding: 0.5rem;
background: rgba(0, 0, 0, 0.3);
border: 1px solid var(--SmartThemeBorderColor);
border-radius: 0.25rem;
color: var(--SmartThemeBodyColor);
font-family: inherit;
font-size: 0.9rem;
}
.rpg-quest-edit-form input:focus,
.rpg-quest-edit-form textarea:focus {
outline: none;
border-color: var(--rpg-highlight);
}
.rpg-quest-edit-actions {
display: flex;
gap: 0.5rem;
justify-content: flex-end;
}
.rpg-quest-save,
.rpg-quest-cancel {
padding: 0.5rem 1rem;
border: 1px solid var(--SmartThemeBorderColor);
border-radius: 0.25rem;
cursor: pointer;
font-size: 0.85rem;
transition: all 0.2s ease;
}
.rpg-quest-save {
background: var(--rpg-highlight);
border-color: var(--rpg-highlight);
color: white;
}
.rpg-quest-save:hover {
background: #ff6b81;
border-color: #ff6b81;
}
.rpg-quest-cancel {
background: transparent;
color: var(--SmartThemeBodyColor);
}
.rpg-quest-cancel:hover {
background: rgba(255, 255, 255, 0.1);
}
/* Mobile Responsive Styles */
@media (max-width: 768px) {
.rpg-quests-subtabs {
flex-direction: column;
gap: 0.5rem;
}
.rpg-quests-subtab {
padding: 0.75rem;
font-size: 1rem;
}
.rpg-quest-header h4 {
font-size: 1rem;
}
.rpg-quest-edit,
.rpg-quest-remove,
.rpg-add-quest-btn {
padding: 0.6rem 1rem;
font-size: 0.95rem;
min-height: 2.5rem;
}
.rpg-quest-item {
flex-direction: column;
align-items: flex-start;
}
.rpg-quest-actions {
width: 100%;
justify-content: space-between;
}
.rpg-main-quest-actions {
flex-direction: column;
}
.rpg-main-quest-actions button {
width: 100%;
}
/* ========================================
MOBILE FONT SIZE OVERRIDES
Fix all vw-based font sizes for mobile readability
======================================== */
/* Collapse toggle button */
.rpg-collapse-toggle {
font-size: clamp(16px, 3vw, 20px) !important;
}
/* Top position panel titles */
.rpg-panel.rpg-position-top .rpg-stats-title {
font-size: clamp(12px, 2.6vw, 16px) !important;
}
.rpg-panel.rpg-position-top .rpg-mood {
font-size: clamp(10px, 2vw, 13px) !important;
}
.rpg-panel.rpg-position-top .rpg-classic-stats-title {
font-size: clamp(10px, 2vw, 13px) !important;
}
.rpg-panel.rpg-position-top .rpg-classic-stat-label {
font-size: clamp(8px, 1.7vw, 11px) !important;
}
.rpg-panel.rpg-position-top .rpg-classic-stat-value {
font-size: clamp(12px, 2.6vw, 16px) !important;
}
.rpg-panel.rpg-position-top .rpg-classic-stat-btn {
font-size: clamp(10px, 2.2vw, 14px) !important;
}
.rpg-panel.rpg-position-top .rpg-info-content,
.rpg-panel.rpg-position-top .rpg-thoughts-content {
font-size: clamp(10px, 2.2vw, 14px) !important;
}
/* Panel header */
.rpg-panel-header h3 {
font-size: clamp(14px, 3.4vw, 18px) !important;
}
/* Loading indicator */
.rpg-loading {
font-size: clamp(12px, 2.6vw, 16px) !important;
}
/* Dice display */
.rpg-dice-display {
font-size: clamp(10px, 2vw, 13px) !important;
}
.rpg-dice-display i {
font-size: clamp(12px, 2.6vw, 16px) !important;
}
.rpg-clear-dice-btn {
font-size: clamp(14px, 3vw, 18px) !important;
}
}
+149
View File
@@ -0,0 +1,149 @@
/* ========================================
MOBILE FONT SIZE OVERRIDES (ALL SCREENS)
Apply to screens up to 1000px
======================================== */
@media (max-width: 1000px) {
/* Collapse toggle button */
.rpg-collapse-toggle {
font-size: clamp(16px, 3vw, 20px) !important;
}
/* Top position panel titles */
.rpg-panel.rpg-position-top .rpg-stats-title {
font-size: clamp(12px, 2.6vw, 16px) !important;
}
.rpg-panel.rpg-position-top .rpg-mood {
font-size: clamp(10px, 2vw, 13px) !important;
}
.rpg-panel.rpg-position-top .rpg-classic-stats-title {
font-size: clamp(10px, 2vw, 13px) !important;
}
.rpg-panel.rpg-position-top .rpg-classic-stat-label {
font-size: clamp(8px, 1.7vw, 11px) !important;
}
.rpg-panel.rpg-position-top .rpg-classic-stat-value {
font-size: clamp(12px, 2.6vw, 16px) !important;
}
.rpg-panel.rpg-position-top .rpg-classic-stat-btn {
font-size: clamp(10px, 2.2vw, 14px) !important;
}
.rpg-panel.rpg-position-top .rpg-info-content,
.rpg-panel.rpg-position-top .rpg-thoughts-content {
font-size: clamp(10px, 2.2vw, 14px) !important;
}
/* Panel header */
.rpg-panel-header h3 {
font-size: clamp(14px, 3.4vw, 18px) !important;
}
/* Loading indicator */
.rpg-loading {
font-size: clamp(12px, 2.6vw, 16px) !important;
}
/* Dice display */
.rpg-dice-display {
font-size: clamp(10px, 2vw, 13px) !important;
}
.rpg-dice-display i {
font-size: clamp(12px, 2.6vw, 16px) !important;
}
.rpg-clear-dice-btn {
font-size: clamp(14px, 3vw, 18px) !important;
}
}
/* ============================================
CHAPTER CHECKPOINT STYLES
============================================ */
/* Checkpoint indicator in messages */
.rpg-checkpoint-indicator {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
margin-bottom: 12px;
background: linear-gradient(135deg, var(--rpg-border-transparent, rgba(74, 158, 255, 0.15)) 0%, var(--rpg-border-transparent-fade, rgba(74, 158, 255, 0.05)) 100%);
border-left: 4px solid var(--rpg-border, #4a7ba7);
border-radius: 4px;
font-size: 13px;
font-weight: 600;
color: var(--rpg-highlight, #4a9eff);
animation: checkpoint-fade-in 0.3s ease-out;
}
.rpg-checkpoint-indicator i {
font-size: 16px;
}
@keyframes checkpoint-fade-in {
from {
opacity: 0;
transform: translateX(-10px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
/* Optional: Add a subtle glow effect for checkpoint messages */
.mes:has(.rpg-checkpoint-indicator) {
position: relative;
}
.mes:has(.rpg-checkpoint-indicator)::before {
content: '';
position: absolute;
left: -4px;
top: 0;
bottom: 0;
width: 3px;
background: linear-gradient(180deg, var(--rpg-border, #4a7ba7) 0%, transparent 100%);
opacity: 0.5;
}
/* Avatar generation loading overlay */
.rpg-avatar-generating {
position: relative;
}
.rpg-generating-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 24px;
border-radius: 8px;
}
/* Textarea style for LLM instruction input */
.rpg-textarea {
width: 100%;
min-height: 80px;
padding: 10px;
border-radius: 4px;
border: 1px solid var(--rpg-border);
background: var(--SmartThemeBlurTintColor);
color: var(--SmartThemeBodyColor);
font-family: inherit;
resize: vertical;
box-sizing: border-box;
}
File diff suppressed because it is too large Load Diff
+132
View File
@@ -0,0 +1,132 @@
/* ============================================
STATUS EFFECTS
============================================ */
/* Status Effects Container */
.rpg-encounter-statuses {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 8px;
}
/* Individual Status Effect */
.rpg-encounter-status {
display: inline-block;
font-size: 20px;
line-height: 1;
padding: 4px;
background: rgba(0, 0, 0, 0.3);
border-radius: 4px;
cursor: help;
transition: transform 0.2s ease;
}
.rpg-encounter-status:hover {
transform: scale(1.2);
}
/* ============================================
PLOT BUTTONS - RESPONSIVE LAYOUT
============================================ */
/* Base styles for plot buttons container */
#rpg-plot-buttons {
display: flex !important;
flex-wrap: wrap;
justify-content: center;
gap: 4px;
align-items: center;
}
/* Base button styles - ensure consistent sizing */
#rpg-plot-random,
#rpg-plot-natural,
#rpg-encounter-button {
flex: 0 1 auto;
white-space: nowrap;
min-width: 0;
}
/* Tablet and smaller screens - adjust button sizing */
@media (max-width: 768px) {
#rpg-plot-buttons {
gap: 3px;
}
#rpg-plot-random,
#rpg-plot-natural,
#rpg-encounter-button {
padding: 6px 10px !important;
font-size: 12px !important;
}
}
/* Small mobile screens - more compact buttons */
@media (max-width: 600px) {
#rpg-plot-buttons {
gap: 2px;
}
#rpg-plot-random,
#rpg-plot-natural,
#rpg-encounter-button {
padding: 5px 8px !important;
font-size: 11px !important;
}
}
/* Very small screens - stack buttons vertically or use icons only */
@media (max-width: 480px) {
#rpg-plot-buttons {
gap: 4px;
}
/* Option 1: Stack vertically (uncomment if preferred)
#rpg-plot-buttons {
flex-direction: column;
width: 100%;
}
#rpg-plot-random,
#rpg-plot-natural,
#rpg-encounter-button {
width: 100%;
padding: 8px !important;
font-size: 13px !important;
}
*/
/* Option 2: Keep horizontal but more compact (default) */
#rpg-plot-random,
#rpg-plot-natural,
#rpg-encounter-button {
padding: 6px 8px !important;
font-size: 11px !important;
}
}
/* Extra small screens - icon-only mode */
@media (max-width: 400px) {
#rpg-plot-random,
#rpg-plot-natural,
#rpg-encounter-button {
padding: 6px !important;
}
/* Hide button text, keep icons */
#rpg-plot-random .rpg-btn-text,
#rpg-plot-natural .rpg-btn-text,
#rpg-encounter-button .rpg-btn-text {
display: none;
}
/* Ensure icons are visible */
#rpg-plot-random i,
#rpg-plot-natural i,
#rpg-encounter-button i {
margin: 0 !important;
font-size: 14px;
}
}
+259
View File
@@ -0,0 +1,259 @@
/* ============================================
SPOTIFY MUSIC WIDGET STYLES
============================================ */
/* Music Widget Container - Positioned above chat input */
.rpg-music-widget {
position: absolute;
top: 0;
left: 50%;
transform: translate(-50%, calc(-100% - 8px));
width: calc(100% - 40px);
max-width: 650px;
z-index: 1000;
animation: rpg-music-slide-in 0.3s ease-out;
}
@keyframes rpg-music-slide-in {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Widget Content - The actual player UI */
.rpg-music-widget-content {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 14px;
background: var(--rpg-accent);
border-radius: 12px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25), 0 2px 4px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
border: 2px solid var(--rpg-border);
}
.rpg-music-widget-content:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3), 0 4px 8px var(--rpg-highlight);
}
/* Spotify Icon */
.rpg-music-widget-icon {
flex-shrink: 0;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.4);
border-radius: 50%;
color: #1DB954;
font-size: 20px;
}
/* Song Info Container */
.rpg-music-widget-info {
flex: 1;
min-width: 0; /* Allows text truncation */
display: flex;
flex-direction: column;
gap: 2px;
}
/* Song Title */
.rpg-music-widget-title {
color: var(--rpg-text);
font-size: 14px;
font-weight: 700;
line-height: 1.3;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
/* Artist Name */
.rpg-music-widget-artist {
color: var(--rpg-text);
opacity: 0.7;
font-size: 12px;
font-weight: 500;
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Play Button */
.rpg-music-widget-play {
flex-shrink: 0;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
background: var(--rpg-highlight);
border: none;
border-radius: 50%;
color: var(--rpg-text);
font-size: 14px;
cursor: pointer;
transition: transform 0.15s ease, box-shadow 0.15s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.rpg-music-widget-play:hover {
transform: scale(1.1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}
.rpg-music-widget-play:active {
transform: scale(0.95);
}
.rpg-music-widget-play i {
margin-left: 2px; /* Visual centering for play icon */
}
/* Close/Dismiss Button */
.rpg-music-widget-close {
flex-shrink: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.3);
border: none;
border-radius: 50%;
color: var(--rpg-text);
opacity: 0.6;
font-size: 12px;
cursor: pointer;
transition: background 0.15s ease, opacity 0.15s ease;
}
.rpg-music-widget-close:hover {
background: rgba(0, 0, 0, 0.5);
opacity: 1;
}
/* Mobile Responsive Styles */
@media (max-width: 1000px) {
.rpg-music-widget {
width: calc(100% - 20px);
max-width: none;
left: 50%;
transform: translate(-50%, calc(-100% - 8px));
}
.rpg-music-widget-content {
padding: 8px 10px;
gap: 8px;
}
.rpg-music-widget-icon {
width: 32px;
height: 32px;
font-size: 16px;
}
.rpg-music-widget-info {
gap: 1px;
}
.rpg-music-widget-title {
font-size: 12px;
}
.rpg-music-widget-artist {
font-size: 10px;
}
.rpg-music-widget-play {
width: 30px;
height: 30px;
font-size: 11px;
}
.rpg-music-widget-close {
width: 20px;
height: 20px;
font-size: 10px;
}
}
@media (max-width: 600px) {
.rpg-music-widget {
width: calc(100% - 12px);
}
.rpg-music-widget-content {
padding: 6px 8px;
gap: 6px;
}
.rpg-music-widget-icon {
width: 28px;
height: 28px;
font-size: 14px;
}
.rpg-music-widget-title {
font-size: 11px;
}
.rpg-music-widget-artist {
font-size: 9px;
}
.rpg-music-widget-play {
width: 28px;
height: 28px;
font-size: 10px;
}
.rpg-music-widget-close {
width: 18px;
height: 18px;
font-size: 9px;
}
}
/* Theme Support - Apply theme colors */
.rpg-panel[data-theme="sci-fi"] ~ .rpg-music-widget .rpg-music-widget-content,
body[data-theme="sci-fi"] .rpg-music-widget-content {
background: linear-gradient(135deg, #8b00ff 0%, #6a00cc 50%, #5000aa 100%);
}
.rpg-panel[data-theme="sci-fi"] ~ .rpg-music-widget .rpg-music-widget-play,
body[data-theme="sci-fi"] .rpg-music-widget-play {
color: #8b00ff;
}
.rpg-panel[data-theme="fantasy"] ~ .rpg-music-widget .rpg-music-widget-content,
body[data-theme="fantasy"] .rpg-music-widget-content {
background: linear-gradient(135deg, #d4af37 0%, #b8962e 50%, #9c7d25 100%);
}
.rpg-panel[data-theme="fantasy"] ~ .rpg-music-widget .rpg-music-widget-play,
body[data-theme="fantasy"] .rpg-music-widget-play {
color: #d4af37;
}
.rpg-panel[data-theme="cyberpunk"] ~ .rpg-music-widget .rpg-music-widget-content,
body[data-theme="cyberpunk"] .rpg-music-widget-content {
background: linear-gradient(135deg, #ff2a6d 0%, #d91e58 50%, #b31848 100%);
}
.rpg-panel[data-theme="cyberpunk"] ~ .rpg-music-widget .rpg-music-widget-play,
body[data-theme="cyberpunk"] .rpg-music-widget-play {
color: #ff2a6d;
}
File diff suppressed because it is too large Load Diff
+35
View File
@@ -0,0 +1,35 @@
/* ============================================
HOLIDAY PROMOTION BANNER
============================================ */
/* Mobile-friendly holiday promo */
.rpg-holiday-promo {
max-width: 100%;
word-wrap: break-word;
}
.rpg-holiday-promo a {
display: inline-block;
max-width: 100%;
}
/* Dismiss button hover effect */
#rpg-dismiss-promo:hover {
opacity: 1 !important;
transform: scale(1.1);
}
/* Mobile responsiveness */
@media (max-width: 600px) {
.rpg-holiday-promo {
font-size: 10px !important;
padding: 10px 8px !important;
}
#rpg-dismiss-promo {
font-size: 12px !important;
top: 2px !important;
right: 2px !important;
}
}
+93
View File
@@ -0,0 +1,93 @@
/* ============================================
THREE-COLUMN FEATURE TOGGLES LAYOUT
============================================ */
/* Features row container */
.rpg-features-row {
display: grid;
grid-template-rows: repeat(2, 1fr);
grid-auto-flow: column;
grid-auto-columns: min-content;
gap: 8px;
margin-bottom: 12px;
overflow-x: auto;
overflow-y: hidden;
flex-wrap: nowrap;
padding: 0 4px; /* Prevent first/last items from being cut off */
-webkit-overflow-scrolling: touch; /* Smooth scrolling on iOS */
}
/* Center items when they fit, allow scrolling when they don't */
/*.rpg-features-row::before,*/
/*.rpg-features-row::after {*/
/* content: '';*/
/* margin: auto;*/
/*}*/
/* Hide scrollbar for cleaner look while maintaining functionality */
.rpg-features-row::-webkit-scrollbar {
height: 4px;
}
.rpg-features-row::-webkit-scrollbar-track {
background: transparent;
}
.rpg-features-row::-webkit-scrollbar-thumb {
background: rgba(128, 128, 128, 0.3);
border-radius: 2px;
}
.rpg-features-row::-webkit-scrollbar-thumb:hover {
background: rgba(128, 128, 128, 0.5);
}
/* Each feature column */
.rpg-feature-col {
flex: 0 0 auto;
min-width: 60px;
}
.rpg-feature-col .rpg-toggle-label {
justify-content: center;
align-items: center;
}
/* Always hide text, show only checkbox + icon */
.rpg-feature-col .rpg-toggle-text {
display: none;
}
.rpg-feature-col .rpg-toggle-label i {
font-size: 0.95rem;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
/* Tablet and below: Hide text, show only checkbox + icon */
@media (max-width: 768px) {
.rpg-feature-col .rpg-toggle-text {
display: none;
}
.rpg-feature-col .rpg-toggle-label i {
font-size: 1rem;
}
}
/* Very small screens: Remove column layout since we have horizontal scroll */
@media (max-width: 400px) {
.rpg-feature-col .rpg-toggle-label i {
font-size: 1rem;
}
}
/* Ensure send_form has relative positioning for music widget placement */
#send_form {
position: relative !important;
}
+100
View File
@@ -0,0 +1,100 @@
/* ============================================
IMPORT MODE DIALOG STYLES
============================================ */
/* Import Mode Dialog Overlay */
.rpg-import-dialog-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 100000;
}
.rpg-import-dialog {
background: var(--SmartThemeBlurTintColor, #2a2a3e);
border: 1px solid var(--SmartThemeBorderColor, #4a4a4a);
border-radius: 8px;
padding: 20px;
min-width: 300px;
max-width: 400px;
text-align: center;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
}
.rpg-import-dialog h4 {
margin: 0 0 15px 0;
color: var(--SmartThemeBodyColor, #eee);
}
.rpg-import-dialog h4 i {
margin-right: 8px;
color: var(--SmartThemeQuoteColor, #e94560);
}
.rpg-import-dialog p {
margin: 0 0 20px 0;
color: var(--SmartThemeBodyColor, #ccc);
font-size: 0.95em;
}
.rpg-import-dialog-buttons {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 15px;
}
.rpg-import-dialog-buttons button {
padding: 10px 15px;
border-radius: 6px;
cursor: pointer;
font-size: 0.95em;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.2s ease;
}
.rpg-import-dialog .rpg-btn-secondary {
background: var(--black30a, rgba(0, 0, 0, 0.3));
border: 1px solid var(--SmartThemeBorderColor, #4a4a4a);
color: var(--SmartThemeBodyColor, #ccc);
}
.rpg-import-dialog .rpg-btn-secondary:hover {
background: var(--SmartThemeBorderColor, #4a4a4a);
color: white;
}
.rpg-import-dialog .rpg-btn-primary {
background: var(--SmartThemeQuoteColor, #e94560);
border: 1px solid var(--SmartThemeQuoteColor, #e94560);
color: white;
}
.rpg-import-dialog .rpg-btn-primary:hover {
background: var(--SmartThemeQuoteColor, #ff5a75);
border-color: var(--SmartThemeQuoteColor, #ff5a75);
}
.rpg-import-dialog .rpg-btn-cancel {
background: transparent;
border: none;
color: var(--SmartThemeBodyColor, #888);
cursor: pointer;
font-size: 0.85em;
padding: 5px;
}
.rpg-import-dialog .rpg-btn-cancel:hover {
color: var(--SmartThemeBodyColor, #ccc);
text-decoration: underline;
}
+332
View File
@@ -0,0 +1,332 @@
/* ============================================
FAB Widget System - Improved Layout
============================================ */
/* Widget container - positioned relative to FAB */
.rpg-fab-widget-container {
position: fixed;
pointer-events: none;
z-index: 9998;
}
/* Hide FAB widgets on desktop viewport */
@media (min-width: 1001px) {
.rpg-fab-widget-container {
display: none !important;
}
}
/* Individual widget base styling */
.rpg-fab-widget {
position: absolute;
pointer-events: auto;
background: rgba(20, 20, 35, 0.95);
border: 1px solid rgba(100, 150, 255, 0.3);
border-radius: 8px;
padding: 6px 10px;
font-size: 11px;
color: #fff;
white-space: nowrap;
backdrop-filter: blur(10px);
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.5), 0 0 1px rgba(100, 150, 255, 0.3);
transition: opacity 0.2s ease, transform 0.15s ease;
}
.rpg-fab-widget:hover {
border-color: rgba(100, 150, 255, 0.5);
}
/* Expanded state for truncated widgets - desktop hover and mobile tap */
.rpg-fab-widget[data-full-text]:hover,
.rpg-fab-widget[data-full-text].expanded {
z-index: 9999 !important;
max-width: none !important;
white-space: nowrap;
}
.rpg-fab-widget[data-full-text]:hover .rpg-fab-widget-text,
.rpg-fab-widget[data-full-text].expanded .rpg-fab-widget-text {
/* Show full text on hover/tap */
}
/* Hide truncated text and show full text on expand */
.rpg-fab-widget[data-full-text]:hover .rpg-truncated,
.rpg-fab-widget[data-full-text].expanded .rpg-truncated {
display: none;
}
.rpg-fab-widget[data-full-text]:hover .rpg-full-text,
.rpg-fab-widget[data-full-text].expanded .rpg-full-text {
display: inline;
}
/* Default: show truncated, hide full */
.rpg-fab-widget .rpg-full-text {
display: none;
}
.rpg-fab-widget .rpg-truncated {
display: inline;
}
/* 8-Position system - spread out more to avoid overlap
Positions: 0=N, 1=NE, 2=E, 3=SE, 4=S, 5=SW, 6=W, 7=NW */
/* Position 0: North (top center) */
.rpg-fab-widget-pos-0 {
bottom: calc(100% + 15px);
left: 50%;
transform: translateX(-50%);
}
.rpg-fab-widget-pos-0:hover { transform: translateX(-50%) scale(1.05); }
/* Position 1: Northeast */
.rpg-fab-widget-pos-1 {
bottom: calc(100% + 10px);
left: calc(100% + 15px);
}
/* Position 2: East (right center) */
.rpg-fab-widget-pos-2 {
top: 50%;
left: calc(100% + 15px);
transform: translateY(-50%);
}
.rpg-fab-widget-pos-2:hover { transform: translateY(-50%) scale(1.05); }
/* Position 3: Southeast */
.rpg-fab-widget-pos-3 {
top: calc(100% + 10px);
left: calc(100% + 15px);
}
/* Position 4: South (bottom center) */
.rpg-fab-widget-pos-4 {
top: calc(100% + 15px);
left: 50%;
transform: translateX(-50%);
}
.rpg-fab-widget-pos-4:hover { transform: translateX(-50%) scale(1.05); }
/* Position 5: Southwest */
.rpg-fab-widget-pos-5 {
top: calc(100% + 10px);
right: calc(100% + 15px);
left: auto;
}
/* Position 6: West - Stats (top edge at FAB center + gap, grows DOWN) */
.rpg-fab-widget-pos-6 {
top: calc(50% + 8px);
right: calc(100% + 15px);
left: auto;
}
/* Position 7: Northwest - Attributes (bottom edge at FAB center - gap, grows UP) */
.rpg-fab-widget-pos-7 {
bottom: calc(50% + 8px);
right: calc(100% + 15px);
left: auto;
}
/* Centered large widget (when only one is visible) - vertically centered */
.rpg-fab-widget-centered.rpg-fab-widget-pos-6,
.rpg-fab-widget-centered.rpg-fab-widget-pos-7 {
top: 50%;
bottom: auto;
transform: translateY(-50%);
}
/* Weather icon widget - larger emoji display */
.rpg-fab-widget-weather-icon {
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
padding: 6px;
min-width: 32px;
min-height: 32px;
width: 32px;
height: 32px;
border-radius: 50%;
}
/* Weather description widget */
.rpg-fab-widget-weather-desc {
max-width: 100px;
font-size: 10px;
}
/* Clock/Time widget - bottom position with animated clock */
.rpg-fab-widget-clock {
display: flex;
flex-direction: column;
align-items: center;
font-family: 'Roboto Mono', 'Consolas', monospace;
font-size: 11px;
letter-spacing: 0.5px;
padding: 4px 8px;
gap: 2px;
}
/* Mini animated clock face */
.rpg-fab-clock-face {
position: relative;
width: 24px;
height: 24px;
border: 2px solid var(--rpg-border, #4a7ba7);
border-radius: 50%;
background: var(--rpg-accent, rgba(22, 33, 62, 0.9));
}
.rpg-fab-clock-hour {
position: absolute;
width: 2px;
height: 7px;
background: var(--rpg-text, #eaeaea);
left: 50%;
bottom: 50%;
margin-left: -1px;
transform-origin: bottom center;
border-radius: 1px;
}
.rpg-fab-clock-minute {
position: absolute;
width: 1.5px;
height: 9px;
background: var(--rpg-highlight, #4a90e2);
left: 50%;
bottom: 50%;
margin-left: -0.75px;
transform-origin: bottom center;
border-radius: 1px;
}
.rpg-fab-clock-center {
position: absolute;
width: 4px;
height: 4px;
background: var(--rpg-highlight, #4a90e2);
border-radius: 50%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.rpg-fab-clock-time {
font-size: 11px;
white-space: nowrap;
}
/* Date widget */
.rpg-fab-widget-date {
font-size: 10px;
}
/* Location widget - two lines */
.rpg-fab-widget-location {
max-width: 90px;
font-size: 10px;
overflow: hidden;
text-overflow: ellipsis;
}
/* Attributes widget - compact grid */
.rpg-fab-widget-attributes {
padding: 6px 10px;
}
.rpg-fab-widget-attr-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 4px;
}
.rpg-fab-widget-attr-item {
display: flex;
flex-direction: column;
align-items: center;
line-height: 1.2;
}
.rpg-fab-widget-attr-name {
font-size: 8px;
opacity: 0.7;
text-transform: uppercase;
}
.rpg-fab-widget-attr-value {
font-size: 12px;
font-weight: bold;
color: var(--rpg-highlight, #4a90e2);
white-space: nowrap;
}
/* Stats widget - vertical compact list */
.rpg-fab-widget-stats {
padding: 6px 10px;
min-width: 70px;
text-align: center;
}
.rpg-fab-widget-stats-row {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}
.rpg-fab-widget-stat-item {
font-size: 11px;
font-family: 'Roboto Mono', 'Consolas', monospace;
font-weight: 600;
white-space: nowrap;
display: block;
}
/* RPG Attributes widget - 2x3 grid */
.rpg-fab-widget-attributes {
padding: 6px 8px;
}
.rpg-fab-widget-attr-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 2px 10px;
}
.rpg-fab-widget-attr-item {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.rpg-fab-widget-attr-name {
font-size: 8px;
color: rgba(255, 255, 255, 0.6);
letter-spacing: 0.5px;
}
.rpg-fab-widget-attr-value {
font-size: 12px;
font-weight: 700;
color: #6af;
font-family: 'Roboto Mono', 'Consolas', monospace;
}
/* FAB Loading State */
#rpg-mobile-toggle.rpg-fab-loading {
animation: fabSpin 1s linear infinite;
}
#rpg-mobile-toggle.rpg-fab-loading i {
opacity: 0.7;
}
@keyframes fabSpin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
+497
View File
@@ -0,0 +1,497 @@
/* ============================================
FAB WIDGET THEME VARIATIONS
============================================ */
/* Sci-Fi / Synthwave Theme for FAB Widgets */
body:has(.rpg-panel[data-theme="sci-fi"]) .rpg-fab-widget {
background: rgba(10, 14, 39, 0.95);
border: 1px solid rgba(139, 0, 255, 0.5);
color: #00fff9;
box-shadow: 0 3px 12px rgba(139, 0, 255, 0.3), 0 0 8px rgba(255, 0, 110, 0.2);
}
body:has(.rpg-panel[data-theme="sci-fi"]) .rpg-fab-widget:hover {
border-color: rgba(255, 0, 110, 0.7);
box-shadow: 0 4px 16px rgba(139, 0, 255, 0.5), 0 0 12px rgba(255, 0, 110, 0.4);
}
/* Fantasy / Rustic Theme for FAB Widgets */
body:has(.rpg-panel[data-theme="fantasy"]) .rpg-fab-widget {
background: rgba(43, 24, 16, 0.95);
border: 1px solid rgba(139, 105, 20, 0.6);
color: #f4e8d0;
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.6), 0 0 1px rgba(212, 175, 55, 0.3);
}
body:has(.rpg-panel[data-theme="fantasy"]) .rpg-fab-widget:hover {
border-color: rgba(212, 175, 55, 0.8);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.7), 0 0 8px rgba(212, 175, 55, 0.4);
}
/* Cyberpunk Theme for FAB Widgets */
body:has(.rpg-panel[data-theme="cyberpunk"]) .rpg-fab-widget {
background: rgba(15, 5, 25, 0.95);
border: 1px solid rgba(255, 0, 255, 0.4);
color: #00ffff;
box-shadow: 0 3px 12px rgba(255, 0, 255, 0.3), 0 0 8px rgba(0, 255, 255, 0.2);
}
body:has(.rpg-panel[data-theme="cyberpunk"]) .rpg-fab-widget:hover {
border-color: rgba(0, 255, 255, 0.7);
box-shadow: 0 4px 16px rgba(255, 0, 255, 0.5), 0 0 12px rgba(0, 255, 255, 0.4);
}
/* Minimal Theme for FAB Widgets */
body:has(.rpg-panel[data-theme="minimal"]) .rpg-fab-widget {
background: rgba(245, 245, 250, 0.98);
border: 1px solid rgba(200, 200, 210, 0.8);
color: #2c3e50;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
body:has(.rpg-panel[data-theme="minimal"]) .rpg-fab-widget:hover {
border-color: rgba(52, 152, 219, 0.6);
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.15);
}
/* Dark Theme for FAB Widgets */
body:has(.rpg-panel[data-theme="dark"]) .rpg-fab-widget {
background: rgba(25, 25, 35, 0.95);
border: 1px solid rgba(70, 70, 90, 0.6);
color: #e0e0e0;
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.5);
}
body:has(.rpg-panel[data-theme="dark"]) .rpg-fab-widget:hover {
border-color: rgba(100, 100, 130, 0.8);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.6);
}
/* Light Theme for FAB Widgets */
body:has(.rpg-panel[data-theme="light"]) .rpg-fab-widget {
background: rgba(255, 255, 255, 0.98);
border: 1px solid rgba(220, 220, 230, 0.8);
color: #2c3e50;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
body:has(.rpg-panel[data-theme="light"]) .rpg-fab-widget:hover {
border-color: rgba(52, 152, 219, 0.6);
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.15);
}
/* ============================================
DESKTOP STRIP WIDGETS (Collapsed Panel)
Shows compact widgets when panel is collapsed
============================================ */
/* Strip Widget Container - hidden by default, shown when collapsed with strip widgets enabled */
.rpg-strip-widget-container {
display: none;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 8px 4px;
padding-top: 40px;
width: 100%;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: thin;
scrollbar-color: var(--rpg-border) transparent;
z-index: 10;
}
/* Show strip widgets when panel is collapsed and strip widgets are enabled */
.rpg-panel.rpg-collapsed.rpg-strip-widgets-enabled .rpg-strip-widget-container {
display: flex;
}
/* Wider collapsed state when strip widgets are enabled */
.rpg-panel.rpg-collapsed.rpg-strip-widgets-enabled {
max-width: 5rem !important;
min-width: 5rem !important;
}
/* Adjust collapse button position for wider strip */
.rpg-panel.rpg-collapsed.rpg-strip-widgets-enabled .rpg-collapse-toggle {
left: -2rem;
}
.rpg-panel.rpg-position-left.rpg-collapsed.rpg-strip-widgets-enabled .rpg-collapse-toggle {
left: auto;
right: -2rem;
}
/* Fix positioning for wider strip */
.rpg-panel.rpg-position-right.rpg-collapsed.rpg-strip-widgets-enabled {
left: calc(100vw - 5rem);
}
.rpg-panel.rpg-position-left.rpg-collapsed.rpg-strip-widgets-enabled {
right: calc(100vw - 5rem);
}
/* Base Strip Widget Styles */
.rpg-strip-widget {
display: none; /* Hidden by default, shown via JS when enabled */
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(20, 20, 35, 0.85);
border: 1px solid var(--rpg-border, rgba(100, 150, 255, 0.3));
border-radius: 6px;
padding: 4px;
font-size: 10px;
color: var(--rpg-text, #eaeaea);
backdrop-filter: blur(8px);
width: calc(100% - 8px);
min-width: 0;
transition: all 0.2s ease;
}
.rpg-strip-widget.rpg-strip-widget-visible {
display: flex;
}
.rpg-strip-widget:hover {
border-color: var(--rpg-highlight, #4a90e2);
box-shadow: 0 0 8px rgba(100, 150, 255, 0.3);
}
/* Weather Icon Widget */
.rpg-strip-widget-weather {
font-size: 18px;
padding: 6px 4px;
gap: 2px;
}
.rpg-strip-widget-weather .rpg-strip-widget-icon {
line-height: 1;
}
.rpg-strip-widget-weather .rpg-strip-widget-desc {
font-size: 8px;
opacity: 0.8;
text-align: center;
word-break: break-word;
line-height: 1.2;
}
/* Clock Widget with animated face */
.rpg-strip-widget-clock {
font-family: 'Roboto Mono', 'Consolas', monospace;
font-size: 10px;
gap: 4px;
padding: 6px 4px;
}
/* Mini animated clock face for strip */
.rpg-strip-clock-face {
position: relative;
width: 28px;
height: 28px;
border: 2px solid var(--rpg-border, #4a7ba7);
border-radius: 50%;
background: var(--rpg-accent, rgba(22, 33, 62, 0.9));
}
.rpg-strip-clock-hour {
position: absolute;
width: 2px;
height: 8px;
background: var(--rpg-text, #eaeaea);
left: 50%;
bottom: 50%;
margin-left: -1px;
transform-origin: bottom center;
border-radius: 1px;
}
.rpg-strip-clock-minute {
position: absolute;
width: 1.5px;
height: 10px;
background: var(--rpg-highlight, #4a90e2);
left: 50%;
bottom: 50%;
margin-left: -0.75px;
transform-origin: bottom center;
border-radius: 1px;
}
.rpg-strip-clock-center {
position: absolute;
width: 4px;
height: 4px;
background: var(--rpg-highlight, #4a90e2);
border-radius: 50%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.rpg-strip-widget-clock .rpg-strip-widget-value {
font-weight: 600;
letter-spacing: 0.5px;
font-size: 10px;
}
/* Date Widget */
.rpg-strip-widget-date {
font-size: 9px;
text-align: center;
gap: 2px;
}
.rpg-strip-widget-date i {
font-size: 11px;
opacity: 0.7;
}
.rpg-strip-widget-date .rpg-strip-widget-value {
word-break: break-word;
line-height: 1.2;
}
/* Location Widget */
.rpg-strip-widget-location {
font-size: 9px;
text-align: center;
gap: 2px;
}
.rpg-strip-widget-location i {
font-size: 11px;
color: var(--rpg-highlight, #4a90e2);
}
.rpg-strip-widget-location .rpg-strip-widget-value {
word-break: break-word;
line-height: 1.2;
max-height: 3em;
overflow: hidden;
text-overflow: ellipsis;
}
/* Stats Widget - Vertical list */
.rpg-strip-widget-stats {
padding: 4px 2px;
}
.rpg-strip-stats-list {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
width: 100%;
}
.rpg-strip-stat-item {
display: flex;
flex-direction: column;
align-items: center;
font-family: 'Roboto Mono', 'Consolas', monospace;
font-size: 9px;
width: 100%;
}
.rpg-strip-stat-name {
font-size: 7px;
opacity: 0.6;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.rpg-strip-stat-value {
font-size: 11px;
font-weight: 700;
}
/* Attributes Widget - 2-column grid */
.rpg-strip-widget-attributes {
padding: 4px 2px;
}
.rpg-strip-attributes-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 2px 4px;
width: 100%;
}
.rpg-strip-attr-item {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.rpg-strip-attr-name {
font-size: 7px;
opacity: 0.6;
text-transform: uppercase;
letter-spacing: 0.3px;
}
.rpg-strip-attr-value {
font-size: 10px;
font-weight: 700;
color: var(--rpg-highlight, #6af);
font-family: 'Roboto Mono', 'Consolas', monospace;
}
/* Strip Refresh Button */
.rpg-strip-refresh-btn {
margin-top: auto;
padding: 8px;
background: var(--rpg-accent, rgba(22, 33, 62, 0.9));
border: 1px solid var(--rpg-border, rgba(100, 150, 255, 0.3));
border-radius: 6px;
color: var(--rpg-highlight, #4a90e2);
cursor: pointer;
font-size: 14px;
width: calc(100% - 8px);
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.rpg-strip-refresh-btn:hover {
background: var(--rpg-highlight, #4a90e2);
color: var(--rpg-bg, #1a1a2e);
box-shadow: 0 0 10px rgba(100, 150, 255, 0.4);
}
.rpg-strip-refresh-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.rpg-strip-refresh-btn i.fa-spin {
animation: fa-spin 1s linear infinite;
}
/* ============================================
STRIP WIDGET THEME VARIATIONS
============================================ */
/* Sci-Fi Theme */
body:has(.rpg-panel[data-theme="sci-fi"]) .rpg-strip-widget {
background: rgba(10, 14, 39, 0.9);
border-color: rgba(139, 0, 255, 0.5);
color: #00fff9;
}
/* Fantasy Theme */
body:has(.rpg-panel[data-theme="fantasy"]) .rpg-strip-widget {
background: rgba(43, 24, 16, 0.9);
border-color: rgba(139, 105, 20, 0.6);
color: #f4e8d0;
}
/* Cyberpunk Theme */
body:has(.rpg-panel[data-theme="cyberpunk"]) .rpg-strip-widget {
background: rgba(15, 5, 25, 0.9);
border-color: rgba(255, 0, 255, 0.4);
color: #00ffff;
}
/* Dark Theme */
body:has(.rpg-panel[data-theme="dark"]) .rpg-strip-widget {
background: rgba(25, 25, 35, 0.9);
border-color: rgba(70, 70, 90, 0.6);
color: #e0e0e0;
}
/* Light Theme */
body:has(.rpg-panel[data-theme="light"]) .rpg-strip-widget {
background: rgba(255, 255, 255, 0.95);
border-color: rgba(220, 220, 230, 0.8);
color: #2c3e50;
}
/* Hide strip widgets on mobile */
@media (max-width: 768px) {
.rpg-strip-widget-container {
display: none !important;
}
.rpg-panel.rpg-collapsed.rpg-strip-widgets-enabled {
max-width: 2.5rem !important;
min-width: 2.5rem !important;
}
}
.rpg-inline-thoughts {
display: flex;
flex-direction: column;
gap: 0.5em;
margin: 0.2em var(--mes-right-spacing, 30px) 0.35em 0;
}
body.documentstyle .rpg-inline-thoughts {
margin-left: 20px;
}
.rpg-inline-thought {
margin: 0;
border: 1px solid rgba(255, 255, 255, 0.12);
border-left: 2px solid rgba(255, 120, 140, 0.6);
border-radius: 0.5em;
background: rgba(255, 255, 255, 0.03);
overflow: hidden;
}
.rpg-inline-thought[open] {
background: rgba(255, 255, 255, 0.045);
}
.rpg-inline-thought-summary {
display: flex;
align-items: center;
gap: 0.45em;
padding: 0.45em 0.7em;
cursor: pointer;
list-style: none;
font-weight: 600;
color: var(--SmartThemeBodyColor);
}
.rpg-inline-thought-summary:hover {
background: rgba(255, 255, 255, 0.035);
}
.rpg-inline-thought-summary::-webkit-details-marker {
display: none;
}
.rpg-inline-thought-icon {
opacity: 0.9;
flex: 0 0 auto;
}
.rpg-inline-thought-name {
line-height: 1.2;
}
.rpg-inline-thought-content {
padding: 0 0.7em 0.65em;
}
.rpg-inline-thought-text {
margin: 0;
padding: 0.6em 0.75em;
border-radius: 0.4em;
background: rgba(0, 0, 0, 0.28);
color: var(--SmartThemeBodyColor);
font-style: italic;
line-height: 1.45;
}
+379
View File
@@ -0,0 +1,379 @@
/* ============================================
Equipment Section Styles
============================================ */
.rpg-equipment-section {
display: none;
}
.rpg-equipment-container {
padding: 8px 0;
}
.rpg-equipment-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid var(--SmartThemeBorderColor);
}
.rpg-equipment-header h3 {
margin: 0;
font-size: 14px;
color: var(--SmartThemeTitleColor);
display: flex;
align-items: center;
gap: 6px;
}
.rpg-equipment-add-btn {
background: var(--SmartThemeAccentColor);
color: var(--SmartThemeBodyColor);
border: none;
border-radius: 4px;
padding: 5px 10px;
font-size: 11px;
cursor: pointer;
display: flex;
align-items: center;
gap: 4px;
transition: background-color 0.2s;
}
.rpg-equipment-add-btn:hover {
background: var(--SmartThemeButtonColor);
}
.rpg-equipment-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 8px;
margin-bottom: 16px;
}
.rpg-equipment-slot {
background: rgba(0, 0, 0, 0.2);
border: 1px solid var(--SmartThemeBorderColor);
border-radius: 6px;
padding: 8px;
min-height: 70px;
transition: border-color 0.2s, background-color 0.2s;
}
.rpg-equipment-slot.equipped {
border-color: var(--SmartThemeAccentColor);
background: rgba(0, 0, 0, 0.3);
}
.rpg-equipment-slot-header {
display: flex;
align-items: center;
gap: 4px;
margin-bottom: 4px;
font-size: 10px;
color: var(--SmartThemeTitleColor);
opacity: 0.7;
}
.rpg-equipment-slot-header i {
font-size: 9px;
}
.rpg-equipment-slot-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.rpg-equipment-unequip-btn {
background: none;
border: none;
color: var(--SmartThemeBodyColor);
opacity: 0.5;
cursor: pointer;
padding: 2px;
font-size: 9px;
transition: opacity 0.2s;
}
.rpg-equipment-unequip-btn:hover {
opacity: 1;
}
.rpg-equipment-item-name {
font-size: 12px;
font-weight: 600;
color: var(--SmartThemeTitleColor);
margin-bottom: 2px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.rpg-equipment-empty {
font-size: 11px;
color: var(--SmartThemeBodyColor);
opacity: 0.4;
font-style: italic;
padding: 4px 0;
}
.rpg-equipment-stats {
display: flex;
flex-wrap: wrap;
gap: 3px;
margin: 4px 0;
}
.rpg-eq-stat {
display: inline-flex;
align-items: center;
gap: 2px;
font-size: 9px;
background: rgba(0, 0, 0, 0.3);
padding: 1px 4px;
border-radius: 3px;
}
.rpg-eq-stat-label {
color: var(--SmartThemeBodyColor);
opacity: 0.7;
}
.rpg-eq-stat-value {
color: #4caf50;
font-weight: 600;
}
.rpg-equipment-description {
font-size: 10px;
color: var(--SmartThemeBodyColor);
opacity: 0.6;
margin-top: 2px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.rpg-equipment-item-actions {
display: flex;
gap: 4px;
margin-top: 4px;
padding-top: 4px;
border-top: 1px solid rgba(255, 255, 255, 0.05);
}
.rpg-equipment-edit-btn,
.rpg-equipment-delete-btn,
.rpg-equipment-equip-btn {
background: none;
border: none;
color: var(--SmartThemeBodyColor);
opacity: 0.5;
cursor: pointer;
padding: 2px 4px;
font-size: 10px;
transition: opacity 0.2s, color 0.2s;
}
.rpg-equipment-edit-btn:hover {
opacity: 1;
color: #4fc3f7;
}
.rpg-equipment-delete-btn:hover {
opacity: 1;
color: #f44336;
}
.rpg-equipment-equip-btn:hover {
opacity: 1;
color: #4caf50;
}
/* Equipment Inventory (unequipped items) */
.rpg-equipment-inventory {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--SmartThemeBorderColor);
}
.rpg-equipment-inventory h4 {
margin: 0 0 8px 0;
font-size: 12px;
color: var(--SmartThemeTitleColor);
opacity: 0.8;
}
.rpg-equipment-inventory-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.rpg-equipment-inventory-item {
background: rgba(0, 0, 0, 0.2);
border: 1px solid var(--SmartThemeBorderColor);
border-radius: 6px;
padding: 8px;
transition: border-color 0.2s;
}
.rpg-equipment-inventory-item:hover {
border-color: var(--SmartThemeAccentColor);
}
.rpg-equipment-inventory-item-header {
display: flex;
align-items: center;
gap: 4px;
margin-bottom: 4px;
}
.rpg-equipment-inventory-item-header i {
font-size: 10px;
opacity: 0.6;
}
.rpg-equipment-inventory-item-name {
flex: 1;
font-size: 12px;
font-weight: 600;
color: var(--SmartThemeTitleColor);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.rpg-equipment-inventory-item-type {
font-size: 9px;
color: var(--SmartThemeBodyColor);
opacity: 0.5;
background: rgba(0, 0, 0, 0.2);
padding: 1px 5px;
border-radius: 3px;
}
/* Equipment Modal */
.rpg-equipment-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
}
.rpg-equipment-modal-content {
background: var(--SmartThemeDialogBgColor, #1e1e2e);
border-radius: 8px;
width: 90%;
max-width: 420px;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
}
.rpg-equipment-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid var(--SmartThemeBorderColor);
}
.rpg-equipment-modal-header h3 {
margin: 0;
font-size: 14px;
color: var(--SmartThemeTitleColor);
display: flex;
align-items: center;
gap: 6px;
}
.rpg-equipment-modal-body {
padding: 16px;
}
.rpg-equipment-form-group {
margin-bottom: 12px;
}
.rpg-equipment-form-group label {
display: block;
font-size: 11px;
color: var(--SmartThemeTitleColor);
margin-bottom: 4px;
opacity: 0.8;
}
.rpg-equipment-form-group .rpg-input,
.rpg-equipment-form-group .rpg-select,
.rpg-equipment-form-group .rpg-textarea {
width: 100%;
padding: 6px 8px;
background: var(--SmartThemeInputBg);
border: 1px solid var(--SmartThemeBorderColor);
border-radius: 4px;
color: var(--SmartThemeBodyColor);
font-size: 12px;
}
.rpg-equipment-form-group .rpg-textarea {
resize: vertical;
}
.rpg-eq-stats-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 6px;
}
.rpg-eq-stat-checkbox {
display: flex;
align-items: center;
gap: 3px;
font-size: 10px;
cursor: pointer;
background: rgba(0, 0, 0, 0.2);
padding: 3px 6px;
border-radius: 3px;
}
.rpg-eq-stat-checkbox input[type="checkbox"] {
margin: 0;
cursor: pointer;
}
.rpg-eq-stat-check-label {
color: var(--SmartThemeBodyColor);
opacity: 0.8;
}
.rpg-eq-stat-value-input {
width: 30px;
padding: 1px 2px;
background: var(--SmartThemeInputBg);
border: 1px solid var(--SmartThemeBorderColor);
border-radius: 2px;
color: var(--SmartThemeBodyColor);
font-size: 10px;
text-align: center;
}
.rpg-equipment-modal-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 12px 16px;
border-top: 1px solid var(--SmartThemeBorderColor);
}
/* Equipment bonus display on RPG attributes */
+26
View File
@@ -0,0 +1,26 @@
/* ============================================
RPG COMPANION - CSS VARIABLES
CSS Custom Properties for Theming
============================================ */
/* CSS Variables for Theming - Default uses SillyTavern's variables */
.rpg-panel,
#rpg-thought-panel,
#rpg-thought-icon,
#rpg-alt-present-characters,
.rpg-mobile-toggle {
--rpg-bg: var(--SmartThemeBlurTintColor, rgba(26, 26, 46, 0.9));
--rpg-accent: var(--black30a, rgba(22, 33, 62, 0.9));
--rpg-text: var(--SmartThemeBodyColor, #eaeaea);
--rpg-highlight: var(--SmartThemeQuoteColor, #e94560);
--rpg-border: var(--SmartThemeBorderColor, #4a7ba7);
--rpg-shadow: rgba(0, 0, 0, 0.5);
}
/* CSS Custom Properties for Responsive Scaling */
:root {
--rpg-panel-min-width: 300px;
--rpg-panel-max-width: 500px;
--rpg-modal-min-width: 320px;
--rpg-modal-max-width: 600px;
}
+6 -3
View File
@@ -35,7 +35,10 @@ export const SLOTS_LIST = Object.entries(EQUIPMENT_CATEGORIES).flatMap(([type, d
*/
export function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
return String(text)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
+1 -10
View File
@@ -115,13 +115,4 @@ export function clearDiceRoll() {
updateDiceDisplay();
}
/**
* Adds the Roll Dice quick reply button.
*/
export function addDiceQuickReply() {
// Create quick reply button if Quick Replies exist
if (window.quickReplyApi) {
// Quick Reply API integration would go here
// For now, the dice display in the sidebar serves as the button
}
}
@@ -11,7 +11,6 @@ import { currentEncounter } from '../features/encounterState.js';
import { repairJSON } from '../../utils/jsonRepair.js';
import { isPresentCharactersEnabled } from '../../utils/presentCharacters.js';
import { buildInventorySummary, generateTrackerInstructions, generateTrackerExample } from './promptBuilder.js';
import { applyLocks } from './lockManager.js';
/**
* Gets character information from the current chat
+6 -8
View File
@@ -4,7 +4,8 @@
*/
import { getContext } from '../../../../../../extensions.js';
import { extension_prompt_types, extension_prompt_roles, setExtensionPrompt, eventSource, event_types } from '../../../../../../../script.js';
import { extension_prompt_types, extension_prompt_roles, setExtensionPrompt, event_types } from '../../../../../../../script.js';
import { on as onEvent } from '../../core/events.js';
import {
extensionSettings,
committedTrackerData,
@@ -38,10 +39,6 @@ let currentSuppressionState = false;
// Type imports
/** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */
// Track the latest user message we committed for to prevent duplicate commits
// when GENERATION_STARTED can fire multiple times for the same turn.
let lastCommittedUserMessageSignature = null;
// Store context map for prompt injection (used by event handlers)
let pendingContextMap = new Map();
@@ -892,15 +889,16 @@ ${contextInstructionsText}
export function initHistoryInjectionListeners() {
// Register persistent listeners for prompt injection
// These check pendingContextMap and only inject if there's data
// Use tracked registration (onEvent) so unregisterAllEvents() cleans them up
// Primary: BEFORE_COMBINE for text completion (more reliable - modifies message objects)
eventSource.on(event_types.GENERATE_BEFORE_COMBINE_PROMPTS, onGenerateBeforeCombinePrompts);
onEvent(event_types.GENERATE_BEFORE_COMBINE_PROMPTS, onGenerateBeforeCombinePrompts);
// Fallback: AFTER_COMBINE for text completion (string-based injection)
eventSource.on(event_types.GENERATE_AFTER_COMBINE_PROMPTS, onGenerateAfterCombinePrompts);
onEvent(event_types.GENERATE_AFTER_COMBINE_PROMPTS, onGenerateAfterCombinePrompts);
// Chat completion (OpenAI, etc.)
eventSource.on(event_types.CHAT_COMPLETION_PROMPT_READY, onChatCompletionPromptReady);
onEvent(event_types.CHAT_COMPLETION_PROMPT_READY, onChatCompletionPromptReady);
console.log('[RPG Companion] History injection listeners initialized');
}
+1 -4
View File
@@ -3,8 +3,7 @@
* Helper functions for building JSON format tracker prompts
*/
import { extensionSettings, committedTrackerData } from '../../core/state.js';
import { getContext } from '../../../../../../extensions.js';
import { extensionSettings } from '../../core/state.js';
import { getWeatherKeywordsAsPromptString } from '../ui/weatherEffects.js';
import { i18n } from '../../core/i18n.js';
@@ -39,7 +38,6 @@ function toFieldKey(name) {
* @returns {string} JSON format instruction for user stats
*/
export function buildUserStatsJSONInstruction() {
const userName = getContext().name1;
const trackerConfig = extensionSettings.trackerConfig;
const userStatsConfig = trackerConfig?.userStats;
const enabledStats = userStatsConfig?.customStats?.filter(s => s && s.enabled && s.name) || [];
@@ -184,7 +182,6 @@ export function buildInfoBoxJSONInstruction() {
* @returns {string} JSON format instruction for present characters
*/
export function buildCharactersJSONInstruction() {
const userName = getContext().name1;
const presentCharsConfig = extensionSettings.trackerConfig?.presentCharacters;
const enabledFields = presentCharsConfig?.customFields?.filter(f => f && f.enabled && f.name) || [];
const relationshipsEnabled = presentCharsConfig?.relationships?.enabled !== false;
+187 -77
View File
@@ -9,6 +9,100 @@ import { saveSettings } from '../../core/persistence.js';
import { extractInventory } from './inventoryParser.js';
import { repairJSON, extractJSONFromText } from '../../utils/jsonRepair.js';
/* ===== Pre-compiled regex patterns (module-level constants) ===== */
// Thinking tag removal
const THINKING_TAG_RE = /<think>[\s\S]*?<\/think>/gi;
const THINKING_TAG_ALT_RE = /<thinking>[\s\S]*?<\/thinking>/gi;
// FORMAT: marker removal
const FORMAT_MARKER_RE = /FORMAT:\s*/gi;
// Emoji extraction (separateEmojiFromText)
const EMOJI_RE = /^[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{1F000}-\u{1F02F}\u{1F0A0}-\u{1F0FF}\u{1F100}-\u{1F64F}\u{1F680}-\u{1F6FF}\u{1F910}-\u{1F96B}\u{1F980}-\u{1F9E0}\u{FE00}-\u{FE0F}\u{200D}\u{20E3}]+/u;
// Bracket stripping
const PLACEHOLDER_PATTERN_RE = /\[([A-Za-z\s\/]+)\]/g;
// JSON code block extraction
const JSON_BLOCK_RE = /```json\s*\n([\s\S]*?)```/g;
// XML trackers
const XML_TRACKERS_RE = /<trackers>([\s\S]*?)<\/trackers>/i;
// Code block extraction
const CODE_BLOCK_RE = /```([^`]+)```/g;
// XML text fallback patterns
const XML_STATS_MATCH_RE = /(User )?Stats\s*\n\s*---[\s\S]*?(?=\n\s*\n\s*(Info Box|Present Characters)|$)/i;
const XML_INFOBOX_MATCH_RE = /Info Box\s*\n\s*---[\s\S]*?(?=\n\s*\n\s*Present Characters|$)/i;
const XML_CHARACTERS_MATCH_RE = /Present Characters\s*\n\s*---[\s\S]*$/i;
// Combined code block section patterns
const COMBINED_STATS_RE = /(User )?Stats\s*\n\s*---[\s\S]*?(?=\n\s*\n\s*(Info Box|Present Characters)|$)/i;
const COMBINED_INFOBOX_RE = /Info Box\s*\n\s*---[\s\S]*?(?=\n\s*\n\s*Present Characters|$)/i;
const COMBINED_CHARACTERS_RE = /Present Characters\s*\n\s*---[\s\S]*$/i;
// Section detection patterns
const IS_STATS_HEADER_RE = /Stats\s*\n\s*---/i;
const IS_USER_STATS_HEADER_RE = /User Stats\s*\n\s*---/i;
const IS_PLAYER_STATS_HEADER_RE = /Player Stats\s*\n\s*---/i;
const IS_STATS_HEALTH_RE = /Health:\s*\d+%/i;
const IS_STATS_ENERGY_RE = /Energy:\s*\d+%/i;
const IS_INFOBOX_HEADER_RE = /Info Box\s*\n\s*---/i;
const IS_SCENE_INFO_HEADER_RE = /Scene Info\s*\n\s*---/i;
const IS_INFORMATION_HEADER_RE = /Information\s*\n\s*---/i;
const IS_INFOBOX_DATE_RE = /Date:/i;
const IS_INFOBOX_LOCATION_RE = /Location:/i;
const IS_INFOBOX_TIME_RE = /Time:/i;
const IS_CHARACTERS_HEADER_RE = /Present Characters\s*\n\s*---/i;
const IS_CHARACTERS_ALT_RE = /Characters\s*\n\s*---/i;
const IS_CHARACTERS_THOUGHTS_RE = /Character Thoughts\s*\n\s*---/i;
const IS_CHARACTERS_BULLET_RE = /^-\s+\w+/m;
const IS_CHARACTERS_DETAILS_RE = /Details:/i;
// Debug pattern checks
const DEBUG_STATS_RE = /Stats\s*\n\s*---/i;
const DEBUG_INFOBOX_RE = /Info Box\s*\n\s*---/i;
const DEBUG_CHARACTERS_RE = /Present Characters\s*\n\s*---/i;
// Final fallback fenced regex
const FENCED_FALLBACK_RE = /```(?:json)?\s*\n?([\s\S]*?)```/gi;
// parseUserStats patterns
const RPG_STR_RE = /STR:\s*(\d+)/i;
const RPG_DEX_RE = /DEX:\s*(\d+)/i;
const RPG_CON_RE = /CON:\s*(\d+)/i;
const RPG_INT_RE = /INT:\s*(\d+)/i;
const RPG_WIS_RE = /WIS:\s*(\d+)/i;
const RPG_CHA_RE = /CHA:\s*(\d+)/i;
const RPG_LVL_RE = /LVL:\s*(\d+)/i;
const STATUS_MATCH_RE = /Status:\s*(.+)/i;
const SKILLS_MATCH_RE = /Skills:\s*(.+)/i;
const INVENTORY_MATCH_RE = /Inventory:\s*(.+)/i;
const MAIN_QUEST_MATCH_RE = /Main Quests?:\s*(.+)/i;
const OPTIONAL_QUESTS_MATCH_RE = /Optional Quests:\s*(.+)/i;
// Section detection helpers
const STATS_SECTION_RE = /Stats\s*\n\s*---/i;
const INFOBOX_SECTION_RE = /Info Box\s*\n\s*---/i;
const CHARACTERS_SECTION_RE = /Present Characters\s*\n\s*---/i;
/* ===== Format detection cache ===== */
// Cache the last detected format to skip unnecessary checks on subsequent calls.
// The cache is invalidated when the page reloads (module re-initializes).
let lastDetectedFormat = null; // 'json', 'json_block', 'xml', 'text', null
const FORMAT_CACHE_HIT_THRESHOLD = 3; // Minimum consecutive hits before trusting cache
let formatCacheHits = 0;
/**
* Clear the format detection cache. Call this when the AI model or prompt changes.
*/
export function clearFormatCache() {
lastDetectedFormat = null;
formatCacheHits = 0;
}
/**
* Unwraps common envelope keys models may use around tracker payloads.
* Keeps extraction resilient when output is nested under wrappers like "trackers".
@@ -52,7 +146,7 @@ function unwrapTrackerEnvelope(payload) {
* @returns {string} snake_case key from the base name only
*/
function toFieldKey(name) {
const baseName = name.replace(/\s*\(.*\)\s*$/, '').trim();
const baseName = name.replace(/\s*\([^)]*\)\s*$/, '').trim();
return baseName
.toLowerCase()
.replace(/[^\p{L}\p{N}]+/gu, '_')
@@ -70,10 +164,7 @@ function separateEmojiFromText(str) {
str = str.trim();
// Regex to match emoji at the start (handles most emoji including compound ones)
// This matches emoji sequences including skin tones, gender modifiers, etc.
const emojiRegex = /^[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{1F000}-\u{1F02F}\u{1F0A0}-\u{1F0FF}\u{1F100}-\u{1F64F}\u{1F680}-\u{1F6FF}\u{1F910}-\u{1F96B}\u{1F980}-\u{1F9E0}\u{FE00}-\u{FE0F}\u{200D}\u{20E3}]+/u;
const emojiMatch = str.match(emojiRegex);
const emojiMatch = str.match(EMOJI_RE);
if (emojiMatch) {
const emoji = emojiMatch[0];
@@ -121,13 +212,8 @@ function stripBrackets(text) {
text = text.substring(1, text.length - 1).trim();
}
// Remove placeholder text patterns like [Location], [Mood Emoji], [Name], etc.
// Pattern matches: [anything with letters/spaces inside]
// This preserves actual content while removing template placeholders
const placeholderPattern = /\[([A-Za-z\s\/]+)\]/g;
// Check if a bracketed text looks like a placeholder vs real content
const isPlaceholder = (match, content) => {
// Replace placeholders with empty string, keep real content
text = text.replace(PLACEHOLDER_PATTERN_RE, (match, content) => {
// Common placeholder words to detect
const placeholderKeywords = [
'location', 'mood', 'emoji', 'name', 'description', 'placeholder',
@@ -141,23 +227,15 @@ function stripBrackets(text) {
// If it contains common placeholder keywords, it's likely a placeholder
if (placeholderKeywords.some(keyword => lowerContent.includes(keyword))) {
return true;
return ''; // Remove placeholder
}
// If it's a short generic phrase (1-3 words) with only letters/spaces, might be placeholder
const wordCount = content.trim().split(/\s+/).length;
if (wordCount <= 3 && /^[A-Za-z\s\/]+$/.test(content)) {
return true;
return '';
}
return false;
};
// Replace placeholders with empty string, keep real content
text = text.replace(placeholderPattern, (match, content) => {
if (isPlaceholder(match, content)) {
return ''; // Remove placeholder
}
return match; // Keep real bracketed content
});
@@ -193,10 +271,11 @@ function debugLog(message, data = null) {
* @param {string} responseText - The raw AI response text
* @param {Object} [options] - Parser behavior options
* @param {boolean} [options.suppressNoDataError=false] - Avoid console error when no tracker data is found
* @param {boolean} [options.forceFormat=null] - Force a specific format check (bypasses cache)
* @returns {{userStats: string|null, infoBox: string|null, characterThoughts: string|null}} Parsed tracker data
*/
export function parseResponse(responseText, options = {}) {
const { suppressNoDataError = false } = options;
const { suppressNoDataError = false, forceFormat = null } = options;
const result = {
userStats: null,
infoBox: null,
@@ -210,14 +289,22 @@ export function parseResponse(responseText, options = {}) {
// Remove content inside thinking tags first (model's internal reasoning)
// This prevents parsing code blocks from the model's thinking process
let cleanedResponse = responseText.replace(/<think>[\s\S]*?<\/think>/gi, '');
cleanedResponse = cleanedResponse.replace(/<thinking>[\s\S]*?<\/thinking>/gi, '');
let cleanedResponse = responseText.replace(THINKING_TAG_RE, '');
cleanedResponse = cleanedResponse.replace(THINKING_TAG_ALT_RE, '');
debugLog('[RPG Parser] Removed thinking tags, new length:', cleanedResponse.length + ' chars');
// Remove "FORMAT:" markers that the model might accidentally output
cleanedResponse = cleanedResponse.replace(/FORMAT:\s*/gi, '');
cleanedResponse = cleanedResponse.replace(FORMAT_MARKER_RE, '');
debugLog('[RPG Parser] Removed FORMAT: markers, new length:', cleanedResponse.length + ' chars');
// Format cache: if we've seen the same format multiple times, try that path first
const cachedFormat = forceFormat ?? lastDetectedFormat;
let detectedFormat = null;
if (cachedFormat && formatCacheHits >= FORMAT_CACHE_HIT_THRESHOLD) {
debugLog('[RPG Parser] Using cached format:', cachedFormat);
}
// First, try to extract raw JSON objects (v3 format)
// Note: Prompts now instruct models to use ```json``` code blocks, but we extract
// from any JSON found using brace-matching for maximum compatibility
@@ -267,6 +354,15 @@ export function parseResponse(responseText, options = {}) {
// console.log(`[RPG Parser] ✓ Found ${extractedObjects.length} raw JSON objects (v3 format)`);
debugLog(`[RPG Parser] ✓ Found ${extractedObjects.length} raw JSON objects (v3 format)`);
// Update format cache
detectedFormat = 'json';
if (cachedFormat === 'json') {
formatCacheHits++;
} else {
lastDetectedFormat = 'json';
formatCacheHits = 1;
}
// First, try to parse as unified JSON structure (new v3.1 format)
// Look through all extracted objects for unified structure
let foundUnified = false;
@@ -377,13 +473,21 @@ export function parseResponse(responseText, options = {}) {
// Check for JSON code blocks (legacy v3 format with ```json fences)
// Look for ```json code blocks which indicate JSON format
const jsonBlockRegex = /```json\s*\n([\s\S]*?)```/g;
const jsonMatches = [...cleanedResponse.matchAll(jsonBlockRegex)];
const jsonMatches = [...cleanedResponse.matchAll(JSON_BLOCK_RE)];
if (jsonMatches.length > 0) {
// console.log('[RPG Parser] ✓ Found', jsonMatches.length, 'JSON code blocks (v3 format with fences)');
debugLog('[RPG Parser] ✓ Found JSON code blocks (v3 format), parsing as JSON');
// Update format cache
detectedFormat = 'json_block';
if (cachedFormat === 'json_block') {
formatCacheHits++;
} else if (!detectedFormat) {
lastDetectedFormat = 'json_block';
formatCacheHits = 1;
}
for (let idx = 0; idx < jsonMatches.length; idx++) {
const match = jsonMatches[idx];
const jsonContent = match[1].trim();
@@ -447,13 +551,22 @@ export function parseResponse(responseText, options = {}) {
}
// Check if response uses XML <trackers> tags (hybrid format)
const xmlMatch = cleanedResponse.match(/<trackers>([\s\S]*?)<\/trackers>/i);
const xmlMatch = cleanedResponse.match(XML_TRACKERS_RE);
if (xmlMatch) {
debugLog('[RPG Parser] ✓ Found XML <trackers> tags, using XML parser');
const trackersContent = xmlMatch[1].trim();
// Update format cache
detectedFormat = 'xml';
if (cachedFormat === 'xml') {
formatCacheHits++;
} else if (!lastDetectedFormat) {
lastDetectedFormat = 'xml';
formatCacheHits = 1;
}
// Try to parse JSON blocks within XML first
const xmlJsonMatches = [...trackersContent.matchAll(jsonBlockRegex)];
const xmlJsonMatches = [...trackersContent.matchAll(JSON_BLOCK_RE)];
if (xmlJsonMatches.length > 0) {
debugLog('[RPG Parser] Found JSON blocks within XML tags');
for (const match of xmlJsonMatches) {
@@ -475,19 +588,19 @@ export function parseResponse(responseText, options = {}) {
}
} else {
// Fallback to text extraction from XML content (legacy v2 text format)
const statsMatch = trackersContent.match(/(User )?Stats\s*\n\s*---[\s\S]*?(?=\n\s*\n\s*(Info Box|Present Characters)|$)/i);
const statsMatch = trackersContent.match(XML_STATS_MATCH_RE);
if (statsMatch) {
result.userStats = stripBrackets(statsMatch[0].trim());
debugLog('[RPG Parser] ✓ Extracted Stats from XML (text format)');
}
const infoBoxMatch = trackersContent.match(/Info Box\s*\n\s*---[\s\S]*?(?=\n\s*\n\s*Present Characters|$)/i);
const infoBoxMatch = trackersContent.match(XML_INFOBOX_MATCH_RE);
if (infoBoxMatch) {
result.infoBox = stripBrackets(infoBoxMatch[0].trim());
debugLog('[RPG Parser] ✓ Extracted Info Box from XML (text format)');
}
const charactersMatch = trackersContent.match(/Present Characters\s*\n\s*---[\s\S]*$/i);
const charactersMatch = trackersContent.match(XML_CHARACTERS_MATCH_RE);
if (charactersMatch) {
result.characterThoughts = stripBrackets(charactersMatch[0].trim());
debugLog('[RPG Parser] ✓ Extracted Present Characters from XML (text format)');
@@ -502,8 +615,7 @@ export function parseResponse(responseText, options = {}) {
debugLog('[RPG Parser] No XML tags found, using code block parser');
// Extract code blocks
const codeBlockRegex = /```([^`]+)```/g;
const matches = [...cleanedResponse.matchAll(codeBlockRegex)];
const matches = [...cleanedResponse.matchAll(CODE_BLOCK_RE)];
debugLog('[RPG Parser] Found', matches.length + ' code blocks');
@@ -516,8 +628,8 @@ export function parseResponse(responseText, options = {}) {
// Check if this is a combined code block with multiple sections
const hasMultipleSections = (
content.match(/Stats\s*\n\s*---/i) &&
(content.match(/Info Box\s*\n\s*---/i) || content.match(/Present Characters\s*\n\s*---/i))
content.match(IS_STATS_HEADER_RE) &&
(content.match(IS_INFOBOX_HEADER_RE) || content.match(IS_CHARACTERS_HEADER_RE))
);
if (hasMultipleSections) {
@@ -525,21 +637,21 @@ export function parseResponse(responseText, options = {}) {
debugLog('[RPG Parser] ✓ Found combined code block with multiple sections');
// Extract User Stats section
const statsMatch = content.match(/(User )?Stats\s*\n\s*---[\s\S]*?(?=\n\s*\n\s*(Info Box|Present Characters)|$)/i);
const statsMatch = content.match(COMBINED_STATS_RE);
if (statsMatch && !result.userStats) {
result.userStats = stripBrackets(statsMatch[0].trim());
debugLog('[RPG Parser] ✓ Extracted Stats from combined block');
}
// Extract Info Box section
const infoBoxMatch = content.match(/Info Box\s*\n\s*---[\s\S]*?(?=\n\s*\n\s*Present Characters|$)/i);
const infoBoxMatch = content.match(COMBINED_INFOBOX_RE);
if (infoBoxMatch && !result.infoBox) {
result.infoBox = stripBrackets(infoBoxMatch[0].trim());
debugLog('[RPG Parser] ✓ Extracted Info Box from combined block');
}
// Extract Present Characters section
const charactersMatch = content.match(/Present Characters\s*\n\s*---[\s\S]*$/i);
const charactersMatch = content.match(COMBINED_CHARACTERS_RE);
if (charactersMatch && !result.characterThoughts) {
result.characterThoughts = stripBrackets(charactersMatch[0].trim());
debugLog('[RPG Parser] ✓ Extracted Present Characters from combined block');
@@ -548,27 +660,27 @@ export function parseResponse(responseText, options = {}) {
// Handle separate code blocks with flexible pattern matching
// Match Stats section - flexible patterns
const isStats =
content.match(/Stats\s*\n\s*---/i) ||
content.match(/User Stats\s*\n\s*---/i) ||
content.match(/Player Stats\s*\n\s*---/i) ||
content.match(IS_STATS_HEADER_RE) ||
content.match(IS_USER_STATS_HEADER_RE) ||
content.match(IS_PLAYER_STATS_HEADER_RE) ||
// Fallback: look for stat keywords without strict header
(content.match(/Health:\s*\d+%/i) && content.match(/Energy:\s*\d+%/i));
(content.match(IS_STATS_HEALTH_RE) && content.match(IS_STATS_ENERGY_RE));
// Match Info Box section - flexible patterns
const isInfoBox =
content.match(/Info Box\s*\n\s*---/i) ||
content.match(/Scene Info\s*\n\s*---/i) ||
content.match(/Information\s*\n\s*---/i) ||
content.match(IS_INFOBOX_HEADER_RE) ||
content.match(IS_SCENE_INFO_HEADER_RE) ||
content.match(IS_INFORMATION_HEADER_RE) ||
// Fallback: look for info box keywords
(content.match(/Date:/i) && content.match(/Location:/i) && content.match(/Time:/i));
(content.match(IS_INFOBOX_DATE_RE) && content.match(IS_INFOBOX_LOCATION_RE) && content.match(IS_INFOBOX_TIME_RE));
// Match Present Characters section - flexible patterns
const isCharacters =
content.match(/Present Characters\s*\n\s*---/i) ||
content.match(/Characters\s*\n\s*---/i) ||
content.match(/Character Thoughts\s*\n\s*---/i) ||
content.match(IS_CHARACTERS_HEADER_RE) ||
content.match(IS_CHARACTERS_ALT_RE) ||
content.match(IS_CHARACTERS_THOUGHTS_RE) ||
// Fallback: look for new multi-line format patterns
(content.match(/^-\s+\w+/m) && content.match(/Details:/i));
(content.match(IS_CHARACTERS_BULLET_RE) && content.match(IS_CHARACTERS_DETAILS_RE));
if (isStats && !result.userStats) {
result.userStats = stripBrackets(content);
@@ -582,12 +694,12 @@ export function parseResponse(responseText, options = {}) {
debugLog('[RPG Parser] Full content:', content);
} else {
debugLog('[RPG Parser] ✗ No match - checking patterns:');
debugLog('[RPG Parser] - Has "Stats\\n---"?', !!content.match(/Stats\s*\n\s*---/i));
debugLog('[RPG Parser] - Has stat keywords?', !!(content.match(/Health:\s*\d+%/i) && content.match(/Energy:\s*\d+%/i)));
debugLog('[RPG Parser] - Has "Info Box\\n---"?', !!content.match(/Info Box\s*\n\s*---/i));
debugLog('[RPG Parser] - Has info keywords?', !!(content.match(/Date:/i) && content.match(/Location:/i)));
debugLog('[RPG Parser] - Has "Present Characters\\n---"?', !!content.match(/Present Characters\s*\n\s*---/i));
debugLog('[RPG Parser] - Has new format ("- Name" + "Details:")?', !!(content.match(/^-\s+\w+/m) && content.match(/Details:/i)));
debugLog('[RPG Parser] - Has "Stats\\n---"?', !!content.match(DEBUG_STATS_RE));
debugLog('[RPG Parser] - Has stat keywords?', !!(content.match(IS_STATS_HEALTH_RE) && content.match(IS_STATS_ENERGY_RE)));
debugLog('[RPG Parser] - Has "Info Box\\n---"?', !!content.match(DEBUG_INFOBOX_RE));
debugLog('[RPG Parser] - Has info keywords?', !!(content.match(IS_INFOBOX_DATE_RE) && content.match(IS_INFOBOX_LOCATION_RE)));
debugLog('[RPG Parser] - Has "Present Characters\\n---"?', !!content.match(DEBUG_CHARACTERS_RE));
debugLog('[RPG Parser] - Has new format ("- Name" + "Details:")?', !!(content.match(IS_CHARACTERS_BULLET_RE) && content.match(IS_CHARACTERS_DETAILS_RE)));
}
}
}
@@ -601,8 +713,7 @@ export function parseResponse(responseText, options = {}) {
// Final fallback: try to extract tracker JSON from any fenced block content
// This catches responses where JSON is embedded in non-standard markdown structure.
if (!result.userStats && !result.infoBox && !result.characterThoughts) {
const fencedRegex = /```(?:json)?\s*\n?([\s\S]*?)```/gi;
const fencedMatches = [...cleanedResponse.matchAll(fencedRegex)];
const fencedMatches = [...cleanedResponse.matchAll(FENCED_FALLBACK_RE)];
for (const match of fencedMatches) {
const fencedContent = (match[1] || '').trim();
@@ -806,13 +917,13 @@ export function parseUserStats(statsText) {
// Parse RPG attributes if enabled
if (trackerConfig?.userStats?.showRPGAttributes) {
const strMatch = statsText.match(/STR:\s*(\d+)/i);
const dexMatch = statsText.match(/DEX:\s*(\d+)/i);
const conMatch = statsText.match(/CON:\s*(\d+)/i);
const intMatch = statsText.match(/INT:\s*(\d+)/i);
const wisMatch = statsText.match(/WIS:\s*(\d+)/i);
const chaMatch = statsText.match(/CHA:\s*(\d+)/i);
const lvlMatch = statsText.match(/LVL:\s*(\d+)/i);
const strMatch = statsText.match(RPG_STR_RE);
const dexMatch = statsText.match(RPG_DEX_RE);
const conMatch = statsText.match(RPG_CON_RE);
const intMatch = statsText.match(RPG_INT_RE);
const wisMatch = statsText.match(RPG_WIS_RE);
const chaMatch = statsText.match(RPG_CHA_RE);
const lvlMatch = statsText.match(RPG_LVL_RE);
if (strMatch) extensionSettings.classicStats.str = parseInt(strMatch[1]);
if (dexMatch) extensionSettings.classicStats.dex = parseInt(dexMatch[1]);
@@ -832,7 +943,7 @@ export function parseUserStats(statsText) {
const customFields = statusConfig.customFields || [];
// Try Status: format
const statusMatch = statsText.match(/Status:\s*(.+)/i);
const statusMatch = statsText.match(STATUS_MATCH_RE);
if (statusMatch) {
const statusContent = statusMatch[1].trim();
@@ -883,7 +994,7 @@ export function parseUserStats(statsText) {
// Parse skills section if enabled
const skillsConfig = trackerConfig?.userStats?.skillsSection;
if (skillsConfig?.enabled) {
const skillsMatch = statsText.match(/Skills:\s*(.+)/i);
const skillsMatch = statsText.match(SKILLS_MATCH_RE);
if (skillsMatch) {
extensionSettings.userStats.skills = skillsMatch[1].trim();
debugLog('[RPG Parser] Skills extracted:', skillsMatch[1].trim());
@@ -901,7 +1012,7 @@ export function parseUserStats(statsText) {
}
} else {
// Legacy v1 parsing for backward compatibility
const inventoryMatch = statsText.match(/Inventory:\s*(.+)/i);
const inventoryMatch = statsText.match(INVENTORY_MATCH_RE);
if (inventoryMatch) {
extensionSettings.userStats.inventory = inventoryMatch[1].trim();
debugLog('[RPG Parser] Inventory v1 extracted:', inventoryMatch[1].trim());
@@ -911,13 +1022,13 @@ export function parseUserStats(statsText) {
}
// Extract quests
const mainQuestMatch = statsText.match(/Main Quests?:\s*(.+)/i);
const mainQuestMatch = statsText.match(MAIN_QUEST_MATCH_RE);
if (mainQuestMatch) {
extensionSettings.quests.main = mainQuestMatch[1].trim();
debugLog('[RPG Parser] Main quests extracted:', mainQuestMatch[1].trim());
}
const optionalQuestsMatch = statsText.match(/Optional Quests:\s*(.+)/i);
const optionalQuestsMatch = statsText.match(OPTIONAL_QUESTS_MATCH_RE);
if (optionalQuestsMatch) {
const questsText = optionalQuestsMatch[1].trim();
if (questsText && questsText !== 'None') {
@@ -960,8 +1071,7 @@ export function parseUserStats(statsText) {
* @returns {Array<string>} Array of code block contents
*/
export function extractCodeBlocks(text) {
const codeBlockRegex = /```([^`]+)```/g;
const matches = [...text.matchAll(codeBlockRegex)];
const matches = [...text.matchAll(CODE_BLOCK_RE)];
return matches.map(match => match[1].trim());
}
@@ -971,7 +1081,7 @@ export function extractCodeBlocks(text) {
* @returns {boolean} True if this is a stats section
*/
export function isStatsSection(content) {
return content.match(/Stats\s*\n\s*---/i) !== null;
return content.match(STATS_SECTION_RE) !== null;
}
/**
@@ -980,7 +1090,7 @@ export function isStatsSection(content) {
* @returns {boolean} True if this is an info box section
*/
export function isInfoBoxSection(content) {
return content.match(/Info Box\s*\n\s*---/i) !== null;
return content.match(INFOBOX_SECTION_RE) !== null;
}
/**
@@ -989,5 +1099,5 @@ export function isInfoBoxSection(content) {
* @returns {boolean} True if this is a character thoughts section
*/
export function isCharacterThoughtsSection(content) {
return content.match(/Present Characters\s*\n\s*---/i) !== null || content.includes(" | ");
return content.match(CHARACTERS_SECTION_RE) !== null || content.includes(" | ");
}
+78 -65
View File
@@ -4,8 +4,8 @@
*/
import { getContext } from '../../../../../../extensions.js';
import { chat, getCurrentChatDetails, characters, this_chid } from '../../../../../../../script.js';
import { selected_group, getGroupMembers, getGroupChat, groups } from '../../../../../../group-chats.js';
import { chat, characters, this_chid } from '../../../../../../../script.js';
import { selected_group, getGroupMembers, groups } from '../../../../../../group-chats.js';
import { extensionSettings, committedTrackerData, FEATURE_FLAGS } from '../../core/state.js';
import {
buildUserStatsJSONInstruction,
@@ -78,85 +78,99 @@ async function getCharacterCardsInfo() {
// Narrator mode: use character card as narrator context, infer characters from story context
if (extensionSettings.narratorMode) {
if (this_chid !== undefined && characters && characters[this_chid]) {
const character = characters[this_chid];
characterInfo += 'You are acting as the narrator for this story. The narrator card provides context for the story tone and style:\n\n';
characterInfo += `<narrator>\n`;
if (character.description) {
characterInfo += `${character.description}\n`;
}
if (character.personality) {
characterInfo += `${character.personality}\n`;
}
characterInfo += `</narrator>\n\n`;
// Use custom narrator prompt if available, otherwise use default
const narratorPrompt = extensionSettings.customNarratorPrompt || DEFAULT_NARRATOR_PROMPT;
characterInfo += narratorPrompt + '\n\n';
characterInfo = buildNarratorCardInfo(characters[this_chid]);
}
return characterInfo;
}
// Check if in group chat
if (selected_group) {
// Find the current group directly from the groups array
const group = groups.find(g => g.id === selected_group);
const groupMembers = getGroupMembers(selected_group);
if (groupMembers && groupMembers.length > 0) {
characterInfo += 'Characters in this roleplay:\n\n';
// Filter out disabled (muted) members
const disabledMembers = group?.disabled_members || [];
// console.log('[RPG Companion] 🔍 Group ID:', selected_group, '| Disabled members:', disabledMembers);
let characterIndex = 0;
groupMembers.forEach((member) => {
if (!member || !member.name) return;
// Skip muted characters - check against avatar filename
if (member.avatar && disabledMembers.includes(member.avatar)) {
// console.log(`[RPG Companion] ❌ Skipping muted: ${member.name} (${member.avatar})`);
return;
}
characterIndex++;
characterInfo += `<character${characterIndex}="${member.name}">\n`;
if (member.description) {
characterInfo += `${member.description}\n`;
}
if (member.personality) {
characterInfo += `${member.personality}\n`;
}
characterInfo += `</character${characterIndex}>\n\n`;
});
const disabledMembers = groups.find(g => g.id === selected_group)?.disabled_members || [];
characterInfo = buildGroupCardInfo(groupMembers, disabledMembers);
}
} else if (this_chid !== undefined && characters && characters[this_chid]) {
// Single character chat
const character = characters[this_chid];
characterInfo += 'Character in this roleplay:\n\n';
characterInfo += `<character="${character.name}">\n`;
if (character.description) {
characterInfo += `${character.description}\n`;
}
if (character.personality) {
characterInfo += `${character.personality}\n`;
}
characterInfo += `</character>\n\n`;
characterInfo = buildSingleCardInfo(characters[this_chid]);
}
return characterInfo;
}
/**
* Builds narrator mode character card information.
* @param {Object} character - Character card data
* @returns {string} Formatted narrator info
*/
function buildNarratorCardInfo(character) {
let info = '';
info += 'You are acting as the narrator for this story. The narrator card provides context for the story tone and style:\n\n';
info += `<narrator>\n`;
info += appendCharacterFields(character);
info += `</narrator>\n\n`;
info += (extensionSettings.customNarratorPrompt || DEFAULT_NARRATOR_PROMPT) + '\n\n';
return info;
}
/**
* Builds character card information for a group chat.
* @param {Array} groupMembers - Array of group member objects
* @param {Array} disabledMembers - Array of disabled (muted) member avatar filenames
* @returns {string} Formatted group character info
*/
function buildGroupCardInfo(groupMembers, disabledMembers) {
let info = 'Characters in this roleplay:\n\n';
let characterIndex = 0;
groupMembers.forEach((member) => {
if (!member || !member.name) return;
// Skip muted characters
if (member.avatar && disabledMembers.includes(member.avatar)) {
return;
}
characterIndex++;
info += `<character${characterIndex}="${member.name}">\n`;
info += appendCharacterFields(member);
info += `</character${characterIndex}>\n\n`;
});
return info;
}
/**
* Builds character card information for a single character chat.
* @param {Object} character - Character card data
* @returns {string} Formatted single character info
*/
function buildSingleCardInfo(character) {
let info = 'Character in this roleplay:\n\n';
info += `<character="${character.name}">\n`;
info += appendCharacterFields(character);
info += `</character>\n\n`;
return info;
}
/**
* Appends description and personality fields from a character object.
* Shared helper used by narrator, group, and single character info builders.
* @param {Object} character - Character object with optional description/personality fields
* @returns {string} Appended fields (empty string if neither present)
*/
function appendCharacterFields(character) {
let fields = '';
if (character.description) {
fields += `${character.description}\n`;
}
if (character.personality) {
fields += `${character.personality}\n`;
}
return fields;
}
/**
* Builds a formatted inventory summary for AI context injection.
* Converts v2 inventory structure to multi-line plaintext format.
@@ -1232,7 +1246,6 @@ export async function generateSeparateUpdatePrompt() {
// /hide command automatically handles checkpoint filtering
// Add chat history as separate user/assistant messages with per-message historical context
const recentMessages = chat.slice(-depth);
const startIndex = chat.length - depth;
const position = historyPersistence?.injectionPosition || 'assistant_message_end';
// Build a map of which messages should get context based on position setting
+1 -1
View File
@@ -26,7 +26,7 @@ export function evaluateSuppression(extensionSettings, context, data) {
let instructContent = '';
if (instructObj) {
if (typeof instructObj === 'object') {
instructContent = String(instructObj.value || instructObj || '');
instructContent = String(instructObj.value || '');
} else {
instructContent = String(instructObj);
}
+23 -1
View File
@@ -4,7 +4,7 @@
*/
import { getContext } from '../../../../../../extensions.js';
import { chat, chat_metadata, user_avatar, setExtensionPrompt, extension_prompt_types } from '../../../../../../../script.js';
import { chat, chat_metadata, user_avatar, setExtensionPrompt, extension_prompt_types, updateMessageBlock } from '../../../../../../../script.js';
// Core modules
import {
@@ -53,6 +53,7 @@ import { renderInventory } from '../rendering/inventory.js';
import { renderEquipment } from '../rendering/equipment.js';
import { renderQuests } from '../rendering/quests.js';
import { renderMusicPlayer } from '../rendering/musicPlayer.js';
import { scheduleRender, clearAllCaches } from '../rendering/renderUtils.js';
// Utils
import { getSafeThumbnailUrl } from '../../utils/avatars.js';
@@ -323,7 +324,28 @@ function restoreOrRepairLatestTrackerState() {
return restored;
}
/**
* Renders all RPG state panels.
* Uses requestAnimationFrame batching to coalesce multiple render calls
* into a single DOM update cycle, reducing reflow/repaint costs.
*/
function rerenderRpgState() {
scheduleRender(renderUserStats);
scheduleRender(renderInfoBox);
scheduleRender(renderThoughts);
scheduleRender(renderInventory);
scheduleRender(renderEquipment);
scheduleRender(renderQuests);
scheduleRender(() => renderMusicPlayer($musicPlayerContainer[0]));
scheduleRender(updateFabWidgets);
scheduleRender(updateStripWidgets);
}
/**
* Immediately renders all RPG state panels (synchronous).
* Use this when you need the DOM to be updated before the next line of code.
*/
function rerenderRpgStateSync() {
renderUserStats();
renderInfoBox();
renderThoughts();
@@ -5,7 +5,6 @@
import { extensionSettings, lastGeneratedData, committedTrackerData } from '../../core/state.js';
import { saveSettings, saveChatData, updateMessageSwipeData } from '../../core/persistence.js';
import { buildInventorySummary } from '../generation/promptBuilder.js';
import { buildUserStatsText } from '../rendering/userStats.js';
import { renderInventory, getLocationId } from '../rendering/inventory.js';
import { parseItems, serializeItems } from '../../utils/itemParser.js';
+21 -1
View File
@@ -6,6 +6,7 @@
import { extensionSettings, $equipmentContainer } from '../../core/state.js';
import { i18n } from '../../core/i18n.js';
import { EQUIPMENT_CATEGORIES, SLOTS_LIST, escapeHtml } from '../equipment/constants.js';
import { updateIfChanged } from './renderUtils.js';
/**
* Renders a single equipment slot
@@ -145,12 +146,31 @@ function generateEquipmentHTML() {
* Gets data from state/settings and updates DOM directly.
*/
export function renderEquipment() {
// Ensure showEquipment defaults to true if undefined
if (extensionSettings.showEquipment === undefined) {
extensionSettings.showEquipment = true;
}
if (!$equipmentContainer || !extensionSettings.showEquipment) {
return;
}
// Ensure equipment data structure exists (defensive initialization)
if (!extensionSettings.userStats?.equipment) {
extensionSettings.userStats = extensionSettings.userStats || {};
extensionSettings.userStats.equipment = {
items: [],
slots: {
helmet: null, ring1: null, ring2: null, ring3: null, ring4: null,
ring5: null, ring6: null, ring7: null, ring8: null, ring9: null, ring10: null,
necklace: null, bodyArmor: null, pants: null, shoes: null, gloves: null,
accessory1: null, accessory2: null, accessory3: null
}
};
}
const html = generateEquipmentHTML();
$equipmentContainer.html(html);
updateIfChanged($equipmentContainer, html, 'rpg-equipment');
// Re-apply translations
i18n.applyTranslations($equipmentContainer[0]);
+19 -15
View File
@@ -10,11 +10,12 @@ import {
committedTrackerData,
$infoBoxContainer
} from '../../core/state.js';
import { saveChatData, setMessageSwipeTrackerField } from '../../core/persistence.js';
import { saveChatData, saveSettings, setMessageSwipeTrackerField } from '../../core/persistence.js';
import { i18n } from '../../core/i18n.js';
import { isItemLocked } from '../generation/lockManager.js';
import { repairJSON } from '../../utils/jsonRepair.js';
import { updateFabWidgets } from '../ui/mobile.js';
import { updateIfChanged } from './renderUtils.js';
/**
* Helper to generate lock icon HTML if setting is enabled
@@ -575,23 +576,25 @@ export function renderInfoBox() {
// Close the scrollable content wrapper
html += '</div>';
$infoBoxContainer.html(html);
const domUpdated = updateIfChanged($infoBoxContainer, html, 'rpg-info-box');
// Add dynamic text scaling for location field
const updateLocationTextSize = ($element) => {
const text = $element.text();
const charCount = text.length;
$element.css('--char-count', Math.min(charCount, 100));
};
// Only re-bind event handlers and apply dynamic styling if DOM was updated
if (domUpdated) {
// Add dynamic text scaling for location field
const updateLocationTextSize = ($element) => {
const text = $element.text();
const charCount = text.length;
$element.css('--char-count', Math.min(charCount, 100));
};
// Initial size update for location
const $locationText = $infoBoxContainer.find('[data-field="location"]');
if ($locationText.length) {
updateLocationTextSize($locationText);
}
// Initial size update for location
const $locationText = $infoBoxContainer.find('[data-field="location"]');
if ($locationText.length) {
updateLocationTextSize($locationText);
}
// Add event handlers for editable Info Box fields
$infoBoxContainer.find('.rpg-editable').on('blur', function () {
// Add event handlers for editable Info Box fields
$infoBoxContainer.find('.rpg-editable').on('blur', function () {
const $this = $(this);
const field = $this.data('field');
const value = $this.text().trim();
@@ -659,6 +662,7 @@ export function renderInfoBox() {
saveSettings();
});
});
} // end if (domUpdated)
// Remove updating class after animation
if (extensionSettings.enableAnimations) {
+13 -6
View File
@@ -10,6 +10,7 @@ import { updateInventoryItem } from '../interaction/inventoryEdit.js';
import { parseItems } from '../../utils/itemParser.js';
import { isItemLocked, setItemLock } from '../generation/lockManager.js';
import { i18n } from '../../core/i18n.js';
import { updateIfChanged } from './renderUtils.js';
// Type imports
/** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */
@@ -594,13 +595,15 @@ export function renderInventory() {
// Generate HTML and update DOM
const html = generateInventoryHTML(inventory, options);
$inventoryContainer.html(html);
const domUpdated = updateIfChanged($inventoryContainer, html, 'rpg-inventory');
// Restore form states after re-rendering (fixes Bug #1)
restoreFormStates();
// Event listener for editing item names (mobile-friendly contenteditable)
$inventoryContainer.find('.rpg-item-name.rpg-editable').on('blur', function() {
// Only re-bind event handlers if DOM was actually updated
if (domUpdated) {
// Event listener for editing item names (mobile-friendly contenteditable)
$inventoryContainer.find('.rpg-item-name.rpg-editable').on('blur', function() {
const field = $(this).data('field');
const index = parseInt($(this).data('index'));
const location = $(this).data('location');
@@ -632,6 +635,7 @@ export function renderInventory() {
// Save settings
saveSettings();
});
} // end if (domUpdated)
}
/**
@@ -641,7 +645,10 @@ export function renderInventory() {
*/
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
return String(text)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
+13 -6
View File
@@ -7,6 +7,7 @@ import { extensionSettings, $questsContainer, committedTrackerData, lastGenerate
import { saveSettings, saveChatData } from '../../core/persistence.js';
import { isItemLocked, setItemLock } from '../generation/lockManager.js';
import { i18n } from '../../core/i18n.js';
import { updateIfChanged } from './renderUtils.js';
/**
* Syncs the current extensionSettings.quests to committedTrackerData.userStats
@@ -56,9 +57,13 @@ function getLockIconHtml(tracker, path) {
* @returns {string} Escaped HTML
*/
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
if (!text) return '';
return String(text)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
/**
@@ -247,10 +252,12 @@ export function renderQuests() {
}
html += '</div></div>';
$questsContainer.html(html);
const domUpdated = updateIfChanged($questsContainer, html, 'rpg-quests');
// Attach event handlers
attachQuestEventHandlers();
// Attach event handlers only if DOM was updated
if (domUpdated) {
attachQuestEventHandlers();
}
}
/**
+148
View File
@@ -0,0 +1,148 @@
/**
* Rendering Utilities Module
* Provides performance optimization utilities for the rendering pipeline:
* - requestAnimationFrame batching for render calls
* - Lightweight change detection to skip unnecessary DOM updates
*/
/**
* Cached HTML content per container for change detection.
* @type {Map<string, string>}
*/
const htmlCache = new Map();
/**
* Pending render functions queued for the next animation frame.
* @type {Array<Function>}
*/
let pendingRenders = [];
/**
* Whether a requestAnimationFrame is already scheduled.
* @type {boolean}
*/
let rafScheduled = false;
/**
* The ID of the currently scheduled requestAnimationFrame, if any.
* @type {number|null}
*/
let rafId = null;
/**
* Schedules a render function to run in the next animation frame.
* Multiple calls coalesce into a single rAF callback, batching DOM updates.
*
* @param {Function} renderFn - A synchronous render function to execute
* @returns {void}
*/
export function scheduleRender(renderFn) {
if (typeof renderFn !== 'function') return;
pendingRenders.push(renderFn);
if (!rafScheduled) {
rafScheduled = true;
rafId = requestAnimationFrame(flushRenders);
}
}
/**
* Flushes all pending render functions in a single animation frame.
* @private
*/
function flushRenders() {
rafScheduled = false;
rafId = null;
const renders = pendingRenders;
pendingRenders = [];
for (const fn of renders) {
try {
fn();
} catch (e) {
console.error('[RPG Companion] Render error:', e);
}
}
}
/**
* Cancels any pending scheduled renders.
* Useful when a newer render call makes older ones stale.
*
* @returns {void}
*/
export function cancelScheduledRenders() {
if (rafId !== null) {
cancelAnimationFrame(rafId);
rafId = null;
}
pendingRenders = [];
rafScheduled = false;
}
/**
* Updates a jQuery container's HTML only if the new content differs from the cached version.
* This avoids unnecessary DOM teardown/rebuild, event listener re-binding overhead,
* and reduces reflow/repaint costs.
*
* @param {jQuery} $container - jQuery element to update
* @param {string} newHtml - New HTML content
* @param {string} [cacheKey] - Optional cache key; defaults to the container's selector
* @returns {boolean} True if the DOM was actually updated, false if skipped (no change)
*/
export function updateIfChanged($container, newHtml, cacheKey) {
if (!$container || !$container.length) return false;
if (!cacheKey) {
cacheKey = $container.selector || $container.attr('id') || String($container[0]);
}
const cached = htmlCache.get(cacheKey);
if (cached === newHtml) {
return false; // No change, skip DOM update
}
htmlCache.set(cacheKey, newHtml);
$container.html(newHtml);
return true;
}
/**
* Clears the HTML cache for a specific key, forcing the next render to update.
* Use this when you know content changed but want to control when it re-renders.
*
* @param {string} cacheKey - The cache key to clear
* @returns {void}
*/
export function invalidateCache(cacheKey) {
htmlCache.delete(cacheKey);
}
/**
* Clears the entire HTML cache.
* Use sparingly e.g., on chat change or full reset.
*
* @returns {void}
*/
export function clearAllCaches() {
htmlCache.clear();
}
/**
* Batches multiple DOM updates into a single reflow by using
* document fragment pattern. Reduces layout thrashing.
*
* @param {Function} callback - Function that receives a document fragment and populates it
* @param {jQuery} $container - Container to append the fragment to
* @returns {void}
*/
export function batchDomUpdates(callback, $container) {
if (!$container || !$container.length) return;
const fragment = document.createDocumentFragment();
callback(fragment);
$container[0].appendChild(fragment);
}
+8 -28
View File
@@ -23,6 +23,7 @@ import {
import { isItemLocked, setItemLock } from '../generation/lockManager.js';
import { renderAlternatePresentCharacters } from '../ui/alternatePresentCharacters.js';
import { queueThoughtBasedExpressionsUpdate } from '../integration/thoughtBasedExpressions.js';
import { updateIfChanged } from './renderUtils.js';
/**
* Helper to generate lock icon HTML if setting is enabled
@@ -477,13 +478,15 @@ export function renderThoughts({ preserveScroll = false, useCommittedFallback =
html += '</div>';
}
$thoughtsContainer.html(html);
const domUpdated = updateIfChanged($thoughtsContainer, html, 'rpg-thoughts');
debugLog('[RPG Thoughts] ✓ HTML rendered to container');
debugLog('[RPG Thoughts] ✓ HTML rendered to container, updated:', domUpdated);
debugLog('[RPG Thoughts] =======================================================');
// Add event handlers for editable character fields
$thoughtsContainer.find('.rpg-editable').on('blur', function () {
// Only re-bind event handlers if DOM was actually updated
if (domUpdated) {
// Add event handlers for editable character fields
$thoughtsContainer.find('.rpg-editable').on('blur', function () {
const character = $(this).data('character');
const field = $(this).data('field');
const value = $(this).text().trim();
@@ -598,6 +601,7 @@ export function renderThoughts({ preserveScroll = false, useCommittedFallback =
}
}
});
} // end if (domUpdated)
// Remove updating class after animation
if (extensionSettings.enableAnimations) {
@@ -1297,30 +1301,6 @@ export function updateCharacterField(characterName, field, value) {
// Note: Don't call renderThoughts() here - it would overwrite the user's edits
}
/**
* Renders only the sidebar thoughts panel without updating chat bubbles
*/
function renderThoughtsSidebarOnly() {
if (!extensionSettings.showCharacterThoughts || !$thoughtsContainer) {
return;
}
// This is a simplified version that only updates the sidebar
// Copy the rendering logic from renderThoughts but skip the updateChatThoughts call
const thoughtsData = lastGeneratedData.characterThoughts || committedTrackerData.characterThoughts;
if (!thoughtsData) {
$thoughtsContainer.html('<div class="rpg-inventory-empty">' + (i18n.getTranslation('thoughts.empty') || 'No character data generated yet') + '</div>');
return;
}
// Re-render sidebar content (this would be the full logic from renderThoughts)
// For now, just call renderThoughts but set a flag
const originalShowInChat = extensionSettings.showThoughtsInChat;
extensionSettings.showThoughtsInChat = false;
renderThoughts();
extensionSettings.showThoughtsInChat = originalShowInChat;
}
/**
* Updates or removes thoughts shown in chat.
* Renders either the original corner bubbles or inline dropdown cards.
+121 -159
View File
@@ -5,25 +5,16 @@
import { getContext } from '../../../../../../extensions.js';
import { user_avatar } from '../../../../../../../script.js';
import {
extensionSettings,
lastGeneratedData,
committedTrackerData,
$userStatsContainer,
FALLBACK_AVATAR_DATA_URI
} from '../../core/state.js';
import { extensionSettings, lastGeneratedData, committedTrackerData, $userStatsContainer, FALLBACK_AVATAR_DATA_URI } from '../../core/state.js';
import { i18n } from '../../core/i18n.js';
import {
saveSettings,
saveChatData,
updateMessageSwipeData
} from '../../core/persistence.js';
import { saveSettings, saveChatData, updateMessageSwipeData } from '../../core/persistence.js';
import { getSafeThumbnailUrl } from '../../utils/avatars.js';
import { buildInventorySummary } from '../generation/promptBuilder.js';
import { isItemLocked, setItemLock } from '../generation/lockManager.js';
import { updateFabWidgets } from '../ui/mobile.js';
import { getStatBarColors } from '../ui/theme.js';
import { getEquipmentBonuses } from '../interaction/equipmentActions.js';
import { updateIfChanged } from './renderUtils.js';
/**
* Extracts the base name (before parentheses) and converts to snake_case for use as JSON key.
@@ -152,7 +143,7 @@ function updateUserStatsData() {
if (!itemString) return [];
const items = itemString.split(',').map(s => s.trim()).filter(s => s);
return items.map(item => {
const qtyMatch = item.match(/^(\\d+)x\\s+(.+)$/);
const qtyMatch = item.match(/^(\d+)x\s+(.+)$/);
if (qtyMatch) {
return { name: qtyMatch[2].trim(), quantity: parseInt(qtyMatch[1]) };
}
@@ -449,164 +440,135 @@ export function renderUserStats() {
// console.log('[RPG UserStats Render] Container exists:', !!$userStatsContainer, '$userStatsContainer length:', $userStatsContainer?.length);
// Always render to the #rpg-user-stats container (mobile layout just moves it around in DOM)
$userStatsContainer.html(html);
// console.log('[RPG UserStats Render] ✓ HTML rendered to #rpg-user-stats container');
// Use change detection to skip unnecessary DOM updates
const domUpdated = updateIfChanged($userStatsContainer, html, 'rpg-user-stats');
// console.log('[RPG UserStats Render] ✓ HTML rendered to #rpg-user-stats container, updated:', domUpdated);
// Add event listeners for editable stat values
$('.rpg-editable-stat').on('blur', function () {
const field = $(this).data('field');
const mode = $(this).data('mode');
const maxValue = parseInt($(this).data('max')) || 100;
const textValue = $(this).text().trim();
let value;
// Only re-bind event listeners if DOM was actually updated
if (domUpdated) {
$userStatsContainer.find('.rpg-editable-stat').on('blur', function () {
const field = $(this).data('field');
const mode = $(this).data('mode');
const maxValue = parseInt($(this).data('max')) || 100;
const textValue = $(this).text().trim();
let value;
if (mode === 'number') {
// In number mode, parse "X/MAX" or just "X"
const parts = textValue.split('/');
value = parseInt(parts[0]);
// Validate and clamp value between 0 and maxValue
if (isNaN(value)) {
value = 0;
if (mode === 'number') {
const parts = textValue.split('/');
value = parseInt(parts[0]);
if (isNaN(value)) {
value = 0;
}
value = Math.max(0, Math.min(maxValue, value));
} else {
value = parseInt(textValue.replace('%', ''));
if (isNaN(value)) {
value = 0;
}
value = Math.max(0, Math.min(100, value));
}
value = Math.max(0, Math.min(maxValue, value));
} else {
// In percentage mode, parse "X%" or just "X"
value = parseInt(textValue.replace('%', ''));
// Validate and clamp value between 0 and 100
if (isNaN(value)) {
value = 0;
extensionSettings.userStats[field] = value;
updateUserStatsData();
saveSettings();
saveChatData();
updateMessageSwipeData();
renderUserStats();
updateFabWidgets();
});
// Add event listeners for mood/conditions editing
$userStatsContainer.find('.rpg-mood-emoji.rpg-editable').on('blur', function () {
const value = $(this).text().trim();
extensionSettings.userStats.mood = value || '😐';
updateUserStatsData();
saveSettings();
saveChatData();
updateMessageSwipeData();
});
$userStatsContainer.find('.rpg-mood-conditions.rpg-editable').on('blur', function () {
const value = $(this).text().trim();
const fieldKey = $(this).data('field');
extensionSettings.userStats[fieldKey] = value || 'None';
updateUserStatsData();
saveSettings();
saveChatData();
updateMessageSwipeData();
});
// Add event listener for skills editing
$userStatsContainer.find('.rpg-skills-value.rpg-editable').on('blur', function () {
const value = $(this).text().trim();
extensionSettings.userStats.skills = value || 'None';
updateUserStatsData();
saveSettings();
saveChatData();
updateMessageSwipeData();
});
// Add event listeners for stat name editing
$userStatsContainer.find('.rpg-editable-stat-name').on('blur', function () {
const field = $(this).data('field');
const value = $(this).text().trim().replace(':', '');
if (!extensionSettings.statNames) {
extensionSettings.statNames = {
health: 'Health',
satiety: 'Satiety',
energy: 'Energy',
hygiene: 'Hygiene',
arousal: 'Arousal'
};
}
value = Math.max(0, Math.min(100, value));
}
// Update the setting
extensionSettings.userStats[field] = value;
extensionSettings.statNames[field] = value || extensionSettings.statNames[field];
saveSettings();
saveChatData();
renderUserStats();
});
// Update userStats data (maintains JSON or text format)
updateUserStatsData();
// Add event listener for level editing
$userStatsContainer.find('.rpg-level-value.rpg-editable').on('blur', function () {
let value = parseInt($(this).text().trim());
if (isNaN(value) || value < 1) {
value = 1;
}
value = Math.min(100, value);
saveSettings();
saveChatData();
updateMessageSwipeData();
extensionSettings.level = value;
saveSettings();
saveChatData();
updateMessageSwipeData();
renderUserStats();
});
// Re-render to update the bar and FAB widgets
renderUserStats();
updateFabWidgets();
});
// Prevent line breaks in level field
$userStatsContainer.find('.rpg-level-value.rpg-editable').on('keydown', function (e) {
if (e.key === 'Enter') {
e.preventDefault();
$(this).blur();
}
});
// Add event listeners for mood/conditions editing
$('.rpg-mood-emoji.rpg-editable').on('blur', function () {
const value = $(this).text().trim();
extensionSettings.userStats.mood = value || '😐';
// Update userStats data (maintains JSON or text format)
updateUserStatsData();
saveSettings();
saveChatData();
updateMessageSwipeData();
});
$('.rpg-mood-conditions.rpg-editable').on('blur', function () {
const value = $(this).text().trim();
const fieldKey = $(this).data('field');
extensionSettings.userStats[fieldKey] = value || 'None';
// Update userStats data (maintains JSON or text format)
updateUserStatsData();
saveSettings();
saveChatData();
updateMessageSwipeData();
});
// Add event listener for skills editing
$('.rpg-skills-value.rpg-editable').on('blur', function () {
const value = $(this).text().trim();
extensionSettings.userStats.skills = value || 'None';
// Update userStats data (maintains JSON or text format)
updateUserStatsData();
saveSettings();
saveChatData();
updateMessageSwipeData();
});
// Add event listeners for stat name editing
$('.rpg-editable-stat-name').on('blur', function () {
const field = $(this).data('field');
const value = $(this).text().trim().replace(':', '');
if (!extensionSettings.statNames) {
extensionSettings.statNames = {
health: 'Health',
satiety: 'Satiety',
energy: 'Energy',
hygiene: 'Hygiene',
arousal: 'Arousal'
};
}
extensionSettings.statNames[field] = value || extensionSettings.statNames[field];
saveSettings();
saveChatData();
// Re-render to update the display
renderUserStats();
});
// Add event listener for level editing
$('.rpg-level-value.rpg-editable').on('blur', function () {
let value = parseInt($(this).text().trim());
if (isNaN(value) || value < 1) {
value = 1;
}
// Set reasonable max level
value = Math.min(100, value);
extensionSettings.level = value;
saveSettings();
saveChatData();
updateMessageSwipeData();
// Re-render to update the display
renderUserStats();
});
// Prevent line breaks in level field
$('.rpg-level-value.rpg-editable').on('keydown', function (e) {
if (e.key === 'Enter') {
// Add event listener for section lock icon clicks (support both click and touch)
$userStatsContainer.find('.rpg-section-lock-icon').on('click touchend', function (e) {
e.preventDefault();
$(this).blur();
}
});
e.stopPropagation();
const $icon = $(this);
const trackerType = $icon.data('tracker');
const itemPath = $icon.data('path');
const currentlyLocked = isItemLocked(trackerType, itemPath);
// Add event listener for section lock icon clicks (support both click and touch)
$('.rpg-section-lock-icon').on('click touchend', function (e) {
e.preventDefault();
e.stopPropagation();
const $icon = $(this);
const trackerType = $icon.data('tracker');
const itemPath = $icon.data('path');
const currentlyLocked = isItemLocked(trackerType, itemPath);
setItemLock(trackerType, itemPath, !currentlyLocked);
// Toggle lock state
setItemLock(trackerType, itemPath, !currentlyLocked);
const newIcon = !currentlyLocked ? '🔒' : '🔓';
const newTitle = !currentlyLocked ? (i18n.getTranslation('infoBox.locked') || 'Locked') : (i18n.getTranslation('infoBox.unlocked') || 'Unlocked');
$icon.text(newIcon);
$icon.attr('title', newTitle);
$icon.toggleClass('locked', !currentlyLocked);
// Update icon
const newIcon = !currentlyLocked ? '🔒' : '🔓';
const newTitle = !currentlyLocked ? (i18n.getTranslation('infoBox.locked') || 'Locked') : (i18n.getTranslation('infoBox.unlocked') || 'Unlocked');
$icon.text(newIcon);
$icon.attr('title', newTitle);
// Toggle 'locked' class for persistent visibility
$icon.toggleClass('locked', !currentlyLocked);
// Save settings
saveSettings();
});
saveSettings();
});
} // end if (domUpdated)
}
-27
View File
@@ -339,31 +339,4 @@ function processExpandedButton(messageBlock) {
}
/**
* Update the checkpoint button in an existing menu
* @param {HTMLElement} menu - The extraMesButtons or mes_buttons container
* @param {number} messageId - The message index
*/
function updateCheckpointButtonInMenu(menu, messageId) {
if (!menu) return;
// Find the checkpoint button (either dropdown or expanded)
const existingButton = menu.querySelector('.rpg-checkpoint-button, .rpg-checkpoint-button-expanded');
if (!existingButton) return;
const isCheckpoint = isCheckpointMessage(messageId);
// Update icon
const icon = existingButton.querySelector('i');
if (icon) {
icon.className = isCheckpoint ? 'fa-solid fa-bookmark' : 'fa-regular fa-bookmark';
icon.style.color = isCheckpoint ? '#4a9eff' : '';
}
// Update tooltip
existingButton.title = isCheckpoint
? 'Clear Chapter Start'
: 'Set Chapter Start — When bookmarked, this message will count as the first message in the chat history, skipping earlier ones.';
const translationKey = isCheckpoint ? 'checkpoint.clearChapterStart' : 'checkpoint.setChapterStart';
existingButton.setAttribute('data-i18n', translationKey);
}
+3 -2
View File
@@ -6,6 +6,7 @@
import { i18n } from '../../core/i18n.js';
import { extensionSettings, lastGeneratedData, committedTrackerData } from '../../core/state.js';
import { hexToRgba } from './theme.js';
import { repairJSON } from '../../utils/jsonRepair.js';
/**
* Helper to parse time string and calculate clock hand angles
@@ -50,7 +51,7 @@ export function updateStripWidgets() {
let infoData = null;
if (infoBox) {
try {
infoData = typeof infoBox === 'string' ? JSON.parse(infoBox) : infoBox;
infoData = typeof infoBox === 'string' ? repairJSON(infoBox) : infoBox;
} catch (e) {
console.warn('[RPG Strip Widgets] Failed to parse infoBox:', e);
}
@@ -124,7 +125,7 @@ export function updateStripWidgets() {
const userStatsData = lastGeneratedData?.userStats || committedTrackerData?.userStats;
if (userStatsData) {
try {
const parsedStats = typeof userStatsData === 'string' ? JSON.parse(userStatsData) : userStatsData;
const parsedStats = typeof userStatsData === 'string' ? repairJSON(userStatsData) : userStatsData;
if (parsedStats?.stats) {
allStats = parsedStats.stats;
}
+8 -435
View File
@@ -9,6 +9,7 @@ import { closeMobilePanelWithAnimation, updateCollapseToggleIcon } from './layou
import { setupDesktopTabs, removeDesktopTabs } from './desktop.js';
import { i18n } from '../../core/i18n.js';
import { hexToRgba } from './theme.js';
import { repairJSON } from '../../utils/jsonRepair.js';
/**
* Updates the text labels of the mobile navigation tabs based on the current language.
@@ -503,7 +504,10 @@ export function setupMobileToggle() {
// top: $panel.css('top'),
// bottom: $panel.css('top'),
// transform: $panel.css('transform'),
// visibility: $panel.css('visibility')\n // }\n // });\n setupMobileTabs();
// visibility: $panel.css('visibility')
// }
// });
setupMobileTabs();
// Set initial icon for mobile
updateCollapseToggleIcon();
// Show mobile toggle on mobile viewport
@@ -860,437 +864,6 @@ export function setupContentEditableScrolling() {
});
}
/**
* Sets up the mobile refresh button with drag functionality.
* Same pattern as mobile toggle button.
* Tap = refresh, drag = reposition
*/
export function setupRefreshButtonDrag() {
const $refreshBtn = $('#rpg-manual-update-mobile');
if ($refreshBtn.length === 0) {
console.warn('[RPG Mobile] Refresh button not found in DOM');
return;
}
// console.log('[RPG Mobile] setupRefreshButtonDrag called');
// Load and apply saved position
if (extensionSettings.mobileRefreshPosition) {
const pos = extensionSettings.mobileRefreshPosition;
// console.log('[RPG Mobile] Loading saved refresh button position:', pos);
// Apply saved position
if (pos.top) $refreshBtn.css('top', pos.top);
if (pos.right) $refreshBtn.css('right', pos.right);
if (pos.bottom) $refreshBtn.css('bottom', pos.bottom);
if (pos.left) $refreshBtn.css('left', pos.left);
// Constrain to viewport after position is applied
requestAnimationFrame(() => constrainFabToViewport($refreshBtn));
}
// Touch/drag state
let isDragging = false;
let touchStartTime = 0;
let touchStartX = 0;
let touchStartY = 0;
let buttonStartX = 0;
let buttonStartY = 0;
const LONG_PRESS_DURATION = 200;
const MOVE_THRESHOLD = 10;
let rafId = null;
let pendingX = null;
let pendingY = null;
// Update position using requestAnimationFrame
function updatePosition() {
if (pendingX !== null && pendingY !== null) {
$refreshBtn.css({
left: pendingX + 'px',
top: pendingY + 'px',
right: 'auto',
bottom: 'auto'
});
pendingX = null;
pendingY = null;
}
rafId = null;
}
// Touch start
$refreshBtn.on('touchstart', function(e) {
const touch = e.originalEvent.touches[0];
touchStartTime = Date.now();
touchStartX = touch.clientX;
touchStartY = touch.clientY;
const offset = $refreshBtn.offset();
buttonStartX = offset.left;
buttonStartY = offset.top;
isDragging = false;
});
// Touch move
$refreshBtn.on('touchmove', function(e) {
const touch = e.originalEvent.touches[0];
const deltaX = touch.clientX - touchStartX;
const deltaY = touch.clientY - touchStartY;
const timeSinceStart = Date.now() - touchStartTime;
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
if (!isDragging && (timeSinceStart > LONG_PRESS_DURATION || distance > MOVE_THRESHOLD)) {
isDragging = true;
$refreshBtn.addClass('dragging');
}
if (isDragging) {
e.preventDefault();
let newX = buttonStartX + deltaX;
let newY = buttonStartY + deltaY;
const buttonWidth = $refreshBtn.outerWidth();
const buttonHeight = $refreshBtn.outerHeight();
const minX = 10;
const maxX = window.innerWidth - buttonWidth - 10;
const minY = 10;
const maxY = window.innerHeight - buttonHeight - 10;
newX = Math.max(minX, Math.min(maxX, newX));
newY = Math.max(minY, Math.min(maxY, newY));
pendingX = newX;
pendingY = newY;
if (!rafId) {
rafId = requestAnimationFrame(updatePosition);
}
}
});
// Touch end
$refreshBtn.on('touchend', function(e) {
if (isDragging) {
// Save new position
const offset = $refreshBtn.offset();
const newPosition = {
left: offset.left + 'px',
top: offset.top + 'px'
};
extensionSettings.mobileRefreshPosition = newPosition;
saveSettings();
setTimeout(() => {
$refreshBtn.removeClass('dragging');
}, 50);
// Set flag to prevent click handler from firing
$refreshBtn.data('just-dragged', true);
setTimeout(() => {
$refreshBtn.data('just-dragged', false);
}, 100);
isDragging = false;
}
});
// Mouse support for desktop
let mouseDown = false;
$refreshBtn.on('mousedown', function(e) {
e.preventDefault();
touchStartTime = Date.now();
touchStartX = e.clientX;
touchStartY = e.clientY;
const offset = $refreshBtn.offset();
buttonStartX = offset.left;
buttonStartY = offset.top;
mouseDown = true;
isDragging = false;
});
$(document).on('mousemove', function(e) {
if (!mouseDown) return;
const deltaX = e.clientX - touchStartX;
const deltaY = e.clientY - touchStartY;
const timeSinceStart = Date.now() - touchStartTime;
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
if (!isDragging && (timeSinceStart > LONG_PRESS_DURATION || distance > MOVE_THRESHOLD)) {
isDragging = true;
$refreshBtn.addClass('dragging');
}
if (isDragging) {
let newX = buttonStartX + deltaX;
let newY = buttonStartY + deltaY;
const buttonWidth = $refreshBtn.outerWidth();
const buttonHeight = $refreshBtn.outerHeight();
const minX = 10;
const maxX = window.innerWidth - buttonWidth - 10;
const minY = 10;
const maxY = window.innerHeight - buttonHeight - 10;
newX = Math.max(minX, Math.min(maxX, newX));
newY = Math.max(minY, Math.min(maxY, newY));
pendingX = newX;
pendingY = newY;
if (!rafId) {
rafId = requestAnimationFrame(updatePosition);
}
}
});
$(document).on('mouseup', function(e) {
if (mouseDown && isDragging) {
const offset = $refreshBtn.offset();
const newPosition = {
left: offset.left + 'px',
top: offset.top + 'px'
};
extensionSettings.mobileRefreshPosition = newPosition;
saveSettings();
setTimeout(() => {
$refreshBtn.removeClass('dragging');
}, 50);
$refreshBtn.data('just-dragged', true);
setTimeout(() => {
$refreshBtn.data('just-dragged', false);
}, 100);
}
mouseDown = false;
isDragging = false;
});
}
/**
* Sets up drag functionality for the debug toggle FAB button
* Same pattern as refresh button drag
*/
export function setupDebugButtonDrag() {
const $debugBtn = $('#rpg-debug-toggle');
if ($debugBtn.length === 0) {
console.warn('[RPG Mobile] Debug button not found in DOM');
return;
}
// console.log('[RPG Mobile] setupDebugButtonDrag called');
// Load and apply saved position
if (extensionSettings.debugFabPosition) {
const pos = extensionSettings.debugFabPosition;
// console.log('[RPG Mobile] Loading saved debug button position:', pos);
// Apply saved position
if (pos.top) $debugBtn.css('top', pos.top);
if (pos.right) $debugBtn.css('right', pos.right);
if (pos.bottom) $debugBtn.css('bottom', pos.bottom);
if (pos.left) $debugBtn.css('left', pos.left);
// Constrain to viewport after position is applied
requestAnimationFrame(() => constrainFabToViewport($debugBtn));
}
// Touch/drag state
let isDragging = false;
let touchStartTime = 0;
let touchStartX = 0;
let touchStartY = 0;
let buttonStartX = 0;
let buttonStartY = 0;
const LONG_PRESS_DURATION = 200;
const MOVE_THRESHOLD = 10;
let rafId = null;
let pendingX = null;
let pendingY = null;
// Update position using requestAnimationFrame
function updatePosition() {
if (pendingX !== null && pendingY !== null) {
$debugBtn.css({
left: pendingX + 'px',
top: pendingY + 'px',
right: 'auto',
bottom: 'auto'
});
pendingX = null;
pendingY = null;
}
rafId = null;
}
// Touch start
$debugBtn.on('touchstart', function(e) {
const touch = e.originalEvent.touches[0];
touchStartTime = Date.now();
touchStartX = touch.clientX;
touchStartY = touch.clientY;
const offset = $debugBtn.offset();
buttonStartX = offset.left;
buttonStartY = offset.top;
isDragging = false;
});
// Touch move
$debugBtn.on('touchmove', function(e) {
const touch = e.originalEvent.touches[0];
const deltaX = touch.clientX - touchStartX;
const deltaY = touch.clientY - touchStartY;
const timeSinceStart = Date.now() - touchStartTime;
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
if (!isDragging && (timeSinceStart > LONG_PRESS_DURATION || distance > MOVE_THRESHOLD)) {
isDragging = true;
$debugBtn.addClass('dragging');
}
if (isDragging) {
e.preventDefault();
let newX = buttonStartX + deltaX;
let newY = buttonStartY + deltaY;
const buttonWidth = $debugBtn.outerWidth();
const buttonHeight = $debugBtn.outerHeight();
const minX = 10;
const maxX = window.innerWidth - buttonWidth - 10;
const minY = 10;
const maxY = window.innerHeight - buttonHeight - 10;
newX = Math.max(minX, Math.min(maxX, newX));
newY = Math.max(minY, Math.min(maxY, newY));
pendingX = newX;
pendingY = newY;
if (!rafId) {
rafId = requestAnimationFrame(updatePosition);
}
}
});
// Touch end
$debugBtn.on('touchend', function(e) {
if (isDragging) {
// Save new position
const offset = $debugBtn.offset();
const newPosition = {
left: offset.left + 'px',
top: offset.top + 'px'
};
extensionSettings.debugFabPosition = newPosition;
saveSettings();
setTimeout(() => {
$debugBtn.removeClass('dragging');
}, 50);
// Set flag to prevent click handler from firing
$debugBtn.data('just-dragged', true);
setTimeout(() => {
$debugBtn.data('just-dragged', false);
}, 100);
isDragging = false;
}
});
// Mouse support for desktop
let mouseDown = false;
$debugBtn.on('mousedown', function(e) {
e.preventDefault();
touchStartTime = Date.now();
touchStartX = e.clientX;
touchStartY = e.clientY;
const offset = $debugBtn.offset();
buttonStartX = offset.left;
buttonStartY = offset.top;
mouseDown = true;
isDragging = false;
});
$(document).on('mousemove.rpgDebugDrag', function(e) {
if (!mouseDown) return;
const deltaX = e.clientX - touchStartX;
const deltaY = e.clientY - touchStartY;
const timeSinceStart = Date.now() - touchStartTime;
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
if (!isDragging && (timeSinceStart > LONG_PRESS_DURATION || distance > MOVE_THRESHOLD)) {
isDragging = true;
$debugBtn.addClass('dragging');
}
if (isDragging) {
let newX = buttonStartX + deltaX;
let newY = buttonStartY + deltaY;
const buttonWidth = $debugBtn.outerWidth();
const buttonHeight = $debugBtn.outerHeight();
const minX = 10;
const maxX = window.innerWidth - buttonWidth - 10;
const minY = 10;
const maxY = window.innerHeight - buttonHeight - 10;
newX = Math.max(minX, Math.min(maxX, newX));
newY = Math.max(minY, Math.min(maxY, newY));
pendingX = newX;
pendingY = newY;
if (!rafId) {
rafId = requestAnimationFrame(updatePosition);
}
}
});
$(document).on('mouseup.rpgDebugDrag', function(e) {
if (mouseDown && isDragging) {
const offset = $debugBtn.offset();
const newPosition = {
left: offset.left + 'px',
top: offset.top + 'px'
};
extensionSettings.debugFabPosition = newPosition;
saveSettings();
setTimeout(() => {
$debugBtn.removeClass('dragging');
}, 50);
$debugBtn.data('just-dragged', true);
setTimeout(() => {
$debugBtn.data('just-dragged', false);
}, 100);
}
mouseDown = false;
isDragging = false;
});
}
// ============================================
// FAB WIDGETS - Info display around FAB button
// ============================================
@@ -1322,7 +895,7 @@ export function updateFabWidgets() {
let infoData = null;
if (infoBox) {
try {
infoData = typeof infoBox === 'string' ? JSON.parse(infoBox) : infoBox;
infoData = typeof infoBox === 'string' ? repairJSON(infoBox) : infoBox;
} catch (e) {
console.warn('[RPG FAB Widgets] Failed to parse infoBox:', e);
}
@@ -1332,7 +905,7 @@ export function updateFabWidgets() {
let statsData = null;
if (userStats) {
try {
statsData = typeof userStats === 'string' ? JSON.parse(userStats) : userStats;
statsData = typeof userStats === 'string' ? repairJSON(userStats) : userStats;
} catch (e) {
console.warn('[RPG FAB Widgets] Failed to parse userStats:', e);
}
@@ -1444,7 +1017,7 @@ export function updateFabWidgets() {
let allStats = [];
try {
const userStatsJson = extensionSettings.userStats;
const parsedUserStats = typeof userStatsJson === 'string' ? JSON.parse(userStatsJson) : userStatsJson;
const parsedUserStats = typeof userStatsJson === 'string' ? repairJSON(userStatsJson) : userStatsJson;
if (parsedUserStats?.stats) {
allStats = parsedUserStats.stats;
}
+1 -10
View File
@@ -26,8 +26,7 @@ import { renderEquipment } from '../rendering/equipment.js';
import {
rollDice as rollDiceCore,
clearDiceRoll as clearDiceRollCore,
updateDiceDisplay as updateDiceDisplayCore,
addDiceQuickReply as addDiceQuickReplyCore
updateDiceDisplay as updateDiceDisplayCore
} from '../features/dice.js';
import { i18n } from '../../core/i18n.js';
@@ -578,14 +577,6 @@ export function updateDiceDisplay() {
updateDiceDisplayCore();
}
/**
* Adds the Roll Dice quick reply button.
* Backwards compatible wrapper for dice module.
*/
export function addDiceQuickReply() {
addDiceQuickReplyCore();
}
/**
* Returns the SettingsModal instance for external use
* @returns {SettingsModal} The global SettingsModal instance
+891
View File
@@ -0,0 +1,891 @@
/**
* Settings UI Event Listeners Module
* Handles all settings toggle/change event bindings for the RPG Companion panel.
* Extracted from index.js initUI() to reduce main entry point size.
*/
import { getContext } from '../../../../../../extensions.js';
import { extensionSettings, $musicPlayerContainer } from '../../core/state.js';
import { saveSettings, commitTrackerDataFromPriorMessage } from '../../core/persistence.js';
import {
applyPanelPosition,
updateGenerationModeUI,
updateSectionVisibility,
togglePlotButtons
} from './layout.js';
import { renderThoughts, updateChatThoughts } from '../rendering/thoughts.js';
import { renderUserStats } from '../rendering/userStats.js';
import { renderInfoBox } from '../rendering/infoBox.js';
import { renderInventory } from '../rendering/inventory.js';
import { renderEquipment } from '../rendering/equipment.js';
import { renderQuests } from '../rendering/quests.js';
import { renderMusicPlayer } from '../rendering/musicPlayer.js';
import { toggleDynamicWeather } from './weatherEffects.js';
import {
applyTheme,
updateSettingsPopupTheme,
toggleCustomColors,
applyCustomTheme,
updateFeatureTogglesVisibility
} from './theme.js';
import {
onAlternatePresentCharactersVisibilityChanged,
onThoughtBasedExpressionsSettingChanged,
onHideDefaultExpressionDisplaySettingChanged
} from '../integration/thoughtBasedExpressions.js';
import { updateStripWidgets } from './desktop.js';
import { updateFabWidgets } from './mobile.js';
import { updateDiceDisplay, getSettingsModal } from './modals.js';
import { updateRPGData, testExternalAPIConnection } from '../generation/apiClient.js';
/**
* Bind all settings panel event listeners.
* Called once during UI initialization after the DOM is ready.
*
* @param {Object} $ - jQuery (injected from SillyTavern global scope)
*/
export function bindSettingsListeners($) {
// Auto-update toggle
$('#rpg-toggle-auto-update').on('change', function() {
extensionSettings.autoUpdate = $(this).prop('checked');
saveSettings();
});
// Panel position selector
$('#rpg-position-select').on('change', function() {
extensionSettings.panelPosition = String($(this).val());
saveSettings();
applyPanelPosition();
// Recreate thought bubbles to update their position
updateChatThoughts();
});
// Update depth selector
$('#rpg-update-depth').on('change', function() {
const value = $(this).val();
extensionSettings.updateDepth = parseInt(String(value));
saveSettings();
});
// Generation mode selector
$('#rpg-generation-mode').on('change', async function() {
extensionSettings.generationMode = String($(this).val());
saveSettings();
updateGenerationModeUI();
});
// Section visibility toggles
bindSectionToggle($, 'rpg-toggle-user-stats', 'showUserStats');
bindSectionToggle($, 'rpg-toggle-info-box', 'showInfoBox');
bindSectionToggle($, 'rpg-toggle-inventory', 'showInventory');
bindSectionToggle($, 'rpg-toggle-equipment', 'showEquipment');
bindSectionToggle($, 'rpg-toggle-quests', 'showQuests');
// Thoughts section with render callback
$('#rpg-toggle-thoughts').on('change', function() {
extensionSettings.showCharacterThoughts = $(this).prop('checked');
saveSettings();
updateSectionVisibility();
renderThoughts();
});
// Alternate present characters
$('#rpg-toggle-alt-present-characters').on('change', function() {
extensionSettings.showAlternatePresentCharactersPanel = $(this).prop('checked');
saveSettings();
renderThoughts();
onAlternatePresentCharactersVisibilityChanged();
});
// Thought-based expressions
$('#rpg-toggle-thought-based-expressions').on('change', function() {
extensionSettings.enableThoughtBasedExpressions = $(this).prop('checked');
saveSettings();
onThoughtBasedExpressionsSettingChanged(extensionSettings.enableThoughtBasedExpressions);
});
// Hide default expressions
$('#rpg-toggle-hide-default-expressions').on('change', function() {
extensionSettings.hideDefaultExpressionDisplay = $(this).prop('checked');
saveSettings();
onHideDefaultExpressionDisplaySettingChanged(extensionSettings.hideDefaultExpressionDisplay);
});
// Lock icons toggle - re-renders all sections
$('#rpg-toggle-lock-icons').on('change', function() {
extensionSettings.showLockIcons = $(this).prop('checked');
saveSettings();
renderUserStats();
renderInfoBox();
renderThoughts();
renderInventory();
renderEquipment();
renderQuests();
});
// Thoughts in chat toggle
$('#rpg-toggle-thoughts-in-chat').on('change', function() {
extensionSettings.showThoughtsInChat = $(this).prop('checked');
saveSettings();
updateChatThoughts();
});
// Inline thoughts style
$('#rpg-toggle-inline-thoughts').on('change', function() {
extensionSettings.thoughtsInChatStyle = $(this).prop('checked') ? 'inline' : 'corner';
saveSettings();
updateChatThoughts();
});
// HTML prompt toggle
$('#rpg-toggle-html-prompt').on('change', function() {
extensionSettings.enableHtmlPrompt = $(this).prop('checked');
saveSettings();
});
// Dialogue coloring
$('#rpg-toggle-dialogue-coloring').on('change', function() {
extensionSettings.enableDialogueColoring = $(this).prop('checked');
saveSettings();
});
// Deception system
$('#rpg-toggle-deception').on('change', function() {
extensionSettings.enableDeceptionSystem = $(this).prop('checked');
saveSettings();
});
// Omniscience filter
$('#rpg-toggle-omniscience').on('change', function() {
extensionSettings.enableOmniscienceFilter = $(this).prop('checked');
saveSettings();
});
// CYOA mode
$('#rpg-toggle-cyoa').on('change', function() {
extensionSettings.enableCYOA = $(this).prop('checked');
saveSettings();
});
// Spotify music
$('#rpg-toggle-spotify-music').on('change', function() {
extensionSettings.enableSpotifyMusic = $(this).prop('checked');
saveSettings();
updateSectionVisibility();
renderMusicPlayer($musicPlayerContainer[0]);
});
// Dynamic weather
$('#rpg-toggle-dynamic-weather').on('change', function() {
extensionSettings.enableDynamicWeather = $(this).prop('checked');
saveSettings();
toggleDynamicWeather(extensionSettings.enableDynamicWeather);
});
// Narrator mode
$('#rpg-toggle-narrator').on('change', function() {
extensionSettings.narratorMode = $(this).prop('checked');
saveSettings();
});
// Skip injections for guided mode
$('#rpg-skip-guided-mode').on('change', function() {
extensionSettings.skipInjectionsForGuided = String($(this).val());
saveSettings();
});
// Randomized plot
$('#rpg-toggle-randomized-plot').on('change', function() {
extensionSettings.enableRandomizedPlot = $(this).prop('checked');
saveSettings();
togglePlotButtons();
});
// Natural plot
$('#rpg-toggle-natural-plot').on('change', function() {
extensionSettings.enableNaturalPlot = $(this).prop('checked');
saveSettings();
togglePlotButtons();
});
// Encounters toggle
$('#rpg-toggle-encounters').on('change', function() {
if (!extensionSettings.encounterSettings) {
extensionSettings.encounterSettings = { enabled: true, historyDepth: 8, autoSaveLogs: true };
}
extensionSettings.encounterSettings.enabled = $(this).prop('checked');
saveSettings();
togglePlotButtons(); // This also controls encounter button visibility
});
// Encounter history depth
$('#rpg-encounter-history-depth').on('change', function() {
if (!extensionSettings.encounterSettings) {
extensionSettings.encounterSettings = { enabled: true, historyDepth: 8, autoSaveLogs: true };
}
const value = $(this).val();
extensionSettings.encounterSettings.historyDepth = parseInt(String(value));
saveSettings();
});
// Auto-save logs
$('#rpg-toggle-autosave-logs').on('change', function() {
if (!extensionSettings.encounterSettings) {
extensionSettings.encounterSettings = { enabled: true, historyDepth: 8, autoSaveLogs: true };
}
extensionSettings.encounterSettings.autoSaveLogs = $(this).prop('checked');
saveSettings();
});
// Feature toggle visibility controls
$('#rpg-toggle-show-html-toggle').on('change', function() {
extensionSettings.showHtmlToggle = $(this).prop('checked');
saveSettings();
updateFeatureTogglesVisibility();
});
$('#rpg-toggle-show-dialogue-coloring-toggle').on('change', function() {
extensionSettings.showDialogueColoringToggle = $(this).prop('checked');
saveSettings();
updateFeatureTogglesVisibility();
});
$('#rpg-toggle-show-deception-toggle').on('change', function() {
extensionSettings.showDeceptionToggle = $(this).prop('checked');
saveSettings();
updateFeatureTogglesVisibility();
});
$('#rpg-toggle-show-omniscience-toggle').on('change', function() {
extensionSettings.showOmniscienceToggle = $(this).prop('checked');
saveSettings();
updateFeatureTogglesVisibility();
});
$('#rpg-toggle-show-cyoa-toggle').on('change', function() {
extensionSettings.showCYOAToggle = $(this).prop('checked');
saveSettings();
updateFeatureTogglesVisibility();
});
$('#rpg-toggle-show-spotify-toggle').on('change', function() {
extensionSettings.showSpotifyToggle = $(this).prop('checked');
saveSettings();
updateFeatureTogglesVisibility();
});
$('#rpg-toggle-show-dynamic-weather-toggle').on('change', function() {
extensionSettings.showDynamicWeatherToggle = $(this).prop('checked');
// Also disable the feature when hiding the toggle
if (!extensionSettings.showDynamicWeatherToggle) {
extensionSettings.enableDynamicWeather = false;
$('#rpg-toggle-dynamic-weather').prop('checked', false);
toggleDynamicWeather(false);
}
saveSettings();
updateFeatureTogglesVisibility();
updateWeatherSubOptionsVisibility();
});
// Weather sub-options (background and foreground) - radio buttons
$('#rpg-toggle-weather-background').on('change', function() {
if ($(this).prop('checked')) {
extensionSettings.weatherBackground = true;
extensionSettings.weatherForeground = false;
saveSettings();
// Re-apply weather effect
if (extensionSettings.enableDynamicWeather) {
toggleDynamicWeather(false);
toggleDynamicWeather(true);
}
}
});
$('#rpg-toggle-weather-foreground').on('change', function() {
if ($(this).prop('checked')) {
extensionSettings.weatherBackground = false;
extensionSettings.weatherForeground = true;
saveSettings();
// Re-apply weather effect
if (extensionSettings.enableDynamicWeather) {
toggleDynamicWeather(false);
toggleDynamicWeather(true);
}
}
});
$('#rpg-toggle-show-narrator-mode').on('change', function() {
extensionSettings.showNarratorMode = $(this).prop('checked');
// Also disable the feature when hiding the toggle
if (!extensionSettings.showNarratorMode) {
extensionSettings.narratorMode = false;
$('#rpg-toggle-narrator').prop('checked', false);
}
saveSettings();
updateFeatureTogglesVisibility();
});
$('#rpg-toggle-show-auto-avatars').on('change', function() {
extensionSettings.showAutoAvatars = $(this).prop('checked');
// Also disable the feature when hiding the toggle
if (!extensionSettings.showAutoAvatars) {
extensionSettings.autoGenerateAvatars = false;
$('#rpg-toggle-auto-avatars-panel').prop('checked', false);
}
saveSettings();
updateFeatureTogglesVisibility();
});
// Auto avatar generation panel toggle
$('#rpg-toggle-auto-avatars-panel').on('change', function() {
extensionSettings.autoGenerateAvatars = $(this).prop('checked');
saveSettings();
// Re-render thoughts to update tooltips (regenerate vs delete)
renderThoughts();
});
// Dice display toggle
$('#rpg-toggle-dice-display').on('change', function() {
extensionSettings.showDiceDisplay = $(this).prop('checked');
saveSettings();
updateDiceDisplay();
});
// Mobile FAB Widget toggles - simplified, no position saving (auto-positioned)
$('#rpg-toggle-fab-widgets-enabled').on('change', function() {
if (!extensionSettings.mobileFabWidgets) extensionSettings.mobileFabWidgets = {};
extensionSettings.mobileFabWidgets.enabled = $(this).prop('checked');
saveSettings();
updateFabWidgets();
$('#rpg-fab-widget-options').toggle(extensionSettings.mobileFabWidgets.enabled);
});
$('#rpg-toggle-fab-weather-icon').on('change', function() {
if (!extensionSettings.mobileFabWidgets) extensionSettings.mobileFabWidgets = {};
if (!extensionSettings.mobileFabWidgets.weatherIcon) extensionSettings.mobileFabWidgets.weatherIcon = {};
extensionSettings.mobileFabWidgets.weatherIcon.enabled = $(this).prop('checked');
saveSettings();
updateFabWidgets();
});
$('#rpg-toggle-fab-weather-desc').on('change', function() {
if (!extensionSettings.mobileFabWidgets) extensionSettings.mobileFabWidgets = {};
if (!extensionSettings.mobileFabWidgets.weatherDesc) extensionSettings.mobileFabWidgets.weatherDesc = {};
extensionSettings.mobileFabWidgets.weatherDesc.enabled = $(this).prop('checked');
saveSettings();
updateFabWidgets();
});
$('#rpg-toggle-fab-clock').on('change', function() {
if (!extensionSettings.mobileFabWidgets) extensionSettings.mobileFabWidgets = {};
if (!extensionSettings.mobileFabWidgets.clock) extensionSettings.mobileFabWidgets.clock = {};
extensionSettings.mobileFabWidgets.clock.enabled = $(this).prop('checked');
saveSettings();
updateFabWidgets();
});
$('#rpg-toggle-fab-date').on('change', function() {
if (!extensionSettings.mobileFabWidgets) extensionSettings.mobileFabWidgets = {};
if (!extensionSettings.mobileFabWidgets.date) extensionSettings.mobileFabWidgets.date = {};
extensionSettings.mobileFabWidgets.date.enabled = $(this).prop('checked');
saveSettings();
updateFabWidgets();
});
$('#rpg-toggle-fab-location').on('change', function() {
if (!extensionSettings.mobileFabWidgets) extensionSettings.mobileFabWidgets = {};
if (!extensionSettings.mobileFabWidgets.location) extensionSettings.mobileFabWidgets.location = {};
extensionSettings.mobileFabWidgets.location.enabled = $(this).prop('checked');
saveSettings();
updateFabWidgets();
});
$('#rpg-toggle-fab-stats').on('change', function() {
if (!extensionSettings.mobileFabWidgets) extensionSettings.mobileFabWidgets = {};
if (!extensionSettings.mobileFabWidgets.stats) extensionSettings.mobileFabWidgets.stats = {};
extensionSettings.mobileFabWidgets.stats.enabled = $(this).prop('checked');
saveSettings();
updateFabWidgets();
});
$('#rpg-toggle-fab-attributes').on('change', function() {
if (!extensionSettings.mobileFabWidgets) extensionSettings.mobileFabWidgets = {};
if (!extensionSettings.mobileFabWidgets.attributes) extensionSettings.mobileFabWidgets.attributes = {};
extensionSettings.mobileFabWidgets.attributes.enabled = $(this).prop('checked');
saveSettings();
updateFabWidgets();
});
// Desktop Strip Widget toggles
$('#rpg-toggle-strip-widgets-enabled').on('change', function() {
if (!extensionSettings.desktopStripWidgets) extensionSettings.desktopStripWidgets = {};
extensionSettings.desktopStripWidgets.enabled = $(this).prop('checked');
saveSettings();
updateStripWidgets();
$('#rpg-strip-widget-options').toggle(extensionSettings.desktopStripWidgets.enabled);
});
$('#rpg-toggle-strip-weather-icon').on('change', function() {
if (!extensionSettings.desktopStripWidgets) extensionSettings.desktopStripWidgets = {};
if (!extensionSettings.desktopStripWidgets.weatherIcon) extensionSettings.desktopStripWidgets.weatherIcon = {};
extensionSettings.desktopStripWidgets.weatherIcon.enabled = $(this).prop('checked');
saveSettings();
updateStripWidgets();
});
$('#rpg-toggle-strip-clock').on('change', function() {
if (!extensionSettings.desktopStripWidgets) extensionSettings.desktopStripWidgets = {};
if (!extensionSettings.desktopStripWidgets.clock) extensionSettings.desktopStripWidgets.clock = {};
extensionSettings.desktopStripWidgets.clock.enabled = $(this).prop('checked');
saveSettings();
updateStripWidgets();
});
$('#rpg-toggle-strip-date').on('change', function() {
if (!extensionSettings.desktopStripWidgets) extensionSettings.desktopStripWidgets = {};
if (!extensionSettings.desktopStripWidgets.date) extensionSettings.desktopStripWidgets.date = {};
extensionSettings.desktopStripWidgets.date.enabled = $(this).prop('checked');
saveSettings();
updateStripWidgets();
});
$('#rpg-toggle-strip-location').on('change', function() {
if (!extensionSettings.desktopStripWidgets) extensionSettings.desktopStripWidgets = {};
if (!extensionSettings.desktopStripWidgets.location) extensionSettings.desktopStripWidgets.location = {};
extensionSettings.desktopStripWidgets.location.enabled = $(this).prop('checked');
saveSettings();
updateStripWidgets();
});
$('#rpg-toggle-strip-stats').on('change', function() {
if (!extensionSettings.desktopStripWidgets) extensionSettings.desktopStripWidgets = {};
if (!extensionSettings.desktopStripWidgets.stats) extensionSettings.desktopStripWidgets.stats = {};
extensionSettings.desktopStripWidgets.stats.enabled = $(this).prop('checked');
saveSettings();
updateStripWidgets();
});
$('#rpg-toggle-strip-attributes').on('change', function() {
if (!extensionSettings.desktopStripWidgets) extensionSettings.desktopStripWidgets = {};
if (!extensionSettings.desktopStripWidgets.attributes) extensionSettings.desktopStripWidgets.attributes = {};
extensionSettings.desktopStripWidgets.attributes.enabled = $(this).prop('checked');
saveSettings();
updateStripWidgets();
});
// Manual update button
$('#rpg-manual-update').on('click', async function() {
if (!extensionSettings.enabled) {
return;
}
const currentChat = getContext().chat;
let lastAssistantIndex = -1;
for (let i = currentChat.length - 1; i >= 0; i--) {
if (!currentChat[i].is_user && !currentChat[i].is_system) {
lastAssistantIndex = i;
break;
}
}
if (lastAssistantIndex !== -1) {
commitTrackerDataFromPriorMessage(lastAssistantIndex);
}
await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory);
});
// Strip widget refresh button - same functionality as main refresh button
$('#rpg-strip-refresh').on('click', async function() {
if (!extensionSettings.enabled) {
return;
}
const currentChat = getContext().chat;
let lastAssistantIndex = -1;
for (let i = currentChat.length - 1; i >= 0; i--) {
if (!currentChat[i].is_user && !currentChat[i].is_system) {
lastAssistantIndex = i;
break;
}
}
if (lastAssistantIndex !== -1) {
commitTrackerDataFromPriorMessage(lastAssistantIndex);
}
await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory);
});
// Stat bar colors
$('#rpg-stat-bar-color-low').on('change', function() {
extensionSettings.statBarColorLow = String($(this).val());
saveSettings();
renderUserStats(); // Re-render with new colors
});
$('#rpg-stat-bar-color-low-opacity').on('input', function() {
const opacity = Number($(this).val());
extensionSettings.statBarColorLowOpacity = opacity;
$('#rpg-stat-bar-color-low-opacity-value').text(opacity + '%');
renderUserStats();
}).on('change', function() {
saveSettings();
});
$('#rpg-stat-bar-color-high').on('change', function() {
extensionSettings.statBarColorHigh = String($(this).val());
saveSettings();
renderUserStats(); // Re-render with new colors
});
$('#rpg-stat-bar-color-high-opacity').on('input', function() {
const opacity = Number($(this).val());
extensionSettings.statBarColorHighOpacity = opacity;
$('#rpg-stat-bar-color-high-opacity-value').text(opacity + '%');
renderUserStats();
}).on('change', function() {
saveSettings();
});
// Theme selection
$('#rpg-theme-select').on('change', function() {
extensionSettings.theme = String($(this).val());
saveSettings();
applyTheme();
toggleCustomColors();
updateSettingsPopupTheme(getSettingsModal()); // Update popup theme instantly
updateChatThoughts(); // Recreate thought bubbles with new theme
});
// Custom color pickers
$('#rpg-custom-bg').on('change', function() {
extensionSettings.customColors.bg = String($(this).val());
saveSettings();
if (extensionSettings.theme === 'custom') {
applyCustomTheme();
updateSettingsPopupTheme(getSettingsModal());
updateChatThoughts();
}
});
$('#rpg-custom-bg-opacity').on('input', function() {
const opacity = Number($(this).val());
extensionSettings.customColors.bgOpacity = opacity;
$('#rpg-custom-bg-opacity-value').text(opacity + '%');
if (extensionSettings.theme === 'custom') {
applyCustomTheme();
updateSettingsPopupTheme(getSettingsModal());
updateChatThoughts();
}
}).on('change', function() {
saveSettings();
});
$('#rpg-custom-accent').on('change', function() {
extensionSettings.customColors.accent = String($(this).val());
saveSettings();
if (extensionSettings.theme === 'custom') {
applyCustomTheme();
updateSettingsPopupTheme(getSettingsModal());
updateChatThoughts();
}
});
$('#rpg-custom-accent-opacity').on('input', function() {
const opacity = Number($(this).val());
extensionSettings.customColors.accentOpacity = opacity;
$('#rpg-custom-accent-opacity-value').text(opacity + '%');
if (extensionSettings.theme === 'custom') {
applyCustomTheme();
updateSettingsPopupTheme(getSettingsModal());
updateChatThoughts();
}
}).on('change', function() {
saveSettings();
});
$('#rpg-custom-text').on('change', function() {
extensionSettings.customColors.text = String($(this).val());
saveSettings();
if (extensionSettings.theme === 'custom') {
applyCustomTheme();
updateSettingsPopupTheme(getSettingsModal());
updateChatThoughts();
}
});
$('#rpg-custom-text-opacity').on('input', function() {
const opacity = Number($(this).val());
extensionSettings.customColors.textOpacity = opacity;
$('#rpg-custom-text-opacity-value').text(opacity + '%');
if (extensionSettings.theme === 'custom') {
applyCustomTheme();
updateSettingsPopupTheme(getSettingsModal());
updateChatThoughts();
}
}).on('change', function() {
saveSettings();
});
$('#rpg-custom-highlight').on('change', function() {
extensionSettings.customColors.highlight = String($(this).val());
saveSettings();
if (extensionSettings.theme === 'custom') {
applyCustomTheme();
updateSettingsPopupTheme(getSettingsModal());
updateChatThoughts();
}
});
$('#rpg-custom-highlight-opacity').on('input', function() {
const opacity = Number($(this).val());
extensionSettings.customColors.highlightOpacity = opacity;
$('#rpg-custom-highlight-opacity-value').text(opacity + '%');
if (extensionSettings.theme === 'custom') {
applyCustomTheme();
updateSettingsPopupTheme(getSettingsModal());
updateChatThoughts();
}
}).on('change', function() {
saveSettings();
});
// External API settings event handlers
$('#rpg-external-base-url').on('change', function() {
if (!extensionSettings.externalApiSettings) {
extensionSettings.externalApiSettings = {
baseUrl: '', apiKey: '', model: '', maxTokens: 8192, temperature: 0.7
};
}
extensionSettings.externalApiSettings.baseUrl = String($(this).val()).trim();
saveSettings();
});
$('#rpg-external-api-key').on('change', function() {
// Securely store API key in localStorage instead of shared extension settings
const apiKey = String($(this).val()).trim();
localStorage.setItem('rpg_companion_external_api_key', apiKey);
// Ensure the externalApiSettings object exists, but don't store the key in it
if (!extensionSettings.externalApiSettings) {
extensionSettings.externalApiSettings = {
baseUrl: '', model: '', maxTokens: 8192, temperature: 0.7
};
saveSettings();
}
});
$('#rpg-external-model').on('change', function() {
if (!extensionSettings.externalApiSettings) {
extensionSettings.externalApiSettings = {
baseUrl: '', apiKey: '', model: '', maxTokens: 8192, temperature: 0.7
};
}
extensionSettings.externalApiSettings.model = String($(this).val()).trim();
saveSettings();
});
$('#rpg-external-max-tokens').on('change', function() {
if (!extensionSettings.externalApiSettings) {
extensionSettings.externalApiSettings = {
baseUrl: '', apiKey: '', model: '', maxTokens: 8192, temperature: 0.7
};
}
extensionSettings.externalApiSettings.maxTokens = parseInt(String($(this).val()));
saveSettings();
});
$('#rpg-external-temperature').on('change', function() {
if (!extensionSettings.externalApiSettings) {
extensionSettings.externalApiSettings = {
baseUrl: '', apiKey: '', model: '', maxTokens: 8192, temperature: 0.7
};
}
extensionSettings.externalApiSettings.temperature = parseFloat(String($(this).val()));
saveSettings();
});
$('#rpg-toggle-api-key-visibility').on('click', function() {
const $input = $('#rpg-external-api-key');
const type = $input.attr('type') === 'password' ? 'text' : 'password';
$input.attr('type', type);
$(this).find('i').toggleClass('fa-eye fa-eye-slash');
});
$('#rpg-test-external-api').on('click', async function() {
const $result = $('#rpg-external-api-test-result');
const $btn = $(this);
const originalText = $btn.html();
$btn.html('<i class="fa-solid fa-spinner fa-spin"></i> Testing...').prop('disabled', true);
$result.hide().removeClass('rpg-success-message rpg-error-message');
try {
const result = await testExternalAPIConnection();
if (result.success) {
$result.addClass('rpg-success-message')
.html(`<i class="fa-solid fa-check-circle"></i> ${result.message}`)
.slideDown();
toastr.success(result.message);
} else {
$result.addClass('rpg-error-message')
.html(`<i class="fa-solid fa-exclamation-circle"></i> ${result.message}`)
.slideDown();
toastr.error(result.message);
}
} catch (error) {
$result.addClass('rpg-error-message')
.html(`<i class="fa-solid fa-exclamation-circle"></i> Error: ${error.message}`)
.slideDown();
} finally {
$btn.html(originalText).prop('disabled', false);
}
});
}
/**
* Synchronize all settings modal inputs with the currently saved settings.
* Called once during UI initialization after the template is in the DOM.
*/
export function initializeSettingsUIState() {
// Initialize UI state (enable/disable is in Extensions tab)
$('#rpg-toggle-auto-update').prop('checked', extensionSettings.autoUpdate);
$('#rpg-position-select').val(extensionSettings.panelPosition);
$('#rpg-update-depth').val(extensionSettings.updateDepth);
$('#rpg-toggle-user-stats').prop('checked', extensionSettings.showUserStats);
$('#rpg-toggle-info-box').prop('checked', extensionSettings.showInfoBox);
$('#rpg-toggle-thoughts').prop('checked', extensionSettings.showCharacterThoughts);
$('#rpg-toggle-alt-present-characters').prop('checked', extensionSettings.showAlternatePresentCharactersPanel ?? false);
$('#rpg-toggle-thought-based-expressions').prop('checked', extensionSettings.enableThoughtBasedExpressions === true);
$('#rpg-toggle-hide-default-expressions').prop('checked', extensionSettings.hideDefaultExpressionDisplay === true);
$('#rpg-toggle-inventory').prop('checked', extensionSettings.showInventory);
$('#rpg-toggle-equipment').prop('checked', extensionSettings.showEquipment);
$('#rpg-toggle-quests').prop('checked', extensionSettings.showQuests);
$('#rpg-toggle-lock-icons').prop('checked', extensionSettings.showLockIcons ?? true);
$('#rpg-toggle-thoughts-in-chat').prop('checked', extensionSettings.showThoughtsInChat);
$('#rpg-toggle-inline-thoughts').prop('checked', (extensionSettings.thoughtsInChatStyle || 'corner') === 'inline');
$('#rpg-toggle-html-prompt').prop('checked', extensionSettings.enableHtmlPrompt);
$('#rpg-toggle-dialogue-coloring').prop('checked', extensionSettings.enableDialogueColoring);
$('#rpg-toggle-deception').prop('checked', extensionSettings.enableDeceptionSystem ?? false);
$('#rpg-toggle-omniscience').prop('checked', extensionSettings.enableOmniscienceFilter ?? false);
$('#rpg-toggle-cyoa').prop('checked', extensionSettings.enableCYOA ?? false);
$('#rpg-toggle-spotify-music').prop('checked', extensionSettings.enableSpotifyMusic);
$('#rpg-toggle-dynamic-weather').prop('checked', extensionSettings.enableDynamicWeather);
$('#rpg-toggle-narrator').prop('checked', extensionSettings.narratorMode);
// Feature toggle visibility settings
$('#rpg-toggle-show-html-toggle').prop('checked', extensionSettings.showHtmlToggle ?? true);
$('#rpg-toggle-show-dialogue-coloring-toggle').prop('checked', extensionSettings.showDialogueColoringToggle ?? true);
$('#rpg-toggle-show-deception-toggle').prop('checked', extensionSettings.showDeceptionToggle ?? true);
$('#rpg-toggle-show-omniscience-toggle').prop('checked', extensionSettings.showOmniscienceToggle ?? true);
$('#rpg-toggle-show-cyoa-toggle').prop('checked', extensionSettings.showCYOAToggle ?? true);
$('#rpg-toggle-show-spotify-toggle').prop('checked', extensionSettings.showSpotifyToggle ?? true);
$('#rpg-toggle-show-dynamic-weather-toggle').prop('checked', extensionSettings.showDynamicWeatherToggle ?? true);
$('#rpg-toggle-weather-background').prop('checked', extensionSettings.weatherBackground ?? true);
$('#rpg-toggle-weather-foreground').prop('checked', extensionSettings.weatherForeground ?? false);
$('#rpg-toggle-show-narrator-mode').prop('checked', extensionSettings.showNarratorMode ?? true);
$('#rpg-toggle-show-auto-avatars').prop('checked', extensionSettings.showAutoAvatars ?? true);
$('#rpg-toggle-randomized-plot').prop('checked', extensionSettings.enableRandomizedPlot ?? true);
$('#rpg-toggle-natural-plot').prop('checked', extensionSettings.enableNaturalPlot ?? true);
$('#rpg-toggle-encounters').prop('checked', extensionSettings.encounterSettings?.enabled ?? true);
$('#rpg-encounter-history-depth').val(extensionSettings.encounterSettings?.historyDepth ?? 8);
$('#rpg-toggle-autosave-logs').prop('checked', extensionSettings.encounterSettings?.autoSaveLogs ?? true);
// Initialize avatar options (panel toggle)
$('#rpg-toggle-auto-avatars-panel').prop('checked', extensionSettings.autoGenerateAvatars || false);
$('#rpg-toggle-dice-display').prop('checked', extensionSettings.showDiceDisplay);
// Initialize Mobile FAB Widget checkboxes
const fabWidgets = extensionSettings.mobileFabWidgets || {};
$('#rpg-toggle-fab-widgets-enabled').prop('checked', fabWidgets.enabled || false);
$('#rpg-toggle-fab-weather-icon').prop('checked', fabWidgets.weatherIcon?.enabled || false);
$('#rpg-toggle-fab-weather-desc').prop('checked', fabWidgets.weatherDesc?.enabled || false);
$('#rpg-toggle-fab-clock').prop('checked', fabWidgets.clock?.enabled || false);
$('#rpg-toggle-fab-date').prop('checked', fabWidgets.date?.enabled || false);
$('#rpg-toggle-fab-location').prop('checked', fabWidgets.location?.enabled || false);
$('#rpg-toggle-fab-stats').prop('checked', fabWidgets.stats?.enabled || false);
$('#rpg-toggle-fab-attributes').prop('checked', fabWidgets.attributes?.enabled || false);
// Toggle visibility of widget options based on master toggle
$('#rpg-fab-widget-options').toggle(fabWidgets.enabled || false);
// Initialize Desktop Strip Widget checkboxes
const stripWidgets = extensionSettings.desktopStripWidgets || {};
$('#rpg-toggle-strip-widgets-enabled').prop('checked', stripWidgets.enabled || false);
$('#rpg-toggle-strip-weather-icon').prop('checked', stripWidgets.weatherIcon?.enabled ?? true);
$('#rpg-toggle-strip-clock').prop('checked', stripWidgets.clock?.enabled ?? true);
$('#rpg-toggle-strip-date').prop('checked', stripWidgets.date?.enabled ?? true);
$('#rpg-toggle-strip-location').prop('checked', stripWidgets.location?.enabled ?? true);
$('#rpg-toggle-strip-stats').prop('checked', stripWidgets.stats?.enabled ?? true);
$('#rpg-toggle-strip-attributes').prop('checked', stripWidgets.attributes?.enabled ?? true);
// Toggle visibility of strip widget options based on master toggle
$('#rpg-strip-widget-options').toggle(stripWidgets.enabled || false);
$('#rpg-stat-bar-color-low').val(extensionSettings.statBarColorLow);
$('#rpg-stat-bar-color-low-opacity').val(extensionSettings.statBarColorLowOpacity ?? 100);
$('#rpg-stat-bar-color-low-opacity-value').text((extensionSettings.statBarColorLowOpacity ?? 100) + '%');
$('#rpg-stat-bar-color-high').val(extensionSettings.statBarColorHigh);
$('#rpg-stat-bar-color-high-opacity').val(extensionSettings.statBarColorHighOpacity ?? 100);
$('#rpg-stat-bar-color-high-opacity-value').text((extensionSettings.statBarColorHighOpacity ?? 100) + '%');
$('#rpg-theme-select').val(extensionSettings.theme);
$('#rpg-custom-bg').val(extensionSettings.customColors.bg);
$('#rpg-custom-bg-opacity').val(extensionSettings.customColors.bgOpacity ?? 100);
$('#rpg-custom-bg-opacity-value').text((extensionSettings.customColors.bgOpacity ?? 100) + '%');
$('#rpg-custom-accent').val(extensionSettings.customColors.accent);
$('#rpg-custom-accent-opacity').val(extensionSettings.customColors.accentOpacity ?? 100);
$('#rpg-custom-accent-opacity-value').text((extensionSettings.customColors.accentOpacity ?? 100) + '%');
$('#rpg-custom-text').val(extensionSettings.customColors.text);
$('#rpg-custom-text-opacity').val(extensionSettings.customColors.textOpacity ?? 100);
$('#rpg-custom-text-opacity-value').text((extensionSettings.customColors.textOpacity ?? 100) + '%');
$('#rpg-custom-highlight').val(extensionSettings.customColors.highlight);
$('#rpg-custom-highlight-opacity').val(extensionSettings.customColors.highlightOpacity ?? 100);
$('#rpg-custom-highlight-opacity-value').text((extensionSettings.customColors.highlightOpacity ?? 100) + '%');
// Initialize External API settings values
if (extensionSettings.externalApiSettings) {
$('#rpg-external-base-url').val(extensionSettings.externalApiSettings.baseUrl || '');
// Load API Key from secure localStorage
const storedApiKey = localStorage.getItem('rpg_companion_external_api_key') || '';
$('#rpg-external-api-key').val(storedApiKey);
$('#rpg-external-model').val(extensionSettings.externalApiSettings.model || '');
$('#rpg-external-max-tokens').val(extensionSettings.externalApiSettings.maxTokens || 8192);
$('#rpg-external-temperature').val(extensionSettings.externalApiSettings.temperature ?? 0.7);
}
$('#rpg-generation-mode').val(extensionSettings.generationMode);
$('#rpg-skip-guided-mode').val(extensionSettings.skipInjectionsForGuided);
// Weather sub-options visibility follows the dynamic weather toggle visibility setting
updateWeatherSubOptionsVisibility();
}
/**
* Helper: Bind a simple section visibility toggle
*/
function bindSectionToggle($, elementId, settingKey) {
$(`#${elementId}`).on('change', function() {
extensionSettings[settingKey] = $(this).prop('checked');
saveSettings();
updateSectionVisibility();
});
}
/**
* Updates the visibility of weather sub-options in settings based on dynamic weather toggle
*/
export function updateWeatherSubOptionsVisibility() {
const $weatherSubOptions = $('#rpg-weather-suboptions');
const isDynamicWeatherEnabled = extensionSettings.showDynamicWeatherToggle ?? true;
if (isDynamicWeatherEnabled) {
$weatherSubOptions.show();
} else {
$weatherSubOptions.hide();
}
}
+1 -1
View File
@@ -1671,7 +1671,7 @@ function setupHistoryPersistenceListeners() {
messageCount: 5,
injectionPosition: 'assistant_message_end',
contextPreamble: '',
externalApiOnly: false
sendAllEnabledOnRefresh: false
};
}
+1 -1
View File
@@ -225,7 +225,7 @@ function getCurrentWeather() {
// Try to parse as JSON first (new format)
try {
const parsed = typeof infoBoxData === 'string' ? JSON.parse(infoBoxData) : infoBoxData;
const parsed = typeof infoBoxData === 'string' ? repairJSON(infoBoxData) : infoBoxData;
if (parsed && parsed.weather) {
// Return the forecast text from the weather object
return parsed.weather.forecast || parsed.weather.emoji || null;
+41 -15
View File
@@ -3,6 +3,31 @@
* Handles parsing and repairing malformed JSON from AI responses
*/
/* ===== Pre-compiled regex patterns (module-level constants) ===== */
// Markdown code fence removal
const MARKDOWN_JSON_FENCE_RE = /```json\s*/gi;
const MARKDOWN_GENERIC_FENCE_RE = /```\s*/g;
// Thinking tag removal
const THINKING_TAG_RE = /<think>[\s\S]*?<\/think>/gi;
const THINKING_TAG_ALT_RE = /<thinking>[\s\S]*?<\/thinking>/gi;
// JSON repair patterns
const TRAILING_COMMA_RE = /,(\s*[}\]])/g;
const JS_LINE_COMMENT_RE = /\/\/.*$/gm;
const JS_BLOCK_COMMENT_RE = /\/\*[\s\S]*?\*\//g;
// JSON extraction patterns
const JSON_OBJECT_RE = /\{[\s\S]*\}/;
const JSON_ARRAY_RE = /\[[\s\S]*\]/;
// extractJSONFromText patterns
const FENCE_JSON_RE = /```json\s*([\s\S]*?)```/i;
const FENCE_GENERIC_RE = /```\s*([\s\S]*?)```/;
const STANDALONE_OBJECT_RE = /\{[\s\S]*\}/;
const STANDALONE_ARRAY_RE = /\[[\s\S]*\]/;
/**
* Repairs malformed JSON from AI responses
* Handles common AI mistakes like trailing commas, missing commas, wrong quotes, etc.
@@ -23,17 +48,17 @@ export function repairJSON(jsonString) {
}
// Remove markdown code fences
cleaned = cleaned.replace(/```json\s*/gi, '');
cleaned = cleaned.replace(/```\s*/g, '');
cleaned = cleaned.replace(MARKDOWN_JSON_FENCE_RE, '');
cleaned = cleaned.replace(MARKDOWN_GENERIC_FENCE_RE, '');
// Remove thinking tags (model's internal reasoning)
cleaned = cleaned.replace(/<think>[\s\S]*?<\/think>/gi, '');
cleaned = cleaned.replace(/<thinking>[\s\S]*?<\/thinking>/gi, '');
cleaned = cleaned.replace(THINKING_TAG_RE, '');
cleaned = cleaned.replace(THINKING_TAG_ALT_RE, '');
// Fix common JSON errors:
// 1. Trailing commas before closing brackets
cleaned = cleaned.replace(/,(\s*[}\]])/g, '$1');
cleaned = cleaned.replace(TRAILING_COMMA_RE, '$1');
// 2. Missing commas between properties - DISABLED because it corrupts valid JSON
// Modern AI models send properly formatted JSON, so this aggressive repair is not needed
@@ -49,32 +74,33 @@ export function repairJSON(jsonString) {
// cleaned = cleaned.replace(/(\{|,)\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*:/g, '$1"$2":');
// 5. Remove JavaScript comments
cleaned = cleaned.replace(/\/\/.*$/gm, '');
cleaned = cleaned.replace(/\/\*[\s\S]*?\*\//g, '');
cleaned = cleaned.replace(JS_LINE_COMMENT_RE, '');
cleaned = cleaned.replace(JS_BLOCK_COMMENT_RE, '');
// Attempt 1: Standard JSON.parse
try {
return JSON.parse(cleaned);
} catch (e) {
console.debug('[RPG JSON Repair] Attempt 1 (JSON.parse) failed:', e.message);
}
// Attempt 2: Extract JSON object between first { and last }
const objectMatch = cleaned.match(/\{[\s\S]*\}/);
const objectMatch = cleaned.match(JSON_OBJECT_RE);
if (objectMatch) {
try {
return JSON.parse(objectMatch[0]);
} catch (e) {
// Silent fail, try next method
console.debug('[RPG JSON Repair] Attempt 2 (object extraction) failed:', e.message);
}
}
// Attempt 3: Try to extract JSON array between first [ and last ]
const arrayMatch = cleaned.match(/\[[\s\S]*\]/);
const arrayMatch = cleaned.match(JSON_ARRAY_RE);
if (arrayMatch) {
try {
return JSON.parse(arrayMatch[0]);
} catch (e) {
// Silent fail, try next method
console.debug('[RPG JSON Repair] Attempt 3 (array extraction) failed:', e.message);
}
}
@@ -149,14 +175,14 @@ export function extractJSONFromText(text) {
}
// Try to extract from ```json code fence
const fenceMatch = text.match(/```json\s*([\s\S]*?)```/i);
const fenceMatch = text.match(FENCE_JSON_RE);
if (fenceMatch && fenceMatch[1]) {
const trimmed = fenceMatch[1].trim();
if (trimmed) return trimmed;
}
// Try to extract from ``` code fence (without json label)
const genericFenceMatch = text.match(/```\s*([\s\S]*?)```/);
const genericFenceMatch = text.match(FENCE_GENERIC_RE);
if (genericFenceMatch && genericFenceMatch[1]) {
const content = genericFenceMatch[1].trim();
// Check if it looks like JSON (starts with { or [)
@@ -166,13 +192,13 @@ export function extractJSONFromText(text) {
}
// Try to find standalone JSON object
const objectMatch = text.match(/\{[\s\S]*\}/);
const objectMatch = text.match(STANDALONE_OBJECT_RE);
if (objectMatch && objectMatch[0].trim()) {
return objectMatch[0];
}
// Try to find standalone JSON array
const arrayMatch = text.match(/\[[\s\S]*\]/);
const arrayMatch = text.match(STANDALONE_ARRAY_RE);
if (arrayMatch && arrayMatch[0].trim()) {
return arrayMatch[0];
}
+1 -1
View File
@@ -10,7 +10,7 @@
* intercepts the raw fetch response as a fallback.
*/
import { generateRaw } from '../../../../../../../script.js';
import { generateRaw } from '../../../../../../script.js';
/**
* Extracts text from any API response shape (Anthropic content-block arrays,
+75 -49
View File
@@ -23,6 +23,70 @@ const BLOCKED_PROPERTY_NAMES = [
'__lookupSetter__'
];
/**
* Creates a generic string validator/sanitizer with configurable rules.
* Returns a function that validates and sanitizes input strings.
*
* @param {Object} options - Validator configuration
* @param {string} options.label - Human-readable label for logging (e.g., 'location name', 'item name')
* @param {number} options.maxLength - Maximum allowed string length
* @param {string[]} [options.blockedNames] - Array of blocked names (defaults to BLOCKED_PROPERTY_NAMES)
* @param {string[]} [options.additionalRejects] - Additional values to reject (lowercase comparison, e.g., ['none'])
* @param {boolean} [options.checkBlockedNames] - Whether to check against blocked property names (default: true)
* @returns {Function} Sanitizer function: (string) => string|null
*
* @example
* const sanitizeLocation = createStringValidator({
* label: 'location name',
* maxLength: 200,
* checkBlockedNames: true
* });
* const sanitizeItem = createStringValidator({
* label: 'item name',
* maxLength: 500,
* additionalRejects: ['none'],
* checkBlockedNames: false
* });
*/
export function createStringValidator(options) {
const { label, maxLength, blockedNames = BLOCKED_PROPERTY_NAMES, additionalRejects = [], checkBlockedNames = true } = options;
return function sanitize(name) {
if (!name || typeof name !== 'string') {
return null;
}
const trimmed = name.trim();
// Empty check
if (trimmed === '') {
return null;
}
// Additional rejects (e.g., 'none' for items)
if (additionalRejects.length > 0 && additionalRejects.includes(trimmed.toLowerCase())) {
return null;
}
// Check for dangerous property names (case-insensitive)
if (checkBlockedNames) {
const lowerName = trimmed.toLowerCase();
if (blockedNames.some(blocked => lowerName === blocked.toLowerCase())) {
console.warn(`[RPG Companion] Blocked dangerous ${label}: "${trimmed}"`);
return null;
}
}
// Max length check
if (trimmed.length > maxLength) {
console.warn(`[RPG Companion] ${label.charAt(0).toUpperCase() + label.slice(1)} too long (${trimmed.length} chars), truncating to ${maxLength}`);
return trimmed.slice(0, maxLength);
}
return trimmed;
};
}
/**
* Validates and sanitizes storage location names.
* Prevents prototype pollution and object property shadowing attacks.
@@ -35,34 +99,11 @@ const BLOCKED_PROPERTY_NAMES = [
* sanitizeLocationName("__proto__") // null (blocked, logs warning)
* sanitizeLocationName("A".repeat(300)) // "AAA..." (truncated to 200 chars)
*/
export function sanitizeLocationName(name) {
if (!name || typeof name !== 'string') {
return null;
}
const trimmed = name.trim();
// Empty check
if (trimmed === '') {
return null;
}
// Check for dangerous property names (case-insensitive)
const lowerName = trimmed.toLowerCase();
if (BLOCKED_PROPERTY_NAMES.some(blocked => lowerName === blocked.toLowerCase())) {
console.warn(`[RPG Companion] Blocked dangerous location name: "${trimmed}"`);
return null;
}
// Max length check (reasonable location name)
const MAX_LOCATION_LENGTH = 200;
if (trimmed.length > MAX_LOCATION_LENGTH) {
console.warn(`[RPG Companion] Location name too long (${trimmed.length} chars), truncating to ${MAX_LOCATION_LENGTH}`);
return trimmed.slice(0, MAX_LOCATION_LENGTH);
}
return trimmed;
}
export const sanitizeLocationName = createStringValidator({
label: 'location name',
maxLength: 200,
checkBlockedNames: true
});
/**
* Validates and sanitizes item names.
@@ -76,27 +117,12 @@ export function sanitizeLocationName(name) {
* sanitizeItemName("") // null
* sanitizeItemName("A".repeat(600)) // "AAA..." (truncated to 500 chars)
*/
export function sanitizeItemName(name) {
if (!name || typeof name !== 'string') {
return null;
}
const trimmed = name.trim();
// Empty check
if (trimmed === '' || trimmed.toLowerCase() === 'none') {
return null;
}
// Max length check (reasonable item name with description)
const MAX_ITEM_LENGTH = 500;
if (trimmed.length > MAX_ITEM_LENGTH) {
console.warn(`[RPG Companion] Item name too long (${trimmed.length} chars), truncating to ${MAX_ITEM_LENGTH}`);
return trimmed.slice(0, MAX_ITEM_LENGTH);
}
return trimmed;
}
export const sanitizeItemName = createStringValidator({
label: 'item name',
maxLength: 500,
additionalRejects: ['none'],
checkBlockedNames: false
});
/**
* Validates and cleans a stored inventory object.
+2 -2
View File
@@ -1,7 +1,6 @@
import { Fuse } from '../../../../../../lib.js';
import {
characters,
eventSource,
event_types,
generateQuietPrompt,
generateRaw,
@@ -11,6 +10,7 @@ import {
substituteParamsExtended,
this_chid
} from '../../../../../../script.js';
import { once as onceEvent } from '../core/events.js';
import {
doExtrasFetch,
extension_settings as stExtensionSettings,
@@ -562,7 +562,7 @@ export async function classifyExpressionText(text, { characterName = '' } = {})
}
};
eventSource.once(event_types.TEXT_COMPLETION_SETTINGS_READY, onReady);
onceEvent(event_types.TEXT_COMPLETION_SETTINGS_READY, onReady);
const responseText = settings.promptType === PROMPT_TYPE.full
? await generateQuietPrompt({ quietPrompt: prompt })
+305 -221
View File
@@ -1,3 +1,7 @@
/* ============================================ */
/* _02_panel.css */
/* ============================================ */
/* Ensure chat stays centered when panel is visible */
body:has(.rpg-panel.rpg-position-right) #sheld {
@@ -263,6 +267,12 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
font-size: clamp(1rem, 1.3vw, 1.3rem);
}
/* ============================================ */
/* _03_components.css */
/* ============================================ */
/* ============================================
ANIMATIONS
============================================ */
@@ -581,6 +591,12 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
display: none; /* Remove diamond decoration for more space */
}
/* ============================================ */
/* _04_user-stats.css */
/* ============================================ */
/* ============================================
USER STATS SECTION
============================================ */
@@ -1216,6 +1232,12 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
transform: scale(0.95);
}
/* ============================================ */
/* _05_info-box.css */
/* ============================================ */
/* ============================================
INFO BOX SECTION
============================================ */
@@ -1956,6 +1978,12 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
transform: translateX(0.312rem);
}
/* ============================================ */
/* _06_thoughts.css */
/* ============================================ */
/* ============================================
CHARACTER THOUGHTS SECTION
============================================ */
@@ -2635,6 +2663,12 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
.rpg-stat-row.updating {
animation: pulse 0.5s ease-in-out;
}
/* ============================================ */
/* _07_settings.css */
/* ============================================ */
/* ============================================
SETTINGS SECTION
============================================ */
@@ -3240,6 +3274,12 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
/* Lorebook Limiter styles removed - feature deprecated */
/* ============================================ */
/* _08_themes.css */
/* ============================================ */
/* ============================================
THEME VARIATIONS
============================================ */
@@ -3252,6 +3292,12 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
--rpg-highlight: #ff006e;
--rpg-border: #8b00ff;
--rpg-shadow: rgba(139, 0, 255, 0.5);
--rpg-content-bg: linear-gradient(135deg, #1a1f3a 0%, #0a0e27 100%);
--rpg-content-box-shadow: 0 0 40px rgba(139, 0, 255, 0.4), inset 0 0 30px rgba(0, 0, 0, 0.5);
--rpg-content-border: 2px solid #8b00ff;
--rpg-header-text-shadow: 0 0 20px #ff006e, 0 0 40px #8b00ff;
--rpg-divider-bg: linear-gradient(to right, transparent, #8b00ff, #ff006e, #8b00ff, transparent);
--rpg-thoughts-bg: linear-gradient(135deg, rgba(10, 14, 39, 0.9) 0%, rgba(26, 31, 58, 0.8) 50%, rgba(10, 14, 39, 0.9) 100%);
}
/* Apply sci-fi theme to thought panel */
@@ -3267,32 +3313,6 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
--rpg-shadow: rgba(139, 0, 255, 0.5);
}
.rpg-panel[data-theme="sci-fi"] .rpg-content-box {
background: linear-gradient(135deg, #1a1f3a 0%, #0a0e27 100%);
box-shadow: 0 0 40px rgba(139, 0, 255, 0.4), inset 0 0 30px rgba(0, 0, 0, 0.5);
border: 2px solid #8b00ff;
}
.rpg-panel[data-theme="sci-fi"] .rpg-panel-header h3,
.rpg-panel[data-theme="sci-fi"] .rpg-stats-title,
.rpg-panel[data-theme="sci-fi"] .rpg-info-header,
.rpg-panel[data-theme="sci-fi"] .rpg-thoughts-header {
text-shadow: 0 0 20px #ff006e, 0 0 40px #8b00ff;
}
.rpg-panel[data-theme="sci-fi"] .rpg-divider {
background: linear-gradient(to right, transparent, #8b00ff, #ff006e, #8b00ff, transparent);
}
.rpg-panel[data-theme="sci-fi"] .rpg-thoughts-content::before {
background: linear-gradient(
135deg,
rgba(10, 14, 39, 0.9) 0%,
rgba(26, 31, 58, 0.8) 50%,
rgba(10, 14, 39, 0.9) 100%
);
}
/* Fantasy / Rustic Parchment Theme */
.rpg-panel[data-theme="fantasy"] {
--rpg-bg: #2b1810;
@@ -3301,6 +3321,16 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
--rpg-highlight: #d4af37;
--rpg-border: #8b6914;
--rpg-shadow: rgba(0, 0, 0, 0.7);
--rpg-bg-image: linear-gradient(rgba(43, 24, 16, 0.9), rgba(43, 24, 16, 0.9)), url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><filter id="noise"><feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="4" /></filter><rect width="100" height="100" filter="url(%23noise)" opacity="0.1"/></svg>');
--rpg-content-bg: linear-gradient(135deg, #3d2414 0%, #2b1810 100%);
--rpg-content-box-shadow: 0 8px 32px rgba(0, 0, 0, 0.8), inset 0 0 40px rgba(139, 105, 20, 0.2);
--rpg-content-border: 3px solid #8b6914;
--rpg-content-border-style: ridge;
--rpg-header-font-family: 'Georgia', serif;
--rpg-header-text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
--rpg-divider-content: '❦';
--rpg-divider-font-size: clamp(1rem, 1.5vw, 1.5rem);
--rpg-thoughts-bg: linear-gradient(135deg, rgba(43, 24, 16, 0.92) 0%, rgba(61, 36, 20, 0.88) 50%, rgba(43, 24, 16, 0.92) 100%);
}
/* Apply fantasy theme to thought panel */
@@ -3316,41 +3346,6 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
--rpg-shadow: rgba(0, 0, 0, 0.7);
}
.rpg-panel[data-theme="fantasy"] {
background-image:
linear-gradient(rgba(43, 24, 16, 0.9), rgba(43, 24, 16, 0.9)),
url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><filter id="noise"><feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="4" /></filter><rect width="100" height="100" filter="url(%23noise)" opacity="0.1"/></svg>');
}
.rpg-panel[data-theme="fantasy"] .rpg-content-box {
background: linear-gradient(135deg, #3d2414 0%, #2b1810 100%);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.8), inset 0 0 40px rgba(139, 105, 20, 0.2);
border: 3px solid #8b6914;
border-style: ridge;
}
.rpg-panel[data-theme="fantasy"] .rpg-panel-header h3,
.rpg-panel[data-theme="fantasy"] .rpg-stats-title,
.rpg-panel[data-theme="fantasy"] .rpg-info-header,
.rpg-panel[data-theme="fantasy"] .rpg-thoughts-header {
font-family: 'Georgia', serif;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
}
.rpg-panel[data-theme="fantasy"] .rpg-divider::after {
content: '❦';
font-size: clamp(1rem, 1.5vw, 1.5rem);
}
.rpg-panel[data-theme="fantasy"] .rpg-thoughts-content::before {
background: linear-gradient(
135deg,
rgba(43, 24, 16, 0.92) 0%,
rgba(61, 36, 20, 0.88) 50%,
rgba(43, 24, 16, 0.92) 100%
);
}
/* Cyberpunk / Neon Grid Theme */
.rpg-panel[data-theme="cyberpunk"] {
--rpg-bg: #000000;
@@ -3359,6 +3354,17 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
--rpg-highlight: #ff2a6d;
--rpg-border: #05d9e8;
--rpg-shadow: rgba(5, 217, 232, 0.5);
--rpg-bg-image: linear-gradient(rgba(0, 0, 0, 0.95), rgba(0, 0, 0, 0.95)), repeating-linear-gradient(0deg, rgba(5, 217, 232, 0.1) 0px, transparent 1px, transparent 2px, rgba(5, 217, 232, 0.1) 3px), repeating-linear-gradient(90deg, rgba(5, 217, 232, 0.1) 0px, transparent 1px, transparent 2px, rgba(5, 217, 232, 0.1) 3px);
--rpg-bg-size: 100% 100%, 30px 30px, 30px 30px;
--rpg-content-bg: linear-gradient(135deg, rgba(13, 13, 13, 0.9) 0%, rgba(0, 0, 0, 0.9) 100%);
--rpg-content-box-shadow: 0 0 40px rgba(255, 42, 109, 0.4), inset 0 0 30px rgba(5, 217, 232, 0.2);
--rpg-content-border: 2px solid #05d9e8;
--rpg-header-font-family: 'Courier New', monospace;
--rpg-header-letter-spacing: 0.125em;
--rpg-header-text-shadow: 0 0 10px #ff2a6d, 0 0 20px #05d9e8, 0 0 30px #ff2a6d;
--rpg-divider-bg: linear-gradient(to right, transparent, #05d9e8, #ff2a6d, #05d9e8, transparent);
--rpg-divider-height: 0.188rem;
--rpg-thoughts-bg: linear-gradient(135deg, rgba(0, 0, 0, 0.92) 0%, rgba(13, 13, 13, 0.85) 50%, rgba(0, 0, 0, 0.92) 100%);
}
/* Apply cyberpunk theme to thought panel */
@@ -3374,251 +3380,188 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
--rpg-shadow: rgba(5, 217, 232, 0.5);
}
.rpg-panel[data-theme="cyberpunk"] {
background: linear-gradient(rgba(0, 0, 0, 0.95), rgba(0, 0, 0, 0.95)),
repeating-linear-gradient(0deg, rgba(5, 217, 232, 0.1) 0px, transparent 1px, transparent 2px, rgba(5, 217, 232, 0.1) 3px),
repeating-linear-gradient(90deg, rgba(5, 217, 232, 0.1) 0px, transparent 1px, transparent 2px, rgba(5, 217, 232, 0.1) 3px);
background-size: 100% 100%, 30px 30px, 30px 30px;
}
.rpg-panel[data-theme="cyberpunk"] .rpg-content-box {
background: linear-gradient(135deg, rgba(13, 13, 13, 0.9) 0%, rgba(0, 0, 0, 0.9) 100%);
box-shadow: 0 0 40px rgba(255, 42, 109, 0.4), inset 0 0 30px rgba(5, 217, 232, 0.2);
border: 2px solid #05d9e8;
}
.rpg-panel[data-theme="cyberpunk"] .rpg-panel-header h3,
.rpg-panel[data-theme="cyberpunk"] .rpg-stats-title,
.rpg-panel[data-theme="cyberpunk"] .rpg-info-header,
.rpg-panel[data-theme="cyberpunk"] .rpg-thoughts-header {
text-shadow: 0 0 10px #ff2a6d, 0 0 20px #05d9e8, 0 0 30px #ff2a6d;
font-family: 'Courier New', monospace;
letter-spacing: 0.125em;
}
.rpg-panel[data-theme="cyberpunk"] .rpg-divider {
background: linear-gradient(to right, transparent, #05d9e8, #ff2a6d, #05d9e8, transparent);
height: 0.188rem;
}
.rpg-panel[data-theme="cyberpunk"] .rpg-thoughts-content::before {
background: linear-gradient(
135deg,
rgba(0, 0, 0, 0.92) 0%,
rgba(13, 13, 13, 0.85) 50%,
rgba(0, 0, 0, 0.92) 100%
);
}
/* ============================================
THEME SUPPORT FOR ALL PANEL ELEMENTS
Uses CSS custom properties defined above no selector duplication needed.
Any element inside .rpg-panel inherits the theme variables automatically.
============================================ */
/* Apply theme colors to tabs navigation */
.rpg-panel[data-theme="sci-fi"] .rpg-tabs-nav,
.rpg-panel[data-theme="fantasy"] .rpg-tabs-nav,
.rpg-panel[data-theme="cyberpunk"] .rpg-tabs-nav {
/* Apply theme background image if defined */
.rpg-panel[data-theme] {
background-image: var(--rpg-bg-image, none);
background-size: var(--rpg-bg-size, auto);
}
/* Content box theming via custom properties */
.rpg-panel[data-theme] .rpg-content-box {
background: var(--rpg-content-bg);
box-shadow: var(--rpg-content-box-shadow);
border: var(--rpg-content-border);
border-style: var(--rpg-content-border-style, solid);
}
/* Header theming */
.rpg-panel[data-theme] .rpg-panel-header h3,
.rpg-panel[data-theme] .rpg-stats-title,
.rpg-panel[data-theme] .rpg-info-header,
.rpg-panel[data-theme] .rpg-thoughts-header {
font-family: var(--rpg-header-font-family, inherit);
text-shadow: var(--rpg-header-text-shadow, none);
letter-spacing: var(--rpg-header-letter-spacing, normal);
}
/* Divider theming */
.rpg-panel[data-theme] .rpg-divider {
background: var(--rpg-divider-bg);
height: var(--rpg-divider-height, auto);
}
.rpg-panel[data-theme] .rpg-divider::after {
content: var(--rpg-divider-content, none);
font-size: var(--rpg-divider-font-size, inherit);
}
/* Thoughts content theming */
.rpg-panel[data-theme] .rpg-thoughts-content::before {
background: var(--rpg-thoughts-bg);
}
/* Tabs navigation theming */
.rpg-panel[data-theme] .rpg-tabs-nav {
border-bottom-color: var(--rpg-border);
}
.rpg-panel[data-theme="sci-fi"] .rpg-tab-btn,
.rpg-panel[data-theme="fantasy"] .rpg-tab-btn,
.rpg-panel[data-theme="cyberpunk"] .rpg-tab-btn {
.rpg-panel[data-theme] .rpg-tab-btn {
background: var(--rpg-accent);
border-color: var(--rpg-border);
color: var(--rpg-text);
}
.rpg-panel[data-theme="sci-fi"] .rpg-tab-btn:hover,
.rpg-panel[data-theme="fantasy"] .rpg-tab-btn:hover,
.rpg-panel[data-theme="cyberpunk"] .rpg-tab-btn:hover {
.rpg-panel[data-theme] .rpg-tab-btn:hover {
background: var(--rpg-highlight);
border-color: var(--rpg-highlight);
}
.rpg-panel[data-theme="sci-fi"] .rpg-tab-btn.active,
.rpg-panel[data-theme="fantasy"] .rpg-tab-btn.active,
.rpg-panel[data-theme="cyberpunk"] .rpg-tab-btn.active {
.rpg-panel[data-theme] .rpg-tab-btn.active {
background: var(--rpg-highlight);
border-color: var(--rpg-highlight);
color: var(--rpg-bg);
}
/* Apply theme colors to inventory subtabs */
.rpg-panel[data-theme="sci-fi"] .rpg-inventory-subtabs,
.rpg-panel[data-theme="fantasy"] .rpg-inventory-subtabs,
.rpg-panel[data-theme="cyberpunk"] .rpg-inventory-subtabs {
/* Inventory subtabs theming */
.rpg-panel[data-theme] .rpg-inventory-subtabs {
border-bottom-color: var(--rpg-border);
}
.rpg-panel[data-theme="sci-fi"] .rpg-inventory-subtab,
.rpg-panel[data-theme="fantasy"] .rpg-inventory-subtab,
.rpg-panel[data-theme="cyberpunk"] .rpg-inventory-subtab {
.rpg-panel[data-theme] .rpg-inventory-subtab {
border-color: var(--rpg-border);
color: var(--rpg-text);
}
.rpg-panel[data-theme="sci-fi"] .rpg-inventory-subtab:hover,
.rpg-panel[data-theme="fantasy"] .rpg-inventory-subtab:hover,
.rpg-panel[data-theme="cyberpunk"] .rpg-inventory-subtab:hover {
.rpg-panel[data-theme] .rpg-inventory-subtab:hover {
border-color: var(--rpg-highlight);
color: var(--rpg-highlight);
}
.rpg-panel[data-theme="sci-fi"] .rpg-inventory-subtab.active,
.rpg-panel[data-theme="fantasy"] .rpg-inventory-subtab.active,
.rpg-panel[data-theme="cyberpunk"] .rpg-inventory-subtab.active {
.rpg-panel[data-theme] .rpg-inventory-subtab.active {
border-color: var(--rpg-highlight);
color: var(--rpg-highlight);
}
/* Apply theme colors to quests subtabs */
.rpg-panel[data-theme="sci-fi"] .rpg-quests-subtabs,
.rpg-panel[data-theme="fantasy"] .rpg-quests-subtabs,
.rpg-panel[data-theme="cyberpunk"] .rpg-quests-subtabs {
/* Quests subtabs theming */
.rpg-panel[data-theme] .rpg-quests-subtabs {
border-bottom-color: var(--rpg-border);
}
.rpg-panel[data-theme="sci-fi"] .rpg-quests-subtab,
.rpg-panel[data-theme="fantasy"] .rpg-quests-subtab,
.rpg-panel[data-theme="cyberpunk"] .rpg-quests-subtab {
.rpg-panel[data-theme] .rpg-quests-subtab {
border-color: var(--rpg-border);
color: var(--rpg-text);
}
.rpg-panel[data-theme="sci-fi"] .rpg-quests-subtab:hover,
.rpg-panel[data-theme="fantasy"] .rpg-quests-subtab:hover,
.rpg-panel[data-theme="cyberpunk"] .rpg-quests-subtab:hover {
.rpg-panel[data-theme] .rpg-quests-subtab:hover {
border-color: var(--rpg-highlight);
color: var(--rpg-highlight);
}
.rpg-panel[data-theme="sci-fi"] .rpg-quests-subtab.active,
.rpg-panel[data-theme="fantasy"] .rpg-quests-subtab.active,
.rpg-panel[data-theme="cyberpunk"] .rpg-quests-subtab.active {
.rpg-panel[data-theme] .rpg-quests-subtab.active {
border-color: var(--rpg-highlight);
color: var(--rpg-highlight);
}
/* Apply theme colors to storage locations */
.rpg-panel[data-theme="sci-fi"] .rpg-storage-location,
.rpg-panel[data-theme="fantasy"] .rpg-storage-location,
.rpg-panel[data-theme="cyberpunk"] .rpg-storage-location {
/* Storage locations theming */
.rpg-panel[data-theme] .rpg-storage-location {
border-color: var(--rpg-border);
}
.rpg-panel[data-theme="sci-fi"] .rpg-storage-header,
.rpg-panel[data-theme="fantasy"] .rpg-storage-header,
.rpg-panel[data-theme="cyberpunk"] .rpg-storage-header {
.rpg-panel[data-theme] .rpg-storage-header {
background: var(--rpg-highlight);
}
.rpg-panel[data-theme="sci-fi"] .rpg-storage-content,
.rpg-panel[data-theme="fantasy"] .rpg-storage-content,
.rpg-panel[data-theme="cyberpunk"] .rpg-storage-content {
.rpg-panel[data-theme] .rpg-storage-content {
background: var(--rpg-accent);
}
/* Apply theme colors to buttons */
.rpg-panel[data-theme="sci-fi"] .rpg-inventory-edit-btn,
.rpg-panel[data-theme="sci-fi"] .rpg-inventory-add-btn,
.rpg-panel[data-theme="sci-fi"] .rpg-inventory-remove-btn,
.rpg-panel[data-theme="sci-fi"] .rpg-quest-edit,
.rpg-panel[data-theme="sci-fi"] .rpg-quest-remove,
.rpg-panel[data-theme="sci-fi"] .rpg-add-quest-btn,
.rpg-panel[data-theme="fantasy"] .rpg-inventory-edit-btn,
.rpg-panel[data-theme="fantasy"] .rpg-inventory-add-btn,
.rpg-panel[data-theme="fantasy"] .rpg-inventory-remove-btn,
.rpg-panel[data-theme="fantasy"] .rpg-quest-edit,
.rpg-panel[data-theme="fantasy"] .rpg-quest-remove,
.rpg-panel[data-theme="fantasy"] .rpg-add-quest-btn,
.rpg-panel[data-theme="cyberpunk"] .rpg-inventory-edit-btn,
.rpg-panel[data-theme="cyberpunk"] .rpg-inventory-add-btn,
.rpg-panel[data-theme="cyberpunk"] .rpg-inventory-remove-btn,
.rpg-panel[data-theme="cyberpunk"] .rpg-quest-edit,
.rpg-panel[data-theme="cyberpunk"] .rpg-quest-remove,
.rpg-panel[data-theme="cyberpunk"] .rpg-add-quest-btn {
/* Button theming */
.rpg-panel[data-theme] .rpg-inventory-edit-btn,
.rpg-panel[data-theme] .rpg-inventory-add-btn,
.rpg-panel[data-theme] .rpg-inventory-remove-btn,
.rpg-panel[data-theme] .rpg-quest-edit,
.rpg-panel[data-theme] .rpg-quest-remove,
.rpg-panel[data-theme] .rpg-add-quest-btn {
background: var(--rpg-accent);
border-color: var(--rpg-border);
color: var(--rpg-text);
}
.rpg-panel[data-theme="sci-fi"] .rpg-inventory-add-btn,
.rpg-panel[data-theme="sci-fi"] .rpg-add-quest-btn,
.rpg-panel[data-theme="fantasy"] .rpg-inventory-add-btn,
.rpg-panel[data-theme="fantasy"] .rpg-add-quest-btn,
.rpg-panel[data-theme="cyberpunk"] .rpg-inventory-add-btn,
.rpg-panel[data-theme="cyberpunk"] .rpg-add-quest-btn {
.rpg-panel[data-theme] .rpg-inventory-add-btn,
.rpg-panel[data-theme] .rpg-add-quest-btn {
background: transparent;
border-color: var(--rpg-highlight);
color: var(--rpg-highlight);
}
/* Apply theme colors to inventory/quest items */
.rpg-panel[data-theme="sci-fi"] .rpg-inventory-text,
.rpg-panel[data-theme="sci-fi"] .rpg-quest-item,
.rpg-panel[data-theme="sci-fi"] .rpg-main-quest-display,
.rpg-panel[data-theme="fantasy"] .rpg-inventory-text,
.rpg-panel[data-theme="fantasy"] .rpg-quest-item,
.rpg-panel[data-theme="fantasy"] .rpg-main-quest-display,
.rpg-panel[data-theme="cyberpunk"] .rpg-inventory-text,
.rpg-panel[data-theme="cyberpunk"] .rpg-quest-item,
.rpg-panel[data-theme="cyberpunk"] .rpg-main-quest-display {
/* Inventory/quest items theming */
.rpg-panel[data-theme] .rpg-inventory-text,
.rpg-panel[data-theme] .rpg-quest-item,
.rpg-panel[data-theme] .rpg-main-quest-display {
background: var(--rpg-accent);
border-color: var(--rpg-border);
color: var(--rpg-text);
}
.rpg-panel[data-theme="sci-fi"] .rpg-quest-item:hover,
.rpg-panel[data-theme="fantasy"] .rpg-quest-item:hover,
.rpg-panel[data-theme="cyberpunk"] .rpg-quest-item:hover {
.rpg-panel[data-theme] .rpg-quest-item:hover {
border-color: var(--rpg-highlight);
}
/* Apply theme colors to hints and empty states */
.rpg-panel[data-theme="sci-fi"] .rpg-inventory-hint,
.rpg-panel[data-theme="sci-fi"] .rpg-quest-hint,
.rpg-panel[data-theme="sci-fi"] .rpg-inventory-empty,
.rpg-panel[data-theme="sci-fi"] .rpg-quest-empty,
.rpg-panel[data-theme="fantasy"] .rpg-inventory-hint,
.rpg-panel[data-theme="fantasy"] .rpg-quest-hint,
.rpg-panel[data-theme="fantasy"] .rpg-inventory-empty,
.rpg-panel[data-theme="fantasy"] .rpg-quest-empty,
.rpg-panel[data-theme="cyberpunk"] .rpg-inventory-hint,
.rpg-panel[data-theme="cyberpunk"] .rpg-quest-hint,
.rpg-panel[data-theme="cyberpunk"] .rpg-inventory-empty,
.rpg-panel[data-theme="cyberpunk"] .rpg-quest-empty {
/* Hints and empty states theming */
.rpg-panel[data-theme] .rpg-inventory-hint,
.rpg-panel[data-theme] .rpg-quest-hint,
.rpg-panel[data-theme] .rpg-inventory-empty,
.rpg-panel[data-theme] .rpg-quest-empty {
border-color: var(--rpg-highlight);
color: var(--rpg-text);
}
/* Apply theme colors to input fields */
.rpg-panel[data-theme="sci-fi"] .rpg-inline-input,
.rpg-panel[data-theme="sci-fi"] .rpg-quest-edit-form input,
.rpg-panel[data-theme="sci-fi"] .rpg-quest-edit-form textarea,
.rpg-panel[data-theme="fantasy"] .rpg-inline-input,
.rpg-panel[data-theme="fantasy"] .rpg-quest-edit-form input,
.rpg-panel[data-theme="fantasy"] .rpg-quest-edit-form textarea,
.rpg-panel[data-theme="cyberpunk"] .rpg-inline-input,
.rpg-panel[data-theme="cyberpunk"] .rpg-quest-edit-form input,
.rpg-panel[data-theme="cyberpunk"] .rpg-quest-edit-form textarea {
/* Input fields theming */
.rpg-panel[data-theme] .rpg-inline-input,
.rpg-panel[data-theme] .rpg-quest-edit-form input,
.rpg-panel[data-theme] .rpg-quest-edit-form textarea {
background: var(--rpg-bg);
border-color: var(--rpg-border);
color: var(--rpg-text);
}
.rpg-panel[data-theme="sci-fi"] .rpg-inline-input:focus,
.rpg-panel[data-theme="sci-fi"] .rpg-quest-edit-form input:focus,
.rpg-panel[data-theme="sci-fi"] .rpg-quest-edit-form textarea:focus,
.rpg-panel[data-theme="fantasy"] .rpg-inline-input:focus,
.rpg-panel[data-theme="fantasy"] .rpg-quest-edit-form input:focus,
.rpg-panel[data-theme="fantasy"] .rpg-quest-edit-form textarea:focus,
.rpg-panel[data-theme="cyberpunk"] .rpg-inline-input:focus,
.rpg-panel[data-theme="cyberpunk"] .rpg-quest-edit-form input:focus,
.rpg-panel[data-theme="cyberpunk"] .rpg-quest-edit-form textarea:focus {
.rpg-panel[data-theme] .rpg-inline-input:focus,
.rpg-panel[data-theme] .rpg-quest-edit-form input:focus,
.rpg-panel[data-theme] .rpg-quest-edit-form textarea:focus {
border-color: var(--rpg-highlight);
}
/* ============================================ */
/* _09_dice-modal.css */
/* ============================================ */
/* ============================================
RESPONSIVE DESIGN
============================================ */
@@ -4044,6 +3987,12 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
}
}
/* ============================================ */
/* _10_tracker-editor.css */
/* ============================================ */
/* ============================================
HTML PROMPT TOGGLE
============================================ */
@@ -4783,6 +4732,12 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
background: var(--rpg-accent);
}
/* ============================================ */
/* _11_settings-modal.css */
/* ============================================ */
/* ============================================
SETTINGS MODAL - MOBILE FIRST
============================================ */
@@ -5029,6 +4984,12 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
}
}
/* ============================================ */
/* _12_thought-bubbles.css */
/* ============================================ */
/* ============================================
BELOW-CHAT PRESENT CHARACTERS
============================================ */
@@ -5527,6 +5488,12 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
}
/* ============================================ */
/* _13_mobile.css */
/* ============================================ */
/* ============================================
MOBILE TOGGLE BUTTON (FAB)
============================================ */
@@ -6786,6 +6753,12 @@ body:has(.rpg-panel.rpg-mobile-open) .rpg-fab-widget-container {
}
}
/* ============================================ */
/* _14_inventory.css */
/* ============================================ */
/* ========================================
Inventory System v2 Styles
======================================== */
@@ -7175,6 +7148,12 @@ body:has(.rpg-panel.rpg-mobile-open) .rpg-fab-widget-container {
font-size: 0.9rem;
}
/* ============================================ */
/* _15_inventory-views.css */
/* ============================================ */
/* ============================================
ITEM LIST AND GRID VIEW STYLES
============================================ */
@@ -7348,6 +7327,12 @@ body:has(.rpg-panel.rpg-mobile-open) .rpg-fab-widget-container {
color: var(--rpg-highlight);
}
/* ============================================ */
/* _16_desktop-tabs.css */
/* ============================================ */
/* ============================================
DESKTOP TABS SYSTEM
============================================ */
@@ -7486,6 +7471,12 @@ body:has(.rpg-panel.rpg-mobile-open) .rpg-fab-widget-container {
}
}
/* ============================================ */
/* _17_quests.css */
/* ============================================ */
/* ============================================
QUESTS SYSTEM STYLING
============================================ */
@@ -7877,6 +7868,12 @@ body:has(.rpg-panel.rpg-mobile-open) .rpg-fab-widget-container {
}
}
/* ============================================ */
/* _18_equipment.css */
/* ============================================ */
/* ========================================
MOBILE FONT SIZE OVERRIDES (ALL SCREENS)
Apply to screens up to 1000px
@@ -8026,6 +8023,12 @@ body:has(.rpg-panel.rpg-mobile-open) .rpg-fab-widget-container {
box-sizing: border-box;
}
/* ============================================ */
/* _19_encounters.css */
/* ============================================ */
/* ============================================
COMBAT ENCOUNTER MODAL STYLES
============================================ */
@@ -9405,6 +9408,12 @@ body:has(.rpg-panel.rpg-mobile-open) .rpg-fab-widget-container {
border-color: rgba(200, 50, 50, 0.8);
}
/* ============================================ */
/* _20_plot-buttons.css */
/* ============================================ */
/* ============================================
STATUS EFFECTS
============================================ */
@@ -9537,6 +9546,12 @@ body:has(.rpg-panel.rpg-mobile-open) .rpg-fab-widget-container {
}
}
/* ============================================ */
/* _21_music-player.css */
/* ============================================ */
/* ============================================
SPOTIFY MUSIC WIDGET STYLES
============================================ */
@@ -9796,6 +9811,12 @@ body[data-theme="cyberpunk"] .rpg-music-widget-content {
body[data-theme="cyberpunk"] .rpg-music-widget-play {
color: #ff2a6d;
}
/* ============================================ */
/* _22_weather.css */
/* ============================================ */
/* ============================================
SNOWFLAKES EFFECT
============================================ */
@@ -10852,6 +10873,12 @@ body[data-theme="cyberpunk"] .rpg-music-widget-play {
}
}
/* ============================================ */
/* _23_holiday-promo.css */
/* ============================================ */
/* ============================================
HOLIDAY PROMOTION BANNER
============================================ */
@@ -10887,6 +10914,12 @@ body[data-theme="cyberpunk"] .rpg-music-widget-play {
}
}
/* ============================================ */
/* _24_feature-toggles.css */
/* ============================================ */
/* ============================================
THREE-COLUMN FEATURE TOGGLES LAYOUT
============================================ */
@@ -10980,6 +11013,12 @@ body[data-theme="cyberpunk"] .rpg-music-widget-play {
position: relative !important;
}
/* ============================================ */
/* _25_import-dialog.css */
/* ============================================ */
/* ============================================
IMPORT MODE DIALOG STYLES
============================================ */
@@ -11080,6 +11119,12 @@ body[data-theme="cyberpunk"] .rpg-music-widget-play {
text-decoration: underline;
}
/* ============================================ */
/* _26_fab-widgets.css */
/* ============================================ */
/* ============================================
FAB Widget System - Improved Layout
============================================ */
@@ -11412,6 +11457,12 @@ body[data-theme="cyberpunk"] .rpg-music-widget-play {
100% { transform: rotate(360deg); }
}
/* ============================================ */
/* _27_strip-widgets.css */
/* ============================================ */
/* ============================================
FAB WIDGET THEME VARIATIONS
============================================ */
@@ -11909,6 +11960,12 @@ body.documentstyle .rpg-inline-thoughts {
}
/* ============================================ */
/* _28_equipment-modal.css */
/* ============================================ */
/* ============================================
Equipment Section Styles
============================================ */
@@ -12288,9 +12345,36 @@ body.documentstyle .rpg-inline-thoughts {
}
/* Equipment bonus display on RPG attributes */
.rpg-classic-stat-bonus {
color: #4caf50;
font-size: 0.85em;
font-weight: 400;
margin-left: 2px;
/* ============================================ */
/* _variables.css */
/* ============================================ */
/* ============================================
RPG COMPANION - CSS VARIABLES
CSS Custom Properties for Theming
============================================ */
/* CSS Variables for Theming - Default uses SillyTavern's variables */
.rpg-panel,
#rpg-thought-panel,
#rpg-thought-icon,
#rpg-alt-present-characters,
.rpg-mobile-toggle {
--rpg-bg: var(--SmartThemeBlurTintColor, rgba(26, 26, 46, 0.9));
--rpg-accent: var(--black30a, rgba(22, 33, 62, 0.9));
--rpg-text: var(--SmartThemeBodyColor, #eaeaea);
--rpg-highlight: var(--SmartThemeQuoteColor, #e94560);
--rpg-border: var(--SmartThemeBorderColor, #4a7ba7);
--rpg-shadow: rgba(0, 0, 0, 0.5);
}
/* CSS Custom Properties for Responsive Scaling */
:root {
--rpg-panel-min-width: 300px;
--rpg-panel-max-width: 500px;
--rpg-modal-min-width: 320px;
--rpg-modal-max-width: 600px;
}
+1 -1
View File
@@ -1114,7 +1114,7 @@
Injected when "Enable Dialogue Coloring" is enabled. Affects all generation modes.
</small>
<textarea id="rpg-prompt-dialogue-coloring" class="rpg-prompt-textarea" rows="4"></textarea>
<button class="menu_button rpg-restore-prompt-btn" data-prompt="dialogue-coloring" style="margin-top: 8px;">
<button class="menu_button rpg-restore-prompt-btn" data-prompt="dialogueColoring" style="margin-top: 8px;">
<i class="fa-solid fa-rotate-left"></i>&nbsp;<span data-i18n-key="template.promptsEditor.restoreDefault">Restore Default</span>
</button>
</div>