Theme system refactor (#31)

This commit is contained in:
Gregory Schier
2024-05-21 17:56:06 -07:00
committed by GitHub
parent 8606940dee
commit 83aaeb94f6
82 changed files with 909 additions and 739 deletions

2
package-lock.json generated
View File

@@ -81,7 +81,7 @@
"prettier": "^2.8.4", "prettier": "^2.8.4",
"react-devtools": "^4.27.2", "react-devtools": "^4.27.2",
"tailwindcss": "^3.2.7", "tailwindcss": "^3.2.7",
"typescript": "^5.3.3", "typescript": "^5.4.5",
"vite": "^5.0.0", "vite": "^5.0.0",
"vite-plugin-svgr": "^4.2.0", "vite-plugin-svgr": "^4.2.0",
"vite-plugin-top-level-await": "^1.4.1", "vite-plugin-top-level-await": "^1.4.1",

View File

@@ -101,7 +101,7 @@
"prettier": "^2.8.4", "prettier": "^2.8.4",
"react-devtools": "^4.27.2", "react-devtools": "^4.27.2",
"tailwindcss": "^3.2.7", "tailwindcss": "^3.2.7",
"typescript": "^5.3.3", "typescript": "^5.4.5",
"vite": "^5.0.0", "vite": "^5.0.0",
"vite-plugin-svgr": "^4.2.0", "vite-plugin-svgr": "^4.2.0",
"vite-plugin-top-level-await": "^1.4.1", "vite-plugin-top-level-await": "^1.4.1",

View File

@@ -42,13 +42,13 @@
"icons/release/icon.icns", "icons/release/icon.icns",
"icons/release/icon.ico" "icons/release/icon.ico"
], ],
"longDescription": "The best cross-platform visual API client", "longDescription": "A cross-platform desktop app for interacting with REST, GraphQL, and gRPC",
"resources": [ "resources": [
"migrations/*", "migrations/*",
"plugins/*", "plugins/*",
"protoc-vendored/include/*" "protoc-vendored/include/*"
], ],
"shortDescription": "The best API client", "shortDescription": "Desktop API client",
"targets": [ "targets": [
"deb", "deb",
"appimage", "appimage",

View File

@@ -46,10 +46,10 @@ export function BinaryFileEditor({
return ( return (
<VStack space={2}> <VStack space={2}>
<HStack space={2} alignItems="center"> <HStack space={2} alignItems="center">
<Button variant="border" color="gray" size="sm" onClick={handleClick}> <Button variant="border" color="secondary" size="sm" onClick={handleClick}>
Choose File Choose File
</Button> </Button>
<div className="text-xs font-mono truncate rtl pr-3 text-gray-800"> <div className="text-xs font-mono truncate rtl pr-3 text-fg">
{/* Special character to insert ltr text in rtl element without making things wonky */} {/* Special character to insert ltr text in rtl element without making things wonky */}
&#x200E; &#x200E;
{filePath ?? 'Select File'} {filePath ?? 'Select File'}
@@ -64,7 +64,7 @@ export function BinaryFileEditor({
<HStack space={1.5} justifyContent="center"> <HStack space={1.5} justifyContent="center">
<Button <Button
variant="solid" variant="solid"
color="gray" color="secondary"
size="xs" size="xs"
onClick={() => onChangeContentType(mimeType)} onClick={() => onChangeContentType(mimeType)}
> >

View File

@@ -1,6 +1,6 @@
import classNames from 'classnames'; import classNames from 'classnames';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { useMemo, useCallback, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId'; import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
import { useAppRoutes } from '../hooks/useAppRoutes'; import { useAppRoutes } from '../hooks/useAppRoutes';
import { getRecentEnvironments } from '../hooks/useRecentEnvironments'; import { getRecentEnvironments } from '../hooks/useRecentEnvironments';
@@ -117,8 +117,8 @@ function CommandPaletteItem({
<button <button
onClick={onClick} onClick={onClick}
className={classNames( className={classNames(
'w-full h-xs flex items-center rounded px-1.5 text-gray-600', 'w-full h-xs flex items-center rounded px-1.5 text-fg-subtle',
active && 'bg-highlightSecondary text-gray-800', active && 'bg-background-highlight-secondary text-fg',
)} )}
> >
{children} {children}

View File

@@ -42,7 +42,7 @@ export const CookieDialog = function ({ cookieJarId }: Props) {
<td className="py-2 select-text cursor-text font-mono font-semibold max-w-0"> <td className="py-2 select-text cursor-text font-mono font-semibold max-w-0">
{cookieDomain(c)} {cookieDomain(c)}
</td> </td>
<td className="py-2 pl-4 select-text cursor-text font-mono text-gray-700 whitespace-nowrap overflow-x-auto max-w-[200px] hide-scrollbars"> <td className="py-2 pl-4 select-text cursor-text font-mono text-fg-subtle whitespace-nowrap overflow-x-auto max-w-[200px] hide-scrollbars">
{c.raw_cookie} {c.raw_cookie}
</td> </td>
<td className="max-w-0 w-10"> <td className="max-w-0 w-10">

View File

@@ -14,7 +14,7 @@ export const DropMarker = memo(
'relative w-full h-0 overflow-visible pointer-events-none', 'relative w-full h-0 overflow-visible pointer-events-none',
)} )}
> >
<div className="absolute z-50 left-2 right-2 -bottom-[0.1rem] h-[0.2rem] bg-blue-500/50 rounded-full" /> <div className="absolute z-50 left-2 right-2 -bottom-[0.1rem] h-[0.2rem] bg-fg-primary rounded-full" />
</div> </div>
); );
}, },

View File

@@ -1,5 +1,6 @@
import classNames from 'classnames'; import classNames from 'classnames';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import React from 'react';
interface Props { interface Props {
children: ReactNode; children: ReactNode;
@@ -11,7 +12,8 @@ export function EmptyStateText({ children, className }: Props) {
<div <div
className={classNames( className={classNames(
className, className,
'rounded-lg border border-dashed border-highlight h-full py-2 text-gray-400 flex items-center justify-center', 'rounded-lg border border-dashed border-background-highlight',
'h-full py-2 text-fg-subtler flex items-center justify-center italic',
)} )}
> >
{children} {children}

View File

@@ -71,8 +71,8 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
size="sm" size="sm"
className={classNames( className={classNames(
className, className,
'text-gray-800 !px-2 truncate', 'text-fg !px-2 truncate',
activeEnvironment == null && 'text-opacity-disabled italic', activeEnvironment == null && 'text-fg-subtler italic',
)} )}
{...buttonProps} {...buttonProps}
> >

View File

@@ -68,7 +68,7 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
color="custom" color="custom"
title="Add sub environment" title="Add sub environment"
icon="plusCircle" icon="plusCircle"
iconClassName="text-gray-500 group-hover:text-gray-700" iconClassName="text-fg-subtler group-hover:text-fg-subtle"
className="group" className="group"
onClick={handleCreateEnvironment} onClick={handleCreateEnvironment}
/> />
@@ -97,7 +97,7 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
secondSlot={() => secondSlot={() =>
activeWorkspace != null && ( activeWorkspace != null && (
<EnvironmentEditor <EnvironmentEditor
className="pt-2 border-l border-highlight" className="pt-2 border-l border-background-highlight-secondary"
environment={selectedEnvironment} environment={selectedEnvironment}
workspace={activeWorkspace} workspace={activeWorkspace}
/> />
@@ -175,7 +175,7 @@ const EnvironmentEditor = function ({
<Heading className="w-full flex items-center gap-1"> <Heading className="w-full flex items-center gap-1">
<div>{environment?.name ?? 'Global Variables'}</div> <div>{environment?.name ?? 'Global Variables'}</div>
<IconButton <IconButton
iconClassName="text-gray-600" iconClassName="text-fg-subtler"
size="sm" size="sm"
icon={valueVisibility.value ? 'eye' : 'eyeClosed'} icon={valueVisibility.value ? 'eye' : 'eyeClosed'}
title={valueVisibility.value ? 'Hide Values' : 'Reveal Values'} title={valueVisibility.value ? 'Hide Values' : 'Reveal Values'}
@@ -244,7 +244,7 @@ function SidebarButton({
size="xs" size="xs"
className={classNames( className={classNames(
'w-full', 'w-full',
active ? 'text-gray-800 bg-highlightSecondary' : 'text-gray-600 hover:text-gray-700', active ? 'text-fg bg-background-active' : 'text-fg-subtle hover:text-fg',
)} )}
justify="start" justify="start"
onClick={onClick} onClick={onClick}

View File

@@ -90,7 +90,7 @@ export function ExportDataDialog({
/> />
</td> </td>
<td <td
className="py-1 pl-4 text-gray-700 whitespace-nowrap overflow-x-auto hide-scrollbars" className="py-1 pl-4 text-fg whitespace-nowrap overflow-x-auto hide-scrollbars"
onClick={() => setSelectedWorkspaces((prev) => ({ ...prev, [w.id]: !prev[w.id] }))} onClick={() => setSelectedWorkspaces((prev) => ({ ...prev, [w.id]: !prev[w.id] }))}
> >
{w.name} {w.id === activeWorkspace.id ? '(current workspace)' : ''} {w.name} {w.id === activeWorkspace.id ? '(current workspace)' : ''}
@@ -100,7 +100,7 @@ export function ExportDataDialog({
</tbody> </tbody>
</table> </table>
<HStack space={2} justifyContent="end"> <HStack space={2} justifyContent="end">
<Button className="focus" color="gray" onClick={onHide}> <Button className="focus" variant="border" onClick={onHide}>
Cancel Cancel
</Button> </Button>
<Button <Button

View File

@@ -1,6 +1,6 @@
import { useEffect } from 'react';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { getCurrent } from '@tauri-apps/api/webviewWindow'; import { getCurrent } from '@tauri-apps/api/webviewWindow';
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { useCommandPalette } from '../hooks/useCommandPalette'; import { useCommandPalette } from '../hooks/useCommandPalette';
import { cookieJarsQueryKey } from '../hooks/useCookieJars'; import { cookieJarsQueryKey } from '../hooks/useCookieJars';
@@ -13,6 +13,7 @@ import { httpRequestsQueryKey } from '../hooks/useHttpRequests';
import { httpResponsesQueryKey } from '../hooks/useHttpResponses'; import { httpResponsesQueryKey } from '../hooks/useHttpResponses';
import { keyValueQueryKey } from '../hooks/useKeyValue'; import { keyValueQueryKey } from '../hooks/useKeyValue';
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent'; import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
import { useNotificationToast } from '../hooks/useNotificationToast';
import { useRecentEnvironments } from '../hooks/useRecentEnvironments'; import { useRecentEnvironments } from '../hooks/useRecentEnvironments';
import { useRecentRequests } from '../hooks/useRecentRequests'; import { useRecentRequests } from '../hooks/useRecentRequests';
import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces'; import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces';
@@ -24,7 +25,6 @@ import { workspacesQueryKey } from '../hooks/useWorkspaces';
import type { Model } from '../lib/models'; import type { Model } from '../lib/models';
import { modelsEq } from '../lib/models'; import { modelsEq } from '../lib/models';
import { setPathname } from '../lib/persistPathname'; import { setPathname } from '../lib/persistPathname';
import { useNotificationToast } from '../hooks/useNotificationToast';
const DEFAULT_FONT_SIZE = 16; const DEFAULT_FONT_SIZE = 16;
@@ -34,6 +34,7 @@ export function GlobalHooks() {
useRecentEnvironments(); useRecentEnvironments();
useRecentRequests(); useRecentRequests();
// Other useful things
useSyncAppearance(); useSyncAppearance();
useSyncWindowTitle(); useSyncWindowTitle();
useGlobalCommands(); useGlobalCommands();

View File

@@ -88,7 +88,7 @@ export function GraphQLEditor({ defaultValue, onChange, baseRequest, ...extraEdi
<Button <Button
key="introspection" key="introspection"
size="xs" size="xs"
color={error ? 'danger' : 'gray'} color={error ? 'danger' : 'secondary'}
isLoading={isLoading} isLoading={isLoading}
onClick={() => { onClick={() => {
dialog.show({ dialog.show({
@@ -105,7 +105,7 @@ export function GraphQLEditor({ defaultValue, onChange, baseRequest, ...extraEdi
refetch(); refetch();
}} }}
className="ml-auto" className="ml-auto"
color="secondary" color="primary"
size="sm" size="sm"
> >
Try Again Try Again

View File

@@ -98,9 +98,10 @@ export function GrpcConnectionLayout({ style }: Props) {
<div <div
style={style} style={style}
className={classNames( className={classNames(
'x-theme-responsePane',
'max-h-full h-full grid grid-rows-[minmax(0,1fr)] grid-cols-1', 'max-h-full h-full grid grid-rows-[minmax(0,1fr)] grid-cols-1',
'bg-gray-50 dark:bg-gray-100 rounded-md border border-highlight', 'bg-background rounded-md border border-background-highlight',
'shadow shadow-gray-100 dark:shadow-gray-0 relative', 'shadow relative',
)} )}
> >
{grpc.go.error ? ( {grpc.go.error ? (

View File

@@ -2,9 +2,11 @@ import classNames from 'classnames';
import { format } from 'date-fns'; import { format } from 'date-fns';
import type { CSSProperties } from 'react'; import type { CSSProperties } from 'react';
import React, { useEffect, useMemo, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { useGrpcConnections } from '../hooks/useGrpcConnections';
import { useGrpcEvents } from '../hooks/useGrpcEvents'; import { useGrpcEvents } from '../hooks/useGrpcEvents';
import { usePinnedGrpcConnection } from '../hooks/usePinnedGrpcConnection';
import { useStateWithDeps } from '../hooks/useStateWithDeps';
import type { GrpcEvent, GrpcRequest } from '../lib/models'; import type { GrpcEvent, GrpcRequest } from '../lib/models';
import { Button } from './core/Button';
import { Icon } from './core/Icon'; import { Icon } from './core/Icon';
import { JsonAttributeTree } from './core/JsonAttributeTree'; import { JsonAttributeTree } from './core/JsonAttributeTree';
import { KeyValueRow, KeyValueRows } from './core/KeyValueRow'; import { KeyValueRow, KeyValueRows } from './core/KeyValueRow';
@@ -13,8 +15,6 @@ import { SplitLayout } from './core/SplitLayout';
import { HStack, VStack } from './core/Stacks'; import { HStack, VStack } from './core/Stacks';
import { EmptyStateText } from './EmptyStateText'; import { EmptyStateText } from './EmptyStateText';
import { RecentConnectionsDropdown } from './RecentConnectionsDropdown'; import { RecentConnectionsDropdown } from './RecentConnectionsDropdown';
import { Button } from './core/Button';
import { useStateWithDeps } from '../hooks/useStateWithDeps';
interface Props { interface Props {
style?: CSSProperties; style?: CSSProperties;
@@ -31,11 +31,11 @@ interface Props {
export function GrpcConnectionMessagesPane({ style, methodType, activeRequest }: Props) { export function GrpcConnectionMessagesPane({ style, methodType, activeRequest }: Props) {
const [activeEventId, setActiveEventId] = useState<string | null>(null); const [activeEventId, setActiveEventId] = useState<string | null>(null);
const connections = useGrpcConnections(activeRequest.id ?? null);
const activeConnection = connections[0] ?? null;
const events = useGrpcEvents(activeConnection?.id ?? null);
const [showLarge, setShowLarge] = useStateWithDeps<boolean>(false, [activeRequest.id]); const [showLarge, setShowLarge] = useStateWithDeps<boolean>(false, [activeRequest.id]);
const [showingLarge, setShowingLarge] = useState<boolean>(false); const [showingLarge, setShowingLarge] = useState<boolean>(false);
const { activeConnection, connections, setPinnedConnectionId } =
usePinnedGrpcConnection(activeRequest);
const events = useGrpcEvents(activeConnection?.id ?? null);
const activeEvent = useMemo( const activeEvent = useMemo(
() => events.find((m) => m.id === activeEventId) ?? null, () => events.find((m) => m.id === activeEventId) ?? null,
@@ -65,15 +65,13 @@ export function GrpcConnectionMessagesPane({ style, methodType, activeRequest }:
<HStack alignItems="center" space={2}> <HStack alignItems="center" space={2}>
<span>{events.length} messages</span> <span>{events.length} messages</span>
{activeConnection.elapsed === 0 && ( {activeConnection.elapsed === 0 && (
<Icon icon="refresh" size="sm" spin className="text-gray-600" /> <Icon icon="refresh" size="sm" spin className="text-fg-subtler" />
)} )}
</HStack> </HStack>
<RecentConnectionsDropdown <RecentConnectionsDropdown
connections={connections} connections={connections}
activeConnection={activeConnection} activeConnection={activeConnection}
onPinned={() => { onPinnedConnectionId={setPinnedConnectionId}
// todo
}}
/> />
</HStack> </HStack>
<div className="overflow-y-auto h-full"> <div className="overflow-y-auto h-full">
@@ -107,7 +105,7 @@ export function GrpcConnectionMessagesPane({ style, methodType, activeRequest }:
Message {activeEvent.eventType === 'client_message' ? 'Sent' : 'Received'} Message {activeEvent.eventType === 'client_message' ? 'Sent' : 'Received'}
</div> </div>
{!showLarge && activeEvent.content.length > 1000 * 1000 ? ( {!showLarge && activeEvent.content.length > 1000 * 1000 ? (
<VStack space={2} className="text-sm italic text-gray-500"> <VStack space={2} className="text-sm italic text-fg-subtler">
Message previews larger than 1MB are hidden Message previews larger than 1MB are hidden
<div> <div>
<Button <Button
@@ -119,7 +117,7 @@ export function GrpcConnectionMessagesPane({ style, methodType, activeRequest }:
}, 500); }, 500);
}} }}
isLoading={showingLarge} isLoading={showingLarge}
color="gray" color="secondary"
variant="border" variant="border"
size="xs" size="xs"
> >
@@ -138,7 +136,7 @@ export function GrpcConnectionMessagesPane({ style, methodType, activeRequest }:
{activeEvent.content} {activeEvent.content}
</div> </div>
{activeEvent.error && ( {activeEvent.error && (
<div className="select-text cursor-text text-xs font-mono py-1 text-orange-700"> <div className="select-text cursor-text text-xs font-mono py-1 text-fg-warning">
{activeEvent.error} {activeEvent.error}
</div> </div>
)} )}
@@ -183,21 +181,21 @@ function EventRow({
className={classNames( className={classNames(
'w-full grid grid-cols-[auto_minmax(0,3fr)_auto] gap-2 items-center text-left', 'w-full grid grid-cols-[auto_minmax(0,3fr)_auto] gap-2 items-center text-left',
'px-1.5 py-1 font-mono cursor-default group focus:outline-none rounded', 'px-1.5 py-1 font-mono cursor-default group focus:outline-none rounded',
isActive && '!bg-highlight text-gray-900', isActive && '!bg-background-highlight-secondary !text-fg',
'text-gray-800 hover:text-gray-900', 'text-fg-subtle hover:text-fg',
)} )}
> >
<Icon <Icon
className={ className={
eventType === 'server_message' eventType === 'server_message'
? 'text-blue-600' ? 'text-fg-info'
: eventType === 'client_message' : eventType === 'client_message'
? 'text-violet-600' ? 'text-fg-primary'
: eventType === 'error' || (status != null && status > 0) : eventType === 'error' || (status != null && status > 0)
? 'text-orange-600' ? 'text-fg-danger'
: eventType === 'connection_end' : eventType === 'connection_end'
? 'text-green-600' ? 'text-fg-success'
: 'text-gray-700' : 'text-fg-subtle'
} }
title={ title={
eventType === 'server_message' eventType === 'server_message'
@@ -224,7 +222,7 @@ function EventRow({
/> />
<div className={classNames('w-full truncate text-2xs')}> <div className={classNames('w-full truncate text-2xs')}>
{content.slice(0, 1000)} {content.slice(0, 1000)}
{error && <span className="text-orange-600"> ({error})</span>} {error && <span className="text-fg-warning"> ({error})</span>}
</div> </div>
<div className={classNames('opacity-50 text-2xs')}> <div className={classNames('opacity-50 text-2xs')}>
{format(createdAt + 'Z', 'HH:mm:ss.SSS')} {format(createdAt + 'Z', 'HH:mm:ss.SSS')}

View File

@@ -199,14 +199,14 @@ export function GrpcConnectionSetupPane({
label: 'Refresh', label: 'Refresh',
type: 'default', type: 'default',
key: 'custom', key: 'custom',
leftSlot: <Icon className="text-gray-600" size="sm" icon="refresh" />, leftSlot: <Icon className="text-fg-subtler" size="sm" icon="refresh" />,
}, },
]} ]}
> >
<Button <Button
size="sm" size="sm"
variant="border" variant="border"
rightSlot={<Icon className="text-gray-600" size="sm" icon="chevronDown" />} rightSlot={<Icon className="text-fg-subtler" size="sm" icon="chevronDown" />}
disabled={isStreaming || services == null} disabled={isStreaming || services == null}
className={classNames( className={classNames(
'font-mono text-xs min-w-[5rem] !ring-0', 'font-mono text-xs min-w-[5rem] !ring-0',
@@ -221,14 +221,14 @@ export function GrpcConnectionSetupPane({
{isStreaming && ( {isStreaming && (
<> <>
<IconButton <IconButton
className="border border-highlight" className="border border-background-highlight-secondary"
size="sm" size="sm"
title="Cancel" title="Cancel"
onClick={onCancel} onClick={onCancel}
icon="x" icon="x"
/> />
<IconButton <IconButton
className="border border-highlight" className="border border-background-highlight-secondary"
size="sm" size="sm"
title="Commit" title="Commit"
onClick={onCommit} onClick={onCommit}
@@ -237,7 +237,7 @@ export function GrpcConnectionSetupPane({
</> </>
)} )}
<IconButton <IconButton
className="border border-highlight" className="border border-background-highlight-secondary"
size="sm" size="sm"
title={isStreaming ? 'Connect' : 'Send'} title={isStreaming ? 'Connect' : 'Send'}
hotkeyAction="grpc_request.send" hotkeyAction="grpc_request.send"
@@ -247,7 +247,7 @@ export function GrpcConnectionSetupPane({
</> </>
) : ( ) : (
<IconButton <IconButton
className="border border-highlight" className="border border-background-highlight-secondary"
size="sm" size="sm"
title={methodType === 'unary' ? 'Send' : 'Connect'} title={methodType === 'unary' ? 'Send' : 'Connect'}
hotkeyAction="grpc_request.send" hotkeyAction="grpc_request.send"
@@ -275,7 +275,6 @@ export function GrpcConnectionSetupPane({
<GrpcEditor <GrpcEditor
onChange={handleChangeMessage} onChange={handleChangeMessage}
services={services} services={services}
className="bg-gray-50"
reflectionError={reflectionError} reflectionError={reflectionError}
reflectionLoading={reflectionLoading} reflectionLoading={reflectionLoading}
request={activeRequest} request={activeRequest}

View File

@@ -133,12 +133,12 @@ export function GrpcEditor({
size="xs" size="xs"
color={ color={
reflectionLoading reflectionLoading
? 'gray'
: reflectionUnavailable
? 'secondary' ? 'secondary'
: reflectionUnavailable
? 'info'
: reflectionError : reflectionError
? 'danger' ? 'danger'
: 'gray' : 'secondary'
} }
isLoading={reflectionLoading} isLoading={reflectionLoading}
onClick={() => { onClick={() => {

View File

@@ -60,7 +60,7 @@ export function GrpcProtoSelection({ requestId }: Props) {
<Button <Button
isLoading={grpc.reflect.isFetching} isLoading={grpc.reflect.isFetching}
disabled={grpc.reflect.isFetching} disabled={grpc.reflect.isFetching}
color="gray" color="secondary"
size="sm" size="sm"
onClick={() => grpc.reflect.refetch()} onClick={() => grpc.reflect.refetch()}
> >
@@ -106,7 +106,7 @@ export function GrpcProtoSelection({ requestId }: Props) {
<table className="w-full divide-y"> <table className="w-full divide-y">
<thead> <thead>
<tr> <tr>
<th className="text-gray-600"> <th className="text-fg-subtler">
<span className="font-mono text-sm">*.proto</span> Files <span className="font-mono text-sm">*.proto</span> Files
</th> </th>
<th></th> <th></th>

View File

@@ -1,9 +1,9 @@
import { motion } from 'framer-motion';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Button } from './core/Button';
import { useClipboardText } from '../hooks/useClipboardText'; import { useClipboardText } from '../hooks/useClipboardText';
import { useImportCurl } from '../hooks/useImportCurl'; import { useImportCurl } from '../hooks/useImportCurl';
import { Button } from './core/Button';
import { Icon } from './core/Icon'; import { Icon } from './core/Icon';
import { motion } from 'framer-motion';
export function ImportCurlButton() { export function ImportCurlButton() {
const [clipboardText] = useClipboardText(); const [clipboardText] = useClipboardText();
@@ -23,7 +23,7 @@ export function ImportCurlButton() {
<Button <Button
size="xs" size="xs"
variant="border" variant="border"
color="secondary" color="primary"
leftSlot={<Icon icon="paste" size="sm" />} leftSlot={<Icon icon="paste" size="sm" />}
isLoading={isLoading} isLoading={isLoading}
onClick={() => { onClick={() => {

View File

@@ -43,10 +43,10 @@ export function Overlay({
onClick={onClose} onClick={onClose}
className={classNames( className={classNames(
'absolute inset-0', 'absolute inset-0',
variant === 'default' && 'bg-gray-600/30 dark:bg-black/30 backdrop-blur-sm', variant === 'default' && 'bg-background-backdrop backdrop-blur-sm',
)} )}
/> />
<div className="bg-red-100">{children}</div> {children}
</motion.div> </motion.div>
</FocusTrap> </FocusTrap>
)} )}

View File

@@ -11,12 +11,17 @@ import { HStack } from './core/Stacks';
interface Props { interface Props {
connections: GrpcConnection[]; connections: GrpcConnection[];
activeConnection: GrpcConnection; activeConnection: GrpcConnection;
onPinned: (r: GrpcConnection) => void; onPinnedConnectionId: (id: string) => void;
} }
export function RecentConnectionsDropdown({ activeConnection, connections, onPinned }: Props) { export function RecentConnectionsDropdown({
activeConnection,
connections,
onPinnedConnectionId,
}: Props) {
const deleteConnection = useDeleteGrpcConnection(activeConnection?.id ?? null); const deleteConnection = useDeleteGrpcConnection(activeConnection?.id ?? null);
const deleteAllConnections = useDeleteGrpcConnections(activeConnection?.requestId); const deleteAllConnections = useDeleteGrpcConnections(activeConnection?.requestId);
const latestConnectionId = connections[0]?.id ?? 'n/a';
return ( return (
<Dropdown <Dropdown
@@ -44,13 +49,13 @@ export function RecentConnectionsDropdown({ activeConnection, connections, onPin
</HStack> </HStack>
), ),
leftSlot: activeConnection?.id === c.id ? <Icon icon="check" /> : <Icon icon="empty" />, leftSlot: activeConnection?.id === c.id ? <Icon icon="check" /> : <Icon icon="empty" />,
onSelect: () => onPinned(c), onSelect: () => onPinnedConnectionId(c.id),
})), })),
]} ]}
> >
<IconButton <IconButton
title="Show connection history" title="Show connection history"
icon="chevronDown" icon={activeConnection?.id === latestConnectionId ? 'chevronDown' : 'pin'}
className="ml-auto" className="ml-auto"
size="sm" size="sm"
iconSize="md" iconSize="md"

View File

@@ -86,8 +86,8 @@ export function RecentRequestsDropdown({ className }: Pick<ButtonProps, 'classNa
hotkeyAction="request_switcher.toggle" hotkeyAction="request_switcher.toggle"
className={classNames( className={classNames(
className, className,
'text-gray-800 text-sm truncate pointer-events-auto', 'text-fg text-sm truncate pointer-events-auto',
activeRequest === null && 'text-opacity-disabled italic', activeRequest === null && 'text-fg-subtler italic',
)} )}
> >
{fallbackRequestName(activeRequest)} {fallbackRequestName(activeRequest)}

View File

@@ -1,26 +1,30 @@
import classNames from 'classnames';
import { useDeleteHttpResponse } from '../hooks/useDeleteHttpResponse'; import { useDeleteHttpResponse } from '../hooks/useDeleteHttpResponse';
import { useDeleteHttpResponses } from '../hooks/useDeleteHttpResponses'; import { useDeleteHttpResponses } from '../hooks/useDeleteHttpResponses';
import type { HttpResponse } from '../lib/models'; import type { HttpResponse } from '../lib/models';
import { Dropdown } from './core/Dropdown';
import { pluralize } from '../lib/pluralize'; import { pluralize } from '../lib/pluralize';
import { HStack } from './core/Stacks'; import { Dropdown } from './core/Dropdown';
import { StatusTag } from './core/StatusTag';
import { Icon } from './core/Icon'; import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton'; import { IconButton } from './core/IconButton';
import { HStack } from './core/Stacks';
import { StatusTag } from './core/StatusTag';
interface Props { interface Props {
responses: HttpResponse[]; responses: HttpResponse[];
activeResponse: HttpResponse; activeResponse: HttpResponse;
onPinnedResponse: (r: HttpResponse) => void; onPinnedResponseId: (id: string) => void;
className?: string;
} }
export const RecentResponsesDropdown = function ResponsePane({ export const RecentResponsesDropdown = function ResponsePane({
activeResponse, activeResponse,
responses, responses,
onPinnedResponse, onPinnedResponseId,
className,
}: Props) { }: Props) {
const deleteResponse = useDeleteHttpResponse(activeResponse?.id ?? null); const deleteResponse = useDeleteHttpResponse(activeResponse?.id ?? null);
const deleteAllResponses = useDeleteHttpResponses(activeResponse?.requestId); const deleteAllResponses = useDeleteHttpResponses(activeResponse?.requestId);
const latestResponseId = responses[0]?.id ?? 'n/a';
return ( return (
<Dropdown <Dropdown
@@ -44,18 +48,19 @@ export const RecentResponsesDropdown = function ResponsePane({
label: ( label: (
<HStack space={2} alignItems="center"> <HStack space={2} alignItems="center">
<StatusTag className="text-xs" response={r} /> <StatusTag className="text-xs" response={r} />
<span>&bull;</span> <span className="font-mono text-xs">{r.elapsed}ms</span> <span>&rarr;</span>{' '}
<span className="font-mono text-xs">{r.elapsed >= 0 ? `${r.elapsed}ms` : 'n/a'}</span>
</HStack> </HStack>
), ),
leftSlot: activeResponse?.id === r.id ? <Icon icon="check" /> : <Icon icon="empty" />, leftSlot: activeResponse?.id === r.id ? <Icon icon="check" /> : <Icon icon="empty" />,
onSelect: () => onPinnedResponse(r), onSelect: () => onPinnedResponseId(r.id),
})), })),
]} ]}
> >
<IconButton <IconButton
title="Show response history" title="Show response history"
icon="chevronDown" icon={activeResponse?.id === latestResponseId ? 'chevronDown' : 'pin'}
className="ml-auto" className={classNames(className, 'm-0.5')}
size="sm" size="sm"
iconSize="md" iconSize="md"
/> />

View File

@@ -1,3 +1,4 @@
import classNames from 'classnames';
import { memo, useMemo } from 'react'; import { memo, useMemo } from 'react';
import { usePrompt } from '../hooks/usePrompt'; import { usePrompt } from '../hooks/usePrompt';
import { Button } from './core/Button'; import { Button } from './core/Button';
@@ -57,7 +58,7 @@ export const RequestMethodDropdown = memo(function RequestMethodDropdown({
return ( return (
<RadioDropdown value={method} items={radioItems} extraItems={extraItems} onChange={onChange}> <RadioDropdown value={method} items={radioItems} extraItems={extraItems} onChange={onChange}>
<Button size="xs" className={className}> <Button size="xs" className={classNames(className, 'text-fg-subtle hover:text-fg')}>
{method.toUpperCase()} {method.toUpperCase()}
</Button> </Button>
</RadioDropdown> </RadioDropdown>

View File

@@ -3,10 +3,12 @@ import type { CSSProperties } from 'react';
import React, { memo, useCallback, useMemo, useState } from 'react'; import React, { memo, useCallback, useMemo, useState } from 'react';
import { createGlobalState } from 'react-use'; import { createGlobalState } from 'react-use';
import { useCancelHttpResponse } from '../hooks/useCancelHttpResponse'; import { useCancelHttpResponse } from '../hooks/useCancelHttpResponse';
import { useContentTypeFromHeaders } from '../hooks/useContentTypeFromHeaders';
import { useImportCurl } from '../hooks/useImportCurl';
import { useIsResponseLoading } from '../hooks/useIsResponseLoading'; import { useIsResponseLoading } from '../hooks/useIsResponseLoading';
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse'; import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
import { useRequests } from '../hooks/useRequests';
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey'; import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
import { useContentTypeFromHeaders } from '../hooks/useContentTypeFromHeaders';
import { useSendRequest } from '../hooks/useSendRequest'; import { useSendRequest } from '../hooks/useSendRequest';
import { useUpdateHttpRequest } from '../hooks/useUpdateHttpRequest'; import { useUpdateHttpRequest } from '../hooks/useUpdateHttpRequest';
import { tryFormatJson } from '../lib/formatters'; import { tryFormatJson } from '../lib/formatters';
@@ -29,6 +31,7 @@ import { BearerAuth } from './BearerAuth';
import { BinaryFileEditor } from './BinaryFileEditor'; import { BinaryFileEditor } from './BinaryFileEditor';
import { CountBadge } from './core/CountBadge'; import { CountBadge } from './core/CountBadge';
import { Editor } from './core/Editor'; import { Editor } from './core/Editor';
import type { GenericCompletionOption } from './core/Editor/genericCompletion';
import type { TabItem } from './core/Tabs/Tabs'; import type { TabItem } from './core/Tabs/Tabs';
import { TabContent, Tabs } from './core/Tabs/Tabs'; import { TabContent, Tabs } from './core/Tabs/Tabs';
import { EmptyStateText } from './EmptyStateText'; import { EmptyStateText } from './EmptyStateText';
@@ -38,9 +41,6 @@ import { GraphQLEditor } from './GraphQLEditor';
import { HeadersEditor } from './HeadersEditor'; import { HeadersEditor } from './HeadersEditor';
import { UrlBar } from './UrlBar'; import { UrlBar } from './UrlBar';
import { UrlParametersEditor } from './UrlParameterEditor'; import { UrlParametersEditor } from './UrlParameterEditor';
import { useImportCurl } from '../hooks/useImportCurl';
import { useRequests } from '../hooks/useRequests';
import type { GenericCompletionOption } from './core/Editor/genericCompletion';
interface Props { interface Props {
style: CSSProperties; style: CSSProperties;
@@ -317,7 +317,6 @@ export const RequestPane = memo(function RequestPane({
useTemplating useTemplating
autocompleteVariables autocompleteVariables
placeholder="..." placeholder="..."
className="!bg-gray-50"
heightMode={fullHeight ? 'full' : 'auto'} heightMode={fullHeight ? 'full' : 'auto'}
defaultValue={`${activeRequest.body?.text ?? ''}`} defaultValue={`${activeRequest.body?.text ?? ''}`}
contentType="application/json" contentType="application/json"
@@ -330,7 +329,6 @@ export const RequestPane = memo(function RequestPane({
useTemplating useTemplating
autocompleteVariables autocompleteVariables
placeholder="..." placeholder="..."
className="!bg-gray-50"
heightMode={fullHeight ? 'full' : 'auto'} heightMode={fullHeight ? 'full' : 'auto'}
defaultValue={`${activeRequest.body?.text ?? ''}`} defaultValue={`${activeRequest.body?.text ?? ''}`}
contentType="text/xml" contentType="text/xml"
@@ -340,7 +338,6 @@ export const RequestPane = memo(function RequestPane({
<GraphQLEditor <GraphQLEditor
forceUpdateKey={forceUpdateKey} forceUpdateKey={forceUpdateKey}
baseRequest={activeRequest} baseRequest={activeRequest}
className="!bg-gray-50"
defaultValue={`${activeRequest.body?.text ?? ''}`} defaultValue={`${activeRequest.body?.text ?? ''}`}
onChange={handleBodyTextChange} onChange={handleBodyTextChange}
/> />
@@ -370,7 +367,6 @@ export const RequestPane = memo(function RequestPane({
useTemplating useTemplating
autocompleteVariables autocompleteVariables
placeholder="..." placeholder="..."
className="!bg-gray-50"
heightMode={fullHeight ? 'full' : 'auto'} heightMode={fullHeight ? 'full' : 'auto'}
defaultValue={`${activeRequest.body?.text ?? ''}`} defaultValue={`${activeRequest.body?.text ?? ''}`}
onChange={handleBodyTextChange} onChange={handleBodyTextChange}

View File

@@ -32,7 +32,7 @@ export function ResizeHandle({
className={classNames( className={classNames(
className, className,
'group z-10 flex', 'group z-10 flex',
// 'bg-blue-100/10', // For debugging // 'bg-fg-info', // For debugging
vertical ? 'w-full h-3 cursor-row-resize' : 'h-full w-3 cursor-col-resize', vertical ? 'w-full h-3 cursor-row-resize' : 'h-full w-3 cursor-col-resize',
justify === 'center' && 'justify-center', justify === 'center' && 'justify-center',
justify === 'end' && 'justify-end', justify === 'end' && 'justify-end',

View File

@@ -2,8 +2,8 @@ import classNames from 'classnames';
import type { CSSProperties } from 'react'; import type { CSSProperties } from 'react';
import { memo, useMemo } from 'react'; import { memo, useMemo } from 'react';
import { createGlobalState } from 'react-use'; import { createGlobalState } from 'react-use';
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
import { useContentTypeFromHeaders } from '../hooks/useContentTypeFromHeaders'; import { useContentTypeFromHeaders } from '../hooks/useContentTypeFromHeaders';
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
import { useResponseViewMode } from '../hooks/useResponseViewMode'; import { useResponseViewMode } from '../hooks/useResponseViewMode';
import type { HttpRequest } from '../lib/models'; import type { HttpRequest } from '../lib/models';
import { isResponseLoading } from '../lib/models'; import { isResponseLoading } from '../lib/models';
@@ -34,7 +34,7 @@ interface Props {
const useActiveTab = createGlobalState<string>('body'); const useActiveTab = createGlobalState<string>('body');
export const ResponsePane = memo(function ResponsePane({ style, className, activeRequest }: Props) { export const ResponsePane = memo(function ResponsePane({ style, className, activeRequest }: Props) {
const { activeResponse, setPinnedResponse, responses } = usePinnedHttpResponse(activeRequest); const { activeResponse, setPinnedResponseId, responses } = usePinnedHttpResponse(activeRequest);
const [viewMode, setViewMode] = useResponseViewMode(activeResponse?.requestId); const [viewMode, setViewMode] = useResponseViewMode(activeResponse?.requestId);
const [activeTab, setActiveTab] = useActiveTab(); const [activeTab, setActiveTab] = useActiveTab();
const contentType = useContentTypeFromHeaders(activeResponse?.headers ?? null); const contentType = useContentTypeFromHeaders(activeResponse?.headers ?? null);
@@ -73,9 +73,10 @@ export const ResponsePane = memo(function ResponsePane({ style, className, activ
style={style} style={style}
className={classNames( className={classNames(
className, className,
'x-theme-responsePane',
'max-h-full h-full', 'max-h-full h-full',
'bg-gray-50 dark:bg-gray-100 rounded-md border border-highlight', 'bg-background rounded-md border border-background-highlight',
'shadow shadow-gray-100 dark:shadow-gray-0 relative', 'shadow relative',
)} )}
> >
{activeResponse == null ? ( {activeResponse == null ? (
@@ -91,39 +92,41 @@ export const ResponsePane = memo(function ResponsePane({ style, className, activ
<HStack <HStack
alignItems="center" alignItems="center"
className={classNames( className={classNames(
'text-gray-700 text-sm w-full flex-shrink-0', 'text-fg-subtle text-sm w-full flex-shrink-0',
// Remove a bit of space because the tabs have lots too // Remove a bit of space because the tabs have lots too
'-mb-1.5', '-mb-1.5',
)} )}
> >
{activeResponse && ( {activeResponse && (
<HStack alignItems="center" className="w-full"> <HStack
<div className="whitespace-nowrap px-3"> space={2}
<HStack space={2}> alignItems="center"
<StatusTag showReason response={activeResponse} /> className="whitespace-nowrap w-full pl-3 overflow-x-auto font-mono text-sm"
{activeResponse.elapsed > 0 && ( >
<> <StatusTag showReason response={activeResponse} />
<span>&bull;</span> {activeResponse.elapsed > 0 && (
<DurationTag <>
headers={activeResponse.elapsedHeaders} <span>&bull;</span>
total={activeResponse.elapsed} <DurationTag
/> headers={activeResponse.elapsedHeaders}
</> total={activeResponse.elapsed}
)} />
{!!activeResponse.contentLength && ( </>
<> )}
<span>&bull;</span> {!!activeResponse.contentLength && (
<SizeTag contentLength={activeResponse.contentLength} /> <>
</> <span>&bull;</span>
)} <SizeTag contentLength={activeResponse.contentLength} />
</HStack> </>
</div> )}
<RecentResponsesDropdown <div className="ml-auto">
responses={responses} <RecentResponsesDropdown
activeResponse={activeResponse} responses={responses}
onPinnedResponse={setPinnedResponse} activeResponse={activeResponse}
/> onPinnedResponseId={setPinnedResponseId}
/>
</div>
</HStack> </HStack>
)} )}
</HStack> </HStack>
@@ -138,7 +141,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className, activ
onChangeValue={setActiveTab} onChangeValue={setActiveTab}
label="Response" label="Response"
tabs={tabs} tabs={tabs}
className="ml-3 mr-1" className="ml-3 mr-3 mb-3"
tabListClassName="mt-1.5" tabListClassName="mt-1.5"
> >
<TabContent value="headers"> <TabContent value="headers">
@@ -146,13 +149,13 @@ export const ResponsePane = memo(function ResponsePane({ style, className, activ
</TabContent> </TabContent>
<TabContent value="body"> <TabContent value="body">
{!activeResponse.contentLength ? ( {!activeResponse.contentLength ? (
<EmptyStateText>Empty Body</EmptyStateText> <div className="pb-2 h-full">
<EmptyStateText>Empty Body</EmptyStateText>
</div>
) : contentType?.startsWith('image') ? ( ) : contentType?.startsWith('image') ? (
<ImageViewer className="pb-2" response={activeResponse} /> <ImageViewer className="pb-2" response={activeResponse} />
) : activeResponse.contentLength > 2 * 1000 * 1000 ? ( ) : activeResponse.contentLength > 2 * 1000 * 1000 ? (
<div className="text-sm italic text-gray-500"> <EmptyStateText>Cannot preview text responses larger than 2MB</EmptyStateText>
Cannot preview text responses larger than 2MB
</div>
) : viewMode === 'pretty' && contentType?.includes('html') ? ( ) : viewMode === 'pretty' && contentType?.includes('html') ? (
<WebPageViewer response={activeResponse} /> <WebPageViewer response={activeResponse} />
) : contentType?.match(/csv|tab-separated/) ? ( ) : contentType?.match(/csv|tab-separated/) ? (

View File

@@ -26,7 +26,7 @@ export default function RouteError() {
> >
Go Home Go Home
</Button> </Button>
<Button color="secondary" onClick={() => window.location.reload()}> <Button color="info" onClick={() => window.location.reload()}>
Refresh Refresh
</Button> </Button>
</VStack> </VStack>

View File

@@ -90,7 +90,7 @@ export const SettingsDialog = () => {
<Heading size={2}> <Heading size={2}>
Workspace{' '} Workspace{' '}
<div className="inline-block ml-1 bg-gray-500 dark:bg-gray-300 px-2 py-0.5 text-sm rounded text-white dark:text-gray-900"> <div className="inline-block ml-1 bg-background-highlight px-2 py-0.5 text-sm rounded text-fg">
{workspace.name} {workspace.name}
</div> </div>
</Heading> </Heading>

View File

@@ -9,6 +9,7 @@ import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
import { useActiveRequest } from '../hooks/useActiveRequest'; import { useActiveRequest } from '../hooks/useActiveRequest';
import { useActiveWorkspace } from '../hooks/useActiveWorkspace'; import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import { useAppRoutes } from '../hooks/useAppRoutes'; import { useAppRoutes } from '../hooks/useAppRoutes';
import { useCopyAsCurl } from '../hooks/useCopyAsCurl';
import { useCreateDropdownItems } from '../hooks/useCreateDropdownItems'; import { useCreateDropdownItems } from '../hooks/useCreateDropdownItems';
import { useDeleteFolder } from '../hooks/useDeleteFolder'; import { useDeleteFolder } from '../hooks/useDeleteFolder';
import { useDeleteRequest } from '../hooks/useDeleteRequest'; import { useDeleteRequest } from '../hooks/useDeleteRequest';
@@ -40,7 +41,6 @@ import { InlineCode } from './core/InlineCode';
import { VStack } from './core/Stacks'; import { VStack } from './core/Stacks';
import { StatusTag } from './core/StatusTag'; import { StatusTag } from './core/StatusTag';
import { DropMarker } from './DropMarker'; import { DropMarker } from './DropMarker';
import { useCopyAsCurl } from '../hooks/useCopyAsCurl';
interface Props { interface Props {
className?: string; className?: string;
@@ -468,6 +468,47 @@ export function Sidebar({ className }: Props) {
handleEnd={handleEnd} handleEnd={handleEnd}
handleDragStart={handleDragStart} handleDragStart={handleDragStart}
/> />
{/*<div className="p-2 flex flex-col gap-1">*/}
{/* <div className="flex flex-wrap gap-1">*/}
{/* <Button color="primary">Primary</Button>*/}
{/* <Button color="secondary">Secondary</Button>*/}
{/* <Button color="info">Info</Button>*/}
{/* <Button color="success">Success</Button>*/}
{/* <Button color="warning">Warning</Button>*/}
{/* <Button color="danger">Danger</Button>*/}
{/* <Button color="default">Default</Button>*/}
{/* </div>*/}
{/* <div className="flex flex-wrap gap-1">*/}
{/* <Button variant="border" color="primary">*/}
{/* Primary*/}
{/* </Button>*/}
{/* <Button variant="border" color="secondary">*/}
{/* Secondary*/}
{/* </Button>*/}
{/* <Button variant="border" color="info">*/}
{/* Info*/}
{/* </Button>*/}
{/* <Button variant="border" color="success">*/}
{/* Success*/}
{/* </Button>*/}
{/* <Button variant="border" color="warning">*/}
{/* Warning*/}
{/* </Button>*/}
{/* <Button variant="border" color="danger">*/}
{/* Danger*/}
{/* </Button>*/}
{/* <Button variant="border" color="default">*/}
{/* Default*/}
{/* </Button>*/}
{/* </div>*/}
{/* <div className="flex flex-col gap-1">*/}
{/* <Banner color="primary">Primary banner</Banner>*/}
{/* <Banner color="secondary">Secondary banner</Banner>*/}
{/* <Banner color="danger">Danger banner</Banner>*/}
{/* <Banner color="warning">Warning banner</Banner>*/}
{/* <Banner color="success">Success banner</Banner>*/}
{/* </div>*/}
{/*</div>*/}
</aside> </aside>
); );
} }
@@ -510,60 +551,66 @@ function SidebarItems({
aria-orientation="vertical" aria-orientation="vertical"
dir="ltr" dir="ltr"
className={classNames( className={classNames(
tree.depth > 0 && 'border-l border-highlight', tree.depth > 0 && 'border-l border-background-highlight-secondary',
tree.depth === 0 && 'ml-0', tree.depth === 0 && 'ml-0',
tree.depth >= 1 && 'ml-[1.2em]', tree.depth >= 1 && 'ml-[1.2em]',
)} )}
> >
{tree.children.map((child, i) => ( {tree.children.map((child, i) => {
<Fragment key={child.item.id}> const selected = selectedId === child.item.id;
{hoveredIndex === i && hoveredTree?.item.id === tree.item.id && <DropMarker />} return (
<DraggableSidebarItem <Fragment key={child.item.id}>
draggable {hoveredIndex === i && hoveredTree?.item.id === tree.item.id && <DropMarker />}
selected={selectedId === child.item.id} <DraggableSidebarItem
itemId={child.item.id} draggable
itemName={child.item.name} selected={selected}
itemFallbackName={ itemId={child.item.id}
child.item.model === 'http_request' || child.item.model === 'grpc_request' itemName={child.item.name}
? fallbackRequestName(child.item) itemFallbackName={
: 'New Folder' child.item.model === 'http_request' || child.item.model === 'grpc_request'
} ? fallbackRequestName(child.item)
itemModel={child.item.model} : 'New Folder'
itemPrefix={ }
(child.item.model === 'http_request' || child.item.model === 'grpc_request') && ( itemModel={child.item.model}
<HttpMethodTag request={child.item} /> itemPrefix={
) (child.item.model === 'http_request' || child.item.model === 'grpc_request') && (
} <HttpMethodTag
onMove={handleMove} request={child.item}
onEnd={handleEnd} className={classNames(!selected && 'text-fg-subtler')}
onSelect={onSelect} />
onDragStart={handleDragStart} )
useProminentStyles={focused} }
isCollapsed={isCollapsed} onMove={handleMove}
child={child} onEnd={handleEnd}
> onSelect={onSelect}
{child.item.model === 'folder' && onDragStart={handleDragStart}
!isCollapsed(child.item.id) && useProminentStyles={focused}
draggingId !== child.item.id && ( isCollapsed={isCollapsed}
<SidebarItems child={child}
treeParentMap={treeParentMap} >
tree={child} {child.item.model === 'folder' &&
isCollapsed={isCollapsed} !isCollapsed(child.item.id) &&
draggingId={draggingId} draggingId !== child.item.id && (
hoveredTree={hoveredTree} <SidebarItems
hoveredIndex={hoveredIndex} treeParentMap={treeParentMap}
focused={focused} tree={child}
selectedId={selectedId} isCollapsed={isCollapsed}
selectedTree={selectedTree} draggingId={draggingId}
onSelect={onSelect} hoveredTree={hoveredTree}
handleMove={handleMove} hoveredIndex={hoveredIndex}
handleEnd={handleEnd} focused={focused}
handleDragStart={handleDragStart} selectedId={selectedId}
/> selectedTree={selectedTree}
)} onSelect={onSelect}
</DraggableSidebarItem> handleMove={handleMove}
</Fragment> handleEnd={handleEnd}
))} handleDragStart={handleDragStart}
/>
)}
</DraggableSidebarItem>
</Fragment>
);
})}
{hoveredIndex === tree.children.length && hoveredTree?.item.id === tree.item.id && ( {hoveredIndex === tree.children.length && hoveredTree?.item.id === tree.item.id && (
<DropMarker /> <DropMarker />
)} )}
@@ -802,12 +849,12 @@ const SidebarItem = forwardRef(function SidebarItem(
data-active={isActive} data-active={isActive}
data-selected={selected} data-selected={selected}
className={classNames( className={classNames(
'w-full flex gap-1.5 items-center text-sm h-xs px-1.5 rounded-md transition-colors', 'w-full flex gap-1.5 items-center text-sm h-xs px-1.5 rounded-md',
editing && 'ring-1 focus-within:ring-focus', editing && 'ring-1 focus-within:ring-focus',
isActive && 'bg-highlightSecondary text-gray-800', isActive && 'bg-background-highlight-secondary text-fg',
!isActive && !isActive &&
'text-gray-600 group-hover/item:text-gray-800 active:bg-highlightSecondary', 'text-fg-subtle group-hover/item:text-fg active:bg-background-highlight-secondary',
selected && useProminentStyles && '!bg-violet-400/20', selected && useProminentStyles && '!bg-background-active',
)} )}
> >
{itemModel === 'folder' && ( {itemModel === 'folder' && (
@@ -815,7 +862,8 @@ const SidebarItem = forwardRef(function SidebarItem(
size="sm" size="sm"
icon="chevronRight" icon="chevronRight"
className={classNames( className={classNames(
'transition-transform opacity-50', 'text-fg-subtler',
'transition-transform',
!isCollapsed(itemId) && 'transform rotate-90', !isCollapsed(itemId) && 'transform rotate-90',
)} )}
/> />
@@ -837,13 +885,13 @@ const SidebarItem = forwardRef(function SidebarItem(
{latestGrpcConnection ? ( {latestGrpcConnection ? (
<div className="ml-auto"> <div className="ml-auto">
{latestGrpcConnection.elapsed === 0 && ( {latestGrpcConnection.elapsed === 0 && (
<Icon spin size="sm" icon="update" className="text-gray-400" /> <Icon spin size="sm" icon="update" className="text-fg-subtler" />
)} )}
</div> </div>
) : latestHttpResponse ? ( ) : latestHttpResponse ? (
<div className="ml-auto"> <div className="ml-auto">
{isResponseLoading(latestHttpResponse) ? ( {isResponseLoading(latestHttpResponse) ? (
<Icon spin size="sm" icon="update" className="text-gray-400" /> <Icon spin size="sm" icon="refresh" className="text-fg-subtler" />
) : ( ) : (
<StatusTag className="text-2xs dark:opacity-80" response={latestHttpResponse} /> <StatusTag className="text-2xs dark:opacity-80" response={latestHttpResponse} />
)} )}

View File

@@ -1,3 +1,4 @@
import classNames from 'classnames';
import type { EditorView } from 'codemirror'; import type { EditorView } from 'codemirror';
import type { FormEvent, ReactNode } from 'react'; import type { FormEvent, ReactNode } from 'react';
import { memo, useRef, useState } from 'react'; import { memo, useRef, useState } from 'react';
@@ -58,7 +59,7 @@ export const UrlBar = memo(function UrlBar({
}; };
return ( return (
<form onSubmit={handleSubmit} className={className}> <form onSubmit={handleSubmit} className={classNames(className)}>
<Input <Input
autocompleteVariables autocompleteVariables
ref={inputRef} ref={inputRef}
@@ -75,7 +76,7 @@ export const UrlBar = memo(function UrlBar({
onFocus={() => setIsFocused(true)} onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)} onBlur={() => setIsFocused(false)}
onPaste={onPaste} onPaste={onPaste}
containerClassName="shadow shadow-gray-100 dark:shadow-gray-50" containerClassName="shadow bg-background border"
onChange={onUrlChange} onChange={onUrlChange}
defaultValue={url} defaultValue={url}
placeholder={placeholder} placeholder={placeholder}

View File

@@ -143,7 +143,8 @@ export default function Workspace() {
initial={{ opacity: 0, x: -20 }} initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }} animate={{ opacity: 1, x: 0 }}
className={classNames( className={classNames(
'absolute top-0 left-0 bottom-0 bg-gray-100 border-r border-highlight w-[14rem]', 'x-theme-sidebar',
'absolute top-0 left-0 bottom-0 bg-background border-r border-background-highlight w-[14rem]',
'grid grid-rows-[auto_1fr]', 'grid grid-rows-[auto_1fr]',
)} )}
> >
@@ -155,8 +156,11 @@ export default function Workspace() {
</Overlay> </Overlay>
) : ( ) : (
<> <>
<div style={side} className={classNames('overflow-hidden bg-gray-100')}> <div
<Sidebar className="border-r border-highlight" /> style={side}
className={classNames('x-theme-sidebar', 'overflow-hidden bg-background')}
>
<Sidebar className="border-r border-background-highlight" />
</div> </div>
<ResizeHandle <ResizeHandle
className="-translate-x-3" className="-translate-x-3"
@@ -168,14 +172,14 @@ export default function Workspace() {
/> />
</> </>
)} )}
<HeaderSize data-tauri-drag-region style={head}> <HeaderSize data-tauri-drag-region className="x-theme-appHeader bg-background" style={head}>
<WorkspaceHeader className="pointer-events-none" /> <WorkspaceHeader className="pointer-events-none" />
</HeaderSize> </HeaderSize>
{activeWorkspace == null ? ( {activeWorkspace == null ? (
<div className="m-auto"> <div className="m-auto">
<Banner color="warning" className="max-w-[30rem]"> <Banner color="warning" className="max-w-[30rem]">
The active workspace{' '} The active workspace{' '}
<InlineCode className="text-orange-800">{activeWorkspaceId}</InlineCode> was not found. <InlineCode className="text-fg-warning">{activeWorkspaceId}</InlineCode> was not found.
Select a workspace from the header menu or report this bug to <FeedbackLink /> Select a workspace from the header menu or report this bug to <FeedbackLink />
</Banner> </Banner>
</div> </div>
@@ -214,10 +218,11 @@ function HeaderSize({ className, style, ...props }: HeaderSizeProps) {
const stoplightsVisible = platform?.osType === 'macos' && !fullscreen; const stoplightsVisible = platform?.osType === 'macos' && !fullscreen;
return ( return (
<div <div
data-tauri-drag-region
style={style} style={style}
className={classNames( className={classNames(
className, className,
'h-md pt-[1px] w-full border-b min-w-0', 'h-md pt-[1px] w-full border-b border-background-highlight min-w-0',
stoplightsVisible ? 'pl-20 pr-1' : 'pl-1', stoplightsVisible ? 'pl-20 pr-1' : 'pl-1',
)} )}
> >

View File

@@ -60,7 +60,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
> >
<Button <Button
className="focus" className="focus"
color="gray" color="primary"
onClick={async () => { onClick={async () => {
hide(); hide();
const environmentId = (await getRecentEnvironments(w.id))[0]; const environmentId = (await getRecentEnvironments(w.id))[0];
@@ -76,7 +76,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
</Button> </Button>
<Button <Button
className="focus" className="focus"
color="gray" color="secondary"
rightSlot={<Icon icon="externalLink" />} rightSlot={<Icon icon="externalLink" />}
onClick={async () => { onClick={async () => {
hide(); hide();
@@ -169,7 +169,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
size="sm" size="sm"
className={classNames( className={classNames(
className, className,
'text-gray-800 !px-2 truncate', 'text-fg !px-2 truncate',
activeWorkspace === null && 'italic opacity-disabled', activeWorkspace === null && 'italic opacity-disabled',
)} )}
{...buttonProps} {...buttonProps}

View File

@@ -7,11 +7,11 @@ import { Button } from './core/Button';
import { Icon } from './core/Icon'; import { Icon } from './core/Icon';
import { HStack } from './core/Stacks'; import { HStack } from './core/Stacks';
import { EnvironmentActionsDropdown } from './EnvironmentActionsDropdown'; import { EnvironmentActionsDropdown } from './EnvironmentActionsDropdown';
import { ImportCurlButton } from './ImportCurlButton';
import { RecentRequestsDropdown } from './RecentRequestsDropdown'; import { RecentRequestsDropdown } from './RecentRequestsDropdown';
import { SettingsDropdown } from './SettingsDropdown'; import { SettingsDropdown } from './SettingsDropdown';
import { SidebarActions } from './SidebarActions'; import { SidebarActions } from './SidebarActions';
import { WorkspaceActionsDropdown } from './WorkspaceActionsDropdown'; import { WorkspaceActionsDropdown } from './WorkspaceActionsDropdown';
import { ImportCurlButton } from './ImportCurlButton';
interface Props { interface Props {
className?: string; className?: string;
@@ -33,7 +33,7 @@ export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Prop
<CookieDropdown /> <CookieDropdown />
<HStack alignItems="center"> <HStack alignItems="center">
<WorkspaceActionsDropdown /> <WorkspaceActionsDropdown />
<Icon icon="chevronRight" className="text-gray-900 text-opacity-disabled" /> <Icon icon="chevronRight" className="text-fg-subtle" />
<EnvironmentActionsDropdown className="w-auto pointer-events-auto" /> <EnvironmentActionsDropdown className="w-auto pointer-events-auto" />
</HStack> </HStack>
</HStack> </HStack>
@@ -46,7 +46,8 @@ export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Prop
{(osInfo?.osType === 'linux' || osInfo?.osType === 'windows') && ( {(osInfo?.osType === 'linux' || osInfo?.osType === 'windows') && (
<HStack className="ml-4" alignItems="center"> <HStack className="ml-4" alignItems="center">
<Button <Button
className="px-4 !text-gray-600 rounded-none" className="px-4 text-fg-subtle hocus:text-fg hocus:bg-background-highlight-secondary rounded-none"
color="custom"
onClick={() => getCurrent().minimize()} onClick={() => getCurrent().minimize()}
> >
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
@@ -54,7 +55,8 @@ export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Prop
</svg> </svg>
</Button> </Button>
<Button <Button
className="px-4 !text-gray-600 rounded-none" className="px-4 text-fg-subtle hocus:text-fg hocus:bg-background-highlight rounded-none"
color="custom"
onClick={async () => { onClick={async () => {
const w = getCurrent(); const w = getCurrent();
await w.toggleMaximize(); await w.toggleMaximize();
@@ -76,7 +78,7 @@ export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Prop
</Button> </Button>
<Button <Button
color="custom" color="custom"
className="px-4 text-gray-600 rounded-none hocus:bg-red-200 hocus:text-gray-800" className="px-4 text-fg-subtle rounded-none hocus:bg-fg-danger hocus:text-fg"
onClick={() => getCurrent().close()} onClick={() => getCurrent().close()}
> >
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">

View File

@@ -4,19 +4,18 @@ import type { ReactNode } from 'react';
interface Props { interface Props {
children: ReactNode; children: ReactNode;
className?: string; className?: string;
color?: 'danger' | 'warning' | 'success' | 'gray'; color?: 'primary' | 'secondary' | 'success' | 'warning' | 'danger';
} }
export function Banner({ children, className, color = 'gray' }: Props) {
export function Banner({ children, className, color = 'secondary' }: Props) {
return ( return (
<div> <div>
<div <div
className={classNames( className={classNames(
className, className,
`x-theme-banner--${color}`,
'border border-dashed italic px-3 py-2 rounded select-auto cursor-text', 'border border-dashed italic px-3 py-2 rounded select-auto cursor-text',
color === 'gray' && 'border-gray-500/60 bg-gray-300/10 text-gray-800', 'border-background-highlight bg-background-highlight-secondary text-fg',
color === 'warning' && 'border-orange-500/60 bg-orange-300/10 text-orange-800',
color === 'danger' && 'border-red-500/60 bg-red-300/10 text-red-800',
color === 'success' && 'border-violet-500/60 bg-violet-300/10 text-violet-800',
)} )}
> >
{children} {children}

View File

@@ -7,7 +7,15 @@ import { Icon } from './Icon';
export type ButtonProps = Omit<HTMLAttributes<HTMLButtonElement>, 'color'> & { export type ButtonProps = Omit<HTMLAttributes<HTMLButtonElement>, 'color'> & {
innerClassName?: string; innerClassName?: string;
color?: 'custom' | 'default' | 'gray' | 'primary' | 'secondary' | 'warning' | 'danger'; color?:
| 'custom'
| 'default'
| 'secondary'
| 'primary'
| 'info'
| 'success'
| 'warning'
| 'danger';
variant?: 'border' | 'solid'; variant?: 'border' | 'solid';
isLoading?: boolean; isLoading?: boolean;
size?: 'xs' | 'sm' | 'md'; size?: 'xs' | 'sm' | 'md';
@@ -48,6 +56,10 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
const classes = classNames( const classes = classNames(
className, className,
'x-theme-button',
`x-theme-button--${variant}`,
`x-theme-button--${variant}--${color}`,
'text-fg',
'max-w-full min-w-0', // Help with truncation 'max-w-full min-w-0', // Help with truncation
'hocus:opacity-100', // Force opacity for certain hover effects 'hocus:opacity-100', // Force opacity for certain hover effects
'whitespace-nowrap outline-none', 'whitespace-nowrap outline-none',
@@ -59,46 +71,26 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
size === 'md' && 'h-md px-3', size === 'md' && 'h-md px-3',
size === 'sm' && 'h-sm px-2.5 text-sm', size === 'sm' && 'h-sm px-2.5 text-sm',
size === 'xs' && 'h-xs px-2 text-sm', size === 'xs' && 'h-xs px-2 text-sm',
// Solids // Solids
variant === 'solid' &&
color !== 'custom' &&
color !== 'default' &&
'bg-background enabled:hocus:bg-background-highlight ring-background-highlight-secondary',
variant === 'solid' && color === 'custom' && 'ring-blue-400', variant === 'solid' && color === 'custom' && 'ring-blue-400',
variant === 'solid' && variant === 'solid' &&
color === 'default' && color === 'default' &&
'text-gray-700 enabled:hocus:bg-gray-700/10 enabled:hocus:text-gray-800 ring-blue-400', 'enabled:hocus:bg-background-highlight ring-fg-info',
variant === 'solid' &&
color === 'gray' &&
'text-gray-800 bg-gray-200/70 enabled:hocus:bg-gray-200 ring-blue-400',
variant === 'solid' &&
color === 'primary' &&
'bg-blue-400 text-white ring-blue-700 enabled:hocus:bg-blue-500',
variant === 'solid' &&
color === 'secondary' &&
'bg-violet-400 text-white ring-violet-700 enabled:hocus:bg-violet-500',
variant === 'solid' &&
color === 'warning' &&
'bg-orange-400 text-white ring-orange-700 enabled:hocus:bg-orange-500',
variant === 'solid' &&
color === 'danger' &&
'bg-red-400 text-white ring-red-700 enabled:hocus:bg-red-500',
// Borders // Borders
variant === 'border' && 'border', variant === 'border' && 'border',
variant === 'border' &&
color !== 'custom' &&
color !== 'default' &&
'border-fg-subtler text-fg-subtle enabled:hocus:border-fg-subtle enabled:hocus:bg-background-highlight enabled:hocus:text-fg ring-fg-subtler',
variant === 'border' && variant === 'border' &&
color === 'default' && color === 'default' &&
'border-highlight text-gray-700 enabled:hocus:border-focus enabled:hocus:text-gray-800 ring-blue-500/50', 'border-background-highlight enabled:hocus:border-fg-subtler enabled:hocus:bg-background-highlight-secondary',
variant === 'border' &&
color === 'gray' &&
'border-gray-500/70 text-gray-700 enabled:hocus:bg-gray-500/20 enabled:hocus:text-gray-800 ring-blue-500/50',
variant === 'border' &&
color === 'primary' &&
'border-blue-500/70 text-blue-700 enabled:hocus:border-blue-500 ring-blue-500/50',
variant === 'border' &&
color === 'secondary' &&
'border-violet-500/70 text-violet-700 enabled:hocus:border-violet-500 ring-violet-500/50',
variant === 'border' &&
color === 'warning' &&
'border-orange-500/70 text-orange-700 enabled:hocus:border-orange-500 ring-orange-500/50',
variant === 'border' &&
color === 'danger' &&
'border-red-500/70 text-red-700 enabled:hocus:border-red-500 ring-red-500/50',
); );
const buttonRef = useRef<HTMLButtonElement>(null); const buttonRef = useRef<HTMLButtonElement>(null);

View File

@@ -28,14 +28,14 @@ export function Checkbox({
as="label" as="label"
space={2} space={2}
alignItems="center" alignItems="center"
className={classNames(className, 'text-gray-900 text-sm', disabled && 'opacity-disabled')} className={classNames(className, 'text-fg text-sm', disabled && 'opacity-disabled')}
> >
<div className={classNames(inputWrapperClassName, 'relative flex')}> <div className={classNames(inputWrapperClassName, 'relative flex')}>
<input <input
aria-hidden aria-hidden
className={classNames( className={classNames(
'appearance-none w-4 h-4 flex-shrink-0 border border-highlight', 'appearance-none w-4 h-4 flex-shrink-0 border border-background-highlight',
'rounded hocus:border-focus hocus:bg-focus/[5%] outline-none ring-0', 'rounded hocus:border-border-focus hocus:bg-focus/[5%] outline-none ring-0',
)} )}
type="checkbox" type="checkbox"
disabled={disabled} disabled={disabled}

View File

@@ -12,7 +12,7 @@ export function CountBadge({ count, className }: Props) {
aria-hidden aria-hidden
className={classNames( className={classNames(
className, className,
'opacity-70 border border-highlight text-4xs rounded mb-0.5 px-1 ml-1 h-4 font-mono', 'opacity-70 border border-background-highlight-secondary text-4xs rounded mb-0.5 px-1 ml-1 h-4 font-mono',
)} )}
> >
{count} {count}

View File

@@ -50,7 +50,7 @@ export function Dialog({
return ( return (
<Overlay open={open} onClose={onClose} portalName="dialog"> <Overlay open={open} onClose={onClose} portalName="dialog">
<div className="absolute inset-0 flex items-center justify-center pointer-events-none"> <div className="x-theme-dialog absolute inset-0 flex items-center justify-center pointer-events-none">
<div <div
role="dialog" role="dialog"
aria-labelledby={titleId} aria-labelledby={titleId}
@@ -63,9 +63,9 @@ export function Dialog({
className={classNames( className={classNames(
className, className,
'grid grid-rows-[auto_auto_minmax(0,1fr)]', 'grid grid-rows-[auto_auto_minmax(0,1fr)]',
'relative bg-gray-50 pointer-events-auto', 'relative bg-background pointer-events-auto',
'rounded-lg', 'rounded-lg',
'dark:border border-highlight shadow shadow-black/10', 'border border-background-highlight shadow-lg shadow-[rgba(0,0,0,0.1)]',
'max-w-[calc(100vw-5rem)] max-h-[calc(100vh-6rem)]', 'max-w-[calc(100vw-5rem)] max-h-[calc(100vh-6rem)]',
size === 'sm' && 'w-[25rem] max-h-[80vh]', size === 'sm' && 'w-[25rem] max-h-[80vh]',
size === 'md' && 'w-[45rem] max-h-[80vh]', size === 'md' && 'w-[45rem] max-h-[80vh]',
@@ -83,7 +83,7 @@ export function Dialog({
)} )}
{description ? ( {description ? (
<p className="px-6 text-gray-700" id={descriptionId}> <p className="px-6 text-fg-subtle" id={descriptionId}>
{description} {description}
</p> </p>
) : ( ) : (

View File

@@ -23,14 +23,14 @@ import React, {
import { useKey, useWindowSize } from 'react-use'; import { useKey, useWindowSize } from 'react-use';
import type { HotkeyAction } from '../../hooks/useHotKey'; import type { HotkeyAction } from '../../hooks/useHotKey';
import { useHotKey } from '../../hooks/useHotKey'; import { useHotKey } from '../../hooks/useHotKey';
import { useStateWithDeps } from '../../hooks/useStateWithDeps';
import { getNodeText } from '../../lib/getNodeText'; import { getNodeText } from '../../lib/getNodeText';
import { Overlay } from '../Overlay'; import { Overlay } from '../Overlay';
import { Button } from './Button'; import { Button } from './Button';
import { HotKey } from './HotKey'; import { HotKey } from './HotKey';
import { Icon } from './Icon';
import { Separator } from './Separator'; import { Separator } from './Separator';
import { HStack, VStack } from './Stacks'; import { HStack, VStack } from './Stacks';
import { Icon } from './Icon';
import { useStateWithDeps } from '../../hooks/useStateWithDeps';
export type DropdownItemSeparator = { export type DropdownItemSeparator = {
type: 'separator'; type: 'separator';
@@ -413,7 +413,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
)} )}
{isOpen && ( {isOpen && (
<Overlay open variant="transparent" portalName="dropdown" zIndex={50}> <Overlay open variant="transparent" portalName="dropdown" zIndex={50}>
<div> <div className="x-theme-dialog">
<div tabIndex={-1} aria-hidden className="fixed inset-0 z-30" onClick={handleClose} /> <div tabIndex={-1} aria-hidden className="fixed inset-0 z-30" onClick={handleClose} />
<motion.div <motion.div
tabIndex={0} tabIndex={0}
@@ -431,7 +431,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
<span <span
aria-hidden aria-hidden
style={triangleStyles} style={triangleStyles}
className="bg-gray-50 absolute rotate-45 border-gray-200 border-t border-l" className="bg-background absolute rotate-45 border-background-highlight border-t border-l"
/> />
)} )}
{containerStyles && ( {containerStyles && (
@@ -440,22 +440,24 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
style={menuStyles} style={menuStyles}
className={classNames( className={classNames(
className, className,
'h-auto bg-gray-50 rounded-md shadow-lg dark:shadow-gray-0 py-1.5 border', 'h-auto bg-background rounded-md shadow-lg py-1.5 border',
'border-gray-200 overflow-auto mb-1 mx-0.5', 'border-background-highlight overflow-auto mb-1 mx-0.5',
)} )}
> >
{filter && ( {filter && (
<HStack <HStack
space={2} space={2}
alignItems="center" alignItems="center"
className="pb-0.5 px-1.5 mb-2 text-xs border border-highlight mx-2 rounded font-mono h-2xs" className="pb-0.5 px-1.5 mb-2 text-xs border border-background-highlight-secondary mx-2 rounded font-mono h-2xs"
> >
<Icon icon="search" size="xs" className="text-gray-700" /> <Icon icon="search" size="xs" className="text-fg-subtle" />
<div className="text-gray-800">{filter}</div> <div className="text-fg">{filter}</div>
</HStack> </HStack>
)} )}
{filteredItems.length === 0 && ( {filteredItems.length === 0 && (
<span className="text-gray-500 text-sm text-center px-2 py-1">No matches</span> <span className="text-fg-subtler text-sm text-center px-2 py-1">
No matches
</span>
)} )}
{filteredItems.map((item, i) => { {filteredItems.map((item, i) => {
if (item.type === 'separator') { if (item.type === 'separator') {
@@ -531,15 +533,17 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men
justify="start" justify="start"
leftSlot={item.leftSlot && <div className="pr-2 flex justify-start">{item.leftSlot}</div>} leftSlot={item.leftSlot && <div className="pr-2 flex justify-start">{item.leftSlot}</div>}
rightSlot={rightSlot && <div className="ml-auto pl-3">{rightSlot}</div>} rightSlot={rightSlot && <div className="ml-auto pl-3">{rightSlot}</div>}
innerClassName="!text-left"
color="custom"
className={classNames( className={classNames(
className, className,
'h-xs', // More compact 'h-xs', // More compact
'min-w-[8rem] outline-none px-2 mx-1.5 flex text-sm text-gray-700 whitespace-nowrap', 'min-w-[8rem] outline-none px-2 mx-1.5 flex text-sm whitespace-nowrap',
'focus:bg-highlight focus:text-gray-800 rounded', 'focus:bg-background-highlight focus:text-fg rounded',
item.variant === 'danger' && 'text-red-600', item.variant === 'default' && 'text-fg-subtle',
item.variant === 'notify' && 'text-pink-600', item.variant === 'danger' && 'text-fg-danger',
item.variant === 'notify' && 'text-fg-primary',
)} )}
innerClassName="!text-left"
{...props} {...props}
> >
<div <div

View File

@@ -5,7 +5,10 @@ interface Props {
export function DurationTag({ total, headers }: Props) { export function DurationTag({ total, headers }: Props) {
return ( return (
<span title={`HEADER: ${formatMillis(headers)}\nTOTAL: ${formatMillis(total)}`}> <span
className="font-mono"
title={`HEADER: ${formatMillis(headers)}\nTOTAL: ${formatMillis(total)}`}
>
{formatMillis(total)} {formatMillis(total)}
</span> </span>
); );

View File

@@ -5,7 +5,9 @@
@apply w-full block text-base; @apply w-full block text-base;
.cm-cursor { .cm-cursor {
@apply border-gray-800 !important; @apply border-fg !important;
/* Widen the cursor */
@apply border-l-2;
} }
&.cm-focused { &.cm-focused {
@@ -17,7 +19,7 @@
} }
.cm-line { .cm-line {
@apply text-gray-800 pl-1 pr-1.5; @apply text-fg pl-1 pr-1.5;
} }
.cm-placeholder { .cm-placeholder {
@@ -47,7 +49,7 @@
/* Style gutters */ /* Style gutters */
.cm-gutters { .cm-gutters {
@apply border-0 text-gray-500/50; @apply border-0 text-fg-subtler bg-transparent;
.cm-gutterElement { .cm-gutterElement {
@apply cursor-default; @apply cursor-default;
@@ -55,19 +57,13 @@
} }
.placeholder-widget { .placeholder-widget {
@apply text-xs text-violet-700 dark:text-violet-700 px-1 mx-[0.5px] rounded cursor-default dark:shadow; /* Colors */
@apply bg-background text-fg-subtle border-background-highlight-secondary;
@apply hover:border-background-highlight hover:text-fg hover:bg-background-highlight-secondary;
/* NOTE: Background and border are translucent so we can see text selection through it */ @apply border px-1 mx-[0.5px] rounded cursor-default dark:shadow;
@apply bg-violet-500/20 border border-violet-500/20 border-opacity-40;
/* Bring above on hover */
@apply hover:z-10 relative;
-webkit-text-security: none; -webkit-text-security: none;
&.placeholder-widget-error {
@apply text-red-700 dark:text-red-800 bg-red-300/30 border-red-300/80 border-opacity-40 hover:border-red-300 hover:bg-red-300/40;
}
} }
.hyperlink-widget { .hyperlink-widget {
@@ -137,7 +133,7 @@
.cm-editor .fold-gutter-icon::after { .cm-editor .fold-gutter-icon::after {
@apply block w-1.5 h-1.5 border-transparent -rotate-45 @apply block w-1.5 h-1.5 border-transparent -rotate-45
border-l border-b border-l-[currentColor] border-b-[currentColor] content-['']; border-l border-b border-l-[currentColor] border-b-[currentColor] content-[''];
} }
.cm-editor .fold-gutter-icon[data-open] { .cm-editor .fold-gutter-icon[data-open] {
@@ -149,12 +145,12 @@
} }
.cm-editor .fold-gutter-icon:hover { .cm-editor .fold-gutter-icon:hover {
@apply text-gray-900 bg-gray-300/50; @apply text-fg bg-background-highlight;
} }
.cm-editor .cm-foldPlaceholder { .cm-editor .cm-foldPlaceholder {
@apply px-2 border border-gray-400/50 bg-gray-300/50; @apply px-2 border border-fg-subtler bg-background-highlight;
@apply hover:text-gray-800 hover:border-gray-400; @apply hover:text-fg hover:border-fg-subtle;
@apply cursor-default !important; @apply cursor-default !important;
} }
@@ -164,11 +160,13 @@
.cm-wrapper:not(.cm-readonly) .cm-editor { .cm-wrapper:not(.cm-readonly) .cm-editor {
&.cm-focused .cm-activeLineGutter { &.cm-focused .cm-activeLineGutter {
@apply text-gray-600; @apply text-fg-subtle;
} }
}
.cm-wrapper.cm-readonly .cm-editor {
.cm-cursor { .cm-cursor {
@apply border-l-2 border-gray-800; @apply border-fg-danger !important;
} }
} }
@@ -187,18 +185,18 @@
} }
.cm-tooltip.cm-tooltip-hover { .cm-tooltip.cm-tooltip-hover {
@apply shadow-lg bg-gray-100 rounded text-gray-700 border border-gray-500 z-50 pointer-events-auto text-xs; @apply shadow-lg bg-background rounded text-fg-subtle border border-fg-subtler z-50 pointer-events-auto text-xs;
@apply px-2 py-1; @apply px-2 py-1;
a { a {
@apply text-gray-800; @apply text-fg;
&:hover { &:hover {
@apply underline; @apply underline;
} }
&::after { &::after {
@apply text-gray-800 bg-gray-800 h-3 w-3 ml-1; @apply text-fg bg-fg-secondary h-3 w-3 ml-1;
content: ''; content: '';
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' fill='black' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5z'/%3E%3Cpath fill-rule='evenodd' d='M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0v-5z'/%3E%3C/svg%3E"); -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' fill='black' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5z'/%3E%3Cpath fill-rule='evenodd' d='M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0v-5z'/%3E%3C/svg%3E");
-webkit-mask-size: contain; -webkit-mask-size: contain;
@@ -210,7 +208,7 @@
/* NOTE: Extra selector required to override default styles */ /* NOTE: Extra selector required to override default styles */
.cm-tooltip.cm-tooltip-autocomplete, .cm-tooltip.cm-tooltip-autocomplete,
.cm-tooltip.cm-completionInfo { .cm-tooltip.cm-completionInfo {
@apply shadow-lg bg-gray-100 rounded text-gray-700 border border-gray-300 z-50 pointer-events-auto text-xs; @apply shadow-lg bg-background rounded text-fg-subtle border border-background-highlight z-50 pointer-events-auto text-xs;
.cm-completionIcon { .cm-completionIcon {
@apply italic font-mono; @apply italic font-mono;
@@ -286,11 +284,11 @@
} }
& > ul > li { & > ul > li {
@apply cursor-default px-2 rounded-sm text-gray-600 h-7 flex items-center; @apply cursor-default px-2 rounded-sm text-fg-subtle h-7 flex items-center;
} }
& > ul > li[aria-selected] { & > ul > li[aria-selected] {
@apply bg-highlight text-gray-900; @apply bg-background-highlight-secondary text-fg;
} }
.cm-completionIcon { .cm-completionIcon {
@@ -298,7 +296,7 @@
} }
.cm-completionLabel { .cm-completionLabel {
@apply text-gray-700; @apply text-fg-subtle;
} }
.cm-completionDetail { .cm-completionDetail {
@@ -308,7 +306,7 @@
} }
.cm-editor .cm-panels { .cm-editor .cm-panels {
@apply bg-gray-100 backdrop-blur-sm p-1 mb-1 text-gray-800 z-20 rounded-md; @apply bg-background-highlight-secondary backdrop-blur-sm p-1 mb-1 text-fg z-20 rounded-md;
input, input,
button { button {
@@ -316,19 +314,21 @@
} }
button { button {
@apply appearance-none bg-none bg-gray-200 hocus:bg-gray-300 hocus:text-gray-950 border-0 text-gray-800 cursor-default; @apply border-fg-subtler bg-background-highlight text-fg hover:border-fg-info;
@apply appearance-none bg-none cursor-default;
} }
button[name='close'] { button[name='close'] {
@apply text-gray-600 hocus:text-gray-900 px-2 -mr-1.5 !important; @apply text-fg-subtle hocus:text-fg px-2 -mr-1.5 !important;
} }
input { input {
@apply bg-gray-50 border border-gray-500/50 focus:border-focus outline-none; @apply bg-background border-background-highlight focus:border-border-focus;
@apply border outline-none cursor-text;
} }
label { label {
@apply focus-within:text-gray-950; @apply focus-within:text-fg;
} }
/* Hide the "All" button */ /* Hide the "All" button */

View File

@@ -206,7 +206,6 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
view = new EditorView({ state, parent: container }); view = new EditorView({ state, parent: container });
cm.current = { view, languageCompartment }; cm.current = { view, languageCompartment };
syncGutterBg({ parent: container, bgClassList });
if (autoFocus) { if (autoFocus) {
view.focus(); view.focus();
} }
@@ -270,7 +269,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
ref={initEditorRef} ref={initEditorRef}
className={classNames( className={classNames(
className, className,
'cm-wrapper text-base bg-gray-50', 'cm-wrapper text-base',
type === 'password' && 'cm-obscure-text', type === 'password' && 'cm-obscure-text',
heightMode === 'auto' ? 'cm-auto-height' : 'cm-full-height', heightMode === 'auto' ? 'cm-auto-height' : 'cm-full-height',
singleLine ? 'cm-singleline' : 'cm-multiline', singleLine ? 'cm-singleline' : 'cm-multiline',
@@ -361,19 +360,6 @@ function getExtensions({
]; ];
} }
const syncGutterBg = ({
parent,
bgClassList,
}: {
parent: HTMLDivElement;
bgClassList: string[];
}) => {
const gutterEl = parent.querySelector<HTMLDivElement>('.cm-gutters');
if (gutterEl) {
gutterEl?.classList.add(...bgClassList);
}
};
const placeholderElFromText = (text: string) => { const placeholderElFromText = (text: string) => {
const el = document.createElement('div'); const el = document.createElement('div');
el.innerHTML = text.replace('\n', '<br/>'); el.innerHTML = text.replace('\n', '<br/>');

View File

@@ -42,23 +42,23 @@ import { url } from './url/extension';
export const myHighlightStyle = HighlightStyle.define([ export const myHighlightStyle = HighlightStyle.define([
{ {
tag: [t.documentMeta, t.blockComment, t.lineComment, t.docComment, t.comment], tag: [t.documentMeta, t.blockComment, t.lineComment, t.docComment, t.comment],
color: 'hsl(var(--color-gray-600))', color: 'var(--fg-subtler)',
fontStyle: 'italic', fontStyle: 'italic',
}, },
{ {
tag: [t.paren], tag: [t.paren],
color: 'hsl(var(--color-gray-900))', color: 'var(--fg)',
}, },
{ {
tag: [t.name, t.tagName, t.angleBracket, t.docString, t.number], tag: [t.name, t.tagName, t.angleBracket, t.docString, t.number],
color: 'hsl(var(--color-blue-600))', color: 'var(--fg-info)',
}, },
{ tag: [t.variableName], color: 'hsl(var(--color-green-600))' }, { tag: [t.variableName], color: 'var(--fg-success)' },
{ tag: [t.bool], color: 'hsl(var(--color-pink-600))' }, { tag: [t.bool], color: 'var(--fg-info)' }, // TODO: Should be pink
{ tag: [t.attributeName, t.propertyName], color: 'hsl(var(--color-violet-600))' }, { tag: [t.attributeName, t.propertyName], color: 'var(--fg-primary)' },
{ tag: [t.attributeValue], color: 'hsl(var(--color-orange-600))' }, { tag: [t.attributeValue], color: 'var(--fg-warning)' },
{ tag: [t.string], color: 'hsl(var(--color-yellow-600))' }, { tag: [t.string], color: 'var(--fg-warning)' }, // TODO: Should be yellow
{ tag: [t.keyword, t.meta, t.operator], color: 'hsl(var(--color-red-600))' }, { tag: [t.keyword, t.meta, t.operator], color: 'var(--fg-danger)' },
]); ]);
const myTheme = EditorView.theme({}, { dark: true }); const myTheme = EditorView.theme({}, { dark: true });
@@ -123,6 +123,7 @@ export const baseExtensions = [
dropCursor(), dropCursor(),
drawSelection(), drawSelection(),
autocompletion({ autocompletion({
tooltipClass: () => 'x-theme-dialog',
closeOnBlur: true, // Set to `false` for debugging in devtools without closing it closeOnBlur: true, // Set to `false` for debugging in devtools without closing it
compareCompletions: (a, b) => { compareCompletions: (a, b) => {
// Don't sort completions at all, only on boost // Don't sort completions at all, only on boost

View File

@@ -11,8 +11,10 @@ class PlaceholderWidget extends WidgetType {
} }
toDOM() { toDOM() {
const elt = document.createElement('span'); const elt = document.createElement('span');
elt.className = `placeholder-widget ${ elt.className = `x-theme-placeholder-widget placeholder-widget ${
!this.isExistingVariable ? 'placeholder-widget-error' : '' this.isExistingVariable
? 'x-theme-placeholder-widget--primary'
: 'x-theme-placeholder-widget--danger'
}`; }`;
elt.title = !this.isExistingVariable ? 'Variable not found in active environment' : ''; elt.title = !this.isExistingVariable ? 'Variable not found in active environment' : '';
elt.textContent = this.name; elt.textContent = this.name;

View File

@@ -10,7 +10,7 @@ export function FormattedError({ children }: Props) {
<pre <pre
className={classNames( className={classNames(
'w-full text-sm select-auto cursor-text bg-gray-100 p-3 rounded', 'w-full text-sm select-auto cursor-text bg-gray-100 p-3 rounded',
'whitespace-pre-wrap border border-red-500 border-dashed overflow-x-auto', 'whitespace-pre-wrap border border-fg-danger border-dashed overflow-x-auto',
)} )}
> >
{children} {children}

View File

@@ -11,7 +11,7 @@ export function Heading({ className, size = 1, ...props }: Props) {
<Component <Component
className={classNames( className={classNames(
className, className,
'font-semibold text-gray-900', 'font-semibold text-fg',
size === 1 && 'text-2xl', size === 1 && 'text-2xl',
size === 2 && 'text-xl', size === 2 && 'text-xl',
size === 3 && 'text-lg', size === 3 && 'text-lg',

View File

@@ -22,7 +22,7 @@ export function HotKey({ action, className, variant }: Props) {
className={classNames( className={classNames(
className, className,
variant === 'with-bg' && 'rounded border', variant === 'with-bg' && 'rounded border',
'text-gray-1000 text-opacity-disabled', 'text-fg-subtler',
)} )}
> >
{labelParts.map((char, index) => ( {labelParts.map((char, index) => (

View File

@@ -7,5 +7,5 @@ interface Props {
export function HotKeyLabel({ action }: Props) { export function HotKeyLabel({ action }: Props) {
const label = useHotKeyLabel(action); const label = useHotKeyLabel(action);
return <span>{label}</span>; return <span className="text-fg-subtle">{label}</span>;
} }

View File

@@ -11,7 +11,7 @@ interface Props {
export const HotKeyList = ({ hotkeys, bottomSlot }: Props) => { export const HotKeyList = ({ hotkeys, bottomSlot }: Props) => {
return ( return (
<div className="h-full flex items-center justify-center text-gray-700 text-sm"> <div className="h-full flex items-center justify-center text-sm">
<VStack space={2}> <VStack space={2}>
{hotkeys.map((hotkey) => ( {hotkeys.map((hotkey) => (
<HStack key={hotkey} className="grid grid-cols-2"> <HStack key={hotkey} className="grid grid-cols-2">

View File

@@ -27,7 +27,7 @@ export function HttpMethodTag({ request, className }: Props) {
const m = method.toLowerCase(); const m = method.toLowerCase();
return ( return (
<span className={classNames(className, 'text-2xs font-mono opacity-50')}> <span className={classNames(className, 'text-2xs font-mono text-fg-subtle')}>
{methodMap[m] ?? m.slice(0, 3).toUpperCase()} {methodMap[m] ?? m.slice(0, 3).toUpperCase()}
</span> </span>
); );

View File

@@ -1,5 +1,5 @@
import * as lucide from 'lucide-react';
import classNames from 'classnames'; import classNames from 'classnames';
import * as lucide from 'lucide-react';
import type { HTMLAttributes } from 'react'; import type { HTMLAttributes } from 'react';
import { memo } from 'react'; import { memo } from 'react';
@@ -44,6 +44,7 @@ const icons = {
moreVertical: lucide.MoreVerticalIcon, moreVertical: lucide.MoreVerticalIcon,
paste: lucide.ClipboardPasteIcon, paste: lucide.ClipboardPasteIcon,
pencil: lucide.PencilIcon, pencil: lucide.PencilIcon,
pin: lucide.PinIcon,
plug: lucide.Plug, plug: lucide.Plug,
plus: lucide.PlusIcon, plus: lucide.PlusIcon,
plusCircle: lucide.PlusCircleIcon, plusCircle: lucide.PlusCircleIcon,

View File

@@ -52,7 +52,7 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(function IconButt
size={size} size={size}
className={classNames( className={classNames(
className, className,
'relative flex-shrink-0 text-gray-700 hover:text-gray-1000', 'group/button relative flex-shrink-0 text-fg-subtle',
'!px-0', '!px-0',
size === 'md' && 'w-9', size === 'md' && 'w-9',
size === 'sm' && 'w-8', size === 'sm' && 'w-8',
@@ -71,6 +71,7 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(function IconButt
spin={spin} spin={spin}
className={classNames( className={classNames(
iconClassName, iconClassName,
'group-hover/button:text-fg',
props.disabled && 'opacity-70', props.disabled && 'opacity-70',
confirmed && 'text-green-600', confirmed && 'text-green-600',
)} )}

View File

@@ -6,8 +6,8 @@ export function InlineCode({ className, ...props }: HTMLAttributes<HTMLSpanEleme
<code <code
className={classNames( className={classNames(
className, className,
'font-mono text-xs bg-highlight border-0 border-gray-200/30', 'font-mono text-xs bg-background-highlight-secondary',
'px-1.5 py-0.5 rounded text-gray-800 shadow-inner', 'px-1.5 py-0.5 rounded text-fg shadow-inner',
)} )}
{...props} {...props}
/> />

View File

@@ -135,7 +135,7 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
htmlFor={id} htmlFor={id}
className={classNames( className={classNames(
labelClassName, labelClassName,
'text-sm text-gray-900 whitespace-nowrap', 'text-sm text-fg whitespace-nowrap',
hideLabel && 'sr-only', hideLabel && 'sr-only',
)} )}
> >
@@ -145,10 +145,10 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
alignItems="stretch" alignItems="stretch"
className={classNames( className={classNames(
containerClassName, containerClassName,
'relative w-full rounded-md text-gray-900', 'relative w-full rounded-md text-fg',
'border', 'border',
focused ? 'border-focus' : 'border-highlight', focused ? 'border-border-focus' : 'border-background-highlight',
!isValid && '!border-invalid', !isValid && '!border-fg-danger',
size === 'md' && 'min-h-md', size === 'md' && 'min-h-md',
size === 'sm' && 'min-h-sm', size === 'sm' && 'min-h-sm',
size === 'xs' && 'min-h-xs', size === 'xs' && 'min-h-xs',
@@ -186,7 +186,7 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
title={obscured ? `Show ${label}` : `Obscure ${label}`} title={obscured ? `Show ${label}` : `Obscure ${label}`}
size="xs" size="xs"
className="mr-0.5 group/obscure !h-auto my-0.5" className="mr-0.5 group/obscure !h-auto my-0.5"
iconClassName="text-gray-500 group-hover/obscure:text-gray-800" iconClassName="text-fg-subtle group-hover/obscure:text-fg"
iconSize="sm" iconSize="sm"
icon={obscured ? 'eye' : 'eyeClosed'} icon={obscured ? 'eye' : 'eyeClosed'}
onClick={() => setObscured((o) => !o)} onClick={() => setObscured((o) => !o)}

View File

@@ -41,7 +41,7 @@ export const JsonAttributeTree = ({ depth = 0, attrKey, attrValue, attrKeyJsonPa
: null, : null,
isExpandable: Object.keys(attrValue).length > 0, isExpandable: Object.keys(attrValue).length > 0,
label: isExpanded ? `{${Object.keys(attrValue).length || ' '}}` : `{⋯}`, label: isExpanded ? `{${Object.keys(attrValue).length || ' '}}` : `{⋯}`,
labelClassName: 'text-gray-600', labelClassName: 'text-fg-subtler',
}; };
} else if (jsonType === '[object Array]') { } else if (jsonType === '[object Array]') {
return { return {
@@ -59,7 +59,7 @@ export const JsonAttributeTree = ({ depth = 0, attrKey, attrValue, attrKeyJsonPa
: null, : null,
isExpandable: attrValue.length > 0, isExpandable: attrValue.length > 0,
label: isExpanded ? `[${attrValue.length || ' '}]` : `[⋯]`, label: isExpanded ? `[${attrValue.length || ' '}]` : `[⋯]`,
labelClassName: 'text-gray-600', labelClassName: 'text-subtler',
}; };
} else { } else {
return { return {
@@ -67,19 +67,17 @@ export const JsonAttributeTree = ({ depth = 0, attrKey, attrValue, attrKeyJsonPa
isExpandable: false, isExpandable: false,
label: jsonType === '[object String]' ? `"${attrValue}"` : `${attrValue}`, label: jsonType === '[object String]' ? `"${attrValue}"` : `${attrValue}`,
labelClassName: classNames( labelClassName: classNames(
jsonType === '[object Boolean]' && 'text-pink-600', jsonType === '[object Boolean]' && 'text-fg-primary',
jsonType === '[object Number]' && 'text-blue-600', jsonType === '[object Number]' && 'text-fg-info',
jsonType === '[object String]' && 'text-yellow-600', jsonType === '[object String]' && 'text-fg-notice',
jsonType === '[object Null]' && 'text-red-600', jsonType === '[object Null]' && 'text-fg-danger',
), ),
}; };
} }
}, [attrValue, attrKeyJsonPath, isExpanded, depth]); }, [attrValue, attrKeyJsonPath, isExpanded, depth]);
const labelEl = ( const labelEl = (
<span className={classNames(labelClassName, 'select-text group-hover:text-gray-800')}> <span className={classNames(labelClassName, 'select-text group-hover:text-fg')}>{label}</span>
{label}
</span>
); );
return ( return (
<div className={classNames(/*depth === 0 && '-ml-4',*/ 'font-mono text-2xs')}> <div className={classNames(/*depth === 0 && '-ml-4',*/ 'font-mono text-2xs')}>
@@ -91,18 +89,18 @@ export const JsonAttributeTree = ({ depth = 0, attrKey, attrValue, attrKeyJsonPa
icon="chevronRight" icon="chevronRight"
className={classNames( className={classNames(
'left-0 absolute transition-transform flex items-center', 'left-0 absolute transition-transform flex items-center',
'text-gray-600 group-hover:text-gray-900', 'text-fg-subtler group-hover:text-fg-subtle',
isExpanded ? 'rotate-90' : '', isExpanded ? 'rotate-90' : '',
)} )}
/> />
<span className="text-violet-600 group-hover:text-violet-700 mr-1.5 whitespace-nowrap"> <span className="text-fg-primary group-hover:text-fg-primary mr-1.5 whitespace-nowrap">
{attrKey === undefined ? '$' : attrKey}: {attrKey === undefined ? '$' : attrKey}:
</span> </span>
{labelEl} {labelEl}
</button> </button>
) : ( ) : (
<> <>
<span className="text-violet-600 mr-1.5 pl-4 whitespace-nowrap select-text"> <span className="text-fg-primary mr-1.5 pl-4 whitespace-nowrap select-text">
{attrKey}: {attrKey}:
</span> </span>
{labelEl} {labelEl}

View File

@@ -30,7 +30,7 @@ export function KeyValueRow({ label, value, labelClassName }: Props) {
return ( return (
<> <>
<td <td
className={classNames('py-0.5 pr-2 text-gray-700 select-text cursor-text', labelClassName)} className={classNames('py-0.5 pr-2 text-fg-subtle select-text cursor-text', labelClassName)}
> >
{label} {label}
</td> </td>

View File

@@ -388,7 +388,7 @@ function PairEditorRow({
{pairContainer.pair.isFile ? ( {pairContainer.pair.isFile ? (
<Button <Button
size="xs" size="xs"
color="gray" color="secondary"
className="font-mono text-xs" className="font-mono text-xs"
onClick={async (e) => { onClick={async (e) => {
e.preventDefault(); e.preventDefault();

View File

@@ -40,7 +40,7 @@ export function Select<T extends string>({
htmlFor={id} htmlFor={id}
className={classNames( className={classNames(
labelClassName, labelClassName,
'text-sm text-gray-900 whitespace-nowrap', 'text-sm text-fg whitespace-nowrap',
hideLabel && 'sr-only', hideLabel && 'sr-only',
)} )}
> >
@@ -52,7 +52,7 @@ export function Select<T extends string>({
onChange={(e) => onChange(e.target.value as T)} onChange={(e) => onChange(e.target.value as T)}
className={classNames( className={classNames(
'font-mono text-xs border w-full outline-none bg-transparent pl-2 pr-7', 'font-mono text-xs border w-full outline-none bg-transparent pl-2 pr-7',
'border-highlight focus:border-focus', 'bg-background-highlight-secondary border-background-highlight focus:border-border-focus',
size === 'xs' && 'h-xs', size === 'xs' && 'h-xs',
size === 'sm' && 'h-sm', size === 'sm' && 'h-sm',
size === 'md' && 'h-md', size === 'md' && 'h-md',

View File

@@ -8,19 +8,13 @@ interface Props {
children?: ReactNode; children?: ReactNode;
} }
export function Separator({ export function Separator({ className, orientation = 'horizontal', children }: Props) {
className,
variant = 'primary',
orientation = 'horizontal',
children,
}: Props) {
return ( return (
<div role="separator" className={classNames(className, 'flex items-center')}> <div role="separator" className={classNames(className, 'flex items-center')}>
{children && <div className="text-xs text-gray-500 mr-2 whitespace-nowrap">{children}</div>} {children && <div className="text-xs text-fg-subtler mr-2 whitespace-nowrap">{children}</div>}
<div <div
className={classNames( className={classNames(
variant === 'primary' && 'bg-highlight', 'bg-background-highlight',
variant === 'secondary' && 'bg-highlightSecondary',
orientation === 'horizontal' && 'w-full h-[1px]', orientation === 'horizontal' && 'w-full h-[1px]',
orientation === 'vertical' && 'h-full w-[1px]', orientation === 'vertical' && 'h-full w-[1px]',
)} )}

View File

@@ -21,7 +21,7 @@ export function SizeTag({ contentLength }: Props) {
} }
return ( return (
<span title={`${contentLength} bytes`}> <span className="font-mono" title={`${contentLength} bytes`}>
{Math.round(num * 10) / 10} {unit} {Math.round(num * 10) / 10} {unit}
</span> </span>
); );

View File

@@ -10,17 +10,18 @@ interface Props {
export function StatusTag({ response, className, showReason }: Props) { export function StatusTag({ response, className, showReason }: Props) {
const { status } = response; const { status } = response;
const label = status < 100 ? 'ERR' : status; const label = status < 100 ? 'ERR' : status;
const category = `${status}`[0];
return ( return (
<span <span
className={classNames( className={classNames(
className, className,
'font-mono', 'font-mono',
status >= 0 && status < 100 && 'text-red-600', category === '0' && 'text-fg-danger',
status >= 100 && status < 200 && 'text-green-600', category === '1' && 'text-fg-info',
status >= 200 && status < 300 && 'text-green-600', category === '2' && 'text-fg-success',
status >= 300 && status < 400 && 'text-pink-600', category === '3' && 'text-fg-primary',
status >= 400 && status < 500 && 'text-orange-600', category === '4' && 'text-fg-warning',
status >= 500 && 'text-red-600', category === '5' && 'text-fg-danger',
)} )}
> >
{label} {showReason && response.statusReason && response.statusReason} {label} {showReason && response.statusReason && response.statusReason}

View File

@@ -82,7 +82,7 @@ export function Tabs({
{tabs.map((t) => { {tabs.map((t) => {
const isActive = t.value === value; const isActive = t.value === value;
const btnClassName = classNames( const btnClassName = classNames(
isActive ? 'text-gray-800' : 'text-gray-600 hover:text-gray-700', isActive ? 'text-fg' : 'text-fg-subtler hover:text-fg-subtle',
'!px-2 ml-[1px]', '!px-2 ml-[1px]',
); );
@@ -108,7 +108,7 @@ export function Tabs({
icon="chevronDown" icon="chevronDown"
className={classNames( className={classNames(
'-mr-1.5 mt-0.5', '-mr-1.5 mt-0.5',
isActive ? 'opacity-100' : 'opacity-20', isActive ? 'text-fg-subtle' : 'opacity-50',
)} )}
/> />
} }

View File

@@ -3,9 +3,9 @@ import { motion } from 'framer-motion';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import React from 'react'; import React from 'react';
import { useKey } from 'react-use'; import { useKey } from 'react-use';
import { IconButton } from './IconButton';
import type { IconProps } from './Icon'; import type { IconProps } from './Icon';
import { Icon } from './Icon'; import { Icon } from './Icon';
import { IconButton } from './IconButton';
export interface ToastProps { export interface ToastProps {
children: ReactNode; children: ReactNode;
@@ -52,14 +52,15 @@ export function Toast({
transition={{ duration: 0.2 }} transition={{ duration: 0.2 }}
className={classNames( className={classNames(
className, className,
'x-theme-dialog',
'pointer-events-auto', 'pointer-events-auto',
'relative bg-gray-50 dark:bg-gray-100 pointer-events-auto', 'relative bg-background pointer-events-auto',
'rounded-lg', 'rounded-lg',
'border border-highlightSecondary dark:border-highlight shadow-xl', 'border border-background-highlight dark:border-background-highlight-secondary shadow-xl',
'max-w-[calc(100vw-5rem)] max-h-[calc(100vh-6rem)]', 'max-w-[calc(100vw-5rem)] max-h-[calc(100vh-6rem)]',
'w-[22rem] max-h-[80vh]', 'w-[22rem] max-h-[80vh]',
'm-2 grid grid-cols-[1fr_auto]', 'm-2 grid grid-cols-[1fr_auto]',
'text-gray-700', 'text-fg',
)} )}
> >
<div className="px-3 py-2 flex items-center gap-2"> <div className="px-3 py-2 flex items-center gap-2">
@@ -67,10 +68,10 @@ export function Toast({
<Icon <Icon
icon={ICONS[variant]} icon={ICONS[variant]}
className={classNames( className={classNames(
variant === 'success' && 'text-green-500', variant === 'success' && 'text-fg-success',
variant === 'warning' && 'text-orange-500', variant === 'warning' && 'text-fg-warning',
variant === 'error' && 'text-red-500', variant === 'error' && 'text-fg-danger',
variant === 'copied' && 'text-violet-500', variant === 'copied' && 'text-fg-primary',
)} )}
/> />
)} )}
@@ -82,7 +83,7 @@ export function Toast({
<IconButton <IconButton
color="custom" color="custom"
className="opacity-50" className="opacity-60"
title="Dismiss" title="Dismiss"
icon="x" icon="x"
onClick={onClose} onClick={onClose}
@@ -91,7 +92,7 @@ export function Toast({
{timeout != null && ( {timeout != null && (
<div className="w-full absolute bottom-0 left-0 right-0"> <div className="w-full absolute bottom-0 left-0 right-0">
<motion.div <motion.div
className="bg-highlight h-0.5" className="bg-background-highlight h-0.5"
initial={{ width: '100%' }} initial={{ width: '100%' }}
animate={{ width: '0%', opacity: 0.2 }} animate={{ width: '0%', opacity: 0.2 }}
transition={{ duration: timeout / 1000, ease: 'linear' }} transition={{ duration: timeout / 1000, ease: 'linear' }}

View File

@@ -1,17 +0,0 @@
import classNames from 'classnames';
import type { ReactNode } from 'react';
interface Props {
className?: string;
children?: ReactNode;
}
export function WindowDragRegion({ className, ...props }: Props) {
return (
<div
data-tauri-drag-region
className={classNames(className, 'w-full flex-shrink-0')}
{...props}
/>
);
}

View File

@@ -2,6 +2,7 @@ import { convertFileSrc } from '@tauri-apps/api/core';
import classNames from 'classnames'; import classNames from 'classnames';
import { useState } from 'react'; import { useState } from 'react';
import type { HttpResponse } from '../../lib/models'; import type { HttpResponse } from '../../lib/models';
import { Button } from '../core/Button';
interface Props { interface Props {
response: HttpResponse; response: HttpResponse;
@@ -20,15 +21,15 @@ export function ImageViewer({ response, className }: Props) {
if (!show) { if (!show) {
return ( return (
<> <>
<div className="text-sm italic text-gray-500"> <div className="text-sm italic text-fg-subtler">
Response body is too large to preview.{' '} Response body is too large to preview.{' '}
<button <Button
className="cursor-pointer underline hover:text-gray-800" className="cursor-pointer underline hover:text-fg"
color="gray" color="secondary"
onClick={() => setShow(true)} onClick={() => setShow(true)}
> >
Show anyway Show anyway
</button> </Button>
</div> </div>
</> </>
); );

View File

@@ -85,7 +85,6 @@ export function TextViewer({ response, pretty }: Props) {
return ( return (
<Editor <Editor
readOnly readOnly
className="bg-gray-50 dark:!bg-gray-100"
forceUpdateKey={body} forceUpdateKey={body}
defaultValue={body} defaultValue={body}
contentType={contentType} contentType={contentType}

View File

@@ -24,7 +24,7 @@ export function WebPageViewer({ response }: Props) {
title="Response preview" title="Response preview"
srcDoc={contentForIframe} srcDoc={contentForIframe}
sandbox="allow-scripts allow-same-origin" sandbox="allow-scripts allow-same-origin"
className="h-full w-full rounded border border-highlightSecondary" className="h-full w-full rounded border border-background-highlight-secondary"
/> />
</div> </div>
); );

View File

@@ -32,10 +32,10 @@ export function Confirm({ onHide, onResult, confirmText, variant = 'confirm' }:
return ( return (
<HStack space={2} justifyContent="start" className="mt-2 mb-4 flex-row-reverse"> <HStack space={2} justifyContent="start" className="mt-2 mb-4 flex-row-reverse">
<Button className="focus" color={colors[variant]} onClick={handleSuccess}> <Button color={colors[variant]} onClick={handleSuccess}>
{confirmText ?? confirmButtonTexts[variant]} {confirmText ?? confirmButtonTexts[variant]}
</Button> </Button>
<Button className="focus" color="gray" onClick={handleHide}> <Button onClick={handleHide} variant="border">
Cancel Cancel
</Button> </Button>
</HStack> </HStack>

View File

@@ -52,10 +52,10 @@ export function Prompt({
onChange={setValue} onChange={setValue}
/> />
<HStack space={2} justifyContent="end"> <HStack space={2} justifyContent="end">
<Button className="focus" color="gray" onClick={onHide}> <Button onClick={onHide} variant="border">
Cancel Cancel
</Button> </Button>
<Button type="submit" className="focus" color="primary"> <Button type="submit" color="primary">
{confirmLabel} {confirmLabel}
</Button> </Button>
</HStack> </HStack>

View File

@@ -2,7 +2,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { buildKeyValueKey, getKeyValue, setKeyValue } from '../lib/keyValueStore'; import { buildKeyValueKey, getKeyValue, setKeyValue } from '../lib/keyValueStore';
const DEFAULT_NAMESPACE = 'app'; const DEFAULT_NAMESPACE = 'global';
export function keyValueQueryKey({ export function keyValueQueryKey({
namespace = DEFAULT_NAMESPACE, namespace = DEFAULT_NAMESPACE,
@@ -20,7 +20,7 @@ export function useKeyValue<T extends Object | null>({
key, key,
fallback, fallback,
}: { }: {
namespace?: 'app' | 'no_sync' | 'global'; namespace?: 'global' | 'no_sync';
key: string | string[]; key: string | string[];
fallback: T; fallback: T;
}) { }) {
@@ -51,7 +51,8 @@ export function useKeyValue<T extends Object | null>({
await mutate.mutateAsync(value); await mutate.mutateAsync(value);
} }
}, },
[fallback, key, mutate, namespace], // eslint-disable-next-line react-hooks/exhaustive-deps
[fallback, key, namespace],
); );
const reset = useCallback(async () => mutate.mutateAsync(fallback), [mutate, fallback]); const reset = useCallback(async () => mutate.mutateAsync(fallback), [mutate, fallback]);

View File

@@ -1,8 +1,8 @@
import { invoke } from '@tauri-apps/api/core';
import { open } from '@tauri-apps/plugin-shell';
import { Button } from '../components/core/Button';
import { useToast } from '../components/ToastContext'; import { useToast } from '../components/ToastContext';
import { useListenToTauriEvent } from './useListenToTauriEvent'; import { useListenToTauriEvent } from './useListenToTauriEvent';
import { Button } from '../components/core/Button';
import { open } from '@tauri-apps/plugin-shell';
import { invoke } from '@tauri-apps/api/core';
export function useNotificationToast() { export function useNotificationToast() {
const toast = useToast(); const toast = useToast();
@@ -31,7 +31,7 @@ export function useNotificationToast() {
actionLabel && actionUrl ? ( actionLabel && actionUrl ? (
<Button <Button
size="xs" size="xs"
color="gray" color="secondary"
className="mr-auto min-w-[5rem]" className="mr-auto min-w-[5rem]"
onClick={() => { onClick={() => {
toast.hide(payload.id); toast.hide(payload.id);

View File

@@ -0,0 +1,19 @@
import type { GrpcConnection, GrpcRequest } from '../lib/models';
import { useGrpcConnections } from './useGrpcConnections';
import { useKeyValue } from './useKeyValue';
import { useLatestGrpcConnection } from './useLatestGrpcConnection';
export function usePinnedGrpcConnection(activeRequest: GrpcRequest) {
const latestConnection = useLatestGrpcConnection(activeRequest.id);
const { set: setPinnedConnectionId, value: pinnedConnectionId } = useKeyValue<string | null>({
// Key on latest connection instead of activeRequest because connections change out of band of active request
key: ['pinned_grpc_connection_id', latestConnection?.id ?? 'n/a'],
fallback: null,
namespace: 'global',
});
const connections = useGrpcConnections(activeRequest.id);
const activeConnection: GrpcConnection | null =
connections.find((r) => r.id === pinnedConnectionId) ?? latestConnection;
return { activeConnection, setPinnedConnectionId, pinnedConnectionId, connections } as const;
}

View File

@@ -1,28 +1,19 @@
import { useCallback, useEffect } from 'react';
import { createGlobalState } from 'react-use';
import type { HttpRequest, HttpResponse } from '../lib/models'; import type { HttpRequest, HttpResponse } from '../lib/models';
import { useHttpResponses } from './useHttpResponses'; import { useHttpResponses } from './useHttpResponses';
import { useKeyValue } from './useKeyValue';
import { useLatestHttpResponse } from './useLatestHttpResponse'; import { useLatestHttpResponse } from './useLatestHttpResponse';
const usePinnedResponseIdState = createGlobalState<string | null>(null);
export function usePinnedHttpResponse(activeRequest: HttpRequest) { export function usePinnedHttpResponse(activeRequest: HttpRequest) {
const [pinnedResponseId, setPinnedResponseId] = usePinnedResponseIdState();
const latestResponse = useLatestHttpResponse(activeRequest.id); const latestResponse = useLatestHttpResponse(activeRequest.id);
const { set: setPinnedResponseId, value: pinnedResponseId } = useKeyValue<string | null>({
// Key on latest response instead of activeRequest because responses change out of band of active request
key: ['pinned_http_response_id', latestResponse?.id ?? 'n/a'],
fallback: null,
namespace: 'global',
});
const responses = useHttpResponses(activeRequest.id); const responses = useHttpResponses(activeRequest.id);
const activeResponse: HttpResponse | null = pinnedResponseId const activeResponse: HttpResponse | null =
? responses.find((r) => r.id === pinnedResponseId) ?? null responses.find((r) => r.id === pinnedResponseId) ?? latestResponse;
: latestResponse ?? null;
// Unset pinned response when a new one comes in return { activeResponse, setPinnedResponseId, pinnedResponseId, responses } as const;
useEffect(() => setPinnedResponseId(null), [responses.length, setPinnedResponseId]);
const setPinnedResponse = useCallback(
(r: HttpResponse) => {
setPinnedResponseId(r.id);
},
[setPinnedResponseId],
);
return { activeResponse, setPinnedResponse, pinnedResponseId, responses } as const;
} }

View File

@@ -7,12 +7,14 @@ export function fallbackRequestName(r: HttpRequest | GrpcRequest | null): string
return r.name; return r.name;
} }
const withoutVariables = r.url.replace(/\$\{\[[^\]]+]}/g, ''); const withoutVariables = r.url.replace(/\$\{\[\s*([^\]]+)\s*]}/g, '$1');
if (withoutVariables.trim() === '') { if (withoutVariables.trim() === '') {
return r.model === 'http_request' ? 'New HTTP Request' : 'new gRPC Request'; return r.model === 'http_request' ? 'New HTTP Request' : 'new gRPC Request';
} }
const fixedUrl = r.url.match(/^https?:\/\//) ? r.url : 'http://' + r.url; const fixedUrl = withoutVariables.match(/^https?:\/\//)
? withoutVariables
: 'http://' + withoutVariables;
if (r.model === 'grpc_request' && r.service != null && r.method != null) { if (r.model === 'grpc_request' && r.service != null && r.method != null) {
const shortService = r.service.split('.').pop(); const shortService = r.service.split('.').pop();
@@ -21,6 +23,7 @@ export function fallbackRequestName(r: HttpRequest | GrpcRequest | null): string
try { try {
const url = new URL(fixedUrl); const url = new URL(fixedUrl);
const pathname = url.pathname === '/' ? '' : url.pathname; const pathname = url.pathname === '/' ? '' : url.pathname;
console.log('hello', fixedUrl);
return `${url.host}${pathname}`; return `${url.host}${pathname}`;
} catch (_) { } catch (_) {
// Nothing // Nothing

6
src-web/lib/indent.ts Normal file
View File

@@ -0,0 +1,6 @@
export function indent(text: string, space = ' '): string {
return text
.split('\n')
.map((line) => space + line)
.join('\n');
}

View File

@@ -0,0 +1,94 @@
import parseColor from 'parse-color';
export class Color {
private theme: 'dark' | 'light' = 'light';
private hue: number = 0;
private saturation: number = 0;
private lightness: number = 0;
private alpha: number = 1;
constructor(cssColor: string, theme: 'dark' | 'light') {
try {
const { hsla } = parseColor(cssColor || '');
this.hue = hsla[0];
this.saturation = hsla[1];
this.lightness = hsla[2];
this.alpha = hsla[3] ?? 1;
this.theme = theme;
} catch (err) {
console.log('Failed to parse CSS color', cssColor, err);
}
}
static transparent(): Color {
return new Color('rgba(0, 0, 0, 0.1)', 'light');
}
private clone(): Color {
return new Color(this.css(), this.theme);
}
lower(mod: number): Color {
return this.theme === 'dark' ? this._darken(mod) : this._lighten(mod);
}
lowerTo(value: number): Color {
return this.theme === 'dark'
? this._darken(1)._lighten(value)
: this._lighten(1)._darken(1 - value);
}
lift(mod: number): Color {
return this.theme === 'dark' ? this._lighten(mod) : this._darken(mod);
}
liftTo(value: number): Color {
return this.theme === 'dark'
? this._lighten(1)._darken(1 - value)
: this._darken(1)._lighten(value);
}
translucify(mod: number): Color {
const c = this.clone();
c.alpha = c.alpha - c.alpha * mod;
return c;
}
desaturate(mod: number): Color {
const c = this.clone();
c.saturation = c.saturation - c.saturation * mod;
return c;
}
saturate(mod: number): Color {
const c = this.clone();
c.saturation = this.saturation + (100 - this.saturation) * mod;
return c;
}
lighterThan(c: Color): boolean {
return this.lightness > c.lightness;
}
css(): string {
// If opacity is 1, allow for Tailwind modification
const h = Math.round(this.hue);
const s = Math.round(this.saturation);
const l = Math.round(this.lightness);
const a = Math.round(this.alpha * 100) / 100;
return `hsla(${h}, ${s}%, ${l}%, ${a})`;
}
private _lighten(mod: number): Color {
const c = this.clone();
c.lightness = this.lightness + (100 - this.lightness) * mod;
return c;
}
private _darken(mod: number): Color {
const c = this.clone();
c.lightness = this.lightness - this.lightness * mod;
return c;
}
}

View File

@@ -1,25 +0,0 @@
import { describe, expect, it } from 'vitest';
import { generateColorVariant, toTailwindVariable } from './theme';
describe('Generate colors', () => {
it('Generates dark colors', () => {
expect(generateColorVariant('hsl(0,0%,50%)', 50, 'dark', 0.2, 0.8)).toBe('hsl(0,0%,14.0%)');
expect(generateColorVariant('hsl(0,0%,50%)', 950, 'dark', 0.2, 0.8)).toBe('hsl(0,0%,77.0%)');
expect(generateColorVariant('hsl(0,0%,50%)', 50, 'dark', 0.4, 0.6)).toBe('hsl(0,0%,23.0%)');
expect(generateColorVariant('hsl(0,0%,50%)', 950, 'dark', 0.4, 0.6)).toBe('hsl(0,0%,59.0%)');
});
it('Generates light colors', () => {
expect(generateColorVariant('hsl(0,0%,50%)', 50, 'light', 0.2, 0.8)).toBe('hsl(0,0%,80.0%)');
expect(generateColorVariant('hsl(0,0%,50%)', 950, 'light', 0.2, 0.8)).toBe('hsl(0,0%,14.0%)');
expect(generateColorVariant('hsl(0,0%,50%)', 50, 'light', 0.4, 0.6)).toBe('hsl(0,0%,60.0%)');
expect(generateColorVariant('hsl(0,0%,50%)', 950, 'light', 0.4, 0.6)).toBe('hsl(0,0%,23.0%)');
});
});
describe('Generates Tailwind color', () => {
it('Does it', () => {
expect(
toTailwindVariable({ name: 'blue', cssColor: 'hsl(10, 20%, 30%)', variant: 100 }),
).toEqual('--color-blue-100: 10 20% 30%;');
});
});

View File

@@ -1,172 +0,0 @@
import parseColor from 'parse-color';
import type { Appearance } from './window';
export type AppThemeColor =
| 'gray'
| 'red'
| 'orange'
| 'yellow'
| 'green'
| 'blue'
| 'pink'
| 'violet';
const colorNames: AppThemeColor[] = [
'gray',
'red',
'orange',
'yellow',
'green',
'blue',
'pink',
'violet',
];
export type AppThemeColorVariant =
| 0
| 50
| 100
| 200
| 300
| 400
| 500
| 600
| 700
| 800
| 900
| 950
| 1000;
export const appThemeVariants: AppThemeColorVariant[] = [
0, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950, 1000,
];
export type AppThemeLayer = 'root' | 'sidebar' | 'titlebar' | 'content' | 'above';
export type AppThemeColors = Record<AppThemeColor, string>;
export interface AppThemeLayerStyle {
colors: AppThemeColors;
blackPoint?: number;
whitePoint?: number;
}
interface ThemeColorObj {
name: AppThemeColor;
variant: AppThemeColorVariant;
cssColor: string;
}
export interface AppTheme {
name: string;
appearance: Appearance;
layers: Partial<Record<AppThemeLayer, AppThemeLayerStyle>>;
}
export function generateCSS(t: AppTheme): ThemeColorObj[] {
const rootColors = t.layers.root?.colors;
if (rootColors === undefined) return [];
const colors: ThemeColorObj[] = [];
for (const color of colorNames) {
const rawValue = rootColors[color];
if (!rawValue) continue;
colors.push(
...generateColors(
color,
rawValue,
t.appearance,
t.layers.root?.blackPoint,
t.layers.root?.whitePoint,
),
);
}
return colors;
}
export function generateColors(
name: AppThemeColor,
color: string,
appearance: Appearance,
blackPoint = 0,
whitePoint = 1,
): ThemeColorObj[] {
const colors = [];
for (const variant of appThemeVariants) {
colors.push({
name,
variant,
cssColor: generateColorVariant(color, variant, appearance, blackPoint, whitePoint),
});
}
return colors;
}
const lightnessMap: Record<Appearance, Record<AppThemeColorVariant, number>> = {
system: {
// Not actually used
0: 1,
50: 1,
100: 0.9,
200: 0.7,
300: 0.4,
400: 0.2,
500: 0,
600: -0.2,
700: -0.4,
800: -0.6,
900: -0.8,
950: -0.9,
1000: -1,
},
light: {
0: 1,
50: 1,
100: 0.9,
200: 0.7,
300: 0.4,
400: 0.2,
500: 0,
600: -0.2,
700: -0.4,
800: -0.6,
900: -0.8,
950: -0.9,
1000: -1,
},
dark: {
0: -1,
50: -0.9,
100: -0.8,
200: -0.6,
300: -0.4,
400: -0.2,
500: 0,
600: 0.2,
700: 0.4,
800: 0.6,
900: 0.8,
950: 0.9,
1000: 1,
},
};
export function generateColorVariant(
color: string,
variant: AppThemeColorVariant,
appearance: Appearance,
blackPoint = 0,
whitePoint = 1,
): string {
const { hsl } = parseColor(color || '');
const lightnessMod = lightnessMap[appearance][variant];
// const lightnessMod = (appearance === 'dark' ? 1 : -1) * ((variant / 1000) * 2 - 1);
const newL =
lightnessMod > 0
? hsl[2] + (100 * whitePoint - hsl[2]) * lightnessMod
: hsl[2] + hsl[2] * (1 - blackPoint) * lightnessMod;
return `hsl(${hsl[0]},${hsl[1]}%,${newL.toFixed(1)}%)`;
}
export function toTailwindVariable({ name, variant, cssColor }: ThemeColorObj): string {
const { hsl } = parseColor(cssColor || '');
return `--color-${name}-${variant}: ${hsl[0]} ${hsl[1]}% ${hsl[2]}%;`;
}

View File

@@ -1,76 +1,313 @@
import type { AppTheme, AppThemeColors } from './theme'; import { indent } from '../indent';
import { generateCSS, toTailwindVariable } from './theme'; import { Color } from './color';
export type Appearance = 'dark' | 'light' | 'system'; export type Appearance = 'dark' | 'light' | 'system';
const DEFAULT_APPEARANCE: Appearance = 'system'; const DEFAULT_APPEARANCE: Appearance = 'system';
enum Theme { interface ThemeComponent {
yaak = 'yaak', background?: Color;
catppuccin = 'catppuccin', backgroundHighlight?: Color;
backgroundHighlightSecondary?: Color;
backgroundActive?: Color;
foreground?: Color;
foregroundSubtle?: Color;
foregroundSubtler?: Color;
colors?: Partial<RootColors>;
} }
const themes: Record<Theme, AppThemeColors> = { interface YaakTheme extends ThemeComponent {
yaak: { name: string;
gray: 'hsl(245, 23%, 45%)', components?: {
red: 'hsl(342,100%, 63%)', dialog?: ThemeComponent;
orange: 'hsl(32, 98%, 54%)', sidebar?: ThemeComponent;
yellow: 'hsl(52, 79%, 58%)', responsePane?: ThemeComponent;
green: 'hsl(136, 62%, 54%)', appHeader?: ThemeComponent;
blue: 'hsl(206, 100%, 56%)', button?: ThemeComponent;
pink: 'hsl(300, 100%, 71%)', banner?: ThemeComponent;
violet: 'hsl(266, 100%, 73%)', placeholder?: ThemeComponent;
}, };
catppuccin: { }
gray: 'hsl(240, 23%, 47%)',
red: 'hsl(343, 91%, 74%)',
orange: 'hsl(23, 92%, 74%)',
yellow: 'hsl(41, 86%, 72%)',
green: 'hsl(115, 54%, 65%)',
blue: 'hsl(217, 92%, 65%)',
pink: 'hsl(316, 72%, 75%)',
violet: 'hsl(267, 84%, 70%)',
},
};
const darkTheme: AppTheme = { interface RootColors {
name: 'Default Dark', primary: Color;
appearance: 'dark', secondary: Color;
layers: { info: Color;
root: { success: Color;
blackPoint: 0.2, notice: Color;
colors: themes.yaak, warning: Color;
danger: Color;
}
type ColorName = keyof RootColors;
type ComponentName = keyof NonNullable<YaakTheme['components']>;
const yaakThemes: Record<string, YaakTheme> = {
yaakLight: {
name: 'Yaak (Light)',
background: new Color('#f2f4f7', 'light').lower(1),
foreground: new Color('hsl(219,23%,15%)', 'light'),
colors: {
primary: new Color('hsl(266,100%,70%)', 'light'),
secondary: new Color('hsl(220,24%,59%)', 'light'),
info: new Color('hsl(206,100%,48%)', 'light'),
success: new Color('hsl(155,95%,33%)', 'light'),
notice: new Color('hsl(45,100%,41%)', 'light'),
warning: new Color('hsl(30,100%,43%)', 'light'),
danger: new Color('hsl(335,75%,57%)', 'light'),
},
components: {
sidebar: {
background: new Color('#f2f4f7', 'light'),
},
},
} as YaakTheme,
yaakDark: {
name: 'Yaak Dark',
background: new Color('hsl(244,23%,12%)', 'dark'),
foreground: new Color('#bcbad4', 'dark'),
colors: {
primary: new Color('hsl(266,100%,79%)', 'dark'),
secondary: new Color('hsl(245,23%,60%)', 'dark'),
info: new Color('hsl(206,100%,63%)', 'dark'),
success: new Color('hsl(150,100%,37%)', 'dark'),
notice: new Color('hsl(48,80%,63%)', 'dark'),
warning: new Color('hsl(28,100%,61%)', 'dark'),
danger: new Color('hsl(342,90%,68%)', 'dark'),
},
components: {
sidebar: {
background: new Color('hsl(243,23%,15%)', 'dark'),
},
responsePane: {
background: new Color('hsl(243,23%,15%)', 'dark'),
},
}, },
}, },
}; catppuccin: {
name: 'Catppuccin',
const lightTheme: AppTheme = { background: new Color('#181825', 'dark'),
name: 'Default Light', foreground: new Color('#cdd6f4', 'dark'),
appearance: 'light', foregroundSubtle: new Color('#cdd6f4', 'dark').lower(0.1).translucify(0.3),
layers: { foregroundSubtler: new Color('#cdd6f4', 'dark').lower(0.1).translucify(0.55),
root: { colors: {
colors: { primary: new Color('#cba6f7', 'dark'),
gray: '#7f8fb0', secondary: new Color('#bac2de', 'dark'),
red: '#ec3f87', info: new Color('#89b4fa', 'dark'),
orange: '#ff8000', success: new Color('#a6e3a1', 'dark'),
yellow: '#e7cf24', notice: new Color('#f9e2af', 'dark'),
green: '#00d365', warning: new Color('#fab387', 'dark'),
blue: '#0090ff', danger: new Color('#f38ba8', 'dark'),
pink: '#ea6cea', },
violet: '#ac6cff', components: {
dialog: {
background: new Color('#181825', 'dark'),
},
sidebar: {
background: new Color('#1e1e2e', 'dark'),
},
appHeader: {
background: new Color('#11111b', 'dark'),
},
responsePane: {
background: new Color('#1e1e2e', 'dark'),
},
button: {
colors: {
primary: new Color('#cba6f7', 'dark').lower(0.2),
secondary: new Color('#bac2de', 'dark').lower(0.2),
info: new Color('#89b4fa', 'dark').lower(0.2),
success: new Color('#a6e3a1', 'dark').lower(0.2),
notice: new Color('#f9e2af', 'dark').lower(0.2),
warning: new Color('#fab387', 'dark').lower(0.2),
danger: new Color('#f38ba8', 'dark').lower(0.2),
},
}, },
}, },
}, },
}; };
type CSSVariables = Record<string, string | undefined>;
function themeVariables(theme?: ThemeComponent, base?: CSSVariables): CSSVariables | null {
const vars: CSSVariables = {
'--background': theme?.background?.css(),
'--background-highlight':
theme?.backgroundHighlight?.css() ?? theme?.background?.lift(0.11).css(),
'--background-highlight-secondary':
theme?.backgroundHighlightSecondary?.css() ?? theme?.background?.lift(0.06).css(),
'--background-active':
theme?.backgroundActive?.css() ?? theme?.colors?.primary?.lower(0.2).translucify(0.8).css(),
'--background-backdrop': theme?.background?.lower(0.2).translucify(0.2).css(),
'--background-selection': theme?.colors?.primary?.lower(0.1).translucify(0.7).css(),
'--fg': theme?.foreground?.css(),
'--fg-subtle': theme?.foregroundSubtle?.css() ?? theme?.foreground?.lower(0.2).css(),
'--fg-subtler': theme?.foregroundSubtler?.css() ?? theme?.foreground?.lower(0.3).css(),
'--border-focus': theme?.colors?.info?.css(),
};
for (const [color, value] of Object.entries(theme?.colors ?? {})) {
vars[`--fg-${color}`] = (value as Color).css();
}
// Extend with base
for (const [k, v] of Object.entries(vars)) {
if (!v && base?.[k]) {
vars[k] = base[k];
}
}
return vars;
}
function placeholderColorVariables(color: Color): CSSVariables {
return {
'--fg': color.lift(0.6).css(),
'--fg-subtle': color.lift(0.4).css(),
'--fg-subtler': color.css(),
'--background': color.lower(0.2).translucify(0.8).css(),
'--background-highlight': color.lower(0.2).translucify(0.2).css(),
'--background-highlight-secondary': color.lower(0.1).translucify(0.7).css(),
};
}
function bannerColorVariables(color: Color): CSSVariables {
return {
'--fg': color.lift(0.8).css(),
'--fg-subtle': color.translucify(0.3).css(),
'--fg-subtler': color.css(),
'--background': color.css(),
'--background-highlight': color.lift(0.3).translucify(0.4).css(),
'--background-highlight-secondary': color.translucify(0.9).css(),
};
}
function buttonSolidColorVariables(color: Color): CSSVariables {
return {
'--fg': new Color('white', 'dark').css(),
'--background': color.lower(0.15).css(),
'--background-highlight': color.css(),
'--background-highlight-secondary': color.lower(0.3).css(),
};
}
function buttonBorderColorVariables(color: Color): CSSVariables {
return {
'--fg': color.lift(0.6).css(),
'--fg-subtle': color.lift(0.4).css(),
'--fg-subtler': color.lift(0.4).translucify(0.6).css(),
'--background': Color.transparent().css(),
'--background-highlight': color.translucify(0.8).css(),
};
}
function variablesToCSS(selector: string | null, vars: CSSVariables | null): string | null {
if (vars == null) {
return null;
}
const css = Object.entries(vars ?? {})
.filter(([, value]) => value)
.map(([name, value]) => `${name}: ${value};`)
.join('\n');
return selector == null ? css : `${selector} {\n${indent(css)}\n}`;
}
function componentCSS(
component: ComponentName,
components?: YaakTheme['components'],
): string | null {
if (components == null) {
return null;
}
const themeVars = themeVariables(components[component]);
return variablesToCSS(`.x-theme-${component}`, themeVars);
}
function buttonCSS(color: ColorName, colors?: Partial<RootColors>): string | null {
const cssColor = colors?.[color];
if (cssColor == null) {
return null;
}
return [
variablesToCSS(`.x-theme-button--solid--${color}`, buttonSolidColorVariables(cssColor)),
variablesToCSS(`.x-theme-button--border--${color}`, buttonBorderColorVariables(cssColor)),
].join('\n\n');
}
function bannerCSS(color: ColorName, colors?: Partial<RootColors>): string | null {
const cssColor = colors?.[color];
if (cssColor == null) {
return null;
}
return [variablesToCSS(`.x-theme-banner--${color}`, bannerColorVariables(cssColor))].join('\n\n');
}
function placeholderCSS(color: ColorName, colors?: Partial<RootColors>): string | null {
const cssColor = colors?.[color];
if (cssColor == null) {
return null;
}
return [
variablesToCSS(`.x-theme-placeholder-widget--${color}`, placeholderColorVariables(cssColor)),
].join('\n\n');
}
function isThemeDark(theme: YaakTheme): boolean {
if (theme.background && theme.foreground) {
return theme.foreground.lighterThan(theme.background);
}
return false;
}
setThemeOnDocument(yaakThemes.yaakLight!);
setThemeOnDocument(yaakThemes.yaakDark!);
export function getThemeCSS(theme: YaakTheme): string {
let themeCSS = '';
try {
const baseCss = variablesToCSS(null, themeVariables(theme));
const { components, colors } = theme;
themeCSS = [
baseCss,
...Object.keys(components ?? {}).map((key) =>
componentCSS(key as ComponentName, theme.components),
),
...Object.keys(colors ?? {}).map((key) =>
buttonCSS(key as ColorName, theme.components?.button?.colors ?? colors),
),
...Object.keys(colors ?? {}).map((key) =>
bannerCSS(key as ColorName, theme.components?.banner?.colors ?? colors),
),
...Object.keys(colors ?? {}).map((key) =>
placeholderCSS(key as ColorName, theme.components?.placeholder?.colors ?? colors),
),
].join('\n\n');
} catch (err) {
console.error(err);
}
return themeCSS;
}
export function setAppearanceOnDocument(appearance: Appearance = DEFAULT_APPEARANCE) { export function setAppearanceOnDocument(appearance: Appearance = DEFAULT_APPEARANCE) {
const resolvedAppearance = appearance === 'system' ? getPreferredAppearance() : appearance; const resolvedAppearance = appearance === 'system' ? getPreferredAppearance() : appearance;
const theme = resolvedAppearance === 'dark' ? darkTheme : lightTheme;
document.documentElement.setAttribute('data-resolved-appearance', resolvedAppearance); document.documentElement.setAttribute('data-resolved-appearance', resolvedAppearance);
}
export function setThemeOnDocument(theme: YaakTheme) {
document.documentElement.setAttribute('data-theme', theme.name); document.documentElement.setAttribute('data-theme', theme.name);
let existingStyleEl = document.head.querySelector(`style[data-theme-definition]`); const darkOrLight = isThemeDark(theme) ? 'dark' : 'light';
let existingStyleEl = document.head.querySelector(`style[data-theme-definition=${darkOrLight}]`);
if (!existingStyleEl) { if (!existingStyleEl) {
const styleEl = document.createElement('style'); const styleEl = document.createElement('style');
document.head.appendChild(styleEl); document.head.appendChild(styleEl);
@@ -78,16 +315,12 @@ export function setAppearanceOnDocument(appearance: Appearance = DEFAULT_APPEARA
} }
existingStyleEl.textContent = [ existingStyleEl.textContent = [
`/* ${darkTheme.name} */`, `/* ${theme.name} */`,
`[data-resolved-appearance="dark"] {`, `[data-resolved-appearance="${isThemeDark(theme) ? 'dark' : 'light'}"] {`,
...generateCSS(darkTheme).map(toTailwindVariable), getThemeCSS(theme),
'}',
`/* ${lightTheme.name} */`,
`[data-resolved-appearance="light"] {`,
...generateCSS(lightTheme).map(toTailwindVariable),
'}', '}',
].join('\n'); ].join('\n');
existingStyleEl.setAttribute('data-theme-definition', ''); existingStyleEl.setAttribute('data-theme-definition', darkOrLight);
} }
export function getPreferredAppearance(): Appearance { export function getPreferredAppearance(): Appearance {

View File

@@ -6,7 +6,7 @@
html, html,
body, body,
#root { #root {
@apply w-full h-full overflow-hidden text-gray-900 bg-gray-50; @apply w-full h-full overflow-hidden text-fg bg-background;
} }
* { * {
@@ -58,7 +58,7 @@
&:hover { &:hover {
&.scrollbar-thumb, &.scrollbar-thumb,
&::-webkit-scrollbar-thumb { &::-webkit-scrollbar-thumb {
@apply bg-gray-500/30 hover:bg-gray-500/50 rounded-full; @apply bg-background-highlight-secondary hover:bg-fg-subtler rounded-full;
} }
} }
} }
@@ -70,7 +70,7 @@
iframe { iframe {
&::-webkit-scrollbar-corner, &::-webkit-scrollbar-corner,
&::-webkit-scrollbar { &::-webkit-scrollbar {
@apply bg-gray-100; @apply bg-background-highlight-secondary !important;
} }
} }

View File

@@ -50,58 +50,45 @@ module.exports = {
'4xs': '0.6rem', '4xs': '0.6rem',
'3xs': '0.675rem', '3xs': '0.675rem',
'2xs': '0.75rem', '2xs': '0.75rem',
xs: '0.8rem', 'xs': '0.8rem',
sm: '0.9rem', 'sm': '0.9rem',
base: '1rem', 'base': '1rem',
xl: '1.25rem', 'xl': '1.25rem',
'2xl': '1.5rem', '2xl': '1.5rem',
'3xl': '2rem', '3xl': '2rem',
'4xl': '2.5rem', '4xl': '2.5rem',
'5xl': '3rem', '5xl': '3rem',
}, },
colors: { colors: {
selection: 'hsl(var(--color-violet-500) / 0.3)', 'transparent': 'transparent',
focus: 'hsl(var(--color-blue-500) / 0.7)', 'placeholder': 'var(--fg-subtler)',
invalid: 'hsl(var(--color-red-500))', 'selection': 'var(--background-selection)',
highlight: 'hsl(var(--color-gray-500) / 0.3)',
highlightSecondary: 'hsl(var(--color-gray-500) / 0.15)', // New theme values
transparent: 'transparent',
white: 'hsl(0 100% 100% / <alpha-value>)', 'border-focus': 'var(--border-focus)',
black: 'hsl(0 100% 0% / <alpha-value>)', 'fg': 'var(--fg)',
placeholder: 'hsl(var(--color-gray-400) / <alpha-value>)', 'fg-danger': 'var(--fg-danger)',
red: color('red'), 'fg-subtle': 'var(--fg-subtle)',
orange: color('orange'), 'fg-subtler': 'var(--fg-subtler)',
yellow: color('yellow'), 'fg-primary': 'var(--fg-primary)',
blue: color('blue'), 'fg-secondary': 'var(--fg-secondary)',
green: color('green'), 'fg-success': 'var(--fg-success)',
pink: color('pink'), 'fg-info': 'var(--fg-info)',
violet: color('violet'), 'fg-notice': 'var(--fg-notice)',
gray: color('gray'), 'fg-warning': 'var(--fg-warning)',
'background': 'var(--background)',
'background-active': 'var(--background-active)',
'background-highlight': 'var(--background-highlight)',
'background-highlight-secondary': 'var(--background-highlight-secondary)',
'background-backdrop': 'var(--background-backdrop)',
}, },
}, },
plugins: [ plugins: [
require('@tailwindcss/container-queries'), require('@tailwindcss/container-queries'),
plugin(function ({ addVariant }) { plugin(function ({addVariant}) {
addVariant('hocus', ['&:hover', '&:focus-visible', '&.focus:focus']); addVariant('hocus', ['&:hover', '&:focus-visible', '&.focus:focus']);
addVariant('focus-visible-or-class', ['&:focus-visible', '&.focus:focus']); addVariant('focus-visible-or-class', ['&:focus-visible']);
}), }),
], ],
}; };
function color(name) {
return {
0: `hsl(var(--color-${name}-0) / <alpha-value>)`,
50: `hsl(var(--color-${name}-50) / <alpha-value>)`,
100: `hsl(var(--color-${name}-100) / <alpha-value>)`,
200: `hsl(var(--color-${name}-200) / <alpha-value>)`,
300: `hsl(var(--color-${name}-300) / <alpha-value>)`,
400: `hsl(var(--color-${name}-400) / <alpha-value>)`,
500: `hsl(var(--color-${name}-500) / <alpha-value>)`,
600: `hsl(var(--color-${name}-600) / <alpha-value>)`,
700: `hsl(var(--color-${name}-700) / <alpha-value>)`,
800: `hsl(var(--color-${name}-800) / <alpha-value>)`,
900: `hsl(var(--color-${name}-900) / <alpha-value>)`,
950: `hsl(var(--color-${name}-950) / <alpha-value>)`,
1000: `hsl(var(--color-${name}-1000) / <alpha-value>)`,
};
}