DECK Frontend v4.6 - EventBus decoupling

Added Events module for inter-component communication:
- Events.on(event, handler) - subscribe
- Events.off(event, handler) - unsubscribe
- Events.emit(event, data) - publish

Decoupled:
- GroupsBar → Events.emit('render') instead of App.renderView()
- LibrariesPanel → Events.emit('render')
- GraphView sidebar → Events.emit('render')
- GraphView node click → Events.emit('detail:show', mrf)

App subscribes to events in init():
- Events.on('render', () => this.renderView())
- Events.on('detail:show', (mrf) => DetailPanel.show(mrf))

Now modules don't know about each other - changes are isolated.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
ARCHITECT
2026-01-16 19:37:34 +00:00
parent daea7753b6
commit 7672d5582f

View File

@@ -7,13 +7,13 @@
<meta name="description" content="DECK Tag Management System">
<!--
DECK FRONTEND v4.5 - All bases + generic structure
DECK FRONTEND v4.6 - EventBus decoupling
Extract: ./extract.sh deck.html [output_dir]
-->
<style>
/* =============================================================================
* DECK STYLES v4.5
* DECK STYLES v4.6
* ============================================================================= */
/* -----------------------------------------------------------------------------
@@ -726,8 +726,8 @@ body {
<script>
/**
* DECK Frontend v4.5
* All 12 bases: HST,FLG,ITM,LOC,PLY,MST,BCK,MTH,ATC,Oracle,MAIL,CHAT
* DECK Frontend v4.6
* EventBus pattern for module decoupling
*/
// =============================================================================
@@ -804,7 +804,20 @@ const State = {
};
// =============================================================================
// 3. UTILS
// 3. EVENTS (desacoplamiento entre módulos)
// =============================================================================
const Events = {
_handlers: {},
on(event, fn) { (this._handlers[event] ||= []).push(fn); },
off(event, fn) {
if (!fn) delete this._handlers[event];
else this._handlers[event] = (this._handlers[event] || []).filter(h => h !== fn);
},
emit(event, data) { (this._handlers[event] || []).forEach(fn => fn(data)); }
};
// =============================================================================
// 4. UTILS
// =============================================================================
const Utils = {
$(selector) { return document.querySelector(selector); },
@@ -881,7 +894,7 @@ const Utils = {
};
// =============================================================================
// 4. API
// 5. API
// =============================================================================
const API = {
async fetch(endpoint, schema, options = {}) {
@@ -950,7 +963,7 @@ const API = {
};
// =============================================================================
// 5. FILTER
// 6. FILTER
// =============================================================================
const Filter = {
apply(tags) {
@@ -972,7 +985,7 @@ const Filter = {
};
// =============================================================================
// 6. COMPONENTS
// 7. COMPONENTS
// =============================================================================
const GroupsBar = {
el: null,
@@ -984,7 +997,7 @@ const GroupsBar = {
if (!btn) return;
State.set({ group: btn.dataset.group });
this.render();
App.renderView();
Events.emit('render');
});
},
@@ -1022,7 +1035,7 @@ const LibrariesPanel = {
State.set({ library: mrf, libraryMembers: new Set(members) });
}
this.render();
App.renderView();
Events.emit('render');
});
},
@@ -1105,7 +1118,7 @@ const DetailPanel = {
};
// =============================================================================
// 7. VIEWS
// 8. VIEWS
// =============================================================================
const GridView = {
render(tags) {
@@ -1318,7 +1331,7 @@ const GraphView = {
.attr("fill", "#e0e0e0").attr("font-size", "10px");
}
node.on("click", (e, d) => { e.stopPropagation(); DetailPanel.show(d.id); });
node.on("click", (e, d) => { e.stopPropagation(); Events.emit('detail:show', d.id); });
this.simulation.on("tick", () => {
link.attr("x1", d => d.source.x).attr("y1", d => d.source.y).attr("x2", d => d.target.x).attr("y2", d => d.target.y);
@@ -1378,7 +1391,7 @@ const GraphView = {
const cats = new Set(graphFilters.categories);
cb.checked ? cats.add(cb.dataset.category) : cats.delete(cb.dataset.category);
State.set({ graphFilters: { ...graphFilters, categories: cats } });
App.renderView();
Events.emit('render');
};
});
@@ -1387,20 +1400,20 @@ const GraphView = {
const edges = new Set(graphFilters.edgeTypes);
cb.checked ? edges.add(cb.dataset.edge) : edges.delete(cb.dataset.edge);
State.set({ graphFilters: { ...graphFilters, edgeTypes: edges } });
App.renderView();
Events.emit('render');
};
});
sidebar.querySelector("#gs-images").onchange = (e) => { State.set({ graphSettings: { ...graphSettings, showImages: e.target.checked } }); App.renderView(); };
sidebar.querySelector("#gs-labels").onchange = (e) => { State.set({ graphSettings: { ...graphSettings, showLabels: e.target.checked } }); App.renderView(); };
sidebar.querySelector("#gs-images").onchange = (e) => { State.set({ graphSettings: { ...graphSettings, showImages: e.target.checked } }); Events.emit('render'); };
sidebar.querySelector("#gs-labels").onchange = (e) => { State.set({ graphSettings: { ...graphSettings, showLabels: e.target.checked } }); Events.emit('render'); };
const sizeSlider = sidebar.querySelector("#gs-size");
sizeSlider.oninput = (e) => { sidebar.querySelector("#gs-size-val").textContent = e.target.value + "px"; };
sizeSlider.onchange = (e) => { State.set({ graphSettings: { ...graphSettings, nodeSize: parseInt(e.target.value) } }); App.renderView(); };
sizeSlider.onchange = (e) => { State.set({ graphSettings: { ...graphSettings, nodeSize: parseInt(e.target.value) } }); Events.emit('render'); };
const distSlider = sidebar.querySelector("#gs-dist");
distSlider.oninput = (e) => { sidebar.querySelector("#gs-dist-val").textContent = e.target.value + "px"; };
distSlider.onchange = (e) => { State.set({ graphSettings: { ...graphSettings, linkDist: parseInt(e.target.value) } }); App.renderView(); };
distSlider.onchange = (e) => { State.set({ graphSettings: { ...graphSettings, linkDist: parseInt(e.target.value) } }); Events.emit('render'); };
},
renderControls(container) {
@@ -1431,7 +1444,7 @@ const GraphView = {
};
// =============================================================================
// 8. APP
// 9. APP
// =============================================================================
const App = {
async init() {
@@ -1439,6 +1452,10 @@ const App = {
if (hashState.base) State.set({ base: hashState.base });
if (hashState.view) State.set({ view: hashState.view });
// Event subscriptions (desacoplamiento)
Events.on('render', () => this.renderView());
Events.on('detail:show', (mrf) => DetailPanel.show(mrf));
GroupsBar.init();
LibrariesPanel.init();
DetailPanel.init();