Views and panels

A view is a file in views/. It has an id, and it contains panels.

id: holopanels:warps

panels:
  title:
    type: text
    offset: { right: -2.4, up: 1.3, forward: 0.0 }
    style:
      background-opacity: 0.0
    lines:
      - "<gradient:#5ee7ff:#7a5cff><bold>WARPS</bold></gradient>"

The id is namespaced and global. Any board can point at it, and any open-view action can navigate to it.

Panel types

text

Fixed lines. Every line is MiniMessage and supports PlaceholderAPI, plus the <entry:…>, <state:…> and <player:…> tokens.

detail:
  type: text
  offset: { right: 2.4, up: 0.0, forward: 0.0 }
  selection: list
  visible-if:
    selected: list
  lines:
    - "<white><bold><entry:label>"
    - "<gray><entry:blurb>"

Lines can also come from a content-provider registered by another plugin, in which case lines is the fallback.

list

Rows with pagination and a per-viewer selection.

list:
  type: list
  offset: { right: -2.4, up: 0.0, forward: 0.0 }
  page-size: 5
  header:
    - "<gray>Left click to preview"
    - ""
  row: "<gray>  <entry:label>"
  selected-row: "<white>▸ <entry:label>"
  empty: "<gray>No warps are configured."
  entries:
    spawn:
      label: "Spawn"
      fields:
        blurb: "Where everyone starts."
      attributes:
        donor: "false"
  clicks:
    left:
      - type: select

An entry has a label, any fields you like (resolved by <entry:name>), and attributes, which are plain strings that entry-attribute conditions compare against. Entries can instead come from source.provider:

list:
  type: list
  source:
    provider: yourplugin:entries
  page-size: 10
  row: "<entry:label>"
  empty: "<gray>Loading..."

A list cannot define both static entries and a provider.

buttons

A strip of individually clickable, individually conditional controls.

pager:
  type: buttons
  offset: { right: -2.4, up: -1.0, forward: 0.0 }
  style:
    background-opacity: 0.0
  buttons:
    - id: previous
      text: "<dark_gray>[ <white>◀ Previous</white> <dark_gray>]"
      clicks:
        left:
          - type: previous-page
            panel: list

Each button has its own id, text, optional visible-if and its own clicks map.

Offsets and layout

offset is right, up and forward in the board's own axes. Negative right is to the viewer's left.

A panel's up offset is its bottom edge, and it grows upward from there. A panel is lines × line-height tall, so with the default line-height: 0.25 a five-row panel at up: 0.0 occupies 0.0 to 1.25, and anything above it needs up: 1.25 or more.

Two more things that bite:

  • interaction-width (default 4.0) is how wide the clickable region is, not how wide the text is. Side-by-side panels want their right offsets at least interaction-width apart, or their click regions overlap and the nearer one wins.
  • A panel with background-opacity: 0.0 still occupies space and still takes clicks. That is exactly what button strips use, so they read as floating text under a solid panel.

Style

line-widthpixels

Where text wraps.

line-heightnumberdefault 0.25

Vertical spacing per line, and therefore how tall the panel is.

alignmentleft | center | right

Horizontal alignment of the text within the panel.

background-colorcolour

The panel's backing plate.

background-opacity0.0-1.0

0.0 removes the plate visually while keeping the panel's footprint and clicks.

text-opacity0-255

Opacity of the text itself.

text-shadowboolean

Vanilla text shadow.

see-throughboolean

Whether the panel draws through blocks.

billboardfixed | vertical | horizontal | center

How the panel turns to face the viewer. fixed keeps the board's own facing.

interaction-widthnumberdefault 4.0

Width of the click region.

A list panel owns a selection. Another panel points at it:

detail:
  type: text
  selection: list # resolve <entry:...> from that panel's selection
  visible-if:
    selected: list # and disappear when nothing is picked

visible-if: { selected: <panel> } is the usual companion, so the detail panel vanishes when nothing is chosen. The mirror image, not: { selected: list }, gives you the "pick something" hint panel.

Selection is per player. Two people at one board highlight different rows.

What is templated and what is not

Only strings shown to the player are templated: lines, row, selected-row, header, empty, button text, and an action's message, title, denied-message and confirm-message. Those get MiniMessage, PlaceholderAPI and the <entry:…> / <state:…> / <player:…> tokens.

Action arguments are not. A command action gets exactly three substitutions, <player>, <uuid> and <entry_id>, which is why per-entry travel runs warp <entry_id> rather than templating coordinates into a teleport action.

Validate the tallest state

Test the first, middle and last list page; a selected and unselected row; every conditional button; and the longest provider result. /holopanels refresh <board> clears provider caches so the test does not accidentally reuse an earlier response. Then verify clicks at the left and right edges of side-by-side panels to catch overlapping interaction widths.