28 Commits

Author SHA1 Message Date
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
Art/Paul
d64f87847c Merge pull request #12 from PaulleDemon/customtk-fixes
fixing customtk layout
2025-03-28 17:13:21 +05:30
33 changed files with 10998 additions and 6273 deletions

1
.gitignore vendored
View File

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

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
20

12
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,12 @@
## Contributing
Currently if you find bugs or typos, I request you to create an issue first, instead of a Pull request.
That is the best way to contribute to this repo in it's current state.
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 ways to contribute.
1. Creating tutorials and helping others on [discord server](https://discord.gg/dHXjrrCA7G)
2. Sharing the tool with others.

View File

@@ -27,10 +27,10 @@ Build Python GUI's with ease of Drag and drop builders.
https://github.com/user-attachments/assets/ac91aa98-843d-4578-b646-88e66bc113de
<sub>**Don't like background music? fell free to mute it**</sub>
<sub>**Don't like background music? feel free to mute it**</sub>
## Try PyUIBuilder
Try [PyUIBuilder](https://pyuibuilder.pages.dev/)
Try [PyUIBuilder](https://pyuibuilder.com)
## Table of contents
@@ -51,9 +51,12 @@ Try [PyUIBuilder](https://pyuibuilder.pages.dev/)
- [Author](#author)
## Tutorials
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)
## 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
@@ -109,7 +112,7 @@ While there are a lot of features, here are few you need to know.
* Framework agnostic - Can outputs code in multiple frameworks.
* Pre-built UI widgets
* 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.
* Support to upload local assets.
* Generates requirements.txt file when needed
@@ -123,7 +126,6 @@ While there are a lot of features, here are few you need to know.
## Roadmap
Here are some of the upcoming features.
* Treeview on the sidebar
* Support for Event Handlers
* Kivy Framework support
* Pyqt/PySide Support
@@ -131,13 +133,13 @@ Here are some of the upcoming features.
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
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.
@@ -148,6 +150,7 @@ The discount's will be available for limited time only on pre-orders.
| **Lifetime license** (one-time purchase) | 👍️ | ✅ | ✅ |
| **Early access** to upcoming features | ❌ | ✅ | ✅ |
| **Downloadable Electron App** (upcoming) | ❌ | ✅ | ✅ |
| **Premium widgets**(tabbed widget, scroll widget etc) (upcoming) | ❌ | ✅ | ✅ |
| **Run Preview live**(upcoming) | ❌ | ✅ | ✅ |
| **Save and Load UI files** (upcoming) | ❌ | ✅ | ✅ |
| **Load 3rd party plugins locally** | ❌ | ✅ | ✅ |

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

View File

@@ -7,7 +7,7 @@
<meta name="description" content="A Drag and Drop builder for Python GUIs, including Tkinter, Custom Tkinter and more" />
<link
rel="shortcut icon"
href="./assets/logo/icon48.png"
href="./assets/logo/logo.png"
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: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:url" content="https://github.com/PaulleDemon/PyUIBuilder" />
<meta property="og:url" content="https://about.pyuibuilder.com" />
<!--Replace with the current website url-->
<meta property="og:image" content="" />
<link rel="stylesheet" href="../tailwind/tailwind-runtime.css" />
<!-- <link rel="stylesheet" href="./css/tailwind-build.css"> -->
<!-- <link rel="stylesheet" href="../tailwind/tailwind-runtime.css" /> -->
<link rel="stylesheet" href="./css/tailwind-build.css">
<link rel="stylesheet" href="css/index.css" />
<link
@@ -66,14 +66,17 @@
<a class="header-links" href=""> About </a>
<a class="header-links" href="#pricing"> Pricing </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
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
href="#pricing"
href="https://pyuibuilder.com"
aria-label="start"
target="_blank"
rel="noreferrer noopener"
class="tw-rounded-md tw-border-[1px] tw-border-black
tw-bg-[#0F1727] tw-px-3 tw-py-2 tw-text-white
tw-duration-[0.3s]
@@ -130,11 +133,11 @@
class="tw-flex tw-flex-col tw-place-content-center tw-items-center"
>
<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-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>
<br />
<span class=" tw-px-3 tw-text-5xl"> Drag and Drop editor </span>
@@ -146,7 +149,7 @@
<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"
>
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
@@ -154,7 +157,9 @@
>
<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]"
href="#pricing"
target="_blank"
rel="noopener noreferrer"
href="https://pyuibuilder.com"
>
Start for free
</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">
<!-- <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"
alt="font tester gif" src="./assets/images/home/promo.gif" >
alt="Pyui gif" src="./assets/images/home/promo.gif" >
</div>
</div>
</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">
<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 mid of April phase wise. This is your last chance to get it before price increase from mid 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-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]">
@@ -214,6 +357,10 @@
<i class="bi bi-x-circle-fill tw-text-red-600 tw-text-base"></i>
<span>Downloadable UI builder exe for local development</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>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>Support for PySlide/PyQt</span>
@@ -249,7 +396,7 @@
</ul>
<a
href="https://pyuibuilder.pages.dev/"
href="https://pyuibuilder.com"
target="_blank"
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"
@@ -287,6 +434,10 @@
<i class="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Downloadable UI builder exe for local development</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>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>Preview live</span>
@@ -338,7 +489,7 @@
</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
!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">
Limited time offer
</div>
@@ -365,6 +516,10 @@
<i class="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Downloadable UI builder exe for local development</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>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>Preview live</span>
@@ -483,25 +638,82 @@
<div
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>
</div>
<div class="content">
You can find the upcoming features on the
<a
target="_blank"
rel="noopener noreferrer"
href="https://github.com/PaulleDemon/font-tester-chrome/blob/main/roadmap.md"
class="tw-underline"
>Roadmap</a
>
Premium features will start rolling out from mid of April in phase wise.
</div>
</div>
<div
class="faq tw-w-full"
>
<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">read more</a>
</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
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
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 +761,7 @@
<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="#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>
</div>
</div>
@@ -557,12 +769,43 @@
<div class="tw-flex tw-flex-col tw-gap-3 max-md:tw-text-sm">
<a href="" class="footer-link">About</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/font-tester-chrome" class="footer-link">Report issue</a>
<a href="https://github.com/PaulleDemon/PyUIBuilder" class="footer-link">Github</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>
</footer>
</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
src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.0/gsap.min.js"

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 */
[hidden] {
[hidden]:where(:not([hidden="until-found"])) {
display: none;
}
@@ -619,6 +619,20 @@ video {
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 {
margin-left: auto;
}
@@ -643,6 +657,10 @@ video {
margin-top: 1.25rem;
}
.tw-mt-6 {
margin-top: 1.5rem;
}
.tw-mt-8 {
margin-top: 2rem;
}
@@ -651,14 +669,14 @@ video {
margin-top: auto;
}
.\!tw-mt-auto {
margin-top: auto !important;
}
.tw-flex {
display: flex;
}
.tw-grid {
display: grid;
}
.\!tw-hidden {
display: none !important;
}
@@ -683,10 +701,18 @@ video {
height: 150px;
}
.tw-h-\[200px\] {
height: 200px;
}
.tw-h-\[40px\] {
height: 40px;
}
.tw-h-\[500px\] {
height: 500px;
}
.tw-h-\[50px\] {
height: 50px;
}
@@ -707,6 +733,14 @@ video {
height: 100%;
}
.tw-h-\[230px\] {
height: 230px;
}
.tw-h-\[360px\] {
height: 360px;
}
.tw-max-h-\[120px\] {
max-height: 120px;
}
@@ -727,6 +761,10 @@ video {
min-height: 100vh;
}
.tw-min-h-\[200px\] {
min-height: 200px;
}
.tw-min-h-\[300px\] {
min-height: 300px;
}
@@ -739,22 +777,18 @@ video {
min-height: 550px;
}
.tw-min-h-\[60vh\] {
min-height: 60vh;
}
.tw-min-h-\[70vh\] {
min-height: 70vh;
}
.tw-min-h-full {
min-height: 100%;
}
.tw-min-h-\[80vh\] {
min-height: 80vh;
}
.tw-min-h-full {
min-height: 100%;
}
.tw-w-8 {
width: 2rem;
}
@@ -767,6 +801,10 @@ video {
width: 250px;
}
.tw-w-\[350px\] {
width: 350px;
}
.tw-w-\[380px\] {
width: 380px;
}
@@ -791,10 +829,18 @@ video {
min-width: 100%;
}
.tw-min-w-\[350px\] {
min-width: 350px;
}
.tw-max-w-\[100vw\] {
max-width: 100vw;
}
.tw-max-w-\[1150px\] {
max-width: 1150px;
}
.tw-max-w-\[120px\] {
max-width: 120px;
}
@@ -803,6 +849,10 @@ video {
max-width: 450px;
}
.tw-max-w-\[650px\] {
max-width: 650px;
}
.tw-max-w-\[80vw\] {
max-width: 80vw;
}
@@ -811,6 +861,10 @@ video {
max-width: 850px;
}
.tw-max-w-\[350px\] {
max-width: 350px;
}
.tw--translate-x-1\/2 {
--tw-translate-x: -50%;
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));
@@ -856,6 +910,14 @@ video {
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 {
flex-direction: column;
}
@@ -920,6 +982,10 @@ video {
gap: 20px;
}
.tw-gap-\[5\%\] {
gap: 5%;
}
.tw-overflow-hidden {
overflow: hidden;
}
@@ -940,6 +1006,10 @@ video {
text-wrap: wrap;
}
.tw-rounded-3xl {
border-radius: 1.5rem;
}
.tw-rounded-full {
border-radius: 9999px;
}
@@ -968,10 +1038,6 @@ video {
border-width: 1px;
}
.tw-border-4 {
border-width: 4px;
}
.tw-border-\[3px\] {
border-width: 3px;
}
@@ -984,65 +1050,65 @@ video {
border-style: solid;
}
.\!tw-border-white {
--tw-border-opacity: 1 !important;
border-color: rgb(255 255 255 / var(--tw-border-opacity)) !important;
}
.tw-border-black {
--tw-border-opacity: 1;
border-color: rgb(0 0 0 / var(--tw-border-opacity));
}
.tw-border-blue-500 {
--tw-border-opacity: 1;
border-color: rgb(59 130 246 / var(--tw-border-opacity));
}
.tw-border-gray-300 {
--tw-border-opacity: 1;
border-color: rgb(209 213 219 / var(--tw-border-opacity));
}
.tw-border-green-600 {
--tw-border-opacity: 1;
border-color: rgb(22 163 74 / var(--tw-border-opacity));
}
.tw-border-white {
--tw-border-opacity: 1;
border-color: rgb(255 255 255 / var(--tw-border-opacity));
}
.\!tw-border-\[\#0F1727\] {
--tw-border-opacity: 1 !important;
border-color: rgb(15 23 39 / var(--tw-border-opacity)) !important;
border-color: rgb(15 23 39 / var(--tw-border-opacity, 1)) !important;
}
.\!tw-border-green-600 {
--tw-border-opacity: 1 !important;
border-color: rgb(22 163 74 / var(--tw-border-opacity)) !important;
border-color: rgb(22 163 74 / var(--tw-border-opacity, 1)) !important;
}
.\!tw-bg-\[\#7E22CE\] {
.tw-border-black {
--tw-border-opacity: 1;
border-color: rgb(0 0 0 / var(--tw-border-opacity, 1));
}
.tw-border-gray-300 {
--tw-border-opacity: 1;
border-color: rgb(209 213 219 / var(--tw-border-opacity, 1));
}
.tw-border-white {
--tw-border-opacity: 1;
border-color: rgb(255 255 255 / var(--tw-border-opacity, 1));
}
.\!tw-border-blue-600 {
--tw-border-opacity: 1 !important;
border-color: rgb(37 99 235 / var(--tw-border-opacity, 1)) !important;
}
.\!tw-bg-\[\#0F1727\] {
--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-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-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 {
background-color: transparent !important;
}
.tw-bg-\[\#0F1727\] {
--tw-bg-opacity: 1;
background-color: rgb(15 23 39 / var(--tw-bg-opacity, 1));
}
.tw-bg-\[\#eeeeee\] {
--tw-bg-opacity: 1;
background-color: rgb(238 238 238 / var(--tw-bg-opacity, 1));
}
.tw-bg-\[\#f0f0f0ef\] {
background-color: #f0f0f0ef;
}
@@ -1053,49 +1119,37 @@ video {
.tw-bg-black {
--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-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-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-green-700 {
--tw-bg-opacity: 1;
background-color: rgb(21 128 61 / var(--tw-bg-opacity));
}
.tw-bg-transparent {
background-color: transparent;
background-color: rgb(21 128 61 / var(--tw-bg-opacity, 1));
}
.tw-bg-white {
--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-\[\#f2f2f2\] {
--tw-bg-opacity: 1;
background-color: rgb(15 23 39 / var(--tw-bg-opacity));
background-color: rgb(242 242 242 / var(--tw-bg-opacity, 1));
}
.\!tw-bg-\[\#\#0F1727\] {
background-color: ##0F1727 !important;
}
.tw-bg-\[\#\#0F1727\] {
background-color: ##0F1727;
.tw-bg-gray-800 {
--tw-bg-opacity: 1;
background-color: rgb(31 41 55 / var(--tw-bg-opacity, 1));
}
.tw-bg-opacity-0 {
@@ -1110,6 +1164,10 @@ video {
padding: 0.25rem;
}
.tw-p-10 {
padding: 2.5rem;
}
.tw-p-2 {
padding: 0.5rem;
}
@@ -1118,6 +1176,10 @@ video {
padding: 0.75rem;
}
.tw-p-4 {
padding: 1rem;
}
.tw-p-6 {
padding: 1.5rem;
}
@@ -1138,6 +1200,10 @@ video {
padding: 5%;
}
.tw-p-\[3\%\] {
padding: 3%;
}
.tw-px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
@@ -1167,6 +1233,10 @@ video {
text-align: center;
}
.tw-align-middle {
vertical-align: middle;
}
.\!tw-text-4xl {
font-size: 2.25rem !important;
line-height: 2.5rem !important;
@@ -1216,6 +1286,11 @@ video {
line-height: 1.75rem;
}
.tw-text-sm {
font-size: 0.875rem;
line-height: 1.25rem;
}
.tw-text-xl {
font-size: 1.25rem;
line-height: 1.75rem;
@@ -1233,10 +1308,6 @@ video {
font-weight: 600;
}
.\!tw-font-medium {
font-weight: 500 !important;
}
.tw-uppercase {
text-transform: uppercase;
}
@@ -1250,84 +1321,73 @@ video {
line-height: 90px;
}
.\!tw-text-black {
--tw-text-opacity: 1 !important;
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-leading-normal {
line-height: 1.5;
}
.\!tw-text-\[\#0F1727\] {
--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-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-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 {
@@ -1364,16 +1424,17 @@ video {
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-color: #4d395e;
--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 {
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
@@ -1392,12 +1453,6 @@ video {
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 {
transition-duration: 300ms;
}
@@ -1410,9 +1465,8 @@ video {
transition-duration: 0.3s;
}
.hover\:tw-translate-x-1:hover {
--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));
.tw-duration-75 {
transition-duration: 75ms;
}
.hover\:tw-translate-x-2:hover {
@@ -1432,6 +1486,18 @@ 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));
}
.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-\[1\.1\]:hover {
--tw-scale-x: 1.1;
--tw-scale-y: 1.1;
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 {
--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));
@@ -1439,47 +1505,22 @@ video {
.hover\:\!tw-bg-gray-100:hover {
--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 {
--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 {
--tw-bg-opacity: 1 !important;
background-color: rgb(255 255 255 / var(--tw-bg-opacity)) !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));
background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1)) !important;
}
.hover\:\!tw-text-black:hover {
--tw-text-opacity: 1 !important;
color: rgb(0 0 0 / var(--tw-text-opacity)) !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));
color: rgb(0 0 0 / var(--tw-text-opacity, 1)) !important;
}
.hover\:tw-transition-transform:hover {
@@ -1488,9 +1529,14 @@ video {
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 *) {
--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 *) {
@@ -1498,6 +1544,14 @@ video {
}
@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 {
place-items: center;
}
@@ -1520,6 +1574,10 @@ video {
height: auto;
}
.max-lg\:tw-h-fit {
height: fit-content;
}
.max-lg\:tw-min-h-\[250px\] {
min-height: 250px;
}
@@ -1528,10 +1586,6 @@ video {
min-height: 400px;
}
.max-lg\:tw-min-h-\[60vh\] {
min-height: 60vh;
}
.max-lg\:tw-min-h-\[80vh\] {
min-height: 80vh;
}
@@ -1556,6 +1610,10 @@ video {
flex-direction: column;
}
.max-lg\:tw-place-content-center {
place-content: center;
}
.max-lg\:tw-place-items-end {
place-items: end;
}

15683
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,10 @@
"engines": {
"node": ">=18"
},
"overrides": {
"picomatch": "^3.0.0",
"postcss-selector-parser": "7.1.0"
},
"dependencies": {
"@ant-design/icons": "^5.4.0",
"@dnd-kit/core": "^6.1.0",
@@ -35,9 +39,9 @@
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "GENERATE_SOURCEMAP=false react-scripts build",
"build:local": "GENERATE_SOURCEMAP=false env-cmd -e production react-scripts build",
"start": "env-cmd -e development cross-env NODE_ENV=development webpack serve",
"build": "NODE_ENV=production webpack",
"build:local": "env-cmd -e production cross-env NODE_ENV=production webpack",
"test": "react-scripts test",
"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",
@@ -63,8 +67,29 @@
]
},
"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",
"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,31 +2,31 @@
<html lang="en">
<head>
<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="theme-color" content="#000000" />
<meta
name="description"
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
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" />
<script async src="https://tally.so/widgets/embed.js"></script>
<!-- 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>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '%REACT_APP_ANALYTICS_SCRIPT_ID%');
gtag('config', '<%= REACT_APP_ANALYTICS_SCRIPT_ID %>');
</script>
<!--
Notice the use of %PUBLIC_URL% in the tags above.

View File

@@ -19,7 +19,8 @@ import { removeDuplicateObjects } from "../utils/common"
// import DotsBackground from "../assets/background/dots.svg"
import { ReactComponent as DotsBackground } from "../assets/background/dots.svg"
// import { ReactComponent as DotsBackground } from "../assets/background/dots.svg"
import DotsBackground from "../assets/background/dots.svg";
import DroppableWrapper from "../components/draggable/droppable"

View File

@@ -41,6 +41,8 @@ class Widget extends React.Component {
static requirements = [] // requirements for the widgets (libraries) eg: tkvideoplayer, tktimepicker
static requiredImports = [] // import statements
static requiredCustomPyFiles = [] // custom widgets inside of pythonWidgets don't add .py at the end
// static contextType = ActiveWidgetContext
constructor(props) {
@@ -185,6 +187,7 @@ class Widget extends React.Component {
this.getImports = this.getImports.bind(this)
this.getRequirements = this.getRequirements.bind(this)
this.getRequiredCustomPyFiles = this.getRequiredCustomPyFiles.bind(this)
// this.openRenaming = this.openRenaming.bind(this)
@@ -215,7 +218,6 @@ class Widget extends React.Component {
this.stateUpdateCallback = null // allowing other components such as toolbar to subscribe to changes in this widget
this.resizeObserver = null
}
componentDidMount() {
@@ -427,6 +429,10 @@ class Widget extends React.Component {
return this.constructor.requiredImports
}
getRequiredCustomPyFiles(){
return this.constructor.requiredCustomPyFiles
}
generateCode(){
throw new NotImplementedError("generateCode() must be implemented by the subclass")
}
@@ -704,8 +710,7 @@ class Widget extends React.Component {
}
getLayout(){
return this.getAttrValue("layout") || Layouts.PLACE
return this.getAttrValue("layout") || {layout: Layouts.PLACE}
}
setLayout(value) {
@@ -918,6 +923,8 @@ class Widget extends React.Component {
pos
}
const {layout} = attrs
this.setState(newData, () => {
// Updates attrs
let newAttrs = { ...this.state.attrs }
@@ -949,6 +956,10 @@ class Widget extends React.Component {
if (selected){
this.select()
}
if (layout){
this.setLayout(layout)
}
})

View File

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

View File

@@ -58,13 +58,35 @@ export class CustomTkBase extends Widget {
const absolutePositioning = this.getAttrValue("positioning")
let layoutManager = `pack()`
const marginX = this.getAttrValue("margin.marginX")
const marginY = this.getAttrValue("margin.marginY")
const paddingX = this.getAttrValue("padding.padX")
const paddingY = this.getAttrValue("padding.padY")
let config = {}
if (marginX){
config["padx"] = marginX
}
if (marginY){
config["pady"] = marginY
}
if (paddingX){
config["ipadx"] = paddingX
}
if (paddingY){
config["ipady"] = paddingY
}
if (parentLayout === Layouts.PLACE || absolutePositioning){
const config = {
x: Math.trunc(this.state.pos.x),
y: Math.trunc(this.state.pos.y),
}
config['x'] = Math.trunc(this.state.pos.x)
config['y'] = Math.trunc(this.state.pos.y)
config["width"] = Math.trunc(this.state.size.width)
config["height"] = Math.trunc(this.state.size.height)
@@ -84,12 +106,6 @@ export class CustomTkBase extends Widget {
const packSide = this.getAttrValue("flexManager.side")
const marginX = this.getAttrValue("margin.marginX")
const marginY = this.getAttrValue("margin.marginY")
const paddingX = this.getAttrValue("padding.padX")
const paddingY = this.getAttrValue("padding.padY")
const config = {}
if (packSide === "" || packSide === "top"){
@@ -113,21 +129,6 @@ export class CustomTkBase extends Widget {
// config["pady"] = gap
// }
if (marginX){
config["padx"] = marginX
}
if (marginY){
config["pady"] = marginY
}
if (paddingX){
config["ipadx"] = paddingX
}
if (paddingY){
config["ipady"] = paddingY
}
// if (align === "start"){
// config["anchor"] = "'nw'"
@@ -167,10 +168,8 @@ export class CustomTkBase extends Widget {
const sticky = this.getAttrValue("gridManager.sticky")
const config = {
row: row-1, // unlike css grid tkinter grid starts from 0
column: column-1, // unlike css grid tkinter grid starts from 0
}
config['row'] = row - 1
config['column'] = column - 1
if (rowSpan > 1){
config['rowspan'] = rowSpan
@@ -574,11 +573,13 @@ export class CustomTkBase extends Widget {
let expandValue = expand ? (isSameSide ? previousExpandValue : widgets.length - index) : 1;
if ((expand && previousExpandValue === 0) || (expand && expandValue === 0)){
if (expand && expandValue === 0){
expandValue = 1 // if there is expand it should expand
}
if (expand && !isSameSide) previousExpandValue = expandValue;
if ((expand && !isSameSide) || (expand && previousExpandValue === 0)){
previousExpandValue = expandValue;
}
lastSide = side; // Update last side for recursion
@@ -686,9 +687,14 @@ export class CustomTkBase extends Widget {
setLayout(value) {
const { layout, direction, grid = { rows: 1, cols: 1 }, gap = 10, align } = value
const gridRow = this.getAttrValue("gridConfig.noOfRows") || 3 // suppose its loaded using this.load, we need this
const gridCol = this.getAttrValue("gridConfig.noOfCols") || 3
if (layout === Layouts.GRID){
const rowWeight = this.getAttrValue("gridWeights.rowWeights") || undefined // suppose its loaded via this.load
const colWeight = this.getAttrValue("gridWeights.colWeights") || undefined // suppose its loaded via this.load
const {...restAttrs} = this.state.attrs
@@ -702,7 +708,7 @@ export class CustomTkBase extends Widget {
label: "No of rows",
tool: Tools.NUMBER_INPUT,
toolProps: { placeholder: "no of rows", max: 1000, min: 1 },
value: 3,
value: gridRow,
onChange: (value) => {
this.setAttrValue("gridConfig.noOfRows", value)
@@ -728,7 +734,7 @@ export class CustomTkBase extends Widget {
label: "No of cols",
tool: Tools.NUMBER_INPUT,
toolProps: { placeholder: "no of cols", max: 1000, min: 1 },
value: 3,
value: gridCol,
onChange: (value) => {
this.setAttrValue("gridConfig.noOfCols", value)
@@ -752,7 +758,7 @@ export class CustomTkBase extends Widget {
// placeholder: "weight",
// defaultWeightMapping: this.getAttrValue("gridWeights.rowWeights"),
},
value: undefined,
value: rowWeight,
Component: DynamicGridWeightInput,
onChange: (value) => {
@@ -780,7 +786,7 @@ export class CustomTkBase extends Widget {
// placeholder: "weight",
// defaultWeightMapping: {0: {weight: 0, gridNo: 0}}
},
value: undefined,
value: colWeight,
Component: DynamicGridWeightInput,
onChange: (value) => {
@@ -811,8 +817,6 @@ export class CustomTkBase extends Widget {
}else if (layout === Layouts.FLEX){
const {gridConfig, gridWeights, ...restAttrs} = this.state.attrs
console.log("Flex: ", restAttrs)
this.updateState((prevState) => ({attrs: {...restAttrs}}))
}
@@ -886,6 +890,18 @@ export class CustomTkBase extends Widget {
}
getToolbarAttrs(){
const {layout, gridConfig, gridWeights, ...toolBarAttrs} = super.getToolbarAttrs()
// places layout at the end
return ({
id: this.__id,
...toolBarAttrs,
layout,
gridConfig,
gridWeights
})
}
serialize(){
return ({
@@ -938,6 +954,7 @@ export class CustomTkBase extends Widget {
pos
}
const {layout} = attrs
this.setState(newData, () => {
let layoutAttrs = this.setParentLayout(parentLayout).attrs || {}
@@ -973,6 +990,10 @@ export class CustomTkBase extends Widget {
if (selected){
this.select()
}
if (layout){
this.setLayout(layout)
}
})
@@ -1199,23 +1220,23 @@ export class CustomTkWidgetBase extends CustomTkBase{
if (this.getAttrValue("cursor"))
config["cursor"] = `"${this.getAttrValue("cursor")}"`
if (this.getAttrValue("padding.padX")){
// inner padding
config["ipadx"] = this.getAttrValue("padding.padX")
}
// if (this.getAttrValue("padding.padX")){
// // inner padding
// config["ipadx"] = this.getAttrValue("padding.padX")
// }
if (this.getAttrValue("padding.padY")){
config["ipady"] = this.getAttrValue("padding.padY")
}
// if (this.getAttrValue("padding.padY")){
// config["ipady"] = this.getAttrValue("padding.padY")
// }
if (this.getAttrValue("margin.marginX")){
config["padx"] = this.getAttrValue("margin.marginX")
}
// if (this.getAttrValue("margin.marginX")){
// config["padx"] = this.getAttrValue("margin.marginX")
// }
if (this.getAttrValue("margin.marginY")){
config["pady"] = this.getAttrValue("margin.marginY")
}
// if (this.getAttrValue("margin.marginY")){
// config["pady"] = this.getAttrValue("margin.marginY")
// }
// FIXME: add width and height, the scales may not be correct as the width and height are based on characters in pack and grid not pixels

View File

@@ -161,6 +161,20 @@ class Frame extends CustomTkBase{
]
}
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(){
// console.log("bounding rect: ", this.getBoundingRect())

View File

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

View File

@@ -4,6 +4,8 @@ import MainWindow from "../widgets/mainWindow"
import { message } from "antd"
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
// 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 code = []
let customPythonWidgets = new Set([])
for (let widget of widgetList) {
const widgetRef = widgetRefs[widget.id].current
let varName = widgetRef.getVariableName()
@@ -20,6 +24,7 @@ function generateTkinterCodeList(widgetList = [], widgetRefs = [], parentVariabl
// Add imports and requirements to sets
widgetRef.getImports().forEach(importItem => imports.add(importItem))
widgetRef.getRequirements().forEach(requirementItem => requirements.add(requirementItem))
widgetRef.getRequiredCustomPyFiles().forEach(customFile => customPythonWidgets.add(customFile))
// Set main variable if the widget is MainWindow
if (widget.widgetType === MainWindow) {
@@ -68,6 +73,8 @@ function generateTkinterCodeList(widgetList = [], widgetRefs = [], parentVariabl
// Merge child imports, requirements, and code
imports = new Set([...imports, ...childResult.imports])
requirements = new Set([...requirements, ...childResult.requirements])
customPythonWidgets = new Set([...customPythonWidgets, ...childResult.customPythonWidgets])
code.push(...childResult.code)
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),
code: code,
requirements: Array.from(requirements),
customPythonWidgets: Array.from(customPythonWidgets),
mainVariable
}
}
@@ -113,9 +121,13 @@ async function generateTkinterCode(projectName, widgetList=[], widgetRefs=[], as
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: import customWidgets
const code = [
"# This code is generated by PyUIbuilder: https://pyuibuilder.com",
"\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){
if (asset.fileType === "image"){

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

@@ -59,13 +59,34 @@ export class TkinterBase extends Widget {
const absolutePositioning = this.getAttrValue("positioning")
let layoutManager = `pack()`
const marginX = this.getAttrValue("margin.marginX")
const marginY = this.getAttrValue("margin.marginY")
const paddingX = this.getAttrValue("padding.padX")
const paddingY = this.getAttrValue("padding.padY")
let config = {}
if (marginX){
config["padx"] = marginX
}
if (marginY){
config["pady"] = marginY
}
if (paddingX){
config["ipadx"] = paddingX
}
if (paddingY){
config["ipady"] = paddingY
}
if (parentLayout === Layouts.PLACE || absolutePositioning){
const config = {
x: Math.trunc(this.state.pos.x),
y: Math.trunc(this.state.pos.y),
}
config['x'] = Math.trunc(this.state.pos.x)
config['y'] = Math.trunc(this.state.pos.y)
config["width"] = Math.trunc(this.state.size.width)
config["height"] = Math.trunc(this.state.size.height)
@@ -85,14 +106,6 @@ export class TkinterBase extends Widget {
const packSide = this.getAttrValue("flexManager.side")
const marginX = this.getAttrValue("margin.marginX")
const marginY = this.getAttrValue("margin.marginY")
const paddingX = this.getAttrValue("padding.padX")
const paddingY = this.getAttrValue("padding.padY")
const config = {}
if (packSide === "" || packSide === "top"){
config['side'] = `tk.TOP`
@@ -114,21 +127,6 @@ export class TkinterBase extends Widget {
// config["pady"] = gap
// }
if (marginX){
config["padx"] = marginX
}
if (marginY){
config["pady"] = marginY
}
if (paddingX){
config["ipadx"] = paddingX
}
if (paddingY){
config["ipady"] = paddingY
}
// if (align === "start"){
// config["anchor"] = "'nw'"
@@ -168,10 +166,8 @@ export class TkinterBase extends Widget {
const sticky = this.getAttrValue("gridManager.sticky")
const config = {
row: row-1, // unlike css grid tkinter grid starts from 0
column: column-1, // unlike css grid tkinter grid starts from 0
}
config['row'] = row - 1 // unlike css grid tkinter grid starts from 0
config['column'] = column - 1 // unlike css grid tkinter grid starts from 0
if (rowSpan > 1){
config['rowspan'] = rowSpan
@@ -561,7 +557,8 @@ export class TkinterBase extends Widget {
const { side = "top", expand = false, anchor } = widgetRef.getPackAttrs() || {};
// console.log("rerendering:", side, expand);
const directionMap = {
top: "column",
bottom: "column-reverse",
@@ -570,17 +567,29 @@ export class TkinterBase extends Widget {
}
const currentWidgetDirection = directionMap[side] || "column"; // Default to "column"
const isSameSide = lastSide === side;
const isVertical = ["top", "bottom"].includes(side);
let expandValue = expand ? (isSameSide ? previousExpandValue : widgets.length - index) : 1;
if ((expand && previousExpandValue === 0) || (expand && expandValue === 0)){
const isSameSide = lastSide === side
const isOppositeSide = ((lastSide === "top" && side === "bottom") || (lastSide === "left" && side === "right"))
const isDiagonal = (!isSameSide && !isOppositeSide && lastSide !== "") // bottom and right, top and left
let expandValue = expand ? ((isSameSide || isOppositeSide) ? previousExpandValue : (widgets.length - index) + 1) : (previousExpandValue > 0) ? 0 : 1
if (expand && expandValue === 0){
expandValue = 1 // if there is expand it should expand
}
if (expand && !isSameSide) previousExpandValue = expandValue;
if (expand && isDiagonal){
expandValue = 1
}
// TODO: if the child widget as fillx or y use flex grow
if ((expand && !isSameSide) || (expand && previousExpandValue === 0)){
previousExpandValue = expandValue;
}
lastSide = side; // Update last side for recursion
const anchorStyles = {
@@ -598,7 +607,10 @@ export class TkinterBase extends Widget {
const stretchClass = isVertical ? "tw-flex-grow" : "tw-h-full"; // Allow only horizontal growth for top/bottom
// TODO: if previous expand value is greater than 0 and current doesn't have expand then it should be 0
// const fill = this.getAttrValue("flexManager.fillX") || this.getAttrValue("flexManager.fillY")
if (isSameSide) {
return (
<>
@@ -627,11 +639,12 @@ export class TkinterBase extends Widget {
return (
<div
data-pack-container={side}
className={`tw-flex ${isVertical ? "!tw-h-full" : "!tw-w-full"}`}
className={`tw-flex tw-flex-auto`}
// className={`tw-flex ${isVertical ? "!tw-h-full" : "!tw-w-full"}`}
style={{
display: "flex",
flexDirection: currentWidgetDirection,
flexGrow: expand ? expandValue : 1, //((widgets.length - 1) === index) ? 1 : 0, // last index will always have flex-grow 1
flexGrow: expandValue, //((widgets.length - 1) === index) ? 1 : 0, // last index will always have flex-grow 1
flexShrink: expand ? 0 : 1,
flexBasis: "auto",
minWidth: isVertical ? "0" : "auto",
@@ -687,9 +700,13 @@ export class TkinterBase extends Widget {
setLayout(value) {
const { layout, direction, grid = { rows: 1, cols: 1 }, gap = 10, align } = value
const gridRow = this.getAttrValue("gridConfig.noOfRows") || 3 // suppose its loaded using this.load, we need this
const gridCol = this.getAttrValue("gridConfig.noOfCols") || 3
if (layout === Layouts.GRID){
const rowWeight = this.getAttrValue("gridWeights.rowWeights") || undefined // suppose its loaded via this.load
const colWeight = this.getAttrValue("gridWeights.colWeights") || undefined // suppose its loaded via this.load
const {...restAttrs} = this.state.attrs
@@ -703,7 +720,7 @@ export class TkinterBase extends Widget {
label: "No of rows",
tool: Tools.NUMBER_INPUT,
toolProps: { placeholder: "no of rows", max: 1000, min: 1 },
value: 3,
value: gridRow,
onChange: (value) => {
this.setAttrValue("gridConfig.noOfRows", value)
@@ -729,7 +746,7 @@ export class TkinterBase extends Widget {
label: "No of cols",
tool: Tools.NUMBER_INPUT,
toolProps: { placeholder: "no of cols", max: 1000, min: 1 },
value: 3,
value: gridCol,
onChange: (value) => {
this.setAttrValue("gridConfig.noOfCols", value)
@@ -753,7 +770,7 @@ export class TkinterBase extends Widget {
// placeholder: "weight",
// defaultWeightMapping: this.getAttrValue("gridWeights.rowWeights"),
},
value: undefined,
value: rowWeight,
Component: DynamicGridWeightInput,
onChange: (value) => {
@@ -781,7 +798,7 @@ export class TkinterBase extends Widget {
// placeholder: "weight",
// defaultWeightMapping: {0: {weight: 0, gridNo: 0}}
},
value: undefined,
value: colWeight,
Component: DynamicGridWeightInput,
onChange: (value) => {
@@ -812,8 +829,6 @@ export class TkinterBase extends Widget {
}else if (layout === Layouts.FLEX){
const {gridConfig, gridWeights, ...restAttrs} = this.state.attrs
console.log("Flex: ", restAttrs)
this.updateState((prevState) => ({attrs: {...restAttrs}}))
}
@@ -827,8 +842,8 @@ export class TkinterBase extends Widget {
flexDirection: "column",
// flexDirection: direction,
gap: `${gap}px`,
gridTemplateColumns: "repeat(3, max-content)",
gridTemplateRows: "repeat(3, max-content)",
gridTemplateColumns: `repeat(${gridRow}, max-content)`,
gridTemplateRows: `repeat(${gridCol}, max-content)`,
// gridTemplateColumns: "repeat(auto-fill, minmax(100px, auto))",
// gridTemplateRows: "repeat(auto-fill, minmax(100px, auto))",
}
@@ -888,6 +903,19 @@ export class TkinterBase extends Widget {
}
getToolbarAttrs(){
const {layout, gridConfig, gridWeights, ...toolBarAttrs} = super.getToolbarAttrs()
// places layout at the end
return ({
id: this.__id,
...toolBarAttrs,
layout,
gridConfig,
gridWeights
})
}
serialize(){
return ({
...super.serialize(),
@@ -938,8 +966,10 @@ export class TkinterBase extends Widget {
...layoutUpdates,
pos
}
const {layout} = attrs
this.setState(newData, () => {
let layoutAttrs = this.setParentLayout(parentLayout).attrs || {}
@@ -965,15 +995,21 @@ export class TkinterBase extends Widget {
if (newAttrs?.styling?.backgroundColor){
// TODO: find a better way to apply innerStyles
// TODO: find a better way to apply innerStyles (we may not need this anymore)
this.setWidgetInnerStyle("backgroundColor", newAttrs.styling.backgroundColor.value)
}
this.updateState({ attrs: newAttrs }, callback)
// FIXME: when changing layouts all the widgets are being selected
if (selected){
this.select()
}
}
if (layout){
this.setLayout(layout)
}
})
@@ -1189,23 +1225,23 @@ export class TkinterWidgetBase extends TkinterBase{
if (this.getAttrValue("cursor"))
config["cursor"] = `"${this.getAttrValue("cursor")}"`
if (this.getAttrValue("padding.padX")){
// inner padding
config["ipadx"] = this.getAttrValue("padding.padX")
}
// if (this.getAttrValue("padding.padX")){
// // inner padding
// config["ipadx"] = this.getAttrValue("padding.padX")
// }
if (this.getAttrValue("padding.padY")){
config["ipady"] = this.getAttrValue("padding.padY")
}
// if (this.getAttrValue("padding.padY")){
// config["ipady"] = this.getAttrValue("padding.padY")
// }
if (this.getAttrValue("margin.marginX")){
config["padx"] = this.getAttrValue("margin.marginX")
}
// if (this.getAttrValue("margin.marginX")){
// config["padx"] = this.getAttrValue("margin.marginX")
// }
if (this.getAttrValue("margin.marginY")){
config["pady"] = this.getAttrValue("margin.marginY")
}
// if (this.getAttrValue("margin.marginY")){
// config["pady"] = this.getAttrValue("margin.marginY")
// }
// FIXME: add width and height, the scales may not be correct as the width and height are based on characters in label and grid not pixels
// if (!this.state.fitContent.width){

View File

@@ -161,7 +161,20 @@ class Frame extends TkinterBase{
]
}
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(){
// console.log("bounding rect: ", this.getBoundingRect())

View File

@@ -41,7 +41,7 @@ export class Input extends TkinterWidgetBase{
const config = convertObjectToKeyValueString(this.getConfigCode())
return [
`${variableName} = tk.Entry(master=${parent}, text="${placeHolderText}")`,
`${variableName} = tk.Entry(master=${parent})`,
`${variableName}.config(${config})`,
`${variableName}.${this.getLayoutCode()}`
]

View File

@@ -1,3 +1,5 @@
import { useEffect, useState } from "react"
import { Layouts } from "../../../canvas/constants/layouts"
import Tools from "../../../canvas/constants/tools"
import { convertObjectToKeyValueString } from "../../../utils/common"
import { getPythonAssetPath } from "../../utils/pythonFilePath"
@@ -10,6 +12,7 @@ class Label extends TkinterWidgetBase{
static widgetType = "label"
static displayName = "Label"
// static requiredCustomPyFiles = ["imageLabel"]
constructor(props) {
super(props)
@@ -55,6 +58,34 @@ class Label extends TkinterWidgetBase{
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)
}
}
},
}
}
@@ -73,11 +104,20 @@ class Label extends TkinterWidgetBase{
const imports = super.getImports()
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
}
getRequiredCustomPyFiles(){
const requiredCustomFiles = super.getRequiredCustomPyFiles()
if (this.getAttrValue("imageUpload"))
requiredCustomFiles.push("imageLabel")
return requiredCustomFiles
}
getRequirements(){
const requirements = super.getRequirements()
@@ -92,10 +132,28 @@ class Label extends TkinterWidgetBase{
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
}
@@ -112,10 +170,10 @@ class Label extends TkinterWidgetBase{
const code = []
if (image?.name){
code.push(`${variableName}_img = Image.open(${getPythonAssetPath(image.name, "image")})`)
code.push(`${variableName}_img = ImageTk.PhotoImage(${variableName}_img)`)
// code.push(`${variableName}_img = Image.open(${getPythonAssetPath(image.name, "image")})`)
// code.push(`${variableName}_img = ImageTk.PhotoImage(${variableName}_img)`)
// 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")
@@ -155,36 +213,53 @@ class Label extends TkinterWidgetBase{
center: { justifyContent: 'center', alignItems: 'center' }
}
return anchorStyles[anchor] || anchorStyles["w"];
return anchorStyles[anchor] || anchorStyles["center"];
}
renderContent(){
//FIXME: label image causing issues
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 (
<div className="tw-w-flex tw-flex-col tw-w-full tw-content-start tw-h-full tw-rounded-md tw-overflow-hidden"
style={{
flexGrow: 1, // Ensure the content grows to fill the parent
minWidth: '100%', // Force the width to 100% of the parent
minHeight: '100%', // Force the height to 100% of the parent
}}
<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={{
// flexGrow: 1, // Ensure the content grows to fill the parent
// minWidth: '100%', // Force the width 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()}>
{/* {this.props.children} */}
{
image && (
<img src={image.previewUrl} className="tw-bg-contain tw-w-full tw-h-full" />
)
}
<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>
{/* {this.props.children} */}
{
image ? (
<div className="tw-relative tw-w-full tw-h-full tw-overflow-hidden">
<img src={image.previewUrl}
className={`${imgClassName}`}
alt={this.getAttrValue("widgetName")}
style={{
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>
)
@@ -193,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

View File

@@ -14,6 +14,9 @@ import "./styles/index.css";
import { FileUploadProvider } from "./contexts/fileUploadContext";
window.React = React
const originalSetItem = localStorage.setItem;
// triggers itemsChaned event whenever the item in localstorage is chanegd.
localStorage.setItem = function (key, value) {

View File

@@ -47,6 +47,10 @@ function Premium({ children, className = "" }) {
>
more.
</a>
<br />
<br />
Premium features will start rolling out phase wise from mid of April, after which there would be a price increase.
</div>

110
webpack.config.js Normal file
View File

@@ -0,0 +1,110 @@
const webpack = require('webpack');
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const dotenv = require('dotenv');
const fs = require('fs');
const isProduction = process.env.NODE_ENV === "production";
// Load environment variables
let envConfig = {};
if (!isProduction && fs.existsSync(".env-cmdrc.json")) {
const envFile = JSON.parse(fs.readFileSync(".env-cmdrc.json", "utf8"));
envConfig = envFile[process.env.NODE_ENV] || {};
} else {
envConfig = Object.fromEntries(Object.entries(process.env));
}
// Convert JSON to a format Webpack understands
const envKeys = Object.keys(envConfig).reduce((prev, next) => {
prev[`process.env.${next}`] = JSON.stringify(envConfig[next]);
return prev;
}, {});
module.exports = {
mode: isProduction ? "production" : "development",
watch: !isProduction,
watchOptions: {
ignored: /node_modules/,
},
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: isProduction ? "js/[name].[contenthash].js" : "js/[name].js",
publicPath: "./",
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env", "@babel/preset-react"],
},
},
},
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, "css-loader", "postcss-loader",],
},
{
test: /\.py$/,
use: "raw-loader", // Load Python files as raw text
},
{
test: /\.svg$/,
use: ["@svgr/webpack"], // Enables importing SVGs as React components
},
{
test: /\.(png|jpe?g|gif|ico)$/,
type: "asset/resource", // Handles image files
},
],
},
plugins: [
new webpack.DefinePlugin(envKeys),
new HtmlWebpackPlugin({
template: "./public/index.html",
minify: isProduction,
inject: true,
templateParameters: envConfig
}),
(isProduction ? new MiniCssExtractPlugin({ filename: 'styles.[contenthash].css' }) : new MiniCssExtractPlugin()),
new CopyWebpackPlugin({
patterns: [
{
from: path.resolve(__dirname, "src/assets"),
to: "assets", // Copies to `dist/assets`
noErrorOnMissing: true, // Prevents errors if the folder is missing
},
{ from: 'public', to: '', noErrorOnMissing: true, globOptions: { ignore: ['**/index.html'] }}, // Copies everything else from public to dist
],
}),
],
optimization: isProduction
? {
minimize: true,
minimizer: [new CssMinimizerPlugin()],
}
: {},
devServer: {
static: path.join(__dirname, "public"),
port: 3000,
hot: true,
historyApiFallback: true,
},
};