From b8494b3e6ab19b1fad806781cd74ed24e854aee3 Mon Sep 17 00:00:00 2001 From: "Beau Beauchamp, WebTigers" Date: Sat, 24 Oct 2026 18:42:56 -0400 Subject: [PATCH 1/4] refactor(views): move every view's JS/CSS into registered assets (TIGER-384) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A `.phtml` owns markup; behavior is a JS asset and styling is the skin's job. The `pageScript()`/`pageStyle()` seam for that was built and tested, the rule was in the helper docblocks — and the tree still carried ~5,400 lines of inline JavaScript across 47 views, because nothing failed when someone typed ` - +pageScript($this->themeAssets . '/vendor/muuri/muuri.min.js'); ?> +pageScript('/_tiger/js/tiger.admin-index.js', ['defer' => true]); ?> diff --git a/core/views/scripts/auth/forgot.phtml b/core/views/scripts/auth/forgot.phtml index ac23658b..7257495d 100644 --- a/core/views/scripts/auth/forgot.phtml +++ b/core/views/scripts/auth/forgot.phtml @@ -36,44 +36,4 @@ $this->i18n([ 'recaptcha' => 'core.js.recaptcha', ]); ?> - +pageScript('/_tiger/js/tiger.auth-forgot.js', ['defer' => true]); ?> diff --git a/core/views/scripts/auth/lock.phtml b/core/views/scripts/auth/lock.phtml index 98ee78a0..da257a07 100644 --- a/core/views/scripts/auth/lock.phtml +++ b/core/views/scripts/auth/lock.phtml @@ -48,7 +48,8 @@ $who = $this->escape(($identity && $identity->username) ? $identity->userna - - +pageScript('/_modules/access/js/access.org-index.js', ['defer' => true]); ?> diff --git a/modules/access/views/scripts/user/edit.phtml b/modules/access/views/scripts/user/edit.phtml index d46a9ea3..13aa5776 100644 --- a/modules/access/views/scripts/user/edit.phtml +++ b/modules/access/views/scripts/user/edit.phtml @@ -70,38 +70,4 @@ $this->i18n([ - +pageScript('/_modules/access/js/access.user-edit.js', ['defer' => true]); ?> diff --git a/modules/access/views/scripts/user/index.phtml b/modules/access/views/scripts/user/index.phtml index bae9ecd4..15cf614b 100644 --- a/modules/access/views/scripts/user/index.phtml +++ b/modules/access/views/scripts/user/index.phtml @@ -62,73 +62,4 @@ $this->i18n([ - +pageScript('/_modules/access/js/access.user-index.js', ['defer' => true]); ?> diff --git a/modules/agent/assets/js/agent.admin-index.js b/modules/agent/assets/js/agent.admin-index.js new file mode 100644 index 00000000..12f07d23 --- /dev/null +++ b/modules/agent/assets/js/agent.admin-index.js @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 WebTigers. Tiger™ and WebTigers™ are trademarks of WebTigers. +/** + * agent.admin-index.js + * + * TigerAgent settings screen — the agent REGISTRY (TIGER-151): a card per agent, each with its + * own persona, provider, model and BYO key, one marked default. Add Agent appends a blank card; + * each card saves and deletes on its own. The API key is write-only — never echoed back; a + * stored key shows a "connected" badge. The install-wide auto-mode ceiling sits in its own + * governance card. + * + * Registered by the view with $this->pageScript(); the view owns the markup, this owns the + * behavior. Localized strings arrive through the $this->i18n() carrier (Tiger.t). + */ +(function () { + var csrf = document.querySelector('[name="_csrf"]'); + var csrfVal = csrf ? csrf.value : ''; + var fb = document.getElementById('agent-feedback'); + + function api(method, extra) { + var fd = new URLSearchParams(); + fd.set('module', 'agent'); fd.set('service', 'agents'); fd.set('method', method); + if (csrfVal) { fd.set('_csrf', csrfVal); } + Object.keys(extra || {}).forEach(function (k) { fd.set(k, extra[k]); }); + return fetch('/api', { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' }, body: fd }) + .then(function (r) { return r.json().catch(function () { return {}; }); }); + } + + // Live model list for one card: from the provider when a key is available (typed here, else the + // stored one), otherwise the curated static fallback (agent/settings/models). + function loadModels(card) { + var provider = card.querySelector('.agent-provider').value; + var keyEl = card.querySelector('.agent-key'); + var sel = card.querySelector('.agent-model'); + var hint = card.querySelector('.agent-model-hint'); + var current = sel.value; + var fd = new URLSearchParams(); + fd.set('module', 'agent'); fd.set('service', 'settings'); fd.set('method', 'models'); + fd.set('provider', provider); + if (keyEl && keyEl.value.trim()) { fd.set('api_key', keyEl.value.trim()); } + if (card.dataset.agentId) { fd.set('agent_id', card.dataset.agentId); } + hint.textContent = '…'; + fetch('/api', { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' }, body: fd }) + .then(function (r) { return r.json().catch(function () { return {}; }); }) + .then(function (res) { + if (!res || res.result !== 1 || !res.data || !res.data.models) { hint.textContent = ''; return; } + sel.innerHTML = ''; + res.data.models.forEach(function (m) { + var o = document.createElement('option'); + o.value = m.id; o.textContent = m.label || m.id; + if (m.id === current) { o.selected = true; } + sel.appendChild(o); + }); + if (!sel.value && sel.options.length) { sel.selectedIndex = 0; } + hint.textContent = res.data.live ? Tiger.t('modelsLive') : Tiger.t('modelsStatic'); + }) + .catch(function () { hint.textContent = ''; }); + } + + function refreshEmpty() { + var any = document.querySelectorAll('#agent-cards .agent-card').length > 0; + document.getElementById('agent-empty').hidden = any; + } + + function wire(card) { + card.querySelector('.agent-provider').addEventListener('change', function () { loadModels(card); }); + card.querySelector('.agent-model-refresh').addEventListener('click', function () { loadModels(card); }); + + card.querySelector('.agent-save').addEventListener('click', function () { + var name = card.querySelector('.agent-name').value.trim(); + if (!name) { TigerDOM.notify(fb, Tiger.t('nameRequired'), { type: 'error' }); return; } + var payload = { + agent_id: card.dataset.agentId || '', + name: name, + persona: card.querySelector('.agent-persona').value, + provider: card.querySelector('.agent-provider').value, + model: card.querySelector('.agent-model').value, + api_key: card.querySelector('.agent-key').value.trim(), + enabled: card.querySelector('.agent-enabled').checked ? '1' : '', + is_default: card.querySelector('.agent-default').checked ? '1' : '' + }; + TigerButton.run(this, function () { return api('save', payload); }).then(function (res) { + if (res && res.result === 1 && res.data && res.data.agent) { + var ag = res.data.agent; + card.dataset.agentId = ag.agent_id; + card.querySelector('.agent-key').value = ''; // clear write-only field + card.querySelector('.agent-connected').hidden = !ag.connected; + card.querySelector('.agent-disconnected').hidden = ag.connected; + if (ag.is_default) { card.querySelector('.agent-default').checked = true; } + TigerDOM.notify(fb, Tiger.t('agentSaved'), { type: 'success' }); + return; + } + (res && res.messages || []).forEach(function (m) { TigerDOM.notify(fb, m.message, { type: m.class }); }); + }).catch(function () { TigerDOM.notify(fb, Tiger.t('networkError'), { type: 'error' }); }); + }); + + card.querySelector('.agent-delete').addEventListener('click', function () { + var id = card.dataset.agentId || ''; + if (!id) { card.remove(); refreshEmpty(); return; } // unsaved card — just drop it + if (!window.confirm(Tiger.t('confirmDelete'))) { return; } + TigerButton.run(this, function () { return api('delete', { agent_id: id }); }).then(function (res) { + if (res && res.result === 1) { + card.remove(); refreshEmpty(); + TigerDOM.notify(fb, Tiger.t('agentDeleted'), { type: 'success' }); + return; + } + (res && res.messages || []).forEach(function (m) { TigerDOM.notify(fb, m.message, { type: m.class }); }); + }).catch(function () { TigerDOM.notify(fb, Tiger.t('networkError'), { type: 'error' }); }); + }); + + loadModels(card); + } + + document.querySelectorAll('#agent-cards .agent-card').forEach(wire); + refreshEmpty(); + + document.getElementById('agent-add').addEventListener('click', function () { + var tpl = document.getElementById('agent-card-tpl'); + var node = tpl.content.firstElementChild.cloneNode(true); + document.getElementById('agent-cards').appendChild(node); + wire(node); + refreshEmpty(); + node.querySelector('.agent-name').focus(); + }); + + document.getElementById('agent-mode-save').addEventListener('click', function () { + var mode = document.getElementById('agent-mode_max').value; + var fd = new URLSearchParams(); + fd.set('module', 'agent'); fd.set('service', 'settings'); fd.set('method', 'mode'); + if (csrfVal) { fd.set('_csrf', csrfVal); } + fd.set('mode_max', mode); + TigerButton.run(this, function () { + return fetch('/api', { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' }, body: fd }) + .then(function (r) { return r.json().catch(function () { return {}; }); }); + }).then(function (res) { + if (res && res.result === 1) { TigerDOM.notify(fb, Tiger.t('settingsSaved'), { type: 'success' }); return; } + (res && res.messages || []).forEach(function (m) { TigerDOM.notify(fb, m.message, { type: m.class }); }); + }).catch(function () { TigerDOM.notify(fb, Tiger.t('networkError'), { type: 'error' }); }); + }); +})(); diff --git a/modules/agent/assets/js/agent.mcp-index.js b/modules/agent/assets/js/agent.mcp-index.js new file mode 100644 index 00000000..8ed74ff1 --- /dev/null +++ b/modules/agent/assets/js/agent.mcp-index.js @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 WebTigers. Tiger™ and WebTigers™ are trademarks of WebTigers. +/** + * agent.mcp-index.js + * + * MCP Connections (admin). Register external MCP servers whose tools the in-app agent may call + * (outbound; TIGERMCP.md §9). Add/edit a connection (label, URL, optional Bearer token, + * enabled), test it (list its tools), remove it. All over /api → Agent_Service_Mcp; the token + * never round-trips to the browser. + * + * Registered by the view with $this->pageScript(); the view owns the markup, this owns the + * behavior. Localized strings arrive through the $this->i18n() carrier (Tiger.t). + */ +document.addEventListener('DOMContentLoaded', function () { + var esc = function (s) { return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { return { '&':'&','<':'<','>':'>','"':'"',"'":''' }[c]; }); }; + var fb = document.getElementById('mcp-feedback'); + + function api(method, body) { + body = body || {}; body.module = 'agent'; body.service = 'mcp'; body.method = method; + return fetch('/api', { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' }, body: new URLSearchParams(body) }).then(function (r) { return r.json().catch(function () { return {}; }); }); + } + function toast(res) { ((res && res.messages) || []).forEach(function (m) { TigerDOM.toast(m.message, { type: m.class }); }); } + + function resetForm() { + document.getElementById('mcp-id').value = ''; + document.getElementById('mcp-label').value = ''; + document.getElementById('mcp-url').value = ''; + document.getElementById('mcp-token').value = ''; + document.getElementById('mcp-enabled').checked = true; + document.getElementById('mcp-form-title').textContent = 'Add a connection'; + document.getElementById('mcp-reset').classList.add('d-none'); + } + document.getElementById('mcp-reset').addEventListener('click', resetForm); + + function render(rows) { + var host = document.getElementById('mcp-list'); + host.innerHTML = (rows || []).map(function (c) { + var state = c.enabled ? 'on' : 'off'; + var tok = c.has_token ? 'token' : ''; + return '
' + + '
' + + '
' + esc(c.label) + ' ' + state + tok + '
' + + '
' + esc(c.url) + '
' + + '
' + + '
' + + '' + + '' + + '' + + '
'; + }).join(''); + document.getElementById('mcp-empty').hidden = (rows || []).length > 0; + window._mcpRows = rows || []; + } + function load() { api('connections').then(function (res) { render((res && res.data && res.data.connections) || []); }); } + + document.getElementById('mcp-save').addEventListener('click', function () { + var body = { + id: document.getElementById('mcp-id').value, + label: document.getElementById('mcp-label').value.trim(), + url: document.getElementById('mcp-url').value.trim(), + token: document.getElementById('mcp-token').value, + enabled: document.getElementById('mcp-enabled').checked ? 1 : 0 + }; + TigerButton.run(this, function () { return api('save', body); }).then(function (res) { + if (res && res.result === 1) { TigerDOM.notify(fb, Tiger.t('connectionSaved'), { type: 'success' }); resetForm(); render(res.data.connections || []); } + else { toast(res); } + }); + }); + + document.getElementById('mcp-list').addEventListener('click', function (e) { + var card = e.target.closest('[data-id]'); if (!card) { return; } + var id = card.getAttribute('data-id'); + var row = (window._mcpRows || []).filter(function (c) { return c.id === id; })[0] || {}; + if (e.target.closest('.mcp-edit')) { + document.getElementById('mcp-id').value = id; + document.getElementById('mcp-label').value = row.label || ''; + document.getElementById('mcp-url').value = row.url || ''; + document.getElementById('mcp-token').value = ''; + document.getElementById('mcp-enabled').checked = !!row.enabled; + document.getElementById('mcp-form-title').textContent = 'Edit connection'; + document.getElementById('mcp-reset').classList.remove('d-none'); + window.scrollTo({ top: 0, behavior: 'smooth' }); + } else if (e.target.closest('.mcp-remove')) { + TigerModal.confirm({ title: Tiger.t('removeConnectionTitle'), body: Tiger.t('removeConnectionBody'), confirmLabel: Tiger.t('removeLabel'), variant: 'danger' }).then(function (ok) { + if (!ok) { return; } + api('remove', { id: id }).then(function (res) { toast(res); render((res.data && res.data.connections) || []); }); + }); + } else if (e.target.closest('.mcp-test')) { + var out = card.querySelector('.mcp-test-out'); out.textContent = 'Testing…'; out.className = 'small text-body-secondary mcp-test-out'; + api('test', { id: id }).then(function (res) { + if (res && res.result === 1) { + var names = (res.data.tools || []).map(function (t) { return t.name; }).slice(0, 8).join(', '); + out.className = 'small text-success mcp-test-out'; + out.textContent = '✓ ' + res.data.count + ' tool(s)' + (names ? ': ' + names : ''); + } else { out.className = 'small text-danger mcp-test-out'; out.textContent = '✗ could not reach the server'; } + }); + } + }); + + load(); +}); diff --git a/modules/agent/assets/js/agent.skills-index.js b/modules/agent/assets/js/agent.skills-index.js new file mode 100644 index 00000000..2a7e5cbd --- /dev/null +++ b/modules/agent/assets/js/agent.skills-index.js @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 WebTigers. Tiger™ and WebTigers™ are trademarks of WebTigers. +/** + * agent.skills-index.js + * + * Agent Skills manager — ONE DataTables grid (like the Modules screen): the browse catalog + * merged with what's installed, so a row's Status + action controls tell you whether it's + * installed. Installed skills pin to the top. Data loads from Agent_Service_Skills::datatable + * over /api; install / toggle / remove / view-source are /api calls. Tiger is NOT a trust + * authority — the grid shows PROVENANCE and you review the SKILL.md before installing + * (TIGERSKILLS.md §2, §5). + * + * Registered by the view with $this->pageScript(); the view owns the markup, this owns the + * behavior. Localized strings arrive through the $this->i18n() carrier (Tiger.t). + */ +document.addEventListener('DOMContentLoaded', function () { + if (typeof tigerDataTable !== 'function') { return; } + var $ = jQuery, esc = tigerDataTable.esc; + var rescan = false; // one-shot: next load re-scans the sources + + function api(method, body) { + body = body || {}; body.module = 'agent'; body.service = 'skills'; body.method = method; + return fetch('/api', { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' }, body: new URLSearchParams(body) }).then(function (r) { return r.json(); }); + } + function toast(res) { ((res && res.messages) || []).forEach(function (m) { TigerDOM.toast(m.message, { type: m.class }); }); } + + // A skill entry rides on the button as individual data-* attrs (not a JSON blob). esc() is + // attribute-safe (escapes quotes), so it can go straight into the double-quoted attributes. + function entryAttrs(row) { + return ' data-source="' + esc(row.source) + '" data-sourcelabel="' + esc(row.sourceLabel) + '"' + + ' data-name="' + esc(row.name) + '" data-repo="' + esc(row.repo) + '"' + + ' data-ref="' + esc(row.ref) + '" data-path="' + esc(row.path) + '" data-url="' + esc(row.url) + '"'; + } + function readEntry(el) { + return { + source: el.getAttribute('data-source') || '', + sourceLabel: el.getAttribute('data-sourcelabel') || '', + name: el.getAttribute('data-name') || '', + repo: el.getAttribute('data-repo') || '', + ref: el.getAttribute('data-ref') || '', + path: el.getAttribute('data-path') || '', + url: el.getAttribute('data-url') || '' + }; + } + + var table = tigerDataTable('#sk-table', { + service: { module: 'agent', service: 'skills', action: 'datatable' }, + ordering: false, // installed-first pinning is authoritative (server-side) + language: { search: '', searchPlaceholder: 'Search skills…' }, + extraData: function () { var r = rescan ? 1 : 0; rescan = false; return { refresh: r }; }, + columns: [ + { data: 'name', render: function (t, type, row) { + if (type !== 'display') { return t; } + return '' + esc(t) + ''; + } }, + { data: 'description', className: 'text-body-secondary small', render: function (t, type) { + if (type !== 'display') { return t; } + t = String(t || ''); if (t.length > 160) { t = t.slice(0, 160) + '…'; } + return esc(t); + } }, + { data: 'sourceLabel', render: function (t, type) { + if (type !== 'display') { return t; } + return t ? '' + esc(t) + '' : '—'; + } }, + { data: 'installed', className: 'text-nowrap', render: function (t, type, row) { + if (type !== 'display') { return t ? 1 : 0; } + if (!row.installed) { return 'Available'; } + return row.active + ? 'On' + : 'Installed'; + } }, + { data: null, orderable: false, searchable: false, className: 'text-end text-nowrap', render: function (t, type, row) { + var h = ''; + // View source (always) — key if installed, else the repo/ref/path for a live fetch. + h += ' '; + if (!row.installed) { + h += ''; + } else { + h += row.active + ? ' ' + : ' '; + h += ''; + } + return h; + } } + ] + }); + + // ---- actions (delegated) ---- + $('#sk-table tbody').on('click', '.sk-install', function () { + var entry = readEntry(this); + TigerButton.run(this, function () { return api('install', entry); }).then(function (res) { + toast(res); table.ajax.reload(null, false); + }); + }); + $('#sk-table tbody').on('click', '.sk-toggle', function () { + var btn = this; + TigerButton.run(btn, function () { return api('toggle', { key: btn.getAttribute('data-key'), active: btn.getAttribute('data-active') }); }) + .then(function (res) { toast(res); table.ajax.reload(null, false); }); + }); + $('#sk-table tbody').on('click', '.sk-remove', function () { + var btn = this, key = btn.getAttribute('data-key'); + TigerModal.confirm({ title: Tiger.t('removeSkillTitle'), body: Tiger.t('removeSkillBody'), confirmLabel: Tiger.t('removeLabel'), variant: 'danger' }).then(function (ok) { + if (!ok) { return; } + api('remove', { key: key }).then(function (res) { toast(res); table.ajax.reload(null, false); }); + }); + }); + + // ---- view source (+ install-from-modal for a not-yet-installed skill) ---- + var srcModalEl = document.getElementById('sk-src-modal'); + var srcModal = (srcModalEl && window.bootstrap) ? new bootstrap.Modal(srcModalEl) : null; + var srcEntry = null; // set when the open row isn't installed (enables the modal Install) + $('#sk-table tbody').on('click', '.sk-src', function () { + var b = this; + var key = b.getAttribute('data-key'); + srcEntry = key ? null : readEntry(b); + document.getElementById('sk-src-title').textContent = (b.getAttribute('data-name') || 'SKILL') + '.md'; + document.getElementById('sk-src-sub').textContent = b.getAttribute('data-repo') || ''; + document.getElementById('sk-src-body').textContent = 'Loading…'; + var inst = document.getElementById('sk-src-install'); inst.hidden = !!key; + if (srcModal) { srcModal.show(); } + api('source', { key: key || '', repo: b.getAttribute('data-repo') || '', ref: b.getAttribute('data-ref') || '', path: b.getAttribute('data-path') || '' }) + .then(function (res) { document.getElementById('sk-src-body').textContent = (res && res.data && res.data.source) || '(empty)'; }); + }); + document.getElementById('sk-src-install').addEventListener('click', function () { + if (!srcEntry) { return; } + TigerButton.run(this, function () { return api('install', srcEntry); }).then(function (res) { + toast(res); if (res && res.result === 1 && srcModal) { srcModal.hide(); } table.ajax.reload(null, false); + }); + }); + + // ---- header controls ---- + document.getElementById('sk-rescan').addEventListener('click', function () { + rescan = true; TigerButton.run(this, function () { return Promise.resolve(table.ajax.reload(null, false)); }); + }); + document.getElementById('sk-url-install').addEventListener('click', function () { + var url = document.getElementById('sk-url').value.trim(); + if (!url) { return; } + TigerButton.run(this, function () { return api('install', { url: url }); }).then(function (res) { + toast(res); + if (res && res.result === 1) { document.getElementById('sk-url').value = ''; table.ajax.reload(null, false); } + }); + }); +}); diff --git a/modules/agent/views/scripts/admin/index.phtml b/modules/agent/views/scripts/admin/index.phtml index a24b8e15..6bb9c6f4 100644 --- a/modules/agent/views/scripts/admin/index.phtml +++ b/modules/agent/views/scripts/admin/index.phtml @@ -104,131 +104,4 @@ $this->i18n([ 'nameRequired' => 'agent.js.name_required', ]); ?> - +pageScript('/_modules/agent/js/agent.admin-index.js', ['defer' => true]); ?> diff --git a/modules/agent/views/scripts/mcp/index.phtml b/modules/agent/views/scripts/mcp/index.phtml index e31509db..a2f796a6 100644 --- a/modules/agent/views/scripts/mcp/index.phtml +++ b/modules/agent/views/scripts/mcp/index.phtml @@ -60,93 +60,4 @@ $this->i18n([ 'removeLabel' => 'agent.js.remove_label', ]); ?> - +pageScript('/_modules/agent/js/agent.mcp-index.js', ['defer' => true]); ?> diff --git a/modules/agent/views/scripts/skills/index.phtml b/modules/agent/views/scripts/skills/index.phtml index 31877325..950a4d9b 100644 --- a/modules/agent/views/scripts/skills/index.phtml +++ b/modules/agent/views/scripts/skills/index.phtml @@ -74,137 +74,4 @@ $this->i18n([ 'removeSkillTitle' => 'agent.js.remove_skill_title', ]); ?> - +pageScript('/_modules/agent/js/agent.skills-index.js', ['defer' => true]); ?> diff --git a/modules/ally/assets/js/ally.index-index.js b/modules/ally/assets/js/ally.index-index.js new file mode 100644 index 00000000..806ee2f4 --- /dev/null +++ b/modules/ally/assets/js/ally.index-index.js @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 WebTigers. Tiger™ and WebTigers™ are trademarks of WebTigers. +/** + * ally.index-index.js + * + * TigerAlly — nominal accessibility inspector. Scan a CMS page, paste HTML, or roll up every + * page. + * + * Registered by the view with $this->pageScript(); the view owns the markup, this owns the + * behavior. Localized strings arrive through the $this->i18n() carrier (Tiger.t). + */ +document.addEventListener('DOMContentLoaded', function () { + var fb = document.getElementById('ally-feedback'); + var pageEl = document.getElementById('ally-page'); + var htmlEl = document.getElementById('ally-html'); + var report = document.getElementById('ally-report'); + + var RULE_ICON = { 'img-alt': 'fa-image', 'link-name': 'fa-link', 'button-name': 'fa-hand-pointer', + 'input-label': 'fa-i-cursor', 'heading-order': 'fa-heading', 'heading-empty': 'fa-heading', + 'table-headers': 'fa-table', 'duplicate-id': 'fa-clone', 'doc-lang': 'fa-language', + 'positive-tabindex': 'fa-arrow-down-1-9' }; + + function esc(s) { var d = document.createElement('div'); d.textContent = s == null ? '' : String(s); return d.innerHTML; } + + function api(method, extra, btn) { + var fd = new URLSearchParams(extra || {}); + fd.set('module', 'ally'); fd.set('service', 'scan'); fd.set('method', method); + var task = function () { + return fetch('/api', { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' }, body: fd }) + .then(function (r) { return r.json().catch(function () { return {}; }); }); + }; + return btn ? TigerButton.run(btn, task) : task(); + } + + function loadPages() { + api('pages').then(function (res) { + if (!res || res.result !== 1) { return; } + var list = (res.data && res.data.pages) || []; + pageEl.innerHTML = ''; + list.forEach(function (p) { + var o = document.createElement('option'); + o.value = p.page_id; + o.textContent = (p.title || p.slug || p.page_id) + ' (/' + (p.slug || '') + (p.locale ? ' · ' + p.locale : '') + ')'; + pageEl.appendChild(o); + }); + if (!list.length) { pageEl.innerHTML = ''; } + }); + } + + function badge(sev, n) { + var cls = sev === 'error' ? 'text-bg-danger' : 'text-bg-warning'; + return '' + n + ' ' + sev + (n === 1 ? '' : 's') + ''; + } + + function renderReport(res) { + if (!res || res.result !== 1 || !res.data) { + (res && res.messages || []).forEach(function (m) { TigerDOM.notify(fb, m.message, { type: m.class }); }); + return; + } + var d = res.data; + var src = d.source ? ('
Source: ' + esc(d.source.title || d.source.slug) + ' ' + esc(d.source.format) + '
') : '
Source: pasted HTML
'; + var head; + if (d.passed && d.summary.warning === 0) { + head = '
Nominal. No accessibility gaps found.
'; + } else { + head = '
' + (d.summary.error ? badge('error', d.summary.error) : '') + (d.summary.warning ? badge('warning', d.summary.warning) : '') + + (d.passed ? ' no blocking errors' : '') + '
'; + } + var body = ''; + (d.findings || []).forEach(function (f) { + var icon = RULE_ICON[f.rule] || 'fa-triangle-exclamation'; + var sevcls = f.severity === 'error' ? 'border-danger' : 'border-warning'; + body += '
' + + '
' + esc(f.message) + + ' ' + f.count + '
'; + (f.samples || []).forEach(function (s) { + body += '
' + esc(s) + '
'; + }); + body += '
'; + }); + report.innerHTML = src + head + body; + } + + function renderAll(res) { + if (!res || res.result !== 1 || !res.data) { return; } + var t = res.data.totals, rows = res.data.results || []; + var html = '
' + t.pages + ' pages' + + '' + t.passed + ' nominal' + + (t.error ? badge('error', t.error) : '') + (t.warning ? badge('warning', t.warning) : '') + '
'; + html += '
'; + rows.forEach(function (r) { + var status = r.skipped ? 'skipped' + : (r.passed ? 'nominal' : 'issues'); + html += '' + + '' + + ''; + }); + html += '
PageStatusErrorsWarnings
' + esc(r.title || r.slug) + + ' /' + esc(r.slug || '') + '' + status + '' + (r.error || 0) + '' + (r.warning || 0) + '
'; + report.innerHTML = html; + report.querySelectorAll('[data-scan-page]').forEach(function (tr) { + tr.addEventListener('click', function () { + pageEl.value = this.getAttribute('data-scan-page'); + api('scan', { page_id: this.getAttribute('data-scan-page') }).then(renderReport); + }); + }); + } + + document.getElementById('ally-scan-page').addEventListener('click', function () { + if (!pageEl.value) { TigerDOM.notify(fb, Tiger.t('choosePage'), { type: 'info' }); return; } + api('scan', { page_id: pageEl.value }, this).then(renderReport) + .catch(function () { TigerDOM.notify(fb, Tiger.t('networkError'), { type: 'error' }); }); + }); + document.getElementById('ally-scan-html').addEventListener('click', function () { + if (!htmlEl.value.trim()) { TigerDOM.notify(fb, Tiger.t('pasteHtml'), { type: 'info' }); return; } + api('scan', { html: htmlEl.value }, this).then(renderReport) + .catch(function () { TigerDOM.notify(fb, Tiger.t('networkError'), { type: 'error' }); }); + }); + document.getElementById('ally-scan-all').addEventListener('click', function () { + api('scanAll', {}, this).then(renderAll) + .catch(function () { TigerDOM.notify(fb, Tiger.t('networkError'), { type: 'error' }); }); + }); + + loadPages(); +}); diff --git a/modules/ally/views/scripts/index/index.phtml b/modules/ally/views/scripts/index/index.phtml index 308a49ce..484a3b71 100644 --- a/modules/ally/views/scripts/index/index.phtml +++ b/modules/ally/views/scripts/index/index.phtml @@ -56,119 +56,4 @@ $this->i18n([ 'pasteHtml' => 'ally.js.paste_html', ]); ?> - +pageScript('/_modules/ally/js/ally.index-index.js', ['defer' => true]); ?> diff --git a/modules/analytics/assets/js/analytics.admin-index.js b/modules/analytics/assets/js/analytics.admin-index.js new file mode 100644 index 00000000..62fb7877 --- /dev/null +++ b/modules/analytics/assets/js/analytics.admin-index.js @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 WebTigers. Tiger™ and WebTigers™ are trademarks of WebTigers. +/** + * analytics.admin-index.js + * + * Analytics settings — the measurement id / property, the OAuth connect-disconnect, and the + * save. Every action is an /api call to Analytics_Service_Analytics. + * + * Registered by the view with $this->pageScript(); the view owns the markup, this owns the + * behavior. Localized strings arrive through the $this->i18n() carrier (Tiger.t). + */ +document.addEventListener('DOMContentLoaded', function () { + var form = document.getElementById('analytics-form'); + var fb = document.getElementById('analytics-feedback'); + + // Show the bring-your-own-client fields only when that method is selected. + var byo = document.getElementById('analytics-byo'); + function syncByo() { + var on = document.getElementById('oauth-mode-byo'); + if (byo) { byo.classList.toggle('d-none', !(on && on.checked)); } + } + form.querySelectorAll('input[name="oauth_mode"]').forEach(function (r) { r.addEventListener('change', syncByo); }); + + // Save the whole form (both tabs) over /api. Resolves true on success, false on a handled error + // (field errors highlighted, messages shown). Shared by the Save button and Connect. + function save(btn) { + fb.innerHTML = ''; + form.querySelectorAll('.is-invalid').forEach(function (e) { e.classList.remove('is-invalid'); }); + var fd = new URLSearchParams(new FormData(form)); + fd.set('module', 'analytics'); fd.set('service', 'analytics'); fd.set('method', 'save'); + return TigerButton.run(btn, function () { + return fetch('/api', { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' }, body: fd }) + .then(function (r) { return r.json().catch(function () { return {}; }); }); + }).then(function (res) { + if (res && res.result === 1) { return true; } + if (res && res.form) { + Object.keys(res.form).forEach(function (field) { + var input = form.querySelector('[name="' + field + '"]'); + if (input) { input.classList.add('is-invalid'); } + }); + } + var msgs = (res && res.messages) || []; + if (msgs.length) { msgs.forEach(function (m) { TigerDOM.notify(fb, m.message, { type: m.class, replace: false }); }); } + else { TigerDOM.notify(fb, Tiger.t('fixFields'), { type: 'error' }); } + return false; + }).catch(function () { TigerDOM.notify(fb, Tiger.t('networkError'), { type: 'error' }); return false; }); + } + + document.getElementById('analytics-save').addEventListener('click', function () { + save(this).then(function (ok) { if (ok) { TigerDOM.notify(fb, Tiger.t('saved'), { type: 'success' }); } }); + }); + + // Connect: save the form first (so the property ID + method the user just typed are persisted), + // then — only if the save succeeded — hand off to the OAuth flow. One click, no separate Save. + var connectBtn = document.getElementById('analytics-connect'); + if (connectBtn) { + connectBtn.addEventListener('click', function () { + var prop = form.querySelector('[name="property_id"]'); + if (prop && prop.value.trim() === '') { + TigerDOM.notify(fb, Tiger.t('connectNeedProperty'), { type: 'error' }); + prop.classList.add('is-invalid'); + prop.focus(); + return; + } + save(this).then(function (ok) { if (ok) { window.location.href = '/analytics/admin/connect'; } }); + }); + } + + // Troubleshooting: live "Test connection" — reports.test runs a real GA call and diagnoses it. + function esc(s) { var d = document.createElement('div'); d.textContent = (s == null ? '' : s); return d.innerHTML; } + var testBtn = document.getElementById('analytics-test'); + if (testBtn) { + testBtn.addEventListener('click', function () { + var out = document.getElementById('analytics-test-result'); + var fd = new URLSearchParams({ module: 'analytics', service: 'reports', method: 'test' }); + TigerButton.run(this, function () { + return fetch('/api', { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' }, body: fd }) + .then(function (r) { return r.json().catch(function () { return {}; }); }); + }).then(function (res) { + var t = res && res.data && res.data.test; + if (!t) { out.innerHTML = '
Couldn’t run the test — please try again.
'; return; } + var kind = t.ok ? 'success' : (t.code === 'not_connected' ? 'secondary' : 'warning'); + var icon = t.ok ? 'circle-check' : (t.code === 'not_connected' ? 'circle-info' : 'triangle-exclamation'); + var html = '

' + esc(t.message) + '

'; + if (t.hint) { html += '

' + esc(t.hint) + '

'; } + if (t.detail) { html += '

' + esc(t.detail) + '

'; } + out.innerHTML = html + '
'; + }).catch(function () { out.innerHTML = '
Network error — please try again.
'; }); + }); + } +}); diff --git a/modules/analytics/views/scripts/admin/dashboard.phtml b/modules/analytics/views/scripts/admin/dashboard.phtml index c6952be2..d659516b 100644 --- a/modules/analytics/views/scripts/admin/dashboard.phtml +++ b/modules/analytics/views/scripts/admin/dashboard.phtml @@ -74,5 +74,5 @@ - +pageScript('/_modules/analytics/js/analytics.dashboard.js', ['defer' => true]); ?> diff --git a/modules/analytics/views/scripts/admin/index.phtml b/modules/analytics/views/scripts/admin/index.phtml index c2ceba71..b58017c5 100644 --- a/modules/analytics/views/scripts/admin/index.phtml +++ b/modules/analytics/views/scripts/admin/index.phtml @@ -11,6 +11,13 @@ $form = $this->form; $el = function ($name) use ($form) { return $form->getElement($name); }; $mode = ($this->ga['mode'] === 'byo') ? 'byo' : 'broker'; +// Localized strings this page's JS references — delivered via the carrier, not an inline script. +$this->i18n([ + 'fixFields' => 'analytics.fix_fields', + 'networkError' => 'analytics.net_error', + 'saved' => 'analytics.saved', + 'connectNeedProperty' => 'analytics.connect_need_property', +]); ?>
@@ -237,85 +244,4 @@ $mode = ($this->ga['mode'] === 'byo') ? 'byo' : 'broker';
- +pageScript('/_modules/analytics/js/analytics.admin-index.js', ['defer' => true]); ?> diff --git a/modules/backup/assets/js/backup.index-index.js b/modules/backup/assets/js/backup.index-index.js new file mode 100644 index 00000000..ef5d5f88 --- /dev/null +++ b/modules/backup/assets/js/backup.index-index.js @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 WebTigers. Tiger™ and WebTigers™ are trademarks of WebTigers. +/** + * backup.index-index.js + * + * Backup & Restore screen (/backup) — create a backup (components + destination), the backup + * history (download / restore / delete), restore from an uploaded file, and the scheduled-backup + * settings (embedding the reusable schedule-control + cron-setup partials). Follows ADMIN.md: + * header + feedback mount + cards; every mutation is an /api call via TigerButton/TigerDOM. No + * page-POSTs, no bespoke CSS. + * + * Registered by the view with $this->pageScript(); the view owns the markup, this owns the + * behavior. Localized strings arrive through the $this->i18n() carrier (Tiger.t). + */ +(function () { + var fb = document.getElementById('backup-feedback'); + function notify(res) { (res && res.messages || []).forEach(function (m) { TigerDOM.notify(fb, m.message, { type: m.class }); }); } + function api(method, params, btn) { + var body = new URLSearchParams(params || {}); + body.set('module', 'backup'); body.set('service', 'backup'); body.set('method', method); + var task = function () { + return fetch('/api', { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' }, body: body }) + .then(function (r) { return r.json().catch(function () { return {}; }); }); + }; + return btn ? TigerButton.run(btn, task) : task(); + } + function checkedComponents() { + return Array.prototype.slice.call(document.querySelectorAll('#backup-components input:checked')).map(function (i) { return i.value; }); + } + + // Back up now + document.getElementById('backup-run').addEventListener('click', function () { + var comps = checkedComponents(); + if (!comps.length) { TigerDOM.notify(fb, Tiger.t('selectComponent'), { type: 'error' }); return; } + var body = new URLSearchParams(); + body.set('module', 'backup'); body.set('service', 'backup'); body.set('method', 'run'); + body.set('disk', document.getElementById('backup-disk').value); + body.set('include_secrets', document.getElementById('backup-secrets').checked ? 1 : 0); + comps.forEach(function (c) { body.append('components[]', c); }); + TigerButton.run(this, function () { + return fetch('/api', { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' }, body: body }).then(function (r) { return r.json().catch(function () { return {}; }); }); + }).then(function (res) { notify(res); if (res && res.result === 1) { setTimeout(function () { location.reload(); }, 900); } }); + }); + + // Delete + document.addEventListener('click', function (e) { + var del = e.target.closest('[data-delete]'); + if (del) { + if (!confirm(Tiger.t('confirmDeleteNamed', del.getAttribute('data-name')))) { return; } + api('remove', { backup_id: del.getAttribute('data-delete') }, del).then(function (res) { notify(res); if (res && res.result === 1) { setTimeout(function () { location.reload(); }, 700); } }); + } + }); + + // Restore (history) — open the confirm modal + var modalEl = document.getElementById('restore-modal'); + var modal = new bootstrap.Modal(modalEl); + var confirmInput = document.getElementById('restore-confirm'); + var pending = null; // { kind:'history', id } | { kind:'upload' } + + document.addEventListener('click', function (e) { + var r = e.target.closest('[data-restore]'); + if (r) { + pending = { kind: 'history', id: r.getAttribute('data-restore') }; + document.getElementById('restore-target').textContent = r.getAttribute('data-name'); + confirmInput.value = ''; modal.show(); + } + }); + + // Restore from upload + document.getElementById('restore-upload').addEventListener('click', function () { + var f = document.getElementById('restore-file').files[0]; + if (!f) { TigerDOM.notify(fb, Tiger.t('chooseZip'), { type: 'error' }); return; } + pending = { kind: 'upload', file: f }; + document.getElementById('restore-target').textContent = f.name; + confirmInput.value = ''; modal.show(); + }); + + // Confirm restore + document.getElementById('restore-go').addEventListener('click', function () { + if (confirmInput.value !== 'RESTORE') { confirmInput.classList.add('is-invalid'); return; } + confirmInput.classList.remove('is-invalid'); + var btn = this; + if (pending && pending.kind === 'history') { + api('restore', { backup_id: pending.id, confirm: 'RESTORE' }, btn).then(function (res) { + modal.hide(); notify(res); if (res && res.result === 1) { setTimeout(function () { location.reload(); }, 1500); } + }); + } else if (pending && pending.kind === 'upload') { + var fd = new FormData(); + fd.append('archive', pending.file); fd.append('confirm', 'RESTORE'); + TigerButton.run(btn, function () { + return fetch('/backup/index/upload', { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' }, body: fd }).then(function (r) { return r.json().catch(function () { return {}; }); }); + }).then(function (res) { modal.hide(); notify(res); if (res && res.result === 1) { setTimeout(function () { location.reload(); }, 1500); } }); + } + }); + + // Save schedule settings (components + destination come from the Create card) + document.getElementById('backup-save-settings').addEventListener('click', function () { + var comps = checkedComponents(); + var body = new URLSearchParams(); + body.set('module', 'backup'); body.set('service', 'backup'); body.set('method', 'saveSettings'); + body.set('disk', document.getElementById('backup-disk').value); + body.set('include_secrets', document.getElementById('backup-secrets').checked ? 1 : 0); + body.set('retention_max', document.getElementById('set-retention').value || '0'); + body.set('notify_enabled', document.getElementById('set-notify').checked ? 1 : 0); + body.set('notify_email', document.getElementById('set-email').value || ''); + comps.forEach(function (c) { body.append('components[]', c); }); + TigerButton.run(this, function () { + return fetch('/api', { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' }, body: body }).then(function (r) { return r.json().catch(function () { return {}; }); }); + }).then(function (res) { notify(res); }); + }); +})(); diff --git a/modules/backup/views/scripts/index/index.phtml b/modules/backup/views/scripts/index/index.phtml index 14f844ce..8a5387ed 100644 --- a/modules/backup/views/scripts/index/index.phtml +++ b/modules/backup/views/scripts/index/index.phtml @@ -217,102 +217,4 @@ $outcomeBadge = [
- +pageScript('/_modules/backup/js/backup.index-index.js', ['defer' => true]); ?> diff --git a/modules/blog/assets/js/blog.post-edit.js b/modules/blog/assets/js/blog.post-edit.js new file mode 100644 index 00000000..9afa3c34 --- /dev/null +++ b/modules/blog/assets/js/blog.post-edit.js @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 WebTigers. Tiger™ and WebTigers™ are trademarks of WebTigers. +/** + * blog.post-edit.js + * + * Medium-style article editor. A distraction-free writing canvas (Kicker · Title · Subtitle · + * Preamble · Body) plus a "Post settings" offcanvas for the metadata. The canvas fields are + * plain named inputs (styled borderless); the body is a contentEditable surface mirrored into a + * hidden ' + + btn + + ''; + host.appendChild(wrap); + }); + + // Context: show grep usage immediately; AI summary behind the button (cached server-side). + var summary = document.getElementById('tr-context-summary'); + var usage = document.getElementById('tr-context-usage'); + summary.textContent = ''; + usage.innerHTML = ''; + (d.usage || []).forEach(function (u) { + var li = document.createElement('li'); + li.innerHTML = '' + esc(u.file) + ':' + u.line + ''; + usage.appendChild(li); + }); + if (!d.usage || !d.usage.length) { usage.innerHTML = '
  • ' + esc(Tiger.t('noRefs')) + '
  • '; } + var ctxBtn = document.getElementById('tr-context-btn'); + ctxBtn.hidden = !d.ai; + } + + // --- per-locale AI translate --- + $('#tr-modal-fields').on('click', '[data-tr-translate]', function () { + var code = this.getAttribute('data-tr-translate'); + var field = document.querySelector('[data-tr-value="' + code + '"]'); + var src = current ? current.source : ''; + var body = new URLSearchParams({ module: 'translations', service: 'translation', method: 'translate', source: src, locale: code, key: current ? current.key : '' }); + TigerButton.run(this, function () { + return fetch('/api', { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' }, body: body }) + .then(function (r) { return r.json().catch(function () { return {}; }); }); + }).then(function (res) { + if (res && res.result === 1 && res.data && field) { field.value = res.data.text; field.focus(); } + else { (res && res.messages || []).forEach(function (m) { TigerDOM.notify(document.getElementById('tr-modal-feedback'), m.message, { type: m.class }); }); } + }).catch(function () {}); + }); + + // --- AI context summary --- + document.getElementById('tr-context-btn').addEventListener('click', function () { + if (!current) { return; } + var body = new URLSearchParams({ module: 'translations', service: 'translation', method: 'context', key: current.key }); + TigerButton.run(this, function () { + return fetch('/api', { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' }, body: body }) + .then(function (r) { return r.json().catch(function () { return {}; }); }); + }).then(function (res) { + if (res && res.result === 1 && res.data) { document.getElementById('tr-context-summary').textContent = res.data.summary; } + }).catch(function () {}); + }); + + // --- save --- + document.getElementById('tr-save').addEventListener('click', function () { + if (!current) { return; } + var fb = document.getElementById('tr-modal-feedback'); + var body = new URLSearchParams({ module: 'translations', service: 'translation', method: 'save', key: current.key }); + document.querySelectorAll('#tr-modal-fields [data-tr-value]').forEach(function (el) { + body.append('values[' + el.getAttribute('data-tr-value') + ']', el.value); + }); + TigerButton.run(this, function () { + return fetch('/api', { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' }, body: body }) + .then(function (r) { return r.json().catch(function () { return {}; }); }); + }).then(function (res) { + if (res && res.result === 1) { + modal.hide(); + TigerDOM.toast(Tiger.t('saved'), { type: 'success' }); + table.ajax.reload(null, false); + return; + } + (res && res.messages || []).forEach(function (m) { TigerDOM.notify(fb, m.message, { type: m.class }); }); + }).catch(function () { TigerDOM.notify(fb, Tiger.t('networkError'), { type: 'error' }); }); + }); + + // --- revert all locales for the key --- + document.getElementById('tr-revert').addEventListener('click', function () { + if (!current) { return; } + var key = current.key, self = this; + TigerModal.confirm({ title: Tiger.t('revertConfirm'), body: key, confirmLabel: Tiger.t('revertConfirm') }).then(function (ok) { + if (!ok) { return; } + var body = new URLSearchParams({ module: 'translations', service: 'translation', method: 'revert', key: key }); + TigerButton.run(self, function () { + return fetch('/api', { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' }, body: body }) + .then(function (r) { return r.json().catch(function () { return {}; }); }); + }).then(function () { + modal.hide(); + TigerDOM.toast(Tiger.t('reverted'), { type: 'success' }); + table.ajax.reload(null, false); + }).catch(function () {}); + }); + }); +}); diff --git a/modules/translations/views/scripts/index/index.phtml b/modules/translations/views/scripts/index/index.phtml index 006aa121..013b89cc 100644 --- a/modules/translations/views/scripts/index/index.phtml +++ b/modules/translations/views/scripts/index/index.phtml @@ -8,6 +8,20 @@ * context. Rows + all writes go over /api (Translations_Service_Translation). ADMIN.md house style. */ $default = (string) $this->default; +// Localized strings this page's JS references — delivered via the carrier, not an inline script. +$this->i18n([ + 'translate' => 'translations.translate', + 'translating' => 'translations.translating', + 'badgeDefault' => 'translations.badge_default', + 'badgeOverridden' => 'translations.badge_overridden', + 'usesFile' => 'translations.uses_file', + 'saved' => 'translations.saved_toast', + 'reverted' => 'translations.reverted_toast', + 'noRefs' => 'translations.no_refs', + 'networkError' => 'translations.net_error', + 'revertConfirm' => 'translations.revert_confirm', + 'searchPlaceholder' => 'translations.search_placeholder', +]); ?>
    @@ -108,190 +122,4 @@ $default = (string) $this->default;
    - +pageScript('/_modules/translations/js/translations.index-index.js', ['defer' => true]); ?> diff --git a/public/css/tiger.admin-index.css b/public/css/tiger.admin-index.css new file mode 100644 index 00000000..ca3bff6b --- /dev/null +++ b/public/css/tiger.admin-index.css @@ -0,0 +1,22 @@ +/*! SPDX-License-Identifier: BSD-3-Clause · © 2026 WebTigers · Tiger™/WebTigers™ are trademarks */ +/** + * tiger.admin-index.css — the admin dashboard's Muuri grid geometry (core admin/index.phtml). + * + * Registered by the view with $this->pageStyle(); a view carries no '))); + $this->assertSame(1, preg_match_all(self::TAG, $this->stripPhp(''))); + $this->assertSame(0, preg_match_all(self::TAG, $this->stripPhp('https://example.com/feed')), + 'an RSS is not a stylesheet'); + + // …and that a +pageScript($this->themeAssets . '/js/tiger.admin-header.js', ['defer' => true]); ?> diff --git a/themes/puma/views/scripts/_partials/consent-banner.phtml b/themes/puma/views/scripts/_partials/consent-banner.phtml index 27a95ad4..c82f9375 100644 --- a/themes/puma/views/scripts/_partials/consent-banner.phtml +++ b/themes/puma/views/scripts/_partials/consent-banner.phtml @@ -41,29 +41,4 @@ $gpc = !empty($c['gpc']) && !empty($c['honor_gpc']); - +pageScript($this->themeAssets . '/js/tiger.consent-banner.js', ['defer' => true]); ?> From a6f997ca7aded330f13c58e49b9ebd5a59d4f764 Mon Sep 17 00:00:00 2001 From: "Beau Beauchamp, WebTigers" Date: Sat, 24 Oct 2026 18:55:19 -0400 Subject: [PATCH 2/4] refactor(views): drop registrations the admin layout already makes; exempt JSON-LD MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four views registered a library the admin layout loads unconditionally on every admin page (tiger-upload, Sortable, tiger.media-picker) — duplicate ')), + 'JSON-LD carries data, not behavior, and the spec requires it inline'); // …and that a