Skip to content

Commit cc9218f

Browse files
committed
feat: add examples for UIManager including floating layout, sidebar navigation, and notifications
1 parent 4280b57 commit cc9218f

7 files changed

Lines changed: 745 additions & 6 deletions

File tree

docs/builtin/paths.json

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
11
[
2-
{
3-
"path": "src/built-in/AppManager/example.ts",
4-
"description": ""
5-
},
62
{
73
"path": "src/built-in/CDEManager/example.ts",
84
"description": "CDEManager — CDEManager is the central state machine for a Common Data Environment. It holds files, folders, metadata schemas, viewer groups, and user permissions, and exposes a typed state machine that all file operations — selecting, editing, uploading, viewing — route through. It has no DOM or Three.js dependencies and can run in Node, a browser, or a server-side worker wherever `@thatopen/components` is available."
@@ -48,7 +44,27 @@
4844
"description": ""
4945
},
5046
{
51-
"path": "src/built-in/ViewportsManager/example.ts",
52-
"description": ""
47+
"path": "src/built-in/UIManager/src/app/example.ts",
48+
"description": "top-app — Front-end developers building BIM web apps need to coordinate several async initialization steps before the UI can be shown — loading the engine, preparing fragment workers, registering loaders — and users need visible feedback on what is happening during that wait, rather than a blank screen."
49+
},
50+
{
51+
"path": "src/built-in/UIManager/src/app/examples/AppState/example.ts",
52+
"description": "Shared reactive state with AppState — Developers building interactive BIM panels often need sibling components — a filter selector and a results table, for example — to stay in sync without a shared parent. Lifting state to a common ancestor and threading it down as props works for shallow trees but becomes unwieldy as the component tree grows."
53+
},
54+
{
55+
"path": "src/built-in/UIManager/src/app/examples/Contexts/example.ts",
56+
"description": "Consuming platform contexts in child components — Developers building custom BIM panels need access to the 3D engine, the platform API, and the current project's metadata — but threading these objects down through props across multiple component layers forces every intermediate element to know about data it doesn't use, making trees brittle and hard to refactor."
57+
},
58+
{
59+
"path": "src/built-in/UIManager/src/app/examples/FloatingLayout/example.ts",
60+
"description": "Floating layout — Developers building BIM coordination tools often want the 3D model to fill the entire window while control panels float above it — but standard grid layouts give every area equal standing, shrinking the viewer into one cell and leaving visible seams between it and the panels."
61+
},
62+
{
63+
"path": "src/built-in/UIManager/src/app/examples/Notifications/example.ts",
64+
"description": "Toasts, file drop, and cross-component notifications — Developers building BIM processing UIs need to surface status updates, errors, and file-drop feedback to users from different points in the component tree — but reaching back to the app root to trigger a notification from a deeply nested component requires threading a reference through every layer in between."
65+
},
66+
{
67+
"path": "src/built-in/UIManager/src/app/examples/Sidebar/example.ts",
68+
"description": "Sidebar navigation — Product teams building BIM apps often need multiple distinct views — a 3D model viewer, a data browser, a settings screen — but wiring a custom nav bar and managing view transitions from scratch adds complexity before any real feature work begins."
5369
}
5470
]
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/* MD
2+
## top-app
3+
---
4+
Front-end developers building BIM web apps need to coordinate several async
5+
initialization steps before the UI can be shown — loading the engine, preparing
6+
fragment workers, registering loaders — and users need visible feedback on what
7+
is happening during that wait, rather than a blank screen.
8+
9+
top-app is the platform's root shell element. It manages the full boot sequence,
10+
tracks each async setup task individually, shows a labelled loading screen until
11+
every task completes, and then mounts the app's visual layout.
12+
13+
This tutorial covers initializing the platform client and registering all
14+
platform web components; registering two async setup tasks — the fragment worker
15+
and the IFC loader — each with its own loading-screen label; declaring a named
16+
layout area as a CSS grid template; wiring a 3D viewer into that layout; and
17+
using a flag to keep the loading screen visible during development.
18+
19+
By the end, you'll have a working app shell that boots the engine, shows a
20+
step-by-step loading screen, and mounts a 3D viewer once every setup task
21+
resolves.
22+
*/
23+
24+
import { html } from "lit";
25+
import * as THREE from "three";
26+
import * as OBC from "@thatopen/components";
27+
import * as OBF from "@thatopen/components-front";
28+
import * as FRAGS from "@thatopen/fragments";
29+
import * as BUI from "@thatopen/ui";
30+
import { PlatformClient } from "thatopen-services";
31+
import uiManagerDef from "../../index";
32+
import type { App } from "./index";
33+
34+
const UIManager = uiManagerDef.componentDefinition;
35+
36+
const client = PlatformClient.fromPlatformContext();
37+
38+
// UIManager must be in the setup call: it registers all platform web components
39+
// (top-app, top-viewer, top-viewer-tools, etc.) before the DOM renders.
40+
const { components } = (await client.setup(
41+
{ OBC, OBF, BUI, THREE, FRAGS },
42+
{ uuid: UIManager.uuid },
43+
)) as { components: OBC.Components };
44+
45+
components.get(UIManager).init();
46+
47+
const app = document.createElement("top-app") as unknown as App;
48+
49+
// setup runs synchronously and returns { components, client }.
50+
// waitUntil registers async tasks that appear as labelled steps in the loading screen.
51+
// All tasks run in parallel; top-app mounts only after every promise resolves.
52+
app.setup = (waitUntil) => {
53+
waitUntil(
54+
(async () => {
55+
const fragments = components.get(OBC.FragmentsManager);
56+
const workerUrl = await FRAGS.FragmentsModels.getWorker();
57+
fragments.init(await FRAGS.toClassicWorker(workerUrl), {
58+
classicWorker: true,
59+
});
60+
})(),
61+
"Fragments Core",
62+
);
63+
64+
waitUntil(
65+
(async () => {
66+
const loader = components.get(OBC.IfcLoader);
67+
await loader.setup();
68+
})(),
69+
"IFC Loader",
70+
);
71+
72+
return { components, client };
73+
};
74+
75+
// Each key must match an area name used in at least one layout template.
76+
app.elements = {
77+
viewer: () =>
78+
html`<top-viewer><top-viewer-tools></top-viewer-tools></top-viewer>`,
79+
};
80+
81+
// CSS grid-template shorthand. Area names are the keys for app.elements.
82+
// "." creates an empty unnamed cell and is never rendered.
83+
app.layouts = {
84+
main: {
85+
label: "Main",
86+
icon: "solar:3d-square-bold",
87+
template: `"viewer" 1fr / 1fr`,
88+
},
89+
};
90+
91+
app.layout = "main";
92+
93+
// forceLoading keeps the boot screen visible even after setup completes.
94+
// Useful during development to inspect the loading UI without real async tasks.
95+
// app.forceLoading = true;
96+
97+
const container = document.getElementById("that-open-app") ?? document.body;
98+
container.appendChild(app);
99+
document.body.style.margin = "0";
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/* MD
2+
## Shared reactive state with AppState
3+
---
4+
Developers building interactive BIM panels often need sibling components — a
5+
filter selector and a results table, for example — to stay in sync without a
6+
shared parent. Lifting state to a common ancestor and threading it down as props
7+
works for shallow trees but becomes unwieldy as the component tree grows.
8+
9+
top-app provides a typed reactive state object available to every component in
10+
the tree through context. Any component can write to it, and all subscribers
11+
re-render automatically — no shared parent required.
12+
13+
This tutorial covers defining a typed payload for custom app state; building a
14+
filter selector that updates shared state on button click; building a results
15+
counter that reads the current filter and count from shared state; composing both
16+
into a panel inside a top-app layout; and noting when the shared state is
17+
available relative to the engine boot sequence.
18+
19+
By the end, you'll have two independent components that stay in sync through
20+
shared reactive state — a filter selector and a results counter that reflect each
21+
other's changes without any direct coupling between them.
22+
*/
23+
24+
import { html, LitElement } from "lit";
25+
import { customElement, state } from "lit/decorators.js";
26+
import { consume } from "@lit/context";
27+
import * as THREE from "three";
28+
import * as OBC from "@thatopen/components";
29+
import * as OBF from "@thatopen/components-front";
30+
import * as FRAGS from "@thatopen/fragments";
31+
import * as BUI from "@thatopen/ui";
32+
import { PlatformClient } from "thatopen-services";
33+
import uiManagerDef from "../../../../index";
34+
import {
35+
appStateContext,
36+
AppState,
37+
AppStateController,
38+
type App,
39+
} from "../../index";
40+
41+
// Define a typed payload for this app's custom state.
42+
type MyAppState = { selectedFilter: string; count: number };
43+
44+
// AppStateController subscribes to appStateContext and triggers a re-render
45+
// on every setCustom call — across any component in the tree that holds one.
46+
@customElement("filter-selector")
47+
class FilterSelector extends LitElement {
48+
@consume({ context: appStateContext, subscribe: true })
49+
@state()
50+
private _appState?: AppState;
51+
52+
// Declare the controller — the LitElement lifecycle handles subscription.
53+
private _stateCtrl = new AppStateController<MyAppState>(this, () => this._appState);
54+
55+
render() {
56+
const filters = ["All", "Walls", "Slabs", "Columns"];
57+
const active = this._stateCtrl.custom?.selectedFilter ?? "All";
58+
return html`
59+
<bim-panel-section label="Filter" icon="solar:filter-bold">
60+
${filters.map(
61+
(f) => html`
62+
<bim-button
63+
.label=${f}
64+
?active=${active === f}
65+
@click=${() => {
66+
this._appState?.setCustom<MyAppState>({ selectedFilter: f, count: 0 });
67+
}}
68+
></bim-button>
69+
`,
70+
)}
71+
</bim-panel-section>
72+
`;
73+
}
74+
}
75+
76+
@customElement("result-counter")
77+
class ResultCounter extends LitElement {
78+
@consume({ context: appStateContext, subscribe: true })
79+
@state()
80+
private _appState?: AppState;
81+
82+
private _stateCtrl = new AppStateController<MyAppState>(this, () => this._appState);
83+
84+
render() {
85+
const { selectedFilter = "All", count = 0 } = this._stateCtrl.custom ?? {};
86+
return html`
87+
<bim-panel-section label="Results" icon="solar:list-bold">
88+
<bim-label>${count} elements match "${selectedFilter}"</bim-label>
89+
</bim-panel-section>
90+
`;
91+
}
92+
}
93+
94+
// ---- app wiring ----
95+
96+
const UIManager = uiManagerDef.componentDefinition;
97+
const client = PlatformClient.fromPlatformContext();
98+
const { components } = (await client.setup(
99+
{ OBC, OBF, BUI, THREE, FRAGS },
100+
{ uuid: UIManager.uuid },
101+
)) as { components: OBC.Components };
102+
components.get(UIManager).init();
103+
104+
const app = document.createElement("top-app") as unknown as App;
105+
106+
// appState is available immediately — before setup completes — so components
107+
// that only consume appStateContext can render while the engine is still booting.
108+
app.setup = (waitUntil) => {
109+
waitUntil(
110+
(async () => {
111+
const fragments = components.get(OBC.FragmentsManager);
112+
const workerUrl = await FRAGS.FragmentsModels.getWorker();
113+
fragments.init(await FRAGS.toClassicWorker(workerUrl), {
114+
classicWorker: true,
115+
});
116+
})(),
117+
"Fragments Core",
118+
);
119+
return { components, client };
120+
};
121+
122+
app.elements = {
123+
panel: () => html`
124+
<bim-panel label="State Demo">
125+
<filter-selector></filter-selector>
126+
<result-counter></result-counter>
127+
</bim-panel>
128+
`,
129+
};
130+
131+
app.layouts = {
132+
main: {
133+
label: "Main",
134+
icon: "solar:home-bold",
135+
template: `"panel" 1fr / 22rem`,
136+
},
137+
};
138+
app.layout = "main";
139+
140+
const container = document.getElementById("that-open-app") ?? document.body;
141+
container.appendChild(app);
142+
document.body.style.margin = "0";

0 commit comments

Comments
 (0)