@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,178 @@
1
+ import * as BUI from "@thatopen/ui";
2
+
3
+ /**
4
+ * Mounts several panels as SEPARATE cards stacked vertically on the RIGHT,
5
+ * together spanning the full viewport height. One right-docked `bim-grid
6
+ * floating` holds one row per region (heights split by `grow`); the floating
7
+ * grid's built-in 1rem gap shows between the cards, and its empty "rest" column
8
+ * clicks through to the model. Each region is its own `bim-panel` (own
9
+ * header/border), so they read as distinct cards — not one card with a divider.
10
+ *
11
+ * Each panel element should be `height: 100%` so it fills its row and scrolls
12
+ * internally when its content is tall.
13
+ *
14
+ * @param container the viewport element to overlay
15
+ * @param regions the panels to stack, top to bottom
16
+ * @returns the floating grid element (already appended to the container)
17
+ */
18
+ export interface StackRegion {
19
+ /** The panel element for this region. */
20
+ element: HTMLElement;
21
+ /** Relative height share (CSS grid `fr`). Defaults to 1 (equal split). */
22
+ grow?: number;
23
+ }
24
+
25
+ export const rightStack = (container: HTMLElement, regions: StackRegion[]) => {
26
+ // ── Collapse toggle (mirror of the left files panel's) ─────────
27
+ // A bare chevron in its own thin grid column in the gutter, just OUTSIDE the
28
+ // cards (to their left). Collapsing swaps the grid layout to drop the card
29
+ // columns entirely; the chevron column stays. When collapsed the chevron sits
30
+ // inset from the right edge (the grid's 1rem padding), so it stays in view.
31
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
32
+ let gridEl: any = null;
33
+ let collapsed = false;
34
+
35
+ const [toggle, toggleUpdate] = BUI.Component.create<
36
+ HTMLElement,
37
+ { collapsed: boolean }
38
+ >(
39
+ (s) => BUI.html`
40
+ <div
41
+ style="
42
+ height: 100%; display: flex; align-items: flex-start;
43
+ pointer-events: none; padding-top: 0.5rem; padding-right: 0.1rem;
44
+ "
45
+ >
46
+ <span
47
+ role="button"
48
+ title=${s.collapsed ? "Show panel" : "Hide panel"}
49
+ @click=${() => setCollapsed(!s.collapsed)}
50
+ style="
51
+ pointer-events: auto; cursor: pointer; display: inline-flex;
52
+ color: var(--bim-ui_bg-contrast-80, #c9c9c9);
53
+ "
54
+ ><bim-icon
55
+ icon=${s.collapsed ? "mdi:chevron-left" : "mdi:chevron-right"}
56
+ style="font-size: 1.2rem;"
57
+ ></bim-icon></span>
58
+ </div>
59
+ `,
60
+ { collapsed: false },
61
+ );
62
+
63
+ const setCollapsed = (v: boolean) => {
64
+ collapsed = v;
65
+ if (gridEl) gridEl.layout = v ? "collapsed" : "main";
66
+ toggleUpdate({ collapsed: v });
67
+ };
68
+
69
+ // ── Floating grid: cards docked full-height right, chevron in the gutter ──
70
+ const grid = BUI.Component.create(() => {
71
+ const onCreated = (element?: Element) => {
72
+ if (!element) return;
73
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
74
+ const g = element as any;
75
+ gridEl = g;
76
+ const names = regions.map((_, i) => `r${i}`);
77
+ g.elements = {
78
+ toggle,
79
+ ...Object.fromEntries(regions.map((r, i) => [names[i], r.element])),
80
+ };
81
+ // One row per region; columns: rest (1fr, click-through) | toggle | cards.
82
+ // The "toggle" area repeats across rows → one full-height gutter column.
83
+ const rows = regions
84
+ .map((r, i) => `"rest toggle ${names[i]}" ${r.grow ?? 1}fr`)
85
+ .join("\n");
86
+ g.layouts = {
87
+ main: { template: `${rows}\n/ 1fr auto auto` },
88
+ // Collapsed: drop the card columns; keep rest + toggle (toggle ends up
89
+ // near the right edge, inset by the grid's 1rem padding).
90
+ collapsed: { template: `"rest toggle" 1fr\n/ 1fr auto` },
91
+ };
92
+ g.layout = collapsed ? "collapsed" : "main";
93
+ };
94
+ return BUI.html`
95
+ <bim-grid style="padding: 1rem;" ${BUI.ref(onCreated)} floating></bim-grid>
96
+ `;
97
+ });
98
+ container.append(grid);
99
+
100
+ return { grid };
101
+ };
102
+
103
+ /**
104
+ * Mirror of {@link rightStack} for the LEFT side: cards stacked vertically on
105
+ * the left (full height), same 1rem gap + outer margins, with a collapse chevron
106
+ * in the gutter to the RIGHT of the cards (facing the viewport). Used for the
107
+ * Files (top) + helper (bottom) stack.
108
+ */
109
+ export const leftStack = (container: HTMLElement, regions: StackRegion[]) => {
110
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
111
+ let gridEl: any = null;
112
+ let collapsed = false;
113
+
114
+ const [toggle, toggleUpdate] = BUI.Component.create<
115
+ HTMLElement,
116
+ { collapsed: boolean }
117
+ >(
118
+ (s) => BUI.html`
119
+ <div
120
+ style="
121
+ height: 100%; display: flex; align-items: flex-start;
122
+ pointer-events: none; padding-top: 0.5rem; padding-left: 0.1rem;
123
+ "
124
+ >
125
+ <span
126
+ role="button"
127
+ title=${s.collapsed ? "Show panel" : "Hide panel"}
128
+ @click=${() => setCollapsed(!s.collapsed)}
129
+ style="
130
+ pointer-events: auto; cursor: pointer; display: inline-flex;
131
+ color: var(--bim-ui_bg-contrast-80, #c9c9c9);
132
+ "
133
+ ><bim-icon
134
+ icon=${s.collapsed ? "mdi:chevron-right" : "mdi:chevron-left"}
135
+ style="font-size: 1.2rem;"
136
+ ></bim-icon></span>
137
+ </div>
138
+ `,
139
+ { collapsed: false },
140
+ );
141
+
142
+ const setCollapsed = (v: boolean) => {
143
+ collapsed = v;
144
+ if (gridEl) gridEl.layout = v ? "collapsed" : "main";
145
+ toggleUpdate({ collapsed: v });
146
+ };
147
+
148
+ const grid = BUI.Component.create(() => {
149
+ const onCreated = (element?: Element) => {
150
+ if (!element) return;
151
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
152
+ const g = element as any;
153
+ gridEl = g;
154
+ const names = regions.map((_, i) => `r${i}`);
155
+ g.elements = {
156
+ toggle,
157
+ ...Object.fromEntries(regions.map((r, i) => [names[i], r.element])),
158
+ };
159
+ // Columns: cards | toggle (gutter, facing viewport) | rest (click-through).
160
+ const rows = regions
161
+ .map((r, i) => `"${names[i]} toggle rest" ${r.grow ?? 1}fr`)
162
+ .join("\n");
163
+ g.layouts = {
164
+ main: { template: `${rows}\n/ auto auto 1fr` },
165
+ // Collapsed: drop the card columns; toggle stays at the left edge (inset
166
+ // by the grid's 1rem padding).
167
+ collapsed: { template: `"toggle rest" 1fr\n/ auto 1fr` },
168
+ };
169
+ g.layout = collapsed ? "collapsed" : "main";
170
+ };
171
+ return BUI.html`
172
+ <bim-grid style="padding: 1rem;" ${BUI.ref(onCreated)} floating></bim-grid>
173
+ `;
174
+ });
175
+ container.append(grid);
176
+
177
+ return { grid };
178
+ };
@@ -0,0 +1,94 @@
1
+ import * as BUI from "@thatopen/ui";
2
+ import { cardHeader } from "./card-header";
3
+
4
+ /**
5
+ * Merged SETTINGS panel (UI-reorg) — ONE scrolling bim-panel with a collapsible
6
+ * section per source panel (Graphics / Clip styling / Measurement / Commands).
7
+ * Reuses each panel's existing element verbatim.
8
+ *
9
+ * Sections use a hand-rolled light-DOM header (NOT bim-panel-section, whose
10
+ * header/content padding is hardcoded in shadow DOM and can't be flattened) so
11
+ * the section title row + its divider span the FULL panel width with no inset,
12
+ * and the section content sits flush. The nested bim-panel's own chrome is
13
+ * removed (transparent bg / no border / no radius) and its height:100% / inner
14
+ * scroll neutralised so it flows and the Settings panel does the single scroll —
15
+ * all scoped to light-DOM descendants, so bim-* widget shadow internals (sliders,
16
+ * dropdowns, color inputs) are untouched. Returns the `bim-panel` (no self-mount).
17
+ *
18
+ * @param sections ordered settings sections (label + icon + the panel element)
19
+ * @returns the merged Settings `bim-panel`
20
+ */
21
+ export const settingsPanel = (
22
+ sections: { label: string; icon: string; el: HTMLElement }[],
23
+ ) => {
24
+ for (const s of sections) {
25
+ s.el.setAttribute("header-hidden", ""); // our section header replaces it
26
+ s.el.style.height = "auto";
27
+ s.el.style.width = "100%";
28
+ }
29
+ const collapsed = new Set<string>(); // section labels currently collapsed
30
+
31
+ const [panel, update] = BUI.Component.create<BUI.Panel, { tick: number }>(
32
+ (_s) => BUI.html`
33
+ <bim-panel
34
+ label="Settings"
35
+ icon="mdi:cog"
36
+ header-hidden
37
+ style="width: 100%; height: 100%; pointer-events: auto;"
38
+ >
39
+ <style>
40
+ .set-scroll { display: flex; flex-direction: column; height: 100%; overflow-y: auto; }
41
+ .set-scroll::-webkit-scrollbar { width: 0.4rem; }
42
+ .set-scroll::-webkit-scrollbar-thumb { border-radius: 0.25rem;
43
+ background-color: var(--bim-scrollbar--c, #3C3C41); }
44
+ /* Full-width section header: title row + divider span the whole panel,
45
+ no side inset/box. */
46
+ .set-hd { display: flex; align-items: center; gap: 0.5rem; cursor: pointer;
47
+ margin-top: 1.2rem;
48
+ padding: 0.45rem 0.75rem; font-weight: 600;
49
+ font-size: var(--bim-ui_size-lg, 0.8125rem);
50
+ color: var(--bim-ui_bg-contrast-100, #f1f2f4);
51
+ /* contrast-40 (#323237) so the divider is VISIBLE on the #262629
52
+ surface — contrast-20 now equals the surface (palette pin). */
53
+ border-bottom: 1px solid var(--bim-ui_bg-contrast-40, rgba(255,255,255,0.12)); }
54
+ .set-hd:hover { background: var(--bim-ui_bg-contrast-20, rgba(255,255,255,0.04)); }
55
+ /* Section icon matches the title font color (not a dimmer grey). */
56
+ .set-hd-ico { flex: 0 0 auto; color: inherit; }
57
+ .set-hd-t { flex: 1 1 auto; }
58
+ .set-hd-x { flex: 0 0 auto; color: var(--bim-ui_bg-contrast-80, #adadad); }
59
+ /* Flatten the nested panel: no "panel inside a panel", content flush to
60
+ the edges (its own bands full-bleed). Light-DOM only — widget shadow
61
+ internals untouched. */
62
+ .set-sec bim-panel {
63
+ height: auto !important;
64
+ width: 100%;
65
+ --bim-panel--bg: transparent;
66
+ --bim-panel--border: none;
67
+ border-radius: 0 !important;
68
+ box-shadow: none !important;
69
+ }
70
+ .set-sec bim-panel * { height: auto !important; max-height: none !important; }
71
+ </style>
72
+ <div class="set-scroll">
73
+ ${cardHeader("mdi:cog", "Settings", "0.75rem")}
74
+ ${sections.map((s, i) => {
75
+ const open = !collapsed.has(s.label);
76
+ return BUI.html`
77
+ <div class="set-hd" style=${i === 0 ? "margin-top: 0;" : ""} @click=${() => {
78
+ if (collapsed.has(s.label)) collapsed.delete(s.label);
79
+ else collapsed.add(s.label);
80
+ update({ tick: 0 });
81
+ }}>
82
+ <bim-icon class="set-hd-ico" icon=${s.icon}></bim-icon>
83
+ <span class="set-hd-t">${s.label}</span>
84
+ <bim-icon class="set-hd-x" icon=${open ? "mdi:chevron-up" : "mdi:chevron-down"}></bim-icon>
85
+ </div>
86
+ <div class="set-sec" style=${open ? "" : "display: none;"}>${s.el}</div>`;
87
+ })}
88
+ </div>
89
+ </bim-panel>`,
90
+ { tick: 0 },
91
+ );
92
+
93
+ return panel;
94
+ };
@@ -0,0 +1,110 @@
1
+ import * as OBC from "@thatopen/components";
2
+ import * as BUI from "@thatopen/ui";
3
+ import { styles, StyleSetting } from "./styles";
4
+
5
+ /**
6
+ * UI side of the Styles tool. Consumes Worker 1's style DESCRIPTOR
7
+ * (`styles(components, world).settings`) and renders it generically in the
8
+ * bottom-left helper panel: checkbox (bool), slider (number), color picker
9
+ * (color), dropdown (enum) — each wired live to the setting's get/set. Settings
10
+ * are grouped by their `group` field.
11
+ *
12
+ * Returned as a "panel tool" `{ label, icon, render }` for the toolbar; `render`
13
+ * receives a `refresh` callback to re-read all controls after a change (needed
14
+ * because e.g. the preset enum rewrites several other settings).
15
+ */
16
+ const control = (s: StyleSetting, refresh: () => void) => {
17
+ switch (s.type) {
18
+ case "bool":
19
+ return BUI.html`<bim-checkbox
20
+ label=${s.label}
21
+ ?checked=${s.get()}
22
+ @change=${(e: Event) => {
23
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
+ s.set(!!(e.target as any).value);
25
+ refresh();
26
+ }}
27
+ ></bim-checkbox>`;
28
+ case "number":
29
+ return BUI.html`<bim-number-input
30
+ label=${s.label}
31
+ slider
32
+ value=${s.get()}
33
+ min=${s.min}
34
+ max=${s.max}
35
+ step=${s.step}
36
+ @change=${(e: Event) => {
37
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
38
+ s.set(Number((e.target as any).value));
39
+ refresh();
40
+ }}
41
+ ></bim-number-input>`;
42
+ case "color":
43
+ return BUI.html`<bim-color-input
44
+ label=${s.label}
45
+ color=${s.get()}
46
+ @input=${(e: Event) => {
47
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
48
+ const t = e.target as any;
49
+ s.set(String(t.color ?? t.value));
50
+ refresh();
51
+ }}
52
+ ></bim-color-input>`;
53
+ case "enum":
54
+ return BUI.html`<bim-dropdown
55
+ label=${s.label}
56
+ @change=${(e: Event) => {
57
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
58
+ const picked = (e.target as any).value?.[0];
59
+ const match = s.options.find((o) => o.label === picked);
60
+ if (match) {
61
+ s.set(match.value);
62
+ refresh();
63
+ }
64
+ }}
65
+ >
66
+ ${s.options.map(
67
+ (o) => BUI.html`<bim-option label=${o.label} ?checked=${o.value === s.get()}></bim-option>`,
68
+ )}
69
+ </bim-dropdown>`;
70
+ }
71
+ };
72
+
73
+ export const stylesTool = (components: OBC.Components, world: OBC.World) => ({
74
+ label: "Styles",
75
+ icon: "mdi:palette",
76
+ render: (refresh: () => void) => {
77
+ const { settings } = styles(components, world);
78
+ // Group settings by their `group` field, preserving first-seen order.
79
+ const groups: { name: string; items: StyleSetting[] }[] = [];
80
+ for (const s of settings) {
81
+ let g = groups.find((x) => x.name === s.group);
82
+ if (!g) {
83
+ g = { name: s.group, items: [] };
84
+ groups.push(g);
85
+ }
86
+ g.items.push(s);
87
+ }
88
+ return BUI.html`
89
+ <div style="display: flex; flex-direction: column; gap: 0.6rem;">
90
+ ${groups.map(
91
+ (g) => BUI.html`
92
+ <div>
93
+ <div style="
94
+ font-size: 0.72rem; font-weight: 600; letter-spacing: 0.04em;
95
+ text-transform: uppercase; opacity: 0.6;
96
+ color: var(--bim-ui_bg-contrast-100, #e3e3e3);
97
+ padding: 0.1rem 0 0.3rem; border-bottom: 1px solid var(--bim-ui_bg-contrast-20, rgba(255,255,255,0.1));
98
+ ">${g.name}</div>
99
+ <div style="display: flex; flex-direction: column; padding-top: 0.25rem;">
100
+ ${g.items.map(
101
+ (s) => BUI.html`<div style="padding: 0.3rem 0; border-bottom: 1px solid var(--bim-ui_bg-contrast-20, rgba(255, 255, 255, 0.1));">${control(s, refresh)}</div>`,
102
+ )}
103
+ </div>
104
+ </div>
105
+ `,
106
+ )}
107
+ </div>
108
+ `;
109
+ },
110
+ });