<?php
/*
 * Owner/admin-only developer guide: Building Designer Components.
 * Served by modules/Help/HelpController (topic = "designer_components").
 * The $c() helper prints an HTML-safe code block from a nowdoc, so code
 * samples containing <, >, & display literally.
 */
$c = static fn (string $s): string => '<pre><code>' . htmlspecialchars($s) . '</code></pre>';
?>

<h1><i class="bi bi-puzzle"></i> Building Designer Components</h1>
<p class="lead">A step-by-step guide to creating your own drag-and-drop blocks for the visual
Designer &mdash; the same way the built-in <em>Menu Card</em>, <em>Clock</em> and <em>KPI Tile</em>
were made. Written for beginners: if you can copy, paste and change a few words, you can add a
component.</p>

<div class="alert alert-info">
    <strong>Who is this for?</strong> Owners and administrators. This guide is hidden from regular users.
</div>

<h2>Contents</h2>
<ul>
    <li><a href="#idea">The big idea &mdash; what a component is</a></li>
    <li><a href="#golden">The golden rule (WYSIWYG) &amp; the three files</a></li>
    <li><a href="#files">Where the files live</a></li>
    <li><a href="#step1">Step 1 &mdash; Register the component (JavaScript)</a></li>
    <li><a href="#fields">The properties panel &mdash; field types you can use</a></li>
    <li><a href="#step2">Step 2 &mdash; Render it in the built app (PHP)</a></li>
    <li><a href="#step3">Step 3 &mdash; Style it (CSS) and make it ship</a></li>
    <li><a href="#theme">The theme rule &mdash; always use colour tokens</a></li>
    <li><a href="#example">Full worked example: a &ldquo;Callout&rdquo; component</a></li>
    <li><a href="#databound">Advanced: components that show live data</a></li>
    <li><a href="#containers">Advanced: container components (that hold others)</a></li>
    <li><a href="#verify">Test &amp; verify your component</a></li>
    <li><a href="#checklist">Checklist &amp; common mistakes</a></li>
</ul>

<!-- ============================================================ -->
<h2 id="idea">The big idea &mdash; what a component is</h2>
<p>A <strong>component</strong> is a building block you can drag from the left palette onto the
canvas &mdash; a heading, a card, a chart, a clock. Each component has:</p>
<ul>
    <li><strong>Properties</strong> (also called <em>props</em>) &mdash; the settings a user changes
        in the right-hand panel (text, colour, size&hellip;).</li>
    <li>A <strong>preview</strong> that shows on the design canvas.</li>
    <li>A <strong>final output</strong> that appears in the generated (published) app.</li>
</ul>

<!-- ============================================================ -->
<h2 id="golden">The golden rule (WYSIWYG) &amp; the three files</h2>
<p><strong>WYSIWYG</strong> means &ldquo;What You See Is What You Get&rdquo;. Whatever the user
designs on the canvas <em>must</em> look the same in the published app. Because of this, a component
is written in <strong>two languages that must agree</strong>: JavaScript for the canvas, and PHP for
the built app.</p>

<table class="table table-bordered">
    <thead><tr><th>File</th><th>Language</th><th>Its job</th></tr></thead>
    <tbody>
        <tr><td><code>registry.js</code></td><td>JavaScript</td><td>Adds the block to the palette &amp; draws its <strong>canvas preview</strong> + its <strong>properties panel</strong>.</td></tr>
        <tr><td><code>ComponentRenderer.php</code></td><td>PHP</td><td>Draws the <strong>real output</strong> in the published app.</td></tr>
        <tr><td><code>designer.css</code></td><td>CSS</td><td>The <strong>styling</strong>, shared by both the canvas and the built app.</td></tr>
    </tbody>
</table>

<div class="alert alert-warning">
    <strong>Two hidden gates you must not forget:</strong>
    <ol class="mb-0">
        <li><code>handles()</code> in <code>ComponentRenderer.php</code> &mdash; a whitelist. If your
            component type is not listed there, it is <strong>silently dropped</strong> from the
            built app (it will show on the canvas but vanish when published).</li>
        <li>The <strong>CSS allow-list</strong> in <code>BuildWriter.php</code> &mdash; if your CSS
            class prefix is not listed, your component <strong>ships unstyled</strong>.</li>
    </ol>
</div>

<!-- ============================================================ -->
<h2 id="files">Where the files live</h2>
<?= $c(<<<'TXT'
modules/Designer/Views/app/registry.js     ← 1. register + canvas preview + properties
core/Build/ComponentRenderer.php            ← 2. render() case + handles() whitelist
modules/Designer/Views/app/designer.css     ← 3. styling (theme-aware)
core/Build/BuildWriter.php                  ←    extractComponentCss() CSS allow-list
TXT) ?>
<p>Tip: search each file for an existing simple component such as <code>'callout'</code>,
<code>'alert'</code> or <code>'heading'</code> and copy its shape.</p>

<!-- ============================================================ -->
<h2 id="step1">Step 1 &mdash; Register the component (JavaScript)</h2>
<p>Open <code>registry.js</code> and add a <code>def(&hellip;)</code> call. <code>def</code> means
&ldquo;define a component&rdquo;. Here is the skeleton, with every part explained:</p>

<?= $c(<<<'JS'
def('callout', {                          // 1. the TYPE: a unique id, lower-case with hyphens
    label: 'Callout',                     // 2. name shown in the palette
    icon: 'bi-megaphone',                 // 3. a Bootstrap icon name (https://icons.getbootstrap.com)
    category: 'Content',                  // 4. which palette group it appears under

    // 5. the STARTING values for the properties (props) when first dropped
    defaults: function () {
        return { text: 'Heads up!', tone: 'info' };
    },

    // 6. the CANVAS PREVIEW — return an HTML string. `n` is the node, n.props are the settings.
    render: function (n) {
        var p = n.props;
        return '<div class="pp-callout pp-callout-' + esc(p.tone || 'info') + '">'
             + esc(p.text || '') + '</div>';
    },

    // 7. the PROPERTIES PANEL — a list of groups, each with fields the user can edit
    schema: function () {
        return [
            generalGroup([
                { key: 'text', label: 'Message', type: 'textarea' },
                { key: 'tone', label: 'Tone', type: 'select',
                  options: ['info', 'success', 'warning', 'danger'] }
            ]),
            styleGroup()
        ];
    }
});
JS) ?>

<p>Three helpers you will use constantly:</p>
<ul>
    <li><code>esc(value)</code> &mdash; makes text safe for HTML (turns <code>&lt;</code> into
        <code>&amp;lt;</code>). <strong>Always wrap user text in <code>esc()</code></strong> to
        prevent broken layouts and security holes.</li>
    <li><code>generalGroup([&hellip;])</code> &mdash; the top &ldquo;General&rdquo; panel section. It
        automatically adds a <em>Name</em> and <em>Visible</em> field around yours.</li>
    <li><code>styleGroup()</code> &mdash; a ready-made &ldquo;Style&rdquo; section (colours, padding,
        margins, border, shadow, custom CSS). Add it and users get all of that for free.</li>
</ul>

<!-- ============================================================ -->
<h2 id="fields">The properties panel &mdash; field types you can use</h2>
<p>Inside a schema group, each field is an object with a <code>key</code> (the prop name),
a <code>label</code>, and a <code>type</code>. These are the available types:</p>

<table class="table table-bordered table-sm">
    <thead><tr><th>type</th><th>What the user sees</th><th>Example</th></tr></thead>
    <tbody>
        <tr><td><code>text</code></td><td>One-line text box</td><td><code>{ key:'title', label:'Title', type:'text' }</code></td></tr>
        <tr><td><code>textarea</code></td><td>Multi-line text box</td><td><code>{ key:'body', label:'Body', type:'textarea' }</code></td></tr>
        <tr><td><code>number</code></td><td>Number box</td><td><code>{ key:'count', label:'Count', type:'number' }</code></td></tr>
        <tr><td><code>bool</code></td><td>Checkbox (on/off)</td><td><code>{ key:'showIcon', label:'Show icon', type:'bool', def:true }</code></td></tr>
        <tr><td><code>select</code></td><td>Dropdown</td><td><code>{ key:'size', label:'Size', type:'select', options:['sm','md','lg'] }</code></td></tr>
        <tr><td><code>color</code></td><td>Colour picker + hex box</td><td><code>{ key:'bg', label:'Background', type:'color' }</code></td></tr>
        <tr><td><code>range</code></td><td>Slider with a live read-out</td><td><code>{ key:'gap', label:'Gap', type:'range', min:0, max:60, step:2, suffix:'px', def:12 }</code></td></tr>
        <tr><td><code>icon</code></td><td>Icon picker</td><td><code>{ key:'icon', label:'Icon', type:'icon' }</code></td></tr>
        <tr><td><code>csv</code></td><td>Comma-separated list</td><td><code>{ key:'tags', label:'Tags (csv)', type:'csv' }</code></td></tr>
        <tr><td><code>dbtable</code></td><td>Dropdown of the project&rsquo;s tables</td><td><code>{ key:'table', label:'Table', type:'dbtable' }</code></td></tr>
        <tr><td><code>dbcolumn</code></td><td>Dropdown of a chosen table&rsquo;s columns</td><td><code>{ key:'col', label:'Column', type:'dbcolumn', tableKey:'table' }</code></td></tr>
    </tbody>
</table>

<p>Handy extras you can add to any field object:</p>
<ul>
    <li><code>placeholder: 'e.g. 12px'</code> &mdash; hint text inside the box.</li>
    <li><code>def: true</code> &mdash; the default shown for a <code>bool</code>/<code>range</code>
        before the user touches it.</li>
    <li><code>when: 'otherKey'</code> &mdash; only show this field when <code>otherKey</code> is set.
        Or <code>when: { key:'centerType', eq:'image' }</code> to show it only when another field
        equals a value.</li>
    <li><code>rerender: true</code> &mdash; on a <code>select</code> or <code>bool</code>, rebuild the
        panel when it changes (used to reveal/hide other fields live).</li>
</ul>

<!-- ============================================================ -->
<h2 id="step2">Step 2 &mdash; Render it in the built app (PHP)</h2>
<p>Now teach the publisher how to draw your component. Open
<code>core/Build/ComponentRenderer.php</code> and add a <code>case</code> inside the big
<code>render()</code> switch. It should produce the <strong>same HTML</strong> as your canvas
preview.</p>

<?= $c(<<<'PHP'
case 'callout':
    // $p holds the props (settings). Always validate/escape.
    $tone = in_array($p['tone'] ?? 'info', ['info', 'success', 'warning', 'danger'], true)
        ? (string) $p['tone'] : 'info';
    return '<div class="pp-callout pp-callout-' . $tone . '">'
        . self::e((string) ($p['text'] ?? '')) . '</div>';
PHP) ?>

<p><code>self::e(&hellip;)</code> is the PHP twin of <code>esc()</code> &mdash; it is
<code>htmlspecialchars()</code>. Use it for every piece of user text.</p>

<div class="alert alert-danger">
    <strong>Do not forget the whitelist!</strong> In the same file, find the
    <code>handles()</code> method and add your type to the list:
    <?= $c(<<<'PHP'
$set = array_flip([
    'card', 'menu-card', 'callout',   // ← add 'callout' here
    'divider', 'alert', 'heading', ...
]);
PHP) ?>
    Without this, your component renders on the canvas but is <strong>silently removed</strong> from
    the published app.
</div>

<!-- ============================================================ -->
<h2 id="step3">Step 3 &mdash; Style it (CSS) and make it ship</h2>
<p>Add your styles to <code>modules/Designer/Views/app/designer.css</code>. This one file styles
both the canvas and the built app, so there is only one place to edit.</p>

<?= $c(<<<'CSS'
.pp-callout { padding: 12px 16px; border-radius: 12px; font-weight: 600;
  border: 1px solid var(--pp-border, #e2e8f0); background: var(--pp-surface, #fff);
  color: var(--pp-text, #0f172a); }
.pp-callout-success { background: color-mix(in srgb, var(--pp-success, #16a34a) 12%, transparent); }
.pp-callout-warning { background: color-mix(in srgb, var(--pp-warning, #d97706) 12%, transparent); }
.pp-callout-danger  { background: color-mix(in srgb, var(--pp-danger,  #dc2626) 12%, transparent); }
CSS) ?>

<div class="alert alert-warning">
    <strong>Make the CSS ship.</strong> The built app only includes CSS whose class prefix is on an
    allow-list. Open <code>core/Build/BuildWriter.php</code>, find
    <code>extractComponentCss()</code> and add your prefix to the <code>$allow</code> array:
    <?= $c(<<<'PHP'
$allow = ['.pp-hero', '.pp-menu-page', '.pp-mc', '.pp-callout', /* … */];
PHP) ?>
    Any selector that <em>contains</em> <code>.pp-callout</code> will then be shipped.
</div>

<!-- ============================================================ -->
<h2 id="theme">The theme rule &mdash; always use colour tokens</h2>
<p><?= htmlspecialchars(\PpPhp\Core\Brand::name()) ?> apps can be re-themed instantly. For your component to follow the theme, <strong>never
hard-code colours</strong>. Instead use a <em>token</em> with a fallback:</p>
<?= $c(<<<'CSS'
/* GOOD — follows the theme, falls back to the hex if the token is missing */
color: var(--pp-primary, #4f46e5);
background: color-mix(in srgb, var(--pp-primary, #4f46e5) 12%, transparent);

/* BAD — frozen colour, ignores the theme */
color: #4f46e5;
CSS) ?>
<p>The most useful tokens:</p>
<table class="table table-bordered table-sm">
    <tbody>
        <tr><td><code>--pp-primary</code> / <code>--pp-primary-text</code></td><td>Brand colour &amp; text on it</td></tr>
        <tr><td><code>--pp-text</code> / <code>--pp-muted</code></td><td>Body text &amp; subtle text</td></tr>
        <tr><td><code>--pp-surface</code> / <code>--pp-surface-soft</code></td><td>Card / soft background</td></tr>
        <tr><td><code>--pp-border</code></td><td>Border colour</td></tr>
        <tr><td><code>--pp-success</code> / <code>--pp-warning</code> / <code>--pp-danger</code> / <code>--pp-info</code></td><td>Status colours</td></tr>
        <tr><td><code>--pp-radius</code> / <code>--pp-radius-lg</code></td><td>Corner rounding</td></tr>
    </tbody>
</table>

<!-- ============================================================ -->
<h2 id="example">Full worked example &mdash; the &ldquo;Callout&rdquo; component</h2>
<p>Putting all three steps together, here is the complete recipe.</p>

<h3>1. registry.js</h3>
<?= $c(<<<'JS'
def('callout', {
    label: 'Callout', icon: 'bi-megaphone', category: 'Content',
    defaults: function () { return { text: 'Heads up!', tone: 'info' }; },
    render: function (n) {
        var p = n.props;
        return '<div class="pp-callout pp-callout-' + esc(p.tone || 'info') + '">'
             + esc(p.text || '') + '</div>';
    },
    schema: function () { return [
        generalGroup([
            { key: 'text', label: 'Message', type: 'textarea' },
            { key: 'tone', label: 'Tone', type: 'select', options: ['info','success','warning','danger'] }
        ]),
        styleGroup()
    ]; }
});
JS) ?>

<h3>2. ComponentRenderer.php &mdash; the <code>case</code> + the <code>handles()</code> entry</h3>
<?= $c(<<<'PHP'
case 'callout':
    $tone = in_array($p['tone'] ?? 'info', ['info','success','warning','danger'], true)
        ? (string) $p['tone'] : 'info';
    return '<div class="pp-callout pp-callout-' . $tone . '">'
        . self::e((string) ($p['text'] ?? '')) . '</div>';

// …and in handles():  'card', 'menu-card', 'callout', …
PHP) ?>

<h3>3. designer.css + the allow-list</h3>
<?= $c(<<<'CSS'
.pp-callout { padding:12px 16px; border-radius:var(--pp-radius-lg,12px); font-weight:600;
  border:1px solid var(--pp-border,#e2e8f0); background:var(--pp-surface,#fff); color:var(--pp-text,#0f172a); }
.pp-callout-success { background: color-mix(in srgb, var(--pp-success,#16a34a) 12%, transparent); }
/* + add '.pp-callout' to $allow in BuildWriter::extractComponentCss() */
CSS) ?>

<p>Rebuild the project and your Callout appears in the palette, previews on the canvas, and renders
identically in the published app. That is the whole pattern. 🎉</p>

<!-- ============================================================ -->
<h2 id="databound">Advanced &mdash; components that show live data</h2>
<p>A component can read from the project&rsquo;s database. Because the canvas has no database, the
<strong>canvas preview shows a placeholder</strong>, and the PHP side emits a little
<code>&lt;?php &hellip; ?&gt;</code> block that queries the data <em>when the page loads</em>. This is
exactly how the KPI tiles, charts and the data-bound dashboard widgets work.</p>

<p>Two safety rules when building the SQL string in PHP:</p>
<ul>
    <li><strong>Sanitise table/column names</strong> &mdash; strip anything that is not a letter,
        number or underscore: <code>preg_replace('/[^A-Za-z0-9_]/', '', $name)</code>.</li>
    <li><strong>Escape output</strong> with <code>htmlspecialchars()</code>.</li>
</ul>

<?= $c(<<<'PHP'
// PHP side (ComponentRenderer): emit a runtime query block
$t   = preg_replace('/[^A-Za-z0-9_]/', '', (string) ($p['table'] ?? ''));
$col = preg_replace('/[^A-Za-z0-9_]/', '', (string) ($p['column'] ?? ''));
if ($t === '') return '<div class="pp-muted">Bind this widget to a table</div>';

return '<div class="pp-count"><?php '
    . '$__n = (int) \PpPhp\Core\DB::FetchOne("SELECT COUNT(`' . $col . '`) FROM `' . $t . '`"); '
    . 'echo htmlspecialchars((string) $__n); ?></div>';
PHP) ?>

<p>In <code>registry.js</code>, give the user pickers with <code>type:'dbtable'</code> and
<code>type:'dbcolumn'</code> (see the field-type table above), and make the canvas preview show a
sample number or the table name.</p>

<!-- ============================================================ -->
<h2 id="containers">Advanced &mdash; container components (that hold others)</h2>
<p>A container (like <em>Section</em>, <em>Group Container</em> or <em>Menu Group</em>) can accept
other components dropped inside it. Set <code>container: true</code> and render a child zone.</p>

<?= $c(<<<'JS'
// registry.js — canvas side
def('panel', {
    label: 'Panel', icon: 'bi-window', category: 'Layout', container: true,
    defaults: function () { return { heading: 'Panel' }; },
    render: function (n, ctx) {                 // note the 2nd argument: ctx
        return '<div class="pp-panel"><div class="pp-panel-h">' + esc(n.props.heading) + '</div>'
             + ctx.renderChildren(n, 'pp-panel-body pp-zone12 gap-normal') + '</div>';
    },
    schema: function () { return [ generalGroup([{ key:'heading', label:'Heading', type:'text' }]), styleGroup() ]; }
});
JS) ?>

<?= $c(<<<'PHP'
// ComponentRenderer.php — PHP side. $renderChildren draws the children.
case 'panel':
    return '<div class="pp-panel"><div class="pp-panel-h">' . self::e((string) ($p['heading'] ?? 'Panel')) . '</div>'
        . '<div class="pp-panel-body pp-zone12 gap-normal">' . $renderChildren($c) . '</div></div>';
    // (remember to add 'panel' to handles())
PHP) ?>
<p>Adding the class <code>pp-zone12</code> to the body makes it a 12-column grid, so children obey
their own <em>Size &amp; Layout</em> width (e.g. a child set to 3/12 = four per row).</p>

<!-- ============================================================ -->
<h2 id="verify">Test &amp; verify your component</h2>
<ol>
    <li><strong>Hard-refresh the Designer</strong> (<kbd>Cmd/Ctrl</kbd> + <kbd>Shift</kbd> +
        <kbd>R</kbd>) so the new <code>registry.js</code> loads. Your block should appear in the
        palette and preview on the canvas.</li>
    <li><strong>Build the project</strong> and open the published page. The output must look the same
        as the canvas.</li>
    <li>If it shows on the canvas but <strong>disappears when published</strong> &rarr; you missed the
        <code>handles()</code> entry.</li>
    <li>If it appears but looks <strong>unstyled</strong> &rarr; you missed the CSS allow-list, or your
        class prefix does not match.</li>
    <li>If colours <strong>ignore the theme</strong> &rarr; you hard-coded a hex instead of a
        <code>var(--pp-*)</code> token.</li>
</ol>

<!-- ============================================================ -->
<h2 id="checklist">Checklist &amp; common mistakes</h2>
<table class="table table-bordered table-sm">
    <thead><tr><th>Done?</th><th>Step</th></tr></thead>
    <tbody>
        <tr><td>☐</td><td><code>def('type', …)</code> added to <code>registry.js</code> with <code>defaults</code>, <code>render</code>, <code>schema</code>.</td></tr>
        <tr><td>☐</td><td><code>case 'type':</code> added to <code>ComponentRenderer::render()</code> &mdash; same HTML as the canvas.</td></tr>
        <tr><td>☐</td><td><code>'type'</code> added to <code>ComponentRenderer::handles()</code>. <strong>(most-forgotten step)</strong></td></tr>
        <tr><td>☐</td><td>CSS added to <code>designer.css</code> using <code>var(--pp-*)</code> tokens.</td></tr>
        <tr><td>☐</td><td>Class prefix added to <code>$allow</code> in <code>BuildWriter::extractComponentCss()</code>.</td></tr>
        <tr><td>☐</td><td>All user text wrapped in <code>esc()</code> (JS) and <code>self::e()</code> (PHP).</td></tr>
        <tr><td>☐</td><td>Canvas preview and PHP output produce the same HTML.</td></tr>
    </tbody>
</table>

<div class="alert alert-success">
    <strong>Rule of thumb:</strong> copy an existing component that is close to what you want
    (<code>alert</code>, <code>menu-card</code>, <code>kpi-stat</code>), rename it, and change the
    parts you need. Every built-in component follows exactly the pattern above.
</div>

<p class="text-muted"><small>See also: the <a href="field_plugins">Field Plugins</a> guide for
building view / edit / filter controls for individual table fields.</small></p>
