115 Commits

Author SHA1 Message Date
Paul
7e7851f6ad fixed small error 2025-09-26 10:33:14 +05:30
Paul
8f43c7518e Merge branch 'main' of https://github.com/PaulleDemon/PyUIBuilder 2025-09-26 10:29:26 +05:30
Paul
96a5874d6c updated docs 2025-09-26 10:28:29 +05:30
Art/Paul
b697b26302 Update README.md 2025-08-27 08:52:29 +05:30
Art/Paul
0a7f8ef8d1 Update README.md 2025-08-02 21:12:31 +05:30
Art/Paul
e10570be4d Update README.md 2025-07-16 17:31:24 +05:30
Art/Paul
f88036c74c Update README.md 2025-07-16 17:29:36 +05:30
Art/Paul
708d8f79f4 Update README.md 2025-07-09 09:42:28 +05:30
Art/Paul
ceae223c3a Update roadmap.md 2025-06-16 09:54:29 +05:30
Art/Paul
f167c42b9a Update README.md 2025-06-16 09:53:44 +05:30
paul
bd68ba8a6b updated readme 2025-05-07 15:28:51 +05:30
paul
4052de4ae2 fixed readme 2025-04-27 19:17:21 +05:30
Art/Paul
bcb7b1c910 Update README.md 2025-04-27 18:30:33 +05:30
paul
f028ddf17b improved readme 2025-04-27 17:23:46 +05:30
paul
25b7a0da31 added privacy link 2025-04-24 20:48:35 +05:30
paul
52d96af200 added privacy 2025-04-24 19:00:15 +05:30
paul
81e5593a3e updated readme 2025-04-16 21:13:53 +05:30
paul
a5334e2ee6 fixed landing page 2025-04-16 21:08:20 +05:30
paul
09ca7f1c0c updated youtube video 2025-04-16 20:55:12 +05:30
paul
efcdcba9cd updated landing page 2025-04-16 06:55:42 +05:30
paul
37369af369 updated readme 2025-04-10 17:39:07 +05:30
paul
aa95ec219d fixed tailwind 2025-04-07 14:44:28 +05:30
paul
5f3525d56b fix: customtk error and option menu 2025-04-05 19:20:58 +05:30
paul
3caa84b7fb added discord invite 2025-04-05 09:25:38 +05:30
paul
57a6eb2759 fixed wording 2025-04-05 08:56:09 +05:30
paul
ae5a9293d6 fixed title for tooltip 2025-04-05 08:47:58 +05:30
paul
fca99f09ea added tooltips 2025-04-05 07:32:14 +05:30
paul
a7949f6c8c docs 2025-04-05 06:48:44 +05:30
paul
cb71ac3ae4 updated docs 2025-04-04 22:36:54 +05:30
paul
1a948a21d2 corrected license 2025-04-04 21:21:23 +05:30
paul
6947ac0e5e fixed tailwind styling 2025-04-04 18:00:31 +05:30
paul
eff608b9f5 updated tailwind build 2025-04-04 17:54:14 +05:30
paul
9efcd8a9c6 add tutorials and discord invite 2025-04-04 07:32:59 +05:30
paul
6af1d1f6e5 asset upload 2025-04-03 23:03:30 +05:30
paul
07f1320f6d added details 2025-04-03 21:09:21 +05:30
paul
5211faf140 working on landing page 2025-04-03 19:13:51 +05:30
paul
1f709a10be added contributing and updated readme 2025-04-03 09:13:30 +05:30
paul
382675c1ba fixed entry widget 2025-04-02 18:00:10 +05:30
paul
495083b416 fixed build command 2025-04-02 11:44:23 +05:30
paul
9dbfdacd9f fixed build step 2025-04-02 11:41:36 +05:30
paul
95321ced95 fixed peer conflicts 2025-04-02 11:35:15 +05:30
paul
d03c3de36b added override for picomatch 2025-04-02 11:15:30 +05:30
paul
fd78229570 fixed pickomatch 2025-04-02 06:01:16 +05:30
paul
e06a105a68 fixed package.json 2025-04-02 05:55:55 +05:30
paul
a82960ca15 added node version 2025-04-02 05:22:22 +05:30
paul
20d21c210e fixed webpack 2025-04-01 22:14:02 +05:30
paul
f1eddf1821 fixed build 2025-04-01 21:15:37 +05:30
paul
fff10097fd updated after time 2025-04-01 20:47:25 +05:30
paul
d10928bb5a fixed padx/y and ipadx/y 2025-04-01 20:42:14 +05:30
paul
0243b3b9e8 added license comment 2025-04-01 19:00:20 +05:30
paul
66d7dfc45f added webpack and fixed ImageLabel 2025-04-01 18:50:14 +05:30
paul
a38cd90c16 working on label image resize fix 2025-03-31 20:38:25 +05:30
paul
bdd3bab3a5 corrected grid weight and grid config position in toolbar 2025-03-30 05:46:03 +05:30
paul
53aaa8a670 fix: fixed grid row col on load 2025-03-30 05:23:18 +05:30
paul
64631caaaa fix: fixed setState warning 2025-03-29 21:03:02 +05:30
paul
8e1f042350 fix: fixed parentLayout taking place on changing layout 2025-03-29 21:00:45 +05:30
paul
128a7c49b9 updated readme 2025-03-28 18:48:41 +05:30
paul
e0fa421459 Merge branch 'customtk-fixes' 2025-03-28 17:14:51 +05:30
paul
0438480620 fixes: corrected tk to ctk in customtk 2025-03-28 17:14:03 +05:30
Art/Paul
d64f87847c Merge pull request #12 from PaulleDemon/customtk-fixes
fixing customtk layout
2025-03-28 17:13:21 +05:30
paul
c6fd4a8275 fixing customtk layout 2025-03-28 15:56:17 +05:30
paul
bfeb1c55b9 added discord invite 2025-03-28 11:17:48 +05:30
paul
9949fb8335 updated manifest 2025-03-27 18:43:56 +05:30
paul
86f84ef998 updated premium message 2025-03-27 17:18:30 +05:30
paul
fc68d407b7 added discord invite 2025-03-27 15:31:27 +05:30
paul
16e0b5862e fixe: the grid layout manager is now hidden when its pack 2025-03-27 07:15:17 +05:30
paul
d36fca5cc6 force renderTkinterLayout error if its not defined for elements with layout 2025-03-26 20:57:45 +05:30
paul
7f379c0ae6 fixed width and height problem with frame 2025-03-26 19:12:11 +05:30
paul
45847df63d fixed small bug related to expandValue for flexGrow / expand 2025-03-26 15:23:08 +05:30
paul
c177a16b33 fixed anchor and side for flex layout 2025-03-26 15:08:25 +05:30
paul
92a967721a added anchor code 2025-03-25 21:01:39 +05:30
paul
1b8ab3938d added label anchor 2025-03-25 20:53:38 +05:30
paul
dcc90dd954 added main window and toplevel logo 2025-03-25 19:11:37 +05:30
paul
565b69db9f fixed flex side issue 2025-03-25 15:34:18 +05:30
paul
5004dd140e fixed uncontrolled input radio list 2025-03-25 05:24:37 +05:30
paul
ede5f276cf fixed sticky problem for grid 2025-03-24 21:04:49 +05:30
paul
c7d30dc392 fixed tkinter output code for grid layout 2025-03-24 19:21:50 +05:30
paul
8b386e3263 worked on grid configure weights 2025-03-24 15:50:46 +05:30
paul
1912e53672 working on grid 2025-03-23 19:02:54 +05:30
paul
b9139469d4 fix: fixed input cursor problem 2025-03-22 21:16:13 +05:30
paul
c2b0532e96 fix: grid view count 2025-03-22 11:11:19 +05:30
paul
22718369e1 fixed resizing handles issue 2025-03-21 09:10:48 +05:30
paul
4403c27c76 fixed tkinter pack layout 2025-03-20 11:58:22 +05:30
paul
14866c8aaf worked on tkinter pack layout 2025-03-20 11:25:07 +05:30
paul
4a85f86c95 working on anchor 2025-03-18 21:29:41 +05:30
paul
a4365861c9 fix: fixed issue with pack manager 2025-03-18 18:08:25 +05:30
paul
81ddfc58c5 fix: added initialPosition to createWidget 2025-03-16 15:43:41 +05:30
paul
5f2d80755e fix: fixed uncontrolled state updates 2025-03-16 10:01:35 +05:30
paul
ee57b43d0e centered widget on open 2025-03-14 16:42:27 +05:30
paul
dc892e6f61 added demo video 2025-03-14 11:21:08 +05:30
paul
9fad1bc5a1 redid the waitlist 2025-03-11 20:18:52 +05:30
paul
ddb1bcae97 added waitlist 2025-03-11 19:12:45 +05:30
paul
e8ab297af0 updated readme 2025-03-11 14:54:31 +05:30
paul
94850c99b1 updated styling 2025-03-11 14:14:52 +05:30
Art/Paul
6d6ce019f1 Merge pull request #10 from PaulleDemon/styling-fixes
styling updates
2025-03-11 14:06:07 +05:30
paul
b3ab3b1193 styling updates 2025-03-11 14:00:09 +05:30
paul
81c3056a10 fixed display name in treeview 2025-03-11 07:23:17 +05:30
paul
80c384a57a Fixed small bugs with resizable rendering 2025-03-11 06:41:52 +05:30
paul
769615c2cc fixed sidebar drag and drop issue 2025-03-10 19:10:36 +05:30
paul
cce577add3 added panToWidget on Double click tree view 2025-03-10 11:58:27 +05:30
paul
16bfd244e6 fixed code engine bugs 2025-03-10 10:37:57 +05:30
paul
d1ef2fa3f8 fixed drag and drop issue 2025-03-09 17:48:23 +05:30
paul
b1abb4651d fixed plugin request yml 2025-03-09 12:24:21 +05:30
paul
da25b7692e added plugin request template 2025-03-09 12:15:45 +05:30
paul
0a2a8601c1 added tree view 2025-03-09 11:07:08 +05:30
paul
0f7d9944a8 working on sidebar and widget context 2025-03-08 20:11:22 +05:30
paul
aa54f9b0bb fixed swapping 2025-03-08 12:02:00 +05:30
paul
6aff8d37e4 removed console statements 2025-03-07 18:29:07 +05:30
paul
217dedc121 fixed drag and drop problem when within parent and dragImage problem 2025-03-07 17:09:35 +05:30
paul
71c2bfd949 fixed resize handles clipping problem 2025-03-07 05:26:36 +05:30
paul
76dd829a78 added a small message 2025-03-06 21:41:54 +05:30
paul
9439eface6 working on resize handler fix 2025-03-06 21:38:23 +05:30
paul
2898bdd817 fixing the resizable div 2025-03-06 19:20:40 +05:30
paul
f3cd37b1f9 fixed drag position 2025-03-06 14:32:00 +05:30
paul
ed333d6ee6 fixed Relief string 2025-01-01 16:43:25 +05:30
103 changed files with 15352 additions and 7136 deletions

View File

@@ -0,0 +1,27 @@
name: Plugin Request
description: Request support for a third-party library
title: "[Plugin Request]: "
labels: ["plugin"]
assignees:
- PaulleDemon
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to request a new plugin.
- type: input
id: plugin
attributes:
label: Name of the plugin
placeholder: Enter the plugin name
validations:
required: true
- type: input
id: plugin-link
attributes:
label: Link to the plugin
placeholder: Enter the plugin link
validations:
required: true

1
.gitignore vendored
View File

@@ -7,6 +7,7 @@
.env-cmdrc.json .env-cmdrc.json
build build
dist
python-tests/ python-tests/

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
20

14
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,14 @@
## Contributing
Currently if you find bugs or typos, Please create an issue first, instead of a Pull request.
That is the best way to contribute to this repo in it's current state.
**The repo will be going through drastic changes including migrations to TS.**
So any changes you bring may have to be rewritten. Plus, I may not get time to review every pull request, unless you are bringing in significant improvement that would have probably taken me weeks to do it myself, I may not be able to take time out of development to review it, and don't take closing pull request personally. It's just that I currently don't have time.
Thank you for understanding.
## Other Best ways to contribute.
1. Creating tutorials and helping others on [discord server](https://discord.gg/dHXjrrCA7G)
2. Sharing the tool with others.

View File

@@ -5,11 +5,10 @@ Below is the License for source code of PyUIBuilder. For license regarding sourc
----------------------- -----------------------
The source code for PyUIBuilder is Dual licensed, most parts of the code comes under AGPL, The source code for PyUIBuilder is Dual licensed, some parts of the code comes under AGPL,
some parts under a different non-commercial use license. Read the second part after the definition of the AGPL license. some parts under a different non-commercial use license. Read the second part after the definition of the AGPL license.
Sub-folders with non-commercial use license will contain the second license as well. Sub-folders with non-commercial use license will contain the second license as well.
- Only the code inside src/canvas/ is kept under a non-commercial use license.
-------------------------- --------------------------
@@ -678,17 +677,11 @@ For more information on this, and how to apply and follow the GNU AGPL, see
-------------------------- --------------------------
Below is the license for code under src/canvas/
License for non-commercial use in short License for non-commercial use in short
* You are free fork, modify and redistribute it given the original or derived works is kept free and open-source and under the same licese. * You are free fork, modify and redistribute it given the original or derived works is kept free and source-available and under the same licese.
* Another restriction is you can't redistribute in binary or executable formats without asking me first. * Another restriction is you can't redistribute in binary or executable formats without asking me first.
* If you plan on making money on this work or derived work you need permission first. * If you plan on making money on this work or derived work you need permission first.
License was planned to be kept under one AGPL or less restrictive license, but since its difficult for me to continue providing free and open-source software without proper funding. It's under this license.
Once there enough money to fund this project and my upcoming FOSS projects, The license will be brought back to a single AGPL or much lesser restrictive license. The fastest way to make this happen is by purchasing a one-time license or fund me.
Read the full license below. Read the full license below.
-------------------- --------------------
@@ -696,7 +689,7 @@ Read the full license below.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to use, copy, modify, merge, publish, and distribute the source code, subject to the following conditions: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to use, copy, modify, merge, publish, and distribute the source code, subject to the following conditions:
2. Derived Work License 2. Derived Work License
Any modifications, derivative works, or copies of the Software must retain this license. All derivative works must be open-sourced under the same license terms. You are not permitted to relicense or sub-license any portion of the Software under a different license without explicit prior written permission from the original author. Any modifications, derivative works, or copies of the Software must retain this license. All derivative works must be source-available and under the same license terms. You are not permitted to relicense or sub-license any portion of the Software under a different license without explicit prior written permission from the original author.
3. Distribution Restriction on Executables 3. Distribution Restriction on Executables
You are not permitted to distribute the Software in compiled, executable, or binary form without prior written consent from the original author. This applies to both original and derivative works. You are not permitted to distribute the Software in compiled, executable, or binary form without prior written consent from the original author. This applies to both original and derivative works.

101
README.md
View File

@@ -1,34 +1,67 @@
# PyUIBuilder - Build python GUIs like Webflow
<sub>Not affiliated to Webflow</sub> <p align="center"><img src="./repo-assets/logo/PyUi.png" alt="font tester logo" width="100" height="100"></p>
<p align="center"> <p align="center">
<h1> PyUIBuilder - Python GUI Builder
</h1>
</p>
Build python GUIs like Canva
<!-- <p align="center">
<a href="https://twitter.com/share?url=https://github.com/PaulleDemon/PyUIBuilder&text=Check out PyUIBuilder tool"> <a href="https://twitter.com/share?url=https://github.com/PaulleDemon/PyUIBuilder&text=Check out PyUIBuilder tool">
<img src="./repo-assets/share/1.png" height="35" /> <img src="./repo-assets/share/1.png" height="35" />
</a> </a>
<a href="https://www.reddit.com/submit?url=https://github.com/PaulleDemon/PyUIBuilder&title=Check out PyUIBuilder tool"> <a href="https://www.reddit.com/submit?url=https://github.com/PaulleDemon/PyUIBuilder&title=Check out PyUIBuilder tool">
<img src="./repo-assets/share/4.png" height="35" /> <img src="./repo-assets/share/4.png" height="35" />
</a> </a>
<a href="https://www.linkedin.com/shareArticle?mini=true&url=https://github.com/PaulleDemon/PyUIBuilder&title=check out PyUIBuilder tool"> <a href="https://www.linkedin.com/shareArticle?mini=true&url=https://github.com/**PaulleDemon**/PyUIBuilder&title=check out PyUIBuilder tool">
<img src="./repo-assets/share/2.png" height="35" /> <img src="./repo-assets/share/2.png" height="35" />
</a> </a>
<a href="https://youtube.com/"> <a href="https://youtube.com/">
Build Python GUI's with ease of Drag and drop builders.
<img src="./repo-assets/share/3.png" height="35" /> <img src="./repo-assets/share/3.png" height="35" />
</a> </a>
</p> </p> -->
<p><img src="./repo-assets/logo/PyUi.png" alt="font tester logo" width="100" height="100"></p>
Build Python GUI's with ease of Drag and drop builders.
https://github.com/user-attachments/assets/ac91aa98-843d-4578-b646-88e66bc113de <a href="https://www.producthunt.com/posts/pyui-builder?embed=true&utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-pyui&#0045;builder" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=949514&theme=light&period=daily&t=1744818177482" alt="PyUI&#0032;Builder - Build&#0032;Python&#0032;GUIs&#0032;like&#0032;Canva | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
<sub>**Don't like background music? fell free to mute it**</sub>
https://github.com/user-attachments/assets/57addb9c-d018-4be9-894d-c7cbce5ff55f
<!-- https://github.com/user-attachments/assets/ac91aa98-843d-4578-b646-88e66bc113de -->
<sub>**Don't like background music? feel free to mute it**</sub>
## Try PyUIBuilder ## Try PyUIBuilder
Try [PyUIBuilder](https://pyuibuilder.pages.dev/) **Try it** [PyUIBuilder](https://pyuibuilder.com)
**About** [About PyUiBuilder](https://about.pyuibuilder.com)
[PyuiBuilder - Python GUI Builder](https://pyuibuilder.com/python-gui-builder)
**Roadmap:** You can check whats upcoming on [Roadmap](https://scrawny-droplet-e97.notion.site/PyUIBuilder-Roadmap-21e45dc1685b8092a47ed4c9f333233c)
**What's new:** You can check what's new [here](https://scrawny-droplet-e97.notion.site/Pyuibuilder-What-s-new-25845dc1685b80a2a802e6f3943085f1)
**Funding development:**
<br>
I am Paul, a solo-developer, which means I work on this project all by myself from design to coding to maintaing the project.
<br/>
<br/>
so, if you'd like to support the development, you can [purchase a license](https://about.pyuibuilder.com/#pricing) with a one time payment, for a limited time, which will give you instant access to all the premium features
for life. Thank you for your support! appreciate it very much
<br/>
<br/>
Best,
<br/>
Paul
**Note:** premium features are being added and maintained in private
## Table of contents ## Table of contents
@@ -45,13 +78,18 @@ Try [PyUIBuilder](https://pyuibuilder.pages.dev/)
- [Web based Editor](#webbased-editor) - [Web based Editor](#webbased-editor)
- [Electron App - Hobbyist License](#electron-app---hobbyist-license) - [Electron App - Hobbyist License](#electron-app---hobbyist-license)
- [Electron App - Commercial License](#electron-app---commercial-license) - [Electron App - Commercial License](#electron-app---commercial-license)
- [Contributing](#contributing)
- [Some of my other open-source](#some-of-my-other-open-source) - [Some of my other open-source](#some-of-my-other-open-source)
- [Author](#author) - [Author](#author)
## Resources
1. Youtube - [PyUibuilder playlist](https://youtube.com/playlist?list=PL0VamwghCfX-KXtGKGLak-C_-Jcx_eOiK&si=vnVr8vdU_JkIEL2f)
2. [Creating sign up form Blog](https://medium.com/python-in-plain-english/create-tkinter-guis-using-tkinter-gui-builder-pyuibuilder-a7422489c55e)
3. [Discord Invite](https://discord.gg/dHXjrrCA7G)
## Docs - Getting started ## Docs - Getting started
Read the docs on the [Docs page](https://pyuibuilder-docs.pages.dev/) Read the docs on the [Docs page](https://docs.pyuibuilder.com/)
## Example app ## Example app
@@ -107,7 +145,7 @@ While there are a lot of features, here are few you need to know.
* Framework agnostic - Can outputs code in multiple frameworks. * Framework agnostic - Can outputs code in multiple frameworks.
* Pre-built UI widgets * Pre-built UI widgets
* Plugins to extend 3rd party UI libraries * Plugins to extend 3rd party UI libraries
* Supports layout managers, such as flex, grid and absolute positioning [read docs](https://pyuibuilder-docs.pages.dev/) * Supports layout managers, such as flex, grid and absolute positioning [read docs](https://docs.pyuibuilder.com/)
* Generates python Code. * Generates python Code.
* Support to upload local assets. * Support to upload local assets.
* Generates requirements.txt file when needed * Generates requirements.txt file when needed
@@ -121,21 +159,19 @@ While there are a lot of features, here are few you need to know.
## Roadmap ## Roadmap
Here are some of the upcoming features. Here are some of the upcoming features.
* Treeview on the sidebar
* Support for Event Handlers * Support for Event Handlers
* Kivy Framework support
* Pyqt/PySide Support * Pyqt/PySide Support
* **Downloadable Electron app** and more. * **Downloadable Electron app** and more.
To learn more/ see upcoming features visit [roadmap](./roadmap.md) To learn more/ see upcoming features visit [roadmap](./roadmap.md)
To stay in loop, subscribe to the free [newsletter](https://paulfreeman.substack.com/subscribe?utm_source=Github-Pybuilder) To stay in loop, subscribe to the free [newsletter](https://tally.so/r/mVDY7N)
## License - Fund the development ## License - Fund the development
Help fund open-source work and development of this and upcoming projects by purchasing a one-time license. Help fund development of this and upcoming projects by purchasing a one-time license.
Purchasing License will allow me to focus on development of this tool and provide you access to more advance features, early access and more. Purchasing License will give you discounted price and provide you access to more advance features, early access and more.
The discount's will be available for limited time only on pre-orders. The discount's will be available for limited time only on pre-orders.
@@ -146,15 +182,16 @@ The discount's will be available for limited time only on pre-orders.
| **Lifetime license** (one-time purchase) | 👍️ | ✅ | ✅ | | **Lifetime license** (one-time purchase) | 👍️ | ✅ | ✅ |
| **Early access** to upcoming features | ❌ | ✅ | ✅ | | **Early access** to upcoming features | ❌ | ✅ | ✅ |
| **Downloadable Electron App** (upcoming) | ❌ | ✅ | ✅ | | **Downloadable Electron App** (upcoming) | ❌ | ✅ | ✅ |
| **Premium widgets**(tabbed widget, scroll widget etc) | ❌ | ✅ | ✅ |
| **Run Preview live**(upcoming) | ❌ | ✅ | ✅ | | **Run Preview live**(upcoming) | ❌ | ✅ | ✅ |
| **Save and Load UI files** (upcoming) | ❌ | ✅ | ✅ | | **Save and Load UI files** | ❌ | ✅ | ✅ |
| **Load 3rd party plugins locally** | ❌ | ✅ | ✅ | | **Load 3rd party plugins locally** | ❌ | ✅ | ✅ |
| **Dark theme** (upcoming) | ❌ | ✅ | ✅ | | **Dark theme** | ❌ | ✅ | ✅ |
| **Commercial Use** | | ❌ | ✅ | | **Commercial Use** | | ❌ | ✅ |
| **Support for PyQt/PySide frameworks** (upcoming) | ❌ | ❌ | ✅ | | **Support for PyQt/PySide frameworks** (upcoming) | ❌ | ❌ | ✅ |
| **More upcoming features and support** | ❓️ | ✅ | ✅ | | **More upcoming features and support** | ❓️ | ✅ | ✅ |
| **Price** | - | ~~$129~~ **$29** (save 77.52% for limited time on pre-order) | ~~180~~ **$49** (Save 72.78% for a limited time on pre-orders) | | **Price** | - | ~~$129~~ **$29** (save 77.52% for limited time on pre-order) | ~~180~~ **$49** (Save 72.78% for a limited time on pre-orders) |
| Pre-order now! | | [Get license](https://ko-fi.com/s/4a3dffb3b9) | [Get license](https://ko-fi.com/s/560a3b6b05) | | order now! | | [Get license](https://about.pyuibuilder.com) | [Get license](https://about.pyuibuilder.com) |
## Newsletter ## Newsletter
@@ -218,6 +255,24 @@ This is meant for students and hobbyist
This is meant for business usecases, you can use the code even for commercial use. This is meant for business usecases, you can use the code even for commercial use.
* All code generated by the builder tools are free to use for commercial and non-commercial purposes. If you are using this for a startup or your business you'll need to get a commercial license. * All code generated by the builder tools are free to use for commercial and non-commercial purposes. If you are using this for a startup or your business you'll need to get a commercial license.
## Contributing
To run it locally you'll need to add .env-cmdrc.json with the following in it. This file should be in the root directory
```json
{
"development":{
"REACT_APP_ANALYTICS_SCRIPT_ID": ""
},
"production":{
"REACT_APP_ANALYTICS_SCRIPT_ID": ""
}
}
```
For contributing read [here](./CONTRIBUTING.md)
## Some of my other open-source ## Some of my other open-source

View File

@@ -2,11 +2,7 @@
# PyUIBuilder Docs # PyUIBuilder Docs
> A Free Python GUI builder tool, like Webflow, but for python GUIs > A Powerful Python GUI builder tool, Build your Python GUis like Canva
- Simple and powerful - Simple and powerful
- Do more with plugins - Do more with plugins
- Works for multiple frameworks - Works for multiple frameworks
[GitHub](https://github.com/PaulleDemon/PyUIBuilder)
[Get Started](#pyuibuilder-documentation)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

BIN
docs/assets/basics.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

BIN
docs/assets/grids.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

@@ -2,10 +2,10 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Document</title> <title>PyUIBuilder - Documentation</title>
<link rel="icon" href="_media/favicon.ico"> <link rel="icon" href="_media/favicon.ico">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" /> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="description" content="Description"> <meta name="description" content="Documentation for PyUiBuilder">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
<!-- <link rel="stylesheet" href="//cdn.jsdelivr.net/gh/LIGMATV/docsify-theme-github/github.css"> --> <!-- <link rel="stylesheet" href="//cdn.jsdelivr.net/gh/LIGMATV/docsify-theme-github/github.css"> -->
<!-- <link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsify@4/lib/themes/vue.css"> --> <!-- <link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsify@4/lib/themes/vue.css"> -->

View File

@@ -5,14 +5,74 @@
This page is still under work in progress. This page will be updated from time to time. This contains the basic documentation for PYUIBuilder This page is still under work in progress. This page will be updated from time to time. This contains the basic documentation for PYUIBuilder
</div> </div>
## Resources
- [Getting started video](https://youtu.be/9dYv3VnchDA?si=kJoSXh0i-r1qm3AU)
- [Creating Calculator apps short/reel](https://youtube.com/shorts/1K2cM1gt13o?si=oxwNcoIpRU-8IkzS)
- [Youtube Tutorials Playlist](https://youtube.com/playlist?list=PL0VamwghCfX-KXtGKGLak-C_-Jcx_eOiK&si=1LagzyjBaifqDcND)
- [Create a signup form](https://medium.com/python-in-plain-english/create-tkinter-guis-using-tkinter-gui-builder-pyuibuilder-a7422489c55e)
- [Discord invite](https://discord.gg/dHXjrrCA7G)
- [basic pack Layout tutorial - Tkinter](https://www.youtube.com/watch?v=rbW1iJO1psk)
## FAQs
### Common faqs
**1. Why does the design I created in tkinter look a bit off from my actual output**
Tkinter is OS dependent framework, and Pyuibuilder provides you with a common interface for you to develop. In the future we may consider rendering the design based on the OS you are using.
For more accurate design to output ratio, you can try using customtk framework that's available in Pyuibuilder.
**2. Why do I see a small padding in design, but not in output tkinter**
The default padding in the design exists, so you can easily drag and drop your widgets. You can adjust the output padding from the toolbar. We may in the near future, make the outputs to have a default padding.
### Login and License FAQ
**I had purchased a paid plan, where's my license key?**
If you had purchased a paid plan, your account has already been upgraded to pro/hobby plan depending on the plan you had selected. You don't require license key, The license key is only required if you want to purchase license in bulk.
The oauth system is used instead of license key to make it convient to login to both the web version as well as desktop versions.
**I am on paid plan but the desktop app splash screen is asking for license key**
It's likely you had logged into a wrong account while logging into the desktop.
Logout by clicking the logout button on the bottom left below the active key button and relogin
![Logout desktop](./assets/logout%20desktop.png)
### Deskop app FAQs
**Should I have the Python pre-installed**
If you are on desktop and if the app detects your system doesn't have a python installed already, it will prompt you to install python so you can run the exported code.
The desktop apps comes pre-packaged with python in them, except for the Mac intel versions. The desktop app uses the python built-into the app, except for Mac intel versions.
**Does workspace automatically store my design?**
No, the workspace you create is used create a virtual environment for the app to run python.
## Profile
To check which plan you are on or to logout hover over the profile icon on the top right corner with a profile icon.
## UI Basics ## UI Basics
Let's start with the basics of UI Let's start with the basics of UI
![Layout basics](./assets/basics.jpg) ![Layout basics](./assets/basics.png)
1. The sidebar on the left will have multiple buttons, each button will provide you with necessary tools. 1. The sidebar on the left will have multiple tabs, each tabs will provide you with necessary tools.
2. The Place where you drag and drop widgets is the canvas 2. The Place where you drag and drop widgets is the canvas
3. The toolbar will only appear if a widget is selected. 3. The toolbar will only appear if a widget is selected.
4. The code editor is to help you write event handler codes.
@@ -23,14 +83,26 @@ Things you can do on canvas.
1. Add widgets from sidebar. 1. Add widgets from sidebar.
2. Zoom and pan using mouse. 2. Zoom and pan using mouse.
3. zoom using `+`/`-` keys 3. zoom using `+`/`-` keys
4. Delete widgets using `del` key or right clicking on the widget 4. Delete widgets using `del` key or right clicking on the widget (on mac function + delete)
5. You can use ctrl/cmd + D to duplicate the selected widget.
## Project name ## Project name
By default all project's are named untitled project, you can change this from the header input next to export code. By default all project's are named `"untitled project"`, you can change this from the header input next to export code.
## Selecting a UI library ## Selecting a UI library
You can select the UI library from the header dropdown. Once selected changing the UI library in between your work, will erase the canvas. You can select the UI library from the header dropdown. Once selected changing the UI library in between your work, will erase the canvas.
## Enabling grids and snapping
![Grids and snapping](./assets/grids.png)
You can open the grid controls from the top left near the delete icons
To enable grid snapping just click on the enable snap switch, you can adjust the grid size using the slider
You can also enable grids for parent widgets by switching the widget grid on.
## Widgets ## Widgets
Every widget has its own attributes, some of the attributes may be common. Every widget has its own attributes, some of the attributes may be common.
@@ -43,11 +115,10 @@ Every widget has its own attributes, some of the attributes may be common.
1. Flex(also known as pack) 1. Flex(also known as pack)
2. Grid 2. Grid
3. Absolute/Place 3. Absolute/Place (flex)
The parents of the child widgets controls the layout. The layout properties such as grid position will be available to the child under the grid-manager/flex-manager section. The parents of the child widgets controls the layout. The layout properties such as grid position will be available to the child under the grid-manager/flex-manager section.
All Widgets attributes are available on the toolbar. All Widgets attributes are available on the toolbar.
The toolbar contains collapsible, which can be opened to modify the widgets attributes such The toolbar contains collapsible, which can be opened to modify the widgets attributes such
as foreground / background colors, themes and more. as foreground / background colors, themes and more.
@@ -126,10 +197,10 @@ flex-manager / grid-manager.
### Flex ### Flex
Flex is similar to pack in tkinter, the widgets will be arranged horizontally/vertically Flex is similar to pack in tkinter, the widgets will be arranged horizontally/vertically
depending on the flex-direction depending on the side. You can use anchor to position within the side. [Understanding the working of pack](https://www.youtube.com/watch?v=rbW1iJO1psk)
### Grid ### Grid
Grid is a 2d layout manager, you can position each widget by clicking on widget -> toolbar -> grid-manager Grid is a 2d layout manager, you can position each widget by clicking on widget(the child) -> toolbar -> grid-manager. You'll also have to define the no of rows and cols using grid configure accord and Grid weight accord on the parent.
![grid-manager](./assets/grid-manager.png) ![grid-manager](./assets/grid-manager.png)
@@ -139,16 +210,6 @@ You can use position absolute for specific widget by checking the absolute posit
![Absolute positioning](./assets/absolute-position.png) ![Absolute positioning](./assets/absolute-position.png)
### Swapping widgets on flex layout
You can swap widget by bringing the widget near the edge of the other widget.
<div class="alert alert-warning">
<div class="alert-title">Warning ⚠️</div>
This feature is still work in progress and sometimes may not work as expected
</div>
![Swappy](./assets/swappy.gif)
## Plugins ## Plugins
@@ -163,6 +224,8 @@ Once you are happy with the UI, you can click on export code from the header and
## Requirements.txt ## Requirements.txt
The requirements.txt files are auto generated, before running the code ensure you have installed the dependencies by using `pip install -r requirements.txt` The requirements.txt files are auto generated, before running the code ensure you have installed the dependencies by using `pip install -r requirements.txt`
## Saving the file ## Saving/load the file
Files are not saved or stored. However this is an upcoming feature for the [Premium users](https://github.com/PaulleDemon/PyUIBuilder?tab=readme-ov-file#license---fund-the-development) You can save the design file by clicking on the save button from the file dropdown
Similiarly, you can load the design by clicking the load button in the file dropdown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@@ -3,8 +3,8 @@
--bg-color: #fff; --bg-color: #fff;
--btn-color: #0F1727;/* button color*/ --btn-color: #ffffff;/* button color*/
--btn-bg: #f1efef;/* button bg color*/ --btn-bg: #0F1727;/* button bg color*/
--primary-text-color: #000; --primary-text-color: #000;
--header-link-hover: #000; --header-link-hover: #000;
@@ -181,7 +181,7 @@ header > .collapsible-header{
} }
.footer-link:hover{ .footer-link:hover{
color: #fff; color: #0b0b0b;
} }
/* Navigation dots styling */ /* Navigation dots styling */

File diff suppressed because one or more lines are too long

View File

@@ -7,7 +7,7 @@
<meta name="description" content="A Drag and Drop builder for Python GUIs, including Tkinter, Custom Tkinter and more" /> <meta name="description" content="A Drag and Drop builder for Python GUIs, including Tkinter, Custom Tkinter and more" />
<link <link
rel="shortcut icon" rel="shortcut icon"
href="./assets/logo/icon48.png" href="./assets/logo/logo.png"
type="image/x-icon" type="image/x-icon"
/> />
@@ -15,12 +15,12 @@
<meta property="og:title" content="PyUIBuilder - A Drag and Drop builder for python GUIs" /> <meta property="og:title" content="PyUIBuilder - A Drag and Drop builder for python GUIs" />
<meta property="og:description" content="A Drag and Drop builder for Python GUIs, including Tkinter, Custom Tkinter and more" /> <meta property="og:description" content="A Drag and Drop builder for Python GUIs, including Tkinter, Custom Tkinter and more" />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:url" content="https://github.com/PaulleDemon/PyUIBuilder" /> <meta property="og:url" content="https://about.pyuibuilder.com" />
<!--Replace with the current website url--> <!--Replace with the current website url-->
<meta property="og:image" content="" /> <meta property="og:image" content="" />
<link rel="stylesheet" href="../tailwind/tailwind-runtime.css" /> <!-- <link rel="stylesheet" href="../tailwind/tailwind-runtime.css" /> -->
<!-- <link rel="stylesheet" href="./css/tailwind-build.css"> --> <link rel="stylesheet" href="./css/tailwind-build.css">
<link rel="stylesheet" href="css/index.css" /> <link rel="stylesheet" href="css/index.css" />
<link <link
@@ -66,14 +66,17 @@
<a class="header-links" href=""> About </a> <a class="header-links" href=""> About </a>
<a class="header-links" href="#pricing"> Pricing </a> <a class="header-links" href="#pricing"> Pricing </a>
<a class="header-links" href="#features"> Features </a> <a class="header-links" href="#features"> Features </a>
<a class="header-links" href="https://github.com/PaulleDemon/PyUIBuilder"> Github </a> <a class="header-links" target="_blank" rel="noopener noreferrer" href="https://docs.pyuibuilder.com"> Docs </a>
<a class="header-links" target="_blank" rel="noopener noreferrer" href="https://github.com/PaulleDemon/PyUIBuilder"> Github </a>
</div> </div>
<div <div
class="tw-mx-4 tw-flex tw-place-items-center tw-gap-[20px] tw-text-base max-md:tw-w-full max-md:tw-flex-col max-md:tw-place-content-center" class="tw-mx-4 tw-flex tw-place-items-center tw-gap-[20px] tw-text-base max-md:tw-w-full max-md:tw-flex-col max-md:tw-place-content-center"
> >
<a <a
href="#pricing" href="https://pyuibuilder.com"
aria-label="start" aria-label="start"
target="_blank"
rel="noreferrer noopener"
class="tw-rounded-md tw-border-[1px] tw-border-black class="tw-rounded-md tw-border-[1px] tw-border-black
tw-bg-[#0F1727] tw-px-3 tw-py-2 tw-text-white tw-bg-[#0F1727] tw-px-3 tw-py-2 tw-text-white
tw-duration-[0.3s] tw-duration-[0.3s]
@@ -130,11 +133,11 @@
class="tw-flex tw-flex-col tw-place-content-center tw-items-center" class="tw-flex tw-flex-col tw-place-content-center tw-items-center"
> >
<div <div
class="reveal-up tw-text-center tw-text-6xl tw-font-semibold tw-uppercase tw-leading-[90px] class="tw-text-center tw-text-6xl tw-font-semibold tw-uppercase tw-leading-[90px]
max-lg:tw-text-4xl tw-text-[#0F1727] max-lg:tw-text-4xl tw-text-[#0F1727]
max-md:tw-leading-snug" max-md:tw-leading-snug"
> >
<span class=""> Build Python UI's using <span class="tw-px-3 t"></span> <span class=""> Build Python UI's using <span class="tw-px-3"></span>
</span> </span>
<br /> <br />
<span class=" tw-px-3 tw-text-5xl"> Drag and Drop editor </span> <span class=" tw-px-3 tw-text-5xl"> Drag and Drop editor </span>
@@ -146,7 +149,7 @@
<div <div
class="reveal-up tw-mt-3 tw-max-w-[450px] tw-p-2 tw-text-center tw-text-gray-700 max-lg:tw-max-w-full" class="reveal-up tw-mt-3 tw-max-w-[450px] tw-p-2 tw-text-center tw-text-gray-700 max-lg:tw-max-w-full"
> >
Tired of writing code to build you python GUIs, well now you can easily The most awaited python tool is here. Now you can use Drag and drop to build Tkinter and customTk GUIs. Soon wil be made available for kivy and PySide
</div> </div>
<div <div
@@ -154,7 +157,9 @@
> >
<a <a
class="btn !tw-bg-[#0F1727] !tw-text-white tw-shadow-lg tw-shadow-[#4d395e] tw-transition-transform tw-duration-[0.3s] hover:tw-scale-x-[1.03]" class="btn !tw-bg-[#0F1727] !tw-text-white tw-shadow-lg tw-shadow-[#4d395e] tw-transition-transform tw-duration-[0.3s] hover:tw-scale-x-[1.03]"
href="#pricing" target="_blank"
rel="noopener noreferrer"
href="https://pyuibuilder.com"
> >
Start for free Start for free
</a> </a>
@@ -183,14 +188,152 @@
<div class="tw-relative tw-bg-black tw-min-w-full tw-min-h-full tw-overflow-clip tw-rounded-md"> <div class="tw-relative tw-bg-black tw-min-w-full tw-min-h-full tw-overflow-clip tw-rounded-md">
<!-- <iframe class="tw-absolute tw-top-[50%] tw--translate-y-[50%] tw-left-[50%] tw--translate-x-[50%] tw-w-full tw-h-full" src="https://www.youtube.com/embed/aXq0A6iJoKU?si=dF3UZT2eTRDU8u7a?si=QOMSCki8Jl30_CkW&amp;controls=1&rel=0&showinfo=0&autoplay=1&loop=1&mute=1" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe> --> <!-- <iframe class="tw-absolute tw-top-[50%] tw--translate-y-[50%] tw-left-[50%] tw--translate-x-[50%] tw-w-full tw-h-full" src="https://www.youtube.com/embed/aXq0A6iJoKU?si=dF3UZT2eTRDU8u7a?si=QOMSCki8Jl30_CkW&amp;controls=1&rel=0&showinfo=0&autoplay=1&loop=1&mute=1" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe> -->
<img class="tw-object-contain tw-w-full tw-h-full" <img class="tw-object-contain tw-w-full tw-h-full"
alt="font tester gif" src="./assets/images/home/promo.gif" > alt="Pyui gif" src="./assets/images/home/promo.gif" >
</div> </div>
</div> </div>
</section> </section>
<section
class="tw-relative tw-flex tw-w-full tw-min-h-[100vh] tw-max-w-[100vw] tw-flex-col tw-place-content-center tw-place-items-center tw-overflow-hidden tw-p-8"
id="features"
>
<h2 class="tw-text-5xl tw-text-center tw-font-medium tw-uppercase">
Everything you need <br>
to perfect Python GUIs
</h2>
<div
class="tw-flex tw-flex-col tw-max-w-[1150px] max-lg:tw-max-w-full tw-h-full
tw-p-4 tw-mt-4 max-lg:tw-place-content-center tw-gap-8"
>
<div class="max-xl:tw-flex max-xl:tw-flex-col tw-place-items-center tw-grid tw-grid-cols-3 tw-gap-8
tw-place-content-center tw-auto-rows-auto">
<div class="tw-w-[350px] tw-h-[500px] max-lg:tw-h-fit tw-flex max-md:tw-w-full">
<div href="#" class=" tw-relative tw-p-10 tw-transition-all tw-duration-300 tw-group/card tw-gap-5 tw-flex
tw-flex-col tw-w-full tw-h-full tw-bg-[#f2f2f2] tw-border-2 tw-border-white tw-rounded-3xl
hover:tw-scale-[1.02]">
<div class="tw-w-full tw-min-h-[200px] tw-h-[200px] tw-overflow-hidden">
<img src="./assets/images/home/benefits/1.png"
alt="Drag and drop" class="tw-w-full tw-h-full tw-object-contain">
<!-- <i class="bi bi-grid-1x2-fill"></i> -->
</div>
<h2 class="tw-text-3xl max-md:tw-text-2xl tw-font-medium tw-uppercase">Drag and Drop editor</h2>
<p class="tw-leading-normal tw-text-gray-800 tw-overflow-clip">
Easily drag and drop desired widgets from sidebar and create beautiful Python GUI's
</p>
</div>
</div>
<div class="tw-w-[350px] tw-h-[500px] max-lg:tw-h-fit tw-flex max-md:tw-w-full">
<div href="#" class=" tw-relative tw-p-10 tw-transition-all tw-duration-300 tw-group/card tw-gap-5 tw-flex
tw-flex-col tw-w-full tw-h-full tw-bg-[#f2f2f2] tw-border-2 tw-border-white tw-rounded-3xl
hover:tw-scale-[1.02]">
<div class="tw-w-full tw-min-h-[200px] tw-h-[200px] tw-overflow-hidden">
<img src="./assets/images/home/benefits/2.png"
alt="live preview" class="tw-w-full tw-h-full tw-object-contain">
<!-- <i class="bi bi-grid-1x2-fill"></i> -->
</div>
<h2 class="tw-text-3xl max-md:tw-text-2xl tw-font-medium tw-uppercase">Support for multiple UI libraries</h2>
<p class="tw-leading-normal tw-text-gray-800 tw-overflow-clip">
We currently support multiple UI libraries such as Tkinter, CustomTk and soon will offer support for PySide and
kivy.
</p>
</div>
</div>
<div class="tw-w-[350px] tw-h-[500px] max-lg:tw-h-fit tw-flex max-md:tw-w-full">
<div href="#" class=" tw-relative tw-p-10 tw-transition-all tw-duration-300 tw-group/card tw-gap-5 tw-flex
tw-flex-col tw-w-full tw-h-full tw-bg-[#f2f2f2] tw-border-2 tw-border-white tw-rounded-3xl
hover:tw-scale-[1.02]">
<div class="tw-w-full tw-min-h-[200px] tw-h-[200px] tw-overflow-hidden">
<img src="./assets/images/home/benefits/3.png"
alt="live preview" class="tw-w-full tw-h-full tw-object-contain">
<!-- <i class="bi bi-grid-1x2-fill"></i> -->
</div>
<h2 class="tw-text-3xl max-md:tw-text-2xl tw-font-medium tw-uppercase">Supports many UI Widgets</h2>
<p class="tw-leading-normal tw-text-gray-800 tw-overflow-clip">
Our editor includes pre-defined widgets like Buttons and Labels, compatible with Pack/Flex, Grid, and absolute layouts.
</p>
</div>
</div>
<div class="tw-w-[350px] tw-h-[500px] max-lg:tw-h-fit tw-flex max-md:tw-w-full">
<div href="#" class=" tw-relative tw-p-10 tw-transition-all tw-duration-300 tw-group/card tw-gap-5 tw-flex
tw-flex-col tw-w-full tw-h-full tw-bg-[#f2f2f2] tw-border-2 tw-border-white tw-rounded-3xl
hover:tw-scale-[1.02]">
<div class="tw-w-full tw-min-h-[200px] tw-h-[200px] tw-overflow-hidden">
<img src="./assets/images/home/benefits/4.png"
alt="live preview" class="tw-w-full tw-h-full tw-object-contain">
<!-- <i class="bi bi-grid-1x2-fill"></i> -->
</div>
<h2 class="tw-text-3xl max-md:tw-text-2xl tw-font-medium tw-uppercase">Generate clean Python code</h2>
<p class="tw-leading-normal tw-text-gray-800">
Our editor doesn't just let you drag and drop, but also lets you generate clean pyhon code in
library of your choice
</p>
</div>
</div>
<div class="tw-w-[350px] tw-h-[500px] max-lg:tw-h-fit tw-flex max-md:tw-w-full">
<div href="#" class=" tw-relative tw-p-10 tw-transition-all tw-duration-300 tw-group/card tw-gap-5 tw-flex
tw-flex-col tw-w-full tw-h-full tw-bg-[#f2f2f2] tw-border-2 tw-border-white tw-rounded-3xl
hover:tw-scale-[1.02]">
<div class="tw-w-full tw-min-h-[200px] tw-h-[200px] tw-overflow-hidden">
<img src="./assets/images/home/benefits/5.png"
alt="live preview" class="tw-w-full tw-h-full tw-object-contain">
<!-- <i class="bi bi-grid-1x2-fill"></i> -->
</div>
<h2 class="tw-text-3xl max-md:tw-text-2xl tw-font-medium tw-uppercase">3rd Party Plugin support</h2>
<p class="tw-leading-normal tw-text-gray-800">
We support your favorite 3rd party widgets such as video player, Map viewer and more.
</p>
</div>
</div>
<div class="tw-w-[350px] tw-h-[500px] max-lg:tw-h-fit tw-flex max-md:tw-w-full">
<div href="#" class=" tw-relative tw-p-10 tw-transition-all tw-duration-300 tw-group/card tw-gap-5 tw-flex
tw-flex-col tw-w-full tw-h-full tw-bg-[#f2f2f2] tw-border-2 tw-border-white tw-rounded-3xl
hover:tw-scale-[1.02]">
<div class="tw-w-full tw-min-h-[200px] tw-h-[200px] tw-overflow-hidden">
<img src="./assets/images/home/benefits/6.png"
alt="live preview" class="tw-w-full tw-h-full tw-object-contain">
<!-- <i class="bi bi-grid-1x2-fill"></i> -->
</div>
<h2 class="tw-text-3xl max-md:tw-text-2xl tw-font-medium tw-uppercase">Supports asset upload</h2>
<p class="tw-leading-normal tw-text-gray-800">
Adding images and logos is important while developing GUIs so we support asset uploads.
The best part is nothing is sent to server.
</p>
</div>
</div>
</div>
</div>
<a
class="tw-mt-6 btn tw-my-6 tw-flex tw-gap-1 tw-bg-[#0F1727]
tw-transition-transform tw-duration-[0.3s] hover:tw-scale-x-[1.03] tw-group"
href="#pricing"
>
Make it yours
<i class="bi bi-arrow-right tw-transition-transform group-hover:tw-translate-x-1 tw-duration-300"></i>
</a>
</section>
<section class="tw-mt-1 tw-flex tw-w-full tw-flex-col tw-place-items-center tw-p-[2%] max-lg:tw-p-2" id="pricing"> <section class="tw-mt-1 tw-flex tw-w-full tw-flex-col tw-place-items-center tw-p-[2%] max-lg:tw-p-2" id="pricing">
<h3 class="tw-text-2xl tw-font-medium max-md:tw-text-2xl">Pre-order your License</h3> <h3 class="tw-text-2xl tw-font-medium max-md:tw-text-2xl">Pre-order your License</h3>
<div class="tw-max-w-[650px] tw-my-2">
Pre-orders for PyUiBuilder premium features are open. Premium features will start rolling out
from end of April phase wise. This is your last chance to get it before price increase from end of April.
</div>
<div class="tw-mt-10 tw-flex tw-place-content-center tw-gap-8 max-lg:tw-flex-col"> <div class="tw-mt-10 tw-flex tw-place-content-center tw-gap-8 max-lg:tw-flex-col">
<div class="tw-flex tw-w-[380px] tw-border-[1px] tw-border-gray-300 tw-border-solid tw-flex-col <div class="tw-flex tw-w-[380px] tw-border-[1px] tw-border-gray-300 tw-border-solid tw-flex-col
tw-place-items-center tw-gap-2 tw-rounded-lg tw-p-8 tw-shadow-xl max-lg:tw-w-[340px]"> tw-place-items-center tw-gap-2 tw-rounded-lg tw-p-8 tw-shadow-xl max-lg:tw-w-[340px]">
@@ -198,7 +341,7 @@
<span class="tw-text-5xl tw-font-semibold">$0</span> <span class="tw-text-5xl tw-font-semibold">$0</span>
</h3> </h3>
<p class="tw-mt-3 tw-text-base tw-text-center tw-text-gray-800"> <p class="tw-mt-3 tw-text-base tw-text-center tw-text-gray-800">
Free to use forever, but for added features and to support open-source development, consider buying a lifetime license. Free to use forever, but for added features and to support development, consider buying a lifetime license.
</p> </p>
<hr /> <hr />
<ul class="tw-mt-4 tw-flex tw-flex-col tw-gap-3 tw-text-xl tw-text-gray-600"> <ul class="tw-mt-4 tw-flex tw-flex-col tw-gap-3 tw-text-xl tw-text-gray-600">
@@ -214,6 +357,14 @@
<i class="bi bi-x-circle-fill tw-text-red-600 tw-text-base"></i> <i class="bi bi-x-circle-fill tw-text-red-600 tw-text-base"></i>
<span>Downloadable UI builder exe for local development</span> <span>Downloadable UI builder exe for local development</span>
</li> </li>
<li class="tw-flex tw-place-items-center tw-gap-2">
<i class="bi bi-x-circle-fill tw-text-red-600 tw-text-base"></i>
<span>Premium widgets <small class="tw-text-sm">(eg: scroll widget, tab widget, Canvas etc)</small></span>
</li>
<li class="tw-flex tw-place-items-center tw-gap-2">
<i class="bi bi-x-circle-fill tw-text-red-600 tw-text-base"></i>
<span>Event handlers and code editor</span>
</li>
<li class="tw-flex tw-place-items-center tw-gap-2"> <li class="tw-flex tw-place-items-center tw-gap-2">
<i class="bi bi-x-circle-fill tw-text-red-600 tw-text-base"></i> <i class="bi bi-x-circle-fill tw-text-red-600 tw-text-base"></i>
<span>Support for PySlide/PyQt</span> <span>Support for PySlide/PyQt</span>
@@ -249,7 +400,7 @@
</ul> </ul>
<a <a
href="https://pyuibuilder.pages.dev/" href="https://pyuibuilder.com"
target="_blank" target="_blank"
rel="noreferrer noopener" rel="noreferrer noopener"
class=" !tw-bg-[#0F1727] tw-duration-[0.3s] hover:tw-transition-transform hover:tw-scale-[1.01] !tw-mt-auto !tw-text-white tw-gap-2 tw-text-lg tw-rounded-md tw-w-full tw-flex tw-place-content-center tw-p-2 tw-mx-2" class=" !tw-bg-[#0F1727] tw-duration-[0.3s] hover:tw-transition-transform hover:tw-scale-[1.01] !tw-mt-auto !tw-text-white tw-gap-2 tw-text-lg tw-rounded-md tw-w-full tw-flex tw-place-content-center tw-p-2 tw-mx-2"
@@ -275,7 +426,7 @@
<span class="tw-text-2xl tw-text-gray-600">Forever</span> <span class="tw-text-2xl tw-text-gray-600">Forever</span>
</h3> </h3>
<p class="tw-mt-3 tw-text-center tw-text-gray-600"> <p class="tw-mt-3 tw-text-center tw-text-gray-600">
Support open-source development 🚀. Plus, get added benefits. Best for hobby users and people who want to learn basic UI development without PySide
</p> </p>
<hr /> <hr />
<ul class="tw-mt-4 tw-flex tw-flex-col tw-gap-3 tw-text-xl tw-text-gray-600"> <ul class="tw-mt-4 tw-flex tw-flex-col tw-gap-3 tw-text-xl tw-text-gray-600">
@@ -287,6 +438,14 @@
<i class="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i> <i class="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Downloadable UI builder exe for local development</span> <span>Downloadable UI builder exe for local development</span>
</li> </li>
<li class="tw-flex tw-place-items-center tw-gap-2">
<i class="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Premium widgets <small class="tw-text-sm">(eg: scroll widget, tab widget, Canvas etc)</small></span>
</li>
<li class="tw-flex tw-place-items-center tw-gap-2">
<i class="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Event handlers and code editor</span>
</li>
<li class="tw-flex tw-place-items-center tw-gap-2"> <li class="tw-flex tw-place-items-center tw-gap-2">
<i class="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i> <i class="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Preview live</span> <span>Preview live</span>
@@ -338,7 +497,7 @@
</div> </div>
<div class="tw-flex tw-w-[380px] tw-flex-col tw-place-items-center tw-gap-2 tw-rounded-lg tw-border-[3px] tw-border-solid <div class="tw-flex tw-w-[380px] tw-flex-col tw-place-items-center tw-gap-2 tw-rounded-lg tw-border-[3px] tw-border-solid
!tw-border-green-600 tw-p-8 tw-shadow-xl max-lg:tw-w-[340px]"> !tw-border-blue-600 tw-p-8 tw-shadow-xl max-lg:tw-w-[340px]">
<div class="tw-text-white tw-p-1 tw-px-3 tw-bg-blue-500 tw-rounded-full"> <div class="tw-text-white tw-p-1 tw-px-3 tw-bg-blue-500 tw-rounded-full">
Limited time offer Limited time offer
</div> </div>
@@ -353,7 +512,7 @@
<span class="tw-text-2xl tw-text-gray-600">Forever</span> <span class="tw-text-2xl tw-text-gray-600">Forever</span>
</h3> </h3>
<p class="tw-mt-3 tw-text-center tw-text-gray-600"> <p class="tw-mt-3 tw-text-center tw-text-gray-600">
Support open-source development 🚀. Plus, get added benefits. Best for startups and teams and people who want to use internally or serves a commercial purpose and want support for PySide
</p> </p>
<hr /> <hr />
<ul class="tw-mt-4 tw-flex tw-flex-col tw-gap-3 tw-text-xl tw-text-gray-600"> <ul class="tw-mt-4 tw-flex tw-flex-col tw-gap-3 tw-text-xl tw-text-gray-600">
@@ -365,6 +524,14 @@
<i class="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i> <i class="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Downloadable UI builder exe for local development</span> <span>Downloadable UI builder exe for local development</span>
</li> </li>
<li class="tw-flex tw-place-items-center tw-gap-2">
<i class="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Premium widgets <small class="tw-text-sm">(eg: scroll widget, tab widget, Canvas etc)</small></span>
</li>
<li class="tw-flex tw-place-items-center tw-gap-2">
<i class="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Event handlers and code editor</span>
</li>
<li class="tw-flex tw-place-items-center tw-gap-2"> <li class="tw-flex tw-place-items-center tw-gap-2">
<i class="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i> <i class="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Preview live</span> <span>Preview live</span>
@@ -458,7 +625,7 @@
</div> </div>
<div class="content"> <div class="content">
Yes, PyUIBuilder is free to use forever, however to support Yes, PyUIBuilder is free to use forever, however to support
open-source development, some features are only available to premium users. development, some features are only available to premium users.
</div> </div>
</div> </div>
@@ -483,25 +650,82 @@
<div <div
class="faq-accordion tw-flex tw-w-full tw-select-none tw-text-xl max-md:tw-text-lg" class="faq-accordion tw-flex tw-w-full tw-select-none tw-text-xl max-md:tw-text-lg"
> >
<span>Where can I find the upcoming features?</span> <span>When will premium features roll out</span>
<i class="bi bi-plus tw-ml-auto tw-font-semibold"></i> <i class="bi bi-plus tw-ml-auto tw-font-semibold"></i>
</div> </div>
<div class="content"> <div class="content">
You can find the upcoming features on the Premium features will start rolling out towards the end of April in phase wise.
<a </div>
target="_blank" </div>
rel="noopener noreferrer" <div
href="https://github.com/PaulleDemon/font-tester-chrome/blob/main/roadmap.md" class="faq tw-w-full"
class="tw-underline" >
>Roadmap</a <div
> class="faq-accordion tw-flex tw-w-full tw-select-none tw-text-xl max-md:tw-text-lg"
>
<span>Can I get a free license?</span>
<i class="bi bi-plus tw-ml-auto tw-font-semibold"></i>
</div>
<div class="content">
Yes, we have a program by which you can get a free license, <a href="https://tally.so/r/mJM22X" class="tw-underline tw-underline-offset-1">read more</a>
</div> </div>
</div> </div>
</div> </div>
<div class="tw-flex tw-flex-col tw-gap-2 tw-place-items-center tw-mt-4">
<div class="tw-text-2xl">Want more help?</div>
<a class="btn hover:tw-scale-[1.02] tw-duration-75" target="_blank" rel="noopener noreferrer"
href="https://discord.gg/dHXjrrCA7G">Discord invite</a>
</div>
</section> </section>
<section
class="tw-flex tw-w-full tw-flex-col tw-place-content-center tw-place-items-center tw-gap-[5%] tw-p-[3%] tw-px-[10%]"
id="blog-section"
>
<h3 class="tw-text-4xl tw-font-medium tw-text-gray-800 max-md:tw-text-2xl">Blogs & Tutorials</h3>
<div class="tw-mt-5 tw-flex tw-gap-8 tw-place-items-center max-lg:tw-flex-col
tw-place-content-center tw-min-h-[300px] tw-w-full">
<div class="tw-flex tw-gap-4" id="blogs">
</div>
<a href="https://youtu.be/MkeMv0X-w_4?si=qeutS58Js7Padd9m&t=49" target="_blank" rel="noopener noreferrer"
class="tw-w-[350px] tw-min-w-[350px] tw-max-w-[350px] max-lg:tw-w-full tw-h-[360px] tw-shadow-xl tw-rounded-md tw-overflow-hidden">
<div class="tw-h-[230px] tw-w-full tw-bg-gray-800 tw-flex tw-place-content-center">
<img src="https://img.youtube.com/vi/MkeMv0X-w_4/hqdefault.jpg"
alt="" class="tw-object-contain tw-w-full">
</div>
<div class="tw-flex tw-flex-col tw-gap-2 tw-p-3">
<h3 class="tw-text-lg tw-font-medium">Introduction to PyUiBuilder - YouTube</h3>
<p class="tw-text-bg-gray-500">Introduction to PyUIBuilder</p>
</div>
</a>
<a href="https://youtu.be/4w3Oy17FE8U?si=-0V0cD5DQAEzCkKm" target="_blank" rel="noopener noreferrer"
class="tw-w-[350px] tw-min-w-[350px] tw-max-w-[350px] max-lg:tw-w-full tw-h-[360px] tw-shadow-xl tw-rounded-md tw-overflow-hidden">
<div class="tw-h-[230px] tw-w-full tw-bg-gray-800 tw-flex tw-place-content-center">
<img src="https://img.youtube.com/vi/4w3Oy17FE8U/hqdefault.jpg"
alt="" class="tw-object-contain tw-w-full">
</div>
<div class="tw-flex tw-flex-col tw-gap-2 tw-p-3">
<h3 class="tw-text-lg tw-font-medium">Create Signup form using PyUIBuilder - YouTube</h3>
<p class="tw-text-bg-gray-500">Learn how to use this tool, via practical example</p>
</div>
</a>
</div>
</section>
<section
class="tw-flex tw-w-full tw-flex-col tw-place-content-center tw-place-items-center tw-gap-2 tw-p-[3%] tw-px-[10%]"
>
<h3 class="tw-text-xl">Get updates right in your mailbox</h3>
<button class="btn hover:tw-scale-[1.05] tw-duration-75" data-tally-open="mVDY7N" data-tally-layout="modal" data-tally-hide-title="1" data-tally-emoji-text="👋" data-tally-emoji-animation="wave" data-tally-auto-close="0">
Get updates
</button>
</section>
<footer <footer
class="tw-mt-auto tw-flex tw-w-full tw-place-content-around tw-gap-3 tw-p-[5%] tw-px-[10%] tw-text-black max-md:tw-flex-col" class="tw-mt-auto tw-flex tw-w-full tw-place-content-around tw-gap-3 tw-p-[5%] tw-px-[10%] tw-text-black max-md:tw-flex-col"
@@ -549,7 +773,8 @@
<div class="tw-flex tw-flex-col tw-gap-3 max-md:tw-text-sm"> <div class="tw-flex tw-flex-col tw-gap-3 max-md:tw-text-sm">
<a href="#features" class="footer-link">Features</a> <a href="#features" class="footer-link">Features</a>
<a href="#pricing" class="footer-link">Pricing</a> <a href="#pricing" class="footer-link">Pricing</a>
<a href="https://github.com/PaulleDemon/font-tester-chrome/blob/main/roadmap.md" class="footer-link">Roadmap</a> <a href="https://github.com/PaulleDemon/PyUIBuilder/blob/main/roadmap.md" class="footer-link">Roadmap</a>
<a href="./privacy.html" class="footer-link">Privacy</a>
</div> </div>
</div> </div>
@@ -557,12 +782,43 @@
<div class="tw-flex tw-flex-col tw-gap-3 max-md:tw-text-sm"> <div class="tw-flex tw-flex-col tw-gap-3 max-md:tw-text-sm">
<a href="" class="footer-link">About</a> <a href="" class="footer-link">About</a>
<a href="#faq" class="footer-link">FAQ</a> <a href="#faq" class="footer-link">FAQ</a>
<a href="https://github.com/PaulleDemon/font-tester-chrome" class="footer-link">Github</a> <a href="https://github.com/PaulleDemon/PyUIBuilder" class="footer-link">Github</a>
<a href="https://github.com/PaulleDemon/font-tester-chrome" class="footer-link">Report issue</a> <a href="https://github.com/PaulleDemon/PyUIBuilder/issues" class="footer-link">Report issue</a>
<a href="https://discord.gg/dHXjrrCA7G" class="footer-link">Discord invite</a>
</div> </div>
</div> </div>
</footer> </footer>
</body> </body>
<script async src="https://tally.so/widgets/embed.js"></script>
<script>
const blogsList = [
{
"title": "How to create Signup form - Blog",
"description": "If your starting out with tkinter, it can be overwhelming without prior understanding of GUIs....",
"url": "https://medium.com/python-in-plain-english/create-tkinter-guis-using-tkinter-gui-builder-pyuibuilder-a7422489c55e",
"image_url": "https://miro.medium.com/v2/resize:fit:720/format:webp/1*BaSis1UO3_zJeQtCnOe3Qg.png"
},
]
const blogContainer = document.getElementById('blogs');
blogContainer.innerHTML = blogsList.map(blog => {
return (`
<a href="${blog.url}" target="_blank" rel="noopener noreferrer"
class="tw-w-[350px] tw-h-[360px] tw-shadow-xl tw-rounded-md tw-overflow-hidden">
<div class="tw-h-[230px] tw-w-full tw-bg-gray-800 tw-flex tw-place-content-center">
<img src="${blog.image_url}" alt="${blog.title}" class="tw-object-contain tw-w-full">
</div>
<div class="tw-flex tw-flex-col tw-gap-2 tw-p-3">
<h3 class="tw-text-lg tw-font-medium">${blog.title}</h3>
<p class="tw-text-bg-gray-500">${blog.description}</p>
</div>
</a>
`)
}).join('')
</script>
<script <script
src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.0/gsap.min.js" src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.0/gsap.min.js"

View File

@@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Privacy Statement for PyUIBuilder, explaining what data we collect and how we protect it." />
<title>Privacy Statement PyUIBuilder</title>
<link rel="stylesheet" href="./css/tailwind-build.css">
<link rel="stylesheet" href="css/index.css" />
</head>
<body class="tw-bg-gray-100 tw-flex tw-flex-col tw-place-content-center tw-font-sans tw-text-gray-800 tw-min-h-screen tw-px-4 tw-py-10">
<section class="tw-max-w-3xl tw-mx-auto tw-p-6 tw-bg-white tw-rounded-2xl tw-shadow-lg">
<h1 class="tw-text-3xl tw-font-bold tw-mb-4">Privacy Statement</h1>
<p class="tw-mb-4">
At <span class="tw-font-semibold">PyUIBuilder</span>, we respect your privacy and are committed to protecting your personal information. This Privacy Statement outlines the data we collect and how it is used.
</p>
<h2 class="tw-text-xl tw-font-semibold tw-mt-6 tw-mb-2">Information We Collect</h2>
<ul class="tw-list-disc tw-pl-6 tw-mb-4">
<li>Your email address</li>
<li>Your name</li>
<li>Your profile information</li>
</ul>
<h2 class="tw-text-xl tw-font-semibold tw-mt-6 tw-mb-2">How We Use Your Information</h2>
<p class="tw-mb-4">
The information we collect is used solely to provide and improve our service, personalize your experience, and ensure platform security.
</p>
<h2 class="tw-text-xl tw-font-semibold tw-mt-6 tw-mb-2">Data Sharing</h2>
<p class="tw-mb-4">
<span class="tw-font-semibold">We do not share your personal information with third parties.</span> All data remains secure and confidential within PyUIBuilder.
</p>
<h2 class="tw-text-xl tw-font-semibold tw-mt-6 tw-mb-2">Your Consent</h2>
<p class="tw-mb-4">
By using our website, you consent to the collection and use of your information as described in this Privacy Statement.
</p>
<h2 class="tw-text-xl tw-font-semibold tw-mt-6 tw-mb-2">Contact Us</h2>
<p class="tw-text-gray-700">
If you have any questions or concerns about our privacy practices, please contact us at
<a href="mailto:info@pyuibuilder.com" class="tw-text-blue-600 hover:tw-underline">info@pyuibuilder.com</a>.
</p>
</section>
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@@ -107,7 +107,7 @@
} }
/* /*
! tailwindcss v3.4.13 | MIT License | https://tailwindcss.com ! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com
*/ */
/* /*
@@ -541,7 +541,7 @@ video {
/* Make elements with the HTML hidden attribute stay hidden by default */ /* Make elements with the HTML hidden attribute stay hidden by default */
[hidden] { [hidden]:where(:not([hidden="until-found"])) {
display: none; display: none;
} }
@@ -619,6 +619,20 @@ video {
margin-right: 1rem; margin-right: 1rem;
} }
.tw-my-2 {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
.tw-my-6 {
margin-top: 1.5rem;
margin-bottom: 1.5rem;
}
.\!tw-mt-auto {
margin-top: auto !important;
}
.tw-ml-auto { .tw-ml-auto {
margin-left: auto; margin-left: auto;
} }
@@ -643,6 +657,10 @@ video {
margin-top: 1.25rem; margin-top: 1.25rem;
} }
.tw-mt-6 {
margin-top: 1.5rem;
}
.tw-mt-8 { .tw-mt-8 {
margin-top: 2rem; margin-top: 2rem;
} }
@@ -651,14 +669,14 @@ video {
margin-top: auto; margin-top: auto;
} }
.\!tw-mt-auto {
margin-top: auto !important;
}
.tw-flex { .tw-flex {
display: flex; display: flex;
} }
.tw-grid {
display: grid;
}
.\!tw-hidden { .\!tw-hidden {
display: none !important; display: none !important;
} }
@@ -683,10 +701,26 @@ video {
height: 150px; height: 150px;
} }
.tw-h-\[200px\] {
height: 200px;
}
.tw-h-\[230px\] {
height: 230px;
}
.tw-h-\[360px\] {
height: 360px;
}
.tw-h-\[40px\] { .tw-h-\[40px\] {
height: 40px; height: 40px;
} }
.tw-h-\[500px\] {
height: 500px;
}
.tw-h-\[50px\] { .tw-h-\[50px\] {
height: 50px; height: 50px;
} }
@@ -727,6 +761,10 @@ video {
min-height: 100vh; min-height: 100vh;
} }
.tw-min-h-\[200px\] {
min-height: 200px;
}
.tw-min-h-\[300px\] { .tw-min-h-\[300px\] {
min-height: 300px; min-height: 300px;
} }
@@ -739,22 +777,18 @@ video {
min-height: 550px; min-height: 550px;
} }
.tw-min-h-\[60vh\] {
min-height: 60vh;
}
.tw-min-h-\[70vh\] { .tw-min-h-\[70vh\] {
min-height: 70vh; min-height: 70vh;
} }
.tw-min-h-full {
min-height: 100%;
}
.tw-min-h-\[80vh\] { .tw-min-h-\[80vh\] {
min-height: 80vh; min-height: 80vh;
} }
.tw-min-h-full {
min-height: 100%;
}
.tw-w-8 { .tw-w-8 {
width: 2rem; width: 2rem;
} }
@@ -767,6 +801,10 @@ video {
width: 250px; width: 250px;
} }
.tw-w-\[350px\] {
width: 350px;
}
.tw-w-\[380px\] { .tw-w-\[380px\] {
width: 380px; width: 380px;
} }
@@ -787,6 +825,10 @@ video {
width: max-content; width: max-content;
} }
.tw-min-w-\[350px\] {
min-width: 350px;
}
.tw-min-w-full { .tw-min-w-full {
min-width: 100%; min-width: 100%;
} }
@@ -795,14 +837,26 @@ video {
max-width: 100vw; max-width: 100vw;
} }
.tw-max-w-\[1150px\] {
max-width: 1150px;
}
.tw-max-w-\[120px\] { .tw-max-w-\[120px\] {
max-width: 120px; max-width: 120px;
} }
.tw-max-w-\[350px\] {
max-width: 350px;
}
.tw-max-w-\[450px\] { .tw-max-w-\[450px\] {
max-width: 450px; max-width: 450px;
} }
.tw-max-w-\[650px\] {
max-width: 650px;
}
.tw-max-w-\[80vw\] { .tw-max-w-\[80vw\] {
max-width: 80vw; max-width: 80vw;
} }
@@ -856,6 +910,14 @@ video {
user-select: none; user-select: none;
} }
.tw-auto-rows-auto {
grid-auto-rows: auto;
}
.tw-grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.tw-flex-col { .tw-flex-col {
flex-direction: column; flex-direction: column;
} }
@@ -920,6 +982,10 @@ video {
gap: 20px; gap: 20px;
} }
.tw-gap-\[5\%\] {
gap: 5%;
}
.tw-overflow-hidden { .tw-overflow-hidden {
overflow: hidden; overflow: hidden;
} }
@@ -940,6 +1006,10 @@ video {
text-wrap: wrap; text-wrap: wrap;
} }
.tw-rounded-3xl {
border-radius: 1.5rem;
}
.tw-rounded-full { .tw-rounded-full {
border-radius: 9999px; border-radius: 9999px;
} }
@@ -968,10 +1038,6 @@ video {
border-width: 1px; border-width: 1px;
} }
.tw-border-4 {
border-width: 4px;
}
.tw-border-\[3px\] { .tw-border-\[3px\] {
border-width: 3px; border-width: 3px;
} }
@@ -984,118 +1050,96 @@ video {
border-style: solid; border-style: solid;
} }
.\!tw-border-white { .\!tw-border-\[\#0F1727\] {
--tw-border-opacity: 1 !important; --tw-border-opacity: 1 !important;
border-color: rgb(255 255 255 / var(--tw-border-opacity)) !important; border-color: rgb(15 23 39 / var(--tw-border-opacity, 1)) !important;
}
.\!tw-border-blue-600 {
--tw-border-opacity: 1 !important;
border-color: rgb(37 99 235 / var(--tw-border-opacity, 1)) !important;
} }
.tw-border-black { .tw-border-black {
--tw-border-opacity: 1; --tw-border-opacity: 1;
border-color: rgb(0 0 0 / var(--tw-border-opacity)); border-color: rgb(0 0 0 / var(--tw-border-opacity, 1));
}
.tw-border-blue-500 {
--tw-border-opacity: 1;
border-color: rgb(59 130 246 / var(--tw-border-opacity));
} }
.tw-border-gray-300 { .tw-border-gray-300 {
--tw-border-opacity: 1; --tw-border-opacity: 1;
border-color: rgb(209 213 219 / var(--tw-border-opacity)); border-color: rgb(209 213 219 / var(--tw-border-opacity, 1));
}
.tw-border-green-600 {
--tw-border-opacity: 1;
border-color: rgb(22 163 74 / var(--tw-border-opacity));
} }
.tw-border-white { .tw-border-white {
--tw-border-opacity: 1; --tw-border-opacity: 1;
border-color: rgb(255 255 255 / var(--tw-border-opacity)); border-color: rgb(255 255 255 / var(--tw-border-opacity, 1));
} }
.\!tw-border-\[\#0F1727\] { .\!tw-bg-\[\#0F1727\] {
--tw-border-opacity: 1 !important;
border-color: rgb(15 23 39 / var(--tw-border-opacity)) !important;
}
.\!tw-border-green-600 {
--tw-border-opacity: 1 !important;
border-color: rgb(22 163 74 / var(--tw-border-opacity)) !important;
}
.\!tw-bg-\[\#7E22CE\] {
--tw-bg-opacity: 1 !important; --tw-bg-opacity: 1 !important;
background-color: rgb(126 34 206 / var(--tw-bg-opacity)) !important; background-color: rgb(15 23 39 / var(--tw-bg-opacity, 1)) !important;
} }
.\!tw-bg-gray-100 { .\!tw-bg-gray-100 {
--tw-bg-opacity: 1 !important; --tw-bg-opacity: 1 !important;
background-color: rgb(243 244 246 / var(--tw-bg-opacity)) !important; background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1)) !important;
} }
.\!tw-bg-purple-500 { .\!tw-bg-purple-500 {
--tw-bg-opacity: 1 !important; --tw-bg-opacity: 1 !important;
background-color: rgb(168 85 247 / var(--tw-bg-opacity)) !important; background-color: rgb(168 85 247 / var(--tw-bg-opacity, 1)) !important;
} }
.\!tw-bg-transparent { .\!tw-bg-transparent {
background-color: transparent !important; background-color: transparent !important;
} }
.tw-bg-\[\#0F1727\] {
--tw-bg-opacity: 1;
background-color: rgb(15 23 39 / var(--tw-bg-opacity, 1));
}
.tw-bg-\[\#f0f0f0ef\] { .tw-bg-\[\#f0f0f0ef\] {
background-color: #f0f0f0ef; background-color: #f0f0f0ef;
} }
.tw-bg-\[\#f2f2f2\] {
--tw-bg-opacity: 1;
background-color: rgb(242 242 242 / var(--tw-bg-opacity, 1));
}
.tw-bg-\[\#fdfdfdaf\] { .tw-bg-\[\#fdfdfdaf\] {
background-color: #fdfdfdaf; background-color: #fdfdfdaf;
} }
.tw-bg-black { .tw-bg-black {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity)); background-color: rgb(0 0 0 / var(--tw-bg-opacity, 1));
} }
.tw-bg-blue-500 { .tw-bg-blue-500 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(59 130 246 / var(--tw-bg-opacity)); background-color: rgb(59 130 246 / var(--tw-bg-opacity, 1));
} }
.tw-bg-gray-100 { .tw-bg-gray-100 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(243 244 246 / var(--tw-bg-opacity)); background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1));
}
.tw-bg-gray-800 {
--tw-bg-opacity: 1;
background-color: rgb(31 41 55 / var(--tw-bg-opacity, 1));
} }
.tw-bg-green-700 { .tw-bg-green-700 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(21 128 61 / var(--tw-bg-opacity)); background-color: rgb(21 128 61 / var(--tw-bg-opacity, 1));
}
.tw-bg-transparent {
background-color: transparent;
} }
.tw-bg-white { .tw-bg-white {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity)); background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1));
}
.\!tw-bg-\[\#0F1727\] {
--tw-bg-opacity: 1 !important;
background-color: rgb(15 23 39 / var(--tw-bg-opacity)) !important;
}
.tw-bg-\[\#0F1727\] {
--tw-bg-opacity: 1;
background-color: rgb(15 23 39 / var(--tw-bg-opacity));
}
.\!tw-bg-\[\#\#0F1727\] {
background-color: ##0F1727 !important;
}
.tw-bg-\[\#\#0F1727\] {
background-color: ##0F1727;
} }
.tw-bg-opacity-0 { .tw-bg-opacity-0 {
@@ -1110,6 +1154,10 @@ video {
padding: 0.25rem; padding: 0.25rem;
} }
.tw-p-10 {
padding: 2.5rem;
}
.tw-p-2 { .tw-p-2 {
padding: 0.5rem; padding: 0.5rem;
} }
@@ -1118,6 +1166,10 @@ video {
padding: 0.75rem; padding: 0.75rem;
} }
.tw-p-4 {
padding: 1rem;
}
.tw-p-6 { .tw-p-6 {
padding: 1.5rem; padding: 1.5rem;
} }
@@ -1130,6 +1182,10 @@ video {
padding: 2%; padding: 2%;
} }
.tw-p-\[3\%\] {
padding: 3%;
}
.tw-p-\[4px\] { .tw-p-\[4px\] {
padding: 4px; padding: 4px;
} }
@@ -1216,6 +1272,11 @@ video {
line-height: 1.75rem; line-height: 1.75rem;
} }
.tw-text-sm {
font-size: 0.875rem;
line-height: 1.25rem;
}
.tw-text-xl { .tw-text-xl {
font-size: 1.25rem; font-size: 1.25rem;
line-height: 1.75rem; line-height: 1.75rem;
@@ -1233,10 +1294,6 @@ video {
font-weight: 600; font-weight: 600;
} }
.\!tw-font-medium {
font-weight: 500 !important;
}
.tw-uppercase { .tw-uppercase {
text-transform: uppercase; text-transform: uppercase;
} }
@@ -1250,84 +1307,73 @@ video {
line-height: 90px; line-height: 90px;
} }
.\!tw-text-black { .tw-leading-normal {
--tw-text-opacity: 1 !important; line-height: 1.5;
color: rgb(0 0 0 / var(--tw-text-opacity)) !important;
}
.\!tw-text-blue-500 {
--tw-text-opacity: 1 !important;
color: rgb(59 130 246 / var(--tw-text-opacity)) !important;
}
.\!tw-text-white {
--tw-text-opacity: 1 !important;
color: rgb(255 255 255 / var(--tw-text-opacity)) !important;
}
.tw-text-black {
--tw-text-opacity: 1;
color: rgb(0 0 0 / var(--tw-text-opacity));
}
.tw-text-gray-200 {
--tw-text-opacity: 1;
color: rgb(229 231 235 / var(--tw-text-opacity));
}
.tw-text-gray-300 {
--tw-text-opacity: 1;
color: rgb(209 213 219 / var(--tw-text-opacity));
}
.tw-text-gray-400 {
--tw-text-opacity: 1;
color: rgb(156 163 175 / var(--tw-text-opacity));
}
.tw-text-gray-600 {
--tw-text-opacity: 1;
color: rgb(75 85 99 / var(--tw-text-opacity));
}
.tw-text-gray-700 {
--tw-text-opacity: 1;
color: rgb(55 65 81 / var(--tw-text-opacity));
}
.tw-text-green-600 {
--tw-text-opacity: 1;
color: rgb(22 163 74 / var(--tw-text-opacity));
}
.tw-text-purple-500 {
--tw-text-opacity: 1;
color: rgb(168 85 247 / var(--tw-text-opacity));
}
.tw-text-red-600 {
--tw-text-opacity: 1;
color: rgb(220 38 38 / var(--tw-text-opacity));
}
.tw-text-white {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
} }
.\!tw-text-\[\#0F1727\] { .\!tw-text-\[\#0F1727\] {
--tw-text-opacity: 1 !important; --tw-text-opacity: 1 !important;
color: rgb(15 23 39 / var(--tw-text-opacity)) !important; color: rgb(15 23 39 / var(--tw-text-opacity, 1)) !important;
}
.\!tw-text-black {
--tw-text-opacity: 1 !important;
color: rgb(0 0 0 / var(--tw-text-opacity, 1)) !important;
}
.\!tw-text-blue-500 {
--tw-text-opacity: 1 !important;
color: rgb(59 130 246 / var(--tw-text-opacity, 1)) !important;
}
.\!tw-text-white {
--tw-text-opacity: 1 !important;
color: rgb(255 255 255 / var(--tw-text-opacity, 1)) !important;
} }
.tw-text-\[\#0F1727\] { .tw-text-\[\#0F1727\] {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(15 23 39 / var(--tw-text-opacity)); color: rgb(15 23 39 / var(--tw-text-opacity, 1));
}
.tw-text-black {
--tw-text-opacity: 1;
color: rgb(0 0 0 / var(--tw-text-opacity, 1));
}
.tw-text-gray-600 {
--tw-text-opacity: 1;
color: rgb(75 85 99 / var(--tw-text-opacity, 1));
}
.tw-text-gray-700 {
--tw-text-opacity: 1;
color: rgb(55 65 81 / var(--tw-text-opacity, 1));
} }
.tw-text-gray-800 { .tw-text-gray-800 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(31 41 55 / var(--tw-text-opacity)); color: rgb(31 41 55 / var(--tw-text-opacity, 1));
}
.tw-text-green-600 {
--tw-text-opacity: 1;
color: rgb(22 163 74 / var(--tw-text-opacity, 1));
}
.tw-text-purple-500 {
--tw-text-opacity: 1;
color: rgb(168 85 247 / var(--tw-text-opacity, 1));
}
.tw-text-red-600 {
--tw-text-opacity: 1;
color: rgb(220 38 38 / var(--tw-text-opacity, 1));
}
.tw-text-white {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity, 1));
} }
.tw-underline { .tw-underline {
@@ -1364,16 +1410,17 @@ video {
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
} }
.tw-shadow-\[\#7E22CE\] {
--tw-shadow-color: #7E22CE;
--tw-shadow: var(--tw-shadow-colored);
}
.tw-shadow-\[\#4d395e\] { .tw-shadow-\[\#4d395e\] {
--tw-shadow-color: #4d395e; --tw-shadow-color: #4d395e;
--tw-shadow: var(--tw-shadow-colored); --tw-shadow: var(--tw-shadow-colored);
} }
.tw-transition-all {
transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.tw-transition-colors { .tw-transition-colors {
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
@@ -1392,12 +1439,6 @@ video {
transition-duration: 150ms; transition-duration: 150ms;
} }
.tw-transition-all {
transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.tw-duration-300 { .tw-duration-300 {
transition-duration: 300ms; transition-duration: 300ms;
} }
@@ -1406,13 +1447,12 @@ video {
transition-duration: 500ms; transition-duration: 500ms;
} }
.tw-duration-\[0\.3s\] { .tw-duration-75 {
transition-duration: 0.3s; transition-duration: 75ms;
} }
.hover\:tw-translate-x-1:hover { .tw-duration-\[0\.3s\] {
--tw-translate-x: 0.25rem; transition-duration: 0.3s;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
} }
.hover\:tw-translate-x-2:hover { .hover\:tw-translate-x-2:hover {
@@ -1432,6 +1472,12 @@ video {
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
} }
.hover\:tw-scale-\[1\.05\]:hover {
--tw-scale-x: 1.05;
--tw-scale-y: 1.05;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.hover\:tw-scale-x-\[1\.03\]:hover { .hover\:tw-scale-x-\[1\.03\]:hover {
--tw-scale-x: 1.03; --tw-scale-x: 1.03;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
@@ -1439,47 +1485,22 @@ video {
.hover\:\!tw-bg-gray-100:hover { .hover\:\!tw-bg-gray-100:hover {
--tw-bg-opacity: 1 !important; --tw-bg-opacity: 1 !important;
background-color: rgb(243 244 246 / var(--tw-bg-opacity)) !important; background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1)) !important;
} }
.hover\:\!tw-bg-gray-300:hover { .hover\:\!tw-bg-gray-300:hover {
--tw-bg-opacity: 1 !important; --tw-bg-opacity: 1 !important;
background-color: rgb(209 213 219 / var(--tw-bg-opacity)) !important; background-color: rgb(209 213 219 / var(--tw-bg-opacity, 1)) !important;
} }
.hover\:\!tw-bg-white:hover { .hover\:\!tw-bg-white:hover {
--tw-bg-opacity: 1 !important; --tw-bg-opacity: 1 !important;
background-color: rgb(255 255 255 / var(--tw-bg-opacity)) !important; background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1)) !important;
}
.hover\:tw-bg-white:hover {
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
}
.hover\:tw-bg-black:hover {
--tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
}
.hover\:tw-bg-\[\#0F1727\]:hover {
--tw-bg-opacity: 1;
background-color: rgb(15 23 39 / var(--tw-bg-opacity));
} }
.hover\:\!tw-text-black:hover { .hover\:\!tw-text-black:hover {
--tw-text-opacity: 1 !important; --tw-text-opacity: 1 !important;
color: rgb(0 0 0 / var(--tw-text-opacity)) !important; color: rgb(0 0 0 / var(--tw-text-opacity, 1)) !important;
}
.hover\:tw-text-black:hover {
--tw-text-opacity: 1;
color: rgb(0 0 0 / var(--tw-text-opacity));
}
.hover\:tw-text-white:hover {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
} }
.hover\:tw-transition-transform:hover { .hover\:tw-transition-transform:hover {
@@ -1488,9 +1509,14 @@ video {
transition-duration: 150ms; transition-duration: 150ms;
} }
.tw-group:hover .group-hover\:tw-translate-x-1 {
--tw-translate-x: 0.25rem;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.dark\:tw-bg-\[\#16171A\]:is(.tw-dark *) { .dark\:tw-bg-\[\#16171A\]:is(.tw-dark *) {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(22 23 26 / var(--tw-bg-opacity)); background-color: rgb(22 23 26 / var(--tw-bg-opacity, 1));
} }
.dark\:tw-bg-\[\#80808085\]:is(.tw-dark *) { .dark\:tw-bg-\[\#80808085\]:is(.tw-dark *) {
@@ -1498,6 +1524,14 @@ video {
} }
@media not all and (min-width: 1280px) { @media not all and (min-width: 1280px) {
.max-xl\:tw-flex {
display: flex;
}
.max-xl\:tw-flex-col {
flex-direction: column;
}
.max-xl\:tw-place-items-center { .max-xl\:tw-place-items-center {
place-items: center; place-items: center;
} }
@@ -1520,6 +1554,10 @@ video {
height: auto; height: auto;
} }
.max-lg\:tw-h-fit {
height: fit-content;
}
.max-lg\:tw-min-h-\[250px\] { .max-lg\:tw-min-h-\[250px\] {
min-height: 250px; min-height: 250px;
} }
@@ -1528,10 +1566,6 @@ video {
min-height: 400px; min-height: 400px;
} }
.max-lg\:tw-min-h-\[60vh\] {
min-height: 60vh;
}
.max-lg\:tw-min-h-\[80vh\] { .max-lg\:tw-min-h-\[80vh\] {
min-height: 80vh; min-height: 80vh;
} }
@@ -1556,6 +1590,10 @@ video {
flex-direction: column; flex-direction: column;
} }
.max-lg\:tw-place-content-center {
place-content: center;
}
.max-lg\:tw-place-items-end { .max-lg\:tw-place-items-end {
place-items: end; place-items: end;
} }

15687
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,10 @@
"engines": { "engines": {
"node": ">=18" "node": ">=18"
}, },
"overrides": {
"picomatch": "^3.0.0",
"postcss-selector-parser": "7.1.0"
},
"dependencies": { "dependencies": {
"@ant-design/icons": "^5.4.0", "@ant-design/icons": "^5.4.0",
"@dnd-kit/core": "^6.1.0", "@dnd-kit/core": "^6.1.0",
@@ -22,6 +26,7 @@
"fabric": "^6.1.0", "fabric": "^6.1.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"lodash": "^4.17.21",
"postcss-cli": "^11.0.0", "postcss-cli": "^11.0.0",
"re-resizable": "^6.9.17", "re-resizable": "^6.9.17",
"react": "^18.3.1", "react": "^18.3.1",
@@ -34,9 +39,9 @@
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "env-cmd -e development cross-env NODE_ENV=development webpack serve",
"build": "GENERATE_SOURCEMAP=false react-scripts build", "build": "NODE_ENV=production webpack",
"build:local": "GENERATE_SOURCEMAP=false env-cmd -e production react-scripts build", "build:local": "env-cmd -e production cross-env NODE_ENV=production webpack",
"test": "react-scripts test", "test": "react-scripts test",
"eject": "react-scripts eject", "eject": "react-scripts eject",
"start:tailwind": "cross-env NODE_ENV=development tailwindcss --postcss -i ./landingpages/tailwind/tailwind.css -o ./landingpages/tailwind/tailwind-runtime.css -w", "start:tailwind": "cross-env NODE_ENV=development tailwindcss --postcss -i ./landingpages/tailwind/tailwind.css -o ./landingpages/tailwind/tailwind-runtime.css -w",
@@ -62,8 +67,29 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"ajv": "^7.2.4", "@babel/core": "^7.26.10",
"@babel/preset-env": "^7.26.9",
"@babel/preset-react": "^7.26.3",
"@svgr/webpack": "^8.1.0",
"@types/react": "^18.3.20",
"ajv": "^8.17.1",
"babel-loader": "^10.0.0",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^13.0.0",
"css-loader": "^7.1.2",
"css-minimizer-webpack-plugin": "^7.0.2",
"docsify-cli": "^4.4.4", "docsify-cli": "^4.4.4",
"typescript": "^4.9.5" "file-loader": "^6.2.0",
"html-webpack-plugin": "^5.6.3",
"mini-css-extract-plugin": "^2.9.2",
"raw-loader": "^4.0.2",
"react-app-rewired": "^2.2.1",
"sass-loader": "^16.0.5",
"style-loader": "^4.0.0",
"terser-webpack-plugin": "^5.3.14",
"typescript": "^4.9.5",
"webpack": "^5.98.0",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.1"
} }
} }

View File

@@ -2,29 +2,31 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <link rel="icon" href="favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta <meta
name="description" name="description"
content="Web site created using create-react-app" content="A python GUI builder. Create tkinter, Customtk, Kivy and PySide using GUI builder"
/> />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <link rel="apple-touch-icon" href="logo192.png" />
<!-- <!--
manifest.json provides metadata used when your web app is installed on a manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
--> -->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <link rel="manifest" href="manifest.json" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-icons/1.11.3/font/bootstrap-icons.min.css" integrity="sha512-dPXYcDub/aeb08c63jRq/k6GaKccl256JQy/AnOq7CAnEZ9FzSL9wSbcZkMp4R26vBsMLFYH4kQ67/bbV8XaCQ==" crossorigin="anonymous" referrerpolicy="no-referrer" /> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-icons/1.11.3/font/bootstrap-icons.min.css" integrity="sha512-dPXYcDub/aeb08c63jRq/k6GaKccl256JQy/AnOq7CAnEZ9FzSL9wSbcZkMp4R26vBsMLFYH4kQ67/bbV8XaCQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<script async src="https://tally.so/widgets/embed.js"></script>
<!-- Google tag (gtag.js) --> <!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=%REACT_APP_ANALYTICS_SCRIPT_ID%"></script> <script async src="https://www.googletagmanager.com/gtag/js?id=<%= REACT_APP_ANALYTICS_SCRIPT_ID %>"></script>
<script> <script>
window.dataLayer = window.dataLayer || []; window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);} function gtag(){dataLayer.push(arguments);}
gtag('js', new Date()); gtag('js', new Date());
gtag('config', '%REACT_APP_ANALYTICS_SCRIPT_ID%'); gtag('config', '<%= REACT_APP_ANALYTICS_SCRIPT_ID %>');
</script> </script>
<!-- <!--
Notice the use of %PUBLIC_URL% in the tags above. Notice the use of %PUBLIC_URL% in the tags above.
@@ -42,5 +44,6 @@
<div id="root"></div> <div id="root"></div>
</body> </body>
<!-- Author: Paul Github: PaulleDemon --> <!-- Author: Paul Github: PaulleDemon -->
</html> </html>

View File

@@ -1,6 +1,6 @@
{ {
"short_name": "React App", "short_name": "PyUiBuilder",
"name": "Create React App Sample", "name": "PyUiBuilder - Python GUI Builder",
"icons": [ "icons": [
{ {
"src": "favicon.ico", "src": "favicon.ico",

View File

@@ -9,21 +9,21 @@ Any feature that has 👑 beside it, is meant only for [premium users](./README.
### 1.2.0 ### 1.2.0
- [ ] UI fixes and enhancement - [ ] UI fixes and enhancement
- [ ] Documentation - [ ] Documentation
- [ ] Tree view for elements on the canvas - [X] Tree view for elements on the canvas
- [ ] Add text editor to support event handlers - [X] Add text editor to support event handlers
- [ ] Support for Event handlers - [X] Support for Event handlers
- [ ] Support more pre-built widgets such as ttk.Notebook, multi-page etc. - [X] Support more pre-built widgets such as ttk.Notebook, multi-page etc.
- [ ] Rewrite DND for better feedback - (swappy/react-dnd-kit/ GSAP draggable) - [ ] Rewrite DND for better feedback - (swappy/react-dnd-kit/ GSAP draggable) - least priority
- [ ] Duplicate widgets - [ ] Duplicate widgets
### 1.5.0 ### 1.5.0
- [ ] Add canvas support tools (lines, rect etc) (try fabricjs) - [ ] Add canvas support tools (lines, rect etc) (try fabricjs)
- [ ] Initial version for Electron App exe 👑 - [X] Initial version for Electron App exe 👑
- [ ] Save files locally 👑 - [X] Save files locally 👑
- [ ] Load UI files 👑 - [X] Load UI files 👑
- [ ] Light/Dark theme 👑 - [X] Light/Dark theme 👑
- [ ] Run the preview 👑 - [X] Run the preview 👑
- [ ] Load templates 👑 - [X] Load templates 👑
### 2.0.0 ### 2.0.0
- [ ] Support for more third party plugins - [ ] Support for more third party plugins
@@ -31,11 +31,8 @@ Any feature that has 👑 beside it, is meant only for [premium users](./README.
- [ ] Allow creating components - [ ] Allow creating components
- [ ] Support for Kivy - [ ] Support for Kivy
- [ ] Sharable Templates - [ ] Sharable Templates
- [ ] Dark theme 👑
### 3.0.0 ### 3.0.0
- [ ] Allow 3rd party UI plugin developers - [ ] Allow 3rd party UI plugin developers
- [ ] Allow Templates to be sharable - [ ] Allow Templates to be sharable
- [ ] Node based System (eg: like blender/ unity node system) - [ ] Support for PySide / PyQt 👑 (commercial license only)
- [ ] Support for PySide / PyQt 👑 (commercial license only)

View File

@@ -4,7 +4,7 @@
*/ */
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { LayoutFilled, ProductFilled, CloudUploadOutlined, DatabaseFilled } from "@ant-design/icons" import { LayoutFilled, ProductFilled, CloudUploadOutlined, DatabaseFilled, AlignLeftOutlined } from "@ant-design/icons"
// import { DndContext, useSensors, useSensor, PointerSensor, closestCorners, DragOverlay, rectIntersection } from '@dnd-kit/core' // import { DndContext, useSensors, useSensor, PointerSensor, closestCorners, DragOverlay, rectIntersection } from '@dnd-kit/core'
// import { snapCenterToCursor } from '@dnd-kit/modifiers' // import { snapCenterToCursor } from '@dnd-kit/modifiers'
@@ -31,6 +31,10 @@ import generateTkinterCode from './frameworks/tkinter/engine/code'
import TkMainWindow from './frameworks/tkinter/widgets/mainWindow' import TkMainWindow from './frameworks/tkinter/widgets/mainWindow'
import CTkMainWindow from './frameworks/customtk/widgets/mainWindow' import CTkMainWindow from './frameworks/customtk/widgets/mainWindow'
import TreeviewContainer from './sidebar/treeviewContainer'
import { WidgetContextProvider } from './canvas/context/widgetContext'
import { isChromium } from './utils/system'
import { Modal } from 'antd'
function App() { function App() {
@@ -42,6 +46,8 @@ function App() {
const [projectName, setProjectName] = useState('untitled project') const [projectName, setProjectName] = useState('untitled project')
const [UIFramework, setUIFramework] = useState(FrameWorks.TKINTER) const [UIFramework, setUIFramework] = useState(FrameWorks.TKINTER)
const [shownNotChromiumAlert, setShownNotChromiumAlert] = useState(false) // if the user isn't using a chromium based browser alerts the user
// const [uploadedAssets, setUploadedAssets] = useState([]) // a global storage for assets, since redux can't store files(serialize files) // const [uploadedAssets, setUploadedAssets] = useState([]) // a global storage for assets, since redux can't store files(serialize files)
@@ -53,6 +59,28 @@ function App() {
// NOTE: the below reference is no longer required // NOTE: the below reference is no longer required
const [canvasWidgets, setCanvasWidgets] = useState([]) // contains the reference to the widgets inside the canvas const [canvasWidgets, setCanvasWidgets] = useState([]) // contains the reference to the widgets inside the canvas
useEffect(() => {
if (!shownNotChromiumAlert){
// this modal may rerender twice only in dev mode because of how react works
isChromium().then((isChrome) => {
if (!isChrome){
Modal.warning({
title: "Use Chromium browser",
onOk: () => setShownNotChromiumAlert(true),
content: (<span>We recommend using Chromium based browser such as Chrome, Brave, Edge etc for best results.
<br />
Join us on
<a href="https://discord.gg/dHXjrrCA7G" target='_blank' rel='noreferrer noopener'> Discord</a> for help and updates</span>)
})
}
})
}
setShownNotChromiumAlert(true)
}, [shownNotChromiumAlert])
const sidebarTabs = [ const sidebarTabs = [
{ {
name: "Widgets", name: "Widgets",
@@ -64,6 +92,11 @@ function App() {
icon: <ProductFilled />, icon: <ProductFilled />,
content: <PluginsContainer sidebarContent={sidebarPlugins}/> content: <PluginsContainer sidebarContent={sidebarPlugins}/>
}, },
{
name: "Tree view",
icon: <AlignLeftOutlined />,
content: <TreeviewContainer />
},
{ {
name: "Uploads", name: "Uploads",
icon: <CloudUploadOutlined />, icon: <CloudUploadOutlined />,
@@ -78,15 +111,59 @@ function App() {
useEffect(() => { useEffect(() => {
if (!canvasRef)
return
const canvasBoundingBox = canvasRef.current.getCanvasContainerBoundingRect()
const canvasCenterX = (canvasBoundingBox.width - canvasBoundingBox.left) / 2
const canvasCenterY = (canvasBoundingBox.height - canvasBoundingBox.top) / 2
// console.log("position: ", TkMainWindow.)
if (UIFramework === FrameWorks.TKINTER){ if (UIFramework === FrameWorks.TKINTER){
canvasRef?.current?.createWidget(TkMainWindow)
const widgetCenterX = (TkMainWindow.initialSize.width - canvasBoundingBox.left) / 2
const widgetCenterY = (TkMainWindow.initialSize.height - canvasBoundingBox.top) / 2
canvasRef?.current?.createWidget(TkMainWindow, {x: canvasCenterX - widgetCenterX, y: canvasCenterY - widgetCenterY}, ({id, widgetRef}) => {
// center the widget when adding to canvas
if (!widgetRef.current){
return
}
const widgetBoundingBox = widgetRef.current?.getBoundingRect()
const widgetCenterX = (widgetBoundingBox.width - widgetBoundingBox.left) / 2
const widgetCenterY = (widgetBoundingBox.height - widgetBoundingBox.top) / 2
// widgetRef.current?.setPos(canvasCenterX-widgetCenterX, canvasCenterY-widgetCenterY)
})
}else if (UIFramework === FrameWorks.CUSTOMTK){ }else if (UIFramework === FrameWorks.CUSTOMTK){
canvasRef?.current?.createWidget(CTkMainWindow)
const widgetCenterX = (TkMainWindow.initialSize.width - canvasBoundingBox.left) / 2
const widgetCenterY = (TkMainWindow.initialSize.height - canvasBoundingBox.top) / 2
canvasRef?.current?.createWidget(CTkMainWindow, {x: canvasCenterX - widgetCenterX, y: canvasCenterY - widgetCenterY}, ({id, widgetRef}) => {
// center the widget when adding to canvas
if (!widgetRef.current){
return
}
const widgetBoundingBox = widgetRef.current?.getBoundingRect()
const widgetCenterX = (widgetBoundingBox.width - widgetBoundingBox.left) / 2
const widgetCenterY = (widgetBoundingBox.height - widgetBoundingBox.top) / 2
// widgetRef.current?.setPos(canvasCenterX-widgetCenterX, canvasCenterY-widgetCenterY)
})
} }
}, [UIFramework]) }, [UIFramework, canvasRef])
// const handleDragStart = (event) => { // const handleDragStart = (event) => {
// console.log("Drag start: ", event) // console.log("Drag start: ", event)
@@ -207,17 +284,21 @@ function App() {
onOk={handleOk} okType={okButtonType} onCancel={handleCancel}> onOk={handleOk} okType={okButtonType} onCancel={handleCancel}>
<p>Are you sure you want to change the framework? This will clear the canvas.</p> <p>Are you sure you want to change the framework? This will clear the canvas.</p>
</Modal> */} </Modal> */}
<WidgetContextProvider>
<DragProvider> <DragProvider>
<div className="tw-w-full tw-h-[94vh] tw-flex"> <div className="tw-w-full tw-h-[94vh] tw-flex">
<Sidebar tabs={sidebarTabs}/> <Sidebar tabs={sidebarTabs}/>
{/* <ActiveWidgetProvider> */} {/* <ActiveWidgetProvider> */}
<Canvas ref={canvasRef} widgets={canvasWidgets} onWidgetAdded={handleWidgetAddedToCanvas}/> <Canvas ref={canvasRef} widgets={canvasWidgets}
/>
{/* </ActiveWidgetProvider> */} {/* </ActiveWidgetProvider> */}
</div> </div>
{/* dragOverlay (dnd-kit) helps move items from one container to another */} {/* dragOverlay (dnd-kit) helps move items from one container to another */}
</DragProvider> </DragProvider>
</WidgetContextProvider>
</div> </div>
) )
} }

View File

@@ -9,13 +9,13 @@ some parts under a different non-commercial use license.
----------------------- -----------------------
License in short License in short
* You are free fork, modify and redistribute it given the original or derived works is kept free and open-source and under the same licese. * You are free fork, modify and redistribute it given the original or derived works is kept free and source-available and under the same license.
* Another restriction is you can't redistribute in binary or executable formats without asking me first. * Another restriction is you can't redistribute in binary or executable formats without asking me first.
* If you plan on making money on this work or derived work you need permission first. * If you plan on making money on this work or derived work you need permission first.
* No AI Training on this source code
License was planned to be kept under one AGPL or less restrictive license, but since its difficult for me to continue providing free and open-source software without proper funding. It's under this license. License was planned to be kept under one AGPL or less restrictive license, but since its difficult for me to continue providing free and source-available software without proper funding. It's under this license.
Once there enough money to fund this project and my upcoming FOSS projects, The license will be brought back to a single AGPL or much lesser restrictive license. The fastest way to make this happen is by purchasing a one-time license or fund me.
Read the full license below. Read the full license below.
-------------------- --------------------
@@ -24,7 +24,7 @@ Read the full license below.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to use, copy, modify, merge, publish, and distribute the source code, subject to the following conditions: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to use, copy, modify, merge, publish, and distribute the source code, subject to the following conditions:
2. Derived Work License 2. Derived Work License
Any modifications, derivative works, or copies of the Software must retain this license. All derivative works must be open-sourced under the same license terms. You are not permitted to relicense or sub-license any portion of the Software under a different license without explicit prior written permission from the original author. Any modifications, derivative works, or copies of the Software must retain this license. All derivative works must be source-available under the same license terms. You are not permitted to relicense or sub-license any portion of the Software under a different license without explicit prior written permission from the original author.
3. Distribution Restriction on Executables 3. Distribution Restriction on Executables
You are not permitted to distribute the Software in compiled, executable, or binary form without prior written consent from the original author. This applies to both original and derivative works. You are not permitted to distribute the Software in compiled, executable, or binary form without prior written consent from the original author. This applies to both original and derivative works.

File diff suppressed because it is too large Load Diff

View File

@@ -11,6 +11,8 @@ const Tools = {
LAYOUT_MANAGER: "layout_manager", LAYOUT_MANAGER: "layout_manager",
UPLOADED_LIST: "uploaded_list", UPLOADED_LIST: "uploaded_list",
CUSTOM: "Custom"
} }

View File

@@ -0,0 +1,18 @@
import React, { createContext, useContext, useState } from 'react';
const selectedWidgetContext = createContext()
export const useSelectedWidgetContext = () => useContext(selectedWidgetContext)
// NOTE: not in use
export const SelectedWidgetProvider = ({ children }) => {
const [selectedWidget, setSelectedWidget] = useState(null)
// const []
return (
<selectedWidgetContext.Provider value={{ selectedWidget, setSelectedWidget }}>
{children}
</selectedWidgetContext.Provider>
)
}

View File

@@ -1,35 +1,23 @@
import React, { createContext, Component } from 'react' import React, { createContext, useContext, useEffect, useRef, useState } from 'react'
const WidgetContext = createContext() export const WidgetContext = createContext()
// NOTE: Don't use this context provider export const useWidgetContext = () => useContext(WidgetContext)
class WidgetProvider extends Component {
state = {
activeWidget: null, // Keeps track of the active widget's data
widgetMethods: null, // Function to update active widget's state
}
setActiveWidget = (widgetData, widgetMethods) => { export const WidgetContextProvider = ({ children }) => {
this.setState({
activeWidget: widgetData, const [activeWidget, setActiveWidget] = useState(null)
widgetMethods: widgetMethods, // Store the update function of the active widget const [widgets, setWidgets] = useState([]) // stores the mapping to widgetRefs, stores id and WidgetType, later used for rendering [{id: , widgetType: WidgetClass, children: [], parent: "", initialData: {}}]
})
}
render() { // don't useState here because the refs are changing often
return ( const widgetRefs = useRef({}) // stores the actual refs to the widgets inside the canvas {id: ref, id2, ref2...}
<WidgetContext.Provider
value={{
activeWidget: this.state.activeWidget, return (
setActiveWidget: this.setActiveWidget, <WidgetContext.Provider value={{ widgets, setWidgets, widgetRefs,
widgetMethods: this.state.widgetMethods, // Expose the update function activeWidget, setActiveWidget }}>
}} {children}
> </WidgetContext.Provider>
{this.props.children} )
</WidgetContext.Provider>
)
}
} }
export { WidgetContext, WidgetProvider }

View File

@@ -16,6 +16,8 @@ const ResizeWidgetContainer = ({selectedWidget, onResize}) => {
const [pos, setPos] = useState({x: 0, y: 0}) const [pos, setPos] = useState({x: 0, y: 0})
const [size, setSize] = useState({width: 0, height: 0}) const [size, setSize] = useState({width: 0, height: 0})
console.log("selected widget: ", selectedWidget)
useEffect(() => { useEffect(() => {
if (selectedWidget){ if (selectedWidget){
@@ -25,15 +27,15 @@ const ResizeWidgetContainer = ({selectedWidget, onResize}) => {
console.log("selected widget resizable: ", selectedWidget) console.log("selected widget resizable: ", selectedWidget)
}, [selectedWidget, selectedWidget?.getPos(), selectedWidget?.getSize()]) }, [selectedWidget])
console.log("pos: ", pos, size)
return ( return (
<div className={`tw-absolute tw-bg-transparent tw-top-[-20px] tw-left-[-20px] tw-opacity-100 <div className={`tw-absolute tw-opacity-100
tw-w-full tw-h-full tw-z-[-1] tw-border-2 tw-border-solid tw-border-blue-500`} tw-w-full tw-h-full tw-z-[2] tw-border-2 tw-border-solid tw-border-blue-500 tw-bg-red-300`}
style={{ style={{
top: `${pos.y - 40}px`, top: `${pos.y - 20}px`,
left: `${pos.x - 20}px`, left: `${pos.x - 20}px`,
width: `${size.width + 40}px`, width: `${size.width + 40}px`,
height: `${size.height + 40}px`, height: `${size.height + 40}px`,

View File

@@ -1,4 +1,4 @@
import { memo, useEffect, useMemo, useState } from "react" import { memo, useEffect, useMemo, useRef, useState } from "react"
import { import {
Checkbox, ColorPicker, Input, Checkbox, ColorPicker, Input,
@@ -12,26 +12,67 @@ import { Layouts } from "./constants/layouts.js"
import { DynamicRadioInputList } from "../components/inputs.js" import { DynamicRadioInputList } from "../components/inputs.js"
import { useFileUploadContext } from "../contexts/fileUploadContext.js" import { useFileUploadContext } from "../contexts/fileUploadContext.js"
import { AudioOutlined, FileImageOutlined, FileTextOutlined, VideoCameraOutlined } from "@ant-design/icons" import { AudioOutlined, FileImageOutlined, FileTextOutlined, VideoCameraOutlined } from "@ant-design/icons"
import { useWidgetContext } from "./context/widgetContext.js"
// FIXME: Maximum recursion error
// FIXME: Every time the parent attrs are changed a remount happens, which causes input cursor to go to the end
/** /**
* *
* @param {boolean} isOpen * @param {boolean} isOpen
* @param {string} widgetType * @param {string} widgetType
* @param {object} attrs - widget attributes * @param {object} attrs - widget attributes
*/ */
const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => { const CanvasToolBar = memo(({ isOpen, widgetType, }) => {
// const { activeWidgetAttrs } = useActiveWidget() // const { activeWidgetAttrs } = useActiveWidget()
const {activeWidget} = useWidgetContext()
// console.log("active widget context: ", activeWidgetAttrs) // console.log("active widget context: ", activeWidgetAttrs)
const [toolbarOpen, setToolbarOpen] = useState(isOpen) const [toolbarOpen, setToolbarOpen] = useState(isOpen)
const [toolbarAttrs, setToolbarAttrs] = useState(attrs) const [toolbarAttrs, setToolbarAttrs] = useState({})
const focusedInputRef = useRef()
const [cursorPos, setCursorPos] = useState(0) // store cursor position for focused input so during remount if the cursor position goes to end we can use this
const [activeInputKey, setActiveInputKey] = useState(null)
const {uploadedAssets} = useFileUploadContext() const {uploadedAssets} = useFileUploadContext()
useEffect(() => {
const stateUpdatedCallback = () => {
setToolbarAttrs(activeWidget.getToolbarAttrs())
}
if (activeWidget){
activeWidget.stateChangeSubscriberCallback(stateUpdatedCallback)
// console.log("sytate update: ", activeWidget.getToolbarAttrs())
setToolbarAttrs(activeWidget.getToolbarAttrs())
}
}, [activeWidget, focusedInputRef, cursorPos]) // , activeWidget?.state
useEffect(() => {
setToolbarOpen(isOpen)
}, [isOpen])
useEffect(() => {
if (focusedInputRef.current?.input && activeInputKey) {
// this fixes the cursor going to the end issue during remount
focusedInputRef.current.setSelectionRange(cursorPos, cursorPos)
}
}, [toolbarAttrs, focusedInputRef, cursorPos])
// useEffect(() => {
// setToolbarAttrs(activeWidget.getToolbarAttrs())
// }, [])
const uploadItems = useMemo(() => { const uploadItems = useMemo(() => {
const returnComponentBasedOnFileType = (file) => { const returnComponentBasedOnFileType = (file) => {
@@ -81,19 +122,30 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => {
}, [uploadedAssets]) }, [uploadedAssets])
useEffect(() => { const handleInputFocus = (key, event) => {
setToolbarOpen(isOpen)
}, [isOpen])
useEffect(() => { const {value, target} = event
setToolbarAttrs(attrs)
}, [attrs])
setActiveInputKey(key)
setCursorPos(target.selectionStart)
focusedInputRef.current = event.target
}
const handleInputBlur = () => {
setActiveInputKey(null);
focusedInputRef.current = null;
}
const handleChange = (value, callback) => { const handleChange = (value, callback) => {
if (callback) { if (callback) {
callback(value) callback(value)
} }
if (focusedInputRef.current?.input) {
setCursorPos(focusedInputRef.current.input.selectionStart)
}else{
setCursorPos(0)
}
} }
function getUploadFileFromName(name){ function getUploadFileFromName(name){
@@ -119,6 +171,7 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => {
placeholder="select content" placeholder="select content"
showSearch showSearch
className="tw-w-full" className="tw-w-full"
notFoundContent="Start uploading from uploads tab"
value={val.value?.name || ""} value={val.value?.name || ""}
onChange={(value) => { onChange={(value) => {
@@ -165,7 +218,7 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => {
onChange={(value) => handleChange({ ...val.value, direction: value }, val.onChange)} onChange={(value) => handleChange({ ...val.value, direction: value }, val.onChange)}
/> />
</div> </div>
<div className="tw-flex tw-flex-col tw-gap-1"> {/* <div className="tw-flex tw-flex-col tw-gap-1">
<span className="tw-text-sm">Align items</span> <span className="tw-text-sm">Align items</span>
<Select <Select
options={[ options={[
@@ -178,7 +231,7 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => {
placeholder={`${val.label}`} placeholder={`${val.label}`}
onChange={(value) => handleChange({ ...val.value, align: value }, val.onChange)} onChange={(value) => handleChange({ ...val.value, align: value }, val.onChange)}
/> />
</div> </div> */}
<div className="tw-flex tw-flex-col"> <div className="tw-flex tw-flex-col">
<span className="tw-text-sm">Gap</span> <span className="tw-text-sm">Gap</span>
<InputNumber <InputNumber
@@ -186,6 +239,9 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => {
min={1} min={1}
value={val.value?.gap || 10} value={val.value?.gap || 10}
size="small" size="small"
onFocus={(e) => handleInputFocus(val.label, e)}
onBlur={handleInputBlur}
ref={activeInputKey === val.label ? focusedInputRef : null}
onChange={(value) => { onChange={(value) => {
handleChange({ ...val.value, gap: value }, val.onChange) handleChange({ ...val.value, gap: value }, val.onChange)
}} }}
@@ -235,6 +291,18 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => {
} }
const renderCustomTool = (val) => {
return (
// NOTE: custom components must accept value and onChange
<val.Component
{...val.toolProps}
value={val.value}
onChange={(value) => handleChange(value, val.onChange)}>
</val.Component>
)
}
const renderTool = (keyName, val) => { const renderTool = (keyName, val) => {
return ( return (
@@ -242,6 +310,10 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => {
{val.tool === Tools.INPUT && ( {val.tool === Tools.INPUT && (
<Input <Input
{...val.toolProps} {...val.toolProps}
// value={val.value}
onFocus={(event) => handleInputFocus(val.label, event)}
onBlur={handleInputBlur}
ref={activeInputKey === val.label ? focusedInputRef : null}
value={val.value} value={val.value}
onChange={(e) => handleChange(e.target.value, val.onChange)} onChange={(e) => handleChange(e.target.value, val.onChange)}
/> />
@@ -252,6 +324,9 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => {
{...val.toolProps} {...val.toolProps}
value={val.value || 0} value={val.value || 0}
size="small" size="small"
onFocus={(event) => handleInputFocus(val.label, event)}
onBlur={handleInputBlur}
ref={activeInputKey === val.label ? focusedInputRef : null}
onChange={(value) => handleChange(value, val.onChange)} onChange={(value) => handleChange(value, val.onChange)}
/> />
)} )}
@@ -277,7 +352,7 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => {
showSearch showSearch
value={val.value || ""} value={val.value || ""}
placeholder={`${val.label}`} placeholder={`${val.label}`}
className="tw-w-full" className="tw-w-full tw-min-w-[80px]"
filterOption={(input, option) => filterOption={(input, option) =>
(option?.label ?? '').toLowerCase().includes(input.toLowerCase()) (option?.label ?? '').toLowerCase().includes(input.toLowerCase())
} }
@@ -294,6 +369,7 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => {
)} )}
{val.tool === Tools.INPUT_RADIO_LIST && ( {val.tool === Tools.INPUT_RADIO_LIST && (
// FIXME: problem with maximum recursion error
<DynamicRadioInputList <DynamicRadioInputList
defaultInputs={val.value.inputs} defaultInputs={val.value.inputs}
defaultSelected={val.value.selectedRadio} defaultSelected={val.value.selectedRadio}
@@ -312,6 +388,12 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => {
) )
} }
{
val.tool === Tools.CUSTOM && (
renderCustomTool(val)
)
}
</> </>
) )
@@ -319,10 +401,12 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => {
const renderToolbar = (obj, parentKey = "", toolCount=0) => { const renderToolbar = (obj, parentKey = "", toolCount=0) => {
// console.log("obj: ", obj)
const keys = [] const keys = []
return Object.entries(obj).map(([key, val], i) => { return Object.entries(obj).map(([key, val], i) => {
const keyName = parentKey ? `${parentKey}.${key}` : key const keyName = parentKey ? `${parentKey}.${key}` : key
// console.log("obj2: ", key, val)
// Highlight outer labels in blue for first-level keys // Highlight outer labels in blue for first-level keys
const isFirstLevel = parentKey === "" const isFirstLevel = parentKey === ""
@@ -331,6 +415,10 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => {
? "tw-text-sm tw-text-black tw-font-medium" ? "tw-text-sm tw-text-black tw-font-medium"
: "tw-text-sm" : "tw-text-sm"
if (!val?.label){
return null
}
// Render tool widgets // Render tool widgets
if (typeof val === "object" && val.tool) { if (typeof val === "object" && val.tool) {
@@ -338,7 +426,7 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => {
if (isFirstLevel){ if (isFirstLevel){
return ( return (
<Collapse key={keyName} ghost defaultActiveKey={keys}> <Collapse key={keyName} defaultActiveKey={keys}>
<Collapse.Panel header={val.label} key={keyName}> <Collapse.Panel header={val.label} key={keyName}>
{renderTool(keyName, val)} {renderTool(keyName, val)}
</Collapse.Panel> </Collapse.Panel>
@@ -367,7 +455,7 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => {
if (isFirstLevel){ if (isFirstLevel){
return ( return (
<Collapse key={keyName} ghost defaultActiveKey={keys}> <Collapse key={keyName} defaultActiveKey={keys}>
<Collapse.Panel header={val.label} key={keyName}> <Collapse.Panel header={val.label} key={keyName}>
<div className={`${containerClass} tw-px-2`}> <div className={`${containerClass} tw-px-2`}>
{renderToolbar(val, keyName, toolCount+1)} {renderToolbar(val, keyName, toolCount+1)}
@@ -394,18 +482,20 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => {
return ( return (
<div <div
className={`tw-absolute tw-top-20 tw-right-5 tw-bg-white className={`tw-absolute tw-top-10 tw-right-5 tw-bg-white
${toolbarOpen ? "tw-translate-x-0" : "tw-translate-x-full"} ${toolbarOpen ? "tw-translate-x-0" : "tw-translate-x-full"}
tw-w-[280px] tw-px-3 tw-p-2 tw-h-[600px] tw-rounded-md tw-z-[1000] tw-shadow-lg tw-w-[280px] tw-px-3 tw-p-2 tw-h-[80vh] tw-rounded-md tw-z-[1000] tw-shadow-lg
tw-transition-transform tw-duration-[0.3s] tw-overflow-x-hidden tw-transition-transform tw-duration-[0.3s] tw-overflow-x-hidden
tw-flex tw-flex-col tw-gap-2 tw-overflow-y-auto`} tw-flex tw-flex-col tw-gap-2 tw-overflow-y-auto tw-border-[2px]
tw-border-solid tw-border-gray-300`}
style={{ style={{
transform: toolbarOpen ? "translateX(0)" : "translateX(calc(100% + 50px))" transform: toolbarOpen ? "translateX(0)" : "translateX(calc(100% + 50px))"
}} }}
> >
<h3 className="tw-text-lg tw-text-center"> <h3 className="tw-text-lg tw-text-center tw-bg-[#FAFAFA] tw-border-[1px] tw-border-solid tw-border-[#D9D9D9]
{capitalize(`${widgetType || ""}`).replace(/_/g, " ")} tw-p-1 tw-px-2 tw-rounded-md tw-font-medium">
{capitalize(`${activeWidget?.getDisplayName() || ""}`).replace(/_/g, " ")}
</h3> </h3>
<div className="tw-flex tw-flex-col tw-gap-2"> <div className="tw-flex tw-flex-col tw-gap-2">

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@ import { useDragWidgetContext } from "./draggableWidgetContext"
import { useDragContext } from "../../components/draggable/draggableContext" import { useDragContext } from "../../components/draggable/draggableContext"
// NOTE: not in use
// FIXME: sometimes even after drag end the showDroppable is visible // FIXME: sometimes even after drag end the showDroppable is visible
/** /**
* @param {} - widgetRef - the widget ref for your widget * @param {} - widgetRef - the widget ref for your widget

View File

@@ -1,13 +1,17 @@
import { useEffect, useMemo, useRef } from "react" import { memo, useEffect, useMemo, useRef, useState } from "react"
import Draggable from "./utils/draggableDnd" import Draggable from "./utils/draggableDnd"
import { Button } from "antd"
import { GithubOutlined, GitlabOutlined, LinkOutlined, import { GithubOutlined, GitlabOutlined, LinkOutlined,
AudioOutlined, FileTextOutlined, AudioOutlined, FileTextOutlined,
DeleteFilled, DeleteFilled,
DeleteOutlined, DeleteOutlined,
GlobalOutlined} from "@ant-design/icons" GlobalOutlined,
EyeOutlined,
EyeInvisibleOutlined} from "@ant-design/icons"
import DraggableWrapper from "./draggable/draggable" import DraggableWrapper from "./draggable/draggable"
import { Button } from "antd" import { useWidgetContext } from "../canvas/context/widgetContext"
export function SidebarWidgetCard({ name, img, url, license, widgetClass, innerRef}){ export function SidebarWidgetCard({ name, img, url, license, widgetClass, innerRef}){
@@ -38,7 +42,7 @@ export function SidebarWidgetCard({ name, img, url, license, widgetClass, innerR
<div ref={innerRef} className="tw-select-none tw-h-[200px] tw-w-[230px] tw-flex tw-flex-col <div ref={innerRef} className="tw-select-none tw-h-[200px] tw-w-[230px] tw-flex tw-flex-col
tw-rounded-md tw-overflow-hidden tw-rounded-md tw-overflow-hidden
tw-gap-2 tw-text-gray-600 tw-bg-[#ffffff44] tw-border-solid tw-border-[1px] tw-gap-2 tw-text-gray-600 tw-bg-[#ffffff44] tw-border-solid tw-border-[1px]
tw-border-blue-500 tw-shadow-md"> tw-border-gray-500 tw-shadow-md">
<div className="tw-h-[200px] tw-pointer-events-none 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" /> <img src={img} alt={name} className="tw-object-contain tw-h-full tw-w-full tw-select-none" />
</div> </div>
@@ -148,4 +152,50 @@ export function DraggableAssetCard({file, onDelete}){
</div> </div>
) )
} }
export const TreeViewCard = memo(({widgetRef, title, isTopLevel}) => {
const [widgetVisible, setWidgetVisible] = useState(widgetRef.current.isWidgetVisible)
const {activeWidget} = useWidgetContext()
const onDelete = () => {
widgetRef.current.deleteWidget()
}
const toggleHideShowWidget = () => {
setWidgetVisible(!widgetRef.current.isWidgetVisible())
if (widgetRef.current.isWidgetVisible())
widgetRef.current.hideFromViewport()
else{
widgetRef.current.unHideFromViewport()
}
}
const handleSingleClick = () => {
activeWidget?.deSelect()
widgetRef.current.select()
}
return (
<div className="tw-flex tw-place-items-center tw-px-2 tw-p-1 tw-place-content-between
tw-gap-4 tw-w-full" style={{width: "100%"}}
onClick={handleSingleClick}
onDoubleClick={() => widgetRef?.current.panToWidget()}
>
<div className={`tw-text-sm ${isTopLevel ? "tw-font-medium" : ""}`}>
{title}
</div>
<div className="tw-ml-auto tw-flex tw-gap-1">
<Button color="danger" title="delete" onClick={onDelete} size="small" variant="text" danger
icon={<DeleteOutlined />}></Button>
<Button variant="text" type="text" title="hide" onClick={toggleHideShowWidget} size="small"
icon={widgetVisible ? <EyeOutlined /> : <EyeInvisibleOutlined/>}></Button>
</div>
</div>
)
})

View File

@@ -0,0 +1,42 @@
License held by paul
Github username: PaulleDemon
Below is the License for source code of PyUIBuilder. For license regarding source code generated by PyUIBuilder Refer Readme.md file (it has basically no restrictions).
The source code for PyUIBuilder is Dual licensed, most parts of the code comes under AGPL,
some parts under a different non-commercial use license.
-----------------------
License in short
* You are free fork, modify and redistribute it given the original or derived works is kept free and source-available and under the same license.
* Another restriction is you can't redistribute in binary or executable formats without asking me first.
* If you plan on making money on this work or derived work you need permission first.
* No AI Training on this source code
License was planned to be kept under one AGPL or less restrictive license, but since its difficult for me to continue providing free and source-available software without proper funding. It's under this license.
Read the full license below.
--------------------
1. License Grant
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to use, copy, modify, merge, publish, and distribute the source code, subject to the following conditions:
2. Derived Work License
Any modifications, derivative works, or copies of the Software must retain this license. All derivative works must be source-available under the same license terms. You are not permitted to relicense or sub-license any portion of the Software under a different license without explicit prior written permission from the original author.
3. Distribution Restriction on Executables
You are not permitted to distribute the Software in compiled, executable, or binary form without prior written consent from the original author. This applies to both original and derivative works.
4. Commercial Use Restriction
The Software, in its original or modified form, cannot be used for any commercial purpose without prior written permission from the original author. This includes selling, licensing, or offering the Software as part of a service.
5. Non-Commercial Use
You may freely use, copy, modify, and distribute the source code of the Software for non-commercial purposes, provided that the same terms of this license are maintained in all copies or derivative works.
6. Changes to the License
The original author reserves the right to modify, amend, or update this license at any time. Any changes will apply only to future versions of the Software. Any prior versions of the Software, including derivative works, will continue to be governed by the version of the license in effect at the time the work was created.
7. Disclaimer
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -7,12 +7,25 @@ import { useDragContext } from "./draggableContext"
* @param {string} - dragElementType - this will set the data-draggable-type which can be accessed on droppable to check if its allowed or not * @param {string} - dragElementType - this will set the data-draggable-type which can be accessed on droppable to check if its allowed or not
* @returns * @returns
*/ */
const DraggableWrapper = memo(({dragElementType, dragWidgetClass=null, className, children, ...props}) => { const DraggableWrapper = memo(({dragElementType, dragWidgetClass=null, currentPos, className, children, ...props}) => {
const { onDragStart, onDragEnd } = useDragContext() const { onDragStart, onDragEnd, setPosMetaData } = useDragContext()
const draggableRef = useRef(null) const draggableRef = useRef(null)
const setInitialPos = (e) => {
const {clientX, clientY} = e
const draggableRect = draggableRef.current.getBoundingClientRect()
const posMetaData = {
dragStartCursorPos: {x: clientX, y: clientY},
initialPos: {x: draggableRect.left, y: draggableRect.top}
}
setPosMetaData(posMetaData)
}
/** /**
* *
* @param {DragEvent} event * @param {DragEvent} event
@@ -38,6 +51,8 @@ const DraggableWrapper = memo(({dragElementType, dragWidgetClass=null, className
onDragStart={handleDragStart} onDragStart={handleDragStart}
onDragEnd={handleDragEnd} onDragEnd={handleDragEnd}
ref={draggableRef} ref={draggableRef}
onPointerDown={setInitialPos}
onMouseDown={setInitialPos}
{...props} {...props}
> >
{children} {children}

View File

@@ -11,8 +11,12 @@ export const DragProvider = ({ children }) => {
const [draggedElement, setDraggedElement] = useState(null) const [draggedElement, setDraggedElement] = useState(null)
const [overElement, setOverElement] = useState(null) // the element the dragged items is over const [overElement, setOverElement] = useState(null) // the element the dragged items is over
const [widgetClass, setWidgetClass] = useState(null) // helper to help pass the widget type from sidebar to canvas const [posMetaData, setPosMetaData] = useState({dragStartCursorPos: {x: 0, y: 0},
initialPos: {x: 0, y: 0}})
const [widgetClass, setWidgetClass] = useState(null) // helper to help pass the widget type from sidebar to canvas
const onDragStart = (element, widgetClass=null) => { const onDragStart = (element, widgetClass=null) => {
setDraggedElement(element) setDraggedElement(element)
@@ -29,7 +33,8 @@ export const DragProvider = ({ children }) => {
return ( return (
<DragContext.Provider value={{ draggedElement, overElement, setOverElement, <DragContext.Provider value={{ draggedElement, overElement, setOverElement,
widgetClass, onDragStart, onDragEnd }}> widgetClass, onDragStart, onDragEnd, posMetaData,
setPosMetaData }}>
{children} {children}
</DragContext.Provider> </DragContext.Provider>
) )

View File

@@ -8,7 +8,7 @@ import { useDragContext } from "./draggableContext"
const DroppableWrapper = memo(({onDrop, droppableTags={}, ...props}) => { const DroppableWrapper = memo(({onDrop, droppableTags={}, ...props}) => {
const { draggedElement, overElement, setOverElement, widgetClass } = useDragContext() const { draggedElement, posMetaData, setOverElement, widgetClass } = useDragContext()
const [showDroppable, setShowDroppable] = useState({ const [showDroppable, setShowDroppable] = useState({
show: false, show: false,
@@ -101,7 +101,7 @@ const DroppableWrapper = memo(({onDrop, droppableTags={}, ...props}) => {
)) ))
if(onDrop && allowDrop){ if(onDrop && allowDrop){
onDrop(e, draggedElement, widgetClass) onDrop(e, draggedElement, widgetClass, posMetaData)
} }
} }

View File

@@ -1,9 +1,10 @@
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { Select, Input, Button } from "antd" import { Select, Input, Button } from "antd"
import { CrownFilled, DownloadOutlined, DownOutlined } from "@ant-design/icons" import { CrownFilled, DownloadOutlined, DownOutlined, PlayCircleFilled, VideoCameraOutlined } from "@ant-design/icons"
import FrameWorks from "../constants/frameworks" import FrameWorks from "../constants/frameworks"
import Premium from "../sidebar/utils/premium" import Premium from "../sidebar/utils/premium"
import VideoPopUp from "./video-popup"
const items = [ const items = [
@@ -23,19 +24,36 @@ function Header({projectName, onProjectNameChange, framework, onFrameworkChange,
return ( return (
<div className={`tw-w-full tw-bg-primaryBg tw-p-2 tw-flex tw-place-items-center <div className={`tw-w-full tw-bg-primaryBg tw-gap-2 tw-overflow-x-auto tw-p-2 tw-flex tw-place-items-center
${className||''}`}> ${className||''}`}>
<Select <div className="tw-flex tw-gap-2 tw-place-content-center">
// defaultValue={framework} <Select
value={framework} // defaultValue={framework}
options={items} value={framework}
// onSelect={(key) => {console.log("value: ", key); onFrameworkChange(key); }} options={items}
onChange={(key) => {onFrameworkChange(key)}} // onSelect={(key) => {console.log("value: ", key); onFrameworkChange(key); }}
className="tw-min-w-[150px]" onChange={(key) => {onFrameworkChange(key)}}
/> className="tw-min-w-[150px]"
/>
<VideoPopUp>
<div className="tw-p-1 tw-w-full tw-outline-none tw-bg-transparent tw-border-[1px]
tw-border-gray-400 tw-rounded-md tw-no-underline tw-border-solid hover:tw-bg-[#9333EA]
hover:tw-text-white tw-duration-200 tw-flex tw-gap-1
tw-text-black tw-text-center tw-px-4 tw-text-sm tw-cursor-pointer">
Watch demo
<PlayCircleFilled className="tw-text-lg"/>
</div>
</VideoPopUp>
</div>
<div className="tw-ml-auto tw-flex tw-gap-2 tw-place-content-center"> <div className="tw-ml-auto tw-flex tw-gap-2 tw-place-content-center">
<button data-tally-open="mVDY7N" data-tally-layout="modal" data-tally-emoji-text="👋"
data-tally-emoji-animation="wave" className="tw-p-1 tw-w-full tw-outline-none tw-bg-transparent tw-border-[1px]
tw-border-gray-400 tw-rounded-md tw-no-underline tw-border-solid hover:tw-bg-[#9333EA]
hover:tw-text-white tw-duration-200
tw-text-black tw-text-center tw-px-4 tw-text-sm tw-cursor-pointer">
Get Updates
</button>
<Premium className="tw-text-2xl tw-bg-purple-600 tw-text-center <Premium className="tw-text-2xl tw-bg-purple-600 tw-text-center
tw-w-[40px] tw-min-w-[40px] tw-h-[35px] tw-rounded-md tw-w-[40px] tw-min-w-[40px] tw-h-[35px] tw-rounded-md
tw-cursor-pointer tw-text-white tw-cursor-pointer tw-text-white

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useState, useRef } from "react" import React, { useEffect, useState, useRef } from "react"
import { Input, Button, Space, Radio } from "antd" import { Input, Button, Space, Radio, InputNumber } from "antd"
import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons" import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons"
import { SearchOutlined, CloseCircleFilled } from '@ant-design/icons' import { SearchOutlined, CloseCircleFilled } from '@ant-design/icons'
@@ -87,19 +87,20 @@ export const DynamicInputList = () => {
} }
export const DynamicRadioInputList = React.memo(({defaultInputs=[""], defaultSelected=null, onChange}) => { export const DynamicRadioInputList = React.memo(({defaultInputs, defaultSelected=null, onChange}) => {
const [inputs, setInputs] = useState([""]) // Initialize with one input const [inputs, setInputs] = useState(defaultInputs || [""]) // Initialize with one input
const [selectedRadio, setSelectedRadio] = useState(null) // Tracks selected radio button const [selectedRadio, setSelectedRadio] = useState(defaultSelected) // Tracks selected radio button
useEffect(() => { useEffect(() => {
setInputs(defaultInputs) setInputs(defaultInputs || [""])
}, [defaultInputs]) }, [defaultInputs])
useEffect(() => { useEffect(() => {
setSelectedRadio(defaultSelected) setSelectedRadio(defaultSelected)
}, [defaultSelected]) }, [defaultSelected])
useEffect(() => { useEffect(() => {
@@ -165,4 +166,95 @@ export const DynamicRadioInputList = React.memo(({defaultInputs=[""], defaultSel
</Button> </Button>
</div> </div>
) )
})
/**
* defaultWeightMapping structure: {0: {gridNo: 0, weight: 0}}
*/
export const DynamicGridWeightInput = React.memo(({value, onChange, gridInputProps, weightInputProps}) => {
const [weightMapping, setWeightMapping] = useState(value) // Initialize with one input
useEffect(() => {
setWeightMapping(value || {})
}, [value])
useEffect(() => {
if(onChange){
onChange(weightMapping)
}
}, [weightMapping])
// Add a new input
const addInput = () => {
const newObjectIndex = Object.keys(weightMapping).length
setWeightMapping({...weightMapping, [newObjectIndex]: {gridNo: 1, weight: 0}})
}
// Remove an input by index, but keep the first one
const removeInput = (index) => {
const newInputs = { ...weightMapping }; // Create a shallow copy
delete newInputs[index]; // Remove the entry by key
setWeightMapping(newInputs); // Update state
}
// Update input value
const handleGridNoChange = (index, gridNo, weight) => {
const newInputs = {...weightMapping}
newInputs[index] = {
weight,
gridNo
}
setWeightMapping(newInputs)
}
return (
<div>
{weightMapping && Object.entries(weightMapping).map(([idx, {weight, gridNo}], index) => (
<Space key={index} style={{ display: "flex", marginBottom: 8 }} align="baseline">
<div className="tw-flex tw-flex-col tw-gap-2">
{ index === 0 &&
<span>Grid no</span>
}
<InputNumber
value={gridNo}
min={1}
onChange={(value) => handleGridNoChange(idx, value, weight)}
placeholder={`Input ${index + 1}`}
{...gridInputProps}
/>
</div>
<div className="tw-flex tw-flex-col tw-gap-2">
{ index === 0 &&
<span>Weight</span>
}
<InputNumber
min={0}
value={weight}
onChange={(value) => handleGridNoChange(idx, gridNo, value)}
placeholder={`Input ${index + 1}`}
{...weightInputProps}
/>
</div>
{/* {index !== 0 && ( // Do not show delete button for the first input */}
<div>
<MinusCircleOutlined className="tw-text-xl tw-text-red-500"
onClick={() => removeInput(index)} />
</div>
{/* )} */}
</Space>
))}
<Button type="dashed" onClick={addInput} icon={<PlusOutlined />}>
Add Weight
</Button>
</div>
)
}) })

View File

@@ -0,0 +1,47 @@
import { Modal } from "antd"
import { useState } from "react"
const VideoPopUp = ({children, url}) => {
const [popUpOpen, setPopUpOpen] = useState(false)
const handleVideoOpen = () => {
setPopUpOpen(true)
}
const handleVideoClose = (event) => {
event.stopPropagation()
setPopUpOpen(false)
}
return (
<div onClick={handleVideoOpen} >
{children}
<Modal title={"demo"}
style={{ zIndex: 14000, gap: '5px', placeContent: "center", }}
className="max-xl:tw-max-w-full max-lg:!tw-max-h-[450px]"
styles={{body: {height: "80vh", width: "100%"}, content: {width: "100%",}}}
onCancel={handleVideoClose}
centered
onOk={handleVideoClose}
footer={null}
width={'90%'}
height={1000}
open={popUpOpen}>
<div className="tw-mt-5 tw-text-lg tw-min-w-[350px] tw-rounded-md tw-overflow-hidden tw-h-full tw-w-full ">
<iframe src="https://www.youtube.com/embed/Lp2-ToDlqSk?si=dpUpg5ZBdUS8EVOw"
className="tw-w-full tw-h-full"
title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen>
</iframe>
</div>
</Modal>
</div>
)
}
export default VideoPopUp

View File

View File

@@ -0,0 +1,42 @@
License held by paul
Github username: PaulleDemon
Below is the License for source code of PyUIBuilder. For license regarding source code generated by PyUIBuilder Refer Readme.md file (it has basically no restrictions).
The source code for PyUIBuilder is Dual licensed, most parts of the code comes under AGPL,
some parts under a different non-commercial use license.
-----------------------
License in short
* You are free fork, modify and redistribute it given the original or derived works is kept free and source-available and under the same license.
* Another restriction is you can't redistribute in binary or executable formats without asking me first.
* If you plan on making money on this work or derived work you need permission first.
* No AI Training on this source code
License was planned to be kept under one AGPL or less restrictive license, but since its difficult for me to continue providing free and source-available software without proper funding. It's under this license.
Read the full license below.
--------------------
1. License Grant
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to use, copy, modify, merge, publish, and distribute the source code, subject to the following conditions:
2. Derived Work License
Any modifications, derivative works, or copies of the Software must retain this license. All derivative works must be source-available under the same license terms. You are not permitted to relicense or sub-license any portion of the Software under a different license without explicit prior written permission from the original author.
3. Distribution Restriction on Executables
You are not permitted to distribute the Software in compiled, executable, or binary form without prior written consent from the original author. This applies to both original and derivative works.
4. Commercial Use Restriction
The Software, in its original or modified form, cannot be used for any commercial purpose without prior written permission from the original author. This includes selling, licensing, or offering the Software as part of a service.
5. Non-Commercial Use
You may freely use, copy, modify, and distribute the source code of the Software for non-commercial purposes, provided that the same terms of this license are maintained in all copies or derivative works.
6. Changes to the License
The original author reserves the right to modify, amend, or update this license at any time. Any changes will apply only to future versions of the Software. Any prior versions of the Software, including derivative works, will continue to be governed by the version of the license in effect at the time the work was created.
7. Disclaimer
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -2,6 +2,7 @@
export const Tkinter_TO_WEB_CURSOR_MAPPING = { export const Tkinter_TO_WEB_CURSOR_MAPPING = {
"": "",
"arrow": "default", "arrow": "default",
"circle": "wait", "circle": "wait",
"clock": "wait", "clock": "wait",

View File

@@ -20,5 +20,25 @@ export const ANCHOR = [
"s", "s",
"e", "e",
"w", "w",
"center" "center",
] "ne",
"se",
"sw",
"nw",
]
export const GRID_STICKY = {
N: "n",
S: "s",
E: "e",
W: "w",
WE: "we",
NS: "ns",
NW: "nw",
NE: "ne",
SW: "sw",
SE: "se",
NEWS: "news",
NONE: "",
}

View File

@@ -111,13 +111,13 @@ async function generateCustomTkCode(projectName, widgetList=[], widgetRefs=[], a
// widget - {id, widgetType: widgetComponentType, children: [], parent: "", initialData: {}} // widget - {id, widgetType: widgetComponentType, children: [], parent: "", initialData: {}}
const generatedObject = generateCustomTkCodeList(filteredWidgetList, widgetRefs, "", "") const generatedObject = generateCustomTkCodeList(filteredWidgetList, widgetRefs.current, "", "")
const {code: codeLines, imports, requirements, mainVariable} = generatedObject const {code: codeLines, imports, requirements, mainVariable} = generatedObject
// TODO: avoid adding \n inside the list instead rewrite using code.join("\n") // TODO: avoid adding \n inside the list instead rewrite using code.join("\n")
const code = [ const code = [
"# This code is generated by PyUIbuilder: https://github.com/PaulleDemon/PyUIBuilder", "# This code is generated by PyUIbuilder: https://pyuibuilder.com",
"\n\n", "\n\n",
...imports.flatMap((item, index) => index < imports.length - 1 ? [item, "\n"] : [item]), //add \n to every line ...imports.flatMap((item, index) => index < imports.length - 1 ? [item, "\n"] : [item]), //add \n to every line
"\n\n", "\n\n",

View File

@@ -20,6 +20,7 @@ const Themes = {
class AnalogTimePicker extends CustomTkBase{ class AnalogTimePicker extends CustomTkBase{
static widgetType = "analog_timepicker" static widgetType = "analog_timepicker"
static displayName = "Analog Timepicker"
static requiredImports = [ static requiredImports = [
...CustomTkBase.requiredImports, ...CustomTkBase.requiredImports,

View File

@@ -13,6 +13,8 @@ import { CustomTkBase } from "../widgets/base"
class MapView extends CustomTkBase{ class MapView extends CustomTkBase{
static widgetType = "map_view" static widgetType = "map_view"
static displayName = "Map View"
static requiredImports = [ static requiredImports = [
...CustomTkBase.requiredImports, ...CustomTkBase.requiredImports,

View File

@@ -70,6 +70,7 @@ const ResizableTable = ({minRows=5, minCols=5}) => {
class PandasTable extends CustomTkBase{ class PandasTable extends CustomTkBase{
static widgetType = "pandas_table" static widgetType = "pandas_table"
static displayName = "Pandas Table"
static requiredImports = [ static requiredImports = [
...CustomTkBase.requiredImports, ...CustomTkBase.requiredImports,

View File

@@ -12,6 +12,7 @@ import { getPythonAssetPath } from "../../utils/pythonFilePath"
class VideoPlayer extends CustomTkBase{ class VideoPlayer extends CustomTkBase{
static widgetType = "video_player" static widgetType = "video_player"
static displayName = "Video Player"
static requiredImports = [ static requiredImports = [
...CustomTkBase.requiredImports, ...CustomTkBase.requiredImports,

View File

@@ -7,7 +7,7 @@ import Label from "./widgets/label"
import Button from "./widgets/button" import Button from "./widgets/button"
import OptionMenu from "./widgets/optionMenu" import OptionMenu from "./widgets/optionMenu"
import Slider from "./widgets/slider" import Slider from "./widgets/slider"
import { CheckBox, RadioButton } from "./widgets/ checkButton" import { CheckBox, RadioButton } from "./widgets/checkButton"
import { Input, Text } from "./widgets/input" import { Input, Text } from "./widgets/input"
import SpinBox from "./widgets/spinBox" import SpinBox from "./widgets/spinBox"

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@ import { CustomTkWidgetBase } from "./base"
class Button extends CustomTkWidgetBase{ class Button extends CustomTkWidgetBase{
static widgetType = "button" static widgetType = "button"
static displayName = "Button"
constructor(props) { constructor(props) {
super(props) super(props)
@@ -69,6 +70,7 @@ class Button extends CustomTkWidgetBase{
<div className="tw-w-flex tw-flex-col tw-w-full tw-h-full tw-rounded-md <div className="tw-w-flex tw-flex-col tw-w-full tw-h-full tw-rounded-md
tw-border tw-border-solid tw-border-gray-400 tw-overflow-hidden"> tw-border tw-border-solid tw-border-gray-400 tw-overflow-hidden">
<div className="tw-p-2 tw-w-full tw-flex tw-place-content-center tw-place-items-center tw-h-full tw-text-center" <div className="tw-p-2 tw-w-full tw-flex tw-place-content-center tw-place-items-center tw-h-full tw-text-center"
ref={this.styleAreaRef}
style={this.getInnerRenderStyling()}> style={this.getInnerRenderStyling()}>
{/* {this.props.children} */} {/* {this.props.children} */}
<div className="tw-text-sm" style={{color: this.getAttrValue("styling.foregroundColor")}}> <div className="tw-text-sm" style={{color: this.getAttrValue("styling.foregroundColor")}}>

View File

@@ -3,11 +3,15 @@ import Tools from "../../../canvas/constants/tools"
import { convertObjectToKeyValueString, removeKeyFromObject } from "../../../utils/common" import { convertObjectToKeyValueString, removeKeyFromObject } from "../../../utils/common"
import { CheckSquareFilled } from "@ant-design/icons" import { CheckSquareFilled } from "@ant-design/icons"
import { CustomTkWidgetBase } from "./base" import { CustomTkWidgetBase } from "./base"
import { Layouts } from "../../../canvas/constants/layouts"
import React from "react"
export class CheckBox extends CustomTkWidgetBase{ export class CheckBox extends CustomTkWidgetBase{
static widgetType = "check_button" static widgetType = "check_button"
static displayName = "Check Box"
constructor(props) { constructor(props) {
super(props) super(props)
@@ -130,6 +134,8 @@ export class RadioButton extends CustomTkWidgetBase{
this.minSize = {width: 50, height: 30} this.minSize = {width: 50, height: 30}
this.firstDivRef = React.createRef()
this.state = { this.state = {
...this.state, ...this.state,
size: { width: 80, height: 30 }, size: { width: 80, height: 30 },
@@ -178,7 +184,7 @@ export class RadioButton extends CustomTkWidgetBase{
code.push(`\n`) code.push(`\n`)
code.push(`${radioBtnVariable} = ctk.CTkRadioButton(master=${parent}, variable=${variableName}_var, text="${radio_text}", value=${idx})`) code.push(`${radioBtnVariable} = ctk.CTkRadioButton(master=${parent}, variable=${variableName}_var, text="${radio_text}", value=${idx})`)
code.push(`${radioBtnVariable}.configure(${convertObjectToKeyValueString(config)})`) code.push(`${radioBtnVariable}.configure(${convertObjectToKeyValueString(config)})`)
code.push(`${radioBtnVariable}.${this.getLayoutCode()}`) code.push(`${radioBtnVariable}.${this.getLayoutCode({index: idx})}`)
}) })
const defaultSelected = radios.selectedRadio const defaultSelected = radios.selectedRadio
@@ -191,6 +197,39 @@ export class RadioButton extends CustomTkWidgetBase{
return code return code
} }
/**
*
* The index is required for pack as in pack every widget would be packed on top of each other in radio button
*/
getLayoutCode({index=0}){
let layoutManager = super.getLayoutCode()
const {layout: parentLayout, direction, gap, align="start"} = this.getParentLayout()
const absolutePositioning = this.getAttrValue("positioning")
if (parentLayout === Layouts.PLACE || absolutePositioning){
const config = {}
const elementRect = this.firstDivRef.current?.getBoundingClientRect()
config['x'] = Math.trunc(this.state.pos.x)
config['y'] = Math.trunc(this.state.pos.y)
if (elementRect?.height){
config['y'] = Math.trunc(config['y'] + (index * elementRect.height)) // the index is the radiobutton index
}
const configStr = convertObjectToKeyValueString(config)
layoutManager = `place(${configStr})`
}
return layoutManager
}
getToolbarAttrs(){ getToolbarAttrs(){
const toolBarAttrs = super.getToolbarAttrs() const toolBarAttrs = super.getToolbarAttrs()
@@ -211,13 +250,18 @@ export class RadioButton extends CustomTkWidgetBase{
return ( return (
<div className="tw-flex tw-p-1 tw-w-full tw-h-full tw-rounded-md tw-overflow-hidden" <div className="tw-flex tw-p-1 tw-w-full tw-h-full tw-rounded-md tw-overflow-hidden"
ref={this.styleAreaRef}
style={this.getInnerRenderStyling()} style={this.getInnerRenderStyling()}
> >
<div className="tw-flex tw-flex-col tw-gap-2 tw-w-fit tw-h-fit"> <div className="tw-flex tw-flex-col tw-gap-2 tw-w-fit tw-h-fit">
{ {
inputs.map((value, index) => { inputs.map((value, index) => {
const ref = index === 0 ? this.firstDivRef : null
return ( return (
<div key={index} className="tw-flex tw-gap-2 tw-w-full tw-h-full tw-place-items-center "> <div key={index} ref={ref} className="tw-flex tw-gap-2 tw-w-full tw-h-full tw-place-items-center ">
<div className="tw-border-solid tw-border-[#D9D9D9] tw-border-2 <div className="tw-border-solid tw-border-[#D9D9D9] tw-border-2
tw-min-w-[20px] tw-min-h-[20px] tw-w-[20px] tw-h-[20px] tw-min-w-[20px] tw-min-h-[20px] tw-w-[20px] tw-h-[20px]
tw-text-blue-600 tw-flex tw-items-center tw-justify-center tw-text-blue-600 tw-flex tw-items-center tw-justify-center

View File

@@ -1,3 +1,5 @@
import { Layouts } from "../../../canvas/constants/layouts"
import Tools from "../../../canvas/constants/tools"
import Widget from "../../../canvas/widgets/base" import Widget from "../../../canvas/widgets/base"
import { CustomTkBase } from "./base" import { CustomTkBase } from "./base"
@@ -5,6 +7,7 @@ import { CustomTkBase } from "./base"
class Frame extends CustomTkBase{ class Frame extends CustomTkBase{
static widgetType = "frame" static widgetType = "frame"
static displayName = "Frame"
constructor(props) { constructor(props) {
super(props) super(props)
@@ -16,7 +19,99 @@ class Frame extends CustomTkBase{
this.state = { this.state = {
...this.state, ...this.state,
fitContent: {width: true, height: true}, fitContent: {width: true, height: true},
widgetName: "Frame" widgetName: "Frame",
attrs: {
...this.state.attrs,
padding: {
label: "padding",
padX: {
label: "Pad X",
tool: Tools.NUMBER_INPUT,
toolProps: {min: 0, max: 140},
value: null,
onChange: (value) => {
// this.setWidgetInnerStyle("paddingLeft", `${value}px`)
// this.setWidgetInnerStyle("paddingRight", `${value}px`)
// const widgetStyle = {
// }
this.setState((prevState) => ({
widgetInnerStyling: {
...prevState.widgetInnerStyling,
paddingLeft: `${value}px`,
paddingRight: `${value}px`
}
}))
this.setAttrValue("padding.padX", value)
}
},
padY: {
label: "Pad Y",
tool: Tools.NUMBER_INPUT,
toolProps: {min: 0, max: 140},
value: null,
onChange: (value) => {
this.setState((prevState) => ({
widgetInnerStyling: {
...prevState.widgetInnerStyling,
paddingTop: `${value}px`,
paddingBottom: `${value}px`
}
}))
// this.setState({
// widgetInnerStyling: widgetStyle
// })
this.setAttrValue("padding.padY", value)
}
},
},
margin: {
label: "Margin",
marginX: {
label: "Margin X",
tool: Tools.NUMBER_INPUT,
toolProps: {min: 0, max: 140},
value: null,
onChange: (value) => {
this.updateState((prev) => ({
widgetOuterStyling: {
...prev.widgetOuterStyling,
marginLeft: `${value}px`,
marginRight: `${value}px`
},
}))
this.setAttrValue("margin.marginX", value)
}
},
marginY: {
label: "Margin Y",
tool: Tools.NUMBER_INPUT,
toolProps: {min: 0, max: 140},
value: null,
onChange: (value) => {
this.updateState((prev) => ({
widgetOuterStyling: {
...prev.widgetOuterStyling,
marginTop: `${value}px`,
marginBottom: `${value}px`
},
}))
this.setAttrValue("margin.marginY", value)
}
},
},
}
} }
} }
@@ -26,6 +121,34 @@ class Frame extends CustomTkBase{
this.setAttrValue("styling.backgroundColor", "#EDECEC") this.setAttrValue("styling.backgroundColor", "#EDECEC")
} }
getConfigCode(){
const bg = this.getAttrValue("styling.backgroundColor")
const fitWidth = this.state.fitContent.width
const fitHeight = this.state.fitContent.height
const {width, height} = this.getSize()
const {layout} = this.getParentLayout()
const config = {
bg: `"${bg}"`
}
if (layout !== Layouts.PLACE){
if (!fitWidth){
config['width'] = width
}
if (!fitHeight){
config['height'] = height
}
}
return config
}
generateCode(variableName, parent){ generateCode(variableName, parent){
const bg = this.getAttrValue("styling.backgroundColor") const bg = this.getAttrValue("styling.backgroundColor")
@@ -33,20 +156,36 @@ class Frame extends CustomTkBase{
return [ return [
`${variableName} = ctk.CTkFrame(master=${parent})`, `${variableName} = ctk.CTkFrame(master=${parent})`,
`${variableName}.configure(fg_color="${bg}")`, `${variableName}.configure(fg_color="${bg}")`,
`${variableName}.${this.getLayoutCode()}` `${variableName}.${this.getLayoutCode()}`,
...this.getGridLayoutConfigurationCode(variableName)
] ]
} }
getToolbarAttrs(){
const {layout, gridConfig, gridWeights, ...toolBarAttrs} = super.getToolbarAttrs()
// places layout at the end
return ({
id: this.__id,
...toolBarAttrs,
padding: this.state.attrs.padding,
margin: this.state.attrs.margin,
layout,
gridConfig,
gridWeights
})
}
renderContent(){ renderContent(){
// console.log("bounding rect: ", this.getBoundingRect()) // console.log("bounding rect: ", this.getBoundingRect())
// console.log("widget styling: ", this.state.widgetInnerStyling) // console.log("widget styling: ", this.state.widgetInnerStyling)
return ( return (
<div className="tw-w-flex tw-flex-col tw-w-full tw-h-full tw-relative tw-rounded-md tw-overflow-hidden"> <div className="tw-w-flex tw-flex-col tw-w-full tw-h-full tw-relative tw-rounded-md tw-overflow-hidden">
<div className="tw-p-2 tw-w-full tw-h-full tw-content-start" style={this.getInnerRenderStyling()}> <div className="tw-p-2 tw-w-full tw-h-full tw-content-start"
{this.props.children} ref={this.styleAreaRef}
style={this.getInnerRenderStyling()}>
{this.renderTkinterLayout()}
</div> </div>
</div> </div>
) )

View File

@@ -6,6 +6,7 @@ import { CustomTkWidgetBase } from "./base"
export class Input extends CustomTkWidgetBase{ export class Input extends CustomTkWidgetBase{
static widgetType = "entry" static widgetType = "entry"
static displayName = "Entry"
constructor(props) { constructor(props) {
super(props) super(props)
@@ -65,6 +66,7 @@ export class Input extends CustomTkWidgetBase{
return ( return (
<div className="tw-w-flex tw-flex-col tw-w-full tw-h-full tw-rounded-md tw-overflow-hidden"> <div className="tw-w-flex tw-flex-col tw-w-full tw-h-full tw-rounded-md tw-overflow-hidden">
<div className="tw-p-2 tw-w-full tw-h-full tw-flex tw-place-items-center" <div className="tw-p-2 tw-w-full tw-h-full tw-flex tw-place-items-center"
ref={this.styleAreaRef}
style={this.getInnerRenderStyling()}> style={this.getInnerRenderStyling()}>
<div className="tw-text-sm tw-text-gray-300"> <div className="tw-text-sm tw-text-gray-300">
{this.getAttrValue("placeHolder")} {this.getAttrValue("placeHolder")}

View File

@@ -7,7 +7,7 @@ import { CustomTkWidgetBase } from "./base"
class Label extends CustomTkWidgetBase{ class Label extends CustomTkWidgetBase{
static widgetType = "label" static widgetType = "label"
static displayName = "Label"
constructor(props) { constructor(props) {
super(props) super(props)
@@ -116,6 +116,22 @@ class Label extends CustomTkWidgetBase{
}) })
} }
getAnchorStyle = (anchor) => {
const anchorStyles = {
n: { justifyContent: 'center', alignItems: 'flex-start' },
s: { justifyContent: 'center', alignItems: 'flex-end' },
e: { justifyContent: 'flex-end', alignItems: 'center' },
w: { justifyContent: 'flex-start', alignItems: 'center' },
ne: { justifyContent: 'flex-end', alignItems: 'flex-start' },
se: { justifyContent: 'flex-end', alignItems: 'flex-end' },
nw: { justifyContent: 'flex-start', alignItems: 'flex-start' },
sw: { justifyContent: 'flex-start', alignItems: 'flex-end' },
center: { justifyContent: 'center', alignItems: 'center' }
}
return anchorStyles[anchor] || anchorStyles["w"];
}
renderContent(){ renderContent(){
const image = this.getAttrValue("imageUpload") const image = this.getAttrValue("imageUpload")
@@ -129,6 +145,7 @@ class Label extends CustomTkWidgetBase{
}} }}
> >
<div className="tw-p-2 tw-w-full tw-h-full tw-flex tw-place-content-center tw-place-items-center " <div className="tw-p-2 tw-w-full tw-h-full tw-flex tw-place-content-center tw-place-items-center "
ref={this.styleAreaRef}
style={this.getInnerRenderStyling()}> style={this.getInnerRenderStyling()}>
{/* {this.props.children} */} {/* {this.props.children} */}
{ {
@@ -136,7 +153,10 @@ class Label extends CustomTkWidgetBase{
<img src={image.previewUrl} className="tw-bg-contain tw-w-full tw-h-full" /> <img src={image.previewUrl} className="tw-bg-contain tw-w-full tw-h-full" />
) )
} }
<div className="" style={{color: this.getAttrValue("styling.foregroundColor")}}> <div className={`tw-flex ${!image ? "tw-w-full tw-h-full" : ""}`} style={{
color: this.getAttrValue("styling.foregroundColor"),
...this.getAnchorStyle(this.getAttrValue("styling.anchor"))
}}>
{this.getAttrValue("labelWidget")} {this.getAttrValue("labelWidget")}
</div> </div>
</div> </div>

View File

@@ -1,11 +1,20 @@
import Widget from "../../../canvas/widgets/base" import Widget from "../../../canvas/widgets/base"
import Tools from "../../../canvas/constants/tools" import Tools from "../../../canvas/constants/tools"
import { CustomTkBase } from "./base" import { CustomTkBase } from "./base"
import { getPythonAssetPath } from "../../utils/pythonFilePath"
class MainWindow extends CustomTkBase{ class MainWindow extends CustomTkBase{
static widgetType = "main_window" static widgetType = "main_window"
static displayName = "Main Window"
static initialSize = {
width: 700,
height: 400
}
constructor(props) { constructor(props) {
super(props) super(props)
@@ -26,6 +35,13 @@ class MainWindow extends CustomTkBase{
toolProps: {placeholder: "Window title", maxLength: 40}, toolProps: {placeholder: "Window title", maxLength: 40},
value: "Main Window", value: "Main Window",
onChange: (value) => this.setAttrValue("title", value) onChange: (value) => this.setAttrValue("title", value)
},
logo: {
label: "Window Logo",
tool: Tools.UPLOADED_LIST,
toolProps: {filterOptions: ["image/jpg", "image/jpeg", "image/png"]},
value: "",
onChange: (value) => this.setAttrValue("logo", value)
} }
} }
@@ -42,12 +58,48 @@ class MainWindow extends CustomTkBase{
generateCode(variableName, parent){ generateCode(variableName, parent){
const backgroundColor = this.getAttrValue("styling.backgroundColor") const backgroundColor = this.getAttrValue("styling.backgroundColor")
const logo = this.getAttrValue("logo")
return [ const {width, height} = this.getSize()
`${variableName} = ctk.CTk()`,
`${variableName}.configure(fg_color="${backgroundColor}")`, const code = [
`${variableName}.title("${this.getAttrValue("title")}")` `${variableName} = ctk.CTk()`,
] `${variableName}.configure(fg_color="${backgroundColor}")`,
`${variableName}.title("${this.getAttrValue("title")}")`,
`${variableName}.geometry("${width}x${height}")`,
...this.getGridLayoutConfigurationCode(variableName)
]
if (logo?.name){
// code.push(`\n`)
code.push(`${variableName}_img = Image.open(${getPythonAssetPath(logo.name, "image")})`)
code.push(`${variableName}_img = ImageTk.PhotoImage(${variableName}_img)`)
code.push(`${variableName}.iconphoto(False, ${variableName}_img)`)
// code.push("\n")
}
return code
}
getImports(){
const imports = super.getImports()
if (this.getAttrValue("logo"))
imports.push("import os", "from PIL import Image, ImageTk", )
return imports
}
getRequirements(){
const requirements = super.getRequirements()
if (this.getAttrValue("logo"))
requirements.push("pillow")
return requirements
} }
getToolbarAttrs(){ getToolbarAttrs(){
@@ -57,8 +109,8 @@ class MainWindow extends CustomTkBase{
id: this.__id, id: this.__id,
widgetName: toolBarAttrs.widgetName, widgetName: toolBarAttrs.widgetName,
title: this.state.attrs.title, title: this.state.attrs.title,
logo: this.state.attrs.logo,
size: toolBarAttrs.size, size: toolBarAttrs.size,
...this.state.attrs, ...this.state.attrs,
}) })
@@ -80,8 +132,9 @@ class MainWindow extends CustomTkBase{
</div> </div>
</div> </div>
<div className="tw-p-2 tw-w-full tw-relative tw-h-full tw-overflow-hidden tw-content-start" <div className="tw-p-2 tw-w-full tw-relative tw-h-full tw-overflow-hidden tw-content-start"
ref={this.styleAreaRef}
style={this.state.widgetInnerStyling}> style={this.state.widgetInnerStyling}>
{this.props.children} {this.renderTkinterLayout()}
</div> </div>
</div> </div>
) )

View File

@@ -7,6 +7,7 @@ import { convertObjectToKeyValueString, removeKeyFromObject } from "../../../uti
class OptionMenu extends CustomTkWidgetBase{ class OptionMenu extends CustomTkWidgetBase{
static widgetType = "option_menu" static widgetType = "option_menu"
static displayName = "Option Menu"
constructor(props) { constructor(props) {
super(props) super(props)
@@ -30,8 +31,8 @@ class OptionMenu extends CustomTkWidgetBase{
label: "Default Value", label: "Default Value",
tool: Tools.INPUT, tool: Tools.INPUT,
value: "Select option", value: "Select option",
onChange: ({inputs, selectedRadio}) => { onChange: (value) => {
this.setAttrValue("options", {inputs, selectedRadio}) this.setAttrValue("defaultValue", value)
} }
}, },
widgetOptions: { widgetOptions: {
@@ -102,6 +103,7 @@ class OptionMenu extends CustomTkWidgetBase{
return ( return (
<div className="tw-flex tw-p-1 tw-w-full tw-h-full tw-rounded-md tw-overflow-hidden" <div className="tw-flex tw-p-1 tw-w-full tw-h-full tw-rounded-md tw-overflow-hidden"
ref={this.styleAreaRef}
style={this.getInnerRenderStyling()} style={this.getInnerRenderStyling()}
onClick={this.toggleDropDownOpen} onClick={this.toggleDropDownOpen}
> >

View File

@@ -7,6 +7,8 @@ import { CustomTkWidgetBase } from "./base"
class Slider extends CustomTkWidgetBase{ class Slider extends CustomTkWidgetBase{
static widgetType = "scale" static widgetType = "scale"
static displayName = "Scale"
// FIXME: You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use // FIXME: You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use
constructor(props) { constructor(props) {
super(props) super(props)
@@ -141,7 +143,9 @@ class Slider extends CustomTkWidgetBase{
return ( return (
<div className="tw-w-flex tw-flex-col tw-w-full tw-h-full tw-rounded-md tw-overflow-hidden"> <div className="tw-w-flex tw-flex-col tw-w-full tw-h-full tw-rounded-md tw-overflow-hidden">
<div className="flex flex-col items-center justify-center h-screen <div className="flex flex-col items-center justify-center h-screen
bg-gray-100" style={this.getInnerRenderStyling()}> bg-gray-100"
ref={this.styleAreaRef}
style={this.getInnerRenderStyling()}>
<div className="w-full max-w-md"> <div className="w-full max-w-md">
<input <input
type="range" type="range"

View File

@@ -8,6 +8,7 @@ import { CustomTkWidgetBase } from "./base"
class SpinBox extends CustomTkWidgetBase{ class SpinBox extends CustomTkWidgetBase{
static widgetType = "spin_box" static widgetType = "spin_box"
static displayName = "Spin Box"
constructor(props) { constructor(props) {
super(props) super(props)
@@ -109,6 +110,7 @@ class SpinBox extends CustomTkWidgetBase{
return ( return (
<div className="tw-w-flex tw-flex-col tw-w-full tw-h-full tw-rounded-md tw-overflow-hidden"> <div className="tw-w-flex tw-flex-col tw-w-full tw-h-full tw-rounded-md tw-overflow-hidden">
<div className="tw-p-2 tw-w-full tw-h-full tw-flex tw-place-items-center tw-justify-between" <div className="tw-p-2 tw-w-full tw-h-full tw-flex tw-place-items-center tw-justify-between"
ref={this.styleAreaRef}
style={this.getInnerRenderStyling()}> style={this.getInnerRenderStyling()}>
<div className="tw-text-sm "> <div className="tw-text-sm ">
{this.getAttrValue("spinProps.default")} {this.getAttrValue("spinProps.default")}

View File

@@ -1,10 +1,13 @@
import Widget from "../../../canvas/widgets/base" import Widget from "../../../canvas/widgets/base"
import Tools from "../../../canvas/constants/tools" import Tools from "../../../canvas/constants/tools"
import { getPythonAssetPath } from "../../utils/pythonFilePath"
import { CustomTkBase } from "./base"
class TopLevel extends Widget{ class TopLevel extends CustomTkBase{
static widgetType = "toplevel" static widgetType = "toplevel"
static displayName = "Top Level"
constructor(props) { constructor(props) {
super(props) super(props)
@@ -26,6 +29,13 @@ class TopLevel extends Widget{
toolProps: {placeholder: "Window title", maxLength: 40}, toolProps: {placeholder: "Window title", maxLength: 40},
value: "Top level", value: "Top level",
onChange: (value) => this.setAttrValue("title", value) onChange: (value) => this.setAttrValue("title", value)
},
logo: {
label: "Toplevel Logo",
tool: Tools.UPLOADED_LIST,
toolProps: {filterOptions: ["image/jpg", "image/jpeg", "image/png"]},
value: "",
onChange: (value) => this.setAttrValue("logo", value)
} }
} }
@@ -41,11 +51,49 @@ class TopLevel extends Widget{
const backgroundColor = this.getAttrValue("styling.backgroundColor") const backgroundColor = this.getAttrValue("styling.backgroundColor")
return [ const logo = this.getAttrValue("logo")
`${variableName} = ctk.CTkToplevel(master=${parent})`,
`${variableName}.configure(fg_color="${backgroundColor}")`, const {width, height} = this.getSize()
`${variableName}.title("${this.getAttrValue("title")}")`
] const code = [
`${variableName} = ctk.CTkToplevel(master=${parent})`,
`${variableName}.configure(fg_color="${backgroundColor}")`,
`${variableName}.title("${this.getAttrValue("title")}")`,
`${variableName}.geometry("${width}x${height}")`,
...this.getGridLayoutConfigurationCode(variableName)
]
if (logo?.name){
// code.push(`\n`)
code.push(`${variableName}_img = Image.open(${getPythonAssetPath(logo.name, "image")})`)
code.push(`${variableName}_img = ImageTk.PhotoImage(${variableName}_img)`)
code.push(`${variableName}.iconphoto(False, ${variableName}_img)`)
// code.push("\n")
}
return code
}
getImports(){
const imports = super.getImports()
if (this.getAttrValue("logo"))
imports.push("import os", "from PIL import Image, ImageTk", )
return imports
}
getRequirements(){
const requirements = super.getRequirements()
if (this.getAttrValue("logo"))
requirements.push("pillow")
return requirements
} }
getToolbarAttrs(){ getToolbarAttrs(){
@@ -54,8 +102,8 @@ class TopLevel extends Widget{
return ({ return ({
widgetName: toolBarAttrs.widgetName, widgetName: toolBarAttrs.widgetName,
title: this.state.attrs.title, title: this.state.attrs.title,
logo: this.state.attrs.logo,
size: toolBarAttrs.size, size: toolBarAttrs.size,
...this.state.attrs, ...this.state.attrs,
}) })
@@ -76,8 +124,10 @@ class TopLevel extends Widget{
</div> </div>
</div> </div>
</div> </div>
<div className="tw-p-2 tw-w-full tw-h-full tw-content-start" style={this.state.widgetInnerStyling}> <div className="tw-p-2 tw-w-full tw-h-full tw-content-start"
{this.props.children} ref={this.styleAreaRef}
style={this.state.widgetInnerStyling}>
{this.renderTkinterLayout()}
</div> </div>
</div> </div>
) )

View File

@@ -2,6 +2,7 @@
export const Tkinter_TO_WEB_CURSOR_MAPPING = { export const Tkinter_TO_WEB_CURSOR_MAPPING = {
"": "",
"arrow": "default", "arrow": "default",
"circle": "wait", "circle": "wait",
"clock": "wait", "clock": "wait",

View File

@@ -20,5 +20,25 @@ export const ANCHOR = [
"s", "s",
"e", "e",
"w", "w",
"center" "center",
] "ne",
"se",
"sw",
"nw",
]
export const GRID_STICKY = {
N: "n",
S: "s",
E: "e",
W: "w",
WE: "we",
NS: "ns",
NW: "nw",
NE: "ne",
SW: "sw",
SE: "se",
NEWS: "news",
NONE: "",
}

View File

@@ -4,6 +4,8 @@ import MainWindow from "../widgets/mainWindow"
import { message } from "antd" import { message } from "antd"
import TopLevel from "../widgets/toplevel" import TopLevel from "../widgets/toplevel"
const pythonFiles = require.context("../pythonWidgets", false, /\.py$/)
// FIXME: if the toplevel comes first, before the MainWindow in widgetlist the root may become null // FIXME: if the toplevel comes first, before the MainWindow in widgetlist the root may become null
// Recursive function to generate the code list, imports, requirements, and track mainVariable // Recursive function to generate the code list, imports, requirements, and track mainVariable
@@ -13,6 +15,8 @@ function generateTkinterCodeList(widgetList = [], widgetRefs = [], parentVariabl
let requirements = new Set([]) let requirements = new Set([])
let code = [] let code = []
let customPythonWidgets = new Set([])
for (let widget of widgetList) { for (let widget of widgetList) {
const widgetRef = widgetRefs[widget.id].current const widgetRef = widgetRefs[widget.id].current
let varName = widgetRef.getVariableName() let varName = widgetRef.getVariableName()
@@ -20,6 +24,7 @@ function generateTkinterCodeList(widgetList = [], widgetRefs = [], parentVariabl
// Add imports and requirements to sets // Add imports and requirements to sets
widgetRef.getImports().forEach(importItem => imports.add(importItem)) widgetRef.getImports().forEach(importItem => imports.add(importItem))
widgetRef.getRequirements().forEach(requirementItem => requirements.add(requirementItem)) widgetRef.getRequirements().forEach(requirementItem => requirements.add(requirementItem))
widgetRef.getRequiredCustomPyFiles().forEach(customFile => customPythonWidgets.add(customFile))
// Set main variable if the widget is MainWindow // Set main variable if the widget is MainWindow
if (widget.widgetType === MainWindow) { if (widget.widgetType === MainWindow) {
@@ -68,6 +73,8 @@ function generateTkinterCodeList(widgetList = [], widgetRefs = [], parentVariabl
// Merge child imports, requirements, and code // Merge child imports, requirements, and code
imports = new Set([...imports, ...childResult.imports]) imports = new Set([...imports, ...childResult.imports])
requirements = new Set([...requirements, ...childResult.requirements]) requirements = new Set([...requirements, ...childResult.requirements])
customPythonWidgets = new Set([...customPythonWidgets, ...childResult.customPythonWidgets])
code.push(...childResult.code) code.push(...childResult.code)
mainVariable = childResult.mainVariable || mainVariable // the main variable is the main window variable mainVariable = childResult.mainVariable || mainVariable // the main variable is the main window variable
@@ -78,6 +85,7 @@ function generateTkinterCodeList(widgetList = [], widgetRefs = [], parentVariabl
imports: Array.from(imports), imports: Array.from(imports),
code: code, code: code,
requirements: Array.from(requirements), requirements: Array.from(requirements),
customPythonWidgets: Array.from(customPythonWidgets),
mainVariable mainVariable
} }
} }
@@ -111,13 +119,17 @@ async function generateTkinterCode(projectName, widgetList=[], widgetRefs=[], as
// widget - {id, widgetType: widgetComponentType, children: [], parent: "", initialData: {}} // widget - {id, widgetType: widgetComponentType, children: [], parent: "", initialData: {}}
const generatedObject = generateTkinterCodeList(filteredWidgetList, widgetRefs, "", "") const generatedObject = generateTkinterCodeList(filteredWidgetList, widgetRefs.current, "", "")
const {code: codeLines, imports, requirements, mainVariable} = generatedObject const {code: codeLines, imports, requirements, mainVariable, customPythonWidgets} = generatedObject
console.log("custom python widgets: ", customPythonWidgets)
// TODO: avoid adding \n inside the list instead rewrite using code.join("\n") // TODO: avoid adding \n inside the list instead rewrite using code.join("\n")
// TODO: import customWidgets
const code = [ const code = [
"# This code is generated by PyUIbuilder: https://github.com/PaulleDemon/PyUIBuilder", "# This code is generated by PyUIbuilder: https://pyuibuilder.com",
"\n\n", "\n\n",
...imports.flatMap((item, index) => index < imports.length - 1 ? [item, "\n"] : [item]), //add \n to every line ...imports.flatMap((item, index) => index < imports.length - 1 ? [item, "\n"] : [item]), //add \n to every line
"\n\n", "\n\n",
@@ -147,6 +159,32 @@ async function generateTkinterCode(projectName, widgetList=[], widgetRefs=[], as
}) })
} }
for (let customWidget of customPythonWidgets){
let [fileName, extension] = customWidget.split(".")
if (!extension){
fileName = `${fileName}.py`
}
const fileContent = pythonFiles(`./${fileName}`).default
createFileList.push({
fileData: new Blob([fileContent], { type: "text/plain" }),
fileName: fileName,
folder: "customWidgets"
})
}
if (customPythonWidgets.length > 0){
createFileList.push({
fileData: new Blob([''], { type: "text/plain" }),
fileName: '__init__.py',
folder: "customWidgets"
})
}
for (let asset of assetFiles){ for (let asset of assetFiles){
if (asset.fileType === "image"){ if (asset.fileType === "image"){

View File

@@ -20,6 +20,7 @@ const Themes = {
class AnalogTimePicker extends TkinterBase{ class AnalogTimePicker extends TkinterBase{
static widgetType = "analog_timepicker" static widgetType = "analog_timepicker"
static displayName = "Analog Timepicker"
static requiredImports = [ static requiredImports = [
...TkinterBase.requiredImports, ...TkinterBase.requiredImports,

View File

@@ -13,6 +13,7 @@ import { TkinterBase } from "../widgets/base"
class MapView extends TkinterBase{ class MapView extends TkinterBase{
static widgetType = "map_view" static widgetType = "map_view"
static displayName = "Map View"
static requiredImports = [ static requiredImports = [
...TkinterBase.requiredImports, ...TkinterBase.requiredImports,

View File

@@ -70,6 +70,7 @@ const ResizableTable = ({minRows=5, minCols=5}) => {
class PandasTable extends TkinterBase{ class PandasTable extends TkinterBase{
static widgetType = "pandas_table" static widgetType = "pandas_table"
static displayName = "Pandas Table"
static requiredImports = [ static requiredImports = [
...TkinterBase.requiredImports, ...TkinterBase.requiredImports,

View File

@@ -12,6 +12,7 @@ import { getPythonAssetPath } from "../../utils/pythonFilePath"
class VideoPlayer extends TkinterBase{ class VideoPlayer extends TkinterBase{
static widgetType = "video_player" static widgetType = "video_player"
static displayName = "Video Player"
static requiredImports = [ static requiredImports = [
...TkinterBase.requiredImports, ...TkinterBase.requiredImports,

View File

@@ -0,0 +1,92 @@
# Author: Paul: https://github.com/PaulleDemon
# Made using PyUibuilder: https://pyuibuilder.com
# MIT License - keep the copy of this license
# By default Label grows to fit the image, which isn't ideal for many use cases (image should grow to fit/cover the image instead)
import tkinter as tk
from PIL import Image, ImageTk
class ImageLabel(tk.Label):
def __init__(self, master, image_path=None, mode="cover", width=100, height=100, *args, **kwargs):
"""
mode:
- "fit" -> Keeps aspect ratio, fits inside label
- "cover" -> Covers label fully, cropping excess
"""
super().__init__(master, width=width, height=height, *args, **kwargs)
self.parent = master
self.image_path = image_path
self.mode = mode
self.original_image = None
self.photo = None
self.resize_job = None # Debounce job reference
if mode not in ['fit', 'cover']:
raise Exception("Mode can only be fit or cover.")
if image_path:
try:
self.original_image = Image.open(image_path)
self.photo = ImageTk.PhotoImage(self.original_image)
self.config(image=self.photo)
self.force_resize()
except Exception as e:
print(f"Error loading image: {e}")
self.after(100, self.init_events)
def init_events(self):
self.parent.bind("<Configure>", self.on_resize)
def on_resize(self, event=None):
"""Debounce resizing to prevent rapid execution."""
if self.resize_job:
self.after_cancel(self.resize_job)
self.resize_job = self.after(1, self.force_resize) # Debounce
def force_resize(self):
"""Resize image using actual widget size."""
if self.original_image is None:
return # Do nothing if no image is loaded
width = self.winfo_width()
height = self.winfo_height()
if width < 5 or height < 5:
return
aspect_ratio = self.original_image.width / self.original_image.height
if self.mode == "fit":
if width / height > aspect_ratio:
new_width = int(height * aspect_ratio)
new_height = height
else:
new_width = width
new_height = int(width / aspect_ratio)
resized = self.original_image.resize((new_width, new_height), Image.LANCZOS)
elif self.mode == "cover":
if width / height > aspect_ratio:
new_width = width
new_height = int(width / aspect_ratio)
else:
new_width = int(height * aspect_ratio)
new_height = height
resized = self.original_image.resize((new_width, new_height), Image.LANCZOS)
# Crop excess
left = (new_width - width) // 2
top = (new_height - height) // 2
right = left + width
bottom = top + height
resized = resized.crop((left, top, right, bottom))
# Update image
self.photo = ImageTk.PhotoImage(resized)
self.config(image=self.photo)

View File

@@ -0,0 +1 @@
# contains python coded custom widgets

View File

@@ -7,7 +7,7 @@ import Label from "./widgets/label"
import Button from "./widgets/button" import Button from "./widgets/button"
import OptionMenu from "./widgets/optionMenu" import OptionMenu from "./widgets/optionMenu"
import Slider from "./widgets/slider" import Slider from "./widgets/slider"
import { CheckBox, RadioButton } from "./widgets/ checkButton" import { CheckBox, RadioButton } from "./widgets/checkButton"
import { Input, Text } from "./widgets/input" import { Input, Text } from "./widgets/input"
import SpinBox from "./widgets/spinBox" import SpinBox from "./widgets/spinBox"

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@ import { TkinterWidgetBase } from "./base"
class Button extends TkinterWidgetBase{ class Button extends TkinterWidgetBase{
static widgetType = "button" static widgetType = "button"
static displayName = "Button"
constructor(props) { constructor(props) {
super(props) super(props)
@@ -68,6 +69,7 @@ class Button extends TkinterWidgetBase{
<div className="tw-w-flex tw-flex-col tw-w-full tw-h-full tw-rounded-md <div className="tw-w-flex tw-flex-col tw-w-full tw-h-full tw-rounded-md
tw-border tw-border-solid tw-border-gray-400 tw-overflow-hidden"> tw-border tw-border-solid tw-border-gray-400 tw-overflow-hidden">
<div className="tw-p-2 tw-w-full tw-flex tw-place-content-center tw-place-items-center tw-h-full tw-text-center" <div className="tw-p-2 tw-w-full tw-flex tw-place-content-center tw-place-items-center tw-h-full tw-text-center"
ref={this.styleAreaRef}
style={this.getInnerRenderStyling()}> style={this.getInnerRenderStyling()}>
{/* {this.props.children} */} {/* {this.props.children} */}
<div className="tw-text-sm" style={{color: this.getAttrValue("styling.foregroundColor")}}> <div className="tw-text-sm" style={{color: this.getAttrValue("styling.foregroundColor")}}>

View File

@@ -3,11 +3,15 @@ import Tools from "../../../canvas/constants/tools"
import { convertObjectToKeyValueString, removeKeyFromObject } from "../../../utils/common" import { convertObjectToKeyValueString, removeKeyFromObject } from "../../../utils/common"
import { CheckSquareFilled } from "@ant-design/icons" import { CheckSquareFilled } from "@ant-design/icons"
import { TkinterWidgetBase } from "./base" import { TkinterWidgetBase } from "./base"
import { Layouts } from "../../../canvas/constants/layouts"
import React from "react"
export class CheckBox extends TkinterWidgetBase{ export class CheckBox extends TkinterWidgetBase{
static widgetType = "check_button" static widgetType = "check_button"
static displayName = "Check Box"
constructor(props) { constructor(props) {
super(props) super(props)
@@ -96,6 +100,7 @@ export class CheckBox extends TkinterWidgetBase{
return ( return (
<div className="tw-flex tw-p-1 tw-w-full tw-h-full tw-rounded-md tw-overflow-hidden" <div className="tw-flex tw-p-1 tw-w-full tw-h-full tw-rounded-md tw-overflow-hidden"
style={this.getInnerRenderStyling()} style={this.getInnerRenderStyling()}
ref={this.styleAreaRef}
> >
<div className="tw-flex tw-gap-2 tw-w-full tw-h-full tw-place-items-center tw-place-content-center"> <div className="tw-flex tw-gap-2 tw-w-full tw-h-full tw-place-items-center tw-place-content-center">
@@ -124,25 +129,31 @@ export class RadioButton extends TkinterWidgetBase{
// FIXME: the radio buttons are not visible because of the default heigh provided // FIXME: the radio buttons are not visible because of the default heigh provided
static widgetType = "radio_button" static widgetType = "radio_button"
static displayName = "Radio Button"
constructor(props) { constructor(props) {
super(props) super(props)
this.minSize = {width: 50, height: 30} this.minSize = {width: 50, height: 30}
let newAttrs = removeKeyFromObject("layout", this.state.attrs)
this.firstDivRef = React.createRef()
this.state = { this.state = {
...this.state, ...this.state,
size: { width: 80, height: 30 }, size: { width: 80, height: 30 },
fitContent: { width: true, height: true }, fitContent: { width: true, height: true },
widgetName: "Radio button", widgetName: "Radio button",
attrs: { attrs: {
...this.state.attrs, ...newAttrs,
radios: { radios: {
label: "Radio Group", label: "Radio Group",
tool: Tools.INPUT_RADIO_LIST, tool: Tools.INPUT_RADIO_LIST,
value: {inputs: ["default"], selectedRadio: -1}, value: {inputs: ["default"], selectedRadio: -1},
onChange: ({inputs, selectedRadio}) => { onChange: ({inputs, selectedRadio}) => {
this.setAttrValue("radios", {inputs, selectedRadio}) this.setAttrValue("radios", {inputs, selectedRadio}, () => {
})
} }
} }
@@ -156,6 +167,49 @@ export class RadioButton extends TkinterWidgetBase{
this.setWidgetInnerStyle("backgroundColor", "#fff0") this.setWidgetInnerStyle("backgroundColor", "#fff0")
} }
/**
*
* The index is required for pack as in pack every widget would be packed on top of each other in radio button
*/
getLayoutCode({index=0}){
let layoutManager = super.getLayoutCode()
const {layout: parentLayout, direction, gap, align="start"} = this.getParentLayout()
const absolutePositioning = this.getAttrValue("positioning")
if (parentLayout === Layouts.PLACE || absolutePositioning){
const config = {}
const elementRect = this.firstDivRef.current?.getBoundingClientRect()
config['x'] = Math.trunc(this.state.pos.x)
config['y'] = Math.trunc(this.state.pos.y)
if (elementRect?.height){
config['y'] = Math.trunc(config['y'] + (index * elementRect.height)) // the index is the radiobutton index
}
// config["width"] = Math.trunc(this.state.size.width)
// config["height"] = Math.trunc(this.state.size.height)
if (!this.state.fitContent.width){
config["width"] = Math.trunc(this.state.size.width)
}
if (!this.state.fitContent.height){
config["height"] = Math.trunc(this.state.size.height)
}
const configStr = convertObjectToKeyValueString(config)
layoutManager = `place(${configStr})`
}
return layoutManager
}
generateCode(variableName, parent){ generateCode(variableName, parent){
const config = convertObjectToKeyValueString(this.getConfigCode()) const config = convertObjectToKeyValueString(this.getConfigCode())
@@ -170,9 +224,10 @@ export class RadioButton extends TkinterWidgetBase{
const radioBtnVariable = `${variableName}_${idx}` const radioBtnVariable = `${variableName}_${idx}`
code.push(`\n`) code.push(`\n`)
// TODO increment the next widget with the height of the previous
code.push(`${radioBtnVariable} = tk.Radiobutton(master=${parent}, variable=${variableName}_var, text="${radio_text}")`) code.push(`${radioBtnVariable} = tk.Radiobutton(master=${parent}, variable=${variableName}_var, text="${radio_text}")`)
code.push(`${radioBtnVariable}.config(${config}, value=${idx})`) code.push(`${radioBtnVariable}.config(${config}, value=${idx})`)
code.push(`${radioBtnVariable}.${this.getLayoutCode()}`) code.push(`${radioBtnVariable}.${this.getLayoutCode({index: idx})}`)
}) })
const defaultSelected = radios.selectedRadio const defaultSelected = radios.selectedRadio
@@ -210,8 +265,13 @@ export class RadioButton extends TkinterWidgetBase{
<div className="tw-flex tw-flex-col tw-gap-2 tw-w-fit tw-h-fit"> <div className="tw-flex tw-flex-col tw-gap-2 tw-w-fit tw-h-fit">
{ {
inputs.map((value, index) => { inputs.map((value, index) => {
const ref = index === 0 ? this.firstDivRef : null
return ( return (
<div key={index} className="tw-flex tw-gap-2 tw-w-full tw-h-full tw-place-items-center "> <div key={index}
ref={ref}
className="tw-flex tw-gap-2 tw-w-full tw-h-full tw-place-items-center ">
<div className="tw-border-solid tw-border-[#D9D9D9] tw-border-2 <div className="tw-border-solid tw-border-[#D9D9D9] tw-border-2
tw-min-w-[20px] tw-min-h-[20px] tw-w-[20px] tw-h-[20px] tw-min-w-[20px] tw-min-h-[20px] tw-w-[20px] tw-h-[20px]
tw-text-blue-600 tw-flex tw-items-center tw-justify-center tw-text-blue-600 tw-flex tw-items-center tw-justify-center

View File

@@ -1,10 +1,14 @@
import { Layouts } from "../../../canvas/constants/layouts"
import Tools from "../../../canvas/constants/tools"
import Widget from "../../../canvas/widgets/base" import Widget from "../../../canvas/widgets/base"
import { convertObjectToKeyValueString } from "../../../utils/common"
import {TkinterBase} from "./base" import {TkinterBase} from "./base"
class Frame extends TkinterBase{ class Frame extends TkinterBase{
static widgetType = "frame" static widgetType = "frame"
static displayName = "Frame"
constructor(props) { constructor(props) {
super(props) super(props)
@@ -16,7 +20,99 @@ class Frame extends TkinterBase{
this.state = { this.state = {
...this.state, ...this.state,
fitContent: {width: true, height: true}, fitContent: {width: true, height: true},
widgetName: "Frame" widgetName: "Frame",
attrs: {
...this.state.attrs,
padding: {
label: "padding",
padX: {
label: "Pad X",
tool: Tools.NUMBER_INPUT,
toolProps: {min: 0, max: 140},
value: null,
onChange: (value) => {
// this.setWidgetInnerStyle("paddingLeft", `${value}px`)
// this.setWidgetInnerStyle("paddingRight", `${value}px`)
// const widgetStyle = {
// }
this.setState((prevState) => ({
widgetInnerStyling: {
...prevState.widgetInnerStyling,
paddingLeft: `${value}px`,
paddingRight: `${value}px`
}
}))
this.setAttrValue("padding.padX", value)
}
},
padY: {
label: "Pad Y",
tool: Tools.NUMBER_INPUT,
toolProps: {min: 0, max: 140},
value: null,
onChange: (value) => {
this.setState((prevState) => ({
widgetInnerStyling: {
...prevState.widgetInnerStyling,
paddingTop: `${value}px`,
paddingBottom: `${value}px`
}
}))
// this.setState({
// widgetInnerStyling: widgetStyle
// })
this.setAttrValue("padding.padY", value)
}
},
},
margin: {
label: "Margin",
marginX: {
label: "Margin X",
tool: Tools.NUMBER_INPUT,
toolProps: {min: 0, max: 140},
value: null,
onChange: (value) => {
this.updateState((prev) => ({
widgetOuterStyling: {
...prev.widgetOuterStyling,
marginLeft: `${value}px`,
marginRight: `${value}px`
},
}))
this.setAttrValue("margin.marginX", value)
}
},
marginY: {
label: "Margin Y",
tool: Tools.NUMBER_INPUT,
toolProps: {min: 0, max: 140},
value: null,
onChange: (value) => {
this.updateState((prev) => ({
widgetOuterStyling: {
...prev.widgetOuterStyling,
marginTop: `${value}px`,
marginBottom: `${value}px`
},
}))
this.setAttrValue("margin.marginY", value)
}
},
},
}
} }
} }
@@ -26,18 +122,59 @@ class Frame extends TkinterBase{
this.setAttrValue("styling.backgroundColor", "#EDECEC") this.setAttrValue("styling.backgroundColor", "#EDECEC")
} }
getConfigCode(){
const bg = this.getAttrValue("styling.backgroundColor")
const fitWidth = this.state.fitContent.width
const fitHeight = this.state.fitContent.height
const {width, height} = this.getSize()
const {layout} = this.getParentLayout()
const config = {
bg: `"${bg}"`
}
if (layout !== Layouts.PLACE){
if (!fitWidth){
config['width'] = width
}
if (!fitHeight){
config['height'] = height
}
}
return config
}
generateCode(variableName, parent){ generateCode(variableName, parent){
const bg = this.getAttrValue("styling.backgroundColor") const config = convertObjectToKeyValueString(this.getConfigCode())
return [ return [
`${variableName} = tk.Frame(master=${parent})`, `${variableName} = tk.Frame(master=${parent})`,
`${variableName}.config(bg="${bg}")`, `${variableName}.config(${config})`,
`${variableName}.${this.getLayoutCode()}` `${variableName}.${this.getLayoutCode()}`,
...this.getGridLayoutConfigurationCode(variableName)
] ]
} }
getToolbarAttrs(){
const {layout, gridConfig, gridWeights, ...toolBarAttrs} = super.getToolbarAttrs()
// places layout at the end
return ({
id: this.__id,
...toolBarAttrs,
padding: this.state.attrs.padding,
margin: this.state.attrs.margin,
layout,
gridConfig,
gridWeights
})
}
renderContent(){ renderContent(){
// console.log("bounding rect: ", this.getBoundingRect()) // console.log("bounding rect: ", this.getBoundingRect())
@@ -45,8 +182,10 @@ class Frame extends TkinterBase{
// console.log("widget styling: ", this.state.widgetInnerStyling) // console.log("widget styling: ", this.state.widgetInnerStyling)
return ( return (
<div className="tw-w-flex tw-flex-col tw-w-full tw-h-full tw-relative tw-rounded-md tw-overflow-hidden"> <div className="tw-w-flex tw-flex-col tw-w-full tw-h-full tw-relative tw-rounded-md tw-overflow-hidden">
<div className="tw-p-2 tw-w-full tw-h-full tw-content-start" style={this.getInnerRenderStyling()}> <div className="tw-p-2 tw-w-full tw-h-full tw-content-start"
{this.props.children} ref={this.styleAreaRef}
style={this.getInnerRenderStyling()}>
{this.renderTkinterLayout()} {/*This is required for pack layouts, so if your widget accepts child widgets, ensure to add this */}
</div> </div>
</div> </div>
) )

View File

@@ -6,6 +6,7 @@ import { TkinterWidgetBase } from "./base"
export class Input extends TkinterWidgetBase{ export class Input extends TkinterWidgetBase{
static widgetType = "entry" static widgetType = "entry"
static displayName = "Entry"
constructor(props) { constructor(props) {
super(props) super(props)
@@ -40,7 +41,7 @@ export class Input extends TkinterWidgetBase{
const config = convertObjectToKeyValueString(this.getConfigCode()) const config = convertObjectToKeyValueString(this.getConfigCode())
return [ return [
`${variableName} = tk.Entry(master=${parent}, text="${placeHolderText}")`, `${variableName} = tk.Entry(master=${parent})`,
`${variableName}.config(${config})`, `${variableName}.config(${config})`,
`${variableName}.${this.getLayoutCode()}` `${variableName}.${this.getLayoutCode()}`
] ]
@@ -65,6 +66,7 @@ export class Input extends TkinterWidgetBase{
return ( return (
<div className="tw-w-flex tw-flex-col tw-w-full tw-h-full tw-rounded-md tw-overflow-hidden"> <div className="tw-w-flex tw-flex-col tw-w-full tw-h-full tw-rounded-md tw-overflow-hidden">
<div className="tw-p-2 tw-w-full tw-h-full tw-flex tw-place-items-center" <div className="tw-p-2 tw-w-full tw-h-full tw-flex tw-place-items-center"
ref={this.styleAreaRef}
style={this.getInnerRenderStyling()}> style={this.getInnerRenderStyling()}>
<div className="tw-text-sm tw-text-gray-300"> <div className="tw-text-sm tw-text-gray-300">
{this.getAttrValue("placeHolder")} {this.getAttrValue("placeHolder")}
@@ -138,6 +140,7 @@ export class Text extends TkinterWidgetBase{
return ( return (
<div className="tw-w-flex tw-flex-col tw-w-full tw-h-full tw-rounded-md tw-overflow-hidden"> <div className="tw-w-flex tw-flex-col tw-w-full tw-h-full tw-rounded-md tw-overflow-hidden">
<div className="tw-p-2 tw-w-full tw-h-full tw-content-start " <div className="tw-p-2 tw-w-full tw-h-full tw-content-start "
ref={this.styleAreaRef}
style={this.getInnerRenderStyling()}> style={this.getInnerRenderStyling()}>
<div className="tw-text-sm tw-text-gray-300"> <div className="tw-text-sm tw-text-gray-300">
{this.getAttrValue("placeHolder")} {this.getAttrValue("placeHolder")}

View File

@@ -1,13 +1,18 @@
import { useEffect, useState } from "react"
import { Layouts } from "../../../canvas/constants/layouts"
import Tools from "../../../canvas/constants/tools" import Tools from "../../../canvas/constants/tools"
import { convertObjectToKeyValueString } from "../../../utils/common" import { convertObjectToKeyValueString } from "../../../utils/common"
import { getPythonAssetPath } from "../../utils/pythonFilePath" import { getPythonAssetPath } from "../../utils/pythonFilePath"
import { ANCHOR } from "../constants/styling"
import { TkinterWidgetBase } from "./base" import { TkinterWidgetBase } from "./base"
class Label extends TkinterWidgetBase{ class Label extends TkinterWidgetBase{
static widgetType = "label" static widgetType = "label"
static displayName = "Label"
// static requiredCustomPyFiles = ["imageLabel"]
constructor(props) { constructor(props) {
super(props) super(props)
@@ -19,6 +24,26 @@ class Label extends TkinterWidgetBase{
size: { width: 80, height: 40 }, size: { width: 80, height: 40 },
attrs: { attrs: {
...this.state.attrs, ...this.state.attrs,
styling: {
...this.state.attrs.styling,
anchor: {
label: "Text align",
tool: Tools.SELECT_DROPDOWN,
options: ANCHOR.map((val) => ({value: val, label: val})),
value: "",
onChange: (value) => {
this.setAttrValue("styling.anchor", value)
this.setState((prevState) => ({
widgetInnerStyle: {
...prevState,
...this.getAnchorStyle(value)
}
}))
}
}
},
labelWidget: { labelWidget: {
label: "Text", label: "Text",
tool: Tools.INPUT, tool: Tools.INPUT,
@@ -33,6 +58,34 @@ class Label extends TkinterWidgetBase{
value: "", value: "",
onChange: (value) => this.setAttrValue("imageUpload", value) onChange: (value) => this.setAttrValue("imageUpload", value)
}, },
imageSize: {
label: "Image size",
display: "horizontal",
// width: {
// label: "Width",
// tool: Tools.NUMBER_INPUT, // the tool to display, can be either HTML ELement or a constant string
// toolProps: { placeholder: "width", max: 3000, min: 1 },
// value: 150,
// onChange: (value) => this.setWidgetSize(value, null)
// },
// height: {
// label: "Height",
// tool: Tools.NUMBER_INPUT,
// toolProps: { placeholder: "height", max: 3000, min: 1 },
// value: 150,
// onChange: (value) => this.setWidgetSize(null, value)
// },
mode: {
label: "Image mode",
tool: Tools.SELECT_DROPDOWN,
options: ["fit", "cover"].map((val) => ({value: val, label: val})),
value: "cover",
onChange: (value) => {
this.setAttrValue("imageSize.mode", value)
}
}
},
} }
} }
@@ -51,11 +104,20 @@ class Label extends TkinterWidgetBase{
const imports = super.getImports() const imports = super.getImports()
if (this.getAttrValue("imageUpload")) if (this.getAttrValue("imageUpload"))
imports.push("import os", "from PIL import Image, ImageTk", ) imports.push("import os", "from PIL import Image, ImageTk", "from customWidgets.imageLabel import ImageLabel")
return imports return imports
} }
getRequiredCustomPyFiles(){
const requiredCustomFiles = super.getRequiredCustomPyFiles()
if (this.getAttrValue("imageUpload"))
requiredCustomFiles.push("imageLabel")
return requiredCustomFiles
}
getRequirements(){ getRequirements(){
const requirements = super.getRequirements() const requirements = super.getRequirements()
@@ -66,10 +128,40 @@ class Label extends TkinterWidgetBase{
return requirements return requirements
} }
getConfigCode(){
const config = super.getConfigCode()
const anchor = this.getAttrValue("styling.anchor")
const fitWidth = this.state.fitContent.width
const fitHeight = this.state.fitContent.height
const {width, height} = this.getSize()
const {layout} = this.getParentLayout()
if (anchor)
config['anchor'] = `"${anchor}"`
// LABEL width and height are not pixel based instead its character based
// if (layout !== Layouts.PLACE){
// if (!fitWidth){
// config['width'] = width
// }
// if (!fitHeight){
// config['height'] = height
// }
// }
return config
}
generateCode(variableName, parent){ generateCode(variableName, parent){
const labelText = this.getAttrValue("labelWidget") const labelText = this.getAttrValue("labelWidget")
const config = convertObjectToKeyValueString(this.getConfigCode()) const config = convertObjectToKeyValueString(this.getConfigCode())
const image = this.getAttrValue("imageUpload") const image = this.getAttrValue("imageUpload")
@@ -78,10 +170,10 @@ class Label extends TkinterWidgetBase{
const code = [] const code = []
if (image?.name){ if (image?.name){
code.push(`${variableName}_img = Image.open(${getPythonAssetPath(image.name, "image")})`) // code.push(`${variableName}_img = Image.open(${getPythonAssetPath(image.name, "image")})`)
code.push(`${variableName}_img = ImageTk.PhotoImage(${variableName}_img)`) // code.push(`${variableName}_img = ImageTk.PhotoImage(${variableName}_img)`)
// code.push("\n") // code.push("\n")
labelInitialization = `${variableName} = tk.Label(master=${parent}, image=${variableName}_img, text="${labelText}", compound=tk.TOP)` labelInitialization = `${variableName} = ImageLabel(master=${parent}, image_path=${getPythonAssetPath(image.name, "image")}, text="${labelText}", compound=tk.TOP, mode="${this.getAttrValue("imageSize.mode")}")`
} }
// code.push("\n") // code.push("\n")
@@ -108,29 +200,66 @@ class Label extends TkinterWidgetBase{
}) })
} }
renderContent(){ getAnchorStyle = (anchor) => {
const anchorStyles = {
n: { justifyContent: 'center', alignItems: 'flex-start' },
s: { justifyContent: 'center', alignItems: 'flex-end' },
e: { justifyContent: 'flex-end', alignItems: 'center' },
w: { justifyContent: 'flex-start', alignItems: 'center' },
ne: { justifyContent: 'flex-end', alignItems: 'flex-start' },
se: { justifyContent: 'flex-end', alignItems: 'flex-end' },
nw: { justifyContent: 'flex-start', alignItems: 'flex-start' },
sw: { justifyContent: 'flex-start', alignItems: 'flex-end' },
center: { justifyContent: 'center', alignItems: 'center' }
}
return anchorStyles[anchor] || anchorStyles["center"];
}
renderContent(){
//FIXME: label image causing issues
const image = this.getAttrValue("imageUpload") const image = this.getAttrValue("imageUpload")
const imageMode = this.getAttrValue("imageSize.mode") || "cover"
const imgClassName = imageMode === "fit" ? "tw-object-contain" : (imageMode === "cover" ? "tw-object-cover" : "")
return ( return (
<div className="tw-w-flex tw-flex-col tw-w-full tw-content-start tw-h-full tw-rounded-md tw-overflow-hidden" <div className="tw-flex tw-flex-col tw-w-full tw-relative tw-content-start tw-h-full tw-rounded-md tw-overflow-hidden"
style={{ // style={{
flexGrow: 1, // Ensure the content grows to fill the parent // flexGrow: 1, // Ensure the content grows to fill the parent
minWidth: '100%', // Force the width to 100% of the parent // minWidth: '100%', // Force the width to 100% of the parent
minHeight: '100%', // Force the height to 100% of the parent // minHeight: '100%', // Force the height to 100% of the parent
}} // }}
> >
<div className="tw-p-2 tw-w-full tw-h-full tw-flex tw-place-content-center tw-place-items-center " <div className="tw-p-2 tw-w-full tw-h-full tw-overflow-hidden tw-flex tw-place-content-center tw-place-items-center"
ref={this.styleAreaRef}
style={this.getInnerRenderStyling()}> style={this.getInnerRenderStyling()}>
{/* {this.props.children} */} {/* {this.props.children} */}
{ {
image && ( image ? (
<img src={image.previewUrl} className="tw-bg-contain tw-w-full tw-h-full" /> <div className="tw-relative tw-w-full tw-h-full tw-overflow-hidden">
) <img src={image.previewUrl}
} className={`${imgClassName}`}
<div className="" style={{color: this.getAttrValue("styling.foregroundColor")}}> alt={this.getAttrValue("widgetName")}
{this.getAttrValue("labelWidget")} style={{
</div> width: "100%",
height: "100%",
position: "absolute",
top: '0px',
left: '0px',
}}
// className="tw-object-cover"
/>
</div>
) : null
}
<div className={`tw-flex ${!image ? "tw-w-full tw-h-full" : ""}`} style={{
color: this.getAttrValue("styling.foregroundColor"),
...this.getAnchorStyle(this.getAttrValue("styling.anchor"))
}}>
{this.getAttrValue("labelWidget")}
</div>
</div> </div>
</div> </div>
) )
@@ -139,4 +268,44 @@ class Label extends TkinterWidgetBase{
} }
function LabelImage({imageSrc, alt, styleAreaRef}){
const [size, setSize] = useState({
width: 0,
height: 0
})
useEffect(() => {
if (!styleAreaRef.current) return;
// Function to update size
const updateSize = () => {
const boundingBox = styleAreaRef.current.getBoundingClientRect();
setSize({ width: boundingBox.width, height: boundingBox.height });
};
// Initial size update
updateSize();
// Observe size changes
const resizeObserver = new ResizeObserver(updateSize);
resizeObserver.observe(styleAreaRef.current);
return () => resizeObserver.disconnect();
}, [styleAreaRef])
return (
<img src={imageSrc} alt={alt} className="tw-object-cover"
style={{
position: "absolute",
top: '0px',
left: '0px',
width: "100%", // Prevent overflow
height: "100%", // Prevent overflow
}}
/>
)
}
export default Label export default Label

View File

@@ -1,11 +1,19 @@
import Widget from "../../../canvas/widgets/base" import Widget from "../../../canvas/widgets/base"
import Tools from "../../../canvas/constants/tools" import Tools from "../../../canvas/constants/tools"
import { TkinterBase } from "./base" import { TkinterBase } from "./base"
import { removeKeyFromObject } from "../../../utils/common"
import { getPythonAssetPath } from "../../utils/pythonFilePath"
class MainWindow extends TkinterBase{ class MainWindow extends TkinterBase{
static widgetType = "main_window" static widgetType = "main_window"
static displayName = "Main Window"
static initialSize = {
width: 700,
height: 400
}
constructor(props) { constructor(props) {
super(props) super(props)
@@ -14,18 +22,27 @@ class MainWindow extends TkinterBase{
exclude: ["image", "video", "media", "main_window", "toplevel"] exclude: ["image", "video", "media", "main_window", "toplevel"]
} }
const newAttrs = removeKeyFromObject("margin", this.state.attrs)
this.state = { this.state = {
...this.state, ...this.state,
size: { width: 700, height: 400 }, size: { width: MainWindow.initialSize.width, height: MainWindow.initialSize.height },
widgetName: "main", widgetName: "main",
attrs: { attrs: {
...this.state.attrs, ...newAttrs,
title: { title: {
label: "Window Title", label: "Window Title",
tool: Tools.INPUT, // the tool to display, can be either HTML ELement or a constant string tool: Tools.INPUT, // the tool to display, can be either HTML ELement or a constant string
toolProps: {placeholder: "Window title", maxLength: 40}, toolProps: {placeholder: "Window title", maxLength: 40},
value: "Main Window", value: "Main Window",
onChange: (value) => this.setAttrValue("title", value) onChange: (value) => this.setAttrValue("title", value)
},
logo: {
label: "Window Logo",
tool: Tools.UPLOADED_LIST,
toolProps: {filterOptions: ["image/jpg", "image/jpeg", "image/png"]},
value: "",
onChange: (value) => this.setAttrValue("logo", value)
} }
} }
@@ -33,29 +50,58 @@ class MainWindow extends TkinterBase{
} }
// componentDidMount(){ // componentDidMount(){
// // this.setAttrValue("styling.backgroundColor", "#E4E2E2")
// super.componentDidMount() // super.componentDidMount()
// //this.setAttrValue("styling", { backgroundColor: "#E4E2E2" }) refactor to something like this?
// this.setAttrValue("styling.backgroundColor", "#E4E2E2")
// console.log("mounted: ", this.state)
// } // }
componentDidMount(){
this.setAttrValue("styling.backgroundColor", "#E4E2E2")
super.componentDidMount()
}
generateCode(variableName, parent){ generateCode(variableName, parent){
const backgroundColor = this.getAttrValue("styling.backgroundColor") const backgroundColor = this.getAttrValue("styling.backgroundColor")
return [ const logo = this.getAttrValue("logo")
`${variableName} = tk.Tk()`,
`${variableName}.config(bg="${backgroundColor}")`, const {width, height} = this.getSize()
`${variableName}.title("${this.getAttrValue("title")}")`
] const code = [
`${variableName} = tk.Tk()`,
`${variableName}.config(bg="${backgroundColor}")`,
`${variableName}.title("${this.getAttrValue("title")}")`,
`${variableName}.geometry("${width}x${height}")`,
...this.getGridLayoutConfigurationCode(variableName)
]
if (logo?.name){
// code.push(`\n`)
code.push(`${variableName}_img = Image.open(${getPythonAssetPath(logo.name, "image")})`)
code.push(`${variableName}_img = ImageTk.PhotoImage(${variableName}_img)`)
code.push(`${variableName}.iconphoto(False, ${variableName}_img)`)
// code.push("\n")
}
return code
}
getImports(){
const imports = super.getImports()
if (this.getAttrValue("logo"))
imports.push("import os", "from PIL import Image, ImageTk", )
return imports
}
getRequirements(){
const requirements = super.getRequirements()
if (this.getAttrValue("logo"))
requirements.push("pillow")
return requirements
} }
getToolbarAttrs(){ getToolbarAttrs(){
@@ -65,18 +111,28 @@ class MainWindow extends TkinterBase{
id: this.__id, id: this.__id,
widgetName: toolBarAttrs.widgetName, widgetName: toolBarAttrs.widgetName,
title: this.state.attrs.title, title: this.state.attrs.title,
logo: this.state.attrs.logo,
size: toolBarAttrs.size, size: toolBarAttrs.size,
...this.state.attrs, ...this.state.attrs,
}) })
} }
renderContent(){ renderContent(){
const logo = this.getAttrValue("logo")
return ( return (
<div className="tw-w-flex tw-flex-col tw-w-full tw-h-full tw-rounded-md tw-overflow-hidden"> <div className="tw-w-flex tw-flex-col tw-w-full tw-h-full tw-rounded-md tw-overflow-hidden">
<div className="tw-flex tw-w-full tw-h-[25px] tw-bg-[#c7c7c7] tw-p-1 <div className="tw-flex tw-w-full tw-h-[25px] tw-bg-[#c7c7c7] tw-p-1
tw-overflow-hidden tw-shadow-xl tw-place-items-center"> tw-overflow-hidden tw-shadow-xl tw-place-items-center tw-gap-1">
{
logo && (
<img src={logo.previewUrl} alt={logo.name}
className="tw-bg-contain tw-w-[15px] tw-h-[15px] tw-rounded-sm" />
)
}
<div className="tw-text-sm">{this.getAttrValue("title")}</div> <div className="tw-text-sm">{this.getAttrValue("title")}</div>
<div className="tw-ml-auto tw-flex tw-gap-1 tw-place-items-center"> <div className="tw-ml-auto tw-flex tw-gap-1 tw-place-items-center">
<div className="tw-bg-yellow-400 tw-rounded-full tw-w-[15px] tw-h-[15px]"> <div className="tw-bg-yellow-400 tw-rounded-full tw-w-[15px] tw-h-[15px]">
@@ -88,8 +144,10 @@ class MainWindow extends TkinterBase{
</div> </div>
</div> </div>
<div className="tw-p-2 tw-w-full tw-relative tw-h-full tw-overflow-hidden tw-content-start" <div className="tw-p-2 tw-w-full tw-relative tw-h-full tw-overflow-hidden tw-content-start"
style={this.getInnerRenderStyling()}> ref={this.styleAreaRef}
{this.props.children} style={{...this.getInnerRenderStyling(), width: "100%", height: "calc(100% - 25px)"}}>
{/* {this.props.children} */}
{this.renderTkinterLayout()} {/* This is required for pack layouts, so if your widget accepts child widgets, ensure to add this */}
</div> </div>
</div> </div>
) )

View File

@@ -7,6 +7,7 @@ import { convertObjectToKeyValueString } from "../../../utils/common"
class OptionMenu extends TkinterWidgetBase{ class OptionMenu extends TkinterWidgetBase{
static widgetType = "option_menu" static widgetType = "option_menu"
static displayName = "Option Menu"
constructor(props) { constructor(props) {
super(props) super(props)
@@ -27,8 +28,8 @@ class OptionMenu extends TkinterWidgetBase{
label: "Default Value", label: "Default Value",
tool: Tools.INPUT, tool: Tools.INPUT,
value: "Select option", value: "Select option",
onChange: ({inputs, selectedRadio}) => { onChange: (value) => {
this.setAttrValue("options", {inputs, selectedRadio}) this.setAttrValue("defaultValue", value)
} }
}, },
widgetOptions: { widgetOptions: {
@@ -100,6 +101,7 @@ class OptionMenu extends TkinterWidgetBase{
return ( return (
<div className="tw-flex tw-p-1 tw-w-full tw-h-full tw-rounded-md tw-overflow-hidden" <div className="tw-flex tw-p-1 tw-w-full tw-h-full tw-rounded-md tw-overflow-hidden"
style={this.getInnerRenderStyling()} style={this.getInnerRenderStyling()}
ref={this.styleAreaRef}
onClick={this.toggleDropDownOpen} onClick={this.toggleDropDownOpen}
> >
<div className="tw-flex tw-justify-between tw-gap-1"> <div className="tw-flex tw-justify-between tw-gap-1">

View File

@@ -7,6 +7,8 @@ import {TkinterBase, TkinterWidgetBase} from "./base"
class Slider extends TkinterWidgetBase{ class Slider extends TkinterWidgetBase{
static widgetType = "scale" static widgetType = "scale"
static displayName = "Scale"
// FIXME: You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use // FIXME: You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use
constructor(props) { constructor(props) {
super(props) super(props)
@@ -136,7 +138,9 @@ class Slider extends TkinterWidgetBase{
return ( return (
<div className="tw-w-flex tw-flex-col tw-w-full tw-h-full tw-rounded-md tw-overflow-hidden"> <div className="tw-w-flex tw-flex-col tw-w-full tw-h-full tw-rounded-md tw-overflow-hidden">
<div className="flex flex-col items-center justify-center h-screen <div className="flex flex-col items-center justify-center h-screen
bg-gray-100" style={this.getInnerRenderStyling()}> bg-gray-100"
ref={this.styleAreaRef}
style={this.getInnerRenderStyling()}>
<div className="w-full max-w-md"> <div className="w-full max-w-md">
<input <input
type="range" type="range"

View File

@@ -8,6 +8,7 @@ import {TkinterBase, TkinterWidgetBase} from "./base"
class SpinBox extends TkinterWidgetBase{ class SpinBox extends TkinterWidgetBase{
static widgetType = "spin_box" static widgetType = "spin_box"
static displayName = "Spin Box"
constructor(props) { constructor(props) {
super(props) super(props)
@@ -109,6 +110,7 @@ class SpinBox extends TkinterWidgetBase{
return ( return (
<div className="tw-w-flex tw-flex-col tw-w-full tw-h-full tw-rounded-md tw-overflow-hidden"> <div className="tw-w-flex tw-flex-col tw-w-full tw-h-full tw-rounded-md tw-overflow-hidden">
<div className="tw-p-2 tw-w-full tw-h-full tw-flex tw-place-items-center tw-justify-between" <div className="tw-p-2 tw-w-full tw-h-full tw-flex tw-place-items-center tw-justify-between"
ref={this.styleAreaRef}
style={this.getInnerRenderStyling()}> style={this.getInnerRenderStyling()}>
<div className="tw-text-sm "> <div className="tw-text-sm ">
{this.getAttrValue("spinProps.default")} {this.getAttrValue("spinProps.default")}

View File

@@ -1,10 +1,14 @@
import Widget from "../../../canvas/widgets/base" import Widget from "../../../canvas/widgets/base"
import Tools from "../../../canvas/constants/tools" import Tools from "../../../canvas/constants/tools"
import { getPythonAssetPath } from "../../utils/pythonFilePath"
import { TkinterBase } from "./base"
class TopLevel extends Widget{ class TopLevel extends TkinterBase{
static widgetType = "toplevel" static widgetType = "toplevel"
static displayName = "Top Level"
constructor(props) { constructor(props) {
super(props) super(props)
@@ -26,6 +30,13 @@ class TopLevel extends Widget{
toolProps: {placeholder: "Window title", maxLength: 40}, toolProps: {placeholder: "Window title", maxLength: 40},
value: "Top level", value: "Top level",
onChange: (value) => this.setAttrValue("title", value) onChange: (value) => this.setAttrValue("title", value)
},
logo: {
label: "Toplevel Logo",
tool: Tools.UPLOADED_LIST,
toolProps: {filterOptions: ["image/jpg", "image/jpeg", "image/png"]},
value: "",
onChange: (value) => this.setAttrValue("logo", value)
} }
} }
@@ -37,15 +48,54 @@ class TopLevel extends Widget{
super.componentDidMount() super.componentDidMount()
} }
generateCode(variableName, parent){ generateCode(variableName, parent){
const backgroundColor = this.getAttrValue("styling.backgroundColor") const backgroundColor = this.getAttrValue("styling.backgroundColor")
return [ const logo = this.getAttrValue("logo")
`${variableName} = tk.Toplevel(master=${parent})`,
`${variableName}.config(bg="${backgroundColor}")`, const {width, height} = this.getSize()
`${variableName}.title("${this.getAttrValue("title")}")`
] const code = [
`${variableName} = tk.Toplevel(master=${parent})`,
`${variableName}.config(bg="${backgroundColor}")`,
`${variableName}.title("${this.getAttrValue("title")}")`,
`${variableName}.geometry("${width}x${height}")`,
...this.getGridLayoutConfigurationCode(variableName)
]
if (logo?.name){
// code.push(`\n`)
code.push(`${variableName}_img = Image.open(${getPythonAssetPath(logo.name, "image")})`)
code.push(`${variableName}_img = ImageTk.PhotoImage(${variableName}_img)`)
code.push(`${variableName}.iconphoto(False, ${variableName}_img)`)
// code.push("\n")
}
return code
}
getImports(){
const imports = super.getImports()
if (this.getAttrValue("logo"))
imports.push("import os", "from PIL import Image, ImageTk", )
return imports
}
getRequirements(){
const requirements = super.getRequirements()
if (this.getAttrValue("logo"))
requirements.push("pillow")
return requirements
} }
getToolbarAttrs(){ getToolbarAttrs(){
@@ -54,18 +104,26 @@ class TopLevel extends Widget{
return ({ return ({
widgetName: toolBarAttrs.widgetName, widgetName: toolBarAttrs.widgetName,
title: this.state.attrs.title, title: this.state.attrs.title,
logo: this.state.attrs.logo,
size: toolBarAttrs.size, size: toolBarAttrs.size,
...this.state.attrs, ...this.state.attrs,
}) })
} }
renderContent(){ renderContent(){
const logo = this.getAttrValue("logo")
return ( return (
<div className="tw-w-flex tw-flex-col tw-w-full tw-h-full tw-rounded-md tw-overflow-hidden"> <div className="tw-w-flex tw-flex-col tw-w-full tw-h-full tw-rounded-md tw-overflow-hidden">
<div className="tw-flex tw-w-full tw-h-[25px] tw-bg-[#c7c7c7] tw-p-1 <div className="tw-flex tw-w-full tw-h-[25px] tw-bg-[#c7c7c7] tw-p-1 tw-gap-1
tw-overflow-hidden tw-shadow-xl tw-place-items-center"> tw-overflow-hidden tw-shadow-xl tw-place-items-center">
{
logo && (
<img src={logo.previewUrl} alt={logo.name}
className="tw-bg-contain tw-w-[15px] tw-h-[15px] tw-rounded-sm" />
)
}
<div className="tw-text-sm">{this.getAttrValue("title")}</div> <div className="tw-text-sm">{this.getAttrValue("title")}</div>
<div className="tw-ml-auto tw-flex tw-gap-1 tw-place-items-center"> <div className="tw-ml-auto tw-flex tw-gap-1 tw-place-items-center">
<div className="tw-bg-yellow-400 tw-rounded-full tw-w-[15px] tw-h-[15px]"> <div className="tw-bg-yellow-400 tw-rounded-full tw-w-[15px] tw-h-[15px]">
@@ -76,8 +134,10 @@ class TopLevel extends Widget{
</div> </div>
</div> </div>
</div> </div>
<div className="tw-p-2 tw-w-full tw-h-full tw-content-start" style={this.state.widgetInnerStyling}> <div className="tw-p-2 tw-w-full tw-h-full tw-content-start"
{this.props.children} ref={this.styleAreaRef}
style={this.state.widgetInnerStyling}>
{this.renderTkinterLayout()}
</div> </div>
</div> </div>
) )

View File

@@ -11,8 +11,12 @@ import { QueryClient, QueryClientProvider } from "react-query";
import "./styles/tailwind.css"; import "./styles/tailwind.css";
import "./styles/index.css"; import "./styles/index.css";
import { FileUploadProvider } from "./contexts/fileUploadContext"; import { FileUploadProvider } from "./contexts/fileUploadContext";
window.React = React
const originalSetItem = localStorage.setItem; const originalSetItem = localStorage.setItem;
// triggers itemsChaned event whenever the item in localstorage is chanegd. // triggers itemsChaned event whenever the item in localstorage is chanegd.
localStorage.setItem = function (key, value) { localStorage.setItem = function (key, value) {

View File

@@ -9,6 +9,7 @@ import ButtonWidget from "../assets/widgets/button.png"
import { filterObjectListStartingWith } from "../utils/filter" import { filterObjectListStartingWith } from "../utils/filter"
import Widget from "../canvas/widgets/base" import Widget from "../canvas/widgets/base"
import { SearchComponent } from "../components/inputs" import { SearchComponent } from "../components/inputs"
import { Button } from "antd"
/** /**
@@ -72,6 +73,13 @@ function PluginsContainer({sidebarContent, onWidgetsUpdate}){
}) })
} }
</div> </div>
<Button href="https://github.com/PaulleDemon/PyUIBuilder/issues"
target="_blank" rel="noopener noreferrer"
className="tw-flex tw-mt-6 btn"
>
Request new plugin
</Button>
</div> </div>
) )

View File

@@ -4,11 +4,12 @@
*/ */
import { useEffect, useRef, useMemo, useState } from "react"; import { useEffect, useRef, useMemo, useState } from "react";
import { BookOutlined, CloseCircleFilled, CrownFilled, GithubFilled, ShareAltOutlined } from "@ant-design/icons"; import { BookOutlined, CloseCircleFilled, CrownFilled, DiscordFilled, GithubFilled, InfoCircleOutlined, ShareAltOutlined } from "@ant-design/icons";
import KO_FI from "../assets/logo/ko-fi.png" import KO_FI from "../assets/logo/ko-fi.png"
import Premium from "./utils/premium"; import Premium from "./utils/premium";
import Share from "./utils/share"; import Share from "./utils/share";
import { Tooltip } from "antd";
@@ -62,7 +63,7 @@ function Sidebar({tabs}){
> >
<div className="tw-min-w-[80px] tw-w-[80px] tw-h-full tw-flex tw-flex-col tw-gap-4 tw-p-3 tw-place-items-center"> <div className="tw-min-w-[80px] tw-w-[80px] tw-bg-gray-100 tw-h-full tw-flex tw-flex-col tw-gap-4 tw-p-3 tw-place-items-center">
{ {
sidebarTabs.map((tab, index) => { sidebarTabs.map((tab, index) => {
return ( return (
@@ -88,6 +89,21 @@ function Sidebar({tabs}){
} }
<div className="tw-flex tw-flex-col tw-place-content-items tw-place-items-center tw-gap-3 tw-mt-auto"> <div className="tw-flex tw-flex-col tw-place-content-items tw-place-items-center tw-gap-3 tw-mt-auto">
<Tooltip title="About">
<a href="https://about.pyuibuilder.com" target="_blank"
title="about"
rel="noopener noreferrer" className="tw-text-2xl tw-text-black tw-cursor-pointer">
<InfoCircleOutlined />
</a>
</Tooltip>
<Tooltip title="Discord invite">
<a href="https://discord.gg/dHXjrrCA7G" target="_blank"
title="discord invite"
rel="noopener noreferrer" className="tw-text-3xl tw-cursor-pointer tw-text-[#5562EA]">
<DiscordFilled />
</a>
</Tooltip>
<Premium className="tw-text-2xl tw-bg-purple-700 tw-text-center <Premium className="tw-text-2xl tw-bg-purple-700 tw-text-center
tw-w-[35px] tw-h-[35px] tw-rounded-md tw-w-[35px] tw-h-[35px] tw-rounded-md
tw-cursor-pointer tw-text-white tw-cursor-pointer tw-text-white
@@ -95,25 +111,33 @@ function Sidebar({tabs}){
hover:tw-scale-[1.2]"> hover:tw-scale-[1.2]">
<CrownFilled /> <CrownFilled />
</Premium> </Premium>
<Share className="tw-cursor-pointer tw-text-xl"> <Share className="tw-cursor-pointer tw-text-xl">
<ShareAltOutlined /> <Tooltip title="Share">
<ShareAltOutlined />
</Tooltip>
</Share> </Share>
<a className="tw-cursor-pointer tw-text-xl tw-text-gray-700"
href="https://pyuibuilder-docs.pages.dev/" <Tooltip title="Docs">
target="_blank" rel="noopener noreferrer"> <a className="tw-cursor-pointer tw-text-xl tw-text-gray-700"
<i className="bi bi-book-half"></i> href="https://docs.pyuibuilder.com/"
</a> target="_blank" rel="noopener noreferrer">
<i className="bi bi-book-half"></i>
</a>
</Tooltip>
<a href="https://github.com/PaulleDemon/PyUIBuilder" target="_blank" <a href="https://github.com/PaulleDemon/PyUIBuilder" target="_blank"
rel="noopener noreferrer" className="tw-text-2xl tw-cursor-pointer tw-text-black"> rel="noopener noreferrer" className="tw-text-2xl tw-cursor-pointer tw-text-black">
<GithubFilled /> <GithubFilled />
</a> </a>
<a href="https://ko-fi.com/artpaul" className="tw-cursor-pointer "> {/* <a href="https://ko-fi.com/artpaul" className="tw-cursor-pointer ">
<img src={KO_FI} alt="ko-fi" className="tw-w-[30px] tw-h-[30px]"/> <img src={KO_FI} alt="ko-fi" className="tw-w-[30px] tw-h-[30px]"/>
</a> </a> */}
</div> </div>
</div> </div>
<div className="tw-w-full tw-h-full tw-bg-inherit tw-flex tw-flex-col tw-overflow-x-hidden" ref={sideBarExtraRef}> <div className="tw-w-full tw-h-full tw-bg-inherit tw-rounded-lg
tw-shadow-[rgb(207,207,207)] tw-shadow-[rgb(207,207,207,0.5)_-3px_0px_10px_] tw-flex tw-flex-col tw-overflow-x-hidden"
ref={sideBarExtraRef}>
<div className="tw-w-full tw-h-[50px] tw-flex tw-place-content-end tw-p-1"> <div className="tw-w-full tw-h-[50px] tw-flex tw-place-content-end tw-p-1">
<button className="tw-outline-none tw-bg-transparent tw-border-none tw-text-gray-600 tw-cursor-pointer tw-text-xl" <button className="tw-outline-none tw-bg-transparent tw-border-none tw-text-gray-600 tw-cursor-pointer tw-text-xl"
onClick={closeSidebar} onClick={closeSidebar}

View File

@@ -50,10 +50,13 @@ function TemplatesContainer(){
<div className="tw-flex tw-flex-col tw-mt-6 tw-gap-2"> <div className="tw-flex tw-flex-col tw-mt-6 tw-gap-2">
<div>Want to be notified of the release?</div> <div>Want to be notified of the release?</div>
<a href="https://paulfreeman.substack.com/subscribe?utm_source=pyGUIBuilder_web" <button data-tally-open="mVDY7N" data-tally-layout="modal" data-tally-emoji-text="👋"
data-tally-emoji-animation="wave" className="tw-p-2 tw-w-full tw-outline-none tw-bg-blue-500 tw-rounded-md tw-no-underline
tw-text-white tw-text-center tw-py-3 tw-border-none tw-text-base tw-shadow-md tw-cursor-pointer">Join the waitlist</button>
{/* <a href="https://paulfreeman.substack.com/subscribe?utm_source=pyGUIBuilder_web"
className="tw-p-2 tw-w-full tw-bg-blue-500 tw-rounded-md tw-no-underline className="tw-p-2 tw-w-full tw-bg-blue-500 tw-rounded-md tw-no-underline
tw-text-white tw-text-center tw-py-3" tw-text-white tw-text-center tw-py-3"
target="_blank" rel="noreferrer noopener">Subscribe to newsletter</a> target="_blank" rel="noreferrer noopener">Join the waitlist</a> */}
</div> </div>
</div> </div>

View File

@@ -0,0 +1,76 @@
import { useEffect, useMemo, useState } from "react"
import { CloseCircleFilled, SearchOutlined } from "@ant-design/icons"
import { SidebarWidgetCard, TreeViewCard } from "../components/cards"
import { Tree } from "antd"
import { useWidgetContext } from "../canvas/context/widgetContext"
function transformWidgets(widgets, widgetRefs, isTopLevel=true) {
// console.log("Wdiegts refs: ", widgetRefs)
return widgets.map(widget => ({
title: widgetRefs.current[widget.id].current.getDisplayName(), // Assuming widgetType is a class
key: widget.id,
isTopLevel: isTopLevel,
widgetRef: widgetRefs.current[widget.id],
children: widget.children.length > 0 ? transformWidgets(widget.children, widgetRefs, false) : []
}))
}
/**
*
* @param {function} onWidgetsUpdate - this is a callback that will be called once the sidebar is populated with widgets
* @returns
*/
function TreeviewContainer() {
const {widgets, widgetRefs} = useWidgetContext()
const transformedContent = useMemo(() => {
return (transformWidgets(widgets, widgetRefs))
}, [widgets, widgetRefs])
const topLevelKeys = transformedContent.filter(cont => cont.isTopLevel).map(cont => cont.key)
return (
<div className="tw-w-full tw-p-2 tw-gap-4 tw-flex tw-flex-col tw-overflow-x-hidden">
{/* <SearchComponent onSearch={onSearch} searchValue={searchValue}
onClear={() => setSearchValue("")} /> */}
<div className="tw-flex tw-flex-col tw-gap-2 tw-w-full tw-h-full tw-p-1">
<Tree showLine
treeData={transformedContent}
titleRender={(nodeData) =>
<TreeViewCard widgetId={nodeData.id} title={nodeData.title}
widgetRef={nodeData.widgetRef}
isTopLevel={nodeData.isTopLevel}/>
}
defaultExpandedKeys={topLevelKeys}
>
</Tree>
{
Object.keys(transformedContent || {}).length === 0 &&
<div className="tw-text-sm tw-place-content-center">
Start adding widgets to view it in tree view
</div>
}
</div>
</div>
)
}
export default TreeviewContainer

View File

@@ -33,31 +33,37 @@ function Premium({ children, className = "" }) {
open={premiumModalOpen} open={premiumModalOpen}
> >
<div className="tw-mt-5 tw-text-lg tw-max-w-[850px] tw-w-full "> <div className="tw-mt-5 tw-text-lg tw-max-w-[850px] tw-w-full ">
I am Paul, a indie open-source dev.
I am Paul, an indie open-source dev.
If you find this tool useful and want to fund and support it's development, consider buying a <b>one time license</b>. If you find this tool useful and want to fund and support it's development, consider buying a <b>one time license</b>.
<br /> <br />
<br /> <br />
By buying pre-order license, you get advance features, priority support, early access, upcoming features, and &nbsp; By pre-ordering license, you get discounted price, advance features, priority support, early access, upcoming features, and &nbsp;
<a <a
href="https://github.com/PaulleDemon/tab=readme-ov-file" href="https://github.com/PaulleDemon/PyUIBuilder/blob/main/roadmap.md"
target="_blank" target="_blank"
rel="noreferrer noopener" rel="noreferrer noopener"
className="!tw-text-blue-500" className="!tw-text-blue-500"
> >
more. more.
</a> </a>
<br />
<br />
Premium features will start rolling out phase wise towards end of April, after which there would be a price increase.
</div> </div>
<section className="tw-mt-1 tw-flex tw-w-full tw-flex-col tw-place-items-center tw-p-[2%] max-lg:tw-p-2" id="pricing"> <section className="tw-mt-1 tw-flex tw-w-full tw-flex-col tw-place-items-center tw-p-[2%] max-lg:tw-p-2" id="pricing">
<h3 className="tw-text-2xl tw-font-medium max-md:tw-text-2xl">Choose your plan</h3> <h3 className="tw-text-2xl tw-font-medium max-md:tw-text-2xl">Choose your plan</h3>
<div className="tw-mt-10 tw-flex tw-place-content-center tw-gap-8 max-lg:tw-flex-col"> <div className="tw-mt-6 tw-flex tw-place-content-center tw-gap-8 max-lg:tw-flex-col">
{/* Free Plan */} {/* Free Plan */}
<div className="tw-flex tw-w-[380px] tw-border-[1px] tw-border-gray-500 tw-border-solid tw-flex-col tw-place-items-center tw-gap-2 tw-rounded-lg tw-p-8 tw-shadow-xl max-lg:tw-w-[340px]"> <div className="tw-flex tw-w-[380px] tw-border-[1px] tw-border-gray-500 tw-border-solid tw-flex-col tw-place-items-center tw-gap-2 tw-rounded-lg tw-p-8 tw-shadow-xl max-lg:tw-w-[340px]">
<h3> <h3>
<span className="tw-text-5xl tw-font-semibold">$0</span> <span className="tw-text-5xl tw-font-semibold">$0</span>
</h3> </h3>
<p className="tw-mt-3 tw-text-base tw-text-center tw-text-gray-600"> <p className="tw-mt-3 tw-text-base tw-text-center tw-text-gray-600">
Free to use forever, but for added features and to support open-source development, consider buying a lifetime license. Free to use forever, but for added features and to support development, consider buying a lifetime license.
</p> </p>
<hr /> <hr />
<ul className="tw-mt-4 tw-flex tw-flex-col tw-gap-3 tw-text-xl tw-text-gray-600"> <ul className="tw-mt-4 tw-flex tw-flex-col tw-gap-3 tw-text-xl tw-text-gray-600">
@@ -69,6 +75,20 @@ function Premium({ children, className = "" }) {
<i className="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i> <i className="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Commercial use</span> <span>Commercial use</span>
</li> </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>Premium widgets &nbsp;
<span className="tw-text-sm">(eg: tab widget, file upload, multi-page support, 3rd party widget library support etc)</span>
</span>
</li>
<li class="tw-flex tw-place-items-center tw-gap-2">
<i class="bi bi-x-circle-fill tw-text-red-600 tw-text-base"></i>
<span>Event handlers and code editor</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>
<span>Premium Templates</span>
</li>
<li className="tw-flex tw-place-items-center tw-gap-2"> <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> <i className="bi bi-x-circle-fill tw-text-red-600 tw-text-base"></i>
<span>Downloadable UI builder exe for local development</span> <span>Downloadable UI builder exe for local development</span>
@@ -113,7 +133,7 @@ function Premium({ children, className = "" }) {
<div className="tw-flex tw-w-[380px] tw-flex-col tw-place-items-center tw-gap-2 tw-rounded-lg tw-border-2 tw-border-solid <div className="tw-flex tw-w-[380px] tw-flex-col tw-place-items-center tw-gap-2 tw-rounded-lg tw-border-2 tw-border-solid
tw-border-blue-500 tw-p-8 tw-shadow-xl max-lg:tw-w-[340px]"> tw-border-blue-500 tw-p-8 tw-shadow-xl max-lg:tw-w-[340px]">
<div className="tw-text-white tw-p-1 tw-px-3 tw-bg-blue-500 tw-rounded-full"> <div className="tw-text-white tw-p-1 tw-px-3 tw-bg-blue-500 tw-rounded-full">
Limited time offer Pre-order offer
</div> </div>
<div className="tw-text-white tw-font-medium tw-p-1 tw-px-3 tw-bg-green-700 tw-rounded-full"> <div className="tw-text-white tw-font-medium tw-p-1 tw-px-3 tw-bg-green-700 tw-rounded-full">
Hobby Hobby
@@ -126,10 +146,10 @@ function Premium({ children, className = "" }) {
<span className="tw-text-2xl tw-text-gray-600">Forever</span> <span className="tw-text-2xl tw-text-gray-600">Forever</span>
</h3> </h3>
<p className="tw-mt-3 tw-text-center tw-text-gray-600"> <p className="tw-mt-3 tw-text-center tw-text-gray-600">
Support open-source development 🚀. Plus, get added benefits. Best for hobby users and people who want to learn basic UI development without PySide.
</p> </p>
<hr /> <hr />
<ul className="tw-mt-4 tw-flex tw-flex-col tw-gap-3 tw-text-xl tw-text-gray-600"> <ul className="tw-mt-4 tw-flex tw-flex-col tw-gap-3 tw-text-xl tw-text-black">
<li className="tw-flex tw-place-items-center tw-gap-2"> <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> <i className="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Access to web-based editor</span> <span>Access to web-based editor</span>
@@ -138,6 +158,20 @@ function Premium({ children, className = "" }) {
<i className="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i> <i className="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Downloadable UI builder exe for local development</span> <span>Downloadable UI builder exe for local development</span>
</li> </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>Premium widgets &nbsp;
<span className="tw-text-sm">(eg: tab widget, file upload, multi-page support, 3rd party widget library support etc)</span>
</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>
<span>Premium Templates</span>
</li>
<li class="tw-flex tw-place-items-center tw-gap-2">
<i class="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Event handlers and code editor</span>
</li>
<li className="tw-flex tw-place-items-center tw-gap-2"> <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> <i className="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Preview live</span> <span>Preview live</span>
@@ -192,7 +226,7 @@ function Premium({ children, className = "" }) {
<div className="tw-flex tw-w-[380px] tw-flex-col tw-place-items-center tw-gap-2 tw-rounded-lg tw-border-3 tw-border-solid <div className="tw-flex tw-w-[380px] tw-flex-col tw-place-items-center tw-gap-2 tw-rounded-lg tw-border-3 tw-border-solid
tw-border-green-600 tw-p-8 tw-shadow-xl max-lg:tw-w-[340px]"> tw-border-green-600 tw-p-8 tw-shadow-xl max-lg:tw-w-[340px]">
<div className="tw-text-white tw-p-1 tw-px-3 tw-bg-blue-500 tw-rounded-full"> <div className="tw-text-white tw-p-1 tw-px-3 tw-bg-blue-500 tw-rounded-full">
Limited time offer Pre-order offer
</div> </div>
<div className="tw-text-white tw-font-medium tw-p-1 tw-px-3 tw-bg-green-700 tw-rounded-full"> <div className="tw-text-white tw-font-medium tw-p-1 tw-px-3 tw-bg-green-700 tw-rounded-full">
Commercial Commercial
@@ -205,10 +239,10 @@ function Premium({ children, className = "" }) {
<span className="tw-text-2xl tw-text-gray-600">Forever</span> <span className="tw-text-2xl tw-text-gray-600">Forever</span>
</h3> </h3>
<p className="tw-mt-3 tw-text-center tw-text-gray-600"> <p className="tw-mt-3 tw-text-center tw-text-gray-600">
Support open-source development 🚀. Plus, get added benefits. Best for startups and teams and people who want to use internally or serves a commercial purpose and want support for PySide
</p> </p>
<hr /> <hr />
<ul className="tw-mt-4 tw-flex tw-flex-col tw-gap-3 tw-text-xl tw-text-gray-600"> <ul className="tw-mt-4 tw-flex tw-flex-col tw-gap-3 tw-text-xl">
<li className="tw-flex tw-place-items-center tw-gap-2"> <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> <i className="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Access to web-based editor</span> <span>Access to web-based editor</span>
@@ -217,6 +251,20 @@ function Premium({ children, className = "" }) {
<i className="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i> <i className="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Downloadable UI builder exe for local development</span> <span>Downloadable UI builder exe for local development</span>
</li> </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>Premium widgets &nbsp;
<span className="tw-text-sm">(eg: tab widget, file upload, multi-page support, 3rd party widget library support etc)</span>
</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>
<span>Premium Templates</span>
</li>
<li class="tw-flex tw-place-items-center tw-gap-2">
<i class="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Event handlers and code editor</span>
</li>
<li className="tw-flex tw-place-items-center tw-gap-2"> <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> <i className="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Preview live</span> <span>Preview live</span>
@@ -267,6 +315,11 @@ function Premium({ children, className = "" }) {
</a> </a>
</div> </div>
</div> </div>
<div className="tw-text-sm tw-mt-4">
Or you could support development by sharing this tool on your socials and you'll be eligible for a
&nbsp;<a href="https://tally.so/r/mJM22X" target="_blank" rel="noreferrer noopener">free license</a>.
</div>
</section> </section>

View File

@@ -45,6 +45,10 @@ function Share({children, className=""}){
footer={null} footer={null}
open={shareModalOpen}> open={shareModalOpen}>
<div className="tw-text-lg">
Share and help speed up development
</div>
<div className="tw-mt-5 tw-flex tw-place-content-center tw-w-full tw-place-items-center"> <div className="tw-mt-5 tw-flex tw-place-content-center tw-w-full tw-place-items-center">
<a onClick={onCopy} <a onClick={onCopy}
className="hover:!tw-bg-gray-100 hover:!tw-color-black !tw-text-4xl" className="hover:!tw-bg-gray-100 hover:!tw-color-black !tw-text-4xl"

View File

@@ -13,7 +13,7 @@ body {
/* width */ /* width */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 8px; width: 6px;
} }
/* Track */ /* Track */

View File

@@ -69,4 +69,29 @@ export function convertObjectToKeyValueString(obj){
return Object.entries(obj) return Object.entries(obj)
.map(([key, value]) => `${key}=${value}`) .map(([key, value]) => `${key}=${value}`)
.join(', ') .join(', ')
}
/**
*
* @param {HTMLElement} widget
* @param {HTMLElement} gridContainer
* @returns
*/
export const getGridPosition = (widget, gridContainer) => {
if (!widget || !gridContainer) return null;
const widgets = Array.from(gridContainer.children); // Get all grid items
// const index = widgets.indexOf(widget);
const widgetIndex = widgets.indexOf(widget);
if (widgetIndex === -1) return null; // Widget not found
const gridStyles = getComputedStyle(gridContainer);
const columnCount = gridStyles.gridTemplateColumns.split(' ').length;
const row = Math.floor(widgetIndex / columnCount) + 1;
const column = (widgetIndex % columnCount) + 1;
return { row, column };
} }

36
src/utils/hooks.js Normal file
View File

@@ -0,0 +1,36 @@
import { useLayoutEffect, useCallback, useState } from 'react';
export const useRect = (ref) => {
const [rect, setRect] = useState({ width: 0, height: 0, top: 0, left: 0, right: 0, bottom: 0 });
const updateRect = useCallback(() => {
if (ref.current) {
setRect(ref.current.getBoundingClientRect());
}
}, [ref]);
useLayoutEffect(() => {
if (!ref.current) return;
const timeout = setTimeout(updateRect, 0); // Delay to next event loop
const observer = new ResizeObserver(updateRect);
observer.observe(ref.current);
return () => {
observer.disconnect()
clearTimeout(timeout)
}
}, [updateRect, ref]);
return rect;
};
/**
* Higher order component to make use of the useRect
*/
export function WithRect({ forwardedRef, children }) {
const rect = useRect(forwardedRef)
return children(rect)
}

Some files were not shown because too many files have changed in this diff Show More