@thatopen/services 0.2.0 → 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.
- package/README.md +59 -59
- package/dist/built-in/index.d.ts +486 -563
- package/dist/cli.js +10 -0
- package/dist/index.cjs.js +2 -2
- package/dist/index.es.js +3 -5
- package/dist/vite-env.d.ts +1 -1
- package/docs/builtin/paths.json +74 -70
- package/docs/cli/create.md +28 -28
- 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/package.json +100 -100
- 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 +99 -99
- package/src/built-in/UIManager/src/app/examples/AppState/example.ts +142 -142
- package/src/built-in/UIManager/src/app/examples/Banner/example.ts +161 -0
- package/src/built-in/UIManager/src/app/examples/Contexts/example.ts +125 -125
- package/src/built-in/UIManager/src/app/examples/FileDrop/example.ts +126 -0
- package/src/built-in/UIManager/src/app/examples/FloatingLayout/example.ts +92 -92
- package/src/built-in/UIManager/src/app/examples/Modal/example.ts +165 -0
- package/src/built-in/UIManager/src/app/examples/Notifications/example.ts +135 -161
- package/src/built-in/UIManager/src/app/examples/Sidebar/example.ts +104 -104
- package/src/built-in/index.ts +649 -755
- package/src/cli/templates/app/README.md +241 -241
- package/src/cli/templates/app/index.html +35 -27
- package/src/cli/templates/app/package.json +31 -27
- package/src/cli/templates/app/src/app.ts +37 -0
- package/src/cli/templates/app/src/assets/tool-placeholder.ts +50 -0
- package/src/cli/templates/app/src/bim-components/CloudRunner/index.ts +95 -87
- 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 -0
- package/src/cli/templates/app/src/main.ts +340 -56
- package/src/cli/templates/app/src/setups/active-tool-hud.ts +69 -0
- package/src/cli/templates/app/src/setups/camera-tools.ts +63 -0
- package/src/cli/templates/app/src/setups/camera-views.ts +201 -0
- package/src/cli/templates/app/src/setups/card-header.ts +35 -0
- package/src/cli/templates/app/src/setups/clipper-panel.ts +167 -0
- package/src/cli/templates/app/src/setups/clipper-tool.ts +754 -0
- package/src/cli/templates/app/src/setups/clipper.ts +75 -0
- package/src/cli/templates/app/src/setups/cloud-runner.ts +13 -9
- package/src/cli/templates/app/src/setups/commands-panel.ts +239 -0
- package/src/cli/templates/app/src/setups/data-table-panel.ts +2224 -0
- package/src/cli/templates/app/src/setups/exploded-view.ts +246 -0
- package/src/cli/templates/app/src/setups/file-format-icons.ts +95 -0
- package/src/cli/templates/app/src/setups/files-panel.ts +1503 -0
- package/src/cli/templates/app/src/setups/fps-indicator.ts +63 -0
- package/src/cli/templates/app/src/setups/graphics-panel.ts +490 -0
- package/src/cli/templates/app/src/setups/helper-panel.ts +88 -0
- package/src/cli/templates/app/src/setups/hider.ts +98 -0
- package/src/cli/templates/app/src/setups/index.ts +33 -3
- package/src/cli/templates/app/src/setups/inspection.ts +136 -0
- package/src/cli/templates/app/src/setups/measurement-panel.ts +188 -0
- package/src/cli/templates/app/src/setups/measurement-settings-panel.ts +161 -0
- package/src/cli/templates/app/src/setups/measurement-tool.ts +570 -0
- package/src/cli/templates/app/src/setups/measurements.ts +133 -0
- package/src/cli/templates/app/src/setups/model-tree.ts +797 -0
- package/src/cli/templates/app/src/setups/navigation-gizmo.ts +316 -0
- package/src/cli/templates/app/src/setups/objects-panel.ts +127 -0
- package/src/cli/templates/app/src/setups/plans-panel.ts +619 -0
- package/src/cli/templates/app/src/setups/properties-panel.ts +1048 -0
- package/src/cli/templates/app/src/setups/reality-capture/lib/hidden-tiles-plugin.ts +88 -0
- package/src/cli/templates/app/src/setups/reality-capture/lib/point-tile-plugin.ts +166 -0
- package/src/cli/templates/app/src/setups/reality-capture/lib/render-frame.ts +31 -0
- package/src/cli/templates/app/src/setups/reality-capture/lib/splat-tile-plugin.ts +51 -0
- package/src/cli/templates/app/src/setups/reality-capture-viewer.ts +1383 -0
- package/src/cli/templates/app/src/setups/right-sidebar.ts +178 -0
- package/src/cli/templates/app/src/setups/settings-panel.ts +94 -0
- package/src/cli/templates/app/src/setups/styles-panel.ts +110 -0
- package/src/cli/templates/app/src/setups/styles.ts +375 -0
- package/src/cli/templates/app/src/setups/tool-mode-manager.ts +150 -0
- package/src/cli/templates/app/src/setups/tool-mode.ts +23 -0
- package/src/cli/templates/app/src/setups/toolbar.ts +229 -0
- package/src/cli/templates/app/src/setups/ui-manager.ts +27 -0
- package/src/cli/templates/app/src/setups/visibility-toolbar.ts +498 -0
- package/src/cli/templates/app/src/setups/walkthrough.ts +528 -0
- package/src/cli/templates/app/src/ui-components/app-info-section/index.ts +26 -0
- package/src/cli/templates/app/src/ui-components/app-info-section/src/index.ts +1 -0
- package/src/cli/templates/app/src/ui-components/app-info-section/src/types.ts +15 -0
- package/src/cli/templates/app/src/ui-components/cloud-runner-section/index.ts +37 -0
- package/src/cli/templates/app/src/ui-components/cloud-runner-section/src/index.ts +1 -0
- package/src/cli/templates/app/src/ui-components/cloud-runner-section/src/types.ts +14 -0
- package/src/cli/templates/app/src/ui-components/index.ts +2 -1
- package/src/cli/templates/app/tsconfig.json +16 -17
- package/src/cli/templates/app/vite.config.js +45 -42
- 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
- package/src/built-in/AppManager/example.ts +0 -86
- package/src/built-in/FileList/example.ts +0 -107
- package/src/built-in/TabbedNavigation/example.ts +0 -115
- package/src/built-in/ViewportsManager/example.ts +0 -106
- package/src/cli/templates/app/src/setups/fragments.ts +0 -18
- package/src/cli/templates/app/src/setups/ifc-loader.ts +0 -12
- package/src/cli/templates/app/src/ui-components/AppPanel/index.ts +0 -160
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small FPS counter, mounted INSIDE the viewer (top-center overlay). Measures
|
|
3
|
+
* the browser animation-frame rate (a good proxy for overall responsiveness).
|
|
4
|
+
*
|
|
5
|
+
* It is styled like the rest of the app (panel surface + 1px contrast-20 border
|
|
6
|
+
* + theme text), not a raw green-on-black badge, and is TOGGLEABLE — the
|
|
7
|
+
* returned controller's `setVisible` is wired to a "Show FPS" switch in the
|
|
8
|
+
* Graphics panel. The rAF loop keeps running while hidden (cost is negligible);
|
|
9
|
+
* only the element's visibility changes.
|
|
10
|
+
*/
|
|
11
|
+
export interface FpsIndicator {
|
|
12
|
+
/** The overlay element (already mounted inside the viewer). */
|
|
13
|
+
element: HTMLElement;
|
|
14
|
+
/** Whether the counter is currently shown. */
|
|
15
|
+
readonly visible: boolean;
|
|
16
|
+
/** Show / hide the counter. */
|
|
17
|
+
setVisible: (v: boolean) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const fpsIndicator = (parent: HTMLElement): FpsIndicator => {
|
|
21
|
+
const el = document.createElement("div");
|
|
22
|
+
el.style.cssText = `
|
|
23
|
+
position: absolute; top: 0.6rem; left: 50%; transform: translateX(-50%); z-index: 10;
|
|
24
|
+
padding: 0.2rem 0.55rem; border-radius: 0.5rem;
|
|
25
|
+
background: var(--bim-ui_bg-contrast-10, #262629);
|
|
26
|
+
border: 1px solid var(--bim-ui_bg-contrast-20, rgba(255,255,255,0.1));
|
|
27
|
+
color: var(--bim-ui_bg-contrast-100, #e3e3e3);
|
|
28
|
+
font: 600 0.72rem/1.2 ui-monospace, monospace;
|
|
29
|
+
pointer-events: none; user-select: none;
|
|
30
|
+
display: none;
|
|
31
|
+
`;
|
|
32
|
+
el.textContent = "-- FPS";
|
|
33
|
+
// The viewer is position:relative (set in viewports-manager for the anchor
|
|
34
|
+
// dot), so an absolutely-positioned child overlays the canvas correctly.
|
|
35
|
+
if (!parent.style.position) parent.style.position = "relative";
|
|
36
|
+
parent.append(el);
|
|
37
|
+
|
|
38
|
+
let visible = false;
|
|
39
|
+
let frames = 0;
|
|
40
|
+
let last = performance.now();
|
|
41
|
+
const tick = (now: number) => {
|
|
42
|
+
frames += 1;
|
|
43
|
+
const dt = now - last;
|
|
44
|
+
if (dt >= 500) {
|
|
45
|
+
el.textContent = `${Math.round((frames * 1000) / dt)} FPS`;
|
|
46
|
+
frames = 0;
|
|
47
|
+
last = now;
|
|
48
|
+
}
|
|
49
|
+
requestAnimationFrame(tick);
|
|
50
|
+
};
|
|
51
|
+
requestAnimationFrame(tick);
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
element: el,
|
|
55
|
+
get visible() {
|
|
56
|
+
return visible;
|
|
57
|
+
},
|
|
58
|
+
setVisible: (v: boolean) => {
|
|
59
|
+
visible = v;
|
|
60
|
+
el.style.display = v ? "block" : "none";
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
};
|
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
import * as OBC from "@thatopen/components";
|
|
2
|
+
import * as BUI from "@thatopen/ui";
|
|
3
|
+
import { cardHeader } from "./card-header";
|
|
4
|
+
import { styles, StyleSetting } from "./styles";
|
|
5
|
+
import type { FpsIndicator } from "./fps-indicator";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* GRAPHICS panel — a docked side-panel (third layout alongside Explorer/Files)
|
|
9
|
+
* exposing the viewer's rendering/graphics settings.
|
|
10
|
+
*
|
|
11
|
+
* It REUSES the typed `styles(components, world)` DESCRIPTOR (the same one that
|
|
12
|
+
* backs the bottom-left "Styles" helper tool) so every control here drives the
|
|
13
|
+
* live postproduction pipeline directly — postproduction on/off & preset, edges
|
|
14
|
+
* (contour), surface color / AO / tonal shading, scene background & grid, and
|
|
15
|
+
* the quality block (FXAA, render scale, adaptive resolution, target FPS, the
|
|
16
|
+
* half-res selection-outline toggle). No setting is invented; each maps 1:1 to
|
|
17
|
+
* a wired pipeline feature. Mutating a control takes effect on the next frame.
|
|
18
|
+
*
|
|
19
|
+
* The generic `control()` renderer (switch/slider/color/dropdown wired to a
|
|
20
|
+
* setting's get/set) mirrors styles-panel.ts so the two stay consistent.
|
|
21
|
+
*
|
|
22
|
+
* LAYOUT: to keep the panel dense, a feature's dependent controls are STACKED
|
|
23
|
+
* onto a single shared row (see ROW_LAYOUT below) — e.g. "Edge color" + "Edge
|
|
24
|
+
* strength" sit together, and a feature's toggle pairs with its one dependent
|
|
25
|
+
* value ("Grid" + grid color, "Ambient occlusion" + AO strength, …). Booleans
|
|
26
|
+
* render as a compact pill TOGGLE SWITCH (purple when on) instead of a checkbox.
|
|
27
|
+
* Only the PRESENTATION changes here — every control still drives the exact same
|
|
28
|
+
* StyleSetting get/set, so the pipeline wiring is untouched.
|
|
29
|
+
*
|
|
30
|
+
* Returns the panel ELEMENT WITHOUT self-mounting (like modelTree / filesPanel)
|
|
31
|
+
* so main.ts can strip the card chrome and dock it into the grid.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
// ── Document-level theming for the dropdown POPUP ───────────────────────────
|
|
35
|
+
// The Style-preset `bim-dropdown` renders its open menu as a `bim-context-menu`
|
|
36
|
+
// that BUI MOVES OUT to a top-level `<dialog data-context-dialog>` appended to
|
|
37
|
+
// `document.body` while visible (see ContextMenu.set visible() in
|
|
38
|
+
// node_modules/@thatopen/ui/dist/index.js: `document.body.append(lt.dialog)` /
|
|
39
|
+
// `lt.dialog.append(this)`). Because the popup leaves this panel's shadow scope,
|
|
40
|
+
// the panel's <style> can't reach it — the popup falls back to BUI defaults
|
|
41
|
+
// (`--bim-ui_bg-contrast-50` surface, 1px shadow-radius) and looks out of place.
|
|
42
|
+
//
|
|
43
|
+
// So we theme it from the DOCUMENT instead, scoped to the context dialog, to
|
|
44
|
+
// match the app's other menus: dark panel surface, 1px contrast-40 border, a
|
|
45
|
+
// small radius + subtle shadow, compact ~0.78rem items, and the SELECTED option
|
|
46
|
+
// drawn with the app's purple accent (var(--bim-ui_accent-base)). `bim-option`'s shadow `:host`
|
|
47
|
+
// sets its own padding/radius (specificity 0,1,0), so where we must beat that we
|
|
48
|
+
// use the scoped dialog selector with `!important`; option BACKGROUND/hover/
|
|
49
|
+
// selected are unset by BUI so a plain rule suffices. The checked-label colour
|
|
50
|
+
// derives from `--bim-ui_main-base`, which we pin to the accent here too.
|
|
51
|
+
const APP_ACCENT = "var(--bim-ui_accent-base)";
|
|
52
|
+
const ensureDropdownPopupStyle = () => {
|
|
53
|
+
const id = "graphics-dropdown-popup-style";
|
|
54
|
+
if (document.getElementById(id)) return;
|
|
55
|
+
const el = document.createElement("style");
|
|
56
|
+
el.id = id;
|
|
57
|
+
el.textContent = `
|
|
58
|
+
/* The popup surface (the relocated bim-context-menu inside the dialog). */
|
|
59
|
+
dialog[data-context-dialog] bim-context-menu {
|
|
60
|
+
border: 1px solid var(--bim-ui_bg-contrast-40, rgba(255,255,255,0.18)) !important;
|
|
61
|
+
border-radius: 0.375rem !important;
|
|
62
|
+
box-shadow: 0 6px 18px rgba(0,0,0,0.45) !important;
|
|
63
|
+
padding: 0.25rem !important;
|
|
64
|
+
min-width: 8rem;
|
|
65
|
+
}
|
|
66
|
+
/* Compact option rows; selected/hover backgrounds (BUI leaves these unset). */
|
|
67
|
+
dialog[data-context-dialog] bim-option {
|
|
68
|
+
font-size: 0.78rem;
|
|
69
|
+
border-radius: 0.25rem !important;
|
|
70
|
+
padding: 0.18rem 0.5rem !important;
|
|
71
|
+
}
|
|
72
|
+
dialog[data-context-dialog] bim-option:hover {
|
|
73
|
+
background-color: var(--bim-ui_bg-contrast-20, rgba(255,255,255,0.08));
|
|
74
|
+
}
|
|
75
|
+
/* SELECTED option: the app purple accent, consistently. */
|
|
76
|
+
dialog[data-context-dialog] bim-option[checked] {
|
|
77
|
+
background-color: ${APP_ACCENT};
|
|
78
|
+
}
|
|
79
|
+
`;
|
|
80
|
+
document.head.append(el);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// ── Toggle SWITCH for a bool setting (library bim-checkbox[toggle]) ──────────
|
|
84
|
+
// The library checkbox has a built-in pill/switch mode (dark ball + outline,
|
|
85
|
+
// purple track when on) — so we use it directly instead of a hand-rolled widget,
|
|
86
|
+
// keeping the toggle styling in ONE place (the library) for the whole platform.
|
|
87
|
+
// It drives the SAME `StyleSetting.set(boolean)`, so behaviour is identical.
|
|
88
|
+
const boolSwitch = (s: StyleSetting & { type: "bool" }, refresh: () => void) => {
|
|
89
|
+
const on = s.get();
|
|
90
|
+
return BUI.html`<bim-checkbox
|
|
91
|
+
class="g-toggle"
|
|
92
|
+
toggle
|
|
93
|
+
?checked=${on}
|
|
94
|
+
@change=${(e: Event) => {
|
|
95
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
96
|
+
s.set(!!(e.target as any).checked);
|
|
97
|
+
refresh();
|
|
98
|
+
}}
|
|
99
|
+
></bim-checkbox>`;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// Generic control for one StyleSetting (same wiring styles-panel.ts uses).
|
|
103
|
+
//
|
|
104
|
+
// IMPORTANT: the label is rendered by the ROW (left cell, muted) so every
|
|
105
|
+
// control here is created WITHOUT its own `label` attribute — it renders as a
|
|
106
|
+
// bare, compact widget that sits in the row's right cell. Sizing/colour is
|
|
107
|
+
// driven entirely by the `.g-row` CSS (BUI control custom-props) below.
|
|
108
|
+
const control = (s: StyleSetting, refresh: () => void) => {
|
|
109
|
+
switch (s.type) {
|
|
110
|
+
case "bool":
|
|
111
|
+
// Pill toggle switch (replaces the former bim-checkbox), same setter.
|
|
112
|
+
return boolSwitch(s, refresh);
|
|
113
|
+
case "number":
|
|
114
|
+
return BUI.html`<bim-number-input
|
|
115
|
+
slider
|
|
116
|
+
value=${s.get()}
|
|
117
|
+
min=${s.min}
|
|
118
|
+
max=${s.max}
|
|
119
|
+
step=${s.step}
|
|
120
|
+
@change=${(e: Event) => {
|
|
121
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
122
|
+
s.set(Number((e.target as any).value));
|
|
123
|
+
refresh();
|
|
124
|
+
}}
|
|
125
|
+
></bim-number-input>`;
|
|
126
|
+
case "color":
|
|
127
|
+
return BUI.html`<bim-color-input
|
|
128
|
+
color=${s.get()}
|
|
129
|
+
@input=${(e: Event) => {
|
|
130
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
131
|
+
const t = e.target as any;
|
|
132
|
+
s.set(String(t.color ?? t.value));
|
|
133
|
+
refresh();
|
|
134
|
+
}}
|
|
135
|
+
></bim-color-input>`;
|
|
136
|
+
case "enum":
|
|
137
|
+
return BUI.html`<bim-dropdown
|
|
138
|
+
@change=${(e: Event) => {
|
|
139
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
140
|
+
const picked = (e.target as any).value?.[0];
|
|
141
|
+
const match = s.options.find((o) => o.label === picked);
|
|
142
|
+
if (match) {
|
|
143
|
+
s.set(match.value);
|
|
144
|
+
refresh();
|
|
145
|
+
}
|
|
146
|
+
}}
|
|
147
|
+
>
|
|
148
|
+
${s.options.map(
|
|
149
|
+
(o) => BUI.html`<bim-option label=${o.label} ?checked=${o.value === s.get()}></bim-option>`,
|
|
150
|
+
)}
|
|
151
|
+
</bim-dropdown>`;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// A control sized for sharing a row with another control / a toggle. No inline
|
|
156
|
+
// mini-label: the row's left label gives context, and the widget itself is
|
|
157
|
+
// self-evident (a colour swatch, or a slider showing its value), so a mini-label
|
|
158
|
+
// just costs width and risks crowding the toggle. The `g-i-<type>` class drives
|
|
159
|
+
// the compact per-type width.
|
|
160
|
+
const inlineControl = (s: StyleSetting, refresh: () => void) =>
|
|
161
|
+
BUI.html`<span class="g-inline g-i-${s.type}">${control(s, refresh)}</span>`;
|
|
162
|
+
|
|
163
|
+
export const graphicsPanel = (components: OBC.Components, fps?: FpsIndicator) => {
|
|
164
|
+
// Theme the Style-preset dropdown's relocated popup (document-level, since it
|
|
165
|
+
// escapes this panel's shadow to a top-level dialog). Idempotent.
|
|
166
|
+
ensureDropdownPopupStyle();
|
|
167
|
+
|
|
168
|
+
// The postproduction pipeline lives on the first world's renderer. It is
|
|
169
|
+
// allocated lazily (once the viewport has a real size), so resolve the world
|
|
170
|
+
// at render time — same pattern the model-tree uses.
|
|
171
|
+
const getWorld = () =>
|
|
172
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
173
|
+
([...components.get(OBC.Worlds).list.values()][0] as OBC.World) ?? null;
|
|
174
|
+
|
|
175
|
+
// Has the deferred pipeline been configured yet? `styles()` reads
|
|
176
|
+
// `renderer.postproduction.deferred`, which throws until the pipeline is up.
|
|
177
|
+
const ready = (world: OBC.World | null) => {
|
|
178
|
+
if (!world) return false;
|
|
179
|
+
try {
|
|
180
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
181
|
+
const r = world.renderer as any;
|
|
182
|
+
return !!r?.postproduction?.deferred;
|
|
183
|
+
} catch {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const [panel, update] = BUI.Component.create<BUI.Panel, { tick: number }>(
|
|
189
|
+
(state) => {
|
|
190
|
+
void state.tick; // bump to force a re-read of all controls
|
|
191
|
+
|
|
192
|
+
const world = getWorld();
|
|
193
|
+
let body: unknown;
|
|
194
|
+
|
|
195
|
+
if (!ready(world)) {
|
|
196
|
+
body = BUI.html`<div style="padding: 0.6rem 0.4rem 0.6rem 1.1rem;">
|
|
197
|
+
<bim-label style="opacity: 0.6; white-space: normal; font-size: 0.78rem;">Rendering pipeline not ready yet. Load a model / wait for the viewer to initialize.</bim-label>
|
|
198
|
+
</div>`;
|
|
199
|
+
} else {
|
|
200
|
+
const { settings } = styles(components, world!);
|
|
201
|
+
const byKey = new Map(settings.map((s) => [s.key, s]));
|
|
202
|
+
const refresh = () => update({ tick: state.tick + 1 });
|
|
203
|
+
|
|
204
|
+
// ── Row layout plan ───────────────────────────────────────────────
|
|
205
|
+
// Each section is a band header followed by a list of ROWS. A row is
|
|
206
|
+
// either a single labelled control (the usual label-left / control-
|
|
207
|
+
// right shape) or a PAIRED row that stacks a feature's dependent
|
|
208
|
+
// controls together so the panel reads as a dense grid. Every key here
|
|
209
|
+
// still resolves to its StyleSetting via `byKey`, so wiring is intact.
|
|
210
|
+
type Row =
|
|
211
|
+
// Standard "label on left, one control on right" row.
|
|
212
|
+
| { kind: "single"; key: string; label?: string }
|
|
213
|
+
// Toggle on the left (with the section's row label), its ONE
|
|
214
|
+
// dependent value inline on the right of the same line.
|
|
215
|
+
| { kind: "toggleWith"; toggle: string; label?: string; dep: string; depLabel: string; depAlwaysEnabled?: boolean }
|
|
216
|
+
// Two dependent controls sharing a row under an obvious section,
|
|
217
|
+
// each with a tight inline mini-label (no wide left label cell).
|
|
218
|
+
| { kind: "pair"; label: string; a: string; aLabel: string; b: string; bLabel: string };
|
|
219
|
+
|
|
220
|
+
const sections: { name: string; icon: string; rows: Row[] }[] = [
|
|
221
|
+
{
|
|
222
|
+
name: "Preset",
|
|
223
|
+
icon: "mdi:palette-swatch",
|
|
224
|
+
rows: [
|
|
225
|
+
{ kind: "single", key: "preset" },
|
|
226
|
+
{ kind: "single", key: "postproductionEnabled" },
|
|
227
|
+
],
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
name: "Edges",
|
|
231
|
+
icon: "mdi:vector-square",
|
|
232
|
+
rows: [
|
|
233
|
+
{ kind: "single", key: "edges" },
|
|
234
|
+
// Edge color + edge strength share one row (swatch + strength).
|
|
235
|
+
{ kind: "pair", label: "Edge", a: "edgeColor", aLabel: "Color", b: "edgeStrength", bLabel: "Strength" },
|
|
236
|
+
],
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
name: "Shading",
|
|
240
|
+
icon: "mdi:circle-half-full",
|
|
241
|
+
rows: [
|
|
242
|
+
{ kind: "single", key: "surfaceColor" },
|
|
243
|
+
// AO toggle + its strength on one line; tonal toggle + its floor.
|
|
244
|
+
{ kind: "toggleWith", toggle: "ao", label: "Ambient occlusion", dep: "aoStrength", depLabel: "Strength" },
|
|
245
|
+
{ kind: "toggleWith", toggle: "tonalShading", label: "Tonal shading", dep: "tonalFloor", depLabel: "Floor" },
|
|
246
|
+
],
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
name: "Scene",
|
|
250
|
+
icon: "mdi:image-outline",
|
|
251
|
+
rows: [
|
|
252
|
+
// Each scene toggle pairs with its own color on the same line.
|
|
253
|
+
// Background colour is relevant when transparent is OFF (opaque),
|
|
254
|
+
// i.e. the INVERSE of the generic toggle→dep relationship, so keep
|
|
255
|
+
// the picker always interactive instead of dimming it.
|
|
256
|
+
{ kind: "toggleWith", toggle: "transparentBackground", label: "Transparent bg", dep: "backgroundColor", depLabel: "Color", depAlwaysEnabled: true },
|
|
257
|
+
{ kind: "toggleWith", toggle: "grid", label: "Grid", dep: "gridColor", depLabel: "Color" },
|
|
258
|
+
],
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
name: "Quality",
|
|
262
|
+
icon: "mdi:speedometer",
|
|
263
|
+
rows: [
|
|
264
|
+
{ kind: "single", key: "fxaa" },
|
|
265
|
+
// Adaptive resolution toggle + its target FPS on one line.
|
|
266
|
+
{ kind: "toggleWith", toggle: "adaptiveResolution", label: "Adaptive resolution", dep: "targetFps", depLabel: "FPS" },
|
|
267
|
+
// Render scale + the high-res selection-outline toggle on one line.
|
|
268
|
+
{ kind: "toggleWith", toggle: "highResOutline", label: "Hi-res outline", dep: "renderScale", depLabel: "Scale" },
|
|
269
|
+
],
|
|
270
|
+
},
|
|
271
|
+
];
|
|
272
|
+
|
|
273
|
+
// "Show FPS" toggle (Quality section) — a synthetic bool wired straight
|
|
274
|
+
// to the FPS overlay's visibility, so it renders through the same control
|
|
275
|
+
// path as every other switch. Only added when an FPS controller exists.
|
|
276
|
+
if (fps) {
|
|
277
|
+
byKey.set("showFps", {
|
|
278
|
+
key: "showFps",
|
|
279
|
+
label: "Show FPS",
|
|
280
|
+
group: "Quality",
|
|
281
|
+
type: "bool",
|
|
282
|
+
default: false,
|
|
283
|
+
get: () => fps.visible,
|
|
284
|
+
set: (v: boolean) => fps.setVisible(v),
|
|
285
|
+
});
|
|
286
|
+
sections.find((s) => s.name === "Quality")?.rows.push({
|
|
287
|
+
kind: "single",
|
|
288
|
+
key: "showFps",
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Render one planned row, resolving each key to its live StyleSetting.
|
|
293
|
+
const renderRow = (row: Row): unknown => {
|
|
294
|
+
if (row.kind === "single") {
|
|
295
|
+
const s = byKey.get(row.key);
|
|
296
|
+
if (!s) return null;
|
|
297
|
+
return BUI.html`<div class="g-row g-${s.type}">
|
|
298
|
+
<span class="g-k">${row.label ?? s.label}</span>
|
|
299
|
+
<span class="g-v">${control(s, refresh)}</span>
|
|
300
|
+
</div>`;
|
|
301
|
+
}
|
|
302
|
+
if (row.kind === "toggleWith") {
|
|
303
|
+
const t = byKey.get(row.toggle);
|
|
304
|
+
const dep = byKey.get(row.dep);
|
|
305
|
+
if (!t || t.type !== "bool" || !dep) return null;
|
|
306
|
+
const off = row.depAlwaysEnabled ? false : !t.get();
|
|
307
|
+
// The dependent control is dimmed/disabled while its toggle is off.
|
|
308
|
+
return BUI.html`<div class="g-row g-toggle-with">
|
|
309
|
+
<span class="g-k">${row.label ?? t.label}</span>
|
|
310
|
+
<span class="g-v g-v-multi">
|
|
311
|
+
<span class="g-dep ${off ? "g-dep-off" : ""}">
|
|
312
|
+
${inlineControl(dep, refresh)}
|
|
313
|
+
</span>
|
|
314
|
+
${boolSwitch(t, refresh)}
|
|
315
|
+
</span>
|
|
316
|
+
</div>`;
|
|
317
|
+
}
|
|
318
|
+
// pair: two dependent controls side by side under one section label.
|
|
319
|
+
const a = byKey.get(row.a);
|
|
320
|
+
const b = byKey.get(row.b);
|
|
321
|
+
if (!a || !b) return null;
|
|
322
|
+
return BUI.html`<div class="g-row g-pair">
|
|
323
|
+
<span class="g-k">${row.label}</span>
|
|
324
|
+
<span class="g-v g-v-multi">
|
|
325
|
+
${inlineControl(a, refresh)}
|
|
326
|
+
${inlineControl(b, refresh)}
|
|
327
|
+
</span>
|
|
328
|
+
</div>`;
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
body = BUI.html`
|
|
332
|
+
${sections.map(
|
|
333
|
+
(sec) => BUI.html`
|
|
334
|
+
<div class="g-band"><bim-icon class="g-band-ico" icon=${sec.icon}></bim-icon>${sec.name}</div>
|
|
335
|
+
${sec.rows.map((row) => renderRow(row))}
|
|
336
|
+
`,
|
|
337
|
+
)}
|
|
338
|
+
`;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return BUI.html`
|
|
342
|
+
<bim-panel
|
|
343
|
+
label="Graphics"
|
|
344
|
+
icon="mdi:tune"
|
|
345
|
+
style="width: 100%; height: 100%; pointer-events: auto;"
|
|
346
|
+
>
|
|
347
|
+
<style>
|
|
348
|
+
.prop-vp::-webkit-scrollbar { width: 0.4rem; height: 0.4rem; }
|
|
349
|
+
.prop-vp::-webkit-scrollbar-thumb { border-radius: 0.25rem; background-color: var(--bim-scrollbar--c, #3C3C41); }
|
|
350
|
+
.prop-vp::-webkit-scrollbar-track { background-color: var(--bim-scrollbar--bgc, var(--bim-ui_bg-contrast-10)); }
|
|
351
|
+
|
|
352
|
+
/* ── Full-bleed rows (mirror properties' .p-row / .p-kv) ──────────
|
|
353
|
+
Rows reach both panel edges; text is re-inset via the same 1.1rem
|
|
354
|
+
left / 0.4rem right padding the panel chrome uses. One consistent
|
|
355
|
+
compact scale (~0.78rem) like the properties rows. */
|
|
356
|
+
/* ── Full-bleed rows (mirror properties' .p-row / .p-kv) ──────────
|
|
357
|
+
Rows reach both panel edges; text is re-inset via the same 1.1rem
|
|
358
|
+
left / 0.4rem right padding the panel chrome uses. One consistent
|
|
359
|
+
compact scale (~0.78rem) like the properties rows. */
|
|
360
|
+
.g-row { box-sizing: border-box; min-height: 32px; display: flex;
|
|
361
|
+
align-items: center; justify-content: space-between; gap: 0.6rem;
|
|
362
|
+
padding: 0.35rem 0.4rem 0.35rem 1.1rem; font-size: 0.78rem;
|
|
363
|
+
line-height: 1.3; color: var(--bim-ui_bg-contrast-100, #e3e3e3);
|
|
364
|
+
border-bottom: 1px solid var(--bim-ui_bg-contrast-20, rgba(255,255,255,0.1)); }
|
|
365
|
+
/* Muted label on the LEFT (like .p-k, opacity ~0.6). Single line +
|
|
366
|
+
ellipsis so a long label never wraps and overlaps its control. */
|
|
367
|
+
.g-k { opacity: 0.6; flex: 0 1 auto; min-width: 0; max-width: 42%;
|
|
368
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
369
|
+
/* Control on the RIGHT (like .p-v), right-aligned, never crushed. */
|
|
370
|
+
.g-v { flex: 1 1 auto; min-width: 0; display: flex;
|
|
371
|
+
align-items: center; justify-content: flex-end; }
|
|
372
|
+
/* Multi-control right cell: dependent value(s) + toggle on one line,
|
|
373
|
+
tight gap, right-aligned, wrap-free. */
|
|
374
|
+
.g-v-multi { gap: 0.65rem; flex-wrap: nowrap; }
|
|
375
|
+
|
|
376
|
+
/* Inline mini-labelled control (the dependent values that share a
|
|
377
|
+
row). The mini-label is muted + tiny; the control sits right next
|
|
378
|
+
to it with a compact field. */
|
|
379
|
+
.g-inline { display: inline-flex; align-items: center; gap: 0.3rem;
|
|
380
|
+
flex: 0 1 auto; min-width: 0; }
|
|
381
|
+
.g-il { opacity: 0.55; font-size: 0.7rem; letter-spacing: 0.01em;
|
|
382
|
+
white-space: nowrap; }
|
|
383
|
+
/* A dependent control whose owning toggle is OFF: dimmed + inert. */
|
|
384
|
+
.g-dep { display: inline-flex; align-items: center; min-width: 0;
|
|
385
|
+
flex: 0 1 auto; }
|
|
386
|
+
.g-dep-off { opacity: 0.4; pointer-events: none; }
|
|
387
|
+
|
|
388
|
+
/* Section band — EXACTLY the properties panel's .p-psh band: muted,
|
|
389
|
+
full-bleed, a subtle gray BACKGROUND (one step up from the panel)
|
|
390
|
+
with top/bottom hairlines matching the same 1px contrast-20 used
|
|
391
|
+
between rows — never a heavier rule, never the bright/gradient
|
|
392
|
+
panel-title treatment. Subordinate to the "Graphics" card title. */
|
|
393
|
+
.g-band { box-sizing: border-box; padding: 0.4rem 0.4rem 0.4rem 1.1rem;
|
|
394
|
+
display: flex; align-items: center; gap: 0.4rem;
|
|
395
|
+
font-size: 0.76rem; font-weight: 500; letter-spacing: 0.01em;
|
|
396
|
+
color: var(--bim-ui_bg-contrast-70, #a7a7ab);
|
|
397
|
+
background: var(--bim-ui_bg-contrast-20, rgba(255,255,255,0.1));
|
|
398
|
+
border-top: 1px solid var(--bim-ui_bg-contrast-20, rgba(255,255,255,0.1));
|
|
399
|
+
border-bottom: 1px solid var(--bim-ui_bg-contrast-20, rgba(255,255,255,0.1)); }
|
|
400
|
+
/* Per-section icon — muted to match the band text, never the accent. */
|
|
401
|
+
.g-band-ico { flex: 0 0 auto; font-size: 0.9rem;
|
|
402
|
+
color: var(--bim-ui_bg-contrast-70, #a7a7ab); }
|
|
403
|
+
.g-band:first-child { border-top: none; }
|
|
404
|
+
|
|
405
|
+
/* ── Slim, dark-themed controls ──────────────────────────────────
|
|
406
|
+
Override BUI control custom-props so the widgets read as compact
|
|
407
|
+
right-aligned controls (matching the searchbar field look used in
|
|
408
|
+
the tree/properties inputs), not chunky default widgets. */
|
|
409
|
+
.g-v bim-number-input,
|
|
410
|
+
.g-v bim-color-input,
|
|
411
|
+
.g-v bim-dropdown {
|
|
412
|
+
/* Subtle app surface (not a near-black box) so the controls read as
|
|
413
|
+
part of the panel, like the app's other surfaces. */
|
|
414
|
+
font-size: 0.78rem;
|
|
415
|
+
}
|
|
416
|
+
/* Number input: small dark field (not a big solid-purple block),
|
|
417
|
+
right-aligned, fixed compact width. Slim the embedded slider too.
|
|
418
|
+
Narrower (5.5rem) inside a shared/inline cell so two controls fit. */
|
|
419
|
+
.g-number bim-number-input { flex: 0 0 auto; width: 7.5rem; }
|
|
420
|
+
/* SLIDER FILL FLUSH TO BORDER.
|
|
421
|
+
In BUI the slider renders as <bim-input>→.input (the rounded
|
|
422
|
+
field, padded by --bim-input--p) → .slider → .slider-indicator
|
|
423
|
+
(the purple fill: position:absolute; top:0; left:0; height:100%;
|
|
424
|
+
width:value%; background:var(--bim-ui_main-base)). The fill is
|
|
425
|
+
absolutely positioned to the .input CONTENT box, so the field's
|
|
426
|
+
inner padding (--bim-input--p, which the shared rule above sets to
|
|
427
|
+
0.15rem 0.35rem) shows as an inset band of field background all
|
|
428
|
+
around the fill. Zero the padding for the slider so the fill
|
|
429
|
+
reaches the rounded border edge-to-edge. The value text is a
|
|
430
|
+
separate z-index:1 <bim-label> centered over the fill, so it stays
|
|
431
|
+
readable. (--bim-slider--* are NOT real BUI hooks — the fill color
|
|
432
|
+
is --bim-ui_main-base, already var(--bim-ui_accent-base) globally.) */
|
|
433
|
+
.g-v bim-number-input {
|
|
434
|
+
}
|
|
435
|
+
.g-v bim-number-input::part(input) { text-align: right; }
|
|
436
|
+
.g-inline bim-number-input { flex: 0 1 auto; width: 4.5rem; min-width: 3rem; }
|
|
437
|
+
/* Color input: small swatch + compact hex, not an oversized control. */
|
|
438
|
+
.g-color bim-color-input { flex: 0 0 auto; width: 6.5rem; }
|
|
439
|
+
.g-v bim-color-input { /* swatch box size */ }
|
|
440
|
+
/* Let the colour input take its NATURAL width (swatch + hex), with no
|
|
441
|
+
grow/shrink — so its real rounded box renders at that size instead
|
|
442
|
+
of a forced narrower width whose content overflows rightward into
|
|
443
|
+
the gap / under the toggle. There is room for it in every row. */
|
|
444
|
+
.g-inline bim-color-input { flex: 0 0 auto; }
|
|
445
|
+
/* Dropdown CLOSED control: compact, right-aligned, and it inherits
|
|
446
|
+
the shared field look above (bg-contrast-20 fill, 1px contrast-40
|
|
447
|
+
outline). The OPEN popup is themed document-level (see
|
|
448
|
+
ensureDropdownPopupStyle) because BUI relocates it to a top-level
|
|
449
|
+
dialog outside this shadow scope. */
|
|
450
|
+
.g-enum bim-dropdown { flex: 0 0 auto; min-width: 7rem; max-width: 10rem; }
|
|
451
|
+
|
|
452
|
+
/* ── Library toggle (bim-checkbox[toggle]) sizing ────────────────
|
|
453
|
+
The checkbox host is display:block and would stretch to fill the
|
|
454
|
+
row's right cell (pushing/overlapping the dependent control). Pin
|
|
455
|
+
it to its intrinsic switch width and keep it from growing so it
|
|
456
|
+
sits flush-right without crowding its neighbour. The track/knob/
|
|
457
|
+
on-colour all come from the library (purple track + #3C3C41 ball
|
|
458
|
+
with an outline) — no per-app restyle. */
|
|
459
|
+
.g-row bim-checkbox.g-toggle { flex: 0 0 2rem; width: 2rem; margin-left: 0.35rem; }
|
|
460
|
+
</style>
|
|
461
|
+
<div style="display: flex; flex-direction: column; height: 100%; width: 100%;">
|
|
462
|
+
<div style="flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; padding: 0 0.4rem 0.4rem 1.1rem;">
|
|
463
|
+
<div class="prop-vp" style="flex: 1 1 auto; min-height: 0; overflow-y: auto;
|
|
464
|
+
margin-left: -1.1rem; margin-right: -0.4rem;">
|
|
465
|
+
${body}
|
|
466
|
+
</div>
|
|
467
|
+
</div>
|
|
468
|
+
</div>
|
|
469
|
+
</bim-panel>
|
|
470
|
+
`;
|
|
471
|
+
},
|
|
472
|
+
{ tick: 0 },
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
// The pipeline configures asynchronously after the first sized frame; the
|
|
476
|
+
// panel may render before it's ready. Poll briefly until ready, then re-render
|
|
477
|
+
// once so the real controls appear (cheap: stops the moment it succeeds).
|
|
478
|
+
let attempts = 0;
|
|
479
|
+
const poll = window.setInterval(() => {
|
|
480
|
+
attempts += 1;
|
|
481
|
+
if (ready(getWorld())) {
|
|
482
|
+
update({ tick: 0 });
|
|
483
|
+
window.clearInterval(poll);
|
|
484
|
+
} else if (attempts > 120) {
|
|
485
|
+
window.clearInterval(poll); // give up after ~60s
|
|
486
|
+
}
|
|
487
|
+
}, 500);
|
|
488
|
+
|
|
489
|
+
return panel;
|
|
490
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import * as BUI from "@thatopen/ui";
|
|
2
|
+
import { cardHeader } from "./card-header";
|
|
3
|
+
import { toolPlaceholderUri } from "../assets/tool-placeholder";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A contextual helper card — ALWAYS present in the left stack (see left-sidebar).
|
|
7
|
+
* It renders the options of whichever panel-tool is active (e.g. Styles); when
|
|
8
|
+
* no tool is active it shows an empty-state illustration + hint. Mirrors the
|
|
9
|
+
* standardized card chrome: dark `bg-base`, `cardHeader` (with divider) fixed,
|
|
10
|
+
* inset `.helper-scroll` `overflow-y:auto` (same scrollbar as the others).
|
|
11
|
+
*
|
|
12
|
+
* Returns the panel ELEMENT (mounted by the left stack) plus a controller:
|
|
13
|
+
* show({ title, icon, render }) — show a tool's content (+ retitle the header)
|
|
14
|
+
* clear() — revert to the empty state
|
|
15
|
+
* refresh() — re-run the current `render`
|
|
16
|
+
*/
|
|
17
|
+
export interface HelperContent {
|
|
18
|
+
title: string;
|
|
19
|
+
icon: string;
|
|
20
|
+
render: () => unknown;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface HelperPanelController {
|
|
24
|
+
element: HTMLElement;
|
|
25
|
+
show(content: HelperContent): void;
|
|
26
|
+
clear(): void;
|
|
27
|
+
refresh(): void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const DEFAULT = { title: "Tools", icon: "mdi:tune" };
|
|
31
|
+
|
|
32
|
+
const emptyState = () => BUI.html`
|
|
33
|
+
<div style="
|
|
34
|
+
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
35
|
+
gap: 0.75rem; height: 100%; min-height: 12rem; padding: 1rem; text-align: center;
|
|
36
|
+
">
|
|
37
|
+
<img src=${toolPlaceholderUri} alt="" style="width: 7.5rem; height: auto; opacity: 0.9;" />
|
|
38
|
+
<bim-label style="opacity: 0.55; white-space: normal;">Select a tool to see it here.</bim-label>
|
|
39
|
+
</div>
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
export const helperPanel = (): HelperPanelController => {
|
|
43
|
+
let renderContent: (() => unknown) | null = null;
|
|
44
|
+
|
|
45
|
+
const [panel, update] = BUI.Component.create<BUI.Panel, { title: string; icon: string }>(
|
|
46
|
+
(state) => BUI.html`
|
|
47
|
+
<bim-panel
|
|
48
|
+
label=${state.title}
|
|
49
|
+
icon=${state.icon}
|
|
50
|
+
style="width: 100%; height: 100%; pointer-events: auto;"
|
|
51
|
+
>
|
|
52
|
+
<style>
|
|
53
|
+
.helper-scroll::-webkit-scrollbar { width: 0.4rem; height: 0.4rem; }
|
|
54
|
+
.helper-scroll::-webkit-scrollbar-thumb { border-radius: 0.25rem; background-color: var(--bim-scrollbar--c, #3C3C41); }
|
|
55
|
+
.helper-scroll::-webkit-scrollbar-track { background-color: var(--bim-scrollbar--bgc, var(--bim-ui_bg-contrast-10)); }
|
|
56
|
+
</style>
|
|
57
|
+
<div style="display: flex; flex-direction: column; height: 100%;">
|
|
58
|
+
<div style="flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; padding: 0.4rem 0.4rem 0.4rem 1.1rem;">
|
|
59
|
+
<!-- Scroll container: scrollbar sits inset from the card (left
|
|
60
|
+
wrapper pad). Inner content has right padding so it clears the
|
|
61
|
+
scrollbar (gap on both sides), mirroring the tree. -->
|
|
62
|
+
<div class="helper-scroll" style="flex: 1 1 auto; min-height: 0; overflow-y: auto;">
|
|
63
|
+
<div style="padding-right: 0.6rem;">
|
|
64
|
+
${renderContent ? renderContent() : emptyState()}
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</bim-panel>
|
|
70
|
+
`,
|
|
71
|
+
{ ...DEFAULT },
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
element: panel,
|
|
76
|
+
show({ title, icon, render }) {
|
|
77
|
+
renderContent = render;
|
|
78
|
+
update({ title, icon });
|
|
79
|
+
},
|
|
80
|
+
clear() {
|
|
81
|
+
renderContent = null;
|
|
82
|
+
update({ ...DEFAULT });
|
|
83
|
+
},
|
|
84
|
+
refresh() {
|
|
85
|
+
update({}); // re-runs the template → re-calls renderContent()
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
};
|