GRIDNET OS UI dApp Design Guidelines — The Complete Developer Reference

GRIDNET OS UI dApp Design Guidelines — The Complete Developer Reference

Everything a third-party developer needs to build production-grade decentralized applications for the world’s first decentralized operating system.

The most important thing we can tell you upfront: If you know HTML, CSS, and JavaScript, you already know how to build UI dApps for GRIDNET OS. There is no new language to learn. No proprietary framework to master. No paradigm shift to survive. You write standard web code — the same <div>s, the same flexbox, the same ES6 classes you’ve been writing for years — and it runs inside a decentralized operating system. The blockchain is underneath, but you interact with it through CVMContext: a clean, familiar JavaScript API. You call methods. You get callbacks. You await promises. That’s it.

Even when you venture deeper — into GridScript, GRIDNET OS’s native shell — you’ll find yourself on familiar ground. GridScript commands mirror the Linux/DOS commands you already know: cd, ls, cat, rm, mkdir, touch, chown, setfacl, getfacl. The send command transfers tokens. BT/CT (Begin Transaction / Commit Transaction) map directly to BEGIN/COMMIT from SQL — same concept, blockchain-backed. It’s a familiar shell with decentralized superpowers underneath.

This guide isn’t about learning something alien. It’s about learning the specific rules that make your existing web development skills work inside a multi-window, Shadow DOM-isolated, decentralized environment. The rules are few, well-motivated, and — once understood — make your dApps automatically responsive, secure, and multi-instance-safe. The payoff is enormous.

GRIDNET OS dApp Architecture
The architecture of a decentralized windowed operating system — where every application is an island of sovereign computation.

I. The Paradigm: Why GRIDNET OS dApps Are Different (And Why That’s Easy)

GRIDNET OS dApp development is web development. You use HTML for structure, CSS for styling, JavaScript for logic. The browser is the runtime. But there is one key difference you must internalise: your application does not own the viewport. Your application lives inside a window — one of potentially dozens of simultaneously running decentralized applications, each isolated from the others by Shadow DOM encapsulation, each communicating with a decentralized blockchain backend through a single, clean JavaScript gateway.

This is not a limitation. This is a superpower.

Shadow DOM isolation means your dApp cannot be tampered with by other applications. It means you can run ten instances of the same application simultaneously without a single variable collision. It means your CSS cannot leak into — or be corrupted by — anything else running on the system. In a decentralized operating system where untrusted code from unknown developers runs side by side, this isolation is not merely convenient — it is essential for security.

The viewport-independence (no vw/vh units) means your application automatically responds to its window being resized, maximised, minimised, or snapped — just like a native desktop application. You design for a container, not a screen, and the system handles the rest.

This guide covers every rule, every pattern, and every technique you need to build a production dApp for GRIDNET OS. If you can write HTML, CSS, and JavaScript, you can build for GRIDNET OS. But you must understand the rules — and more importantly, why they exist.

Prerequisites: Familiarity with JavaScript ES6 (classes, modules, arrow functions), HTML5, and CSS3. For a gentler introduction, see the Hello World UI dApp Tutorial.

II. Architecture: The Three-Layer Stack

Three-layer architecture diagram
The three-layer architecture: your dApp, CVMContext, and the decentralized GRIDNET OS Core.

Every GRIDNET OS UI dApp operates within a three-layer architecture:

GRIDNET OS Three-Layer Architecture Diagram
The three-layer architecture: Your UI dApp communicates through CVMContext to the GRIDNET OS Core — clean separation of concerns.

Layer 1: Your UI dApp (CWindow Instance)

Your application is a JavaScript ES6 class that extends CWindow from /lib/window.js. The CWindow base class provides window management (dragging, resizing, minimising, maximising), Shadow DOM encapsulation, DOM access methods, curtain/loading UI, dialog systems, and lifecycle hooks. Your dApp inherits all of this automatically.

Layer 2: CVMContext (The Gateway)

The CVMContext singleton (/lib/VMContext.js) is the only legitimate interface between your dApp and the GRIDNET OS Core. It provides APIs for blockchain queries, Decentralized File System (DFS) operations, network messaging, thread management, settings persistence, and event subscription. Access it via CVMContext.getInstance(). For comprehensive CVMContext documentation, see the CVMContext reference.

Layer 3: GRIDNET OS Core

The C++ full-node software that manages the blockchain, consensus, networking, and decentralized file storage. Your dApp never communicates with it directly — CVMContext handles all communication over WebSocket/WebRTC. For blockchain data exploration APIs, see the Explorer API documentation.

III. Getting Started: The Official Template

Template structure blueprint
The official AppTemplate.js — your blueprint for every GRIDNET OS dApp.

The fastest path to a working dApp is the official template at WebUI/dApps/AppTemplate.js. Here is the customisation checklist:

  1. Copy and rename AppTemplate.js to your dApp name (e.g., MyCoolDApp.js)
  2. Rename the class from CUIAppTemplate to your class name (e.g., CMyCoolDApp)
  3. Set the window title in the super() call
  4. Update the default export at the bottom of the file
  5. Set your icon in static getIcon() as a Base64-encoded data:image/png;base64,… string
  6. Define your HTML body in the body variable
  7. Set a unique package ID in static getPackageID() — must use reverse-domain notation starting with org.gridnetproject.UIdApps.
  8. Optionally set a category via static getDefaultCategory() — options include 'dApps', 'explore', 'productivity'
  9. Optionally register file handlers via static getFileHandlers()

Minimal Complete Example

Here is the absolute minimum viable dApp — a complete, working application:

"use strict"

import { CWindow } from "/lib/window.js"

const myBody = `
<link rel="stylesheet" href="/css/windowDefault.css" />
<style>
.container {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 100%;
    width: 100%;
    font-family: 'Rajdhani', sans-serif;
    color: #22fafc;
    background: #0a0a14;
}
.greeting {
    font-size: 2rem;
    text-shadow: 0 0 10px rgba(34, 250, 252, 0.5);
}
</style>
<div class="container">
    <div class="greeting" id="msg">Hello, GRIDNET OS!</div>
</div>
`;

class CMyFirstDApp extends CWindow {
    constructor(positionX, positionY, width, height, data, dataType, filePath, thread) {
        super(positionX, positionY, width, height, myBody, "My First dApp",
              CMyFirstDApp.getIcon(), true);
    }

    static getPackageID() {
        return "org.gridnetproject.UIdApps.myFirstDApp";
    }

    static getDefaultCategory() {
        return 'dApps';
    }

    static getIcon() {
        return ''; // data:image/png;base64,...
    }

    open() {
        super.open();
        const msg = this.getControl('msg');
        msg.textContent = 'Welcome to the Decentralized Future!';
    }

    closeWindow() {
        super.closeWindow();
    }
}

export default CMyFirstDApp;

IV. JavaScript Rules: The Ten Commandments

JavaScript rules enforcement
The JavaScript rules — forged in the fires of multi-instance isolation and decentralized security.

These rules are mandatory. Violating them will cause subtle, hard-to-debug failures when multiple dApp instances run simultaneously, or when your dApp interacts with others on the system.

Rule 1: No Global Scope Pollution

The rule: DO NOT define variables, functions, or objects in the global (window) scope.

Why it exists: GRIDNET OS runs multiple dApp instances in the same browser tab. Global variables from one instance will collide with globals from another instance — or worse, from a completely different dApp. A global let count = 0 in your code becomes a shared mutable variable across every running application.

❌ BAD — Global state that will collide:

let balance = 0;  // GLOBAL — shared across ALL instances!
let isConnected = false;

function updateBalance(val) {  // GLOBAL function!
    balance = val;
}

class CMyDApp extends CWindow {
    constructor(...args) {
        super(...args);
        updateBalance(100); // Modifies shared global state
    }
}

✅ GOOD — All state encapsulated in the class:

"use strict"

import { CWindow } from "/lib/window.js"

class CMyDApp extends CWindow {
    constructor(positionX, positionY, width, height, data, dataType, filePath, thread) {
        super(positionX, positionY, width, height, body, "My dApp", CMyDApp.getIcon(), true);
        this.mBalance = 0;        // Instance-scoped
        this.mIsConnected = false; // Instance-scoped
    }

    updateBalance(val) {  // Instance method
        this.mBalance = val;
    }
}

Production example — from the Wallet dApp constructor:

class CUIWallet extends CWindow {
    constructor(positionX, positionY, width, height, data, dataType, filePath, thread) {
        super(positionX, positionY, width, height, windowBodyHTML, "Wallet",
              CUIWallet.getIcon(), true, data, dataType, filePath, thread);
        // ALL state is instance-scoped via this.*
        this.mBalance = 0;
        this.mElements = {};
        this.mTxHistoryTable = null;
        this.mRecentTxTable = null;
        this.mAutoLockTimer = null;
        // ... dozens more instance properties
    }
}

Rule 2: No Direct DOM Access — Use Shadow DOM Methods

The rule: DO NOT use document.getElementById(), document.querySelector(), or document.querySelectorAll().

Why it exists: Your dApp’s elements live inside a Shadow DOM tree. The document.* methods search the main document — the GRIDNET OS shell — not your dApp’s shadow root. They will never find your elements, and if they find anything, it will be a system element you should not be touching.

❌ BAD — Searches the main document, finds nothing:

open() {
    super.open();
    const btn = document.getElementById('send-btn');  // Returns null!
    const labels = document.querySelectorAll('.label'); // Returns OS shell elements!
    btn.addEventListener('click', () => {}); // TypeError: null
}

✅ GOOD — Uses CWindow’s scoped access methods:

open() {
    super.open();
    // getControl(id) — find element by ID within Shadow DOM
    const btn = this.getControl('send-btn');

    // getBody.querySelector — CSS selector within Shadow DOM
    const firstLabel = this.getBody.querySelector('.label');

    // getBody.querySelectorAll — all matching elements within Shadow DOM
    const allLabels = this.getBody.querySelectorAll('.label');

    btn.addEventListener('click', () => this.handleSend());
}

Production example — from the Wallet dApp’s dashboard initialisation (showing extensive use of getControl):

// Wallet.js — caching element references for performance
this.mElements.availableBalance = this.getControl('available-balance-value');
this.mElements.lockedBalance = this.getControl('locked-balance-value');
this.mElements.totalBalance = this.getControl('total-balance-value');
this.mElements.currentAddress = this.getControl('current-address-value');
this.mElements.activeKeychainName = this.getControl('active-keychain-name');

// Event listeners using getControl
this.getControl('refresh-balance-btn').addEventListener('click', () => this.retrieveBalance(true));
this.getControl('send-action-btn').addEventListener('click', () => this.changeTab('send'));
this.getControl('receive-action-btn').addEventListener('click', () => this.changeTab('receive'));
this.getControl('copy-address-btn').addEventListener('click', () => this.copyAddressToClipboard());

Production example — using getBody for querySelector operations:

// Creating and appending elements to Shadow DOM
const notification = /* create element */;
this.getBody.appendChild(notification);

// Querying within shadow root
const existingModal = this.getBody.querySelector('.tt-result-modal');
const typeOptions = this.getBody.querySelectorAll('.type-option');
const shadowRoot = this.getBody.getRootNode();

Rule 3: Single Module Deployment

The rule: DO NOT rely on multi-file JavaScript deployments. Your dApp must be a single .app file.

Why it exists: The GRIDNET OS PackageManager expects a single file. The .app extension is simply a renamed .js file containing your bundled module.

❌ BAD — Multiple files that won’t deploy:

// utils.js (separate file — won't be available at runtime!)
export function formatBalance(val) { return val.toFixed(2); }

// myDApp.js
import { formatBalance } from './utils.js'; // FAILS at runtime

✅ GOOD — Everything in one file, or use a bundler:

// Either inline everything:
function formatBalance(val) { return val.toFixed(2); }

class CMyDApp extends CWindow { /* ... */ }

// Or use Rollup/Webpack/Parcel to bundle before deployment.
// System libraries (/lib/*) are imported normally — they're provided by the OS.
import { CWindow } from "/lib/window.js"      // OK — system-provided
import { CTools } from "/lib/tools.js"          // OK — system-provided

Important distinction: System libraries (anything under /lib/) and pre-loaded third-party libraries (Tabulator.js, Plotly.js, etc.) are provided by the OS environment. Import them freely. Only your own code and non-system dependencies must be bundled.

Rule 4: Use WeakMap for Private Instance Data

The rule: Use WeakMap to store truly private instance data when needed.

Why it exists: JavaScript class properties prefixed with this. are technically accessible from outside the class. WeakMap provides true encapsulation with the added benefit of automatic garbage collection when the instance is destroyed — preventing memory leaks in a long-running OS environment.

❌ BAD — Pseudo-private with underscore convention:

class CMyDApp extends CWindow {
    constructor(...args) {
        super(...args);
        this._privateKey = 'secret123'; // Accessible as instance._privateKey
    }
}

✅ GOOD — Truly private via WeakMap:

const _private = new WeakMap();

class CMyDApp extends CWindow {
    constructor(positionX, positionY, width, height, data, dataType, filePath, thread) {
        super(positionX, positionY, width, height, body, "My dApp", CMyDApp.getIcon(), true);
        _private.set(this, {
            privateKey: null,
            encryptionState: null,
            sensitiveData: new Map()
        });
    }

    setPrivateKey(key) {
        _private.get(this).privateKey = key;
    }

    getPrivateKey() {
        return _private.get(this).privateKey;
    }
}
// _private is module-scoped, not global — only accessible within this file.

Rule 5: Strict Mode

The rule: Always use "use strict"; at the beginning of your module.

Why it exists: Strict mode catches common coding mistakes (silent errors become thrown errors), prevents accidental globals, and enables JavaScript engine optimisations. In a multi-instance environment, accidental globals are catastrophic.

"use strict"  // FIRST LINE of your file

import { CWindow } from "/lib/window.js"
// ... rest of your code

Both the Wallet and Terminal dApps begin with "use strict".

Rule 6: All OS Interaction Through CVMContext

The rule: Use CVMContext.getInstance() for ALL interactions with GRIDNET OS features — blockchain, DFS, network, threading, settings.

Why it exists: CVMContext is the designated, secure, managed gateway. It handles connection management, request routing, event dispatch, and thread synchronisation. Bypassing it would create unmanaged connections, resource leaks, and security vulnerabilities.

❌ BAD — Direct WebSocket connection:

// NEVER do this
const ws = new WebSocket('wss://node.gridnet.org:8080');
ws.onmessage = (e) => { /* ... */ };

✅ GOOD — All communication through CVMContext:

const vmContext = CVMContext.getInstance();

// Register for blockchain events
vmContext.addVMMetaDataListener(this.newVMMetaDataCallback.bind(this), this.mID);
vmContext.addNewDFSMsgListener(this.newDFSMsgCallback.bind(this), this.mID);
vmContext.addNewGridScriptResultListener(this.newGridScriptResultCallback.bind(this), this.mID);

// Request data asynchronously
const reqID = vmContext.genRequestID(); // Generate unique request ID
this.addNetworkRequestID(reqID);  // Track request ownership

// Async pattern
async initialize() {
    try {
        const status = await vmContext.getBlockchainStatusA(
            this.getSystemThreadID(), this, eVMMetaCodeExecutionMode.RAW
        );
        this.updateUI(status.data);
    } catch (error) {
        this.handleError(error);
    }
}

Rule 7: Do Not Bundle System Libraries

The rule: DO NOT include GRIDNET OS system libraries or OS-provided third-party libraries in your bundle.

Why it exists: System libraries are loaded and managed by the OS. Bundling your own copy would create version conflicts, waste memory, and potentially break when the OS updates.

// ✅ GOOD — import system libraries directly
import { CWindow } from "/lib/window.js"
import { CVMMetaSection, CVMMetaEntry, CVMMetaGenerator, CVMMetaParser } from '/lib/MetaData.js'
import { CNetMsg } from '/lib/NetMsg.js'
import { CTools, CDataConcatenator } from '/lib/tools.js'
import { CAppSettings, CSettingsManager } from "/lib/SettingsManager.js"
import { CContentHandler } from "/lib/AppSelector.js"
import { CBlockDesc } from '/lib/BlockDesc.js'
import { CTransaction } from '/lib/Transaction.js'
import { GridScriptCompiler } from '/lib/GridScriptCompiler.js'

// Libraries like Tabulator.js, Plotly.js are pre-loaded by the OS
// Reference them directly if they're available in the environment

Rule 8: Bind Event Listener Context

The rule: Always bind this context when using class methods as callbacks.

Why it exists: When a method is passed as a callback, this loses its class context and becomes the event target (or undefined in strict mode). Without binding, your callback cannot access instance properties or methods.

❌ BAD — Lost context:

constructor(...args) {
    super(...args);
    // this.handleClick will have wrong 'this' when called
    CVMContext.getInstance().addVMMetaDataListener(this.newVMMetaDataCallback, this.mID);
}

✅ GOOD — Bound context:

constructor(...args) {
    super(...args);
    // .bind(this) preserves class context
    CVMContext.getInstance().addVMMetaDataListener(this.newVMMetaDataCallback.bind(this), this.mID);
    CVMContext.getInstance().addNewDFSMsgListener(this.newDFSMsgCallback.bind(this), this.mID);
    CVMContext.getInstance().addNewGridScriptResultListener(
        this.newGridScriptResultCallback.bind(this), this.mID
    );
}

// For UI event listeners, arrow functions also work:
open() {
    super.open();
    this.getControl('my-btn').addEventListener('click', (e) => this.handleClick(e));
    // OR
    this.getControl('my-btn').addEventListener('click', this.handleClick.bind(this));
}

Production example — the Terminal dApp constructor registers five listeners, all with .bind(this):

// Terminal.js constructor
CVMContext.getInstance().addVMMetaDataListener(this.newVMMetaDataCallback.bind(this), this.mID);
CVMContext.getInstance().addNewDFSMsgListener(this.newDFSMsgCallback.bind(this), this.mID);
CVMContext.getInstance().addNewGridScriptResultListener(
    this.newGridScriptResultCallback.bind(this), this.mID
);
CVMContext.getInstance().addNewTerminalDataListener(
    this.newTerminalDataCallback.bind(this), this.mID
);
CVMContext.getInstance().addVMStateChangedListener(
    this.VMStateChangedCallback.bind(this), this.mID
);

Rule 9: Track Network Request Ownership

The rule: Register network request IDs and verify ownership in callbacks.

Why it exists: Multiple dApp instances share CVMContext event channels. Without checking request IDs, your callback would process data meant for another instance.

// Requesting data
const reqID = vmContext.genRequestID(); // Generate unique request ID
this.addNetworkRequestID(reqID);

// In callback — verify this response is yours
newDFSMsgCallback(dfsMsg) {
    if (!this.hasNetworkRequestID(dfsMsg.getReqID))
        return; // Not our request — ignore

    // Process the data
    if (dfsMsg.getData1.byteLength > 0) {
        let metaData = this.mMetaParser.parse(dfsMsg.getData1);
        // ...
    }
}

Rule 10: Clean Up in closeWindow()

The rule: Always override closeWindow() to stop threads, clear timers, unregister listeners, and destroy resources. Always call super.closeWindow() at the end.

Why it exists: Without cleanup, threads keep running, timers keep firing, event listeners keep receiving data — all for a window that no longer exists. This causes memory leaks, phantom processing, and eventual browser tab crashes.

Production example — the Wallet dApp’s comprehensive cleanup:

closeWindow() {
    // Stop auto-lock timer
    if (this.mAutoLockTimer) {
        clearInterval(this.mAutoLockTimer);
        this.mAutoLockTimer = null;
    }

    // Stop transaction tracking timer
    if (this.mTxTrackingInterval) {
        clearInterval(this.mTxTrackingInterval);
        this.mTxTrackingInterval = null;
    }

    // Unregister all event listeners
    this.mVMContext.unregisterEventListenerByID(this.mID);

    // Clean up tables
    if (this.mTxHistoryTable) {
        this.mTxHistoryTable.destroy();
    }
    if (this.mRecentTxTable) {
        this.mRecentTxTable.destroy();
    }
    if (this.mOutgressPoolsTable) {
        this.mOutgressPoolsTable.destroy();
    }
    if (this.mIngressPoolsTable) {
        this.mIngressPoolsTable.destroy();
    }

    // ALWAYS call parent close last
    super.closeWindow();
}

V. CSS Rules: Designing for a Window, Not a Viewport

CSS responsive design in windows
CSS in GRIDNET OS — designing fluid layouts that respond to windows, not viewports.

The CSS rules flow from one fundamental truth: your dApp lives in a resizable window, not a full-screen browser tab. Every rule below serves this reality.

Rule 1: No Viewport Units (vw/vh)

The rule: DO NOT use vw or vh units anywhere in your CSS.

Why it exists: vw and vh are relative to the browser viewport, not your CWindow container. A width: 50vw element will be half the browser window — which could be vastly larger or smaller than your dApp’s window. When the user resizes the dApp window, vw/vh values don’t change, breaking your layout.

The strength: By using %, flex, and grid instead, your layout automatically responds to window resize, maximise, minimise, and snap — no media queries needed for basic responsiveness.

❌ BAD — Viewport-relative sizing:

.sidebar {
    width: 25vw;     /* Relative to browser, not dApp window! */
    height: 100vh;   /* Will overflow or underflow the window! */
}

.modal {
    max-width: 80vw; /* Meaningless inside a 400px window */
    font-size: 3vw;  /* Text size changes with browser zoom, not window size */
}

✅ GOOD — Container-relative sizing:

.sidebar {
    width: 25%;      /* 25% of the dApp's container */
    height: 100%;    /* Full height of the container */
}

.modal {
    max-width: 90%;  /* Relative to the window content area */
    font-size: 1.2rem; /* Consistent, readable size */
}

Rule 2: No position: fixed or position: sticky

The rule: DO NOT use position: fixed or position: sticky in ways that depend on the browser window.

Why it exists: position: fixed positions relative to the browser viewport, not your dApp’s window. A “fixed” header would float over other dApp windows or the OS shell. position: sticky can behave unpredictably within Shadow DOM scroll containers.

❌ BAD — Escapes the window:

.header {
    position: fixed;  /* Positions relative to browser viewport! */
    top: 0;
    left: 0;
    width: 100%;
    z-index: 9999;    /* Floats above EVERYTHING, including other windows */
}

✅ GOOD — Contained within the window:

.wallet-container {
    position: absolute; /* OK — relative to parent within Shadow DOM */
    top: 0;
    left: 0;
    height: 100%;
    width: 100%;
    display: flex;
    flex-direction: column;
}

.wallet-header {
    /* No position: fixed needed — flex layout keeps it at top */
    display: flex;
    justify-content: space-between;
    align-items: center;
    height: 60px;
    flex-shrink: 0; /* Don't compress the header */
}

.wallet-content {
    flex: 1;         /* Takes remaining space */
    overflow-y: auto; /* Scrollable content area */
}

Production example — the Wallet dApp uses exactly this pattern:

.wallet-container {
    height: 100%;
    width: 100%;
    position: absolute;
    top: 0;
    left: 0;
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

Rule 3: Use Flexbox and Grid for Layouts

The rule: Use display: flex and display: grid with relative units (%, fr, em, rem) for all layouts.

Why it exists: Flex and grid layouts automatically adapt to their container size. Combined with the ban on viewport units, this gives you inherently responsive layouts that work at any window size.

/* Three-column layout that adapts to window width */
.dashboard-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 1rem;
    padding: 1rem;
}

/* Vertical stack with fixed header/footer, flexible content */
.app-layout {
    display: flex;
    flex-direction: column;
    height: 100%;
}

.app-header { flex-shrink: 0; }
.app-content { flex: 1; overflow-y: auto; }
.app-footer { flex-shrink: 0; }

/* Horizontal toolbar with even spacing */
.toolbar {
    display: flex;
    gap: 0.5rem;
    align-items: center;
    flex-wrap: wrap; /* Wraps on narrow windows */
}

Rule 4: No Pixel-Perfect Major Layouts

The rule: DO NOT use absolute pixel positioning for major layout elements.

Why it exists: Pixel values don’t scale with window resize. Reserve pixels only for fine details: icon padding, border widths, small gaps.

❌ BAD:

.content {
    width: 800px;   /* What if the window is 600px wide? */
    margin-left: 200px; /* What if sidebar is different size? */
}

✅ GOOD:

.content {
    flex: 1;        /* Takes remaining space after sidebar */
    min-width: 0;   /* Prevents flex overflow */
}

.sidebar {
    width: 25%;
    min-width: 150px;
    max-width: 300px;
}

Rule 5: Avoid !important

The rule: DO NOT use !important excessively.

Why it exists: !important breaks the CSS cascade and makes styles unmaintainable. Use more specific selectors instead. The one acceptable use is overriding third-party library styles (like Tabulator.js) that require specificity battles.

❌ BAD:

.button {
    color: blue !important;
    background: red !important;
    padding: 10px !important;
}

✅ GOOD:

/* More specific selector instead of !important */
.wallet-container .action-panel .button {
    color: blue;
    background: red;
    padding: 10px;
}

/* Acceptable: overriding third-party library */
.tabulator-col-title {
    color: #22fafc !important;  /* Tabulator needs !important for theme overrides */
}

Rule 6: All Styles Scoped to Shadow DOM

The rule: Define ALL styles within your dApp’s HTML/CSS payload. Do not rely on external stylesheets not loaded inside your Shadow DOM.

Why it exists: Shadow DOM provides style isolation — styles from outside cannot leak in, and your styles cannot leak out. This is a security feature. But it means you must include everything you need.

The strength: You can use any class names without worrying about collisions. .button, .container, .header — these are yours and yours alone.

const myBody = `
<!-- System stylesheet — provided by OS -->
<link rel="stylesheet" href="/css/windowDefault.css" />

<!-- YOUR styles — scoped to this Shadow DOM -->
<style>
/* No collision risk — .header is ONLY yours */
.header {
    background: linear-gradient(90deg, #090918, #141432);
    border-bottom: 1px solid #22fafc;
}

/* Class name 'button' won't affect any other dApp */
.button {
    background: linear-gradient(90deg, #22fafc, #00b4ff);
    color: #0a0a14;
}
</style>

<div class="header">...</div>
`;

Rule 7: No External CSS Variables

The rule: DO NOT rely on CSS variables (--my-color) defined outside your Shadow DOM. Define all variables within your own scope.

Why it exists: CSS custom properties defined in the main document do not penetrate Shadow DOM boundaries (unless explicitly inherited). Never assume external variables exist.

/* ✅ Define your own variables */
<style>
:host {
    --primary: #22fafc;
    --bg-dark: #0a0a14;
    --text: #e0e0ff;
}

.panel {
    color: var(--text);
    background: var(--bg-dark);
    border-color: var(--primary);
}
</style>

VI. CVMContext Interaction Patterns

CVMContext event flow
CVMContext — the single point of truth between your dApp and the decentralized world.

All communication between your dApp and GRIDNET OS flows through the CVMContext singleton. Here are the essential interaction patterns:

Event Listener Registration

Register in the constructor, unregister in closeWindow():

constructor(...args) {
    super(...args);
    const vm = CVMContext.getInstance();

    // Blockchain metadata responses
    vm.addVMMetaDataListener(this.newVMMetaDataCallback.bind(this), this.mID);

    // Decentralized File System responses
    vm.addNewDFSMsgListener(this.newDFSMsgCallback.bind(this), this.mID);

    // GridScript execution results
    vm.addNewGridScriptResultListener(this.newGridScriptResultCallback.bind(this), this.mID);

    // VM state changes (connection state, etc.)
    vm.addVMStateChangedListener(this.VMStateChangedCallback.bind(this), this.mID);

    // Terminal data (for terminal-type dApps)
    vm.addNewTerminalDataListener(this.newTerminalDataCallback.bind(this), this.mID);
}

closeWindow() {
    CVMContext.getInstance().unregisterEventListenerByID(this.mID);
    super.closeWindow();
}

Async Data Retrieval

async loadBlockchainStatus() {
    try {
        const vmContext = CVMContext.getInstance();
        const status = await vmContext.getBlockchainStatusA(
            this.getSystemThreadID(),
            this,
            eVMMetaCodeExecutionMode.RAW
        );
        this.updateUIWithStatus(status.data);
    } catch (error) {
        console.error('Failed to load status:', error);
        this.showNotification('Error loading blockchain status', 'error');
    }
}

Thread Management

// Create a periodic background thread
initialize() {
    this.mControllerThreadInterval = 1000; // 1 second
    this.mControler = CVMContext.getInstance().createJSThread(
        this.mControllerThreadF.bind(this),
        this.getProcessID,
        this.mControllerThreadInterval
    );
}

// Thread function with mutex protection
mControllerThreadF() {
    if (this.mControllerExecuting) return false;
    this.mControllerExecuting = true;

    // Your periodic logic here
    this.refreshData();

    this.mControllerExecuting = false;
}

// Always stop in closeWindow()
closeWindow() {
    if (this.mControler > 0) {
        CVMContext.getInstance().stopJSThread(this.mControler);
    }
    super.closeWindow();
}

Settings Persistence

// Load settings
loadSettings() {
    CVMContext.getInstance().getSettingsManager.loadSettings(CMyDApp.getPackageID());
    return this.activateSettings();
}

// Save settings
saveSettings() {
    let sets = CMyDApp.getSettings();
    CVMContext.getInstance().getSettingsManager.saveAppSettings(sets);
}

// Static settings storage
static getSettings() {
    return CMyDApp.sCurrentSettings;
}

static setSettings(sets) {
    if (!(sets instanceof CAppSettings)) return false;
    CMyDApp.sCurrentSettings = sets;
    return true;
}

// Default settings
static getDefaultSettings() {
    let obj = { theme: 'dark', refreshInterval: 30, version: 1 };
    return new CAppSettings(CMyDApp.getPackageID(), obj);
}

// Initialize at bottom of file
CMyDApp.sCurrentSettings = new CAppSettings(CMyDApp.getPackageID());

VII. Window Lifecycle and Events

Window lifecycle
The lifecycle of a GRIDNET OS window — from creation to destruction.
CWindow Lifecycle Diagram
The complete CWindow lifecycle: constructor → open() → runtime → closeWindow() → destroyed.

Your dApp’s lifecycle follows a predictable sequence of events:

Constructor → open() → [Runtime] → closeWindow()

class CMyDApp extends CWindow {
    // 1. CONSTRUCTOR — called when window object is created
    constructor(positionX, positionY, width, height, data, dataType, filePath, thread) {
        super(positionX, positionY, width, height, body, "Title", CMyDApp.getIcon(), true);

        // Initialize instance state
        this.mTools = CTools.getInstance();
        this.mMetaParser = new CVMMetaParser();

        // Register event listeners
        CVMContext.getInstance().addVMMetaDataListener(
            this.newVMMetaDataCallback.bind(this), this.mID
        );
    }

    // 2. OPEN — called when window is displayed and ready for interaction
    open() {
        super.open(); // MUST call super
        this.initialize();
        // DOM is now accessible via getControl/getBody
        // Set up UI event listeners, populate content
    }

    // 3. RESIZE EVENTS — fired during window resize
    finishResize(isFallbackEvent) {
        super.finishResize(isFallbackEvent);
        // React to new dimensions:
        // this.getClientWidth, this.getClientHeight
    }

    stopResize(handle) {
        super.stopResize(handle);
    }

    // 4. SCROLL EVENTS
    onScroll(event) {
        super.onScroll(event);
    }

    // 5. CLOSE — clean up everything
    closeWindow() {
        // Stop threads
        if (this.mControler > 0) {
            CVMContext.getInstance().stopJSThread(this.mControler);
        }
        // Unregister listeners
        CVMContext.getInstance().unregisterEventListenerByID(this.mID);
        // Destroy UI components
        // ALWAYS call super last
        super.closeWindow();
    }
}

Curtain Control

The “curtain” is a loading overlay managed by CWindow. It appears during DOM mutations to prevent users from interacting with partially-rendered UI:

// Pause curtain during rapid DOM updates (e.g., slider interactions)
this.pauseCurtain(3); // Pause for 3 seconds

// Whitelist elements that cause frequent DOM mutations
this.addCurtainWhitelist({
    classNames: ['slider-handle', 'slider-track', 'live-counter'],
    ids: ['main-balance-display', 'progress-indicator'],
    maxDepth: 5  // Check up to 5 parent levels
});

// Manual curtain control for long operations
this.showCurtain(true, false, false);
// ... perform long operation ...
this.hideCurtain();

User Dialogs

// Ask user for string input
this.askString('⋮⋮⋮ Title', 'What is your question?', this.handleResponse.bind(this), true);

// Handle the response
handleResponse(e) {
    if (e.answer) {
        const userInput = e.answer;
        // Use the input
    }
}

// Log notifications
this.mTools.logEvent('Operation complete!',
    eLogEntryCategory.dApp, 0, eLogEntryType.notification, this);

VIII. Responsive Design Patterns for Windowed Applications

Responsive window design
Responsive design in a windowed world — where media queries meet window dimensions.

Since you can’t use viewport units or media queries based on screen size, responsive design in GRIDNET OS uses a different approach: class-based breakpoints applied programmatically via JavaScript, combined with fluid CSS.

The Pattern: JS-Driven Breakpoint Classes

The Wallet dApp demonstrates this pattern in its finishResize() handler:

finishResize(isFallbackEvent) {
    super.finishResize(isFallbackEvent);

    const width = this.getClientWidth;
    const container = this.getControl('wallet-container');

    // Apply breakpoint classes based on window width
    container.classList.remove('wallet-narrow', 'wallet-mobile', 'wallet-tablet');

    if (width < 480) {
        container.classList.add('wallet-narrow');
    } else if (width < 600) {
        container.classList.add('wallet-mobile');
    } else if (width < 900) {
        container.classList.add('wallet-tablet');
    }
}

Then in CSS, target these classes instead of media queries:

/* Default (wide) layout */
.wallet-header {
    padding: 10px 20px;
    height: 60px;
}

/* Narrow window layout */
.wallet-narrow .wallet-header {
    padding: 8px 10px;
    height: 50px;
}

.wallet-narrow .wallet-logo {
    display: none; /* Hide logo in very narrow windows */
}

.wallet-narrow .wallet-nav-item {
    width: 30px;
    height: 30px;
}

/* Mobile-width window layout */
.wallet-mobile .wallet-header {
    padding: 10px 12px;
    height: 54px;
}

.wallet-mobile .wallet-logo {
    display: none;
}

Note: CSS @media queries referencing max-width still work but they reference the viewport width, not the window width. The JS-driven approach gives you true window-responsive behavior.

IX. Complete Production-Ready Example: A Data Dashboard dApp

Complete example dApp
From template to production — a complete working dApp demonstrating every pattern.

Here is a complete, production-ready dApp demonstrating all the patterns covered in this guide — a blockchain data dashboard that queries blockchain status, displays it in a responsive layout, persists settings, and handles all lifecycle events correctly:

"use strict"

import { CWindow } from "/lib/window.js"
import { CVMMetaSection, CVMMetaEntry, CVMMetaGenerator, CVMMetaParser } from '/lib/MetaData.js'
import { CTools, CDataConcatenator } from '/lib/tools.js'
import { CAppSettings, CSettingsManager } from "/lib/SettingsManager.js"
import { CContentHandler } from "/lib/AppSelector.js"

const _private = new WeakMap();

const dashboardBody = `
<link rel="stylesheet" href="/css/windowDefault.css" />
<style>
:host {
    --primary: #22fafc;
    --bg-dark: #0a0a14;
    --bg-panel: rgba(10, 10, 25, 0.8);
    --text: #e0e0ff;
    --text-dim: #b0b0dd;
    --border: rgba(34, 250, 252, 0.2);
}
.dashboard {
    font-family: 'Rajdhani', 'Roboto', sans-serif;
    color: var(--text);
    background: var(--bg-dark);
    height: 100%; width: 100%;
    position: absolute; top: 0; left: 0;
    display: flex; flex-direction: column;
    overflow: hidden;
}
.dash-header {
    background: linear-gradient(90deg, #090918, #141432);
    border-bottom: 1px solid var(--primary);
    padding: 10px 20px;
    display: flex; justify-content: space-between; align-items: center;
    flex-shrink: 0;
}
.dash-title {
    font-size: 1.3rem; color: var(--primary);
    text-shadow: 0 0 8px rgba(34, 250, 252, 0.4);
}
.dash-content {
    flex: 1; padding: 20px; overflow-y: auto;
}
.cards {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
    gap: 15px;
}
.card {
    background: var(--bg-panel);
    border: 1px solid var(--border);
    border-radius: 4px; padding: 15px;
}
.card-label { color: var(--text-dim); font-size: 0.85rem; text-transform: uppercase; }
.card-value { font-size: 1.5rem; color: var(--primary); font-weight: bold; margin-top: 5px; }
.status-bar {
    background: linear-gradient(90deg, #090918, #141432);
    border-top: 1px solid var(--primary);
    padding: 6px 20px;
    font-size: 0.8rem; color: #808080;
    flex-shrink: 0;
    display: flex; justify-content: space-between;
}
.btn-refresh {
    background: linear-gradient(90deg, rgba(34, 250, 252, 0.1), rgba(34, 250, 252, 0.2));
    border: 1px solid rgba(34, 250, 252, 0.3);
    border-radius: 4px; padding: 6px 12px;
    color: var(--primary); cursor: pointer;
    font-family: inherit; font-size: 0.85rem;
}
.btn-refresh:hover {
    background: rgba(34, 250, 252, 0.2);
    box-shadow: 0 0 10px rgba(34, 250, 252, 0.3);
}
.dash-narrow .dash-header { padding: 8px 12px; }
.dash-narrow .dash-content { padding: 10px; }
.dash-narrow .cards { grid-template-columns: 1fr; }
</style>

<div class="dashboard" id="dashboard-container">
    <div class="dash-header">
        <span class="dash-title">Blockchain Dashboard</span>
        <button class="btn-refresh" id="refresh-btn">⟳ Refresh</button>
    </div>
    <div class="dash-content">
        <div class="cards">
            <div class="card">
                <div class="card-label">Block Height</div>
                <div class="card-value" id="block-height">—</div>
            </div>
            <div class="card">
                <div class="card-label">Network Peers</div>
                <div class="card-value" id="peer-count">—</div>
            </div>
            <div class="card">
                <div class="card-label">Transactions Today</div>
                <div class="card-value" id="tx-count">—</div>
            </div>
            <div class="card">
                <div class="card-label">Connection State</div>
                <div class="card-value" id="conn-state">—</div>
            </div>
        </div>
    </div>
    <div class="status-bar">
        <span id="last-update">Last update: never</span>
        <span id="refresh-interval">Auto-refresh: 30s</span>
    </div>
</div>
`;

class CBlockchainDashboard extends CWindow {
    constructor(positionX, positionY, width, height, data, dataType, filePath, thread) {
        super(positionX, positionY, width, height, dashboardBody,
              "Blockchain Dashboard", CBlockchainDashboard.getIcon(), true);

        // Private data via WeakMap
        _private.set(this, {
            lastUpdateTime: null,
            refreshCount: 0
        });

        // Instance state
        this.mTools = CTools.getInstance();
        this.mMetaParser = new CVMMetaParser();
        this.mControllerThreadInterval = 30000; // 30 seconds
        this.mControlerExecuting = false;
        this.mControler = 0;

        // Register for events
        CVMContext.getInstance().addVMMetaDataListener(
            this.newVMMetaDataCallback.bind(this), this.mID
        );
        CVMContext.getInstance().addNewDFSMsgListener(
            this.newDFSMsgCallback.bind(this), this.mID
        );
        CVMContext.getInstance().addVMStateChangedListener(
            this.onConnectionStateChanged.bind(this), this.mID
        );
    }

    static getPackageID() {
        return "org.gridnetproject.UIdApps.blockchainDashboard";
    }

    static getDefaultCategory() { return 'dApps'; }

    static getIcon() { return ''; }

    static getFileHandlers() {
        return []; // No file associations
    }

    // — Lifecycle —

    open() {
        super.open();
        this.initialize();

        // Set up UI event listeners
        this.getControl('refresh-btn').addEventListener('click',
            () => this.refreshData()
        );

        // Initial data load
        this.refreshData();
    }

    initialize() {
        // Load saved settings
        if (this.loadSettings()) {
            this.mTools.logEvent('[Dashboard] Settings loaded.',
                eLogEntryCategory.dApp, 0, eLogEntryType.notification);
        } else {
            CBlockchainDashboard.setSettings(
                CBlockchainDashboard.getDefaultSettings()
            );
        }

        // Start auto-refresh thread
        this.mControler = CVMContext.getInstance().createJSThread(
            this.mControllerThreadF.bind(this),
            this.getProcessID,
            this.mControllerThreadInterval
        );
    }

    closeWindow() {
        if (this.mControler > 0) {
            CVMContext.getInstance().stopJSThread(this.mControler);
        }
        CVMContext.getInstance().unregisterEventListenerByID(this.mID);
        _private.delete(this);
        super.closeWindow();
    }

    // — Resize handling —

    finishResize(isFallbackEvent) {
        super.finishResize(isFallbackEvent);
        const container = this.getControl('dashboard-container');
        container.classList.remove('dash-narrow');
        if (this.getClientWidth < 500) {
            container.classList.add('dash-narrow');
        }
    }

    // — Data refresh —

    mControllerThreadF() {
        if (this.mControlerExecuting) return false;
        this.mControlerExecuting = true;
        this.refreshData();
        this.mControlerExecuting = false;
    }

    async refreshData() {
        try {
            const vm = CVMContext.getInstance();
            const status = await vm.getBlockchainStatusA(
                this.getSystemThreadID(), this, eVMMetaCodeExecutionMode.RAW
            );
            if (status && status.data) {
                this.updateDashboard(status.data);
            }
        } catch (err) {
            console.error('[Dashboard] Refresh failed:', err);
        }
    }

    updateDashboard(data) {
        const priv = _private.get(this);
        priv.lastUpdateTime = new Date();
        priv.refreshCount++;

        if (data.blockHeight !== undefined) {
            this.getControl('block-height').textContent =
                data.blockHeight.toLocaleString();
        }
        if (data.peerCount !== undefined) {
            this.getControl('peer-count').textContent = data.peerCount;
        }
        if (data.txCount !== undefined) {
            this.getControl('tx-count').textContent =
                data.txCount.toLocaleString();
        }

        this.getControl('last-update').textContent =
            'Last update: ' + priv.lastUpdateTime.toLocaleTimeString();
    }

    // — Event callbacks —

    onConnectionStateChanged(eventData) {
        const stateEl = this.getControl('conn-state');
        if (eventData && eventData.state !== undefined) {
            stateEl.textContent = eventData.state === eConnectionState.connected
                ? '🟢 Connected' : '🔴 Disconnected';
        }
    }

    newVMMetaDataCallback(msg) {
        if (!this.hasNetworkRequestID(msg.getReqID)) return;
        // Process metadata response
    }

    newDFSMsgCallback(dfsMsg) {
        if (!this.hasNetworkRequestID(dfsMsg.getReqID)) return;
        // Process DFS response
    }

    // — Settings —

    static getSettings() { return CBlockchainDashboard.sCurrentSettings; }
    static setSettings(sets) {
        if (!(sets instanceof CAppSettings)) return false;
        CBlockchainDashboard.sCurrentSettings = sets;
        return true;
    }

    loadSettings() {
        CVMContext.getInstance().getSettingsManager.loadSettings(
            CBlockchainDashboard.getPackageID()
        );
        return this.activateSettings();
    }

    activateSettings() {
        const sets = CBlockchainDashboard.getSettings();
        if (!sets || typeof sets.getVersion === 'undefined') return false;
        if (sets.getVersion !== 1) return false;
        const data = sets.getData;
        if (!data) return false;
        if (data.refreshInterval) {
            this.mControllerThreadInterval = data.refreshInterval * 1000;
        }
        return true;
    }

    saveSettings() {
        const sets = CBlockchainDashboard.getSettings();
        CVMContext.getInstance().getSettingsManager.saveAppSettings(sets);
    }

    static getDefaultSettings() {
        return new CAppSettings(CBlockchainDashboard.getPackageID(), {
            refreshInterval: 30,
            version: 1
        });
    }
}

CBlockchainDashboard.sCurrentSettings = new CAppSettings(
    CBlockchainDashboard.getPackageID()
);

export default CBlockchainDashboard;

X. Deployment Workflow

Deployment pipeline
From code to the decentralised network — the deployment pipeline.
Deployment Pipeline Diagram
The five-step deployment pipeline: Bundle → Rename → Upload → Analyse → Commit to DFS.

Deploying your dApp to GRIDNET OS follows five steps:

  1. Bundle — Combine all your custom JavaScript and non-system dependencies into a single file using Rollup, Webpack, or Parcel. System libraries (/lib/*) and OS-provided libraries (Tabulator, Plotly) should not be bundled.
  2. Rename — Change the file extension from .js to .app (e.g., BlockchainDashboard.app).
  3. Upload — Connect to the GRIDNET OS UI at https://ui.gridnet.org (or your local instance). Drag and drop the .app file onto the Desktop or into the File Manager dApp.
  4. Analyse & Install — The OS analyses the package. Events appear in the log pane. Once analysed, your dApp’s icon appears on the Desktop. You can run it in “sandbox” mode for testing.
  5. (Optional) Commit to DFS — To make the dApp persistent and available across the decentralised network, select the .app file in the File Manager and use the ⋮⋮⋮ Magic Button to commit it to the Decentralised File System. This requires GNC for storage fees.

XI. Going Deeper — GridScript: A Familiar Shell with Blockchain Superpowers

Most UI dApp developers will never need to write GridScript directly — CVMContext abstracts the blockchain layer into clean JavaScript calls. But when you do venture deeper — for advanced operations, custom smart contracts, or direct blockchain manipulation via the Terminal dApp — you’ll find a command set that feels like coming home.

GridScript’s commands are deliberately modelled on Linux/UNIX and DOS conventions:

GridScript Command Linux/DOS Equivalent What It Does in GRIDNET OS
ls ls / dir List contents of the current domain (blockchain namespace)
cd cd Change current domain (navigate the blockchain namespace tree)
cat cat / type Display contents of a blockchain state entry
mkdir mkdir Create a new domain (blockchain namespace)
touch touch Create a new state entry
rm rm / del Remove a state entry or domain
chown chown Change ownership of a domain
setfacl setfacl Set access control lists on blockchain resources
getfacl getfacl View access control lists
send (blockchain-native) Transfer GNC tokens to another domain
BT SQL BEGIN Begin a blockchain transaction
CT SQL COMMIT Commit the transaction to the blockchain

The analogy to SQL is particularly illuminating: just as a database transaction groups multiple operations into an atomic unit with BEGIN and COMMIT, GridScript’s BT/CT pair groups blockchain operations into an atomic on-chain transaction. If you’ve ever written BEGIN; UPDATE ...; INSERT ...; COMMIT;, you already understand the pattern — except now the database is a global, decentralized, tamper-proof state machine.

Your UI dApp typically invokes these operations indirectly through CVMContext methods — you don’t type ls in your JavaScript. But understanding the underlying model makes debugging easier, the Terminal dApp immediately useful, and the entire system less mysterious. GRIDNET OS was designed to feel familiar to anyone who’s ever opened a terminal.

XII. Quick Reference Card

Category DO DON’T
DOM Access this.getControl('id'), this.getBody.querySelector() document.getElementById(), document.querySelector()
State this.mMyVar, WeakMap Global variables, window.myVar
CSS Sizing %, em, rem, fr, flex vw, vh
Positioning relative, absolute (within container), flex fixed, sticky
Layout display: flex, display: grid Pixel-perfect absolute positioning
OS Interaction CVMContext.getInstance() Direct WebSocket, fetch() to blockchain
Event Listeners callback.bind(this), arrow functions Unbound method references
Cleanup Stop threads, unregister listeners in closeWindow() Leaving threads running, listeners leaking
Deployment Single .app file, system imports Multi-file deployments, bundled system libs
Styles Scoped within Shadow DOM, own CSS variables External CDN stylesheets, external CSS vars

XIII. Further Reading

Welcome to the decentralised future. Build something extraordinary.

GRIDNET

Author

GRIDNET

Up Next

Related Posts