diff --git a/apps/studio/.env.example b/apps/studio/.env.example index 877dfa8ed..90ee6e9ad 100644 --- a/apps/studio/.env.example +++ b/apps/studio/.env.example @@ -4,4 +4,7 @@ VITE_MIXPANEL_TOKEN= VITE_ANTHROPIC_API_KEY= VITE_OPENAI_API_KEY= VITE_LANGFUSE_SECRET_KEY= -VITE_LANGFUSE_PUBLIC_KEY= \ No newline at end of file +VITE_LANGFUSE_PUBLIC_KEY= +VITE_ZONKE_API_ENDPOINT= +VITE_ZONKE_API_KEY= +VITE_ZONKE_API_TOKEN= diff --git a/apps/studio/electron/main/events/hosting.ts b/apps/studio/electron/main/events/hosting.ts new file mode 100644 index 000000000..ae8027c1d --- /dev/null +++ b/apps/studio/electron/main/events/hosting.ts @@ -0,0 +1,30 @@ +import { MainChannels } from '@onlook/models/constants'; +import { ipcMain } from 'electron'; +import hostingManager from '../hosting'; + +export function listenForHostingMessages() { + ipcMain.handle( + MainChannels.CREATE_PROJECT_HOSTING_ENV, + (e: Electron.IpcMainInvokeEvent, args) => { + return hostingManager.createEnv(args); + }, + ); + + ipcMain.handle(MainChannels.GET_PROJECT_HOSTING_ENV, (e: Electron.IpcMainInvokeEvent, args) => { + return hostingManager.getEnv(args.envId); + }); + + ipcMain.handle(MainChannels.GET_DEPLOYMENT_STATUS, (e: Electron.IpcMainInvokeEvent, args) => { + return hostingManager.getDeploymentStatus(args.envId, args.versionId); + }); + + ipcMain.handle( + MainChannels.PUBLISH_PROJECT_HOSTING_ENV, + (e: Electron.IpcMainInvokeEvent, args) => { + const { envId, folderPath, buildScript } = args; + return hostingManager.publishEnv(envId, folderPath, buildScript); + }, + ); + + +} diff --git a/apps/studio/electron/main/events/index.ts b/apps/studio/electron/main/events/index.ts index 00a84e388..0bafa9bcd 100644 --- a/apps/studio/electron/main/events/index.ts +++ b/apps/studio/electron/main/events/index.ts @@ -10,6 +10,7 @@ import { listenForAuthMessages } from './auth'; import { listenForChatMessages } from './chat'; import { listenForCodeMessages } from './code'; import { listenForCreateMessages } from './create'; +import { listenForHostingMessages } from './hosting'; import { listenForRunMessages } from './run'; import { listenForStorageMessages } from './storage'; @@ -22,6 +23,7 @@ export function listenForIpcMessages() { listenForCreateMessages(); listenForChatMessages(); listenForRunMessages(); + listenForHostingMessages(); } function listenForGeneralMessages() { diff --git a/apps/studio/electron/main/hosting/index.ts b/apps/studio/electron/main/hosting/index.ts new file mode 100644 index 000000000..4fc8d3e65 --- /dev/null +++ b/apps/studio/electron/main/hosting/index.ts @@ -0,0 +1,187 @@ +import { MainChannels } from '@onlook/models/constants'; +import { DeployState, VersionStatus, type CreateEnvOptions, type DeploymentStatus } from '@onlook/models/hosting'; +import { PreviewEnvironmentClient, SupportedFrameworks } from '@zonke-cloud/sdk'; +import { exec } from 'node:child_process'; +import { mainWindow } from '..'; +import { PersistentStorage } from '../storage'; +const MOCK_ENV = { + endpoint: 'o95ewhbkzx.preview.zonke.market', + environmentId: '850540f8-a168-43a6-9772-6a1727d73b93', + versions: [ + { + message: 'Testing', + environmentId: '850540f8-a168-43a6-9772-6a1727d73b93', + buildOutputDirectory: '/Users/kietho/workplace/onlook/test/docs/.next' + } + ], +}; + +class HostingManager { + private static instance: HostingManager; + + private zonke: PreviewEnvironmentClient; + private userId: string | null = null; + private state: DeployState = DeployState.NONE; + + private constructor() { + this.restoreSettings(); + this.zonke = this.initZonkeClient(); + } + + initZonkeClient() { + if ( + !import.meta.env.VITE_ZONKE_API_KEY || + !import.meta.env.VITE_ZONKE_API_TOKEN || + !import.meta.env.VITE_ZONKE_API_ENDPOINT + ) { + throw new Error('Zonke API key, token, and endpoint must be set'); + } + return new PreviewEnvironmentClient({ + apiKey: import.meta.env.VITE_ZONKE_API_KEY, + apiToken: import.meta.env.VITE_ZONKE_API_TOKEN, + apiEndpoint: import.meta.env.VITE_ZONKE_API_ENDPOINT, + }); + } + + public static getInstance(): HostingManager { + if (!HostingManager.instance) { + HostingManager.instance = new HostingManager(); + } + return HostingManager.instance; + } + + private restoreSettings() { + const settings = PersistentStorage.USER_SETTINGS.read() || {}; + this.userId = settings.id || null; + } + + createEnv(options: CreateEnvOptions) { + if (this.userId === null) { + console.error('User ID not found'); + return; + } + + const framework = options.framework as SupportedFrameworks; + const awsHostedZone = 'zonke.market'; + + return this.zonke.createPreviewEnvironment({ + userId: this.userId, + framework, + awsHostedZone, + }); + } + + async getEnv(envId: string) { + try { + return await this.zonke.getPreviewEnvironment(envId); + } catch (error) { + console.error('Failed to get preview environment', error); + return null; + } + } + + async publishEnv(envId: string, folderPath: string, buildScript: string) { + console.log('Publishing environment', { + envId, + folderPath, + buildScript, + }); + + // TODO: Infer this from project + const BUILD_OUTPUT_PATH = folderPath + '/.next'; + + try { + this.setState(DeployState.BUILDING, 'Building project'); + const success = await this.runBuildScript(folderPath, buildScript); + if (!success) { + this.setState(DeployState.ERROR, 'Build failed'); + return null; + } + + this.setState(DeployState.DEPLOYING, 'Deploying to preview environment'); + const version = await this.zonke.deployToPreviewEnvironment({ + message: 'New deployment', + environmentId: envId, + buildOutputDirectory: BUILD_OUTPUT_PATH, + }); + + this.pollDeploymentStatus(envId, version.versionId); + return version; + + } catch (error) { + console.error('Failed to deploy to preview environment', error); + this.setState(DeployState.ERROR, 'Deployment failed'); + return null; + } + } + + pollDeploymentStatus(envId: string, versionId: string) { + const interval = 3000; + const timeout = 300000; + const startTime = Date.now(); + + const intervalId = setInterval(async () => { + try { + const status = await this.getDeploymentStatus(envId, versionId); + + if (status.status === VersionStatus.SUCCEEDED) { + clearInterval(intervalId); + const env = await this.getEnv(envId); + this.setState(DeployState.DEPLOYED, 'Deployment successful', env?.endpoint); + } else if (status.status === VersionStatus.FAILED) { + clearInterval(intervalId); + this.setState(DeployState.ERROR, 'Deployment failed'); + } else if (Date.now() - startTime > timeout) { + clearInterval(intervalId); + this.setState(DeployState.ERROR, 'Deployment timed out'); + } + } catch (error) { + clearInterval(intervalId); + this.setState(DeployState.ERROR, 'Failed to check deployment status'); + } + }, interval); + + setTimeout(() => { + clearInterval(intervalId); + }, timeout); + } + + async getDeploymentStatus(envId: string, versionId: string) { + return await this.zonke.getDeploymentStatus({ + environmentId: envId, + sourceVersion: versionId, + }); + } + + runBuildScript(folderPath: string, buildScript: string): Promise { + this.setState(DeployState.BUILDING, 'Building project'); + + return new Promise((resolve, reject) => { + exec(buildScript, { cwd: folderPath, env: { ...process.env, NODE_ENV: 'production' } }, (error: Error | null, stdout: string, stderr: string) => { + if (error) { + console.error(`Build script error: ${error}`); + resolve(false); + return; + } + + if (stderr) { + console.warn(`Build script stderr: ${stderr}`); + } + + console.log(`Build script output: ${stdout}`); + resolve(true); + }); + }); + } + + setState(state: DeployState, message?: string, endpoint?: string) { + this.state = state; + mainWindow?.webContents.send(MainChannels.DEPLOY_STATE_CHANGED, { state, message, endpoint }); + } + + getState(): DeploymentStatus { + return { state: this.state }; + } +} + +export default HostingManager.getInstance(); diff --git a/apps/studio/src/lib/projects/hosting.ts b/apps/studio/src/lib/projects/hosting.ts new file mode 100644 index 000000000..f63711a79 --- /dev/null +++ b/apps/studio/src/lib/projects/hosting.ts @@ -0,0 +1,95 @@ +import { MainChannels } from '@onlook/models/constants'; +import { DeployState } from '@onlook/models/hosting'; +import type { Project } from '@onlook/models/projects'; +import type { PreviewEnvironment } from '@zonke-cloud/sdk'; +import { makeAutoObservable } from 'mobx'; +import { invokeMainChannel } from '../utils'; + +export class HostingManager { + private project: Project; + env: PreviewEnvironment | null = null; + state: DeployState = DeployState.NONE; + message: string = ''; + + constructor(project: Project) { + makeAutoObservable(this); + this.project = project; + this.restoreState(); + this.listenForStateChanges(); + + } + + async listenForStateChanges() { + const res = await this.getDeploymentStatus('850540f8-a168-43a6-9772-6a1727d73b93', 'eYu9codOymFSFLt6e634lu073BkaWSQo'); + console.log(res); + + window.api.on(MainChannels.DEPLOY_STATE_CHANGED, async (args) => { + const { state, message } = args as { state: DeployState; message: string }; + this.state = state; + this.message = message; + }); + } + + async restoreState() { + this.env = await this.getEnv(); + } + + async create() { + const res: PreviewEnvironment | null = await invokeMainChannel( + MainChannels.CREATE_PROJECT_HOSTING_ENV, + { + userId: 'testUserId', + framework: 'nextjs', + }, + ); + if (!res) { + console.error('Failed to create hosting environment'); + return; + } + this.env = res; + } + + async getEnv() { + const res = await invokeMainChannel(MainChannels.GET_PROJECT_HOSTING_ENV, { + envId: '850540f8-a168-43a6-9772-6a1727d73b93', + }); + return res as PreviewEnvironment | null; + } + + async publish() { + const folderPath = this.project.folderPath; + const buildScript: string = this.project.commands?.build || 'npm run build'; + const envId = this.env?.environmentId; + + if (!folderPath || !buildScript || !envId) { + console.error('Missing required data for publishing'); + return; + } + + const res = await invokeMainChannel(MainChannels.PUBLISH_PROJECT_HOSTING_ENV, { + folderPath, + buildScript, + envId, + }); + + if (!res) { + console.error('Failed to publish hosting environment'); + } + } + + get isDeploying() { + return [DeployState.BUILDING, DeployState.DEPLOYING].includes(this.state); + } + + async restart() { } + + async dispose() { } + + async getDeploymentStatus(envId: string, versionId: string) { + const res = await invokeMainChannel(MainChannels.GET_DEPLOYMENT_STATUS, { + envId, + versionId, + }); + return res; + } +} diff --git a/apps/studio/src/lib/projects/index.ts b/apps/studio/src/lib/projects/index.ts index 9d5288341..3bffa0a2c 100644 --- a/apps/studio/src/lib/projects/index.ts +++ b/apps/studio/src/lib/projects/index.ts @@ -4,11 +4,14 @@ import type { AppState, ProjectsCache } from '@onlook/models/settings'; import { makeAutoObservable } from 'mobx'; import { nanoid } from 'nanoid/non-secure'; import { invokeMainChannel, sendAnalytics } from '../utils'; +import { HostingManager } from './hosting'; import { RunManager } from './run'; export class ProjectsManager { private activeProject: Project | null = null; private activeRunManager: RunManager | null = null; + private activeHostingManager: HostingManager | null = null; + private projectList: Project[] = []; constructor() { @@ -32,7 +35,15 @@ export class ProjectsManager { } } - createProject(name: string, url: string, folderPath: string, runCommand: string): Project { + createProject( + name: string, + url: string, + folderPath: string, + commands: { + run: string; + build: string; + }, + ): Project { const newProject: Project = { id: nanoid(), name, @@ -40,7 +51,7 @@ export class ProjectsManager { folderPath, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), - runCommand, + commands, }; const updatedProjects = [...this.projectList, newProject]; @@ -82,20 +93,35 @@ export class ProjectsManager { return this.activeRunManager; } + get hosting(): HostingManager | null { + return this.activeHostingManager; + } + set project(newProject: Project | null) { if (!newProject || newProject.id !== this.activeProject?.id) { - this.activeRunManager?.dispose(); - this.activeRunManager = null; + this.disposeManagers(); } if (newProject) { - this.activeRunManager = new RunManager(newProject); + this.setManagers(newProject); } this.activeProject = newProject; this.saveActiveProject(); } + setManagers(project: Project) { + this.activeRunManager = new RunManager(project); + this.activeHostingManager = new HostingManager(project); + } + + disposeManagers() { + this.activeRunManager?.dispose(); + this.activeHostingManager?.dispose(); + this.activeRunManager = null; + this.activeHostingManager = null; + } + get projects() { return this.projectList; } diff --git a/apps/studio/src/lib/projects/run.ts b/apps/studio/src/lib/projects/run.ts index 94515cf24..203933a66 100644 --- a/apps/studio/src/lib/projects/run.ts +++ b/apps/studio/src/lib/projects/run.ts @@ -26,7 +26,7 @@ export class RunManager { return await invokeMainChannel(MainChannels.RUN_START, { id: this.project.id, folderPath: this.project.folderPath, - command: this.project.runCommand || 'npm run dev', + command: this.project.commands.run || 'npm run dev', }); } @@ -41,7 +41,7 @@ export class RunManager { return await invokeMainChannel(MainChannels.RUN_RESTART, { id: this.project.id, folderPath: this.project.folderPath, - command: this.project.runCommand || 'npm run dev', + command: this.project.commands.run || 'npm run dev', }); } diff --git a/apps/studio/src/routes/editor/TopBar/OpenCode/index.tsx b/apps/studio/src/routes/editor/TopBar/OpenCode/index.tsx index 303d01624..5497fb3f3 100644 --- a/apps/studio/src/routes/editor/TopBar/OpenCode/index.tsx +++ b/apps/studio/src/routes/editor/TopBar/OpenCode/index.tsx @@ -105,7 +105,7 @@ const OpenCode = observer(() => { } return ( -
+
diff --git a/apps/studio/src/routes/editor/TopBar/ShareProject/index.tsx b/apps/studio/src/routes/editor/TopBar/ShareProject/index.tsx new file mode 100644 index 000000000..5b2876936 --- /dev/null +++ b/apps/studio/src/routes/editor/TopBar/ShareProject/index.tsx @@ -0,0 +1,204 @@ +import { useProjectsManager } from '@/components/Context'; +import { Button } from '@onlook/ui/button'; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@onlook/ui/dialog'; +import { Icons } from '@onlook/ui/icons'; +import { cn } from '@onlook/ui/utils'; +import { AnimatePresence, motion } from 'framer-motion'; +import { observer } from 'mobx-react-lite'; +import { useMemo, useState } from 'react'; + +const ShareProject = observer(() => { + const projectsManager = useProjectsManager(); + const hosting = projectsManager.hosting; + const env = hosting?.env; + const endpoint = `https://${env?.endpoint}`; + + const [isOpen, setIsOpen] = useState(false); + const [isCopied, setIsCopied] = useState(false); + + const handleCopyUrl = async () => { + await navigator.clipboard.writeText(endpoint); + setIsCopied(true); + setTimeout(() => setIsCopied(false), 2000); + }; + + const copyTextCharacters = useMemo(() => { + const text = isCopied ? 'Copied!' : 'Copy link'; + return text.split('').map((ch, index) => ({ + id: `copytext_${ch}${index}`, + label: ch === ' ' ? '\u00A0' : ch, + })); + }, [isCopied]); + + const createLink = async () => { + if (!hosting) { + console.error('Hosting is not available'); + return; + } + + hosting.create(); + }; + + const publish = async () => { + if (!hosting) { + console.error('Hosting is not available'); + return; + } + + hosting.publish(); + }; + + return ( + <> + + + + + + + {env ? 'Public link' : 'Share public link'} + + + + + + + {!env ? ( + +

+ Share your app with the world and update it at any time in + Onlook. +

+ +
+ ) : ( + +

+ Your app is now public – What you see is what your users see. + You can unpublish or update it at any time here. +

+ +
+
+
+ +
+ +
+ +
+ + +
+
+
+ )} +
+
+
+ + ); +}); + +export default ShareProject; diff --git a/apps/studio/src/routes/editor/TopBar/index.tsx b/apps/studio/src/routes/editor/TopBar/index.tsx index d20c5f057..3b6a1fd99 100644 --- a/apps/studio/src/routes/editor/TopBar/index.tsx +++ b/apps/studio/src/routes/editor/TopBar/index.tsx @@ -9,6 +9,7 @@ import OpenCode from './OpenCode'; import ProjectBreadcrumb from './ProjectSelect'; import ZoomControls from './ZoomControls'; import { Hotkey } from '/common/hotkeys'; +import ShareProject from './ShareProject'; const EditorTopBar = observer( ({ @@ -93,6 +94,7 @@ const EditorTopBar = observer( handleScale={handleScale} /> +
); diff --git a/apps/studio/src/routes/projects/ProjectSettingsModal.tsx b/apps/studio/src/routes/projects/ProjectSettingsModal.tsx index b1c454885..dad16b9a1 100644 --- a/apps/studio/src/routes/projects/ProjectSettingsModal.tsx +++ b/apps/studio/src/routes/projects/ProjectSettingsModal.tsx @@ -12,7 +12,7 @@ import { import { Input } from '@onlook/ui/input'; import { Label } from '@onlook/ui/label'; import { observer } from 'mobx-react-lite'; -import { useState, useCallback } from 'react'; +import { useState } from 'react'; const ProjectSettingsModal = observer( ({ @@ -31,7 +31,10 @@ const ProjectSettingsModal = observer( const [formValues, setFormValues] = useState({ name: projectToUpdate?.name || '', url: projectToUpdate?.url || '', - runCommand: projectToUpdate?.runCommand || 'npm run dev', + commands: projectToUpdate?.commands || { + run: 'npm run dev', + build: 'npm run build', + }, }); const [uncontrolledOpen, setUncontrolledOpen] = useState(false); @@ -92,11 +95,25 @@ const ProjectSettingsModal = observer( htmlFor="runCommand" className="text-right text-foreground-secondary" > - Command + Run +
+
+ + diff --git a/apps/studio/src/routes/projects/ProjectsTab/Create/Load/SetUrl.tsx b/apps/studio/src/routes/projects/ProjectsTab/Create/Load/SetUrl.tsx index a17acc3e0..6689db43b 100644 --- a/apps/studio/src/routes/projects/ProjectsTab/Create/Load/SetUrl.tsx +++ b/apps/studio/src/routes/projects/ProjectsTab/Create/Load/SetUrl.tsx @@ -11,7 +11,8 @@ import type { StepComponent } from '../withStepProps'; const LoadSetUrl: StepComponent = ({ props, variant }) => { const { projectData, setProjectData, prevStep, nextStep } = props; const [projectUrl, setProjectUrl] = useState(projectData.url || ''); - const [runCommand, setRunCommand] = useState(projectData.runCommand || ''); + const [runCommand, setRunCommand] = useState(projectData.commands?.run || ''); + const [buildCommand, setBuildCommand] = useState(projectData.commands?.build || ''); const [error, setError] = useState(null); function handleUrlInput(e: React.FormEvent) { @@ -32,7 +33,21 @@ const LoadSetUrl: StepComponent = ({ props, variant }) => { setRunCommand(e.currentTarget.value); setProjectData({ ...projectData, - runCommand: e.currentTarget.value, + commands: { + ...projectData.commands, + run: e.currentTarget.value, + }, + }); + } + + function handleBuildCommandInput(e: React.FormEvent) { + setBuildCommand(e.currentTarget.value); + setProjectData({ + ...projectData, + commands: { + ...projectData.commands, + build: e.currentTarget.value, + }, }); } @@ -75,6 +90,14 @@ const LoadSetUrl: StepComponent = ({ props, variant }) => { placeholder="npm run dev" onInput={handleRunCommandInput} /> + +

{error || ''}

); @@ -88,8 +111,10 @@ const LoadSetUrl: StepComponent = ({ props, variant }) => { disabled={ !projectData.url || projectData.url.length === 0 || - !projectData.runCommand || - projectData.runCommand.length === 0 + !projectData.commands?.run || + projectData.commands?.run.length === 0 || + !projectData.commands?.build || + projectData.commands?.build.length === 0 } type="button" onClick={nextStep} diff --git a/apps/studio/src/routes/projects/ProjectsTab/Create/index.tsx b/apps/studio/src/routes/projects/ProjectsTab/Create/index.tsx index 009087509..55cf24cec 100644 --- a/apps/studio/src/routes/projects/ProjectsTab/Create/index.tsx +++ b/apps/studio/src/routes/projects/ProjectsTab/Create/index.tsx @@ -32,7 +32,10 @@ const variants = { const DEFAULT_PROJECT_DATA = { url: 'http://localhost:3000', - runCommand: 'npm run dev', + commands: { + run: 'npm run dev', + build: 'npm run build', + }, hasCopied: false, }; @@ -109,7 +112,8 @@ const CreateProject = ({ !projectData.name || !projectData.url || !projectData.folderPath || - !projectData.runCommand + !projectData.commands?.run || + !projectData.commands?.build ) { throw new Error('Project data is missing.'); } @@ -118,7 +122,10 @@ const CreateProject = ({ projectData.name, projectData.url, projectData.folderPath, - projectData.runCommand, + { + run: projectData.commands.run, + build: projectData.commands.build, + }, ); projectsManager.project = newProject; diff --git a/bun.lockb b/bun.lockb index 6a4c917ac..712e3d54b 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index aeeed227b..820a9ddd3 100644 --- a/package.json +++ b/package.json @@ -44,5 +44,8 @@ }, "devDependencies": { "husky": "^9.1.6" + }, + "dependencies": { + "fast-xml-parser": "^4.5.1" } } \ No newline at end of file diff --git a/packages/models/src/constants/index.ts b/packages/models/src/constants/index.ts index b43b9c620..a592b34a5 100644 --- a/packages/models/src/constants/index.ts +++ b/packages/models/src/constants/index.ts @@ -133,6 +133,13 @@ export enum MainChannels { TERMINAL_RESIZE = 'terminal-resize', TERMINAL_KILL = 'terminal-kill', TERMINAL_GET_HISTORY = 'terminal-get-history', + + // Hosting + CREATE_PROJECT_HOSTING_ENV = 'create-project-hosting-env', + GET_PROJECT_HOSTING_ENV = 'get-project-hosting-env', + PUBLISH_PROJECT_HOSTING_ENV = 'publish-project-hosting-env', + DEPLOY_STATE_CHANGED = 'deploy-state-changed', + GET_DEPLOYMENT_STATUS = 'get-deployment-status', } export enum Links { diff --git a/packages/models/src/hosting/index.ts b/packages/models/src/hosting/index.ts new file mode 100644 index 000000000..ad1025a52 --- /dev/null +++ b/packages/models/src/hosting/index.ts @@ -0,0 +1,23 @@ +export interface CreateEnvOptions { + framework: 'nextjs' | 'remix' | 'react'; +} + +export enum DeployState { + NONE = 'none', + BUILDING = 'building', + DEPLOYING = 'deploying', + DEPLOYED = 'deployed', + ERROR = 'error', +} + +export enum VersionStatus { + IN_PROGRESS = 'IN_PROGRESS', + SUCCEEDED = 'SUCCEEDED', + FAILED = 'FAILED', +} + +export interface DeploymentStatus { + state: DeployState; + message?: string; + endpoint?: string; +} diff --git a/packages/models/src/projects/index.ts b/packages/models/src/projects/index.ts index 5693f9baa..77cb97d0e 100644 --- a/packages/models/src/projects/index.ts +++ b/packages/models/src/projects/index.ts @@ -34,7 +34,10 @@ export const ProjectSchema = z.object({ createdAt: z.string(), // ISO 8601 updatedAt: z.string(), // ISO 8601 settings: ProjectSettingsSchema.optional(), - runCommand: z.string().optional(), + commands: z.object({ + build: z.string().optional(), + run: z.string().optional(), + }), }); export type Project = z.infer; diff --git a/packages/ui/src/components/dialog.tsx b/packages/ui/src/components/dialog.tsx index 0ce7abcf2..4e8b6ecfe 100644 --- a/packages/ui/src/components/dialog.tsx +++ b/packages/ui/src/components/dialog.tsx @@ -73,7 +73,7 @@ const DialogTitle = React.forwardRef< >(({ className, ...props }, ref) => ( )); diff --git a/packages/ui/src/components/icons/index.tsx b/packages/ui/src/components/icons/index.tsx index 938608811..9083b15a3 100644 --- a/packages/ui/src/components/icons/index.tsx +++ b/packages/ui/src/components/icons/index.tsx @@ -59,6 +59,7 @@ import { FrameIcon, GearIcon, GitHubLogoIcon, + GlobeIcon, GroupIcon, HandIcon, ImageIcon, @@ -89,6 +90,7 @@ import { ScissorsIcon, SectionIcon, ShadowIcon, + Share2Icon, SizeIcon, SpaceBetweenHorizontallyIcon, SpaceBetweenVerticallyIcon, @@ -1009,6 +1011,7 @@ export const Icons = { Frame: FrameIcon, Gear: GearIcon, GitHubLogo: GitHubLogoIcon, + Globe: GlobeIcon, Group: GroupIcon, Image: ImageIcon, Input: InputIcon, @@ -1036,6 +1039,7 @@ export const Icons = { Scissors: ScissorsIcon, Section: SectionIcon, Shadow: ShadowIcon, + Share: Share2Icon, Size: SizeIcon, Sun: SunIcon, Stop: StopIcon,