Skip to main content

porting-tools-to-fluent

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.

Source facts

Repository
BabylonJS/Babylon.js
Last source activity
June 27, 2026 at 05:46
Detected SKILL.md language
English
Stars
26,167
Forks
3,707

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
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
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub