Tweak light theme, high contrast themes, and fix env null reference

This commit is contained in:
Gregory Schier
2025-09-22 08:36:40 -07:00
parent c6666b9623
commit 7951f3a7bd
8 changed files with 112 additions and 52 deletions

View File

@@ -2,6 +2,49 @@ import type { PluginDefinition } from '@yaakapp/api';
export const plugin: PluginDefinition = { export const plugin: PluginDefinition = {
themes: [ themes: [
{
id: 'high-contrast',
label: 'High Contrast Light',
dark: false,
base: {
surface: 'white',
surfaceHighlight: 'hsl(218,24%,93%)',
text: 'black',
textSubtle: 'hsl(217,24%,40%)',
textSubtlest: 'hsl(217,24%,40%)',
border: 'hsl(217,22%,50%)',
borderSubtle: 'hsl(217,22%,60%)',
primary: 'hsl(267,67%,47%)',
secondary: 'hsl(218,18%,53%)',
info: 'hsl(206,100%,36%)',
success: 'hsl(155,100%,26%)',
notice: 'hsl(45,100%,31%)',
warning: 'hsl(30,99%,34%)',
danger: 'hsl(334,100%,35%)',
},
},
{
id: 'high-contrast-dark',
label: 'High Contrast Dark',
dark: true,
base: {
surface: 'hsl(0,0%,0%)',
surfaceHighlight: 'hsl(0,0%,20%)',
text: 'hsl(0,0%,100%)',
textSubtle: 'hsl(0,0%,90%)',
textSubtlest: 'hsl(0,0%,80%)',
selection: 'hsl(276,100%,30%)',
surfaceActive: 'hsl(276,100%,30%)',
border: 'hsl(0,0%,60%)',
primary: 'hsl(266,100%,85%)',
secondary: 'hsl(242,20%,72%)',
info: 'hsl(208,100%,83%)',
success: 'hsl(150,100%,63%)',
notice: 'hsl(49,100%,77%)',
warning: 'hsl(28,100%,73%)',
danger: 'hsl(343,100%,79%)',
},
},
{ {
id: 'catppuccin-frappe', id: 'catppuccin-frappe',
label: 'Catppuccin Frappé', label: 'Catppuccin Frappé',

View File

@@ -51,6 +51,7 @@ impl<'a> DbContext<'a> {
&Environment { &Environment {
workspace_id: workspace_id.to_string(), workspace_id: workspace_id.to_string(),
name: "Global Variables".to_string(), name: "Global Variables".to_string(),
parent_model: "workspace".to_string(),
..Default::default() ..Default::default()
}, },
&UpdateSource::Background, &UpdateSource::Background,

View File

@@ -24,7 +24,7 @@ import { EnvironmentColorIndicator } from './EnvironmentColorIndicator';
import { EnvironmentSharableTooltip } from './EnvironmentSharableTooltip'; import { EnvironmentSharableTooltip } from './EnvironmentSharableTooltip';
export function EnvironmentEditor({ export function EnvironmentEditor({
environment: selectedEnvironment, environment,
hideName, hideName,
className, className,
}: { }: {
@@ -32,7 +32,7 @@ export function EnvironmentEditor({
hideName?: boolean; hideName?: boolean;
className?: string; className?: string;
}) { }) {
const workspaceId = selectedEnvironment.workspaceId; const workspaceId = environment.workspaceId;
const isEncryptionEnabled = useIsEncryptionEnabled(); const isEncryptionEnabled = useIsEncryptionEnabled();
const valueVisibility = useKeyValue<boolean>({ const valueVisibility = useKeyValue<boolean>({
namespace: 'global', namespace: 'global',
@@ -41,15 +41,15 @@ export function EnvironmentEditor({
}); });
const { allEnvironments } = useEnvironmentsBreakdown(); const { allEnvironments } = useEnvironmentsBreakdown();
const handleChange = useCallback( const handleChange = useCallback(
(variables: PairWithId[]) => patchModel(selectedEnvironment, { variables }), (variables: PairWithId[]) => patchModel(environment, { variables }),
[selectedEnvironment], [environment],
); );
const [forceUpdateKey, regenerateForceUpdateKey] = useRandomKey(); const [forceUpdateKey, regenerateForceUpdateKey] = useRandomKey();
// Gather a list of env names from other environments to help the user get them aligned // Gather a list of env names from other environments to help the user get them aligned
const nameAutocomplete = useMemo<GenericCompletionConfig>(() => { const nameAutocomplete = useMemo<GenericCompletionConfig>(() => {
const options: GenericCompletionOption[] = []; const options: GenericCompletionOption[] = [];
if (isBaseEnvironment(selectedEnvironment)) { if (isBaseEnvironment(environment)) {
return { options }; return { options };
} }
@@ -59,8 +59,10 @@ export function EnvironmentEditor({
const containingEnvs = allEnvironments.filter((e) => const containingEnvs = allEnvironments.filter((e) =>
e.variables.some((v) => v.name === name), e.variables.some((v) => v.name === name),
); );
const isAlreadyInActive = containingEnvs.find((e) => e.id === selectedEnvironment.id); const isAlreadyInActive = containingEnvs.find((e) => e.id === environment.id);
if (isAlreadyInActive) continue; if (isAlreadyInActive) {
continue;
}
options.push({ options.push({
label: name, label: name,
type: 'constant', type: 'constant',
@@ -68,7 +70,7 @@ export function EnvironmentEditor({
}); });
} }
return { options }; return { options };
}, [selectedEnvironment, allEnvironments]); }, [environment, allEnvironments]);
const validateName = useCallback((name: string) => { const validateName = useCallback((name: string) => {
// Empty just means the variable doesn't have a name yet and is unusable // Empty just means the variable doesn't have a name yet and is unusable
@@ -79,10 +81,8 @@ export function EnvironmentEditor({
const valueType = !isEncryptionEnabled && valueVisibility.value ? 'text' : 'password'; const valueType = !isEncryptionEnabled && valueVisibility.value ? 'text' : 'password';
const allVariableAreEncrypted = useMemo( const allVariableAreEncrypted = useMemo(
() => () =>
selectedEnvironment.variables.every( environment.variables.every((v) => v.value === '' || analyzeTemplate(v.value) !== 'insecure'),
(v) => v.value === '' || analyzeTemplate(v.value) !== 'insecure', [environment.variables],
),
[selectedEnvironment.variables],
); );
const encryptEnvironment = (environment: Environment) => { const encryptEnvironment = (environment: Environment) => {
@@ -100,11 +100,11 @@ export function EnvironmentEditor({
return ( return (
<VStack space={4} className={className}> <VStack space={4} className={className}>
<Heading className="w-full flex items-center gap-0.5"> <Heading className="w-full flex items-center gap-0.5">
<EnvironmentColorIndicator clickToEdit environment={selectedEnvironment ?? null} /> <EnvironmentColorIndicator clickToEdit environment={environment ?? null} />
{!hideName && <div className="mr-2">{selectedEnvironment?.name}</div>} {!hideName && <div className="mr-2">{environment?.name}</div>}
{isEncryptionEnabled ? ( {isEncryptionEnabled ? (
!allVariableAreEncrypted ? ( !allVariableAreEncrypted ? (
<BadgeButton color="notice" onClick={() => encryptEnvironment(selectedEnvironment)}> <BadgeButton color="notice" onClick={() => encryptEnvironment(environment)}>
Encrypt All Variables Encrypt All Variables
</BadgeButton> </BadgeButton>
) : ( ) : (
@@ -121,21 +121,21 @@ export function EnvironmentEditor({
color="secondary" color="secondary"
rightSlot={<EnvironmentSharableTooltip />} rightSlot={<EnvironmentSharableTooltip />}
onClick={async () => { onClick={async () => {
await patchModel(selectedEnvironment, { public: !selectedEnvironment.public }); await patchModel(environment, { public: !environment.public });
}} }}
> >
{selectedEnvironment.public ? 'Sharable' : 'Private'} {environment.public ? 'Sharable' : 'Private'}
</BadgeButton> </BadgeButton>
</Heading> </Heading>
{selectedEnvironment.public && (!isEncryptionEnabled || !allVariableAreEncrypted) && ( {environment.public && (!isEncryptionEnabled || !allVariableAreEncrypted) && (
<DismissibleBanner <DismissibleBanner
id={`warn-unencrypted-${selectedEnvironment.id}`} id={`warn-unencrypted-${environment.id}`}
color="notice" color="notice"
className="mr-3" className="mr-3"
actions={[ actions={[
{ {
label: 'Encrypt Variables', label: 'Encrypt Variables',
onClick: () => encryptEnvironment(selectedEnvironment), onClick: () => encryptEnvironment(environment),
color: 'primary', color: 'primary',
}, },
]} ]}
@@ -153,15 +153,11 @@ export function EnvironmentEditor({
valueType={valueType} valueType={valueType}
valueAutocompleteVariables valueAutocompleteVariables
valueAutocompleteFunctions valueAutocompleteFunctions
forceUpdateKey={`${selectedEnvironment.id}::${forceUpdateKey}`} forceUpdateKey={`${environment.id}::${forceUpdateKey}`}
pairs={selectedEnvironment.variables} pairs={environment.variables}
onChange={handleChange} onChange={handleChange}
stateKey={`environment.${selectedEnvironment.id}`} stateKey={`environment.${environment.id}`}
forcedEnvironmentId={ forcedEnvironmentId={environment.id}
// Editing the base environment should resolve variables using the active environment.
// Editing a sub environment should resolve variables as if it's the active environment
isBaseEnvironment(selectedEnvironment) ? undefined : selectedEnvironment.id
}
/> />
</div> </div>
</VStack> </VStack>

View File

@@ -1,16 +1,22 @@
import type { Environment, EnvironmentVariable } from '@yaakapp-internal/models'; import type { Environment, EnvironmentVariable } from '@yaakapp-internal/models';
import { foldersAtom } from '@yaakapp-internal/models'; import { foldersAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { jotaiStore } from '../lib/jotai'; import { jotaiStore } from '../lib/jotai';
import { isFolderEnvironment } from '../lib/model_util';
import { useActiveEnvironment } from './useActiveEnvironment';
import { useActiveRequest } from './useActiveRequest'; import { useActiveRequest } from './useActiveRequest';
import { useEnvironmentsBreakdown } from './useEnvironmentsBreakdown'; import { useEnvironmentsBreakdown } from './useEnvironmentsBreakdown';
import { useParentFolders } from './useParentFolders'; import { useParentFolders } from './useParentFolders';
export function useEnvironmentVariables(environmentId: string | null) { export function useEnvironmentVariables(targetEnvironmentId: string | null) {
const { baseEnvironment, folderEnvironments, subEnvironments } = useEnvironmentsBreakdown(); const { baseEnvironment, folderEnvironments, allEnvironments } = useEnvironmentsBreakdown();
const activeEnvironment = subEnvironments.find((e) => e.id === environmentId) ?? null; const activeEnvironment = useActiveEnvironment();
const targetEnvironment = allEnvironments.find((e) => e.id === targetEnvironmentId) ?? null;
const activeRequest = useActiveRequest(); const activeRequest = useActiveRequest();
const parentFolders = useParentFolders(activeRequest); const folders = useAtomValue(foldersAtom);
const activeFolder = folders.find((f) => f.id === targetEnvironment?.parentId) ?? null;
const parentFolders = useParentFolders(activeFolder ?? activeRequest);
return useMemo(() => { return useMemo(() => {
const varMap: Record<string, WrappedEnvironmentVariable> = {}; const varMap: Record<string, WrappedEnvironmentVariable> = {};
@@ -18,9 +24,15 @@ export function useEnvironmentVariables(environmentId: string | null) {
wrapVariables(folderEnvironments.find((fe) => fe.parentId === f.id) ?? null), wrapVariables(folderEnvironments.find((fe) => fe.parentId === f.id) ?? null),
); );
// Folder environments also can auto-complete from the active environment
const activeEnvironmentVariables =
targetEnvironment != null && isFolderEnvironment(targetEnvironment)
? wrapVariables(activeEnvironment)
: [];
const allVariables = [ const allVariables = [
...folderVariables, ...folderVariables,
...wrapVariables(activeEnvironment), ...activeEnvironmentVariables,
...wrapVariables(baseEnvironment), ...wrapVariables(baseEnvironment),
]; ];
@@ -32,7 +44,7 @@ export function useEnvironmentVariables(environmentId: string | null) {
} }
return Object.values(varMap); return Object.values(varMap);
}, [activeEnvironment, baseEnvironment, folderEnvironments, parentFolders]); }, [activeEnvironment, baseEnvironment, folderEnvironments, parentFolders, targetEnvironment]);
} }
export interface WrappedEnvironmentVariable { export interface WrappedEnvironmentVariable {

View File

@@ -15,10 +15,10 @@ function getParentFolders(
): Folder[] { ): Folder[] {
if (currentModel == null) return []; if (currentModel == null) return [];
const folder = currentModel.folderId ? folders.find((f) => f.id === currentModel.folderId) : null; const parentFolder = currentModel.folderId ? folders.find((f) => f.id === currentModel.folderId) : null;
if (folder == null) { if (parentFolder == null) {
return []; return [];
} }
return [folder, ...getParentFolders(folders, folder)]; return [parentFolder, ...getParentFolders(folders, parentFolder)];
} }

View File

@@ -51,3 +51,11 @@ export function getCharsetFromContentType(headers: HttpResponseHeader[]): string
export function isBaseEnvironment(environment: Environment): boolean { export function isBaseEnvironment(environment: Environment): boolean {
return environment.parentId == null; return environment.parentId == null;
} }
export function isSubEnvironment(environment: Environment): boolean {
return environment.parentModel == 'environment';
}
export function isFolderEnvironment(environment: Environment): boolean {
return environment.parentModel == 'folder';
}

View File

@@ -36,7 +36,7 @@ const yaakDark = {
base: { base: {
surface: 'hsl(244,23%,14%)', surface: 'hsl(244,23%,14%)',
surfaceHighlight: 'hsl(244,23%,20%)', surfaceHighlight: 'hsl(244,23%,20%)',
text: 'hsl(245,23%,84%)', text: 'hsl(245,23%,85%)',
textSubtle: 'hsl(245,18%,58%)', textSubtle: 'hsl(245,18%,58%)',
textSubtlest: 'hsl(245,18%,45%)', textSubtlest: 'hsl(245,18%,45%)',
border: 'hsl(244,23%,25%)', border: 'hsl(244,23%,25%)',
@@ -67,11 +67,11 @@ const yaakDark = {
}, },
responsePane: { responsePane: {
surface: 'hsl(243,23%,16%)', surface: 'hsl(243,23%,16%)',
border: 'hsl(246,23%,22.72%)', border: 'hsl(246,23%,23%)',
}, },
appHeader: { appHeader: {
surface: 'hsl(244,23%,12%)', surface: 'hsl(244,23%,12%)',
border: 'hsl(244,23%,20.8%)', border: 'hsl(244,23%,21%)',
}, },
}, },
}; };
@@ -83,22 +83,22 @@ const yaakLight = {
base: { base: {
surface: 'hsl(0,0%,100%)', surface: 'hsl(0,0%,100%)',
surfaceHighlight: 'hsl(218,24%,87%)', surfaceHighlight: 'hsl(218,24%,87%)',
text: 'hsl(217,24%,15%)', text: 'hsl(217,24%,10%)',
textSubtle: 'hsl(217,24%,40%)', textSubtle: 'hsl(217,24%,40%)',
textSubtlest: 'hsl(217,24%,58%)', textSubtlest: 'hsl(217,24%,58%)',
border: 'hsl(217,22%,93%)', border: 'hsl(217,22%,90%)',
primary: 'hsl(266,100%,70%)', primary: 'hsl(266,100%,60%)',
secondary: 'hsl(220,24%,59%)', secondary: 'hsl(220,24%,50%)',
info: 'hsl(206,100%,48%)', info: 'hsl(206,100%,40%)',
success: 'hsl(155,95%,33%)', success: 'hsl(139,66%,34%)',
notice: 'hsl(45,100%,41%)', notice: 'hsl(45,100%,34%)',
warning: 'hsl(30,100%,43%)', warning: 'hsl(30,100%,36%)',
danger: 'hsl(335,75%,57%)', danger: 'hsl(335,75%,48%)',
}, },
components: { components: {
sidebar: { sidebar: {
surface: 'hsl(220,20%,97%)', surface: 'hsl(220,20%,98%)',
border: 'hsl(217,22%,93%)', border: 'hsl(217,22%,88%)',
surfaceHighlight: 'hsl(217,25%,90%)', surfaceHighlight: 'hsl(217,25%,90%)',
}, },
}, },

View File

@@ -6,7 +6,7 @@
"scripts": { "scripts": {
"dev": "vite dev --force", "dev": "vite dev --force",
"build": "vite build", "build": "vite build",
"lint": "eslint . --ext .ts,.tsx" "lint": "tsc --noEmit && eslint . --ext .ts,.tsx"
}, },
"dependencies": { "dependencies": {
"@codemirror/commands": "^6.8.1", "@codemirror/commands": "^6.8.1",