- name
- porting-tools-to-fluent
- description
- Guide for porting Babylon.js tools from legacy shared-ui-components to Fluent UI using MakeModularTool. Use when: port to fluent, migrate to fluent, fluent migration, porting tool UI.
# Porting Babylon.js Tools to Fluent UI
Guide for porting Babylon.js tools (NME, NGE, NPE, NRGE, Playground, etc.) from the legacy shared-ui-components to Fluent UI using the `MakeModularTool` framework from `shared-ui-components/modularTool/`.
**Reference implementation:** `packages/tools/viewer-configurator/` (fully ported).
**Design guidelines:** For the shared design system these ports target — color schema, shell/panel structure, component and styling conventions — see [`.github/design-guidelines.md`](../../design-guidelines.md).
---
## Overview
A Fluent port replaces four layers:
1. **Bootstrapping** — from ad-hoc `createRoot` + `<App />` to `MakeModularTool` (provides theming, settings, shell layout)
2. **Layout** — from hand-rolled split containers / panes to `IShellService` (central content, side panes, toolbars)
3. **Components** — from legacy shared-ui-components to `shared-ui-components/fluent/` primitives and HOCs
4. **Styling** — from raw SCSS/CSS to `makeStyles` from `@fluentui/react-components`
5. **Icons** — from FontAwesome to `@fluentui/react-icons`
---
## 1. Dependencies
### Add
```jsonc
// package.json devDependencies
"@fluentui/react-components": "^9.x", // for makeStyles, tokens, low-level Fluent components
"@fluentui/react-icons": "^2.x" // for all icons
```
Note: `@dev/shared-ui-components` should already be a dependency. It contains both the Fluent primitives (`fluent/`) and the ModularTool framework (`modularTool/`). No dependency on `@dev/inspector` is needed.
### Remove
```jsonc
"@fortawesome/fontawesome-svg-core": "...",
"@fortawesome/free-solid-svg-icons": "...",
"@fortawesome/free-regular-svg-icons": "...",
"@fortawesome/react-fontawesome": "...",
"sass": "...",
"sass-loader": "..." // if no other SCSS remains
```
### Vite config
Ensure the `shared-ui-components` alias is present:
```js
commonDevViteConfiguration({
aliases: {
"shared-ui-components": path.resolve("../../dev/sharedUiComponents/src"),
// ... other aliases as needed
},
});
```
### tsconfig.json
Ensure the `shared-ui-components` path mapping is present (no `inspector` mapping needed):
```jsonc
"paths": {
"shared-ui-components/*": ["../../dev/sharedUiComponents/src/*"]
}
```
---
## 2. Bootstrapping with MakeModularTool
Replace the old entry point:
```tsx
// BEFORE
const root = createRoot(document.getElementById("root")!);
root.render(<App />);
// AFTER
import { MakeModularTool } from "shared-ui-components/modularTool/modularTool";
MakeModularTool({
namespace: "MyToolName",
containerElement: document.getElementById("root")!,
serviceDefinitions: [
/* your service definitions */
],
toolbarMode: "compact", // "compact" for minimal toolbar, "full" for full toolbar
showThemeSelector: true, // adds theme toggle to toolbar
// Do NOT pass extensionFeeds to disable the extensions dialog
});
```
`MakeModularTool` automatically provides:
- `FluentProvider` + theme (light/dark)
- `SettingsStore` (persisted user preferences)
- `ThemeService` + optional `ThemeSelectorService`
- `ShellService` (layout: central content, side panes, toolbars)
- `ToastProvider` + `IToastService` for toast notifications (consume `ToastServiceIdentity` — do not roll your own container)
- `IDialogService` (consume `DialogServiceIdentity`) for modal alert/confirm dialogs — replaces ad-hoc `MessageDialog`
### Cross-window / popup hosting
`MakeModularTool` derives `targetDocument` from `containerElement.ownerDocument`. If your tool's entry function (e.g. `Show(options)`) hosts the editor in a popup window, just pass the popup body as `containerElement` — Fluent/Griffel/`Theme` plumb cross-window automatically.
- For a fully-Fluent popup, use `OpenPopupWindow` from `shared-ui-components/fluent/hoc/popupWindow`.
- If part of your tool still ships traditional CSS/SCSS (e.g. `shared-ui-components/nodeGraphSystem/`'s graph canvas), keep the legacy `CreatePopup` from `shared-ui-components/popupHelper` — it copies stylesheets into the popup. Fluent and `CreatePopup` coexist fine.
---
## 3. Service Architecture
Each tool should define its own services that populate the shell. A service is a `ServiceDefinition<Produces, Consumes>` with:
- `friendlyName` — human-readable name for debugging
- `produces` — array of service identity symbols this service provides
- `consumes` — array of service identity symbols this service depends on
- `factory(…consumedServices)` — returns an object satisfying the produced contracts + optional `IDisposable`
### Defining a service identity and contract
```tsx
export const MyServiceIdentity = Symbol("MyService");
export interface IMyService extends IService<typeof MyServiceIdentity> {
readonly someData: SomeType | undefined;
readonly onStateChanged: IReadonlyObservable<void>;
}
```
### Service factory pattern
```tsx
export const MyServiceDefinition: ServiceDefinition<[IMyService], [IShellService]> = {
friendlyName: "My Service",
produces: [MyServiceIdentity],
consumes: [ShellServiceIdentity],
factory: (shellService) => {
const onStateChanged = new Observable<void>();
let someData: SomeType | undefined;
// Register shell content
const registration = shellService.addCentralContent({
key: "MyContent",
component: () => <MyComponent />,
});
return {
get someData() {
return someData;
},
onStateChanged,
dispose: () => {
onStateChanged.clear();
registration.dispose();
},
} satisfies IMyService & IDisposable;
},
};
```
### Shell service APIs
- `shellService.addCentralContent({ key, component })` — main content area
- `shellService.addSidePane({ key, title, icon, horizontalLocation, verticalLocation, teachingMoment, content })` — side pane
- `shellService.addToolbarItem({ key, horizontalLocation, verticalLocation, teachingMoment, component })` — toolbar button
All return `IDisposable` — clean up in your service's `dispose()`.
### Reactive state with useObservableState
Use the `useObservableState` hook from `shared-ui-components/modularTool/` to subscribe to service state in React components:
```tsx
import { useObservableState } from "shared-ui-components/modularTool/hooks/observableHooks";
const myData = useObservableState(
() => myService.someData, // getter
myService.onStateChanged // observable to subscribe to
);
```
---
## 4. Component Mapping
### Legacy → Fluent shared component mapping
| Legacy Component | Fluent Replacement | Import Path |
| -------------------------------------- | -------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `LineContainerComponent` | `AccordionSection` | `shared-ui-components/fluent/primitives/accordion` |
| Side pane container | `Accordion` (or `ExtensibleAccordion`) | `shared-ui-components/fluent/primitives/accordion` |
| `CheckBoxLineComponent` | `Switch` (primitive) or `SwitchPropertyLine` (with label) | `shared-ui-components/fluent/primitives/switch` or `.../hoc/propertyLines/switchPropertyLine` |
| `SliderLineComponent` | `SyncedSliderInput` (primitive) or `SyncedSliderPropertyLine` (with label) | `shared-ui-components/fluent/primitives/syncedSlider` or `.../hoc/propertyLines/syncedSliderPropertyLine` |
| `OptionsLine` | `Dropdown` (primitive) or `StringDropdownPropertyLine` (with label) | `shared-ui-components/fluent/primitives/dropdown` or `.../hoc/propertyLines/dropdownPropertyLine` |
| `ButtonLineComponent` | `Button` (primitive) | `shared-ui-components/fluent/primitives/button` |
| `TextInputLineComponent` (single-line) | `TextInput` (primitive) or `TextInputPropertyLine` (with label) | `shared-ui-components/fluent/primitives/textInput` or `.../hoc/propertyLines/inputPropertyLine` |
| `TextInputLineComponent` (multiline) | Fluent `Textarea` + slot props | `@fluentui/react-components` |
| `MessageLineComponent` | `MessageBar` | `shared-ui-components/fluent/primitives/messageBar` |
| `Color4LineComponent` | `ColorPickerPopup` (primitive) or `Color4PropertyLine` (with label) | `shared-ui-components/fluent/primitives/colorPicker` or `.../hoc/propertyLines/colorPropertyLine` |
| `LockObject` | Not needed (Fluent property lines don't use it) | — |
| `FontAwesomeIconButton` | `Button` with `icon` prop | `shared-ui-components/fluent/primitives/button` |
| `SplitContainer` / `Splitter` | Shell service layout (side panes) | Handled by `MakeModularTool` |
### When to use PropertyLine vs primitives
- **`PropertyLine`** — Use when a row has a label. Renders `InfoLabel` + child content in a standardized layout with hover border. Import from `shared-ui-components/fluent/hoc/propertyLines/propertyLine`.
- **`LineContainer`** — Use for rows without a label. Simple wrapper with hover border. Same import path.
- **Primitives directly** — Use when you need custom layout, e.g. button groups or compound rows.
### Composing controls within PropertyLine
`PropertyLine` constrains its children via an internal `childWrapper` div with `overflow: hidden` and `whiteSpace: nowrap`. For multi-element children, wrap them in a flex container:
```tsx
const useStyles = makeStyles({
propertyContent: {
display: "flex",
flexDirection: "row",
alignItems: "center",
gap: tokens.spacingHorizontalS,
width: "100%",
},
fillControl: { flex: 1, minWidth: 0 },
buttonGroup: { display: "flex", flexDirection: "row", alignItems: "center" },
});
// Example: TextInput + action buttons
<PropertyLine label="Model URL">
<div className={classes.propertyContent}>
<div className={classes.fillControl}>
<TextInput value={url} onChange={setUrl} className={classes.fullWidth} />
</div>
<div className={classes.buttonGroup}>
<Button icon={ArrowUploadRegular} onClick={onUpload} />
<Button icon={ArrowResetRegular} onClick={onReset} />
</div>
</div>
</PropertyLine>;
```
---
## 5. Icons
Replace all FontAwesome icons with `@fluentui/react-icons`. For general icon conventions (unsized variants, `fontSize` sizing), see [fluent.instructions.md](fluent.instructions.md).
Common FontAwesome → Fluent mappings:
| FontAwesome | Fluent Icon |
| --------------------- | ---------------------------- |
| `faQuestionCircle` | `QuestionCircleRegular` |
| `faBullseye` | `TargetRegular` |
| `faCamera` | `CameraRegular` |
| `faCheck` | `CheckmarkRegular` |
| `faCopy` | `CopyRegular` |
| `faGripVertical` | `ReOrderDotsVerticalRegular` |
| `faRotateLeft` | `ArrowResetRegular` |
| `faSave` | `SaveRegular` |
| `faSquarePlus` | `AddSquareRegular` |
| `faTrashCan` | `DeleteRegular` |
| `faUpload` | `ArrowUploadRegular` |
| `faChevronDown` | `ChevronDownRegular` |
| `faChevronUp` | `ChevronUpRegular` |
| `faGear` / `faCog` | `SettingsRegular` |
| `faEye` | `EyeRegular` |
| `faEyeSlash` | `EyeOffRegular` |
| `faPlus` | `AddRegular` |
View on GitHub