@thatopen/services 0.1.3 → 0.3.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 (120) hide show
  1. package/README.md +59 -46
  2. package/dist/built-in/index.d.ts +486 -563
  3. package/dist/cli/lib/npmrc.d.ts +28 -0
  4. package/dist/cli/lib/npmrc.test.d.ts +1 -0
  5. package/dist/cli.js +91 -7
  6. package/dist/core/client.d.ts +10 -0
  7. package/dist/index.cjs.js +2 -2
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.es.js +42 -32
  10. package/dist/types/npm.d.ts +14 -0
  11. package/dist/vite-env.d.ts +1 -1
  12. package/docs/builtin/paths.json +74 -70
  13. package/docs/cli/create.md +28 -28
  14. package/docs/cli/local-server.md +38 -38
  15. package/docs/cli/login.md +32 -32
  16. package/docs/cli/publish.md +31 -31
  17. package/docs/cli/run.md +26 -26
  18. package/docs/cli/serve.md +43 -43
  19. package/docs/cli/swap.md +30 -30
  20. package/package.json +100 -100
  21. package/resources/AGENTS.md +40 -40
  22. package/src/built-in/CDEManager/example.ts +214 -214
  23. package/src/built-in/CDEManager/examples/FileEditing/example.ts +226 -226
  24. package/src/built-in/CDEManager/examples/FileMoves/example.ts +186 -186
  25. package/src/built-in/CDEManager/examples/FileOperations/example.ts +295 -295
  26. package/src/built-in/CDEManager/examples/FileTransfers/example.ts +210 -210
  27. package/src/built-in/CDEManager/examples/FolderManagement/example.ts +230 -230
  28. package/src/built-in/CDEManager/examples/MetadataSchema/example.ts +230 -230
  29. package/src/built-in/CDEManager/examples/MetadataViews/example.ts +166 -166
  30. package/src/built-in/CDEManager/examples/ProjectConfig/example.ts +177 -177
  31. package/src/built-in/UIManager/src/app/example.ts +99 -99
  32. package/src/built-in/UIManager/src/app/examples/AppState/example.ts +142 -142
  33. package/src/built-in/UIManager/src/app/examples/Banner/example.ts +161 -0
  34. package/src/built-in/UIManager/src/app/examples/Contexts/example.ts +125 -125
  35. package/src/built-in/UIManager/src/app/examples/FileDrop/example.ts +126 -0
  36. package/src/built-in/UIManager/src/app/examples/FloatingLayout/example.ts +92 -92
  37. package/src/built-in/UIManager/src/app/examples/Modal/example.ts +165 -0
  38. package/src/built-in/UIManager/src/app/examples/Notifications/example.ts +135 -161
  39. package/src/built-in/UIManager/src/app/examples/Sidebar/example.ts +104 -104
  40. package/src/built-in/index.ts +649 -755
  41. package/src/cli/templates/app/README.md +241 -241
  42. package/src/cli/templates/app/index.html +35 -27
  43. package/src/cli/templates/app/package.json +31 -27
  44. package/src/cli/templates/app/src/app.ts +37 -0
  45. package/src/cli/templates/app/src/assets/tool-placeholder.ts +50 -0
  46. package/src/cli/templates/app/src/bim-components/CloudRunner/index.ts +95 -87
  47. package/src/cli/templates/app/src/bim-components/CloudRunner/src/index.ts +1 -1
  48. package/src/cli/templates/app/src/bim-components/CloudRunner/src/types.ts +5 -5
  49. package/src/cli/templates/app/src/bim-components/index.ts +1 -1
  50. package/src/cli/templates/app/src/globals.ts +1 -0
  51. package/src/cli/templates/app/src/main.ts +340 -56
  52. package/src/cli/templates/app/src/setups/active-tool-hud.ts +69 -0
  53. package/src/cli/templates/app/src/setups/camera-tools.ts +63 -0
  54. package/src/cli/templates/app/src/setups/camera-views.ts +201 -0
  55. package/src/cli/templates/app/src/setups/card-header.ts +35 -0
  56. package/src/cli/templates/app/src/setups/clipper-panel.ts +167 -0
  57. package/src/cli/templates/app/src/setups/clipper-tool.ts +754 -0
  58. package/src/cli/templates/app/src/setups/clipper.ts +75 -0
  59. package/src/cli/templates/app/src/setups/cloud-runner.ts +13 -9
  60. package/src/cli/templates/app/src/setups/commands-panel.ts +239 -0
  61. package/src/cli/templates/app/src/setups/data-table-panel.ts +2224 -0
  62. package/src/cli/templates/app/src/setups/exploded-view.ts +246 -0
  63. package/src/cli/templates/app/src/setups/file-format-icons.ts +95 -0
  64. package/src/cli/templates/app/src/setups/files-panel.ts +1503 -0
  65. package/src/cli/templates/app/src/setups/fps-indicator.ts +63 -0
  66. package/src/cli/templates/app/src/setups/graphics-panel.ts +490 -0
  67. package/src/cli/templates/app/src/setups/helper-panel.ts +88 -0
  68. package/src/cli/templates/app/src/setups/hider.ts +98 -0
  69. package/src/cli/templates/app/src/setups/index.ts +33 -3
  70. package/src/cli/templates/app/src/setups/inspection.ts +136 -0
  71. package/src/cli/templates/app/src/setups/measurement-panel.ts +188 -0
  72. package/src/cli/templates/app/src/setups/measurement-settings-panel.ts +161 -0
  73. package/src/cli/templates/app/src/setups/measurement-tool.ts +570 -0
  74. package/src/cli/templates/app/src/setups/measurements.ts +133 -0
  75. package/src/cli/templates/app/src/setups/model-tree.ts +797 -0
  76. package/src/cli/templates/app/src/setups/navigation-gizmo.ts +316 -0
  77. package/src/cli/templates/app/src/setups/objects-panel.ts +127 -0
  78. package/src/cli/templates/app/src/setups/plans-panel.ts +619 -0
  79. package/src/cli/templates/app/src/setups/properties-panel.ts +1048 -0
  80. package/src/cli/templates/app/src/setups/reality-capture/lib/hidden-tiles-plugin.ts +88 -0
  81. package/src/cli/templates/app/src/setups/reality-capture/lib/point-tile-plugin.ts +166 -0
  82. package/src/cli/templates/app/src/setups/reality-capture/lib/render-frame.ts +31 -0
  83. package/src/cli/templates/app/src/setups/reality-capture/lib/splat-tile-plugin.ts +51 -0
  84. package/src/cli/templates/app/src/setups/reality-capture-viewer.ts +1383 -0
  85. package/src/cli/templates/app/src/setups/right-sidebar.ts +178 -0
  86. package/src/cli/templates/app/src/setups/settings-panel.ts +94 -0
  87. package/src/cli/templates/app/src/setups/styles-panel.ts +110 -0
  88. package/src/cli/templates/app/src/setups/styles.ts +375 -0
  89. package/src/cli/templates/app/src/setups/tool-mode-manager.ts +150 -0
  90. package/src/cli/templates/app/src/setups/tool-mode.ts +23 -0
  91. package/src/cli/templates/app/src/setups/toolbar.ts +229 -0
  92. package/src/cli/templates/app/src/setups/ui-manager.ts +27 -0
  93. package/src/cli/templates/app/src/setups/visibility-toolbar.ts +498 -0
  94. package/src/cli/templates/app/src/setups/walkthrough.ts +528 -0
  95. package/src/cli/templates/app/src/ui-components/app-info-section/index.ts +26 -0
  96. package/src/cli/templates/app/src/ui-components/app-info-section/src/index.ts +1 -0
  97. package/src/cli/templates/app/src/ui-components/app-info-section/src/types.ts +15 -0
  98. package/src/cli/templates/app/src/ui-components/cloud-runner-section/index.ts +37 -0
  99. package/src/cli/templates/app/src/ui-components/cloud-runner-section/src/index.ts +1 -0
  100. package/src/cli/templates/app/src/ui-components/cloud-runner-section/src/types.ts +14 -0
  101. package/src/cli/templates/app/src/ui-components/index.ts +2 -1
  102. package/src/cli/templates/app/tsconfig.json +16 -17
  103. package/src/cli/templates/app/vite.config.js +45 -42
  104. package/src/cli/templates/cloud-component/README.md +205 -205
  105. package/src/cli/templates/cloud-component/_thatopen +5 -5
  106. package/src/cli/templates/cloud-component/declarations.json +4 -4
  107. package/src/cli/templates/cloud-component/package.json +21 -21
  108. package/src/cli/templates/cloud-component/src/main.ts +70 -70
  109. package/src/cli/templates/cloud-component/tsconfig.json +16 -16
  110. package/src/cli/templates/cloud-component/vite.config.js +46 -46
  111. package/src/cli/templates/shared/AGENTS.md +8 -8
  112. package/src/cli/templates/shared/CLAUDE.md +1 -1
  113. package/src/cli/templates/shared/_gitignore +5 -4
  114. package/src/built-in/AppManager/example.ts +0 -86
  115. package/src/built-in/FileList/example.ts +0 -107
  116. package/src/built-in/TabbedNavigation/example.ts +0 -115
  117. package/src/built-in/ViewportsManager/example.ts +0 -106
  118. package/src/cli/templates/app/src/setups/fragments.ts +0 -18
  119. package/src/cli/templates/app/src/setups/ifc-loader.ts +0 -12
  120. package/src/cli/templates/app/src/ui-components/AppPanel/index.ts +0 -160
@@ -0,0 +1,75 @@
1
+ import * as OBC from "@thatopen/components";
2
+ import * as OBF from "@thatopen/components-front";
3
+ import type { ModeTool } from "./tool-mode";
4
+
5
+ /**
6
+ * Section / clipping-planes tool. Wraps `OBC.Clipper` as a toolbar mode tool:
7
+ *
8
+ * - double-click on the model → create a clip plane on the surface under the
9
+ * pointer (raycast; the plane's normal is the hit face normal),
10
+ * - drag the plane's arrow gizmo to move it along its normal (handled by the
11
+ * Clipper's built-in TransformControls while the mode is active),
12
+ * - Delete / Backspace → delete the plane under the cursor,
13
+ * - Shift + Delete / Shift + Backspace → clear ALL section planes.
14
+ *
15
+ * Clipping is applied through `renderer.three.clippingPlanes` with
16
+ * `localClippingEnabled = true` (set by the SimpleRenderer), so the cut is
17
+ * honored during the deferred pipeline's single capture render — no
18
+ * deferred-lib change is needed. (Worth an explicit live check that the section
19
+ * actually cuts geometry in deferred mode.)
20
+ *
21
+ * Leaving the mode keeps the existing section planes cutting the model (so a
22
+ * section persists while navigating); it only stops creating/dragging new ones.
23
+ */
24
+ export const clipper = (
25
+ components: OBC.Components,
26
+ world: OBC.World,
27
+ ): ModeTool => {
28
+ const clipperComp = components.get(OBC.Clipper);
29
+
30
+ const canvas = (world.renderer as OBF.PostproductionRenderer | null)?.three
31
+ .domElement;
32
+ if (!canvas) {
33
+ throw new Error("clipper setup: world has no renderer with a canvas");
34
+ }
35
+
36
+ const onDblClick = () => {
37
+ void clipperComp.create(world);
38
+ };
39
+
40
+ const onKeyDown = (event: KeyboardEvent) => {
41
+ if (event.code !== "Delete" && event.code !== "Backspace") return;
42
+ if (event.shiftKey) {
43
+ clipperComp.deleteAll();
44
+ } else {
45
+ // No-op when nothing is hovered (the Clipper raycasts for a plane).
46
+ void clipperComp.delete(world);
47
+ }
48
+ };
49
+
50
+ let active = false;
51
+
52
+ return {
53
+ label: "Section",
54
+ icon: "mdi:scissors-cutting",
55
+ activate() {
56
+ if (active) return;
57
+ active = true;
58
+ clipperComp.enabled = true; // allow create + drag-to-move
59
+ clipperComp.visible = true;
60
+ canvas.addEventListener("dblclick", onDblClick);
61
+ window.addEventListener("keydown", onKeyDown);
62
+ canvas.style.cursor = "crosshair";
63
+ },
64
+ deactivate() {
65
+ if (!active) return;
66
+ active = false;
67
+ // Stop creating / dragging, but keep the planes in the renderer's
68
+ // clippingPlanes so the section stays cut while navigating.
69
+ clipperComp.enabled = false;
70
+ canvas.removeEventListener("dblclick", onDblClick);
71
+ window.removeEventListener("keydown", onKeyDown);
72
+ canvas.style.cursor = "";
73
+ },
74
+ };
75
+ };
@@ -1,9 +1,13 @@
1
- import * as OBC from "@thatopen/components";
2
- import { EngineServicesClient } from "@thatopen/services";
3
- import { CloudRunner } from "../bim-components";
4
-
5
- export const setupCloudRunner = (components: OBC.Components, client: EngineServicesClient) => {
6
- const runner = components.get(CloudRunner);
7
- runner.client = client;
8
- return runner;
9
- };
1
+ import * as OBC from "@thatopen/components";
2
+ import { CloudRunner } from "../bim-components";
3
+ import { getUIManager } from "./ui-manager";
4
+
5
+ export const cloudRunner = (components: OBC.Components) => {
6
+ const runner = components.get(CloudRunner);
7
+ const uis = getUIManager(components);
8
+
9
+ // Re-render all appInfoSection instances whenever execution state changes.
10
+ runner.onExecutionUpdated.add(() => {
11
+ uis.custom.get("cloudRunnerSection").updateInstances();
12
+ });
13
+ };
@@ -0,0 +1,239 @@
1
+ import * as THREE from "three";
2
+ import * as OBC from "@thatopen/components";
3
+ import * as OBF from "@thatopen/components-front";
4
+ import * as BUI from "@thatopen/ui";
5
+ import { hider } from "./hider";
6
+
7
+ /**
8
+ * COMMANDS — a docked side-panel (alongside Explorer / Files / Graphics) that is
9
+ * both the keyboard-shortcut registry and its palette UI.
10
+ *
11
+ * A single `COMMANDS` table maps each viewer action to a shortcut + an mdi icon
12
+ * and a `run()` wired to the EXISTING components (the `hider` controller for
13
+ * visibility, the Highlighter for the current selection + clear, camera-controls
14
+ * for focus). The panel lists every command with its shortcut (command-palette
15
+ * style, click-to-run), and a global `keydown` handler dispatches the shortcuts —
16
+ * ignored while typing in an input so search fields aren't hijacked.
17
+ *
18
+ * Self-contained: returns the panel element WITHOUT self-mounting (like
19
+ * modelTree / graphicsPanel) and installs its own window keydown listener.
20
+ *
21
+ * @param components engine components
22
+ */
23
+
24
+ interface ViewerCommand {
25
+ id: string;
26
+ label: string;
27
+ icon: string;
28
+ /** Display chips, e.g. ["Shift", "H"]. */
29
+ keys: string[];
30
+ /** Does this keydown event trigger the command? */
31
+ match: (e: KeyboardEvent) => boolean;
32
+ run: () => void;
33
+ }
34
+
35
+ const noMods = (e: KeyboardEvent) =>
36
+ !e.shiftKey && !e.ctrlKey && !e.metaKey && !e.altKey;
37
+
38
+ // Don't hijack shortcuts while the user is typing in a field.
39
+ const typingInField = (e: KeyboardEvent) => {
40
+ const path = (e.composedPath?.() ?? []) as HTMLElement[];
41
+ const el = (path[0] ?? (e.target as HTMLElement)) || null;
42
+ const active = (document.activeElement as HTMLElement) || null;
43
+ const isField = (n: HTMLElement | null) =>
44
+ !!n &&
45
+ (n.isContentEditable ||
46
+ /^(input|textarea|select)$/i.test(n.tagName) ||
47
+ /^bim-(text|number)-input$/i.test(n.tagName));
48
+ return path.some(isField) || isField(el) || isField(active);
49
+ };
50
+
51
+ export const commandsPanel = (components: OBC.Components) => {
52
+ const fragments = components.get(OBC.FragmentsManager);
53
+ const highlighter = components.get(OBF.Highlighter);
54
+ const selectName = highlighter.config.selectName;
55
+ const h = hider(components);
56
+
57
+ const hasSelection = () => {
58
+ const map = highlighter.selection[selectName];
59
+ return !!map && Object.values(map).some((s) => s.size > 0);
60
+ };
61
+
62
+ const focusSelection = async () => {
63
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
64
+ const world = [...components.get(OBC.Worlds).list.values()][0] as any;
65
+ const controls = world?.camera?.controls;
66
+ if (!controls) return;
67
+ const map = highlighter.selection[selectName];
68
+ const hasSel = !!map && Object.values(map).some((s) => s.size > 0);
69
+ try {
70
+ const box = new THREE.Box3();
71
+ if (hasSel) {
72
+ const boxes = (await fragments.getBBoxes(map)) as THREE.Box3[];
73
+ for (const b of boxes) box.union(b);
74
+ } else {
75
+ // Nothing selected → focus = zoom to fit the WHOLE model (union of all
76
+ // loaded models' bounds). This makes a dedicated fit/home button unnecessary.
77
+ for (const model of fragments.list.values()) {
78
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
79
+ const b = (model as any).box as THREE.Box3 | undefined;
80
+ if (b && !b.isEmpty()) box.union(b);
81
+ }
82
+ }
83
+ if (box.isEmpty()) return;
84
+ const sphere = box.getBoundingSphere(new THREE.Sphere());
85
+ await controls.fitToSphere(sphere, true);
86
+ } catch (error) {
87
+ console.warn("[commands] focus failed", error);
88
+ }
89
+ };
90
+
91
+ const clearSelection = () => void highlighter.clear(selectName);
92
+
93
+ const COMMANDS: ViewerCommand[] = [
94
+ {
95
+ id: "focus",
96
+ label: "Focus selection",
97
+ icon: "mdi:image-filter-center-focus",
98
+ keys: ["F"],
99
+ match: (e) => e.key.toLowerCase() === "f" && noMods(e),
100
+ run: () => void focusSelection(),
101
+ },
102
+ {
103
+ id: "hide",
104
+ label: "Hide selected",
105
+ icon: "mdi:eye-off-outline",
106
+ keys: ["H"],
107
+ match: (e) => e.key.toLowerCase() === "h" && noMods(e),
108
+ run: () => h.hideSelected(),
109
+ },
110
+ {
111
+ id: "isolate",
112
+ label: "Isolate selected",
113
+ icon: "mdi:select-search",
114
+ keys: ["Shift", "H"],
115
+ match: (e) =>
116
+ e.key.toLowerCase() === "h" &&
117
+ e.shiftKey &&
118
+ !e.ctrlKey &&
119
+ !e.metaKey &&
120
+ !e.altKey,
121
+ run: () => h.isolateSelected(),
122
+ },
123
+ {
124
+ id: "ghost",
125
+ label: "Ghost selected",
126
+ icon: "mdi:ghost-outline",
127
+ keys: ["G"],
128
+ match: (e) => e.key.toLowerCase() === "g" && noMods(e),
129
+ run: () => h.ghostSelected(),
130
+ },
131
+ {
132
+ id: "showAll",
133
+ label: "Show all",
134
+ icon: "mdi:eye-outline",
135
+ keys: ["Shift", "A"],
136
+ match: (e) =>
137
+ e.key.toLowerCase() === "a" &&
138
+ e.shiftKey &&
139
+ !e.ctrlKey &&
140
+ !e.metaKey &&
141
+ !e.altKey,
142
+ run: () => h.showAll(),
143
+ },
144
+ {
145
+ id: "clear",
146
+ label: "Clear selection",
147
+ icon: "mdi:close",
148
+ keys: ["Esc"],
149
+ match: (e) => e.key === "Escape" && noMods(e),
150
+ run: () => clearSelection(),
151
+ },
152
+ ];
153
+
154
+ // ── Global shortcut dispatch ───────────────────────────────────
155
+ const onKeyDown = (e: KeyboardEvent) => {
156
+ if (typingInField(e)) return;
157
+ for (const cmd of COMMANDS) {
158
+ if (cmd.match(e)) {
159
+ e.preventDefault();
160
+ cmd.run();
161
+ return;
162
+ }
163
+ }
164
+ };
165
+ window.addEventListener("keydown", onKeyDown);
166
+
167
+ // ── Panel UI (command-palette list; mirrors graphics-panel chrome) ──
168
+ const [panel, update] = BUI.Component.create<BUI.Panel, { query: string }>(
169
+ (state) => {
170
+ const q = state.query.trim().toLowerCase();
171
+ const rows = COMMANDS.filter((c) => !q || c.label.toLowerCase().includes(q));
172
+ return BUI.html`
173
+ <bim-panel
174
+ label="Commands"
175
+ icon="mdi:keyboard"
176
+ style="width: 100%; height: 100%; pointer-events: auto;"
177
+ >
178
+ <style>
179
+ .cmd-vp::-webkit-scrollbar { width: 0.4rem; height: 0.4rem; }
180
+ .cmd-vp::-webkit-scrollbar-thumb { border-radius: 0.25rem; background-color: var(--bim-scrollbar--c, #3C3C41); }
181
+ .cmd-vp::-webkit-scrollbar-track { background-color: var(--bim-scrollbar--bgc, var(--bim-ui_bg-contrast-10)); }
182
+ .cmd-row { box-sizing: border-box; min-height: 32px; display: flex;
183
+ align-items: center; gap: 0.55rem; padding: 0.35rem 0.4rem 0.35rem 1.1rem;
184
+ font-size: 0.78rem; line-height: 1.3; cursor: pointer;
185
+ color: var(--bim-ui_bg-contrast-100, #e3e3e3);
186
+ border-bottom: 1px solid var(--bim-ui_bg-contrast-20, rgba(255,255,255,0.1)); }
187
+ .cmd-row:hover { background: var(--bim-ui_bg-contrast-20, rgba(255,255,255,0.08)); }
188
+ .cmd-ico { flex: 0 0 auto; color: #99a0ae; font-size: 0.95rem; }
189
+ .cmd-label { flex: 1 1 auto; min-width: 0; overflow: hidden;
190
+ text-overflow: ellipsis; white-space: nowrap; }
191
+ .cmd-keys { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 0.2rem; }
192
+ .cmd-kbd { font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
193
+ font-size: 0.66rem; line-height: 1; padding: 0.12rem 0.35rem;
194
+ border-radius: 0.25rem; color: var(--bim-ui_bg-contrast-80, #c9c9c9);
195
+ background: var(--bim-ui_bg-contrast-20, rgba(255,255,255,0.08));
196
+ border: 1px solid var(--bim-ui_bg-contrast-40, rgba(255,255,255,0.18)); }
197
+ .cmd-plus { opacity: 0.4; font-size: 0.66rem; }
198
+ </style>
199
+ <div style="display: flex; flex-direction: column; height: 100%; width: 100%;">
200
+ <div style="flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; padding: 0.4rem 0.4rem 0.4rem 1.1rem; gap: 0.25rem;">
201
+ <bim-text-input
202
+ icon="mdi:magnify"
203
+ icon-inside
204
+ placeholder="Search commands…"
205
+ style="flex: 0 0 auto; width: 100%; margin: 0 0 0.25rem -0.35rem;"
206
+ @input=${(e: Event) => {
207
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
208
+ update({ query: String((e.target as any).value ?? "") });
209
+ }}
210
+ ></bim-text-input>
211
+ <div style="flex: 0 0 auto; border-bottom: 1px solid var(--bim-ui_bg-contrast-20, rgba(255,255,255,0.1)); margin: 0 -0.4rem 0.1rem -1.1rem;"></div>
212
+ <div class="cmd-vp" style="flex: 1 1 auto; min-height: 0; overflow-y: auto; margin-left: -1.1rem; margin-right: -0.4rem;">
213
+ ${rows.length === 0
214
+ ? BUI.html`<div style="padding: 0.75rem 1.1rem;"><bim-label style="opacity: 0.6;">No commands.</bim-label></div>`
215
+ : rows.map(
216
+ (c) => BUI.html`
217
+ <div class="cmd-row" title=${c.label} @click=${() => c.run()}>
218
+ <bim-icon class="cmd-ico" icon=${c.icon}></bim-icon>
219
+ <span class="cmd-label">${c.label}</span>
220
+ <span class="cmd-keys">
221
+ ${c.keys.map(
222
+ (k, i) => BUI.html`${i > 0
223
+ ? BUI.html`<span class="cmd-plus">+</span>`
224
+ : null}<span class="cmd-kbd">${k}</span>`,
225
+ )}
226
+ </span>
227
+ </div>`,
228
+ )}
229
+ </div>
230
+ </div>
231
+ </div>
232
+ </bim-panel>
233
+ `;
234
+ },
235
+ { query: "" },
236
+ );
237
+
238
+ return panel;
239
+ };