# Tutorial Part 3: Building a C++ UI Module This is Part 3 of the Logos module tutorial series. In [Part 2](tutorial-qml-ui-app.md) you built a QML UI plugin. Now you'll build a **native C++ Qt widget plugin** that calls `calc_module` through a typed backend class. **What you'll build:** A `calc_ui_cpp` C++ plugin with two options for the UI: - **Option A — QML loaded from C++:** A `QQuickWidget` inside the plugin loading the same `Main.qml` as the QML plugin, with `CalcBackend` exposed as a context property — plus dev mode for editing QML without rebuilding - **Option B — Pure Qt widget:** `QPushButton`, `QLineEdit`, `QLabel` wired directly to a backend class **Why C++ over QML-only?** | | QML plugin (Part 2) | C++ UI plugin (Part 3) | |---|---|---| | Compilation | No | Yes (CMake) | | Backend calls | Via `logos.callModule()` IPC bridge | Via `LogosAPI*` directly in C++ | | Type safety | Weak — all args travel as `QVariant` | Strong — C++ types preserved | | Sandboxing | Yes | No | | QML support | Native | Optional via `QQuickWidget` | The C++ backend class also fixes the type coercion issue from Part 2 — `int` arguments stay `int` all the way to the module. **Prerequisites:** - Completed [Part 1](tutorial-wrapping-c-library.md) — you have a working `calc_module` - Nix with flakes enabled --- ## How It Works ``` +----------------------+ CalcBackend::add(3, 5) +-------------------+ | calc_ui_cpp | --------------------------------> | calc_module | | C++ Qt plugin | LogosAPI* / invokeRemoteMethod | C++ plugin | | createWidget() | | add(int, int) | +----------------------+ +-------------------+ ^ | loaded by v logos-standalone-app / logos-basecamp ``` The plugin implements `createWidget()` which returns a `QWidget*`. The widget is shown in the host app's window. A `CalcBackend` class holds `LogosAPI*` and makes typed calls to `calc_module`. --- ## Step 1: Scaffold ```bash mkdir logos-calc-ui-cpp && cd logos-calc-ui-cpp nix flake init -t github:logos-co/logos-module-builder#ui-module git init && git add -A ``` This gives you: ``` logos-calc-ui-cpp/ ├── flake.nix ├── module.yaml ├── metadata.json ├── CMakeLists.txt └── src/ ├── ui_example_interface.h ├── ui_example_plugin.h └── ui_example_plugin.cpp ``` Rename the source files to match your module: ```bash mv src/ui_example_interface.h src/calc_ui_cpp_interface.h mv src/ui_example_plugin.h src/calc_ui_cpp_plugin.h mv src/ui_example_plugin.cpp src/calc_ui_cpp_plugin.cpp ``` --- ## Step 2: `module.yaml` ```yaml name: calc_ui_cpp version: 1.0.0 type: ui category: tools description: "Calculator C++ UI — widget frontend for calc_module" dependencies: - calc_module nix_packages: build: [] runtime: [] external_libraries: [] cmake: find_packages: [] extra_sources: [] proto_files: [] ``` --- ## Step 3: `metadata.json` ```json { "name": "calc_ui_cpp", "version": "1.0.0", "description": "Calculator C++ UI — widget frontend for calc_module", "type": "ui", "main": "calc_ui_cpp_plugin", "dependencies": ["calc_module"], "category": "tools" } ``` --- ## Step 4: `CMakeLists.txt` ```cmake cmake_minimum_required(VERSION 3.14) project(CalcUiCppPlugin LANGUAGES CXX) if(DEFINED ENV{LOGOS_MODULE_BUILDER_ROOT}) include($ENV{LOGOS_MODULE_BUILDER_ROOT}/cmake/LogosModule.cmake) else() message(FATAL_ERROR "LogosModule.cmake not found. Set LOGOS_MODULE_BUILDER_ROOT.") endif() logos_module( NAME calc_ui_cpp SOURCES src/calc_ui_cpp_interface.h src/calc_ui_cpp_plugin.h src/calc_ui_cpp_plugin.cpp src/calc_backend.h src/calc_backend.cpp ) find_package(Qt6 REQUIRED COMPONENTS Widgets) target_link_libraries(calc_ui_cpp_module_plugin PRIVATE Qt6::Widgets) ``` > For Option A (QML inside the plugin) you will add `Quick QuickWidgets` and `qt_add_resources` — covered in [Step 7](#step-7-option-a--qml-loaded-from-c). --- ## Step 5: Interface Header (`src/calc_ui_cpp_interface.h`) ```cpp #ifndef CALC_UI_CPP_INTERFACE_H #define CALC_UI_CPP_INTERFACE_H #include "interface.h" class CalcUiCppInterface : public PluginInterface { public: virtual ~CalcUiCppInterface() = default; }; #define CalcUiCppInterface_iid "org.logos.CalcUiCppInterface" Q_DECLARE_INTERFACE(CalcUiCppInterface, CalcUiCppInterface_iid) #endif // CALC_UI_CPP_INTERFACE_H ``` --- ## Step 6: Backend Class The backend class is the key addition over the QML plugin. It holds a `LogosModules*` wrapper — a typed C++ SDK generated at build time from `module.yaml` — and exposes `Q_INVOKABLE` methods that call `calc_module` through it. Because the calls go through a generated typed class, argument types are preserved — no `QString`/`int` coercion issues. ### How the generated SDK works When `module.yaml` declares `dependencies: [calc_module]` and `calc_module` is passed as a flake input via `moduleInputs`, the build system runs `logos-cpp-generator` before compilation. This produces: - `logos_sdk.h` / `logos_sdk.cpp` — the `LogosModules` umbrella class with one typed member per dependency - `calc_module_api.h` / `calc_module_api.cpp` — the per-module wrapper included by `logos_sdk.h` `LogosModules` is constructed with a `LogosAPI*` and provides a member named after each declared dependency (snake_case). All IPC routing happens inside the generated code — your backend just calls methods directly: ```cpp m_logos->calc_module.add(3, 5) // typed: int add(int, int) over IPC ``` This is the same pattern used in production modules such as `logos-storage-ui`. ### `src/calc_backend.h` ```cpp #ifndef CALC_BACKEND_H #define CALC_BACKEND_H #include #include #include "logos_sdk.h" // generated at build time from module.yaml dependencies class LogosAPI; class CalcBackend : public QObject { Q_OBJECT public: explicit CalcBackend(LogosAPI* api, QObject* parent = nullptr); Q_INVOKABLE int add(int a, int b); Q_INVOKABLE int multiply(int a, int b); Q_INVOKABLE int factorial(int n); Q_INVOKABLE int fibonacci(int n); Q_INVOKABLE QString libVersion(); private: LogosModules* m_logos; // generated umbrella wrapper }; #endif // CALC_BACKEND_H ``` ### `src/calc_backend.cpp` ```cpp #include "calc_backend.h" CalcBackend::CalcBackend(LogosAPI* api, QObject* parent) : QObject(parent), m_logos(new LogosModules(api)) {} int CalcBackend::add(int a, int b) { return m_logos->calc_module.add(a, b); } int CalcBackend::multiply(int a, int b) { return m_logos->calc_module.multiply(a, b); } int CalcBackend::factorial(int n) { return m_logos->calc_module.factorial(n); } int CalcBackend::fibonacci(int n) { return m_logos->calc_module.fibonacci(n); } QString CalcBackend::libVersion() { return m_logos->calc_module.libVersion(); } ``` `LogosModules` is constructed once with `LogosAPI*`. Each member (`calc_module`) is a generated proxy that routes calls to the corresponding module process over Qt Remote Objects IPC. No raw `invokeRemoteMethod`, no string method names, no manual `QVariant` unwrapping. --- ## Step 7: Option A — QML Loaded from C++ The plugin loads `src/qml/Main.qml` into a `QQuickWidget` and exposes `CalcBackend` as a QML context property. The QML is identical in structure to `logos-calc-ui/Main.qml` (Part 2), but calls `backend.*` methods directly instead of routing through the `logos.callModule()` IPC bridge — so argument types are preserved and there is no sandboxing overhead. ### 7.1 Add the QML file Create `src/qml/Main.qml`. The structure mirrors `logos-calc-ui/Main.qml` exactly; the only difference is that buttons call `backend.*` methods directly instead of routing through `logos.callModule(...)`: ```qml import QtQuick import QtQuick.Controls import QtQuick.Layouts Item { id: root property string result: "" property string errorText: "" ColumnLayout { anchors.fill: parent anchors.margins: 24 spacing: 16 // ── Title ────────────────────────────────────────────── Text { text: "Logos Calculator (C++ backend)" font.pixelSize: 20 color: "#ffffff" Layout.alignment: Qt.AlignHCenter } // ── Two-operand operations ───────────────────────────── RowLayout { spacing: 12 Layout.fillWidth: true TextField { id: inputA; placeholderText: "a"; Layout.preferredWidth: 80; validator: IntValidator {} } TextField { id: inputB; placeholderText: "b"; Layout.preferredWidth: 80; validator: IntValidator {} } Button { text: "Add" onClicked: root.result = String(backend.add(inputA.text, inputB.text)) } Button { text: "Multiply" onClicked: root.result = String(backend.multiply(inputA.text, inputB.text)) } } // ── Single-operand operations ────────────────────────── RowLayout { spacing: 12 Layout.fillWidth: true TextField { id: inputN; placeholderText: "n"; Layout.preferredWidth: 80; validator: IntValidator { bottom: 0 } } Button { text: "Factorial"; onClicked: root.result = String(backend.factorial(inputN.text)) } Button { text: "Fibonacci"; onClicked: root.result = String(backend.fibonacci(inputN.text)) } Button { text: "libcalc version"; onClicked: root.result = backend.libVersion() } } // ── Result display ───────────────────────────────────── Rectangle { Layout.fillWidth: true height: 56 color: root.errorText.length > 0 ? "#3d1a1a" : "#1a2d1a" radius: 8 Text { anchors.centerIn: parent text: root.errorText.length > 0 ? root.errorText : (root.result.length > 0 ? root.result : "Enter values and press a button") color: root.errorText.length > 0 ? "#f85149" : "#56d364" font.pixelSize: 15 } } Item { Layout.fillHeight: true } } } ``` ### 7.2 Update `CMakeLists.txt` Add `Quick` and `QuickWidgets`, and embed the QML as a Qt resource: ```cmake cmake_minimum_required(VERSION 3.14) project(CalcUiCppPlugin LANGUAGES CXX) if(DEFINED ENV{LOGOS_MODULE_BUILDER_ROOT}) include($ENV{LOGOS_MODULE_BUILDER_ROOT}/cmake/LogosModule.cmake) else() message(FATAL_ERROR "LogosModule.cmake not found. Set LOGOS_MODULE_BUILDER_ROOT.") endif() logos_module( NAME calc_ui_cpp SOURCES src/calc_ui_cpp_interface.h src/calc_ui_cpp_plugin.h src/calc_ui_cpp_plugin.cpp src/calc_backend.h src/calc_backend.cpp ) find_package(Qt6 REQUIRED COMPONENTS Widgets Quick QuickWidgets) target_link_libraries(calc_ui_cpp_module_plugin PRIVATE Qt6::Widgets Qt6::Quick Qt6::QuickWidgets ) qt_add_resources(calc_ui_cpp_module_plugin "qml_resources" PREFIX "/" FILES src/qml/Main.qml ) ``` ### 7.3 `createWidget()` — load QML Replace `calc_ui_cpp_plugin.cpp` with: ```cpp #include "calc_ui_cpp_plugin.h" #include "calc_backend.h" #include "logos_api.h" #include #include #include #include #include CalcUiCppPlugin::CalcUiCppPlugin(QObject* parent) : QObject(parent) {} CalcUiCppPlugin::~CalcUiCppPlugin() {} void CalcUiCppPlugin::initLogos(LogosAPI* api) { m_logosAPI = api; } QWidget* CalcUiCppPlugin::createWidget(LogosAPI* logosAPI) { auto* backend = new CalcBackend(logosAPI); auto* quickWidget = new QQuickWidget(); quickWidget->setResizeMode(QQuickWidget::SizeRootObjectToView); quickWidget->rootContext()->setContextProperty("backend", backend); // Dev mode: set QML_PATH to the directory containing Main.qml to load // from the filesystem without rebuilding. Example: export QML_PATH=$PWD/src/qml QString devSource = qgetenv("QML_PATH"); QUrl qmlUrl = devSource.isEmpty() ? QUrl("qrc:/src/qml/Main.qml") : QUrl::fromLocalFile(QDir(devSource).filePath("Main.qml")); quickWidget->setSource(qmlUrl); if (quickWidget->status() == QQuickWidget::Error) { qWarning() << "CalcUiCppPlugin: failed to load QML"; for (const auto& e : quickWidget->errors()) qWarning() << e.toString(); } return quickWidget; } void CalcUiCppPlugin::destroyWidget(QWidget* widget) { delete widget; } ``` ### 7.4 Dev Mode When `QML_PATH` is set, the plugin loads `Main.qml` from disk instead of the embedded resource. You can edit QML layout, styling, and property bindings without a Nix rebuild — just restart the app to pick up changes. ```bash # Run with dev mode enabled QML_PATH=$PWD/src/qml \ nix run . --override-input calc_module path:../logos-calc-module -- \ --modules-dir ./modules ``` > **What still requires a rebuild:** > - Changes to `.cpp` / `.h` files (backend logic, plugin interface) > - Changes to `CMakeLists.txt` or `module.yaml` > > **What does not require a rebuild:** > - Any `.qml` change — layout, styling, property bindings, JS logic --- ## Step 8: Option B — Pure Qt Widget The plugin creates a standard Qt widget using layouts and connects button clicks to the backend. No QML, no additional Qt modules — just `Qt6::Widgets`. ### `src/calc_ui_cpp_plugin.h` ```cpp #ifndef CALC_UI_CPP_PLUGIN_H #define CALC_UI_CPP_PLUGIN_H #include #include #include #include "calc_ui_cpp_interface.h" class LogosAPI; class CalcUiCppPlugin : public QObject, public CalcUiCppInterface { Q_OBJECT Q_PLUGIN_METADATA(IID CalcUiCppInterface_iid FILE "metadata.json") Q_INTERFACES(CalcUiCppInterface PluginInterface) public: explicit CalcUiCppPlugin(QObject* parent = nullptr); ~CalcUiCppPlugin() override; QString name() const override { return "calc_ui_cpp"; } QString version() const override { return "1.0.0"; } Q_INVOKABLE void initLogos(LogosAPI* api); Q_INVOKABLE QWidget* createWidget(LogosAPI* logosAPI = nullptr); Q_INVOKABLE void destroyWidget(QWidget* widget); signals: void eventResponse(const QString& eventName, const QVariantList& args); private: LogosAPI* m_logosAPI = nullptr; }; #endif // CALC_UI_CPP_PLUGIN_H ``` ### `src/calc_ui_cpp_plugin.cpp` ```cpp #include "calc_ui_cpp_plugin.h" #include "calc_backend.h" #include "logos_api.h" #include #include #include #include #include #include CalcUiCppPlugin::CalcUiCppPlugin(QObject* parent) : QObject(parent) {} CalcUiCppPlugin::~CalcUiCppPlugin() {} void CalcUiCppPlugin::initLogos(LogosAPI* api) { m_logosAPI = api; } QWidget* CalcUiCppPlugin::createWidget(LogosAPI* logosAPI) { auto* backend = new CalcBackend(logosAPI); auto* widget = new QWidget(); auto* layout = new QVBoxLayout(widget); layout->setContentsMargins(24, 24, 24, 24); layout->setSpacing(16); // ── Title ────────────────────────────────────────────────── auto* title = new QLabel("Logos Calculator (C++)"); title->setAlignment(Qt::AlignHCenter); layout->addWidget(title); // ── Two-operand row ──────────────────────────────────────── auto* twoOpRow = new QHBoxLayout(); auto* inputA = new QLineEdit(); inputA->setPlaceholderText("a"); inputA->setMaximumWidth(80); auto* inputB = new QLineEdit(); inputB->setPlaceholderText("b"); inputB->setMaximumWidth(80); auto* addBtn = new QPushButton("Add"); auto* mulBtn = new QPushButton("Multiply"); twoOpRow->addWidget(inputA); twoOpRow->addWidget(inputB); twoOpRow->addWidget(addBtn); twoOpRow->addWidget(mulBtn); twoOpRow->addStretch(); layout->addLayout(twoOpRow); // ── Single-operand row ───────────────────────────────────── auto* oneOpRow = new QHBoxLayout(); auto* inputN = new QLineEdit(); inputN->setPlaceholderText("n"); inputN->setMaximumWidth(80); auto* facBtn = new QPushButton("Factorial"); auto* fibBtn = new QPushButton("Fibonacci"); auto* verBtn = new QPushButton("libcalc version"); oneOpRow->addWidget(inputN); oneOpRow->addWidget(facBtn); oneOpRow->addWidget(fibBtn); oneOpRow->addWidget(verBtn); oneOpRow->addStretch(); layout->addLayout(oneOpRow); // ── Result display ───────────────────────────────────────── auto* resultLabel = new QLabel("Enter values and press a button"); resultLabel->setAlignment(Qt::AlignHCenter); layout->addWidget(resultLabel); layout->addStretch(); // ── Wire up buttons ──────────────────────────────────────── auto show = [resultLabel](const QString& v) { resultLabel->setText(v); }; QObject::connect(addBtn, &QPushButton::clicked, [=] { show(QString::number(backend->add(inputA->text().toInt(), inputB->text().toInt()))); }); QObject::connect(mulBtn, &QPushButton::clicked, [=] { show(QString::number(backend->multiply(inputA->text().toInt(), inputB->text().toInt()))); }); QObject::connect(facBtn, &QPushButton::clicked, [=] { show(QString::number(backend->factorial(inputN->text().toInt()))); }); QObject::connect(fibBtn, &QPushButton::clicked, [=] { show(QString::number(backend->fibonacci(inputN->text().toInt()))); }); QObject::connect(verBtn, &QPushButton::clicked, [=] { show(backend->libVersion()); }); return widget; } void CalcUiCppPlugin::destroyWidget(QWidget* widget) { delete widget; } ``` --- ## Step 9: `flake.nix` Pass `standaloneApp` to `mkLogosModule` and you get `apps.default` (i.e. `nix run`) for free — no manual `apps` block required. **Important — `moduleInputs`:** Because `module.yaml` declares `dependencies: [calc_module]`, the build system runs `logos-cpp-generator` before compiling your C++ sources. The generator introspects `calc_module`'s built plugin to produce `logos_sdk.h` / `logos_sdk.cpp` (and per-module `calc_module_api.h` / `calc_module_api.cpp`). These are the files your backend includes as `#include "logos_sdk.h"`. For this to work, `calc_module` must be available as a built Nix package at code-generation time — that is what `moduleInputs` provides. Without it, the build fails with `'logos_sdk.h' file not found`. ```nix { description = "Calculator C++ UI plugin for Logos - widget frontend for calc_module"; inputs = { logos-module-builder.url = "github:logos-co/logos-module-builder"; logos-standalone-app.url = "github:logos-co/logos-standalone-app"; calc_module.url = "github:logos-co/logos-tutorial?dir=logos-calc-module"; }; outputs = { logos-module-builder, logos-standalone-app, calc_module, ... }: logos-module-builder.lib.mkLogosModule { src = ./.; configFile = ./module.yaml; moduleInputs = { inherit calc_module; }; logosStandalone = logos-standalone-app; iconFiles = [ ./icons/calc.png ]; }; } ``` `standaloneApp` tells `mkLogosModule` to wire up `apps.default` automatically. It stages the compiled plugin alongside `metadata.json` and any icon files into a Nix store directory, then produces a shell script that calls `logos-standalone-app` with that directory — exactly what `nix run` executes. --- ## Step 10: Build and Test ### 10.1 Build ```bash git add -A nix build --override-input calc_module path:../logos-calc-module ``` Inspect the output: ```bash lm ./result/lib/calc_ui_cpp_plugin.dylib ``` You should see `createWidget` and `destroyWidget` in the methods list. ### 10.2 UI only (layout preview) ```bash nix run . --override-input calc_module path:../logos-calc-module ``` The widget opens. No backend connected yet, so button clicks will silently return 0 (CalcBackend logs a warning when `calc_module` is not connected). > **Why `--override-input`?** `calc_module.url` in `flake.nix` points to the published GitHub URL. For local development, `--override-input` redirects it to the local sibling directory. This is the same mechanism `ws build --local` / `ws build --auto-local` uses throughout the workspace. ### 10.3 Full functionality (with modules) ```bash nix build 'github:logos-co/logos-package-manager-module#cli' --out-link ./pm mkdir -p modules # Install capability_module nix bundle --bundler 'github:logos-co/nix-bundle-lgx' \ 'github:logos-co/logos-capability-module' -o lgx-capability ./pm/bin/lgpm --modules-dir ./modules install --file lgx-capability/*.lgx # Bundle and install calc_module (from Part 1) cd ../logos-calc-module nix bundle --bundler 'github:logos-co/nix-bundle-lgx' '.#lib' -o lgx-result cd ../logos-calc-ui-cpp ./pm/bin/lgpm --modules-dir ./modules install --file ../logos-calc-module/lgx-result/*.lgx nix run . --override-input calc_module path:../logos-calc-module -- --modules-dir ./modules ``` --- ## Step 11: Load in `logos-basecamp` ### 11.1 Create LGX packages ```bash # Package calc_module (from Part 1) cd ../logos-calc-module nix bundle --bundler 'github:logos-co/nix-bundle-lgx#portable' '.#lib' -o lgx-calc-module cd ../logos-calc-ui-cpp # Package the C++ UI plugin nix bundle --bundler 'github:logos-co/nix-bundle-lgx#portable' '.' -o lgx-calc-ui-cpp ``` ### 11.2 Install via logos-basecamp UI 1. Open `logos-basecamp` 2. Go to **Package Manager** 3. Click **Install from file** 4. Select `lgx-calc-module/*.lgx` — installs `calc_module` 5. Repeat for `lgx-calc-ui-cpp/*.lgx` — installs `calc_ui_cpp` The "Calculator" tab appears in the sidebar. ### 11.3 Install via CLI (alternative) ```bash nix build 'github:logos-co/logos-package-manager-module#cli' --out-link ./pm ./pm/bin/lgpm install --file lgx-calc-module/*.lgx ./pm/bin/lgpm install --file lgx-calc-ui-cpp/*.lgx ``` ### 11.4 Build logos-basecamp from source Build a local `logos-basecamp` binary, then use `lgpm` to populate a modules directory and run it: ```bash # Build logos-basecamp nix build 'github:logos-co/logos-basecamp' -o basecamp-result # Create module directories mkdir -p modules ui-plugins # Build lgpm CLI nix build 'github:logos-co/logos-package-manager-module#cli' --out-link ./pm # Install capability_module (required by all UI plugins) nix bundle --bundler 'github:logos-co/nix-bundle-lgx' \ 'github:logos-co/logos-capability-module' -o lgx-capability ./pm/bin/lgpm --modules-dir ./modules install --file lgx-capability/*.lgx # Bundle and install calc_module (local, not portable) cd ../logos-calc-module nix bundle --bundler 'github:logos-co/nix-bundle-lgx' '.#lib' -o lgx-calc-module-local cd ../logos-calc-ui-cpp ./pm/bin/lgpm --modules-dir ./modules install --file ../logos-calc-module/lgx-calc-module-local/*.lgx # Bundle and install the C++ UI plugin nix bundle --bundler 'github:logos-co/nix-bundle-lgx' '.' -o lgx-calc-ui-cpp-local ./pm/bin/lgpm --modules-dir ./ui-plugins install --file lgx-calc-ui-cpp-local/*.lgx # Run basecamp with the populated directories ./basecamp-result/bin/logos-basecamp \ --modules-dir ./modules \ --ui-plugins-dir ./ui-plugins ``` > **Local vs portable:** A locally-built `logos-basecamp` (via `nix build`) expects **local** `.lgx` packages (built without `#portable`). Portable builds (AppImage, macOS app bundle) expect **portable** `.lgx` packages. --- ## Recap: Three Module Types | | Core (Part 1) | QML UI (Part 2) | C++ UI (Part 3) | |---|---|---|---| | Language | C++ | QML / JS | C++ (+ optional QML) | | Compilation | Yes | No | Yes | | Backend calls | Exposed via `Q_INVOKABLE` | `logos.callModule()` IPC | `LogosAPI*` → `invokeRemoteMethod()` | | Type safety | Strong | Weak (QVariant/QString) | Strong | | Sandboxed | No | Yes | No | | QML support | — | Native | Via `QQuickWidget` | | Template | `#default` | `#ui-qml-module` | `#ui-module` | ## What's Next - **Generated type-safe wrappers** — instead of raw `invokeRemoteMethod`, use `logos-cpp-generator` to generate a typed `CalcModuleClient` class. See [Developer Guide](logos-developer-guide.md) Section 6.2 - **Events** — core modules emit `eventResponse` signals; connect to them from your backend class via `LogosAPIClient` - **Use the Logos Design System** in Option B QML — `import Logos.Theme` and `import Logos.Controls` are available when running inside `logos-basecamp`