Compare commits

..

4 Commits

Author SHA1 Message Date
Gregory Schier 95ac3e310a Improve response history menu (#492) 2026-07-02 10:04:57 -07:00
Gregory Schier 9b524e3dc7 Upgrade Tailwind to v4 (#491) 2026-07-02 09:53:22 -07:00
Gregory Schier bdf78254b5 Disable automatic Flathub workflow 2026-07-01 13:51:00 -07:00
Gregory Schier c5545c8781 Document release note contributor attribution 2026-07-01 13:39:05 -07:00
123 changed files with 1107 additions and 1108 deletions
@@ -19,10 +19,12 @@ Generate formatted markdown release notes for a Yaak tag.
- `gh pr view <PR_NUMBER> --json number,title,body,author,url` - `gh pr view <PR_NUMBER> --json number,title,body,author,url`
5. Extract useful details: 5. Extract useful details:
- Feedback URLs (`feedback.yaak.app`) - Feedback URLs (`feedback.yaak.app`)
- Contributor GitHub handles from `author.login`
- Plugin install links or other notable context - Plugin install links or other notable context
6. Format notes using Yaak style: 6. Format notes using Yaak style:
- Changelog badge at top - Changelog badge at top
- Bulleted items with PR links where available - Bulleted items with PR links where available
- Contributor handles for external PRs
- Feedback links where available - Feedback links where available
- Full changelog compare link at bottom - Full changelog compare link at bottom
@@ -31,6 +33,7 @@ Generate formatted markdown release notes for a Yaak tag.
- Wrap final notes in a markdown code fence. - Wrap final notes in a markdown code fence.
- Keep a blank line before and after the code fence. - Keep a blank line before and after the code fence.
- Output the markdown code block last. - Output the markdown code block last.
- Append contributor attribution to PR-backed bullets for non-`@gschier` authors, using `by [@handle](https://github.com/handle)`.
- Do not append `by @gschier` for PRs authored by `@gschier`. - Do not append `by @gschier` for PRs authored by `@gschier`.
- These are app release notes. Exclude CLI-only changes (commits prefixed with `cli:` or only touching `crates-cli/`) since the CLI has its own release process. - These are app release notes. Exclude CLI-only changes (commits prefixed with `cli:` or only touching `crates-cli/`) since the CLI has its own release process.
+9 -6
View File
@@ -1,7 +1,12 @@
name: Update Flathub name: Update Flathub
on: on:
release: workflow_dispatch:
types: [published] inputs:
tag:
description: Release tag to publish to Flathub
required: true
type: string
permissions: permissions:
contents: read contents: read
@@ -10,8 +15,6 @@ jobs:
update-flathub: update-flathub:
name: Update Flathub manifest name: Update Flathub manifest
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Only run for stable releases (skip betas/pre-releases)
if: ${{ !github.event.release.prerelease }}
steps: steps:
- name: Checkout app repo - name: Checkout app repo
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -39,7 +42,7 @@ jobs:
git clone --depth 1 https://github.com/flatpak/flatpak-builder-tools flatpak/flatpak-builder-tools git clone --depth 1 https://github.com/flatpak/flatpak-builder-tools flatpak/flatpak-builder-tools
- name: Run update-manifest.sh - name: Run update-manifest.sh
run: bash flatpak/update-manifest.sh "${{ github.event.release.tag_name }}" flathub-repo run: bash flatpak/update-manifest.sh "${{ inputs.tag }}" flathub-repo
- name: Commit and push to Flathub - name: Commit and push to Flathub
working-directory: flathub-repo working-directory: flathub-repo
@@ -48,5 +51,5 @@ jobs:
git config user.email "github-actions[bot]@users.noreply.github.com" git config user.email "github-actions[bot]@users.noreply.github.com"
git add -A git add -A
git diff --cached --quiet && echo "No changes to commit" && exit 0 git diff --cached --quiet && echo "No changes to commit" && exit 0
git commit -m "Update to ${{ github.event.release.tag_name }}" git commit -m "Update to ${{ inputs.tag }}"
git push git push
@@ -10,7 +10,7 @@ export function openFolderSettings(folderId: string, tab?: FolderSettingsTab) {
id: "folder-settings", id: "folder-settings",
title: null, title: null,
size: "lg", size: "lg",
className: "h-[50rem]", className: "h-200",
noPadding: true, noPadding: true,
render: () => <FolderSettingsDialog folderId={folderId} tab={tab} />, render: () => <FolderSettingsDialog folderId={folderId} tab={tab} />,
}); });
@@ -38,7 +38,7 @@ export function BinaryFileEditor({
<VStack space={2}> <VStack space={2}>
<SelectFile onChange={handleChange} filePath={filePath} /> <SelectFile onChange={handleChange} filePath={filePath} />
{filePath != null && mimeType !== contentType && !ignoreContentType.value && ( {filePath != null && mimeType !== contentType && !ignoreContentType.value && (
<Banner className="mt-3 !py-5"> <Banner className="mt-3 py-5!">
<div className="mb-4 text-center"> <div className="mb-4 text-center">
<div>Set Content-Type header</div> <div>Set Content-Type header</div>
<InlineCode>{mimeType}</InlineCode> for current request? <InlineCode>{mimeType}</InlineCode> for current request?
@@ -108,7 +108,7 @@ export function CloneGitRepositoryDialog({ hide }: Props) {
rightSlot={ rightSlot={
<IconButton <IconButton
size="xs" size="xs"
className="mr-0.5 !h-auto my-0.5" className="mr-0.5 h-auto! my-0.5"
icon="folder" icon="folder"
title="Browse" title="Browse"
onClick={handleSelectDirectory} onClick={handleSelectDirectory}
@@ -11,7 +11,7 @@ export function ColorIndicator({ color, onClick, className }: Props) {
const style: CSSProperties = { backgroundColor: color ?? undefined }; const style: CSSProperties = { backgroundColor: color ?? undefined };
const finalClassName = classNames( const finalClassName = classNames(
className, className,
"inline-block w-[0.75em] h-[0.75em] rounded-full mr-1.5 border border-transparent flex-shrink-0", "inline-block w-[0.75em] h-[0.75em] rounded-full mr-1.5 border border-transparent shrink-0",
); );
if (onClick) { if (onClick) {
@@ -439,7 +439,7 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
name="command" name="command"
label="Command" label="Command"
placeholder="Search or type a command" placeholder="Search or type a command"
className="font-sans !text-base" className="font-sans text-base!"
defaultValue={command} defaultValue={command}
onChange={handleSetCommand} onChange={handleSetCommand}
onKeyDownCapture={handleKeyDown} onKeyDownCapture={handleKeyDown}
@@ -448,7 +448,7 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
<div className="h-full px-1.5 overflow-y-auto pt-2 pb-1"> <div className="h-full px-1.5 overflow-y-auto pt-2 pb-1">
{filteredGroups.map((g) => ( {filteredGroups.map((g) => (
<div key={g.key} className="mb-1.5 w-full"> <div key={g.key} className="mb-1.5 w-full">
<Heading level={2} className="!text-xs uppercase px-1.5 h-sm flex items-center"> <Heading level={2} className="text-xs! uppercase px-1.5 h-sm flex items-center">
{g.label} {g.label}
</Heading> </Heading>
{g.items.map((v) => ( {g.items.map((v) => (
@@ -491,7 +491,7 @@ function CommandPaletteItem({
color="custom" color="custom"
justify="start" justify="start"
className={classNames( className={classNames(
"w-full h-sm flex items-center rounded px-1.5", "w-full h-sm flex items-center rounded-sm px-1.5",
"hover:text-text", "hover:text-text",
active && "bg-surface-highlight", active && "bg-surface-highlight",
!active && "text-text-subtle", !active && "text-text-subtle",
+5 -5
View File
@@ -155,7 +155,7 @@ export const CookieDialog = ({ cookieJarId }: Props) => {
rightSlot={ rightSlot={
filter.length > 0 && ( filter.length > 0 && (
<IconButton <IconButton
className="!bg-transparent !h-auto min-h-full opacity-50 hover:opacity-100 -mr-1" className="bg-transparent! h-auto! min-h-full opacity-50 hover:opacity-100 -mr-1"
icon="x" icon="x"
title="Clear filter" title="Clear filter"
onClick={() => { onClick={() => {
@@ -239,7 +239,7 @@ export const CookieDialog = ({ cookieJarId }: Props) => {
<TableCell className={classNames("pl-2", isSelected && "rounded-l")}> <TableCell className={classNames("pl-2", isSelected && "rounded-l")}>
{c.name} {c.name}
</TableCell> </TableCell>
<TruncatedWideTableCell className="min-w-[10rem]"> <TruncatedWideTableCell className="min-w-40">
{c.value} {c.value}
</TruncatedWideTableCell> </TruncatedWideTableCell>
<TableCell>{cookieDomain(c)}</TableCell> <TableCell>{cookieDomain(c)}</TableCell>
@@ -547,7 +547,7 @@ function CookieEditor({
} }
function CookieKeyValueRow({ labelClassName, ...props }: ComponentProps<typeof KeyValueRow>) { function CookieKeyValueRow({ labelClassName, ...props }: ComponentProps<typeof KeyValueRow>) {
return <KeyValueRow labelClassName={classNames("w-[7rem]", labelClassName)} {...props} />; return <KeyValueRow labelClassName={classNames("w-28", labelClassName)} {...props} />;
} }
function CookieTextInput({ function CookieTextInput({
@@ -589,7 +589,7 @@ function CookieTextarea({ onChange, value }: { onChange: (value: string) => void
<textarea <textarea
autoCapitalize="off" autoCapitalize="off"
autoCorrect="off" autoCorrect="off"
className={classNames(cookieInputClassName, "min-h-[5rem] resize-y")} className={classNames(cookieInputClassName, "min-h-20 resize-y")}
onChange={(event) => onChange(event.target.value)} onChange={(event) => onChange(event.target.value)}
value={value} value={value}
/> />
@@ -600,7 +600,7 @@ const NEW_COOKIE_KEY = "__new-cookie__";
const NON_EMPTY_INPUT_PATTERN = ".*\\S.*"; const NON_EMPTY_INPUT_PATTERN = ".*\\S.*";
const cookieInputClassName = classNames( const cookieInputClassName = classNames(
"x-theme-input w-full min-w-0 min-h-sm rounded-md bg-transparent", "x-theme-input w-full min-w-0 min-h-sm rounded-md bg-transparent",
"border border-border-subtle outline-none", "border border-border-subtle outline-hidden",
"px-2 text-xs font-mono cursor-text placeholder:text-placeholder", "px-2 text-xs font-mono cursor-text placeholder:text-placeholder",
"focus:border-border-focus invalid:border-danger", "focus:border-border-focus invalid:border-danger",
"disabled:opacity-disabled disabled:border-dotted", "disabled:opacity-disabled disabled:border-dotted",
@@ -75,7 +75,7 @@ export function DnsOverridesEditor({ workspace }: Props) {
<VStack space={3} className="pb-3"> <VStack space={3} className="pb-3">
<div className="text-text-subtle text-sm"> <div className="text-text-subtle text-sm">
Override DNS resolution for specific hostnames. This works like{" "} Override DNS resolution for specific hostnames. This works like{" "}
<code className="text-text-subtlest bg-surface-highlight px-1 rounded">/etc/hosts</code> but <code className="text-text-subtlest bg-surface-highlight px-1 rounded-sm">/etc/hosts</code> but
only for requests made from this workspace. only for requests made from this workspace.
</div> </div>
+2 -2
View File
@@ -23,8 +23,8 @@ export const DropMarker = memo(
<div <div
className={classNames( className={classNames(
"absolute bg-primary rounded-full", "absolute bg-primary rounded-full",
orientation === "horizontal" && "left-2 right-2 -bottom-[0.1rem] h-[0.2rem]", orientation === "horizontal" && "left-2 right-2 bottom-[-0.1rem] h-[0.2rem]",
orientation === "vertical" && "-left-[0.1rem] top-0 bottom-0 w-[0.2rem]", orientation === "vertical" && "left-[-0.1rem] top-0 bottom-0 w-[0.2rem]",
)} )}
/> />
</div> </div>
+5 -5
View File
@@ -204,7 +204,7 @@ function FormInputs<T extends Record<string, JsonPrimitive>>({
<div key={i + stateKey}> <div key={i + stateKey}>
<DetailsBanner <DetailsBanner
summary={input.label} summary={input.label}
className={classNames("!mb-auto", disabled && "opacity-disabled")} className={classNames("mb-auto!", disabled && "opacity-disabled")}
> >
<div className="mt-3"> <div className="mt-3">
<FormInputsStack <FormInputsStack
@@ -300,7 +300,7 @@ function TextArg({
onChange, onChange,
name: arg.name, name: arg.name,
multiLine: arg.multiLine, multiLine: arg.multiLine,
className: arg.multiLine ? "min-h-[4rem]" : undefined, className: arg.multiLine ? "min-h-16" : undefined,
defaultValue: value === DYNAMIC_FORM_NULL_ARG ? arg.defaultValue : value, defaultValue: value === DYNAMIC_FORM_NULL_ARG ? arg.defaultValue : value,
required: !arg.optional, required: !arg.optional,
disabled: arg.disabled, disabled: arg.disabled,
@@ -359,7 +359,7 @@ function EditorArg({
className={classNames( className={classNames(
"border border-border rounded-md overflow-hidden px-2 py-1", "border border-border rounded-md overflow-hidden px-2 py-1",
"focus-within:border-border-focus", "focus-within:border-border-focus",
!arg.rows && "max-h-[10rem]", // So it doesn't take up too much space !arg.rows && "max-h-40", // So it doesn't take up too much space
)} )}
style={arg.rows ? { height: `${arg.rows * 1.4 + 0.75}rem` } : undefined} style={arg.rows ? { height: `${arg.rows * 1.4 + 0.75}rem` } : undefined}
> >
@@ -372,7 +372,7 @@ function EditorArg({
onChange={onChange} onChange={onChange}
hideGutter hideGutter
heightMode="auto" heightMode="auto"
className="min-h-[3rem]" className="min-h-12"
defaultValue={value === DYNAMIC_FORM_NULL_ARG ? arg.defaultValue : value} defaultValue={value === DYNAMIC_FORM_NULL_ARG ? arg.defaultValue : value}
placeholder={arg.placeholder ?? undefined} placeholder={arg.placeholder ?? undefined}
autocompleteFunctions={autocompleteFunctions} autocompleteFunctions={autocompleteFunctions}
@@ -392,7 +392,7 @@ function EditorArg({
id: "id", id: "id",
size: "full", size: "full",
title: arg.readOnly ? "View Value" : "Edit Value", title: arg.readOnly ? "View Value" : "Edit Value",
className: "!max-w-[50rem] !max-h-[60rem]", className: "max-w-200! max-h-240!",
description: arg.label && ( description: arg.label && (
<Label <Label
htmlFor={id} htmlFor={id}
@@ -62,7 +62,7 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
size="sm" size="sm"
className={classNames( className={classNames(
className, className,
"text !px-2 truncate", "text px-2! truncate",
!activeEnvironment && !hasBaseVars && "text-text-subtlest italic", !activeEnvironment && !hasBaseVars && "text-text-subtlest italic",
)} )}
// If no environments, the button simply opens the dialog. // If no environments, the button simply opens the dialog.
@@ -57,7 +57,7 @@ export function EnvironmentEditDialog({ initialEnvironmentId, setRef }: Props) {
defaultRatio={0.75} defaultRatio={0.75}
layout="horizontal" layout="horizontal"
className="gap-0" className="gap-0"
resizeHandleClassName="-translate-x-[1px]" resizeHandleClassName="-translate-x-px"
firstSlot={() => ( firstSlot={() => (
<EnvironmentEditDialogSidebar <EnvironmentEditDialogSidebar
selectedEnvironmentId={selectedEnvironment?.id ?? null} selectedEnvironmentId={selectedEnvironment?.id ?? null}
+2 -2
View File
@@ -163,7 +163,7 @@ function HttpRequestCard({ request }: { request: HttpRequest }) {
return ( return (
<div className="grid grid-rows-2 grid-cols-[minmax(0,1fr)] gap-2 overflow-hidden"> <div className="grid grid-rows-2 grid-cols-[minmax(0,1fr)] gap-2 overflow-hidden">
<code className="font-mono text-editor text-info border border-info rounded px-2.5 py-0.5 truncate w-full min-w-0"> <code className="font-mono text-editor text-info border border-info rounded-sm px-2.5 py-0.5 truncate w-full min-w-0">
{request.method} {request.url} {request.method} {request.url}
</code> </code>
{latestResponse ? ( {latestResponse ? (
@@ -190,7 +190,7 @@ function HttpRequestCard({ request }: { request: HttpRequest }) {
className={classNames( className={classNames(
"cursor-default select-none", "cursor-default select-none",
"whitespace-nowrap w-full pl-3 overflow-x-auto font-mono text-sm hide-scrollbars", "whitespace-nowrap w-full pl-3 overflow-x-auto font-mono text-sm hide-scrollbars",
"font-mono text-editor border rounded px-1.5 py-0.5 truncate w-full", "font-mono text-editor border rounded-sm px-1.5 py-0.5 truncate w-full",
)} )}
> >
{latestResponse.state !== "closed" && <LoadingIcon size="sm" />} {latestResponse.state !== "closed" && <LoadingIcon size="sm" />}
@@ -84,12 +84,12 @@ export function FolderSettingsDialog({ folderId, tab }: Props) {
return ( return (
<div className="h-full flex flex-col"> <div className="h-full flex flex-col">
<div className="flex items-center gap-3 px-6 pr-10 mt-4 mb-2 min-w-0 text-xl"> <div className="flex items-center gap-3 px-6 pr-10 mt-4 mb-2 min-w-0 text-xl">
<Icon icon="folder_cog" size="lg" color="secondary" className="flex-shrink-0" /> <Icon icon="folder_cog" size="lg" color="secondary" className="shrink-0" />
<div className="flex items-center gap-1.5 font-semibold text-text min-w-0 overflow-hidden flex-1"> <div className="flex items-center gap-1.5 font-semibold text-text min-w-0 overflow-hidden flex-1">
{breadcrumbs.map((item, index) => ( {breadcrumbs.map((item, index) => (
<Fragment key={item.id}> <Fragment key={item.id}>
{index > 0 && ( {index > 0 && (
<Icon icon="chevron_right" size="lg" className="opacity-50 flex-shrink-0" /> <Icon icon="chevron_right" size="lg" className="opacity-50 shrink-0" />
)} )}
<span className="text-text-subtle truncate min-w-0" title={item.name}> <span className="text-text-subtle truncate min-w-0" title={item.name}>
{item.name} {item.name}
@@ -97,7 +97,7 @@ export function FolderSettingsDialog({ folderId, tab }: Props) {
</Fragment> </Fragment>
))} ))}
{breadcrumbs.length > 0 && ( {breadcrumbs.length > 0 && (
<Icon icon="chevron_right" size="lg" className="opacity-50 flex-shrink-0" /> <Icon icon="chevron_right" size="lg" className="opacity-50 shrink-0" />
)} )}
<span className="whitespace-nowrap" title={folder.name}> <span className="whitespace-nowrap" title={folder.name}>
{folder.name} {folder.name}
@@ -149,7 +149,7 @@ export function FolderSettingsDialog({ folderId, tab }: Props) {
<InlineCode className="flex gap-1 items-center text-primary pl-2.5"> <InlineCode className="flex gap-1 items-center text-primary pl-2.5">
{folder.id} {folder.id}
<CopyIconButton <CopyIconButton
className="opacity-70 !text-primary" className="opacity-70 text-primary!"
size="2xs" size="2xs"
iconSize="sm" iconSize="sm"
title="Copy folder ID" title="Copy folder ID"
+1 -1
View File
@@ -126,7 +126,7 @@ export function GrpcEditor({
const actions = useMemo( const actions = useMemo(
() => [ () => [
<div key="reflection" className={classNames(services == null && "!opacity-100")}> <div key="reflection" className={classNames(services == null && "opacity-100!")}>
<Button <Button
size="xs" size="xs"
color={ color={
@@ -162,7 +162,7 @@ export function GrpcRequestPane({
className={classNames( className={classNames(
"grid grid-cols-[minmax(0,1fr)_auto] gap-1.5", "grid grid-cols-[minmax(0,1fr)_auto] gap-1.5",
paneWidth === 0 && "opacity-0", paneWidth === 0 && "opacity-0",
paneWidth > 0 && paneWidth < 400 && "!grid-cols-1", paneWidth > 0 && paneWidth < 400 && "grid-cols-1!",
)} )}
> >
<UrlBar <UrlBar
@@ -201,7 +201,7 @@ export function GrpcRequestPane({
rightSlot={<Icon size="sm" icon="chevron_down" />} rightSlot={<Icon size="sm" icon="chevron_down" />}
disabled={isStreaming || services == null} disabled={isStreaming || services == null}
className={classNames( className={classNames(
"font-mono text-editor min-w-[5rem] !ring-0", "font-mono text-editor min-w-20 ring-0!",
paneWidth < 400 && "flex-1", paneWidth < 400 && "flex-1",
)} )}
> >
@@ -259,7 +259,7 @@ export function GrpcRequestPane({
<Tabs <Tabs
label="Request" label="Request"
tabs={tabs} tabs={tabs}
tabListClassName="mt-1 !mb-1.5" tabListClassName="mt-1 mb-1.5!"
storageKey="grpc_request_tabs" storageKey="grpc_request_tabs"
activeTabKey={activeRequest.id} activeTabKey={activeRequest.id}
> >
@@ -296,7 +296,7 @@ export function GrpcRequestPane({
hideLabel hideLabel
forceUpdateKey={forceUpdateKey} forceUpdateKey={forceUpdateKey}
defaultValue={activeRequest.name} defaultValue={activeRequest.name}
className="font-sans !text-xl !px-0" className="font-sans text-xl! px-0!"
containerClassName="border-0" containerClassName="border-0"
placeholder={resolvedModelName(activeRequest)} placeholder={resolvedModelName(activeRequest)}
onChange={(name) => patchModel(activeRequest, { name })} onChange={(name) => patchModel(activeRequest, { name })}
@@ -156,7 +156,7 @@ export function HttpAuthenticationEditor({ model }: Props) {
title="Authentication Actions" title="Authentication Actions"
icon="settings" icon="settings"
size="xs" size="xs"
className="!text-secondary" className="text-secondary!"
/> />
</Dropdown> </Dropdown>
)} )}
@@ -57,7 +57,7 @@ export function HttpRequestLayout({ activeRequest, style }: Props) {
<GraphQLDocsExplorer <GraphQLDocsExplorer
requestId={activeRequest.id} requestId={activeRequest.id}
schema={graphQLSchema} schema={graphQLSchema}
className={classNames(orientation === "horizontal" && "!ml-0")} className={classNames(orientation === "horizontal" && "ml-0!")}
style={style} style={style}
/> />
)} )}
@@ -346,7 +346,7 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
onUrlChange={handleUrlChange} onUrlChange={handleUrlChange}
leftSlot={ leftSlot={
<div className="py-0.5"> <div className="py-0.5">
<RequestMethodDropdown request={activeRequest} className="ml-0.5 !h-full" /> <RequestMethodDropdown request={activeRequest} className="ml-0.5 h-full!" />
</div> </div>
} }
forceUpdateKey={updateKey} forceUpdateKey={updateKey}
@@ -456,7 +456,7 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
hideLabel hideLabel
forceUpdateKey={updateKey} forceUpdateKey={updateKey}
defaultValue={activeRequest.name} defaultValue={activeRequest.name}
className="font-sans !text-xl !px-0" className="font-sans text-xl! px-0!"
containerClassName="border-0" containerClassName="border-0"
placeholder={resolvedModelName(activeRequest)} placeholder={resolvedModelName(activeRequest)}
onChange={(name) => patchModel(activeRequest, { name })} onChange={(name) => patchModel(activeRequest, { name })}
@@ -4,10 +4,12 @@ import classNames from "classnames";
import type { ComponentType, CSSProperties } from "react"; import type { ComponentType, CSSProperties } from "react";
import { lazy, Suspense, useMemo } from "react"; import { lazy, Suspense, useMemo } from "react";
import { useCancelHttpResponse } from "../hooks/useCancelHttpResponse"; import { useCancelHttpResponse } from "../hooks/useCancelHttpResponse";
import { useCopyHttpResponse } from "../hooks/useCopyHttpResponse";
import { useHttpResponseEvents } from "../hooks/useHttpResponseEvents"; import { useHttpResponseEvents } from "../hooks/useHttpResponseEvents";
import { usePinnedHttpResponse } from "../hooks/usePinnedHttpResponse"; import { usePinnedHttpResponse } from "../hooks/usePinnedHttpResponse";
import { useResponseBodyBytes, useResponseBodyText } from "../hooks/useResponseBodyText"; import { useResponseBodyBytes, useResponseBodyText } from "../hooks/useResponseBodyText";
import { useResponseViewMode } from "../hooks/useResponseViewMode"; import { useResponseViewMode } from "../hooks/useResponseViewMode";
import { useSaveResponse } from "../hooks/useSaveResponse";
import { useTimelineViewMode } from "../hooks/useTimelineViewMode"; import { useTimelineViewMode } from "../hooks/useTimelineViewMode";
import { getMimeTypeFromContentType } from "../lib/contentType"; import { getMimeTypeFromContentType } from "../lib/contentType";
import { getContentTypeFromHeaders, getCookieCounts } from "../lib/model_util"; import { getContentTypeFromHeaders, getCookieCounts } from "../lib/model_util";
@@ -78,6 +80,8 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
activeResponse?.state === "closed" && redirectDropWarning != null; activeResponse?.state === "closed" && redirectDropWarning != null;
const cookieCounts = useMemo(() => getCookieCounts(responseEvents.data), [responseEvents.data]); const cookieCounts = useMemo(() => getCookieCounts(responseEvents.data), [responseEvents.data]);
const saveResponse = useSaveResponse(activeResponse ?? null);
const copyResponse = useCopyHttpResponse(activeResponse ?? null);
const tabs = useMemo<TabItem[]>( const tabs = useMemo<TabItem[]>(
() => [ () => [
@@ -93,6 +97,22 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
? [] ? []
: [{ label: "Response (Raw)", shortLabel: "Raw", value: "raw" }]), : [{ label: "Response (Raw)", shortLabel: "Raw", value: "raw" }]),
], ],
itemsAfter: [
{
label: "Save to File",
onSelect: saveResponse.mutate,
leftSlot: <Icon icon="save" />,
hidden: activeResponse == null || !!activeResponse.error,
disabled: activeResponse?.state !== "closed" && (activeResponse?.status ?? 0) >= 100,
},
{
label: "Copy Body",
onSelect: copyResponse.mutate,
leftSlot: <Icon icon="copy" />,
hidden: activeResponse == null || !!activeResponse.error,
disabled: activeResponse?.state !== "closed" && (activeResponse?.status ?? 0) >= 100,
},
],
}, },
}, },
{ {
@@ -135,12 +155,18 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
], ],
[ [
activeResponse?.headers, activeResponse?.headers,
activeResponse,
activeResponse?.error,
activeResponse?.requestContentLength, activeResponse?.requestContentLength,
activeResponse?.requestHeaders.length, activeResponse?.requestHeaders.length,
activeResponse?.state,
activeResponse?.status,
cookieCounts.sent, cookieCounts.sent,
cookieCounts.received, cookieCounts.received,
copyResponse.mutate,
mimeType, mimeType,
responseEvents.data?.length, responseEvents.data?.length,
saveResponse.mutate,
setViewMode, setViewMode,
viewMode, viewMode,
timelineViewMode, timelineViewMode,
@@ -167,7 +193,7 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
<div className="h-full w-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1"> <div className="h-full w-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1">
<HStack <HStack
className={classNames( className={classNames(
"text-text-subtle w-full flex-shrink-0", "text-text-subtle w-full 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",
)} )}
@@ -180,7 +206,7 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
"whitespace-nowrap w-full pl-3 overflow-x-auto font-mono text-sm hide-scrollbars", "whitespace-nowrap w-full pl-3 overflow-x-auto font-mono text-sm hide-scrollbars",
)} )}
> >
<HStack space={2} className="w-full flex-shrink-0"> <HStack space={2} className="w-full shrink-0">
{activeResponse.state !== "closed" && <LoadingIcon size="sm" />} {activeResponse.state !== "closed" && <LoadingIcon size="sm" />}
<HttpStatusTag showReason response={activeResponse} /> <HttpStatusTag showReason response={activeResponse} />
<span>&bull;</span> <span>&bull;</span>
@@ -194,7 +220,7 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
{shouldShowRedirectDropWarning ? ( {shouldShowRedirectDropWarning ? (
<Tooltip <Tooltip
tabIndex={0} tabIndex={0}
className="my-auto pl-3 flex-shrink-0 max-w-full justify-self-end overflow-hidden" className="my-auto pl-3 shrink-0 max-w-full justify-self-end overflow-hidden"
content={ content={
<VStack alignItems="start" space={1} className="text-xs"> <VStack alignItems="start" space={1} className="text-xs">
<span className="font-medium text-warning"> <span className="font-medium text-warning">
@@ -223,7 +249,7 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
<span className="inline-flex min-w-0"> <span className="inline-flex min-w-0">
<PillButton <PillButton
color="warning" color="warning"
className="font-sans text-sm !flex-shrink max-w-full" className="font-sans text-sm shrink! max-w-full"
innerClassName="flex items-center" innerClassName="flex items-center"
leftSlot={<Icon icon="alert_triangle" size="xs" color="warning" />} leftSlot={<Icon icon="alert_triangle" size="xs" color="warning" />}
> >
@@ -236,7 +262,7 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
) : ( ) : (
<span /> <span />
)} )}
<div className="justify-self-end flex-shrink-0"> <div className="justify-self-end shrink-0">
<RecentHttpResponsesDropdown <RecentHttpResponsesDropdown
responses={responses} responses={responses}
activeResponse={activeResponse} activeResponse={activeResponse}
@@ -249,7 +275,7 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
<div className="overflow-hidden flex flex-col min-h-0"> <div className="overflow-hidden flex flex-col min-h-0">
{activeResponse?.error && ( {activeResponse?.error && (
<Banner color="danger" className="mx-3 mt-1 flex-shrink-0"> <Banner color="danger" className="mx-3 mt-1 shrink-0">
{activeResponse.error} {activeResponse.error}
</Banner> </Banner>
)} )}
@@ -73,14 +73,14 @@ export function JsonBodyEditor({ forceUpdateKey, heightMode, request }: Props) {
const actions = useMemo<EditorProps["actions"]>( const actions = useMemo<EditorProps["actions"]>(
() => [ () => [
showBanner && ( showBanner && (
<Banner color="notice" className="!opacity-100 h-sm !py-0 !px-2 flex items-center text-xs"> <Banner color="notice" className="opacity-100! h-sm py-0! px-2! flex items-center text-xs">
<p className="inline-flex items-center gap-1 min-w-0"> <p className="inline-flex items-center gap-1 min-w-0">
<span className="truncate">Auto-fix enabled</span> <span className="truncate">Auto-fix enabled</span>
<Icon icon="arrow_right" size="sm" className="opacity-disabled" /> <Icon icon="arrow_right" size="sm" className="opacity-disabled" />
</p> </p>
</Banner> </Banner>
), ),
<div key="settings" className="!opacity-100 !shadow"> <div key="settings" className="opacity-100! shadow!">
<Dropdown <Dropdown
onOpen={handleDropdownOpen} onOpen={handleDropdownOpen}
items={ items={
+1 -1
View File
@@ -59,7 +59,7 @@ function getDetail(
label: `License expired ${formatDate(data.data.periodEnd, "MMM dd, yyyy")}`, label: `License expired ${formatDate(data.data.periodEnd, "MMM dd, yyyy")}`,
}, },
{ {
label: <div className="min-w-[12rem]">Renew License</div>, label: <div className="min-w-48">Renew License</div>,
leftSlot: <Icon icon="refresh" />, leftSlot: <Icon icon="refresh" />,
rightSlot: <Icon icon="external_link" size="sm" className="opacity-disabled" />, rightSlot: <Icon icon="external_link" size="sm" className="opacity-disabled" />,
hidden: data.data.changesUrl == null, hidden: data.data.changesUrl == null,
@@ -33,7 +33,7 @@ export function MarkdownEditor({
<Editor <Editor
hideGutter hideGutter
wrapLines wrapLines
className={classNames(editorClassName, "[&_.cm-line]:!max-w-lg max-h-full")} className={classNames(editorClassName, "[&_.cm-line]:max-w-lg! max-h-full")}
language="markdown" language="markdown"
defaultValue={defaultValue} defaultValue={defaultValue}
onChange={onChange} onChange={onChange}
@@ -46,7 +46,7 @@ export function MarkdownEditor({
defaultValue.length === 0 ? ( defaultValue.length === 0 ? (
<p className="text-text-subtlest">No description</p> <p className="text-text-subtlest">No description</p>
) : ( ) : (
<div className="pr-1.5 overflow-y-auto max-h-full [&_*]:cursor-auto [&_*]:select-auto"> <div className="pr-1.5 overflow-y-auto max-h-full **:cursor-auto **:select-auto">
<Markdown className="max-w-lg select-auto cursor-auto">{defaultValue}</Markdown> <Markdown className="max-w-lg select-auto cursor-auto">{defaultValue}</Markdown>
</div> </div>
); );
@@ -539,7 +539,7 @@ function NumberUnitInput({
placeholder={placeholder} placeholder={placeholder}
defaultValue={value} defaultValue={value}
className="[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" className="[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
containerClassName="!w-48" containerClassName="w-48!"
validate={validate} validate={validate}
rightSlot={ rightSlot={
<span className="flex self-stretch items-center border-l border-border-subtle px-2 text-xs font-medium text-text-subtle"> <span className="flex self-stretch items-center border-l border-border-subtle px-2 text-xs font-medium text-text-subtle">
@@ -66,7 +66,7 @@ export function MoveToWorkspaceDialog({ onDone, requests, activeWorkspaceId }: P
<Button <Button
size="xs" size="xs"
color="secondary" color="secondary"
className="mr-auto min-w-[5rem]" className="mr-auto min-w-20"
onClick={async () => { onClick={async () => {
await router.navigate({ await router.navigate({
to: "/workspaces/$workspaceId", to: "/workspaces/$workspaceId",
+7 -5
View File
@@ -1,3 +1,5 @@
@reference "../main.css";
.prose { .prose {
@apply text-text; @apply text-text;
@@ -98,7 +100,7 @@
@apply text-notice hover:underline; @apply text-notice hover:underline;
* { * {
@apply text-notice !important; @apply text-notice!;
} }
} }
@@ -113,12 +115,12 @@
ol code, ol code,
ul code { ul code {
@apply text-xs bg-surface-active text-info font-normal whitespace-nowrap; @apply text-xs bg-surface-active text-info font-normal whitespace-nowrap;
@apply px-1.5 py-0.5 rounded not-italic; @apply px-1.5 py-0.5 rounded-sm not-italic;
@apply select-text; @apply select-text;
} }
pre { pre {
@apply bg-surface-highlight text-text !important; @apply bg-surface-highlight! text-text!;
@apply px-4 py-3 rounded-md; @apply px-4 py-3 rounded-md;
@apply overflow-auto whitespace-pre; @apply overflow-auto whitespace-pre;
@apply text-editor font-mono; @apply text-editor font-mono;
@@ -130,7 +132,7 @@
.banner { .banner {
@apply border border-dashed; @apply border border-dashed;
@apply border-border bg-surface-highlight text-text px-4 py-3 rounded text-base; @apply border-border bg-surface-highlight text-text px-4 py-3 rounded-sm text-base;
&::before { &::before {
@apply block font-bold mb-1; @apply block font-bold mb-1;
@@ -161,7 +163,7 @@
} }
blockquote { blockquote {
@apply italic py-3 pl-5 pr-3 border-l-8 border-surface-active text-lg text-text bg-surface-highlight rounded shadow-lg; @apply italic py-3 pl-5 pr-3 border-l-8 border-surface-active text-lg text-text bg-surface-highlight rounded-sm shadow-lg;
p { p {
@apply m-0; @apply m-0;
@@ -1,10 +1,17 @@
import type { GrpcConnection } from "@yaakapp-internal/models"; import type { GrpcConnection } from "@yaakapp-internal/models";
import { deleteModel } from "@yaakapp-internal/models"; import { deleteModel } from "@yaakapp-internal/models";
import { HStack, Icon } from "@yaakapp-internal/ui"; import { HStack, Icon } from "@yaakapp-internal/ui";
import { formatDistanceToNowStrict } from "date-fns"; import {
differenceInHours,
differenceInMinutes,
format,
isToday,
isYesterday,
} from "date-fns";
import { useDeleteGrpcConnections } from "../hooks/useDeleteGrpcConnections"; import { useDeleteGrpcConnections } from "../hooks/useDeleteGrpcConnections";
import { pluralizeCount } from "../lib/pluralize"; import { pluralizeCount } from "../lib/pluralize";
import { Dropdown } from "./core/Dropdown"; import { Dropdown, type DropdownItem } from "./core/Dropdown";
import { formatMillis } from "./core/HttpResponseDurationTag";
import { IconButton } from "./core/IconButton"; import { IconButton } from "./core/IconButton";
interface Props { interface Props {
@@ -20,6 +27,63 @@ export function RecentGrpcConnectionsDropdown({
}: Props) { }: Props) {
const deleteAllConnections = useDeleteGrpcConnections(activeConnection?.requestId); const deleteAllConnections = useDeleteGrpcConnections(activeConnection?.requestId);
const latestConnectionId = connections[0]?.id ?? "n/a"; const latestConnectionId = connections[0]?.id ?? "n/a";
const connectionHistoryItems: DropdownItem[] = [];
let lastHistoryGroup: string | null = null;
let hasRecentConnections = false;
let hasShownRecentEmptyState = false;
const now = new Date();
for (const c of connections) {
const createdAt = `${c.createdAt}Z`;
const createdAtDate = new Date(createdAt);
const minutesAgo = differenceInMinutes(now, createdAtDate);
const hoursAgo = differenceInHours(now, createdAtDate);
let historyGroup = format(createdAtDate, "MMM d, yyyy");
if (minutesAgo < 5) historyGroup = "Just now";
else if (minutesAgo < 15) historyGroup = "5 minutes ago";
else if (minutesAgo < 60) historyGroup = "15 minutes ago";
else if (hoursAgo < 3) historyGroup = "1 hour ago";
else if (hoursAgo < 6) historyGroup = "3 hours ago";
else if (isToday(createdAtDate)) historyGroup = "Today";
else if (isYesterday(createdAtDate)) historyGroup = "Yesterday";
else if (createdAtDate.getFullYear() === now.getFullYear()) historyGroup = format(createdAtDate, "MMM d");
const absoluteTime = format(createdAt, "MMM d, yyyy, h:mm:ss a O");
if (historyGroup === "Just now") {
hasRecentConnections = true;
} else if (!hasRecentConnections && !hasShownRecentEmptyState) {
connectionHistoryItems.push({
type: "content",
label: <span className="block px-4 py-1 text-sm text-text-subtle">No recent connections</span>,
});
hasShownRecentEmptyState = true;
}
if (historyGroup !== "Just now" && historyGroup !== lastHistoryGroup) {
connectionHistoryItems.push({
type: "separator",
label: <span title={absoluteTime}>{historyGroup}</span>,
});
lastHistoryGroup = historyGroup;
}
connectionHistoryItems.push({
label: (
<HStack space={2} className="text-sm" title={absoluteTime}>
<span className="font-mono">{formatMillis(c.elapsed)}</span>
</HStack>
),
leftSlot: activeConnection?.id === c.id ? <Icon icon="check" /> : <Icon icon="empty" />,
onSelect: () => onPinnedConnectionId(c.id),
});
}
if (!hasRecentConnections && !hasShownRecentEmptyState) {
connectionHistoryItems.push({
type: "content",
label: <span className="block px-4 py-1 text-sm text-text-subtle">No recent connections</span>,
});
}
return ( return (
<Dropdown <Dropdown
@@ -36,16 +100,7 @@ export function RecentGrpcConnectionsDropdown({
disabled: connections.length === 0, disabled: connections.length === 0,
}, },
{ type: "separator", label: "History" }, { type: "separator", label: "History" },
...connections.map((c) => ({ ...connectionHistoryItems,
label: (
<HStack space={2}>
{formatDistanceToNowStrict(`${c.createdAt}Z`)} ago &bull;{" "}
<span className="font-mono text-sm">{c.elapsed}ms</span>
</HStack>
),
leftSlot: activeConnection?.id === c.id ? <Icon icon="check" /> : <Icon icon="empty" />,
onSelect: () => onPinnedConnectionId(c.id),
})),
]} ]}
> >
<IconButton <IconButton
@@ -1,13 +1,21 @@
import type { HttpResponse } from "@yaakapp-internal/models"; import type { HttpResponse } from "@yaakapp-internal/models";
import { deleteModel } from "@yaakapp-internal/models"; import { deleteModel } from "@yaakapp-internal/models";
import { HStack, Icon } from "@yaakapp-internal/ui"; import { HStack, Icon } from "@yaakapp-internal/ui";
import { useCopyHttpResponse } from "../hooks/useCopyHttpResponse"; import {
differenceInHours,
differenceInMinutes,
format,
isToday,
isYesterday,
} from "date-fns";
import { useDeleteHttpResponses } from "../hooks/useDeleteHttpResponses"; import { useDeleteHttpResponses } from "../hooks/useDeleteHttpResponses";
import { useSaveResponse } from "../hooks/useSaveResponse"; import { useKeyValue } from "../hooks/useKeyValue";
import { pluralize } from "../lib/pluralize"; import { DismissibleBanner } from "./core/DismissibleBanner";
import { Dropdown } from "./core/Dropdown"; import { Dropdown, type DropdownItem } from "./core/Dropdown";
import { formatMillis } from "./core/HttpResponseDurationTag";
import { HttpStatusTag } from "./core/HttpStatusTag"; import { HttpStatusTag } from "./core/HttpStatusTag";
import { IconButton } from "./core/IconButton"; import { IconButton } from "./core/IconButton";
import { SizeTag } from "./core/SizeTag";
interface Props { interface Props {
responses: HttpResponse[]; responses: HttpResponse[];
@@ -22,32 +30,93 @@ export const RecentHttpResponsesDropdown = function ResponsePane({
onPinnedResponseId, onPinnedResponseId,
}: Props) { }: Props) {
const deleteAllResponses = useDeleteHttpResponses(activeResponse?.requestId); const deleteAllResponses = useDeleteHttpResponses(activeResponse?.requestId);
const movedActionsBannerId = "response-actions-moved-to-response-menu-2026-07-02-v2";
const { value: dismissedMovedActions } = useKeyValue<boolean>({
namespace: "global",
key: ["dismiss-banner", movedActionsBannerId],
fallback: false,
});
const latestResponseId = responses[0]?.id ?? "n/a"; const latestResponseId = responses[0]?.id ?? "n/a";
const saveResponse = useSaveResponse(activeResponse); const responseHistoryItems: DropdownItem[] = [];
const copyResponse = useCopyHttpResponse(activeResponse); let lastHistoryGroup: string | null = null;
let hasRecentResponses = false;
let hasShownRecentEmptyState = false;
const now = new Date();
for (const r of responses) {
const createdAt = `${r.createdAt}Z`;
const createdAtDate = new Date(createdAt);
const minutesAgo = differenceInMinutes(now, createdAtDate);
const hoursAgo = differenceInHours(now, createdAtDate);
let historyGroup = format(createdAtDate, "MMM d, yyyy");
if (minutesAgo < 5) historyGroup = "Just now";
else if (minutesAgo < 15) historyGroup = "5 minutes ago";
else if (minutesAgo < 60) historyGroup = "15 minutes ago";
else if (hoursAgo < 3) historyGroup = "1 hour ago";
else if (hoursAgo < 6) historyGroup = "3 hours ago";
else if (isToday(createdAtDate)) historyGroup = "Today";
else if (isYesterday(createdAtDate)) historyGroup = "Yesterday";
else if (createdAtDate.getFullYear() === now.getFullYear()) historyGroup = format(createdAtDate, "MMM d");
const absoluteTime = format(createdAt, "MMM d, yyyy, h:mm:ss a O");
if (historyGroup === "Just now") {
hasRecentResponses = true;
} else if (!hasRecentResponses && !hasShownRecentEmptyState) {
responseHistoryItems.push({
type: "content",
label: <span className="block px-4 py-1 text-sm text-text-subtle">No recent requests</span>,
});
hasShownRecentEmptyState = true;
}
if (historyGroup !== "Just now" && historyGroup !== lastHistoryGroup) {
responseHistoryItems.push({
type: "separator",
label: <span title={absoluteTime}>{historyGroup}</span>,
});
lastHistoryGroup = historyGroup;
}
responseHistoryItems.push({
label: (
<HStack space={2} className="text-sm" title={absoluteTime}>
<HttpStatusTag short className="text-xs" response={r} />
<span className="text-text-subtlest">&bull;</span>
<span className="font-mono">{r.elapsed >= 0 ? formatMillis(r.elapsed) : "n/a"}</span>
<span className="text-text-subtlest">&bull;</span>
<SizeTag
className="text-xs"
contentLength={r.contentLength ?? 0}
contentLengthCompressed={r.contentLengthCompressed}
/>
</HStack>
),
leftSlot: activeResponse?.id === r.id ? <Icon icon="check" /> : <Icon icon="empty" />,
onSelect: () => onPinnedResponseId(r.id),
});
}
if (!hasRecentResponses && !hasShownRecentEmptyState) {
responseHistoryItems.push({
type: "content",
label: <span className="block px-4 py-1 text-sm text-text-subtle">No recent requests</span>,
});
}
return ( return (
<Dropdown <Dropdown
items={[ items={[
{
label: "Save to File",
onSelect: saveResponse.mutate,
leftSlot: <Icon icon="save" />,
hidden: responses.length === 0 || !!activeResponse.error,
disabled: activeResponse.state !== "closed" && activeResponse.status >= 100,
},
{
label: "Copy Body",
onSelect: copyResponse.mutate,
leftSlot: <Icon icon="copy" />,
hidden: responses.length === 0 || !!activeResponse.error,
disabled: activeResponse.state !== "closed" && activeResponse.status >= 100,
},
{ {
label: "Delete", label: "Delete",
leftSlot: <Icon icon="trash" />, leftSlot: <Icon icon="trash" />,
onSelect: () => deleteModel(activeResponse), onSelect: () => deleteModel(activeResponse),
}, },
{
label: "Delete all",
leftSlot: <Icon icon="trash" />,
onSelect: deleteAllResponses.mutate,
disabled: responses.length === 0,
},
{ {
label: "Unpin Response", label: "Unpin Response",
onSelect: () => onPinnedResponseId(activeResponse.id), onSelect: () => onPinnedResponseId(activeResponse.id),
@@ -55,25 +124,25 @@ export const RecentHttpResponsesDropdown = function ResponsePane({
hidden: latestResponseId === activeResponse.id, hidden: latestResponseId === activeResponse.id,
disabled: responses.length === 0, disabled: responses.length === 0,
}, },
{ type: "separator", label: "History" },
{ {
label: `Delete ${responses.length} ${pluralize("Response", responses.length)}`, type: "content",
onSelect: deleteAllResponses.mutate, hidden: dismissedMovedActions === true,
hidden: responses.length === 0,
disabled: responses.length === 0,
},
{ type: "separator" },
...responses.map((r: HttpResponse) => ({
label: ( label: (
<HStack space={2}> <DismissibleBanner
<HttpStatusTag short className="text-xs" response={r} /> id={movedActionsBannerId}
<span className="text-text-subtle">&rarr;</span>{" "} color="info"
<span className="font-mono text-sm">{r.elapsed >= 0 ? `${r.elapsed}ms` : "n/a"}</span> size="xs"
</HStack> className="max-w-72"
>
<p>Copy and save actions moved to the Response tab menu.</p>
</DismissibleBanner>
), ),
leftSlot: activeResponse?.id === r.id ? <Icon icon="check" /> : <Icon icon="empty" />, },
onSelect: () => onPinnedResponseId(r.id), {
})), type: "separator",
label: "Recent",
},
...responseHistoryItems,
]} ]}
> >
<IconButton <IconButton
@@ -1,10 +1,17 @@
import type { WebsocketConnection } from "@yaakapp-internal/models"; import type { WebsocketConnection } from "@yaakapp-internal/models";
import { deleteModel, getModel } from "@yaakapp-internal/models"; import { deleteModel, getModel } from "@yaakapp-internal/models";
import { HStack, Icon } from "@yaakapp-internal/ui"; import { HStack, Icon } from "@yaakapp-internal/ui";
import { formatDistanceToNowStrict } from "date-fns"; import {
differenceInHours,
differenceInMinutes,
format,
isToday,
isYesterday,
} from "date-fns";
import { deleteWebsocketConnections } from "../commands/deleteWebsocketConnections"; import { deleteWebsocketConnections } from "../commands/deleteWebsocketConnections";
import { pluralizeCount } from "../lib/pluralize"; import { pluralizeCount } from "../lib/pluralize";
import { Dropdown } from "./core/Dropdown"; import { Dropdown, type DropdownItem } from "./core/Dropdown";
import { formatMillis } from "./core/HttpResponseDurationTag";
import { IconButton } from "./core/IconButton"; import { IconButton } from "./core/IconButton";
interface Props { interface Props {
@@ -19,6 +26,63 @@ export function RecentWebsocketConnectionsDropdown({
onPinnedConnectionId, onPinnedConnectionId,
}: Props) { }: Props) {
const latestConnectionId = connections[0]?.id ?? "n/a"; const latestConnectionId = connections[0]?.id ?? "n/a";
const connectionHistoryItems: DropdownItem[] = [];
let lastHistoryGroup: string | null = null;
let hasRecentConnections = false;
let hasShownRecentEmptyState = false;
const now = new Date();
for (const c of connections) {
const createdAt = `${c.createdAt}Z`;
const createdAtDate = new Date(createdAt);
const minutesAgo = differenceInMinutes(now, createdAtDate);
const hoursAgo = differenceInHours(now, createdAtDate);
let historyGroup = format(createdAtDate, "MMM d, yyyy");
if (minutesAgo < 5) historyGroup = "Just now";
else if (minutesAgo < 15) historyGroup = "5 minutes ago";
else if (minutesAgo < 60) historyGroup = "15 minutes ago";
else if (hoursAgo < 3) historyGroup = "1 hour ago";
else if (hoursAgo < 6) historyGroup = "3 hours ago";
else if (isToday(createdAtDate)) historyGroup = "Today";
else if (isYesterday(createdAtDate)) historyGroup = "Yesterday";
else if (createdAtDate.getFullYear() === now.getFullYear()) historyGroup = format(createdAtDate, "MMM d");
const absoluteTime = format(createdAt, "MMM d, yyyy, h:mm:ss a O");
if (historyGroup === "Just now") {
hasRecentConnections = true;
} else if (!hasRecentConnections && !hasShownRecentEmptyState) {
connectionHistoryItems.push({
type: "content",
label: <span className="block px-4 py-1 text-sm text-text-subtle">No recent connections</span>,
});
hasShownRecentEmptyState = true;
}
if (historyGroup !== "Just now" && historyGroup !== lastHistoryGroup) {
connectionHistoryItems.push({
type: "separator",
label: <span title={absoluteTime}>{historyGroup}</span>,
});
lastHistoryGroup = historyGroup;
}
connectionHistoryItems.push({
label: (
<HStack space={2} className="text-sm" title={absoluteTime}>
<span className="font-mono">{formatMillis(c.elapsed)}</span>
</HStack>
),
leftSlot: activeConnection?.id === c.id ? <Icon icon="check" /> : <Icon icon="empty" />,
onSelect: () => onPinnedConnectionId(c.id),
});
}
if (!hasRecentConnections && !hasShownRecentEmptyState) {
connectionHistoryItems.push({
type: "content",
label: <span className="block px-4 py-1 text-sm text-text-subtle">No recent connections</span>,
});
}
return ( return (
<Dropdown <Dropdown
@@ -40,16 +104,7 @@ export function RecentWebsocketConnectionsDropdown({
disabled: connections.length === 0, disabled: connections.length === 0,
}, },
{ type: "separator", label: "History" }, { type: "separator", label: "History" },
...connections.map((c) => ({ ...connectionHistoryItems,
label: (
<HStack space={2}>
{formatDistanceToNowStrict(`${c.createdAt}Z`)} ago &bull;{" "}
<span className="font-mono text-sm">{c.elapsed}ms</span>
</HStack>
),
leftSlot: activeConnection?.id === c.id ? <Icon icon="check" /> : <Icon icon="empty" />,
onSelect: () => onPinnedConnectionId(c.id),
})),
]} ]}
> >
<IconButton <IconButton
@@ -167,7 +167,7 @@ export function ResponseCookies({ response }: Props) {
{cookie.value} {cookie.value}
</span> </span>
{cookie.isDeleted && ( {cookie.isDeleted && (
<span className="text-xs font-sans text-danger bg-danger/10 px-1.5 py-0.5 rounded"> <span className="text-xs font-sans text-danger bg-danger/10 px-1.5 py-0.5 rounded-sm">
Deleted Deleted
</span> </span>
)} )}
@@ -1,5 +1,6 @@
import { openUrl } from "@tauri-apps/plugin-opener"; import { openUrl } from "@tauri-apps/plugin-opener";
import type { HttpResponse } from "@yaakapp-internal/models"; import type { HttpResponse } from "@yaakapp-internal/models";
import { format, formatDistanceToNowStrict } from "date-fns";
import { useMemo } from "react"; import { useMemo } from "react";
import { CountBadge } from "./core/CountBadge"; import { CountBadge } from "./core/CountBadge";
import { DetailsBanner } from "./core/DetailsBanner"; import { DetailsBanner } from "./core/DetailsBanner";
@@ -29,12 +30,20 @@ export function ResponseHeaders({ response }: Props) {
<div className="overflow-auto h-full pb-4 gap-y-3 flex flex-col pr-0.5"> <div className="overflow-auto h-full pb-4 gap-y-3 flex flex-col pr-0.5">
<DetailsBanner storageKey={`${response.requestId}.general`} summary={<h2>Info</h2>}> <DetailsBanner storageKey={`${response.requestId}.general`} summary={<h2>Info</h2>}>
<KeyValueRows> <KeyValueRows>
<KeyValueRow labelColor="secondary" label="Sent">
<time
dateTime={new Date(`${response.createdAt}Z`).toISOString()}
title={formatDistanceToNowStrict(`${response.createdAt}Z`, { addSuffix: true })}
>
{format(`${response.createdAt}Z`, "MMM d, yyyy, h:mm:ss a O")}
</time>
</KeyValueRow>
<KeyValueRow labelColor="secondary" label="Request URL"> <KeyValueRow labelColor="secondary" label="Request URL">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span className="select-text cursor-text">{response.url}</span> <span className="select-text cursor-text">{response.url}</span>
<IconButton <IconButton
iconSize="sm" iconSize="sm"
className="inline-block w-auto !h-auto opacity-50 hover:opacity-100" className="inline-block w-auto h-auto! opacity-50 hover:opacity-100"
icon="external_link" icon="external_link"
onClick={() => openUrl(response.url)} onClick={() => openUrl(response.url)}
title="Open in browser" title="Open in browser"
+1 -1
View File
@@ -24,7 +24,7 @@ export function ResponseInfo({ response }: Props) {
URL URL
<IconButton <IconButton
iconSize="sm" iconSize="sm"
className="inline-block w-auto ml-1 !h-auto opacity-50 hover:opacity-100" className="inline-block w-auto ml-1 h-auto! opacity-50 hover:opacity-100"
icon="external_link" icon="external_link"
onClick={() => openUrl(response.url)} onClick={() => openUrl(response.url)}
title="Open in browser" title="Open in browser"
+1 -1
View File
@@ -10,7 +10,7 @@ export default function RouteError({ error }: { error: unknown }) {
typeof error === "object" && error != null && "stack" in error ? String(error.stack) : null; typeof error === "object" && error != null && "stack" in error ? String(error.stack) : null;
return ( return (
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full">
<VStack space={5} className="w-[50rem] !h-auto"> <VStack space={5} className="w-200 h-auto!">
<Heading>Route Error 🔥</Heading> <Heading>Route Error 🔥</Heading>
<FormattedError> <FormattedError>
{message} {message}
+1 -1
View File
@@ -108,7 +108,7 @@ export function SelectFile({
"rtl mr-1.5", "rtl mr-1.5",
inline && "w-full", inline && "w-full",
filePath && inline && "font-mono text-xs", filePath && inline && "font-mono text-xs",
isHovering && "!border-notice", isHovering && "border-notice!",
)} )}
color={isHovering ? "primary" : "secondary"} color={isHovering ? "primary" : "secondary"}
onClick={handleClick} onClick={handleClick}
@@ -93,7 +93,7 @@ export default function Settings({ hide }: Props) {
layout="horizontal" layout="horizontal"
defaultValue={mainTab || tabFromQuery} defaultValue={mainTab || tabFromQuery}
addBorders addBorders
tabListClassName="min-w-[10rem] bg-surface x-theme-sidebar border-r border-border pl-3" tabListClassName="min-w-40 bg-surface x-theme-sidebar border-r border-border pl-3"
label="Settings" label="Settings"
tabs={tabs.map( tabs={tabs.map(
(value): TabItem => ({ (value): TabItem => ({
@@ -131,28 +131,28 @@ export default function Settings({ hide }: Props) {
}), }),
)} )}
> >
<TabContent value={TAB_GENERAL} className="overflow-y-auto h-full px-6 !py-4"> <TabContent value={TAB_GENERAL} className="overflow-y-auto h-full px-6 py-4!">
<SettingsGeneral /> <SettingsGeneral />
</TabContent> </TabContent>
<TabContent value={TAB_INTERFACE} className="overflow-y-auto h-full px-6 !py-4"> <TabContent value={TAB_INTERFACE} className="overflow-y-auto h-full px-6 py-4!">
<SettingsInterface /> <SettingsInterface />
</TabContent> </TabContent>
<TabContent value={TAB_THEME} className="overflow-y-auto h-full px-6 !py-4"> <TabContent value={TAB_THEME} className="overflow-y-auto h-full px-6 py-4!">
<SettingsTheme /> <SettingsTheme />
</TabContent> </TabContent>
<TabContent value={TAB_SHORTCUTS} className="overflow-y-auto h-full px-6 !py-4"> <TabContent value={TAB_SHORTCUTS} className="overflow-y-auto h-full px-6 py-4!">
<SettingsHotkeys /> <SettingsHotkeys />
</TabContent> </TabContent>
<TabContent value={TAB_PLUGINS} className="h-full grid grid-rows-1"> <TabContent value={TAB_PLUGINS} className="h-full grid grid-rows-1">
<SettingsPlugins defaultSubtab={mainTab === TAB_PLUGINS ? subtab : undefined} /> <SettingsPlugins defaultSubtab={mainTab === TAB_PLUGINS ? subtab : undefined} />
</TabContent> </TabContent>
<TabContent value={TAB_PROXY} className="overflow-y-auto h-full px-6 !py-4"> <TabContent value={TAB_PROXY} className="overflow-y-auto h-full px-6 py-4!">
<SettingsProxy /> <SettingsProxy />
</TabContent> </TabContent>
<TabContent value={TAB_CERTIFICATES} className="overflow-y-auto h-full px-6 !py-4"> <TabContent value={TAB_CERTIFICATES} className="overflow-y-auto h-full px-6 py-4!">
<SettingsCertificates /> <SettingsCertificates />
</TabContent> </TabContent>
<TabContent value={TAB_LICENSE} className="overflow-y-auto h-full px-6 !py-4"> <TabContent value={TAB_LICENSE} className="overflow-y-auto h-full px-6 py-4!">
<SettingsLicense /> <SettingsLicense />
</TabContent> </TabContent>
</Tabs> </Tabs>
@@ -56,7 +56,7 @@ export function SettingsGeneral() {
model={settings} model={settings}
modelKey="updateChannel" modelKey="updateChannel"
label="Update Channel" label="Update Channel"
selectClassName="!w-full" selectClassName="w-full!"
options={[ options={[
{ label: "Stable", value: "stable" }, { label: "Stable", value: "stable" },
{ label: "Beta", value: "beta" }, { label: "Beta", value: "beta" },
@@ -341,7 +341,7 @@ function RecordHotkeyDialog({ label, onSave, onCancel }: RecordHotkeyDialogProps
}} }}
className={classNames( className={classNames(
"flex items-center justify-center", "flex items-center justify-center",
"px-4 py-2 rounded-lg bg-surface-highlight border outline-none cursor-default w-full", "px-4 py-2 rounded-lg bg-surface-highlight border outline-hidden cursor-default w-full",
"border-border-subtle focus:border-border-focus", "border-border-subtle focus:border-border-focus",
)} )}
> >
@@ -89,7 +89,7 @@ export function SettingsInterface() {
<SettingSelectControl <SettingSelectControl
name="uiFont" name="uiFont"
label="Interface font" label="Interface font"
selectClassName="!w-72" selectClassName="w-72!"
value={settings.interfaceFont ?? NULL_FONT_VALUE} value={settings.interfaceFont ?? NULL_FONT_VALUE}
defaultValue={NULL_FONT_VALUE} defaultValue={NULL_FONT_VALUE}
options={[ options={[
@@ -106,7 +106,7 @@ export function SettingsInterface() {
<SettingSelectControl <SettingSelectControl
name="interfaceFontSize" name="interfaceFontSize"
label="Interface Font Size" label="Interface Font Size"
selectClassName="!w-20" selectClassName="w-20!"
value={`${settings.interfaceFontSize}`} value={`${settings.interfaceFontSize}`}
defaultValue="14" defaultValue="14"
options={fontSizeOptions} options={fontSizeOptions}
@@ -123,7 +123,7 @@ export function SettingsInterface() {
<SettingSelectControl <SettingSelectControl
name="editorFont" name="editorFont"
label="Editor font" label="Editor font"
selectClassName="!w-72" selectClassName="w-72!"
value={settings.editorFont ?? NULL_FONT_VALUE} value={settings.editorFont ?? NULL_FONT_VALUE}
defaultValue={NULL_FONT_VALUE} defaultValue={NULL_FONT_VALUE}
options={[ options={[
@@ -139,7 +139,7 @@ export function SettingsInterface() {
<SettingSelectControl <SettingSelectControl
name="editorFontSize" name="editorFontSize"
label="Editor Font Size" label="Editor Font Size"
selectClassName="!w-20" selectClassName="w-20!"
value={`${settings.editorFontSize}`} value={`${settings.editorFontSize}`}
defaultValue="12" defaultValue="12"
options={fontSizeOptions} options={fontSizeOptions}
@@ -211,7 +211,7 @@ function PluginTableRow({
return ( return (
<TableRow> <TableRow>
{showCheckbox && ( {showCheckbox && (
<TableCell className="!py-0"> <TableCell className="py-0!">
<Checkbox <Checkbox
hideLabel hideLabel
title={plugin?.enabled ? "Disable plugin" : "Enable plugin"} title={plugin?.enabled ? "Disable plugin" : "Enable plugin"}
@@ -249,7 +249,7 @@ function PluginTableRow({
)} )}
</HStack> </HStack>
</TableCell> </TableCell>
<TableCell className="!py-0"> <TableCell className="py-0!">
<HStack justifyContent="end" space={1.5}> <HStack justifyContent="end" space={1.5}>
{plugin != null && latestVersion != null ? ( {plugin != null && latestVersion != null ? (
<Button <Button
@@ -56,7 +56,7 @@ export function SettingsProxy() {
{ label: "Custom proxy configuration", value: "enabled" }, { label: "Custom proxy configuration", value: "enabled" },
{ label: "No proxy", value: "disabled" }, { label: "No proxy", value: "disabled" },
]} ]}
selectClassName="!w-64" selectClassName="w-64!"
/> />
</SettingsSection> </SettingsSection>
@@ -99,7 +99,7 @@ export function SettingsProxy() {
description="Comma-separated list of hosts that should bypass the proxy." description="Comma-separated list of hosts that should bypass the proxy."
value={settings.proxy.bypass} value={settings.proxy.bypass}
placeholder="127.0.0.1, *.example.com, localhost:3000" placeholder="127.0.0.1, *.example.com, localhost:3000"
inputWidthClassName="!w-96" inputWidthClassName="w-96!"
onChange={(bypass) => patchProxy({ bypass })} onChange={(bypass) => patchProxy({ bypass })}
/> />
</SettingsSection> </SettingsSection>
@@ -120,7 +120,7 @@ export function SettingsTheme() {
<SettingsSection title="Preview"> <SettingsSection title="Preview">
<VStack <VStack
space={3} space={3}
className="mt-4 w-full bg-surface p-3 border border-dashed border-border-subtle rounded overflow-x-auto" className="mt-4 w-full bg-surface p-3 border border-dashed border-border-subtle rounded-sm overflow-x-auto"
> >
<HStack className="text" space={1.5}> <HStack className="text" space={1.5}>
<Icon icon={appearance === "dark" ? "moon" : "sun"} /> <Icon icon={appearance === "dark" ? "moon" : "sun"} />
+6 -6
View File
@@ -588,7 +588,7 @@ function Sidebar({ className }: { className?: string }) {
rightSlot={ rightSlot={
filterText.text && ( filterText.text && (
<IconButton <IconButton
className="!bg-transparent !h-auto min-h-full opacity-50 hover:opacity-100 -mr-1" className="bg-transparent! h-auto! min-h-full opacity-50 hover:opacity-100 -mr-1"
icon="x" icon="x"
title="Clear filter" title="Clear filter"
onClick={clearFilterText} onClick={clearFilterText}
@@ -667,8 +667,8 @@ function Sidebar({ className }: { className?: string }) {
<div className="p-3 text-sm text-center"> <div className="p-3 text-sm text-center">
{(emptyFilterSuggestions?.length ?? 0) > 0 ? ( {(emptyFilterSuggestions?.length ?? 0) > 0 ? (
<EmptyStateText <EmptyStateText
wrapperClassName="!h-auto mb-auto" wrapperClassName="h-auto! mb-auto"
className="!h-auto py-3 px-3 !text-text-subtle text-sm leading-relaxed text-center" className="h-auto! py-3 px-3 text-text-subtle! text-sm leading-relaxed text-center"
> >
<div> <div>
No results, but found matches for{" "} No results, but found matches for{" "}
@@ -677,7 +677,7 @@ function Sidebar({ className }: { className?: string }) {
{i > 0 && " or "} {i > 0 && " or "}
<button <button
type="button" type="button"
className="max-w-full rounded align-middle focus-visible:outline focus-visible:outline-2 focus-visible:outline-info" className="max-w-full rounded-sm align-middle focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-info"
onClick={() => applyFilterExample(suggestion.filterText)} onClick={() => applyFilterExample(suggestion.filterText)}
> >
<InlineCode className="inline-block max-w-36 truncate align-middle whitespace-nowrap transition-colors hover:border-border hover:bg-surface-active hover:text-text"> <InlineCode className="inline-block max-w-36 truncate align-middle whitespace-nowrap transition-colors hover:border-border hover:bg-surface-active hover:text-text">
@@ -690,8 +690,8 @@ function Sidebar({ className }: { className?: string }) {
</EmptyStateText> </EmptyStateText>
) : ( ) : (
<EmptyStateText <EmptyStateText
wrapperClassName="!h-auto mb-auto" wrapperClassName="h-auto! mb-auto"
className="!h-auto py-3 px-3 !text-text-subtle text-sm leading-relaxed text-center" className="h-auto! py-3 px-3 text-text-subtle! text-sm leading-relaxed text-center"
> >
<div> <div>
No results for{" "} No results for{" "}
@@ -208,10 +208,10 @@ function InitializedTemplateFunctionDialog({
)} )}
/> />
</HStack> </HStack>
<div className="relative w-full max-h-[10rem]"> <div className="relative w-full max-h-40">
<InlineCode <InlineCode
className={classNames( className={classNames(
"block whitespace-pre-wrap !select-text cursor-text max-h-[10rem] overflow-auto hide-scrollbars !border-text-subtlest", "block whitespace-pre-wrap select-text! cursor-text max-h-40 overflow-auto hide-scrollbars border-text-subtlest!",
tooLarge && "italic text-danger", tooLarge && "italic text-danger",
)} )}
> >
@@ -246,7 +246,7 @@ function InitializedTemplateFunctionDialog({
) : ( ) : (
<span /> <span />
)} )}
<div className="flex justify-stretch w-full flex-grow gap-2 [&>*]:flex-1"> <div className="flex justify-stretch w-full grow gap-2 *:flex-1">
{templateFunction.data.name === "secure" && ( {templateFunction.data.name === "secure" && (
<Button variant="border" color="secondary" onClick={setupOrConfigureEncryption}> <Button variant="border" color="secondary" onClick={setupOrConfigureEncryption}>
Reveal Encryption Key Reveal Encryption Key
@@ -271,7 +271,7 @@ TemplateFunctionDialog.show = (
showDialog({ showDialog({
id: `template-function-${Math.random()}`, // Allow multiple at once id: `template-function-${Math.random()}`, // Allow multiple at once
size: "md", size: "md",
className: "h-[60rem]", className: "h-240",
noPadding: true, noPadding: true,
title: <InlineCode>{fn.name}()</InlineCode>, title: <InlineCode>{fn.name}()</InlineCode>,
description: fn.description, description: fn.description,
+1 -1
View File
@@ -94,7 +94,7 @@ export const UrlBar = memo(function UrlBar({
iconSize="md" iconSize="md"
title="Send Request" title="Send Request"
type="submit" type="submit"
className="w-8 mr-0.5 !h-full" className="w-8 mr-0.5 h-full!"
iconColor="secondary" iconColor="secondary"
icon={isLoading ? "x" : submitIcon} icon={isLoading ? "x" : submitIcon}
hotkeyAction="request.send" hotkeyAction="request.send"
@@ -217,7 +217,7 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque
title="Close connection" title="Close connection"
icon="x" icon="x"
iconColor="secondary" iconColor="secondary"
className="w-8 mr-0.5 !h-full" className="w-8 mr-0.5 h-full!"
onClick={handleCancel} onClick={handleCancel}
/> />
) )
@@ -236,7 +236,7 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque
ref={tabsRef} ref={tabsRef}
label="Request" label="Request"
tabs={tabs} tabs={tabs}
tabListClassName="mt-1 !mb-1.5" tabListClassName="mt-1 mb-1.5!"
storageKey={TABS_STORAGE_KEY} storageKey={TABS_STORAGE_KEY}
activeTabKey={activeRequestId} activeTabKey={activeRequestId}
> >
@@ -283,7 +283,7 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque
hideLabel hideLabel
forceUpdateKey={forceUpdateKey} forceUpdateKey={forceUpdateKey}
defaultValue={activeRequest.name} defaultValue={activeRequest.name}
className="font-sans !text-xl !px-0" className="font-sans text-xl! px-0!"
containerClassName="border-0" containerClassName="border-0"
placeholder={resolvedModelName(activeRequest)} placeholder={resolvedModelName(activeRequest)}
onChange={(name) => patchModel(activeRequest, { name })} onChange={(name) => patchModel(activeRequest, { name })}
+2 -2
View File
@@ -85,7 +85,7 @@ export function Workspace() {
<div style={environmentBgStyle} className="absolute inset-0 opacity-[0.07]" /> <div style={environmentBgStyle} className="absolute inset-0 opacity-[0.07]" />
<div <div
style={environmentBgStyle} style={environmentBgStyle}
className="absolute left-0 right-0 -bottom-[1px] h-[1px] opacity-20" className="absolute left-0 right-0 -bottom-px h-px opacity-20"
/> />
</div> </div>
<WorkspaceHeader className="pointer-events-none" floatingSidebar={floating} /> <WorkspaceHeader className="pointer-events-none" floatingSidebar={floating} />
@@ -162,7 +162,7 @@ function WorkspaceBody() {
// Delay the entering because the workspaces might load after a slight delay // Delay the entering because the workspaces might load after a slight delay
transition={{ delay: 0.5 }} transition={{ delay: 0.5 }}
> >
<Banner color="warning" className="max-w-[30rem]"> <Banner color="warning" className="max-w-120">
The active workspace was not found. Select a workspace from the header menu or report this The active workspace was not found. Select a workspace from the header menu or report this
bug to <FeedbackLink /> bug to <FeedbackLink />
</Banner> </Banner>
@@ -176,7 +176,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
size="sm" size="sm"
className={classNames( className={classNames(
className, className,
"text !px-2 truncate", "text px-2! truncate",
workspace === null && "italic opacity-disabled", workspace === null && "italic opacity-disabled",
)} )}
{...buttonProps} {...buttonProps}
@@ -324,7 +324,7 @@ function KeyRevealer({
function HighlightedKey({ keyText, show }: { keyText: string; show: boolean }) { function HighlightedKey({ keyText, show }: { keyText: string; show: boolean }) {
return ( return (
<span className="text-xs font-mono [&_*]:cursor-auto [&_*]:select-text"> <span className="text-xs font-mono **:cursor-auto **:select-text">
{show ? ( {show ? (
keyText.split("").map((c, i) => { keyText.split("").map((c, i) => {
return ( return (
@@ -127,7 +127,7 @@ export function WorkspaceSettingsDialog({ workspaceId, hide, tab }: Props) {
placeholder="Workspace Name" placeholder="Workspace Name"
label="Name" label="Name"
defaultValue={workspace.name} defaultValue={workspace.name}
className="!text-base font-sans" className="text-base! font-sans"
onChange={(name) => patchModel(workspace, { name })} onChange={(name) => patchModel(workspace, { name })}
/> />
@@ -161,7 +161,7 @@ export function WorkspaceSettingsDialog({ workspaceId, hide, tab }: Props) {
<InlineCode className="flex gap-1 items-center text-primary pl-2.5"> <InlineCode className="flex gap-1 items-center text-primary pl-2.5">
{workspaceId} {workspaceId}
<CopyIconButton <CopyIconButton
className="opacity-70 !text-primary" className="opacity-70 text-primary!"
size="2xs" size="2xs"
iconSize="sm" iconSize="sm"
title="Copy workspace ID" title="Copy workspace ID"
@@ -182,7 +182,7 @@ WorkspaceSettingsDialog.show = (workspaceId: string, tab?: WorkspaceSettingsTab)
showDialog({ showDialog({
id: "workspace-settings", id: "workspace-settings",
size: "lg", size: "lg",
className: "h-[calc(100vh-5rem)] !max-h-[50rem]", className: "h-[calc(100vh-5rem)] max-h-200!",
noPadding: true, noPadding: true,
render: ({ hide }) => ( render: ({ hide }) => (
<WorkspaceSettingsDialog workspaceId={workspaceId} hide={hide} tab={tab} /> <WorkspaceSettingsDialog workspaceId={workspaceId} hide={hide} tab={tab} />
@@ -72,7 +72,7 @@ export function AutoScroller<T>({
size="sm" size="sm"
iconSize="md" iconSize="md"
variant="border" variant="border"
className="!bg-surface z-10" className="bg-surface! z-10"
onClick={() => setAutoScroll((v) => !v)} onClick={() => setAutoScroll((v) => !v)}
/> />
</div> </div>
@@ -80,7 +80,7 @@ export function AutoScroller<T>({
{header ?? <span aria-hidden />} {header ?? <span aria-hidden />}
<div <div
ref={containerRef} ref={containerRef}
className="h-full w-full overflow-y-auto focus:outline-none" className="h-full w-full overflow-y-auto focus:outline-hidden"
onScroll={handleScroll} onScroll={handleScroll}
tabIndex={focusable ? 0 : undefined} tabIndex={focusable ? 0 : undefined}
> >
@@ -39,10 +39,10 @@ export function Checkbox({
<input <input
aria-hidden aria-hidden
className={classNames( className={classNames(
"appearance-none flex-shrink-0 border border-border", "appearance-none shrink-0 border border-border",
size === "sm" && "w-4 h-4", size === "sm" && "w-4 h-4",
size === "md" && "w-5 h-5", size === "md" && "w-5 h-5",
"rounded outline-none ring-0", "rounded-sm outline-hidden ring-0",
!disabled && "hocus:border-border-focus hocus:bg-focus/[5%]", !disabled && "hocus:border-border-focus hocus:bg-focus/[5%]",
disabled && "border-dotted", disabled && "border-dotted",
)} )}
@@ -17,7 +17,7 @@ export function ColorPicker({ onChange, color, className }: Props) {
<div className={className}> <div className={className}>
<HexColorPicker <HexColorPicker
color={color ?? undefined} color={color ?? undefined}
className="!w-full" className="w-full!"
onChange={(color) => { onChange={(color) => {
onChange(color); onChange(color);
regenerateKey(); // To force input to change regenerateKey(); // To force input to change
@@ -96,7 +96,7 @@ export function ColorPickerWithThemeColors({ onChange, color, className }: Props
<> <>
<HexColorPicker <HexColorPicker
color={color ?? undefined} color={color ?? undefined}
className="!w-full" className="w-full!"
onChange={(color) => { onChange={(color) => {
onChange(color); onChange(color);
regenerateKey(); // To force input to change regenerateKey(); // To force input to change
@@ -18,7 +18,7 @@ export function CountBadge({ count, count2, className, color, showZero }: Props)
className={classNames( className={classNames(
className, className,
"flex items-center", "flex items-center",
"opacity-70 border text-4xs rounded mb-0.5 px-1 ml-1 h-4 font-mono", "opacity-70 border text-4xs rounded-sm mb-0.5 px-1 ml-1 h-4 font-mono",
color == null && "border-border-subtle", color == null && "border-border-subtle",
color === "primary" && "text-primary", color === "primary" && "text-primary",
color === "secondary" && "text-secondary", color === "secondary" && "text-secondary",
@@ -42,7 +42,7 @@ export function DetailsBanner({
return ( return (
<Banner color={color} className={className}> <Banner color={color} className={className}>
<details className="group list-none" open={isOpen} onToggle={handleToggle} {...extraProps}> <details className="group list-none" open={isOpen} onToggle={handleToggle} {...extraProps}>
<summary className="!cursor-default !select-none list-none flex items-center gap-3 focus:outline-none opacity-70"> <summary className="cursor-default! select-none! list-none flex items-center gap-3 focus:outline-hidden opacity-70">
<div <div
className={classNames( className={classNames(
"transition-transform", "transition-transform",
+6 -6
View File
@@ -74,13 +74,13 @@ export function Dialog({
"relative bg-surface pointer-events-auto", "relative bg-surface pointer-events-auto",
"rounded-lg", "rounded-lg",
"border border-border-subtle shadow-lg shadow-[rgba(0,0,0,0.1)]", "border border-border-subtle shadow-lg shadow-[rgba(0,0,0,0.1)]",
"min-h-[10rem]", "min-h-40",
"max-w-[calc(100vw-5rem)] max-h-[calc(100vh-5rem)]", "max-w-[calc(100vw-5rem)] max-h-[calc(100vh-5rem)]",
size === "sm" && "w-[30rem]", size === "sm" && "w-120",
size === "md" && "w-[50rem]", size === "md" && "w-200",
size === "lg" && "w-[70rem]", size === "lg" && "w-280",
size === "full" && "w-[100vw] h-[100vh]", size === "full" && "w-screen h-screen",
size === "dynamic" && "min-w-[20rem] max-w-[100vw]", size === "dynamic" && "min-w-80 max-w-[100vw]",
)} )}
> >
{title ? ( {title ? (
@@ -2,21 +2,26 @@ import type { Color } from "@yaakapp-internal/plugins";
import type { BannerProps } from "@yaakapp-internal/ui"; import type { BannerProps } from "@yaakapp-internal/ui";
import { Banner } from "@yaakapp-internal/ui"; import { Banner } from "@yaakapp-internal/ui";
import classNames from "classnames"; import classNames from "classnames";
import type { MouseEvent } from "react";
import { useEffect } from "react"; import { useEffect } from "react";
import { useKeyValue } from "../../hooks/useKeyValue"; import { useKeyValue } from "../../hooks/useKeyValue";
import type { ButtonProps } from "./Button"; import type { ButtonProps } from "./Button";
import { Button } from "./Button"; import { Button } from "./Button";
type DismissibleBannerSize = "sm" | "xs";
export function DismissibleBanner({ export function DismissibleBanner({
children, children,
className, className,
id, id,
size = "sm",
onDismiss, onDismiss,
onShow, onShow,
actions, actions,
...props ...props
}: BannerProps & { }: BannerProps & {
id: string; id: string;
size?: DismissibleBannerSize;
onDismiss?: () => void | Promise<void>; onDismiss?: () => void | Promise<void>;
onShow?: () => void | Promise<void>; onShow?: () => void | Promise<void>;
actions?: { actions?: {
@@ -46,17 +51,36 @@ export function DismissibleBanner({
if (!shouldShow) return null; if (!shouldShow) return null;
const actionSize: ButtonProps["size"] = size === "xs" ? "2xs" : "xs";
const stopParentClick = (event: MouseEvent) => {
event.preventDefault();
event.stopPropagation();
};
return ( return (
<Banner className={classNames(className, "relative")} {...props}> <Banner
className={classNames(
className,
"relative",
size === "xs" && "!px-2 !py-2 text-xs",
)}
{...props}
>
<div className="@container"> <div className="@container">
<div className="grid gap-2 @[34rem]:grid-cols-[minmax(0,1fr)_auto] @[34rem]:items-center @[34rem]:gap-3"> <div
className={classNames(
"grid @[34rem]:grid-cols-[minmax(0,1fr)_auto] @[34rem]:items-center",
size === "xs" ? "gap-1.5 @[34rem]:gap-2" : "gap-2 @[34rem]:gap-3",
)}
>
{children} {children}
<div className="flex flex-wrap gap-1.5 @[34rem]:justify-end"> <div className="flex flex-wrap gap-1.5 @[34rem]:justify-end">
<Button <Button
variant="border" variant="border"
color={props.color} color={props.color}
size="xs" size={actionSize}
onClick={() => { onClick={(event) => {
stopParentClick(event);
setDismissed(true).catch(console.error); setDismissed(true).catch(console.error);
Promise.resolve(onDismiss?.()).catch(console.error); Promise.resolve(onDismiss?.()).catch(console.error);
}} }}
@@ -69,8 +93,11 @@ export function DismissibleBanner({
key={a.label} key={a.label}
variant={a.variant ?? "border"} variant={a.variant ?? "border"}
color={a.color ?? props.color} color={a.color ?? props.color}
size="xs" size={actionSize}
onClick={a.onClick} onClick={(event) => {
stopParentClick(event);
a.onClick();
}}
title={a.label} title={a.label}
> >
{a.label} {a.label}
+13 -13
View File
@@ -712,7 +712,7 @@ const Menu = forwardRef<Omit<DropdownRef, "open" | "isOpen" | "toggle" | "items"
className={classNames( className={classNames(
className, className,
"x-theme-menu", "x-theme-menu",
"outline-none my-1 pointer-events-auto z-40", "outline-hidden my-1 pointer-events-auto z-40",
"fixed", "fixed",
)} )}
> >
@@ -734,7 +734,7 @@ const Menu = forwardRef<Omit<DropdownRef, "open" | "isOpen" | "toggle" | "items"
{filter && ( {filter && (
<HStack <HStack
space={2} space={2}
className="pb-0.5 px-1.5 mb-2 text-sm border border-border-subtle mx-2 rounded font-mono h-xs" className="pb-0.5 px-1.5 mb-2 text-sm border border-border-subtle mx-2 rounded-sm font-mono h-xs"
> >
<Icon icon="search" size="xs" /> <Icon icon="search" size="xs" />
<div className="text">{filter}</div> <div className="text">{filter}</div>
@@ -916,24 +916,24 @@ function MenuItem({
) )
} }
rightSlot={rightSlot && <div className="ml-auto pl-3">{rightSlot}</div>} rightSlot={rightSlot && <div className="ml-auto pl-3">{rightSlot}</div>}
innerClassName="!text-left" innerClassName="text-left!"
color="custom" 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 whitespace-nowrap", "min-w-32 outline-hidden px-2 mx-1.5 flex whitespace-nowrap",
"focus:bg-surface-highlight focus:text rounded focus:outline-none focus-visible:outline-1", "focus:bg-surface-highlight focus:text rounded-sm focus:outline-hidden focus-visible:outline-1",
isParentOfActiveSubmenu && "bg-surface-highlight text rounded", isParentOfActiveSubmenu && "bg-surface-highlight text rounded-sm",
item.color === "danger" && "!text-danger", item.color === "danger" && "text-danger!",
item.color === "primary" && "!text-primary", item.color === "primary" && "text-primary!",
item.color === "success" && "!text-success", item.color === "success" && "text-success!",
item.color === "warning" && "!text-warning", item.color === "warning" && "text-warning!",
item.color === "notice" && "!text-notice", item.color === "notice" && "text-notice!",
item.color === "info" && "!text-info", item.color === "info" && "text-info!",
)} )}
{...props} {...props}
> >
<div className={classNames("truncate min-w-[5rem]")}>{item.label}</div> <div className={classNames("truncate min-w-20")}>{item.label}</div>
</Button> </Button>
); );
} }
@@ -1,3 +1,5 @@
@reference "../../../main.css";
.cm-wrapper.cm-multiline .cm-mergeView { .cm-wrapper.cm-multiline .cm-mergeView {
@apply h-full w-full overflow-auto pr-0.5; @apply h-full w-full overflow-auto pr-0.5;
@@ -9,7 +11,7 @@
@apply w-full min-h-full relative; @apply w-full min-h-full relative;
.cm-collapsedLines { .cm-collapsedLines {
@apply bg-none bg-surface border border-border py-1 mx-0.5 text-text opacity-80 hover:opacity-100 rounded cursor-default; @apply bg-none bg-surface border border-border py-1 mx-0.5 text-text opacity-80 hover:opacity-100 rounded-sm cursor-default;
} }
} }
@@ -19,21 +21,21 @@
.cm-changedLine { .cm-changedLine {
/* Round top corners only if previous line is not a changed line */ /* Round top corners only if previous line is not a changed line */
&:not(.cm-changedLine + &) { &:not(.cm-changedLine + &) {
@apply rounded-t; @apply rounded-t-sm;
} }
/* Round bottom corners only if next line is not a changed line */ /* Round bottom corners only if next line is not a changed line */
&:not(:has(+ .cm-changedLine)) { &:not(:has(+ .cm-changedLine)) {
@apply rounded-b; @apply rounded-b-sm;
} }
} }
/* Let content grow and disable individual scrolling for sync */ /* Let content grow and disable individual scrolling for sync */
.cm-editor { .cm-editor {
@apply h-auto relative !important; @apply h-auto! relative!;
position: relative !important; position: relative !important;
} }
.cm-scroller { .cm-scroller {
@apply overflow-visible !important; @apply overflow-visible!;
} }
} }
@@ -1,3 +1,5 @@
@reference "../../../main.css";
.cm-wrapper { .cm-wrapper {
@apply h-full overflow-hidden; @apply h-full overflow-hidden;
@@ -7,7 +9,7 @@
/* Regular cursor */ /* Regular cursor */
.cm-cursor { .cm-cursor {
@apply border-text !important; @apply border-text!;
/* Widen the cursor a bit */ /* Widen the cursor a bit */
@apply border-l-[2px]; @apply border-l-[2px];
} }
@@ -15,8 +17,8 @@
/* Vim-mode cursor */ /* Vim-mode cursor */
.cm-fat-cursor { .cm-fat-cursor {
@apply outline-0 bg-text !important; @apply outline-0! bg-text!;
@apply text-surface !important; @apply text-surface!;
} }
/* Matching bracket */ /* Matching bracket */
@@ -59,12 +61,12 @@
* { * {
@apply cursor-text; @apply cursor-text;
@apply caret-transparent !important; @apply caret-transparent!;
} }
} }
.cm-selectionBackground { .cm-selectionBackground {
@apply bg-selection !important; @apply bg-selection!;
} }
/* Fix WebKit/WKWebView rendering bug where selection layer leaves a ghost /* Fix WebKit/WKWebView rendering bug where selection layer leaves a ghost
@@ -88,7 +90,7 @@
} }
.cm-gutter-lint { .cm-gutter-lint {
@apply w-auto !important; @apply w-auto!;
.cm-gutterElement { .cm-gutterElement {
@apply px-0; @apply px-0;
@@ -111,7 +113,7 @@
@apply bg-surface text-text-subtle border-border-subtle whitespace-nowrap cursor-default; @apply bg-surface text-text-subtle border-border-subtle whitespace-nowrap cursor-default;
@apply hover:border-border hover:text-text hover:bg-surface-highlight; @apply hover:border-border hover:text-text hover:bg-surface-highlight;
@apply inline border px-1 mx-[0.5px] rounded dark:shadow; @apply inline border px-1 mx-[0.5px] rounded-sm dark:shadow;
-webkit-text-security: none; -webkit-text-security: none;
@@ -162,7 +164,7 @@
&::-webkit-scrollbar-corner, &::-webkit-scrollbar-corner,
&::-webkit-scrollbar { &::-webkit-scrollbar {
@apply hidden !important; @apply hidden!;
} }
} }
} }
@@ -189,16 +191,16 @@
/* Style search matches */ /* Style search matches */
.cm-searchMatch { .cm-searchMatch {
@apply bg-transparent !important; @apply bg-transparent!;
@apply rounded-[2px] outline outline-1; @apply rounded-[2px] outline outline-1;
&.cm-searchMatch-selected { &.cm-searchMatch-selected {
@apply outline-text; @apply outline-text;
@apply bg-text !important; @apply bg-text!;
&, &,
* { * {
@apply text-surface font-semibold !important; @apply text-surface! font-semibold!;
} }
} }
} }
@@ -223,8 +225,8 @@
} }
.cm-editor .fold-gutter-icon { .cm-editor .fold-gutter-icon {
@apply pt-[0.25em] pl-[0.4em] px-[0.4em] h-4 rounded; @apply pt-[0.25em] pl-[0.4em] px-[0.4em] h-4 rounded-sm;
@apply cursor-default !important; @apply cursor-default!;
} }
.cm-editor .fold-gutter-icon::after { .cm-editor .fold-gutter-icon::after {
@@ -248,7 +250,7 @@
.cm-editor .cm-foldPlaceholder { .cm-editor .cm-foldPlaceholder {
@apply px-2 border border-border-subtle bg-surface-highlight; @apply px-2 border border-border-subtle bg-surface-highlight;
@apply hover:text-text hover:border-border-subtle text-text; @apply hover:text-text hover:border-border-subtle text-text;
@apply cursor-default !important; @apply cursor-default!;
} }
.cm-editor .cm-activeLineGutter { .cm-editor .cm-activeLineGutter {
@@ -277,7 +279,7 @@
} }
.cm-tooltip-lint { .cm-tooltip-lint {
@apply font-mono text-editor rounded overflow-hidden bg-surface-highlight border border-border shadow !important; @apply font-mono! text-editor! rounded-sm! overflow-hidden! bg-surface-highlight! border! border-border! shadow!;
.cm-diagnostic-error { .cm-diagnostic-error {
@apply border-l-danger px-4 py-2; @apply border-l-danger px-4 py-2;
@@ -293,18 +295,18 @@
} }
.cm-tooltip.cm-tooltip-hover { .cm-tooltip.cm-tooltip-hover {
@apply shadow-lg bg-surface rounded text-text-subtle border border-border-subtle z-50 pointer-events-auto text-sm; @apply shadow-lg bg-surface rounded-sm text-text-subtle border border-border-subtle z-50 pointer-events-auto text-sm;
@apply p-1.5; @apply p-1.5;
/* Style the tooltip for popping up "open in browser" and other stuff */ /* Style the tooltip for popping up "open in browser" and other stuff */
a, a,
button { button {
@apply text-text hover:bg-surface-highlight w-full h-sm flex items-center px-2 rounded; @apply text-text hover:bg-surface-highlight w-full h-sm flex items-center px-2 rounded-sm;
} }
a { a {
@apply cursor-default !important; @apply cursor-default!;
&::after { &::after {
@apply text-text bg-text h-3 w-3 ml-1; @apply text-text bg-text h-3 w-3 ml-1;
@@ -319,10 +321,10 @@
/* 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-surface rounded text-text-subtle border border-border-subtle z-50 pointer-events-auto; @apply shadow-lg bg-surface rounded-sm text-text-subtle border border-border-subtle z-50 pointer-events-auto;
& * { & * {
@apply font-mono text-editor !important; @apply font-mono! text-editor!;
} }
.cm-completionIcon { .cm-completionIcon {
@@ -409,7 +411,7 @@
} }
.cm-completionIcon { .cm-completionIcon {
@apply text-sm flex items-center pb-0.5 flex-shrink-0; @apply text-sm flex items-center pb-0.5 shrink-0;
} }
.cm-completionLabel { .cm-completionLabel {
@@ -427,7 +429,7 @@
input, input,
button { button {
@apply rounded-sm outline-none; @apply rounded-sm outline-hidden;
} }
button { button {
@@ -436,12 +438,12 @@
} }
button[name="close"] { button[name="close"] {
@apply text-text-subtle hocus:text-text px-2 -mr-1.5 !important; @apply text-text-subtle! hocus:text-text! px-2! -mr-1.5!;
} }
input { input {
@apply bg-surface border-border-subtle focus:border-border-focus; @apply bg-surface border-border-subtle focus:border-border-focus;
@apply border outline-none; @apply border outline-hidden;
} }
input.cm-textfield { input.cm-textfield {
@@ -486,7 +486,7 @@ function EditorInner({
const decoratedActions = useMemo(() => { const decoratedActions = useMemo(() => {
const results = []; const results = [];
const actionClassName = classNames( const actionClassName = classNames(
"bg-surface transition-opacity transform-gpu opacity-0 group-hover:opacity-100 hover:!opacity-100 shadow", "bg-surface transition-opacity transform-gpu opacity-0 group-hover:opacity-100 hover:opacity-100! shadow",
); );
if (format) { if (format) {
@@ -24,8 +24,8 @@ export function EventViewerRow({
onClick={onClick} onClick={onClick}
className={classNames( className={classNames(
"w-full grid grid-cols-[auto_minmax(0,1fr)_auto] gap-2 items-center text-left", "w-full grid grid-cols-[auto_minmax(0,1fr)_auto] gap-2 items-center text-left",
"px-1.5 h-xs font-mono text-editor cursor-default group focus:outline-none focus:text-text rounded", "px-1.5 h-xs font-mono text-editor cursor-default group focus:outline-hidden focus:text-text rounded-sm",
isActive && "bg-surface-active !text-text", isActive && "bg-surface-active text-text!",
"text-text-subtle hover:text", "text-text-subtle hover:text",
)} )}
> >
+1 -1
View File
@@ -30,7 +30,7 @@ export function HotkeyRaw({ labelParts, className, variant }: HotkeyRawProps) {
className={classNames( className={classNames(
className, className,
variant === "with-bg" && variant === "with-bg" &&
"rounded bg-surface-highlight px-1 border border-border text-text-subtle", "rounded-sm bg-surface-highlight px-1 border border-border text-text-subtle",
variant === "text" && "text-text-subtlest", variant === "text" && "text-text-subtlest",
)} )}
> >
@@ -81,7 +81,7 @@ export function HttpMethodTagRaw({
colored && m === "PATCH" && "text-notice", colored && m === "PATCH" && "text-notice",
colored && m === "POST" && "text-success", colored && m === "POST" && "text-success",
colored && m === "DELETE" && "text-danger", colored && m === "DELETE" && "text-danger",
"font-mono flex-shrink-0 whitespace-pre", "font-mono shrink-0 whitespace-pre",
"pt-[0.15em]", // Fix for monospace font not vertically centering "pt-[0.15em]", // Fix for monospace font not vertically centering
)} )}
> >
@@ -31,7 +31,7 @@ export function HttpResponseDurationTag({ response }: Props) {
); );
} }
function formatMillis(ms: number) { export function formatMillis(ms: number) {
if (ms < 1000) { if (ms < 1000) {
return `${ms} ms`; return `${ms} ms`;
} }
+5 -5
View File
@@ -201,7 +201,7 @@ function BaseInput({
const id = useRef(`input-${generateId()}`); const id = useRef(`input-${generateId()}`);
const editorClassName = classNames( const editorClassName = classNames(
className, className,
"!bg-transparent min-w-0 h-auto w-full focus:outline-none placeholder:text-placeholder", "bg-transparent! min-w-0 h-auto w-full focus:outline-hidden placeholder:text-placeholder",
); );
const isValid = useMemo(() => { const isValid = useMemo(() => {
@@ -264,7 +264,7 @@ function BaseInput({
"border", "border",
focused && !disabled ? "border-border-focus" : "border-border", focused && !disabled ? "border-border-focus" : "border-border",
disabled && "border-dotted", disabled && "border-dotted",
!isValid && hasChanged && "!border-danger", !isValid && hasChanged && "border-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",
@@ -333,7 +333,7 @@ function BaseInput({
: `Obscure ${typeof label === "string" ? label : "field"}` : `Obscure ${typeof label === "string" ? label : "field"}`
} }
size="xs" size="xs"
className={classNames("mr-0.5 !h-auto my-0.5", disabled && "opacity-disabled")} className={classNames("mr-0.5 h-auto! my-0.5", disabled && "opacity-disabled")}
color={tint} color={tint}
// iconClassName={classNames( // iconClassName={classNames(
// tint === 'primary' && 'text-primary', // tint === 'primary' && 'text-primary',
@@ -548,9 +548,9 @@ function EncryptionInput({
color={tint} color={tint}
aria-label="Configure encryption" aria-label="Configure encryption"
className={classNames( className={classNames(
"flex items-center justify-center !h-full !px-1", "flex items-center justify-center h-full! px-1!",
"opacity-70", // Makes it a bit subtler "opacity-70", // Makes it a bit subtler
props.disabled && "!opacity-disabled", props.disabled && "opacity-disabled!",
)} )}
> >
<HStack space={0.5}> <HStack space={0.5}>
@@ -73,7 +73,7 @@ export function KeyValueRow({
<> <>
<td <td
className={classNames( className={classNames(
"select-none py-0.5 pr-2 h-full max-w-[10rem]", "select-none py-0.5 pr-2 h-full max-w-40",
align === "top" && "align-top", align === "top" && "align-top",
align === "middle" && "align-middle", align === "middle" && "align-middle",
labelClassName, labelClassName,
@@ -86,12 +86,12 @@ export function KeyValueRow({
</td> </td>
<td <td
className={classNames( className={classNames(
"select-none py-0.5 break-all max-w-[15rem]", "select-none py-0.5 break-all max-w-60",
align === "top" && "align-top", align === "top" && "align-top",
align === "middle" && "align-middle", align === "middle" && "align-middle",
)} )}
> >
<div className="select-text cursor-text max-h-[12rem] overflow-y-auto grid grid-cols-[auto_minmax(0,1fr)_auto]"> <div className="select-text cursor-text max-h-48 overflow-y-auto grid grid-cols-[auto_minmax(0,1fr)_auto]">
{leftSlot ?? <span aria-hidden />} {leftSlot ?? <span aria-hidden />}
{children} {children}
{resolvedRightSlot ? ( {resolvedRightSlot ? (
+1 -1
View File
@@ -27,7 +27,7 @@ export function Label({
className={classNames( className={classNames(
className, className,
visuallyHidden && "sr-only", visuallyHidden && "sr-only",
"flex-shrink-0 text-sm", "shrink-0 text-sm",
"text-text-subtle whitespace-nowrap flex items-center gap-1 mb-0.5", "text-text-subtle whitespace-nowrap flex items-center gap-1 mb-0.5",
)} )}
{...props} {...props}
@@ -566,7 +566,7 @@ export function PairEditorRow({
title={pair.enabled ? "Disable item" : "Enable item"} title={pair.enabled ? "Disable item" : "Enable item"}
disabled={isLast || disabled} disabled={isLast || disabled}
checked={isLast ? false : !!pair.enabled} checked={isLast ? false : !!pair.enabled}
className={classNames(isLast && "!opacity-disabled")} className={classNames(isLast && "opacity-disabled!")}
onChange={handleChangeEnabled} onChange={handleChangeEnabled}
/> />
{!isLast && !disableDrag ? ( {!isLast && !disableDrag ? (
@@ -586,7 +586,7 @@ export function PairEditorRow({
<div <div
className={classNames( className={classNames(
"grid items-center", "grid items-center",
"@xs:gap-2 @xs:!grid-rows-1 @xs:!grid-cols-[minmax(0,1fr)_minmax(0,1fr)]", "@xs:gap-2 @xs:grid-rows-1! @xs:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]!",
"gap-0.5 grid-cols-1 grid-rows-2", "gap-0.5 grid-cols-1 grid-rows-2",
)} )}
> >
@@ -830,7 +830,7 @@ function MultilineEditDialog({
const [value, setValue] = useState<string>(defaultValue); const [value, setValue] = useState<string>(defaultValue);
const language = languageFromContentType(contentType, value); const language = languageFromContentType(contentType, value);
return ( return (
<div className="w-[100vw] max-w-[40rem] h-[50vh] max-h-full grid grid-rows-[minmax(0,1fr)_auto]"> <div className="w-screen max-w-160 h-[50vh] max-h-full grid grid-rows-[minmax(0,1fr)_auto]">
<Editor <Editor
heightMode="auto" heightMode="auto"
defaultValue={defaultValue} defaultValue={defaultValue}
@@ -26,7 +26,7 @@ export function PairOrBulkEditor({ preferenceName, ...props }: Props) {
variant="border" variant="border"
title={useBulk ? "Enable form edit" : "Enable bulk edit"} title={useBulk ? "Enable form edit" : "Enable bulk edit"}
className={classNames( className={classNames(
"transition-opacity opacity-0 group-hover:opacity-80 hover:!opacity-100 shadow", "transition-opacity opacity-0 group-hover:opacity-80 hover:opacity-100! shadow",
"bg-surface hover:text group-hover/wrapper:opacity-100", "bg-surface hover:text group-hover/wrapper:opacity-100",
)} )}
onClick={() => setUseBulk((b) => !b)} onClick={() => setUseBulk((b) => !b)}
@@ -7,7 +7,7 @@ export function PillButton({ className, ...props }: ButtonProps) {
<Button <Button
size="2xs" size="2xs"
variant="border" variant="border"
className={classNames(className, "!rounded-full mx-1 !px-3")} className={classNames(className, "rounded-full! mx-1 px-3!")}
{...props} {...props}
/> />
); );
@@ -116,7 +116,7 @@ export const PlainInput = forwardRef<{ focus: () => void }, PlainInputProps>(fun
const id = useRef(`input-${generateId()}`); const id = useRef(`input-${generateId()}`);
const commonClassName = classNames( const commonClassName = classNames(
className, className,
"!bg-transparent min-w-0 w-full focus:outline-none placeholder:text-placeholder", "bg-transparent! min-w-0 w-full focus:outline-hidden placeholder:text-placeholder",
"px-2 text-xs font-mono cursor-text", "px-2 text-xs font-mono cursor-text",
); );
@@ -167,7 +167,7 @@ export const PlainInput = forwardRef<{ focus: () => void }, PlainInputProps>(fun
"overflow-hidden", "overflow-hidden",
focused && !disabled ? "border-border-focus" : "border-border-subtle", focused && !disabled ? "border-border-focus" : "border-border-subtle",
disabled && "border-dotted", disabled && "border-dotted",
hasChanged && "has-[:invalid]:border-danger", // For built-in HTML validation hasChanged && "has-invalid:border-danger", // For built-in HTML validation
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",
@@ -225,7 +225,7 @@ export const PlainInput = forwardRef<{ focus: () => void }, PlainInputProps>(fun
: `Obscure ${typeof label === "string" ? label : "field"}` : `Obscure ${typeof label === "string" ? label : "field"}`
} }
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="group-hover/obscure:text" iconClassName="group-hover/obscure:text"
iconSize="sm" iconSize="sm"
icon={obscured ? "eye" : "eye_closed"} icon={obscured ? "eye" : "eye_closed"}
@@ -43,7 +43,7 @@ export function RadioCards<T extends string>({
/> />
<div <div
className={classNames( className={classNames(
"mt-1 w-4 h-4 flex-shrink-0 rounded-full border", "mt-1 w-4 h-4 shrink-0 rounded-full border",
"flex items-center justify-center", "flex items-center justify-center",
selected ? "border-focus" : "border-border", selected ? "border-focus" : "border-border",
)} )}
@@ -92,7 +92,7 @@ export function SegmentedControl<T extends string>({
role="radio" role="radio"
tabIndex={isSelected ? 0 : -1} tabIndex={isSelected ? 0 : -1}
className={classNames( className={classNames(
isActive && "!text-text", isActive && "text-text!",
"focus:ring-1 focus:ring-border-focus", "focus:ring-1 focus:ring-border-focus",
)} )}
onClick={() => onChange(o.value)} onClick={() => onChange(o.value)}
@@ -111,8 +111,8 @@ export function SegmentedControl<T extends string>({
role="radio" role="radio"
tabIndex={isSelected ? 0 : -1} tabIndex={isSelected ? 0 : -1}
className={classNames( className={classNames(
isActive && "!text-text", isActive && "text-text!",
"!px-1.5 !w-auto", "px-1.5! w-auto!",
"focus:ring-border-focus", "focus:ring-border-focus",
)} )}
title={o.label} title={o.label}
+2 -2
View File
@@ -90,8 +90,8 @@ export function Select<T extends string>({
onBlur={() => setFocused(false)} onBlur={() => setFocused(false)}
disabled={disabled} disabled={disabled}
className={classNames( className={classNames(
"pr-7 w-full outline-none bg-transparent disabled:opacity-disabled", "pr-7 w-full outline-hidden bg-transparent disabled:opacity-disabled",
"leading-[1] rounded-none", // Center the text better vertically "leading-none rounded-none", // Center the text better vertically
)} )}
> >
{isInvalidSelection && <option value={"__NONE__"}>-- Select an Option --</option>} {isInvalidSelection && <option value={"__NONE__"}>-- Select an Option --</option>}
@@ -189,7 +189,7 @@ export function ModelSettingRowBoolean<M extends AnyModel, K extends ModelKeyOfV
export function SettingRowNumber({ export function SettingRowNumber({
inputClassName, inputClassName,
inputWidthClassName = "!w-48", inputWidthClassName = "w-48!",
name, name,
onChange, onChange,
placeholder, placeholder,
@@ -251,7 +251,7 @@ export function ModelSettingRowNumber<M extends AnyModel, K extends ModelKeyOfVa
export function SettingRowText({ export function SettingRowText({
inputClassName, inputClassName,
inputWidthClassName = "!w-80", inputWidthClassName = "w-80!",
name, name,
onChange, onChange,
placeholder, placeholder,
@@ -358,7 +358,7 @@ export function SettingRowSelect<T extends string>({
name, name,
onChange, onChange,
options, options,
selectClassName = "!w-48", selectClassName = "w-48!",
title, title,
value, value,
...props ...props
@@ -393,7 +393,7 @@ export function SettingSelectControl<T extends string>({
name, name,
onChange, onChange,
options, options,
selectClassName = "!w-48", selectClassName = "w-48!",
value, value,
}: { }: {
defaultValue?: T; defaultValue?: T;
+4 -2
View File
@@ -1,14 +1,16 @@
import { formatSize } from "@yaakapp-internal/lib/formatSize"; import { formatSize } from "@yaakapp-internal/lib/formatSize";
import classNames from "classnames";
interface Props { interface Props {
className?: string;
contentLength: number; contentLength: number;
contentLengthCompressed?: number | null; contentLengthCompressed?: number | null;
} }
export function SizeTag({ contentLength, contentLengthCompressed }: Props) { export function SizeTag({ className, contentLength, contentLengthCompressed }: Props) {
return ( return (
<span <span
className="font-mono" className={classNames("font-mono", className)}
title={ title={
`${contentLength} bytes` + `${contentLength} bytes` +
(contentLengthCompressed ? `\n${contentLengthCompressed} bytes compressed` : "") (contentLengthCompressed ? `\n${contentLengthCompressed} bytes compressed` : "")
@@ -342,7 +342,7 @@ export const Tabs = forwardRef<TabsRef, Props>(function Tabs(
<div <div
className={classNames( className={classNames(
layout === "horizontal" && "flex flex-col w-full pb-3 mb-auto", layout === "horizontal" && "flex flex-col w-full pb-3 mb-auto",
layout === "vertical" && "flex flex-row flex-shrink-0 w-full", layout === "vertical" && "flex flex-row shrink-0 w-full",
)} )}
> >
{tabButtons} {tabButtons}
@@ -456,9 +456,9 @@ function TabButton({
onChangeValue?.(tab.value); onChangeValue?.(tab.value);
}, },
className: classNames( className: classNames(
"flex items-center rounded whitespace-nowrap", "flex items-center rounded-sm whitespace-nowrap",
"!px-2 ml-[1px]", "px-2! ml-px",
"outline-none", "outline-hidden",
"ring-none", "ring-none",
"focus-visible-or-class:outline-2", "focus-visible-or-class:outline-2",
addBorders && "border focus-visible:bg-surface-highlight", addBorders && "border focus-visible:bg-surface-highlight",
@@ -468,7 +468,7 @@ function TabButton({
: layout === "vertical" : layout === "vertical"
? "border-border-subtle" ? "border-border-subtle"
: "border-transparent", : "border-transparent",
layout === "horizontal" && "min-w-[10rem]", layout === "horizontal" && "min-w-40",
isDragging && "opacity-50", isDragging && "opacity-50",
overlay && "opacity-80", overlay && "opacity-80",
), ),
+4 -4
View File
@@ -54,11 +54,11 @@ export function Toast({ children, open, onClose, timeout, action, icon, color }:
`x-theme-toast x-theme-toast--${color}`, `x-theme-toast x-theme-toast--${color}`,
"pointer-events-auto overflow-hidden", "pointer-events-auto overflow-hidden",
"relative pointer-events-auto bg-surface text-text rounded-lg", "relative pointer-events-auto bg-surface text-text rounded-lg",
"border border-border shadow-lg w-[25rem]", "border border-border shadow-lg w-100",
)} )}
> >
<div className="pl-3 py-3 pr-10 flex items-start gap-2 w-full max-h-[11rem] overflow-auto"> <div className="pl-3 py-3 pr-10 flex items-start gap-2 w-full max-h-44 overflow-auto">
{toastIcon && <Icon icon={toastIcon} color={color} className="mt-1 flex-shrink-0" />} {toastIcon && <Icon icon={toastIcon} color={color} className="mt-1 shrink-0" />}
<VStack space={2} className="w-full min-w-0"> <VStack space={2} className="w-full min-w-0">
<div className="select-auto">{children}</div> <div className="select-auto">{children}</div>
{action?.({ hide: onClose })} {action?.({ hide: onClose })}
@@ -68,7 +68,7 @@ export function Toast({ children, open, onClose, timeout, action, icon, color }:
<IconButton <IconButton
color={color} color={color}
variant="border" variant="border"
className="opacity-60 border-0 !absolute top-2 right-2" className="opacity-60 border-0 absolute! top-2 right-2"
title="Dismiss" title="Dismiss"
icon="x" icon="x"
onClick={onClose} onClick={onClose}
+4 -4
View File
@@ -116,7 +116,7 @@ export function Tooltip({ children, className, content, tabIndex, size = "md" }:
role="button" role="button"
aria-describedby={openState ? id.current : undefined} aria-describedby={openState ? id.current : undefined}
tabIndex={tabIndex ?? -1} tabIndex={tabIndex ?? -1}
className={classNames(className, "flex-grow-0 flex items-center")} className={classNames(className, "grow-0 flex items-center")}
onClick={handleToggleImmediate} onClick={handleToggleImmediate}
onMouseEnter={handleOpen} onMouseEnter={handleOpen}
onMouseLeave={handleClose} onMouseLeave={handleClose}
@@ -141,10 +141,10 @@ function Triangle({ className, position }: { className?: string; position: "top"
shapeRendering="crispEdges" shapeRendering="crispEdges"
className={classNames( className={classNames(
className, className,
"absolute z-50 left-[calc(50%-0.4rem)] h-[0.5rem] w-[0.8rem]", "absolute z-50 left-[calc(50%-0.4rem)] h-2 w-[0.8rem]",
isBottom isBottom
? "border-t-[2px] border-surface-highlight -bottom-[calc(0.5rem-3px)] mb-2" ? "border-t-2 border-surface-highlight -bottom-[calc(0.5rem-3px)] mb-2"
: "border-b-[2px] border-surface-highlight -top-[calc(0.5rem-3px)] mt-2", : "border-b-2 border-surface-highlight -top-[calc(0.5rem-3px)] mt-2",
)} )}
> >
<title>Triangle</title> <title>Triangle</title>
@@ -117,7 +117,7 @@ function CommitListItem({
<button <button
type="button" type="button"
className={classNames( className={classNames(
"w-full min-w-0 text-left rounded px-2 py-1.5", "w-full min-w-0 text-left rounded-sm px-2 py-1.5",
selected && "bg-surface-active", selected && "bg-surface-active",
)} )}
onClick={onSelect} onClick={onSelect}
@@ -241,7 +241,7 @@ export function GitCommitDialog({ syncDir, onDone, workspace }: Props) {
secondSlot={({ style: innerStyle }) => ( secondSlot={({ style: innerStyle }) => (
<div style={innerStyle} className="grid grid-rows-[minmax(0,1fr)_auto] gap-3 pb-2"> <div style={innerStyle} className="grid grid-rows-[minmax(0,1fr)_auto] gap-3 pb-2">
<Input <Input
className="!text-base font-sans rounded-md" className="text-base! font-sans rounded-md"
placeholder="Commit message..." placeholder="Commit message..."
onChange={setMessage} onChange={setMessage}
stateKey={null} stateKey={null}
@@ -325,7 +325,7 @@ function TreeNodeChildren({
)} )}
> >
{isSelected && ( {isSelected && (
<div className="absolute -left-[100vw] right-0 top-0 bottom-0 bg-surface-active opacity-30 -z-10" /> <div className="absolute left-[-100vw] right-0 top-0 bottom-0 bg-surface-active opacity-30 -z-10" />
)} )}
<Checkbox <Checkbox
checked={checked} checked={checked}
@@ -358,7 +358,7 @@ function TreeNodeChildren({
{node.status.status !== "current" && ( {node.status.status !== "current" && (
<InlineCode <InlineCode
className={classNames( className={classNames(
"py-0 bg-transparent w-[6rem] text-center shrink-0", "py-0 bg-transparent w-24 text-center shrink-0",
node.status.status === "modified" && "text-info", node.status.status === "modified" && "text-info",
node.status.status === "untracked" && "text-success", node.status.status === "untracked" && "text-success",
node.status.status === "removed" && "text-danger", node.status.status === "removed" && "text-danger",
@@ -400,7 +400,7 @@ function ExternalTreeNode({
return ( return (
<Checkbox <Checkbox
fullWidth fullWidth
className="h-xs w-full hover:bg-surface-highlight rounded px-1 group" className="h-xs w-full hover:bg-surface-highlight rounded-sm px-1 group"
checked={entry.staged} checked={entry.staged}
onChange={() => onCheck(entry)} onChange={() => onCheck(entry)}
title={ title={
@@ -409,7 +409,7 @@ function ExternalTreeNode({
<div className="truncate">{entry.relaPath}</div> <div className="truncate">{entry.relaPath}</div>
<InlineCode <InlineCode
className={classNames( className={classNames(
"py-0 ml-auto bg-transparent w-[6rem] text-center", "py-0 ml-auto bg-transparent w-24 text-center",
entry.status === "modified" && "text-info", entry.status === "modified" && "text-info",
entry.status === "untracked" && "text-success", entry.status === "untracked" && "text-success",
entry.status === "removed" && "text-danger", entry.status === "removed" && "text-danger",
@@ -559,7 +559,7 @@ const GitMenuButton = forwardRef<HTMLButtonElement, HTMLAttributes<HTMLButtonEle
ref={ref} ref={ref}
className={classNames( className={classNames(
className, className,
"px-3 h-md border-t border-border flex items-center justify-between text-text-subtle outline-none focus-visible:bg-surface-highlight", "px-3 h-md border-t border-border flex items-center justify-between text-text-subtle outline-hidden focus-visible:bg-surface-highlight",
)} )}
{...props} {...props}
/> />
@@ -113,19 +113,19 @@ export const GraphQLDocsExplorer = memo(function GraphQLDocsExplorer({
name={{ value: "query", color: "primary" }} name={{ value: "query", color: "primary" }}
item={qryItem} item={qryItem}
setItem={setActiveItem} setItem={setActiveItem}
className="!my-0" className="my-0!"
/> />
<GqlTypeRow <GqlTypeRow
name={{ value: "mutation", color: "primary" }} name={{ value: "mutation", color: "primary" }}
item={mutItem} item={mutItem}
setItem={setActiveItem} setItem={setActiveItem}
className="!my-0" className="my-0!"
/> />
<GqlTypeRow <GqlTypeRow
name={{ value: "subscription", color: "primary" }} name={{ value: "subscription", color: "primary" }}
item={subItem} item={subItem}
setItem={setActiveItem} setItem={setActiveItem}
className="!my-0" className="my-0!"
/> />
<Subheading count={Object.keys(allTypes).length}>All Schema Types</Subheading> <Subheading count={Object.keys(allTypes).length}>All Schema Types</Subheading>
<DocMarkdown>{schema.description ?? null}</DocMarkdown> <DocMarkdown>{schema.description ?? null}</DocMarkdown>
@@ -192,7 +192,7 @@ function GraphQLExplorerHeader({
noTruncate noTruncate
item={crumb} item={crumb}
setItem={setItem} setItem={setItem}
className="!font-sans !text-sm flex-shrink-0" className="font-sans! text-sm! shrink-0"
/> />
)} )}
</Fragment> </Fragment>
@@ -208,7 +208,7 @@ function GraphQLExplorerHeader({
className="hidden @[10rem]:block" className="hidden @[10rem]:block"
/> />
</div> </div>
<div className="ml-auto flex gap-1 [&>*]:text-text-subtle"> <div className="ml-auto flex gap-1 *:text-text-subtle">
<IconButton icon="x" size="sm" title="Close documentation explorer" onClick={onClose} /> <IconButton icon="x" size="sm" title="Close documentation explorer" onClick={onClose} />
</div> </div>
</nav> </nav>
@@ -528,7 +528,7 @@ function GqlTypeRow({
<span className="text-text-subtle">:</span>{" "} <span className="text-text-subtle">:</span>{" "}
<GqlTypeLink color="notice" item={returnItem} setItem={setItem} /> <GqlTypeLink color="notice" item={returnItem} setItem={setItem} />
</div> </div>
<DocMarkdown className="!text-text-subtle mt-0.5"> <DocMarkdown className="text-text-subtle! mt-0.5">
{item.type.description ?? null} {item.type.description ?? null}
</DocMarkdown> </DocMarkdown>
</div> </div>
@@ -786,8 +786,8 @@ function GqlSchemaSearch({
className={classNames( className={classNames(
className, className,
"relative flex items-center bg-surface z-20 min-w-0", "relative flex items-center bg-surface z-20 min-w-0",
!focused && "max-w-[6rem] ml-auto", !focused && "max-w-24 ml-auto",
focused && "!absolute top-0 left-1.5 right-1.5 bottom-0 pt-1.5", focused && "absolute! top-0 left-1.5 right-1.5 bottom-0 pt-1.5",
)} )}
> >
<PlainInput <PlainInput
@@ -879,7 +879,7 @@ function SearchResult({
ref={initRef} ref={initRef}
className={classNames( className={classNames(
className, className,
"px-3 truncate w-full text-left h-sm rounded text-editor font-mono", "px-3 truncate w-full text-left h-sm rounded-sm text-editor font-mono",
isActive && "bg-surface-highlight", isActive && "bg-surface-highlight",
)} )}
{...extraProps} {...extraProps}
@@ -893,7 +893,7 @@ function Heading({ children }: { children: ReactNode }) {
function DocMarkdown({ children, className }: { children: string | null; className?: string }) { function DocMarkdown({ children, className }: { children: string | null; className?: string }) {
return ( return (
<Markdown className={classNames(className, "!text-text-subtle italic")}>{children}</Markdown> <Markdown className={classNames(className, "text-text-subtle! italic")}>{children}</Markdown>
); );
} }
@@ -77,8 +77,8 @@ function GraphQLEditorInner({ request, onChange, baseRequest, ...extraEditorProp
const actions = useMemo<EditorProps["actions"]>( const actions = useMemo<EditorProps["actions"]>(
() => [ () => [
<div key="actions" className="flex flex-row !opacity-100 !shadow"> <div key="actions" className="flex flex-row opacity-100! shadow!">
<div key="introspection" className="!opacity-100"> <div key="introspection" className="opacity-100!">
{schema === undefined ? null /* Initializing */ : ( {schema === undefined ? null /* Initializing */ : (
<Dropdown <Dropdown
items={[ items={[
@@ -217,7 +217,7 @@ function GraphQLEditorInner({ request, onChange, baseRequest, ...extraEditorProp
stateKey={`graphql_body.${request.id}`} stateKey={`graphql_body.${request.id}`}
{...extraEditorProps} {...extraEditorProps}
/> />
<div className="grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1 min-h-[5rem]"> <div className="grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1 min-h-20">
<Separator dashed className="pb-1"> <Separator dashed className="pb-1">
Variables Variables
</Separator> </Separator>
@@ -298,7 +298,7 @@ function SseSummaryFooter({
<Markdown className="select-auto cursor-auto">{summary}</Markdown> <Markdown className="select-auto cursor-auto">{summary}</Markdown>
</div> </div>
) : ( ) : (
<pre className="font-mono whitespace-pre-wrap break-words select-auto cursor-auto"> <pre className="font-mono whitespace-pre-wrap wrap-break-word select-auto cursor-auto">
{summary} {summary}
</pre> </pre>
) )
@@ -1,9 +1,12 @@
import type { HttpResponse } from "@yaakapp-internal/models"; import type { HttpResponse } from "@yaakapp-internal/models";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { useCopyHttpResponse } from "../../hooks/useCopyHttpResponse";
import { useResponseBodyText } from "../../hooks/useResponseBodyText"; import { useResponseBodyText } from "../../hooks/useResponseBodyText";
import { useSaveResponse } from "../../hooks/useSaveResponse";
import { languageFromContentType } from "../../lib/contentType"; import { languageFromContentType } from "../../lib/contentType";
import { getContentTypeFromHeaders } from "../../lib/model_util"; import { getContentTypeFromHeaders } from "../../lib/model_util";
import type { EditorProps } from "../core/Editor/Editor"; import type { EditorProps } from "../core/Editor/Editor";
import { IconButton } from "../core/IconButton";
import { EmptyStateText } from "../EmptyStateText"; import { EmptyStateText } from "../EmptyStateText";
import { TextViewer } from "./TextViewer"; import { TextViewer } from "./TextViewer";
import { WebPageViewer } from "./WebPageViewer"; import { WebPageViewer } from "./WebPageViewer";
@@ -51,6 +54,9 @@ interface HttpTextViewerProps {
function HttpTextViewer({ response, text, language, pretty, className }: HttpTextViewerProps) { function HttpTextViewer({ response, text, language, pretty, className }: HttpTextViewerProps) {
const [currentFilter, setCurrentFilter] = useState<string | null>(null); const [currentFilter, setCurrentFilter] = useState<string | null>(null);
const filteredBody = useResponseBodyText({ response, filter: currentFilter }); const filteredBody = useResponseBodyText({ response, filter: currentFilter });
const saveResponse = useSaveResponse(response);
const copyResponse = useCopyHttpResponse(response);
const actionsDisabled = response.state !== "closed" && response.status >= 100;
const filterCallback = useMemo( const filterCallback = useMemo(
() => (filter: string) => { () => (filter: string) => {
@@ -72,6 +78,26 @@ function HttpTextViewer({ response, text, language, pretty, className }: HttpTex
filterStateKey={`response.body.${response.requestId}`} filterStateKey={`response.body.${response.requestId}`}
pretty={pretty} pretty={pretty}
className={className} className={className}
footerActions={[
<IconButton
key="save"
size="sm"
icon="save"
title="Save response to file"
disabled={actionsDisabled}
onClick={() => saveResponse.mutate()}
className="border !border-border-subtle"
/>,
<IconButton
key="copy"
size="sm"
icon="copy"
title="Copy response body"
disabled={actionsDisabled}
onClick={() => copyResponse.mutate()}
className="border !border-border-subtle"
/>,
]}
onFilter={filterCallback} onFilter={filterCallback}
/> />
); );
@@ -63,7 +63,7 @@ export function MultipartViewer({ data, boundary, idPrefix = "multipart" }: Prop
<div className="h-5 w-5 overflow-auto flex items-center justify-end"> <div className="h-5 w-5 overflow-auto flex items-center justify-end">
<ImageViewer <ImageViewer
data={part.arrayBuffer} data={part.arrayBuffer}
className="ml-auto w-auto rounded overflow-hidden" className="ml-auto w-auto rounded-sm overflow-hidden"
/> />
</div> </div>
) : part.filename ? ( ) : part.filename ? (
@@ -76,7 +76,7 @@ export function MultipartViewer({ data, boundary, idPrefix = "multipart" }: Prop
// oxlint-disable-next-line react/no-array-index-key -- Nothing else to key on // oxlint-disable-next-line react/no-array-index-key -- Nothing else to key on
key={idPrefix + part.name + i} key={idPrefix + part.name + i}
value={tabValue(part, i)} value={tabValue(part, i)}
className="pl-3 !pt-0" className="pl-3 pt-0!"
> >
<Part part={part} /> <Part part={part} />
</TabContent> </TabContent>
@@ -1,6 +1,6 @@
import classNames from "classnames"; import classNames from "classnames";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { useCallback, useMemo } from "react"; import { Children, useCallback, useMemo } from "react";
import { createGlobalState } from "react-use"; import { createGlobalState } from "react-use";
import { useDebouncedValue } from "@yaakapp-internal/ui"; import { useDebouncedValue } from "@yaakapp-internal/ui";
import { useFormatText } from "../../hooks/useFormatText"; import { useFormatText } from "../../hooks/useFormatText";
@@ -19,6 +19,7 @@ interface Props {
filterStateKey?: string | null; filterStateKey?: string | null;
pretty?: boolean; pretty?: boolean;
className?: string; className?: string;
footerActions?: ReactNode;
onFilter?: (filter: string) => { onFilter?: (filter: string) => {
data: string | null | undefined; data: string | null | undefined;
isPending: boolean; isPending: boolean;
@@ -35,6 +36,7 @@ export function TextViewer({
filterStateKey, filterStateKey,
pretty, pretty,
className, className,
footerActions,
onFilter, onFilter,
}: Props) { }: Props) {
const filterKey = filterStateKey ?? stateKey; const filterKey = filterStateKey ?? stateKey;
@@ -66,13 +68,13 @@ export function TextViewer({
const canFilter = onFilter && (language === "json" || language === "xml" || language === "html"); const canFilter = onFilter && (language === "json" || language === "xml" || language === "html");
const actions = useMemo<ReactNode[]>(() => { const actions = useMemo<ReactNode[]>(() => {
const nodes: ReactNode[] = []; const nodes: ReactNode[] = isSearching ? [] : Children.toArray(footerActions);
if (!canFilter) return nodes; if (!canFilter) return nodes;
if (isSearching) { if (isSearching) {
nodes.push( nodes.push(
<div key="input" className="w-full !opacity-100"> <div key="input" className="w-full opacity-100!">
<Input <Input
key={filterKey ?? "filter"} key={filterKey ?? "filter"}
validate={!filteredResponse.error} validate={!filteredResponse.error}
@@ -100,13 +102,14 @@ export function TextViewer({
icon={isSearching ? "x" : "filter"} icon={isSearching ? "x" : "filter"}
title={isSearching ? "Close filter" : "Filter response"} title={isSearching ? "Close filter" : "Filter response"}
onClick={toggleSearch} onClick={toggleSearch}
className={classNames("border !border-border-subtle", isSearching && "!opacity-100")} className={classNames("border border-border-subtle!", isSearching && "opacity-100!")}
/>, />,
); );
return nodes; return nodes;
}, [ }, [
canFilter, canFilter,
footerActions,
filterKey, filterKey,
filterText, filterText,
filteredResponse.error, filteredResponse.error,
@@ -3,10 +3,12 @@ import { copyToClipboard } from "../lib/copy";
import { getResponseBodyText } from "../lib/responseBody"; import { getResponseBodyText } from "../lib/responseBody";
import { useFastMutation } from "./useFastMutation"; import { useFastMutation } from "./useFastMutation";
export function useCopyHttpResponse(response: HttpResponse) { export function useCopyHttpResponse(response: HttpResponse | null) {
return useFastMutation({ return useFastMutation({
mutationKey: ["copy_http_response", response.id], mutationKey: ["copy_http_response", response?.id],
async mutationFn() { async mutationFn() {
if (response == null) return;
const body = await getResponseBodyText({ response, filter: null }); const body = await getResponseBodyText({ response, filter: null });
copyToClipboard(body); copyToClipboard(body);
}, },
+4 -2
View File
@@ -9,10 +9,12 @@ import { invokeCmd } from "../lib/tauri";
import { showToast } from "../lib/toast"; import { showToast } from "../lib/toast";
import { useFastMutation } from "./useFastMutation"; import { useFastMutation } from "./useFastMutation";
export function useSaveResponse(response: HttpResponse) { export function useSaveResponse(response: HttpResponse | null) {
return useFastMutation({ return useFastMutation({
mutationKey: ["save_response", response.id], mutationKey: ["save_response", response?.id],
mutationFn: async () => { mutationFn: async () => {
if (response == null) return null;
const request = getModel("http_request", response.requestId); const request = getModel("http_request", response.requestId);
if (request == null) return null; if (request == null) return null;
@@ -8,7 +8,7 @@ export function useToggleCommandPalette() {
id: "command_palette", id: "command_palette",
size: "dynamic", size: "dynamic",
hideX: true, hideX: true,
className: "mb-auto mt-[4rem] !max-h-[min(30rem,calc(100vh-4rem))]", className: "mb-auto mt-16 max-h-[min(30rem,calc(100vh-4rem))]!",
vAlign: "top", vAlign: "top",
noPadding: true, noPadding: true,
noScroll: true, noScroll: true,
+1 -1
View File
@@ -45,7 +45,7 @@ export async function editEnvironment(
id: "environment-editor", id: "environment-editor",
noPadding: true, noPadding: true,
size: "lg", size: "lg",
className: "h-[90vh] max-h-[60rem]", className: "h-[90vh] max-h-240",
render: () => ( render: () => (
<EnvironmentEditDialog <EnvironmentEditDialog
initialEnvironmentId={environment?.id ?? null} initialEnvironmentId={environment?.id ?? null}
+4 -4
View File
@@ -166,7 +166,7 @@ function showUpdateInstalledToast(version: string) {
action: ({ hide }) => ( action: ({ hide }) => (
<ButtonInfiniteLoading <ButtonInfiniteLoading
size="xs" size="xs"
className="mr-auto min-w-[5rem]" className="mr-auto min-w-20"
color="primary" color="primary"
loadingChildren="Restarting..." loadingChildren="Restarting..."
onClick={() => { onClick={() => {
@@ -206,7 +206,7 @@ async function showUpdateAvailableToast(updateInfo: UpdateInfo) {
<ButtonInfiniteLoading <ButtonInfiniteLoading
size="xs" size="xs"
color="info" color="info"
className="min-w-[10rem]" className="min-w-40"
loadingChildren={downloaded ? "Installing..." : "Downloading..."} loadingChildren={downloaded ? "Installing..." : "Downloading..."}
onClick={async () => { onClick={async () => {
await emit<UpdateResponse>(replyEventId, { await emit<UpdateResponse>(replyEventId, {
@@ -259,7 +259,7 @@ function showPluginUpdatesToast(updateInfo: PluginUpdateNotification) {
<ButtonInfiniteLoading <ButtonInfiniteLoading
size="xs" size="xs"
color="info" color="info"
className="min-w-[5rem]" className="min-w-20"
loadingChildren="Updating..." loadingChildren="Updating..."
onClick={async () => { onClick={async () => {
const updated = await updateAllPlugins(); const updated = await updateAllPlugins();
@@ -311,7 +311,7 @@ function showNotificationToast(n: YaakNotification) {
<Button <Button
size="xs" size="xs"
color={stringToColor(n.color) ?? undefined} color={stringToColor(n.color) ?? undefined}
className="mr-auto min-w-[5rem]" className="mr-auto min-w-20"
rightSlot={<Icon icon="external_link" />} rightSlot={<Icon icon="external_link" />}
onClick={() => { onClick={() => {
hide(); hide();
+1 -1
View File
@@ -61,7 +61,7 @@ export function showErrorToast<T>({
message: ( message: (
<div className="w-full"> <div className="w-full">
<h2 className="text-lg font-bold mb-2">{title}</h2> <h2 className="text-lg font-bold mb-2">{title}</h2>
<div className="whitespace-pre-wrap break-words">{String(message)}</div> <div className="whitespace-pre-wrap wrap-break-word">{String(message)}</div>
</div> </div>
), ),
}); });
+6 -6
View File
@@ -1,6 +1,6 @@
@tailwind base; @import "tailwindcss";
@tailwind components; @import "../../packages/tailwind-config/index.css";
@tailwind utilities; @source "../../packages/ui/src";
@layer base { @layer base {
html, html,
@@ -50,7 +50,7 @@
a, a,
a[href] * { a[href] * {
@apply cursor-pointer !important; @apply cursor-pointer!;
} }
table th { table th {
@@ -72,14 +72,14 @@
} }
&::-webkit-scrollbar-thumb:hover { &::-webkit-scrollbar-thumb:hover {
@apply opacity-40 !important; @apply opacity-40!;
} }
} }
.hide-scrollbars { .hide-scrollbars {
&::-webkit-scrollbar-corner, &::-webkit-scrollbar-corner,
&::-webkit-scrollbar { &::-webkit-scrollbar {
@apply hidden !important; @apply hidden!;
} }
} }
-6
View File
@@ -79,8 +79,6 @@
"devDependencies": { "devDependencies": {
"@lezer/generator": "^1.8.0", "@lezer/generator": "^1.8.0",
"@rolldown/plugin-babel": "^0.2.3", "@rolldown/plugin-babel": "^0.2.3",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/nesting": "^0.0.0-insiders.565cd3e",
"@tanstack/router-plugin": "^1.127.5", "@tanstack/router-plugin": "^1.127.5",
"@types/babel__core": "^7.20.5", "@types/babel__core": "^7.20.5",
"@types/node": "^24.0.13", "@types/node": "^24.0.13",
@@ -94,14 +92,10 @@
"@vitejs/plugin-react": "^6.0.1", "@vitejs/plugin-react": "^6.0.1",
"@yaakapp-internal/theme": "^1.0.0", "@yaakapp-internal/theme": "^1.0.0",
"@yaakapp-internal/ui": "^1.0.0", "@yaakapp-internal/ui": "^1.0.0",
"autoprefixer": "^10.4.21",
"babel-plugin-react-compiler": "^1.0.0", "babel-plugin-react-compiler": "^1.0.0",
"decompress": "^4.2.1", "decompress": "^4.2.1",
"internal-ip": "^8.0.0", "internal-ip": "^8.0.0",
"postcss": "^8.5.14",
"postcss-nesting": "^13.0.2",
"rollup": "^4.60.3", "rollup": "^4.60.3",
"tailwindcss": "^3.4.17",
"vite": "npm:@voidzero-dev/vite-plus-core@^0.2.1", "vite": "npm:@voidzero-dev/vite-plus-core@^0.2.1",
"vite-plugin-static-copy": "^3.3.0", "vite-plugin-static-copy": "^3.3.0",
"vite-plugin-svgr": "^4.5.0", "vite-plugin-svgr": "^4.5.0",
+1 -5
View File
@@ -1,7 +1,3 @@
module.exports = { module.exports = {
plugins: [ plugins: [require("@tailwindcss/postcss")],
require("@tailwindcss/nesting")(require("postcss-nesting")),
require("tailwindcss"),
require("autoprefixer"),
],
}; };
-16
View File
@@ -1,16 +0,0 @@
const sharedConfig = require("@yaakapp-internal/tailwind-config");
/** @type {import('tailwindcss').Config} */
module.exports = {
...sharedConfig,
content: [
"./*.{html,ts,tsx}",
"./commands/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}",
"./hooks/**/*.{ts,tsx}",
"./init/**/*.{ts,tsx}",
"./lib/**/*.{ts,tsx}",
"./routes/**/*.{ts,tsx}",
"../../packages/ui/src/**/*.{ts,tsx}",
],
};
+6 -6
View File
@@ -1,6 +1,6 @@
@tailwind base; @import "tailwindcss";
@tailwind components; @import "../../packages/tailwind-config/index.css";
@tailwind utilities; @source "../../packages/ui/src";
@layer base { @layer base {
html, html,
@@ -46,7 +46,7 @@
a, a,
a[href] * { a[href] * {
@apply cursor-pointer !important; @apply cursor-pointer!;
} }
table th { table th {
@@ -68,14 +68,14 @@
} }
&::-webkit-scrollbar-thumb:hover { &::-webkit-scrollbar-thumb:hover {
@apply opacity-40 !important; @apply opacity-40!;
} }
} }
.hide-scrollbars { .hide-scrollbars {
&::-webkit-scrollbar-corner, &::-webkit-scrollbar-corner,
&::-webkit-scrollbar { &::-webkit-scrollbar {
@apply hidden !important; @apply hidden!;
} }
} }

Some files were not shown because too many files have changed in this diff Show More