Custom Vue Features
Directives
v-persist
Keeps images in browser memory cache after they load. Prevents the browser from evicting decoded image data when elements are removed from the DOM (e.g., when closing a panel that uses v-if).
Usage:
<img :src="iconPath" v-persist />
Why it exists:
When a Vue component is destroyed (via v-if), all its <img> elements are removed from the DOM. The browser may then evict the decoded image data from memory. When the component is recreated, the browser needs to re-decode the image from disk cache, causing a brief visual delay.
v-persist creates a hidden JavaScript reference to each loaded image, telling the browser to keep the decoded data in memory.
Example - Item display component:
const { vue, game } = window.engine;
const { defineComponent, computed } = vue;
const ItemIcon = defineComponent({
props: ['item'],
setup(props) {
const icon = computed(() => props.item.getTrait('image'));
return { icon };
},
template: /*html*/`
<img v-if="icon" :src="icon" class="item-icon" v-persist />
`
});
When to use:
- Images that appear in panels or screens that open/close frequently (character sheets, inventory, etc.)
- Character portraits and doll layers
- Any image that should always display instantly when its container is reopened
When not needed:
- Images that are always visible (static backgrounds, persistent UI elements)
- Images shown only once (splash screens, one-time animations)
The cache holds up to 600 images. When full, the oldest entry is removed to make room for new ones.
v-fit
Shrinks font size so text fits within its container without clipping. Reacts to text changes and container resizes automatically.
Usage:
<div v-fit>{{ characterName }}</div>
<div v-fit="{ min: 8 }">{{ longTitle }}</div>
Options:
| Option | Type | Default | Description |
|---|---|---|---|
min | number | 6 | Minimum font size in px |
Example - Character name badge:
const { vue, game } = window.engine;
const { defineComponent, computed } = vue;
const CharacterBadge = defineComponent({
props: ['character'],
setup(props) {
const name = computed(() => props.character.getTrait('name'));
return { name };
},
template: /*html*/`
<div class="badge" v-fit>{{ name }}</div>
`
});
When to use:
- Name labels on fixed-width containers (character portraits, item slots)
- Any single-line text that must not clip or overflow
When not needed:
- Text that can wrap to multiple lines
- Text in containers that grow to fit content
v-script
Renders DryadScript text on any element with full lore-link interactivity. Resolves the input through the engine's text pipeline by default and attaches the hover/click event delegation needed to open lore tooltip popups on [[record_id]] references. Use this anywhere you display engine-resolved text in your own templates — plain v-html will render visually but the lore links won't react to hover or click. See Lore & Encyclopedia for the full lore system.
Usage (string form):
<div v-script="rawText" />
Resolves rawText through the text pipeline (placeholders, if{}, [[lore-links]], etc.) and renders it. Equivalent to v-script="{ html: rawText }".
Usage (object form):
<div v-script="{ html, resolver, navMode, onNavigate, disabled }" />
| Option | Type | Default | Description |
|---|---|---|---|
html | string | required | text to render |
resolver | boolean | true | run html through game.resolveString(html, true).output (noExecuteActions=true; rendering must never fire side effects). Set false if html is already resolved upstream. |
navMode | boolean | false | for in-place navigation (e.g., the Encyclopedia tab): clicks call onNavigate(recordId) instead of opening a popup. Hover is suppressed in this mode. |
onNavigate | (recordId: string) => void | — | callback for navMode clicks |
disabled | boolean | false | suppress all hover/click handling. Use during DOM-unstable phases like a typing animation that re-parses HTML each frame, so the popup doesn't latch onto a stale anchor. |
Example - choice description:
const { vue } = window.engine;
const { defineComponent } = vue;
const ChoiceCard = defineComponent({
props: ['choice'],
template: /*html*/`
<div class="choice">
<h3>{{ choice.name }}</h3>
<div v-if="choice.description" v-script="choice.description" class="choice-description"></div>
</div>
`
});
Reactivity caveat: the directive re-renders only when its bound value changes. Reactive dependencies inside resolveString (e.g., a placeholder that reads a ref the player can change mid-session) won't trigger a re-render automatically. For those cases, wrap in your own computed(() => game.resolveString(text).output) and pass with { resolver: false }.
When to use:
- Any custom component that displays user-authored text containing
[[record]]references,|placeholders|, or other DryadScript syntax - Choice descriptions, item descriptions, status descriptions in plugin UIs
When not needed:
- Plain text without any DryadScript syntax — use
{{ text }}interpolation instead.
v-popover
Floating UI based hover popover that just works on any element.
Usage (string form — HTML rendered via v-script):
<button v-popover="'<b>Strength</b>: raises [[stat_attack]]'">Stats</button>
The string is treated as DryadScript HTML and rendered through v-script, so [[record]] lore-links, |placeholders|, and if{} blocks all work inside the popover.
Usage (object form — html mode):
<div v-popover="{ html: skill.description, width: 400 }">{{ skill.name }}</div>
Usage (object form — Vue component mode):
<div v-popover="{ component: ItemCard, props: { item }, width: 360 }">
<img :src="item.icon">
</div>
When component is supplied it wins over html. The component receives props via v-bind.
Placement modifiers (sugar for the placement option):
<div v-popover.bottom="content">Below</div>
<div v-popover.right="content">To the right</div>
Equivalent to v-popover="{ ..., placement: 'bottom' }". The default placement is 'top'. Floating UI's flip() and shift() middleware automatically rescue placement near viewport edges.
Binding shape:
| Field | Type | Default | Description |
|---|---|---|---|
html | string | — | content to render through v-script |
component | Component | — | Vue component to render inside (wins over html) |
props | object | {} | props passed to component via v-bind |
width | number | string | '300px' | popover width; numbers become ${n}px |
placement | Placement | 'top' | any Floating UI placement (top, bottom-start, left-end, etc.) |
Hover handoff:
The popover sits at offset: 0 against the target side, and a 100 ms close-debounce covers the cursor's transition between target and popover. Hovering the popover keeps it open; leaving the popover closes it after 100 ms.
Example — character status with rich popover content:
const { vue, game } = window.engine;
const { defineComponent } = vue;
const { ItemCard } = window.engine.components;
const InventoryRow = defineComponent({
props: ['item'],
setup(props) {
return { item: props.item, ItemCard };
},
template: /*html*/`
<div class="row" v-popover="{ component: ItemCard, props: { item }, placement: 'right' }">
<img :src="item.icon">
<span>{{ item.name }}</span>
</div>
`
});
When to use:
- Rich hover-over information panels (item details, ability tooltips, character cards)
- Anywhere you'd reach for a tooltip but need clickable content inside
- Any place game text with
[[lore-links]]should appear on hover
When not needed:
- Plain text tooltips — use
v-tooltip(PrimeVue), it's lighter
v-dragscroll
Enables drag-to-scroll on any scrollable container. Click and drag to scroll horizontally, vertically, or both.
Usage:
<!-- Horizontal only -->
<div class="scroll-container" v-dragscroll.x>...</div>
<!-- Vertical only -->
<div class="scroll-container" v-dragscroll.y>...</div>
<!-- Both directions (default) -->
<div class="scroll-container" v-dragscroll>...</div>
When to use:
- Horizontal card/character lists that overflow their container
- Any scrollable area where drag-to-scroll improves UX (especially touch devices)
When not needed:
- Containers that don't overflow
- Areas where drag conflicts with other interactions (text selection, sliders)
Powered by vue-dragscroll.
Quick Reference
| Directive | Element | Purpose |
|---|---|---|
v-persist | <img> | Keep loaded images in browser memory cache |
v-fit | Any | Shrink font size so text fits without clipping |
v-script | Any | Render DryadScript text with [[lore-link]] interactivity |
v-popover | Any | Hover popover with HTML or Vue-component content (Floating UI) |
v-dragscroll | Any | Drag-to-scroll on scrollable containers |
v-tooltip | Any | Show tooltip on hover (from PrimeVue) |