From 2d47a8058d7d8b7876090cacb0b871fa689e617c Mon Sep 17 00:00:00 2001 From: Michael Nguyen <13559011+nguyenm7@users.noreply.github.com> Date: Mon, 14 Sep 2026 08:05:27 -0700 Subject: [PATCH] fix(apps): update connector artwork and theme fallback (#13361) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit > Awaiting author review. Do not merge until the author explicitly approves. ## Thinking Path > - Paperclip helps people manage AI agents for work. > - Connector screens need recognizable app artwork. > - Several bundled marks have inconsistent artwork or dark-theme behavior. > - The shared logo component should retain the existing frame. > - This change replaces selected artwork and fixes local theme fallback. > - Users see consistent connector icons across shared component callers. ## Linked Issues or Issue Description **Current behavior** Some connector marks use outdated artwork or unsuitable theme variants. A remote dark logo can override a canonical local mark that works in both themes. **Proposed behavior** Use the selected bundled artwork with the existing gray rounded frame. Use local light artwork in both themes unless a distinct local dark variant exists. **Subsystem affected** Connector artwork, the shared AppLogo resolver, and its validation. **Breaking changes** No connector capability, permission, credential, or catalog activation changes. No duplicate PR was found in the earlier search. ## What Changed - Update 46 artwork files and only the app definitions whose logo paths need to change. - Keep a compact public manifest of identities, paths, visibility, and aliases. - Prefer local artwork in both themes and preserve the existing frame and padding. - Add locally runnable artwork safety checks and a light/dark Storybook gallery; leave PR workflows unchanged. - Document artwork conventions. Source research is kept outside the public manifest. ## Verification - Artwork check: 69 identities pass. - Node artwork validation tests: 13 pass (rechecked September 14). - Focused resolver, component, and catalog tests: 40 pass (rechecked September 14). - Maintainer decision: dedicated icon-validation CI is not required; the workflow remains unchanged. Greptile acknowledged 5/5 with no remaining code concerns on September 14. - UI typecheck, token gates, and Storybook build pass. - Earlier full build and repository typecheck passed. The broad local test run was stopped after workspace-runtime dependency fixture failures outside this change. Current GitHub CI remains the full-suite gate. - Visual approval remains outstanding. Review the canonical icon registry in both themes at 24–48px. ## Risks The SVG checker rejects common active features; it is not a general sanitizer for arbitrary uploads. Optical balance still requires human review. Remote fallback for unknown brands retains existing behavior. This change does not add an asset importer or new connector capabilities. ## Model Used OpenAI Codex, assisted by GPT-5 and GPT-6 with code execution and browser tooling. Exact hosted model IDs and context window sizes were not exposed. ## 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 - [ ] 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 - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge ## Artwork comparison Before and after for every affected identity. Images follow your GitHub light/dark theme and are pinned to the base and PR commits. This compares artwork; the existing gray rounded product frame and padding are unchanged. | Connector | Before | After | |---|:---:|:---:| | AgentMail | AgentMail | AgentMail | | Airtable | Airtable | Airtable | | Asana | Asana | Asana | | ClickHouse | ClickHouse | ClickHouse | | Cloudflare | Cloudflare | Cloudflare | | Cloudinary | Cloudinary | Cloudinary | | Discord | Discord | Discord | | GitHub | GitHub | GitHub | | Gmail | Gmail | Gmail | | Google Calendar | Google Calendar | Google Calendar | | Google Chat | Google Chat | Google Chat | | Google Docs | Google Docs | Google Docs | | Google Drive | Google Drive | Google Drive | | Google People | Google People | Google People | | Google Sheets | Google Sheets | Google Sheets | | Google Slides | Google Slides | Google Slides | | Google Workspace Search | Google Workspace Search | Google Workspace Search | | Hugging Face | Hugging Face | Hugging Face | | Jam.dev (library only) | New library entry | Jam.dev | | Jira | Jira | Jira | | Linear | Linear | Linear | | Manufact | Manufact | Manufact | | Microsoft Teams | Microsoft Teams | Microsoft Teams | | Miro | Miro | Miro | | Mixpanel | Mixpanel | Mixpanel | | Netlify | Netlify | Netlify | | Notion | Notion | Notion | | PagerDuty | PagerDuty | PagerDuty | | PostHog | PostHog | PostHog | | Postman | Postman | Postman | | Shopify | Shopify | Shopify | | Slack | Slack | Slack | | Stripe | Stripe | Stripe | | Supabase | Supabase | Supabase | | Telegram | Telegram | Telegram | | Todoist | Todoist | Todoist | | Wix | Wix | Wix | | Zapier | Zapier | Zapier | --- doc/connections/CONNECTOR-ICONS.md | 29 ++ doc/connections/CONNECTOR-PLAYBOOK.md | 14 +- packages/shared/src/app-definitions.test.ts | 23 +- .../shared/src/app-definitions/agentmail.json | 3 +- .../src/app-definitions/clickhouse.json | 3 +- .../shared/src/app-definitions/discord.json | 3 +- packages/shared/src/app-definitions/jira.json | 3 +- .../shared/src/app-definitions/linear.json | 3 +- .../shared/src/app-definitions/manufact.json | 3 +- packages/shared/src/app-definitions/miro.json | 3 +- .../shared/src/app-definitions/mixpanel.json | 3 +- .../shared/src/app-definitions/netlify.json | 3 +- .../shared/src/app-definitions/notion.json | 3 +- .../shared/src/app-definitions/slack.json | 2 +- packages/shared/src/app-definitions/wix.json | 3 +- scripts/app-brand-validation.mjs | 47 ++ scripts/app-brand-validation.test.mjs | 34 ++ scripts/check-app-brand-assets.mjs | 8 + ui/public/brands/apps/agentmail.svg | 31 +- ui/public/brands/apps/airtable.svg | 2 +- ui/public/brands/apps/asana.svg | 13 +- ui/public/brands/apps/clickhouse-dark.svg | 1 + ui/public/brands/apps/clickhouse.svg | 2 +- ui/public/brands/apps/cloudflare.svg | 2 +- ui/public/brands/apps/cloudinary.svg | 4 +- ui/public/brands/apps/discord-dark.svg | 1 + ui/public/brands/apps/discord.svg | 5 +- ui/public/brands/apps/github-dark.svg | 11 +- ui/public/brands/apps/github.svg | 11 +- ui/public/brands/apps/gmail.svg | 2 +- ui/public/brands/apps/google-calendar.svg | 2 +- ui/public/brands/apps/google-chat.svg | 2 +- ui/public/brands/apps/google-docs.svg | 2 +- ui/public/brands/apps/google-drive.svg | 2 +- ui/public/brands/apps/google-people.svg | 2 +- ui/public/brands/apps/google-sheets.svg | 2 +- ui/public/brands/apps/google-slides.svg | 2 +- .../brands/apps/google-workspace-search.svg | 2 +- ui/public/brands/apps/hugging-face.svg | 9 +- ui/public/brands/apps/jam-dev-dark.svg | 1 + ui/public/brands/apps/jam-dev.svg | 1 + ui/public/brands/apps/jira.svg | 5 +- ui/public/brands/apps/linear-dark.svg | 5 + ui/public/brands/apps/linear.svg | 6 +- ui/public/brands/apps/manifest.json | 425 ++++-------------- ui/public/brands/apps/manufact-dark.svg | 6 + ui/public/brands/apps/manufact.svg | 25 +- ui/public/brands/apps/microsoft-teams.svg | 88 +++- ui/public/brands/apps/miro.svg | 5 +- ui/public/brands/apps/mixpanel-dark.svg | 1 + ui/public/brands/apps/netlify-dark.svg | 1 + ui/public/brands/apps/netlify.svg | 17 +- ui/public/brands/apps/notion.svg | 2 +- ui/public/brands/apps/pagerduty.svg | 5 +- ui/public/brands/apps/posthog.svg | 2 +- ui/public/brands/apps/postman.svg | 2 +- ui/public/brands/apps/shopify.svg | 27 +- ui/public/brands/apps/slack.svg | 17 + ui/public/brands/apps/stripe.svg | 12 +- ui/public/brands/apps/supabase.svg | 16 +- ui/public/brands/apps/telegram.svg | 14 +- ui/public/brands/apps/todoist.svg | 2 +- ui/public/brands/apps/wix-dark.svg | 12 + ui/public/brands/apps/wix.svg | 13 +- ui/public/brands/apps/zapier.svg | 2 +- ui/src/lib/app-brand-assets.test.ts | 31 +- ui/src/lib/app-brand-assets.ts | 20 +- .../pages/apps/AppLogo.brand-assets.test.tsx | 24 + ui/src/pages/apps/AppLogo.tsx | 4 +- .../stories/AppLogoRegistry.stories.tsx | 38 ++ 70 files changed, 642 insertions(+), 487 deletions(-) create mode 100644 doc/connections/CONNECTOR-ICONS.md create mode 100644 scripts/app-brand-validation.mjs create mode 100644 scripts/app-brand-validation.test.mjs create mode 100644 scripts/check-app-brand-assets.mjs create mode 100644 ui/public/brands/apps/clickhouse-dark.svg create mode 100644 ui/public/brands/apps/discord-dark.svg create mode 100644 ui/public/brands/apps/jam-dev-dark.svg create mode 100644 ui/public/brands/apps/jam-dev.svg create mode 100644 ui/public/brands/apps/linear-dark.svg create mode 100644 ui/public/brands/apps/manufact-dark.svg create mode 100644 ui/public/brands/apps/mixpanel-dark.svg create mode 100644 ui/public/brands/apps/netlify-dark.svg create mode 100644 ui/public/brands/apps/slack.svg create mode 100644 ui/public/brands/apps/wix-dark.svg create mode 100644 ui/storybook/stories/AppLogoRegistry.stories.tsx diff --git a/doc/connections/CONNECTOR-ICONS.md b/doc/connections/CONNECTOR-ICONS.md new file mode 100644 index 00000000000..3648ebbed73 --- /dev/null +++ b/doc/connections/CONNECTOR-ICONS.md @@ -0,0 +1,29 @@ +# Connector icons + +Use the shared `AppLogo` component and bundled artwork in `ui/public/brands/apps`. +Keep the existing gray rounded frame, caller size and border, and contained image +padding. Preserve vendor shapes and colors; use a separate dark asset only when +the light artwork is unsuitable on the dark frame. Do not invert or stretch marks. + +The public manifest contains only identity, catalog visibility, artwork paths, +and optional aliases. Omit `darkAsset` when both themes use the same file. Keep +matching logo paths in the app definition. Brand-library membership does not +enable a connector. Google People and Workspace Search share the Google mark. + +Use reviewed vendor or supplied source files. Keep source research and review +records outside the browser-served manifest. Never add credentials or private +review links to public assets. Render SVGs as images, not inline HTML. The +structural safety check rejects common active SVG features; it is not a general +sanitizer for untrusted uploads. + +Before submitting artwork, run: + +```sh +node scripts/check-app-brand-assets.mjs +node --test scripts/app-brand-validation.test.mjs +pnpm exec vitest run ui/src/lib/app-brand-assets.test.ts ui/src/pages/apps/AppLogo.brand-assets.test.tsx packages/shared/src/app-definitions.test.ts +``` + +Run the structural artwork check locally. Review the Storybook canonical icon registry +in light and dark themes at 24–48px, then inspect affected product surfaces. Check +contrast, optical size, native details, and the existing image-error fallback. diff --git a/doc/connections/CONNECTOR-PLAYBOOK.md b/doc/connections/CONNECTOR-PLAYBOOK.md index ca77a2600b2..6bd8a9de77c 100644 --- a/doc/connections/CONNECTOR-PLAYBOOK.md +++ b/doc/connections/CONNECTOR-PLAYBOOK.md @@ -5,6 +5,8 @@ and shipping Paperclip app connections. Status: canonical end-to-end authoring guide for Apps v2 catalog connections. +For connector artwork, follow [Connector icons](./CONNECTOR-ICONS.md): fixed gray Paperclip frames, authentic vendor artwork, explicit theme variants, optical fit and exact provenance. Brand-library additions do not activate connectors. Use the shared registry/resolver and branding generator; do not introduce per-screen logos or outer-surface overrides. + This runbook is the repeatable, agent-executable procedure for adding a vendor to the Apps catalog as data, not as a plugin. It follows the accepted connections framework in [PAP-13211](/PAP/issues/PAP-13211), the first-30 @@ -87,7 +89,7 @@ scripts/ingest-app-definitions.mjs # human-authored definition so packages/shared/src/app-definitions/.json # generated definition packages/shared/src/app-definitions.generated.ts # generated registry ui/public/brands/apps/.svg # official, sanitized mark -ui/public/brands/apps/manifest.json # branding provenance +ui/public/brands/apps/manifest.json # runtime branding paths packages/shared/src/app-definitions.test.ts # manifest/provider assertions ``` @@ -571,15 +573,15 @@ only a runtime image-failure fallback. external executable content, or unsafe references. 5. Save assets under `ui/public/brands/apps/`. Add a `-dark` variant only when the normal mark loses contrast in dark mode. -6. Add the provider to `ui/public/brands/apps/manifest.json` with slug, local - asset, optional dark asset, official source URL, exact upstream asset URL, - asset type, visibility, and dark-variant requirement. +6. Add the provider to `ui/public/brands/apps/manifest.json` with slug, name, + local asset, optional dark asset, visibility, and optional aliases. Keep source + URLs and verification notes in the review record, outside the public manifest. 7. Let the ingestion script derive `branding.logoUrl` and `darkLogoUrl` from the - provenance manifest. + runtime manifest. The manifest test decodes PNG headers, requires at least 128 by 128 pixels, sanity-checks SVG markup, verifies files exist, and requires store-visible -definitions and visible provenance entries to match exactly. +definitions and visible manifest entries to match exactly. ### Phase 5: Author the definition at the durable source diff --git a/packages/shared/src/app-definitions.test.ts b/packages/shared/src/app-definitions.test.ts index 37cbc570459..2f70bd73e57 100644 --- a/packages/shared/src/app-definitions.test.ts +++ b/packages/shared/src/app-definitions.test.ts @@ -698,7 +698,7 @@ describe("AppDefinition catalog", () => { expect(storeSlugs.has(slug), slug).toBe(false); } }); - it("ships complete local branding provenance for all 46 store-visible providers", () => { + it("ships matching local artwork for every store-visible provider", () => { const uiPublic = path.resolve( path.dirname(fileURLToPath(import.meta.url)), "../../../ui/public", @@ -711,21 +711,13 @@ describe("AppDefinition catalog", () => { catalogVisible: boolean; localAsset: string; darkAsset?: string; - officialSourceUrl: string; - upstreamAssetUrl: string; - assetType: "svg" | "png"; - darkVariantRequired: boolean; }>; }; const visible = manifest.providers.filter((entry) => entry.catalogVisible); - expect(visible).toHaveLength(46); + expect(visible).toHaveLength(APP_STORE_DEFINITIONS.length); expect(new Set(visible.map((entry) => entry.slug))).toHaveProperty( "size", - 46, - ); - expect(new Set(visible.map((entry) => entry.localAsset))).toHaveProperty( - "size", - 46, + visible.length, ); expect(new Set(APP_STORE_DEFINITIONS.map((entry) => entry.slug))).toEqual( new Set(visible.map((entry) => entry.slug)), @@ -735,21 +727,16 @@ describe("AppDefinition catalog", () => { expect(provenance).toBeTruthy(); expect(provenance.localAsset).toBe(app.branding.logoUrl); expect(provenance.darkAsset).toBe(app.branding.darkLogoUrl); - expect(provenance.darkVariantRequired).toBe( - Boolean(provenance.darkAsset), - ); - expect(new URL(provenance.officialSourceUrl).protocol).toBe("https:"); - expect(new URL(provenance.upstreamAssetUrl).protocol).toBe("https:"); expect(provenance.localAsset).toMatch(/^\/brands\/apps\/.+\.(svg|png)$/); expect(provenance.localAsset).not.toContain("google.com/s2/favicons"); const asset = fs.readFileSync(path.join(uiPublic, provenance.localAsset)); - if (provenance.assetType === "png") { + if (provenance.localAsset.endsWith(".png")) { expect(asset.subarray(0, 8).toString("hex")).toBe("89504e470d0a1a0a"); expect(asset.readUInt32BE(16)).toBeGreaterThanOrEqual(128); expect(asset.readUInt32BE(20)).toBeGreaterThanOrEqual(128); } else { const svg = asset.toString("utf8"); - expect(svg).toMatch(/^\s*)? value.trim().toLowerCase().replace(/&/g, "and").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, ""); + +// Conservative structural rejection, not a general-purpose SVG sanitizer. +// Original accepted artwork is copied byte-for-byte, never rewritten here. +export function validateArtwork(bytes, filename) { + if (filename.endsWith(".png")) { + if (bytes.length < 45 || !bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])) + || bytes.toString("ascii", 12, 16) !== "IHDR" || bytes.readUInt32BE(16) === 0 || bytes.readUInt32BE(20) === 0 + || bytes.toString("ascii", bytes.length - 8, bytes.length - 4) !== "IEND") { + throw new Error(`${filename}: invalid PNG signature`); + } + return; + } + const svg = bytes.toString("utf8"); + if (!filename.endsWith(".svg") || (!/]*\bviewBox\s*=\s*["'][^"']+["']/i.test(svg) && !(/]*\bwidth\s*=\s*["'][0-9.]+(?:px)?["']/i.test(svg) && /]*\bheight\s*=\s*["'][0-9.]+(?:px)?["']/i.test(svg)))) { + throw new Error(`${filename}: SVG requires a viewBox or intrinsic width and height`); + } + if (/ typeof alias !== "string" || !alias.trim())) throw new Error(`${row.slug}: invalid aliases`); + for (const name of [row.slug, row.provider, ...aliases]) { + const key = normalizeBrandKey(name); + if (keys.has(key) && keys.get(key) !== row.slug) throw new Error(`${row.slug}: ambiguous brand alias ${name}`); + keys.set(key, row.slug); + } + for (const asset of new Set([row.localAsset, ...(row.darkAsset === undefined ? [] : [row.darkAsset])])) { + if (typeof asset !== "string" || !assetPathPattern.test(asset)) throw new Error(`${row.slug}: invalid asset path`); + validateArtwork(readAsset(asset), asset); + } + } + return manifest.providers.length; +} diff --git a/scripts/app-brand-validation.test.mjs b/scripts/app-brand-validation.test.mjs new file mode 100644 index 00000000000..df7a427776b --- /dev/null +++ b/scripts/app-brand-validation.test.mjs @@ -0,0 +1,34 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { validateArtwork, validateManifest } from "./app-brand-validation.mjs"; + +const svg = Buffer.from(''); +const record = () => ({ + slug: "example", provider: "Example", aliases: [], localAsset: "/brands/apps/example.svg", darkAsset: "/brands/apps/example.svg", +}); +const check = (row, read = () => svg) => validateManifest({ schemaVersion: 1, providers: [row] }, read); + +test("accepts authentic vector artwork, native gradients and actual PNG bytes", () => { + assert.equal(check(record()), 1); + validateArtwork(Buffer.from(''), "gradient.svg"); + validateArtwork(Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jRZkAAAAASUVORK5CYII=", "base64"), "native.png"); + assert.throws(() => validateArtwork(Buffer.from("89504e470d0a1a0a", "hex"), "truncated.png"), /PNG/); + assert.throws(() => validateArtwork(svg, "fake.png"), /PNG/); +}); +for (const payload of ['