added swappable layout

This commit is contained in:
paul
2024-09-21 18:37:28 +05:30
parent 248de2167c
commit 3ad39c74d6
6 changed files with 423 additions and 180 deletions

View File

@@ -5,14 +5,14 @@ Any feature that has 👑 beside it, is meant only for [premium users](./readme.
### 1.0.0
- [x] Create the initial version for UI builder
### 1.1.0
- [ ] Allow swappable layout - (swappy/react-dnd-kit)
### 1.2.0
- [ ] UI fixes and enhancement
- [ ] Add text editor to help with event handlers
- [ ] Add text editor to support event handlers
- [ ] Rewrite DND for better feedback - (swappy/react-dnd-kit/ GSAP draggable)
- [ ] Duplicate widgets
### 1.5.0
- [ ] Add canvas support (try fabricjs)
- [ ] Add canvas support tools (lines, rect etc) (try fabricjs)
- [ ] Initial version for Electron App 👑
- [ ] Save files locally 👑
- [ ] Load UI files 👑
@@ -21,6 +21,7 @@ Any feature that has 👑 beside it, is meant only for [premium users](./readme.
### 2.0.0
- [ ] Support for more third party plugins
- [ ] Support more templates
- [ ] Support for Kivy
- [ ] Sharable Templates
- [ ] Dark theme 👑

View File

@@ -368,7 +368,7 @@ class Canvas extends React.Component {
this.setState({ widgetResizing: "" })
}
for (let [key, widget] of Object.entries(this.widgetRefs)){
for (let [key, widget] of Object.entries(this.widgetRefs)) {
// since the mouseUp event is not triggered inside the widget once its outside,
// we'll need a global mouse up event to re-enable drag
widget.current.enableDrag()
@@ -643,46 +643,76 @@ class Canvas extends React.Component {
* @param {object} dragElement
* @param {boolean} create - if create is set to true the widget will be created before adding to the child tree
*/
handleAddWidgetChild = (parentWidgetId, dragElementID, create = false) => {
handleAddWidgetChild = ({ parentWidgetId, dragElementID, create = false, swap = false }) => {
// TODO: creation of the child widget if its not created
// widgets data structure { id, widgetType: widgetComponentType, children: [], parent: "" }
const parentWidgetObj = this.findWidgetFromListById(parentWidgetId)
let childWidgetObj = this.findWidgetFromListById(dragElementID)
const dropWidgetObj = this.findWidgetFromListById(parentWidgetId)
// Find the dragged widget object
let dragWidgetObj = this.findWidgetFromListById(dragElementID)
if (parentWidgetObj && childWidgetObj) {
if (dropWidgetObj && dragWidgetObj) {
const dragWidget = this.widgetRefs[dragWidgetObj.id]
const dragData = dragWidget.current.serialize()
const childWidget = this.widgetRefs[childWidgetObj.id]
const childData = childWidget.current.serialize()
// Update the child widget's properties (position type, zIndex, etc.)
const updatedChildWidget = {
...childWidgetObj,
parent: parentWidgetId,
initialData: {
...childData,
positionType: PosType.NONE,
zIndex: 0,
widgetContainer: WidgetContainer.WIDGET
if (swap) {
// If swapping, we need to find the common parent
const grandParentWidgetObj = this.findWidgetFromListById(dropWidgetObj.parent)
console.log("parent widget: ", grandParentWidgetObj, dropWidgetObj, this.state.widgets)
if (grandParentWidgetObj) {
// Find the indices of the dragged and drop widgets in the grandparent's children array
const dragIndex = grandParentWidgetObj.children.findIndex(child => child.id === dragElementID)
const dropIndex = grandParentWidgetObj.children.findIndex(child => child.id === parentWidgetId)
if (dragIndex !== -1 && dropIndex !== -1) {
// Swap their positions
let childrenCopy = [...grandParentWidgetObj.children]
const temp = childrenCopy[dragIndex]
childrenCopy[dragIndex] = childrenCopy[dropIndex]
childrenCopy[dropIndex] = temp
// Update the grandparent with the swapped children
const updatedGrandParentWidget = {
...grandParentWidgetObj,
children: childrenCopy
}
// Update the state with the new widget hierarchy
this.setState((prevState) => ({
widgets: this.updateWidgetRecursively(prevState.widgets, updatedGrandParentWidget)
}))
}
}
} else {
// Non-swap mode: Add the dragged widget as a child of the drop widget
let updatedWidgets = this.removeWidgetFromCurrentList(dragElementID)
const updatedDragWidget = {
...dragWidgetObj,
parent: dropWidgetObj.id, // Keep the parent reference
initialData: {
...dragData,
positionType: PosType.NONE,
zIndex: 0,
widgetContainer: WidgetContainer.WIDGET
}
}
const updatedDropWidget = {
...dropWidgetObj,
children: [...dropWidgetObj.children, updatedDragWidget]
}
// Recursively update the widget structure
updatedWidgets = this.updateWidgetRecursively(updatedWidgets, updatedDropWidget, updatedDragWidget)
// Update the state with the new widget hierarchy
this.setState({
widgets: updatedWidgets
})
}
// Remove the child from its previous location
let updatedWidgets = this.removeWidgetFromCurrentList(dragElementID)
// Add the child widget to the new parent's children
const updatedParentWidget = {
...parentWidgetObj,
children: [...parentWidgetObj.children, updatedChildWidget]
}
// Recursively update the widget structure with the new parent and child
updatedWidgets = this.updateWidgetRecursively(updatedWidgets, updatedParentWidget, updatedChildWidget)
// Update the state with the new widget hierarchy
this.setState({
widgets: updatedWidgets
})
}
}

View File

@@ -13,6 +13,7 @@ import { ActiveWidgetContext } from "../activeWidgetContext"
import { DragWidgetProvider } from "./draggableWidgetContext"
import WidgetDraggable from "./widgetDragDrop"
import WidgetContainer from "../constants/containers"
import { DragContext } from "../../components/draggable/draggableContext"
@@ -48,14 +49,15 @@ class Widget extends React.Component {
this.icon = "" // antd icon name representing this widget
this.elementRef = React.createRef()
this.elementRef = React.createRef() // this is the outer ref for draggable area
this.swappableAreaRef = React.createRef() // helps identify if the users intent is to swap or drop inside the widget
this.innerAreaRef = React.createRef() // this is the inner area where swap is prevented and only drop is accepted
this.functions = {
"load": { "args1": "number", "args2": "string" }
}
this.layout = Layouts.FLEX
this.droppableTags = ["widget"] // This indicates if the draggable can be dropped on this widget
this.boundingRect = {
x: 0,
y: 0,
@@ -68,6 +70,8 @@ class Widget extends React.Component {
selected: false,
widgetName: widgetName || 'widget', // this will later be converted to variable name
enableRename: false, // will open the widgets editable div for renaming
isDragging: false, // tells if the widget is currently being dragged
dragEnabled: true,
widgetContainer: WidgetContainer.CANVAS, // what is the parent of the widget
@@ -532,22 +536,6 @@ class Widget extends React.Component {
})
}
handleDrop = (event, dragElement) => {
console.log("dragging event: ", event, dragElement)
const container = dragElement.getAttribute("data-container")
// TODO: check if the drop is allowed
if (container === "canvas"){
this.props.onAddChildWidget(this.__id, dragElement.getAttribute("data-widget-id"))
}else if (container === "sidebar"){
this.props.onAddChildWidget(this.__id, null, true) // if dragged from the sidebar create the widget first
}
}
/**
*
@@ -591,7 +579,190 @@ class Widget extends React.Component {
}
// FIXME: children outside the bounding box
/**
*
* @depreciated - This function is depreciated in favour of handleDropEvent()
*/
handleDrop = (event, dragElement) => {
// THIS function is depreciated in favour of handleDropEvent()
console.log("dragging event: ", event, dragElement)
const container = dragElement.getAttribute("data-container")
// TODO: check if the drop is allowed
if (container === "canvas"){
this.props.onAddChildWidget(this.__id, dragElement.getAttribute("data-widget-id"))
}else if (container === "sidebar"){
this.props.onAddChildWidget(this.__id, null, true) // if dragged from the sidebar create the widget first
}
}
handleDragStart = (e, callback) => {
e.stopPropagation()
this.setState({isDragging: true})
callback(this.elementRef?.current || null)
console.log("Drag start: ", this.elementRef)
// Create custom drag image with full opacity, this will ensure the image isn't taken from part of the canvas
const dragImage = this.elementRef?.current.cloneNode(true)
dragImage.style.opacity = '1' // Ensure full opacity
dragImage.style.position = 'absolute'
dragImage.style.top = '-9999px' // Move it out of view
document.body.appendChild(dragImage)
const rect = this.elementRef?.current.getBoundingClientRect()
const offsetX = e.clientX - rect.left
const offsetY = e.clientY - rect.top
// Set the custom drag image with correct offset to avoid snapping to the top-left corner
e.dataTransfer.setDragImage(dragImage, offsetX, offsetY)
// Remove the custom drag image after some time to avoid leaving it in the DOM
setTimeout(() => {
document.body.removeChild(dragImage)
}, 0)
}
handleDragEnter = (e, draggedElement, setOverElement) => {
const dragEleType = draggedElement.getAttribute("data-draggable-type")
console.log("Drag entering...", dragEleType, draggedElement, this.droppableTags)
// FIXME: the outer widget shouldn't be swallowed by inner widget
if (draggedElement === this.elementRef.current){
// prevent drop on itself, since the widget is invisible when dragging, if dropped on itself, it may consume itself
return
}
setOverElement(e.currentTarget) // provide context to the provider
let showDrop = {
allow: true,
show: true
}
if (this.droppableTags.length === 0 || this.droppableTags.includes(dragEleType)) {
showDrop = {
allow: true,
show: true
}
} else {
showDrop = {
allow: false,
show: true
}
}
this.setState({
showDroppableStyle: showDrop
})
}
handleDragOver = (e, draggedElement) => {
if (draggedElement === this.elementRef.current){
// prevent drop on itself, since the widget is invisible when dragging, if dropped on itself, it may consume itself
return
}
// console.log("Drag over: ", e.dataTransfer.getData("text/plain"), e.dataTransfer)
const dragEleType = draggedElement.getAttribute("data-draggable-type")
if (this.droppableTags.length === 0 || this.droppableTags.includes(dragEleType)) {
e.preventDefault() // NOTE: this is necessary to allow drop to take place
}
}
handleDropEvent = (e, draggedElement) => {
e.preventDefault()
e.stopPropagation()
// FIXME: sometimes the elements shoDroppableStyle is not gone, when dropping on the same widget
this.setState({
showDroppableStyle: {
allow: false,
show: false
}
}, () => {
console.log("droppable cleared: ", this.elementRef.current, this.state.showDroppableStyle)
})
const dragEleType = draggedElement.getAttribute("data-draggable-type")
if (this.droppableTags.length > 0 && !this.droppableTags.includes(dragEleType)) {
return // prevent drop if the draggable element doesn't match
}
if (draggedElement === this.elementRef.current){
// prevent drop on itself, since the widget is invisible when dragging, if dropped on itself, it may consume itself
return
}
let currentElement = e.currentTarget
while (currentElement) {
if (currentElement === draggedElement) {
console.log("Dropped into a descendant element, ignoring drop")
return // Exit early to prevent the drop
}
currentElement = currentElement.parentElement // Traverse up to check ancestors
}
const container = draggedElement.getAttribute("data-container")
const thisContainer = this.elementRef.current.getAttribute("data-container")
// console.log("Dropped as swappable: ", e.target, this.swappableAreaRef.current.contains(e.target))
// If swaparea is true, then it swaps instead of adding it as a child, also make sure that the parent widget(this widget) is on the widget and not on the canvas
const swapArea = (this.swappableAreaRef.current.contains(e.target) && !this.innerAreaRef.current.contains(e.target) && thisContainer === WidgetContainer.WIDGET)
// TODO: check if the drop is allowed
if ([WidgetContainer.CANVAS, WidgetContainer.WIDGET].includes(container)){
// console.log("Dropped on meee: ", swapArea, this.swappableAreaRef.current.contains(e.target), thisContainer)
this.props.onAddChildWidget({parentWidgetId: this.__id,
dragElementID: draggedElement.getAttribute("data-widget-id"),
swap: swapArea || false
})
}else if (container === WidgetContainer.SIDEBAR){
// console.log("Dropped on Sidebar: ", this.__id)
this.props.onAddChildWidget({parentWidgetId: this.__id, create: true}) // if dragged from the sidebar create the widget first
}
}
handleDragLeave = (e, draggedElement) => {
// console.log("Left: ", e.currentTarget, e.relatedTarget, draggedElement)
if (!e.currentTarget.contains(draggedElement)) {
this.setState({
showDroppableStyle: {
allow: false,
show: false
}
})
}
}
handleDragEnd = (callback) => {
callback()
this.setState({isDragging: false})
}
// FIXME: children outside the bounding box, add tw-overflow-hidden
renderContent() {
// throw new NotImplementedError("render method has to be implemented")
return (
@@ -616,125 +787,165 @@ class Widget extends React.Component {
left: `${this.state.pos.x}px`,
width: `${this.state.size.width}px`,
height: `${this.state.size.height}px`,
opacity: this.state.isDragging ? 0.3 : 1
}
// console.log("selected: ", this.state.dragEnabled)
return (
<WidgetDraggable widgetRef={this.elementRef}
enableDrag={this.state.dragEnabled}
onDrop={this.handleDrop}
onDragEnter={({dragElement, showDrop}) => {
this.setState({
showDroppableStyle: showDrop
})
}
}
onDragLeave={ () => {
this.setState({
showDroppableStyle: {
allow: false,
show: false
}
})
}
}
>
<div data-widget-id={this.__id}
ref={this.elementRef}
className="tw-absolute tw-shadow-xl tw-w-fit tw-h-fit"
style={outerStyle}
data-draggable-type={this.getWidgetType()} // helps with droppable
data-container={this.state.widgetContainer} // indicates how the canvas should handle dragging, one is sidebar other is canvas
>
<DragContext.Consumer>
<div className="tw-relative tw-w-full tw-h-full tw-top-0 tw-left-0"
>
{this.renderContent()}
{
({draggedElement, onDragStart, onDragEnd, setOverElement}) => (
{
// show drop style on drag hover
this.state.showDroppableStyle.show &&
<div className={`${this.state.showDroppableStyle.allow ? "tw-border-blue-600" : "tw-border-red-600"}
tw-absolute tw-top-[-5px] tw-left-[-5px] tw-w-full tw-h-full tw-z-[2]
tw-border-2 tw-border-dashed tw-rounded-lg tw-pointer-events-none
<div data-widget-id={this.__id}
ref={this.elementRef}
className="tw-shadow-xl tw-w-fit tw-h-fit"
style={outerStyle}
data-draggable-type={this.getWidgetType()} // helps with droppable
data-container={this.state.widgetContainer} // indicates how the canvas should handle dragging, one is sidebar other is canvas
draggable={this.state.dragEnabled}
onDragOver={(e) => this.handleDragOver(e, draggedElement)}
onDrop={(e) => this.handleDropEvent(e, draggedElement)}
`}
style={
{
width: "calc(100% + 10px)",
height: "calc(100% + 10px)",
}
}
>
onDragEnter={(e) => this.handleDragEnter(e, draggedElement, setOverElement)}
onDragLeave={(e) => this.handleDragLeave(e, draggedElement)}
onDragStart={(e) => this.handleDragStart(e, onDragStart)}
onDragEnd={(e) => this.handleDragEnd(onDragEnd)}
>
{/* FIXME: Swappable when the parent layout is flex/grid and gap is more, this trick won't work, add bg color to check */}
{/* FIXME: Swappable, when the parent layout is gap is 0, it doesn't work well */}
<div className="tw-relative tw-w-full tw-h-full tw-top-0 tw-left-0"
>
<div className={`tw-absolute tw-top-[-5px] tw-left-[-5px]
tw-border-1 tw-opacity-0 tw-border-solid tw-border-black
tw-w-full tw-h-full
tw-scale-[1.1] tw-opacity-1 tw-z-[-1] `}
style={{
width: "calc(100% + 10px)",
height: "calc(100% + 10px)",
}}
ref={this.swappableAreaRef}
>
{/* helps with swappable: if the mouse is in this area while hovering/dropping, then swap */}
</div>
<div className="tw-relative tw-w-full tw-h-full" ref={this.innerAreaRef}>
{this.renderContent()}
</div>
}
<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'}`}>
{
// show drop style on drag hover
this.state.showDroppableStyle.show &&
<div className={`${this.state.showDroppableStyle.allow ? "tw-border-blue-600" : "tw-border-red-600"}
tw-absolute tw-top-[-5px] tw-left-[-5px] tw-w-full tw-h-full tw-z-[2]
tw-border-2 tw-border-dashed tw-rounded-lg tw-pointer-events-none
<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"
/>
`}
style={{
width: "calc(100% + 10px)",
height: "calc(100% + 10px)",
}}
>
</div>
}
<div className={`tw-absolute tw-bg-transparent tw-top-[-10px] tw-left-[-10px] tw-opacity-100
tw-w-full tw-h-full
${this.state.selected ? 'tw-border-2 tw-border-solid tw-border-blue-500' : 'tw-hidden'}`}
style={{
width: "calc(100% + 20px)",
height: "calc(100% + 20px)",
}}
>
<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) => {
e.stopPropagation()
e.preventDefault()
this.props.onWidgetResizing("nw")
this.setState({dragEnabled: false})
}}
onMouseUp={() => this.setState({dragEnabled: true})}
/>
<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) => {
e.stopPropagation()
e.preventDefault()
this.props.onWidgetResizing("ne")
this.setState({dragEnabled: false})
}}
onMouseUp={() => this.setState({dragEnabled: true})}
/>
<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) => {
e.stopPropagation()
e.preventDefault()
this.props.onWidgetResizing("sw")
this.setState({dragEnabled: false})
}}
onMouseUp={() => this.setState({dragEnabled: true})}
/>
<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) => {
e.stopPropagation()
e.preventDefault()
this.props.onWidgetResizing("se")
this.setState({dragEnabled: false})
}}
onMouseUp={() => this.setState({dragEnabled: true})}
/>
</div>
</div>
<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) => {
e.stopPropagation()
e.preventDefault()
this.props.onWidgetResizing("nw")
this.setState({dragEnabled: false})
}}
onMouseUp={() => this.setState({dragEnabled: true})}
/>
<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) => {
e.stopPropagation()
e.preventDefault()
this.props.onWidgetResizing("ne")
this.setState({dragEnabled: false})
}}
onMouseUp={() => this.setState({dragEnabled: true})}
/>
<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) => {
e.stopPropagation()
e.preventDefault()
this.props.onWidgetResizing("sw")
this.setState({dragEnabled: false})
}}
onMouseUp={() => this.setState({dragEnabled: true})}
/>
<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) => {
e.stopPropagation()
e.preventDefault()
this.props.onWidgetResizing("se")
this.setState({dragEnabled: false})
}}
onMouseUp={() => this.setState({dragEnabled: true})}
/>
</div>
</div>
)
}
</DragContext.Consumer>
// <WidgetDraggable widgetRef={this.elementRef}
// enableDrag={this.state.dragEnabled}
// onDrop={this.handleDrop}
// onDragEnter={({dragElement, showDrop}) => {
// this.setState({
// showDroppableStyle: showDrop
// })
// }
// }
// onDragLeave={ () => {
// this.setState({
// showDroppableStyle: {
// allow: false,
// show: false
// }
// })
// }
// }
</div>
</div>
</WidgetDraggable>
// >
// </WidgetDraggable>
)
}

View File

@@ -13,7 +13,7 @@ import { useDragContext } from "../../components/draggable/draggableContext"
*
*/
const WidgetDraggable = memo(({ widgetRef, enableDrag=true, dragElementType="widget",
onDragEnter, onDragLeave, onDrop,
onDragEnter, onDragLeave, onDrop, style={},
droppableTags = ["widget"], ...props }) => {
// FIXME: It's not possible to move the widget ~10px because, its considered as self drop, so fix it
@@ -112,11 +112,20 @@ const WidgetDraggable = memo(({ widgetRef, enableDrag=true, dragElementType="wid
e.stopPropagation()
console.log("Dropped: ", draggedElement, props.children)
if (draggedElement === widgetRef.current){
// prevent drop on itself, since the widget is invisible when dragging, if dropped on itself, it may consume itself
return
setShowDroppable({
allow: false,
show: false
})
if (onDrop) {
onDrop(e, draggedElement)
}
// if (draggedElement === widgetRef.current){
// // prevent drop on itself, since the widget is invisible when dragging, if dropped on itself, it may consume itself
// return
// }
let currentElement = e.currentTarget
while (currentElement) {
if (currentElement === draggedElement) {
@@ -126,14 +135,6 @@ const WidgetDraggable = memo(({ widgetRef, enableDrag=true, dragElementType="wid
currentElement = currentElement.parentElement // Traverse up to check ancestors
}
setShowDroppable({
allow: false,
show: false
})
if (onDrop) {
onDrop(e, draggedElement)
}
}
@@ -155,8 +156,9 @@ const WidgetDraggable = memo(({ widgetRef, enableDrag=true, dragElementType="wid
setIsDragging(false)
}
// TODO: FIXME, currently the draggable div doesn't move with the child, instead only child div moves, simulating childrens movement, add color and check
return (
<div className={`${props.className || ""} tw-w-fit tw-h-fit tw-bg-blue`}
<div className={`${props.className || ""}`}
onDragOver={handleDragOver}
onDrop={handleDropEvent}
onDragEnter={handleDragEnter}
@@ -164,10 +166,9 @@ const WidgetDraggable = memo(({ widgetRef, enableDrag=true, dragElementType="wid
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
draggable={enableDrag}
style={{ opacity: isDragging ? 0.3 : 1}} // hide the initial position when dragging
style={{ opacity: isDragging ? 0.3 : 1, ...style}} // hide the initial position when dragging
>
{props.children}
</div>
)

View File

@@ -1,6 +1,6 @@
import React, { createContext, useContext, useState } from 'react';
import React, { createContext, useContext, useState } from 'react'
const DragContext = createContext()
export const DragContext = createContext()
export const useDragContext = () => useContext(DragContext)

View File

@@ -71,7 +71,7 @@ function Premium({ children, className = "" }) {
</li>
<li className="tw-flex tw-place-items-center tw-gap-2">
<i className="bi bi-x-circle-fill tw-text-red-600 tw-text-base"></i>
<span>Access to UI builder exe for local development</span>
<span>Downloadable UI builder exe for local development</span>
</li>
<li className="tw-flex tw-place-items-center tw-gap-2">
<i className="bi bi-x-circle-fill tw-text-red-600 tw-text-base"></i>
@@ -132,7 +132,7 @@ function Premium({ children, className = "" }) {
</li>
<li className="tw-flex tw-place-items-center tw-gap-2">
<i className="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Access to UI builder exe for local development</span>
<span>Downloadable UI builder exe for local development</span>
</li>
<li className="tw-flex tw-place-items-center tw-gap-2">
<i className="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
@@ -207,7 +207,7 @@ function Premium({ children, className = "" }) {
</li>
<li className="tw-flex tw-place-items-center tw-gap-2">
<i className="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Access to UI builder exe for local development</span>
<span>Downloadable UI builder exe for local development</span>
</li>
<li className="tw-flex tw-place-items-center tw-gap-2">
<i className="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>