@thatopen/services 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -62
- package/dist/built-in/index.d.ts +66 -41
- package/dist/cli/lib/revit-addin.d.ts +8 -0
- package/dist/cli.js +154 -13
- package/dist/core/client.d.ts +6 -0
- package/dist/core/platform-client.d.ts +139 -0
- package/dist/core/platform-client.live.test.d.ts +1 -0
- package/dist/core/platform-client.notifications.test.d.ts +1 -0
- package/dist/index.cjs.js +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +635 -445
- package/dist/types/notifications.d.ts +54 -0
- package/dist/vite-env.d.ts +1 -1
- package/docs/ai-quickstart.md +120 -1
- package/docs/cli/create.md +29 -29
- package/docs/cli/local-server.md +38 -38
- package/docs/cli/login.md +32 -32
- package/docs/cli/publish.md +31 -31
- package/docs/cli/run.md +26 -26
- package/docs/cli/serve.md +43 -43
- package/docs/cli/swap.md +30 -30
- package/docs/flow-plugin-guide.md +26 -0
- package/docs/revit-collab-quickstart.md +26 -17
- package/docs/rhino-interop-quickstart.md +74 -7
- package/package.json +1 -1
- package/resources/AGENTS.md +40 -40
- package/src/built-in/CDEManager/example.ts +214 -214
- package/src/built-in/CDEManager/examples/FileEditing/example.ts +226 -226
- package/src/built-in/CDEManager/examples/FileMoves/example.ts +186 -186
- package/src/built-in/CDEManager/examples/FileOperations/example.ts +295 -295
- package/src/built-in/CDEManager/examples/FileTransfers/example.ts +210 -210
- package/src/built-in/CDEManager/examples/FolderManagement/example.ts +230 -230
- package/src/built-in/CDEManager/examples/MetadataSchema/example.ts +230 -230
- package/src/built-in/CDEManager/examples/MetadataViews/example.ts +166 -166
- package/src/built-in/CDEManager/examples/ProjectConfig/example.ts +177 -177
- package/src/built-in/UIManager/src/app/example.ts +97 -97
- package/src/built-in/UIManager/src/app/examples/AppState/example.ts +140 -140
- package/src/built-in/UIManager/src/app/examples/Banner/example.ts +159 -159
- package/src/built-in/UIManager/src/app/examples/Contexts/example.ts +123 -123
- package/src/built-in/UIManager/src/app/examples/FileDrop/example.ts +124 -124
- package/src/built-in/UIManager/src/app/examples/FloatingLayout/example.ts +90 -90
- package/src/built-in/UIManager/src/app/examples/Modal/example.ts +163 -163
- package/src/built-in/UIManager/src/app/examples/Notifications/example.ts +133 -133
- package/src/built-in/UIManager/src/app/examples/ProjectFileStorage/example.ts +150 -150
- package/src/built-in/UIManager/src/app/examples/Sidebar/example.ts +102 -102
- package/src/built-in/UIManager/src/clashes-panel/example.ts +183 -183
- package/src/built-in/UIManager/src/git-history-panel/example.ts +70 -70
- package/src/built-in/UIManager/src/models-list/example.ts +122 -122
- package/src/built-in/UIManager/src/topics-panel/example.ts +170 -170
- package/src/built-in/index.ts +66 -41
- package/src/cli/templates/app/index.html +35 -35
- package/src/cli/templates/app/package.json +32 -32
- package/src/cli/templates/app/src/bim-components/CloudRunner/index.ts +95 -95
- package/src/cli/templates/app/src/bim-components/CloudRunner/src/index.ts +1 -1
- package/src/cli/templates/app/src/bim-components/CloudRunner/src/types.ts +5 -5
- package/src/cli/templates/app/src/bim-components/index.ts +1 -1
- package/src/cli/templates/app/src/globals.ts +1 -1
- package/src/cli/templates/app/src/main.history.ts +191 -0
- package/src/cli/templates/app/src/ui-components/app-info-section/index.ts +26 -26
- package/src/cli/templates/app/src/ui-components/app-info-section/src/index.ts +1 -1
- package/src/cli/templates/app/src/ui-components/app-info-section/src/types.ts +15 -15
- package/src/cli/templates/app/src/ui-components/cloud-runner-section/index.ts +37 -37
- package/src/cli/templates/app/src/ui-components/cloud-runner-section/src/index.ts +1 -1
- package/src/cli/templates/app/src/ui-components/cloud-runner-section/src/types.ts +14 -14
- package/src/cli/templates/app/src/ui-components/index.ts +2 -2
- package/src/cli/templates/app/tsconfig.json +16 -16
- package/src/cli/templates/app/vite.config.js +57 -57
- package/src/cli/templates/cloud-component/README.md +205 -205
- package/src/cli/templates/cloud-component/_thatopen +5 -5
- package/src/cli/templates/cloud-component/declarations.json +4 -4
- package/src/cli/templates/cloud-component/package.json +21 -21
- package/src/cli/templates/cloud-component/src/main.ts +70 -70
- package/src/cli/templates/cloud-component/tsconfig.json +16 -16
- package/src/cli/templates/cloud-component/vite.config.js +46 -46
- package/src/cli/templates/shared/AGENTS.md +8 -8
- package/src/cli/templates/shared/CLAUDE.md +1 -1
- package/src/cli/templates/shared/_gitignore +5 -5
|
@@ -1,140 +1,140 @@
|
|
|
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, UIManager } from "@thatopen/services";
|
|
33
|
-
import {
|
|
34
|
-
appStateContext,
|
|
35
|
-
AppState,
|
|
36
|
-
AppStateController,
|
|
37
|
-
type App,
|
|
38
|
-
} from "../../index";
|
|
39
|
-
|
|
40
|
-
// Define a typed payload for this app's custom state.
|
|
41
|
-
type MyAppState = { selectedFilter: string; count: number };
|
|
42
|
-
|
|
43
|
-
// AppStateController subscribes to appStateContext and triggers a re-render
|
|
44
|
-
// on every setCustom call — across any component in the tree that holds one.
|
|
45
|
-
@customElement("filter-selector")
|
|
46
|
-
class FilterSelector extends LitElement {
|
|
47
|
-
@consume({ context: appStateContext, subscribe: true })
|
|
48
|
-
@state()
|
|
49
|
-
private _appState?: AppState;
|
|
50
|
-
|
|
51
|
-
// Declare the controller — the LitElement lifecycle handles subscription.
|
|
52
|
-
private _stateCtrl = new AppStateController<MyAppState>(this, () => this._appState);
|
|
53
|
-
|
|
54
|
-
render() {
|
|
55
|
-
const filters = ["All", "Walls", "Slabs", "Columns"];
|
|
56
|
-
const active = this._stateCtrl.custom?.selectedFilter ?? "All";
|
|
57
|
-
return html`
|
|
58
|
-
<bim-panel-section label="Filter" icon="solar:filter-bold">
|
|
59
|
-
${filters.map(
|
|
60
|
-
(f) => html`
|
|
61
|
-
<bim-button
|
|
62
|
-
.label=${f}
|
|
63
|
-
?active=${active === f}
|
|
64
|
-
@click=${() => {
|
|
65
|
-
this._appState?.setCustom<MyAppState>({ selectedFilter: f, count: 0 });
|
|
66
|
-
}}
|
|
67
|
-
></bim-button>
|
|
68
|
-
`,
|
|
69
|
-
)}
|
|
70
|
-
</bim-panel-section>
|
|
71
|
-
`;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
@customElement("result-counter")
|
|
76
|
-
class ResultCounter extends LitElement {
|
|
77
|
-
@consume({ context: appStateContext, subscribe: true })
|
|
78
|
-
@state()
|
|
79
|
-
private _appState?: AppState;
|
|
80
|
-
|
|
81
|
-
private _stateCtrl = new AppStateController<MyAppState>(this, () => this._appState);
|
|
82
|
-
|
|
83
|
-
render() {
|
|
84
|
-
const { selectedFilter = "All", count = 0 } = this._stateCtrl.custom ?? {};
|
|
85
|
-
return html`
|
|
86
|
-
<bim-panel-section label="Results" icon="solar:list-bold">
|
|
87
|
-
<bim-label>${count} elements match "${selectedFilter}"</bim-label>
|
|
88
|
-
</bim-panel-section>
|
|
89
|
-
`;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// ---- app wiring ----
|
|
94
|
-
|
|
95
|
-
const client = PlatformClient.fromPlatformContext();
|
|
96
|
-
const { components } = (await client.setup(
|
|
97
|
-
{ OBC, OBF, BUI, THREE, FRAGS },
|
|
98
|
-
{ uuid: UIManager.uuid },
|
|
99
|
-
)) as { components: OBC.Components };
|
|
100
|
-
components.get(UIManager).init();
|
|
101
|
-
|
|
102
|
-
const app = document.createElement("top-app") as unknown as App;
|
|
103
|
-
|
|
104
|
-
// appState is available immediately — before setup completes — so components
|
|
105
|
-
// that only consume appStateContext can render while the engine is still booting.
|
|
106
|
-
app.setup = (waitUntil) => {
|
|
107
|
-
waitUntil(
|
|
108
|
-
(async () => {
|
|
109
|
-
const fragments = components.get(OBC.FragmentsManager);
|
|
110
|
-
const workerUrl = await FRAGS.FragmentsModels.getWorker();
|
|
111
|
-
fragments.init(await FRAGS.toClassicWorker(workerUrl), {
|
|
112
|
-
classicWorker: true,
|
|
113
|
-
});
|
|
114
|
-
})(),
|
|
115
|
-
"Fragments Core",
|
|
116
|
-
);
|
|
117
|
-
return { components, client };
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
app.elements = {
|
|
121
|
-
panel: () => html`
|
|
122
|
-
<bim-panel label="State Demo">
|
|
123
|
-
<filter-selector></filter-selector>
|
|
124
|
-
<result-counter></result-counter>
|
|
125
|
-
</bim-panel>
|
|
126
|
-
`,
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
app.layouts = {
|
|
130
|
-
main: {
|
|
131
|
-
label: "Main",
|
|
132
|
-
icon: "solar:home-bold",
|
|
133
|
-
template: `"panel" 1fr / 22rem`,
|
|
134
|
-
},
|
|
135
|
-
};
|
|
136
|
-
app.layout = "main";
|
|
137
|
-
|
|
138
|
-
const container = document.getElementById("that-open-app") ?? document.body;
|
|
139
|
-
container.appendChild(app);
|
|
140
|
-
document.body.style.margin = "0";
|
|
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, UIManager } from "@thatopen/services";
|
|
33
|
+
import {
|
|
34
|
+
appStateContext,
|
|
35
|
+
AppState,
|
|
36
|
+
AppStateController,
|
|
37
|
+
type App,
|
|
38
|
+
} from "../../index";
|
|
39
|
+
|
|
40
|
+
// Define a typed payload for this app's custom state.
|
|
41
|
+
type MyAppState = { selectedFilter: string; count: number };
|
|
42
|
+
|
|
43
|
+
// AppStateController subscribes to appStateContext and triggers a re-render
|
|
44
|
+
// on every setCustom call — across any component in the tree that holds one.
|
|
45
|
+
@customElement("filter-selector")
|
|
46
|
+
class FilterSelector extends LitElement {
|
|
47
|
+
@consume({ context: appStateContext, subscribe: true })
|
|
48
|
+
@state()
|
|
49
|
+
private _appState?: AppState;
|
|
50
|
+
|
|
51
|
+
// Declare the controller — the LitElement lifecycle handles subscription.
|
|
52
|
+
private _stateCtrl = new AppStateController<MyAppState>(this, () => this._appState);
|
|
53
|
+
|
|
54
|
+
render() {
|
|
55
|
+
const filters = ["All", "Walls", "Slabs", "Columns"];
|
|
56
|
+
const active = this._stateCtrl.custom?.selectedFilter ?? "All";
|
|
57
|
+
return html`
|
|
58
|
+
<bim-panel-section label="Filter" icon="solar:filter-bold">
|
|
59
|
+
${filters.map(
|
|
60
|
+
(f) => html`
|
|
61
|
+
<bim-button
|
|
62
|
+
.label=${f}
|
|
63
|
+
?active=${active === f}
|
|
64
|
+
@click=${() => {
|
|
65
|
+
this._appState?.setCustom<MyAppState>({ selectedFilter: f, count: 0 });
|
|
66
|
+
}}
|
|
67
|
+
></bim-button>
|
|
68
|
+
`,
|
|
69
|
+
)}
|
|
70
|
+
</bim-panel-section>
|
|
71
|
+
`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@customElement("result-counter")
|
|
76
|
+
class ResultCounter extends LitElement {
|
|
77
|
+
@consume({ context: appStateContext, subscribe: true })
|
|
78
|
+
@state()
|
|
79
|
+
private _appState?: AppState;
|
|
80
|
+
|
|
81
|
+
private _stateCtrl = new AppStateController<MyAppState>(this, () => this._appState);
|
|
82
|
+
|
|
83
|
+
render() {
|
|
84
|
+
const { selectedFilter = "All", count = 0 } = this._stateCtrl.custom ?? {};
|
|
85
|
+
return html`
|
|
86
|
+
<bim-panel-section label="Results" icon="solar:list-bold">
|
|
87
|
+
<bim-label>${count} elements match "${selectedFilter}"</bim-label>
|
|
88
|
+
</bim-panel-section>
|
|
89
|
+
`;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---- app wiring ----
|
|
94
|
+
|
|
95
|
+
const client = PlatformClient.fromPlatformContext();
|
|
96
|
+
const { components } = (await client.setup(
|
|
97
|
+
{ OBC, OBF, BUI, THREE, FRAGS },
|
|
98
|
+
{ uuid: UIManager.uuid },
|
|
99
|
+
)) as { components: OBC.Components };
|
|
100
|
+
components.get(UIManager).init();
|
|
101
|
+
|
|
102
|
+
const app = document.createElement("top-app") as unknown as App;
|
|
103
|
+
|
|
104
|
+
// appState is available immediately — before setup completes — so components
|
|
105
|
+
// that only consume appStateContext can render while the engine is still booting.
|
|
106
|
+
app.setup = (waitUntil) => {
|
|
107
|
+
waitUntil(
|
|
108
|
+
(async () => {
|
|
109
|
+
const fragments = components.get(OBC.FragmentsManager);
|
|
110
|
+
const workerUrl = await FRAGS.FragmentsModels.getWorker();
|
|
111
|
+
fragments.init(await FRAGS.toClassicWorker(workerUrl), {
|
|
112
|
+
classicWorker: true,
|
|
113
|
+
});
|
|
114
|
+
})(),
|
|
115
|
+
"Fragments Core",
|
|
116
|
+
);
|
|
117
|
+
return { components, client };
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
app.elements = {
|
|
121
|
+
panel: () => html`
|
|
122
|
+
<bim-panel label="State Demo">
|
|
123
|
+
<filter-selector></filter-selector>
|
|
124
|
+
<result-counter></result-counter>
|
|
125
|
+
</bim-panel>
|
|
126
|
+
`,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
app.layouts = {
|
|
130
|
+
main: {
|
|
131
|
+
label: "Main",
|
|
132
|
+
icon: "solar:home-bold",
|
|
133
|
+
template: `"panel" 1fr / 22rem`,
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
app.layout = "main";
|
|
137
|
+
|
|
138
|
+
const container = document.getElementById("that-open-app") ?? document.body;
|
|
139
|
+
container.appendChild(app);
|
|
140
|
+
document.body.style.margin = "0";
|
|
@@ -1,159 +1,159 @@
|
|
|
1
|
-
/* MD
|
|
2
|
-
## Persistent banners with top:banner
|
|
3
|
-
---
|
|
4
|
-
Some application states need a persistent, visible indicator that stays on
|
|
5
|
-
screen until the state is explicitly cleared — an archived-files mode, a
|
|
6
|
-
read-only permission warning, a degraded-connectivity notice. A toast
|
|
7
|
-
auto-dismisses after a few seconds, which is wrong for these cases. A modal
|
|
8
|
-
blocks interaction entirely, which is too heavy.
|
|
9
|
-
|
|
10
|
-
top-app provides a banner slot at the top of the layout that occupies real
|
|
11
|
-
space (pushes content down) and stays visible until dismissed programmatically.
|
|
12
|
-
Any descendant component can show or hide it by dispatching standard DOM events
|
|
13
|
-
— no reference to the root element required.
|
|
14
|
-
|
|
15
|
-
This tutorial covers showing a banner from a child component by dispatching
|
|
16
|
-
top:banner; customising the banner type, icon, message, and description;
|
|
17
|
-
making a banner user-dismissible with the dismissible flag; and hiding it
|
|
18
|
-
programmatically with top:banner:hide.
|
|
19
|
-
|
|
20
|
-
By the end, you'll have an app where a panel can activate and deactivate a
|
|
21
|
-
persistent banner that pushes the layout content down while it is visible.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
import { html, LitElement } from "lit";
|
|
25
|
-
import { customElement, state } from "lit/decorators.js";
|
|
26
|
-
import * as THREE from "three";
|
|
27
|
-
import * as OBC from "@thatopen/components";
|
|
28
|
-
import * as OBF from "@thatopen/components-front";
|
|
29
|
-
import * as FRAGS from "@thatopen/fragments";
|
|
30
|
-
import * as BUI from "@thatopen/ui";
|
|
31
|
-
import { PlatformClient, UIManager } from "@thatopen/services";
|
|
32
|
-
import type { App } from "../../index";
|
|
33
|
-
|
|
34
|
-
// A child component that controls a banner without holding a reference to top-app.
|
|
35
|
-
// It dispatches top:banner to show and top:banner:hide to remove it.
|
|
36
|
-
@customElement("banner-controller")
|
|
37
|
-
class BannerController extends LitElement {
|
|
38
|
-
@state() private _active = false;
|
|
39
|
-
|
|
40
|
-
private _showReadOnly() {
|
|
41
|
-
this._active = true;
|
|
42
|
-
|
|
43
|
-
// top:banner shows a persistent bar at the top of the nearest top-app ancestor.
|
|
44
|
-
// It replaces any existing banner — only one banner is shown at a time.
|
|
45
|
-
this.dispatchEvent(
|
|
46
|
-
new CustomEvent("top:banner", {
|
|
47
|
-
bubbles: true,
|
|
48
|
-
composed: true,
|
|
49
|
-
detail: {
|
|
50
|
-
type: "warning",
|
|
51
|
-
icon: "tabler:lock",
|
|
52
|
-
message: "Read-only mode",
|
|
53
|
-
description: "You do not have write permissions on this project.",
|
|
54
|
-
// dismissible:true adds a close button on the banner itself.
|
|
55
|
-
// Omit it (or set false) when the banner must only be cleared by code.
|
|
56
|
-
dismissible: true,
|
|
57
|
-
},
|
|
58
|
-
}),
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
private _showArchived() {
|
|
63
|
-
this._active = true;
|
|
64
|
-
|
|
65
|
-
// Calling top:banner again replaces the previous banner immediately.
|
|
66
|
-
this.dispatchEvent(
|
|
67
|
-
new CustomEvent("top:banner", {
|
|
68
|
-
bubbles: true,
|
|
69
|
-
composed: true,
|
|
70
|
-
detail: {
|
|
71
|
-
type: "warning",
|
|
72
|
-
icon: "tabler:archive",
|
|
73
|
-
message: "Viewing archived files",
|
|
74
|
-
description: "Archived items are permanently deleted after 30 days.",
|
|
75
|
-
// No dismissible — this banner should only be cleared when the mode exits.
|
|
76
|
-
},
|
|
77
|
-
}),
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
private _hide() {
|
|
82
|
-
this._active = false;
|
|
83
|
-
|
|
84
|
-
// top:banner:hide clears the current banner unconditionally.
|
|
85
|
-
this.dispatchEvent(
|
|
86
|
-
new CustomEvent("top:banner:hide", { bubbles: true, composed: true }),
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
render() {
|
|
91
|
-
return html`
|
|
92
|
-
<bim-panel label="Banner Controller">
|
|
93
|
-
<bim-panel-section label="Banners" icon="tabler:message">
|
|
94
|
-
<bim-button
|
|
95
|
-
label="Show read-only banner (dismissible)"
|
|
96
|
-
icon="tabler:lock"
|
|
97
|
-
@click=${this._showReadOnly}
|
|
98
|
-
></bim-button>
|
|
99
|
-
<bim-button
|
|
100
|
-
label="Show archived banner (programmatic only)"
|
|
101
|
-
icon="tabler:archive"
|
|
102
|
-
@click=${this._showArchived}
|
|
103
|
-
></bim-button>
|
|
104
|
-
<bim-button
|
|
105
|
-
label="Hide banner"
|
|
106
|
-
icon="tabler:x"
|
|
107
|
-
?disabled=${!this._active}
|
|
108
|
-
@click=${this._hide}
|
|
109
|
-
></bim-button>
|
|
110
|
-
</bim-panel-section>
|
|
111
|
-
</bim-panel>
|
|
112
|
-
`;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// ---- app wiring ----
|
|
117
|
-
|
|
118
|
-
const client = PlatformClient.fromPlatformContext();
|
|
119
|
-
const { components } = (await client.setup(
|
|
120
|
-
{ OBC, OBF, BUI, THREE, FRAGS },
|
|
121
|
-
{ uuid: UIManager.uuid },
|
|
122
|
-
)) as { components: OBC.Components };
|
|
123
|
-
components.get(UIManager).init();
|
|
124
|
-
|
|
125
|
-
const app = document.createElement("top-app") as unknown as App;
|
|
126
|
-
|
|
127
|
-
app.setup = (waitUntil) => {
|
|
128
|
-
waitUntil(
|
|
129
|
-
(async () => {
|
|
130
|
-
const fragments = components.get(OBC.FragmentsManager);
|
|
131
|
-
const workerUrl = await FRAGS.FragmentsModels.getWorker();
|
|
132
|
-
fragments.init(await FRAGS.toClassicWorker(workerUrl), {
|
|
133
|
-
classicWorker: true,
|
|
134
|
-
});
|
|
135
|
-
})(),
|
|
136
|
-
"Fragments Core",
|
|
137
|
-
);
|
|
138
|
-
return { components, client };
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
app.elements = {
|
|
142
|
-
viewer: () =>
|
|
143
|
-
html`<top-viewer><top-viewer-tools></top-viewer-tools></top-viewer>`,
|
|
144
|
-
panel: () => html`<banner-controller></banner-controller>`,
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
app.base = "viewer";
|
|
148
|
-
app.layouts = {
|
|
149
|
-
main: {
|
|
150
|
-
label: "Main",
|
|
151
|
-
icon: "solar:home-bold",
|
|
152
|
-
template: `"panel" 1fr / 22rem`,
|
|
153
|
-
},
|
|
154
|
-
};
|
|
155
|
-
app.layout = "main";
|
|
156
|
-
|
|
157
|
-
const container = document.getElementById("that-open-app") ?? document.body;
|
|
158
|
-
container.appendChild(app);
|
|
159
|
-
document.body.style.margin = "0";
|
|
1
|
+
/* MD
|
|
2
|
+
## Persistent banners with top:banner
|
|
3
|
+
---
|
|
4
|
+
Some application states need a persistent, visible indicator that stays on
|
|
5
|
+
screen until the state is explicitly cleared — an archived-files mode, a
|
|
6
|
+
read-only permission warning, a degraded-connectivity notice. A toast
|
|
7
|
+
auto-dismisses after a few seconds, which is wrong for these cases. A modal
|
|
8
|
+
blocks interaction entirely, which is too heavy.
|
|
9
|
+
|
|
10
|
+
top-app provides a banner slot at the top of the layout that occupies real
|
|
11
|
+
space (pushes content down) and stays visible until dismissed programmatically.
|
|
12
|
+
Any descendant component can show or hide it by dispatching standard DOM events
|
|
13
|
+
— no reference to the root element required.
|
|
14
|
+
|
|
15
|
+
This tutorial covers showing a banner from a child component by dispatching
|
|
16
|
+
top:banner; customising the banner type, icon, message, and description;
|
|
17
|
+
making a banner user-dismissible with the dismissible flag; and hiding it
|
|
18
|
+
programmatically with top:banner:hide.
|
|
19
|
+
|
|
20
|
+
By the end, you'll have an app where a panel can activate and deactivate a
|
|
21
|
+
persistent banner that pushes the layout content down while it is visible.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { html, LitElement } from "lit";
|
|
25
|
+
import { customElement, state } from "lit/decorators.js";
|
|
26
|
+
import * as THREE from "three";
|
|
27
|
+
import * as OBC from "@thatopen/components";
|
|
28
|
+
import * as OBF from "@thatopen/components-front";
|
|
29
|
+
import * as FRAGS from "@thatopen/fragments";
|
|
30
|
+
import * as BUI from "@thatopen/ui";
|
|
31
|
+
import { PlatformClient, UIManager } from "@thatopen/services";
|
|
32
|
+
import type { App } from "../../index";
|
|
33
|
+
|
|
34
|
+
// A child component that controls a banner without holding a reference to top-app.
|
|
35
|
+
// It dispatches top:banner to show and top:banner:hide to remove it.
|
|
36
|
+
@customElement("banner-controller")
|
|
37
|
+
class BannerController extends LitElement {
|
|
38
|
+
@state() private _active = false;
|
|
39
|
+
|
|
40
|
+
private _showReadOnly() {
|
|
41
|
+
this._active = true;
|
|
42
|
+
|
|
43
|
+
// top:banner shows a persistent bar at the top of the nearest top-app ancestor.
|
|
44
|
+
// It replaces any existing banner — only one banner is shown at a time.
|
|
45
|
+
this.dispatchEvent(
|
|
46
|
+
new CustomEvent("top:banner", {
|
|
47
|
+
bubbles: true,
|
|
48
|
+
composed: true,
|
|
49
|
+
detail: {
|
|
50
|
+
type: "warning",
|
|
51
|
+
icon: "tabler:lock",
|
|
52
|
+
message: "Read-only mode",
|
|
53
|
+
description: "You do not have write permissions on this project.",
|
|
54
|
+
// dismissible:true adds a close button on the banner itself.
|
|
55
|
+
// Omit it (or set false) when the banner must only be cleared by code.
|
|
56
|
+
dismissible: true,
|
|
57
|
+
},
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private _showArchived() {
|
|
63
|
+
this._active = true;
|
|
64
|
+
|
|
65
|
+
// Calling top:banner again replaces the previous banner immediately.
|
|
66
|
+
this.dispatchEvent(
|
|
67
|
+
new CustomEvent("top:banner", {
|
|
68
|
+
bubbles: true,
|
|
69
|
+
composed: true,
|
|
70
|
+
detail: {
|
|
71
|
+
type: "warning",
|
|
72
|
+
icon: "tabler:archive",
|
|
73
|
+
message: "Viewing archived files",
|
|
74
|
+
description: "Archived items are permanently deleted after 30 days.",
|
|
75
|
+
// No dismissible — this banner should only be cleared when the mode exits.
|
|
76
|
+
},
|
|
77
|
+
}),
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private _hide() {
|
|
82
|
+
this._active = false;
|
|
83
|
+
|
|
84
|
+
// top:banner:hide clears the current banner unconditionally.
|
|
85
|
+
this.dispatchEvent(
|
|
86
|
+
new CustomEvent("top:banner:hide", { bubbles: true, composed: true }),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
render() {
|
|
91
|
+
return html`
|
|
92
|
+
<bim-panel label="Banner Controller">
|
|
93
|
+
<bim-panel-section label="Banners" icon="tabler:message">
|
|
94
|
+
<bim-button
|
|
95
|
+
label="Show read-only banner (dismissible)"
|
|
96
|
+
icon="tabler:lock"
|
|
97
|
+
@click=${this._showReadOnly}
|
|
98
|
+
></bim-button>
|
|
99
|
+
<bim-button
|
|
100
|
+
label="Show archived banner (programmatic only)"
|
|
101
|
+
icon="tabler:archive"
|
|
102
|
+
@click=${this._showArchived}
|
|
103
|
+
></bim-button>
|
|
104
|
+
<bim-button
|
|
105
|
+
label="Hide banner"
|
|
106
|
+
icon="tabler:x"
|
|
107
|
+
?disabled=${!this._active}
|
|
108
|
+
@click=${this._hide}
|
|
109
|
+
></bim-button>
|
|
110
|
+
</bim-panel-section>
|
|
111
|
+
</bim-panel>
|
|
112
|
+
`;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---- app wiring ----
|
|
117
|
+
|
|
118
|
+
const client = PlatformClient.fromPlatformContext();
|
|
119
|
+
const { components } = (await client.setup(
|
|
120
|
+
{ OBC, OBF, BUI, THREE, FRAGS },
|
|
121
|
+
{ uuid: UIManager.uuid },
|
|
122
|
+
)) as { components: OBC.Components };
|
|
123
|
+
components.get(UIManager).init();
|
|
124
|
+
|
|
125
|
+
const app = document.createElement("top-app") as unknown as App;
|
|
126
|
+
|
|
127
|
+
app.setup = (waitUntil) => {
|
|
128
|
+
waitUntil(
|
|
129
|
+
(async () => {
|
|
130
|
+
const fragments = components.get(OBC.FragmentsManager);
|
|
131
|
+
const workerUrl = await FRAGS.FragmentsModels.getWorker();
|
|
132
|
+
fragments.init(await FRAGS.toClassicWorker(workerUrl), {
|
|
133
|
+
classicWorker: true,
|
|
134
|
+
});
|
|
135
|
+
})(),
|
|
136
|
+
"Fragments Core",
|
|
137
|
+
);
|
|
138
|
+
return { components, client };
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
app.elements = {
|
|
142
|
+
viewer: () =>
|
|
143
|
+
html`<top-viewer><top-viewer-tools></top-viewer-tools></top-viewer>`,
|
|
144
|
+
panel: () => html`<banner-controller></banner-controller>`,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
app.base = "viewer";
|
|
148
|
+
app.layouts = {
|
|
149
|
+
main: {
|
|
150
|
+
label: "Main",
|
|
151
|
+
icon: "solar:home-bold",
|
|
152
|
+
template: `"panel" 1fr / 22rem`,
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
app.layout = "main";
|
|
156
|
+
|
|
157
|
+
const container = document.getElementById("that-open-app") ?? document.body;
|
|
158
|
+
container.appendChild(app);
|
|
159
|
+
document.body.style.margin = "0";
|