Skip to main content
Version: 6

v6 Extension Conversion Guide

Audience: Plugin and module developers porting an extension from Blesta 5.x to Blesta 6.

Purpose: A single, exhaustive reference of the changes a v5 extension's admin views need in order to render natively under Paradigm. Sections are independently consumable: tables, before/after snippets, and rules — no narrative.

Source: Distilled from a side-by-side comparison of the Blesta 5.13 and Blesta 6 codebases for support_manager, order, cms, feed_reader, system_overview, and the core module base classes.

Download as AI context

This entire guide is also available as a single Markdown file you can drop into Claude, ChatGPT, Cursor, or any other AI assistant to perform a one-shot extension conversion: v6-extension-conversion.md. §17 shows a working prompt to paste alongside it.

Extension Development Kit

For a broader, per-extension-type reference, see the Blesta Extension Development Kit on GitHub. Where this page focuses on the view/UI conversion rules, the dev kit covers the full PHP contract for each extension type and three workflows:

  • Author a new Blesta 6 extension — modules (hosting and registrar), plugins, and payment gateways (merchant and nonmerchant).
  • Convert a 5.x extension to 6.0 — porting guides per extension type, plus the shared UI conversion reference.
  • Convert a WHMCS extension to Blesta — provisioning/registrar modules, addon modules, and gateways, with a shared WHMCS→Blesta conventions guide.

The kit is plain Markdown, designed to be used two ways: drop the relevant reference file into any AI tool (Claude, ChatGPT, Cursor, Copilot) as context, or install the folder as a Claude Code skill so the right reference is loaded automatically.


1. What changed at a glance

LayerBlesta 5.x (default admin theme)Blesta 6 (paradigm admin theme)
CSS frameworkBootstrap 4Bootstrap 5
Icon setFontAwesome 5 (fas fa-…)Bootstrap Icons (bi bi-…)
JavaScriptjQuery + custom pluginsVanilla JS (no jQuery)
Theme switchingNoneLight / Dark mode via data-bs-theme
Tooltipsdata-toggle="tooltip" (auto-init)data-bs-toggle="tooltip" (manual init)
Modal confirms$.fn.blestaModalConfirmblestaModalConfirm() global + declarative .modal-confirm-delete class
SortablejQuery UI (blestaSortable)Sortable.js
AJAX widget swap target.common_box_content.card-content
Default PHP version≥ 7.2≥ 8.2
Forms inside cards<div class="inner"> + <ul><li><div class="card-body"> + <div class="mb-3">

The legacy default admin theme has been removed in Blesta 6 — Paradigm is the only admin template, and the app/views/admin/default/ view tree no longer contains any .pdt files. Paradigm does ship a SCSS compatibility shim (assets/sass/admin/paradigm/_backwards.scss) that re-styles many legacy v5 class names (common_box, inner, pad, title_row, pull-right, etc.) onto Bootstrap 5 — so a v5 plugin's .pdt files will still render acceptably under Paradigm without conversion. They will look out of place, however; convert your views per the rules below for a native experience.

Modules in v6 core ship UNCONVERTED (still BS4 + FA + jQuery markup, leaning on the SCSS shim). Don't model your conversion on the shipped core modules — model it on app/views/admin/paradigm/admin_clients.pdt or any of the converted plugins (order, support_manager, cms, feed_reader, system_overview).


2. Conversion checklist (TL;DR)

Apply these in order:

  1. Bootstrap data attributes: every data-toggle, data-target, data-dismiss, data-parent, data-placement, data-html, data-container, data-auto-close gets a -bs- infix → data-bs-toggle, data-bs-target, etc.
  2. Float/margin/text utilities: pull-right/float-rightfloat-end; pull-left/float-leftfloat-start; ml-N/mr-Nms-N/me-N; pl-N/pr-Nps-N/pe-N; text-left/text-righttext-start/text-end; font-weight-boldfw-bold; sr-onlyvisually-hidden.
  3. Buttons: btn-defaultbtn-secondary (or btn-outline-secondary for icon-only row actions); btn-blockw-100 (or wrap in d-grid).
  4. Badges: badge badge-{color}badge bg-{color}. Yellow needs dark text: badge bg-warning text-dark.
  5. Form controls: pass 'class' => 'form-control' to text/textarea/file, 'class' => 'form-select' to selects, 'class' => 'form-check-input' to checkboxes/radios. Replace form-group with mb-3. Replace <ul><li> form rows with <div class="mb-3"> blocks. Replace custom-control family with form-check family.
  6. Form labels: pass 'class' => 'form-label' to top-block labels, 'class' => 'form-check-label' to checkbox/radio labels. Replace <label class="inline"> with <label class="form-check-label">.
  7. Cards: replace <div class="inner"> / <div class="pad"> / <div class="title_row"> / <div class="button_row"> with BS5 card sections (see §7.2).
  8. Tables: wrap in <div class="table-responsive">; use <table class="table table-hover mb-0">; add proper <thead>/<tbody>; drop class="heading_row", class="last", manual odd_row zebra striping.
  9. Modal close: <button class="close" data-dismiss="modal"><span>&times;</span></button><button class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>.
  10. Icons: replace every <i class="fas|far|fa fa-…"> with <i class="bi bi-…"> (see §5 for the mapping table).
  11. JavaScript: replace every $(document).ready(...), jQuery selector, jQuery event, $.fn.blestaXxx, blestaRequest({dataType:'json'}) with vanilla JS equivalents (see §6).
  12. Widget icon registrations: in setLinkButtons, setLinks, setWidgetButton, and any 'icon' => 'fas fa-…' keys in PHP, change values to bi bi-….
  13. Widget renamed buttons: setWidgetButton('full_screen')setWidgetButton('full-screen') (hyphen, not underscore). Optionally add setWidgetButton('widget-width') for dashboard widgets.
  14. AJAX swap target: if your code emits an AJAX widget that replaces .common_box_content, change the target to .card-content.
  15. Module config.json: optionally add "icon": "bi bi-…" so Paradigm renders a nice icon for your module.
  16. Module PHP base class: rename arrayToModuleFields() overrides to arrayToInputFields() (old name still works as a deprecated alias).
  17. Test in both light and dark mode by toggling <html data-bs-theme="dark">/"light" in DevTools.

3. Admin theme: defaultparadigm

3.1 Paradigm is the only admin theme

The legacy default-template view tree (public_html/app/views/admin/default/*.pdt) has been removed in Blesta 6. Some legacy CSS, JS, and font assets remain at that path (admin/default/css/, admin/default/javascript/, admin/default/webfonts/), but no view code is loaded from them — they are orphaned files retained only to avoid breaking any plugin that hardcoded an asset URL during the transition.

v5 (default)v6 (paradigm)
Viewspublic_html/app/views/admin/default/public_html/app/views/admin/paradigm/
SCSSassets/sass/admin/default/assets/sass/admin/paradigm/
JSassets/scripts/admin/default/assets/scripts/admin/paradigm/
Compiled CSS…/css/application.min.css (gitignored, generated)…/css/application.min.css (gitignored, generated)
Compiled JS…/javascript/app.min.js…/javascript/app.min.js
Theme manifestn/a…/paradigm/config.json declares assets to load

The active layout is set in public_html/app/app_controller.php (paradigm for admin, with a template_dir company-level override). Do not assume default is the active admin theme.

A SCSS backwards-compatibility shim (assets/sass/admin/paradigm/_backwards.scss) re-styles many legacy v5 class names (common_box, inner, pad, title_row, button_row, pull-right, etc.) onto Bootstrap 5 so unconverted plugin and module .pdt files still render acceptably inside Paradigm. See §11.5 for the full list.

3.2 Plugin structure.pdt

Plugin views/default/structure.pdt is unchanged — it remains a pass-through <?php echo $content ?? null;?>. Plugins delegate chrome to the host theme. All your conversion work is in your action .pdt files.

3.3 New partials directory

Paradigm extracts shared chrome into app/views/admin/paradigm/partials/:

  • sidebar.pdt, notifications.pdt, admin_query_logger.pdt
  • main_sidebar.pdt, admin_clients_sidebar.pdt, admin_settings_sidebar.pdt, admin_chatbot_sidebar.pdt, plugin_sidebar.pdt

plugin_sidebar.pdt is the slot your plugin renders into when registering a side panel — see §10.


4. Bootstrap 4 → Bootstrap 5 class changes

4.1 Float / spacing / text utilities

BS4BS5
pull-right, float-rightfloat-end
pull-left, float-leftfloat-start
ml-N (1–5)ms-N
mr-Nme-N
pl-Nps-N
pr-Npe-N
text-lefttext-start
text-righttext-end
font-weight-boldfw-bold
font-weight-normalfw-normal
font-weight-lightfw-light
sr-onlyvisually-hidden
sr-only-focusablevisually-hidden-focusable
text-monospacefont-monospace

mt-N, mb-N, pt-N, pb-N, m-N, p-N are unchanged.

4.2 Buttons

BS4BS5
btn-defaultbtn-secondary (or btn-outline-secondary for icon-only)
btn-blockw-100 (or wrap parent in d-grid)
- <a class="btn btn-default float-right btn-sm" href="#">Cancel</a>
+ <a class="btn btn-outline-secondary float-end btn-sm" href="#">Cancel</a>

- <button class="btn btn-block btn-info">Action</button>
+ <button class="btn w-100 btn-info">Action</button>

Row action buttons are now icon-only outline buttons:

<a href="<?php echo $edit_url;?>" class="btn btn-sm btn-outline-secondary" title="Edit">
<i class="bi bi-pencil"></i>
</a>
<button type="button" class="btn btn-sm btn-outline-danger modal-confirm-delete"
data-confirm-message="<?php echo $this->Html->safe($confirm_msg);?>"
title="Delete">
<i class="bi bi-trash"></i>
</button>

4.3 Badges

- <span class="badge badge-secondary">5</span>
- <span class="badge badge-info">Info</span>
- <span class="badge badge-warning">Warn</span>
+ <span class="badge bg-secondary">5</span>
+ <span class="badge bg-info text-dark">Info</span>
+ <span class="badge bg-warning text-dark">Warn</span>

The yellow palette (bg-warning, bg-info, bg-light) needs text-dark for contrast in BS5.

Tab counters become real badges:

- 'name' => $label . ' <span>(' . $count . ')</span>'
+ 'name' => $label . ' <span class="badge bg-secondary ms-2">' . $count . '</span>'

A typical priority/status mapping helper:

$priority_classes = [
'emergency' => 'bg-danger',
'critical' => 'bg-warning text-dark',
'high' => 'bg-info text-dark',
'medium' => 'bg-primary',
'low' => 'bg-secondary'
];
$badge_class = $priority_classes[$ticket->priority] ?? 'bg-secondary';
?>
<span class="badge <?php echo $badge_class;?>"><?php echo $priorities[$ticket->priority];?></span>

4.4 Bootstrap data attributes (the -bs- infix)

Every Bootstrap-controlled data-* attribute gets a bs- prefix:

BS4BS5
data-toggle="tab"data-bs-toggle="tab"
data-toggle="collapse"data-bs-toggle="collapse"
data-toggle="tooltip"data-bs-toggle="tooltip"
data-toggle="modal"data-bs-toggle="modal"
data-toggle="dropdown"data-bs-toggle="dropdown"
data-target="#x"data-bs-target="#x"
data-dismiss="modal"data-bs-dismiss="modal"
data-dismiss="alert"data-bs-dismiss="alert"
data-parent="#x"data-bs-parent="#x"
data-placement="top"data-bs-placement="top"
data-html="true"data-bs-html="true"
data-container="body"data-bs-container="body"
data-auto-close="outside"data-bs-auto-close="outside"

4.5 Forms — controls, groups, checks

BS4BS5
<div class="form-group"><div class="mb-3">
<div class="form-row"><div class="row g-3">
<input class="form-control">unchanged
<select class="form-control"><select class="form-select">
<div class="custom-control custom-checkbox"><div class="form-check">
<input class="custom-control-input"><input class="form-check-input">
<label class="custom-control-label"><label class="form-check-label">
<select class="custom-select"><select class="form-select">
<input type="file" class="custom-file"><input type="file" class="form-control">

The Blesta Form helper does not auto-inject BS5 classes. You must pass them on every call:

- $this->Form->fieldText('name', $val, ['id' => 'name']);
- $this->Form->fieldSelect('priority', $opts, $val, ['id' => 'priority']);
- $this->Form->fieldCheckbox('clients_only', '1', $checked, ['id' => 'clients_only']);
- $this->Form->label('Name', 'name');
+ $this->Form->fieldText('name', $val, ['id' => 'name', 'class' => 'form-control']);
+ $this->Form->fieldSelect('priority', $opts, $val, ['id' => 'priority', 'class' => 'form-select']);
+ $this->Form->fieldCheckbox('clients_only', '1', $checked, ['id' => 'clients_only', 'class' => 'form-check-input']);
+ $this->Form->label('Name', 'name', ['class' => 'form-label']);

4.6 Form layout: <ul><li><div class="mb-3">

The single largest pattern change. The legacy "list of fields" wrapper is gone:

- <div class="pad">
- <ul>
- <li>
- <?php $this->Form->label('Name', 'name'); ?>
- <?php $this->Form->fieldText('name', null, ['id' => 'name']); ?>
- </li>
- <li>
- <?php $this->Form->fieldCheckbox('enabled', '1', $checked, ['id' => 'enabled']); ?>
- <?php $this->Form->label('Enabled', 'enabled', ['class' => 'inline']); ?>
- </li>
- </ul>
- </div>
+ <div class="mb-3">
+ <?php $this->Form->label('Name', 'name', ['class' => 'form-label']); ?>
+ <?php $this->Form->fieldText('name', null, ['id' => 'name', 'class' => 'form-control']); ?>
+ </div>
+ <div class="mb-3">
+ <div class="form-check">
+ <?php $this->Form->fieldCheckbox('enabled', '1', $checked, ['id' => 'enabled', 'class' => 'form-check-input']); ?>
+ <?php $this->Form->label('Enabled', 'enabled', ['class' => 'form-check-label']); ?>
+ </div>
+ </div>

Inline checkbox/radio groups use form-check-inline:

<div class="form-check form-check-inline">
<?php $this->Form->fieldCheckbox('opt[a]', '1', $checked, [
'id' => 'opt_a', 'class' => 'form-check-input'
]); ?>
<label class="form-check-label" for="opt_a">Option A</label>
</div>

Toggle switches:

<div class="form-check form-switch">
<?php $this->Form->fieldCheckbox('feature_enabled', 'true', $vars->feature_enabled === 'true', [
'id' => 'feature_enabled', 'class' => 'form-check-input'
]); ?>
<label class="form-check-label" for="feature_enabled">Enable feature</label>
</div>

Bracketed names need underscore IDs (because <label for=""> requires a valid HTML id):

- 'id' => 'settings[email][' . $key . ']'
- $this->Form->label($label, 'settings[email][' . $key . ']', ['class' => 'inline']);
+ 'id' => 'settings_email_' . $key
+ <label class="form-check-label" for="settings_email_<?php echo $key;?>"><?php echo $label;?></label>

4.7 Sized form controls

<input class="form-control form-control-sm">
<select class="form-select form-select-sm">
<button class="btn btn-sm btn-primary">

4.8 Modal close button

- <button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">&times;</span></button>
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>

.close is removed in BS5. The new .btn-close is an icon-only square button — no text content.

4.9 Tabs

Native BS5 tabs. The legacy Blesta tab_content / tabs / current markup (driven by jQuery blestaTabbedContent) is replaced:

- <div class="tab_content">
- <ul class="tabs">
- <li class="current"><a href="#">Reply</a></li>
- <li><a href="#">Note</a></li>
- </ul>
- <div class="inner_content">
- <div>...</div>
- <div>...</div>
- </div>
- </div>
+ <ul class="nav nav-tabs-connected" role="tablist">
+ <li class="nav-item" role="presentation">
+ <button class="nav-link active" id="tab-reply-btn" type="button" role="tab"
+ data-bs-toggle="tab" data-bs-target="#tab-reply">Reply</button>
+ </li>
+ <li class="nav-item" role="presentation">
+ <button class="nav-link" id="tab-note-btn" type="button" role="tab"
+ data-bs-toggle="tab" data-bs-target="#tab-note">Note</button>
+ </li>
+ </ul>
+ <div class="tab-content tab-content-connected mb-3">
+ <div class="tab-pane fade show active" id="tab-reply" role="tabpanel">...</div>
+ <div class="tab-pane fade" id="tab-note" role="tabpanel">...</div>
+ </div>

nav-tabs-connected and tab-content-connected are Paradigm-specific classes that flush-mount tabs against a card edge. Stock BS5 nav-tabs works too.

4.10 Tooltips

- <a data-toggle="tooltip" title="Help text">
- <i class="fas fa-question-circle"></i>
- </a>
+ <i class="bi bi-info-circle ms-1"
+ data-bs-toggle="tooltip" data-bs-placement="top"
+ title="<?php echo $this->Html->safe($help_text);?>"></i>

Tooltips are no longer auto-initialized. After page load (and after AJAX swaps), explicitly init:

document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(function(el) {
new bootstrap.Tooltip(el);
});

For HTML content add data-bs-html="true". For tooltips that escape overflow:hidden parents add data-bs-container="body".

4.11 Backgrounds / theme-aware colors

- <div class="bg-light p-3">
+ <div class="bg-body-secondary p-3">

- <pre class="bg-light"><code>...</code></pre>
+ <pre class="bg-body-secondary"><code>...</code></pre>

bg-body-secondary is BS 5.3+ and is theme-aware (works in light and dark mode). bg-light is hard-coded light gray and looks broken in dark mode.

4.12 Column / grid

BS4BS5
col-xs-Ncol-N (bare col- is now the smallest breakpoint)
row.equal (custom equal-height)row + g-N gutter / Bootstrap's flex utilities
- <div class="col-md-2 col-xs-4">
- <div class="row equal">
+ <div class="col-md-2 col-4">
+ <div class="row g-2">

4.13 Spinners

<div class="spinner-border spinner-border-sm text-secondary" role="status">
<span class="visually-hidden">Loading...</span>
</div>

4.14 Empty states

- <div class="empty_section">
- <div class="empty_box">No results.</div>
- </div>
+ <div class="card-body text-center text-muted py-5">
+ <i class="bi bi-inbox" style="font-size: 3rem; opacity: 0.3;"></i>
+ <p class="mt-3 mb-0">No results.</p>
+ </div>

Pick a contextual icon: bi-inbox (tickets, lists), bi-cart (orders), bi-cash-stack (invoices/payouts), bi-people (clients/staff), bi-box (products), bi-search (search), bi-file-earmark-text (articles), bi-building (departments), bi-input-cursor-text (custom fields), bi-calendar-week (schedules).

4.15 Pagination

- <?php $this->Pagination->build();?>
+ <?php if (isset($this->Pagination) && $this->Pagination->hasPages()) { ?>
+ <div class="card-footer">
+ <?php $this->Pagination->build();?>
+ </div>
+ <?php } ?>

Wrap in a card-footer and guard with hasPages() so empty paginators don't render.

4.16 Bulk actions bar

<div class="bulk-actions-bar" id="ticket_actions" style="display:none;">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2">
<span class="selected-count">0 items selected</span>
<div class="d-flex gap-2 align-items-center flex-wrap">
<?php $this->Form->fieldSelect('action', $actions, '', [
'id' => 'ticket_action',
'class' => 'form-select form-select-sm',
'style' => 'width: auto;'
]); ?>
<?php $this->Form->fieldSubmit('save', 'Update', ['class' => 'btn btn-sm btn-primary']); ?>
</div>
</div>
</div>

.bulk-actions-bar and .selected-count are Paradigm helper classes.

4.17 Misc

OldNew
<div class="clear"></div>(drop — flex/grid replaces clearfix needs)
<div class="accent_box"><div class="alert alert-info">
<div class="title_row [first]"><h3>…</h3></div><h6 class="form-section-header">…</h6>
<div class="thumbnail"><div class="template-preview-card"> (Paradigm class)
class="quiet"drop
class="block", class="inline", class="stretch"drop or replace with utility (d-block, d-inline, flex-grow-1)

5. Icons: FontAwesome → Bootstrap Icons

Every <i class="fas|far|fa fa-…"> must become <i class="bi bi-…">. Drop fa-fw (no equivalent).

5.1 Comprehensive mapping

FontAwesome 5/6Bootstrap IconsNotes
fas fa-plus / fa-plus fa-fwbi bi-plus-lg"Add" buttons
fas fa-plus-circlebi bi-plus-circleAdd row/option
fas fa-minusbi bi-dash-lg
fas fa-timesbi bi-x-lgClose
fas fa-times-circlebi bi-x-circleClear filter
fas fa-checkbi bi-check-lg
fas fa-check-circlebi bi-check-circle (or bi-check-circle-fill text-success)
fas fa-pencil-alt / fa-pencilbi bi-pencilEdit
fas fa-trash / fa-trash-altbi bi-trashDelete
fas fa-cog / fa-gear / fa-cogsbi bi-gear (or bi-gear-wide-connected)Settings
fas fa-eyebi bi-eyeVisible
fas fa-eye-slashbi bi-eye-slashHidden
fas fa-info-circlebi bi-info-circleInfo / tooltip
fas fa-exclamation-trianglebi bi-exclamation-triangleWarning
fas fa-question-circlebi bi-question-circleHelp tooltip
fas fa-shield / fa-shield-altbi bi-shield-exclamationSecurity/CSRF warning
fas fa-banbi bi-ban (or bi-x-circle)
fas fa-arrow-upbi bi-arrow-up
fas fa-arrow-downbi bi-arrow-down
fas fa-arrow-leftbi bi-arrow-leftBack
fas fa-arrow-rightbi bi-arrow-rightForward
fas fa-arrow-circle-leftbi bi-arrow-left-circle
fas fa-arrow-circle-rightbi bi-arrow-right-circle
fas fa-angle-left / fa-chevron-leftbi bi-chevron-left
fas fa-angle-right / fa-chevron-rightbi bi-chevron-right
fas fa-chevron-upbi bi-chevron-up
fas fa-chevron-downbi bi-chevron-down
fas fa-caret-downbi bi-chevron-downCollapse caret
fas fa-caret-upbi bi-chevron-up
fas fa-arrows-altbi bi-grip-verticalDrag handle (use bi-grip-vertical for sortable)
fas fa-grip-verticalbi bi-grip-vertical
fas fa-arrows-fullscreenbi bi-arrows-fullscreen
fas fa-arrows-angle-contractbi bi-arrows-angle-contract
fas fa-arrows-angle-expandbi bi-arrows-angle-expand
fas fa-fullscreen-exitbi bi-fullscreen-exit
fas fa-level-up-alt fa-rotate-90bi bi-arrow-90deg-up"Service info" header arrow
fas fa-level-down-alt fa-rotate-180bi bi-arrow-return-left
fas fa-share fa-flip-verticalbi bi-reply-fill (or bi-arrow-return-left)"Service info" client arrow
fas fa-replybi bi-reply
fas fa-redo-alt / fa-sync-altbi bi-arrow-clockwiseRefresh / regenerate
fas fa-exchange-altbi bi-arrow-left-right
fas fa-linkbi bi-link-45deg
fas fa-unlinkbi bi-link-45deg (no exact unlink — combine with overlay)
fas fa-external-link-altbi bi-box-arrow-up-rightExternal link
fas fa-sign-in-altbi bi-box-arrow-in-rightLogin as client
fas fa-sign-out-altbi bi-box-arrow-right
fas fa-power-offbi bi-power
fas fa-playbi bi-play-fill
fas fa-stopbi bi-stop-fill
fas fa-lockbi bi-lock
fas fa-keybi bi-key
fas fa-envelope / fa-envelope-open-textbi bi-envelope
fas fa-calendarbi bi-calendar3
fas fa-calendar-xbi bi-calendar-x
fas fa-calendar-weekbi bi-calendar-week
fas fa-clockbi bi-clock
fas fa-history / fa-clock-historybi bi-clock-history
fas fa-userbi bi-person
fas fa-usersbi bi-people
fas fa-user-circlebi bi-person-circle
fas fa-tagbi bi-tag
fas fa-tagsbi bi-tags
fas fa-folderbi bi-folder2 (or bi-folder-fill for solid)
fas fa-folder-openbi bi-folder2-open
fas fa-folder-plusbi bi-folder-plus
fas fa-file / fa-file-altbi bi-file-text (or bi-file-earmark-text)
fas fa-paperclipbi bi-paperclipAttachments
fas fa-imagebi bi-image
fas fa-cloud-upload-altbi bi-cloud-uploadFile drop zone
fas fa-uploadbi bi-upload
fas fa-downloadbi bi-download
fas fa-clone / fa-copybi bi-files (or bi-clipboard)
fas fa-clipboardbi bi-clipboard
fas fa-clipboard-checkbi bi-clipboard-check
fas fa-searchbi bi-search
fas fa-filterbi bi-funnel (or bi-funnel-fill)
fas fa-barsbi bi-list
fas fa-listbi bi-list-ul
fas fa-money-bill-wavebi bi-cash-stack
fas fa-dollar-signbi bi-currency-dollar
fas fa-chart-barbi bi-bar-chartStats
fas fa-chart-linebi bi-graph-up
fas fa-chart-piebi bi-pie-chart
fas fa-databasebi bi-database
fas fa-serverbi bi-hdd-rack
fas fa-hddbi bi-hdd
fas fa-microchipbi bi-cpu
fas fa-ethernetbi bi-ethernet (or bi-hdd-network)
fas fa-globe / fa-globe-americasbi bi-globe-americas
fas fa-sitemapbi bi-diagram-3
fas fa-terminalbi bi-terminal
fas fa-musicbi bi-music-note
fas fa-moonbi bi-moonDark mode toggle
fas fa-sunbi bi-sunLight mode toggle
fas fa-bellbi bi-bell
fas fa-star (filled)bi bi-star-fill
far fa-star (empty)bi bi-star
fas fa-starsbi bi-starsAI feature indicator
fas fa-stickynote / fa-sticky-notebi bi-stickyInternal notes
fas fa-chat-square-textbi bi-chat-square-textPredefined responses
fas fa-chat-left-quotebi bi-chat-left-quoteCustomer comment
fas fa-commentbi bi-chat
fas fa-lightbulbbi bi-lightbulb
fas fa-certificatebi bi-patch-check (or bi-award)
fas fa-hand-point-rightbi bi-hand-index
fas fa-shield-exclamationbi bi-shield-exclamation
fas fa-inboxbi bi-inbox
fas fa-buildingbi bi-building

5.2 Icon classes in PHP

The framework's setLinkButtons, setLinks, setWidgetButton, and module getAdminTabs/getClientTabs arrays all carry an 'icon' key:

  $link_buttons[] = [
'name' => $this->_('AdminMain.btn_add', true),
'attributes' => ['href' => $url],
- 'icon' => 'fas fa-plus'
+ 'icon' => 'bi bi-plus-lg'
];

// Module PHP file (e.g. cpanel.php)
- 'tabClientActions' => ['name' => Language::_('Cpanel.tab_client_actions', true), 'icon' => 'fas fa-cog'],
- 'tabClientLogin' => ['name' => Language::_('Cpanel.tab_client_login', true), 'icon' => 'fas fa-sign-in-alt'],
+ 'tabClientActions' => ['name' => Language::_('Cpanel.tab_client_actions', true), 'icon' => 'bi bi-gear'],
+ 'tabClientLogin' => ['name' => Language::_('Cpanel.tab_client_login', true), 'icon' => 'bi bi-box-arrow-in-right'],

5.3 Status icons with semantic colors

- <i class="fas fa-check-circle"></i>  <!-- CSS-styled green -->
- <i class="fas fa-times-circle"></i> <!-- CSS-styled red -->
+ <i class="bi bi-check-circle text-success"></i>
+ <i class="bi bi-x-circle text-danger"></i>

For services:

  • Active service: bi bi-check-circle-fill text-success
  • Scheduled cancellation: bi bi-calendar-x text-warning
  • Pending change (client): bi bi-clock-history text-warning
  • Pending change (billing): bi bi-clock text-info

5.4 Empty state icons

<i class="bi bi-inbox" style="font-size: 3rem; opacity: 0.3;"></i> or class="bi bi-inbox fs-1 mb-3 d-block opacity-50". Pick by context (see §4.14).


6. JavaScript: jQuery → vanilla JS

Paradigm bundles no jQuery. Every script in your converted views must be vanilla JS.

6.1 DOM ready

- $(document).ready(function() { ... });
+ document.addEventListener('DOMContentLoaded', function() { ... });

For widget/AJAX-loaded scripts that may run after DOMContentLoaded has already fired, use the IIFE pattern:

(function() {
'use strict';
function init() { /* ... */ }
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
document.addEventListener('blestaAjaxComplete', init); // re-bind after AJAX swaps
})();

6.2 Selector / event idiom replacements

jQueryVanilla JS
$('#x')document.getElementById('x')
$('.x')document.querySelectorAll('.x')
$(el).find('.x')el.querySelectorAll('.x')
$(el).closest('.x')el.closest('.x')
$(el).val() / .val(v)el.value / el.value = v
$(el).text(s)el.textContent = s
$(el).html(s)el.innerHTML = s
$(el).attr('href', x)el.setAttribute('href', x) (or el.href = x)
$(el).prop('checked', true)el.checked = true
$(el).prop('disabled', true)el.disabled = true
$(el).addClass('x') / .removeClass('x') / .toggleClass('x')el.classList.add/remove/toggle('x')
$(el).show() / .hide()el.style.display = 'block' / 'none' (or el.classList.toggle('d-none'))
$(el).next()el.nextElementSibling
$(el).parents('ul').first()el.closest('ul')
$(el).each(fn)Array.from(els).forEach(fn) or els.forEach(fn) (for NodeList)
$('#x').click(fn)document.getElementById('x').addEventListener('click', fn)
$(document).on('click', '.x', fn)document-level delegation (see §6.3)
$.param(obj)new URLSearchParams(obj).toString()
$('form').serialize()new URLSearchParams(new FormData(form)).toString()
$('form').serializeArray()Object.fromEntries(new FormData(form))

6.3 Document-level delegation

Replace $(document).on('click', '.foo', fn) with:

document.addEventListener('click', function(e) {
var btn = e.target.closest('.foo');
if (!btn) return;
e.preventDefault();
// ...
});

This survives AJAX content swaps automatically.

6.4 Bootstrap 5 component access

// Tooltips
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(function(el) {
new bootstrap.Tooltip(el);
});

// Modals
var modalEl = document.getElementById('myModal');
var modal = bootstrap.Modal.getOrCreateInstance(modalEl);
modal.show(); // .hide() / .toggle()

// Dropdowns
var ddEl = document.getElementById('myDropdownBtn');
var dd = bootstrap.Dropdown.getOrCreateInstance(ddEl);
dd.toggle();

// Collapse
var cEl = document.getElementById('myCollapse');
var collapse = bootstrap.Collapse.getOrCreateInstance(cEl, { toggle: false });
collapse.show(); // .hide() / .toggle()

// Tabs
var tabEl = document.querySelector('button[data-bs-target="#tab-reply"]');
var tab = bootstrap.Tab.getOrCreateInstance(tabEl);
tab.show();

6.5 Paradigm global helpers (replacing jQuery plugin form)

assets/scripts/admin/paradigm/application.js exposes these globals on window. They replace the jQuery plugin form ($.fn.blestaXxx):

v5 (jQuery plugin)v6 (vanilla global)
$('a').blestaModal({…})blestaModal(element, {…})
$('a').blestaModalConfirm({…})blestaModalConfirm(element, {…}) (or declarative .modal-confirm-delete class)
$('.col').blestaLoadWidgets({…})blestaLoadWidgets('.col', {…})
$.blestaRequest(method, uri, data, …)blestaRequest(method, uri, data, …)
$('input').blestaBindToolTips()blestaBindToolTips(container)
$('textarea').blestaBindWysiwygEditor({…})blestaBindWysiwygEditor(selector, {…})
(legacy markdown editor)blestaBindMarkdownEditor(container) (Toast UI)
$('.drag').blestaSortable(handle, {…})blestaSortable(element, handle, {…}) (Sortable.js wrapper)
$('table').blestaUpdateRow(uri, '.x')blestaUpdateRow(element, uri, container)
$('a').blestaTabbedContent()use native BS5 tabs (data-bs-toggle="tab")
$('.area').blestaBindToggleEvent(…)use BS5 Collapse (data-bs-toggle="collapse")
$('input').blestaBindPasswordGenerator()blestaBindPasswordGenerator(container)

Other useful globals: blestaBindGuiEvents(container, options) (master init), blestaBindGlobalGuiEvents(container, options), blestaBindDatePicker(container), blestaBindDropZone(container), blestaSubmitOnClick(element, formId), blestaScrollDom, blestaSetInvTotals, blestaPushState, blestaSlideToggle, blestaBindCloseMessage, blestaBindPreventDoubleSubmission, blestaDisableFormSubmission, blestaEnableFormSubmission.

6.6 The blestaAjaxComplete event

After AJAX-swapping content into the DOM, dispatch this event so global initializers (expandable rows, quick-jump menu, tooltips, modal-confirm bindings) re-attach:

document.dispatchEvent(new Event('blestaAjaxComplete'));

Listeners include expandable-rows.js, the quick-jump dropdown, and various view-level widgets. You must dispatch this when you replace a chunk of DOM with innerHTML = ….

6.7 Modal confirm pattern (declarative)

Replaces $.fn.blestaModalConfirm:

<button type="button" class="btn btn-sm btn-outline-danger modal-confirm-delete"
data-confirm-message="<?php echo $this->Html->safe($confirm_msg);?>"
title="Delete">
<i class="bi bi-trash"></i>
</button>

Three button classes set color/icon: .modal-confirm (neutral), .modal-confirm-success (green), .modal-confirm-delete (red). They are auto-bound by Paradigm globals. After AJAX swaps, re-bind explicitly:

function initModalConfirms() {
if (typeof blestaModalConfirm === 'function') {
blestaModalConfirm('.modal-confirm');
blestaModalConfirm('.modal-confirm-delete');
blestaModalConfirm('.modal-confirm-success');
}
}
document.addEventListener('blestaAjaxComplete', initModalConfirms);

When the trigger is inside a <form> (delete is a real form submit, not just a confirm dialog) wrap in Form->create([...], ['class' => 'd-inline']) so the form sits inline.

6.8 Sortable (Sortable.js)

// jQuery UI sortable (legacy)
$('table.fields_table tbody').blestaSortable(false, {
cancel: 'tr.expand_details,input,textarea',
start: function() { ... },
stop: function() { ... }
});

// Sortable.js (Paradigm)
var tbody = document.querySelector('table.fields_table tbody');
if (tbody && typeof Sortable !== 'undefined') {
Sortable.create(tbody, {
animation: 150,
handle: '.sortable-drag-handle', // or .movable
draggable: 'tr[data-field-id]',
ghostClass: 'sortable-ghost',
onEnd: function(evt) {
// POST new order to server with CSRF token
}
});
}

6.9 Toast UI Editor access

// Get the editor instance from a textarea with data-markdown-editor
var textarea = document.getElementById('reply_details');
var editorDiv = textarea ? textarea.nextElementSibling : null;
while (editorDiv && !editorDiv._editor) {
editorDiv = editorDiv.nextElementSibling;
}
if (editorDiv && editorDiv._editor) {
var editor = editorDiv._editor;
editor.changeMode('wysiwyg'); // or 'markdown'
editor.changePreviewStyle('vertical'); // or 'tab'
editor.insertText('text\n');
editor.setMarkdown(currentContent);
editor.focus();
}

The editor is bound by the global blestaBindMarkdownEditor() which scans for textarea[data-markdown-editor] on DOMContentLoaded. To get inline image upload support, set data-image-upload-url:

$this->Form->fieldTextarea('details', $val, [
'id' => 'reply_details',
'rows' => 21,
'data-markdown-editor' => '',
'data-image-upload-url' => $this->Html->safe($this->base_uri . 'plugin/your_plugin/yourcontroller/uploadimage/')
]);

6.10 Submit-button FormData gotcha

new FormData(form) does NOT include the value of the submit button that was clicked. If your code submits programmatically and you need the submit button name/value, append manually:

var formData = new FormData(form);
formData.append(submitBtn.name, submitBtn.value);

6.11 CSRF tokens

Every AJAX POST needs _csrf_token. Get it from Form->getCsrfToken() (no args) and emit it from PHP into the script, or read it from a hidden input:

var csrfInput = document.querySelector('input[name="_csrf_token"]');
var csrfToken = csrfInput ? csrfInput.value : '<?php echo $this->Form->getCsrfToken();?>';

var formData = new FormData();
formData.append('_csrf_token', csrfToken);
formData.append('foo', 'bar');

fetch(url, {
method: 'POST',
body: formData,
headers: { 'X-Requested-With': 'XMLHttpRequest' }
})
.then(r => r.json())
.then(data => { /* … */ });

6.12 Locale-safe interpolation in JS strings

When a translated string is placed inside a JS string literal, run it through Html->safe to escape quotes:

- var text = '<?php echo $this->_('Foo.text', true);?>';
+ var text = '<?php echo $this->Html->safe($this->_('Foo.text', true));?>';

6.13 Including plugin JS files

For ad-hoc plugin JS (non-Gulp-compiled), include via direct <script> tag — not Javascript->setFile():

<script src="<?php echo $this->view_dir;?>js/your-plugin.js"></script>

Javascript->setFile() prepends a default_path that double-prefixes the already-complete $this->view_dir and produces broken URLs.

A plugin's views/default/js/ directory is not Gulp-compiled — it's served as-is from the plugin views directory.


7. HTML / markup structural changes

7.1 Form + Widget ordering reversed

In v5, Widget opens before Form. In v6, Form opens first, Widget closes first. This lets the submit button live in the card-footer while still belonging to the form.

- $this->Widget->create($title);
- $this->Form->create($url);
- <!-- content + submit -->
- $this->Form->end();
- $this->Widget->end();
+ $this->Form->create($url);
+ $this->Widget->create($title);
+ <!-- content -->
+ $this->Widget->footer();
+ <!-- submit buttons in card-footer -->
+ $this->Widget->end();
+ $this->Form->end();

<div class="inner">, <div class="pad">, <div class="title_row">, <div class="button_row"> are all gone. Replace with BS5 card sections:

- $this->Widget->create('Title');
- ?>
- <div class="inner">
- <div class="title_row first"><h3>Section Heading</h3></div>
- <div class="pad">
- <ul>
- <li>... fields ...</li>
- </ul>
- </div>
- <div class="button_row">
- <?php $this->Form->fieldSubmit('save', 'Save', ['class' => 'btn btn-primary float-right']);?>
- </div>
- </div>
- <?php $this->Widget->end(); ?>

+ <?php $this->Form->create(null, ['class' => 'disable-on-submit']); ?>
+ <?php $this->Widget->setBodyWrapper(false); // listing pages only — see §9 ?>
+ <?php $this->Widget->create('Title'); ?>
+ <h6 class="form-section-header">Section Heading</h6>
+ <div class="mb-3"> ... fields ... </div>
+ <?php $this->Widget->footer(); ?>
+ <div class="d-flex justify-content-end gap-2">
+ <a href="<?php echo $cancel_url;?>" class="btn btn-outline-secondary">Cancel</a>
+ <?php $this->Form->fieldSubmit('save', 'Save', ['class' => 'btn btn-primary']);?>
+ </div>
+ <?php $this->Widget->end(); ?>
+ <?php $this->Form->end(); ?>

If you suppress the body wrapper with setBodyWrapper(false), you provide your own <div class="card-body"> (or run flush — useful for tables).

7.3 Tables

- <table class="table" id="ticket_list">
- <tr class="heading_row">
- <td>Heading</td>
- <td class="last">Last</td>
- </tr>
- <?php foreach ($items as $i => $item) { ?>
- <tr class="<?php echo ($i%2==1) ? 'odd_row' : '';?>">
- <td>...</td>
- <td class="last">...</td>
- </tr>
- <?php } ?>
- </table>
+ <div class="table-responsive">
+ <table class="table table-hover mb-0" id="ticket_list">
+ <thead>
+ <tr>
+ <th>Heading</th>
+ <th>Last</th>
+ </tr>
+ </thead>
+ <tbody>
+ <?php foreach ($items as $item) { ?>
+ <tr>
+ <td>...</td>
+ <td>...</td>
+ </tr>
+ <?php } ?>
+ </tbody>
+ </table>
+ </div>

Drop manual zebra striping (odd_row) — .table-hover and .table-striped handle row backgrounds.

7.4 Sortable column headers

- <td><span><a href="?sort=code" class="ajax<?php echo $sort=='code' ? ' '.$order : '';?>">Code</a></span></td>
+ <th>
+ <a href="?sort=code" class="ajax text-decoration-none">
+ Code
+ <?php if ($sort == 'code') { ?>
+ <i class="bi bi-chevron-<?php echo $order == 'asc' ? 'up' : 'down';?>"></i>
+ <?php } ?>
+ </a>
+ </th>

7.5 Expandable rows (Paradigm pattern)

Replace v5's <tr class="expand_details" id="orders_123"><td class="subtable"> lazy-load pattern with data-collapse-row on a trigger row + <tr class="collapse"> sibling:

<tr class="order-row" data-collapse-row data-order-id="123" aria-expanded="false" style="cursor: pointer;">
<td>...</td>
</tr>
<tr class="collapse">
<td colspan="7" class="order-details-cell" style="padding: 0; background-color: var(--bg-secondary);">
<div class="order-details-content p-3" data-order-id="123">
<div class="text-center py-3">
<div class="spinner-border spinner-border-sm" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</div>
</td>
</tr>

Auto-wired by assets/scripts/admin/paradigm/expandable-rows.js. Lazy-load on show.bs.collapse:

document.addEventListener('show.bs.collapse', function(e) {
if (!e.target.classList.contains('collapse')) return;
var container = e.target.querySelector('.order-details-content');
if (!container || container.hasAttribute('data-loaded')) return;
var orderId = container.getAttribute('data-order-id');
fetch('<?php echo $this->base_uri;?>plugin/order/admin_main/orderdetails/' + orderId, {
headers: { 'X-Requested-With': 'XMLHttpRequest' }
})
.then(r => r.text())
.then(html => {
container.innerHTML = html;
container.setAttribute('data-loaded', 'true');
document.dispatchEvent(new Event('blestaAjaxComplete'));
});
});

Interactive children (a, button, input, select, textarea, label) are auto-excluded from the row's click handler — clicking a link inside the row won't toggle the collapse.

7.6 Dropdowns

- <a class="dropdown-toggle" data-toggle="dropdown" href="#">Menu</a>
- <ul class="dropdown-menu"><li><a>Item</a></li></ul>
+ <button type="button" class="btn btn-outline-secondary btn-sm dropdown-toggle"
+ data-bs-toggle="dropdown" data-bs-auto-close="outside" aria-expanded="false">
+ Menu
+ </button>
+ <ul class="dropdown-menu">
+ <li><a class="dropdown-item" href="#">Item</a></li>
+ </ul>

<a class="dropdown-item"> is required for menu items (BS5 styles target this class). For dropdowns containing a search box, use data-bs-auto-close="outside" so clicking the input doesn't close the menu.

7.7 Accordions

<div class="accordion accordion-flush" id="myAccordion">
<div class="accordion-item border-0 bg-transparent">
<h2 class="accordion-header">
<button class="accordion-button bg-transparent px-0 py-1<?php echo $expanded ? '' : ' collapsed';?>"
type="button" data-bs-toggle="collapse" data-bs-target="#myCollapse"
aria-expanded="<?php echo $expanded ? 'true' : 'false';?>">
Title
</button>
</h2>
<div id="myCollapse" class="accordion-collapse collapse<?php echo $expanded ? ' show' : '';?>"
data-bs-parent="#myAccordion">
<div class="accordion-body p-0 pt-2">...</div>
</div>
</div>
</div>

7.8 Alert boxes

- <div class="accent_box">
- <code>{tag1}</code> <code>{tag2}</code>
- </div>
+ <div class="alert alert-info">
+ <h6 class="alert-heading mb-2"><i class="bi bi-tags me-2"></i>Tags</h6>
+ <div class="d-flex flex-wrap gap-2">
+ <code>{tag1}</code>
+ <code>{tag2}</code>
+ </div>
+ </div>

For alerts with icons:

<div class="alert alert-warning d-flex align-items-start" role="alert">
<i class="bi bi-shield-exclamation me-2 mt-1"></i>
<div class="flex-grow-1">
<strong>Heads up!</strong>
<pre class="small mt-2 mb-0 p-2 bg-body-secondary rounded"><code>example</code></pre>
</div>
</div>

7.9 File upload zone

<div class="file-upload-compact mb-3" id="fileDropZone">
<?php $this->Form->fieldFile('attachment[]', [
'id' => 'fileInput',
'class' => 'drag-drop',
'rel' => $this->_('AppController.dropzone.text', true),
'style' => 'display:none;',
'multiple' => 'multiple'
]);?>
<div class="d-flex align-items-center gap-3">
<i class="bi bi-cloud-upload upload-icon"></i>
<div class="flex-grow-1"><div class="upload-text">Drop files here</div></div>
<button type="button" class="btn btn-outline-primary btn-sm"
onclick="document.getElementById('fileInput').click()">Browse</button>
</div>
</div>
<div id="fileList" class="mb-3"></div>

7.10 Dual-select / drag-and-drop list

Replaces the v5 multi-select pair. Uses HTML5 drag-and-drop with a hidden <select multiple> so the form still submits the same field name.

<div class="dual-select-container mb-4">
<div class="dual-select-panel">
<label class="form-label">Assigned</label>
<div class="dual-select-dropzone">
<div class="drag-list" id="assigned_departments">
<div class="drag-item" data-value="1" draggable="true">Sales</div>
</div>
</div>
<?php $this->Form->fieldMultiSelect('departments[]', [], [], [
'id' => 'assigned', 'style' => 'display:none;'
]);?>
</div>
<div class="dual-select-controls">
<button type="button" class="btn btn-sm btn-outline-primary dual-select-btn move_left">
<i class="bi bi-arrow-left"></i>
</button>
<button type="button" class="btn btn-sm btn-outline-primary dual-select-btn move_right">
<i class="bi bi-arrow-right"></i>
</button>
</div>
<div class="dual-select-panel">
<label class="form-label">Available</label>
<div class="dual-select-dropzone">
<div class="drag-list" id="available_departments">...</div>
</div>
</div>
</div>

7.11 Card grids (folders / responses)

<div class="row g-3">
<div class="col-12 col-sm-6 col-md-4 col-xl-3">
<div class="response-folder-card" role="link" tabindex="0" data-href="<?php echo $url;?>">
<div class="response-folder-icon"><i class="bi bi-folder-fill"></i></div>
<div class="response-folder-name"><?php echo $cat->name;?></div>
<span class="response-folder-count badge bg-secondary"><?php echo $cat->item_count;?></span>
<div class="response-folder-actions">
<a href="<?php echo $edit_url;?>" class="response-folder-action-btn"><i class="bi bi-pencil"></i></a>
<button type="button" class="response-folder-action-btn modal-confirm-delete"
data-confirm-message="<?php echo $this->Html->safe($confirm);?>">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
</div>
</div>

JS makes the card behave as a link via data-href + click/Enter keydown.

7.12 No more is_ajax wrappers

Remove the legacy <?php if (!$is_ajax) { ?> <div id="right_outer"><section id="right_container"> <?php } ?> scaffolding and matching close. The two-column layout is owned by Paradigm's structure now (see §10). Plugin views emit content only.


8. Form helper usage

8.1 Required class injection

The Form helper does NOT auto-inject BS5 classes. On every call, pass:

HelperRequired class
fieldText, fieldPassword, fieldFile, fieldTextarea (non-markdown), fieldHidden (no class needed), fieldUrl, fieldEmail, fieldTel, fieldNumber, fieldDate'class' => 'form-control'
fieldSelect, fieldMultiSelect'class' => 'form-select'
fieldCheckbox, fieldRadio'class' => 'form-check-input'
Form->label (block)'class' => 'form-label'
Form->label (next to checkbox/radio)'class' => 'form-check-label'
fieldSubmit'class' => 'btn btn-primary' (or appropriate)

8.2 ID conventions for bracketed names

Replace [] with _ in IDs because <label for=""> requires a valid HTML id:

- 'id' => 'settings[ticket_emails][' . $priority . ']'
+ 'id' => 'ticket_emails_' . $priority

The form field name keeps the brackets — only the id changes.

8.3 The form attribute (sidebar inputs)

When form inputs live in a separately-rendered sidebar partial (not inside the <form>), each must reference the form by id:

$this->Form->fieldSelect('department_id', $departments, $val, [
'id' => 'department_id',
'class' => 'form-select form-select-sm',
'form' => 'create_ticket' // ties this input to <form id="create_ticket">
]);

8.4 Inline forms

Wrap delete-button forms with 'class' => 'd-inline' so they sit inline with adjacent buttons:

- $this->Form->create($delete_url);
+ $this->Form->create($delete_url, ['class' => 'd-inline']);

8.5 Section headers

- <div class="title_row first"><h3>Section</h3></div>
+ <h6 class="form-section-header">Section</h6>

form-section-header is a Paradigm helper class.

8.6 Hint text below fields

<div class="form-text">
<i class="bi bi-info-circle"></i> Helpful description of the field.
</div>

8.7 Tooltip indicator

<i class="bi bi-info-circle ms-1"
data-bs-toggle="tooltip" data-bs-placement="top"
title="<?php echo $this->Html->safe($this->_('Foo.tooltip.text', true));?>"></i>

The tooltip text moves from a <div> body of the legacy <span class="tooltip">…</span> into the title attribute.

8.8 Cancel/save button row

<div class="d-flex justify-content-end gap-2">
<a href="<?php echo $cancel_url;?>" class="btn btn-outline-secondary">Cancel</a>
<?php $this->Form->fieldSubmit('save', 'Save', ['class' => 'btn btn-primary']); ?>
</div>

Drop pull-right/float-end from the submit class — the wrapper handles alignment.

8.9 Radio button groups (BS5 button-group style)

<div class="btn-group" role="group" aria-label="Status">
<input type="radio" class="btn-check" name="status" id="status_active" value="active" checked>
<label class="btn btn-outline-success" for="status_active">
<i class="bi bi-check-circle me-1"></i>Active
</label>
<input type="radio" class="btn-check" name="status" id="status_inactive" value="inactive">
<label class="btn btn-outline-danger" for="status_inactive">
<i class="bi bi-x-circle me-1"></i>Inactive
</label>
</div>

9. Widget helper changes

9.1 New methods

MethodPurpose
setBodyWrapper(bool $wrap)Suppress the default <div class="card-body"> wrapper. Use for tables that should run flush.
setNavStyle(string $style)Override default nav-tabs-box. Common: nav-tabs-custom.
footer()Close the card-body and open a <div class="card-footer"> for buttons / pagination.

9.2 setWidgetButton keyword changes

v5 keywordv6 keywordIcon
arrowarrowbi bi-arrows-collapse / bi bi-arrows-expand
settingsettingbi bi-gear-fill
filter-togglefilter-togglebi bi-funnel / bi bi-funnel-fill
full_screenfull-screen (renamed: underscore → hyphen)bi bi-arrows-angle-contract / bi bi-arrows-angle-expand
(none)widget-width (NEW)bi bi-arrows-angle-contract / bi bi-arrows-angle-expand

widget-width toggles a dashboard widget between column width and full-width (<section class="card" data-width="full">).

- $this->Widget->setWidgetButton('full_screen');
+ $this->Widget->setWidgetButton('full-screen');

// Optional: add expandable-width to dashboard widgets
+ $this->Widget->setWidgetButton('widget-width');
+ $this->Widget->setWidgetButton('arrow');

9.3 Icon keys in arrays

  $this->Widget->setLinkButtons([
[
'name' => 'Add',
'attributes' => ['href' => $url],
- 'icon' => 'fas fa-plus'
+ 'icon' => 'bi bi-plus-lg'
],
[
'name' => 'Back',
'attributes' => ['href' => $back_url],
- 'icon' => 'fas fa-arrow-left'
+ 'icon' => 'bi bi-arrow-left me-1'
]
]);

9.4 Tab attribute keys

When using setLinks for tabs, switch to data-bs- attributes:

  $this->Widget->setLinks([
[
'name' => 'Basic',
'attributes' => [
'id' => 'tab-basic',
- 'data-toggle' => 'tab',
- 'data-target' => '#tab_basic',
+ 'data-bs-toggle' => 'tab',
+ 'data-bs-target' => '#tab_basic',
'href' => '#'
]
]
]);

Wrap tab panes:

<div class="tab-content">
<div class="tab-pane fade show active" id="tab_basic" role="tabpanel"></div>
<div class="tab-pane fade" id="tab_advanced" role="tabpanel"></div>
</div>

9.5 Widget HTML output (so AJAX swap targets are right)

V6 emits:

<section class="card" id="..." data-width="full">
<div class="card-header d-flex justify-content-between align-items-center"></div>
<div class="card-content">
{nav}
{filters}
<div class="card-body">…content…</div>
[optional <div class="card-footer"></div>]
</div>
</section>

The AJAX replacer is now .card-content (was .common_box_content for admin in v5). If your code emits {replacer: '.common_box_content', content: '…'}, change it to {replacer: '.card-content', content: '…'}.

9.6 Widget pagination placement

- $this->Pagination->build();
- $this->Widget->end();

+ if (isset($this->Pagination) && $this->Pagination->hasPages()) {
+ $this->Widget->footer();
+ $this->Pagination->build();
+ }
+ $this->Widget->end();

9.7 setStyleSheet with a Paradigm-specific stylesheet

When you maintain both a v5-compatible CSS and a v6 Paradigm CSS, use a separate file for each:

// Old (kept for legacy default theme)
$this->Widget->setStyleSheet($this->view_dir . 'css/styles.css', ['id' => 'plugin_styles']);

// New (Paradigm)
$this->Widget->setStyleSheet($this->view_dir . 'css/paradigm.css', ['id' => 'plugin_styles']);
Related reference

For the broader Widget helper API — including setBodyWrapper, setNavStyle, card-filter-bar auto-routing for setLinkButtons, and the data-collapse-row expandable-rows pattern — see Widgets. This section focuses on the changes specific to converting a v5 extension; the linked page is the canonical reference for the helper itself.


10. The new sidebar pattern (Paradigm)

Paradigm renders a two-column shell when the controller publishes a side panel. Use this for ticket detail forms, package settings, etc., that benefit from a sidebar of meta fields.

10.1 Controller side

// In your action method:
$sidebar_html = $this->partial('admin_yourcontroller_sidebar', [
'vars' => $vars,
'options' => $options,
// ... whatever the partial needs
]);

$this->structure->set('side_bar_content', $sidebar_html);
$this->structure->set('side_bar', ['partials/plugin_sidebar', null, 'side-content-mobile-visible']);

The third element of side_bar is an optional layout class.

10.2 Partial shell (already in core)

app/views/admin/paradigm/partials/plugin_sidebar.pdt:

<?php /** @var View $this **/ ?>
<?php echo $side_bar_content ?? '';?>

A thin pass-through.

10.3 Plugin sidebar partial conventions

<?php /** @var View $this **/ ?>
<div class="side-content-section">
<h6 class="side-content-title">
<i class="bi bi-info-circle"></i>
<?php $this->_('AdminYour.heading_details');?>
</h6>

<div class="mb-3">
<label class="form-label small text-secondary mb-1">Department</label>
<?php $this->Form->fieldSelect('department_id', $departments, $vars->department_id ?? null, [
'id' => 'department_id',
'class' => 'form-select form-select-sm',
'form' => 'create_ticket' // tied to main form by id
]);?>
</div>

<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary btn-sm" form="create_ticket">Save</button>
</div>
</div>

Conventions:

  • Each grouped block: <div class="side-content-section">
  • Section title: <h6 class="side-content-title"> with leading <i class="bi bi-…">
  • Field labels: <label class="form-label small text-secondary mb-1">
  • Inputs use -sm size variants
  • Inputs that should submit with a form elsewhere on the page: add 'form' => 'main_form_id'
  • Full-width stacked buttons: <div class="d-grid gap-2">

10.4 Sidebar JS communication

For sidebar inputs that submit a separate filter form, use document-level event delegation:

document.addEventListener('submit', function(e) {
if (e.target.id === 'ticket_filters') {
e.preventDefault();
applyTicketFilters();
}
});

Inputs added by your sidebar partial appear in the parent form's submission automatically because of the form attribute.


11. CSS, SASS, and theme variables

11.1 Plugin CSS location

Same as v5: plugins/{plugin}/views/default/css/styles.css. Register via Widget:

$this->Widget->setStyleSheet($this->view_dir . 'css/styles.css', ['id' => 'your_plugin_styles']);

Plugin CSS is not Gulp-compiled — it's served as-is.

11.2 CSS variables (theme tokens)

Paradigm exposes its own CSS custom properties. Use these — do NOT hard-code colors so your styles work in light AND dark mode.

/* Paradigm tokens (NOT Bootstrap's --bs-* — those exist too but Paradigm prefers these) */
var(--bg-primary)
var(--bg-secondary) /* common for striped / inset backgrounds */
var(--bg-tertiary)
var(--bg-card)
var(--border-color) /* common for dividers */
var(--text-primary)
var(--text-secondary)
var(--text-muted)
var(--accent-color)
var(--link-color)
var(--success-color)
var(--warning-color)
var(--danger-color)

/* Some --bs-* overrides set by Paradigm (use these too) */
var(--bs-modal-bg)
var(--bs-modal-color)
var(--bs-modal-border-color)
var(--bs-body-bg)
var(--bs-border-color-translucent)

Active theme is set via <html data-bs-theme="dark|light"> (also data-theme). Test both modes.

/* Good */
.your-element {
background-color: var(--bg-secondary);
color: var(--text-primary);
border: 1px solid var(--border-color);
}

/* Bad — breaks in dark mode */
.your-element {
background-color: #f8f9fa;
color: #212529;
border: 1px solid #dee2e6;
}

11.3 Theme-aware Bootstrap utilities

Prefer these over hard-coded BS classes:

ReplaceWith
bg-lightbg-body-secondary
bg-whitebg-body
text-dark (when meaning "default body color")text-body
text-secondarytext-secondary (works)
border (default border color)works (uses --bs-border-color)

11.4 Code editors that respond to theme

For embedded code editors (Ace, Monaco, etc.), watch for theme changes:

function getAceTheme() {
return document.documentElement.getAttribute('data-bs-theme') === 'dark'
? 'ace/theme/tomorrow_night'
: 'ace/theme/tomorrow';
}
new MutationObserver(function() {
var theme = getAceTheme();
Object.values(editors).forEach(function(e) { e.setTheme(theme); });
}).observe(document.documentElement, {
attributes: true, attributeFilter: ['data-bs-theme']
});

11.5 SCSS backwards-compat shims

Paradigm's _backwards.scss re-styles many v5 class names (pull-left, pull-right, common_box, inner, pad, title_row, button_row, etc.) on top of BS5 so unconverted plugins look acceptable. Don't rely on these for new code — they exist only to soften the migration.


12. AJAX patterns

12.1 Replace blestaRequest with fetch

- $(document).blestaRequest('POST', url,
- { _csrf_token: token, foo: 'bar' },
- function(data) { /* success */ },
- null,
- { dataType: 'json' }
- );

+ var formData = new FormData();
+ formData.append('_csrf_token', token);
+ formData.append('foo', 'bar');
+
+ fetch(url, {
+ method: 'POST',
+ body: formData,
+ headers: { 'X-Requested-With': 'XMLHttpRequest' }
+ })
+ .then(r => r.json())
+ .then(data => { /* success */ })
+ .catch(err => console.error(err));

The global blestaRequest() (vanilla version) still exists if you prefer it.

12.2 The X-Requested-With header

It's required to trigger the controller's renderAjaxWidgetIfAsync() JSON-envelope wrapping. jQuery added it automatically; with fetch you must add it manually.

12.3 The JSON envelope

When the controller wraps a widget response, you get:

{ "replacer": ".card-content", "content": "<div>…</div>" }

Handle both cases (replacer null = full content swap):

fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' }})
.then(r => r.json())
.then(data => {
var widget = document.getElementById('admin_tickets');
if (data.replacer == null) {
widget.innerHTML = data.content;
} else {
var target = widget.querySelector(data.replacer);
if (target) target.innerHTML = data.content;
}
document.dispatchEvent(new Event('blestaAjaxComplete'));
});

12.4 New AJAX endpoints in plugin controllers

Plugin controllers ARE directly routable (unlike main app controllers). Adding public function myAjaxEndpoint() to a plugin controller exposes it at /plugin/your_plugin/your_controller/myajaxendpoint/. No index() switching needed.

12.5 outputAsJson for custom JSON responses

public function myAjaxEndpoint()
{
$result = ['ok' => true, 'data' => [...]];
$this->outputAsJson($result);
return false;
}

12.6 Lazy-load on collapse

document.addEventListener('show.bs.collapse', function(e) {
if (!e.target.classList.contains('collapse')) return;
var container = e.target.querySelector('.your-content-container');
if (!container || container.hasAttribute('data-loaded')) return;
var id = container.getAttribute('data-record-id');
fetch(url + id, { headers: { 'X-Requested-With': 'XMLHttpRequest' }})
.then(r => r.text())
.then(html => {
container.innerHTML = html;
container.setAttribute('data-loaded', 'true');
document.dispatchEvent(new Event('blestaAjaxComplete'));
});
});

13. Module-specific changes (PHP base classes)

13.1 Files removed

These v5 files are deleted in v6:

  • components/modules/module_field.php
  • components/modules/module_fields.php

The global short names ModuleField and ModuleFields are kept as runtime class aliases in config/aliases.php:

'ModuleFields' => Blesta\Core\Util\Input\Fields\InputFields::class,
'ModuleField' => Blesta\Core\Util\Input\Fields\InputField::class,

No source change requirednew ModuleFields() and new ModuleField(...) still work. The canonical class is Blesta\Core\Util\Input\Fields\InputFields.

13.2 Module base class additions

New: getIcon()

Modules can declare a Bootstrap icon for Paradigm-native rendering by adding "icon": "bi bi-…" to their config.json:

{
"version": "2.20.0",
"name": "Cpanel.name",
"description": "Cpanel.description",
"icon": "bi bi-server",
"authors": [ /* ... */ ],
"module": { /* ... */ }
}

The base Module::getIcon() reads $this->config->icon and returns it (or null). Paradigm uses this in its module list / overview screens.

Related reference

The icon field is documented for each extension type alongside the rest of the config.json definition: Plugin configuration, Module configuration, Gateway configuration, Messenger configuration.

Renamed: arrayToModuleFields()arrayToInputFields()

- protected function arrayToModuleFields($arr, ModuleFields $fields = null, $vars = null)
+ protected function arrayToInputFields($arr, InputFields $fields = null, $vars = null)
{
if (!$fields) {
- $fields = new ModuleFields();
+ $fields = new InputFields();
}
// body unchanged
}

// Backwards-compatible alias kept (deprecated):
protected function arrayToModuleFields($arr, InputFields $fields = null, $vars = null)
{
return $this->arrayToInputFields($arr, $fields, $vars);
}

If your module overrides this, prefer arrayToInputFields. Old name still works.

The @return docblocks on getPackageFields(), getAdminAddFields(), getClientAddFields(), getAdminEditFields() updated @return ModuleFields@return InputFields (runtime types are equivalent due to alias).

13.3 RegistrarModule base class

Unchanged. All public methods (supportsDnsManagement, checkAvailability, getDomainContacts, setDomainNameservers, getTldPricing, getFilteredTldPricing, registerDomain, transferDomain, renewDomain, restoreDomain, etc.) have identical signatures.

13.4 Module field rendering / form patterns

Field-rendering helpers (getPackageFields, getAdminAddFields, getClientAddFields, getAdminEditFields) remain unchanged. They return Blesta\Core\Util\Input\Fields\InputFields and the framework iterates and renders.

The shared service_fields*.pdt views still use <div class="form-group"> and class="inline" checkbox labels — these need the BS5 conversion (§4.5–4.6).

13.5 Module manage.pdt shape

The legacy widget chrome (title_row, pad, inner, heading_row, odd_row, empty_section, <a class="manage rel="...">) survives in shipped core modules but should be converted in your own modules using §4 / §7 rules.

13.6 Tab icon strings in module PHP

Module getAdminTabs()/getClientTabs() arrays carry 'icon' => 'fas fa-…'. Convert to Bootstrap Icons:

  public function getAdminTabs($package)
{
return [
- 'tabActions' => ['name' => Language::_('Mod.tab_actions', true), 'icon' => 'fas fa-cog'],
+ 'tabActions' => ['name' => Language::_('Mod.tab_actions', true), 'icon' => 'bi bi-gear'],
];
}

13.7 Blesta-specific theme classes (KEEP these)

These are Blesta theme classes, not Bootstrap classes, and are styled by Paradigm SCSS — leave them alone:

title_row, pad, inner, heading_row, odd_row, empty_section, empty_box, tab_content, tabs, inline, stretch, red_txt, buff, running, stopped, accent_box, manage, last, border_left, border_none, fixed_small, center, field_row.

That said, prefer BS5 utility classes when writing new code — those Paradigm-styled legacy classes exist for backward compat.


14. Composer / PHP / dependency notes

14.1 PHP requirement

  "require": {
- "php": ">=7.2.0",
+ "php": ">=8.2.0",
...
}

Use modern PHP idioms in new code:

  • Null coalescing: $foo ?? null (not isset($foo) ? $foo : null)
  • Null safe: $foo?->bar
  • Match expressions, named args, readonly props, enums, first-class callable syntax — all available

14.2 New top-level deps available to plugins

  • Illuminate (Laravel) Support, Collections, Str, Arr — usable directly
  • HTMLPurifier (ezyang/htmlpurifier)
  • Carbon (via Illuminate)

Example:

use Illuminate\Support\Str;

echo Str::of($description)->stripTags()->squish()->limit(140);

14.3 Your plugin's config.json

Bump require.blesta to >=6.0.0 and your plugin version (typically a major bump). For modules, optionally add the icon key:

{
"version": "3.0.0",
"name": "YourPlugin.name",
"description": "YourPlugin.description",
"icon": "bi bi-puzzle",
"require": { "blesta": ">=6.0.0" },
"authors": [ ... ]
}

14.4 Your composer.json

Standard skeleton remains:

{
"name": "your/plugin",
"type": "blesta-plugin",
"require": {
"blesta/composer-installer": "~1.0"
}
}

The blesta/composer-installer type keys (blesta-plugin, blesta-module, blesta-gateway, blesta-invoice-format) are unchanged.

Related reference

For broader Composer guidance — Composer 2 requirements, the new policy of committing composer.lock, the type → install-path mapping for the four extension kinds, and full extension composer.json examples — see Composer.


15. Common gotchas

15.1 new FormData(form) omits submit button values

Manually append:

formData.append(submitBtn.name, submitBtn.value);

15.2 PHPIDS scans every POST

Don't put base64-encoded binary data in form POST fields — PHPIDS's regex chokes on large strings and burns memory. Use file uploads or dedicated endpoints.

15.3 Plugin AJAX endpoints

Plugin controllers ARE directly routable: public function myAjax() on your plugin controller is reachable at /plugin/your_plugin/your_controller/myajax/. No index() switch needed. (Main-app controllers are different — there, custom action methods aren't routable and you must dispatch via a POST parameter inside index().)

15.4 setFile() double-prefix bug

Don't use $this->Javascript->setFile($this->view_dir . 'js/foo.js'). The helper prepends a default_path that double-prefixes. Use a direct <script> tag:

<script src="<?php echo $this->view_dir;?>js/foo.js"></script>

15.5 Tooltips don't auto-init

After page load and after every AJAX swap, run:

document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(function(el) {
new bootstrap.Tooltip(el);
});

Or rely on Paradigm's blestaBindToolTips(container) global.

15.6 Forgetting the -bs- infix

Easy to miss. Search-and-replace cycle:

data-toggle      → data-bs-toggle
data-target → data-bs-target
data-dismiss → data-bs-dismiss
data-parent → data-bs-parent
data-placement → data-bs-placement
data-html → data-bs-html
data-container → data-bs-container
data-auto-close → data-bs-auto-close

15.7 <select class="form-control"> looks wrong

In BS5 selects need class="form-select" to get the chevron caret styling. form-control still works mechanically but looks broken.

15.8 <label class="inline"> next to checkboxes

Replace with <label class="form-check-label"> and wrap both in <div class="form-check">.

15.9 Hard-coded colors break dark mode

Always use CSS variables (var(--border-color), var(--bg-secondary)) or theme-aware utility classes (bg-body-secondary, not bg-light). Test in both light AND dark mode.

15.10 Deleting AJAX-bound content silently breaks event handlers

When you innerHTML = … a region of DOM, any event listeners attached to the replaced elements are gone. Either:

  • Use document-level delegation (document.addEventListener('click', e => { var btn = e.target.closest('.foo'); … }))
  • Re-bind after AJAX by listening to / dispatching blestaAjaxComplete

Wrap pagination output in if ($this->Pagination->hasPages()) { … } so single-page lists don't render an empty card-footer with just a page-of-1 indicator.

15.12 Widget AJAX swap target changed

Existing v5 plugin endpoints that emitted {replacer: '.common_box_content', content: '…'} need to switch to {replacer: '.card-content', content: '…'} for admin. (Client portal already used .card-content.)


16. Canonical reference files

Use these v6 files as references when porting your own:

PatternReference file
List view with sortable headers, action buttons, pagination, modal-confirm-deleteplugins/order/views/default/admin_forms.pdt
Multi-tab settings form, BS5 button-radio groups, tooltip icons, template chooser, dual-selectplugins/order/views/default/admin_forms_add.pdt
Widget with category tabs (badges), expandable rows via Bootstrap Collapse, lazy-load on show.bs.collapse, bulk-actions barplugins/order/views/default/admin_main.pdt
Simple settings form with card-body + card-footer + cancel/saveplugins/order/views/default/admin_settings.pdt
Connected language-tab form with CKEditor 5 lazy-init per tabplugins/cms/views/default/admin_main_manage.pdt
Status-conditional action buttons on rowsplugins/order/views/default/admin_payouts.pdt
Embed-codes alert + Ace editor theme-aware integrationplugins/order/views/default/admin_forms_embed_codes.pdt
Paradigm sidebar (collapsible groups)plugins/order/views/default/partials/admin_affiliates_sidebar.pdt
Tooltip-wrapped row content using data-bs-toggle="tooltip" data-bs-html="true"plugins/order/views/default/admin_main_search.pdt
Sidebar pattern (ticket meta in side panel)plugins/support_manager/views/default/admin_tickets_reply_sidebar.pdt
Toast UI editor / ticket reply / inline image uploadplugins/support_manager/views/default/admin_tickets_reply.pdt
Ticket replies card patternplugins/support_manager/views/default/admin_tickets_replies.pdt
File upload zone + previewplugins/support_manager/views/default/admin_staff_edit.pdt
Card grid (folders)plugins/support_manager/views/default/admin_responses_response_list.pdt
Dual-select drag-drop listplugins/support_manager/views/default/admin_staff_add.pdt
Modal confirm pattern (declarative)plugins/support_manager/views/default/admin_departments.pdt
Widget feed with paginationplugins/feed_reader/views/default/admin_main.pdt
Dashboard widget (widget-width button, ApexCharts, theme-aware filters)plugins/system_overview/views/default/admin_main.pdt

Core paradigm references:

Main structure shellapp/views/admin/paradigm/structure.pdt
Sidebar partialsapp/views/admin/paradigm/partials/sidebar.pdt, …/plugin_sidebar.pdt, …/main_sidebar.pdt
Theme manifestapp/views/admin/paradigm/config.json
Helper library JS (globals)assets/scripts/admin/paradigm/application.js
Theme variables SCSSassets/sass/admin/paradigm/base/_variables.scss
Backwards-compat shimsassets/sass/admin/paradigm/_backwards.scss
Expandable rows JSassets/scripts/admin/paradigm/expandable-rows.js
Widget helper PHPpublic_html/helpers/widget/widget.php
AbstractWidget basepublic_html/core/Util/Widgets/AbstractWidget.php
AppController (asset/AJAX/CSRF)public_html/app/app_controller.php
Module base classpublic_html/components/modules/module.php
Class aliases (ModuleFields, etc.)public_html/config/aliases.php
Gulp asset buildgulpfile.js

17. Using AI to convert your extension

Save a copy of this guide alongside your extension's source — the downloadable Markdown file at the top of this page is the same content in raw form, suitable for dropping into Claude, ChatGPT, Cursor, or another AI assistant as conversion context. The prompt below applies the rules above exhaustively to every .pdt file and inline <script> in your extension.

You are converting a Blesta 5.x plugin (or module) to be compatible with the
Blesta 6 Paradigm admin theme (Bootstrap 5 + vanilla JS + Bootstrap Icons).

Apply the rules in BLESTA-6-EXTENSION-CONVERSION-GUIDE.md exhaustively to
every .pdt file and inline <script> in the extension. Specifically:

1. Apply §2 conversion checklist line by line.
2. Apply §4 Bootstrap 4→5 class changes (data-bs- infix, float-end, ms/me,
form-select, form-check, form-label, btn-secondary, badge bg-…, etc.).
3. Apply §5 icon mapping (every fas/far/fa fa-… becomes bi bi-…), including
icon strings inside PHP arrays (setLinkButtons, setLinks, setWidgetButton,
getAdminTabs, getClientTabs, config.json).
4. Apply §6 jQuery → vanilla JS conversion (no jQuery anywhere). Replace
$(document).ready, all selectors, all events, all $.fn.blestaXxx calls,
blestaRequest, jQuery UI sortable, CKEditor binding. Use `fetch()` with
X-Requested-With header for AJAX.
5. Apply §7 markup changes (card-body/card-content/card-footer, table-responsive,
table-hover, expandable rows via data-collapse-row, modal-confirm-delete buttons).
6. Apply §8 Form helper class injection (form-control, form-select, form-check-input,
form-label, form-check-label) — every Form helper call must pass the right class.
7. Apply §9 Widget API changes (setBodyWrapper, footer(), full_screen → full-screen,
widget-width, icon class strings in arrays).
8. Apply §11 theme variables — never hard-code colors. Use var(--border-color),
var(--bg-secondary), bg-body-secondary, etc.
9. For modules, also apply §13: optional config.json icon key, and rename
arrayToModuleFields overrides to arrayToInputFields.
10. Test mentally in both light AND dark mode. Flag any hard-coded colors.

After each file, tell me what you changed. Don't add features or refactor
anything beyond what the guide requires.

Replace the file name in the first paragraph (BLESTA-6-EXTENSION-CONVERSION-GUIDE.md) with whatever you saved the file as. Some assistants accept the file as an attachment instead of inline context — either works.


Appendix A — Summary "before vs after" for a typical admin form

Before (Blesta 5):

<?php
$this->Widget->create($this->_('AdminFoo.add.boxtitle', true));
?>
<div class="inner">
<div class="title_row first"><h3><?php $this->_('AdminFoo.add.section');?></h3></div>
<?php $this->Form->create($this->base_uri . 'plugin/your_plugin/admin_foo/add/'); ?>
<div class="pad">
<ul>
<li>
<?php
$this->Form->label($this->_('AdminFoo.add.field_name', true), 'name');
$this->Form->fieldText('name', $vars->name, ['id' => 'name']);
?>
</li>
<li>
<?php
$this->Form->label($this->_('AdminFoo.add.field_status', true), 'status');
$this->Form->fieldSelect('status', $statuses, $vars->status, ['id' => 'status']);
?>
</li>
<li>
<?php
$this->Form->fieldCheckbox('enabled', '1', $vars->enabled == '1', ['id' => 'enabled']);
$this->Form->label($this->_('AdminFoo.add.field_enabled', true), 'enabled', ['class' => 'inline']);
?>
</li>
</ul>
</div>
<div class="button_row">
<a class="btn btn-default pull-right ajax" href="<?php echo $this->base_uri . 'plugin/your_plugin/admin_foo/';?>">
<i class="fas fa-times"></i> <?php $this->_('AdminFoo.add.field_cancel');?>
</a>
<?php $this->Form->fieldSubmit('save', $this->_('AdminFoo.add.field_save', true),
['class' => 'btn btn-primary pull-right']); ?>
</div>
<?php $this->Form->end(); ?>
</div>
<?php $this->Widget->end(); ?>

<script type="text/javascript">
$(document).ready(function() {
$('[data-toggle="tooltip"]').tooltip();
$('#enabled').change(function() {
$('.dependent-section').toggle(this.checked);
});
});
</script>

After (Blesta 6):

<?php /** @var View $this */ ?>
<?php $this->Form->create($this->base_uri . 'plugin/your_plugin/admin_foo/add/'); ?>
<?php $this->Widget->create($this->_('AdminFoo.add.boxtitle', true)); ?>
<h6 class="form-section-header"><?php $this->_('AdminFoo.add.section');?></h6>

<div class="mb-3">
<?php
$this->Form->label($this->_('AdminFoo.add.field_name', true), 'name', ['class' => 'form-label']);
$this->Form->fieldText('name', $vars->name ?? '', ['id' => 'name', 'class' => 'form-control']);
?>
</div>

<div class="mb-3">
<?php
$this->Form->label($this->_('AdminFoo.add.field_status', true), 'status', ['class' => 'form-label']);
$this->Form->fieldSelect('status', $statuses, $vars->status ?? null, ['id' => 'status', 'class' => 'form-select']);
?>
</div>

<div class="mb-3">
<div class="form-check">
<?php
$this->Form->fieldCheckbox('enabled', '1', ($vars->enabled ?? '') == '1',
['id' => 'enabled', 'class' => 'form-check-input']);
$this->Form->label($this->_('AdminFoo.add.field_enabled', true), 'enabled', ['class' => 'form-check-label']);
?>
</div>
</div>

<?php $this->Widget->footer(); ?>
<div class="d-flex justify-content-end gap-2">
<a class="btn btn-outline-secondary ajax" href="<?php echo $this->base_uri . 'plugin/your_plugin/admin_foo/';?>">
<?php $this->_('AdminFoo.add.field_cancel');?>
</a>
<?php $this->Form->fieldSubmit('save', $this->_('AdminFoo.add.field_save', true),
['class' => 'btn btn-primary']); ?>
</div>
<?php $this->Widget->end(); ?>
<?php $this->Form->end(); ?>

<script type="text/javascript">
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(function(el) {
new bootstrap.Tooltip(el);
});

var enabled = document.getElementById('enabled');
if (enabled) {
var sections = document.querySelectorAll('.dependent-section');
var toggle = function() {
sections.forEach(function(s) {
s.style.display = enabled.checked ? '' : 'none';
});
};
enabled.addEventListener('change', toggle);
toggle();
}
});
</script>

Appendix B — Quick-reference search/replace table

Suitable for a regex-aware editor. Apply in this order, eyeballing each match:

FindReplace
data-toggle=data-bs-toggle=
data-target=data-bs-target=
data-dismiss=data-bs-dismiss=
data-parent=data-bs-parent=
data-placement=data-bs-placement=
data-html=data-bs-html=
data-container=data-bs-container=
\bpull-right\bfloat-end
\bpull-left\bfloat-start
\bfloat-right\bfloat-end
\bfloat-left\bfloat-start
\bml-(\d)\bms-$1
\bmr-(\d)\bme-$1
\bpl-(\d)\bps-$1
\bpr-(\d)\bpe-$1
\btext-left\btext-start
\btext-right\btext-end
\bfont-weight-bold\bfw-bold
\bfont-weight-normal\bfw-normal
\bsr-only\bvisually-hidden
\bbtn-default\bbtn-secondary
\bbtn-block\bw-100
\bbadge-(primary|secondary|success|danger|warning|info|light|dark)\bbg-$1
\bform-group\bmb-3
\bcustom-control custom-checkbox\bform-check
\bcustom-control custom-radio\bform-check
\bcustom-control-input\bform-check-input
\bcustom-control-label\bform-check-label
\bcustom-select\bform-select
\bbg-light\bbg-body-secondary
class="close"class="btn-close"
<i class="fas fa-plus(\s|")<i class="bi bi-plus-lg$1
<i class="fas fa-pencil-alt(\s|")<i class="bi bi-pencil$1
<i class="fas fa-trash(\s|")<i class="bi bi-trash$1
<i class="fas fa-cog(\s|")<i class="bi bi-gear$1
<i class="fas fa-times(\s|")<i class="bi bi-x-lg$1
<i class="fas fa-eye(\s|")<i class="bi bi-eye$1
<i class="fas fa-question-circle(\s|")<i class="bi bi-question-circle$1
<i class="fas fa-info-circle(\s|")<i class="bi bi-info-circle$1
<i class="fas fa-power-off(\s|")<i class="bi bi-power$1
<i class="fas fa-sync-alt(\s|")<i class="bi bi-arrow-clockwise$1
<i class="fas fa-sign-in-alt(\s|")<i class="bi bi-box-arrow-in-right$1
<i class="fas fa-(\w[\w-]*)(\s|")<i class="bi bi-$1$2 (then verify each)
\$\(document\)\.ready\(document.addEventListener('DOMContentLoaded',
\.blestaRequest\((rewrite manually as fetch)
'icon' => 'fas fa-(\w[\w-]*)''icon' => 'bi bi-$1'
setWidgetButton\('full_screen'\)setWidgetButton('full-screen')

After bulk replacements, manually verify icon mappings against §5.1 (some FA names don't have a 1:1 BS Icons equivalent and need a different name).