mirror of
https://github.com/stackblitz/bolt.new
synced 2025-02-06 04:48:04 +00:00
fix(ui): mobile friendly
This commit is contained in:
parent
95776af641
commit
f644066189
@ -10,11 +10,7 @@ interface APIKeyManagerProps {
|
|||||||
labelForGetApiKey?: string;
|
labelForGetApiKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const APIKeyManager: React.FC<APIKeyManagerProps> = ({
|
export const APIKeyManager: React.FC<APIKeyManagerProps> = ({ provider, apiKey, setApiKey }) => {
|
||||||
provider,
|
|
||||||
apiKey,
|
|
||||||
setApiKey,
|
|
||||||
}) => {
|
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [tempKey, setTempKey] = useState(apiKey);
|
const [tempKey, setTempKey] = useState(apiKey);
|
||||||
|
|
||||||
@ -24,15 +20,29 @@ export const APIKeyManager: React.FC<APIKeyManagerProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2 mt-2 mb-2">
|
<div className="flex items-start sm:items-center mt-2 mb-2 flex-col sm:flex-row">
|
||||||
<span className="text-sm text-bolt-elements-textSecondary">{provider?.name} API Key:</span>
|
<div>
|
||||||
|
<span className="text-sm text-bolt-elements-textSecondary">{provider?.name} API Key:</span>
|
||||||
|
{!isEditing && (
|
||||||
|
<div className="flex items-center mb-4">
|
||||||
|
<span className="flex-1 text-xs text-bolt-elements-textPrimary mr-2">
|
||||||
|
{apiKey ? '••••••••' : 'Not set (will still work if set in .env file)'}
|
||||||
|
</span>
|
||||||
|
<IconButton onClick={() => setIsEditing(true)} title="Edit API Key">
|
||||||
|
<div className="i-ph:pencil-simple" />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<>
|
<div className="flex items-center gap-3 mt-2">
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={tempKey}
|
value={tempKey}
|
||||||
|
placeholder="Your API Key"
|
||||||
onChange={(e) => setTempKey(e.target.value)}
|
onChange={(e) => setTempKey(e.target.value)}
|
||||||
className="flex-1 p-1 text-sm rounded border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus"
|
className="flex-1 px-2 py-1 text-xs lg:text-sm rounded border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus"
|
||||||
/>
|
/>
|
||||||
<IconButton onClick={handleSave} title="Save API Key">
|
<IconButton onClick={handleSave} title="Save API Key">
|
||||||
<div className="i-ph:check" />
|
<div className="i-ph:check" />
|
||||||
@ -40,20 +50,15 @@ export const APIKeyManager: React.FC<APIKeyManagerProps> = ({
|
|||||||
<IconButton onClick={() => setIsEditing(false)} title="Cancel">
|
<IconButton onClick={() => setIsEditing(false)} title="Cancel">
|
||||||
<div className="i-ph:x" />
|
<div className="i-ph:x" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<span className="flex-1 text-sm text-bolt-elements-textPrimary">
|
{provider?.getApiKeyLink && (
|
||||||
{apiKey ? '••••••••' : 'Not set (will still work if set in .env file)'}
|
<IconButton className="ml-auto" onClick={() => window.open(provider?.getApiKeyLink)} title="Edit API Key">
|
||||||
</span>
|
<span className="mr-2 text-xs lg:text-sm">{provider?.labelForGetApiKey || 'Get API Key'}</span>
|
||||||
<IconButton onClick={() => setIsEditing(true)} title="Edit API Key">
|
<div className={provider?.icon || 'i-ph:key'} />
|
||||||
<div className="i-ph:pencil-simple" />
|
</IconButton>
|
||||||
</IconButton>
|
)}
|
||||||
|
|
||||||
{provider?.getApiKeyLink && <IconButton onClick={() => window.open(provider?.getApiKeyLink)} title="Edit API Key">
|
|
||||||
<span className="mr-2">{provider?.labelForGetApiKey || 'Get API Key'}</span>
|
|
||||||
<div className={provider?.icon || "i-ph:key"} />
|
|
||||||
</IconButton>}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -27,9 +27,9 @@ const EXAMPLE_PROMPTS = [
|
|||||||
|
|
||||||
const providerList = PROVIDER_LIST;
|
const providerList = PROVIDER_LIST;
|
||||||
|
|
||||||
const ModelSelector = ({ model, setModel, provider, setProvider, modelList, providerList }) => {
|
const ModelSelector = ({ model, setModel, provider, setProvider, modelList, providerList, apiKeys }) => {
|
||||||
return (
|
return (
|
||||||
<div className="mb-2 flex gap-2">
|
<div className="mb-2 flex gap-2 flex-col sm:flex-row">
|
||||||
<select
|
<select
|
||||||
value={provider?.name}
|
value={provider?.name}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@ -49,8 +49,7 @@ const ModelSelector = ({ model, setModel, provider, setProvider, modelList, prov
|
|||||||
key={provider?.name}
|
key={provider?.name}
|
||||||
value={model}
|
value={model}
|
||||||
onChange={(e) => setModel(e.target.value)}
|
onChange={(e) => setModel(e.target.value)}
|
||||||
style={{ maxWidth: '70%' }}
|
className="flex-1 p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all lg:max-w-[70%] "
|
||||||
className="flex-1 p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all"
|
|
||||||
>
|
>
|
||||||
{[...modelList]
|
{[...modelList]
|
||||||
.filter((e) => e.provider == provider?.name && e.name)
|
.filter((e) => e.provider == provider?.name && e.name)
|
||||||
@ -157,25 +156,25 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
styles.BaseChat,
|
styles.BaseChat,
|
||||||
'relative flex h-full w-full overflow-hidden bg-bolt-elements-background-depth-1',
|
'relative flex flex-col lg:flex-row h-full w-full overflow-hidden bg-bolt-elements-background-depth-1',
|
||||||
)}
|
)}
|
||||||
data-chat-visible={showChat}
|
data-chat-visible={showChat}
|
||||||
>
|
>
|
||||||
<ClientOnly>{() => <Menu />}</ClientOnly>
|
<ClientOnly>{() => <Menu />}</ClientOnly>
|
||||||
<div ref={scrollRef} className="flex overflow-y-auto w-full h-full">
|
<div ref={scrollRef} className="flex flex-col lg:flex-row overflow-y-auto w-full h-full">
|
||||||
<div className={classNames(styles.Chat, 'flex flex-col flex-grow min-w-[var(--chat-min-width)] h-full')}>
|
<div className={classNames(styles.Chat, 'flex flex-col flex-grow lg:min-w-[var(--chat-min-width)] h-full')}>
|
||||||
{!chatStarted && (
|
{!chatStarted && (
|
||||||
<div id="intro" className="mt-[26vh] max-w-chat mx-auto text-center">
|
<div id="intro" className="mt-[26vh] max-w-chat mx-auto text-center px-4 lg:px-0">
|
||||||
<h1 className="text-6xl font-bold text-bolt-elements-textPrimary mb-4 animate-fade-in">
|
<h1 className="text-3xl lg:text-6xl font-bold text-bolt-elements-textPrimary mb-4 animate-fade-in">
|
||||||
Where ideas begin
|
Where ideas begin
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-xl mb-8 text-bolt-elements-textSecondary animate-fade-in animation-delay-200">
|
<p className="text-md lg:text-xl mb-8 text-bolt-elements-textSecondary animate-fade-in animation-delay-200">
|
||||||
Bring ideas to life in seconds or get help on existing projects.
|
Bring ideas to life in seconds or get help on existing projects.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className={classNames('pt-6 px-6', {
|
className={classNames('pt-6 px-2 sm:px-6', {
|
||||||
'h-full flex flex-col': chatStarted,
|
'h-full flex flex-col': chatStarted,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
@ -184,7 +183,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
return chatStarted ? (
|
return chatStarted ? (
|
||||||
<Messages
|
<Messages
|
||||||
ref={messageRef}
|
ref={messageRef}
|
||||||
className="flex flex-col w-full flex-1 max-w-chat px-4 pb-6 mx-auto z-1"
|
className="flex flex-col w-full flex-1 max-w-chat pb-6 mx-auto z-1"
|
||||||
messages={messages}
|
messages={messages}
|
||||||
isStreaming={isStreaming}
|
isStreaming={isStreaming}
|
||||||
/>
|
/>
|
||||||
@ -193,10 +192,11 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'bg-bolt-elements-background-depth-2 border-y border-bolt-elements-borderColor relative w-full max-w-chat mx-auto z-prompt',
|
' bg-bolt-elements-background-depth-2 p-3 rounded-lg border border-bolt-elements-borderColor relative w-full max-w-chat mx-auto z-prompt mb-6',
|
||||||
{
|
{
|
||||||
'sticky bottom-0': chatStarted
|
'sticky bottom-2': chatStarted,
|
||||||
})}
|
},
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<ModelSelector
|
<ModelSelector
|
||||||
key={provider?.name + ':' + modelList.length}
|
key={provider?.name + ':' + modelList.length}
|
||||||
@ -206,7 +206,9 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
provider={provider}
|
provider={provider}
|
||||||
setProvider={setProvider}
|
setProvider={setProvider}
|
||||||
providerList={PROVIDER_LIST}
|
providerList={PROVIDER_LIST}
|
||||||
|
apiKeys={apiKeys}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{provider && (
|
{provider && (
|
||||||
<APIKeyManager
|
<APIKeyManager
|
||||||
provider={provider}
|
provider={provider}
|
||||||
@ -214,6 +216,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
setApiKey={(key) => updateApiKey(provider.name, key)}
|
setApiKey={(key) => updateApiKey(provider.name, key)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'shadow-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background backdrop-filter backdrop-blur-[8px] rounded-lg overflow-hidden transition-all',
|
'shadow-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background backdrop-filter backdrop-blur-[8px] rounded-lg overflow-hidden transition-all',
|
||||||
@ -221,7 +224,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
>
|
>
|
||||||
<textarea
|
<textarea
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
className={`w-full pl-4 pt-4 pr-16 focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus resize-none text-md text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary bg-transparent transition-all`}
|
className={`w-full pl-4 pt-4 pr-16 focus:outline-none focus:ring-0 focus:border-none focus:shadow-none resize-none text-md text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary bg-transparent transition-all`}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
if (event.shiftKey) {
|
if (event.shiftKey) {
|
||||||
@ -294,7 +297,6 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-bolt-elements-background-depth-1 pb-6">{/* Ghost Element */}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!chatStarted && (
|
{!chatStarted && (
|
||||||
|
@ -4,7 +4,7 @@ import { classNames } from '~/utils/classNames';
|
|||||||
import { AssistantMessage } from './AssistantMessage';
|
import { AssistantMessage } from './AssistantMessage';
|
||||||
import { UserMessage } from './UserMessage';
|
import { UserMessage } from './UserMessage';
|
||||||
import * as Tooltip from '@radix-ui/react-tooltip';
|
import * as Tooltip from '@radix-ui/react-tooltip';
|
||||||
import { useLocation, useNavigate } from '@remix-run/react';
|
import { useLocation } from '@remix-run/react';
|
||||||
import { db, chatId } from '~/lib/persistence/useChatHistory';
|
import { db, chatId } from '~/lib/persistence/useChatHistory';
|
||||||
import { forkChat } from '~/lib/persistence/db';
|
import { forkChat } from '~/lib/persistence/db';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
@ -19,7 +19,6 @@ interface MessagesProps {
|
|||||||
export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props: MessagesProps, ref) => {
|
export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props: MessagesProps, ref) => {
|
||||||
const { id, isStreaming = false, messages = [] } = props;
|
const { id, isStreaming = false, messages = [] } = props;
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const handleRewind = (messageId: string) => {
|
const handleRewind = (messageId: string) => {
|
||||||
const searchParams = new URLSearchParams(location.search);
|
const searchParams = new URLSearchParams(location.search);
|
||||||
@ -69,53 +68,57 @@ export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props:
|
|||||||
<div className="grid grid-col-1 w-full">
|
<div className="grid grid-col-1 w-full">
|
||||||
{isUserMessage ? <UserMessage content={content} /> : <AssistantMessage content={content} />}
|
{isUserMessage ? <UserMessage content={content} /> : <AssistantMessage content={content} />}
|
||||||
</div>
|
</div>
|
||||||
{!isUserMessage && (<div className="flex gap-2">
|
{!isUserMessage && (
|
||||||
<Tooltip.Root>
|
<div className="flex gap-2 flex-col lg:flex-row">
|
||||||
<Tooltip.Trigger asChild>
|
<Tooltip.Root>
|
||||||
{messageId && (<button
|
<Tooltip.Trigger asChild>
|
||||||
onClick={() => handleRewind(messageId)}
|
{messageId && (
|
||||||
key='i-ph:arrow-u-up-left'
|
<button
|
||||||
className={classNames(
|
onClick={() => handleRewind(messageId)}
|
||||||
'i-ph:arrow-u-up-left',
|
key="i-ph:arrow-u-up-left"
|
||||||
'text-xl text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary transition-colors'
|
className={classNames(
|
||||||
|
'i-ph:arrow-u-up-left',
|
||||||
|
'text-xl text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary transition-colors',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
/>)}
|
</Tooltip.Trigger>
|
||||||
</Tooltip.Trigger>
|
<Tooltip.Portal>
|
||||||
<Tooltip.Portal>
|
<Tooltip.Content
|
||||||
<Tooltip.Content
|
className="bg-bolt-elements-tooltip-background text-bolt-elements-textPrimary px-3 py-2 rounded-lg text-sm shadow-lg"
|
||||||
className="bg-bolt-elements-tooltip-background text-bolt-elements-textPrimary px-3 py-2 rounded-lg text-sm shadow-lg"
|
sideOffset={5}
|
||||||
sideOffset={5}
|
style={{ zIndex: 1000 }}
|
||||||
style={{zIndex: 1000}}
|
>
|
||||||
>
|
Revert to this message
|
||||||
Revert to this message
|
<Tooltip.Arrow className="fill-bolt-elements-tooltip-background" />
|
||||||
<Tooltip.Arrow className="fill-bolt-elements-tooltip-background" />
|
</Tooltip.Content>
|
||||||
</Tooltip.Content>
|
</Tooltip.Portal>
|
||||||
</Tooltip.Portal>
|
</Tooltip.Root>
|
||||||
</Tooltip.Root>
|
|
||||||
|
|
||||||
<Tooltip.Root>
|
<Tooltip.Root>
|
||||||
<Tooltip.Trigger asChild>
|
<Tooltip.Trigger asChild>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleFork(messageId)}
|
onClick={() => handleFork(messageId)}
|
||||||
key='i-ph:git-fork'
|
key="i-ph:git-fork"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'i-ph:git-fork',
|
'i-ph:git-fork',
|
||||||
'text-xl text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary transition-colors'
|
'text-xl text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary transition-colors',
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Tooltip.Trigger>
|
</Tooltip.Trigger>
|
||||||
<Tooltip.Portal>
|
<Tooltip.Portal>
|
||||||
<Tooltip.Content
|
<Tooltip.Content
|
||||||
className="bg-bolt-elements-tooltip-background text-bolt-elements-textPrimary px-3 py-2 rounded-lg text-sm shadow-lg"
|
className="bg-bolt-elements-tooltip-background text-bolt-elements-textPrimary px-3 py-2 rounded-lg text-sm shadow-lg"
|
||||||
sideOffset={5}
|
sideOffset={5}
|
||||||
style={{zIndex: 1000}}
|
style={{ zIndex: 1000 }}
|
||||||
>
|
>
|
||||||
Fork chat from this message
|
Fork chat from this message
|
||||||
<Tooltip.Arrow className="fill-bolt-elements-tooltip-background" />
|
<Tooltip.Arrow className="fill-bolt-elements-tooltip-background" />
|
||||||
</Tooltip.Content>
|
</Tooltip.Content>
|
||||||
</Tooltip.Portal>
|
</Tooltip.Portal>
|
||||||
</Tooltip.Root>
|
</Tooltip.Root>
|
||||||
</div>)}
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
|
import useViewport from '~/lib/hooks';
|
||||||
import { chatStore } from '~/lib/stores/chat';
|
import { chatStore } from '~/lib/stores/chat';
|
||||||
import { workbenchStore } from '~/lib/stores/workbench';
|
import { workbenchStore } from '~/lib/stores/workbench';
|
||||||
import { classNames } from '~/utils/classNames';
|
import { classNames } from '~/utils/classNames';
|
||||||
@ -9,6 +10,8 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
|
|||||||
const showWorkbench = useStore(workbenchStore.showWorkbench);
|
const showWorkbench = useStore(workbenchStore.showWorkbench);
|
||||||
const { showChat } = useStore(chatStore);
|
const { showChat } = useStore(chatStore);
|
||||||
|
|
||||||
|
const isSmallViewport = useViewport(1024);
|
||||||
|
|
||||||
const canHideChat = showWorkbench || !showChat;
|
const canHideChat = showWorkbench || !showChat;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -16,7 +19,7 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
|
|||||||
<div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden">
|
<div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden">
|
||||||
<Button
|
<Button
|
||||||
active={showChat}
|
active={showChat}
|
||||||
disabled={!canHideChat}
|
disabled={!canHideChat || isSmallViewport} // expand button is disabled on mobile as it's needed
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (canHideChat) {
|
if (canHideChat) {
|
||||||
chatStore.setKey('showChat', !showChat);
|
chatStore.setKey('showChat', !showChat);
|
||||||
|
@ -16,6 +16,7 @@ import { cubicEasingFn } from '~/utils/easings';
|
|||||||
import { renderLogger } from '~/utils/logger';
|
import { renderLogger } from '~/utils/logger';
|
||||||
import { EditorPanel } from './EditorPanel';
|
import { EditorPanel } from './EditorPanel';
|
||||||
import { Preview } from './Preview';
|
import { Preview } from './Preview';
|
||||||
|
import useViewport from '~/lib/hooks';
|
||||||
|
|
||||||
interface WorkspaceProps {
|
interface WorkspaceProps {
|
||||||
chatStarted?: boolean;
|
chatStarted?: boolean;
|
||||||
@ -65,6 +66,8 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
|
|||||||
const files = useStore(workbenchStore.files);
|
const files = useStore(workbenchStore.files);
|
||||||
const selectedView = useStore(workbenchStore.currentView);
|
const selectedView = useStore(workbenchStore.currentView);
|
||||||
|
|
||||||
|
const isSmallViewport = useViewport(1024);
|
||||||
|
|
||||||
const setSelectedView = (view: WorkbenchViewType) => {
|
const setSelectedView = (view: WorkbenchViewType) => {
|
||||||
workbenchStore.currentView.set(view);
|
workbenchStore.currentView.set(view);
|
||||||
};
|
};
|
||||||
@ -128,12 +131,14 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
|
|||||||
className={classNames(
|
className={classNames(
|
||||||
'fixed top-[calc(var(--header-height)+1.5rem)] bottom-6 w-[var(--workbench-inner-width)] mr-4 z-0 transition-[left,width] duration-200 bolt-ease-cubic-bezier',
|
'fixed top-[calc(var(--header-height)+1.5rem)] bottom-6 w-[var(--workbench-inner-width)] mr-4 z-0 transition-[left,width] duration-200 bolt-ease-cubic-bezier',
|
||||||
{
|
{
|
||||||
|
'w-full': isSmallViewport,
|
||||||
|
'left-0': showWorkbench && isSmallViewport,
|
||||||
'left-[var(--workbench-left)]': showWorkbench,
|
'left-[var(--workbench-left)]': showWorkbench,
|
||||||
'left-[100%]': !showWorkbench,
|
'left-[100%]': !showWorkbench,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="absolute inset-0 px-6">
|
<div className="absolute inset-0 px-2 lg:px-6">
|
||||||
<div className="h-full flex flex-col bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor shadow-sm rounded-lg overflow-hidden">
|
<div className="h-full flex flex-col bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor shadow-sm rounded-lg overflow-hidden">
|
||||||
<div className="flex items-center px-3 py-2 border-b border-bolt-elements-borderColor">
|
<div className="flex items-center px-3 py-2 border-b border-bolt-elements-borderColor">
|
||||||
<Slider selected={selectedView} options={sliderOptions} setSelected={setSelectedView} />
|
<Slider selected={selectedView} options={sliderOptions} setSelected={setSelectedView} />
|
||||||
|
@ -2,3 +2,4 @@ export * from './useMessageParser';
|
|||||||
export * from './usePromptEnhancer';
|
export * from './usePromptEnhancer';
|
||||||
export * from './useShortcuts';
|
export * from './useShortcuts';
|
||||||
export * from './useSnapScroll';
|
export * from './useSnapScroll';
|
||||||
|
export { default } from './useViewport';
|
||||||
|
18
app/lib/hooks/useViewport.ts
Normal file
18
app/lib/hooks/useViewport.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
const useViewport = (threshold = 1024) => {
|
||||||
|
const [isSmallViewport, setIsSmallViewport] = useState(window.innerWidth < threshold);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleResize = () => setIsSmallViewport(window.innerWidth < threshold);
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', handleResize);
|
||||||
|
};
|
||||||
|
}, [threshold]);
|
||||||
|
|
||||||
|
return isSmallViewport;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useViewport;
|
Loading…
Reference in New Issue
Block a user