Skip to content

Commit 4c577c0

Browse files
committed
feat(masonry):[palette]: update components in palette
Signed-off-by: Justin Charles <[email protected]>
1 parent 713009e commit 4c577c0

File tree

5 files changed

+452
-0
lines changed

5 files changed

+452
-0
lines changed
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import React, { useEffect, useRef, useState, Suspense } from 'react';
2+
import { defaultCategories as categories } from './categories';
3+
import bricksData from './config/brick-config.json';
4+
import type { BrickConfig } from './types';
5+
import { brickViews } from './registry';
6+
import './palette.css';
7+
8+
interface BrickListPanelProps {
9+
selectedCategory: string;
10+
onClose: () => void;
11+
filter?: string;
12+
}
13+
14+
const bricks: BrickConfig[] = (bricksData as any[]).map((b) => ({
15+
...b,
16+
argCount: b.argCount ?? 0,
17+
}));
18+
19+
const groupBricksByCategory = (bricks: BrickConfig[]) => {
20+
const grouped: Record<string, BrickConfig[]> = {};
21+
bricks.forEach((brick) => {
22+
if (!grouped[brick.category]) {
23+
grouped[brick.category] = [];
24+
}
25+
grouped[brick.category].push(brick);
26+
});
27+
return grouped;
28+
};
29+
30+
const BrickListPanel: React.FC<BrickListPanelProps> = ({
31+
selectedCategory,
32+
onClose,
33+
filter = '',
34+
}) => {
35+
// Handle close button click
36+
const handleClose = (e: React.MouseEvent) => {
37+
e.stopPropagation();
38+
onClose();
39+
};
40+
const containerRef = useRef<HTMLDivElement>(null);
41+
const [filteredBricks, setFilteredBricks] = useState<Record<string, BrickConfig[]>>({});
42+
43+
useEffect(() => {
44+
const filtered = filter
45+
? bricks.filter((brick) => brick.label.toLowerCase().includes(filter.toLowerCase()))
46+
: bricks;
47+
48+
setFilteredBricks(groupBricksByCategory(filtered));
49+
}, [filter]);
50+
51+
useEffect(() => {
52+
if (selectedCategory && containerRef.current) {
53+
const categoryElement = containerRef.current.querySelector(
54+
`[data-category="${selectedCategory}"]`,
55+
) as HTMLElement;
56+
57+
if (categoryElement) {
58+
categoryElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
59+
}
60+
}
61+
}, [selectedCategory]);
62+
63+
return (
64+
<div className="brick-panel">
65+
<div className="brick-panel-header">
66+
<h3>Blocks</h3>
67+
<button
68+
className="close-button"
69+
onClick={handleClose}
70+
aria-label="Close panel"
71+
type="button"
72+
>
73+
×
74+
</button>
75+
</div>
76+
<div className="brick-list" ref={containerRef}>
77+
{Object.entries(filteredBricks).map(([category, categoryBricks]) => (
78+
<div key={category} className="brick-category" data-category={category}>
79+
<h4 className="category-header">{category}</h4>
80+
<div className="brick-category-list">
81+
{categoryBricks.map((brick) => (
82+
<div key={brick.id} className="brick-item">
83+
<Suspense fallback={<div>Loading brick...</div>}>
84+
<AsyncBrickView brick={brick} />
85+
</Suspense>
86+
</div>
87+
))}
88+
</div>
89+
</div>
90+
))}
91+
</div>
92+
</div>
93+
);
94+
};
95+
96+
const AsyncBrickView = ({ brick }: { brick: BrickConfig }) => {
97+
const [BrickComponent, setBrickComponent] = React.useState<React.FC<BrickConfig> | null>(null);
98+
const [error, setError] = React.useState<Error | null>(null);
99+
100+
React.useEffect(() => {
101+
let isMounted = true;
102+
103+
const loadBrick = async () => {
104+
try {
105+
const Component = brickViews[brick.type];
106+
if (!Component) {
107+
throw new Error(`No view found for brick type: ${brick.type}`);
108+
}
109+
110+
if (isMounted) {
111+
setBrickComponent(() => Component);
112+
}
113+
} catch (err) {
114+
if (isMounted) setError(err as Error);
115+
}
116+
};
117+
118+
loadBrick();
119+
120+
return () => {
121+
isMounted = false;
122+
};
123+
}, [brick]);
124+
125+
if (error) {
126+
return (
127+
<div className="brick-default">
128+
<div className="brick-name">Error loading brick</div>
129+
<div className="brick-description">{error.message}</div>
130+
</div>
131+
);
132+
}
133+
134+
if (!BrickComponent) {
135+
return (
136+
<div className="brick-default">
137+
<div className="brick-name">Loading...</div>
138+
</div>
139+
);
140+
}
141+
142+
return <BrickComponent {...brick} />;
143+
};
144+
145+
export default BrickListPanel;
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// src/palette/CategoryGrid.tsx
2+
import React from 'react';
3+
import { getCategories, PaletteMode } from './categories';
4+
5+
interface Props {
6+
/** Called when a category is selected */
7+
onSelect: (id: string) => void;
8+
/** Currently selected category ID */
9+
selected?: string;
10+
/** Current palette mode */
11+
mode: PaletteMode;
12+
}
13+
14+
/**
15+
* CategoryGrid
16+
* Renders a vertical list of category buttons
17+
*/
18+
const CategoryGrid: React.FC<Props> = ({ onSelect, selected, mode }) => {
19+
const categories = getCategories(mode);
20+
21+
return (
22+
<div className="category-grid">
23+
{categories.map((category) => (
24+
<button
25+
key={category.id}
26+
onClick={() => onSelect(category.id)}
27+
className={`category-button ${selected === category.id ? 'selected' : ''}`}
28+
style={{
29+
backgroundColor: category.color,
30+
color: 'white',
31+
}}
32+
aria-label={category.label}
33+
>
34+
{category.label}
35+
</button>
36+
))}
37+
</div>
38+
);
39+
};
40+
41+
export default CategoryGrid;
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import React, { useState } from 'react';
2+
import Sidebar from './sidebar';
3+
import BrickListPanel from './brickListPanel';
4+
import './palette.css';
5+
6+
const PaletteWrapper: React.FC = () => {
7+
const [selectedCategory, setSelectedCategory] = useState<string | undefined>(undefined);
8+
const [query, setQuery] = useState('');
9+
const [showBrickPanel, setShowBrickPanel] = useState(false);
10+
const prevSelectedCategory = React.useRef<string | undefined>(undefined);
11+
12+
const handleCategorySelect = (categoryId: string) => {
13+
if (selectedCategory === categoryId) {
14+
setShowBrickPanel(!showBrickPanel);
15+
} else {
16+
setSelectedCategory(categoryId);
17+
setShowBrickPanel(true);
18+
}
19+
};
20+
21+
const handleCloseBrickPanel = () => {
22+
setShowBrickPanel(false);
23+
};
24+
25+
React.useEffect(() => {
26+
if (selectedCategory && selectedCategory !== prevSelectedCategory.current) {
27+
setShowBrickPanel(true);
28+
}
29+
prevSelectedCategory.current = selectedCategory;
30+
}, [selectedCategory]);
31+
32+
return (
33+
<>
34+
{/* Primary Palette */}
35+
<div className="palette-container">
36+
<Sidebar
37+
selected={selectedCategory}
38+
onSelect={handleCategorySelect}
39+
query={query}
40+
setQuery={setQuery}
41+
/>
42+
</div>
43+
44+
{/* Secondary Panel - Only show when a category is selected and panel is open */}
45+
{selectedCategory && showBrickPanel && (
46+
<div className="brick-list-container">
47+
<BrickListPanel
48+
selectedCategory={selectedCategory}
49+
onClose={handleCloseBrickPanel}
50+
filter={query}
51+
/>
52+
</div>
53+
)}
54+
</>
55+
);
56+
};
57+
58+
export default PaletteWrapper;
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import React from 'react';
2+
import { SimpleBrickView } from '../brick/view/components/simple';
3+
import { ExpressionBrickView } from '../brick/view/components/expression';
4+
import { CompoundBrickView } from '../brick/view/components/compound';
5+
import type { BrickConfig } from './types';
6+
7+
export type BrickType = 'simple' | 'expression' | 'compound';
8+
9+
const DEFAULT_STROKE_WIDTH = 1;
10+
const DEFAULT_SCALE = 1;
11+
const DEFAULT_COLOR_FG = '#000';
12+
const DEFAULT_SHADOW = false;
13+
const DEFAULT_VISIBLE = true;
14+
const DEFAULT_STATE = 'default';
15+
16+
function makeArgs(config: BrickConfig) {
17+
return config.argCount > 0 ? Array(config.argCount).fill({ w: 20, h: 20 }) : [];
18+
}
19+
20+
function wrap<P extends object>(View: React.ComponentType<P>): React.FC<BrickConfig> {
21+
return function WrappedBrickView(cfg: BrickConfig) {
22+
const [content, setContent] = React.useState<React.ReactNode>(null);
23+
24+
React.useEffect(() => {
25+
let isMounted = true;
26+
27+
const renderView = async () => {
28+
const common = {
29+
label: cfg.label,
30+
labelType: 'text' as const,
31+
colorBg: cfg.color,
32+
colorFg: DEFAULT_COLOR_FG,
33+
strokeColor: DEFAULT_COLOR_FG,
34+
strokeWidth: DEFAULT_STROKE_WIDTH,
35+
scale: DEFAULT_SCALE,
36+
shadow: DEFAULT_SHADOW,
37+
tooltip: cfg.id,
38+
bboxArgs: makeArgs(cfg),
39+
visualState: DEFAULT_STATE,
40+
isActionMenuOpen: false,
41+
isVisible: DEFAULT_VISIBLE,
42+
};
43+
44+
let result: React.ReactNode;
45+
46+
try {
47+
switch (cfg.type) {
48+
case 'simple':
49+
result = (
50+
<SimpleBrickView
51+
{...(common as any)}
52+
topNotch={cfg.notches.top}
53+
bottomNotch={cfg.notches.bottom}
54+
/>
55+
);
56+
break;
57+
case 'expression':
58+
result = (
59+
<ExpressionBrickView
60+
{...(common as any)}
61+
topNotch={cfg.notches.top}
62+
bottomNotch={cfg.notches.bottom}
63+
/>
64+
);
65+
break;
66+
case 'compound':
67+
result = (
68+
<CompoundBrickView
69+
{...(common as any)}
70+
topNotch={cfg.notches.top}
71+
bottomNotch={cfg.notches.bottom}
72+
/>
73+
);
74+
break;
75+
default:
76+
result = null;
77+
}
78+
79+
const finalResult = (await (result as any)?.then?.((x: React.ReactNode) => x)) ?? result;
80+
81+
if (isMounted) {
82+
setContent(finalResult);
83+
}
84+
} catch (error) {
85+
console.error('Error rendering brick:', error);
86+
if (isMounted) {
87+
setContent(<div>Error rendering brick</div>);
88+
}
89+
}
90+
};
91+
92+
renderView();
93+
94+
return () => {
95+
isMounted = false;
96+
};
97+
}, [cfg]);
98+
99+
return (
100+
content || (
101+
<div
102+
style={{
103+
width: '100%',
104+
height: '40px',
105+
display: 'flex',
106+
alignItems: 'center',
107+
justifyContent: 'center',
108+
}}
109+
>
110+
<span>Loading...</span>
111+
</div>
112+
)
113+
);
114+
};
115+
}
116+
117+
export const brickViews: Record<BrickType, React.FC<BrickConfig>> = {
118+
simple: wrap(SimpleBrickView),
119+
expression: wrap(ExpressionBrickView),
120+
compound: wrap(CompoundBrickView),
121+
};

0 commit comments

Comments
 (0)