SLOPSHOPPER

views-demo

The views-demo hooks module

new
A shopper browsing a rack in a slop shop
README

views-demo

Sample mod for $.view custom views. It:

  • opens a Views demo panel in the left sidebar: a counter saved with $.store (called from inside the view through pawn.call), a Toast button, and buttons that open the same info page as a tab above the chat or as a dialog,
  • answers the view's pawn.send messages in a view.message hook and replies with $.view.post.

Not installed by default. Copy this folder to ~/.pawn/mods/views-demo and allow it in the review dialog; the dialog lists Show custom views (medium risk) and the store/UI permissions the view bridges to.

Docs: docs/agent/MODS.md.

Source 1 files
hooks/register.js 79 lines
1// A custom view is plain HTML plus one script, run in a sandboxed iframe.
2// Inside it, `pawn.call(method, ...args)` reaches only the bridge calls this file
3// names literally ($.ui.toast, $.store.get, $.store.set below), and
4// `pawn.send(data)` arrives here as a `view.message` event.
5
6const STYLE = `<style>
7  button { font: inherit; padding: 4px 10px; border-radius: var(--radius-sm); border: 1px solid var(--border-color);
8           background: var(--bg-secondary); color: var(--text-primary); cursor: pointer; }
9  button.primary { background: var(--primary); color: var(--on-primary); border-color: transparent; }
10  .row { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
11  .big { font-size: 28px; font-weight: 600; color: var(--accent); }
12  .muted { color: var(--text-muted); font-size: 12px; }
13</style>`
14
15const PANEL_HTML = STYLE + `
16  <div class="muted">Saved with $.store, inside a sandboxed frame</div>
17  <div class="big" id="n">…</div>
18  <div class="row">
19    <button class="primary" id="inc">+1</button>
20    <button id="toast">Toast</button>
21  </div>
22  <div class="row">
23    <button id="tab">Open tab</button>
24    <button id="modal">Open dialog</button>
25  </div>
26  <div class="muted" id="msg"></div>`
27
28const PANEL_SCRIPT = `
29  const n = document.getElementById('n')
30  const show = (v) => { n.textContent = String(v) }
31  pawn.call('store.get', 'count').then((v) => show(v || 0))
32  document.getElementById('inc').addEventListener('click', async () => {
33    const v = ((await pawn.call('store.get', 'count')) || 0) + 1
34    await pawn.call('store.set', 'count', v)
35    show(v)
36    pawn.send({ type: 'count', value: v })
37  })
38  document.getElementById('toast').addEventListener('click', () => pawn.call('ui.toast', 'Hello from a mod view'))
39  document.getElementById('tab').addEventListener('click', () => pawn.send({ type: 'open', placement: 'tab' }))
40  document.getElementById('modal').addEventListener('click', () => pawn.send({ type: 'open', placement: 'modal' }))
41  pawn.onMessage((data) => { document.getElementById('msg').textContent = data && data.text ? data.text : '' })
42  // Not on the bridge list: refused even though the frame asks.
43  pawn.call('process.run', ['id']).catch((e) => console.log('refused:', e.message))
44`
45
46const INFO_HTML = STYLE + `
47  <h3 style="margin:0 0 6px">Views demo</h3>
48  <p class="muted">This page is a mod view. It has no network access, no access to Pawn's window,
49  and can only call the $ functions its mod names in source.</p>
50  <div class="row"><button class="primary" id="close">Close</button></div>`
51
52const INFO_SCRIPT = `document.getElementById('close').addEventListener('click', () => pawn.send({ type: 'close' }))`
53
54export function register(on) {
55  on('session.start', async ($, e, next) => {
56    if (e.plugin !== $.plugin.name) return next(e)
57    $.view.open({ id: 'counter', title: 'Views demo', placement: 'panel', html: PANEL_HTML, script: PANEL_SCRIPT, height: 190 })
58    return next(e)
59  })
60
61  on('view.message', { plugin: 'views-demo' }, async ($, e, next) => {
62    const d = e.data || {}
63    if (d.type === 'count') {
64      $.view.post({ id: 'counter', data: { text: 'mod saw count ' + d.value } })
65    }
66    if (d.type === 'open') {
67      const id = d.placement === 'modal' ? 'about-modal' : 'about-tab'
68      $.view.open({ id, title: 'About views', placement: d.placement, html: INFO_HTML, script: INFO_SCRIPT, height: 170 })
69    }
70    if (d.type === 'close') {
71      $.view.close({ id: e.id })
72    }
73    return next(e)
74  })
75}
76
77// Bridge calls the view uses must appear literally in source:
78// $.store.get  $.store.set  $.ui.toast
79