ui(segmentation): Design Prototype for split panel component (#4378)
This commit is contained in:
parent
cb1f3b877b
commit
7f17d5d733
@ -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'],
|
||||
|
||||
@ -13,6 +13,12 @@ const App: React.FC = () => (
|
||||
>
|
||||
Playground
|
||||
</a>
|
||||
<a
|
||||
href="patterns.html"
|
||||
className="text-blue-500 hover:text-blue-700"
|
||||
>
|
||||
Patterns
|
||||
</a>
|
||||
<a
|
||||
href="viewer.html"
|
||||
className="text-blue-500 hover:text-blue-700"
|
||||
|
||||
43
platform/ui-next/src/_pages/patterns.tsx
Normal file
43
platform/ui-next/src/_pages/patterns.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
// src/_pages/patterns.tsx
|
||||
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import '../tailwind.css';
|
||||
|
||||
// component imports
|
||||
import { Button } from '../components/Button';
|
||||
import { Input } from '../components/Input';
|
||||
import { Label } from '../components/Label';
|
||||
import {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
} from '../components/Select';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '../components/Tabs';
|
||||
import Separator from '../components/Separator';
|
||||
import { Switch } from '../components/Switch';
|
||||
import { Checkbox } from '../components/Checkbox';
|
||||
import { Toggle, toggleVariants } from '../components/Toggle';
|
||||
import { Slider } from '../components/Slider';
|
||||
import { ScrollArea, ScrollBar } from '../components/ScrollArea';
|
||||
import { PanelSplit } from '../_prototypes/PanelSplit'; // Updated import path
|
||||
|
||||
function Patterns() {
|
||||
return (
|
||||
<div className="my-4 mx-auto max-w-6xl py-6">
|
||||
<div className="text-3xl"> Patterns 1 </div>
|
||||
<PanelSplit />;
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const container = document.getElementById('root');
|
||||
const root = createRoot(container);
|
||||
root.render(React.createElement(Patterns));
|
||||
@ -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';
|
||||
|
||||
213
platform/ui-next/src/_prototypes/PanelSplit/ItemList.tsx
Normal file
213
platform/ui-next/src/_prototypes/PanelSplit/ItemList.tsx
Normal file
@ -0,0 +1,213 @@
|
||||
import React from 'react';
|
||||
import { Item, VisibilityState, AvailabilityState } from './types';
|
||||
import { Button } from '../../components/Button'; // Updated import path
|
||||
|
||||
interface ItemListProps {
|
||||
items: Item[];
|
||||
onSelectItem: (item: Item) => void;
|
||||
selectedItem: Item | null;
|
||||
onToggleVisibility: (itemId: number) => void; // Prop for visibility toggle
|
||||
onAddItem: (itemId: number) => void; // Prop for add button
|
||||
}
|
||||
|
||||
/**
|
||||
* ItemList Component
|
||||
*
|
||||
* Displays a list of items that can be selected, toggled for visibility,
|
||||
* and added (changing availability).
|
||||
*
|
||||
* @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.
|
||||
* @param onAddItem - Callback when an item's availability is changed to 'loaded'.
|
||||
*/
|
||||
const ItemList: React.FC<ItemListProps> = ({
|
||||
items,
|
||||
onSelectItem,
|
||||
selectedItem,
|
||||
onToggleVisibility,
|
||||
onAddItem,
|
||||
}) => {
|
||||
return (
|
||||
<ul
|
||||
aria-label="Item List"
|
||||
className="space-y-1"
|
||||
>
|
||||
{items.map(item => (
|
||||
<li key={item.id}>
|
||||
<div className="flex items-center">
|
||||
{/* Conditionally render the Add button for 'available' items */}
|
||||
{item.availability === 'available' && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={e => {
|
||||
e.stopPropagation(); // Prevent parent onClick
|
||||
onAddItem(item.id);
|
||||
}}
|
||||
aria-label={`Add ${item.name}`}
|
||||
className="ml-1 mr-1"
|
||||
>
|
||||
{/* Plus Icon */}
|
||||
<svg
|
||||
width="16px"
|
||||
height="16px"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 5V19"
|
||||
stroke="#348CFD"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M5 12H19"
|
||||
stroke="#348CFD"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* All items are now rendered as clickable rows */}
|
||||
<button
|
||||
onClick={() => onSelectItem(item)}
|
||||
className={`text-foreground flex h-7 w-full flex-grow cursor-pointer items-center justify-between rounded p-1 text-sm ${
|
||||
item.id === selectedItem?.id ? 'bg-primary/20' : 'bg-muted hover:bg-primary/30'
|
||||
} focus-visible:ring-ring focus-visible:outline-none focus-visible:ring-1`}
|
||||
aria-pressed={item.id === selectedItem?.id}
|
||||
>
|
||||
<span className="ml-1">{item.name}</span>
|
||||
|
||||
{/* Conditionally render the "Available" label for 'available' items */}
|
||||
{item.availability === 'available' && (
|
||||
<span className="text-muted-foreground text-xs">Available</span>
|
||||
)}
|
||||
|
||||
{/* Conditionally render the visibility icon for 'loaded' items */}
|
||||
{item.availability === 'loaded' && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={e => {
|
||||
e.stopPropagation(); // Prevent parent onClick
|
||||
onToggleVisibility(item.id);
|
||||
}}
|
||||
aria-label={
|
||||
item.visibility === 'Visible' ? `Hide ${item.name}` : `Show ${item.name}`
|
||||
}
|
||||
>
|
||||
{item.visibility === 'Visible' ? (
|
||||
// SVG Icon for "Visible"
|
||||
<svg
|
||||
width="24px"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g
|
||||
stroke="none"
|
||||
strokeWidth="1"
|
||||
fill="none"
|
||||
fillRule="evenodd"
|
||||
>
|
||||
<rect
|
||||
x="0"
|
||||
y="0"
|
||||
width="24"
|
||||
height="24"
|
||||
></rect>
|
||||
<circle
|
||||
stroke="#348CFD"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
cx="12.4986195"
|
||||
cy="11.8041442"
|
||||
r="2.58684689"
|
||||
></circle>
|
||||
<path
|
||||
d="M20.906611,11.5617197 C20.0470387,10.5861089 16.6094888,7 12.4986195,7
|
||||
C8.38775024,7 4.95020027,10.5861089 4.090628,11.5617197
|
||||
C3.96979067,11.7007491 3.96979067,11.9075393 4.090628,12.0465687
|
||||
C4.95020027,13.0221796 8.38775024,16.6082885 12.4986195,16.6082885
|
||||
C16.6094888,16.6082885 20.0470387,13.0221796 20.906611,12.0465687
|
||||
C21.0274483,11.9075393 21.0274483,11.7007491 20.906611,11.5617197 Z"
|
||||
stroke="#348CFD"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
) : (
|
||||
// SVG Icon for "Hidden"
|
||||
<svg
|
||||
width="24px"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g
|
||||
stroke="none"
|
||||
strokeWidth="1"
|
||||
fill="none"
|
||||
fillRule="evenodd"
|
||||
>
|
||||
<path
|
||||
d="M18.0567826,8.96286957
|
||||
C19.1471229,9.75269568 20.1356859,10.674229 21,11.7065217
|
||||
C21,11.7065217 17.1949565,16.5108696 12.5,16.5108696
|
||||
C11.7479876,16.5066962 11.0007435,16.3911225 10.2826087,16.167913"
|
||||
stroke="#348CFD"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="M6.93286957,14.4413043
|
||||
C5.84666081,13.6535964 4.86162018,12.7350857 4,11.7065217
|
||||
C4,11.7065217 7.80504348,6.90217391 12.5,6.90217391
|
||||
C13.1235541,6.90480509 13.7443251,6.98550531 14.3478261,7.1423913"
|
||||
stroke="#348CFD"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="M9.54347826,11.7065217
|
||||
C9.54347826,10.0736799 10.8671581,8.75 12.5,8.75"
|
||||
stroke="#348CFD"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="M15.4565217,11.7065217
|
||||
C15.4565217,13.3393636 14.1328419,14.6630435 12.5,14.6630435"
|
||||
stroke="#348CFD"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
<line
|
||||
x1="19.7065217"
|
||||
y1="4.5"
|
||||
x2="5.29347826"
|
||||
y2="18.9130435"
|
||||
stroke="#348CFD"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></line>
|
||||
</g>
|
||||
</svg>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
export default ItemList;
|
||||
331
platform/ui-next/src/_prototypes/PanelSplit/PanelSplit.tsx
Normal file
331
platform/ui-next/src/_prototypes/PanelSplit/PanelSplit.tsx
Normal file
@ -0,0 +1,331 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import ItemList from './ItemList';
|
||||
import PropertiesPanel from './PropertiesPanel';
|
||||
import { Item, DisplayMode, VisibilityState, AvailabilityState } from './types';
|
||||
import { ScrollArea } from '../../components/ScrollArea';
|
||||
|
||||
const PanelSplit: React.FC = () => {
|
||||
const [selectedItem, setSelectedItem] = useState<Item | null>(null);
|
||||
const [items, setItems] = useState<Item[]>([
|
||||
{
|
||||
id: 1,
|
||||
name: 'All Items',
|
||||
controlsAll: true, // Master item
|
||||
displayMode: 'Fill & Outline', // Default display mode
|
||||
visibility: 'Visible', // Default visibility
|
||||
availability: 'loaded', // Set to 'loaded'
|
||||
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
|
||||
availability: 'loaded', // Set to 'loaded'
|
||||
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
|
||||
availability: 'loaded', // Set to 'loaded'
|
||||
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: 4,
|
||||
name: 'List item 3',
|
||||
series: 'Series C',
|
||||
displayMode: 'Fill & Outline', // Default display mode
|
||||
visibility: 'Visible', // Visibility will not be shown initially
|
||||
availability: 'available', // Set to 'available'
|
||||
properties: [
|
||||
{
|
||||
key: 'opacity',
|
||||
label: 'Opacity',
|
||||
type: 'slider',
|
||||
value: 80,
|
||||
min: 1,
|
||||
max: 100,
|
||||
step: 1,
|
||||
},
|
||||
{
|
||||
key: 'outline',
|
||||
label: 'Outline',
|
||||
type: 'slider',
|
||||
value: 8,
|
||||
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 if (propertyKey === 'availability') {
|
||||
// Update availability only for the selected item
|
||||
setItems(prevItems =>
|
||||
prevItems.map(item => (item.id === itemId ? { ...item, availability: newValue } : item))
|
||||
);
|
||||
|
||||
// Update selectedItem
|
||||
setSelectedItem(prevSelected =>
|
||||
prevSelected ? { ...prevSelected, availability: 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);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles adding an item (changing availability from 'available' to 'loaded').
|
||||
*
|
||||
* @param itemId - The ID of the item being added.
|
||||
*/
|
||||
const handleAddItem = (itemId: number) => {
|
||||
handleUpdateProperty(itemId, 'availability', 'loaded');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-[262px] flex-col">
|
||||
{/* Top: List of Selectable Items */}
|
||||
<ScrollArea className="bg-muted h-[132px] rounded-t border-gray-300 p-1">
|
||||
<ItemList
|
||||
items={items}
|
||||
onSelectItem={handleSelectItem}
|
||||
selectedItem={selectedItem}
|
||||
onToggleVisibility={handleToggleVisibility} // Pass the toggle handler
|
||||
onAddItem={handleAddItem} // Pass the add handler
|
||||
/>
|
||||
</ScrollArea>
|
||||
|
||||
{/* Bottom: Properties of Selected Item */}
|
||||
<ScrollArea className="bg-popover max-h-[400px] flex-grow overflow-auto rounded p-1">
|
||||
<PropertiesPanel
|
||||
selectedItem={selectedItem}
|
||||
onUpdateProperty={handleUpdateProperty}
|
||||
onAddItem={handleAddItem} // Pass the add handler
|
||||
/>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PanelSplit;
|
||||
337
platform/ui-next/src/_prototypes/PanelSplit/PropertiesPanel.tsx
Normal file
337
platform/ui-next/src/_prototypes/PanelSplit/PropertiesPanel.tsx
Normal file
@ -0,0 +1,337 @@
|
||||
import React from 'react';
|
||||
import { Item, Property, DisplayMode, AvailabilityState } from './types';
|
||||
import { Label } from '../../components/Label';
|
||||
import { Slider } from '../../components/Slider';
|
||||
import { Input } from '../../components/Input';
|
||||
import { Switch } from '../../components/Switch';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '../../components/Tabs';
|
||||
import { Button } from '../../components/Button';
|
||||
|
||||
interface PropertiesPanelProps {
|
||||
selectedItem: Item | null;
|
||||
onUpdateProperty: (itemId: number, propertyKey: string, newValue: any) => void;
|
||||
onAddItem: (itemId: number) => void; // Prop for adding item
|
||||
}
|
||||
|
||||
/**
|
||||
* PropertiesPanel Component
|
||||
*
|
||||
* Displays and manages the properties of the selected item.
|
||||
* Renders different content based on the item's availability state.
|
||||
*
|
||||
* @param selectedItem - The currently selected item.
|
||||
* @param onUpdateProperty - Callback to handle property updates.
|
||||
* @param onAddItem - Callback to handle adding the item.
|
||||
*/
|
||||
const PropertiesPanel: React.FC<PropertiesPanelProps> = ({
|
||||
selectedItem,
|
||||
onUpdateProperty,
|
||||
onAddItem,
|
||||
}) => {
|
||||
if (!selectedItem) {
|
||||
return (
|
||||
<div className="text-gray-500">
|
||||
<p>No item selected.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedItem.availability === 'available') {
|
||||
return (
|
||||
<div className="flex flex-col items-center p-1.5 text-sm">
|
||||
{/* "Add this item" Button */}
|
||||
<Button
|
||||
onClick={() => onAddItem(selectedItem.id)}
|
||||
className="mb-4 mt-2"
|
||||
variant="default"
|
||||
>
|
||||
Add this item
|
||||
</Button>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="border-primary/30 mb-2 w-full border-b"></div>
|
||||
|
||||
{/* Series Name */}
|
||||
<div className="text-foreground w-full text-left">
|
||||
Series: <span className="text-muted-foreground">{selectedItem.series}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedItem.availability === 'loaded') {
|
||||
/**
|
||||
* 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 (
|
||||
<div className="p-1.5 text-sm">
|
||||
<div className="items-top mb-2.5 flex justify-between">
|
||||
<div className="text-foreground text-sm font-semibold">
|
||||
Properties <br />
|
||||
<span className="text-muted-foreground font-normal">{selectedItem.name}</span>
|
||||
</div>
|
||||
|
||||
{/* Tabs component for Outline and Fill control */}
|
||||
<Tabs
|
||||
value={selectedItem.displayMode}
|
||||
onValueChange={handleDisplayModeChange}
|
||||
className="ml-auto"
|
||||
>
|
||||
<TabsList>
|
||||
<TabsTrigger value="Fill & Outline">
|
||||
{/* SVG Icon for Fill & Outline */}
|
||||
<svg
|
||||
width="18px"
|
||||
height="18px"
|
||||
viewBox="0 0 18 18"
|
||||
>
|
||||
<g
|
||||
id="view-outline-fill"
|
||||
stroke="none"
|
||||
strokeWidth="1"
|
||||
fill="none"
|
||||
fillRule="evenodd"
|
||||
>
|
||||
<g id="Group-13">
|
||||
<rect
|
||||
id="Rectangle"
|
||||
x="0"
|
||||
y="0"
|
||||
width="18"
|
||||
height="18"
|
||||
></rect>
|
||||
<rect
|
||||
id="Rectangle"
|
||||
stroke="#348CFD"
|
||||
x="1.5"
|
||||
y="1.5"
|
||||
width="15"
|
||||
height="15"
|
||||
rx="1"
|
||||
></rect>
|
||||
<rect
|
||||
id="Rectangle"
|
||||
fill="#348CFD"
|
||||
x="3.5"
|
||||
y="3.5"
|
||||
width="11"
|
||||
height="11"
|
||||
rx="1"
|
||||
></rect>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="Outline Only">
|
||||
{/* SVG Icon for Outline Only */}
|
||||
<svg
|
||||
width="18px"
|
||||
height="18px"
|
||||
viewBox="0 0 18 18"
|
||||
>
|
||||
<g
|
||||
id="view-outline"
|
||||
stroke="none"
|
||||
strokeWidth="1"
|
||||
fill="none"
|
||||
fillRule="evenodd"
|
||||
>
|
||||
<g id="Group-13">
|
||||
<rect
|
||||
id="Rectangle"
|
||||
x="0"
|
||||
y="0"
|
||||
width="18"
|
||||
height="18"
|
||||
></rect>
|
||||
<rect
|
||||
id="Rectangle"
|
||||
stroke="#348CFD"
|
||||
x="1.5"
|
||||
y="1.5"
|
||||
width="15"
|
||||
height="15"
|
||||
rx="1"
|
||||
></rect>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="Fill Only">
|
||||
{/* SVG Icon for Fill Only */}
|
||||
<svg
|
||||
width="18px"
|
||||
height="18px"
|
||||
viewBox="0 0 18 18"
|
||||
>
|
||||
<g
|
||||
id="view-fill"
|
||||
stroke="none"
|
||||
strokeWidth="1"
|
||||
fill="none"
|
||||
fillRule="evenodd"
|
||||
>
|
||||
<g id="Group-13">
|
||||
<rect
|
||||
id="Rectangle"
|
||||
x="0"
|
||||
y="0"
|
||||
width="18"
|
||||
height="18"
|
||||
></rect>
|
||||
<rect
|
||||
id="Rectangle"
|
||||
fill="#348CFD"
|
||||
x="2"
|
||||
y="2"
|
||||
width="14"
|
||||
height="14"
|
||||
rx="1"
|
||||
></rect>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Display dynamic text under the tabs */}
|
||||
<div className="mt-0">
|
||||
<TabsContent value="Fill & Outline">
|
||||
<p className="text-muted-foreground text-xxs text-center">Fill & Outline</p>
|
||||
</TabsContent>
|
||||
<TabsContent value="Outline Only">
|
||||
<p className="text-muted-foreground text-xxs text-center">Outline Only</p>
|
||||
</TabsContent>
|
||||
<TabsContent value="Fill Only">
|
||||
<p className="text-muted-foreground text-xxs text-center">Fill Only</p>
|
||||
</TabsContent>
|
||||
</div>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
{/* Properties List */}
|
||||
<div className="mb-3 space-y-3">
|
||||
{selectedItem.properties.map(prop => (
|
||||
<div
|
||||
key={prop.key}
|
||||
className="flex items-center justify-between space-x-4"
|
||||
>
|
||||
{/* Label takes up space and doesn't wrap */}
|
||||
<Label
|
||||
htmlFor={prop.key}
|
||||
className="flex-grow whitespace-nowrap"
|
||||
>
|
||||
{prop.label}
|
||||
</Label>
|
||||
|
||||
{/* Flex container for input elements, with spacing */}
|
||||
<div className="flex items-center space-x-3">
|
||||
{renderPropertyInput(prop, handleChange)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Conditionally render the details section for non-master items */}
|
||||
{!isMaster && (
|
||||
<div className="text-foreground mb-1">
|
||||
<div className="border-primary/30 mb-2 w-full border-b"></div>
|
||||
Series: <span className="text-muted-foreground">{selectedItem.series}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// For other availability states, you can add additional conditions if needed
|
||||
return (
|
||||
<div className="text-gray-500">
|
||||
<p>No properties available for the selected item.</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 (
|
||||
<>
|
||||
<Slider
|
||||
id={prop.key}
|
||||
value={[prop.value as number]} // Pass as an array for Radix UI Slider
|
||||
min={prop.min}
|
||||
max={prop.max}
|
||||
step={prop.step}
|
||||
onValueChange={values => {
|
||||
console.log(`Slider '${prop.key}' changed to`, values[0]); // Debug log
|
||||
handleChange(prop, values[0]);
|
||||
}}
|
||||
className="w-28"
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
id={prop.key}
|
||||
value={prop.value as number}
|
||||
min={prop.min}
|
||||
max={prop.max}
|
||||
step={prop.step}
|
||||
onChange={e => {
|
||||
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 (
|
||||
<Switch
|
||||
id={prop.key}
|
||||
checked={prop.value as boolean}
|
||||
onCheckedChange={checked => {
|
||||
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;
|
||||
7
platform/ui-next/src/_prototypes/PanelSplit/index.ts
Normal file
7
platform/ui-next/src/_prototypes/PanelSplit/index.ts
Normal file
@ -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';
|
||||
24
platform/ui-next/src/_prototypes/PanelSplit/types.ts
Normal file
24
platform/ui-next/src/_prototypes/PanelSplit/types.ts
Normal file
@ -0,0 +1,24 @@
|
||||
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 type AvailabilityState = 'loaded' | 'available' | 'not available';
|
||||
|
||||
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; // Existing property for visibility
|
||||
availability: AvailabilityState; // New property for availability
|
||||
properties: Property[];
|
||||
}
|
||||
@ -29,6 +29,11 @@ import PanelSection from './PanelSection';
|
||||
import DisplaySetMessageListTooltip from './DisplaySetMessageListTooltip';
|
||||
import { Toolbox, ToolboxUI } from './Toolbox';
|
||||
|
||||
// Prototypes
|
||||
import { PanelSplit } from '../_prototypes/PanelSplit';
|
||||
import { ItemList } from '../_prototypes/PanelSplit/ItemList';
|
||||
import { PropertiesPanel } from '../_prototypes/PanelSplit/PropertiesPanel';
|
||||
|
||||
export {
|
||||
Button,
|
||||
buttonVariants,
|
||||
@ -71,4 +76,7 @@ export {
|
||||
DisplaySetMessageListTooltip,
|
||||
Toolbox,
|
||||
ToolboxUI,
|
||||
PanelSplit,
|
||||
ItemList,
|
||||
PropertiesPanel,
|
||||
};
|
||||
|
||||
@ -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 */
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user