Merge branch 'main' into pr/62

This commit is contained in:
Abi Raja 2023-11-30 11:36:39 -05:00
commit 339f07373a
39 changed files with 2215 additions and 266 deletions

7
.gitignore vendored
View File

@ -1,6 +1,13 @@
.aider*
# Project-related files
# Run logs
backend/run_logs/*
# Weird Docker setup related files
backend/backend/*
# Env vars
frontend/.env.local
.env

View File

@ -1,6 +1,6 @@
# screenshot-to-code
This simple app converts a screenshot to HTML/Tailwind CSS. It uses GPT-4 Vision to generate the code and DALL-E 3 to generate similar-looking images.
This simple app converts a screenshot to code (HTML/Tailwind CSS, or React or Vue or Bootstrap). It uses GPT-4 Vision to generate the code and DALL-E 3 to generate similar-looking images. You can now also enter a URL to clone a live website!
https://github.com/abi/screenshot-to-code/assets/23818/6cebadae-2fe3-4986-ac6a-8fb9db030045
@ -8,15 +8,18 @@ See the [Examples](#examples) section below for more demos.
## 🚀 Try It Out!
🆕 [Try it here](https://picoapps.xyz/free-tools/screenshot-to-code) (bring your own OpenAI key - **your key must have access to GPT-4 Vision. See [FAQ](#%EF%B8%8F-faqs) section below for details**). Or see [Getting Started](#-getting-started) below for local install instructions.
🆕 [Try it here](https://screenshottocode.com) (bring your own OpenAI key - **your key must have access to GPT-4 Vision. See [FAQ](#%EF%B8%8F-faqs) section below for details**). Or see [Getting Started](#-getting-started) below for local install instructions.
## 🌟 Recent Updates
- Nov 28 - 🔥 🔥 🔥 Get output code in React or Bootstrap or TailwindCSS
- Nov 23 - Send in a screenshot of the current replicated version (sometimes improves quality of subsequent generations)
- Nov 21 - Edit code in the code editor and preview changes live thanks to [@clean99](https://github.com/clean99)
- Nov 20 - Paste in a URL to screenshot and clone (requires [ScreenshotOne free API key](https://screenshotone.com?via=screenshot-to-code))
- Nov 19 - Support for dark/light code editor theme - thanks https://github.com/kachbit
- Nov 19 - Support for dark/light code editor theme - thanks [@kachbit](https://github.com/kachbit)
- Nov 16 - Added a setting to disable DALL-E image generation if you don't need that
- Nov 16 - View code directly within the app
- Nov 15 - 🔥 You can now instruct the AI to update the code as you wish. It is helpful if the AI messed up some styles or missed a section.
- Nov 15 - You can now instruct the AI to update the code as you wish. It is helpful if the AI messed up some styles or missed a section.
## 🛠 Getting Started
@ -44,6 +47,12 @@ Open http://localhost:5173 to use the app.
If you prefer to run the backend on a different port, update VITE_WS_BACKEND_URL in `frontend/.env.local`
For debugging purposes, if you don't want to waste GPT4-Vision credits, you can run the backend in mock mode (which streams a pre-recorded response):
```bash
MOCK=true poetry run uvicorn main:app --reload --port 7001
```
## Docker
If you have Docker installed on your system, in the root directory, run:
@ -58,7 +67,7 @@ The app will be up and running at http://localhost:5173. Note that you can't dev
## 🙋‍♂️ FAQs
- **I'm running into an error when setting up the backend. How can I fix it?** [Try this](https://github.com/abi/screenshot-to-code/issues/3#issuecomment-1814777959). If that still doesn't work, open an issue.
- **How do I get an OpenAI API key that has the GPT4 Vision model available?** Create an OpenAI account. And then, you need to buy at least $1 worth of credit on the [Billing dashboard](https://platform.openai.com/account/billing/overview).
- **How do I get an OpenAI API key?** See https://github.com/abi/screenshot-to-code/blob/main/Troubleshooting.md
- **How can I provide feedback?** For feedback, feature requests and bug reports, open an issue or ping me on [Twitter](https://twitter.com/_abi_).
## 📚 Examples
@ -66,11 +75,9 @@ The app will be up and running at http://localhost:5173. Note that you can't dev
**NYTimes**
| Original | Replica |
| -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| <img width="1238" alt="Screenshot 2023-11-20 at 12 54 03 PM" src="https://github.com/abi/screenshot-to-code/assets/23818/3b644dfa-9ca6-4148-84a7-3405b6671922"> | <img width="1414" alt="Screenshot 2023-11-20 at 12 59 56 PM" src="https://github.com/abi/screenshot-to-code/assets/23818/26201c9f-1a28-4f35-a3b1-1f04e2b8ce2a"> |
**Instagram page (with not Taylor Swift pics)**
https://github.com/abi/screenshot-to-code/assets/23818/503eb86a-356e-4dfc-926a-dabdb1ac7ba1
@ -81,4 +88,6 @@ https://github.com/abi/screenshot-to-code/assets/23818/3fec0f77-44e8-4fb3-a769-a
## 🌍 Hosted Version
🆕 [Try it here](https://picoapps.xyz/free-tools/screenshot-to-code) (bring your own OpenAI key - **your key must have access to GPT-4 Vision. See [FAQ](#%EF%B8%8F-faqs) section for details**). Or see [Getting Started](#-getting-started) for local install instructions.
🆕 [Try it here](https://screenshottocode.com) (bring your own OpenAI key - **your key must have access to GPT-4 Vision. See [FAQ](#%EF%B8%8F-faqs) section for details**). Or see [Getting Started](#-getting-started) for local install instructions.
[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/abiraja)

17
Troubleshooting.md Normal file
View File

@ -0,0 +1,17 @@
### Getting an OpenAI API key with GPT4-Vision model access
You don't need a ChatGPT Pro account. Screenshot to code uses API keys from your OpenAI developer account. In order to get access to the GPT4 Vision model, log into your OpenAI account and then, follow these instructions:
1. Open [OpenAI Dashboard](https://platform.openai.com/)
1. Go to Settings > Billing
1. Click at the Add payment details
<img width="1030" alt="285636868-c80deb92-ab47-45cd-988f-deee67fbd44d" src="https://github.com/abi/screenshot-to-code/assets/23818/4e0f4b77-9578-4f9a-803c-c12b1502f3d7">
4. You have to buy some credits. The minimum is $5.
5. Go to Settings > Limits and check at the bottom of the page, your current tier has to be "Tier 1" to have GPT4 access
<img width="785" alt="285636973-da38bd4d-8a78-4904-8027-ca67d729b933" src="https://github.com/abi/screenshot-to-code/assets/23818/8d07cd84-0cf9-4f88-bc00-80eba492eadf">
6. Go to Screenshot to code and paste it in the Settings dialog under OpenAI key (gear icon). Your key is only stored in your browser. Never stored on our servers.
Some users have also reported that it can take upto 30 minutes after your credit purchase for the GPT4 vision model to be activated.
If you've followed these steps, and it still doesn't work, feel free to open a Github issue.

3
backend/README.md Normal file
View File

@ -0,0 +1,3 @@
Run tests
pytest test_prompts.py

27
backend/access_token.py Normal file
View File

@ -0,0 +1,27 @@
import json
import os
import httpx
async def validate_access_token(access_code: str):
async with httpx.AsyncClient() as client:
url = (
"https://backend.buildpicoapps.com/screenshot_to_code/validate_access_token"
)
data = json.dumps(
{
"access_code": access_code,
"secret": os.environ.get("PICO_BACKEND_SECRET"),
}
)
headers = {"Content-Type": "application/json"}
response = await client.post(url, content=data, headers=headers)
response_data = response.json()
if response_data["success"]:
print("Access token is valid.")
return True
else:
print(f"Access token validation failed: {response_data['failure_reason']}")
return False

View File

@ -1,6 +1,5 @@
# Load environment variables first
from dotenv import load_dotenv
from pydantic import BaseModel
load_dotenv()
@ -16,6 +15,7 @@ from mock import mock_completion
from image_generation import create_alt_url_mapping, generate_images
from prompts import assemble_prompt
from routes import screenshot
from access_token import validate_access_token
app = FastAPI(openapi_url=None, docs_url=None, redoc_url=None)
@ -33,7 +33,8 @@ app.add_middleware(
# Useful for debugging purposes when you don't want to waste GPT4-Vision credits
# Setting to True will stream a mock response instead of calling the OpenAI API
SHOULD_MOCK_AI_RESPONSE = False
# TODO: Should only be set to true when value is 'True', not any abitrary truthy value
SHOULD_MOCK_AI_RESPONSE = bool(os.environ.get("MOCK", False))
app.include_router(screenshot.router)
@ -59,13 +60,39 @@ def write_logs(prompt_messages, completion):
@app.websocket("/generate-code")
async def stream_code_test(websocket: WebSocket):
async def stream_code(websocket: WebSocket):
await websocket.accept()
print("Incoming websocket connection...")
params = await websocket.receive_json()
print("Received params")
# Read the output settings from the request. Fall back to default if not provided.
output_settings = {"css": "tailwind", "js": "vanilla"}
if params["outputSettings"] and params["outputSettings"]["css"]:
output_settings["css"] = params["outputSettings"]["css"]
if params["outputSettings"] and params["outputSettings"]["js"]:
output_settings["js"] = params["outputSettings"]["js"]
print("Using output settings:", output_settings)
# Get the OpenAI API key from the request. Fall back to environment variable if not provided.
# If neither is provided, we throw an error.
openai_api_key = None
if "accessCode" in params and params["accessCode"]:
print("Access code - using platform API key")
if await validate_access_token(params["accessCode"]):
openai_api_key = os.environ.get("PLATFORM_OPENAI_API_KEY")
else:
await websocket.send_json(
{
"type": "error",
"value": "Invalid access code or you're out of credits. Please try again.",
}
)
return
else:
if params["openAiApiKey"]:
openai_api_key = params["openAiApiKey"]
print("Using OpenAI API key from client-side settings dialog")
@ -73,13 +100,6 @@ async def stream_code_test(websocket: WebSocket):
openai_api_key = os.environ.get("OPENAI_API_KEY")
if openai_api_key:
print("Using OpenAI API key from environment variable")
if params["openAiBaseURL"]:
openai_base_url = params["openAiBaseURL"]
print("Using OpenAI Base URL from client-side settings dialog")
else:
openai_base_url = os.environ.get("OPENAI_BASE_URL")
if openai_base_url:
print("Using OpenAI Base URL from environment variable")
if not openai_api_key:
print("OpenAI API key not found")
@ -90,12 +110,21 @@ async def stream_code_test(websocket: WebSocket):
}
)
return
# openai_base_url="https://flag.smarttrot.com/v1"
if not openai_base_url:
# Get the OpenAI Base URL from the request. Fall back to environment variable if not provided.
openai_base_url = None
if params["openAiBaseURL"]:
openai_base_url = params["openAiBaseURL"]
print("Using OpenAI Base URL from client-side settings dialog")
else:
openai_base_url = os.environ.get("OPENAI_BASE_URL")
if openai_base_url:
print("Using OpenAI Base URL from environment variable")
if not openai_base_url:
print("Using Offical OpenAI Base URL")
# Get the image generation flag from the request. Fall back to True if not provided.
should_generate_images = (
params["isImageGenerationEnabled"]
if "isImageGenerationEnabled" in params
@ -108,7 +137,12 @@ async def stream_code_test(websocket: WebSocket):
async def process_chunk(content):
await websocket.send_json({"type": "chunk", "value": content})
prompt_messages = assemble_prompt(params["image"])
if params.get("resultImage") and params["resultImage"]:
prompt_messages = assemble_prompt(
params["image"], output_settings, params["resultImage"]
)
else:
prompt_messages = assemble_prompt(params["image"], output_settings)
# Image cache for updates so that we don't have to regenerate images
image_cache = {}
@ -142,7 +176,10 @@ async def stream_code_test(websocket: WebSocket):
{"type": "status", "value": "Generating images..."}
)
updated_html = await generate_images(
completion, api_key=openai_api_key, base_url=openai_base_url, image_cache=image_cache
completion,
api_key=openai_api_key,
base_url=openai_base_url,
image_cache=image_cache,
)
else:
updated_html = completion

View File

@ -1,9 +1,9 @@
SYSTEM_PROMPT = """
TAILWIND_SYSTEM_PROMPT = """
You are an expert Tailwind developer
You take screenshots of a reference web page from the user, and then build single page apps
using Tailwind, HTML and JS.
You might also be given a screenshot of a web page that you have already built, and asked to
update it to look more like the reference image.
You might also be given a screenshot(The second image) of a web page that you have already built, and asked to
update it to look more like the reference image(The first image).
- Make sure the app looks exactly like the screenshot.
- Pay close attention to background color, text color, font size, font family,
@ -23,17 +23,79 @@ Return only the full code in <html></html> tags.
Do not include markdown "```" or "```html" at the start or end.
"""
BOOTSTRAP_SYSTEM_PROMPT = """
You are an expert Bootstrap developer
You take screenshots of a reference web page from the user, and then build single page apps
using Bootstrap, HTML and JS.
You might also be given a screenshot(The second image) of a web page that you have already built, and asked to
update it to look more like the reference image(The first image).
- Make sure the app looks exactly like the screenshot.
- Pay close attention to background color, text color, font size, font family,
padding, margin, border, etc. Match the colors and sizes exactly.
- Use the exact text from the screenshot.
- Do not add comments in the code such as "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" in place of writing the full code. WRITE THE FULL CODE.
- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "<!-- Repeat for each news item -->" or bad things will happen.
- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.
In terms of libraries,
- Use this script to include Bootstrap: <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
- You can use Google Fonts
- Font Awesome for icons: <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"></link>
Return only the full code in <html></html> tags.
Do not include markdown "```" or "```html" at the start or end.
"""
REACT_TAILWIND_SYSTEM_PROMPT = """
You are an expert React/Tailwind developer
You take screenshots of a reference web page from the user, and then build single page apps
using React and Tailwind CSS.
You might also be given a screenshot(The second image) of a web page that you have already built, and asked to
update it to look more like the reference image(The first image).
- Make sure the app looks exactly like the screenshot.
- Pay close attention to background color, text color, font size, font family,
padding, margin, border, etc. Match the colors and sizes exactly.
- Use the exact text from the screenshot.
- Do not add comments in the code such as "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" in place of writing the full code. WRITE THE FULL CODE.
- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "<!-- Repeat for each news item -->" or bad things will happen.
- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.
In terms of libraries,
- Use these script to include React so that it can run on a standalone page:
<script src="https://unpkg.com/react/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.js"></script>
- Use this script to include Tailwind: <script src="https://cdn.tailwindcss.com"></script>
- You can use Google Fonts
- Font Awesome for icons: <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"></link>
Return only the full code in <html></html> tags.
Do not include markdown "```" or "```html" at the start or end.
"""
USER_PROMPT = """
Generate code for a web page that looks exactly like this.
"""
def assemble_prompt(image_data_url):
return [
{"role": "system", "content": SYSTEM_PROMPT},
{
"role": "user",
"content": [
def assemble_prompt(image_data_url, output_settings: dict, result_image_data_url=None):
# Set the system prompt based on the output settings
chosen_prompt_name = "tailwind"
system_content = TAILWIND_SYSTEM_PROMPT
if output_settings["css"] == "bootstrap":
chosen_prompt_name = "bootstrap"
system_content = BOOTSTRAP_SYSTEM_PROMPT
if output_settings["js"] == "react":
chosen_prompt_name = "react-tailwind"
system_content = REACT_TAILWIND_SYSTEM_PROMPT
print("Using system prompt:", chosen_prompt_name)
user_content = [
{
"type": "image_url",
"image_url": {"url": image_data_url, "detail": "high"},
@ -42,6 +104,24 @@ def assemble_prompt(image_data_url):
"type": "text",
"text": USER_PROMPT,
},
],
]
# Include the result image if it exists
if result_image_data_url:
user_content.insert(
1,
{
"type": "image_url",
"image_url": {"url": result_image_data_url, "detail": "high"},
},
)
return [
{
"role": "system",
"content": system_content,
},
{
"role": "user",
"content": user_content,
},
]

97
backend/test_prompts.py Normal file
View File

@ -0,0 +1,97 @@
from prompts import assemble_prompt
TAILWIND_SYSTEM_PROMPT = """
You are an expert Tailwind developer
You take screenshots of a reference web page from the user, and then build single page apps
using Tailwind, HTML and JS.
You might also be given a screenshot(The second image) of a web page that you have already built, and asked to
update it to look more like the reference image(The first image).
- Make sure the app looks exactly like the screenshot.
- Pay close attention to background color, text color, font size, font family,
padding, margin, border, etc. Match the colors and sizes exactly.
- Use the exact text from the screenshot.
- Do not add comments in the code such as "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" in place of writing the full code. WRITE THE FULL CODE.
- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "<!-- Repeat for each news item -->" or bad things will happen.
- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.
In terms of libraries,
- Use this script to include Tailwind: <script src="https://cdn.tailwindcss.com"></script>
- You can use Google Fonts
- Font Awesome for icons: <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"></link>
Return only the full code in <html></html> tags.
Do not include markdown "```" or "```html" at the start or end.
"""
BOOTSTRAP_SYSTEM_PROMPT = """
You are an expert Bootstrap developer
You take screenshots of a reference web page from the user, and then build single page apps
using Bootstrap, HTML and JS.
You might also be given a screenshot(The second image) of a web page that you have already built, and asked to
update it to look more like the reference image(The first image).
- Make sure the app looks exactly like the screenshot.
- Pay close attention to background color, text color, font size, font family,
padding, margin, border, etc. Match the colors and sizes exactly.
- Use the exact text from the screenshot.
- Do not add comments in the code such as "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" in place of writing the full code. WRITE THE FULL CODE.
- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "<!-- Repeat for each news item -->" or bad things will happen.
- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.
In terms of libraries,
- Use this script to include Bootstrap: <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
- You can use Google Fonts
- Font Awesome for icons: <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"></link>
Return only the full code in <html></html> tags.
Do not include markdown "```" or "```html" at the start or end.
"""
REACT_TAILWIND_SYSTEM_PROMPT = """
You are an expert React/Tailwind developer
You take screenshots of a reference web page from the user, and then build single page apps
using React and Tailwind CSS.
You might also be given a screenshot(The second image) of a web page that you have already built, and asked to
update it to look more like the reference image(The first image).
- Make sure the app looks exactly like the screenshot.
- Pay close attention to background color, text color, font size, font family,
padding, margin, border, etc. Match the colors and sizes exactly.
- Use the exact text from the screenshot.
- Do not add comments in the code such as "<!-- Add other navigation links as needed -->" and "<!-- ... other news items ... -->" in place of writing the full code. WRITE THE FULL CODE.
- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "<!-- Repeat for each news item -->" or bad things will happen.
- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later.
In terms of libraries,
- Use these script to include React so that it can run on a standalone page:
<script src="https://unpkg.com/react/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.js"></script>
- Use this script to include Tailwind: <script src="https://cdn.tailwindcss.com"></script>
- You can use Google Fonts
- Font Awesome for icons: <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"></link>
Return only the full code in <html></html> tags.
Do not include markdown "```" or "```html" at the start or end.
"""
def test_prompts():
tailwind_prompt = assemble_prompt(
"image_data_url", {"css": "tailwind", "js": "vanilla"}, "result_image_data_url"
)
assert tailwind_prompt[0]["content"] == TAILWIND_SYSTEM_PROMPT
bootstrap_prompt = assemble_prompt(
"image_data_url", {"css": "bootstrap", "js": "vanilla"}, "result_image_data_url"
)
assert bootstrap_prompt[0]["content"] == BOOTSTRAP_SYSTEM_PROMPT
react_tailwind_prompt = assemble_prompt(
"image_data_url", {"css": "tailwind", "js": "react"}, "result_image_data_url"
)
assert react_tailwind_prompt[0]["content"] == REACT_TAILWIND_SYSTEM_PROMPT

3
frontend/.gitignore vendored
View File

@ -22,3 +22,6 @@ dist-ssr
*.njsproj
*.sln
*.sw?
# Env files
.env*

View File

@ -17,7 +17,38 @@
rel="stylesheet"
/>
<!-- Injected code for hosted version -->
<%- injectHead %>
<title>Screenshot to Code</title>
<!-- Open Graph Meta Tags -->
<meta property="og:title" content="Screenshot to Code" />
<meta
property="og:description"
content="Convert any screenshot or design to clean code"
/>
<meta
property="og:image"
content="https://screenshottocode.com/brand/twitter-summary-card.png"
/>
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="628" />
<meta property="og:url" content="https://screenshottocode.com" />
<meta property="og:type" content="website" />
<!-- Twitter Card tags -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@picoapps" />
<!-- Keep in sync with og:title, og:description and og:image -->
<meta name="twitter:title" content="Screenshot to Code" />
<meta
name="twitter:description"
content="Convert any screenshot or design to clean code"
/>
<meta
name="twitter:image"
content="https://screenshottocode.com/brand/twitter-summary-card.png"
/>
</head>
<body>
<div id="root"></div>

View File

@ -5,15 +5,22 @@
"type": "module",
"scripts": {
"dev": "vite",
"dev-hosted": "vite --mode prod",
"build": "tsc && vite build",
"build-hosted": "tsc && vite build --mode prod",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@codemirror/lang-html": "^6.4.6",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
@ -22,6 +29,8 @@
"classnames": "^2.3.2",
"clsx": "^2.0.0",
"codemirror": "^6.0.1",
"copy-to-clipboard": "^3.3.3",
"html2canvas": "^1.4.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",
@ -46,7 +55,8 @@
"postcss": "^8.4.31",
"tailwindcss": "^3.3.5",
"typescript": "^5.0.2",
"vite": "^4.4.5"
"vite": "^4.4.5",
"vite-plugin-html": "^3.2.0"
},
"engines": {
"node": ">=14.18.0"

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

View File

@ -1,4 +1,4 @@
import { useState } from "react";
import { useRef, useState } from "react";
import ImageUpload from "./components/ImageUpload";
import CodePreview from "./components/CodePreview";
import Preview from "./components/Preview";
@ -12,23 +12,33 @@ import {
FaMobile,
FaUndo,
} from "react-icons/fa";
import { Switch } from "./components/ui/switch";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./components/ui/tabs";
import CodeMirror from "./components/CodeMirror";
import SettingsDialog from "./components/SettingsDialog";
import { Settings } from "./types";
import {
Settings,
EditorTheme,
AppState,
CSSOption,
OutputSettings,
JSFrameworkOption,
} from "./types";
import { IS_RUNNING_ON_CLOUD } from "./config";
import { PicoBadge } from "./components/PicoBadge";
import { OnboardingNote } from "./components/OnboardingNote";
import { usePersistedState } from "./hooks/usePersistedState";
import { UrlInputSection } from "./components/UrlInputSection";
import TermsOfServiceDialog from "./components/TermsOfServiceDialog";
import html2canvas from "html2canvas";
import { USER_CLOSE_WEB_SOCKET_CODE } from "./constants";
import CodeTab from "./components/CodeTab";
import OutputSettingsSection from "./components/OutputSettingsSection";
function App() {
const [appState, setAppState] = useState<"INITIAL" | "CODING" | "CODE_READY">(
"INITIAL"
);
const [appState, setAppState] = useState<AppState>(AppState.INITIAL);
const [generatedCode, setGeneratedCode] = useState<string>("");
const [referenceImages, setReferenceImages] = useState<string[]>([]);
const [executionConsole, setExecutionConsole] = useState<string[]>([]);
@ -40,10 +50,33 @@ function App() {
openAiBaseURL: null,
screenshotOneApiKey: null,
isImageGenerationEnabled: true,
editorTheme: "cobalt",
editorTheme: EditorTheme.COBALT,
isTermOfServiceAccepted: false,
accessCode: null,
},
"setting"
);
const [outputSettings, setOutputSettings] = useState<OutputSettings>({
css: CSSOption.TAILWIND,
js: JSFrameworkOption.NO_FRAMEWORK,
});
const [shouldIncludeResultImage, setShouldIncludeResultImage] =
useState<boolean>(false);
const wsRef = useRef<WebSocket>(null);
const takeScreenshot = async (): Promise<string> => {
const iframeElement = document.querySelector(
"#preview-desktop"
) as HTMLIFrameElement;
if (!iframeElement?.contentWindow?.document.body) {
return "";
}
const canvas = await html2canvas(iframeElement.contentWindow.document.body);
const png = canvas.toDataURL("image/png");
return png;
};
const downloadCode = () => {
// Create a blob from the generated code
@ -63,31 +96,41 @@ function App() {
};
const reset = () => {
setAppState("INITIAL");
setAppState(AppState.INITIAL);
setGeneratedCode("");
setReferenceImages([]);
setExecutionConsole([]);
setHistory([]);
};
const stop = () => {
wsRef.current?.close?.(USER_CLOSE_WEB_SOCKET_CODE);
// make sure stop can correct the state even if the websocket is already closed
setAppState(AppState.CODE_READY);
};
function doGenerateCode(params: CodeGenerationParams) {
setExecutionConsole([]);
setAppState("CODING");
setAppState(AppState.CODING);
// Merge settings with params
const updatedParams = { ...params, ...settings };
const updatedParams = { ...params, ...settings, outputSettings };
generateCode(
wsRef,
updatedParams,
(token) => setGeneratedCode((prev) => prev + token),
(code) => setGeneratedCode(code),
(line) => setExecutionConsole((prev) => [...prev, line]),
() => setAppState("CODE_READY")
() => setAppState(AppState.CODE_READY)
);
}
// Initial version creation
function doCreate(referenceImages: string[]) {
// Reset any existing state
reset();
setReferenceImages(referenceImages);
if (referenceImages.length > 0) {
doGenerateCode({
@ -98,53 +141,88 @@ function App() {
}
// Subsequent updates
function doUpdate() {
async function doUpdate() {
const updatedHistory = [...history, generatedCode, updateInstruction];
if (shouldIncludeResultImage) {
const resultImage = await takeScreenshot();
doGenerateCode({
generationType: "update",
image: referenceImages[0],
resultImage: resultImage,
history: updatedHistory,
});
} else {
doGenerateCode({
generationType: "update",
image: referenceImages[0],
history: updatedHistory,
});
}
setHistory(updatedHistory);
setGeneratedCode("");
setUpdateInstruction("");
}
return (
<div className="mt-2">
{IS_RUNNING_ON_CLOUD && <PicoBadge />}
{IS_RUNNING_ON_CLOUD && <TermsOfServiceDialog />}
const handleTermDialogOpenChange = (open: boolean) => {
setSettings((s) => ({
...s,
isTermOfServiceAccepted: !open,
}));
};
<div className="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-96 lg:flex-col">
<div className="flex grow flex-col gap-y-2 overflow-y-auto border-r border-gray-200 bg-white px-6">
<div className="flex items-center justify-between mt-10">
return (
<div className="mt-2 dark:bg-black dark:text-white">
{IS_RUNNING_ON_CLOUD && <PicoBadge settings={settings} />}
{IS_RUNNING_ON_CLOUD && (
<TermsOfServiceDialog
open={!settings.isTermOfServiceAccepted}
onOpenChange={handleTermDialogOpenChange}
/>
)}
<div className="lg:fixed lg:inset-y-0 lg:z-40 lg:flex lg:w-96 lg:flex-col">
<div className="flex grow flex-col gap-y-2 overflow-y-auto border-r border-gray-200 bg-white px-6 dark:bg-zinc-950 dark:text-white">
<div className="flex items-center justify-between mt-10 mb-2">
<h1 className="text-2xl ">Screenshot to Code</h1>
<SettingsDialog settings={settings} setSettings={setSettings} />
</div>
{appState === "INITIAL" && (
<h2 className="text-sm text-gray-500 mb-2">
Drag & drop a screenshot to get started.
</h2>
<OutputSettingsSection
outputSettings={outputSettings}
setOutputSettings={setOutputSettings}
shouldDisableUpdates={
appState === AppState.CODING || appState === AppState.CODE_READY
}
/>
{IS_RUNNING_ON_CLOUD &&
!(settings.openAiApiKey || settings.accessCode) && (
<OnboardingNote />
)}
{IS_RUNNING_ON_CLOUD && !settings.openAiApiKey && <OnboardingNote />}
{(appState === "CODING" || appState === "CODE_READY") && (
{(appState === AppState.CODING ||
appState === AppState.CODE_READY) && (
<>
{/* Show code preview only when coding */}
{appState === "CODING" && (
{appState === AppState.CODING && (
<div className="flex flex-col">
<div className="flex items-center gap-x-1">
<Spinner />
{executionConsole.slice(-1)[0]}
</div>
<div className="flex mt-4 w-full">
<Button
onClick={stop}
className="w-full dark:text-white dark:bg-gray-700"
>
Stop
</Button>
</div>
<CodePreview code={generatedCode} />
</div>
)}
{appState === "CODE_READY" && (
{appState === AppState.CODE_READY && (
<div>
<div className="grid w-full gap-2">
<Textarea
@ -152,18 +230,33 @@ function App() {
onChange={(e) => setUpdateInstruction(e.target.value)}
value={updateInstruction}
/>
<Button onClick={doUpdate}>Update</Button>
<div className="flex justify-between items-center gap-x-2">
<div className="font-500 text-xs text-slate-700 dark:text-white">
Include screenshot of current version?
</div>
<Switch
checked={shouldIncludeResultImage}
onCheckedChange={setShouldIncludeResultImage}
className="dark:bg-gray-700"
/>
</div>
<Button
onClick={doUpdate}
className="dark:text-white dark:bg-gray-700"
>
Update
</Button>
</div>
<div className="flex items-center gap-x-2 mt-2">
<Button
onClick={downloadCode}
className="flex items-center gap-x-2"
className="flex items-center gap-x-2 dark:text-white dark:bg-gray-700"
>
<FaDownload /> Download
</Button>
<Button
onClick={reset}
className="flex items-center gap-x-2"
className="flex items-center gap-x-2 dark:text-white dark:bg-gray-700"
>
<FaUndo />
Reset
@ -177,7 +270,7 @@ function App() {
<div className="flex flex-col">
<div
className={classNames({
"scanning relative": appState === "CODING",
"scanning relative": appState === AppState.CODING,
})}
>
<img
@ -210,7 +303,7 @@ function App() {
</div>
<main className="py-2 lg:pl-96">
{appState === "INITIAL" && (
{appState === AppState.INITIAL && (
<div className="flex flex-col justify-center items-center gap-y-10">
<ImageUpload setReferenceImages={doCreate} />
<UrlInputSection
@ -220,7 +313,7 @@ function App() {
</div>
)}
{(appState === "CODING" || appState === "CODE_READY") && (
{(appState === AppState.CODING || appState === AppState.CODE_READY) && (
<div className="ml-4">
<Tabs defaultValue="desktop">
<div className="flex justify-end mr-8 mb-4">
@ -244,9 +337,10 @@ function App() {
<Preview code={generatedCode} device="mobile" />
</TabsContent>
<TabsContent value="code">
<CodeMirror
<CodeTab
code={generatedCode}
editorTheme={settings.editorTheme}
setCode={setGeneratedCode}
settings={settings}
/>
</TabsContent>
</Tabs>

View File

@ -1,6 +1,6 @@
import { useRef, useEffect } from "react";
import { useRef, useEffect, useMemo } from "react";
import { EditorState } from "@codemirror/state";
import { EditorView, keymap, lineNumbers } from "@codemirror/view";
import { EditorView, keymap, lineNumbers, ViewUpdate } from "@codemirror/view";
import { espresso, cobalt } from "thememirror";
import {
defaultKeymap,
@ -11,24 +11,20 @@ import {
} from "@codemirror/commands";
import { bracketMatching } from "@codemirror/language";
import { html } from "@codemirror/lang-html";
import { EditorTheme } from "@/types";
interface Props {
code: string;
editorTheme: string;
editorTheme: EditorTheme;
onCodeChange: (code: string) => void;
}
function CodeMirror({ code, editorTheme }: Props) {
function CodeMirror({ code, editorTheme, onCodeChange }: Props) {
const ref = useRef<HTMLDivElement>(null);
const view = useRef<EditorView | null>(null);
useEffect(() => {
let selectedTheme = cobalt;
if (editorTheme === "espresso") {
selectedTheme = espresso;
}
view.current = new EditorView({
state: EditorState.create({
doc: code,
const editorState = useMemo(
() =>
EditorState.create({
extensions: [
history(),
keymap.of([
@ -40,10 +36,21 @@ function CodeMirror({ code, editorTheme }: Props) {
lineNumbers(),
bracketMatching(),
html(),
selectedTheme,
editorTheme === EditorTheme.ESPRESSO ? espresso : cobalt,
EditorView.lineWrapping,
EditorView.updateListener.of((update: ViewUpdate) => {
if (update.docChanged) {
const updatedCode = update.state.doc.toString();
onCodeChange(updatedCode);
}
}),
],
}),
[editorTheme]
);
useEffect(() => {
view.current = new EditorView({
state: editorState,
parent: ref.current as Element,
});
@ -53,7 +60,7 @@ function CodeMirror({ code, editorTheme }: Props) {
view.current = null;
}
};
}, [code, editorTheme]);
}, []);
useEffect(() => {
if (view.current && view.current.state.doc.toString() !== code) {
@ -64,7 +71,10 @@ function CodeMirror({ code, editorTheme }: Props) {
}, [code]);
return (
<div className="overflow-x-scroll overflow-y-scroll mx-2 border-[4px] border-black rounded-[20px]" ref={ref} />
<div
className="overflow-x-scroll overflow-y-scroll mx-2 border-[4px] border-black rounded-[20px]"
ref={ref}
/>
);
}

View File

@ -0,0 +1,87 @@
import { FaCopy } from "react-icons/fa";
import CodeMirror from "./CodeMirror";
import { Button } from "./ui/button";
import { Settings } from "../types";
import copy from "copy-to-clipboard";
import { useCallback } from "react";
import toast from "react-hot-toast";
interface Props {
code: string;
setCode: React.Dispatch<React.SetStateAction<string>>;
settings: Settings;
}
function CodeTab({ code, setCode, settings }: Props) {
const copyCode = useCallback(() => {
copy(code);
toast.success("Copied to clipboard");
}, [code]);
const doOpenInCodepenio = useCallback(async () => {
// TODO: Update CSS and JS external links depending on the framework being used
const data = {
html: code,
editors: "100", // 1: Open HTML, 0: Close CSS, 0: Close JS
layout: "left",
css_external:
"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css" +
(code.includes("<ion-")
? ",https://cdn.jsdelivr.net/npm/@ionic/core/css/ionic.bundle.css"
: ""),
js_external:
"https://cdn.tailwindcss.com " +
(code.includes("<ion-")
? ",https://cdn.jsdelivr.net/npm/@ionic/core/dist/ionic/ionic.esm.js,https://cdn.jsdelivr.net/npm/@ionic/core/dist/ionic/ionic.js"
: ""),
};
// Create a hidden form and submit it to open the code in CodePen
// Can't use fetch API directly because we want to open the URL in a new tab
const input = document.createElement("input");
input.setAttribute("type", "hidden");
input.setAttribute("name", "data");
input.setAttribute("value", JSON.stringify(data));
const form = document.createElement("form");
form.setAttribute("method", "POST");
form.setAttribute("action", "https://codepen.io/pen/define");
form.setAttribute("target", "_blank");
form.appendChild(input);
document.body.appendChild(form);
form.submit();
}, [code]);
return (
<div className="relative">
<div className="flex justify-start items-center px-4 mb-2">
<span
title="Copy Code"
className="bg-black text-white flex items-center justify-center hover:text-black hover:bg-gray-100 cursor-pointer rounded-lg text-sm p-2.5"
onClick={copyCode}
>
Copy Code <FaCopy className="ml-2" />
</span>
<Button
onClick={doOpenInCodepenio}
className="bg-gray-100 text-black ml-2 py-2 px-4 border border-black rounded-md hover:bg-gray-400 focus:outline-none"
>
Open in{" "}
<img
src="https://assets.codepen.io/t-1/codepen-logo.svg"
alt="codepen.io"
className="h-4 ml-1"
/>
</Button>
</div>
<CodeMirror
code={code}
editorTheme={settings.editorTheme}
onCodeChange={setCode}
/>
</div>
);
}
export default CodeTab;

View File

@ -106,7 +106,9 @@ function ImageUpload({ setReferenceImages }: Props) {
// Convert images to data URLs and set the prompt images state
Promise.all(files.map((file) => fileToDataURL(file)))
.then((dataUrls) => {
if (dataUrls.length > 0) {
setReferenceImages(dataUrls.map((dataUrl) => dataUrl as string));
}
})
.catch((error) => {
// TODO: Display error to user
@ -140,7 +142,11 @@ function ImageUpload({ setReferenceImages }: Props) {
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
<div {...getRootProps({ style: style as any })}>
<input {...getInputProps()} />
<p>Drop a screenshot here, paste from clipboard, or click to select</p>
<p className="text-slate-700 text-lg">
Drag & drop a screenshot here, <br />
or paste from clipboard, <br />
or click to upload
</p>
</div>
</section>
);

View File

@ -1,20 +1,25 @@
export function OnboardingNote() {
return (
<div className="flex flex-col space-y-4 bg-slate-500 p-2 rounded text-stone-200">
Please add your OpenAI API key (must have GPT4 vision access) in the
settings dialog (gear icon above).
<br />
<br />
How do you get an OpenAI API key that has the GPT4 Vision model available?
Create an OpenAI account. And then, you need to buy at least $1 worth of
credit on the Billing dashboard.
<br />
<div className="flex flex-col space-y-4 bg-green-700 p-2 rounded text-stone-200 text-sm">
<span>
This key is only stored in your browser. Never stored on servers. This
app is open source. You can{" "}
<a href="https://github.com/abi/screenshot-to-code" className="inline">
check the code to confirm.
</a>
To use Screenshot to Code,{" "}
<a
className="inline underline hover:opacity-70"
href="https://buy.stripe.com/8wM6sre70gBW1nqaEE"
target="_blank"
>
buy some credits (100 generations for $36)
</a>{" "}
or use your own OpenAI API key with GPT4 vision access.{" "}
<a
href="https://github.com/abi/screenshot-to-code/blob/main/Troubleshooting.md"
className="inline underline hover:opacity-70"
target="_blank"
>
Follow these instructions to get yourself a key.
</a>{" "}
and paste it in the Settings dialog (gear icon above). Your key is only
stored in your browser. Never stored on our servers.
</span>
</div>
);

View File

@ -0,0 +1,201 @@
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
} from "./ui/select";
import { CSSOption, JSFrameworkOption, OutputSettings } from "../types";
import toast from "react-hot-toast";
import { Label } from "@radix-ui/react-label";
import { Button } from "./ui/button";
import { Popover, PopoverTrigger, PopoverContent } from "./ui/popover";
function displayCSSOption(option: CSSOption) {
switch (option) {
case CSSOption.TAILWIND:
return "Tailwind";
case CSSOption.BOOTSTRAP:
return "Bootstrap";
default:
return option;
}
}
function displayJSOption(option: JSFrameworkOption) {
switch (option) {
case JSFrameworkOption.REACT:
return "React";
case JSFrameworkOption.NO_FRAMEWORK:
return "No Framework";
default:
return option;
}
}
function convertStringToCSSOption(option: string) {
switch (option) {
case "tailwind":
return CSSOption.TAILWIND;
case "bootstrap":
return CSSOption.BOOTSTRAP;
default:
throw new Error(`Unknown CSS option: ${option}`);
}
}
function generateDisplayString(settings: OutputSettings) {
if (
settings.js === JSFrameworkOption.REACT &&
settings.css === CSSOption.TAILWIND
) {
return (
<div className="text-gray-800 dark:text-white">
Generating <span className="font-bold">React</span> +{" "}
<span className="font-bold">Tailwind</span> code
</div>
);
} else if (
settings.js === JSFrameworkOption.NO_FRAMEWORK &&
settings.css === CSSOption.TAILWIND
) {
return (
<div className="text-gray-800 dark:text-white">
Generating <span className="font-bold">HTML</span> +{" "}
<span className="font-bold">Tailwind</span> code
</div>
);
} else if (
settings.js === JSFrameworkOption.NO_FRAMEWORK &&
settings.css === CSSOption.BOOTSTRAP
) {
return (
<div className="text-gray-800 dark:text-white">
Generating <span className="font-bold">HTML</span> +{" "}
<span className="font-bold">Bootstrap</span> code
</div>
);
}
}
interface Props {
outputSettings: OutputSettings;
setOutputSettings: React.Dispatch<React.SetStateAction<OutputSettings>>;
shouldDisableUpdates?: boolean;
}
function OutputSettingsSection({
outputSettings,
setOutputSettings,
shouldDisableUpdates = false,
}: Props) {
const onCSSValueChange = (value: string) => {
setOutputSettings((prev) => {
if (prev.js === JSFrameworkOption.REACT) {
if (value !== CSSOption.TAILWIND) {
toast.error(
'React only supports Tailwind CSS. Change JS framework to "No Framework" to use Bootstrap.'
);
}
return {
css: CSSOption.TAILWIND,
js: JSFrameworkOption.REACT,
};
} else {
return {
...prev,
css: convertStringToCSSOption(value),
};
}
});
};
const onJsFrameworkChange = (value: string) => {
if (value === JSFrameworkOption.REACT) {
setOutputSettings(() => ({
css: CSSOption.TAILWIND,
js: value as JSFrameworkOption,
}));
} else {
setOutputSettings((prev) => ({
...prev,
js: value as JSFrameworkOption,
}));
}
};
return (
<div className="flex flex-col gap-y-2 justify-between text-sm">
{generateDisplayString(outputSettings)}{" "}
{!shouldDisableUpdates && (
<Popover>
<PopoverTrigger asChild>
<Button variant="outline">Customize</Button>
</PopoverTrigger>
<PopoverContent className="w-80 text-sm">
<div className="grid gap-4">
<div className="space-y-2">
<h4 className="font-medium leading-none">Code Settings</h4>
<p className="text-muted-foreground">
Customize your code output
</p>
</div>
<div className="grid gap-2">
<div className="grid grid-cols-3 items-center gap-4">
<Label htmlFor="output-settings-js">JS</Label>
<Select
value={outputSettings.js}
onValueChange={onJsFrameworkChange}
>
<SelectTrigger
className="col-span-2 h-8"
id="output-settings-js"
>
{displayJSOption(outputSettings.js)}
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value={JSFrameworkOption.NO_FRAMEWORK}>
No Framework
</SelectItem>
<SelectItem value={JSFrameworkOption.REACT}>
React
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-3 items-center gap-4">
<Label htmlFor="output-settings-css">CSS</Label>
<Select
value={outputSettings.css}
onValueChange={onCSSValueChange}
>
<SelectTrigger
className="col-span-2 h-8"
id="output-settings-css"
>
{displayCSSOption(outputSettings.css)}
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value={CSSOption.TAILWIND}>
Tailwind
</SelectItem>
<SelectItem value={CSSOption.BOOTSTRAP}>
Bootstrap
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
</div>
</div>
</PopoverContent>
</Popover>
)}
</div>
);
}
export default OutputSettingsSection;

View File

@ -1,12 +1,39 @@
export function PicoBadge() {
import { Settings } from "../types";
export function PicoBadge({ settings }: { settings: Settings }) {
return (
<>
<a
href="https://screenshot-to-code.canny.io/feature-requests"
target="_blank"
>
<div
className="fixed z-50 bottom-16 right-5 rounded-md shadow bg-black
text-white px-4 text-xs py-3 cursor-pointer"
>
feature requests?
</div>
</a>
{!settings.accessCode && (
<a href="https://picoapps.xyz?ref=screenshot-to-code" target="_blank">
<div
className="fixed z-50 bottom-5 right-5 rounded-md shadow bg-black
text-white px-4 text-xs py-3 cursor-pointer"
className="fixed z-50 bottom-5 right-5 rounded-md shadow text-black
bg-white px-4 text-xs py-3 cursor-pointer"
>
an open source project by Pico
</div>
</a>
)}
{settings.accessCode && (
<a href="mailto:support@picoapps.xyz" target="_blank">
<div
className="fixed z-50 bottom-5 right-5 rounded-md shadow text-black
bg-white px-4 text-xs py-3 cursor-pointer"
>
email support
</div>
</a>
)}
</>
);
}

View File

@ -1,3 +1,4 @@
import { useEffect, useRef } from 'react';
import classNames from "classnames";
import useThrottle from "../hooks/useThrottle";
@ -8,12 +9,23 @@ interface Props {
function Preview({ code, device }: Props) {
const throttledCode = useThrottle(code, 200);
const iframeRef = useRef<HTMLIFrameElement | null>(null);
useEffect(() => {
const iframe = iframeRef.current;
if (iframe && iframe.contentDocument) {
iframe.contentDocument.open();
iframe.contentDocument.write(throttledCode);
iframe.contentDocument.close();
}
}, [throttledCode]);
return (
<div className="flex justify-center mx-2">
<iframe
id={`preview-${device}`}
ref={iframeRef}
title="Preview"
srcDoc={throttledCode}
className={classNames(
"border-[4px] border-black rounded-[20px] shadow-lg",
"transform scale-[0.9] origin-top",
@ -26,4 +38,5 @@ function Preview({ code, device }: Props) {
</div>
);
}
export default Preview;

View File

@ -9,11 +9,19 @@ import {
DialogTrigger,
} from "@/components/ui/dialog";
import { FaCog } from "react-icons/fa";
import { Settings } from "../types";
import { EditorTheme, Settings } from "../types";
import { Switch } from "./ui/switch";
import { Label } from "./ui/label";
import { Input } from "./ui/input";
import { Select } from "./ui/select";
import { Select, SelectContent, SelectItem, SelectTrigger } from "./ui/select";
import { capitalize } from "../lib/utils";
import { IS_RUNNING_ON_CLOUD } from "../config";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "./ui/accordion";
interface Props {
settings: Settings;
@ -21,7 +29,7 @@ interface Props {
}
function SettingsDialog({ settings, setSettings }: Props) {
const handleThemeChange = (theme: string) => {
const handleThemeChange = (theme: EditorTheme) => {
setSettings((s) => ({
...s,
editorTheme: theme,
@ -37,6 +45,31 @@ function SettingsDialog({ settings, setSettings }: Props) {
<DialogHeader>
<DialogTitle className="mb-4">Settings</DialogTitle>
</DialogHeader>
{/* Access code */}
{IS_RUNNING_ON_CLOUD && (
<div className="flex flex-col space-y-4 bg-slate-300 p-4 rounded dark:text-white dark:bg-slate-800">
<Label htmlFor="access-code">
<div>Access Code</div>
<div className="font-light mt-1 leading-relaxed">
Buy an access code.
</div>
</Label>
<Input
id="access-code dark:border-gray-700 dark:bg-gray-800 dark:text-white"
placeholder="Enter your Screenshot to Code access code"
value={settings.accessCode || ""}
onChange={(e) =>
setSettings((s) => ({
...s,
accessCode: e.target.value,
}))
}
/>
</div>
)}
<div className="flex items-center space-x-2">
<Label htmlFor="image-generation">
<div>DALL-E Placeholder Image Generation</div>
@ -95,10 +128,14 @@ function SettingsDialog({ settings, setSettings }: Props) {
}
/>
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="item-1">
<AccordionTrigger>Screenshot by URL Config</AccordionTrigger>
<AccordionContent>
<Label htmlFor="screenshot-one-api-key">
<div>ScreenshotOne API key</div>
<div className="font-light mt-2 leading-relaxed">
Only stored in your browser. Never stored on servers.{" "}
<div className="leading-normal font-normal text-xs">
If you want to use URLs directly instead of taking the
screenshot yourself, add a ScreenshotOne API key.{" "}
<a
href="https://screenshotone.com?via=screenshot-to-code"
className="underline"
@ -111,6 +148,7 @@ function SettingsDialog({ settings, setSettings }: Props) {
<Input
id="screenshot-one-api-key"
className="mt-2"
placeholder="ScreenshotOne API key"
value={settings.screenshotOneApiKey || ""}
onChange={(e) =>
@ -120,23 +158,61 @@ function SettingsDialog({ settings, setSettings }: Props) {
}))
}
/>
</AccordionContent>
</AccordionItem>
</Accordion>
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="item-1">
<AccordionTrigger>Theme Settings</AccordionTrigger>
<AccordionContent className="space-y-4 flex flex-col">
<div className="flex items-center justify-between">
<Label htmlFor="app-theme">
<div>App Theme</div>
</Label>
<div>
<button
className="flex rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50t"
onClick={() => {
document
.querySelector("div.mt-2")
?.classList.toggle("dark"); // enable dark mode for sidebar
document.body.classList.toggle("dark");
document
.querySelector('div[role="presentation"]')
?.classList.toggle("dark"); // enable dark mode for upload container
}}
>
Toggle dark mode
</button>
</div>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="editor-theme">
<div>Editor Theme</div>
<div>Code Editor Theme</div>
</Label>
<div>
<Select // Use the custom Select component here
id="editor-theme"
name="editor-theme"
value={settings.editorTheme}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
handleThemeChange(e.target.value)
onValueChange={(value) =>
handleThemeChange(value as EditorTheme)
}
>
<option value="cobalt">Cobalt</option>
<option value="espresso">Espresso</option>
<SelectTrigger className="w-[180px]">
{capitalize(settings.editorTheme)}
</SelectTrigger>
<SelectContent>
<SelectItem value="cobalt">Cobalt</SelectItem>
<SelectItem value="espresso">Espresso</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
<DialogFooter>
<DialogClose>Save</DialogClose>

View File

@ -1,25 +1,54 @@
import React from "react";
import {
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { useState } from "react";
AlertDialog,
AlertDialogAction,
AlertDialogContent,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "./ui/alert-dialog";
import { Input } from "./ui/input";
import toast from "react-hot-toast";
import { PICO_BACKEND_FORM_SECRET } from "../config";
function TermsOfServiceDialog() {
const [isOpen, setIsOpen] = useState(true);
const TermsOfServiceDialog: React.FC<{
open: boolean;
onOpenChange: (open: boolean) => void;
}> = ({ open, onOpenChange }) => {
const [email, setEmail] = React.useState("");
const onSubscribe = async () => {
await fetch("https://backend.buildpicoapps.com/form", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ email, secret: PICO_BACKEND_FORM_SECRET }),
});
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle className="mb-4">Terms of Service</DialogTitle>
</DialogHeader>
<AlertDialog open={open} onOpenChange={onOpenChange}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="mb-2">
Enter your email to get started
</AlertDialogTitle>
</AlertDialogHeader>
<div className="mb-2">
<Input
placeholder="Email"
value={email}
onChange={(e) => {
setEmail(e.target.value);
}}
/>
</div>
<div className="flex items-center space-x-2">
<span>
By using this website, you agree to the{" "}
By providing your email, you consent to receiving occasional product
updates, and you accept the{" "}
<a
href="https://a.picoapps.xyz/camera-write"
target="_blank"
@ -27,24 +56,36 @@ function TermsOfServiceDialog() {
>
terms of service
</a>
. This project is MIT licensed.{" "}
. <br />
<br />
Prefer to run it yourself locally? This project is open source.{" "}
<a
href="https://github.com/abi/screenshot-to-code"
target="_blank"
className="underline"
>
You can run this app locally by downloading the source code from
Github.
Download the code and get started on Github.
</a>
</span>
</div>
<DialogFooter>
<DialogClose>Agree</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
<AlertDialogFooter>
<AlertDialogAction
onClick={(e) => {
if (!email.trim() || !email.trim().includes("@")) {
e.preventDefault();
toast.error("Please enter your email");
} else {
onSubscribe();
}
}}
>
Agree
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};
export default TermsOfServiceDialog;

View File

@ -12,11 +12,13 @@ interface Props {
export function UrlInputSection({ doCreate, screenshotOneApiKey }: Props) {
const [isLoading, setIsLoading] = useState(false);
const [referenceUrl, setReferenceUrl] = useState("");
const isDisabled = !screenshotOneApiKey;
async function takeScreenshot() {
if (!screenshotOneApiKey) {
toast.error("Please add a Screenshot API key in the settings dialog");
toast.error(
"Please add a ScreenshotOne API key in the Settings dialog. This is optional - you can also drag/drop and upload images directly.",
{ duration: 8000 }
);
return;
}
@ -64,24 +66,13 @@ export function UrlInputSection({ doCreate, screenshotOneApiKey }: Props) {
onChange={(e) => setReferenceUrl(e.target.value)}
value={referenceUrl}
/>
<Button onClick={takeScreenshot} disabled={isDisabled || isLoading}>
<Button
onClick={takeScreenshot}
disabled={isLoading}
className="bg-slate-400"
>
{isLoading ? "Capturing..." : "Capture"}
</Button>
{isDisabled && (
<div className="flex space-y-4 bg-slate-200 p-2 rounded text-stone-800 text-sm">
<span>
To screenshot a URL, add a{" "}
<a
href="https://screenshotone.com?via=screenshot-to-code"
className="underline"
target="_blank"
>
ScreenshotOne API key
</a>{" "}
in the settings dialog.
</span>
</div>
)}
</div>
);
}

View File

@ -0,0 +1,55 @@
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDownIcon } from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
const Accordion = AccordionPrimitive.Root
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn("border-b", className)}
{...props}
/>
))
AccordionItem.displayName = "AccordionItem"
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}
>
{children}
<ChevronDownIcon className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
))
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
))
AccordionContent.displayName = AccordionPrimitive.Content.displayName
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }

View File

@ -0,0 +1,139 @@
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
const AlertDialog = AlertDialogPrimitive.Root
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold", className)}
{...props}
/>
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
className
)}
{...props}
/>
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}

View File

@ -0,0 +1,36 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

View File

@ -0,0 +1,28 @@
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { CheckIcon } from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<CheckIcon className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }

View File

@ -0,0 +1,29 @@
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent }

View File

@ -1,24 +1,162 @@
import * as React from "react";
import { cn } from "@/lib/utils";
import * as React from "react"
import {
CaretSortIcon,
CheckIcon,
ChevronDownIcon,
ChevronUpIcon,
} from "@radix-ui/react-icons"
import * as SelectPrimitive from "@radix-ui/react-select"
export interface SelectProps
extends React.SelectHTMLAttributes<HTMLSelectElement> {}
import { cn } from "@/lib/utils"
const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
({ className, ...props }, ref) => {
return (
<select
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"block appearance-none w-full text-sm shadow-sm rounded-md border border-input bg-white py-2 px-3 focus:outline-none focus:border-black",
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<CaretSortIcon className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUpIcon />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDownIcon />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
{...props}
/>
);
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}
);
Select.displayName = "Select";
export { Select };

View File

@ -7,3 +7,6 @@ export const WS_BACKEND_URL =
export const HTTP_BACKEND_URL =
import.meta.env.VITE_HTTP_BACKEND_URL || "http://127.0.0.1:7001";
export const PICO_BACKEND_FORM_SECRET =
import.meta.env.VITE_PICO_BACKEND_FORM_SECRET || null;

View File

@ -0,0 +1 @@
export const USER_CLOSE_WEB_SOCKET_CODE = 4333;

View File

@ -1,17 +1,22 @@
import toast from "react-hot-toast";
import { WS_BACKEND_URL } from "./config";
import { USER_CLOSE_WEB_SOCKET_CODE } from "./constants";
const ERROR_MESSAGE =
"Error generating code. Check the Developer Console for details. Feel free to open a Github issue";
"Error generating code. Check the Developer Console AND the backend logs for details. Feel free to open a Github issue.";
const STOP_MESSAGE = "Code generation stopped";
export interface CodeGenerationParams {
generationType: "create" | "update";
image: string;
resultImage?: string;
history?: string[];
// isImageGenerationEnabled: boolean; // TODO: Merge with Settings type in types.ts
}
export function generateCode(
wsRef: React.MutableRefObject<WebSocket | null>,
params: CodeGenerationParams,
onChange: (chunk: string) => void,
onSetCode: (code: string) => void,
@ -22,6 +27,7 @@ export function generateCode(
console.log("Connecting to backend @ ", wsUrl);
const ws = new WebSocket(wsUrl);
wsRef.current = ws;
ws.addEventListener("open", () => {
ws.send(JSON.stringify(params));
@ -40,15 +46,15 @@ export function generateCode(
toast.error(response.value);
}
});
ws.addEventListener("close", (event) => {
console.log("Connection closed", event.code, event.reason);
if (event.code != 1000) {
if (event.code === USER_CLOSE_WEB_SOCKET_CODE) {
toast.success(STOP_MESSAGE);
} else if (event.code !== 1000) {
console.error("WebSocket error code", event);
toast.error(ERROR_MESSAGE);
} else {
onComplete();
}
onComplete();
});
ws.addEventListener("error", (error) => {

View File

@ -62,8 +62,20 @@
--radius: 0.5rem;
}
body.dark {
background-color: black;
}
div[role="presentation"].dark {
background-color: #09090b !important;
}
iframe {
background-color: white !important;
}
.dark {
--background: 222.2 84% 4.9%;
--background: 222.2 0% 0%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;

View File

@ -1,6 +1,10 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
return twMerge(clsx(inputs));
}
export function capitalize(str: string) {
return str.charAt(0).toUpperCase() + str.slice(1);
}

View File

@ -7,6 +7,6 @@ import { Toaster } from "react-hot-toast";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App />
<Toaster />
<Toaster toastOptions={{ className:"dark:bg-zinc-950 dark:text-white" }}/>
</React.StrictMode>
);

View File

@ -1,7 +1,36 @@
export enum EditorTheme {
ESPRESSO = "espresso",
COBALT = "cobalt",
}
export enum CSSOption {
TAILWIND = "tailwind",
BOOTSTRAP = "bootstrap",
}
export enum JSFrameworkOption {
NO_FRAMEWORK = "vanilla",
REACT = "react",
VUE = "vue",
}
export interface OutputSettings {
css: CSSOption;
js: JSFrameworkOption;
}
export interface Settings {
openAiApiKey: string | null;
openAiBaseURL: string | null;
screenshotOneApiKey: string | null;
isImageGenerationEnabled: boolean;
editorTheme: string;
editorTheme: EditorTheme;
isTermOfServiceAccepted: boolean; // Only relevant for hosted version
accessCode: string | null; // Only relevant for hosted version
}
export enum AppState {
INITIAL = "INITIAL",
CODING = "CODING",
CODE_READY = "CODE_READY",
}

View File

@ -1,15 +1,31 @@
import path from "path";
import { defineConfig } from "vite";
import { defineConfig, loadEnv } from "vite";
import checker from "vite-plugin-checker";
import react from "@vitejs/plugin-react";
import { createHtmlPlugin } from "vite-plugin-html";
// https://vitejs.dev/config/
export default defineConfig({
base: process.env.VITE_IS_DEPLOYED ? "/free-tools/screenshot-to-code/" : "",
plugins: [react(), checker({ typescript: true })],
export default ({ mode }) => {
process.env = { ...process.env, ...loadEnv(mode, process.cwd()) };
return defineConfig({
base: "",
plugins: [
react(),
checker({ typescript: true }),
createHtmlPlugin({
inject: {
data: {
injectHead: process.env.VITE_IS_DEPLOYED
? '<script defer="" data-domain="screenshottocode.com" src="https://plausible.io/js/script.js"></script>'
: "",
},
},
}),
],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
});
};

View File

@ -470,6 +470,33 @@
resolved "https://registry.npmjs.org/@eslint/js/-/js-8.53.0.tgz"
integrity sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==
"@floating-ui/core@^1.4.2":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.5.0.tgz#5c05c60d5ae2d05101c3021c1a2a350ddc027f8c"
integrity sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==
dependencies:
"@floating-ui/utils" "^0.1.3"
"@floating-ui/dom@^1.5.1":
version "1.5.3"
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.5.3.tgz#54e50efcb432c06c23cd33de2b575102005436fa"
integrity sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==
dependencies:
"@floating-ui/core" "^1.4.2"
"@floating-ui/utils" "^0.1.3"
"@floating-ui/react-dom@^2.0.0":
version "2.0.4"
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.0.4.tgz#b076fafbdfeb881e1d86ae748b7ff95150e9f3ec"
integrity sha512-CF8k2rgKeh/49UrnIBs4BdxPUV6vize/Db1d/YbCLyp9GiVZ0BEwf5AiDSxJRCr6yOkGqTFHtmrULxkEfYZ7dQ==
dependencies:
"@floating-ui/dom" "^1.5.1"
"@floating-ui/utils@^0.1.3":
version "0.1.6"
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.1.6.tgz#22958c042e10b67463997bd6ea7115fe28cbcaf9"
integrity sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==
"@humanwhocodes/config-array@^0.11.13":
version "0.11.13"
resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz"
@ -508,6 +535,14 @@
resolved "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz"
integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==
"@jridgewell/source-map@^0.3.3":
version "0.3.5"
resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.5.tgz#a3bb4d5c6825aab0d281268f47f6ad5853431e91"
integrity sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==
dependencies:
"@jridgewell/gen-mapping" "^0.3.0"
"@jridgewell/trace-mapping" "^0.3.9"
"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14":
version "1.4.15"
resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz"
@ -586,6 +621,13 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@radix-ui/number@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/number/-/number-1.0.1.tgz#644161a3557f46ed38a042acf4a770e826021674"
integrity sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.0.1.tgz#e46f9958b35d10e9f6dc71c497305c22e3e55dbd"
@ -593,6 +635,73 @@
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-accordion@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-accordion/-/react-accordion-1.1.2.tgz#738441f7343e5142273cdef94d12054c3287966f"
integrity sha512-fDG7jcoNKVjSK6yfmuAs0EnPDro0WMXIhMtXdTBWqEioVW206ku+4Lw07e+13lUkFkpoEQ2PdeMIAGpdqEAmDg==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "1.0.1"
"@radix-ui/react-collapsible" "1.0.3"
"@radix-ui/react-collection" "1.0.3"
"@radix-ui/react-compose-refs" "1.0.1"
"@radix-ui/react-context" "1.0.1"
"@radix-ui/react-direction" "1.0.1"
"@radix-ui/react-id" "1.0.1"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-use-controllable-state" "1.0.1"
"@radix-ui/react-alert-dialog@^1.0.5":
version "1.0.5"
resolved "https://registry.yarnpkg.com/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.0.5.tgz#70dd529cbf1e4bff386814d3776901fcaa131b8c"
integrity sha512-OrVIOcZL0tl6xibeuGt5/+UxoT2N27KCFOPjFyfXMnchxSHZ/OW7cCX2nGlIYJrbHK/fczPcFzAwvNBB6XBNMA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "1.0.1"
"@radix-ui/react-compose-refs" "1.0.1"
"@radix-ui/react-context" "1.0.1"
"@radix-ui/react-dialog" "1.0.5"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-slot" "1.0.2"
"@radix-ui/react-arrow@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz#c24f7968996ed934d57fe6cde5d6ec7266e1d25d"
integrity sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-checkbox@^1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-checkbox/-/react-checkbox-1.0.4.tgz#98f22c38d5010dd6df4c5744cac74087e3275f4b"
integrity sha512-CBuGQa52aAYnADZVt/KBQzXrwx6TqnlwtcIPGtVt5JkkzQwMOLJjPukimhfKEr4GQNd43C+djUh5Ikopj8pSLg==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "1.0.1"
"@radix-ui/react-compose-refs" "1.0.1"
"@radix-ui/react-context" "1.0.1"
"@radix-ui/react-presence" "1.0.1"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-use-controllable-state" "1.0.1"
"@radix-ui/react-use-previous" "1.0.1"
"@radix-ui/react-use-size" "1.0.1"
"@radix-ui/react-collapsible@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-collapsible/-/react-collapsible-1.0.3.tgz#df0e22e7a025439f13f62d4e4a9e92c4a0df5b81"
integrity sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "1.0.1"
"@radix-ui/react-compose-refs" "1.0.1"
"@radix-ui/react-context" "1.0.1"
"@radix-ui/react-id" "1.0.1"
"@radix-ui/react-presence" "1.0.1"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-use-controllable-state" "1.0.1"
"@radix-ui/react-use-layout-effect" "1.0.1"
"@radix-ui/react-collection@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.0.3.tgz#9595a66e09026187524a36c6e7e9c7d286469159"
@ -618,7 +727,7 @@
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-dialog@^1.0.5":
"@radix-ui/react-dialog@1.0.5", "@radix-ui/react-dialog@^1.0.5":
version "1.0.5"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz#71657b1b116de6c7a0b03242d7d43e01062c7300"
integrity sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==
@ -696,6 +805,45 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-popover@^1.0.7":
version "1.0.7"
resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.0.7.tgz#23eb7e3327330cb75ec7b4092d685398c1654e3c"
integrity sha512-shtvVnlsxT6faMnK/a7n0wptwBD23xc1Z5mdrtKLwVEfsEMXodS0r5s0/g5P0hX//EKYZS2sxUjqfzlg52ZSnQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "1.0.1"
"@radix-ui/react-compose-refs" "1.0.1"
"@radix-ui/react-context" "1.0.1"
"@radix-ui/react-dismissable-layer" "1.0.5"
"@radix-ui/react-focus-guards" "1.0.1"
"@radix-ui/react-focus-scope" "1.0.4"
"@radix-ui/react-id" "1.0.1"
"@radix-ui/react-popper" "1.1.3"
"@radix-ui/react-portal" "1.0.4"
"@radix-ui/react-presence" "1.0.1"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-slot" "1.0.2"
"@radix-ui/react-use-controllable-state" "1.0.1"
aria-hidden "^1.1.1"
react-remove-scroll "2.5.5"
"@radix-ui/react-popper@1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.1.3.tgz#24c03f527e7ac348fabf18c89795d85d21b00b42"
integrity sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==
dependencies:
"@babel/runtime" "^7.13.10"
"@floating-ui/react-dom" "^2.0.0"
"@radix-ui/react-arrow" "1.0.3"
"@radix-ui/react-compose-refs" "1.0.1"
"@radix-ui/react-context" "1.0.1"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-use-callback-ref" "1.0.1"
"@radix-ui/react-use-layout-effect" "1.0.1"
"@radix-ui/react-use-rect" "1.0.1"
"@radix-ui/react-use-size" "1.0.1"
"@radix-ui/rect" "1.0.1"
"@radix-ui/react-portal@1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.0.4.tgz#df4bfd353db3b1e84e639e9c63a5f2565fb00e15"
@ -737,6 +885,34 @@
"@radix-ui/react-use-callback-ref" "1.0.1"
"@radix-ui/react-use-controllable-state" "1.0.1"
"@radix-ui/react-select@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-select/-/react-select-2.0.0.tgz#a3511792a51a7018d6559357323a7f52e0e38887"
integrity sha512-RH5b7af4oHtkcHS7pG6Sgv5rk5Wxa7XI8W5gvB1N/yiuDGZxko1ynvOiVhFM7Cis2A8zxF9bTOUVbRDzPepe6w==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/number" "1.0.1"
"@radix-ui/primitive" "1.0.1"
"@radix-ui/react-collection" "1.0.3"
"@radix-ui/react-compose-refs" "1.0.1"
"@radix-ui/react-context" "1.0.1"
"@radix-ui/react-direction" "1.0.1"
"@radix-ui/react-dismissable-layer" "1.0.5"
"@radix-ui/react-focus-guards" "1.0.1"
"@radix-ui/react-focus-scope" "1.0.4"
"@radix-ui/react-id" "1.0.1"
"@radix-ui/react-popper" "1.1.3"
"@radix-ui/react-portal" "1.0.4"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-slot" "1.0.2"
"@radix-ui/react-use-callback-ref" "1.0.1"
"@radix-ui/react-use-controllable-state" "1.0.1"
"@radix-ui/react-use-layout-effect" "1.0.1"
"@radix-ui/react-use-previous" "1.0.1"
"@radix-ui/react-visually-hidden" "1.0.3"
aria-hidden "^1.1.1"
react-remove-scroll "2.5.5"
"@radix-ui/react-separator@^1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-separator/-/react-separator-1.0.3.tgz#be5a931a543d5726336b112f465f58585c04c8aa"
@ -819,6 +995,14 @@
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-rect@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-1.0.1.tgz#fde50b3bb9fd08f4a1cd204572e5943c244fcec2"
integrity sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/rect" "1.0.1"
"@radix-ui/react-use-size@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz#1c5f5fea940a7d7ade77694bb98116fb49f870b2"
@ -827,6 +1011,29 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-layout-effect" "1.0.1"
"@radix-ui/react-visually-hidden@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.0.3.tgz#51aed9dd0fe5abcad7dee2a234ad36106a6984ac"
integrity sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/rect@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.0.1.tgz#bf8e7d947671996da2e30f4904ece343bc4a883f"
integrity sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@rollup/pluginutils@^4.2.0":
version "4.2.1"
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d"
integrity sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==
dependencies:
estree-walker "^2.0.1"
picomatch "^2.2.2"
"@types/babel__core@^7.20.3":
version "7.20.4"
resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.4.tgz"
@ -1009,7 +1216,7 @@ acorn-jsx@^5.3.2:
resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz"
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
acorn@^8.9.0:
acorn@^8.8.2, acorn@^8.9.0:
version "8.11.2"
resolved "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz"
integrity sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==
@ -1085,6 +1292,11 @@ array-union@^2.1.0:
resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz"
integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
async@^3.2.3:
version "3.2.5"
resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66"
integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==
attr-accept@^2.2.2:
version "2.2.2"
resolved "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz"
@ -1107,11 +1319,21 @@ balanced-match@^1.0.0:
resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
base64-arraybuffer@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc"
integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==
binary-extensions@^2.0.0:
version "2.2.0"
resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
boolbase@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz"
@ -1120,6 +1342,13 @@ brace-expansion@^1.1.7:
balanced-match "^1.0.0"
concat-map "0.0.1"
brace-expansion@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae"
integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==
dependencies:
balanced-match "^1.0.0"
braces@^3.0.2, braces@~3.0.2:
version "3.0.2"
resolved "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz"
@ -1137,11 +1366,24 @@ browserslist@^4.21.10, browserslist@^4.21.9:
node-releases "^2.0.13"
update-browserslist-db "^1.0.13"
buffer-from@^1.0.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
callsites@^3.0.0:
version "3.1.0"
resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz"
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
camel-case@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.2.tgz#9728072a954f805228225a6deea6b38461e1bd5a"
integrity sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==
dependencies:
pascal-case "^3.1.2"
tslib "^2.0.3"
camelcase-css@^2.0.1:
version "2.0.1"
resolved "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz"
@ -1161,7 +1403,7 @@ chalk@^2.4.2:
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
chalk@^4.0.0, chalk@^4.1.1:
chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.1:
version "4.1.2"
resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz"
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
@ -1196,6 +1438,13 @@ classnames@^2.3.2:
resolved "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz"
integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==
clean-css@^5.2.2:
version "5.3.2"
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.2.tgz#70ecc7d4d4114921f5d298349ff86a31a9975224"
integrity sha512-JVJbM+f3d3Q704rF4bqQ5UUyTtuJ0JRKNbTKVEeujCCBoMdkEi+V+e8oktO9qGQNSvHrFTM6JZRXrUvGR1czww==
dependencies:
source-map "~0.6.0"
clsx@2.0.0, clsx@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.0.0.tgz#12658f3fd98fafe62075595a5c30e43d18f3d00b"
@ -1238,12 +1487,22 @@ color-name@~1.1.4:
resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
colorette@^2.0.16:
version "2.0.20"
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a"
integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==
commander@^2.20.0:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
commander@^4.0.0:
version "4.1.1"
resolved "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz"
integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==
commander@^8.0.0:
commander@^8.0.0, commander@^8.3.0:
version "8.3.0"
resolved "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz"
integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==
@ -1253,11 +1512,28 @@ concat-map@0.0.1:
resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
connect-history-api-fallback@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc"
integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==
consola@^2.15.3:
version "2.15.3"
resolved "https://registry.yarnpkg.com/consola/-/consola-2.15.3.tgz#2e11f98d6a4be71ff72e0bdf07bd23e12cb61550"
integrity sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==
convert-source-map@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz"
integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==
copy-to-clipboard@^3.3.3:
version "3.3.3"
resolved "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz#55ac43a1db8ae639a4bd99511c148cdd1b83a1b0"
integrity sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==
dependencies:
toggle-selection "^1.0.6"
crelt@^1.0.5:
version "1.0.6"
resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72"
@ -1272,6 +1548,29 @@ cross-spawn@^7.0.2:
shebang-command "^2.0.0"
which "^2.0.1"
css-line-break@^2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz#bfef660dfa6f5397ea54116bb3cb4873edbc4fa0"
integrity sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==
dependencies:
utrie "^1.0.2"
css-select@^4.2.1:
version "4.3.0"
resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b"
integrity sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==
dependencies:
boolbase "^1.0.0"
css-what "^6.0.1"
domhandler "^4.3.1"
domutils "^2.8.0"
nth-check "^2.0.1"
css-what@^6.0.1:
version "6.1.0"
resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4"
integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==
cssesc@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz"
@ -1323,11 +1622,71 @@ doctrine@^3.0.0:
dependencies:
esutils "^2.0.2"
dom-serializer@^1.0.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30"
integrity sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==
dependencies:
domelementtype "^2.0.1"
domhandler "^4.2.0"
entities "^2.0.0"
domelementtype@^2.0.1, domelementtype@^2.2.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d"
integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==
domhandler@^4.2.0, domhandler@^4.3.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c"
integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==
dependencies:
domelementtype "^2.2.0"
domutils@^2.8.0:
version "2.8.0"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"
integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==
dependencies:
dom-serializer "^1.0.1"
domelementtype "^2.2.0"
domhandler "^4.2.0"
dot-case@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751"
integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==
dependencies:
no-case "^3.0.4"
tslib "^2.0.3"
dotenv-expand@^8.0.2:
version "8.0.3"
resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-8.0.3.tgz#29016757455bcc748469c83a19b36aaf2b83dd6e"
integrity sha512-SErOMvge0ZUyWd5B0NXMQlDkN+8r+HhVUsxgOO7IoPDOdDRD2JjExpN6y3KnFR66jsJMwSn1pqIivhU5rcJiNg==
dotenv@^16.0.0:
version "16.3.1"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e"
integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==
ejs@^3.1.6:
version "3.1.9"
resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.9.tgz#03c9e8777fe12686a9effcef22303ca3d8eeb361"
integrity sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==
dependencies:
jake "^10.8.5"
electron-to-chromium@^1.4.535:
version "1.4.582"
resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.582.tgz"
integrity sha512-89o0MGoocwYbzqUUjc+VNpeOFSOK9nIdC5wY4N+PVUarUK0MtjyTjks75AZS2bW4Kl8MdewdFsWaH0jLy+JNoA==
entities@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==
esbuild@^0.18.10:
version "0.18.20"
resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz"
@ -1466,6 +1825,11 @@ estraverse@^5.1.0, estraverse@^5.2.0:
resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz"
integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
estree-walker@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
esutils@^2.0.2:
version "2.0.3"
resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz"
@ -1476,7 +1840,7 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
fast-glob@^3.2.7, fast-glob@^3.2.9, fast-glob@^3.3.0:
fast-glob@^3.2.11, fast-glob@^3.2.7, fast-glob@^3.2.9, fast-glob@^3.3.0:
version "3.3.2"
resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz"
integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==
@ -1518,6 +1882,13 @@ file-selector@^0.6.0:
dependencies:
tslib "^2.4.0"
filelist@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5"
integrity sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==
dependencies:
minimatch "^5.0.1"
fill-range@^7.0.1:
version "7.0.1"
resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz"
@ -1552,6 +1923,15 @@ fraction.js@^4.3.6:
resolved "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz"
integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==
fs-extra@^10.0.1:
version "10.1.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf"
integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==
dependencies:
graceful-fs "^4.2.0"
jsonfile "^6.0.1"
universalify "^2.0.0"
fs-extra@^11.1.0:
version "11.1.1"
resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz"
@ -1680,6 +2060,32 @@ hasown@^2.0.0:
dependencies:
function-bind "^1.1.2"
he@1.2.0, he@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
html-minifier-terser@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#bfc818934cc07918f6b3669f5774ecdfd48f32ab"
integrity sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==
dependencies:
camel-case "^4.1.2"
clean-css "^5.2.2"
commander "^8.3.0"
he "^1.2.0"
param-case "^3.0.4"
relateurl "^0.2.7"
terser "^5.10.0"
html2canvas@^1.4.1:
version "1.4.1"
resolved "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543"
integrity sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==
dependencies:
css-line-break "^2.1.0"
text-segmentation "^1.0.3"
ignore@^5.2.0, ignore@^5.2.4:
version "5.2.4"
resolved "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz"
@ -1759,6 +2165,16 @@ isexe@^2.0.0:
resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz"
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
jake@^10.8.5:
version "10.8.7"
resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.7.tgz#63a32821177940c33f356e0ba44ff9d34e1c7d8f"
integrity sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==
dependencies:
async "^3.2.3"
chalk "^4.0.2"
filelist "^1.0.4"
minimatch "^3.1.2"
jiti@^1.19.1:
version "1.21.0"
resolved "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz"
@ -1864,6 +2280,13 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
lower-case@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28"
integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==
dependencies:
tslib "^2.0.3"
lru-cache@^5.1.1:
version "5.1.1"
resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz"
@ -1898,6 +2321,13 @@ minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
dependencies:
brace-expansion "^1.1.7"
minimatch@^5.0.1:
version "5.1.6"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96"
integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==
dependencies:
brace-expansion "^2.0.1"
ms@2.1.2:
version "2.1.2"
resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz"
@ -1922,6 +2352,22 @@ natural-compare@^1.4.0:
resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz"
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
no-case@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d"
integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==
dependencies:
lower-case "^2.0.2"
tslib "^2.0.3"
node-html-parser@^5.3.3:
version "5.4.2"
resolved "https://registry.yarnpkg.com/node-html-parser/-/node-html-parser-5.4.2.tgz#93e004038c17af80226c942336990a0eaed8136a"
integrity sha512-RaBPP3+51hPne/OolXxcz89iYvQvKOydaqoePpOgXcrOKZhjVIzmpKZz+Hd/RBO2/zN2q6CNJhQzucVz+u3Jyw==
dependencies:
css-select "^4.2.1"
he "1.2.0"
node-releases@^2.0.13:
version "2.0.13"
resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz"
@ -1944,6 +2390,13 @@ npm-run-path@^4.0.1:
dependencies:
path-key "^3.0.0"
nth-check@^2.0.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d"
integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==
dependencies:
boolbase "^1.0.0"
object-assign@^4.0.1, object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz"
@ -1987,6 +2440,14 @@ p-locate@^5.0.0:
dependencies:
p-limit "^3.0.2"
param-case@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5"
integrity sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==
dependencies:
dot-case "^3.0.4"
tslib "^2.0.3"
parent-module@^1.0.0:
version "1.0.1"
resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz"
@ -1994,6 +2455,14 @@ parent-module@^1.0.0:
dependencies:
callsites "^3.0.0"
pascal-case@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-3.1.2.tgz#b48e0ef2b98e205e7c1dae747d0b1508237660eb"
integrity sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==
dependencies:
no-case "^3.0.4"
tslib "^2.0.3"
path-exists@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz"
@ -2019,12 +2488,17 @@ path-type@^4.0.0:
resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
pathe@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/pathe/-/pathe-0.2.0.tgz#30fd7bbe0a0d91f0e60bae621f5d19e9e225c339"
integrity sha512-sTitTPYnn23esFR3RlqYBWn4c45WGeLcsKzQiUpXJAyfcWkolvlYpV8FLo7JishK946oQwMFUCHXQ9AjGPKExw==
picocolors@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz"
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.3.1:
version "2.3.1"
resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
@ -2209,6 +2683,11 @@ regenerator-runtime@^0.14.0:
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45"
integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==
relateurl@^0.2.7:
version "0.2.7"
resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
integrity sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==
resolve-from@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz"
@ -2290,6 +2769,19 @@ source-map-js@^1.0.2:
resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz"
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
source-map-support@~0.5.20:
version "0.5.21"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==
dependencies:
buffer-from "^1.0.0"
source-map "^0.6.0"
source-map@^0.6.0, source-map@~0.6.0:
version "0.6.1"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
@ -2379,6 +2871,23 @@ tailwindcss@^3.3.5:
resolve "^1.22.2"
sucrase "^3.32.0"
terser@^5.10.0:
version "5.24.0"
resolved "https://registry.yarnpkg.com/terser/-/terser-5.24.0.tgz#4ae50302977bca4831ccc7b4fef63a3c04228364"
integrity sha512-ZpGR4Hy3+wBEzVEnHvstMvqpD/nABNelQn/z2r0fjVWGQsN3bpOLzQlqDxmb4CDZnXq5lpjnQ+mHQLAOpfM5iw==
dependencies:
"@jridgewell/source-map" "^0.3.3"
acorn "^8.8.2"
commander "^2.20.0"
source-map-support "~0.5.20"
text-segmentation@^1.0.3:
version "1.0.3"
resolved "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz#52a388159efffe746b24a63ba311b6ac9f2d7943"
integrity sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==
dependencies:
utrie "^1.0.2"
text-table@^0.2.0:
version "0.2.0"
resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz"
@ -2420,6 +2929,11 @@ to-regex-range@^5.0.1:
dependencies:
is-number "^7.0.0"
toggle-selection@^1.0.6:
version "1.0.6"
resolved "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32"
integrity sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==
ts-api-utils@^1.0.1:
version "1.0.3"
resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz"
@ -2430,7 +2944,7 @@ ts-interface-checker@^0.1.9:
resolved "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz"
integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==
tslib@^2.0.0, tslib@^2.1.0, tslib@^2.4.0:
tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0:
version "2.6.2"
resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz"
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
@ -2502,6 +3016,13 @@ util-deprecate@^1.0.2:
resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
utrie@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz#d42fe44de9bc0119c25de7f564a6ed1b2c87a645"
integrity sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==
dependencies:
base64-arraybuffer "^1.0.2"
vite-plugin-checker@^0.6.2:
version "0.6.2"
resolved "https://registry.npmjs.org/vite-plugin-checker/-/vite-plugin-checker-0.6.2.tgz"
@ -2525,6 +3046,24 @@ vite-plugin-checker@^0.6.2:
vscode-languageserver-textdocument "^1.0.1"
vscode-uri "^3.0.2"
vite-plugin-html@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/vite-plugin-html/-/vite-plugin-html-3.2.0.tgz#0d4df9900642a321a139f1c25c05195ba9d0ec79"
integrity sha512-2VLCeDiHmV/BqqNn5h2V+4280KRgQzCFN47cst3WiNK848klESPQnzuC3okH5XHtgwHH/6s1Ho/YV6yIO0pgoQ==
dependencies:
"@rollup/pluginutils" "^4.2.0"
colorette "^2.0.16"
connect-history-api-fallback "^1.6.0"
consola "^2.15.3"
dotenv "^16.0.0"
dotenv-expand "^8.0.2"
ejs "^3.1.6"
fast-glob "^3.2.11"
fs-extra "^10.0.1"
html-minifier-terser "^6.1.0"
node-html-parser "^5.3.3"
pathe "^0.2.0"
vite@^4.4.5:
version "4.5.0"
resolved "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz"

42
sweep.yaml Normal file
View File

@ -0,0 +1,42 @@
# Sweep AI turns bugs & feature requests into code changes (https://sweep.dev)
# For details on our config file, check out our docs at https://docs.sweep.dev/usage/config
# This setting contains a list of rules that Sweep will check for. If any of these rules are broken in a new commit, Sweep will create an pull request to fix the broken rule.
rules:
- "All docstrings and comments should be up to date."
['All new business logic should have corresponding unit tests.', 'Refactor large functions to be more modular.', 'Add docstrings to all functions and file headers.']
# This is the branch that Sweep will develop from and make pull requests to. Most people use 'main' or 'master' but some users also use 'dev' or 'staging'.
branch: 'main'
# By default Sweep will read the logs and outputs from your existing Github Actions. To disable this, set this to false.
gha_enabled: True
# This is the description of your project. It will be used by sweep when creating PRs. You can tell Sweep what's unique about your project, what frameworks you use, or anything else you want.
#
# Example:
#
# description: sweepai/sweep is a python project. The main api endpoints are in sweepai/api.py. Write code that adheres to PEP8.
description: ''
# This sets whether to create pull requests as drafts. If this is set to True, then all pull requests will be created as drafts and GitHub Actions will not be triggered.
draft: False
# This is a list of directories that Sweep will not be able to edit.
blocked_dirs: []
# This is a list of documentation links that Sweep will use to help it understand your code. You can add links to documentation for any packages you use here.
#
# Example:
#
# docs:
# - PyGitHub: ["https://pygithub.readthedocs.io/en/latest/", "We use pygithub to interact with the GitHub API"]
docs: []
# Sandbox executes commands in a sandboxed environment to validate code changes after every edit to guarantee pristine code. For more details, see the [Sandbox](./sandbox) page.
sandbox:
install:
- trunk init
check:
- trunk fmt {file_path} || return 0
- trunk check --fix --print-failures {file_path}