Compare commits

..

3 Commits

Author SHA1 Message Date
Gregory Schier 580302cbd2 Use baseUrl variable for OpenAPI imports 2026-06-29 14:42:29 -07:00
Gregory Schier 3b9c311dc5 Avoid regex trimming in OpenAPI importer 2026-06-29 14:32:21 -07:00
Gregory Schier 016fcba1c6 Add native OpenAPI importer 2026-06-29 14:23:36 -07:00
202 changed files with 10148 additions and 5520 deletions
@@ -19,12 +19,10 @@ 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
@@ -33,7 +31,6 @@ 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.
+3 -4
View File
@@ -4,14 +4,13 @@
## Submission ## Submission
- [ ] This PR is a bug fix. - [ ] This PR is a bug fix or small-scope improvement.
- [ ] If this PR is not a bug fix, I linked the feedback item where @gschier explicitly gave me permission to work on it. - [ ] If this PR is not a bug fix or small-scope improvement, I linked an approved feedback item below.
- [ ] I have read and followed [`CONTRIBUTING.md`](CONTRIBUTING.md). - [ ] I have read and followed [`CONTRIBUTING.md`](CONTRIBUTING.md).
- [ ] I tested this change locally. - [ ] I tested this change locally.
- [ ] I added or updated tests when reasonable. - [ ] I added or updated tests when reasonable.
- [ ] I added screenshots or recordings for UI changes when reasonable.
Explicit permission feedback item (required if not a bug fix): Approved feedback item (required if not a bug fix or small-scope improvement):
<!-- https://yaak.app/feedback/... --> <!-- https://yaak.app/feedback/... -->
@@ -1,848 +0,0 @@
const fs = require("node:fs");
const COMMENT_MARKER = "<!-- yaak-contribution-policy -->";
const MAINTAINER_LOGINS = new Set(["gschier"]);
const MAINTAINER_ASSOCIATIONS = new Set(["OWNER", "MEMBER", "COLLABORATOR"]);
const MAINTAINER_PERMISSIONS = new Set(["admin", "maintain", "write"]);
const REVIEWER_LOGIN = "gschier";
const LARGE_DIFF_CHANGED_FILES = 20;
const LARGE_DIFF_CHANGED_LINES = 800;
const SUMMARY_TITLE_MAX_LENGTH = 80;
const AUTOMATIC_PR_CREATED_AFTER = "2026-06-30T07:00:00.000Z";
const AUTOMATIC_PR_CREATED_AFTER_LABEL = "June 30, 2026";
const LABELS = {
inScope: {
name: "contribution: in scope",
color: "0E8A16",
description: "Community PR appears to be in scope for maintainer review.",
},
outOfScope: {
name: "contribution: out of scope",
color: "B60205",
description: "Community PR does not match Yaak's contribution policy.",
},
explicitPermission: {
name: "contribution: explicit permission",
color: "5319E7",
description:
"Community PR links feedback where @gschier explicitly allowed the work.",
},
missingTemplate: {
name: "contribution: missing template",
color: "D93F0B",
description:
"Community PR is missing enough of the pull request template to review.",
},
policyUnmet: {
name: "contribution: policy unmet",
color: "B60205",
description:
"Community PR does not currently satisfy the contribution policy.",
},
needsScopeReview: {
name: "contribution: needs scope review",
color: "FBCA04",
description:
"Community PR may be broader than Yaak's bug-fix contribution policy.",
},
};
const MANAGED_LABEL_NAMES = [
...new Set(Object.values(LABELS).map((label) => label.name)),
];
const CHECKBOXES = {
bugFix: "This PR is a bug fix.",
explicitPermission:
"If this PR is not a bug fix, I linked the feedback item where @gschier explicitly gave me permission to work on it.",
readContributing:
"I have read and followed [`CONTRIBUTING.md`](CONTRIBUTING.md).",
testedLocally: "I tested this change locally.",
testsUpdated: "I added or updated tests when reasonable.",
screenshotsAdded:
"I added screenshots or recordings for UI changes when reasonable.",
};
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function normalizeBody(body) {
return (body || "").replace(/\r\n/g, "\n");
}
function stripComments(value) {
return value.replace(/<!--[\s\S]*?-->/g, "").trim();
}
function getSection(body, heading) {
const pattern = new RegExp(`^##\\s+${escapeRegExp(heading)}\\s*$`, "gim");
const match = pattern.exec(body);
if (match == null) {
return null;
}
const rest = body.slice(match.index + match[0].length);
const nextHeadingIndex = rest.search(/^##\s+/m);
return nextHeadingIndex === -1 ? rest : rest.slice(0, nextHeadingIndex);
}
function hasMeaningfulText(value) {
return stripComments(value || "").length > 0;
}
function normalizeCheckboxLabel(label) {
return label
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
.replace(/`/g, "")
.replace(/\s+/g, " ")
.trim();
}
function checkboxState(body, label) {
const expectedLabel = normalizeCheckboxLabel(label);
for (const line of body.split("\n")) {
const match = line.match(/^\s*[-*]\s*\[([ xX])\]\s*(.*?)\s*$/i);
if (match == null) {
continue;
}
if (normalizeCheckboxLabel(match[2]) === expectedLabel) {
return match[1].toLowerCase() === "x";
}
}
return null;
}
function findFeedbackUrl(body) {
return (
body.match(
/https?:\/\/(?:www\.)?(?:yaak\.app\/feedback|feedback\.yaak\.app)\/[^\s)>\]]+/i,
)?.[0] ?? null
);
}
function getLabelNames(pr) {
return new Set((pr.labels || []).map((label) => label.name));
}
function analyzePullRequest(pr) {
const body = normalizeBody(pr.body);
const labelNames = getLabelNames(pr);
const states = Object.fromEntries(
Object.entries(CHECKBOXES).map(([key, label]) => [
key,
checkboxState(body, label),
]),
);
const sectionCount = ["Summary", "Submission", "Related"].filter(
(heading) => getSection(body, heading) != null,
).length;
const checkboxCount = Object.values(states).filter(
(state) => state != null,
).length;
const templateUsed = sectionCount >= 2 && checkboxCount >= 3;
const blockers = [];
const totalChangedLines =
Number(pr.additions || 0) + Number(pr.deletions || 0);
const changedFiles = Number(pr.changed_files || 0);
const largeDiff =
changedFiles > LARGE_DIFF_CHANGED_FILES ||
totalChangedLines > LARGE_DIFF_CHANGED_LINES;
if (labelNames.has(LABELS.outOfScope.name)) {
return {
blockers: [
{
label: LABELS.outOfScope.name,
message: "Marked out of scope by maintainer label.",
},
],
changedFiles,
desiredLabels: [LABELS.outOfScope.name],
largeDiff,
status: "out_of_scope",
templateUsed,
totalChangedLines,
};
}
if (labelNames.has(LABELS.inScope.name)) {
return {
blockers: [],
changedFiles,
desiredLabels: [LABELS.inScope.name],
largeDiff,
status: "in_scope",
templateUsed,
totalChangedLines,
};
}
if (!templateUsed) {
blockers.push({
label: LABELS.missingTemplate.name,
message:
"Update the PR description with the repository pull request template.",
});
} else {
const summary = getSection(body, "Summary");
const hasSummary = hasMeaningfulText(summary);
const feedbackUrl = findFeedbackUrl(body);
const bugFix = states.bugFix === true;
const explicitPermission = states.explicitPermission === true;
if (!hasSummary) {
blockers.push({
label: LABELS.policyUnmet.name,
message:
"Add a short summary describing the bug fix or permitted change.",
});
}
if (bugFix && explicitPermission) {
blockers.push({
label: LABELS.policyUnmet.name,
message:
"Choose either the bug-fix checkbox or the explicit-permission checkbox, not both.",
});
} else if (!bugFix && !explicitPermission) {
blockers.push({
label: LABELS.policyUnmet.name,
message:
"Check whether this is a bug fix, or confirm that explicit permission from @gschier is linked.",
});
} else if (explicitPermission && feedbackUrl == null) {
blockers.push({
label: LABELS.policyUnmet.name,
message:
"Link the feedback item where @gschier explicitly gave you permission to work on this.",
});
}
if (states.readContributing !== true) {
blockers.push({
label: LABELS.policyUnmet.name,
message: "Confirm that `CONTRIBUTING.md` was read and followed.",
});
}
if (states.testedLocally !== true) {
blockers.push({
label: LABELS.policyUnmet.name,
message: "Confirm that the change was tested locally.",
});
}
if (states.testsUpdated !== true) {
blockers.push({
label: LABELS.policyUnmet.name,
message: "Confirm that tests were added or updated when reasonable.",
});
}
if (states.screenshotsAdded !== true) {
blockers.push({
label: LABELS.policyUnmet.name,
message:
"Confirm that screenshots or recordings were added for UI changes when reasonable.",
});
}
}
const desiredLabels = new Set();
if (blockers.length === 0) {
desiredLabels.add(
largeDiff
? LABELS.needsScopeReview.name
: states.explicitPermission
? LABELS.explicitPermission.name
: LABELS.inScope.name,
);
} else if (
blockers.some((blocker) => blocker.label === LABELS.missingTemplate.name)
) {
desiredLabels.add(LABELS.missingTemplate.name);
} else {
desiredLabels.add(LABELS.policyUnmet.name);
}
return {
blockers,
changedFiles,
desiredLabels: [...desiredLabels],
largeDiff,
status: blockers.length === 0 ? "in_scope" : "blocked",
templateUsed,
totalChangedLines,
};
}
function buildBlockingComment(analysis) {
const lines = [
COMMENT_MARKER,
"Thanks for the PR. Yaak currently accepts community PRs for bug fixes, plus larger changes that link a feedback item where @gschier explicitly gave permission to work on it.",
"",
"This PR cannot be accepted yet because the following contribution policy requirements were unmet:",
"",
...analysis.blockers.map((blocker) => `- ${blocker.message}`),
];
if (!analysis.templateUsed) {
lines.push(
"",
"You can copy this template into the PR description and keep any existing context that is still useful.",
"",
"<details>",
"<summary>PR description template</summary>",
"",
"```md",
getPullRequestTemplate(),
"```",
"",
"</details>",
);
}
if (analysis.largeDiff) {
lines.push(
"",
`This PR also changes ${analysis.changedFiles} files and ${analysis.totalChangedLines} lines, so it has been labeled as needing scope review. That label is advisory, but maintainers may ask for the scope to be reduced.`,
);
}
return lines.join("\n");
}
function getPullRequestTemplate() {
return fs.readFileSync(".github/pull_request_template.md", "utf8").trim();
}
function buildInScopeComment() {
return [
COMMENT_MARKER,
"Thanks for the PR. This appears to match Yaak's contribution policy and is awaiting review by @gschier.",
"",
"This only means the PR is in scope for review. It does not mean the change has been reviewed or accepted for merge.",
].join("\n");
}
function buildOutOfScopeComment() {
return [
COMMENT_MARKER,
"Thanks for the PR. This does not appear to match Yaak's current contribution policy.",
"",
"Yaak currently accepts community PRs for bug fixes, or changes tied to a feedback item where @gschier explicitly gave permission to work on it.",
"",
"If this PR is tied to a feedback item where @gschier explicitly gave permission, please link it in the PR description.",
].join("\n");
}
function buildPolicyComment(analysis) {
if (analysis.status === "out_of_scope") {
return buildOutOfScopeComment();
}
if (analysis.blockers.length > 0) {
return buildBlockingComment(analysis);
}
return buildInScopeComment();
}
function escapeHtml(value) {
return String(value)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function truncateTitle(title) {
if (title.length <= SUMMARY_TITLE_MAX_LENGTH) {
return title;
}
return `${title.slice(0, SUMMARY_TITLE_MAX_LENGTH - 3).trimEnd()}...`;
}
function escapeTableText(value) {
return escapeHtml(value).replace(/\n/g, "<br>");
}
function summarizeResult({ pr, analysis, skipped, skipReason }) {
const comment =
analysis == null
? "None"
: buildPolicyComment(analysis).replace(COMMENT_MARKER, "").trim();
const summary = {
blocked: analysis?.blockers.length > 0,
comment,
details: "None",
labels:
analysis?.desiredLabels.length > 0
? analysis.desiredLabels.join(", ")
: "None",
number: pr.number,
prLink: `<a href="${escapeHtml(pr.html_url)}">#${pr.number}</a>`,
status: "In scope",
title: escapeHtml(truncateTitle(pr.title)),
};
if (skipped) {
return {
...summary,
blocked: false,
comment: "None",
details: escapeHtml(skipReason),
labels: "None",
status: "Skipped",
};
}
if (summary.blocked) {
return {
...summary,
comment: escapeTableText(summary.comment),
details: escapeHtml(
analysis.blockers.map((blocker) => blocker.message).join("; "),
),
labels: escapeHtml(summary.labels),
status: analysis.status === "out_of_scope" ? "Out of scope" : "Blocked",
};
}
return {
...summary,
comment: escapeTableText(summary.comment),
labels: escapeHtml(summary.labels),
};
}
function wasCreatedBefore(value, cutoff) {
return Date.parse(value) < Date.parse(cutoff);
}
async function isOfficialMaintainer({ github, owner, repo, pr }) {
if (MAINTAINER_LOGINS.has(pr.user.login)) {
return true;
}
if (MAINTAINER_ASSOCIATIONS.has(pr.author_association)) {
return true;
}
try {
const response = await github.rest.repos.getCollaboratorPermissionLevel({
owner,
repo,
username: pr.user.login,
});
return MAINTAINER_PERMISSIONS.has(response.data.permission);
} catch (error) {
if (error.status === 404) {
return false;
}
throw error;
}
}
async function ensureManagedLabels({ github, owner, repo }) {
for (const label of Object.values(LABELS)) {
try {
await github.rest.issues.getLabel({
owner,
repo,
name: label.name,
});
} catch (error) {
if (error.status !== 404) {
throw error;
}
await github.rest.issues.createLabel({
owner,
repo,
name: label.name,
color: label.color,
description: label.description,
});
}
}
}
async function syncLabels({ github, owner, repo, issueNumber, desiredLabels }) {
const desired = new Set(desiredLabels);
await ensureManagedLabels({ github, owner, repo });
for (const labelName of MANAGED_LABEL_NAMES) {
if (desired.has(labelName)) {
continue;
}
try {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: issueNumber,
name: labelName,
});
} catch (error) {
if (error.status !== 404) {
throw error;
}
}
}
if (desired.size > 0) {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: issueNumber,
labels: [...desired],
});
}
}
async function findPolicyComment({ github, owner, repo, issueNumber }) {
const comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number: issueNumber,
per_page: 100,
});
return comments.find(
(comment) =>
comment.user.type === "Bot" && comment.body?.includes(COMMENT_MARKER),
);
}
async function upsertPolicyComment({ github, owner, repo, issueNumber, body }) {
const existingComment = await findPolicyComment({
github,
owner,
repo,
issueNumber,
});
if (existingComment == null) {
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body,
});
return;
}
await github.rest.issues.updateComment({
owner,
repo,
comment_id: existingComment.id,
body,
});
}
async function deletePolicyComment({ github, owner, repo, issueNumber }) {
const existingComment = await findPolicyComment({
github,
owner,
repo,
issueNumber,
});
if (existingComment == null) {
return;
}
await github.rest.issues.deleteComment({
owner,
repo,
comment_id: existingComment.id,
});
}
async function requestMaintainerReview({ github, owner, repo, pr }) {
if (pr.user.login === REVIEWER_LOGIN) {
return;
}
try {
await github.rest.pulls.requestReviewers({
owner,
repo,
pull_number: pr.number,
reviewers: [REVIEWER_LOGIN],
});
} catch (error) {
if (error.status === 422) {
return;
}
throw error;
}
}
async function checkPullRequest({
github,
core,
owner,
repo,
pullNumber,
dryRun,
skipCreatedBefore,
}) {
const response = await github.rest.pulls.get({
owner,
repo,
pull_number: pullNumber,
});
const pr = response.data;
const issueNumber = pr.number;
if (
skipCreatedBefore != null &&
wasCreatedBefore(pr.created_at, skipCreatedBefore)
) {
core.notice(
`Skipping contribution policy for PR #${pr.number} because it was created before ${AUTOMATIC_PR_CREATED_AFTER_LABEL}.`,
);
return {
blocked: false,
number: pr.number,
summary: summarizeResult({
pr,
skipped: true,
skipReason: `created before ${AUTOMATIC_PR_CREATED_AFTER_LABEL}`,
}),
skipped: true,
};
}
if (pr.draft) {
core.notice(`Skipping contribution policy for draft PR #${pr.number}.`);
return {
blocked: false,
number: pr.number,
summary: summarizeResult({
pr,
skipped: true,
skipReason: "draft",
}),
skipped: true,
};
}
if (await isOfficialMaintainer({ github, owner, repo, pr })) {
core.notice(
`Skipping contribution policy for maintainer PR #${pr.number} from @${pr.user.login}.`,
);
if (!dryRun) {
await syncLabels({ github, owner, repo, issueNumber, desiredLabels: [] });
await deletePolicyComment({ github, owner, repo, issueNumber });
}
return {
blocked: false,
number: pr.number,
summary: summarizeResult({
pr,
skipped: true,
skipReason: `maintainer @${pr.user.login}`,
}),
skipped: true,
};
}
const analysis = analyzePullRequest(pr);
if (dryRun) {
const summary = summarizeResult({ pr, analysis });
core.notice(
`[dry-run] PR #${summary.number}: ${summary.status}; labels: ${summary.labels}; details: ${summary.details}`,
);
return {
blocked: analysis.blockers.length > 0,
number: pr.number,
summary,
skipped: false,
};
}
await syncLabels({
github,
owner,
repo,
issueNumber,
desiredLabels: analysis.desiredLabels,
});
if (analysis.blockers.length > 0) {
await upsertPolicyComment({
github,
owner,
repo,
issueNumber,
body: buildPolicyComment(analysis),
});
return {
blocked: true,
number: pr.number,
summary: summarizeResult({ pr, analysis }),
skipped: false,
};
}
await upsertPolicyComment({
github,
owner,
repo,
issueNumber,
body: buildPolicyComment(analysis),
});
await requestMaintainerReview({ github, owner, repo, pr });
core.notice(`Contribution policy check passed for PR #${pr.number}.`);
return {
blocked: false,
number: pr.number,
summary: summarizeResult({ pr, analysis }),
skipped: false,
};
}
async function listOpenPullRequests({ github, owner, repo }) {
return github.paginate(github.rest.pulls.list, {
owner,
repo,
state: "open",
per_page: 100,
});
}
function getManualPullRequestNumbers({ context, core }) {
const value = String(context.payload.inputs?.pr || "all").trim();
if (value.toLowerCase() === "all") {
return null;
}
const pullNumber = Number(value);
if (!Number.isInteger(pullNumber) || pullNumber <= 0) {
core.setFailed('The "pr" input must be "all" or a positive PR number.');
return [];
}
return [pullNumber];
}
async function run({ github, context, core }) {
const { owner, repo } = context.repo;
const payloadPr = context.payload.pull_request;
const dryRunInput = context.payload.inputs?.dry_run;
const dryRun =
context.eventName === "workflow_dispatch" &&
dryRunInput !== false &&
dryRunInput !== "false";
const skipCreatedBefore =
payloadPr == null ? null : AUTOMATIC_PR_CREATED_AFTER;
let pullNumbers;
if (payloadPr != null) {
pullNumbers = [payloadPr.number];
} else {
pullNumbers = getManualPullRequestNumbers({ context, core });
}
if (pullNumbers?.length === 0) {
return;
}
const pullRequests =
pullNumbers == null
? await listOpenPullRequests({ github, owner, repo })
: pullNumbers.map((number) => ({ number }));
const results = [];
if (dryRun) {
core.notice(
`Running contribution policy in dry-run mode for ${
pullNumbers == null
? "all open PRs"
: pullNumbers.map((number) => `#${number}`).join(", ")
}.`,
);
}
for (const pr of pullRequests) {
results.push(
await checkPullRequest({
github,
core,
owner,
repo,
pullNumber: pr.number,
dryRun,
skipCreatedBefore,
}),
);
}
await core.summary
.addHeading(`Contribution Policy ${dryRun ? "Dry Run" : "Results"}`)
.addTable([
[
{ data: "PR", header: true },
{ data: "Title", header: true },
{ data: "Status", header: true },
{ data: "Labels", header: true },
{ data: "Details", header: true },
{ data: "Comment", header: true },
],
...results.map((result) => [
result.summary.prLink,
result.summary.title,
result.summary.status,
result.summary.labels,
result.summary.details,
result.summary.comment,
]),
])
.write();
const blockedPullRequests = results.filter((result) => result.blocked);
if (blockedPullRequests.length > 0) {
if (dryRun) {
core.warning(
`Dry run found contribution policy failures for PR(s): ${blockedPullRequests
.map((result) => `#${result.number}`)
.join(", ")}`,
);
return;
}
core.setFailed(
`Contribution policy failed for PR(s): ${blockedPullRequests
.map((result) => `#${result.number}`)
.join(", ")}`,
);
}
}
module.exports = {
analyzePullRequest,
run,
};
-47
View File
@@ -1,47 +0,0 @@
name: Contribution Policy
on:
workflow_dispatch:
inputs:
pr:
description: PR number or all
required: true
default: all
type: string
dry_run:
description: Dry run
required: true
default: true
type: boolean
pull_request_target:
types:
- opened
- edited
- reopened
- synchronize
- ready_for_review
- labeled
- unlabeled
permissions:
contents: read
issues: write
pull-requests: write
jobs:
check:
name: Check contribution policy
runs-on: ubuntu-latest
steps:
- name: Checkout policy script
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.base.sha || github.ref }}
fetch-depth: 1
- name: Check contribution policy
uses: actions/github-script@v7
with:
script: |
const { run } = require("./.github/scripts/check-contribution-policy.js");
await run({ github, context, core });
+6 -9
View File
@@ -1,12 +1,7 @@
name: Update Flathub name: Update Flathub
on: on:
workflow_dispatch: release:
inputs: types: [published]
tag:
description: Release tag to publish to Flathub
required: true
type: string
permissions: permissions:
contents: read contents: read
@@ -15,6 +10,8 @@ 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
@@ -42,7 +39,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 "${{ inputs.tag }}" flathub-repo run: bash flatpak/update-manifest.sh "${{ github.event.release.tag_name }}" flathub-repo
- name: Commit and push to Flathub - name: Commit and push to Flathub
working-directory: flathub-repo working-directory: flathub-repo
@@ -51,5 +48,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 ${{ inputs.tag }}" git commit -m "Update to ${{ github.event.release.tag_name }}"
git push git push
+2 -1
View File
@@ -3,12 +3,13 @@
Yaak accepts community pull requests for: Yaak accepts community pull requests for:
- Bug fixes - Bug fixes
- Small-scope improvements directly tied to existing behavior
Pull requests that introduce broad new features, major redesigns, or large refactors are out of scope unless explicitly approved first. Pull requests that introduce broad new features, major redesigns, or large refactors are out of scope unless explicitly approved first.
## Approval for Non-Bugfix Changes ## Approval for Non-Bugfix Changes
If your PR is not a bug fix, include a link to the [feedback item](https://yaak.app/feedback) where @gschier explicitly gave you permission to work on it. If your PR is not a bug fix or small-scope improvement, include a link to the approved [feedback item](https://yaak.app/feedback) where contribution approval was explicitly stated.
## Development Setup ## Development Setup
Generated
+9 -9
View File
@@ -215,7 +215,7 @@ dependencies = [
"objc2-foundation 0.3.1", "objc2-foundation 0.3.1",
"parking_lot", "parking_lot",
"percent-encoding", "percent-encoding",
"windows-sys 0.52.0", "windows-sys 0.59.0",
"wl-clipboard-rs", "wl-clipboard-rs",
"x11rb", "x11rb",
] ]
@@ -1151,7 +1151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [ dependencies = [
"lazy_static 1.5.0", "lazy_static 1.5.0",
"windows-sys 0.48.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@@ -1970,7 +1970,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.52.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@@ -6534,7 +6534,7 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys 0.4.15", "linux-raw-sys 0.4.15",
"windows-sys 0.52.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@@ -6547,7 +6547,7 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys 0.9.4", "linux-raw-sys 0.9.4",
"windows-sys 0.52.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@@ -7508,9 +7508,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]] [[package]]
name = "tar" name = "tar"
version = "0.4.46" version = "0.4.45"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973"
dependencies = [ dependencies = [
"filetime", "filetime",
"libc", "libc",
@@ -7988,7 +7988,7 @@ dependencies = [
"getrandom 0.3.3", "getrandom 0.3.3",
"once_cell", "once_cell",
"rustix 1.0.7", "rustix 1.0.7",
"windows-sys 0.52.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@@ -9317,7 +9317,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
+2 -2
View File
@@ -57,8 +57,8 @@ Built with [Tauri](https://tauri.app), Rust, and React, its fast, lightweight
## Contribution Policy ## Contribution Policy
> [!IMPORTANT] > [!IMPORTANT]
> Community PRs are currently limited to bug fixes. > Community PRs are currently limited to bug fixes and small-scope improvements.
> If your PR is not a bug fix, link the [feedback item](https://yaak.app/feedback) where @gschier explicitly gave you permission to work on it. > If your PR is out of scope, link an approved feedback item from [yaak.app/feedback](https://yaak.app/feedback).
> See [`CONTRIBUTING.md`](CONTRIBUTING.md) for policy details and [`DEVELOPMENT.md`](DEVELOPMENT.md) for local setup. > See [`CONTRIBUTING.md`](CONTRIBUTING.md) for policy details and [`DEVELOPMENT.md`](DEVELOPMENT.md) for local setup.
## Useful Resources ## Useful Resources
@@ -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-200", className: "h-[50rem]",
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?
@@ -4,7 +4,6 @@ import { Banner, VStack } from "@yaakapp-internal/ui";
import { useState } from "react"; import { useState } from "react";
import { openWorkspaceFromSyncDir } from "../commands/openWorkspaceFromSyncDir"; import { openWorkspaceFromSyncDir } from "../commands/openWorkspaceFromSyncDir";
import { appInfo } from "../lib/appInfo"; import { appInfo } from "../lib/appInfo";
import { CommercialUseBanner } from "./CommercialUseBanner";
import { showErrorToast } from "../lib/toast"; import { showErrorToast } from "../lib/toast";
import { Button } from "./core/Button"; import { Button } from "./core/Button";
import { Checkbox } from "./core/Checkbox"; import { Checkbox } from "./core/Checkbox";
@@ -90,8 +89,6 @@ export function CloneGitRepositoryDialog({ hide }: Props) {
</Banner> </Banner>
)} )}
<CommercialUseBanner source="git-clone" title="Using Git for work?" />
<PlainInput <PlainInput
required required
label="Repository URL" label="Repository URL"
@@ -108,7 +105,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 shrink-0", "inline-block w-[0.75em] h-[0.75em] rounded-full mr-1.5 border border-transparent flex-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-sm px-1.5", "w-full h-sm flex items-center rounded 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",
@@ -1,130 +0,0 @@
import { invoke } from "@tauri-apps/api/core";
import { openUrl } from "@tauri-apps/plugin-opener";
import type { LicenseCheckStatus } from "@yaakapp-internal/license";
import { useCallback, useEffect, useRef, useState } from "react";
import { useKeyValue } from "../hooks/useKeyValue";
import { appInfo } from "../lib/appInfo";
import { pricingUrl } from "../lib/pricingUrl";
import { DismissibleBanner } from "./core/DismissibleBanner";
const COMMERCIAL_USE_SNOOZE_MS = 7 * 24 * 60 * 60 * 1000;
const COMMERCIAL_USE_BANNER_MESSAGE =
"Personal use of Yaak is free. If youre using Yaak at work, please purchase a license.";
export function CommercialUseBanner({
source,
title,
}: {
source: string;
title: string;
}) {
const [visible, setVisible] = useState(false);
const snoozeStartedRef = useRef(false);
const {
isLoading: isSnoozeLoading,
set: setSnoozedAt,
value: snoozedAt,
} = useKeyValue<string | null>({
namespace: "global",
key: "commercial-use-banner-snoozed-at",
fallback: null,
});
useEffect(() => {
let canceled = false;
shouldShowCommercialUsePrompt()
.then((shouldShow) => {
if (!canceled) setVisible(shouldShow);
})
.catch(console.error);
return () => {
canceled = true;
};
}, [source]);
const snoozed = isSnoozed(snoozedAt, COMMERCIAL_USE_SNOOZE_MS);
const handleShow = useCallback(() => {
if (snoozeStartedRef.current || snoozed) {
return;
}
snoozeStartedRef.current = true;
setSnoozedAt(JSON.stringify({ source, at: new Date().toISOString() })).catch(console.error);
}, [setSnoozedAt, snoozed, source]);
if (!visible || isSnoozeLoading || (snoozed && !snoozeStartedRef.current)) {
return null;
}
return (
<div className="w-full">
<DismissibleBanner
id={`commercial-use:${source}`}
color="info"
className="w-full"
onDismiss={() =>
setSnoozedAt(JSON.stringify({ source, at: new Date().toISOString() }))
}
onShow={handleShow}
actions={[
{
label: "Purchase License",
color: "info",
variant: "solid",
onClick: () => {
openCommercialUsePricing(source).catch(console.error);
},
},
]}
>
<div className="text-sm">
<p className="font-semibold text-text">{title}</p>
<p className="mt-0.5 text-text-subtle">{COMMERCIAL_USE_BANNER_MESSAGE}</p>
</div>
</DismissibleBanner>
</div>
);
}
async function shouldShowCommercialUsePrompt(): Promise<boolean> {
// Open-source builds omit the Rust license plugin, so never show commercial-use prompts there.
if (appInfo.featureLicense !== true) {
return false;
}
try {
const license = await invoke<LicenseCheckStatus>("plugin:yaak-license|check");
return license.status === "personal_use";
} catch (err) {
console.log("Failed to check license before commercial-use prompt", err);
return false;
}
}
async function openCommercialUsePricing(source: string): Promise<void> {
await openUrl(pricingUrl(`app.commercial-use.${source}`)).catch(console.error);
}
function isSnoozed(value: string | null, ms: number): boolean {
if (value == null) return false;
try {
const snooze = JSON.parse(value) as { at?: unknown };
const at = typeof snooze.at === "string" ? snooze.at : null;
return isWithinMs(at, ms);
} catch {
// Older builds stored only the timestamp, so keep respecting that as a global snooze.
return isWithinMs(value, ms);
}
}
function isWithinMs(date: string | null, ms: number): boolean {
if (date == null) return false;
const time = new Date(date).getTime();
if (Number.isNaN(time)) return false;
return Date.now() - time < ms;
}
+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-40"> <TruncatedWideTableCell className="min-w-[10rem]">
{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-28", labelClassName)} {...props} />; return <KeyValueRow labelClassName={classNames("w-[7rem]", 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-20 resize-y")} className={classNames(cookieInputClassName, "min-h-[5rem] 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-hidden", "border border-border-subtle outline-none",
"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-sm">/etc/hosts</code> but <code className="text-text-subtlest bg-surface-highlight px-1 rounded">/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-16" : undefined, className: arg.multiLine ? "min-h-[4rem]" : 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-40", // So it doesn't take up too much space !arg.rows && "max-h-[10rem]", // 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-12" className="min-h-[3rem]"
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-200! max-h-240!", className: "!max-w-[50rem] !max-h-[60rem]",
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-px" resizeHandleClassName="-translate-x-[1px]"
firstSlot={() => ( firstSlot={() => (
<EnvironmentEditDialogSidebar <EnvironmentEditDialogSidebar
selectedEnvironmentId={selectedEnvironment?.id ?? null} selectedEnvironmentId={selectedEnvironment?.id ?? null}
@@ -8,7 +8,6 @@ import slugify from "slugify";
import { activeWorkspaceAtom } from "../hooks/useActiveWorkspace"; import { activeWorkspaceAtom } from "../hooks/useActiveWorkspace";
import { pluralizeCount } from "../lib/pluralize"; import { pluralizeCount } from "../lib/pluralize";
import { invokeCmd } from "../lib/tauri"; import { invokeCmd } from "../lib/tauri";
import { CommercialUseBanner } from "./CommercialUseBanner";
import { Button } from "./core/Button"; import { Button } from "./core/Button";
import { Checkbox } from "./core/Checkbox"; import { Checkbox } from "./core/Checkbox";
import { DetailsBanner } from "./core/DetailsBanner"; import { DetailsBanner } from "./core/DetailsBanner";
@@ -86,10 +85,8 @@ function ExportDataDialogContent({
const numSelected = Object.values(selectedWorkspaces).filter(Boolean).length; const numSelected = Object.values(selectedWorkspaces).filter(Boolean).length;
const noneSelected = numSelected === 0; const noneSelected = numSelected === 0;
return ( return (
<div className="h-full w-full grid grid-rows-[minmax(0,1fr)_auto] overflow-hidden rounded-b-lg"> <div className="w-full grid grid-rows-[minmax(0,1fr)_auto]">
<VStack space={3} className="overflow-auto px-5 pb-6"> <VStack space={3} className="overflow-auto px-5 pb-6">
<CommercialUseBanner source="data-export" title="Exporting work data?" />
<table className="w-full mb-auto min-w-full max-w-full divide-y divide-surface-highlight"> <table className="w-full mb-auto min-w-full max-w-full divide-y divide-surface-highlight">
<thead> <thead>
<tr> <tr>
@@ -140,9 +137,9 @@ function ExportDataDialogContent({
/> />
</DetailsBanner> </DetailsBanner>
</VStack> </VStack>
<footer className="px-5 grid grid-cols-[1fr_auto] items-center bg-surface py-3 border-t border-border-subtle"> <footer className="px-5 grid grid-cols-[1fr_auto] items-center bg-surface-highlight py-2 border-t border-border-subtle">
<div> <div>
<Link href="https://yaak.app/button/new" noUnderline className="text-text-subtlest"> <Link href="https://yaak.app/button/new" noUnderline className="text-text-subtle">
Create Run Button Create Run Button
</Link> </Link>
</div> </div>
+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-sm px-2.5 py-0.5 truncate w-full min-w-0"> <code className="font-mono text-editor text-info border border-info rounded 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-sm px-1.5 py-0.5 truncate w-full", "font-mono text-editor border rounded 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="shrink-0" /> <Icon icon="folder_cog" size="lg" color="secondary" className="flex-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 shrink-0" /> <Icon icon="chevron_right" size="lg" className="opacity-50 flex-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 shrink-0" /> <Icon icon="chevron_right" size="lg" className="opacity-50 flex-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-20 ring-0!", "font-mono text-editor min-w-[5rem] !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 })}
@@ -10,17 +10,14 @@ import { HStack, Icon, InlineCode } from "@yaakapp-internal/ui";
import { useCallback } from "react"; import { useCallback } from "react";
import { openFolderSettings } from "../commands/openFolderSettings"; import { openFolderSettings } from "../commands/openFolderSettings";
import { openWorkspaceSettings } from "../commands/openWorkspaceSettings"; import { openWorkspaceSettings } from "../commands/openWorkspaceSettings";
import { useAuthDropdownOptions } from "../hooks/useAuthTab";
import { useHttpAuthenticationConfig } from "../hooks/useHttpAuthenticationConfig"; import { useHttpAuthenticationConfig } from "../hooks/useHttpAuthenticationConfig";
import { useInheritedAuthentication } from "../hooks/useInheritedAuthentication"; import { useInheritedAuthentication } from "../hooks/useInheritedAuthentication";
import { useRenderTemplate } from "../hooks/useRenderTemplate"; import { useRenderTemplate } from "../hooks/useRenderTemplate";
import { resolvedModelName } from "../lib/resolvedModelName"; import { resolvedModelName } from "../lib/resolvedModelName";
import { Button } from "./core/Button";
import { Dropdown, type DropdownItem } from "./core/Dropdown"; import { Dropdown, type DropdownItem } from "./core/Dropdown";
import { IconButton } from "./core/IconButton"; import { IconButton } from "./core/IconButton";
import { Input, type InputProps } from "./core/Input"; import { Input, type InputProps } from "./core/Input";
import { Link } from "./core/Link"; import { Link } from "./core/Link";
import { RadioDropdown } from "./core/RadioDropdown";
import { SegmentedControl } from "./core/SegmentedControl"; import { SegmentedControl } from "./core/SegmentedControl";
import { DynamicForm } from "./DynamicForm"; import { DynamicForm } from "./DynamicForm";
import { EmptyStateText } from "./EmptyStateText"; import { EmptyStateText } from "./EmptyStateText";
@@ -38,8 +35,7 @@ export function HttpAuthenticationEditor({ model }: Props) {
); );
const handleChange = useCallback( const handleChange = useCallback(
async (authentication: Record<string, unknown>) => async (authentication: Record<string, unknown>) => await patchModel(model, { authentication }),
await patchModel(model, { authentication }),
[model], [model],
); );
@@ -51,8 +47,7 @@ export function HttpAuthenticationEditor({ model }: Props) {
return ( return (
<EmptyStateText> <EmptyStateText>
<p> <p>
Auth plugin not found for{" "} Auth plugin not found for <InlineCode>{model.authenticationType}</InlineCode>
<InlineCode>{model.authenticationType}</InlineCode>
</p> </p>
</EmptyStateText> </EmptyStateText>
); );
@@ -61,20 +56,11 @@ export function HttpAuthenticationEditor({ model }: Props) {
if (inheritedAuth == null) { if (inheritedAuth == null) {
if (model.model === "workspace" || model.model === "folder") { if (model.model === "workspace" || model.model === "folder") {
return ( return (
<EmptyStateText className="flex-col gap-3"> <EmptyStateText className="flex-col gap-1">
<div className="not-italic flex flex-col items-center gap-3 text-center"> <p>
<p className="max-w-md text-sm text-text-subtle"> Apply auth to all requests in <strong>{resolvedModelName(model)}</strong>
Choose an auth method to apply it to all requests in{" "}
<strong className="font-semibold text-text-subtle">
{resolvedModelName(model)}
</strong>
.
</p> </p>
<AuthenticationTypeDropdown model={model} /> <Link href="https://yaak.app/docs/using-yaak/request-inheritance">Documentation</Link>
<Link href="https://yaak.app/docs/using-yaak/request-inheritance">
Documentation
</Link>
</div>
</EmptyStateText> </EmptyStateText>
); );
} }
@@ -97,8 +83,7 @@ export function HttpAuthenticationEditor({ model }: Props) {
type="submit" type="submit"
className="underline hover:text-text" className="underline hover:text-text"
onClick={() => { onClick={() => {
if (inheritedAuth.model === "folder") if (inheritedAuth.model === "folder") openFolderSettings(inheritedAuth.id, "auth");
openFolderSettings(inheritedAuth.id, "auth");
else openWorkspaceSettings("auth"); else openWorkspaceSettings("auth");
}} }}
> >
@@ -118,8 +103,7 @@ export function HttpAuthenticationEditor({ model }: Props) {
hideLabel hideLabel
name="enabled" name="enabled"
value={ value={
model.authentication.disabled === false || model.authentication.disabled === false || model.authentication.disabled == null
model.authentication.disabled == null
? "__TRUE__" ? "__TRUE__"
: model.authentication.disabled === true : model.authentication.disabled === true
? "__FALSE__" ? "__FALSE__"
@@ -156,7 +140,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>
)} )}
@@ -167,9 +151,7 @@ export function HttpAuthenticationEditor({ model }: Props) {
className="w-full" className="w-full"
stateKey={`auth.${model.id}.dynamic`} stateKey={`auth.${model.id}.dynamic`}
value={model.authentication.disabled} value={model.authentication.disabled}
onChange={(v) => onChange={(v) => handleChange({ ...model.authentication, disabled: v })}
handleChange({ ...model.authentication, disabled: v })
}
/> />
</div> </div>
)} )}
@@ -187,33 +169,6 @@ export function HttpAuthenticationEditor({ model }: Props) {
); );
} }
function AuthenticationTypeDropdown({ model }: Props) {
const options = useAuthDropdownOptions(model);
if (options == null) return null;
return (
<RadioDropdown
items={options.items}
itemsAfter={options.itemsAfter}
itemsBefore={options.itemsBefore}
value={options.value}
onChange={options.onChange}
>
<Button
color="secondary"
variant="border"
size="sm"
rightSlot={
<Icon icon="chevron_down" size="sm" className="text-text-subtle" />
}
>
Select Auth
</Button>
</RadioDropdown>
);
}
function AuthenticationDisabledInput({ function AuthenticationDisabledInput({
value, value,
onChange, onChange,
@@ -243,11 +198,7 @@ function AuthenticationDisabledInput({
rightSlot={ rightSlot={
<div className="px-1 flex items-center"> <div className="px-1 flex items-center">
<div className="rounded-full bg-surface-highlight text-xs px-1.5 py-0.5 text-text-subtle whitespace-nowrap"> <div className="rounded-full bg-surface-highlight text-xs px-1.5 py-0.5 text-text-subtle whitespace-nowrap">
{rendered.isPending {rendered.isPending ? "loading" : rendered.data ? "enabled" : "disabled"}
? "loading"
: rendered.data
? "enabled"
: "disabled"}
</div> </div>
</div> </div>
} }
@@ -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}
/> />
)} )}
@@ -19,7 +19,6 @@ import { useSendAnyHttpRequest } from "../hooks/useSendAnyHttpRequest";
import { deepEqualAtom } from "../lib/atoms"; import { deepEqualAtom } from "../lib/atoms";
import { languageFromContentType } from "../lib/contentType"; import { languageFromContentType } from "../lib/contentType";
import { generateId } from "../lib/generateId"; import { generateId } from "../lib/generateId";
import { extractPathPlaceholders } from "../lib/pathPlaceholders";
import { import {
BODY_TYPE_BINARY, BODY_TYPE_BINARY,
BODY_TYPE_FORM_MULTIPART, BODY_TYPE_FORM_MULTIPART,
@@ -132,7 +131,9 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
); );
const { urlParameterPairs, urlParametersKey } = useMemo(() => { const { urlParameterPairs, urlParametersKey } = useMemo(() => {
const placeholderNames = extractPathPlaceholders(activeRequest.url); const placeholderNames = Array.from(activeRequest.url.matchAll(/\/(:[^/]+)/g)).map(
(m) => m[1] ?? "",
);
const nonEmptyParameters = activeRequest.urlParameters.filter((p) => p.name || p.value); const nonEmptyParameters = activeRequest.urlParameters.filter((p) => p.name || p.value);
const items: Pair[] = [...nonEmptyParameters]; const items: Pair[] = [...nonEmptyParameters];
for (const name of placeholderNames) { for (const name of placeholderNames) {
@@ -346,7 +347,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 +457,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,12 +4,10 @@ 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";
@@ -80,8 +78,6 @@ 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[]>(
() => [ () => [
@@ -97,22 +93,6 @@ 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,
},
],
}, },
}, },
{ {
@@ -155,18 +135,12 @@ 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,
@@ -193,7 +167,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 shrink-0", "text-text-subtle w-full flex-shrink-0",
// Remove a bit of space because the tabs have lots too // Remove a bit of space because the tabs have lots too
"-mb-1.5", "-mb-1.5",
)} )}
@@ -206,7 +180,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 shrink-0"> <HStack space={2} className="w-full flex-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>
@@ -220,7 +194,7 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
{shouldShowRedirectDropWarning ? ( {shouldShowRedirectDropWarning ? (
<Tooltip <Tooltip
tabIndex={0} tabIndex={0}
className="my-auto pl-3 shrink-0 max-w-full justify-self-end overflow-hidden" className="my-auto pl-3 flex-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">
@@ -249,7 +223,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 shrink! max-w-full" className="font-sans text-sm !flex-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" />}
> >
@@ -262,7 +236,7 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
) : ( ) : (
<span /> <span />
)} )}
<div className="justify-self-end shrink-0"> <div className="justify-self-end flex-shrink-0">
<RecentHttpResponsesDropdown <RecentHttpResponsesDropdown
responses={responses} responses={responses}
activeResponse={activeResponse} activeResponse={activeResponse}
@@ -275,7 +249,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 shrink-0"> <Banner color="danger" className="mx-3 mt-1 flex-shrink-0">
{activeResponse.error} {activeResponse.error}
</Banner> </Banner>
)} )}
@@ -1,7 +1,6 @@
import { VStack } from "@yaakapp-internal/ui"; import { VStack } from "@yaakapp-internal/ui";
import { useState } from "react"; import { useState } from "react";
import { useLocalStorage } from "react-use"; import { useLocalStorage } from "react-use";
import { CommercialUseBanner } from "./CommercialUseBanner";
import { Button } from "./core/Button"; import { Button } from "./core/Button";
import { SelectFile } from "./SelectFile"; import { SelectFile } from "./SelectFile";
@@ -15,8 +14,6 @@ export function ImportDataDialog({ importData }: Props) {
return ( return (
<VStack space={5} className="pb-4"> <VStack space={5} className="pb-4">
<CommercialUseBanner source="data-import" title="Importing work data?" />
<VStack space={1}> <VStack space={1}>
<ul className="list-disc pl-5"> <ul className="list-disc pl-5">
<li>OpenAPI 3.0, 3.1</li> <li>OpenAPI 3.0, 3.1</li>
@@ -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-48">Renew License</div>, label: <div className="min-w-[12rem]">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>
); );
@@ -13,7 +13,6 @@ import {
modelSupportsSetting, modelSupportsSetting,
type RequestSettingDefinition, type RequestSettingDefinition,
SETTING_FOLLOW_REDIRECTS, SETTING_FOLLOW_REDIRECTS,
SETTING_REQUEST_MESSAGE_SIZE,
SETTING_REQUEST_TIMEOUT, SETTING_REQUEST_TIMEOUT,
SETTING_SEND_COOKIES, SETTING_SEND_COOKIES,
SETTING_STORE_COOKIES, SETTING_STORE_COOKIES,
@@ -23,44 +22,21 @@ import { Checkbox } from "./core/Checkbox";
import { PlainInput } from "./core/PlainInput"; import { PlainInput } from "./core/PlainInput";
import { import {
SettingOverrideRow, SettingOverrideRow,
SettingRow,
SettingRowBoolean, SettingRowBoolean,
SettingRowNumber,
SettingsList, SettingsList,
SettingsSection, SettingsSection,
} from "./core/SettingRow"; } from "./core/SettingRow";
const BYTES_PER_MB = 1024 * 1024;
const MAX_REQUEST_MESSAGE_SIZE_BYTES = 2_147_483_647;
const MAX_MESSAGE_SIZE_MB = MAX_REQUEST_MESSAGE_SIZE_BYTES / BYTES_PER_MB;
interface Props { interface Props {
showSectionTitles?: boolean; showSectionTitles?: boolean;
model: ModelWithSettings; model: ModelWithSettings;
} }
type ModelWithSettings = type ModelWithSettings = Workspace | Folder | HttpRequest | WebsocketRequest | GrpcRequest;
| Workspace
| Folder
| HttpRequest
| WebsocketRequest
| GrpcRequest;
type ModelWithHttpSettings = Workspace | Folder | HttpRequest; type ModelWithHttpSettings = Workspace | Folder | HttpRequest;
type ModelWithTlsSettings = type ModelWithTlsSettings = Workspace | Folder | HttpRequest | WebsocketRequest | GrpcRequest;
| Workspace type ModelWithCookieSettings = Workspace | Folder | HttpRequest | WebsocketRequest;
| Folder
| HttpRequest
| WebsocketRequest
| GrpcRequest;
type ModelWithCookieSettings =
| Workspace
| Folder
| HttpRequest
| WebsocketRequest;
type ModelWithMessageSizeSettings =
| Workspace
| Folder
| WebsocketRequest
| GrpcRequest;
type BooleanSetting = boolean | InheritedBoolSetting; type BooleanSetting = boolean | InheritedBoolSetting;
type IntegerSetting = number | InheritedIntSetting; type IntegerSetting = number | InheritedIntSetting;
type CookieSettingsPatch = { type CookieSettingsPatch = {
@@ -74,19 +50,12 @@ type HttpSettingsPatch = {
type TlsSettingsPatch = { type TlsSettingsPatch = {
settingValidateCertificates?: ModelWithTlsSettings["settingValidateCertificates"]; settingValidateCertificates?: ModelWithTlsSettings["settingValidateCertificates"];
}; };
type MessageSizeSettingsPatch = {
settingRequestMessageSize?: ModelWithMessageSizeSettings["settingRequestMessageSize"];
};
export function ModelSettingsEditor({ export function ModelSettingsEditor({ model, showSectionTitles = false }: Props) {
model,
showSectionTitles = false,
}: Props) {
const ancestors = useModelAncestors(model); const ancestors = useModelAncestors(model);
const supportsHttpSettings = modelSupportsHttpSettings(model); const supportsHttpSettings = modelSupportsHttpSettings(model);
const supportsCookieSettings = modelSupportsCookieSettings(model); const supportsCookieSettings = modelSupportsCookieSettings(model);
const supportsTlsSettings = modelSupportsTlsSettings(model); const supportsTlsSettings = modelSupportsTlsSettings(model);
const supportsMessageSizeSettings = modelSupportsMessageSizeSettings(model);
return ( return (
<SettingsList className="space-y-8"> <SettingsList className="space-y-8">
@@ -108,22 +77,6 @@ export function ModelSettingsEditor({
} }
/> />
)} )}
{supportsMessageSizeSettings && (
<MessageSizeSettingRow
settingDefinition={SETTING_REQUEST_MESSAGE_SIZE}
setting={model.settingRequestMessageSize}
inheritedValue={resolveInheritedValue(
ancestors,
SETTING_REQUEST_MESSAGE_SIZE.modelKey,
model.settingRequestMessageSize,
)}
onChange={(settingRequestMessageSize) =>
patchMessageSizeSettings(model, {
settingRequestMessageSize,
})
}
/>
)}
<BooleanSettingRow <BooleanSettingRow
settingDefinition={SETTING_VALIDATE_CERTIFICATES} settingDefinition={SETTING_VALIDATE_CERTIFICATES}
setting={model.settingValidateCertificates} setting={model.settingValidateCertificates}
@@ -157,9 +110,7 @@ export function ModelSettingsEditor({
</SettingsSection> </SettingsSection>
)} )}
{supportsCookieSettings && ( {supportsCookieSettings && (
<SettingsSection <SettingsSection title={supportsTlsSettings || showSectionTitles ? "Cookies" : null}>
title={supportsTlsSettings || showSectionTitles ? "Cookies" : null}
>
<BooleanSettingRow <BooleanSettingRow
settingDefinition={SETTING_SEND_COOKIES} settingDefinition={SETTING_SEND_COOKIES}
setting={model.settingSendCookies} setting={model.settingSendCookies}
@@ -207,103 +158,46 @@ export function countOverriddenSettings(model: ModelWithSettings) {
settings.push(model.settingFollowRedirects, model.settingRequestTimeout); settings.push(model.settingFollowRedirects, model.settingRequestTimeout);
} }
if (modelSupportsMessageSizeSettings(model)) { return settings.filter((setting) => isInheritedSetting(setting) && setting.enabled === true)
settings.push(model.settingRequestMessageSize); .length;
} }
return settings.filter( function patchCookieSettings(model: ModelWithCookieSettings, patch: Partial<CookieSettingsPatch>) {
(setting) => isInheritedSetting(setting) && setting.enabled === true, if (model.model === "workspace") return patchModel(model, patch as Partial<Workspace>);
).length; if (model.model === "folder") return patchModel(model, patch as Partial<Folder>);
} if (model.model === "http_request") return patchModel(model, patch as Partial<HttpRequest>);
if (model.model === "websocket_request")
function patchCookieSettings(
model: ModelWithCookieSettings,
patch: Partial<CookieSettingsPatch>,
) {
switch (model.model) {
case "workspace":
return patchModel(model, patch as Partial<Workspace>);
case "folder":
return patchModel(model, patch as Partial<Folder>);
case "http_request":
return patchModel(model, patch as Partial<HttpRequest>);
case "websocket_request":
return patchModel(model, patch as Partial<WebsocketRequest>); return patchModel(model, patch as Partial<WebsocketRequest>);
} throw new Error("Unsupported cookie settings model");
} }
function patchHttpSettings( function patchHttpSettings(model: ModelWithHttpSettings, patch: Partial<HttpSettingsPatch>) {
model: ModelWithHttpSettings, if (model.model === "workspace") return patchModel(model, patch as Partial<Workspace>);
patch: Partial<HttpSettingsPatch>, if (model.model === "folder") return patchModel(model, patch as Partial<Folder>);
) {
switch (model.model) {
case "workspace":
return patchModel(model, patch as Partial<Workspace>);
case "folder":
return patchModel(model, patch as Partial<Folder>);
case "http_request":
return patchModel(model, patch as Partial<HttpRequest>); return patchModel(model, patch as Partial<HttpRequest>);
} }
}
function patchTlsSettings( function patchTlsSettings(model: ModelWithTlsSettings, patch: Partial<TlsSettingsPatch>) {
model: ModelWithTlsSettings, if (model.model === "workspace") return patchModel(model, patch as Partial<Workspace>);
patch: Partial<TlsSettingsPatch>, if (model.model === "folder") return patchModel(model, patch as Partial<Folder>);
) { if (model.model === "http_request") return patchModel(model, patch as Partial<HttpRequest>);
switch (model.model) { if (model.model === "websocket_request")
case "workspace":
return patchModel(model, patch as Partial<Workspace>);
case "folder":
return patchModel(model, patch as Partial<Folder>);
case "http_request":
return patchModel(model, patch as Partial<HttpRequest>);
case "websocket_request":
return patchModel(model, patch as Partial<WebsocketRequest>); return patchModel(model, patch as Partial<WebsocketRequest>);
case "grpc_request":
return patchModel(model, patch as Partial<GrpcRequest>); return patchModel(model, patch as Partial<GrpcRequest>);
} }
}
function patchMessageSizeSettings( function modelSupportsHttpSettings(model: ModelWithSettings): model is ModelWithHttpSettings {
model: ModelWithMessageSizeSettings,
patch: Partial<MessageSizeSettingsPatch>,
) {
switch (model.model) {
case "workspace":
return patchModel(model, patch as Partial<Workspace>);
case "folder":
return patchModel(model, patch as Partial<Folder>);
case "websocket_request":
return patchModel(model, patch as Partial<WebsocketRequest>);
case "grpc_request":
return patchModel(model, patch as Partial<GrpcRequest>);
}
}
function modelSupportsHttpSettings(
model: ModelWithSettings,
): model is ModelWithHttpSettings {
return modelSupportsSetting(model, SETTING_REQUEST_TIMEOUT); return modelSupportsSetting(model, SETTING_REQUEST_TIMEOUT);
} }
function modelSupportsCookieSettings( function modelSupportsCookieSettings(model: ModelWithSettings): model is ModelWithCookieSettings {
model: ModelWithSettings,
): model is ModelWithCookieSettings {
return modelSupportsSetting(model, SETTING_SEND_COOKIES); return modelSupportsSetting(model, SETTING_SEND_COOKIES);
} }
function modelSupportsTlsSettings( function modelSupportsTlsSettings(model: ModelWithSettings): model is ModelWithTlsSettings {
model: ModelWithSettings,
): model is ModelWithTlsSettings {
return modelSupportsSetting(model, SETTING_VALIDATE_CERTIFICATES); return modelSupportsSetting(model, SETTING_VALIDATE_CERTIFICATES);
} }
function modelSupportsMessageSizeSettings(
model: ModelWithSettings,
): model is ModelWithMessageSizeSettings {
return modelSupportsSetting(model, SETTING_REQUEST_MESSAGE_SIZE);
}
function BooleanSettingRow({ function BooleanSettingRow({
inheritedValue, inheritedValue,
setting, setting,
@@ -317,11 +211,7 @@ function BooleanSettingRow({
}) { }) {
const inherited = isInheritedSetting(setting); const inherited = isInheritedSetting(setting);
const overridden = inherited ? setting.enabled === true : false; const overridden = inherited ? setting.enabled === true : false;
const value = inherited const value = inherited ? (overridden ? setting.value : inheritedValue) : setting;
? overridden
? setting.value
: inheritedValue
: setting;
if (!inherited) { if (!inherited) {
return ( return (
@@ -365,189 +255,48 @@ function IntegerSettingRow({
}) { }) {
const inherited = isInheritedSetting(setting); const inherited = isInheritedSetting(setting);
const overridden = inherited ? setting.enabled === true : false; const overridden = inherited ? setting.enabled === true : false;
const value = inherited const value = inherited ? (overridden ? setting.value : inheritedValue) : setting;
? overridden
? setting.value
: inheritedValue
: setting;
if (!inherited) { if (!inherited) {
return ( return (
<SettingRow <SettingRowNumber
name={settingDefinition.modelKey}
title={settingDefinition.title} title={settingDefinition.title}
description={settingDefinition.description} description={settingDefinition.description}
>
<NumberUnitInput
name={settingDefinition.modelKey}
label={settingDefinition.title}
unit="ms"
value={`${value}`}
placeholder={`${settingDefinition.defaultValue}`}
validate={isValidInteger}
onChange={(value) => onChange(parseInteger(value))}
/>
</SettingRow>
);
}
return (
<SettingOverrideRow
title={settingDefinition.title}
description={settingDefinition.description}
overridden={overridden}
onResetOverride={() => onChange({ ...setting, enabled: false })}
>
<NumberUnitInput
name={settingDefinition.modelKey}
label={settingDefinition.title}
unit="ms"
value={`${value}`}
placeholder={`${settingDefinition.defaultValue}`}
validate={isValidInteger}
onChange={(value) =>
onChange({
...setting,
enabled: true,
value: parseInteger(value),
})
}
/>
</SettingOverrideRow>
);
}
function MessageSizeSettingRow({
inheritedValue,
setting,
settingDefinition,
onChange,
}: {
inheritedValue: number;
setting: IntegerSetting;
settingDefinition: RequestSettingDefinition<"settingRequestMessageSize">;
onChange: (setting: IntegerSetting) => void;
}) {
const inherited = isInheritedSetting(setting);
const overridden = inherited ? setting.enabled === true : false;
const value = inherited
? overridden
? setting.value
: inheritedValue
: setting;
const displayValue = formatMegabytes(value);
const placeholder = "0";
if (!inherited) {
return (
<SettingRow
title={settingDefinition.title}
description={settingDefinition.description}
>
<MessageSizeInput
name={settingDefinition.modelKey}
label={settingDefinition.title}
value={displayValue}
placeholder={placeholder}
onChange={(value) => onChange(parseMegabytes(value))}
/>
</SettingRow>
);
}
return (
<SettingOverrideRow
title={settingDefinition.title}
description={settingDefinition.description}
overridden={overridden}
onResetOverride={() => onChange({ ...setting, enabled: false })}
>
<MessageSizeInput
name={settingDefinition.modelKey}
label={settingDefinition.title}
value={displayValue}
placeholder={placeholder}
onChange={(value) =>
onChange({
...setting,
enabled: true,
value: parseMegabytes(value),
})
}
/>
</SettingOverrideRow>
);
}
function MessageSizeInput({
label,
name,
onChange,
placeholder,
value,
}: {
label: string;
name: string;
onChange: (value: string) => void;
placeholder: string;
value: string;
}) {
return (
<NumberUnitInput
name={name}
label={label}
unit="MB"
value={value} value={value}
inputMode="decimal" placeholder={`${settingDefinition.defaultValue}`}
step="any" validate={(value) => value === "" || Number.parseInt(value, 10) >= 0}
placeholder={placeholder} onChange={(value) => onChange(value)}
validate={isValidMegabytes}
onChange={onChange}
/> />
); );
} }
function NumberUnitInput({
inputMode,
label,
name,
onChange,
placeholder,
step,
unit,
validate,
value,
}: {
inputMode?: "decimal" | "numeric";
label: string;
name: string;
onChange: (value: string) => void;
placeholder: string;
step?: number | "any";
unit: string;
validate: (value: string) => boolean;
value: string;
}) {
return ( return (
<SettingOverrideRow
title={settingDefinition.title}
description={settingDefinition.description}
overridden={overridden}
onResetOverride={() => onChange({ ...setting, enabled: false })}
>
<PlainInput <PlainInput
hideLabel hideLabel
name={name} name={settingDefinition.modelKey}
label={label} label={settingDefinition.title}
size="sm" size="sm"
type="number" type="number"
inputMode={inputMode} placeholder={`${settingDefinition.defaultValue}`}
step={step} defaultValue={`${value}`}
placeholder={placeholder} containerClassName="!w-48"
defaultValue={value} validate={(value) => value === "" || Number.parseInt(value, 10) >= 0}
className="[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" onChange={(value) =>
containerClassName="w-48!" onChange({
validate={validate} ...setting,
rightSlot={ enabled: true,
<span className="flex self-stretch items-center border-l border-border-subtle px-2 text-xs font-medium text-text-subtle"> value: Number.parseInt(value, 10) || 0,
{unit} })
</span>
} }
onChange={onChange}
/> />
</SettingOverrideRow>
); );
} }
@@ -559,7 +308,7 @@ function isInheritedSetting<T>(
function resolveInheritedValue( function resolveInheritedValue(
ancestors: (Folder | Workspace)[], ancestors: (Folder | Workspace)[],
key: "settingRequestTimeout" | "settingRequestMessageSize", key: "settingRequestTimeout",
fallback: IntegerSetting, fallback: IntegerSetting,
): number; ): number;
function resolveInheritedValue( function resolveInheritedValue(
@@ -589,46 +338,10 @@ function resolveInheritedValue(
type WorkspaceSettings = Pick< type WorkspaceSettings = Pick<
Workspace, Workspace,
| "settingFollowRedirects" | "settingFollowRedirects"
| "settingRequestMessageSize"
| "settingRequestTimeout" | "settingRequestTimeout"
| "settingSendCookies" | "settingSendCookies"
| "settingStoreCookies" | "settingStoreCookies"
| "settingValidateCertificates" | "settingValidateCertificates"
>; >;
type BooleanWorkspaceSettingKey = Exclude< type BooleanWorkspaceSettingKey = Exclude<keyof WorkspaceSettings, "settingRequestTimeout">;
keyof WorkspaceSettings,
"settingRequestTimeout" | "settingRequestMessageSize"
>;
function formatMegabytes(bytes: number) {
const megabytes = bytes / BYTES_PER_MB;
return Number.isInteger(megabytes)
? `${megabytes}`
: megabytes.toFixed(3).replace(/\.?0+$/, "");
}
function parseMegabytes(value: string) {
const megabytes = Number(value);
return Number.isFinite(megabytes) ? Math.round(megabytes * BYTES_PER_MB) : 0;
}
function parseInteger(value: string) {
const parsed = Number(value);
return Number.isFinite(parsed) ? Math.trunc(parsed) : 0;
}
function isValidInteger(value: string) {
const parsed = Number(value);
return value === "" || (Number.isInteger(parsed) && parsed >= 0);
}
function isValidMegabytes(value: string) {
if (value === "") return true;
const megabytes = Number(value);
return (
Number.isFinite(megabytes) &&
megabytes >= 0 &&
megabytes <= MAX_MESSAGE_SIZE_MB
);
}
@@ -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-20" className="mr-auto min-w-[5rem]"
onClick={async () => { onClick={async () => {
await router.navigate({ await router.navigate({
to: "/workspaces/$workspaceId", to: "/workspaces/$workspaceId",
+5 -7
View File
@@ -1,5 +1,3 @@
@reference "../main.css";
.prose { .prose {
@apply text-text; @apply text-text;
@@ -100,7 +98,7 @@
@apply text-notice hover:underline; @apply text-notice hover:underline;
* { * {
@apply text-notice!; @apply text-notice !important;
} }
} }
@@ -115,12 +113,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-sm not-italic; @apply px-1.5 py-0.5 rounded not-italic;
@apply select-text; @apply select-text;
} }
pre { pre {
@apply bg-surface-highlight! text-text!; @apply bg-surface-highlight text-text !important;
@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;
@@ -132,7 +130,7 @@
.banner { .banner {
@apply border border-dashed; @apply border border-dashed;
@apply border-border bg-surface-highlight text-text px-4 py-3 rounded-sm text-base; @apply border-border bg-surface-highlight text-text px-4 py-3 rounded text-base;
&::before { &::before {
@apply block font-bold mb-1; @apply block font-bold mb-1;
@@ -163,7 +161,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-sm shadow-lg; @apply italic py-3 pl-5 pr-3 border-l-8 border-surface-active text-lg text-text bg-surface-highlight rounded shadow-lg;
p { p {
@apply m-0; @apply m-0;
@@ -1,17 +1,10 @@
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 { import { formatDistanceToNowStrict } from "date-fns";
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, type DropdownItem } from "./core/Dropdown"; import { Dropdown } from "./core/Dropdown";
import { formatMillis } from "./core/HttpResponseDurationTag";
import { IconButton } from "./core/IconButton"; import { IconButton } from "./core/IconButton";
interface Props { interface Props {
@@ -27,63 +20,6 @@ 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
@@ -100,7 +36,16 @@ export function RecentGrpcConnectionsDropdown({
disabled: connections.length === 0, disabled: connections.length === 0,
}, },
{ type: "separator", label: "History" }, { type: "separator", label: "History" },
...connectionHistoryItems, ...connections.map((c) => ({
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,21 +1,13 @@
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 { import { useCopyHttpResponse } from "../hooks/useCopyHttpResponse";
differenceInHours,
differenceInMinutes,
format,
isToday,
isYesterday,
} from "date-fns";
import { useDeleteHttpResponses } from "../hooks/useDeleteHttpResponses"; import { useDeleteHttpResponses } from "../hooks/useDeleteHttpResponses";
import { useKeyValue } from "../hooks/useKeyValue"; import { useSaveResponse } from "../hooks/useSaveResponse";
import { DismissibleBanner } from "./core/DismissibleBanner"; import { pluralize } from "../lib/pluralize";
import { Dropdown, type DropdownItem } from "./core/Dropdown"; import { Dropdown } 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[];
@@ -30,93 +22,32 @@ 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 responseHistoryItems: DropdownItem[] = []; const saveResponse = useSaveResponse(activeResponse);
let lastHistoryGroup: string | null = null; const copyResponse = useCopyHttpResponse(activeResponse);
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),
@@ -124,25 +55,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" },
{ {
type: "content", label: `Delete ${responses.length} ${pluralize("Response", responses.length)}`,
hidden: dismissedMovedActions === true, onSelect: deleteAllResponses.mutate,
hidden: responses.length === 0,
disabled: responses.length === 0,
},
{ type: "separator" },
...responses.map((r: HttpResponse) => ({
label: ( label: (
<DismissibleBanner <HStack space={2}>
id={movedActionsBannerId} <HttpStatusTag short className="text-xs" response={r} />
color="info" <span className="text-text-subtle">&rarr;</span>{" "}
size="xs" <span className="font-mono text-sm">{r.elapsed >= 0 ? `${r.elapsed}ms` : "n/a"}</span>
className="max-w-72" </HStack>
>
<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,17 +1,10 @@
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 { import { formatDistanceToNowStrict } from "date-fns";
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, type DropdownItem } from "./core/Dropdown"; import { Dropdown } from "./core/Dropdown";
import { formatMillis } from "./core/HttpResponseDurationTag";
import { IconButton } from "./core/IconButton"; import { IconButton } from "./core/IconButton";
interface Props { interface Props {
@@ -26,63 +19,6 @@ 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
@@ -104,7 +40,16 @@ export function RecentWebsocketConnectionsDropdown({
disabled: connections.length === 0, disabled: connections.length === 0,
}, },
{ type: "separator", label: "History" }, { type: "separator", label: "History" },
...connectionHistoryItems, ...connections.map((c) => ({
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-sm"> <span className="text-xs font-sans text-danger bg-danger/10 px-1.5 py-0.5 rounded">
Deleted Deleted
</span> </span>
)} )}
@@ -1,6 +1,5 @@
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";
@@ -30,20 +29,12 @@ 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-200 h-auto!"> <VStack space={5} className="w-[50rem] !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-40 bg-surface x-theme-sidebar border-r border-border pl-3" tabListClassName="min-w-[10rem] 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>
@@ -4,7 +4,6 @@ import { Heading, HStack, InlineCode, VStack } from "@yaakapp-internal/ui";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { useRef } from "react"; import { useRef } from "react";
import { showConfirmDelete } from "../../lib/confirm"; import { showConfirmDelete } from "../../lib/confirm";
import { CommercialUseBanner } from "../CommercialUseBanner";
import { Button } from "../core/Button"; import { Button } from "../core/Button";
import { Checkbox } from "../core/Checkbox"; import { Checkbox } from "../core/Checkbox";
import { DetailsBanner } from "../core/DetailsBanner"; import { DetailsBanner } from "../core/DetailsBanner";
@@ -233,8 +232,6 @@ export function SettingsCertificates() {
</HStack> </HStack>
</div> </div>
<CommercialUseBanner source="client-certificates" title="Using certificates for work?" />
{certificates.length > 0 && ( {certificates.length > 0 && (
<VStack space={3}> <VStack space={3}>
{certificates.map((cert, index) => ( {certificates.map((cert, index) => (
@@ -2,15 +2,22 @@ import { revealItemInDir } from "@tauri-apps/plugin-opener";
import { patchModel, settingsAtom } from "@yaakapp-internal/models"; import { patchModel, settingsAtom } from "@yaakapp-internal/models";
import { Heading, VStack } from "@yaakapp-internal/ui"; import { Heading, VStack } from "@yaakapp-internal/ui";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { activeWorkspaceAtom } from "../../hooks/useActiveWorkspace";
import { useCheckForUpdates } from "../../hooks/useCheckForUpdates"; import { useCheckForUpdates } from "../../hooks/useCheckForUpdates";
import { appInfo } from "../../lib/appInfo"; import { appInfo } from "../../lib/appInfo";
import {
SETTING_FOLLOW_REDIRECTS,
SETTING_REQUEST_TIMEOUT,
SETTING_SEND_COOKIES,
SETTING_STORE_COOKIES,
SETTING_VALIDATE_CERTIFICATES,
} from "../../lib/requestSettings";
import { revealInFinderText } from "../../lib/reveal"; import { revealInFinderText } from "../../lib/reveal";
import { CargoFeature } from "../CargoFeature"; import { CargoFeature } from "../CargoFeature";
import { CommercialUseBanner } from "../CommercialUseBanner";
import { DismissibleBanner } from "../core/DismissibleBanner";
import { IconButton } from "../core/IconButton"; import { IconButton } from "../core/IconButton";
import { import {
ModelSettingRowBoolean, ModelSettingRowBoolean,
ModelSettingRowNumber,
ModelSettingSelectControl, ModelSettingSelectControl,
SettingValue, SettingValue,
SettingRow, SettingRow,
@@ -20,29 +27,20 @@ import {
SettingsSection, SettingsSection,
} from "../core/SettingRow"; } from "../core/SettingRow";
const WORKSPACE_SETTINGS_MOVED_AT = "2026-06-30";
export function SettingsGeneral() { export function SettingsGeneral() {
const workspace = useAtomValue(activeWorkspaceAtom);
const settings = useAtomValue(settingsAtom); const settings = useAtomValue(settingsAtom);
const checkForUpdates = useCheckForUpdates(); const checkForUpdates = useCheckForUpdates();
if (settings == null) { if (settings == null || workspace == null) {
return null; return null;
} }
const showWorkspaceSettingsMovedBanner =
settings.createdAt.slice(0, 10) < WORKSPACE_SETTINGS_MOVED_AT;
return ( return (
<VStack space={1.5} className="mb-4"> <VStack space={1.5} className="mb-4">
<div> <div className="mb-4">
<Heading>General</Heading> <Heading>General</Heading>
<p className="text-text-subtle"> <p className="text-text-subtle">Configure general settings for update behavior and more.</p>
Configure general settings for update behavior and more.
</p>
</div>
<div className="mt-3 mb-5">
<CommercialUseBanner source="settings-general" title="Using Yaak for work?" />
</div> </div>
<SettingsList className="space-y-8"> <SettingsList className="space-y-8">
<CargoFeature feature="updater"> <CargoFeature feature="updater">
@@ -56,7 +54,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" },
@@ -78,9 +76,7 @@ export function SettingsGeneral() {
description="Choose whether updates are installed automatically or manually." description="Choose whether updates are installed automatically or manually."
name="autoupdate" name="autoupdate"
value={settings.autoupdate ? "auto" : "manual"} value={settings.autoupdate ? "auto" : "manual"}
onChange={(v) => onChange={(v) => patchModel(settings, { autoupdate: v === "auto" })}
patchModel(settings, { autoupdate: v === "auto" })
}
options={[ options={[
{ label: "Automatic", value: "auto" }, { label: "Automatic", value: "auto" },
{ label: "Manual", value: "manual" }, { label: "Manual", value: "manual" },
@@ -112,19 +108,54 @@ export function SettingsGeneral() {
</SettingsSection> </SettingsSection>
</CargoFeature> </CargoFeature>
{showWorkspaceSettingsMovedBanner && ( <SettingsSection
<DismissibleBanner title={
id="workspace-settings-moved-2026-06-30" <>
color="info" Workspace{" "}
className="p-4 max-w-xl mx-auto" <span className="inline-block bg-surface-highlight px-2 py-0.5 rounded text">
{workspace.name}
</span>
</>
}
> >
<p> <ModelSettingRowNumber
Workspace specific settings have moved to{" "} model={workspace}
<b>Workspace Settings</b>, accessible from the workspace switcher modelKey={SETTING_REQUEST_TIMEOUT.modelKey}
menu. title={SETTING_REQUEST_TIMEOUT.title}
</p> description={SETTING_REQUEST_TIMEOUT.description}
</DismissibleBanner> placeholder={`${SETTING_REQUEST_TIMEOUT.defaultValue}`}
)} required
validate={(value) => Number.parseInt(value, 10) >= 0}
/>
<ModelSettingRowBoolean
model={workspace}
modelKey={SETTING_VALIDATE_CERTIFICATES.modelKey}
title={SETTING_VALIDATE_CERTIFICATES.title}
description={SETTING_VALIDATE_CERTIFICATES.description}
/>
<ModelSettingRowBoolean
model={workspace}
modelKey={SETTING_FOLLOW_REDIRECTS.modelKey}
title={SETTING_FOLLOW_REDIRECTS.title}
description={SETTING_FOLLOW_REDIRECTS.description}
/>
<ModelSettingRowBoolean
model={workspace}
modelKey={SETTING_SEND_COOKIES.modelKey}
title={SETTING_SEND_COOKIES.title}
description={SETTING_SEND_COOKIES.description}
/>
<ModelSettingRowBoolean
model={workspace}
modelKey={SETTING_STORE_COOKIES.modelKey}
title={SETTING_STORE_COOKIES.title}
description={SETTING_STORE_COOKIES.description}
/>
</SettingsSection>
<SettingsSection title="App Info"> <SettingsSection title="App Info">
<SettingRow title="Version" description="Current Yaak version."> <SettingRow title="Version" description="Current Yaak version.">
@@ -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-hidden cursor-default w-full", "px-4 py-2 rounded-lg bg-surface-highlight border outline-none cursor-default w-full",
"border-border-subtle focus:border-border-focus", "border-border-subtle focus:border-border-focus",
)} )}
> >
@@ -8,7 +8,6 @@ import { useAtomValue } from "jotai";
import { useState } from "react"; import { useState } from "react";
import { activeWorkspaceAtom } from "../../hooks/useActiveWorkspace"; import { activeWorkspaceAtom } from "../../hooks/useActiveWorkspace";
import { showConfirm } from "../../lib/confirm"; import { showConfirm } from "../../lib/confirm";
import { pricingUrl } from "../../lib/pricingUrl";
import { invokeCmd } from "../../lib/tauri"; import { invokeCmd } from "../../lib/tauri";
import { CargoFeature } from "../CargoFeature"; import { CargoFeature } from "../CargoFeature";
import { Button } from "../core/Button"; import { Button } from "../core/Button";
@@ -89,7 +88,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 +105,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 +122,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 +138,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}
@@ -253,9 +252,7 @@ function LicenseSettings({ settings }: { settings: Settings }) {
</p> </p>
<p> <p>
Licenses help keep Yaak independent and sustainable.{" "} Licenses help keep Yaak independent and sustainable.{" "}
<Link href={pricingUrl("app.license.badge-hide-confirm")}> <Link href="https://yaak.app/pricing?s=badge">Purchase a License </Link>
Purchase a License
</Link>
</p> </p>
</VStack> </VStack>
), ),
@@ -6,7 +6,6 @@ import { formatDate } from "date-fns/format";
import { useState } from "react"; import { useState } from "react";
import { useToggle } from "../../hooks/useToggle"; import { useToggle } from "../../hooks/useToggle";
import { pluralizeCount } from "../../lib/pluralize"; import { pluralizeCount } from "../../lib/pluralize";
import { pricingUrl } from "../../lib/pricingUrl";
import { CargoFeature } from "../CargoFeature"; import { CargoFeature } from "../CargoFeature";
import { Button } from "../core/Button"; import { Button } from "../core/Button";
import { Link } from "../core/Link"; import { Link } from "../core/Link";
@@ -49,7 +48,7 @@ function SettingsLicenseCmp() {
<span className="opacity-50">Personal use is always free, forever.</span> <span className="opacity-50">Personal use is always free, forever.</span>
<Separator className="my-2" /> <Separator className="my-2" />
<div className="flex flex-wrap items-center gap-x-2 text-sm text-notice"> <div className="flex flex-wrap items-center gap-x-2 text-sm text-notice">
<Link noUnderline href={pricingUrl(`app.license.learn.${check.data.status}`)}> <Link noUnderline href={`https://yaak.app/pricing?s=learn&t=${check.data.status}`}>
Learn More Learn More
</Link> </Link>
</div> </div>
@@ -69,7 +68,7 @@ function SettingsLicenseCmp() {
</span> </span>
<Separator className="my-2" /> <Separator className="my-2" />
<div className="flex flex-wrap items-center gap-x-2 text-sm text-notice"> <div className="flex flex-wrap items-center gap-x-2 text-sm text-notice">
<Link noUnderline href={pricingUrl(`app.license.learn.${check.data.status}`)}> <Link noUnderline href={`https://yaak.app/pricing?s=learn&t=${check.data.status}`}>
Learn More Learn More
</Link> </Link>
</div> </div>
@@ -135,7 +134,7 @@ function SettingsLicenseCmp() {
<Button <Button
color="secondary" color="secondary"
size="sm" size="sm"
onClick={() => openUrl("https://yaak.app/dashboard?intent=app.license.support")} onClick={() => openUrl("https://yaak.app/dashboard?s=support&ref=app.yaak.desktop")}
rightSlot={<Icon icon="external_link" />} rightSlot={<Icon icon="external_link" />}
> >
Direct Support Direct Support
@@ -151,7 +150,9 @@ function SettingsLicenseCmp() {
color="primary" color="primary"
rightSlot={<Icon icon="external_link" />} rightSlot={<Icon icon="external_link" />}
onClick={() => onClick={() =>
openUrl(pricingUrl(`app.license.purchase.${check.data?.status ?? "unknown"}`)) openUrl(
`https://yaak.app/pricing?s=purchase&ref=app.yaak.desktop&t=${check.data?.status ?? ""}`,
)
} }
> >
Purchase License Purchase License
@@ -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
@@ -2,7 +2,6 @@ import { patchModel, settingsAtom } from "@yaakapp-internal/models";
import type { ProxySetting } from "@yaakapp-internal/models"; import type { ProxySetting } from "@yaakapp-internal/models";
import { Heading, InlineCode, VStack } from "@yaakapp-internal/ui"; import { Heading, InlineCode, VStack } from "@yaakapp-internal/ui";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { CommercialUseBanner } from "../CommercialUseBanner";
import { import {
SettingRowBoolean, SettingRowBoolean,
SettingRowSelect, SettingRowSelect,
@@ -34,7 +33,6 @@ export function SettingsProxy() {
traffic, or routing through specific infrastructure. traffic, or routing through specific infrastructure.
</p> </p>
</div> </div>
<CommercialUseBanner source="proxy-settings" title="Using a proxy for work?" />
<SettingsList className="space-y-8"> <SettingsList className="space-y-8">
<SettingsSection title="Proxy"> <SettingsSection title="Proxy">
<SettingRowSelect <SettingRowSelect
@@ -56,7 +54,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 +97,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-sm overflow-x-auto" className="mt-4 w-full bg-surface p-3 border border-dashed border-border-subtle rounded 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"} />
@@ -7,7 +7,6 @@ import { useExportData } from "../hooks/useExportData";
import { appInfo } from "../lib/appInfo"; import { appInfo } from "../lib/appInfo";
import { showDialog } from "../lib/dialog"; import { showDialog } from "../lib/dialog";
import { importData } from "../lib/importData"; import { importData } from "../lib/importData";
import { pricingUrl } from "../lib/pricingUrl";
import type { DropdownRef } from "./core/Dropdown"; import type { DropdownRef } from "./core/Dropdown";
import { Dropdown } from "./core/Dropdown"; import { Dropdown } from "./core/Dropdown";
import { Icon } from "@yaakapp-internal/ui"; import { Icon } from "@yaakapp-internal/ui";
@@ -77,8 +76,7 @@ export function SettingsDropdown() {
hidden: check.data == null || check.data.status === "active", hidden: check.data == null || check.data.status === "active",
leftSlot: <Icon icon="circle_dollar_sign" />, leftSlot: <Icon icon="circle_dollar_sign" />,
rightSlot: <Icon icon="external_link" color="success" className="opacity-60" />, rightSlot: <Icon icon="external_link" color="success" className="opacity-60" />,
onSelect: () => onSelect: () => openUrl("https://yaak.app/pricing"),
openUrl(pricingUrl(`app.menu.purchase.${check.data?.status ?? "unknown"}`)),
}, },
{ {
label: "Install CLI", label: "Install CLI",
+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-sm align-middle focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-info" className="max-w-full rounded align-middle focus-visible:outline 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-40"> <div className="relative w-full max-h-[10rem]">
<InlineCode <InlineCode
className={classNames( className={classNames(
"block whitespace-pre-wrap select-text! cursor-text max-h-40 overflow-auto hide-scrollbars border-text-subtlest!", "block whitespace-pre-wrap !select-text cursor-text max-h-[10rem] 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 grow gap-2 *:flex-1"> <div className="flex justify-stretch w-full flex-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-240", className: "h-[60rem]",
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"
@@ -21,7 +21,6 @@ import { useRequestUpdateKey } from "../hooks/useRequestUpdateKey";
import { deepEqualAtom } from "../lib/atoms"; import { deepEqualAtom } from "../lib/atoms";
import { languageFromContentType } from "../lib/contentType"; import { languageFromContentType } from "../lib/contentType";
import { generateId } from "../lib/generateId"; import { generateId } from "../lib/generateId";
import { extractPathPlaceholders } from "../lib/pathPlaceholders";
import { prepareImportQuerystring } from "../lib/prepareImportQuerystring"; import { prepareImportQuerystring } from "../lib/prepareImportQuerystring";
import { resolvedModelName } from "../lib/resolvedModelName"; import { resolvedModelName } from "../lib/resolvedModelName";
import { CountBadge } from "./core/CountBadge"; import { CountBadge } from "./core/CountBadge";
@@ -84,7 +83,9 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque
); );
const { urlParameterPairs, urlParametersKey } = useMemo(() => { const { urlParameterPairs, urlParametersKey } = useMemo(() => {
const placeholderNames = extractPathPlaceholders(activeRequest.url); const placeholderNames = Array.from(activeRequest.url.matchAll(/\/(:[^/]+)/g)).map(
(m) => m[1] ?? "",
);
const nonEmptyParameters = activeRequest.urlParameters.filter((p) => p.name || p.value); const nonEmptyParameters = activeRequest.urlParameters.filter((p) => p.name || p.value);
const items: Pair[] = [...nonEmptyParameters]; const items: Pair[] = [...nonEmptyParameters];
for (const name of placeholderNames) { for (const name of placeholderNames) {
@@ -217,7 +218,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 +237,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 +284,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 })}
@@ -105,18 +105,10 @@ function WebsocketEventRow({
: ""; : "";
const iconColor = const iconColor =
messageType === "error" messageType === "close" || messageType === "open" ? "secondary" : isServer ? "info" : "primary";
? "warning"
: messageType === "close" || messageType === "open"
? "secondary"
: isServer
? "info"
: "primary";
const icon = const icon =
messageType === "error" messageType === "close" || messageType === "open"
? "alert_triangle"
: messageType === "close" || messageType === "open"
? "info" ? "info"
: isServer : isServer
? "arrow_big_down_dash" ? "arrow_big_down_dash"
@@ -127,8 +119,6 @@ function WebsocketEventRow({
"Disconnected from server" "Disconnected from server"
) : messageType === "open" ? ( ) : messageType === "open" ? (
"Connected to server" "Connected to server"
) : messageType === "error" ? (
<span className="text-warning">{message}</span>
) : message === "" ? ( ) : message === "" ? (
<em className="italic text-text-subtlest">No content</em> <em className="italic text-text-subtlest">No content</em>
) : ( ) : (
@@ -180,8 +170,6 @@ function WebsocketEventDetail({
? "Connection Closed" ? "Connection Closed"
: event.messageType === "open" : event.messageType === "open"
? "Connection Open" ? "Connection Open"
: event.messageType === "error"
? "WebSocket Error"
: `Message ${event.isServer ? "Received" : "Sent"}`; : `Message ${event.isServer ? "Received" : "Sent"}`;
const actions: EventDetailAction[] = const actions: EventDetailAction[] =
+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-px h-px opacity-20" className="absolute left-0 right-0 -bottom-[1px] h-[1px] 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-120"> <Banner color="warning" className="max-w-[30rem]">
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 (
@@ -112,9 +112,7 @@ export function WorkspaceSettingsDialog({ workspaceId, hide, tab }: Props) {
onCreateNewWorkspace={hide} onCreateNewWorkspace={hide}
onChange={({ filePath }) => patchModel(workspaceMeta, { settingSyncDir: filePath })} onChange={({ filePath }) => patchModel(workspaceMeta, { settingSyncDir: filePath })}
/> />
<div className="mt-4">
<WorkspaceEncryptionSetting layout="settings" size="xs" /> <WorkspaceEncryptionSetting layout="settings" size="xs" />
</div>
</SettingsSection> </SettingsSection>
<ModelSettingsEditor model={workspace} showSectionTitles /> <ModelSettingsEditor model={workspace} showSectionTitles />
</SettingsList> </SettingsList>
@@ -127,7 +125,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 +159,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 +180,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-200!", className: "h-[calc(100vh-5rem)] !max-h-[50rem]",
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-hidden" className="h-full w-full overflow-y-auto focus:outline-none"
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 shrink-0 border border-border", "appearance-none flex-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-sm outline-hidden ring-0", "rounded outline-none 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-sm mb-0.5 px-1 ml-1 h-4 font-mono", "opacity-70 border text-4xs rounded 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-hidden opacity-70"> <summary className="!cursor-default !select-none list-none flex items-center gap-3 focus:outline-none 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-40", "min-h-[10rem]",
"max-w-[calc(100vw-5rem)] max-h-[calc(100vh-5rem)]", "max-w-[calc(100vw-5rem)] max-h-[calc(100vh-5rem)]",
size === "sm" && "w-120", size === "sm" && "w-[30rem]",
size === "md" && "w-200", size === "md" && "w-[50rem]",
size === "lg" && "w-280", size === "lg" && "w-[70rem]",
size === "full" && "w-screen h-screen", size === "full" && "w-[100vw] h-[100vh]",
size === "dynamic" && "min-w-80 max-w-[100vw]", size === "dynamic" && "min-w-[20rem] max-w-[100vw]",
)} )}
> >
{title ? ( {title ? (
@@ -1,111 +1,57 @@
import type { Color } from "@yaakapp-internal/plugins"; 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, HStack } from "@yaakapp-internal/ui";
import classNames from "classnames"; import classNames from "classnames";
import type { MouseEvent } from "react";
import { useEffect } from "react";
import { useKeyValue } from "../../hooks/useKeyValue"; import { useKeyValue } from "../../hooks/useKeyValue";
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,
onShow,
actions, actions,
...props ...props
}: BannerProps & { }: BannerProps & {
id: string; id: string;
size?: DismissibleBannerSize; actions?: { label: string; onClick: () => void; color?: Color }[];
onDismiss?: () => void | Promise<void>;
onShow?: () => void | Promise<void>;
actions?: {
label: string;
onClick: () => void;
color?: Color;
variant?: ButtonProps["variant"];
}[];
}) { }) {
const { const { set: setDismissed, value: dismissed } = useKeyValue<boolean>({
isLoading,
set: setDismissed,
value: dismissed,
} = useKeyValue<boolean>({
namespace: "global", namespace: "global",
key: ["dismiss-banner", id], key: ["dismiss-banner", id],
fallback: false, fallback: false,
}); });
const shouldShow = !isLoading && !dismissed; if (dismissed) return null;
useEffect(() => {
if (shouldShow) {
Promise.resolve(onShow?.()).catch(console.error);
}
}, [onShow, shouldShow]);
if (!shouldShow) return null;
const actionSize: ButtonProps["size"] = size === "xs" ? "2xs" : "xs";
const stopParentClick = (event: MouseEvent) => {
event.preventDefault();
event.stopPropagation();
};
return ( return (
<Banner <Banner
className={classNames( className={classNames(className, "relative grid grid-cols-[1fr_auto] gap-3")}
className,
"relative",
size === "xs" && "!px-2 !py-2 text-xs",
)}
{...props} {...props}
>
<div className="@container">
<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"> <HStack space={1.5}>
<Button
variant="border"
color={props.color}
size={actionSize}
onClick={(event) => {
stopParentClick(event);
setDismissed(true).catch(console.error);
Promise.resolve(onDismiss?.()).catch(console.error);
}}
title="Dismiss message"
>
Dismiss
</Button>
{actions?.map((a) => ( {actions?.map((a) => (
<Button <Button
key={a.label} key={a.label}
variant={a.variant ?? "border"} variant="border"
color={a.color ?? props.color} color={a.color ?? props.color}
size={actionSize} size="xs"
onClick={(event) => { onClick={a.onClick}
stopParentClick(event);
a.onClick();
}}
title={a.label} title={a.label}
> >
{a.label} {a.label}
</Button> </Button>
))} ))}
</div> <Button
</div> variant="border"
</div> color={props.color}
size="xs"
onClick={() => setDismissed((d) => !d)}
title="Dismiss message"
>
Dismiss
</Button>
</HStack>
</Banner> </Banner>
); );
} }
+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-hidden my-1 pointer-events-auto z-40", "outline-none 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-sm font-mono h-xs" className="pb-0.5 px-1.5 mb-2 text-sm border border-border-subtle mx-2 rounded 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-32 outline-hidden px-2 mx-1.5 flex whitespace-nowrap", "min-w-[8rem] outline-none px-2 mx-1.5 flex whitespace-nowrap",
"focus:bg-surface-highlight focus:text rounded-sm focus:outline-hidden focus-visible:outline-1", "focus:bg-surface-highlight focus:text rounded focus:outline-none focus-visible:outline-1",
isParentOfActiveSubmenu && "bg-surface-highlight text rounded-sm", isParentOfActiveSubmenu && "bg-surface-highlight text rounded",
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-20")}>{item.label}</div> <div className={classNames("truncate min-w-[5rem]")}>{item.label}</div>
</Button> </Button>
); );
} }
@@ -1,5 +1,3 @@
@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;
@@ -11,7 +9,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-sm cursor-default; @apply bg-none bg-surface border border-border py-1 mx-0.5 text-text opacity-80 hover:opacity-100 rounded cursor-default;
} }
} }
@@ -21,21 +19,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-sm; @apply rounded-t;
} }
/* 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-sm; @apply rounded-b;
} }
} }
/* 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!; @apply h-auto relative !important;
position: relative !important; position: relative !important;
} }
.cm-scroller { .cm-scroller {
@apply overflow-visible!; @apply overflow-visible !important;
} }
} }
@@ -1,5 +1,3 @@
@reference "../../../main.css";
.cm-wrapper { .cm-wrapper {
@apply h-full overflow-hidden; @apply h-full overflow-hidden;
@@ -9,7 +7,7 @@
/* Regular cursor */ /* Regular cursor */
.cm-cursor { .cm-cursor {
@apply border-text!; @apply border-text !important;
/* Widen the cursor a bit */ /* Widen the cursor a bit */
@apply border-l-[2px]; @apply border-l-[2px];
} }
@@ -17,8 +15,8 @@
/* Vim-mode cursor */ /* Vim-mode cursor */
.cm-fat-cursor { .cm-fat-cursor {
@apply outline-0! bg-text!; @apply outline-0 bg-text !important;
@apply text-surface!; @apply text-surface !important;
} }
/* Matching bracket */ /* Matching bracket */
@@ -61,12 +59,12 @@
* { * {
@apply cursor-text; @apply cursor-text;
@apply caret-transparent!; @apply caret-transparent !important;
} }
} }
.cm-selectionBackground { .cm-selectionBackground {
@apply bg-selection!; @apply bg-selection !important;
} }
/* Fix WebKit/WKWebView rendering bug where selection layer leaves a ghost /* Fix WebKit/WKWebView rendering bug where selection layer leaves a ghost
@@ -90,7 +88,7 @@
} }
.cm-gutter-lint { .cm-gutter-lint {
@apply w-auto!; @apply w-auto !important;
.cm-gutterElement { .cm-gutterElement {
@apply px-0; @apply px-0;
@@ -113,7 +111,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-sm dark:shadow; @apply inline border px-1 mx-[0.5px] rounded dark:shadow;
-webkit-text-security: none; -webkit-text-security: none;
@@ -164,7 +162,7 @@
&::-webkit-scrollbar-corner, &::-webkit-scrollbar-corner,
&::-webkit-scrollbar { &::-webkit-scrollbar {
@apply hidden!; @apply hidden !important;
} }
} }
} }
@@ -191,16 +189,16 @@
/* Style search matches */ /* Style search matches */
.cm-searchMatch { .cm-searchMatch {
@apply bg-transparent!; @apply bg-transparent !important;
@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!; @apply bg-text !important;
&, &,
* { * {
@apply text-surface! font-semibold!; @apply text-surface font-semibold !important;
} }
} }
} }
@@ -225,8 +223,8 @@
} }
.cm-editor .fold-gutter-icon { .cm-editor .fold-gutter-icon {
@apply pt-[0.25em] pl-[0.4em] px-[0.4em] h-4 rounded-sm; @apply pt-[0.25em] pl-[0.4em] px-[0.4em] h-4 rounded;
@apply cursor-default!; @apply cursor-default !important;
} }
.cm-editor .fold-gutter-icon::after { .cm-editor .fold-gutter-icon::after {
@@ -250,7 +248,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!; @apply cursor-default !important;
} }
.cm-editor .cm-activeLineGutter { .cm-editor .cm-activeLineGutter {
@@ -279,7 +277,7 @@
} }
.cm-tooltip-lint { .cm-tooltip-lint {
@apply font-mono! text-editor! rounded-sm! overflow-hidden! bg-surface-highlight! border! border-border! shadow!; @apply font-mono text-editor rounded overflow-hidden bg-surface-highlight border border-border shadow !important;
.cm-diagnostic-error { .cm-diagnostic-error {
@apply border-l-danger px-4 py-2; @apply border-l-danger px-4 py-2;
@@ -295,18 +293,18 @@
} }
.cm-tooltip.cm-tooltip-hover { .cm-tooltip.cm-tooltip-hover {
@apply shadow-lg bg-surface rounded-sm text-text-subtle border border-border-subtle z-50 pointer-events-auto text-sm; @apply shadow-lg bg-surface rounded 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-sm; @apply text-text hover:bg-surface-highlight w-full h-sm flex items-center px-2 rounded;
} }
a { a {
@apply cursor-default!; @apply cursor-default !important;
&::after { &::after {
@apply text-text bg-text h-3 w-3 ml-1; @apply text-text bg-text h-3 w-3 ml-1;
@@ -321,10 +319,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-sm text-text-subtle border border-border-subtle z-50 pointer-events-auto; @apply shadow-lg bg-surface rounded text-text-subtle border border-border-subtle z-50 pointer-events-auto;
& * { & * {
@apply font-mono! text-editor!; @apply font-mono text-editor !important;
} }
.cm-completionIcon { .cm-completionIcon {
@@ -411,7 +409,7 @@
} }
.cm-completionIcon { .cm-completionIcon {
@apply text-sm flex items-center pb-0.5 shrink-0; @apply text-sm flex items-center pb-0.5 flex-shrink-0;
} }
.cm-completionLabel { .cm-completionLabel {
@@ -429,7 +427,7 @@
input, input,
button { button {
@apply rounded-sm outline-hidden; @apply rounded-sm outline-none;
} }
button { button {
@@ -438,12 +436,12 @@
} }
button[name="close"] { button[name="close"] {
@apply text-text-subtle! hocus:text-text! px-2! -mr-1.5!; @apply text-text-subtle hocus:text-text px-2 -mr-1.5 !important;
} }
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-hidden; @apply border outline-none;
} }
input.cm-textfield { input.cm-textfield {
@@ -282,22 +282,6 @@ function EditorInner({
[disableTabIndent], [disableTabIndent],
); );
// Update read-only
const readOnlyCompartment = useRef(new Compartment());
useEffect(
function configureReadOnly() {
if (cm.current === null) return;
const current = readOnlyCompartment.current.get(cm.current.view.state) ?? emptyExtension;
const next = readOnly ? readonlyExtensions : emptyExtension;
// PERF: This is expensive with hundreds of editors on screen, so only do it when necessary
if (current === next) return;
const effects = readOnlyCompartment.current.reconfigure(next);
cm.current?.view.dispatch({ effects });
},
[readOnly],
);
const onClickFunction = useCallback( const onClickFunction = useCallback(
async (fn: TemplateFunction, tagValue: string, startPos: number) => { async (fn: TemplateFunction, tagValue: string, startPos: number) => {
const show = () => { const show = () => {
@@ -410,9 +394,9 @@ function EditorInner({
keymapCompartment.current.of( keymapCompartment.current.of(
keymapExtensions[settings.editorKeymap] ?? keymapExtensions.default, keymapExtensions[settings.editorKeymap] ?? keymapExtensions.default,
), ),
readOnlyCompartment.current.of(readOnly ? readonlyExtensions : emptyExtension),
...getExtensions({ ...getExtensions({
container, container,
readOnly,
singleLine, singleLine,
hideGutter, hideGutter,
stateKey, stateKey,
@@ -486,7 +470,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) {
@@ -569,6 +553,7 @@ function EditorInner({
function getExtensions({ function getExtensions({
stateKey, stateKey,
container, container,
readOnly,
singleLine, singleLine,
hideGutter, hideGutter,
onChange, onChange,
@@ -577,7 +562,7 @@ function getExtensions({
onFocus, onFocus,
onBlur, onBlur,
onKeyDown, onKeyDown,
}: Pick<EditorProps, "singleLine" | "hideGutter"> & { }: Pick<EditorProps, "singleLine" | "readOnly" | "hideGutter"> & {
stateKey: EditorProps["stateKey"]; stateKey: EditorProps["stateKey"];
container: HTMLDivElement | null; container: HTMLDivElement | null;
onChange: RefObject<EditorProps["onChange"]>; onChange: RefObject<EditorProps["onChange"]>;
@@ -627,6 +612,7 @@ function getExtensions({
keymap.of(singleLine ? defaultKeymap.filter((k) => k.key !== "Enter") : defaultKeymap), keymap.of(singleLine ? defaultKeymap.filter((k) => k.key !== "Enter") : defaultKeymap),
...(singleLine ? [singleLineExtensions()] : []), ...(singleLine ? [singleLineExtensions()] : []),
...(!singleLine ? multiLineExtensions({ hideGutter }) : []), ...(!singleLine ? multiLineExtensions({ hideGutter }) : []),
...(readOnly ? readonlyExtensions : []),
// ------------------------ // // ------------------------ //
// Things that must be last // // Things that must be last //
@@ -53,17 +53,19 @@ function pathParameters(
if (node.name === "Text") { if (node.name === "Text") {
// Find the `url` node and then jump into it to find the placeholders // Find the `url` node and then jump into it to find the placeholders
for (let i = node.from; i < node.to; i++) { for (let i = node.from; i < node.to; i++) {
const innerTree = tree.resolveInner(i); const innerTree = syntaxTree(view.state).resolveInner(i);
if (innerTree.node.name === "url") { if (innerTree.node.name === "url") {
innerTree.node.cursor().iterate((node) => { innerTree.toTree().iterate({
enter(node) {
if (node.name !== "Placeholder") return; if (node.name !== "Placeholder") return;
const globalFrom = node.from; const globalFrom = innerTree.node.from + node.from;
const globalTo = node.to; const globalTo = innerTree.node.from + node.to;
const rawText = view.state.doc.sliceString(globalFrom, globalTo); const rawText = view.state.doc.sliceString(globalFrom, globalTo);
const onClick = () => onClickPathParameter(rawText); const onClick = () => onClickPathParameter(rawText);
const widget = new PathPlaceholderWidget(rawText, globalFrom, onClick); const widget = new PathPlaceholderWidget(rawText, globalFrom, onClick);
const deco = Decoration.replace({ widget, inclusive: false }); const deco = Decoration.replace({ widget, inclusive: false });
widgets.push(deco.range(globalFrom, globalTo)); widgets.push(deco.range(globalFrom, globalTo));
},
}); });
break; break;
} }
@@ -1,13 +1,6 @@
// Host is optional so URLs starting with `/` go straight to Path. Without this, @top url { Protocol? Host Path? Query? }
// the parser error-recovers past the leading `/` and consumes the first segment as
// Host (since Host's char class includes `:` for `host:port`), eating an initial
// `:name` placeholder like `/:foo/:bar`.
@top url { Protocol? Host? Path? Query? }
Path { ("/" PathSegment)+ } Path { ("/" (Placeholder | PathSegment))+ }
Placeholder { ":" pathChars }
PathSegment { Placeholder (":" pathChars)* | pathChars (":" pathChars)* }
Query { "?" queryPair ("&" queryPair)* } Query { "?" queryPair ("&" queryPair)* }
@@ -16,7 +9,9 @@ Query { "?" queryPair ("&" queryPair)* }
Host { $[a-zA-Z0-9-_.:\[\]]+ } Host { $[a-zA-Z0-9-_.:\[\]]+ }
@precedence { Protocol, Host } @precedence { Protocol, Host }
pathChars { ![/?#:]+ } Placeholder { ":" ![/?#]+ }
PathSegment { ![?#/]+ }
@precedence { Placeholder, PathSegment }
queryPair { ($[a-zA-Z0-9]+ ("=" $[a-zA-Z0-9]*)?) } queryPair { ($[a-zA-Z0-9]+ ("=" $[a-zA-Z0-9]*)?) }
} }
@@ -1,9 +1,9 @@
// This file was generated by lezer-generator. You probably shouldn't edit it. // This file was generated by lezer-generator. You probably shouldn't edit it.
export const export const url = 1,
url = 1,
Protocol = 2, Protocol = 2,
Host = 3, Host = 3,
Path = 4, Port = 4,
PathSegment = 5, Path = 5,
Placeholder = 6, Placeholder = 6,
Query = 7 PathSegment = 7,
Query = 8;
@@ -1,52 +0,0 @@
import { describe, expect, test } from "vite-plus/test";
import { parser } from "./url";
function expectValidParse(input: string) {
expect(parser.parse(input).toString()).not.toContain("⚠");
}
function placeholderValues(input: string): string[] {
const values: string[] = [];
parser
.parse(input)
.cursor()
.iterate((node) => {
if (node.name === "Placeholder") values.push(input.slice(node.from, node.to));
});
return values;
}
describe("URL grammar Placeholder", () => {
test("recognizes path placeholders", () => {
expectValidParse("https://x.com/users/:id");
expect(placeholderValues("https://x.com/users/:id")).toEqual([":id"]);
});
test("treats a colon suffix as literal path text", () => {
expectValidParse("https://yaak.app/x/echo/:foo:bar/baz");
expect(placeholderValues("https://yaak.app/x/echo/:foo:bar/baz")).toEqual([":foo"]);
});
test("treats repeated colon suffixes as literal path text", () => {
expectValidParse("https://yaak.app/x/echo/:foo:bar:baz");
expect(placeholderValues("https://yaak.app/x/echo/:foo:bar:baz")).toEqual([":foo"]);
});
test("does not recognize a colon in the middle of a plain path segment", () => {
expectValidParse("https://yaak.app/x/echo/foo:bar/baz");
expect(placeholderValues("https://yaak.app/x/echo/foo:bar/baz")).toEqual([]);
});
test("does not recognize query parameters as path placeholders", () => {
expect(placeholderValues("https://yaak.app/x/echo/:foo?bar=ss&:bar=baz")).toEqual([":foo"]);
});
test("recognizes placeholders in a path fragment after a templated base URL", () => {
// Mixed Twig parsing can feed the URL parser only the text after a template tag,
// as in `${[ URL ]}/x/:foo/:hello`.
expect(placeholderValues("/x/hi:echo/:foo/:hello?bar=ss&:bar=baz")).toEqual([
":foo",
":hello",
]);
});
});
@@ -1,18 +1,20 @@
// This file was generated by lezer-generator. You probably shouldn't edit it. // This file was generated by lezer-generator. You probably shouldn't edit it.
import {LRParser} from "@lezer/lr" import { LRParser } from "@lezer/lr";
import {highlight} from "./highlight" import { highlight } from "./highlight";
export const parser = LRParser.deserialize({ export const parser = LRParser.deserialize({
version: 14, version: 14,
states: "#xQQOPOOO`OQO'#CdOhOPO'#C`OsOSO'#CcQOOOOOQZOPOOQWOPOOQTOPOOOxOQO'#CbO}OQO'#CaOOOO,59O,59OOOOO-E6b-E6bO!]OPO,58}OOOO,58|,58|O!eOQO'#CeO!jOQO,58{O!xOSO'#CfO!}OPO1G.iOOOO,59P,59POOOO-E6c-E6cOOOO,59Q,59QOOOO-E6d-E6d", states:
stateData: "#Y~OQVORUO[PO_RO~O]WO^XO~O[POZSX_SX~O`[O~O^]O~O]^OZTX[TX_TX~Oa`OZVa~O^bO~O]^OZTa[Ta_Ta~O`dO~Oa`OZVi~OQR~", "!|OQOPOOQYOPOOOTOPOOObOQO'#CdOjOPO'#C`OuOSO'#CcQOOOOOQ]OPOOOOOO,59O,59OOOOO-E6b-E6bOzOPO,58}O!SOSO'#CeO!XOPO1G.iOOOO,59P,59POOOO-E6c-E6c",
goto: "!RZPPPP[adgmu{VTOUVRYPRXPXSOTUVUQOUVRZQQ_XRc_Qa[Rea", stateData: "!g~OQQORPO~OZRO[TO~OTWOUWO~OZROYSX[SX~O]YO~O^ZOYVa~O]]O~O^ZOYVi~OQRTUT~",
nodeNames: "⚠ url Protocol Host Path PathSegment Placeholder Query", goto: "nYPPPPZPP^bhRVPTUPVQSPRXSQ[YR^[",
maxTerm: 17, nodeNames: "⚠ url Protocol Host Path Placeholder PathSegment Query",
maxTerm: 14,
propSources: [highlight], propSources: [highlight],
skippedNodes: [0], skippedNodes: [0],
repeatNodeCount: 3, repeatNodeCount: 2,
tokenData: "+z~RgOs!jtv!jvw#[w}!j}!O#x!O!P#x!P!Q%|!Q![&R![!](g!]!a!j!a!b)Z!b!c!j!c!})`!}#O#x#O#P!j#P#Q#x#Q#R!j#R#S#x#S#T!j#T#o)`#o;'S!j;'S;=`#U<%lO!jQ!oV^QOs!jt!P!j!Q![!j!]!a!j!b;'S!j;'S;=`#U<%lO!jQ#XP;=`<%l!jR#cVaP^QOs!jt!P!j!Q![!j!]!a!j!b;'S!j;'S;=`#U<%lO!jR$Pc^QRPOs!jt}!j}!O#x!O!P#x!Q![#x![!]%[!]!a!j!b!c!j!c!}#x!}#O#x#O#P!j#P#Q#x#Q#R!j#R#S#x#S#T!j#T#o#x#o;'S!j;'S;=`#U<%lO!jP%aXRP}!O%[!O!P%[!Q![%[![!]%[!c!}%[!}#O%[#P#Q%[#R#S%[#T#o%[~&RO[~V&[e^Q`SRPOs!jt}!j}!O#x!O!P#x!Q![&R![!]%[!]!_!j!_!`'m!`!a!j!b!c!j!c!}&R!}#O#x#O#P!j#P#Q#x#Q#R!j#R#S#x#S#T!j#T#o&R#o;'S!j;'S;=`#U<%lO!jU'tZ^Q`SOs!jt!P!j!Q!['m!]!a!j!b!c!j!c!}'m!}#T!j#T#o'm#o;'S!j;'S;=`#U<%lO!jR(nX]QRP}!O%[!O!P%[!Q![%[![!]%[!c!}%[!}#O%[#P#Q%[#R#S%[#T#o%[~)`O_~V)ie^Q`SRPOs!jt}!j}!O#x!O!P#x!Q![&R![!]*z!]!_!j!_!`'m!`!a!j!b!c!j!c!})`!}#O#x#O#P!j#P#Q#x#Q#R!j#R#S#x#S#T!j#T#o)`#o;'S!j;'S;=`#U<%lO!jP+PYRP}!O%[!O!P%[!P!Q+o!Q![%[![!]%[!c!}%[!}#O%[#P#Q%[#R#S%[#T#o%[P+rP!P!Q+uP+zOQP", tokenData:
".i~RgOs!jtv!jvw#Xw}!j}!O#r!O!P#r!P!Q%U!Q![%Z![!]'o!]!a!j!a!b+W!b!c!j!c!}+]!}#O#r#O#P!j#P#Q#r#Q#R!j#R#S#r#S#T!j#T#o+]#o;'S!j;'S;=`#R<%lO!jQ!oUUQOs!jt!P!j!Q!a!j!b;'S!j;'S;=`#R<%lO!jQ#UP;=`<%l!jR#`U^PUQOs!jt!P!j!Q!a!j!b;'S!j;'S;=`#R<%lO!jR#ycRPUQOs!jt}!j}!O#r!O!P#r!Q![#r![!]#r!]!a!j!b!c!j!c!}#r!}#O#r#O#P!j#P#Q#r#Q#R!j#R#S#r#S#T!j#T#o#r#o;'S!j;'S;=`#R<%lO!j~%ZOZ~V%de]SRPUQOs!jt}!j}!O#r!O!P#r!Q![%Z![!]#r!]!_!j!_!`&u!`!a!j!b!c!j!c!}%Z!}#O#r#O#P!j#P#Q#r#Q#R!j#R#S#r#S#T!j#T#o%Z#o;'S!j;'S;=`#R<%lO!jU&|Z]SUQOs!jt!P!j!Q![&u![!a!j!b!c!j!c!}&u!}#T!j#T#o&u#o;'S!j;'S;=`#R<%lO!jR'vcRPUQOs)Rt})R}!O)r!O!P)r!Q![)r![!])r!]!a)R!b!c)R!c!})r!}#O)r#O#P)R#P#Q)r#Q#R)R#R#S)r#S#T)R#T#o)r#o;'S)R;'S;=`)l<%lO)RQ)YUTQUQOs)Rt!P)R!Q!a)R!b;'S)R;'S;=`)l<%lO)RQ)oP;=`<%l)RR){cRPTQUQOs)Rt})R}!O)r!O!P)r!Q![)r![!])r!]!a)R!b!c)R!c!})r!}#O)r#O#P)R#P#Q)r#Q#R)R#R#S)r#S#T)R#T#o)r#o;'S)R;'S;=`)l<%lO)R~+]O[~V+fe]SRPUQOs!jt}!j}!O#r!O!P#r!Q![%Z![!],w!]!_!j!_!`&u!`!a!j!b!c!j!c!}+]!}#O#r#O#P!j#P#Q#r#Q#R!j#R#S#r#S#T!j#T#o+]#o;'S!j;'S;=`#R<%lO!jR-OdRPUQOs!jt}!j}!O#r!O!P#r!P!Q.^!Q![#r![!]#r!]!a!j!b!c!j!c!}#r!}#O#r#O#P!j#P#Q#r#Q#R!j#R#S#r#S#T!j#T#o#r#o;'S!j;'S;=`#R<%lO!jP.aP!P!Q.dP.iOQP",
tokenizers: [0, 1, 2], tokenizers: [0, 1, 2],
topRules: {"url":[0,1]}, topRules: { url: [0, 1] },
tokenPrec: 99 tokenPrec: 63,
}) });
@@ -9,8 +9,6 @@ import { CopyIconButton } from "../CopyIconButton";
import { AutoScroller } from "./AutoScroller"; import { AutoScroller } from "./AutoScroller";
import { Button } from "./Button"; import { Button } from "./Button";
import { IconButton } from "./IconButton"; import { IconButton } from "./IconButton";
import type { SelectProps } from "./Select";
import { Select } from "./Select";
import { Separator } from "./Separator"; import { Separator } from "./Separator";
interface EventViewerProps<T> { interface EventViewerProps<T> {
@@ -153,7 +151,7 @@ export function EventViewer<T>({
layout="vertical" layout="vertical"
storageKey={splitLayoutStorageKey} storageKey={splitLayoutStorageKey}
defaultRatio={defaultRatio} defaultRatio={defaultRatio}
minHeightPx={72} minHeightPx={10}
firstSlot={({ style }) => ( firstSlot={({ style }) => (
<div style={style} className="w-full h-full grid grid-rows-[auto_minmax(0,1fr)]"> <div style={style} className="w-full h-full grid grid-rows-[auto_minmax(0,1fr)]">
{header ?? <span aria-hidden />} {header ?? <span aria-hidden />}
@@ -204,9 +202,7 @@ export function EventViewer<T>({
); );
} }
export type EventDetailAction = export interface EventDetailAction {
| {
type?: "button";
/** Unique key for React */ /** Unique key for React */
key: string; key: string;
/** Button label */ /** Button label */
@@ -216,26 +212,13 @@ export type EventDetailAction =
/** Click handler */ /** Click handler */
onClick: () => void; onClick: () => void;
} }
| {
type: "select";
/** Unique key for React */
key: string;
/** Select label */
label: string;
/** Selected value */
value: string;
/** Select options */
options: SelectProps<string>["options"];
/** Change handler */
onChange: (value: string) => void;
};
interface EventDetailHeaderProps { interface EventDetailHeaderProps {
title: string; title: string;
prefix?: ReactNode; prefix?: ReactNode;
timestamp?: string; timestamp?: string;
actions?: EventDetailAction[]; actions?: EventDetailAction[];
copyText?: string | (() => Promise<string | null>); copyText?: string;
onClose?: () => void; onClose?: () => void;
} }
@@ -256,20 +239,7 @@ export function EventDetailHeader({
<h3 className="font-semibold select-auto cursor-auto truncate">{title}</h3> <h3 className="font-semibold select-auto cursor-auto truncate">{title}</h3>
</HStack> </HStack>
<HStack space={2} className="items-center"> <HStack space={2} className="items-center">
{actions?.map((action) => {actions?.map((action) => (
action.type === "select" ? (
<div key={action.key} className="w-32">
<Select
name={action.key}
label={action.label}
hideLabel
size="xs"
value={action.value}
options={action.options}
onChange={action.onChange}
/>
</div>
) : (
<Button <Button
key={action.key} key={action.key}
type="button" type="button"
@@ -280,15 +250,13 @@ export function EventDetailHeader({
{action.icon} {action.icon}
{action.label} {action.label}
</Button> </Button>
), ))}
)}
{copyText != null && ( {copyText != null && (
<CopyIconButton text={copyText} size="xs" title="Copy" variant="border" iconSize="sm" /> <CopyIconButton text={copyText} size="xs" title="Copy" variant="border" iconSize="sm" />
)} )}
{formattedTime && ( {formattedTime && (
<span className="text-text-subtlest font-mono text-editor ml-2">{formattedTime}</span> <span className="text-text-subtlest font-mono text-editor ml-2">{formattedTime}</span>
)} )}
{onClose != null && (
<div <div
className={classNames( className={classNames(
copyText != null || copyText != null ||
@@ -305,7 +273,6 @@ export function EventDetailHeader({
onClick={onClose} onClick={onClose}
/> />
</div> </div>
)}
</HStack> </HStack>
</div> </div>
); );
@@ -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-hidden focus:text-text rounded-sm", "px-1.5 h-xs font-mono text-editor cursor-default group focus:outline-none focus:text-text rounded",
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-sm bg-surface-highlight px-1 border border-border text-text-subtle", "rounded 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 shrink-0 whitespace-pre", "font-mono flex-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) {
); );
} }
export function formatMillis(ms: number) { 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-hidden placeholder:text-placeholder", "!bg-transparent min-w-0 h-auto w-full focus:outline-none 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-40", "select-none py-0.5 pr-2 h-full max-w-[10rem]",
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-60", "select-none py-0.5 break-all max-w-[15rem]",
align === "top" && "align-top", align === "top" && "align-top",
align === "middle" && "align-middle", align === "middle" && "align-middle",
)} )}
> >
<div className="select-text cursor-text max-h-48 overflow-y-auto grid grid-cols-[auto_minmax(0,1fr)_auto]"> <div className="select-text cursor-text max-h-[12rem] 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",
"shrink-0 text-sm", "flex-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-screen max-w-160 h-[50vh] max-h-full grid grid-rows-[minmax(0,1fr)_auto]"> <div className="w-[100vw] max-w-[40rem] 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}
/> />
); );
@@ -1,6 +1,6 @@
import { HStack } from "@yaakapp-internal/ui"; import { HStack } from "@yaakapp-internal/ui";
import classNames from "classnames"; import classNames from "classnames";
import type { FocusEvent, InputHTMLAttributes, ReactNode } from "react"; import type { FocusEvent, HTMLAttributes, ReactNode } from "react";
import { import {
forwardRef, forwardRef,
useCallback, useCallback,
@@ -28,9 +28,10 @@ export type PlainInputProps = Omit<
| "extraExtensions" | "extraExtensions"
| "forcedEnvironmentId" | "forcedEnvironmentId"
> & > &
Pick<InputHTMLAttributes<HTMLInputElement>, "inputMode" | "onKeyDownCapture" | "step"> & { Pick<HTMLAttributes<HTMLInputElement>, "onKeyDownCapture"> & {
onFocusRaw?: InputHTMLAttributes<HTMLInputElement>["onFocus"]; onFocusRaw?: HTMLAttributes<HTMLInputElement>["onFocus"];
type?: "text" | "password" | "number"; type?: "text" | "password" | "number";
step?: number;
hideObscureToggle?: boolean; hideObscureToggle?: boolean;
labelRightSlot?: ReactNode; labelRightSlot?: ReactNode;
}; };
@@ -51,7 +52,6 @@ export const PlainInput = forwardRef<{ focus: () => void }, PlainInputProps>(fun
labelClassName, labelClassName,
labelPosition = "top", labelPosition = "top",
labelRightSlot, labelRightSlot,
inputMode,
leftSlot, leftSlot,
name, name,
onBlur, onBlur,
@@ -64,7 +64,6 @@ export const PlainInput = forwardRef<{ focus: () => void }, PlainInputProps>(fun
required, required,
rightSlot, rightSlot,
size = "md", size = "md",
step,
tint, tint,
type = "text", type = "text",
validate, validate,
@@ -116,7 +115,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-hidden placeholder:text-placeholder", "!bg-transparent min-w-0 w-full focus:outline-none placeholder:text-placeholder",
"px-2 text-xs font-mono cursor-text", "px-2 text-xs font-mono cursor-text",
); );
@@ -167,7 +166,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",
@@ -205,14 +204,12 @@ export const PlainInput = forwardRef<{ focus: () => void }, PlainInputProps>(fun
autoComplete="off" autoComplete="off"
autoCapitalize="off" autoCapitalize="off"
autoCorrect="off" autoCorrect="off"
inputMode={inputMode}
onChange={(e) => handleChange(e.target.value)} onChange={(e) => handleChange(e.target.value)}
onPaste={(e) => onPaste?.(e.clipboardData.getData("Text"))} onPaste={(e) => onPaste?.(e.clipboardData.getData("Text"))}
className={classNames(commonClassName, "h-full disabled:opacity-disabled")} className={classNames(commonClassName, "h-full disabled:opacity-disabled")}
onFocus={handleFocus} onFocus={handleFocus}
onBlur={handleBlur} onBlur={handleBlur}
required={required} required={required}
step={step}
placeholder={placeholder} placeholder={placeholder}
onKeyDownCapture={onKeyDownCapture} onKeyDownCapture={onKeyDownCapture}
/> />
@@ -225,7 +222,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 shrink-0 rounded-full border", "mt-1 w-4 h-4 flex-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-hidden bg-transparent disabled:opacity-disabled", "pr-7 w-full outline-none bg-transparent disabled:opacity-disabled",
"leading-none rounded-none", // Center the text better vertically "leading-[1] 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;
+2 -4
View File
@@ -1,16 +1,14 @@
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({ className, contentLength, contentLengthCompressed }: Props) { export function SizeTag({ contentLength, contentLengthCompressed }: Props) {
return ( return (
<span <span
className={classNames("font-mono", className)} className="font-mono"
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 shrink-0 w-full", layout === "vertical" && "flex flex-row flex-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-sm whitespace-nowrap", "flex items-center rounded whitespace-nowrap",
"px-2! ml-px", "!px-2 ml-[1px]",
"outline-hidden", "outline-none",
"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-40", layout === "horizontal" && "min-w-[10rem]",
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-100", "border border-border shadow-lg w-[25rem]",
)} )}
> >
<div className="pl-3 py-3 pr-10 flex items-start gap-2 w-full max-h-44 overflow-auto"> <div className="pl-3 py-3 pr-10 flex items-start gap-2 w-full max-h-[11rem] overflow-auto">
{toastIcon && <Icon icon={toastIcon} color={color} className="mt-1 shrink-0" />} {toastIcon && <Icon icon={toastIcon} color={color} className="mt-1 flex-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, "grow-0 flex items-center")} className={classNames(className, "flex-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-2 w-[0.8rem]", "absolute z-50 left-[calc(50%-0.4rem)] h-[0.5rem] w-[0.8rem]",
isBottom isBottom
? "border-t-2 border-surface-highlight -bottom-[calc(0.5rem-3px)] mb-2" ? "border-t-[2px] border-surface-highlight -bottom-[calc(0.5rem-3px)] mb-2"
: "border-b-2 border-surface-highlight -top-[calc(0.5rem-3px)] mt-2", : "border-b-[2px] 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-sm px-2 py-1.5", "w-full min-w-0 text-left rounded px-2 py-1.5",
selected && "bg-surface-active", selected && "bg-surface-active",
)} )}
onClick={onSelect} onClick={onSelect}

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