+ );
+};
diff --git a/packages/dev/sharedUiComponents/src/fluent/hoc/syncedSliderLine.tsx b/packages/dev/sharedUiComponents/src/fluent/hoc/syncedSliderLine.tsx
new file mode 100644
index 00000000000..66b466a462c
--- /dev/null
+++ b/packages/dev/sharedUiComponents/src/fluent/hoc/syncedSliderLine.tsx
@@ -0,0 +1,45 @@
+import { PropertyLine } from "./propertyLine";
+import type { PropertyLineProps } from "./propertyLine";
+import { SyncedSliderInput } from "../primitives/syncedSlider";
+import type { SyncedSliderProps } from "../primitives/syncedSlider";
+
+export type SyncedSliderLineProps = PropertyLineProps &
+ Omit & {
+ /**
+ * String key
+ */
+ propertyKey: K;
+ /**
+ * target where O[K] is a number
+ */
+ target: O;
+ /**
+ * Callback when either the slider or input value changes
+ */
+ onChange?: (value: number) => void;
+ };
+
+/**
+ * Renders a SyncedSlider within a PropertyLine for a given key/value pair, where the value is number (ex: can be used for a color's RGBA values, a vector's XYZ values, etc)
+ * When value changes, updates the object with the new value and calls the onChange callback.
+ *
+ * Example usage looks like
+ * \
+ * \
+ * @param props
+ * @returns
+ */
+export const SyncedSliderLine = , K extends PropertyKey>(props: SyncedSliderLineProps): React.ReactElement => {
+ return (
+
+ {
+ props.target[props.propertyKey] = val as O[K];
+ props.onChange?.(val);
+ }}
+ />
+
+ );
+};
diff --git a/packages/dev/sharedUiComponents/src/fluent/hoc/vectorPropertyLine.tsx b/packages/dev/sharedUiComponents/src/fluent/hoc/vectorPropertyLine.tsx
new file mode 100644
index 00000000000..9019e95baea
--- /dev/null
+++ b/packages/dev/sharedUiComponents/src/fluent/hoc/vectorPropertyLine.tsx
@@ -0,0 +1,43 @@
+import type { FunctionComponent } from "react";
+
+import { Body1 } from "@fluentui/react-components";
+import { PropertyLine } from "./propertyLine";
+import type { PropertyLineProps } from "./propertyLine";
+
+import { SyncedSliderLine } from "./syncedSliderLine";
+
+import { Vector4 } from "core/Maths/math.vector";
+import type { Vector3 } from "core/Maths/math.vector";
+
+type VectorSliderProps = {
+ vector: Vector3 | Vector4;
+ min?: number;
+ max?: number;
+ step?: number;
+};
+
+const VectorSliders: FunctionComponent = (props) => {
+ const { vector, ...sliderProps } = props;
+ return (
+ <>
+
+
+
+ {vector instanceof Vector4 && }
+ >
+ );
+};
+
+/**
+ * Reusable component which renders a vector property line containing a label, vector value, and expandable XYZW values
+ * The expanded section contains a slider/input box for each component of the vector (x, y, z, w)
+ * @param props
+ * @returns
+ */
+export const VectorPropertyLine: FunctionComponent = (props) => {
+ return (
+ }>
+ {props.vector.toString()}
+
+ );
+};
diff --git a/packages/dev/sharedUiComponents/src/fluent/primitives/dropdown.tsx b/packages/dev/sharedUiComponents/src/fluent/primitives/dropdown.tsx
new file mode 100644
index 00000000000..b631b8790bf
--- /dev/null
+++ b/packages/dev/sharedUiComponents/src/fluent/primitives/dropdown.tsx
@@ -0,0 +1,48 @@
+import { Dropdown as FluentDropdown, makeStyles, Option } from "@fluentui/react-components";
+import type { FunctionComponent } from "react";
+
+const useDropdownStyles = makeStyles({
+ dropdownOption: {
+ textAlign: "right",
+ minWidth: "40px",
+ },
+ optionsLine: {},
+});
+
+export type DropdownOption = {
+ /**
+ * Defines the visible part of the option
+ */
+ label: string;
+ /**
+ * Defines the value part of the option (returned through the callback)
+ */
+ value: string | number;
+};
+
+type DropdownProps = { options: readonly DropdownOption[]; onSelect: (o: string) => void; defaultValue?: DropdownOption };
+
+/**
+ * Renders a fluent UI dropdown with a calback for selection and a required default value
+ * @param props
+ * @returns dropdown component
+ */
+export const Dropdown: FunctionComponent = (props) => {
+ const classes = useDropdownStyles();
+ return (
+ {
+ data.optionValue != undefined && props.onSelect(data.optionValue);
+ }}
+ defaultValue={props.defaultValue?.label}
+ defaultSelectedOptions={props.defaultValue && [props.defaultValue.value.toString()]}
+ >
+ {props.options.map((option: DropdownOption) => (
+
+ ))}
+
+ );
+};
diff --git a/packages/dev/sharedUiComponents/src/fluent/primitives/input.tsx b/packages/dev/sharedUiComponents/src/fluent/primitives/input.tsx
new file mode 100644
index 00000000000..39c1448d191
--- /dev/null
+++ b/packages/dev/sharedUiComponents/src/fluent/primitives/input.tsx
@@ -0,0 +1,47 @@
+import type { FunctionComponent, KeyboardEvent, ChangeEvent } from "react";
+import { useEffect, useState } from "react";
+
+import type { InputProps as FluentInputProps } from "@fluentui/react-components";
+import { Input as FluentInput, makeStyles } from "@fluentui/react-components";
+
+const useInputStyles = makeStyles({
+ text: {
+ height: "auto",
+ },
+ float: {
+ height: "auto",
+ width: "80px", // Fixed width for number input
+ flexShrink: 0,
+ },
+});
+
+/**
+ * This is an input text box that stops propagation of change events and sets its width based on the type of input (text or number)
+ * @param props
+ * @returns
+ */
+export const Input: FunctionComponent = (props) => {
+ const classes = useInputStyles();
+ const [value, setValue] = useState(props.value ?? "");
+
+ useEffect(() => {
+ setValue(props.value ?? ""); // Update local state when props.value changes
+ }, [props.value]);
+
+ const handleChange = (event: ChangeEvent, data: any) => {
+ event.stopPropagation(); // Prevent event propagation
+ if (props.onChange) {
+ props.onChange(event, data); // Call the original onChange handler passed as prop
+ }
+ setValue(event.target.value); // Update local state with the new value
+ };
+
+ const handleKeyDown = (event: KeyboardEvent) => {
+ event.stopPropagation(); // Prevent event propagation
+ if (props.onKeyDown) {
+ props.onKeyDown(event); // Call the original onKeyDown handler passed as prop
+ }
+ };
+
+ return ;
+};
diff --git a/packages/dev/sharedUiComponents/src/fluent/primitives/link.tsx b/packages/dev/sharedUiComponents/src/fluent/primitives/link.tsx
new file mode 100644
index 00000000000..0df5e9f8709
--- /dev/null
+++ b/packages/dev/sharedUiComponents/src/fluent/primitives/link.tsx
@@ -0,0 +1,3 @@
+// No special styling / functionality yet!
+
+export { Link } from "@fluentui/react-components";
diff --git a/packages/dev/sharedUiComponents/src/fluent/primitives/switch.tsx b/packages/dev/sharedUiComponents/src/fluent/primitives/switch.tsx
new file mode 100644
index 00000000000..00ffbfb38bb
--- /dev/null
+++ b/packages/dev/sharedUiComponents/src/fluent/primitives/switch.tsx
@@ -0,0 +1,33 @@
+// eslint-disable-next-line import/no-internal-modules
+
+import type { SwitchOnChangeData, SwitchProps as FluentSwitchProps } from "@fluentui/react-components";
+import type { ChangeEvent, FunctionComponent } from "react";
+
+import { makeStyles, Switch as FluentSwitch } from "@fluentui/react-components";
+import { useState } from "react";
+
+const useSwitchStyles = makeStyles({
+ switch: {
+ marginLeft: "auto",
+ },
+ indicator: {
+ marginRight: 0, // Remove the default right margin so the switch aligns well on the right side inside panels like the properties pane.
+ },
+});
+
+/**
+ * This is a primitive fluent boolean switch component whose only knowledge is the shared styling across all tools
+ * @param props
+ * @returns Switch component
+ */
+export const Switch: FunctionComponent = (props) => {
+ const classes = useSwitchStyles();
+ const [checked, setChecked] = useState(() => props.checked ?? false);
+
+ const onChange = (event: ChangeEvent, data: SwitchOnChangeData) => {
+ props.onChange && props.onChange(event, data);
+ setChecked(event.target.checked);
+ };
+
+ return ;
+};
diff --git a/packages/dev/sharedUiComponents/src/fluent/primitives/syncedSlider.tsx b/packages/dev/sharedUiComponents/src/fluent/primitives/syncedSlider.tsx
new file mode 100644
index 00000000000..b976814f61a
--- /dev/null
+++ b/packages/dev/sharedUiComponents/src/fluent/primitives/syncedSlider.tsx
@@ -0,0 +1,67 @@
+import type { InputProps, SliderOnChangeData, SliderProps } from "@fluentui/react-components";
+import { makeStyles, Slider } from "@fluentui/react-components";
+import { Input } from "./input";
+import type { ChangeEvent, FunctionComponent } from "react";
+import { useEffect, useState } from "react";
+
+const useSyncedSliderStyles = makeStyles({
+ syncedSlider: {
+ display: "flex",
+ alignItems: "center",
+ gap: "1rem",
+ width: "100%", // Only fill available space
+ },
+ slider: {
+ flexGrow: 1, // Let slider grow
+ minWidth: 0, // Allow shrink if needed
+ },
+ input: {
+ width: "80px", // Fixed width for number input
+ flexShrink: 0,
+ },
+});
+
+export type SyncedSliderProps = Omit & {
+ /**
+ * Callback to notify parent of value change, override both of the slider/input handlers
+ */
+ onChange: (value: number) => void;
+ /**
+ * Controlled value for the slider and input
+ */
+ value: number;
+};
+
+/**
+ * Component which synchronizes a slider and an input field, allowing the user to change a value using either control
+ * @param props
+ * @returns SyncedSlider component
+ */
+export const SyncedSliderInput: FunctionComponent = (props) => {
+ const classes = useSyncedSliderStyles();
+ const [value, setValue] = useState(props.value);
+
+ useEffect(() => {
+ setValue(props.value ?? ""); // Update local state when props.value changes
+ }, [props.value]);
+
+ const handleSliderChange = (_: ChangeEvent, data: SliderOnChangeData) => {
+ setValue(data.value);
+ props.onChange(data.value); // Notify parent
+ };
+
+ const handleInputChange = (e: ChangeEvent) => {
+ const newValue = Number(e.target.value);
+ if (!isNaN(newValue)) {
+ setValue(newValue);
+ props.onChange(newValue); // Notify parent
+ }
+ };
+
+ return (
+
+
+
+
+ );
+};
diff --git a/packages/dev/sharedUiComponents/src/fluent/primitives/text.tsx b/packages/dev/sharedUiComponents/src/fluent/primitives/text.tsx
new file mode 100644
index 00000000000..5812e2cf140
--- /dev/null
+++ b/packages/dev/sharedUiComponents/src/fluent/primitives/text.tsx
@@ -0,0 +1,3 @@
+export { Text } from "@fluentui/react-components";
+
+// No special styling / functionality yet!
diff --git a/packages/dev/sharedUiComponents/src/fluent/primitives/textarea.tsx b/packages/dev/sharedUiComponents/src/fluent/primitives/textarea.tsx
new file mode 100644
index 00000000000..f53a70c1fab
--- /dev/null
+++ b/packages/dev/sharedUiComponents/src/fluent/primitives/textarea.tsx
@@ -0,0 +1,31 @@
+import type { TextareaProps as FluentTextareaProps } from "@fluentui/react-components";
+import { Textarea as FluentTextarea, makeStyles } from "@fluentui/react-components";
+import type { FunctionComponent, KeyboardEvent, ChangeEvent } from "react";
+
+const useInputStyles = makeStyles({
+ textarea: {},
+});
+
+/**
+ * This is a texarea box that stops propagation of change/keydown events
+ * @param props
+ * @returns
+ */
+export const Textarea: FunctionComponent = (props) => {
+ const classes = useInputStyles();
+ const handleChange = (event: ChangeEvent, data: any) => {
+ event.stopPropagation(); // Prevent event propagation
+ if (props.onChange) {
+ props.onChange(event, data); // Call the original onChange handler passed as prop
+ }
+ };
+
+ const handleKeyDown = (event: KeyboardEvent) => {
+ event.stopPropagation(); // Prevent event propagation
+ if (props.onKeyDown) {
+ props.onKeyDown(event); // Call the original onKeyDown handler passed as prop
+ }
+ };
+
+ return ;
+};
diff --git a/packages/dev/sharedUiComponents/src/fluent/readme.md b/packages/dev/sharedUiComponents/src/fluent/readme.md
new file mode 100644
index 00000000000..02e4da93ed0
--- /dev/null
+++ b/packages/dev/sharedUiComponents/src/fluent/readme.md
@@ -0,0 +1,19 @@
+# Fluent!
+
+We are embarking on an effort to replace our common controls with modernized fluent UX controls, used by the existing tools and the in-progress inspectorV2.
+
+This work is split into several distinct chunks
+
+1. **Fluent primitives**
+ These are lightweight wrappers around existing fluent components, with some minor styling applied. Depending on the primitive at hand, the wrapper may do very little (ex: apply some minor styling) or more complex logic (ex: SyncedSlider.tsx coordinates changes between a slider component and an input component).
+ These primitive can be used standalone or within a pre-styled component such as PropertyLine.tsx, see #2
+2. **Fluent higher order components (hoc)**
+ These are wrappers around the above primitives to handle styling for common UX patterns, ex: PropertyLine.tsx which renders a label, fluent primtive child component (to modify said propetry), and optional copy button. This is a common UX pattern used in the property pane of all of our tools.
+3. **Render fluent components directly from inspectorV2**
+ InspectorV2 will be using the new fluent components directly, rather than using any existing shared-ui-components.You can see examples of this in various files within packages\dev\inspector-v2 (ex: meshGeneralProperties.tsx, commonGeneralProperties.tsx, outlineOverlayProperties.tsx).
+4. **Conditionally render fluent components within existing shared-ui-components**
+ We are using a contextConsumer to read context.useFluent from within each shared component and conditionally render the fluent version of each shared component. This means that existing tools do not have to update every callsite of a shared component, instead the tool just needs to ensure the contextProvider is properly created at the tool root, see below
+5. **Incrementally move each tool over to fluent**
+ 1 by 1 we will wrap each tool's root in a , which both creates a fluent provider and ensures useFluent context is set to true (so shared components render their fluent versions). This will allow for modular testing of the new components within each tool before lighting it up. Note that we also check for a 'newUX' Query String Parameter in the URL so that we can check-in the new logic without default enabling it for all users (to enable, add ?newUX=true to the URL)
+6. **Incrementally refactor existing tools**
+ After each tool is fluentized, we can incrementally refactor the way the shared components are being initialized (ex: see PropertyTabComponent) to extract common tool paradigms into our shared tooling folder and reduce duplication of logic across our tools.
diff --git a/packages/dev/sharedUiComponents/src/lines/optionsLineComponent.tsx b/packages/dev/sharedUiComponents/src/lines/optionsLineComponent.tsx
index b48ea1b36cf..9f3c8a10cd3 100644
--- a/packages/dev/sharedUiComponents/src/lines/optionsLineComponent.tsx
+++ b/packages/dev/sharedUiComponents/src/lines/optionsLineComponent.tsx
@@ -4,6 +4,9 @@ import type { PropertyChangedEvent } from "../propertyChangedEvent";
import { copyCommandToClipboard, getClassNameWithNamespace } from "../copyCommandToClipboard";
import type { IInspectableOptions } from "core/Misc/iInspectable";
import copyIcon from "../imgs/copy.svg";
+import { PropertyLine } from "../fluent/hoc/propertyLine";
+import { Dropdown } from "../fluent/primitives/dropdown";
+import { ToolContext } from "../fluent/hoc/fluentToolWrapper";
// eslint-disable-next-line @typescript-eslint/naming-convention
export const Null_Value = Number.MAX_SAFE_INTEGER;
@@ -103,20 +106,34 @@ export class OptionsLine extends React.Component this.onCopyClickStr()}>
+ {
+ val !== undefined && this.updateValue(val);
+ }}
+ defaultValue={this.props.options.find((o) => o.value === this.state.value || o.selected)}
+ />
+
+ );
+ }
+
+ private _renderOriginal() {
return (