Topbar Segment Plan - JS

Andrei Milea
Admin
Joined: 2025-05-11 19:18:17
2025-10-09 17:31:29

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 with fg:* 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 → add data-state="dirty" to #fg-status-dot

    • Listen fg:save:success → pulse success; set data-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 with data-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 via data-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 or data-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 via data-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 persist fg:theme)

  • Autosave:

    • toggle → fg:autosave:toggle {enabled}

Persistence

  • Save all to localStorage on change

  • On fg:init, rehydrate UI from localStorage

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 emits fg: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:requestPOST /includes/ajax/builder_save.php

    • fg:publish:requestPOST /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.

modula Install Digital