mirror of
				https://github.com/stackblitz-labs/bolt.diy
				synced 2025-06-26 18:26:38 +00:00 
			
		
		
		
	feat: refactor layout and introduce workspace panel and fix some bugs
This commit is contained in:
		
							parent
							
								
									5fa2ee53cc
								
							
						
					
					
						commit
						ab9d59a30d
					
				@ -2,7 +2,7 @@ import { IconButton } from './ui/IconButton';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export function Header() {
 | 
					export function Header() {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <header className="flex items-center bg-white p-4 border-b border-gray-200">
 | 
					    <header className="flex items-center bg-white p-4 border-b border-gray-200 h-[var(--header-height)]">
 | 
				
			||||||
      <div className="flex items-center gap-2">
 | 
					      <div className="flex items-center gap-2">
 | 
				
			||||||
        <div className="text-2xl font-semibold text-accent">Bolt</div>
 | 
					        <div className="text-2xl font-semibold text-accent">Bolt</div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
				
			|||||||
@ -3,17 +3,22 @@ import { workspaceStore } from '~/lib/stores/workspace';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
interface ArtifactProps {
 | 
					interface ArtifactProps {
 | 
				
			||||||
  messageId: string;
 | 
					  messageId: string;
 | 
				
			||||||
  onClick?: () => void;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function Artifact({ messageId, onClick }: ArtifactProps) {
 | 
					export function Artifact({ messageId }: ArtifactProps) {
 | 
				
			||||||
  const artifacts = useStore(workspaceStore.artifacts);
 | 
					  const artifacts = useStore(workspaceStore.artifacts);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const artifact = artifacts[messageId];
 | 
					  const artifact = artifacts[messageId];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <button className="flex border rounded-lg overflow-hidden items-stretch bg-gray-50/25 w-full" onClick={onClick}>
 | 
					    <button
 | 
				
			||||||
      <div className="border-r flex items-center px-6 bg-gray-50">
 | 
					      className="flex border rounded-lg overflow-hidden items-stretch bg-gray-50/25 w-full"
 | 
				
			||||||
 | 
					      onClick={() => {
 | 
				
			||||||
 | 
					        const showWorkspace = workspaceStore.showWorkspace.get();
 | 
				
			||||||
 | 
					        workspaceStore.showWorkspace.set(!showWorkspace);
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <div className="border-r flex items-center px-6 bg-gray-100/50">
 | 
				
			||||||
        {!artifact?.closed ? (
 | 
					        {!artifact?.closed ? (
 | 
				
			||||||
          <div className="i-svg-spinners:90-ring-with-bg scale-130"></div>
 | 
					          <div className="i-svg-spinners:90-ring-with-bg scale-130"></div>
 | 
				
			||||||
        ) : (
 | 
					        ) : (
 | 
				
			||||||
 | 
				
			|||||||
@ -1,8 +1,11 @@
 | 
				
			|||||||
 | 
					import type { Message } from 'ai';
 | 
				
			||||||
import type { LegacyRef } from 'react';
 | 
					import type { LegacyRef } from 'react';
 | 
				
			||||||
import React from 'react';
 | 
					import React from 'react';
 | 
				
			||||||
import { ClientOnly } from 'remix-utils/client-only';
 | 
					import { ClientOnly } from 'remix-utils/client-only';
 | 
				
			||||||
import { IconButton } from '~/components/ui/IconButton';
 | 
					import { IconButton } from '~/components/ui/IconButton';
 | 
				
			||||||
 | 
					import { Workspace } from '~/components/workspace/Workspace.client';
 | 
				
			||||||
import { classNames } from '~/utils/classNames';
 | 
					import { classNames } from '~/utils/classNames';
 | 
				
			||||||
 | 
					import { Messages } from './Messages.client';
 | 
				
			||||||
import { SendButton } from './SendButton.client';
 | 
					import { SendButton } from './SendButton.client';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface BaseChatProps {
 | 
					interface BaseChatProps {
 | 
				
			||||||
@ -10,6 +13,8 @@ interface BaseChatProps {
 | 
				
			|||||||
  messagesSlot?: React.ReactNode;
 | 
					  messagesSlot?: React.ReactNode;
 | 
				
			||||||
  workspaceSlot?: React.ReactNode;
 | 
					  workspaceSlot?: React.ReactNode;
 | 
				
			||||||
  chatStarted?: boolean;
 | 
					  chatStarted?: boolean;
 | 
				
			||||||
 | 
					  isStreaming?: boolean;
 | 
				
			||||||
 | 
					  messages?: Message[];
 | 
				
			||||||
  enhancingPrompt?: boolean;
 | 
					  enhancingPrompt?: boolean;
 | 
				
			||||||
  promptEnhanced?: boolean;
 | 
					  promptEnhanced?: boolean;
 | 
				
			||||||
  input?: string;
 | 
					  input?: string;
 | 
				
			||||||
@ -27,10 +32,10 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
 | 
				
			|||||||
    {
 | 
					    {
 | 
				
			||||||
      textareaRef,
 | 
					      textareaRef,
 | 
				
			||||||
      chatStarted = false,
 | 
					      chatStarted = false,
 | 
				
			||||||
 | 
					      isStreaming = false,
 | 
				
			||||||
      enhancingPrompt = false,
 | 
					      enhancingPrompt = false,
 | 
				
			||||||
      promptEnhanced = false,
 | 
					      promptEnhanced = false,
 | 
				
			||||||
      messagesSlot,
 | 
					      messages,
 | 
				
			||||||
      workspaceSlot,
 | 
					 | 
				
			||||||
      input = '',
 | 
					      input = '',
 | 
				
			||||||
      sendMessage,
 | 
					      sendMessage,
 | 
				
			||||||
      handleInputChange,
 | 
					      handleInputChange,
 | 
				
			||||||
@ -41,14 +46,14 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
 | 
				
			|||||||
    const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
 | 
					    const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <div ref={ref} className="h-full flex w-full overflow-scroll px-6">
 | 
					      <div ref={ref} className="relative flex h-full w-full overflow-hidden ">
 | 
				
			||||||
        <div className="flex flex-col items-center w-full h-full">
 | 
					        <div className="flex overflow-scroll w-full h-full">
 | 
				
			||||||
          <div id="chat" className="w-full">
 | 
					          <div id="chat" className="flex flex-col w-full h-full px-6">
 | 
				
			||||||
            {!chatStarted && (
 | 
					            {!chatStarted && (
 | 
				
			||||||
              <div id="intro" className="mt-[20vh] mb-14 max-w-2xl mx-auto">
 | 
					              <div id="intro" className="mt-[20vh] mb-14 max-w-3xl mx-auto">
 | 
				
			||||||
                <h2 className="text-4xl text-center font-bold text-slate-800 mb-2">Where ideas begin.</h2>
 | 
					                <h2 className="text-4xl text-center font-bold text-slate-800 mb-2">Where ideas begin.</h2>
 | 
				
			||||||
                <p className="mb-14 text-center">Bring ideas to life in seconds or get help on existing projects.</p>
 | 
					                <p className="mb-14 text-center">Bring ideas to life in seconds or get help on existing projects.</p>
 | 
				
			||||||
                <div className="grid max-md:grid-cols-[repeat(2,1fr)] md:grid-cols-[repeat(2,minmax(200px,1fr))] gap-4">
 | 
					                <div className="grid max-md:grid-cols-[repeat(1,1fr)] md:grid-cols-[repeat(2,minmax(300px,1fr))] gap-4">
 | 
				
			||||||
                  {EXAMPLES.map((suggestion, index) => (
 | 
					                  {EXAMPLES.map((suggestion, index) => (
 | 
				
			||||||
                    <button key={index} className="p-4 rounded-lg shadow-xs bg-white border border-gray-200 text-left">
 | 
					                    <button key={index} className="p-4 rounded-lg shadow-xs bg-white border border-gray-200 text-left">
 | 
				
			||||||
                      {suggestion.text}
 | 
					                      {suggestion.text}
 | 
				
			||||||
@ -57,83 +62,95 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
 | 
				
			|||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
            )}
 | 
					            )}
 | 
				
			||||||
            {messagesSlot}
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
          <div
 | 
					 | 
				
			||||||
            className={classNames('w-full md:max-w-[720px] mx-auto', {
 | 
					 | 
				
			||||||
              'fixed bg-bolt-elements-app-backgroundColor bottom-0': chatStarted,
 | 
					 | 
				
			||||||
            })}
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <div
 | 
					            <div
 | 
				
			||||||
              className={classNames(
 | 
					              className={classNames('pt-10', {
 | 
				
			||||||
                'relative shadow-sm border border-gray-200 md:mb-6 bg-white rounded-lg overflow-hidden',
 | 
					                'h-full flex flex-col': chatStarted,
 | 
				
			||||||
                {
 | 
					              })}
 | 
				
			||||||
                  'max-md:rounded-none max-md:border-x-none': chatStarted,
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
              )}
 | 
					 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              <textarea
 | 
					              <ClientOnly>
 | 
				
			||||||
                ref={textareaRef}
 | 
					                {() => {
 | 
				
			||||||
                onKeyDown={(event) => {
 | 
					                  return chatStarted ? (
 | 
				
			||||||
                  if (event.key === 'Enter') {
 | 
					                    <Messages
 | 
				
			||||||
                    if (event.shiftKey) {
 | 
					                      className="flex flex-col w-full flex-1 max-w-3xl px-4 pb-10 mx-auto z-1"
 | 
				
			||||||
                      return;
 | 
					                      messages={messages}
 | 
				
			||||||
                    }
 | 
					                      isStreaming={isStreaming}
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
 | 
					                  ) : null;
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					              </ClientOnly>
 | 
				
			||||||
 | 
					              <div
 | 
				
			||||||
 | 
					                className={classNames('relative w-full max-w-3xl md:mx-auto z-2', {
 | 
				
			||||||
 | 
					                  'sticky bottom-0 bg-bolt-elements-app-backgroundColor': chatStarted,
 | 
				
			||||||
 | 
					                })}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <div
 | 
				
			||||||
 | 
					                  className={classNames('shadow-sm mb-6 border border-gray-200 bg-white rounded-lg overflow-hidden')}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <textarea
 | 
				
			||||||
 | 
					                    ref={textareaRef}
 | 
				
			||||||
 | 
					                    onKeyDown={(event) => {
 | 
				
			||||||
 | 
					                      if (event.key === 'Enter') {
 | 
				
			||||||
 | 
					                        if (event.shiftKey) {
 | 
				
			||||||
 | 
					                          return;
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    event.preventDefault();
 | 
					                        event.preventDefault();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    sendMessage?.();
 | 
					                        sendMessage?.();
 | 
				
			||||||
                  }
 | 
					                      }
 | 
				
			||||||
                }}
 | 
					                    }}
 | 
				
			||||||
                value={input}
 | 
					                    value={input}
 | 
				
			||||||
                onChange={(event) => {
 | 
					                    onChange={(event) => {
 | 
				
			||||||
                  handleInputChange?.(event);
 | 
					                      handleInputChange?.(event);
 | 
				
			||||||
                }}
 | 
					                    }}
 | 
				
			||||||
                className={`w-full pl-4 pt-4 pr-16 focus:outline-none resize-none`}
 | 
					                    className={`w-full pl-4 pt-4 pr-16 focus:outline-none resize-none`}
 | 
				
			||||||
                style={{
 | 
					                    style={{
 | 
				
			||||||
                  minHeight: TEXTAREA_MIN_HEIGHT,
 | 
					                      minHeight: TEXTAREA_MIN_HEIGHT,
 | 
				
			||||||
                  maxHeight: TEXTAREA_MAX_HEIGHT,
 | 
					                      maxHeight: TEXTAREA_MAX_HEIGHT,
 | 
				
			||||||
                }}
 | 
					                    }}
 | 
				
			||||||
                placeholder="How can Bolt help you today?"
 | 
					                    placeholder="How can Bolt help you today?"
 | 
				
			||||||
                translate="no"
 | 
					                    translate="no"
 | 
				
			||||||
              />
 | 
					                  />
 | 
				
			||||||
              <ClientOnly>{() => <SendButton show={input.length > 0} onClick={sendMessage} />}</ClientOnly>
 | 
					                  <ClientOnly>{() => <SendButton show={input.length > 0} onClick={sendMessage} />}</ClientOnly>
 | 
				
			||||||
              <div className="flex justify-between text-sm p-4 pt-2">
 | 
					                  <div className="flex justify-between text-sm p-4 pt-2">
 | 
				
			||||||
                <div className="flex gap-1 items-center">
 | 
					                    <div className="flex gap-1 items-center">
 | 
				
			||||||
                  <IconButton icon="i-ph:microphone-duotone" className="-ml-1" />
 | 
					                      <IconButton icon="i-ph:microphone-duotone" className="-ml-1" />
 | 
				
			||||||
                  <IconButton icon="i-ph:plus-circle-duotone" />
 | 
					                      <IconButton icon="i-ph:plus-circle-duotone" />
 | 
				
			||||||
                  <IconButton
 | 
					                      <IconButton icon="i-ph:pencil-simple-duotone" />
 | 
				
			||||||
                    disabled={input.length === 0 || enhancingPrompt}
 | 
					                      <IconButton
 | 
				
			||||||
                    className={classNames({
 | 
					                        disabled={input.length === 0 || enhancingPrompt}
 | 
				
			||||||
                      'opacity-100!': enhancingPrompt,
 | 
					                        className={classNames({
 | 
				
			||||||
                      'text-accent! pr-1.5 enabled:hover:bg-accent/12!': promptEnhanced,
 | 
					                          'opacity-100!': enhancingPrompt,
 | 
				
			||||||
                    })}
 | 
					                          'text-accent! pr-1.5 enabled:hover:bg-accent/12!': promptEnhanced,
 | 
				
			||||||
                    onClick={() => enhancePrompt?.()}
 | 
					                        })}
 | 
				
			||||||
                  >
 | 
					                        onClick={() => enhancePrompt?.()}
 | 
				
			||||||
                    {enhancingPrompt ? (
 | 
					                      >
 | 
				
			||||||
                      <>
 | 
					                        {enhancingPrompt ? (
 | 
				
			||||||
                        <div className="i-svg-spinners:90-ring-with-bg text-black text-xl"></div>
 | 
					                          <>
 | 
				
			||||||
                        <div className="ml-1.5">Enhancing prompt...</div>
 | 
					                            <div className="i-svg-spinners:90-ring-with-bg text-black text-xl"></div>
 | 
				
			||||||
                      </>
 | 
					                            <div className="ml-1.5">Enhancing prompt...</div>
 | 
				
			||||||
                    ) : (
 | 
					                          </>
 | 
				
			||||||
                      <>
 | 
					                        ) : (
 | 
				
			||||||
                        <div className="i-blitz:stars text-xl"></div>
 | 
					                          <>
 | 
				
			||||||
                        {promptEnhanced && <div className="ml-1.5">Prompt enhanced</div>}
 | 
					                            <div className="i-blitz:stars text-xl"></div>
 | 
				
			||||||
                      </>
 | 
					                            {promptEnhanced && <div className="ml-1.5">Prompt enhanced</div>}
 | 
				
			||||||
                    )}
 | 
					                          </>
 | 
				
			||||||
                  </IconButton>
 | 
					                        )}
 | 
				
			||||||
                </div>
 | 
					                      </IconButton>
 | 
				
			||||||
                {input.length > 3 ? (
 | 
					                    </div>
 | 
				
			||||||
                  <div className="text-xs">
 | 
					                    {input.length > 3 ? (
 | 
				
			||||||
                    Use <kbd className="bg-gray-100 p-1 rounded-md">Shift</kbd> +{' '}
 | 
					                      <div className="text-xs">
 | 
				
			||||||
                    <kbd className="bg-gray-100 p-1 rounded-md">Return</kbd> for a new line
 | 
					                        Use <kbd className="bg-gray-100 p-1 rounded-md">Shift</kbd> +{' '}
 | 
				
			||||||
 | 
					                        <kbd className="bg-gray-100 p-1 rounded-md">Return</kbd> for a new line
 | 
				
			||||||
 | 
					                      </div>
 | 
				
			||||||
 | 
					                    ) : null}
 | 
				
			||||||
                  </div>
 | 
					                  </div>
 | 
				
			||||||
                ) : null}
 | 
					                </div>
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
 | 
					          <ClientOnly>{() => <Workspace chatStarted={chatStarted} />}</ClientOnly>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        {workspaceSlot}
 | 
					 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
				
			|||||||
@ -1,13 +1,12 @@
 | 
				
			|||||||
import { useChat } from 'ai/react';
 | 
					import { useChat } from 'ai/react';
 | 
				
			||||||
import { cubicBezier, useAnimate } from 'framer-motion';
 | 
					import { useAnimate } from 'framer-motion';
 | 
				
			||||||
import { useEffect, useRef, useState } from 'react';
 | 
					import { useEffect, useRef, useState } from 'react';
 | 
				
			||||||
import { useMessageParser, usePromptEnhancer } from '~/lib/hooks';
 | 
					import { useMessageParser, usePromptEnhancer } from '~/lib/hooks';
 | 
				
			||||||
 | 
					import { cubicEasingFn } from '~/utils/easings';
 | 
				
			||||||
import { createScopedLogger } from '~/utils/logger';
 | 
					import { createScopedLogger } from '~/utils/logger';
 | 
				
			||||||
import { BaseChat } from './BaseChat';
 | 
					import { BaseChat } from './BaseChat';
 | 
				
			||||||
import { Messages } from './Messages';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const logger = createScopedLogger('Chat');
 | 
					const logger = createScopedLogger('Chat');
 | 
				
			||||||
const customEasingFn = cubicBezier(0.4, 0, 0.2, 1);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function Chat() {
 | 
					export function Chat() {
 | 
				
			||||||
  const textareaRef = useRef<HTMLTextAreaElement>(null);
 | 
					  const textareaRef = useRef<HTMLTextAreaElement>(null);
 | 
				
			||||||
@ -61,10 +60,7 @@ export function Chat() {
 | 
				
			|||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await Promise.all([
 | 
					    await animate('#intro', { opacity: 0, flex: 1 }, { duration: 0.2, ease: cubicEasingFn });
 | 
				
			||||||
      animate('#chat', { height: '100%' }, { duration: 0.3, ease: customEasingFn }),
 | 
					 | 
				
			||||||
      animate('#intro', { opacity: 0, display: 'none' }, { duration: 0.15, ease: customEasingFn }),
 | 
					 | 
				
			||||||
    ]);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    setChatStarted(true);
 | 
					    setChatStarted(true);
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
@ -87,31 +83,21 @@ export function Chat() {
 | 
				
			|||||||
      textareaRef={textareaRef}
 | 
					      textareaRef={textareaRef}
 | 
				
			||||||
      input={input}
 | 
					      input={input}
 | 
				
			||||||
      chatStarted={chatStarted}
 | 
					      chatStarted={chatStarted}
 | 
				
			||||||
 | 
					      isStreaming={isLoading}
 | 
				
			||||||
      enhancingPrompt={enhancingPrompt}
 | 
					      enhancingPrompt={enhancingPrompt}
 | 
				
			||||||
      promptEnhanced={promptEnhanced}
 | 
					      promptEnhanced={promptEnhanced}
 | 
				
			||||||
      sendMessage={sendMessage}
 | 
					      sendMessage={sendMessage}
 | 
				
			||||||
      handleInputChange={handleInputChange}
 | 
					      handleInputChange={handleInputChange}
 | 
				
			||||||
      messagesSlot={
 | 
					      messages={messages.map((message, i) => {
 | 
				
			||||||
        chatStarted ? (
 | 
					        if (message.role === 'user') {
 | 
				
			||||||
          <Messages
 | 
					          return message;
 | 
				
			||||||
            classNames={{
 | 
					        }
 | 
				
			||||||
              root: 'h-full pt-10',
 | 
					 | 
				
			||||||
              messagesContainer: 'max-w-2xl mx-auto max-md:pb-[calc(140px+1.5rem)] md:pb-[calc(140px+3rem)]',
 | 
					 | 
				
			||||||
            }}
 | 
					 | 
				
			||||||
            messages={messages.map((message, i) => {
 | 
					 | 
				
			||||||
              if (message.role === 'user') {
 | 
					 | 
				
			||||||
                return message;
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
              return {
 | 
					        return {
 | 
				
			||||||
                ...message,
 | 
					          ...message,
 | 
				
			||||||
                content: parsedMessages[i] || '',
 | 
					          content: parsedMessages[i] || '',
 | 
				
			||||||
              };
 | 
					        };
 | 
				
			||||||
            })}
 | 
					      })}
 | 
				
			||||||
            isLoading={isLoading}
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
        ) : null
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      enhancePrompt={() => {
 | 
					      enhancePrompt={() => {
 | 
				
			||||||
        enhancePrompt(input, (input) => {
 | 
					        enhancePrompt(input, (input) => {
 | 
				
			||||||
          setInput(input);
 | 
					          setInput(input);
 | 
				
			||||||
 | 
				
			|||||||
@ -64,7 +64,7 @@ export const CodeBlock = memo(({ code, language, theme }: CodeBlockProps) => {
 | 
				
			|||||||
      >
 | 
					      >
 | 
				
			||||||
        <button
 | 
					        <button
 | 
				
			||||||
          className={classNames(
 | 
					          className={classNames(
 | 
				
			||||||
            'flex items-center p-[6px] justify-center before:bg-white before:rounded-l-md before:text-gray-500 before:border-r before:border-gray-300',
 | 
					            'flex items-center bg-transparent p-[6px] justify-center before:bg-white before:rounded-l-md before:text-gray-500 before:border-r before:border-gray-300',
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
              'before:opacity-0': !copied,
 | 
					              'before:opacity-0': !copied,
 | 
				
			||||||
              'before:opacity-100': copied,
 | 
					              'before:opacity-100': copied,
 | 
				
			||||||
 | 
				
			|||||||
@ -95,7 +95,7 @@ $color-blockquote-border: #dfe2e5;
 | 
				
			|||||||
  :is(ul, ol) {
 | 
					  :is(ul, ol) {
 | 
				
			||||||
    padding-left: 2em;
 | 
					    padding-left: 2em;
 | 
				
			||||||
    margin-top: 0;
 | 
					    margin-top: 0;
 | 
				
			||||||
    margin-bottom: 16px;
 | 
					    margin-bottom: 24px;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ul {
 | 
					  ul {
 | 
				
			||||||
@ -106,6 +106,14 @@ $color-blockquote-border: #dfe2e5;
 | 
				
			|||||||
    list-style-type: decimal;
 | 
					    list-style-type: decimal;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  li + li {
 | 
				
			||||||
 | 
					    margin-top: 8px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  li > *:not(:last-child) {
 | 
				
			||||||
 | 
					    margin-bottom: 16px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  img {
 | 
					  img {
 | 
				
			||||||
    max-width: 100%;
 | 
					    max-width: 100%;
 | 
				
			||||||
    box-sizing: border-box;
 | 
					    box-sizing: border-box;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,5 @@
 | 
				
			|||||||
import { memo } from 'react';
 | 
					import { memo, useMemo } from 'react';
 | 
				
			||||||
import ReactMarkdown from 'react-markdown';
 | 
					import ReactMarkdown, { type Components } from 'react-markdown';
 | 
				
			||||||
import type { BundledLanguage } from 'shiki';
 | 
					import type { BundledLanguage } from 'shiki';
 | 
				
			||||||
import { createScopedLogger } from '~/utils/logger';
 | 
					import { createScopedLogger } from '~/utils/logger';
 | 
				
			||||||
import { rehypePlugins, remarkPlugins } from '~/utils/markdown';
 | 
					import { rehypePlugins, remarkPlugins } from '~/utils/markdown';
 | 
				
			||||||
@ -16,47 +16,51 @@ interface MarkdownProps {
 | 
				
			|||||||
export const Markdown = memo(({ children }: MarkdownProps) => {
 | 
					export const Markdown = memo(({ children }: MarkdownProps) => {
 | 
				
			||||||
  logger.trace('Render');
 | 
					  logger.trace('Render');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const components = useMemo<Components>(() => {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      div: ({ className, children, node, ...props }) => {
 | 
				
			||||||
 | 
					        if (className?.includes('__boltArtifact__')) {
 | 
				
			||||||
 | 
					          const messageId = node?.properties.dataMessageId as string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (!messageId) {
 | 
				
			||||||
 | 
					            logger.warn(`Invalud message id ${messageId}`);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          return <Artifact messageId={messageId} />;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					          <div className={className} {...props}>
 | 
				
			||||||
 | 
					            {children}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      pre: (props) => {
 | 
				
			||||||
 | 
					        const { children, node, ...rest } = props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const [firstChild] = node?.children ?? [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (
 | 
				
			||||||
 | 
					          firstChild &&
 | 
				
			||||||
 | 
					          firstChild.type === 'element' &&
 | 
				
			||||||
 | 
					          firstChild.tagName === 'code' &&
 | 
				
			||||||
 | 
					          firstChild.children[0].type === 'text'
 | 
				
			||||||
 | 
					        ) {
 | 
				
			||||||
 | 
					          const { className, ...rest } = firstChild.properties;
 | 
				
			||||||
 | 
					          const [, language = 'plaintext'] = /language-(\w+)/.exec(String(className) || '') ?? [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          return <CodeBlock code={firstChild.children[0].value} language={language as BundledLanguage} {...rest} />;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return <pre {...rest}>{children}</pre>;
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <ReactMarkdown
 | 
					    <ReactMarkdown
 | 
				
			||||||
      className={styles.MarkdownContent}
 | 
					      className={styles.MarkdownContent}
 | 
				
			||||||
      components={{
 | 
					      components={components}
 | 
				
			||||||
        div: ({ className, children, node, ...props }) => {
 | 
					 | 
				
			||||||
          if (className?.includes('__boltArtifact__')) {
 | 
					 | 
				
			||||||
            const messageId = node?.properties.dataMessageId as string;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (!messageId) {
 | 
					 | 
				
			||||||
              logger.warn(`Invalud message id ${messageId}`);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return <Artifact messageId={messageId} />;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          return (
 | 
					 | 
				
			||||||
            <div className={className} {...props}>
 | 
					 | 
				
			||||||
              {children}
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        pre: (props) => {
 | 
					 | 
				
			||||||
          const { children, node, ...rest } = props;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          const [firstChild] = node?.children ?? [];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          if (
 | 
					 | 
				
			||||||
            firstChild &&
 | 
					 | 
				
			||||||
            firstChild.type === 'element' &&
 | 
					 | 
				
			||||||
            firstChild.tagName === 'code' &&
 | 
					 | 
				
			||||||
            firstChild.children[0].type === 'text'
 | 
					 | 
				
			||||||
          ) {
 | 
					 | 
				
			||||||
            const { className, ...rest } = firstChild.properties;
 | 
					 | 
				
			||||||
            const [, language = 'plaintext'] = /language-(\w+)/.exec(String(className) || '') ?? [];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return <CodeBlock code={firstChild.children[0].value} language={language as BundledLanguage} {...rest} />;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          return <pre {...rest}>{children}</pre>;
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
      }}
 | 
					 | 
				
			||||||
      remarkPlugins={remarkPlugins}
 | 
					      remarkPlugins={remarkPlugins}
 | 
				
			||||||
      rehypePlugins={rehypePlugins}
 | 
					      rehypePlugins={rehypePlugins}
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										62
									
								
								packages/bolt/app/components/chat/Messages.client.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								packages/bolt/app/components/chat/Messages.client.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,62 @@
 | 
				
			|||||||
 | 
					import type { Message } from 'ai';
 | 
				
			||||||
 | 
					import { classNames } from '~/utils/classNames';
 | 
				
			||||||
 | 
					import { AssistantMessage } from './AssistantMessage';
 | 
				
			||||||
 | 
					import { UserMessage } from './UserMessage';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface MessagesProps {
 | 
				
			||||||
 | 
					  id?: string;
 | 
				
			||||||
 | 
					  className?: string;
 | 
				
			||||||
 | 
					  isStreaming?: boolean;
 | 
				
			||||||
 | 
					  messages?: Message[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function Messages(props: MessagesProps) {
 | 
				
			||||||
 | 
					  const { id, isStreaming = false, messages = [] } = props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div id={id} className={props.className}>
 | 
				
			||||||
 | 
					      {messages.length > 0
 | 
				
			||||||
 | 
					        ? messages.map((message, i) => {
 | 
				
			||||||
 | 
					            const { role, content } = message;
 | 
				
			||||||
 | 
					            const isUser = role === 'user';
 | 
				
			||||||
 | 
					            const isFirst = i === 0;
 | 
				
			||||||
 | 
					            const isLast = i === messages.length - 1;
 | 
				
			||||||
 | 
					            const isUserMessage = message.role === 'user';
 | 
				
			||||||
 | 
					            const isAssistantMessage = message.role === 'assistant';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return (
 | 
				
			||||||
 | 
					              <div
 | 
				
			||||||
 | 
					                key={message.id}
 | 
				
			||||||
 | 
					                className={classNames('relative overflow-hidden rounded-md p-[1px]', {
 | 
				
			||||||
 | 
					                  'mt-4': !isFirst,
 | 
				
			||||||
 | 
					                  'bg-gray-200': isUserMessage || !isStreaming || (isStreaming && isAssistantMessage && !isLast),
 | 
				
			||||||
 | 
					                  'bg-gradient-to-b from-gray-200 to-transparent': isStreaming && isAssistantMessage && isLast,
 | 
				
			||||||
 | 
					                })}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <div
 | 
				
			||||||
 | 
					                  className={classNames('flex gap-4 p-6 w-full rounded-[calc(0.375rem-1px)]', {
 | 
				
			||||||
 | 
					                    'bg-white': isUserMessage || !isStreaming || (isStreaming && !isLast),
 | 
				
			||||||
 | 
					                    'bg-gradient-to-b from-white from-30% to-transparent': isStreaming && isLast,
 | 
				
			||||||
 | 
					                  })}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <div
 | 
				
			||||||
 | 
					                    className={classNames(
 | 
				
			||||||
 | 
					                      'flex items-center justify-center min-w-[34px] min-h-[34px] text-gray-600 rounded-md p-1 self-start',
 | 
				
			||||||
 | 
					                      {
 | 
				
			||||||
 | 
					                        'bg-gray-100': isUserMessage,
 | 
				
			||||||
 | 
					                        'bg-accent text-xl': isAssistantMessage,
 | 
				
			||||||
 | 
					                      },
 | 
				
			||||||
 | 
					                    )}
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    <div className={isUserMessage ? 'i-ph:user-fill text-xl' : 'i-blitz:logo'}></div>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                  {isUser ? <UserMessage content={content} /> : <AssistantMessage content={content} />}
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
 | 
					        : null}
 | 
				
			||||||
 | 
					      {isStreaming && <div className="text-center w-full i-svg-spinners:3-dots-fade text-4xl mt-4"></div>}
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,55 +0,0 @@
 | 
				
			|||||||
import type { Message } from 'ai';
 | 
					 | 
				
			||||||
import { useRef } from 'react';
 | 
					 | 
				
			||||||
import { classNames } from '~/utils/classNames';
 | 
					 | 
				
			||||||
import { AssistantMessage } from './AssistantMessage';
 | 
					 | 
				
			||||||
import { UserMessage } from './UserMessage';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
interface MessagesProps {
 | 
					 | 
				
			||||||
  id?: string;
 | 
					 | 
				
			||||||
  classNames?: { root?: string; messagesContainer?: string };
 | 
					 | 
				
			||||||
  isLoading?: boolean;
 | 
					 | 
				
			||||||
  messages?: Message[];
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function Messages(props: MessagesProps) {
 | 
					 | 
				
			||||||
  const { id, isLoading, messages = [] } = props;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const containerRef = useRef<HTMLDivElement>(null);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <div id={id} ref={containerRef} className={props.classNames?.root}>
 | 
					 | 
				
			||||||
      <div className={classNames('flex flex-col', props.classNames?.messagesContainer)}>
 | 
					 | 
				
			||||||
        {messages.length > 0
 | 
					 | 
				
			||||||
          ? messages.map((message, i) => {
 | 
					 | 
				
			||||||
              const { role, content } = message;
 | 
					 | 
				
			||||||
              const isUser = role === 'user';
 | 
					 | 
				
			||||||
              const isFirst = i === 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              return (
 | 
					 | 
				
			||||||
                <div
 | 
					 | 
				
			||||||
                  key={message.id}
 | 
					 | 
				
			||||||
                  className={classNames('flex gap-4 border rounded-md p-6 bg-white/80 backdrop-blur-sm', {
 | 
					 | 
				
			||||||
                    'mt-4': !isFirst,
 | 
					 | 
				
			||||||
                  })}
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  <div
 | 
					 | 
				
			||||||
                    className={classNames(
 | 
					 | 
				
			||||||
                      'flex items-center justify-center min-w-[34px] min-h-[34px] text-gray-600 rounded-md p-1 self-start',
 | 
					 | 
				
			||||||
                      {
 | 
					 | 
				
			||||||
                        'bg-gray-100': role === 'user',
 | 
					 | 
				
			||||||
                        'bg-accent text-xl': role === 'assistant',
 | 
					 | 
				
			||||||
                      },
 | 
					 | 
				
			||||||
                    )}
 | 
					 | 
				
			||||||
                  >
 | 
					 | 
				
			||||||
                    <div className={role === 'user' ? 'i-ph:user-fill text-xl' : 'i-blitz:logo'}></div>
 | 
					 | 
				
			||||||
                  </div>
 | 
					 | 
				
			||||||
                  {isUser ? <UserMessage content={content} /> : <AssistantMessage content={content} />}
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
              );
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
          : null}
 | 
					 | 
				
			||||||
        {isLoading && <div className="text-center w-full i-svg-spinners:3-dots-fade text-4xl mt-4"></div>}
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,7 +1,7 @@
 | 
				
			|||||||
import { memo } from 'react';
 | 
					import { memo } from 'react';
 | 
				
			||||||
import { classNames } from '~/utils/classNames';
 | 
					import { classNames } from '~/utils/classNames';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type IconSize = 'sm' | 'md' | 'xl';
 | 
					type IconSize = 'sm' | 'md' | 'xl' | 'xxl';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface BaseIconButtonProps {
 | 
					interface BaseIconButtonProps {
 | 
				
			||||||
  size?: IconSize;
 | 
					  size?: IconSize;
 | 
				
			||||||
@ -64,7 +64,9 @@ function getIconSize(size: IconSize) {
 | 
				
			|||||||
    return 'text-sm';
 | 
					    return 'text-sm';
 | 
				
			||||||
  } else if (size === 'md') {
 | 
					  } else if (size === 'md') {
 | 
				
			||||||
    return 'text-md';
 | 
					    return 'text-md';
 | 
				
			||||||
  } else {
 | 
					  } else if (size === 'xl') {
 | 
				
			||||||
    return 'text-xl';
 | 
					    return 'text-xl';
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    return 'text-2xl';
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										55
									
								
								packages/bolt/app/components/workspace/Workspace.client.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								packages/bolt/app/components/workspace/Workspace.client.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,55 @@
 | 
				
			|||||||
 | 
					import { useStore } from '@nanostores/react';
 | 
				
			||||||
 | 
					import { AnimatePresence, motion, type Variants } from 'framer-motion';
 | 
				
			||||||
 | 
					import { IconButton } from '~/components/ui/IconButton';
 | 
				
			||||||
 | 
					import { cubicEasingFn } from '~/utils/easings';
 | 
				
			||||||
 | 
					import { workspaceStore } from '../../lib/stores/workspace';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface WorkspaceProps {
 | 
				
			||||||
 | 
					  chatStarted?: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const workspaceVariants = {
 | 
				
			||||||
 | 
					  closed: {
 | 
				
			||||||
 | 
					    width: 0,
 | 
				
			||||||
 | 
					    transition: {
 | 
				
			||||||
 | 
					      duration: 0.2,
 | 
				
			||||||
 | 
					      ease: cubicEasingFn,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  open: {
 | 
				
			||||||
 | 
					    width: '100%',
 | 
				
			||||||
 | 
					    transition: {
 | 
				
			||||||
 | 
					      duration: 0.5,
 | 
				
			||||||
 | 
					      type: 'spring',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					} satisfies Variants;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function Workspace({ chatStarted }: WorkspaceProps) {
 | 
				
			||||||
 | 
					  const showWorkspace = useStore(workspaceStore.showWorkspace);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    chatStarted && (
 | 
				
			||||||
 | 
					      <AnimatePresence>
 | 
				
			||||||
 | 
					        {showWorkspace && (
 | 
				
			||||||
 | 
					          <motion.div initial="closed" animate="open" exit="closed" variants={workspaceVariants}>
 | 
				
			||||||
 | 
					            <div className="fixed top-[calc(var(--header-height)+1.5rem)] bottom-6 w-[50vw] mr-4 z-0">
 | 
				
			||||||
 | 
					              <div className="bg-white border border-gray-200 shadow-sm rounded-lg overflow-hidden absolute inset-0 right-8">
 | 
				
			||||||
 | 
					                <header className="px-3 py-2 border-b border-gray-200">
 | 
				
			||||||
 | 
					                  <IconButton
 | 
				
			||||||
 | 
					                    icon="i-ph:x-circle"
 | 
				
			||||||
 | 
					                    className="ml-auto"
 | 
				
			||||||
 | 
					                    size="xxl"
 | 
				
			||||||
 | 
					                    onClick={() => {
 | 
				
			||||||
 | 
					                      workspaceStore.showWorkspace.set(false);
 | 
				
			||||||
 | 
					                    }}
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                </header>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </motion.div>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					      </AnimatePresence>
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,3 +0,0 @@
 | 
				
			|||||||
export function Workspace() {
 | 
					 | 
				
			||||||
  return <div>WORKSPACE PANEL</div>;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -19,7 +19,7 @@ export async function action({ context, request }: ActionFunctionArgs) {
 | 
				
			|||||||
        {
 | 
					        {
 | 
				
			||||||
          role: 'user',
 | 
					          role: 'user',
 | 
				
			||||||
          content: stripIndents`
 | 
					          content: stripIndents`
 | 
				
			||||||
            I want you to improve the following prompt.
 | 
					            I want you to improve the user prompt that is wrapped in \`<original_prompt>\` tags.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            IMPORTANT: Only respond with the improved prompt and nothing else!
 | 
					            IMPORTANT: Only respond with the improved prompt and nothing else!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -14,7 +14,7 @@ body {
 | 
				
			|||||||
    mask: linear-gradient(-25deg, transparent 60%, white);
 | 
					    mask: linear-gradient(-25deg, transparent 60%, white);
 | 
				
			||||||
    pointer-events: none;
 | 
					    pointer-events: none;
 | 
				
			||||||
    position: fixed;
 | 
					    position: fixed;
 | 
				
			||||||
    top: 0;
 | 
					    top: -8px;
 | 
				
			||||||
    transform-style: flat;
 | 
					    transform-style: flat;
 | 
				
			||||||
    width: 100vw;
 | 
					    width: 100vw;
 | 
				
			||||||
    z-index: -1;
 | 
					    z-index: -1;
 | 
				
			||||||
 | 
				
			|||||||
@ -16,6 +16,8 @@
 | 
				
			|||||||
 * Hierarchy: Element Token -> (Element Token | Color Tokens) -> Primitives
 | 
					 * Hierarchy: Element Token -> (Element Token | Color Tokens) -> Primitives
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
:root {
 | 
					:root {
 | 
				
			||||||
 | 
					  --header-height: 65px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /* App */
 | 
					  /* App */
 | 
				
			||||||
  --bolt-elements-app-backgroundColor: var(--bolt-background-primary);
 | 
					  --bolt-elements-app-backgroundColor: var(--bolt-background-primary);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										3
									
								
								packages/bolt/app/utils/easings.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								packages/bolt/app/utils/easings.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					import { cubicBezier } from 'framer-motion';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const cubicEasingFn = cubicBezier(0.4, 0, 0.2, 1);
 | 
				
			||||||
		Loading…
	
		Reference in New Issue
	
	Block a user