added key events to canvas

This commit is contained in:
paul
2024-09-16 22:04:24 +05:30
parent f79b6514db
commit e5ef6fd37a
8 changed files with 275 additions and 182 deletions

View File

@@ -1 +1,15 @@
# TkBuilder
# PyUIBuilder - The only Python GUI builder you'll ever need
## Features
* Framework agnostic - Can outputs code in multiple frameworks.
* Easy to use.
* Plugins to extend 3rd party UI libraries
* Generate Code.
## Roadmap

1
notes.md Normal file
View File

@@ -0,0 +1 @@
### State management in react is a f*king mess

View File

@@ -13,6 +13,7 @@ import WidgetsContainer from './sidebar/widgetsContainer'
import Widget from './canvas/widgets/base'
import { DraggableWidgetCard } from './components/cards'
import { DragProvider } from './components/draggable/draggableContext'
import { ActiveWidgetProvider } from './canvas/activeWidgetContext'
function App() {
@@ -141,7 +142,10 @@ function App() {
<DragProvider>
<div className="tw-w-full tw-h-[94vh] tw-flex">
<Sidebar tabs={sidebarTabs}/>
{/* <ActiveWidgetProvider> */}
<Canvas ref={canvasRef} widgets={canvasWidgets} onWidgetAdded={handleWidgetAddedToCanvas}/>
{/* </ActiveWidgetProvider> */}
</div>
{/* dragOverlay (dnd-kit) helps move items from one container to another */}

View File

@@ -0,0 +1,61 @@
import React, { createContext, Component, useContext, useState, createRef, useMemo } from 'react'
// NOTE: using this context provider causes many re-rendering when the canvas is panned the toolbar updates unnecessarily
// Create the Context
export const ActiveWidgetContext = createContext()
// Create the Provider component
export class ActiveWidgetProvider extends Component {
state = {
activeWidgetId: null,
activeWidgetAttrs: {}
}
updateActiveWidget = (widget) => {
this.setState({ activeWidgetId: widget })
}
updateToolAttrs = (widgetAttrs) => {
this.setState({activeWidgetAttrs: widgetAttrs})
}
render() {
return (
<ActiveWidgetContext.Provider
value={{ ...this.state, updateActiveWidget: this.updateActiveWidget, updateToolAttrs: this.updateToolAttrs }}
>
{this.props.children}
</ActiveWidgetContext.Provider>
);
}
}
// Custom hook for function components
export const useActiveWidget = () => {
const context = useContext(ActiveWidgetContext)
if (context === undefined) {
throw new Error('useActiveWidget must be used within an ActiveWidgetProvider')
}
return useMemo(() => context, [context.activeWidgetId, context.activeWidgetAttrs, context.updateToolAttrs, context.updateActiveWidget])
}
// Higher-Order Component for class-based components
export const withActiveWidget = (WrappedComponent) => {
return class extends Component {
render() {
return (
<ActiveWidgetContext.Consumer>
{context => <WrappedComponent {...this.props} {...context} />}
</ActiveWidgetContext.Consumer>
)
}
}
}

View File

@@ -19,6 +19,7 @@ import { WidgetContext } from './context/widgetContext'
import DotsBackground from "../assets/background/dots.svg"
import DroppableWrapper from "../components/draggable/droppable"
import { ActiveWidgetContext, ActiveWidgetProvider, withActiveWidget } from "./activeWidgetContext"
// const DotsBackground = require("../assets/background/dots.svg")
@@ -31,6 +32,8 @@ const CanvasModes = {
class Canvas extends React.Component {
// static contextType = ActiveWidgetContext
constructor(props) {
super(props)
@@ -76,6 +79,8 @@ class Canvas extends React.Component {
this.mouseDownEvent = this.mouseDownEvent.bind(this)
this.mouseMoveEvent = this.mouseMoveEvent.bind(this)
this.mouseUpEvent = this.mouseUpEvent.bind(this)
this.keyDownEvent = this.keyDownEvent.bind(this)
this.wheelZoom = this.wheelZoom.bind(this)
this.onActiveWidgetUpdate = this.onActiveWidgetUpdate.bind(this)
@@ -107,15 +112,42 @@ class Canvas extends React.Component {
componentWillUnmount() {
this.canvasContainerRef.current.removeEventListener("mousedown", this.mouseDownEvent)
this.canvasContainerRef.current.removeEventListener("mouseup", this.mouseUpEvent)
this.canvasContainerRef.current.removeEventListener("mousemove", this.mouseMoveEvent)
this.canvasContainerRef.current.removeEventListener("wheel", this.wheelZoom)
this.canvasContainerRef.current.removeEventListener("keydown", this.keyDownEvent)
// NOTE: this will clear the canvas
this.clearCanvas()
}
/**
initEvents(){
this.canvasContainerRef.current.addEventListener("mousedown", this.mouseDownEvent)
this.canvasContainerRef.current.addEventListener("mouseup", this.mouseUpEvent)
this.canvasContainerRef.current.addEventListener("mousemove", this.mouseMoveEvent)
this.canvasContainerRef.current.addEventListener("wheel", this.wheelZoom)
this.canvasContainerRef.current.addEventListener("keydown", this.keyDownEvent, true)
// window.addEventListener("keydown", this.keyDownEvent, true)
}
applyTransform(){
const { currentTranslate, zoom } = this.state
this.canvasRef.current.style.transform = `translate(${currentTranslate.x}px, ${currentTranslate.y}px) scale(${zoom})`
}
/**
*
* @returns {import("./widgets/base").Widget[]}
*/
getWidgets(){
getWidgets(){
return this.state.widgets
}
@@ -130,22 +162,6 @@ class Canvas extends React.Component {
})
}
initEvents(){
this.canvasContainerRef.current.addEventListener("mousedown", this.mouseDownEvent)
this.canvasContainerRef.current.addEventListener("mouseup", this.mouseUpEvent)
this.canvasContainerRef.current.addEventListener("mousemove", this.mouseMoveEvent)
this.canvasContainerRef.current.addEventListener('wheel', (event) => {
this.wheelZoom(event)
})
}
applyTransform(){
const { currentTranslate, zoom } = this.state
this.canvasRef.current.style.transform = `translate(${currentTranslate.x}px, ${currentTranslate.y}px) scale(${zoom})`
}
/**
* returns the widget that contains the target
@@ -155,6 +171,7 @@ class Canvas extends React.Component {
getWidgetFromTarget(target){
for (let [key, ref] of Object.entries(this.widgetRefs)){
// console.log("ref: ", ref)
if (ref.current.getElement().contains(target)){
return ref.current
}
@@ -162,6 +179,21 @@ class Canvas extends React.Component {
}
keyDownEvent(event){
if (event.key === "Delete"){
this.deleteSelectedWidgets()
}
if (event.key === "+"){
this.setZoom(this.state.zoom + 0.1)
}
if (event.key === "-"){
this.setZoom(this.state.zoom - 0.1)
}
}
mouseDownEvent(event){
@@ -190,6 +222,10 @@ class Canvas extends React.Component {
selectedWidgets: [selectedWidget],
toolbarAttrs: selectedWidget.getToolbarAttrs()
})
this.context.updateActiveWidget(selectedWidget.__id)
this.context.updateToolAttrs(selectedWidget.getToolbarAttrs())
// this.props.updateActiveWidget(selectedWidget)
}
this.currentMode = CanvasModes.MOVE_WIDGET
}
@@ -302,18 +338,6 @@ class Canvas extends React.Component {
return this.state.currentTranslate
}
/**
* Given a position relative to canvas container,
* returns the position relative to the canvas
*/
getRelativePositionToCanvas(x, y){
const canvasRect = this.canvasRef.current.getBoundingClientRect()
let zoom = this.state.zoom
return {x: (canvasRect.left - x ), y: (canvasRect.top - y)}
}
/**
* fits the canvas size to fit the widgets bounding box
*/
@@ -341,32 +365,30 @@ class Canvas extends React.Component {
this.canvasContainerRef.current.style.cursor = cursor
}
setZoom(zoom, pos={x:0, y:0}){
setZoom(zoom, pos){
// if (zoom < 0.5 || zoom > 2){
// return
// }
const { currentTranslate } = this.state
let newTranslate = currentTranslate
if (pos){
// Calculate the new translation to zoom into the mouse position
const offsetX = pos.x - (this.canvasContainerRef.current.clientWidth / 2 + currentTranslate.x)
const offsetY = pos.y - (this.canvasContainerRef.current.clientHeight / 2 + currentTranslate.y)
// Calculate the new translation to zoom into the mouse position
const offsetX = pos.x - (this.canvasContainerRef.current.clientWidth / 2 + currentTranslate.x)
const offsetY = pos.y - (this.canvasContainerRef.current.clientHeight / 2 + currentTranslate.y)
const newTranslateX = currentTranslate.x - offsetX * (zoom - this.state.zoom)
const newTranslateY = currentTranslate.y - offsetY * (zoom - this.state.zoom)
this.setState({
zoom: Math.max(0.5, Math.min(zoom, 1.5)), // clamp between 0.5 and 1.5
currentTranslate: {
const newTranslateX = currentTranslate.x - offsetX * (zoom - this.state.zoom)
const newTranslateY = currentTranslate.y - offsetY * (zoom - this.state.zoom)
newTranslate = {
x: newTranslateX,
y: newTranslateY
}
}
this.setState({
zoom: Math.max(0.5, Math.min(zoom, 1.5)), // clamp between 0.5 and 1.5
currentTranslate: newTranslate
}, this.applyTransform)
// this.canvasRef.current.style.width = `${100/zoom}%`
// this.canvasRef.current.style.height = `${100/zoom}%`
}
@@ -390,6 +412,9 @@ class Canvas extends React.Component {
widget.current?.deSelect()
})
this.context?.updateActiveWidget("")
this.context.updateToolAttrs({})
this.setState({
selectedWidgets: [],
toolbarAttrs: null,
@@ -433,7 +458,6 @@ class Canvas extends React.Component {
// Store the ref in the instance variable
this.widgetRefs[id] = widgetRef
// console.log("widget ref: ", this.widgetRefs)
const widgets = [...this.state.widgets, { id, widgetType: widgetComponentType }] // don't add the widget refs in the state
// Update the state to include the new widget's type and ID
@@ -452,6 +476,10 @@ class Canvas extends React.Component {
return {id, widgetRef}
}
/**
* delete's the selected widgets from the canvas
* @param {null|Widget} widgets - optional widgets that can be deleted along the selected widgets
*/
deleteSelectedWidgets(widgets=[]){
@@ -519,7 +547,9 @@ class Canvas extends React.Component {
if (this.state.selectedWidgets.length === 0 || widgetId !== this.state.selectedWidgets[0].__id)
return
// console.log("updating...")
console.log("updating...", this.state.toolbarAttrs, this.state.selectedWidgets.at(0).getToolbarAttrs())
// console.log("attrs: ", this.state.selectedWidgets.at(0).getToolbarAttrs())
this.setState({
toolbarAttrs: this.state.selectedWidgets.at(0).getToolbarAttrs()
@@ -575,36 +605,39 @@ class Canvas extends React.Component {
</Tooltip>
</div>
{/* <ActiveWidgetProvider> */}
<DroppableWrapper id="canvas-droppable"
className="tw-w-full tw-h-full" onDrop={this.handleDropEvent}>
{/* <Dropdown trigger={['contextMenu']} mouseLeaveDelay={0} menu={{items: this.state.contextMenuItems, }}> */}
<div className="dots-bg tw-w-full tw-h-full tw-flex tw-relative tw-bg-[#f2f2f2] tw-overflow-hidden"
ref={this.canvasContainerRef}
style={{
transition: " transform 0.3s ease-in-out",
backgroundImage: `url('${DotsBackground}')`,
backgroundSize: 'cover', // Ensure proper sizing if needed
backgroundRepeat: 'no-repeat',
}}
>
{/* Canvas */}
<div data-canvas className="tw-w-full tw-h-full tw-absolute tw-top-0 tw-select-none
tw-bg-green-300"
ref={this.canvasRef}>
<div className="tw-relative tw-w-full tw-h-full">
{
this.state.widgets.map(this.renderWidget)
}
</div>
<Dropdown trigger={['contextMenu']} mouseLeaveDelay={0} menu={{items: this.state.contextMenuItems, }}>
<div className="dots-bg tw-w-full tw-h-full tw-outline-none tw-flex tw-relative tw-bg-[#f2f2f2] tw-overflow-hidden"
ref={this.canvasContainerRef}
style={{
transition: " transform 0.3s ease-in-out",
backgroundImage: `url('${DotsBackground}')`,
backgroundSize: 'cover', // Ensure proper sizing if needed
backgroundRepeat: 'no-repeat',
}}
tabIndex={0} // allow focus
>
{/* Canvas */}
<div data-canvas className="tw-w-full tw-h-full tw-absolute tw-top-0 tw-select-none
tw-bg-green-300"
ref={this.canvasRef}>
<div className="tw-relative tw-w-full tw-h-full">
{
this.state.widgets.map(this.renderWidget)
}
</div>
</div>
{/* </Dropdown> */}
</div>
</Dropdown>
</DroppableWrapper>
<CanvasToolBar isOpen={this.state.toolbarOpen}
widgetType={this.state.selectedWidgets?.at(0)?.getWidgetType() || ""}
attrs={this.state.toolbarAttrs}
/>
{/* </ActiveWidgetProvider> */}
</div>
)
}

View File

@@ -4,6 +4,7 @@ import { ColorPicker, Input, InputNumber, Select } from "antd"
import { capitalize } from "../utils/common"
import Tools from "./constants/tools.js"
import { useActiveWidget } from "./activeWidgetContext.js"
// FIXME: Maximum recursion error
@@ -16,6 +17,9 @@ import Tools from "./constants/tools.js"
*/
const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => {
// const { activeWidgetAttrs } = useActiveWidget()
// console.log("active widget context: ", activeWidgetAttrs)
const [toolbarOpen, setToolbarOpen] = useState(isOpen)
const [toolbarAttrs, setToolbarAttrs] = useState(attrs)
@@ -24,11 +28,19 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => {
}, [isOpen])
useEffect(() => {
console.log("active widget: ", attrs)
setToolbarAttrs(attrs)
}, [attrs])
// useEffect(() => {
// console.log("active widget: ", activeWidgetAttrs)
// setToolbarAttrs(activeWidgetAttrs || {})
// }, [activeWidgetAttrs])
const handleChange = (value, callback) => {
console.log("changed...")
if (callback) {
callback(value)
}
@@ -42,14 +54,14 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => {
const isFirstLevel = parentKey === ""
const outerLabelClass = isFirstLevel
? "tw-text-lg tw-text-blue-700 tw-font-medium"
: "tw-text-lg"
? "tw-text-base tw-text-blue-700 tw-font-medium"
: "tw-text-base"
// Render tool widgets
if (typeof val === "object" && val.tool) {
return (
<div key={keyName} className="tw-flex tw-flex-col tw-gap-2">
<div className={`${isFirstLevel ? outerLabelClass : "tw-text-base"}`}>{val.label}</div>
<div className={`${isFirstLevel ? outerLabelClass : "tw-text-sm"}`}>{val.label}</div>
{val.tool === Tools.INPUT && (
<Input

View File

@@ -6,6 +6,10 @@ import Layouts from "../constants/layouts"
import Cursor from "../constants/cursor"
import { toSnakeCase } from "../utils/utils"
import EditableDiv from "../../components/editableDiv"
import DraggableWrapper from "../../components/draggable/draggable"
import DroppableWrapper from "../../components/draggable/droppable"
import { ActiveWidgetContext } from "../activeWidgetContext"
/**
@@ -15,6 +19,8 @@ class Widget extends React.Component {
static widgetType = "widget"
// static contextType = ActiveWidgetContext
constructor(props) {
super(props)
@@ -75,7 +81,10 @@ class Widget extends React.Component {
label: "Background Color",
tool: Tools.COLOR_PICKER, // the tool to display, can be either HTML ELement or a constant string
value: "",
onChange: (value) => this.setWidgetStyling("backgroundColor", value)
onChange: (value) => {
this.setWidgetStyling("backgroundColor", value)
this.setAttrValue("styling.backgroundColor", value)
}
},
foregroundColor: {
label: "Foreground Color",
@@ -145,13 +154,33 @@ class Widget extends React.Component {
this.canvas.addEventListener("mouseup", this.stopResizing)
}
componentDidUpdate(prevProps, prevState) {
// if (prevState !== this.state) {
// // State has changed
// console.log('State has been updated')
// } else {
// // State has not changed
// console.log('State has not changed')
// }
}
updateState = (newState, callback) => {
// FIXME: maximum recursion error when updating size
this.setState(newState, () => {
const { onWidgetUpdate } = this.props
if (onWidgetUpdate) {
onWidgetUpdate(this.__id)
}
// const { activeWidgetId, updateToolAttrs } = this.context
// if (activeWidgetId === this.__id)
// updateToolAttrs(this.getToolbarAttrs())
if (callback) callback()
})
}
@@ -174,7 +203,7 @@ class Widget extends React.Component {
onChange: (value) => this.setWidgetName(value)
},
size: {
label: "Sizing",
label: "Size",
display: "horizontal",
width: {
label: "Width",
@@ -228,16 +257,6 @@ class Widget extends React.Component {
mousePress(event) {
// event.preventDefault()
if (!this._disableSelection) {
// const widgetSelected = new CustomEvent("selection:created", {
// detail: {
// event,
// id: this.__id,
// element: this
// },
// // bubbles: true // Allow the event to bubble up the DOM tree
// })
// this.canvas.dispatchEvent(widgetSelected)
}
}
@@ -264,13 +283,14 @@ class Widget extends React.Component {
// don't change position when resizing the widget
return
}
// this.setState({
// pos: { x, y }
// })
this.updateState({
this.setState({
pos: { x, y }
})
// this.updateState({
// pos: { x, y }
// })
}
setParent(parentId) {
@@ -303,38 +323,6 @@ class Widget extends React.Component {
return this.state.size
}
getScaleAwareDimensions() {
// Get the bounding rectangle
const rect = this.elementRef.current.getBoundingClientRect()
// Get the computed style of the element
const style = window.getComputedStyle(this.elementRef.current)
// Get the transform matrix
const transform = style.transform
// Extract scale factors from the matrix
let scaleX = 1
let scaleY = 1
if (transform && transform !== 'none') {
// For 2D transforms (a, c, b, d)
const matrix = transform.match(/^matrix\(([^,]+),[^,]+,([^,]+),[^,]+,[^,]+,[^,]+\)$/);
if (matrix) {
scaleX = parseFloat(matrix[1])
scaleY = parseFloat(matrix[2])
}
}
// Return scaled width and height
return {
width: rect.width / scaleX,
height: rect.height / scaleY
}
}
getWidgetFunctions() {
return this.functions
}
@@ -400,7 +388,7 @@ class Widget extends React.Component {
* @param {string} value - Value of the style
* @param {function():void} [callback] - optional callback, thats called after setting the internal state
*/
setWidgetStyling(key, value, callback) {
setWidgetStyling(key, value) {
const widgetStyle = {
...this.state.widgetStyling,
@@ -409,9 +397,6 @@ class Widget extends React.Component {
this.setState({
widgetStyling: widgetStyle
}, () => {
if (callback)
callback(widgetStyle)
})
}
@@ -420,28 +405,15 @@ class Widget extends React.Component {
*
* @param {number|null} width
* @param {number|null} height
* @param {function():void} [callback] - optional callback, thats called after setting the internal state
*/
setWidgetSize(width, height, callback) {
setWidgetSize(width, height) {
const newSize = {
width: Math.max(this.minSize.width, Math.min(width || this.state.size.width, this.maxSize.width)),
height: Math.max(this.minSize.height, Math.min(height || this.state.size.height, this.maxSize.height)),
}
this.setState({
size: newSize
}, () => {
if (callback) {
callback(newSize)
}
})
this.updateState({
size: newSize
}, () => {
if (callback)
callback(newSize)
})
}
@@ -530,9 +502,6 @@ class Widget extends React.Component {
*/
render() {
const widgetStyle = this.state.widgetStyling
let outerStyle = {
cursor: this.cursor,
zIndex: this.state.zIndex,
@@ -552,51 +521,50 @@ class Widget extends React.Component {
// console.log("selected: ", this.state.selected)
return (
<div data-id={this.__id} ref={this.elementRef} className="tw-absolute tw-shadow-xl tw-w-fit tw-h-fit"
style={outerStyle}
>
<div data-id={this.__id} ref={this.elementRef} className="tw-absolute tw-shadow-xl tw-w-fit tw-h-fit"
style={outerStyle}
>
{this.renderContent()}
<div className={`tw-absolute tw-bg-transparent tw-scale-[1.1] tw-opacity-100
tw-w-full tw-h-full tw-top-0
${this.state.selected ? 'tw-border-2 tw-border-solid tw-border-blue-500' : 'tw-hidden'}`}>
{this.renderContent()}
<div className={`tw-absolute tw-bg-transparent tw-scale-[1.1] tw-opacity-100
tw-w-full tw-h-full tw-top-0
${this.state.selected ? 'tw-border-2 tw-border-solid tw-border-blue-500' : 'tw-hidden'}`}>
<div className="tw-relative tw-w-full tw-h-full">
<EditableDiv value={this.state.widgetName} onChange={this.setWidgetName}
maxLength={40}
openEdit={this.state.enableRename}
className="tw-text-sm tw-w-fit tw-max-w-[160px] tw-text-clip tw-min-w-[150px]
tw-overflow-hidden tw-absolute tw--top-6 tw-h-6"
/>
<div
className="tw-w-2 tw-h-2 tw-absolute tw--left-1 tw--top-1 tw-bg-blue-500"
style={{ cursor: Cursor.NW_RESIZE }}
onMouseDown={(e) => this.startResizing("nw", e)}
/>
<div
className="tw-w-2 tw-h-2 tw-absolute tw--right-1 tw--top-1 tw-bg-blue-500"
style={{ cursor: Cursor.SW_RESIZE }}
onMouseDown={(e) => this.startResizing("ne", e)}
/>
<div
className="tw-w-2 tw-h-2 tw-absolute tw--left-1 tw--bottom-1 tw-bg-blue-500"
style={{ cursor: Cursor.SW_RESIZE }}
onMouseDown={(e) => this.startResizing("sw", e)}
/>
<div
className="tw-w-2 tw-h-2 tw-absolute tw--right-1 tw--bottom-1 tw-bg-blue-500"
style={{ cursor: Cursor.SE_RESIZE }}
onMouseDown={(e) => this.startResizing("se", e)}
/>
</div>
<div className="tw-relative tw-w-full tw-h-full">
<EditableDiv value={this.state.widgetName} onChange={this.setWidgetName}
maxLength={40}
openEdit={this.state.enableRename}
className="tw-text-sm tw-w-fit tw-max-w-[160px] tw-text-clip tw-min-w-[150px]
tw-overflow-hidden tw-absolute tw--top-6 tw-h-6"
/>
<div
className="tw-w-2 tw-h-2 tw-absolute tw--left-1 tw--top-1 tw-bg-blue-500"
style={{ cursor: Cursor.NW_RESIZE }}
onMouseDown={(e) => this.startResizing("nw", e)}
/>
<div
className="tw-w-2 tw-h-2 tw-absolute tw--right-1 tw--top-1 tw-bg-blue-500"
style={{ cursor: Cursor.SW_RESIZE }}
onMouseDown={(e) => this.startResizing("ne", e)}
/>
<div
className="tw-w-2 tw-h-2 tw-absolute tw--left-1 tw--bottom-1 tw-bg-blue-500"
style={{ cursor: Cursor.SW_RESIZE }}
onMouseDown={(e) => this.startResizing("sw", e)}
/>
<div
className="tw-w-2 tw-h-2 tw-absolute tw--right-1 tw--bottom-1 tw-bg-blue-500"
style={{ cursor: Cursor.SE_RESIZE }}
onMouseDown={(e) => this.startResizing("se", e)}
/>
</div>
</div>
</div>
)
}

View File

@@ -32,11 +32,11 @@ export function DraggableWidgetCard({ name, img, url, innerRef}){
// <Draggable className="tw-cursor-pointer" id={name}>
<DraggableWrapper dragElementType={"widget"} className="tw-cursor-pointer tw-w-fit tw-h-fit">
<div ref={innerRef} className="tw-select-none tw-pointer-events-none tw-h-[240px] tw-w-[280px] tw-flex tw-flex-col
<div ref={innerRef} className="tw-select-none tw-h-[240px] tw-w-[280px] tw-flex tw-flex-col
tw-rounded-md tw-overflow-hidden
tw-gap-2 tw-text-gray-600 tw-bg-[#ffffff44] tw-border-solid tw-border-[1px]
tw-border-[#888] ">
<div className="tw-h-[200px] tw-w-full tw-overflow-hidden">
<div className="tw-h-[200px] tw-pointer-events-none tw-w-full tw-overflow-hidden">
<img src={img} alt={name} className="tw-object-contain tw-h-full tw-w-full tw-select-none" />
</div>
<span className="tw-text-xl tw-text-center">{name}</span>