Environment colors (#225)

This commit is contained in:
Gregory Schier
2025-06-07 18:21:54 -07:00
committed by GitHub
parent 27901231dc
commit d0fde99b1c
19 changed files with 182 additions and 25 deletions

View File

@@ -4,11 +4,12 @@ import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
import { useEnvironmentsBreakdown } from '../hooks/useEnvironmentsBreakdown';
import { toggleDialog } from '../lib/dialog';
import { setWorkspaceSearchParams } from '../lib/setWorkspaceSearchParams';
import type { ButtonProps } from './core/Button';
import { Button } from './core/Button';
import type { ButtonProps } from './core/Button';
import type { DropdownItem } from './core/Dropdown';
import { Dropdown } from './core/Dropdown';
import { Icon } from './core/Icon';
import { EnvironmentColorCircle } from './EnvironmentColorCircle';
import { EnvironmentEditDialog } from './EnvironmentEditDialog';
type Props = {
@@ -38,6 +39,7 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
(e) => ({
key: e.id,
label: e.name,
rightSlot: <EnvironmentColorCircle environment={e} />,
leftSlot: e.id === activeEnvironment?.id ? <Icon icon="check" /> : <Icon icon="empty" />,
onSelect: async () => {
if (e.id !== activeEnvironment?.id) {
@@ -80,6 +82,7 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
onClick={subEnvironments.length === 0 ? showEnvironmentDialog : undefined}
{...buttonProps}
>
<EnvironmentColorCircle environment={activeEnvironment ?? null} />
{activeEnvironment?.name ?? (hasBaseVars ? 'Environment' : 'No Environment')}
</Button>
</Dropdown>

View File

@@ -0,0 +1,29 @@
import type { Environment } from '@yaakapp-internal/models';
import classNames from 'classnames';
import { showColorPicker } from '../lib/showColorPicker';
export function EnvironmentColorCircle({
environment,
clickToEdit,
}: {
environment: Environment | null;
clickToEdit?: boolean;
}) {
if (environment?.color == null) return null;
const style = { backgroundColor: environment.color };
const className =
'inline-block w-[0.75em] h-[0.75em] rounded-full mr-1.5 border border-transparent';
if (clickToEdit) {
return (
<button
onClick={() => showColorPicker(environment)}
style={style}
className={classNames(className, 'hover:border-text')}
/>
);
} else {
return <span style={style} className={className} />;
}
}

View File

@@ -0,0 +1,26 @@
import { useState } from 'react';
import { Button } from './core/Button';
import { ColorPicker } from './core/ColorPicker';
export function EnvironmentColorPicker({
color: defaultColor,
onChange,
}: {
color: string | null;
onChange: (color: string | null) => void;
}) {
const [color, setColor] = useState<string | null>(defaultColor);
return (
<div className="flex flex-col items-stretch gap-3 pb-2 w-full">
<ColorPicker color={color} onChange={setColor} />
<div className="grid grid-cols-[1fr_1fr] gap-1.5">
<Button variant="border" color="secondary" onClick={() => onChange(null)}>
Clear
</Button>
<Button color="primary" onClick={() => onChange(color)}>
Save
</Button>
</div>
</div>
);
}

View File

@@ -17,6 +17,7 @@ import {
setupOrConfigureEncryption,
withEncryptionEnabled,
} from '../lib/setupOrConfigureEncryption';
import { showColorPicker } from '../lib/showColorPicker';
import { BadgeButton } from './core/BadgeButton';
import { Banner } from './core/Banner';
import { Button } from './core/Button';
@@ -35,6 +36,7 @@ import { PairOrBulkEditor } from './core/PairOrBulkEditor';
import { Separator } from './core/Separator';
import { SplitLayout } from './core/SplitLayout';
import { VStack } from './core/Stacks';
import { EnvironmentColorCircle } from './EnvironmentColorCircle';
interface Props {
initialEnvironment: Environment | null;
@@ -123,7 +125,7 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
))}
{subEnvironments.length > 0 && (
<div className="px-2">
<Separator className="my-3"></Separator>
<Separator className="my-3" />
</div>
)}
{subEnvironments.map((e) => (
@@ -237,6 +239,7 @@ const EnvironmentEditor = function ({
return (
<VStack space={4} className={classNames(className, 'pl-4')}>
<Heading className="w-full flex items-center gap-0.5">
<EnvironmentColorCircle clickToEdit environment={selectedEnvironment ?? null} />
<div className="mr-2">{selectedEnvironment?.name}</div>
{isEncryptionEnabled ? (
promptToEncrypt ? (
@@ -344,6 +347,7 @@ function SidebarButton({
onContextMenu={handleContextMenu}
rightSlot={rightSlot}
>
<EnvironmentColorCircle environment={environment} />
{children}
</Button>
{outerRightSlot}
@@ -385,6 +389,12 @@ function SidebarButton({
},
]
: []) as DropdownItem[]),
{
label: 'Set Color',
leftSlot: <Icon icon="palette" />,
hidden: environment.base,
onSelect: async () => showColorPicker(environment),
},
{
label: `Make ${environment.public ? 'Private' : 'Sharable'}`,
leftSlot: <Icon icon={environment.public ? 'eye_closed' : 'eye'} />,

View File

@@ -8,7 +8,10 @@ import {
useEnsureActiveCookieJar,
useSubscribeActiveCookieJarId,
} from '../hooks/useActiveCookieJar';
import { useSubscribeActiveEnvironmentId } from '../hooks/useActiveEnvironment';
import {
activeEnvironmentAtom,
useSubscribeActiveEnvironmentId,
} from '../hooks/useActiveEnvironment';
import { activeRequestAtom } from '../hooks/useActiveRequest';
import { useSubscribeActiveRequestId } from '../hooks/useActiveRequestId';
import { activeWorkspaceAtom } from '../hooks/useActiveWorkspace';
@@ -32,6 +35,7 @@ import { HotKeyList } from './core/HotKeyList';
import { FeedbackLink } from './core/Link';
import { HStack } from './core/Stacks';
import { CreateDropdown } from './CreateDropdown';
import { ErrorBoundary } from './ErrorBoundary';
import { GrpcConnectionLayout } from './GrpcConnectionLayout';
import { HeaderSize } from './HeaderSize';
import { HttpRequestLayout } from './HttpRequestLayout';
@@ -41,7 +45,6 @@ import { Sidebar } from './sidebar/Sidebar';
import { SidebarActions } from './sidebar/SidebarActions';
import { WebsocketRequestLayout } from './WebsocketRequestLayout';
import { WorkspaceHeader } from './WorkspaceHeader';
import { ErrorBoundary } from './ErrorBoundary';
const side = { gridArea: 'side' };
const head = { gridArea: 'head' };
@@ -56,6 +59,7 @@ export function Workspace() {
const { setWidth, width, resetWidth } = useSidebarWidth();
const [sidebarHidden, setSidebarHidden] = useSidebarHidden();
const [floatingSidebarHidden, setFloatingSidebarHidden] = useFloatingSidebarHidden();
const activeEnvironment = useAtomValue(activeEnvironmentAtom);
const floating = useShouldFloatSidebar();
const [isResizing, setIsResizing] = useState<boolean>(false);
const moveState = useRef<{ move: (e: MouseEvent) => void; up: (e: MouseEvent) => void } | null>(
@@ -117,6 +121,12 @@ export function Workspace() {
[sideWidth, floating],
);
const environmentBg = useMemo(() => {
if (activeEnvironment?.color == null) return undefined;
const background = `linear-gradient(to right, ${activeEnvironment.color} 15%, transparent 40%)`;
return { background };
}, [activeEnvironment?.color ?? 'n/a']);
// We're loading still
if (workspaces.length === 0) {
return null;
@@ -175,9 +185,19 @@ export function Workspace() {
<HeaderSize
data-tauri-drag-region
size="lg"
className="x-theme-appHeader bg-surface"
className="relative x-theme-appHeader bg-surface"
style={head}
>
<div className="absolute inset-0 pointer-events-none">
<div // Add subtle background
style={environmentBg}
className="absolute inset-0 opacity-5"
/>
<div // Add subtle border bottom
style={environmentBg}
className="absolute left-0 right-0 bottom-0 h-[0.5px] opacity-20"
/>
</div>
<WorkspaceHeader className="pointer-events-none" />
</HeaderSize>
<ErrorBoundary name="Workspace Body">

View File

@@ -0,0 +1,40 @@
import { useState } from 'react';
import { HexColorPicker } from 'react-colorful';
import { useRandomKey } from '../../hooks/useRandomKey';
import { PlainInput } from './PlainInput';
interface Props {
onChange: (value: string | null) => void;
color: string | null;
}
export function ColorPicker({ onChange, color: defaultColor }: Props) {
const [updateKey, regenerateKey] = useRandomKey();
const [color, setColor] = useState<string | null>(defaultColor);
return (
<form
className="flex flex-col gap-3 items-stretch w-full"
onSubmit={(e) => {
e.preventDefault();
onChange(color);
}}
>
<HexColorPicker
color={color ?? undefined}
className="!w-full"
onChange={(color) => {
setColor(color.toUpperCase());
regenerateKey();
}}
/>
<PlainInput
hideLabel
label="Plain Color"
forceUpdateKey={updateKey}
defaultValue={color ?? ''}
onChange={(c) => setColor(c.toUpperCase())}
validate={(color) => color.match(/#[0-9a-fA-F]{6}$/) !== null}
/>
</form>
);
}

View File

@@ -26,9 +26,9 @@ const icons = {
cake: lucide.CakeIcon,
chat: lucide.MessageSquare,
check: lucide.CheckIcon,
check_circle: lucide.CheckCircleIcon,
check_square_checked: lucide.SquareCheckIcon,
check_square_unchecked: lucide.SquareIcon,
check_circle: lucide.CheckCircleIcon,
chevron_down: lucide.ChevronDownIcon,
chevron_right: lucide.ChevronRightIcon,
circle_alert: lucide.CircleAlertIcon,
@@ -78,6 +78,7 @@ const icons = {
minus_circle: lucide.MinusCircleIcon,
moon: lucide.MoonIcon,
more_vertical: lucide.MoreVerticalIcon,
palette: lucide.PaletteIcon,
paste: lucide.ClipboardPasteIcon,
pencil: lucide.PencilIcon,
pin: lucide.PinIcon,