diff --git a/platform/ui-next/.webpack/webpack.playground.js b/platform/ui-next/.webpack/webpack.playground.js index b6f59876829..11f2b79d7e4 100644 --- a/platform/ui-next/.webpack/webpack.playground.js +++ b/platform/ui-next/.webpack/webpack.playground.js @@ -38,6 +38,7 @@ module.exports = { entry: { home: './src/_pages/index.tsx', playground: './src/_pages/playground.tsx', + patterns: './src/_pages/patterns.tsx', viewer: './src/_pages/viewer.tsx', colors: './src/_pages/colors.tsx', // add other pages here @@ -82,6 +83,11 @@ module.exports = { chunks: ['playground'], filename: 'playground.html', }), + new HtmlWebpackPlugin({ + template: './.webpack/template.html', + chunks: ['patterns'], + filename: 'patterns.html', + }), new HtmlWebpackPlugin({ template: './.webpack/template.html', chunks: ['viewer'], diff --git a/platform/ui-next/src/_pages/index.tsx b/platform/ui-next/src/_pages/index.tsx index 27b8e74e217..5165e636f85 100644 --- a/platform/ui-next/src/_pages/index.tsx +++ b/platform/ui-next/src/_pages/index.tsx @@ -13,6 +13,12 @@ const App: React.FC = () => ( > Playground + + Patterns + +
Patterns
+ ; + + ); +} + +const container = document.getElementById('root'); +const root = createRoot(container); +root.render(React.createElement(Patterns)); diff --git a/platform/ui-next/src/_pages/playground.tsx b/platform/ui-next/src/_pages/playground.tsx index 2c73a43214e..fc1bf3a7e03 100644 --- a/platform/ui-next/src/_pages/playground.tsx +++ b/platform/ui-next/src/_pages/playground.tsx @@ -18,7 +18,7 @@ import { SelectScrollDownButton, } from '../components/Select'; import { Tabs, TabsList, TabsTrigger, TabsContent } from '../components/Tabs'; -import { Separator } from '../components/Separator'; +import Separator from '../components/Separator'; import { Switch } from '../components/Switch'; import { Checkbox } from '../components/Checkbox'; import { Toggle, toggleVariants } from '../components/Toggle'; @@ -26,6 +26,7 @@ import { Slider } from '../components/Slider'; import { ScrollArea, ScrollBar } from '../components/ScrollArea'; import { BackgroundColorSelect } from '../components/BackgroundColorSelect'; + // import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '../components/Tooltip'; // import type { Metadata } from 'next'; diff --git a/platform/ui-next/src/components/PanelSplit/ItemList.tsx b/platform/ui-next/src/components/PanelSplit/ItemList.tsx new file mode 100644 index 00000000000..1c475df89ee --- /dev/null +++ b/platform/ui-next/src/components/PanelSplit/ItemList.tsx @@ -0,0 +1,157 @@ +import React from 'react'; +import { Item, VisibilityState } from './types'; +import { Button } from '../Button'; + +interface ItemListProps { + items: Item[]; + onSelectItem: (item: Item) => void; + selectedItem: Item | null; + onToggleVisibility: (itemId: number) => void; // Prop for visibility toggle +} + +/** + * ItemList Component + * + * Displays a list of items that can be selected and toggled for visibility. + * + * @param items - Array of items to display. + * @param onSelectItem - Callback when an item is selected. + * @param selectedItem - The currently selected item. + * @param onToggleVisibility - Callback when an item's visibility is toggled. + */ +const ItemList: React.FC = ({ + items, + onSelectItem, + selectedItem, + onToggleVisibility, +}) => { + return ( +
    + {items.map(item => ( +
  • + + +
  • + ))} +
+ ); +}; + +export default ItemList; diff --git a/platform/ui-next/src/components/PanelSplit/ItemListWithProperties.tsx b/platform/ui-next/src/components/PanelSplit/ItemListWithProperties.tsx new file mode 100644 index 00000000000..8ac4fdcae2d --- /dev/null +++ b/platform/ui-next/src/components/PanelSplit/ItemListWithProperties.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import ItemList from './ItemList'; +import PropertiesPanel from './PropertiesPanel'; +import { Item } from './types'; + +interface ItemListWithPropertiesProps { + items: Item[]; + onSelectItem: (item: Item) => void; + selectedItem: Item | null; +} + +const ItemListWithProperties: React.FC = ({ + items, + onSelectItem, + selectedItem, +}) => { + return ( +
+ + +
+ ); +}; + +export default ItemListWithProperties; diff --git a/platform/ui-next/src/components/PanelSplit/PanelSplit-backup.tsx b/platform/ui-next/src/components/PanelSplit/PanelSplit-backup.tsx new file mode 100644 index 00000000000..dd826ab2d09 --- /dev/null +++ b/platform/ui-next/src/components/PanelSplit/PanelSplit-backup.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { Slider } from '../Slider'; + +const PanelSplit: React.FC = () => { + return ( +
+

This is the PanelSplit component.

+ +
+ ); +}; + +export default PanelSplit; diff --git a/platform/ui-next/src/components/PanelSplit/PanelSplit.tsx b/platform/ui-next/src/components/PanelSplit/PanelSplit.tsx new file mode 100644 index 00000000000..922db065b88 --- /dev/null +++ b/platform/ui-next/src/components/PanelSplit/PanelSplit.tsx @@ -0,0 +1,279 @@ +import React, { useState, useEffect } from 'react'; +import ItemList from './ItemList'; +import PropertiesPanel from './PropertiesPanel'; +import { Item, DisplayMode, VisibilityState } from './types'; +import { ScrollArea } from '../ScrollArea'; // Importing ScrollArea + +const PanelSplit: React.FC = () => { + const [selectedItem, setSelectedItem] = useState(null); + const [items, setItems] = useState([ + { + id: 1, + name: 'All Items', + controlsAll: true, // Master item + displayMode: 'Fill & Outline', // Default display mode + visibility: 'Visible', // Default visibility + properties: [ + { + key: 'opacity', + label: 'Opacity', + type: 'slider', + value: 50, + min: 1, + max: 100, + step: 1, + }, + { + key: 'outline', + label: 'Outline', + type: 'slider', + value: 5, + min: 1, + max: 10, + step: 1, + }, + { + key: 'displayInactiveSegments', + label: 'Display inactive segments', + type: 'boolean', + value: false, + }, + ], + }, + { + id: 2, + name: 'List item 1', + series: 'Series A', + displayMode: 'Fill & Outline', // Default display mode + visibility: 'Visible', // Default visibility + properties: [ + { + key: 'opacity', + label: 'Opacity', + type: 'slider', + value: 70, + min: 1, + max: 100, + step: 1, + }, + { + key: 'outline', + label: 'Outline', + type: 'slider', + value: 7, + min: 1, + max: 10, + step: 1, + }, + ], + }, + { + id: 3, + name: 'List item 2', + series: 'Series B', + displayMode: 'Fill & Outline', // Default display mode + visibility: 'Visible', // Default visibility + properties: [ + { + key: 'opacity', + label: 'Opacity', + type: 'slider', + value: 70, + min: 1, + max: 100, + step: 1, + }, + { + key: 'outline', + label: 'Outline', + type: 'slider', + value: 7, + min: 1, + max: 10, + step: 1, + }, + ], + }, + // Add more items as needed + ]); + + // Set the master item as selected by default on mount + useEffect(() => { + if (items.length > 0 && !selectedItem) { + setSelectedItem(items.find(item => item.controlsAll) || items[0]); + } + }, [items, selectedItem]); + + /** + * Handles updating a property's value. + * + * @param itemId - The ID of the item being updated. + * @param propertyKey - The key identifying the property. + * @param newValue - The new value for the property. + */ + const handleUpdateProperty = (itemId: number, propertyKey: string, newValue: any) => { + const masterItem = items.find(item => item.controlsAll); + + if (masterItem && itemId === masterItem.id) { + if (propertyKey === 'displayMode') { + // Update displayMode for all items + setItems(prevItems => + prevItems.map(item => ({ + ...item, + displayMode: newValue, // Set to the new displayMode + properties: item.properties.map(prop => + prop.key === propertyKey ? { ...prop, value: newValue } : prop + ), + })) + ); + + // Also update the selectedItem if it's the master + setSelectedItem(prevSelected => + prevSelected + ? { + ...prevSelected, + displayMode: newValue, + properties: prevSelected.properties.map(prop => + prop.key === propertyKey ? { ...prop, value: newValue } : prop + ), + } + : prevSelected + ); + } else if (propertyKey === 'visibility') { + // Update visibility for all items + setItems(prevItems => + prevItems.map(item => ({ + ...item, + visibility: newValue, // Set to the new visibility + })) + ); + + // Also update the selectedItem if it's the master + setSelectedItem(prevSelected => + prevSelected + ? { + ...prevSelected, + visibility: newValue, + } + : prevSelected + ); + } else { + // Update other properties for all items + setItems(prevItems => + prevItems.map(item => ({ + ...item, + properties: item.properties.map(prop => + prop.key === propertyKey ? { ...prop, value: newValue } : prop + ), + })) + ); + + // Also update the selectedItem if it's the master + setSelectedItem(prevSelected => + prevSelected + ? { + ...prevSelected, + properties: prevSelected.properties.map(prop => + prop.key === propertyKey ? { ...prop, value: newValue } : prop + ), + } + : prevSelected + ); + } + } else { + if (propertyKey === 'displayMode') { + // Update displayMode only for the selected item + setItems(prevItems => + prevItems.map(item => (item.id === itemId ? { ...item, displayMode: newValue } : item)) + ); + + // Update selectedItem + setSelectedItem(prevSelected => + prevSelected ? { ...prevSelected, displayMode: newValue } : prevSelected + ); + } else if (propertyKey === 'visibility') { + // Toggle visibility only for the selected item + setItems(prevItems => + prevItems.map(item => (item.id === itemId ? { ...item, visibility: newValue } : item)) + ); + + // Update selectedItem + setSelectedItem(prevSelected => + prevSelected ? { ...prevSelected, visibility: newValue } : prevSelected + ); + } else { + // Update only the selected item's properties + setItems(prevItems => + prevItems.map(item => + item.id === itemId + ? { + ...item, + properties: item.properties.map(prop => + prop.key === propertyKey ? { ...prop, value: newValue } : prop + ), + } + : item + ) + ); + + // Update selectedItem + setSelectedItem(prevSelected => + prevSelected + ? { + ...prevSelected, + properties: prevSelected.properties.map(prop => + prop.key === propertyKey ? { ...prop, value: newValue } : prop + ), + } + : prevSelected + ); + } + } + }; + + /** + * Handles selecting an item from the list. + * + * @param item - The item being selected. + */ + const handleSelectItem = (item: Item) => { + setSelectedItem(item); + }; + + /** + * Handles toggling the visibility of an item. + * + * @param itemId - The ID of the item being toggled. + */ + const handleToggleVisibility = (itemId: number) => { + const item = items.find(item => item.id === itemId); + if (item) { + const newVisibility: VisibilityState = item.visibility === 'Visible' ? 'Hidden' : 'Visible'; + handleUpdateProperty(itemId, 'visibility', newVisibility); + } + }; + + return ( +
+ {/* Top: List of Selectable Items */} + + + + + {/* Bottom: Properties of Selected Item */} + + + +
+ ); +}; + +export default PanelSplit; diff --git a/platform/ui-next/src/components/PanelSplit/PropertiesPanel.tsx b/platform/ui-next/src/components/PanelSplit/PropertiesPanel.tsx new file mode 100644 index 00000000000..09af3d3b429 --- /dev/null +++ b/platform/ui-next/src/components/PanelSplit/PropertiesPanel.tsx @@ -0,0 +1,303 @@ +// src/components/PanelSplit/PropertiesPanel.tsx + +import React from 'react'; +import { Item, Property, DisplayMode } from './types'; +import { Label } from '../Label'; +import { Slider } from '../Slider'; +import { Input } from '../Input'; +import { Switch } from '../Switch'; +import { Tabs, TabsList, TabsTrigger, TabsContent } from '../Tabs'; + +interface PropertiesPanelProps { + selectedItem: Item | null; + onUpdateProperty: (itemId: number, propertyKey: string, newValue: any) => void; +} + +/** + * PropertiesPanel Component + * + * Displays and manages the properties of the selected item. + * Renders different input components based on the property's type. + * + * @param selectedItem - The currently selected item. + * @param onUpdateProperty - Callback to handle property updates. + */ +const PropertiesPanel: React.FC = ({ selectedItem, onUpdateProperty }) => { + if (!selectedItem) { + return ( +
+

No item selected.

+
+ ); + } + + /** + * Handles changes to a property's value. + * + * @param property - The property being updated. + * @param newValue - The new value for the property. + */ + const handleChange = (property: Property, newValue: any) => { + console.log(`Updating property '${property.key}' to`, newValue); // Debug log + onUpdateProperty(selectedItem.id, property.key, newValue); + }; + + // Determine if the selected item is the master + const isMaster = selectedItem.controlsAll; + + /** + * Handles changes to the display mode via Tabs. + * + * @param newDisplayMode - The new display mode selected. + */ + const handleDisplayModeChange = (newDisplayMode: DisplayMode) => { + console.log(`Display mode changed to`, newDisplayMode); // Debug log + onUpdateProperty(selectedItem.id, 'displayMode', newDisplayMode); + }; + + return ( +
+
+
+ Properties
+ {selectedItem.name} +
+ + {/* Tabs component for Outline and Fill control */} + + + + {/* SVG Icon for Fill & Outline */} + + + + + + + + + + + + + {/* SVG Icon for Outline Only */} + + + + + + + + + + + + {/* SVG Icon for Fill Only */} + + + + + + + + + + + + {/* Display dynamic text under the tabs */} +
+ +

Fill & Outline

+
+ +

Outline Only

+
+ +

Fill Only

+
+
+
+
+ + {/* Properties List */} +
+ {selectedItem.properties.map(prop => ( +
+ {/* Label takes up space and doesn't wrap */} + + + {/* Flex container for input elements, with spacing */} +
+ {renderPropertyInput(prop, handleChange)} +
+
+ ))} +
+ + {/* Conditionally render the details section for non-master items */} + {!isMaster && ( +
+
+ Series: {selectedItem.series} +
+ )} +
+ ); +}; + +/** + * Renders the appropriate input component based on the property's type. + * + * @param prop - The property to render. + * @param handleChange - Function to handle value changes. + * @returns JSX Element corresponding to the property type. + */ +const renderPropertyInput = ( + prop: Property, + handleChange: (prop: Property, value: any) => void +) => { + switch (prop.type) { + case 'slider': + return ( + <> + { + console.log(`Slider '${prop.key}' changed to`, values[0]); // Debug log + handleChange(prop, values[0]); + }} + className="w-28" + /> + { + const newVal = Number(e.target.value); + console.log(`Input '${prop.key}' changed to`, newVal); // Debug log + handleChange(prop, newVal); + }} + className="w-14" + /> + + ); + + case 'boolean': + return ( + { + console.log(`Switch '${prop.key}' toggled to`, checked); // Debug log + handleChange(prop, checked); + }} + /> + ); + + // Add more cases if you have other property types + default: + return null; + } +}; + +export default PropertiesPanel; diff --git a/platform/ui-next/src/components/PanelSplit/index.ts b/platform/ui-next/src/components/PanelSplit/index.ts new file mode 100644 index 00000000000..7d211b0a05e --- /dev/null +++ b/platform/ui-next/src/components/PanelSplit/index.ts @@ -0,0 +1,7 @@ +import PanelSplit from './PanelSplit'; +import ItemList from './ItemList'; +import PropertiesPanel from './PropertiesPanel'; + +export { default as PanelSplit } from './PanelSplit'; +export { default as PropertiesPanel } from './PropertiesPanel'; +export { default as ItemList } from './ItemList'; diff --git a/platform/ui-next/src/components/PanelSplit/types.ts b/platform/ui-next/src/components/PanelSplit/types.ts new file mode 100644 index 00000000000..4bacb66336e --- /dev/null +++ b/platform/ui-next/src/components/PanelSplit/types.ts @@ -0,0 +1,22 @@ +export interface Property { + key: string; + label: string; + type: 'slider' | 'boolean' | string; // Extend types as needed + value: any; + min?: number; + max?: number; + step?: number; +} + +export type DisplayMode = 'Fill & Outline' | 'Outline Only' | 'Fill Only'; +export type VisibilityState = 'Visible' | 'Hidden'; + +export interface Item { + id: number; + name: string; + controlsAll?: boolean; // Indicates if the item is the master + series?: string; // Optional, only for non-master items + displayMode: DisplayMode; // Existing property for display mode + visibility: VisibilityState; // New property for visibility + properties: Property[]; +} diff --git a/platform/ui-next/src/components/index.ts b/platform/ui-next/src/components/index.ts index c8951c2d705..abf75b21f05 100644 --- a/platform/ui-next/src/components/index.ts +++ b/platform/ui-next/src/components/index.ts @@ -28,6 +28,9 @@ import ThumbnailList from './ThumbnailList'; import PanelSection from './PanelSection'; import DisplaySetMessageListTooltip from './DisplaySetMessageListTooltip'; import { Toolbox, ToolboxUI } from './Toolbox'; +import PanelSplit from './PanelSplit'; +import ItemList from './PanelSplit/ItemList'; +import PropertiesPanel from './PanelSplit/PropertiesPanel'; export { Button, @@ -71,4 +74,7 @@ export { DisplaySetMessageListTooltip, Toolbox, ToolboxUI, + PanelSplit, + ItemList, + PropertiesPanel, }; diff --git a/platform/ui-next/src/tailwind.css b/platform/ui-next/src/tailwind.css index dc715aa6521..dd417ce9398 100644 --- a/platform/ui-next/src/tailwind.css +++ b/platform/ui-next/src/tailwind.css @@ -197,7 +197,7 @@ } body { - @apply bg-black; + @apply !bg-black; } } @@ -228,3 +228,13 @@ h3.section-header { .TooltipContent[data-side='bottom'] { animation-name: slideDown; } + +/* Custom CSS to hide default number input arrows */ +input[type='number']::-webkit-inner-spin-button, +input[type='number']::-webkit-outer-spin-button { + @apply appearance-none; +} + +input[type='number'] { + -moz-appearance: textfield; /* For Firefox */ +} diff --git a/platform/ui-next/tailwind.config.js b/platform/ui-next/tailwind.config.js index e89be1e467c..75765742feb 100644 --- a/platform/ui-next/tailwind.config.js +++ b/platform/ui-next/tailwind.config.js @@ -13,6 +13,7 @@ module.exports = { inter: ['Inter', 'sans-serif'], }, fontSize: { + xxs: '0.6875rem', // 11px xs: '0.75rem', // 12px sm: '0.8125rem', // 13px base: '0.875rem', // 14px