@@ -12,6 +12,7 @@ import { Cell } from "@commontools/runner";
1212import ShapeLogo from "@/assets/ShapeLogo.tsx" ;
1313import { MdOutlineStar } from "react-icons/md" ;
1414import { useSyncedStatus } from "@/hooks/use-synced-status.ts" ;
15+ import { CharmRenderer } from "@/components/CharmRunner.tsx" ;
1516
1617export interface CommonDataEvent extends CustomEvent {
1718 detail : {
@@ -96,9 +97,163 @@ function CharmPreview(
9697 ) ;
9798}
9899
100+ interface HoverPreviewProps {
101+ hoveredCharm : string | null ;
102+ charms : Cell < Charm > [ ] ;
103+ position : { x : number ; y : number } ;
104+ replicaName : string ;
105+ }
106+ const HoverPreview = (
107+ { hoveredCharm, charms, position, replicaName } : HoverPreviewProps ,
108+ ) => {
109+ // Find the charm that matches the hoveredCharm ID
110+ const charm = hoveredCharm
111+ ? charms . find ( ( c ) => charmId ( c ) === hoveredCharm )
112+ : null ;
113+
114+ if ( ! charm || ! hoveredCharm ) return null ;
115+
116+ const id = charmId ( charm ) ;
117+ const name = charm . get ( ) [ NAME ] || "Unnamed Charm" ;
118+
119+ return (
120+ < div
121+ className = "fixed z-50 w-128 shadow-xl pointer-events-none"
122+ style = { {
123+ left : `${ position . x } px` ,
124+ top : `${ position . y } px` ,
125+ transform : "translate(25%, 25%)" ,
126+ } }
127+ >
128+ < CommonCard className = "p-2 shadow-xl bg-white" >
129+ < h3 className = "text-xl font-semibold text-gray-800 mb-4" >
130+ { name + ` (#${ id ! . slice ( - 4 ) } )` }
131+ </ h3 >
132+ < div className = "w-full bg-gray-50 rounded border border-gray-100 min-h-[256px] pointer-events-none select-none" >
133+ < CharmRenderer className = "h-full" charm = { charm } />
134+ </ div >
135+ </ CommonCard >
136+ </ div >
137+ ) ;
138+ } ;
139+
140+ interface CharmTableProps {
141+ charms : Cell < Charm > [ ] ;
142+ replicaName : string ;
143+ charmManager : any ;
144+ }
145+
146+ const CharmTable = (
147+ { charms, replicaName, charmManager } : CharmTableProps ,
148+ ) => {
149+ const [ hoveredCharm , setHoveredCharm ] = useState < string | null > ( null ) ;
150+ const [ previewPosition , setPreviewPosition ] = useState ( { x : 0 , y : 0 } ) ;
151+ // Use a ref to cache the last hovered charm to prevent thrashing
152+ const hoveredCharmRef = useRef < string | null > ( null ) ;
153+
154+ const handleMouseMove = ( e : React . MouseEvent , id : string ) => {
155+ // Only update state if the hovered charm has changed
156+ if ( hoveredCharmRef . current !== id ) {
157+ hoveredCharmRef . current = id ;
158+ setHoveredCharm ( id ) ;
159+ }
160+
161+ // Position the preview card relative to the cursor
162+ setPreviewPosition ( {
163+ x : e . clientX + 20 , // offset to the right of cursor
164+ y : e . clientY - 100 , // offset above the cursor
165+ } ) ;
166+ } ;
167+
168+ const handleMouseLeave = ( ) => {
169+ hoveredCharmRef . current = null ;
170+ setHoveredCharm ( null ) ;
171+ } ;
172+
173+ return (
174+ < div className = "relative" >
175+ < div className = "overflow-x-auto shadow-md sm:rounded-lg" >
176+ < table className = "w-full text-sm text-left text-gray-500" >
177+ < thead className = "text-xs text-gray-700 uppercase bg-gray-50" >
178+ < tr >
179+ < th scope = "col" className = "px-6 py-3" > Name</ th >
180+ < th scope = "col" className = "px-6 py-3" > ID</ th >
181+ < th scope = "col" className = "px-6 py-3" > Actions</ th >
182+ </ tr >
183+ </ thead >
184+ < tbody >
185+ { charms . map ( ( charm ) => {
186+ const id = charmId ( charm ) ;
187+ const name = charm . get ( ) [ NAME ] || "Unnamed Charm" ;
188+
189+ return (
190+ < tr
191+ key = { id }
192+ className = "bg-white border-b hover:bg-gray-50 relative"
193+ onMouseMove = { ( e ) => handleMouseMove ( e , id ! ) }
194+ onMouseLeave = { handleMouseLeave }
195+ >
196+ < td className = "px-6 py-4 font-medium text-gray-900" >
197+ < NavLink to = { `/${ replicaName } /${ id } ` } >
198+ { name }
199+ </ NavLink >
200+ </ td >
201+ < td className = "px-6 py-4" >
202+ < NavLink to = { `/${ replicaName } /${ id } ` } >
203+ #{ id ?. slice ( - 4 ) }
204+ </ NavLink >
205+ </ td >
206+ < td className = "px-6 py-4" >
207+ < button
208+ type = "button"
209+ onClick = { ( e ) => {
210+ e . preventDefault ( ) ;
211+ if (
212+ globalThis . confirm (
213+ "Are you sure you want to remove this charm?" ,
214+ )
215+ ) {
216+ charmManager . remove ( { "/" : id ! } ) ;
217+ }
218+ } }
219+ className = "text-gray-400 hover:text-red-500 transition-colors"
220+ >
221+ < svg
222+ xmlns = "http://www.w3.org/2000/svg"
223+ width = "20"
224+ height = "20"
225+ viewBox = "0 0 24 24"
226+ fill = "none"
227+ stroke = "currentColor"
228+ strokeWidth = "2"
229+ >
230+ < path d = "M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2" />
231+ </ svg >
232+ </ button >
233+ </ td >
234+ </ tr >
235+ ) ;
236+ } ) }
237+ </ tbody >
238+ </ table >
239+ </ div >
240+
241+ { hoveredCharm && (
242+ < HoverPreview
243+ hoveredCharm = { hoveredCharm }
244+ charms = { charms }
245+ position = { previewPosition }
246+ replicaName = { replicaName }
247+ />
248+ ) }
249+ </ div >
250+ ) ;
251+ } ;
252+
99253export default function CharmList ( ) {
100254 const { replicaName } = useParams < { replicaName : string } > ( ) ;
101255 const { charmManager } = useCharmManager ( ) ;
256+ const [ pinned ] = useCell ( charmManager . getPinned ( ) ) ;
102257 const [ charms ] = useCell ( charmManager . getCharms ( ) ) ;
103258 const { isSyncing } = useSyncedStatus ( charmManager ) ;
104259
@@ -131,15 +286,28 @@ export default function CharmList() {
131286 }
132287
133288 return (
134- < div className = "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 p-8" >
135- { replicaName &&
136- charms . map ( ( charm ) => (
137- < CharmPreview
138- key = { charmId ( charm ) }
139- charm = { charm }
289+ < div className = "p-2" >
290+ < h1 > Pinned</ h1 >
291+ < div className = "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 p-8" >
292+ { replicaName &&
293+ pinned . map ( ( charm ) => (
294+ < CharmPreview
295+ key = { charmId ( charm ) }
296+ charm = { charm }
297+ replicaName = { replicaName }
298+ />
299+ ) ) }
300+ </ div >
301+ < h1 > All Charms</ h1 >
302+ < div className = "p-8" >
303+ { replicaName && (
304+ < CharmTable
305+ charms = { charms }
140306 replicaName = { replicaName }
307+ charmManager = { charmManager }
141308 />
142- ) ) }
309+ ) }
310+ </ div >
143311 </ div >
144312 ) ;
145313}
0 commit comments