@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.
Files changed (77) hide show
  1. package/README.md +62 -62
  2. package/dist/built-in/index.d.ts +66 -41
  3. package/dist/cli/lib/revit-addin.d.ts +8 -0
  4. package/dist/cli.js +154 -13
  5. package/dist/core/client.d.ts +6 -0
  6. package/dist/core/platform-client.d.ts +139 -0
  7. package/dist/core/platform-client.live.test.d.ts +1 -0
  8. package/dist/core/platform-client.notifications.test.d.ts +1 -0
  9. package/dist/index.cjs.js +2 -2
  10. package/dist/index.d.ts +1 -0
  11. package/dist/index.es.js +635 -445
  12. package/dist/types/notifications.d.ts +54 -0
  13. package/dist/vite-env.d.ts +1 -1
  14. package/docs/ai-quickstart.md +120 -1
  15. package/docs/cli/create.md +29 -29
  16. package/docs/cli/local-server.md +38 -38
  17. package/docs/cli/login.md +32 -32
  18. package/docs/cli/publish.md +31 -31
  19. package/docs/cli/run.md +26 -26
  20. package/docs/cli/serve.md +43 -43
  21. package/docs/cli/swap.md +30 -30
  22. package/docs/flow-plugin-guide.md +26 -0
  23. package/docs/revit-collab-quickstart.md +26 -17
  24. package/docs/rhino-interop-quickstart.md +74 -7
  25. package/package.json +1 -1
  26. package/resources/AGENTS.md +40 -40
  27. package/src/built-in/CDEManager/example.ts +214 -214
  28. package/src/built-in/CDEManager/examples/FileEditing/example.ts +226 -226
  29. package/src/built-in/CDEManager/examples/FileMoves/example.ts +186 -186
  30. package/src/built-in/CDEManager/examples/FileOperations/example.ts +295 -295
  31. package/src/built-in/CDEManager/examples/FileTransfers/example.ts +210 -210
  32. package/src/built-in/CDEManager/examples/FolderManagement/example.ts +230 -230
  33. package/src/built-in/CDEManager/examples/MetadataSchema/example.ts +230 -230
  34. package/src/built-in/CDEManager/examples/MetadataViews/example.ts +166 -166
  35. package/src/built-in/CDEManager/examples/ProjectConfig/example.ts +177 -177
  36. package/src/built-in/UIManager/src/app/example.ts +97 -97
  37. package/src/built-in/UIManager/src/app/examples/AppState/example.ts +140 -140
  38. package/src/built-in/UIManager/src/app/examples/Banner/example.ts +159 -159
  39. package/src/built-in/UIManager/src/app/examples/Contexts/example.ts +123 -123
  40. package/src/built-in/UIManager/src/app/examples/FileDrop/example.ts +124 -124
  41. package/src/built-in/UIManager/src/app/examples/FloatingLayout/example.ts +90 -90
  42. package/src/built-in/UIManager/src/app/examples/Modal/example.ts +163 -163
  43. package/src/built-in/UIManager/src/app/examples/Notifications/example.ts +133 -133
  44. package/src/built-in/UIManager/src/app/examples/ProjectFileStorage/example.ts +150 -150
  45. package/src/built-in/UIManager/src/app/examples/Sidebar/example.ts +102 -102
  46. package/src/built-in/UIManager/src/clashes-panel/example.ts +183 -183
  47. package/src/built-in/UIManager/src/git-history-panel/example.ts +70 -70
  48. package/src/built-in/UIManager/src/models-list/example.ts +122 -122
  49. package/src/built-in/UIManager/src/topics-panel/example.ts +170 -170
  50. package/src/built-in/index.ts +66 -41
  51. package/src/cli/templates/app/index.html +35 -35
  52. package/src/cli/templates/app/package.json +32 -32
  53. package/src/cli/templates/app/src/bim-components/CloudRunner/index.ts +95 -95
  54. package/src/cli/templates/app/src/bim-components/CloudRunner/src/index.ts +1 -1
  55. package/src/cli/templates/app/src/bim-components/CloudRunner/src/types.ts +5 -5
  56. package/src/cli/templates/app/src/bim-components/index.ts +1 -1
  57. package/src/cli/templates/app/src/globals.ts +1 -1
  58. package/src/cli/templates/app/src/main.history.ts +191 -0
  59. package/src/cli/templates/app/src/ui-components/app-info-section/index.ts +26 -26
  60. package/src/cli/templates/app/src/ui-components/app-info-section/src/index.ts +1 -1
  61. package/src/cli/templates/app/src/ui-components/app-info-section/src/types.ts +15 -15
  62. package/src/cli/templates/app/src/ui-components/cloud-runner-section/index.ts +37 -37
  63. package/src/cli/templates/app/src/ui-components/cloud-runner-section/src/index.ts +1 -1
  64. package/src/cli/templates/app/src/ui-components/cloud-runner-section/src/types.ts +14 -14
  65. package/src/cli/templates/app/src/ui-components/index.ts +2 -2
  66. package/src/cli/templates/app/tsconfig.json +16 -16
  67. package/src/cli/templates/app/vite.config.js +57 -57
  68. package/src/cli/templates/cloud-component/README.md +205 -205
  69. package/src/cli/templates/cloud-component/_thatopen +5 -5
  70. package/src/cli/templates/cloud-component/declarations.json +4 -4
  71. package/src/cli/templates/cloud-component/package.json +21 -21
  72. package/src/cli/templates/cloud-component/src/main.ts +70 -70
  73. package/src/cli/templates/cloud-component/tsconfig.json +16 -16
  74. package/src/cli/templates/cloud-component/vite.config.js +46 -46
  75. package/src/cli/templates/shared/AGENTS.md +8 -8
  76. package/src/cli/templates/shared/CLAUDE.md +1 -1
  77. package/src/cli/templates/shared/_gitignore +5 -5
@@ -1,133 +1,133 @@
1
- /* MD
2
- ## Toasts and cross-component notifications
3
- ---
4
- Developers building BIM processing UIs need to surface status updates, errors,
5
- and async feedback to users from different points in the component tree —
6
- but reaching back to the app root to trigger a notification from a deeply
7
- nested component requires threading a reference through every layer in between.
8
-
9
- top-app provides a built-in toast system that any descendant can trigger by
10
- dispatching a standard DOM event. Toasts are auto-dismissed after a type-based
11
- duration and stack at the bottom-left of the screen.
12
-
13
- This tutorial covers showing a toast on app boot via showToast; building a
14
- component that dispatches top:notification to report async progress and
15
- completion without a reference to the root; and using the four toast types
16
- (success, error, warning, info) with their respective durations.
17
-
18
- By the end, you'll have an app with a boot confirmation toast and a panel that
19
- reports async work status without holding any reference to top-app.
20
- */
21
-
22
- import { html, LitElement } from "lit";
23
- import { customElement, state } from "lit/decorators.js";
24
- import * as THREE from "three";
25
- import * as OBC from "@thatopen/components";
26
- import * as OBF from "@thatopen/components-front";
27
- import * as FRAGS from "@thatopen/fragments";
28
- import * as BUI from "@thatopen/ui";
29
- import { PlatformClient, UIManager } from "@thatopen/services";
30
- import type { App, AppReadyEvent } from "../../index";
31
-
32
- // A child component that dispatches top:notification to surface messages
33
- // to the nearest top-app ancestor — no reference to the app element needed.
34
- @customElement("status-reporter")
35
- class StatusReporter extends LitElement {
36
- @state() private _busy = false;
37
-
38
- private _simulateWork() {
39
- this._busy = true;
40
-
41
- // Dispatching top:notification from any descendant of top-app shows a toast.
42
- // bubbles:true + composed:true lets it cross shadow-DOM boundaries.
43
- this.dispatchEvent(
44
- new CustomEvent("top:notification", {
45
- bubbles: true,
46
- composed: true,
47
- detail: { message: "Analysis started", type: "info" },
48
- }),
49
- );
50
-
51
- setTimeout(() => {
52
- this._busy = false;
53
- this.dispatchEvent(
54
- new CustomEvent("top:notification", {
55
- bubbles: true,
56
- composed: true,
57
- detail: {
58
- message: "Analysis complete",
59
- type: "success",
60
- description: "Found 342 elements across 5 categories.",
61
- },
62
- }),
63
- );
64
- }, 2000);
65
- }
66
-
67
- render() {
68
- return html`
69
- <bim-panel label="Reporter">
70
- <bim-panel-section label="Actions" icon="solar:play-bold">
71
- <bim-button
72
- label="Run Analysis"
73
- icon="solar:play-bold"
74
- .loading=${this._busy}
75
- @click=${this._simulateWork}
76
- ></bim-button>
77
- </bim-panel-section>
78
- </bim-panel>
79
- `;
80
- }
81
- }
82
-
83
- // ---- app wiring ----
84
-
85
- const client = PlatformClient.fromPlatformContext();
86
- const { components } = (await client.setup(
87
- { OBC, OBF, BUI, THREE, FRAGS },
88
- { uuid: UIManager.uuid },
89
- )) as { components: OBC.Components };
90
- components.get(UIManager).init();
91
-
92
- const app = document.createElement("top-app") as unknown as App;
93
-
94
- app.setup = (waitUntil) => {
95
- waitUntil(
96
- (async () => {
97
- const fragments = components.get(OBC.FragmentsManager);
98
- const workerUrl = await FRAGS.FragmentsModels.getWorker();
99
- fragments.init(await FRAGS.toClassicWorker(workerUrl), {
100
- classicWorker: true,
101
- });
102
- })(),
103
- "Fragments Core",
104
- );
105
- return { components, client };
106
- };
107
-
108
- app.elements = {
109
- viewer: () =>
110
- html`<top-viewer><top-viewer-tools></top-viewer-tools></top-viewer>`,
111
- panel: () => html`<status-reporter></status-reporter>`,
112
- };
113
-
114
- app.base = "viewer";
115
- app.layouts = {
116
- main: {
117
- label: "Main",
118
- icon: "solar:home-bold",
119
- template: `"panel" 1fr / 22rem`,
120
- },
121
- };
122
- app.layout = "main";
123
-
124
- // top:app-ready fires after all waitUntil promises resolve.
125
- // e.detail.app is a reference to the top-app element itself.
126
- app.addEventListener("top:app-ready", (e) => {
127
- const { app: readyApp } = (e as AppReadyEvent).detail;
128
- readyApp.showToast("App ready", "success");
129
- });
130
-
131
- const container = document.getElementById("that-open-app") ?? document.body;
132
- container.appendChild(app);
133
- document.body.style.margin = "0";
1
+ /* MD
2
+ ## Toasts and cross-component notifications
3
+ ---
4
+ Developers building BIM processing UIs need to surface status updates, errors,
5
+ and async feedback to users from different points in the component tree —
6
+ but reaching back to the app root to trigger a notification from a deeply
7
+ nested component requires threading a reference through every layer in between.
8
+
9
+ top-app provides a built-in toast system that any descendant can trigger by
10
+ dispatching a standard DOM event. Toasts are auto-dismissed after a type-based
11
+ duration and stack at the bottom-left of the screen.
12
+
13
+ This tutorial covers showing a toast on app boot via showToast; building a
14
+ component that dispatches top:notification to report async progress and
15
+ completion without a reference to the root; and using the four toast types
16
+ (success, error, warning, info) with their respective durations.
17
+
18
+ By the end, you'll have an app with a boot confirmation toast and a panel that
19
+ reports async work status without holding any reference to top-app.
20
+ */
21
+
22
+ import { html, LitElement } from "lit";
23
+ import { customElement, state } from "lit/decorators.js";
24
+ import * as THREE from "three";
25
+ import * as OBC from "@thatopen/components";
26
+ import * as OBF from "@thatopen/components-front";
27
+ import * as FRAGS from "@thatopen/fragments";
28
+ import * as BUI from "@thatopen/ui";
29
+ import { PlatformClient, UIManager } from "@thatopen/services";
30
+ import type { App, AppReadyEvent } from "../../index";
31
+
32
+ // A child component that dispatches top:notification to surface messages
33
+ // to the nearest top-app ancestor — no reference to the app element needed.
34
+ @customElement("status-reporter")
35
+ class StatusReporter extends LitElement {
36
+ @state() private _busy = false;
37
+
38
+ private _simulateWork() {
39
+ this._busy = true;
40
+
41
+ // Dispatching top:notification from any descendant of top-app shows a toast.
42
+ // bubbles:true + composed:true lets it cross shadow-DOM boundaries.
43
+ this.dispatchEvent(
44
+ new CustomEvent("top:notification", {
45
+ bubbles: true,
46
+ composed: true,
47
+ detail: { message: "Analysis started", type: "info" },
48
+ }),
49
+ );
50
+
51
+ setTimeout(() => {
52
+ this._busy = false;
53
+ this.dispatchEvent(
54
+ new CustomEvent("top:notification", {
55
+ bubbles: true,
56
+ composed: true,
57
+ detail: {
58
+ message: "Analysis complete",
59
+ type: "success",
60
+ description: "Found 342 elements across 5 categories.",
61
+ },
62
+ }),
63
+ );
64
+ }, 2000);
65
+ }
66
+
67
+ render() {
68
+ return html`
69
+ <bim-panel label="Reporter">
70
+ <bim-panel-section label="Actions" icon="solar:play-bold">
71
+ <bim-button
72
+ label="Run Analysis"
73
+ icon="solar:play-bold"
74
+ .loading=${this._busy}
75
+ @click=${this._simulateWork}
76
+ ></bim-button>
77
+ </bim-panel-section>
78
+ </bim-panel>
79
+ `;
80
+ }
81
+ }
82
+
83
+ // ---- app wiring ----
84
+
85
+ const client = PlatformClient.fromPlatformContext();
86
+ const { components } = (await client.setup(
87
+ { OBC, OBF, BUI, THREE, FRAGS },
88
+ { uuid: UIManager.uuid },
89
+ )) as { components: OBC.Components };
90
+ components.get(UIManager).init();
91
+
92
+ const app = document.createElement("top-app") as unknown as App;
93
+
94
+ app.setup = (waitUntil) => {
95
+ waitUntil(
96
+ (async () => {
97
+ const fragments = components.get(OBC.FragmentsManager);
98
+ const workerUrl = await FRAGS.FragmentsModels.getWorker();
99
+ fragments.init(await FRAGS.toClassicWorker(workerUrl), {
100
+ classicWorker: true,
101
+ });
102
+ })(),
103
+ "Fragments Core",
104
+ );
105
+ return { components, client };
106
+ };
107
+
108
+ app.elements = {
109
+ viewer: () =>
110
+ html`<top-viewer><top-viewer-tools></top-viewer-tools></top-viewer>`,
111
+ panel: () => html`<status-reporter></status-reporter>`,
112
+ };
113
+
114
+ app.base = "viewer";
115
+ app.layouts = {
116
+ main: {
117
+ label: "Main",
118
+ icon: "solar:home-bold",
119
+ template: `"panel" 1fr / 22rem`,
120
+ },
121
+ };
122
+ app.layout = "main";
123
+
124
+ // top:app-ready fires after all waitUntil promises resolve.
125
+ // e.detail.app is a reference to the top-app element itself.
126
+ app.addEventListener("top:app-ready", (e) => {
127
+ const { app: readyApp } = (e as AppReadyEvent).detail;
128
+ readyApp.showToast("App ready", "success");
129
+ });
130
+
131
+ const container = document.getElementById("that-open-app") ?? document.body;
132
+ container.appendChild(app);
133
+ document.body.style.margin = "0";
@@ -1,150 +1,150 @@
1
- /* MD
2
- ## Persisting project-scoped data with projectStorageContext
3
- ---
4
- Most non-trivial apps built on top-app eventually need to persist some of
5
- their own data inside the project — user preferences, cached results,
6
- domain state that isn't a 3D model or a regular CDE file. The naive
7
- approach is for each panel/manager to talk to the CDE client directly:
8
- resolve or create a folder under `__project_data`, resolve or create the
9
- file, decide between uploading a new file and versioning an existing one.
10
- That's the same handful of steps every single consumer ends up
11
- re-implementing.
12
-
13
- top-app centralizes exactly that plumbing behind `projectStorageContext`, a
14
- `@lit/context` value any descendant can `@consume`. It provides a small,
15
- stable service object — not reactive state, since `save`/`flush` are
16
- actions, not values to observe:
17
-
18
- ```ts
19
- interface ProjectStorage {
20
- save(path: string, blob: Blob): void; // debounced (~2s), per path
21
- flush(): Promise<void>; // cancel debounces, save everything NOW, await it
22
- }
23
- ```
24
-
25
- `path` is resolved under `__project_data/`. Calling `save` schedules a
26
- debounced upload — top-app creates whichever folders in the path don't
27
- exist yet, creates the file the first time, and versions it (no explicit
28
- `versionTag` needed — the backend assigns the next one) every time after.
29
- Saves are debounced *per path*, so unrelated consumers saving around the
30
- same time don't reset each other's timers, and a small floating indicator
31
- (bottom-right, so it doesn't rely on the sidebar, which is optional and can
32
- be hidden) reflects both phases distinctly: just the icon, no background/
33
- border, while a save is debouncing (nothing to tell the user yet — it's
34
- still waiting to see if more changes come in), and the full "Saving..."
35
- pill once an upload is actually in flight. Each consumer only ever decides
36
- *what* to save and *where* — never the CDE mechanics, nor the indicator.
37
-
38
- Use `flush()` whenever you're about to do something that would conflict
39
- with a save still in flight — e.g. triggering a cloud job that reads the
40
- same file you just edited. It cancels any pending debounce, starts the
41
- upload right away, and its returned promise resolves only once that
42
- upload has actually finished.
43
-
44
- By the end, you'll have a tiny "project notes" panel that autosaves free
45
- text to `__project_data/notes/notes.json`, plus a "Save now" button that
46
- demonstrates `flush()`.
47
- */
48
-
49
- import { html, LitElement } from "lit";
50
- import { customElement, state } from "lit/decorators.js";
51
- import { consume } from "@lit/context";
52
- import * as THREE from "three";
53
- import * as OBC from "@thatopen/components";
54
- import * as OBF from "@thatopen/components-front";
55
- import * as FRAGS from "@thatopen/fragments";
56
- import * as BUI from "@thatopen/ui";
57
- import { PlatformClient, UIManager } from "@thatopen/services";
58
- import type { App } from "../../index";
59
- import { projectStorageContext, type ProjectStorage } from "../../src/context";
60
-
61
- // The path is always relative to `__project_data/` — top-app creates
62
- // whichever folders in it don't exist yet.
63
- const NOTES_PATH = "notes/notes.json";
64
-
65
- function notesBlob(text: string): Blob {
66
- return new Blob([JSON.stringify({ text, savedAt: new Date().toISOString() })], {
67
- type: "application/json",
68
- });
69
- }
70
-
71
- @customElement("notes-panel")
72
- class NotesPanel extends LitElement {
73
- @consume({ context: projectStorageContext })
74
- private _storage?: ProjectStorage;
75
-
76
- @state() private _text = "";
77
- @state() private _flushing = false;
78
-
79
- render() {
80
- return html`
81
- <bim-panel label="Project Notes">
82
- <bim-panel-section label="Notes" icon="tabler:notes">
83
- <bim-text-input
84
- vertical
85
- type="area"
86
- label="Note"
87
- placeholder="Type something — autosaves as you go"
88
- .value=${this._text}
89
- @input=${(e: Event) => {
90
- this._text = (e.target as HTMLInputElement).value ?? "";
91
- // Debounced — top-app coalesces rapid edits into one upload.
92
- this._storage?.save(NOTES_PATH, notesBlob(this._text));
93
- }}
94
- ></bim-text-input>
95
- <bim-button
96
- label="Save now"
97
- icon="tabler:cloud-upload"
98
- .loading=${this._flushing}
99
- @click=${async () => {
100
- // Same blob, but flush() cancels the debounce and waits for
101
- // the upload to actually finish before this resolves.
102
- this._storage?.save(NOTES_PATH, notesBlob(this._text));
103
- this._flushing = true;
104
- await this._storage?.flush();
105
- this._flushing = false;
106
- }}
107
- ></bim-button>
108
- </bim-panel-section>
109
- </bim-panel>
110
- `;
111
- }
112
- }
113
-
114
- // ---- app wiring ----
115
-
116
- const client = PlatformClient.fromPlatformContext();
117
- const { components } = (await client.setup(
118
- { OBC, OBF, BUI, THREE, FRAGS },
119
- { uuid: UIManager.uuid },
120
- )) as { components: OBC.Components };
121
- components.get(UIManager).init();
122
-
123
- const app = document.createElement("top-app") as unknown as App;
124
-
125
- app.setup = (waitUntil) => {
126
- waitUntil(
127
- (async () => {
128
- const fragments = components.get(OBC.FragmentsManager);
129
- const workerUrl = await FRAGS.FragmentsModels.getWorker();
130
- fragments.init(await FRAGS.toClassicWorker(workerUrl), { classicWorker: true });
131
- })(),
132
- "Fragments Core",
133
- );
134
- return { components, client };
135
- };
136
-
137
- app.elements = {
138
- viewer: () => html`<top-viewer><top-viewer-tools></top-viewer-tools></top-viewer>`,
139
- panel: () => html`<notes-panel></notes-panel>`,
140
- };
141
-
142
- app.base = "viewer";
143
- app.layouts = {
144
- main: { label: "Main", icon: "solar:home-bold", template: `"panel" 1fr / 22rem` },
145
- };
146
- app.layout = "main";
147
-
148
- const container = document.getElementById("that-open-app") ?? document.body;
149
- container.appendChild(app);
150
- document.body.style.margin = "0";
1
+ /* MD
2
+ ## Persisting project-scoped data with projectStorageContext
3
+ ---
4
+ Most non-trivial apps built on top-app eventually need to persist some of
5
+ their own data inside the project — user preferences, cached results,
6
+ domain state that isn't a 3D model or a regular CDE file. The naive
7
+ approach is for each panel/manager to talk to the CDE client directly:
8
+ resolve or create a folder under `__project_data`, resolve or create the
9
+ file, decide between uploading a new file and versioning an existing one.
10
+ That's the same handful of steps every single consumer ends up
11
+ re-implementing.
12
+
13
+ top-app centralizes exactly that plumbing behind `projectStorageContext`, a
14
+ `@lit/context` value any descendant can `@consume`. It provides a small,
15
+ stable service object — not reactive state, since `save`/`flush` are
16
+ actions, not values to observe:
17
+
18
+ ```ts
19
+ interface ProjectStorage {
20
+ save(path: string, blob: Blob): void; // debounced (~2s), per path
21
+ flush(): Promise<void>; // cancel debounces, save everything NOW, await it
22
+ }
23
+ ```
24
+
25
+ `path` is resolved under `__project_data/`. Calling `save` schedules a
26
+ debounced upload — top-app creates whichever folders in the path don't
27
+ exist yet, creates the file the first time, and versions it (no explicit
28
+ `versionTag` needed — the backend assigns the next one) every time after.
29
+ Saves are debounced *per path*, so unrelated consumers saving around the
30
+ same time don't reset each other's timers, and a small floating indicator
31
+ (bottom-right, so it doesn't rely on the sidebar, which is optional and can
32
+ be hidden) reflects both phases distinctly: just the icon, no background/
33
+ border, while a save is debouncing (nothing to tell the user yet — it's
34
+ still waiting to see if more changes come in), and the full "Saving..."
35
+ pill once an upload is actually in flight. Each consumer only ever decides
36
+ *what* to save and *where* — never the CDE mechanics, nor the indicator.
37
+
38
+ Use `flush()` whenever you're about to do something that would conflict
39
+ with a save still in flight — e.g. triggering a cloud job that reads the
40
+ same file you just edited. It cancels any pending debounce, starts the
41
+ upload right away, and its returned promise resolves only once that
42
+ upload has actually finished.
43
+
44
+ By the end, you'll have a tiny "project notes" panel that autosaves free
45
+ text to `__project_data/notes/notes.json`, plus a "Save now" button that
46
+ demonstrates `flush()`.
47
+ */
48
+
49
+ import { html, LitElement } from "lit";
50
+ import { customElement, state } from "lit/decorators.js";
51
+ import { consume } from "@lit/context";
52
+ import * as THREE from "three";
53
+ import * as OBC from "@thatopen/components";
54
+ import * as OBF from "@thatopen/components-front";
55
+ import * as FRAGS from "@thatopen/fragments";
56
+ import * as BUI from "@thatopen/ui";
57
+ import { PlatformClient, UIManager } from "@thatopen/services";
58
+ import type { App } from "../../index";
59
+ import { projectStorageContext, type ProjectStorage } from "../../src/context";
60
+
61
+ // The path is always relative to `__project_data/` — top-app creates
62
+ // whichever folders in it don't exist yet.
63
+ const NOTES_PATH = "notes/notes.json";
64
+
65
+ function notesBlob(text: string): Blob {
66
+ return new Blob([JSON.stringify({ text, savedAt: new Date().toISOString() })], {
67
+ type: "application/json",
68
+ });
69
+ }
70
+
71
+ @customElement("notes-panel")
72
+ class NotesPanel extends LitElement {
73
+ @consume({ context: projectStorageContext })
74
+ private _storage?: ProjectStorage;
75
+
76
+ @state() private _text = "";
77
+ @state() private _flushing = false;
78
+
79
+ render() {
80
+ return html`
81
+ <bim-panel label="Project Notes">
82
+ <bim-panel-section label="Notes" icon="tabler:notes">
83
+ <bim-text-input
84
+ vertical
85
+ type="area"
86
+ label="Note"
87
+ placeholder="Type something — autosaves as you go"
88
+ .value=${this._text}
89
+ @input=${(e: Event) => {
90
+ this._text = (e.target as HTMLInputElement).value ?? "";
91
+ // Debounced — top-app coalesces rapid edits into one upload.
92
+ this._storage?.save(NOTES_PATH, notesBlob(this._text));
93
+ }}
94
+ ></bim-text-input>
95
+ <bim-button
96
+ label="Save now"
97
+ icon="tabler:cloud-upload"
98
+ .loading=${this._flushing}
99
+ @click=${async () => {
100
+ // Same blob, but flush() cancels the debounce and waits for
101
+ // the upload to actually finish before this resolves.
102
+ this._storage?.save(NOTES_PATH, notesBlob(this._text));
103
+ this._flushing = true;
104
+ await this._storage?.flush();
105
+ this._flushing = false;
106
+ }}
107
+ ></bim-button>
108
+ </bim-panel-section>
109
+ </bim-panel>
110
+ `;
111
+ }
112
+ }
113
+
114
+ // ---- app wiring ----
115
+
116
+ const client = PlatformClient.fromPlatformContext();
117
+ const { components } = (await client.setup(
118
+ { OBC, OBF, BUI, THREE, FRAGS },
119
+ { uuid: UIManager.uuid },
120
+ )) as { components: OBC.Components };
121
+ components.get(UIManager).init();
122
+
123
+ const app = document.createElement("top-app") as unknown as App;
124
+
125
+ app.setup = (waitUntil) => {
126
+ waitUntil(
127
+ (async () => {
128
+ const fragments = components.get(OBC.FragmentsManager);
129
+ const workerUrl = await FRAGS.FragmentsModels.getWorker();
130
+ fragments.init(await FRAGS.toClassicWorker(workerUrl), { classicWorker: true });
131
+ })(),
132
+ "Fragments Core",
133
+ );
134
+ return { components, client };
135
+ };
136
+
137
+ app.elements = {
138
+ viewer: () => html`<top-viewer><top-viewer-tools></top-viewer-tools></top-viewer>`,
139
+ panel: () => html`<notes-panel></notes-panel>`,
140
+ };
141
+
142
+ app.base = "viewer";
143
+ app.layouts = {
144
+ main: { label: "Main", icon: "solar:home-bold", template: `"panel" 1fr / 22rem` },
145
+ };
146
+ app.layout = "main";
147
+
148
+ const container = document.getElementById("that-open-app") ?? document.body;
149
+ container.appendChild(app);
150
+ document.body.style.margin = "0";