diff --git a/README.md b/README.md index 434c558..ad3a267 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@ A fast, design-first GitHub dashboard for developers who want to stay on top of their pull requests, issues, and code reviews — without the noise. +> [!WARNING] +> **Alpha** — DiffKit is in early release. Expect bugs, errors, and rough edges. Feedback and issue reports are welcome on [GitHub Issues](https://github.com/stylessh/diffkit/issues). + ## Features - **Pull Requests** — View, filter, and manage your open PRs across repos diff --git a/apps/dashboard/public/logo-128.png b/apps/dashboard/public/logo-128.png new file mode 100644 index 0000000..0edb2b8 Binary files /dev/null and b/apps/dashboard/public/logo-128.png differ diff --git a/apps/dashboard/src/components/layouts/alpha-notice-dialog.tsx b/apps/dashboard/src/components/layouts/alpha-notice-dialog.tsx new file mode 100644 index 0000000..3e8a204 --- /dev/null +++ b/apps/dashboard/src/components/layouts/alpha-notice-dialog.tsx @@ -0,0 +1,90 @@ +"use client"; + +import { + Alert, + AlertDescription, + AlertTitle, +} from "@diffkit/ui/components/alert"; +import { Button } from "@diffkit/ui/components/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@diffkit/ui/components/dialog"; +import { useEffect, useState } from "react"; +import { siteConfig } from "#/lib/site-config"; +import { useHasMounted } from "#/lib/use-has-mounted"; + +const STORAGE_KEY = "diffkit-alpha-notice-dismissed"; + +const issuesUrl = `${siteConfig.githubRepositoryUrl}/issues`; + +export function AlphaNoticeDialog() { + const hasMounted = useHasMounted(); + const [open, setOpen] = useState(false); + + useEffect(() => { + if (!hasMounted) { + return; + } + try { + if (!localStorage.getItem(STORAGE_KEY)) { + setOpen(true); + } + } catch { + // Storage blocked — skip modal + } + }, [hasMounted]); + + function dismiss() { + try { + localStorage.setItem(STORAGE_KEY, "1"); + } catch { + // ignore + } + setOpen(false); + } + + return ( + !next && dismiss()}> + + + + Welcome to {siteConfig.name} + + + Thanks for trying the dashboard — here is what you should know + before you dive in. + + + +
+ + + Alpha software + + + DiffKit is in early release. Expect bugs, rough edges, and + unfinished flows. When something breaks, please report it on our + GitHub issues board — it helps us ship a stable product. + + +
+ + + + + +
+
+ ); +} diff --git a/apps/dashboard/src/components/layouts/dashboard-bottombar.tsx b/apps/dashboard/src/components/layouts/dashboard-bottombar.tsx index c7ff3de..af9435d 100644 --- a/apps/dashboard/src/components/layouts/dashboard-bottombar.tsx +++ b/apps/dashboard/src/components/layouts/dashboard-bottombar.tsx @@ -3,15 +3,15 @@ import { cn } from "@diffkit/ui/lib/utils"; import { useShowOrgSetupQueryState } from "#/lib/github-access-dialog-query"; import { openGitHubAccessPrompt } from "#/lib/github-access-modal-store"; import { removeWarning, useWarnings } from "#/lib/warning-store"; +import { ExtensionInstallPrompt } from "./extension-install-prompt"; export function DashboardBottomBar() { const warnings = useWarnings(); const [, setShowOrgSetup] = useShowOrgSetupQueryState(); - if (warnings.length === 0) return null; - return ( -
+
+ {warnings.map((warning) => (
default: mod.GitHubAccessDialog, })), ); +const AlphaNoticeDialog = lazy(() => + import("./alpha-notice-dialog").then((mod) => ({ + default: mod.AlphaNoticeDialog, + })), +); const routeApi = getRouteApi("/_protected"); @@ -61,6 +69,8 @@ export function DashboardLayout() { const tabsReady = hasMounted && Boolean(pullsQuery.data && issuesQuery.data); const sidePanel = useSidePanelSlot(); + const isXl = useMediaQuery("(min-width: 1280px)"); + const showPanel = isXl && sidePanel.hasContent && !sidePanel.collapsed; return (
@@ -83,19 +93,29 @@ export function DashboardLayout() { toggle: sidePanel.toggle, }} > -
-
+ +
+ -
+ +
diff --git a/apps/dashboard/src/components/layouts/dashboard-side-panel.tsx b/apps/dashboard/src/components/layouts/dashboard-side-panel.tsx index f202028..bbe5bf4 100644 --- a/apps/dashboard/src/components/layouts/dashboard-side-panel.tsx +++ b/apps/dashboard/src/components/layouts/dashboard-side-panel.tsx @@ -10,6 +10,9 @@ import { } from "react"; import { createPortal } from "react-dom"; +// w-72 (288px) + pl-2 (8px) +export const SIDE_PANEL_WIDTH = 296; + type SidePanelState = { node: HTMLDivElement | null; collapsed: boolean; @@ -66,6 +69,8 @@ export function SidePanelSlot({ }) { const [hasChildren, setHasChildren] = useState(false); const innerRef = useRef(null); + const ghostRef = useRef(null); + const exitTimer = useRef | null>(null); const refCallback = useCallback( (el: HTMLDivElement | null) => { @@ -86,31 +91,52 @@ export function SidePanelSlot({ }; check(); - const observer = new MutationObserver(check); + const observer = new MutationObserver((mutations) => { + const has = el.childNodes.length > 0; + + if (!has && ghostRef.current) { + // Content removed — clone into ghost for exit animation + ghostRef.current.innerHTML = ""; + for (const mutation of mutations) { + for (const node of mutation.removedNodes) { + ghostRef.current.appendChild(node.cloneNode(true)); + } + } + if (exitTimer.current) clearTimeout(exitTimer.current); + exitTimer.current = setTimeout(() => { + if (ghostRef.current) ghostRef.current.innerHTML = ""; + }, 500); + } else if (has && ghostRef.current) { + // Content added — clear ghost + ghostRef.current.innerHTML = ""; + if (exitTimer.current) clearTimeout(exitTimer.current); + } + + setHasChildren(has); + onHasContent(has); + }); observer.observe(el, { childList: true }); el.addEventListener("sidepanel-content", check); return () => { observer.disconnect(); el.removeEventListener("sidepanel-content", check); + if (exitTimer.current) clearTimeout(exitTimer.current); }; }, [onHasContent]); const show = hasChildren && !collapsed; return ( - +
+
- +
); } diff --git a/apps/dashboard/src/components/layouts/extension-install-prompt.tsx b/apps/dashboard/src/components/layouts/extension-install-prompt.tsx new file mode 100644 index 0000000..aa03663 --- /dev/null +++ b/apps/dashboard/src/components/layouts/extension-install-prompt.tsx @@ -0,0 +1,89 @@ +"use client"; + +import { XIcon } from "@diffkit/icons"; +import { Logo } from "@diffkit/ui/components/logo"; +import { useEffect, useState } from "react"; +import { isDiffKitExtensionPresent } from "#/lib/diffkit-extension-detect"; +import { + recordExtensionInstallPromptDismissed, + shouldShowExtensionInstallPrompt, +} from "#/lib/extension-install-prompt-storage"; +import { siteConfig } from "#/lib/site-config"; +import { useHasMounted } from "#/lib/use-has-mounted"; + +export function ExtensionInstallPrompt() { + const hasMounted = useHasMounted(); + const [visible, setVisible] = useState(false); + + useEffect(() => { + if (!hasMounted) { + return; + } + + function applyVisibility() { + setVisible(shouldShowExtensionInstallPrompt(isDiffKitExtensionPresent())); + } + + applyVisibility(); + + if (isDiffKitExtensionPresent()) { + return; + } + + const el = document.documentElement; + const observer = new MutationObserver(() => { + if (isDiffKitExtensionPresent()) { + setVisible(false); + observer.disconnect(); + } else { + applyVisibility(); + } + }); + observer.observe(el, { + attributes: true, + attributeFilter: ["data-diffkit-extension"], + }); + return () => observer.disconnect(); + }, [hasMounted]); + + function dismiss() { + recordExtensionInstallPromptDismissed(); + setVisible(false); + } + + if (!visible) { + return null; + } + + const installHref = siteConfig.browserExtensionInstallUrl; + + return ( +
+ + + Install the DiffKit extension to redirect GitHub PRs, issues, and + matching pages here. + + + Install + + +
+ ); +} diff --git a/apps/dashboard/src/components/legal/legal-document-layout.tsx b/apps/dashboard/src/components/legal/legal-document-layout.tsx new file mode 100644 index 0000000..f3902ce --- /dev/null +++ b/apps/dashboard/src/components/legal/legal-document-layout.tsx @@ -0,0 +1,60 @@ +import { Logo } from "@diffkit/ui/components/logo"; +import { cn } from "@diffkit/ui/lib/utils"; +import { Link } from "@tanstack/react-router"; +import { siteConfig } from "#/lib/site-config"; + +type LegalDocumentLayoutProps = { + title: string; + children: React.ReactNode; +}; + +export function LegalDocumentLayout({ + title, + children, +}: LegalDocumentLayoutProps) { + return ( +
+
+
+ + + + {siteConfig.name} + + +

+ {title} +

+

+ + ← Back to home + +

+
+ +
p:first-of-type]:text-[15px] [&>p:first-of-type]:leading-[1.75] [&>p:first-of-type]:text-foreground/90", + "prose-headings:font-semibold prose-headings:tracking-tight prose-headings:text-foreground", + "prose-h2:mt-12 prose-h2:mb-4 prose-h2:border-b prose-h2:border-border/60 prose-h2:pb-3 prose-h2:text-xl prose-h2:leading-snug", + "prose-ul:my-6 prose-ul:space-y-3 prose-li:my-0 prose-li:text-[15px] prose-li:leading-[1.75] prose-li:text-muted-foreground", + "prose-a:font-medium prose-a:text-primary prose-a:no-underline prose-a:underline-offset-4 hover:prose-a:underline", + )} + > + {children} +
+
+
+ ); +} diff --git a/apps/dashboard/src/lib/diffkit-extension-detect.ts b/apps/dashboard/src/lib/diffkit-extension-detect.ts new file mode 100644 index 0000000..c73d354 --- /dev/null +++ b/apps/dashboard/src/lib/diffkit-extension-detect.ts @@ -0,0 +1,11 @@ +/** + * The DiffKit browser extension sets `data-diffkit-extension="1"` on `` via + * `extensions/diffkit-redirect/dashboard-presence.js` (content script on the + * dashboard origin). Page JS cannot read the extension isolated world; DOM only. + */ +export function isDiffKitExtensionPresent(): boolean { + if (typeof document === "undefined") { + return false; + } + return document.documentElement.dataset.diffkitExtension === "1"; +} diff --git a/apps/dashboard/src/lib/extension-install-prompt-storage.ts b/apps/dashboard/src/lib/extension-install-prompt-storage.ts new file mode 100644 index 0000000..2f86908 --- /dev/null +++ b/apps/dashboard/src/lib/extension-install-prompt-storage.ts @@ -0,0 +1,55 @@ +/** Hide the install chip for 30 days after dismiss; then show again if extension still missing. */ +const COOLDOWN_MS = 30 * 24 * 60 * 60 * 1000; + +const STORAGE_KEY_AT = "diffkit-extension-prompt-dismissed-at"; +/** Legacy boolean dismiss — migrated on read */ +const LEGACY_STORAGE_KEY = "diffkit-extension-prompt-dismissed"; + +function readDismissedAtMs(): number | null { + try { + const legacy = localStorage.getItem(LEGACY_STORAGE_KEY); + if (legacy === "1") { + localStorage.removeItem(LEGACY_STORAGE_KEY); + return null; + } + + const raw = localStorage.getItem(STORAGE_KEY_AT); + if (!raw) { + return null; + } + const n = Number(raw); + if (!Number.isFinite(n) || n <= 0) { + return null; + } + return n; + } catch { + return null; + } +} + +/** + * Whether to show the “install extension” prompt. + * When extension is present, never show. + * When dismissed, hide until 30 days after that timestamp. + */ +export function shouldShowExtensionInstallPrompt( + isExtensionPresent: boolean, +): boolean { + if (isExtensionPresent) { + return false; + } + const dismissedAt = readDismissedAtMs(); + if (dismissedAt === null) { + return true; + } + return Date.now() - dismissedAt >= COOLDOWN_MS; +} + +export function recordExtensionInstallPromptDismissed(): void { + try { + localStorage.setItem(STORAGE_KEY_AT, String(Date.now())); + localStorage.removeItem(LEGACY_STORAGE_KEY); + } catch { + // ignore + } +} diff --git a/apps/dashboard/src/lib/site-config.ts b/apps/dashboard/src/lib/site-config.ts index f03f014..cf241ea 100644 --- a/apps/dashboard/src/lib/site-config.ts +++ b/apps/dashboard/src/lib/site-config.ts @@ -2,6 +2,8 @@ type SiteConfig = { name: string; domain: string; url: string; + /** Where users install the GitHub → DiffKit redirect browser extension (store or docs). */ + browserExtensionInstallUrl: string; githubRepositoryUrl: string; themeColor: string; socialImagePath: string; @@ -15,6 +17,8 @@ export const siteConfig: SiteConfig = { name: "DiffKit", domain: "diff-kit.com", url: "https://diff-kit.com", + browserExtensionInstallUrl: + "https://github.com/stylessh/diffkit/blob/main/extensions/diffkit-redirect/README.md#install-locally", githubRepositoryUrl: "https://github.com/stylessh/diffkit", themeColor: "#00C943", socialImagePath: "/logo512.png", diff --git a/apps/dashboard/src/lib/use-media-query.ts b/apps/dashboard/src/lib/use-media-query.ts new file mode 100644 index 0000000..64d49e3 --- /dev/null +++ b/apps/dashboard/src/lib/use-media-query.ts @@ -0,0 +1,15 @@ +import { useEffect, useState } from "react"; + +export function useMediaQuery(query: string) { + const [matches, setMatches] = useState(false); + + useEffect(() => { + const mq = window.matchMedia(query); + setMatches(mq.matches); + const handler = (e: MediaQueryListEvent) => setMatches(e.matches); + mq.addEventListener("change", handler); + return () => mq.removeEventListener("change", handler); + }, [query]); + + return matches; +} diff --git a/apps/dashboard/src/routeTree.gen.ts b/apps/dashboard/src/routeTree.gen.ts index 1814f1c..de249ee 100644 --- a/apps/dashboard/src/routeTree.gen.ts +++ b/apps/dashboard/src/routeTree.gen.ts @@ -9,7 +9,9 @@ // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. import { Route as rootRouteImport } from './routes/__root' +import { Route as TermsRouteImport } from './routes/terms' import { Route as SetupRouteImport } from './routes/setup' +import { Route as PrivacyRouteImport } from './routes/privacy' import { Route as LoginRouteImport } from './routes/login' import { Route as ProtectedRouteImport } from './routes/_protected' import { Route as ProtectedIndexRouteImport } from './routes/_protected/index' @@ -29,11 +31,21 @@ import { Route as ProtectedOwnerRepoReviewPullIdRouteImport } from './routes/_pr import { Route as ProtectedOwnerRepoPullPullIdRouteImport } from './routes/_protected/$owner/$repo/pull.$pullId' import { Route as ProtectedOwnerRepoIssuesIssueIdRouteImport } from './routes/_protected/$owner/$repo/issues.$issueId' +const TermsRoute = TermsRouteImport.update({ + id: '/terms', + path: '/terms', + getParentRoute: () => rootRouteImport, +} as any) const SetupRoute = SetupRouteImport.update({ id: '/setup', path: '/setup', getParentRoute: () => rootRouteImport, } as any) +const PrivacyRoute = PrivacyRouteImport.update({ + id: '/privacy', + path: '/privacy', + getParentRoute: () => rootRouteImport, +} as any) const LoginRoute = LoginRouteImport.update({ id: '/login', path: '/login', @@ -131,7 +143,9 @@ const ProtectedOwnerRepoIssuesIssueIdRoute = export interface FileRoutesByFullPath { '/': typeof ProtectedIndexRoute '/login': typeof LoginRoute + '/privacy': typeof PrivacyRoute '/setup': typeof SetupRoute + '/terms': typeof TermsRoute '/issues': typeof ProtectedIssuesRoute '/pulls': typeof ProtectedPullsRoute '/reviews': typeof ProtectedReviewsRoute @@ -150,7 +164,9 @@ export interface FileRoutesByFullPath { } export interface FileRoutesByTo { '/login': typeof LoginRoute + '/privacy': typeof PrivacyRoute '/setup': typeof SetupRoute + '/terms': typeof TermsRoute '/issues': typeof ProtectedIssuesRoute '/pulls': typeof ProtectedPullsRoute '/reviews': typeof ProtectedReviewsRoute @@ -171,7 +187,9 @@ export interface FileRoutesById { __root__: typeof rootRouteImport '/_protected': typeof ProtectedRouteWithChildren '/login': typeof LoginRoute + '/privacy': typeof PrivacyRoute '/setup': typeof SetupRoute + '/terms': typeof TermsRoute '/_protected/issues': typeof ProtectedIssuesRoute '/_protected/pulls': typeof ProtectedPullsRoute '/_protected/reviews': typeof ProtectedReviewsRoute @@ -194,7 +212,9 @@ export interface FileRouteTypes { fullPaths: | '/' | '/login' + | '/privacy' | '/setup' + | '/terms' | '/issues' | '/pulls' | '/reviews' @@ -213,7 +233,9 @@ export interface FileRouteTypes { fileRoutesByTo: FileRoutesByTo to: | '/login' + | '/privacy' | '/setup' + | '/terms' | '/issues' | '/pulls' | '/reviews' @@ -233,7 +255,9 @@ export interface FileRouteTypes { | '__root__' | '/_protected' | '/login' + | '/privacy' | '/setup' + | '/terms' | '/_protected/issues' | '/_protected/pulls' | '/_protected/reviews' @@ -255,7 +279,9 @@ export interface FileRouteTypes { export interface RootRouteChildren { ProtectedRoute: typeof ProtectedRouteWithChildren LoginRoute: typeof LoginRoute + PrivacyRoute: typeof PrivacyRoute SetupRoute: typeof SetupRoute + TermsRoute: typeof TermsRoute ApiAuthSplatRoute: typeof ApiAuthSplatRoute ApiWebhooksGithubRoute: typeof ApiWebhooksGithubRoute ApiGithubAppAuthorizeRoute: typeof ApiGithubAppAuthorizeRoute @@ -264,6 +290,13 @@ export interface RootRouteChildren { declare module '@tanstack/react-router' { interface FileRoutesByPath { + '/terms': { + id: '/terms' + path: '/terms' + fullPath: '/terms' + preLoaderRoute: typeof TermsRouteImport + parentRoute: typeof rootRouteImport + } '/setup': { id: '/setup' path: '/setup' @@ -271,6 +304,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof SetupRouteImport parentRoute: typeof rootRouteImport } + '/privacy': { + id: '/privacy' + path: '/privacy' + fullPath: '/privacy' + preLoaderRoute: typeof PrivacyRouteImport + parentRoute: typeof rootRouteImport + } '/login': { id: '/login' path: '/login' @@ -446,7 +486,9 @@ const ProtectedRouteWithChildren = ProtectedRoute._addFileChildren( const rootRouteChildren: RootRouteChildren = { ProtectedRoute: ProtectedRouteWithChildren, LoginRoute: LoginRoute, + PrivacyRoute: PrivacyRoute, SetupRoute: SetupRoute, + TermsRoute: TermsRoute, ApiAuthSplatRoute: ApiAuthSplatRoute, ApiWebhooksGithubRoute: ApiWebhooksGithubRoute, ApiGithubAppAuthorizeRoute: ApiGithubAppAuthorizeRoute, diff --git a/apps/dashboard/src/routes/privacy.tsx b/apps/dashboard/src/routes/privacy.tsx new file mode 100644 index 0000000..8cbb2a4 --- /dev/null +++ b/apps/dashboard/src/routes/privacy.tsx @@ -0,0 +1,100 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { LegalDocumentLayout } from "#/components/legal/legal-document-layout"; +import { buildSeo, formatPageTitle } from "#/lib/seo"; +import { siteConfig } from "#/lib/site-config"; + +const issuesUrl = `${siteConfig.githubRepositoryUrl}/issues`; + +export const Route = createFileRoute("/privacy")({ + head: ({ match }) => + buildSeo({ + path: match.pathname, + title: formatPageTitle("Privacy Policy"), + description: `How ${siteConfig.name} handles GitHub account data, sessions, and your information.`, + robots: "index", + }), + component: PrivacyPage, +}); + +function PrivacyPage() { + return ( + +

+ This policy describes what {siteConfig.name} (“we”, “us”) collects and + how we use it when you use our web app and related services. We process + data only as needed to provide the product and to keep your account + secure. +

+ +

What we collect

+

+ When you sign in with GitHub, we receive basic profile information that + GitHub makes available to the OAuth application (for example your GitHub + username and, where applicable, your email address). We also create and + store session and account records so you can stay signed in and use the + service. +

+

+ To show pull requests, issues, reviews, and repository context, we fetch + data from GitHub’s APIs on your behalf. We may cache or store limited + metadata and content needed to make the dashboard fast and reliable (for + example identifiers, titles, state, timestamps, and similar fields). +

+ +

Where your actions happen

+

+ Actions that change data on GitHub—such as posting comments, submitting + reviews, merging, or editing resources—are performed through GitHub’s + platform. {siteConfig.name} does not replace GitHub’s own terms or + privacy commitments for how GitHub processes data when you use{" "} + + github.com + {" "} + or GitHub’s APIs. +

+ +

How we use data

+
    +
  • Operating authentication, sessions, and security.
  • +
  • + Displaying and syncing GitHub information you choose to access in the + product. +
  • +
  • + Improving reliability and fixing errors (including limited logs). +
  • +
+ +

Sharing

+

+ We do not sell your personal information. We use infrastructure + providers (such as hosting and database services) to run the + application; they process data only to provide the service. +

+ +

Retention and deletion

+

+ We retain data for as long as your account is active and as needed for + the purposes above. You can disconnect access from GitHub’s side + according to GitHub’s settings for authorized applications. For data + held by us, contact us via{" "} + + GitHub Issues + {" "} + and we will handle reasonable requests in line with applicable law. +

+ +

Changes

+

+ We may update this policy from time to time. The “Last updated” date + below will change when we do. +

+ +
+

+ Last updated · April 12, 2026 +

+
+
+ ); +} diff --git a/apps/dashboard/src/routes/terms.tsx b/apps/dashboard/src/routes/terms.tsx new file mode 100644 index 0000000..2ef528d --- /dev/null +++ b/apps/dashboard/src/routes/terms.tsx @@ -0,0 +1,100 @@ +import { createFileRoute, Link } from "@tanstack/react-router"; +import { LegalDocumentLayout } from "#/components/legal/legal-document-layout"; +import { buildSeo, formatPageTitle } from "#/lib/seo"; +import { siteConfig } from "#/lib/site-config"; + +const issuesUrl = `${siteConfig.githubRepositoryUrl}/issues`; + +export const Route = createFileRoute("/terms")({ + head: ({ match }) => + buildSeo({ + path: match.pathname, + title: formatPageTitle("Terms of Service"), + description: `Terms for using ${siteConfig.name} and how the service relates to GitHub.`, + robots: "index", + }), + component: TermsPage, +}); + +function TermsPage() { + return ( + +

+ These terms govern your use of {siteConfig.name} (the “Service”), + offered at {siteConfig.url}. By using the Service, you agree to these + terms. +

+ +

The Service

+

+ {siteConfig.name} is a dashboard that helps you view and work with + GitHub pull requests, issues, and related activity. The Service is + provided for your use subject to these terms and our{" "} + + Privacy Policy + + . +

+ +

GitHub

+

+ You must have a GitHub account and comply with GitHub’s terms and + policies when using GitHub. Signing in through GitHub is subject to + GitHub’s authentication and permission flows. Data creation, changes, + and permissions on repositories are ultimately governed by GitHub and + your settings on{" "} + + github.com + + . +

+ +

Your responsibilities

+
    +
  • You are responsible for activity under your account.
  • +
  • + You may not misuse the Service, attempt unauthorized access, or use it + in violation of law or third-party rights. +
  • +
+ +

Availability and changes

+

+ We may modify, suspend, or discontinue features (including during early + or beta periods). We strive for reliability but do not guarantee + uninterrupted or error-free operation. +

+ +

Disclaimer

+

+ The Service is provided “as is” without warranties of any kind, to the + maximum extent permitted by law. +

+ +

Limitation of liability

+

+ To the maximum extent permitted by law, we are not liable for indirect, + incidental, special, consequential, or punitive damages, or for loss of + profits, data, or goodwill, arising from your use of the Service. +

+ +

Contact

+

+ Questions about these terms: open a discussion on{" "} + + GitHub Issues + + . +

+ + +
+ ); +} diff --git a/extensions/diffkit-redirect/README.md b/extensions/diffkit-redirect/README.md index 1e40264..cf6a164 100644 --- a/extensions/diffkit-redirect/README.md +++ b/extensions/diffkit-redirect/README.md @@ -10,6 +10,7 @@ Standalone browser extension for redirecting only selected GitHub URLs to DiffKi - Uses a configurable list of rules instead of redirecting every GitHub page - Supports both exact URL redirects and regex-based route schemas - Supports custom route remaps like GitHub PR changes pages to DiffKit review pages +- On the DiffKit web app (`diff-kit.com` and local dev), sets `data-diffkit-extension="1"` on `` so the site can hide “install extension” prompts when the add-on is already loaded ## Default rule @@ -21,12 +22,16 @@ The extension ships with these enabled rules: - `https://diff-kit.com/pulls` - `https://github.com/issues/*` - `https://diff-kit.com/issues` +- `https://github.com/:owner/:repo` (repository overview; excludes `orgs`, `settings`, etc.) +- `https://diff-kit.com/:owner/:repo` - `https://github.com/:owner/:repo/pull/:number` - `https://diff-kit.com/:owner/:repo/pull/:number` - `https://github.com/:owner/:repo/pull/:number/changes` - `https://diff-kit.com/:owner/:repo/review/:number` - `https://github.com/:owner/:repo/issues/:number` - `https://diff-kit.com/:owner/:repo/issues/:number` +- `https://github.com/:owner` (profile; excludes global pages like `/pulls`, `/explore`, etc.) +- `https://diff-kit.com/:owner` ## Rule format diff --git a/extensions/diffkit-redirect/dashboard-presence.js b/extensions/diffkit-redirect/dashboard-presence.js new file mode 100644 index 0000000..8a202e6 --- /dev/null +++ b/extensions/diffkit-redirect/dashboard-presence.js @@ -0,0 +1,12 @@ +/** + * Runs on DiffKit web app origins so the site can detect the extension via DOM + * (`data-diffkit-extension` on ). Content scripts cannot share `window` + * with the page, but DOM attributes are visible to the app. + */ +(function markDiffKitExtensionPresent() { + try { + document.documentElement.dataset.diffkitExtension = "1"; + } catch { + // ignore + } +})(); diff --git a/extensions/diffkit-redirect/manifest.json b/extensions/diffkit-redirect/manifest.json index feec289..eba61d5 100644 --- a/extensions/diffkit-redirect/manifest.json +++ b/extensions/diffkit-redirect/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "DiffKit", - "version": "0.1.0", + "version": "0.1.1", "description": "Redirect selected GitHub URLs to matching DiffKit routes.", "icons": { "16": "icons/icon-16.png", @@ -10,7 +10,12 @@ "128": "icons/icon-128.png" }, "permissions": ["storage"], - "host_permissions": ["https://github.com/*"], + "host_permissions": [ + "https://github.com/*", + "https://diff-kit.com/*", + "http://localhost:3000/*", + "http://127.0.0.1:3000/*" + ], "action": { "default_title": "DiffKit", "default_popup": "popup.html", @@ -25,6 +30,15 @@ "matches": ["https://github.com/*"], "js": ["shared.js", "content.js"], "run_at": "document_start" + }, + { + "matches": [ + "https://diff-kit.com/*", + "http://localhost:3000/*", + "http://127.0.0.1:3000/*" + ], + "js": ["dashboard-presence.js"], + "run_at": "document_start" } ] } diff --git a/extensions/diffkit-redirect/shared.js b/extensions/diffkit-redirect/shared.js index f3f4352..7ce9c86 100644 --- a/extensions/diffkit-redirect/shared.js +++ b/extensions/diffkit-redirect/shared.js @@ -43,6 +43,22 @@ url: "https://diff-kit.com/issues", }, }, + { + id: "github-repo-overview", + label: "Repository overview", + description: + "Redirect GitHub repository home (two-segment path) to DiffKit repo overview.", + enabled: true, + match: { + urlRegex: "^https://github\\.com/([^/?#]+)/([^/?#]+)/?(?:[?#].*)?$", + excludeUrlRegexes: [ + "^https://github\\.com/(?:orgs|new|settings|organizations|account)(?:/|$|[?#])", + ], + }, + redirect: { + replacement: "https://diff-kit.com/$1/$2", + }, + }, { id: "github-pull-details", label: "Pull request details", @@ -80,6 +96,22 @@ replacement: "https://diff-kit.com/$1/$2/issues/$3", }, }, + { + id: "github-user-profile", + label: "User profile", + description: + "Redirect GitHub user/org profile home (single-segment path) to DiffKit profile.", + enabled: true, + match: { + urlRegex: "^https://github\\.com/([^/?#]+)/?(?:[?#].*)?$", + excludeUrlRegexes: [ + "^https://github\\.com/(?:pulls|issues|notifications|explore|marketplace|settings|login|join|sponsors?|topics|collections|codespaces|features|enterprise|team|pricing|resources|readme|security|opensource|copilot|education|orgs|organizations|new|account|watching|dashboard|sessions)(?:/|$|[?#])", + ], + }, + redirect: { + replacement: "https://diff-kit.com/$1", + }, + }, ]; function deepClone(value) { diff --git a/packages/ui/package.json b/packages/ui/package.json index 32606fa..1f184b4 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -38,6 +38,7 @@ "@radix-ui/react-toggle": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.8", "@shikijs/rehype": "^4.0.2", + "@tailwindcss/typography": "^0.5.19", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "1.0.0", diff --git a/packages/ui/src/styles/globals.css b/packages/ui/src/styles/globals.css index f2485c3..cc16fcf 100644 --- a/packages/ui/src/styles/globals.css +++ b/packages/ui/src/styles/globals.css @@ -3,6 +3,7 @@ @import "tailwindcss"; @plugin "tailwindcss-animate"; +@plugin "@tailwindcss/typography"; @source "../../../apps/**/*.{ts,tsx}"; @source "../**/*.{ts,tsx}"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 11500b3..45e6c59 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -246,6 +246,9 @@ importers: '@shikijs/rehype': specifier: ^4.0.2 version: 4.0.2 + '@tailwindcss/typography': + specifier: ^0.5.19 + version: 0.5.19(tailwindcss@4.2.2) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -2659,6 +2662,11 @@ packages: resolution: {integrity: sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==} engines: {node: '>= 20'} + '@tailwindcss/typography@0.5.19': + resolution: {integrity: sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' + '@tailwindcss/vite@4.2.2': resolution: {integrity: sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==} peerDependencies: @@ -3324,6 +3332,11 @@ packages: resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} engines: {node: '>= 6'} + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + cssstyle@6.2.0: resolution: {integrity: sha512-Fm5NvhYathRnXNVndkUsCCuR63DCLVVwGOOwQw782coXFi5HhkXdu289l59HlXZBawsyNccXfWRYvLzcDCdDig==} engines: {node: '>=20'} @@ -4235,6 +4248,10 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} + postcss-selector-parser@6.0.10: + resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} + engines: {node: '>=4'} + postcss@8.5.8: resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} engines: {node: ^10 || ^12 || >=14} @@ -4735,6 +4752,9 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + vaul@1.1.2: resolution: {integrity: sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==} peerDependencies: @@ -6920,6 +6940,11 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.2.2 '@tailwindcss/oxide-win32-x64-msvc': 4.2.2 + '@tailwindcss/typography@0.5.19(tailwindcss@4.2.2)': + dependencies: + postcss-selector-parser: 6.0.10 + tailwindcss: 4.2.2 + '@tailwindcss/vite@4.2.2(vite@7.3.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@tailwindcss/node': 4.2.2 @@ -7668,6 +7693,8 @@ snapshots: css-what@6.2.2: {} + cssesc@3.0.0: {} + cssstyle@6.2.0: dependencies: '@asamuzakjp/css-color': 5.1.6 @@ -8782,6 +8809,11 @@ snapshots: picomatch@4.0.4: {} + postcss-selector-parser@6.0.10: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + postcss@8.5.8: dependencies: nanoid: 3.3.11 @@ -9340,6 +9372,8 @@ snapshots: dependencies: react: 19.2.4 + util-deprecate@1.0.2: {} + vaul@1.1.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)