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: {
|
entry: {
|
||||||
home: './src/_pages/index.tsx',
|
home: './src/_pages/index.tsx',
|
||||||
playground: './src/_pages/playground.tsx',
|
playground: './src/_pages/playground.tsx',
|
||||||
|
patterns: './src/_pages/patterns.tsx',
|
||||||
viewer: './src/_pages/viewer.tsx',
|
viewer: './src/_pages/viewer.tsx',
|
||||||
colors: './src/_pages/colors.tsx',
|
colors: './src/_pages/colors.tsx',
|
||||||
// add other pages here
|
// add other pages here
|
||||||
@ -82,6 +83,11 @@ module.exports = {
|
|||||||
chunks: ['playground'],
|
chunks: ['playground'],
|
||||||
filename: 'playground.html',
|
filename: 'playground.html',
|
||||||
}),
|
}),
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
template: './.webpack/template.html',
|
||||||
|
chunks: ['patterns'],
|
||||||
|
filename: 'patterns.html',
|
||||||
|
}),
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
template: './.webpack/template.html',
|
template: './.webpack/template.html',
|
||||||
chunks: ['viewer'],
|
chunks: ['viewer'],
|
||||||
|
|||||||
@ -13,6 +13,12 @@ const App: React.FC = () => (
|
|||||||
>
|
>
|
||||||
Playground
|
Playground
|
||||||
</a>
|
</a>
|
||||||
|
<a
|
||||||
|
href="patterns.html"
|
||||||
|
className="text-blue-500 hover:text-blue-700"
|
||||||
|
>
|
||||||
|
Patterns
|
||||||
|
</a>
|
||||||
<a
|
<a
|
||||||
href="viewer.html"
|
href="viewer.html"
|
||||||
className="text-blue-500 hover:text-blue-700"
|
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,
|
SelectScrollDownButton,
|
||||||
} from '../components/Select';
|
} from '../components/Select';
|
||||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '../components/Tabs';
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from '../components/Tabs';
|
||||||
import { Separator } from '../components/Separator';
|
import Separator from '../components/Separator';
|
||||||
import { Switch } from '../components/Switch';
|
import { Switch } from '../components/Switch';
|
||||||
import { Checkbox } from '../components/Checkbox';
|
import { Checkbox } from '../components/Checkbox';
|
||||||
import { Toggle, toggleVariants } from '../components/Toggle';
|
import { Toggle, toggleVariants } from '../components/Toggle';
|
||||||
@ -26,6 +26,7 @@ import { Slider } from '../components/Slider';
|
|||||||
import { ScrollArea, ScrollBar } from '../components/ScrollArea';
|
import { ScrollArea, ScrollBar } from '../components/ScrollArea';
|
||||||
|
|
||||||
import { BackgroundColorSelect } from '../components/BackgroundColorSelect';
|
import { BackgroundColorSelect } from '../components/BackgroundColorSelect';
|
||||||
|
|
||||||
// import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '../components/Tooltip';
|
// import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '../components/Tooltip';
|
||||||
|
|
||||||
// import type { Metadata } from 'next';
|
// 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 DisplaySetMessageListTooltip from './DisplaySetMessageListTooltip';
|
||||||
import { Toolbox, ToolboxUI } from './Toolbox';
|
import { Toolbox, ToolboxUI } from './Toolbox';
|
||||||
|
|
||||||
|
// Prototypes
|
||||||
|
import { PanelSplit } from '../_prototypes/PanelSplit';
|
||||||
|
import { ItemList } from '../_prototypes/PanelSplit/ItemList';
|
||||||
|
import { PropertiesPanel } from '../_prototypes/PanelSplit/PropertiesPanel';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Button,
|
Button,
|
||||||
buttonVariants,
|
buttonVariants,
|
||||||
@ -71,4 +76,7 @@ export {
|
|||||||
DisplaySetMessageListTooltip,
|
DisplaySetMessageListTooltip,
|
||||||
Toolbox,
|
Toolbox,
|
||||||
ToolboxUI,
|
ToolboxUI,
|
||||||
|
PanelSplit,
|
||||||
|
ItemList,
|
||||||
|
PropertiesPanel,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -197,7 +197,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-black;
|
@apply !bg-black;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -228,3 +228,13 @@ h3.section-header {
|
|||||||
.TooltipContent[data-side='bottom'] {
|
.TooltipContent[data-side='bottom'] {
|
||||||
animation-name: slideDown;
|
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'],
|
inter: ['Inter', 'sans-serif'],
|
||||||
},
|
},
|
||||||
fontSize: {
|
fontSize: {
|
||||||
|
xxs: '0.6875rem', // 11px
|
||||||
xs: '0.75rem', // 12px
|
xs: '0.75rem', // 12px
|
||||||
sm: '0.8125rem', // 13px
|
sm: '0.8125rem', // 13px
|
||||||
base: '0.875rem', // 14px
|
base: '0.875rem', // 14px
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user