<?php
/**
 * Plugin Developer Guide — an in-app, theme-aware reference for building
 * View / Edit / Filter / Validation plugins for the platform.
 *
 * Rendered by PluginManagerController::guide(). Self-contained: reuses the
 * shared --pp-* theme tokens (theme-head.php) so it follows light/dark.
 */
if (!isset($baseUrl)) $baseUrl = '';
if (!isset($assetsUrl)) $assetsUrl = rtrim($baseUrl, '/') . '/assets';

/** Render a fenced code block with a language label + copy button. $src is literal. */
$code = function (string $src, string $lang = 'php'): string {
    $id = 'c' . substr(md5($src . mt_rand()), 0, 8);
    $h  = '<div class="pm-code" data-lang="' . htmlspecialchars($lang) . '">';
    $h .= '<button type="button" class="pm-code-copy" data-copy="' . $id . '" title="Copy"><i class="bi bi-clipboard"></i></button>';
    $h .= '<pre><code id="' . $id . '">' . htmlspecialchars(rtrim($src, "\n")) . '</code></pre>';
    $h .= '</div>';
    return $h;
};

/** Table of contents entries: [anchor, label, icon]. */
$toc = [
    ['intro',        'Introduction',           'bi-lightbulb'],
    ['families',     'The four plugin families','bi-diagram-3'],
    ['anatomy',      'Anatomy of a plugin',    'bi-folder2-open'],
    ['view',         'Building a View plugin', 'bi-eye'],
    ['edit',         'Building an Edit plugin', 'bi-pencil-square'],
    ['selfenhance',  'Self-enhancing controls','bi-magic'],
    ['filter',       'Building a Filter plugin','bi-funnel'],
    ['validation',   'Building a Validation rule','bi-shield-check'],
    ['schema',       'settingsSchema reference','bi-sliders'],
    ['assets',       'Assets & bundled libraries','bi-box-seam'],
    ['metadata',     'Plugin Manager metadata','bi-info-circle'],
    ['packaging',    'Packaging & installing', 'bi-file-earmark-zip'],
    ['bestpractices','Best practices',         'bi-check2-circle'],
    ['checklist',    'Release checklist',      'bi-list-check'],
];
?>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><?= htmlspecialchars(\PpPhp\Core\Brand::name()) ?> · Plugin Developer Guide</title>
    <link href="<?= $assetsUrl ?>/bootstrap/css/bootstrap.min.css" rel="stylesheet">
    <link href="<?= $assetsUrl ?>/icons/bootstrap-icons.css" rel="stylesheet">
    <script src="<?= $assetsUrl ?>/bootstrap/js/bootstrap.bundle.min.js" defer></script>
    <?php include constant('BASE_PATH') . '/core/views/theme-head.php'; ?>
    <?= \PpPhp\Core\ModuleAssets::style('PluginManager', 'Views/plugin-manager-theme.css') ?>
    <style>
    body { padding-top: 56px; }
    /* ── Guide-specific layout ─────────────────────────────────────── */
    .pm-guide { display:grid; grid-template-columns: 264px minmax(0,1fr); gap:2rem; align-items:start; max-width:1320px; }
    .pm-toc { position:sticky; top:78px; align-self:start; max-height:calc(100vh - 96px); overflow:auto;
              border:1px solid var(--pp-border); border-radius:16px; background:var(--pp-surface);
              padding:.85rem; box-shadow:0 8px 24px var(--pp-shadow-color); }
    .pm-toc-title { font-size:.66rem; letter-spacing:.14em; text-transform:uppercase; font-weight:800;
                    color:var(--pp-text-muted); padding:.2rem .55rem .55rem; }
    .pm-toc a { display:flex; align-items:center; gap:.55rem; padding:.5rem .6rem; border-radius:9px;
                color:var(--pp-text); font-size:.83rem; font-weight:600; text-decoration:none; line-height:1.2;
                transition:background-color .14s ease, color .14s ease; }
    .pm-toc a i { color:var(--pp-accent); font-size:.95rem; flex:0 0 auto; }
    .pm-toc a:hover { background:var(--pp-surface-soft); }
    .pm-toc a.active { background:var(--pp-accent-soft); color:var(--pp-accent); }
    .pm-guide-main { min-width:0; }
    .pm-sec { scroll-margin-top:82px; margin-bottom:2.4rem; }
    .pm-sec > h2 { display:flex; align-items:center; gap:.6rem; font-size:1.4rem; font-weight:800; color:var(--pp-text);
                   margin:0 0 .4rem; padding-bottom:.55rem; border-bottom:1px solid var(--pp-border); }
    .pm-sec > h2 .num { display:inline-grid; place-items:center; width:30px; height:30px; border-radius:9px;
                        background:var(--pp-accent-soft); color:var(--pp-accent); font-size:.82rem; font-weight:800; flex:0 0 auto; }
    .pm-sec h3 { font-size:1.02rem; font-weight:750; color:var(--pp-text); margin:1.5rem 0 .5rem; }
    .pm-sec p, .pm-sec li { color:var(--pp-text); line-height:1.62; font-size:.92rem; }
    .pm-sec p, .pm-sec ul, .pm-sec ol { margin-bottom:.85rem; }
    .pm-sec a { color:var(--pp-accent); }
    .pm-sec code:not(pre code) { background:var(--pp-surface-muted); color:var(--pp-accent);
                                 padding:.08em .4em; border-radius:5px; font-size:.86em;
                                 border:1px solid color-mix(in srgb, var(--pp-border) 70%, transparent); }
    .pm-lead { font-size:1.02rem; color:var(--pp-text-muted); line-height:1.6; }

    /* Code blocks */
    .pm-code { position:relative; margin:.4rem 0 1rem; border:1px solid var(--pp-border-strong);
               border-radius:12px; overflow:hidden; background:var(--pp-surface-muted,#0b1020); }
    .pm-code::before { content:attr(data-lang); position:absolute; top:8px; left:14px; font-size:.62rem;
                       letter-spacing:.12em; text-transform:uppercase; font-weight:800; color:var(--pp-text-muted,#8ea0c4); }
    .pm-code pre { margin:0; padding:2rem 1rem 1rem; overflow-x:auto; }
    .pm-code code { font-family:'SF Mono',ui-monospace,Menlo,Consolas,monospace; font-size:.8rem; line-height:1.65; color:var(--pp-text,#dfe6f5); white-space:pre; }
    .pm-code-copy { position:absolute; top:6px; right:6px; z-index:2; width:30px; height:30px; border-radius:8px;
                    border:1px solid var(--pp-border,rgba(255,255,255,.14)); background:color-mix(in srgb, var(--pp-accent,#4f46e5) 8%, transparent); color:var(--pp-text-muted,#c7d2e8);
                    display:grid; place-items:center; cursor:pointer; transition:background .14s ease, color .14s ease; }
    .pm-code-copy:hover { background:color-mix(in srgb, var(--pp-accent,#4f46e5) 16%, transparent); color:var(--pp-text,#fff); }
    .pm-code-copy.copied { color:#4ade80; border-color:#4ade80; }

    /* Callouts */
    .pm-note { display:flex; gap:.7rem; padding:.85rem 1rem; border-radius:12px; margin:.4rem 0 1rem;
               border:1px solid var(--pp-border); background:var(--pp-surface-soft); font-size:.88rem; line-height:1.55; color:var(--pp-text); }
    .pm-note i { font-size:1.05rem; flex:0 0 auto; margin-top:.06rem; }
    .pm-note.tip    { border-color:color-mix(in srgb,#22c55e 45%,var(--pp-border)); background:color-mix(in srgb,#22c55e 10%,var(--pp-surface)); }
    .pm-note.tip i    { color:#16a34a; }
    .pm-note.warn   { border-color:color-mix(in srgb,#f59e0b 50%,var(--pp-border)); background:color-mix(in srgb,#f59e0b 12%,var(--pp-surface)); }
    .pm-note.warn i   { color:#d97706; }
    .pm-note.danger { border-color:color-mix(in srgb,#ef4444 48%,var(--pp-border)); background:color-mix(in srgb,#ef4444 10%,var(--pp-surface)); }
    .pm-note.danger i { color:#dc2626; }

    /* Family cards */
    .pm-fam-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); gap:.9rem; margin:.6rem 0 1rem; }
    .pm-fam { border:1px solid var(--pp-border); border-radius:14px; padding:1rem; background:var(--pp-surface); }
    .pm-fam .ic { width:40px; height:40px; border-radius:11px; display:grid; place-items:center; font-size:1.15rem;
                  background:var(--pp-accent-soft); color:var(--pp-accent); margin-bottom:.6rem; }
    .pm-fam h4 { font-size:.95rem; font-weight:750; margin:0 0 .25rem; color:var(--pp-text); }
    .pm-fam p { font-size:.82rem; color:var(--pp-text-muted); margin:0 0 .5rem; line-height:1.5; }
    .pm-fam .path { font-size:.72rem; font-family:ui-monospace,monospace; color:var(--pp-accent); word-break:break-all; }

    /* Reference table */
    .pm-ref { width:100%; border-collapse:separate; border-spacing:0; font-size:.85rem; margin:.3rem 0 1rem;
              border:1px solid var(--pp-border); border-radius:12px; overflow:hidden; }
    .pm-ref th { text-align:left; background:var(--pp-surface-muted); color:var(--pp-text); font-weight:750;
                 padding:.55rem .7rem; font-size:.76rem; text-transform:uppercase; letter-spacing:.04em; }
    .pm-ref td { padding:.55rem .7rem; border-top:1px solid var(--pp-border); color:var(--pp-text); vertical-align:top; }
    .pm-ref td code { background:var(--pp-surface-muted); color:var(--pp-accent); padding:.05em .35em; border-radius:4px; font-size:.9em; }
    .pm-ref-wrap { overflow-x:auto; }

    .pm-guide-hero { border-radius:18px; padding:1.6rem 1.8rem; margin-bottom:1.8rem;
        background:linear-gradient(135deg, color-mix(in srgb,var(--pp-accent) 22%, var(--pp-surface)), var(--pp-surface));
        border:1px solid var(--pp-border); }
    .pm-guide-hero h1 { font-size:1.7rem; font-weight:850; margin:0 0 .35rem; color:var(--pp-text); }
    .pm-guide-hero p { margin:0; color:var(--pp-text-muted); font-size:.98rem; max-width:60ch; }
    .pm-guide-hero .eyebrow { font-size:.68rem; letter-spacing:.14em; text-transform:uppercase; font-weight:800; color:var(--pp-accent); margin-bottom:.4rem; }

    @media (max-width: 991.98px) {
        .pm-guide { grid-template-columns:1fr; }
        .pm-toc { position:static; max-height:none; }
    }
    </style>
</head>
<body class="plugin-manager-page">
<?php
    $navBrandLabel = 'Plugin Manager Guide';
    $navBrandIcon  = 'journal-code';
    $navBack = ['url' => $baseUrl . '/plugin-manager', 'label' => 'Plugin Manager', 'icon' => 'arrow-left'];
    include constant('BASE_PATH') . '/core/views/console-navbar.php';
?>

<div class="container plugin-manager-content py-4">
  <div class="pm-guide">
    <!-- ── Sidebar TOC ─────────────────────────────────────────────── -->
    <aside class="pm-toc" id="pmToc">
        <div class="pm-toc-title">On this page</div>
        <?php foreach ($toc as $i => [$anchor, $label, $icon]): ?>
            <a href="#<?= $anchor ?>"<?= $i === 0 ? ' class="active"' : '' ?>><i class="bi <?= $icon ?>"></i><span><?= htmlspecialchars($label) ?></span></a>
        <?php endforeach; ?>
    </aside>

    <!-- ── Main content ────────────────────────────────────────────── -->
    <main class="pm-guide-main">
        <div class="pm-guide-hero">
            <div class="eyebrow">EXTEND THE BUILDER</div>
            <h1>Plugin Developer Guide</h1>
            <p>Everything you need to build a plugin: display renderers, input controls, list filters and validation rules that snap into the Designer and every generated app — no core changes required.</p>
        </div>

        <!-- 1. Intro -->
        <section class="pm-sec" id="intro">
            <h2><span class="num">1</span> Introduction</h2>
            <p class="pm-lead">A <strong>plugin</strong> is a small, self-contained PHP class that teaches the platform a new way to <em>show</em>, <em>edit</em>, <em>filter</em> or <em>validate</em> a field. Drop a folder in the right place and it is discovered automatically — it appears in the Designer's palette and is compiled into every app you build.</p>
            <p>Plugins are the platform's primary extension point. They are:</p>
            <ul>
                <li><strong>Zero-config discoverable</strong> — no registration file to edit. A folder with a <code>Plugin.php</code> is all it takes.</li>
                <li><strong>Modular &amp; isolated</strong> — adding, updating or removing one never touches the core. A broken plugin is skipped, not fatal.</li>
                <li><strong>Portable</strong> — the same class runs inside the Designer (live preview) and inside the generated application.</li>
                <li><strong>Packageable</strong> — zip a folder and install it through the Plugin Manager, with automatic backup and rollback.</li>
            </ul>
            <div class="pm-note tip"><i class="bi bi-lightbulb"></i><div>The fastest way to learn is to copy an existing plugin from <code>plugins/&lt;Category&gt;/</code> and adapt it. <code>TextBox</code>, <code>Badge</code>, <code>TextFilter</code> and <code>EmailRule</code> are the simplest starting points for each family. Each section below carries <strong>two complete, working samples</strong> you can lift and adapt.</div></div>
            <div class="pm-note"><i class="bi bi-journal-code"></i><div>A full offline reference — every contract, the modularity rules, worked recipes and a troubleshooting table — lives in the codebase at <code>docs/plugin-development.md</code>, and the same material is in <strong>Help → Building Field Plugins</strong>.</div></div>
        </section>

        <!-- 2. Families -->
        <section class="pm-sec" id="families">
            <h2><span class="num">2</span> The four plugin families</h2>
            <p>Every plugin belongs to exactly one family, decided by which folder it lives in and which interface it implements:</p>
            <div class="pm-fam-grid">
                <div class="pm-fam">
                    <div class="ic"><i class="bi bi-eye"></i></div>
                    <h4>View</h4>
                    <p>Turns a stored value into display HTML on list/view pages (badges, currency, images, QR codes…).</p>
                    <div class="path">plugins/View/&lt;Name&gt;</div>
                </div>
                <div class="pm-fam">
                    <div class="ic"><i class="bi bi-pencil-square"></i></div>
                    <h4>Edit</h4>
                    <p>Renders the input control used on Add/Edit forms and inline editors (date pickers, dropdowns, switches…).</p>
                    <div class="path">plugins/Edit/&lt;Name&gt;</div>
                </div>
                <div class="pm-fam">
                    <div class="ic"><i class="bi bi-funnel"></i></div>
                    <h4>Filter</h4>
                    <p>Provides the search/filter widget shown above a list for a column (text search, ranges, selects…).</p>
                    <div class="path">plugins/Filter/&lt;Name&gt;</div>
                </div>
                <div class="pm-fam">
                    <div class="ic"><i class="bi bi-shield-check"></i></div>
                    <h4>Validation</h4>
                    <p>A single reusable rule enforced on both client and server when a record is saved (email, phone, regex…).</p>
                    <div class="path">plugins/Validation/&lt;Name&gt;</div>
                </div>
            </div>
            <div class="pm-note"><i class="bi bi-diagram-3"></i><div>All four families share the same discovery model and the same <code>settingsSchema()</code> vocabulary for their options, so once you learn one you know them all.</div></div>
        </section>

        <!-- 3. Anatomy -->
        <section class="pm-sec" id="anatomy">
            <h2><span class="num">3</span> Anatomy of a plugin</h2>
            <p>A plugin is a folder containing a single <code>Plugin.php</code>. Optional <code>assets/</code> hold bundled JS/CSS/images:</p>
            <?= $code("plugins/
└── Edit/
    └── ColorPicker/
        ├── Plugin.php          ← required: the plugin class
        └── assets/             ← optional: bundled libraries, images
            ├── js/colorpicker.min.js
            └── css/colorpicker.min.css", 'text') ?>
            <h3>Naming rules</h3>
            <ul>
                <li>Every plugin class lives in the <code>PpPhp\Plugins</code> namespace.</li>
                <li>By convention the <strong>class name matches the folder name</strong> (folder <code>ColorPicker</code> → <code>class ColorPicker</code>).</li>
                <li>If the folder name is a PHP reserved word (e.g. <code>Switch</code>), name the class differently (e.g. <code>SwitchPlugin</code>) — the registry falls back to the class actually declared in the file.</li>
                <li>Validation rules <strong>always</strong> resolve by their declared class name, so name them with a <code>Rule</code> suffix (e.g. <code>EmailRule</code>) to avoid colliding with a same-named View/Edit class.</li>
            </ul>
            <div class="pm-note warn"><i class="bi bi-exclamation-triangle"></i><div>The registry <code>require_once</code>s each <code>Plugin.php</code> once and reads it by reflection. Keep the file side-effect free — declare the class and nothing else. No top-level output, no global mutations.</div></div>
        </section>

        <!-- 4. View plugin -->
        <section class="pm-sec" id="view">
            <h2><span class="num">4</span> Building a View plugin</h2>
            <p>A View plugin implements <code>PpPhp\Core\ViewPlugin</code>. Its job: take the stored <code>$value</code> plus the designer-chosen <code>$settings</code> and return display HTML.</p>
            <?= $code("interface ViewPlugin
{
    public static function id(): string;                            // stable machine id
    public static function label(): string;                        // shown in the palette
    public static function render(mixed \$value, array \$settings): string;
    public static function settingsSchema(): array;                // designer options
    public static function assets(): array;                        // ['js'=>[], 'css'=>[]]
}") ?>
            <p>Here is a complete, working example — an <strong>Uppercase</strong> renderer with one option:</p>
            <?= $code("<?php
namespace PpPhp\\Plugins;

use PpPhp\\Core\\ViewPlugin;

class Uppercase implements ViewPlugin
{
    public static function id(): string    { return 'Uppercase'; }
    public static function label(): string { return 'Uppercase Text'; }

    public static function render(mixed \$value, array \$settings): string
    {
        \$val = trim((string) (\$value ?? ''));
        if (\$val === '') {
            return '<span class=\"pp-muted\">—</span>';
        }
        \$text = mb_strtoupper(\$val);
        \$bold = (\$settings['bold'] ?? 'no') === 'yes';

        // ALWAYS escape values you render — never trust stored data.
        \$safe = htmlspecialchars(\$text, ENT_QUOTES, 'UTF-8');
        return \$bold ? '<strong>' . \$safe . '</strong>' : \$safe;
    }

    public static function settingsSchema(): array
    {
        return [
            ['key' => 'bold', 'type' => 'bool', 'label' => 'Bold', 'default' => 'no'],
        ];
    }

    public static function assets(): array
    {
        return ['js' => [], 'css' => []];
    }
}") ?>
            <div class="pm-note danger"><i class="bi bi-shield-exclamation"></i><div><strong>Security:</strong> a View plugin outputs into a page. Escape every dynamic value with <code>htmlspecialchars(..., ENT_QUOTES)</code> unless you are deliberately rendering trusted HTML. Stored data is user data.</div></div>

            <h3>Example — a status badge (value → coloured label)</h3>
            <p>A common real-world View plugin: map a stored code to a coloured badge. Note the guard that stops a crafted colour value injecting a CSS class.</p>
            <?= $code(<<<'PHP'
<?php
namespace PpPhp\Plugins;

use PpPhp\Core\ViewPlugin;

class StatusBadge implements ViewPlugin
{
    public static function id(): string    { return 'StatusBadge'; }
    public static function label(): string { return 'Status badge'; }

    public static function render(mixed $value, array $settings): string
    {
        $v = trim((string) ($value ?? ''));
        if ($v === '') return '';

        // "active:success, pending:warning, closed:secondary"
        $map = [];
        foreach (explode(',', (string) ($settings['colors'] ?? '')) as $pair) {
            if (str_contains($pair, ':')) {
                [$k, $c] = array_map('trim', explode(':', $pair, 2));
                if ($k !== '') $map[strtolower($k)] = $c;
            }
        }
        $color = $map[strtolower($v)] ?? 'secondary';
        $safe  = preg_match('/^[a-z-]+$/', $color) ? $color : 'secondary';  // block class injection

        return '<span class="badge bg-' . $safe . '">'
             . htmlspecialchars($v, ENT_QUOTES) . '</span>';
    }

    public static function settingsSchema(): array
    {
        return [
            ['key' => 'colors', 'type' => 'text', 'label' => 'Value → colour', 'default' => '',
             'placeholder' => 'active:success, pending:warning, closed:secondary',
             'hint' => 'Bootstrap colours: primary, success, warning, danger, info, secondary, dark.'],
        ];
    }

    public static function assets(): array { return ['js' => [], 'css' => []]; }
}
PHP) ?>
        </section>

        <!-- 5. Edit plugin -->
        <section class="pm-sec" id="edit">
            <h2><span class="num">5</span> Building an Edit plugin</h2>
            <p>An Edit plugin implements <code>PpPhp\Core\EditPlugin</code>. <code>render()</code> receives the form field name, current value and settings, and returns the input HTML. The critical contract: <strong>the control must contain a form element whose <code>name</code> equals <code>$fieldName</code></strong> so the value posts back correctly.</p>
            <?= $code("interface EditPlugin
{
    public static function id(): string;
    public static function label(): string;
    public static function render(string \$fieldName, mixed \$value, array \$settings): string;
    public static function settingsSchema(): array;
    public static function assets(): array;
}") ?>
            <p>The simplest possible Edit plugin — a text box with a configurable placeholder:</p>
            <?= $code("<?php
namespace PpPhp\\Plugins;

use PpPhp\\Core\\EditPlugin;

class TextBox implements EditPlugin
{
    public static function id(): string    { return 'TextBox'; }
    public static function label(): string { return 'Text Box'; }

    public static function render(string \$fieldName, mixed \$value, array \$settings): string
    {
        \$ph   = htmlspecialchars((string) (\$settings['placeholder'] ?? ''), ENT_QUOTES);
        \$safe = htmlspecialchars((string) (\$value ?? ''), ENT_QUOTES);
        return '<input type=\"text\" name=\"' . \$fieldName . '\" value=\"' . \$safe . '\"'
             . ' class=\"form-control\" placeholder=\"' . \$ph . '\">';
    }

    public static function settingsSchema(): array
    {
        return [
            ['key' => 'placeholder', 'type' => 'text', 'label' => 'Placeholder', 'default' => ''],
        ];
    }

    public static function assets(): array { return ['js' => [], 'css' => []]; }
}") ?>
            <div class="pm-note tip"><i class="bi bi-check2"></i><div>Edit controls automatically work in the generated app's <strong>inline-edit</strong>, <strong>spreadsheet</strong> and <strong>inline-add</strong> modes — the list mounts the exact same control you return here. Nothing extra to do, as long as your value input carries <code>name="$fieldName"</code>.</div></div>

            <h3>Example — a dropdown from options (with <code>value:Label</code> pairs)</h3>
            <p>A choice control: parse a comma-separated option list, support <code>value:Label</code> pairs, and mark the current value <code>selected</code> so edits round-trip.</p>
            <?= $code(<<<'PHP'
<?php
namespace PpPhp\Plugins;

use PpPhp\Core\EditPlugin;

class SimpleSelect implements EditPlugin
{
    public static function id(): string    { return 'SimpleSelect'; }
    public static function label(): string { return 'Simple dropdown'; }

    public static function render(string $fieldName, mixed $value, array $settings): string
    {
        $name = htmlspecialchars($fieldName, ENT_QUOTES);
        $cur  = (string) ($value ?? '');
        $html = '<select name="' . $name . '" class="form-select">';
        $html .= '<option value="">'
              . htmlspecialchars((string) ($settings['empty'] ?? '— Select —'), ENT_QUOTES)
              . '</option>';

        // Options: "A, B, C" or "1:Active, 0:Inactive" (value:Label pairs)
        foreach (explode(',', (string) ($settings['options'] ?? '')) as $opt) {
            $opt = trim($opt);
            if ($opt === '') continue;
            [$val, $lbl] = str_contains($opt, ':')
                ? array_map('trim', explode(':', $opt, 2))
                : [$opt, $opt];
            $sel = ($val === $cur) ? ' selected' : '';
            $html .= '<option value="' . htmlspecialchars($val, ENT_QUOTES) . '"' . $sel . '>'
                   . htmlspecialchars($lbl, ENT_QUOTES) . '</option>';
        }
        return $html . '</select>';
    }

    public static function settingsSchema(): array
    {
        return [
            ['key' => 'options', 'type' => 'text', 'full' => true, 'label' => 'Options',
             'placeholder' => 'active:Active, closed:Closed',
             'hint' => 'Comma-separated. Use value:Label to store a code but show a friendly label.'],
            ['key' => 'empty', 'type' => 'text', 'label' => 'Empty label', 'default' => '— Select —'],
        ];
    }

    public static function assets(): array { return ['js' => [], 'css' => []]; }
}
PHP) ?>
        </section>

        <!-- 6. Self-enhancing controls -->
        <section class="pm-sec" id="selfenhance">
            <h2><span class="num">6</span> Self-enhancing controls</h2>
            <p>Plain inputs need no JavaScript. But a rich control (calendar, tag picker, masked input) needs to enhance a DOM node after render. The platform uses a <strong>self-enhancing control contract</strong> so the same code lights up in three places: the Designer canvas, the config-modal live preview, and the generated app.</p>
            <h3>The contract</h3>
            <ol>
                <li><code>render()</code> returns the input <strong>plus</strong> a guarded <code>&lt;style&gt; + &lt;script&gt;</code> block.</li>
                <li>The script defines a global init function, e.g. <code>window.__ppColorInit(root)</code>, guarded so it only defines once.</li>
                <li><code>init(root)</code> finds un-initialised inputs under <code>root</code>, enhances them, and marks them done (a per-element flag) so re-runs are safe.</li>
                <li>Heavy libraries load once via <code>assets()</code> (see below); the inline script only wires them up.</li>
            </ol>
            <p>The Designer re-executes injected <code>&lt;script&gt;</code>s and calls your init after every re-render; the generated app calls it on <code>DOMContentLoaded</code> and when it mounts an inline editor. Follow the pattern and you get all of that for free:</p>
            <?= $code("public static function render(string \$fieldName, mixed \$value, array \$settings): string
{
    \$safe = htmlspecialchars((string) (\$value ?? ''), ENT_QUOTES);
    \$input = '<input type=\"text\" name=\"' . \$fieldName . '\"'
           . ' id=\"field_' . \$fieldName . '\" value=\"' . \$safe . '\"'
           . ' class=\"form-control pp-color-input\" autocomplete=\"off\">';

    return \$input . self::enhance();
}

/** One-time, idempotent enhancement script. */
private static function enhance(): string
{
    return <<<'HTML'
<script>
(function(){
  if (window.__ppColor) return;   // define once per page
  window.__ppColor = true;
  window.__ppColorInit = function(root){
    root = root || document;
    root.querySelectorAll('input.pp-color-input').forEach(function(el){
      if (el._ppColor) return;     // enhance each element once
      el._ppColor = true;
      // …attach your library / behaviour to `el` here…
      // On value change, fire a real event so inline editors save:
      el.addEventListener('change', function(){
        el.dispatchEvent(new Event('change', {bubbles:true}));
      });
    });
  };
  if (document.readyState !== 'loading') window.__ppColorInit();
  else document.addEventListener('DOMContentLoaded', function(){ window.__ppColorInit(); });
})();
</script>
HTML;
}", 'php') ?>
            <div class="pm-note warn"><i class="bi bi-arrow-repeat"></i><div>Both guards matter: the <code>window.__ppColor</code> guard prevents redefining the init on every card, and the per-element <code>el._ppColor</code> guard prevents double-enhancing the same input when the Designer re-renders. Spreadsheet/inline-edit saves on the control's <code>change</code> event — so dispatch one when your value updates.</div></div>
            <div class="pm-note tip"><i class="bi bi-palette"></i><div>Read the theme when you enhance: check <code>document.documentElement</code> for <code>data-pp-theme</code> / <code>data-bs-theme</code> (or <code>prefers-color-scheme</code>) so your control matches light/dark. See <code>plugins/Edit/NepaliDatePicker/Plugin.php</code> for a full reference implementation with two-way field sync.</div></div>
        </section>

        <!-- 7. Filter plugin -->
        <section class="pm-sec" id="filter">
            <h2><span class="num">7</span> Building a Filter plugin</h2>
            <p>A Filter plugin implements <code>PpPhp\Core\FilterPlugin</code>. It renders the widget shown above a list column. <strong>Name the input <code>f[&lt;column&gt;]</code></strong> — the generated list reads <code>$_GET['f'][column]</code> and builds the <code>WHERE</code> clause from it. (Legacy <code>filter_&lt;field&gt;</code> names are ignored.)</p>
            <?= $code("<?php
namespace PpPhp\\Plugins;

use PpPhp\\Core\\FilterPlugin;

class TextFilter implements FilterPlugin
{
    public static function id(): string    { return 'TextFilter'; }
    public static function label(): string { return 'Text Filter'; }

    public static function render(string \$fieldName, array \$settings): string
    {
        \$col = preg_replace('/[^A-Za-z0-9_]/', '', \$fieldName);
        \$cur = htmlspecialchars((string) (\$_GET['f'][\$col] ?? ''), ENT_QUOTES);
        return '<input type=\"text\" name=\"f[' . \$col . ']\" value=\"' . \$cur . '\"'
             . ' class=\"ppd-grid-search pp-input\" placeholder=\"Search…\">';
    }

    public static function settingsSchema(): array { return []; }
    public static function assets(): array { return ['js' => [], 'css' => []]; }
}") ?>
            <p>Note the signature difference: filters have no <code>$value</code> argument — they render an empty search widget, not a bound field. Read the current selection from <code>$_GET['f'][column]</code> so the widget stays populated after a search. Naming conventions for richer widgets: <code>f[col][]</code> for a multi-select (query does <code>IN (…)</code>), <code>f[col][min]</code>/<code>f[col][max]</code> for a number range, and <code>f[col][from]</code>/<code>f[col][to]</code> for a date range.</p>

            <h3>Example — a dropdown of the column's existing values</h3>
            <p>The <code>PluginSupport</code> trait (which ships into every generated app) can query a column's <code>DISTINCT</code> values for you — perfect for a self-populating select. Pass the field's table/column through the settings so the query is scoped and identifier-safe.</p>
            <?= $code(<<<'PHP'
<?php
namespace PpPhp\Plugins;

use PpPhp\Core\FilterPlugin;
use PpPhp\Core\PluginSupport;

class DistinctSelect implements FilterPlugin
{
    use PluginSupport;

    public static function id(): string    { return 'DistinctSelect'; }
    public static function label(): string { return 'Value dropdown'; }

    public static function render(string $fieldName, array $settings): string
    {
        $col = preg_replace('/[^A-Za-z0-9_]/', '', $fieldName);
        $cur = (string) ($_GET['f'][$col] ?? '');

        // distinctOptions() reads the column's DISTINCT values (identifier-safe).
        // The build passes __table / __column into $settings for this control.
        $html = '<select name="f[' . $col . ']" class="ppd-grid-search pp-input">';
        $html .= '<option value="">— All —</option>';
        foreach (self::distinctOptions($settings) as $opt) {
            $sel = ((string) $opt === $cur) ? ' selected' : '';
            $html .= '<option value="' . self::e($opt) . '"' . $sel . '>'
                   . self::e($opt) . '</option>';
        }
        return $html . '</select>';
    }

    public static function settingsSchema(): array { return []; }
    public static function assets(): array { return ['js' => [], 'css' => []]; }
}
PHP) ?>
            <div class="pm-note tip"><i class="bi bi-magic"></i><div>Reusing <code>PluginSupport</code> keeps your plugin small and consistent: <code>self::e()</code> (null-safe escape), <code>self::safeUrl()</code>, <code>self::formatDate()</code> and <code>self::distinctOptions()</code> are all available and all ship with the generated app.</div></div>
        </section>

        <!-- 8. Validation rule -->
        <section class="pm-sec" id="validation">
            <h2><span class="num">8</span> Building a Validation rule</h2>
            <p>A validation rule implements <code>PpPhp\Core\ValidationPlugin</code>. Each rule declares <strong>both sides</strong> of the check so the browser and the server stay in sync:</p>
            <ul>
                <li><code>serverCheck()</code> is authoritative — it runs on save in the generated app. Return <code>true</code> when the value is <strong>valid</strong>.</li>
                <li><code>clientRule()</code> projects the rule onto a small generic vocabulary the in-page validator understands: <code>required</code>, <code>checked</code>, <code>pattern</code>&nbsp;(+<code>flags</code>), <code>minLength</code>/<code>maxLength</code>, <code>words</code>, <code>number</code>&nbsp;(+<code>integer</code>/<code>min</code>/<code>max</code>), <code>phone</code>, <code>match</code>, <code>different</code>, <code>compare</code>, <code>in</code>&nbsp;(+<code>ci</code>), <code>luhn</code>, <code>dateValid</code>, <code>timeValid</code>, <code>password</code>, <code>json</code>. Return <code>[]</code> for a server-only rule (it still runs on save).</li>
            </ul>
            <div class="pm-note tip"><i class="bi bi-shield-check"></i><div><strong>You get three things for free.</strong> (1) Rules run <strong>on blur</strong> in the generated app and re-check as the user fixes them (reward early, punish late), with an inline message under the field. (2) A rule can be gated by a <strong>condition</strong> — the field's Designer "Only validate when &lt;field&gt; &lt;op&gt; &lt;value&gt;" — honoured on both sides automatically; you don't handle it in your rule. (3) The built-in <code>unique</code> rule is verified <strong>live</strong> against the server (<code>api/validate-unique.php</code>) as the user types, in addition to the authoritative check on save. There are <strong>22 built-in rules</strong> in <code>plugins/Validation/</code> to copy from.</div></div>
            <div class="pm-note danger"><i class="bi bi-info-circle"></i><div><strong>Empty values are the <code>Required</code> rule's business.</strong> Every other rule must treat an empty value as valid (skip it), so optional fields don't fire format errors when left blank. The runtime enforces this: your <code>serverCheck()</code> only receives non-empty values unless you opt in with <code>public static function validatesEmpty(): bool { return true; }</code>.</div></div>
            <p>A complete rule with a parameter, plus its client projection:</p>
            <?= $code("<?php
namespace PpPhp\\Plugins;

use PpPhp\\Core\\ValidationPlugin;

class FullNameRule implements ValidationPlugin
{
    public static function id(): string    { return 'fullName'; }
    public static function label(): string { return 'Full name (min words)'; }
    public static function group(): string { return 'Format'; }        // picker grouping

    public static function paramsSchema(): array
    {
        return [
            ['key' => 'minWords', 'type' => 'number', 'label' => 'Minimum words', 'default' => 2],
        ];
    }

    public static function appliesTo(): array { return ['text']; }      // picker hint
    public static function defaultMessage(): string
    {
        return 'Enter at least {minWords} words (e.g. first and last name).';
    }

    public static function serverCheck(mixed \$value, array \$params, array \$record = []): bool
    {
        \$min   = max(1, (int) (\$params['minWords'] ?? 2));
        \$words = preg_split('/\\s+/u', trim((string) \$value), -1, PREG_SPLIT_NO_EMPTY) ?: [];
        if (count(\$words) < \$min) return false;
        foreach (\$words as \$w) {
            if (!preg_match(\"/^[\\\\p{L}'\\\\-]+\$/u\", \$w)) return false;
        }
        return true;
    }

    public static function clientRule(array \$params): array
    {
        return ['words' => max(1, (int) (\$params['minWords'] ?? 2))];
    }
}") ?>
            <p><strong>Error messages</strong> support <code>{label}</code> and <code>{paramKey}</code> placeholders — <code>{minWords}</code> above is replaced with the resolved parameter. A rule that needs table context (like <code>Unique</code>) may declare an extra <code>array $context = []</code> parameter on <code>serverCheck()</code> — the runtime passes <code>{ table, field, id }</code> and rules that don't declare it simply ignore it.</p>

            <h3>Example — a server-only rule (DB lookup with <code>$context</code>)</h3>
            <p>Some rules can only be checked on the server. Declare the 4th <code>$context</code> parameter to receive <code>{ table, field, id }</code>, and return <code>[]</code> from <code>clientRule()</code> so nothing runs in the browser (the server still enforces it on save). Always fail <em>open</em> on a DB error — a validator must never block a save because a query hiccuped.</p>
            <?= $code(<<<'PHP'
<?php
namespace PpPhp\Plugins;

use PpPhp\Core\ValidationPlugin;

class ExistsRule implements ValidationPlugin
{
    public static function id(): string    { return 'exists'; }
    public static function label(): string { return 'Exists in table'; }
    public static function group(): string { return 'Data'; }

    public static function paramsSchema(): array
    {
        return [
            ['key' => 'table',  'type' => 'text', 'label' => 'Lookup table',  'default' => ''],
            ['key' => 'column', 'type' => 'text', 'label' => 'Lookup column', 'default' => 'id'],
        ];
    }

    public static function appliesTo(): array { return []; }
    public static function defaultMessage(): string { return 'No matching {table} record.'; }

    public static function serverCheck(mixed $value, array $params, array $record = [], array $context = []): bool
    {
        $t = preg_replace('/[^A-Za-z0-9_]/', '', (string) ($params['table'] ?? ''));
        $c = preg_replace('/[^A-Za-z0-9_]/', '', (string) ($params['column'] ?? 'id')) ?: 'id';
        if ($t === '') return true;                       // not configured → don't block
        try {
            $rows = \PpPhp\Core\DB::FetchAll("SELECT 1 FROM `{$t}` WHERE `{$c}` = ? LIMIT 1", [$value]);
        } catch (\Throwable $e) {
            return true;                                  // fail open — never block a save on a query error
        }
        return $rows !== [];
    }

    public static function clientRule(array $params): array
    {
        return [];   // server-only: nothing to check in the browser
    }
}
PHP) ?>
            <div class="pm-note tip"><i class="bi bi-shield-check"></i><div><strong>What every rule gets for free:</strong> rules run <em>on blur</em> in the generated app and clear the instant they're fixed; a rule can be gated by the field's Designer <em>“Only validate when …”</em> condition (you don't handle it); and the built-in <code>unique</code> rule is checked <em>live</em> against the server as the user types. There are <strong>22 built-in rules</strong> in <code>plugins/Validation/</code> to copy from.</div></div>
        </section>

        <!-- 9. settingsSchema -->
        <section class="pm-sec" id="schema">
            <h2><span class="num">9</span> settingsSchema reference</h2>
            <p>Every plugin describes its designer options with <code>settingsSchema()</code> (validation rules use the identically-shaped <code>paramsSchema()</code>). It returns an array of field descriptors. The Designer renders them into the properties panel automatically, and the chosen values arrive back in <code>$settings</code>.</p>
            <div class="pm-ref-wrap">
            <table class="pm-ref">
                <thead><tr><th>Key</th><th>Purpose</th></tr></thead>
                <tbody>
                    <tr><td><code>key</code></td><td>The settings array key the value is stored/read under. Required.</td></tr>
                    <tr><td><code>type</code></td><td>Control type: <code>text</code>, <code>number</code>, <code>bool</code>, <code>select</code>, <code>radiogroup</code>, <code>maincolumn</code> (a picker of the table's columns).</td></tr>
                    <tr><td><code>label</code></td><td>Human label shown beside the control.</td></tr>
                    <tr><td><code>default</code></td><td>Default value applied when the user hasn't set one. For <code>bool</code>, use <code>'yes'</code> / <code>'no'</code>.</td></tr>
                    <tr><td><code>options</code></td><td>Array of choices for <code>select</code> / <code>radiogroup</code>.</td></tr>
                    <tr><td><code>hint</code></td><td>Small helper text under the control. Great for examples.</td></tr>
                    <tr><td><code>placeholder</code></td><td>Placeholder for <code>text</code> / <code>number</code> inputs.</td></tr>
                    <tr><td><code>section</code></td><td>Groups options under a titled section (e.g. <code>Display</code>, <code>Constraints</code>).</td></tr>
                    <tr><td><code>showIf</code></td><td>Conditional visibility: <code>['key'=&gt;'pairMode', 'in'=&gt;['double']]</code> shows this option only when another option has a given value.</td></tr>
                    <tr><td><code>full</code></td><td><code>true</code> makes the control span the full panel width.</td></tr>
                </tbody>
            </table>
            </div>
            <div class="pm-note warn"><i class="bi bi-toggle-on"></i><div><strong>Boolean gotcha:</strong> <code>bool</code> settings are stored as the strings <code>'yes'</code> / <code>'no'</code>, not PHP booleans. Read them defensively: <code>in_array(strtolower((string)($settings['x'] ?? '')), ['1','yes','true','on'], true)</code>.</div></div>
        </section>

        <!-- 10. Assets -->
        <section class="pm-sec" id="assets">
            <h2><span class="num">10</span> Assets &amp; bundled libraries</h2>
            <p><code>assets()</code> declares the JS/CSS your control needs. Paths are relative to the app's public assets root. The build engine injects them into the head of any generated page that uses your control — deduped, and only when actually used.</p>
            <?= $code("public static function assets(): array
{
    return [
        'js'  => ['assets/plugins/ColorPicker/colorpicker.min.js'],
        'css' => ['assets/plugins/ColorPicker/colorpicker.min.css'],
    ];
}", 'php') ?>
            <p>If you bundle libraries <strong>inside your plugin folder</strong> under <code>assets/</code>, the build engine auto-copies <code>plugins/&lt;Name&gt;/assets/</code> to <code>output/assets/plugins/&lt;Name&gt;/</code> so the generated app is fully self-contained. Declare those same paths in <code>assets()</code> and everything wires itself up.</p>
            <div class="pm-note danger"><i class="bi bi-shield-lock"></i><div><strong>No CDNs.</strong> Generated apps (and the Designer) run under a strict content policy — bundle every library locally. Never reference an external <code>&lt;script src&gt;</code> or web font. Ship the file inside your plugin.</div></div>
        </section>

        <!-- 11. Metadata -->
        <section class="pm-sec" id="metadata">
            <h2><span class="num">11</span> Plugin Manager metadata</h2>
            <p>These optional static methods enrich the Plugin Manager's details view. All are guarded by <code>method_exists</code>, so omit any you don't need:</p>
            <div class="pm-ref-wrap">
            <table class="pm-ref">
                <thead><tr><th>Method</th><th>Returns</th><th>Shown as</th></tr></thead>
                <tbody>
                    <tr><td><code>version()</code></td><td>string, e.g. <code>'1.2.0'</code></td><td>Version badge</td></tr>
                    <tr><td><code>description()</code></td><td>string</td><td>Description paragraph</td></tr>
                    <tr><td><code>docsUrl()</code></td><td>string (URL)</td><td>Documentation link</td></tr>
                    <tr><td><code>vendor()</code></td><td>array of bundled-library records</td><td>Bundled libraries cards + update notes</td></tr>
                </tbody>
            </table>
            </div>
            <?= $code("public static function version(): string { return '1.0.0'; }

public static function description(): string
{
    return 'A themeable colour picker with hex / rgb output and swatches.';
}

public static function docsUrl(): string { return 'https://example.com/colorpicker'; }

/** Bundled third-party assets so the Plugin Manager can show/track updates. */
public static function vendor(): array
{
    return [[
        'name'    => 'colorpicker.js',
        'version' => '3.4.1',
        'author'  => 'Jane Dev',
        'license' => 'MIT',
        'files'   => ['assets/plugins/ColorPicker/colorpicker.min.js'],
        'update'  => 'Drop the newer colorpicker.min.js into the plugin assets/ folder.',
    ]];
}", 'php') ?>
        </section>

        <!-- 12. Packaging -->
        <section class="pm-sec" id="packaging">
            <h2><span class="num">12</span> Packaging &amp; installing</h2>
            <p>To share a plugin, zip its folder so the archive has a <strong>single top-level folder</strong> containing <code>Plugin.php</code>:</p>
            <?= $code("ColorPicker.zip
└── ColorPicker/
    ├── Plugin.php
    └── assets/…", 'text') ?>
            <p>Then in the Plugin Manager: choose the family, upload the ZIP, Install. The installer is defensive by design and rejects a bad package with a clear message <em>before</em> anything touches the live folder — no code from the package is executed during verification:</p>
            <ul>
                <li><strong>Staged &amp; verified</strong> — extracted to an isolated staging area, then checked: <code>Plugin.php</code> is present, <strong>parses cleanly</strong> (a real syntax check, no <code>exec</code>), declares a class, and that class <strong>implements the interface for the chosen family</strong> (a View plugin can't be installed as a Filter).</li>
                <li><strong>Unique class name</strong> — because all plugins share the <code>PpPhp\Plugins</code> namespace, a package whose class name is already used is rejected: <em>“Class Badge is already used by the View/Badge plugin — rename your class.”</em></li>
                <li><strong>Zip-slip safe</strong> — entries with absolute paths, <code>..</code> traversal, or multiple top-level folders are rejected.</li>
                <li><strong>Backup &amp; rollback</strong> — <em>Update</em> backs up the current version first and restores it automatically if the new package is invalid. The enabled/disabled state and ownership are preserved.</li>
            </ul>
            <div class="pm-note tip"><i class="bi bi-life-preserver"></i><div><strong>One bad plugin never breaks the platform.</strong> Even if an invalid or colliding plugin reaches disk by hand, discovery <em>skips it and keeps going</em> — the Designer and every build stay up. Skipped plugins are recorded (surfaced via <code>PluginRegistry::errors()</code>) instead of crashing the scan.</div></div>
            <h3>Ownership &amp; Core vs Custom</h3>
            <p>Whoever installs a plugin owns it: developers can manage their own installs; administrators can manage any. <strong>Core</strong> plugins (built-ins, or anything the owner marks Core) are managed exclusively by the platform owner. This keeps a shared install safe as it grows.</p>
        </section>

        <!-- 13. Best practices -->
        <section class="pm-sec" id="bestpractices">
            <h2><span class="num">13</span> Best practices</h2>
            <ul>
                <li><strong>Escape everything.</strong> Every dynamic value that reaches HTML goes through <code>htmlspecialchars(..., ENT_QUOTES)</code>. This is the single most important rule.</li>
                <li><strong>Be side-effect free.</strong> <code>Plugin.php</code> declares a class and nothing more — no output, no globals, no DB calls at file scope.</li>
                <li><strong>Keep JS idempotent.</strong> Guard the global init once per page and each element once. Assume your init runs many times.</li>
                <li><strong>Fire real events.</strong> When your control changes a value programmatically, dispatch a bubbling <code>change</code> event so inline editors and paired fields react.</li>
                <li><strong>Respect the theme.</strong> Read <code>data-pp-theme</code> / <code>prefers-color-scheme</code> and style for both light and dark.</li>
                <li><strong>Store safe values.</strong> Post values a database column will accept (e.g. an ISO <code>Y-m-d</code> for a DATE column) even if you <em>display</em> another format.</li>
                <li><strong>Sync validation.</strong> Keep <code>serverCheck()</code> and <code>clientRule()</code> equivalent; the server is authoritative, the client is UX.</li>
                <li><strong>Bundle locally.</strong> No CDNs, no external fonts — ship libraries inside the plugin.</li>
                <li><strong>Fail soft.</strong> Degrade gracefully if a library is missing (a rich input should still accept typed text).</li>
            </ul>
        </section>

        <!-- 14. Checklist -->
        <section class="pm-sec" id="checklist">
            <h2><span class="num">14</span> Release checklist</h2>
            <ul>
                <li>☐ Folder under the correct family; <code>Plugin.php</code> declares the class in <code>PpPhp\Plugins</code>.</li>
                <li>☐ Implements the right interface; all required static methods present.</li>
                <li>☐ Edit controls: value input carries <code>name="$fieldName"</code>.</li>
                <li>☐ All dynamic output escaped.</li>
                <li>☐ <code>settingsSchema()</code> covers every option, with sensible defaults and hints.</li>
                <li>☐ Any JS is guarded and idempotent; enhancement re-runs safely.</li>
                <li>☐ Libraries bundled locally and declared in <code>assets()</code>.</li>
                <li>☐ Optional <code>version()</code>/<code>description()</code>/<code>docsUrl()</code>/<code>vendor()</code> filled in.</li>
                <li>☐ Verified in the Designer preview <em>and</em> a generated app (including inline-edit for Edit plugins).</li>
                <li>☐ Zipped with a single top-level folder for distribution.</li>
            </ul>
            <div class="pm-note tip"><i class="bi bi-rocket-takeoff"></i><div>That's it — you're ready to build. Start from a sibling plugin, keep it small, verify early in the Designer, and grow from there.</div></div>
        </section>
    </main>
  </div>
</div>

<script src="<?= $assetsUrl ?>/bootstrap/js/bootstrap.bundle.min.js"></script>
<script>
(function(){
  // Copy-to-clipboard for code blocks
  document.querySelectorAll('.pm-code-copy').forEach(function(btn){
    btn.addEventListener('click', function(){
      var code = document.getElementById(btn.getAttribute('data-copy'));
      if (!code) return;
      var text = code.textContent;
      var done = function(){
        btn.classList.add('copied');
        btn.innerHTML = '<i class="bi bi-check-lg"></i>';
        setTimeout(function(){ btn.classList.remove('copied'); btn.innerHTML = '<i class="bi bi-clipboard"></i>'; }, 1400);
      };
      if (navigator.clipboard && navigator.clipboard.writeText) {
        navigator.clipboard.writeText(text).then(done).catch(function(){});
      } else {
        var ta = document.createElement('textarea'); ta.value = text; document.body.appendChild(ta);
        ta.select(); try { document.execCommand('copy'); } catch(e){} document.body.removeChild(ta); done();
      }
    });
  });

  // Scroll-spy for the TOC
  var links = Array.prototype.slice.call(document.querySelectorAll('#pmToc a'));
  var map = {}; links.forEach(function(a){ map[a.getAttribute('href').slice(1)] = a; });
  var sections = Array.prototype.slice.call(document.querySelectorAll('.pm-sec'));
  var spy = function(){
    var pos = window.scrollY + 120, current = sections[0];
    sections.forEach(function(s){ if (s.offsetTop <= pos) current = s; });
    links.forEach(function(a){ a.classList.remove('active'); });
    if (current && map[current.id]) map[current.id].classList.add('active');
  };
  window.addEventListener('scroll', spy, { passive:true });
  spy();
})();
</script>
</body>
</html>
