Perfect—here’s a full JS implementation plan for the builder_topbar.tpl
you posted. It’s organized by Segment NN → file name, with:
-
the IDs and key selectors each segment owns,
-
what each control does (events → actions),
-
emitted/consumed fg:* events,
-
any AJAX / shared-state touch points,
-
keyboard shortcuts and edge cases.
I’m keeping this 100% aligned with your TPL (IDs exactly as in your snippet), and I’m using your segment anchors so you can wire Segment_NN.js
one-to-one.
Core wiring
-
Global namespace:
window.ModulaBuilder
(safe single object) -
State (shared):
ModulaBuilder.state
(what you already outlined in the MDK—projectId, pageId, mode, breakpoint, selection, history, dirty) -
Bus (DOM events): dispatch and listen on
document
withfg:*
custom events -
i18n strings: read from
data-*
attributes on owning nodes (as provided in the TPL) -
Read-only guard: respect
[aria-disabled="true"]
on controls when$ro===true
-
Live regions: write status messages to the associated
aria-live
nodes
Boot order (scripts load before DOM):
In builder_scripts.tpl
you can include:
<!-- Segment loader — loaded early (HEAD), no defer -->
<script>window.ModulaBuilder = window.ModulaBuilder || {}; ModulaBuilder.modules = {};</script>
<script src="/includes/assets/js/Segment_10.js?v={$code|escape}"></script>
<script src="/includes/assets/js/Segment_12.js?v={$code|escape}"></script>
<script src="/includes/assets/js/Segment_14.js?v={$code|escape}"></script>
<script src="/includes/assets/js/Segment_20.js?v={$code|escape}"></script>
<script src="/includes/assets/js/Segment_30.js?v={$code|escape}"></script>
<script src="/includes/assets/js/Segment_40.js?v={$code|escape}"></script>
<script src="/includes/assets/js/Segment_50.js?v={$code|escape}"></script>
<script src="/includes/assets/js/Segment_60.js?v={$code|escape}"></script>
<script src="/includes/assets/js/Segment_70.js?v={$code|escape}"></script>
<script src="/includes/assets/js/Segment_99.js?v={$code|escape}"></script>
<!-- Final orchestrator -->
<script src="/includes/assets/js/Segment_boot.js?v={$code|escape}"></script>
Each Segment_XX.js
should register itself into ModulaBuilder.modules["XX"] = { init(el){…}, destroy(){…} }
.Segment_boot.js
finds each [data-seg="NN"]
and calls its module’s init(el)
immediately if DOM is already there, or on DOMContentLoaded
if not.
Segment 10 — Brand / Build / Status
File: /includes/assets/js/Segment_10.js
Owns IDs:
-
#fg-open-left
-
#fg-build-badge
-
#fg-env-chip
(optional, present only if$env
set) -
#fg-status-dot
-
#fg-clock
Selectors:
-
Root:
[data-seg="10"]
Responsibilities & Events
-
#fg-open-left
click →dispatchEvent("fg:left:toggle")
-
Clock tick (1s) → update
#fg-clock
text (local time) -
Status LED:
-
Listen
fg:state:dirty
→ adddata-state="dirty"
to#fg-status-dot
-
Listen
fg:save:success
→ pulse success; setdata-state="clean"
-
Listen
fg:save:error
→ pulse error
-
-
Build badge:
-
On
fg:version:update
event: update innerText if backend returns version label
-
Edge cases
-
If left panel fails to open (no target), emit
fg:warn
with a message.
Segment 12 — Project • Page • Variant • Locale
File: /includes/assets/js/Segment_12.js
Owns IDs:
-
#fg-project-select
-
#fg-branch-select
-
#fg-env-select
-
#fg-page-select
-
#fg-variant-select
-
#fg-lang-select
-
buttons in
.fg-split
withdata-project="new|open|rename"
Selectors:
-
Root:
[data-seg="12"]
-
New/Open/Rename:
[data-project]
Responsibilities & Events
-
Changes:
-
#fg-project-select
change →fg:project:select
{projectId}
-
#fg-branch-select
change →fg:branch:select
{branch}
-
#fg-env-select
change →fg:env:select
{env}
-
#fg-page-select
change →fg:page:select
{pageId}
-
#fg-variant-select
change →fg:variant:select
{variant}
-
#fg-lang-select
change →fg:lang:select
{lang}
-
-
Buttons:
-
data-project="new"
→fg:project:new
-
data-project="open"
→fg:project:open
-
data-project="rename"
→fg:project:rename
-
-
On
fg:init
:-
preload values from
localStorage
(fg:lastProject
,fg:lastPage
,fg:env
,fg:lang
) -
set selects accordingly (without re-triggering events)
-
-
On
fg:project:loaded
(from backend draft load):-
hydrate selects from payload
-
-
Persist:
-
After any change, write to
localStorage
-
AJAX (optional hooks)
-
Project change can trigger draft load:
-
Emit
fg:load:draft
with{projectId, pageId}
; another core module handles the XHR
-
Edge cases
-
Read-only: if
$ro
, disable actions that mutate (new/rename) -
If project load fails, revert select to previous value & toast error
Segment 14 — Presence (avatars, invite, handoff)
File: /includes/assets/js/Segment_14.js
Owns IDs:
-
#fg-presence-tray
-
Live region:
[data-collab="live"]
(inside the tray)
Selectors:
-
Avatars container:
[data-collab="avatars"]
-
Invite button:
[data-collab="invite"]
-
Handoff button:
[data-collab="handoff"]
Responsibilities & Events
-
Invite flow:
-
click invite → open small prompt (your UI) → validate →
fg:collab:invite
{emailOrName}
-
On success → update avatars list; announce via live region using
data-msg-*
-
-
Handoff:
-
click → copy share link (from backend or generated) → announce copied / fail using
data-msg-*
-
-
Presence updates:
-
Listen
fg:collab:join
/fg:collab:left
events and update avatars -
Update
aria-label
counts viadata-msg-aria-count
-
Edge cases
-
No Clipboard API → fallback to input/select copy
-
Duplicate invite → show
data-msg-exists
Segment 20 — Edit & Tools
File: /includes/assets/js/Segment_20.js
Owns IDs:
-
#fg-edit-tools
(root) -
Buttons:
-
#fg-undo
,#fg-redo
-
#fg-clean
,#fg-wrap
,#fg-format
-
#fg-copy
,#fg-cut
,#fg-paste
-
#fg-snapshot
-
-
Tool buttons (no IDs):
[data-tool="select|pan|measure|picker|note"]
-
Live regions:
-
[data-edit="live"]
,[data-tools-live]
-
Responsibilities & Events
-
Edit actions:
-
undo →
fg:history:undo
-
redo →
fg:history:redo
-
clean →
fg:edit:clean
-
wrap →
fg:edit:wrap
-
format→
fg:edit:format
-
copy →
fg:edit:copy
-
cut →
fg:edit:cut
-
paste →
fg:edit:paste
(open modal prompt if needed) -
snapshot →
fg:snapshot:save
-
-
Tool set:
-
click tool →
fg:tool:set
{tool}
-
reflect active tool via
aria-pressed
ordata-active
-
-
Keyboard:
-
⌘/Ctrl+Z redo with Shift, copy/cut/paste when focus in canvas (not in inputs)
-
-
Messaging:
-
On success events from the core (
fg:edit:*:ok
) → announce via[data-edit="live"]
-
Block actions in RO mode (
[aria-disabled="true"]
) and announce viadata-msg-readonly
-
Edge cases
-
If nothing to undo/redo → announce empty
-
Pasting empty → announce
data-msg-paste-empty
Segment 30 — Devices & Zoom
File: /includes/assets/js/Segment_30.js
Owns IDs:
-
#fg-break-zoom
(root) -
Breakpoint preset buttons:
[data-bp="1280|840|420"]
-
#fg-bp-custom
-
#fg-bp-rotate
-
Zoom buttons:
[data-zoom="out|reset|in|fit|fill"]
-
#fg-zoom-pct
-
Live regions:
[data-bp="live"]
,[data-zoom-live]
Responsibilities & Events
-
Breakpoints:
-
preset click →
fg:breakpoint:change
{width, name}
-
custom input (on Enter / blur, validate 240–3840) → same event
-
rotate click →
fg:breakpoint:rotate
-
-
Zoom:
-
out/in/reset/fit/fill →
fg:zoom:set
{type, value?}
-
direct percent (on Enter/blur, validate min/max, e.g., 10–400) →
fg:zoom:set
{type:"percent", value}
-
-
Feedback:
-
Announce applied/invalid via live regions, using
data-msg-*
on root
-
-
Keyboard:
-
⌘/Ctrl + plus/minus → zoom in/out
-
⌘/Ctrl + 0 → reset
-
⇧1 → fit; ⇧2 → fill
-
State syncing
-
On
fg:breakpoint:changed
from canvas, set active button/fields -
On
fg:zoom:changed
, update percent field
Edge cases
-
If out-of-range custom width → show invalid message and revert
Segment 40 — View & Prefs (overlays, units/bg, lab, density, theme, autosave)
File: /includes/assets/js/Segment_40.js
Owns IDs:
-
Overlays switches:
-
#fg-ov-grid
,#fg-ov-rulers
,#fg-ov-baseline
,#fg-ov-outline
-
-
Pref selects/inputs:
-
#fg-pref-units
,#fg-pref-bg
,#fg-pref-grid
,#fg-pref-base
-
-
Lab/DPR:
-
#fg-lab-device
,#fg-lab-dpr
-
-
UI density select:
-
#fg-density
-
-
Theme:
-
#fg-theme-toggle
-
-
Autosave:
-
#fg-autosave
-
Responsibilities & Events
-
Overlays:
-
toggle change →
fg:overlay:toggle
{grid|rulers|baseline|outline, value}
-
-
Prefs:
-
units/bg/grid/base changes →
fg:pref:set
{key,value}
-
-
Lab:
-
device change →
fg:lab:device
{device}
-
dpr change →
fg:lab:dpr
{dpr}
-
-
Density:
-
change →
fg:ui:density
{density}
-
-
Theme:
-
click →
fg:theme:toggle
(and persistfg:theme
)
-
-
Autosave:
-
toggle →
fg:autosave:toggle
{enabled}
-
Persistence
-
Save all to
localStorage
on change -
On
fg:init
, rehydrate UI fromlocalStorage
Edge cases
-
Invalid numbers (grid/base/dpr): clamp & announce
Segment 50 — Preview • Export • Publish • Save • Share
File: /includes/assets/js/Segment_50.js
Owns IDs:
-
#fg-pub
(root) -
#fg-preview
-
#fg-export
,#fg-export-caret
, dropdown#fg-dd-export
(menu) -
#fg-save
,#fg-publish
,#fg-share
-
Live regions:
[data-preview-export-live]
,[data-pubshare-live]
Responsibilities & Events
-
Preview:
-
click →
fg:preview:toggle
(pressed state toggles)
-
-
Export:
-
main button → export with default format from
data-export-default
(zip)-
emit
fg:export:request
{format}
-
-
caret opens menu (
#fg-dd-export
) → click item emitsfg:export:request
{format}
-
-
Save:
-
click →
fg:save:request
{autosave:false}
-
Listen
fg:save:success|error
for feedback
-
-
Publish:
-
click →
fg:publish:request
-
-
Share:
-
click →
fg:share:copy
(uses Clipboard, falls back otherwise)
-
-
Keyboard:
-
⌘/Ctrl+S → save
-
⌘/Ctrl+Shift+P → publish
-
⌘/Ctrl+L → share link copy
-
Space when focused on preview → toggle preview
-
AJAX handoff (to your existing PHP)
-
A separate “transport” module (or the core) should listen:
-
fg:save:request
→ POST/includes/ajax/builder_save.php
-
fg:publish:request
→ POST/includes/ajax/builder_publish.php
-
fg:export:request
→ POST or browser download, depending on your flow
-
-
On completion, the transport emits:
-
fg:save:success|error
,fg:publish:success|error
,fg:export:done|error
-
-
This segment only triggers the requests and renders outcome in live regions
Edge cases
-
While saving/publishing, set
aria-busy="true"
on the button; clear on completion -
Disable buttons in RO mode
Segment 60 — Command Palette
File: /includes/assets/js/Segment_60.js
Owns IDs:
-
#fg-qa
-
Live:
[data-qa-live]
Responsibilities & Events
-
Input behavior:
-
⌘/Ctrl+K global shortcut → focus
#fg-qa
-
Parse commands:
-
fit
,fill
,zoom 120
,bp 375
,export png
,grid on
,units rem
,bg dark
,dpr 2
,preview
,save
, etc.
-
-
Map to events:
-
fg:zoom:set
,fg:breakpoint:change
,fg:export:request
,fg:overlay:toggle
,fg:pref:set
,fg:preview:toggle
,fg:save:request
-
-
-
Autocomplete (optional):
-
provide lightweight inline suggestions
-
-
Feedback:
-
success/fail short message to
[data-qa-live]
-
Edge cases
-
Unknown command → suggest nearest valid command
Segment 70 — Notifications • Help • User account
File: /includes/assets/js/Segment_70.js
Owns IDs:
-
#fg-bell
-
Account dropdown:
-
trigger
[data-dd="fg-dd-user"]
-
menu
#fg-dd-user
-
-
Menu items by
data-user="profile|prefs|tokens|theme|logout"
Responsibilities & Events
-
Notifications:
-
click bell →
fg:notif:open
-
-
Account menu:
-
toggle show/hide with focus trap
-
each menu item emits:
-
fg:user:open:profile
-
fg:user:open:prefs
-
fg:user:open:tokens
-
fg:theme:toggle
-
fg:user:logout
-
-
-
Close on Esc / outside click
Edge cases
-
Don’t trap focus if menu hidden
-
Sync “Toggle theme” with Segment 40
Segment 99 — Plugin lane (top-level slot)
File: /includes/assets/js/Segment_99.js
Owns IDs:
-
#fg-plugins-topbar
(slot)
Responsibilities & Events
-
Listen
fg:plugins:mount
with payload{nodes:HTMLElement[]}
or descriptors; append into the slot -
Listen
fg:plugins:clear
to empty the slot -
If
hidden
and there are plugins, unhide; if empty, hide
IDs inventory (quick reference)
Row 1
-
fg-open-left
-
fg-build-badge
-
fg-env-chip
(conditional) -
fg-status-dot
-
fg-clock
-
fg-project-select
-
fg-branch-select
-
fg-env-select
-
fg-page-select
-
fg-variant-select
-
fg-lang-select
-
fg-presence-tray
Row 2
-
fg-edit-tools
-
fg-undo
,fg-redo
,fg-clean
,fg-wrap
,fg-format
,fg-copy
,fg-cut
,fg-paste
,fg-snapshot
-
fg-break-zoom
-
fg-bp-custom
,fg-bp-rotate
,fg-zoom-pct
-
fg-view-prefs
-
fg-ov-grid
,fg-ov-rulers
,fg-ov-baseline
,fg-ov-outline
-
fg-pref-units
,fg-pref-bg
,fg-pref-grid
,fg-pref-base
-
fg-lab-device
,fg-lab-dpr
-
fg-density
-
fg-theme-toggle
-
fg-autosave
-
fg-pub
-
fg-preview
,fg-export
,fg-export-caret
,fg-dd-export
,fg-save
,fg-publish
,fg-share
-
fg-qa
-
fg-account
-
fg-bell
-
fg-dd-user
Plugins
-
fg-plugins-topbar
Event catalog (emitted)
-
fg:left:toggle
-
fg:project:new|open|rename|select
-
fg:branch:select
-
fg:env:select
-
fg:page:select
-
fg:variant:select
-
fg:lang:select
-
fg:collab:invite
-
fg:collab:handoff
-
fg:history:undo|redo
-
fg:edit:clean|wrap|format|copy|cut|paste
-
fg:snapshot:save
-
fg:tool:set
-
fg:breakpoint:change|rotate
-
fg:zoom:set
-
fg:overlay:toggle
-
fg:pref:set
(units, bg, grid, base) -
fg:lab:device|dpr
-
fg:ui:density
-
fg:theme:toggle
-
fg:autosave:toggle
-
fg:preview:toggle
-
fg:export:request
-
fg:save:request
-
fg:publish:request
-
fg:share:copy
-
fg:notif:open
-
fg:user:open:profile|prefs|tokens|logout
Event catalog (consumed / listen for)
-
fg:init
,fg:project:loaded
-
fg:state:dirty
-
fg:save:success|error
-
fg:publish:success|error
-
fg:export:done|error
-
fg:breakpoint:changed
,fg:zoom:changed
-
fg:collab:join|left
-
fg:version:update
-
fg:plugins:mount|clear
Transport (one shared module, not tied to a segment)
File: /includes/assets/js/transport.js
(or Segment_00_transport.js
)
-
Listens to:
-
fg:save:request
→POST /includes/ajax/builder_save.php
-
fg:publish:request
→POST /includes/ajax/builder_publish.php
-
fg:export:request
→ download/export flow
-
-
Emits:
-
fg:save:success|error
,fg:publish:success|error
,fg:export:done|error
-
-
Sets/clears
ModulaBuilder.state.dirty
-
Handles CSRF, ownership checks (per your backend)
Keyboard map (topbar-related)
-
⌘/Ctrl+S
→ save -
⌘/Ctrl+Shift+P
→ publish -
⌘/Ctrl+L
→ share link -
Space
(focus on Preview) → toggle preview -
⌘/Ctrl+K
→ focus command palette -
⌘/Ctrl +
/⌘/Ctrl -
/⌘/Ctrl 0
→ zoom in/out/reset -
⇧1
/⇧2
→ fit / fill -
⌘/Ctrl+Z
/⌘/Ctrl+Shift+Z
→ undo / redo
Minimal module shape (copy for each Segment_XX.js)
// /includes/assets/js/Segment_50.js
(function () {
const id = "50";
function on(el) {
const q = sel => el.querySelector(sel);
const qa = sel => Array.from(el.querySelectorAll(sel));
const btnPreview = q('#fg-preview');
const btnExport = q('#fg-export');
const btnCaret = q('#fg-export-caret');
const menuExport = q('#fg-dd-export');
const btnSave = q('#fg-save');
const btnPublish = q('#fg-publish');
const btnShare = q('#fg-share');
// wire…
btnPreview?.addEventListener('click', () => dispatch('fg:preview:toggle'));
btnExport?.addEventListener('click', () => dispatch('fg:export:request', { format: btnExport.dataset.exportDefault || 'zip' }));
btnCaret?.addEventListener('click', () => toggleMenu(menuExport));
menuExport?.addEventListener('click', (e) => {
const b = e.target.closest('[data-export]');
if (!b) return;
dispatch('fg:export:request', { format: b.dataset.export });
closeMenu(menuExport);
});
btnSave?.addEventListener('click', () => dispatch('fg:save:request', { autosave:false }));
btnPublish?.addEventListener('click', () => dispatch('fg:publish:request'));
btnShare?.addEventListener('click', copyShare);
// listen for responses
document.addEventListener('fg:save:success', () => announce('[data-pubshare-live]', 'Saved'));
document.addEventListener('fg:save:error', () => announce('[data-pubshare-live]', 'Save failed'));
}
function dispatch(type, detail) { document.dispatchEvent(new CustomEvent(type, { detail })); }
function announce(sel, msg) {
const live = document.querySelector(sel);
if (live) { live.textContent = ''; requestAnimationFrame(() => live.textContent = msg); }
}
function toggleMenu(menu){ if (!menu) return; const hidden = menu.hasAttribute('hidden'); hidden?menu.removeAttribute('hidden'):menu.setAttribute('hidden',''); }
function closeMenu(menu){ menu?.setAttribute('hidden',''); }
async function copyShare(){
dispatch('fg:share:copy');
}
ModulaBuilder.modules[id] = { init: on };
})();
If you want, I can generate stubs for all 10 segment files in the exact shape above so you can drop them in and start filling the core logic.