feat(ui): ask self-hosted users to star Paperclip on GitHub after 5 done tasks (#20453)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Many teams run Paperclip self-hosted, so the project grows when
those users tell others about it
> - A GitHub star is the cheapest way for a user to do that, but the app
never asks
> - Asking too early is noise. A user who has finished a few tasks knows
whether Paperclip helps them
> - This pull request adds a one-time, dismissable modal that asks
self-hosted users for a star after their company has 5 done tasks
> - The benefit is more visibility for the project, and no repeated
prompt for the user

## Linked Issues or Issue Description

**Subsystem affected**

Board UI (dialogs shown in the main app shell).

**Problem or motivation**

Self-hosted users have no prompt to star the repository. Users who like
Paperclip often do not think of it. A prompt at the right moment helps
other teams find the project.

**Proposed solution**

Show a small modal once the selected company has 5 tasks with status
"done". The modal shows the Paperclip mark, the headline "You completed
5 tasks!", a short body, a "Star on GitHub" button and a "Not now"
button. It shows only once per browser. Closing it in any way counts as
seen.

**Alternatives considered**

A permanent banner or sidebar link. Both stay on screen after the user
decides, so they cost attention every day. A one-time modal costs
attention once.

**Roadmap alignment**

This is a small UI addition. It does not overlap with planned core work
in ROADMAP.md.

No related public issues or PRs found (searched for "star", "github
star").

## What Changed

- Added `GitHubStarPrompt` (`ui/src/components/GitHubStarPrompt.tsx`).
It reads the health endpoint and shows only on self-hosted instances
(never on Cloud), never during onboarding, and only after 5 done tasks
in the selected company. It waits for any other open dialog to close. It
stores a "seen" flag in `localStorage` when it opens, so a reload does
not show it again.
- Mounted the prompt in `BoardDialogs`.
- Exported the modal-detection helpers (`MODAL_SELECTOR`,
`useModalOpen`) from `AnnouncementWell` so both components use the same
rule.
- Added `PaperclipMark`, the mark alone from the existing lockup
geometry. The lockup and the mark now share one path constant.
- Added the `issues.starPromptDone` query key.
- Added tests for when the prompt opens and when it stays hidden.

## Verification

- `pnpm --filter @paperclipai/ui exec vitest run
src/components/GitHubStarPrompt.test.tsx
src/components/AnnouncementWell.test.tsx`
- `pnpm check:token-gates` (all gates clean)
- `pnpm typecheck`
- Manual: on a self-hosted dev instance, mark 5 tasks in one company as
done. The modal opens. "Star on GitHub" opens
github.com/paperclipai/paperclip in a new tab. Reload: the modal does
not open again. To see it again, delete the
`paperclip.github-star-prompt.seen` key from `localStorage`.

## Risks

- Low risk. The change is UI only and additive.
- The prompt runs one small issues query (limit 5) while it is still
eligible. After it is seen, the query is disabled.
- If `localStorage` is not available, the prompt treats itself as
already seen and does not show.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- Claude Opus 5.5 (Anthropic), model id `claude-opus-5-5`, run through
Claude Code with tool use (shell, file edits). Standard context window;
extended thinking at low effort.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: squadbot <squadbot@squadbots-Mac-mini.local>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-11-09 21:11:29 -08:00
1 parent 2c2186706e
commit a5e01634f6
8 files changed
+245 -7

No files matched your search

+6 -6
View File
@@ -13,8 +13,8 @@ import { queryKeys } from "@/lib/queryKeys";
import { AnnouncementCard } from "./AnnouncementCard";
// Includes the command palette, sheets and dialogs created outside DialogContext.
const MODAL_SELECTOR = '[role="dialog"][data-state="open"], [role="alertdialog"][data-state="open"], [aria-modal="true"]:not([data-state="closed"]), dialog[open]';
function useModalOpen(enabled: boolean) {
export const MODAL_SELECTOR = '[role="dialog"][data-state="open"], [role="alertdialog"][data-state="open"], [aria-modal="true"]:not([data-state="closed"]), dialog[open]';
export function useModalOpen(enabled: boolean) {
const [open, setOpen] = useState(false);
useEffect(() => {
if (!enabled) return;
@@ -30,11 +30,11 @@ function useModalOpen(enabled: boolean) {
const FINISHED_ONBOARDING_STATUSES = new Set(["done", "cancelled"]);
/**
* A new user's first session is the onboarding first task. Announcements wait
* until that task is finished so they cannot cover its questions. Unknown
* state (loading or a failed read) keeps the optional card hidden.
* A new user's first session is the onboarding first task. Announcements and
* other optional prompts wait until that task is finished so they cannot cover
* its questions. Unknown state (loading or a failed read) keeps them hidden.
*/
function useOnboardingInProgress(companyId: string | null) {
export function useOnboardingInProgress(companyId: string | null) {
const query = useQuery({
queryKey: queryKeys.issues.onboardingFirstTask(companyId ?? "__none__"),
queryFn: ({ signal }) => issuesApi.list(companyId!, { originKind: ONBOARDING_FIRST_TASK_ORIGIN_KIND, limit: 1 }, { signal }),
+1
View File
@@ -11,6 +11,7 @@ vi.mock("./NewIssueDialog", () => ({ NewIssueDialog: () => <input aria-label="Ta
vi.mock("./NewProjectDialog", () => ({ NewProjectDialog: () => <div>Project dialog</div> }));
vi.mock("./NewGoalDialog", () => ({ NewGoalDialog: () => <div>Goal dialog</div> }));
vi.mock("./NewAgentDialog", () => ({ NewAgentDialog: () => <div>Agent dialog</div> }));
vi.mock("./GitHubStarPrompt", () => ({ GitHubStarPrompt: () => null }));
let container: HTMLDivElement;
let root: ReturnType<typeof createRoot>;
beforeEach(() => {
+2
View File
@@ -1,6 +1,7 @@
import { lazy } from "react";
import { useDialogActions, useDialogState } from "../context/DialogContext";
import { DeferredOverlay } from "./DeferredOverlay";
import { GitHubStarPrompt } from "./GitHubStarPrompt";
import { NewIssueDialog } from "./NewIssueDialog";
const NewProjectDialog = lazy(() => import("./NewProjectDialog").then((module) => ({ default: module.NewProjectDialog })));
@@ -15,5 +16,6 @@ export function BoardDialogs() {
<DeferredOverlay requested={newProjectOpen} title="New project" onClose={closeNewProject}><NewProjectDialog /></DeferredOverlay>
<DeferredOverlay requested={newGoalOpen} title="New goal" onClose={closeNewGoal}><NewGoalDialog /></DeferredOverlay>
<DeferredOverlay requested={newAgentOpen} title="New agent" onClose={closeNewAgent}><NewAgentDialog /></DeferredOverlay>
<GitHubStarPrompt />
</>;
}
+109
View File
@@ -0,0 +1,109 @@
// @vitest-environment jsdom
import { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { ONBOARDING_FIRST_TASK_ORIGIN_KIND } from "@paperclipai/shared";
import type { HealthStatus } from "@/api/health";
import { GitHubStarPrompt, STAR_PROMPT_SEEN_KEY } from "./GitHubStarPrompt";
const state = vi.hoisted(() => ({ companyId: "company", loading: false, onboardingOpen: false, onboardingStatus: null as string | null, done: 5, listCompact: vi.fn() }));
vi.mock("@/context/CompanyContext", () => ({ useCompany: () => ({ selectedCompanyId: state.companyId, loading: state.loading }) }));
vi.mock("@/context/DialogContext", () => ({ useDialogState: () => ({ onboardingOpen: state.onboardingOpen }) }));
vi.mock("@/api/issues", () => ({ issuesApi: {
list: () => Promise.resolve(state.onboardingStatus ? [{ id: "onboarding", originKind: ONBOARDING_FIRST_TASK_ORIGIN_KIND, status: state.onboardingStatus }] : []),
listCompact: (...args: unknown[]) => {
state.listCompact(...args);
return Promise.resolve(Array.from({ length: state.done }, (_, i) => ({ id: `issue-${i}` })));
},
} }));
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
describe("GitHub star prompt", () => {
let container: HTMLDivElement;
let root: Root;
const render = async (health: Partial<HealthStatus> | null = { deploymentMode: "local_trusted" }) => {
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
if (health) client.setQueryData(["health"], health);
await act(async () => root.render(<QueryClientProvider client={client}><GitHubStarPrompt /></QueryClientProvider>));
// Let the onboarding and done-count queries settle, however slow the run.
for (let idle = 0, tick = 0; idle < 2 && tick < 100; tick++) {
await act(async () => { await new Promise((resolve) => setTimeout(resolve, 20)); });
idle = client.isFetching() === 0 ? idle + 1 : 0;
}
};
const visible = () => Boolean(document.querySelector('[data-testid="github-star-prompt"]'));
beforeEach(() => {
Object.assign(state, { companyId: "company", loading: false, onboardingOpen: false, onboardingStatus: null, done: 5 });
state.listCompact.mockClear();
window.localStorage.clear();
container = document.createElement("div"); document.body.append(container); root = createRoot(container);
});
afterEach(async () => { await act(async () => root.unmount()); container.remove(); });
it("opens once a self-hosted company has five done tasks, and only once", async () => {
await render();
expect(visible()).toBe(true);
expect(state.listCompact).toHaveBeenCalledWith("company", { status: "done", limit: 5 }, expect.anything());
expect(window.localStorage.getItem(STAR_PROMPT_SEEN_KEY)).not.toBeNull();
const star = document.querySelector<HTMLAnchorElement>('a[href="https://github.com/paperclipai/paperclip"]');
expect(star?.target).toBe("_blank");
await act(async () => root.unmount());
root = createRoot(container);
await render();
expect(visible()).toBe(false);
});
it("waits for another open dialog to close", async () => {
const other = document.createElement("div");
other.setAttribute("role", "dialog"); other.setAttribute("data-state", "open");
document.body.append(other);
await render();
expect(visible()).toBe(false);
await act(async () => { other.remove(); });
expect(visible()).toBe(true);
});
it("does not open after another tab has shown it", async () => {
const other = document.createElement("div");
other.setAttribute("role", "dialog"); other.setAttribute("data-state", "open");
document.body.append(other);
await render();
window.localStorage.setItem(STAR_PROMPT_SEEN_KEY, new Date().toISOString());
await act(async () => { window.dispatchEvent(new StorageEvent("storage", { key: STAR_PROMPT_SEEN_KEY })); });
await act(async () => { other.remove(); });
expect(visible()).toBe(false);
});
it("waits until the onboarding task is finished", async () => {
state.onboardingStatus = "in_progress";
await render();
expect(visible()).toBe(false);
expect(state.listCompact).not.toHaveBeenCalled();
await act(async () => root.unmount());
root = createRoot(container);
state.onboardingStatus = "done";
await render();
expect(visible()).toBe(true);
});
it("stays hidden below five done tasks", async () => {
state.done = 4;
await render();
expect(visible()).toBe(false);
expect(window.localStorage.getItem(STAR_PROMPT_SEEN_KEY)).toBeNull();
});
it("stays hidden on cloud, during onboarding, and before health is known", async () => {
await render({ deploymentMode: "authenticated", cloud: {} as HealthStatus["cloud"] });
expect(visible()).toBe(false);
state.onboardingOpen = true;
await render();
expect(visible()).toBe(false);
state.onboardingOpen = false;
await render(null);
expect(visible()).toBe(false);
expect(state.listCompact).not.toHaveBeenCalled();
});
});
+94
View File
@@ -0,0 +1,94 @@
import { useEffect, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { Star } from "lucide-react";
import { healthApi } from "@/api/health";
import { issuesApi } from "@/api/issues";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { useCompany } from "@/context/CompanyContext";
import { useDialogState } from "@/context/DialogContext";
import { queryKeys } from "@/lib/queryKeys";
import { MODAL_SELECTOR, useModalOpen, useOnboardingInProgress } from "./AnnouncementWell";
import { PaperclipMark } from "./PaperclipLockup";
export const STAR_PROMPT_COMPLETED_TASKS = 5;
export const STAR_PROMPT_SEEN_KEY = "paperclip.github-star-prompt.seen";
export const PAPERCLIP_GITHUB_URL = "https://github.com/paperclipai/paperclip";
function readSeen() {
try { return window.localStorage.getItem(STAR_PROMPT_SEEN_KEY) !== null; } catch { return true; }
}
function markSeen() {
try { window.localStorage.setItem(STAR_PROMPT_SEEN_KEY, new Date().toISOString()); } catch { /* shown once per visit instead */ }
}
/**
* Asks self-hosted users to star the repository once they have finished
* enough tasks to know whether Paperclip helps them. It shows at most once
* per browser: showing it counts as seen, so a reload or another open tab
* never repeats it. It waits for any other dialog to close. Cloud instances,
* unfinished onboarding and unknown state (loading or a failed read) keep it
* hidden.
*/
export function GitHubStarPrompt() {
const { selectedCompanyId, loading } = useCompany();
const { onboardingOpen } = useDialogState();
const [seen, setSeen] = useState(readSeen);
const [open, setOpen] = useState(false);
const onboardingInProgress = useOnboardingInProgress(selectedCompanyId);
const health = useQuery({ queryKey: queryKeys.health, queryFn: () => healthApi.get(), enabled: false });
const selfHosted = health.isSuccess && !health.data.cloud;
const eligible = selfHosted && !seen && !loading && Boolean(selectedCompanyId) && !onboardingOpen && !onboardingInProgress;
const done = useQuery({
queryKey: queryKeys.issues.starPromptDone(selectedCompanyId ?? "__none__"),
queryFn: ({ signal }) => issuesApi.listCompact(selectedCompanyId!, { status: "done", limit: STAR_PROMPT_COMPLETED_TASKS }, { signal }),
enabled: eligible,
staleTime: 60_000,
});
const reached = eligible && done.isSuccess && done.data.length >= STAR_PROMPT_COMPLETED_TASKS;
const otherModalOpen = useModalOpen(reached);
useEffect(() => {
if (seen) return;
const onStorage = (event: StorageEvent) => {
if (event.key === STAR_PROMPT_SEEN_KEY || event.key === null) setSeen(readSeen());
};
window.addEventListener("storage", onStorage);
return () => window.removeEventListener("storage", onStorage);
}, [seen]);
useEffect(() => {
if (!reached || otherModalOpen || document.querySelector(MODAL_SELECTOR)) return;
// Another tab may have shown it after this one mounted.
if (readSeen()) { setSeen(true); return; }
markSeen();
setSeen(true);
setOpen(true);
}, [reached, otherModalOpen]);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="sm:max-w-md" data-testid="github-star-prompt">
<DialogHeader className="items-center text-center sm:text-center">
<div className="mb-2 flex size-14 items-center justify-center rounded-full border border-border bg-muted">
<PaperclipMark decorative className="size-7" />
</div>
<DialogTitle>You completed {STAR_PROMPT_COMPLETED_TASKS} tasks!</DialogTitle>
<DialogDescription className="max-w-xs">
If you're enjoying Paperclip, consider giving us a star on GitHub!
</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-2">
<Button asChild className="w-full">
<a href={PAPERCLIP_GITHUB_URL} target="_blank" rel="noopener noreferrer" onClick={() => setOpen(false)}>
<Star className="size-4 fill-current" aria-hidden />
Star on GitHub
</a>
</Button>
<Button variant="ghost" className="w-full text-muted-foreground" onClick={() => setOpen(false)}>Not now</Button>
</div>
</DialogContent>
</Dialog>
);
}
+4
View File
@@ -164,6 +164,10 @@ vi.mock("./AnnouncementWell", () => ({
AnnouncementWell: () => <div data-announcement-well />,
}));
vi.mock("./GitHubStarPrompt", () => ({
GitHubStarPrompt: () => null,
}));
vi.mock("./MobileBottomNav", () => ({
MobileBottomNav: () => null,
}));
+27 -1
View File
@@ -5,6 +5,9 @@ interface PaperclipLockupProps extends Omit<SVGProps<SVGSVGElement>, "children">
title?: string;
}
const PAPERCLIP_MARK_PATH =
"M46.2611 33.6556L34.7307 44.6408C32.7445 46.5331 29.5205 46.5323 27.5303 44.6361C25.5404 42.74 25.54 39.669 27.5261 37.7768L39.0565 26.7916C40.2474 25.657 42.1811 25.6567 43.3752 26.7943C44.5693 27.9319 44.5689 29.7742 43.378 30.9088L31.849 41.8954C31.4528 42.2728 30.8077 42.273 30.4092 41.8933C30.011 41.5136 30.0117 40.8996 30.4078 40.5223L41.9382 29.5371C42.336 29.1581 42.336 28.5437 41.9382 28.1647C41.5404 27.786 40.8954 27.7858 40.4977 28.1647L28.9673 39.1499C27.7742 40.2866 27.7764 42.1295 28.9687 43.2657C30.1613 44.402 32.0963 44.4046 33.2895 43.2678L44.8192 32.2805C46.8068 30.3865 46.8038 27.3159 44.8157 25.4219C42.8275 23.5277 39.6039 23.5253 37.616 25.4192L26.0856 36.4044C23.3024 39.056 23.3058 43.3558 26.0898 46.0085C28.8741 48.6612 33.388 48.6649 36.1712 46.0132L47.7017 35.028C48.0994 34.649 48.0994 34.0346 47.7017 33.6556C47.3039 33.2766 46.6589 33.2766 46.2611 33.6556Z";
/**
* The full Paperclip lockup — mark plus wordmark — from the brand asset
* (`paperclip_lockup_dark_mode alt.svg`). The export is white-filled for dark
@@ -41,7 +44,30 @@ export function PaperclipLockup({
<path d="M78.2437 48.4902V31.9902H81.0151V33.9775H81.187C81.3302 33.6911 81.5343 33.3831 81.7993 33.0537C82.0643 32.7171 82.4224 32.4307 82.8735 32.1943C83.3319 31.958 83.9155 31.8398 84.6245 31.8398C85.5483 31.8398 86.3826 32.0762 87.1274 32.5488C87.8722 33.0215 88.4631 33.7161 88.8999 34.6328C89.3439 35.5495 89.5659 36.6774 89.5659 38.0166C89.5659 39.3343 89.3511 40.4551 88.9214 41.3789C88.4917 42.3027 87.9009 43.0081 87.1489 43.4951C86.4041 43.9749 85.5591 44.2148 84.6138 44.2148C83.9334 44.2148 83.3641 44.1003 82.9058 43.8711C82.4546 43.6348 82.0894 43.3555 81.8101 43.0332C81.5379 42.7038 81.3302 42.3923 81.187 42.0986H81.0688V48.4902H78.2437ZM83.8403 41.8945C84.4634 41.8945 84.9826 41.7262 85.3979 41.3896C85.8205 41.0531 86.1392 40.5911 86.354 40.0039C86.5688 39.4167 86.6763 38.7507 86.6763 38.0059C86.6763 37.2611 86.5688 36.5986 86.354 36.0186C86.1392 35.4385 85.8241 34.9837 85.4087 34.6543C84.9933 34.3249 84.4705 34.1602 83.8403 34.1602C83.2316 34.1602 82.716 34.3177 82.2935 34.6328C81.8781 34.9479 81.5594 35.3955 81.3374 35.9756C81.1226 36.5485 81.0151 37.2253 81.0151 38.0059C81.0151 38.7793 81.1226 39.4596 81.3374 40.0469C81.5594 40.627 81.8817 41.0817 82.3042 41.4111C82.7267 41.7334 83.2388 41.8945 83.8403 41.8945Z" />
<path d="M70.042 44.2471C69.2757 44.2471 68.5882 44.111 67.9795 43.8389C67.3708 43.5596 66.891 43.1514 66.54 42.6143C66.1891 42.0771 66.0137 41.4147 66.0137 40.627C66.0137 39.9538 66.139 39.3952 66.3896 38.9512C66.6403 38.5072 66.9805 38.1527 67.4102 37.8877C67.847 37.6227 68.3376 37.4222 68.8818 37.2861C69.4261 37.1429 69.9883 37.0426 70.5684 36.9854C71.2773 36.9066 71.8503 36.8385 72.2871 36.7812C72.724 36.724 73.0391 36.6309 73.2324 36.502C73.4329 36.373 73.5332 36.1725 73.5332 35.9004V35.8467C73.5332 35.2666 73.3613 34.8154 73.0176 34.4932C72.6738 34.1709 72.1725 34.0098 71.5137 34.0098C70.819 34.0098 70.2676 34.1602 69.8594 34.4609C69.4583 34.7617 69.1862 35.1162 69.043 35.5244L66.4111 35.1484C66.6188 34.418 66.9626 33.8092 67.4424 33.3223C67.9222 32.8281 68.5059 32.4593 69.1934 32.2158C69.888 31.9652 70.6543 31.8398 71.4922 31.8398C72.0651 31.8398 72.638 31.9079 73.2109 32.0439C73.7839 32.1729 74.3102 32.3949 74.79 32.71C75.2699 33.0251 75.653 33.4476 75.9395 33.9775C76.2259 34.5075 76.3691 35.1699 76.3691 35.9648V44H73.6514V42.3457H73.5654C73.3936 42.6751 73.1501 42.9867 72.835 43.2803C72.527 43.5667 72.1403 43.7995 71.6748 43.9785C71.2165 44.1576 70.6722 44.2471 70.042 44.2471ZM70.7617 42.1631C71.3346 42.1631 71.8288 42.0521 72.2441 41.8301C72.6595 41.6009 72.9818 41.3001 73.2109 40.9277C73.4401 40.5482 73.5547 40.1364 73.5547 39.6924V38.2852C73.4616 38.3568 73.3112 38.4248 73.1035 38.4893C72.8958 38.5465 72.6595 38.6003 72.3945 38.6504C72.1296 38.7005 71.8682 38.7435 71.6104 38.7793C71.3597 38.8151 71.1413 38.8438 70.9551 38.8652C70.5326 38.9225 70.153 39.0192 69.8164 39.1553C69.487 39.2842 69.2256 39.4632 69.0322 39.6924C68.8389 39.9215 68.7422 40.2188 68.7422 40.584C68.7422 41.1068 68.932 41.5007 69.3115 41.7656C69.6911 42.0306 70.1745 42.1631 70.7617 42.1631Z" />
<path d="M53.7939 44V27.9941H59.7988C61.0306 27.9941 62.0618 28.2233 62.8926 28.6816C63.7305 29.14 64.3643 29.7702 64.7939 30.5723C65.2236 31.3743 65.4385 32.2839 65.4385 33.3008C65.4385 34.3249 65.2201 35.238 64.7832 36.04C64.3535 36.835 63.7161 37.4616 62.8711 37.9199C62.026 38.3711 60.9876 38.5967 59.7559 38.5967H55.7812V36.2119H59.3584C60.0817 36.2119 60.6725 36.0902 61.1309 35.8467C61.5964 35.596 61.9365 35.2523 62.1514 34.8154C62.3734 34.3714 62.4844 33.8665 62.4844 33.3008C62.4844 32.7279 62.3734 32.2266 62.1514 31.7969C61.9365 31.36 61.5964 31.0234 61.1309 30.7871C60.6725 30.5436 60.0781 30.4219 59.3477 30.4219H56.6943V44H53.7939Z" />
<path d="M46.2611 33.6556L34.7307 44.6408C32.7445 46.5331 29.5205 46.5323 27.5303 44.6361C25.5404 42.74 25.54 39.669 27.5261 37.7768L39.0565 26.7916C40.2474 25.657 42.1811 25.6567 43.3752 26.7943C44.5693 27.9319 44.5689 29.7742 43.378 30.9088L31.849 41.8954C31.4528 42.2728 30.8077 42.273 30.4092 41.8933C30.011 41.5136 30.0117 40.8996 30.4078 40.5223L41.9382 29.5371C42.336 29.1581 42.336 28.5437 41.9382 28.1647C41.5404 27.786 40.8954 27.7858 40.4977 28.1647L28.9673 39.1499C27.7742 40.2866 27.7764 42.1295 28.9687 43.2657C30.1613 44.402 32.0963 44.4046 33.2895 43.2678L44.8192 32.2805C46.8068 30.3865 46.8038 27.3159 44.8157 25.4219C42.8275 23.5277 39.6039 23.5253 37.616 25.4192L26.0856 36.4044C23.3024 39.056 23.3058 43.3558 26.0898 46.0085C28.8741 48.6612 33.388 48.6649 36.1712 46.0132L47.7017 35.028C48.0994 34.649 48.0994 34.0346 47.7017 33.6556C47.3039 33.2766 46.6589 33.2766 46.2611 33.6556Z" />
<path d={PAPERCLIP_MARK_PATH} />
</svg>
);
}
/** The mark alone, cropped square from the same geometry as the lockup. */
export function PaperclipMark({
decorative = false,
title = "Paperclip",
className,
...rest
}: PaperclipLockupProps) {
return (
<svg
{...rest}
className={className}
viewBox="22.5 22.5 26.5 26.5"
fill="currentColor"
role={decorative ? undefined : "img"}
aria-hidden={decorative ? true : undefined}
aria-label={decorative ? undefined : title}
focusable="false"
>
<path d={PAPERCLIP_MARK_PATH} />
</svg>
);
}
+2
View File
@@ -314,6 +314,8 @@ export const queryKeys = {
list: (companyId: string) => ["issues", companyId] as const,
onboardingFirstTask: (companyId: string) =>
["issues", companyId, "onboarding-first-task"] as const,
starPromptDone: (companyId: string) =>
["issues", companyId, "star-prompt-done"] as const,
mentionPool: (companyId: string) =>
["issues", companyId, "mention-pool"] as const,
search: (