@kortix/ui 0.14.9 → 0.14.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_vendor/kortix-shared.js +55 -2
- package/dist/_vendor/shared/artifact-comments.d.ts +235 -0
- package/dist/_vendor/shared/channel-descriptors.d.ts +96 -0
- package/dist/_vendor/shared/channel-message.d.ts +13 -2
- package/dist/_vendor/shared/index.d.ts +1 -0
- package/dist/_vendor/shared/runtime-versions.d.ts +9 -0
- package/dist/apps/app-preview-pool.js +19 -2
- package/dist/artifacts/artifact-frame.js +7 -3
- package/dist/customize/sections/component/channel-access-panel.js +4 -4
- package/dist/customize/sections/component/telegram-channel-row.js +7 -5
- package/dist/customize/sections/view/agent-editor-catalog.js +5 -0
- package/dist/customize/sections/view/channels-view.d.ts +2 -1
- package/dist/customize/sections/view/channels-view.js +12 -7
- package/dist/customize/settings/rail.js +9 -0
- package/dist/customize/settings/settings-tabs.d.ts +1 -1
- package/dist/customize/settings/settings-tabs.js +1 -0
- package/dist/customize/settings/tabs/chrome-connections-tab.d.ts +6 -0
- package/dist/customize/settings/tabs/chrome-connections-tab.js +99 -0
- package/dist/customize/settings/tabs/sandbox-template-form.js +8 -1
- package/dist/customize/shared/catalog/catalog-empty-state.d.ts +3 -3
- package/dist/customize/shared/catalog/catalog-empty-state.js +6 -4
- package/dist/files/renderers/image-frame.d.ts +3 -1
- package/dist/files/renderers/image-frame.js +2 -2
- package/dist/files/renderers/show-content-renderer.d.ts +2 -2
- package/dist/files/renderers/show-content-renderer.js +2 -1
- package/dist/i18n/en.d.ts +206 -0
- package/dist/i18n/en.js +219 -13
- package/dist/i18n/ui-catalog/remaining-ui-translation-keys.generated.js +3 -3
- package/dist/internal/apps-and-extensions/apps-modal.d.ts +9 -0
- package/dist/internal/apps-and-extensions/apps-modal.js +107 -0
- package/dist/internal/branding/branding-provider.js +3 -3
- package/dist/internal/chrome/browser-icon.d.ts +24 -0
- package/dist/internal/chrome/browser-icon.js +58 -0
- package/dist/internal/chrome/chrome-card-state.d.ts +16 -0
- package/dist/internal/chrome/chrome-card-state.js +9 -0
- package/dist/internal/chrome/chrome-connect.d.ts +69 -0
- package/dist/internal/chrome/chrome-connect.js +178 -0
- package/dist/internal/chrome/chrome-mock.d.ts +4 -0
- package/dist/internal/chrome/chrome-mock.js +15 -0
- package/dist/internal/chrome/chrome-names.d.ts +15 -0
- package/dist/internal/chrome/chrome-names.js +35 -0
- package/dist/internal/chrome/extension.d.ts +47 -0
- package/dist/internal/chrome/extension.js +91 -0
- package/dist/internal/chrome/use-chrome-offered.d.ts +11 -0
- package/dist/internal/chrome/use-chrome-offered.js +22 -0
- package/dist/internal/connector-logo/connector-logo-source.d.ts +2 -1
- package/dist/internal/connector-logo/connector-logo-source.js +7 -6
- package/dist/internal/hooks/use-notification-center.d.ts +10 -6
- package/dist/internal/hooks/use-notification-center.js +32 -10
- package/dist/internal/icons/edge.d.ts +5 -0
- package/dist/internal/icons/edge.js +10 -0
- package/dist/internal/lib/browser-noise/rules/extensions.js +17 -3
- package/dist/internal/lib/detect-os.d.ts +43 -0
- package/dist/internal/lib/detect-os.js +70 -0
- package/dist/internal/lib/site-metadata.js +4 -4
- package/dist/internal/lib/use-project-feature-flags.js +5 -1
- package/dist/internal/notifications/notification-bell.js +4 -3
- package/dist/internal/notifications/notification-rows.d.ts +1 -1
- package/dist/internal/notifications/notification-rows.js +2 -0
- package/dist/internal/stores/apps-modal-store.d.ts +8 -0
- package/dist/internal/stores/apps-modal-store.js +6 -0
- package/dist/internal/stores/composer-prefill-store.d.ts +12 -5
- package/dist/internal/stores/kortix-computer-store.d.ts +1 -1
- package/dist/markdown/setup-links/approval-decision.js +3 -1
- package/dist/primitives/empty-state.d.ts +2 -2
- package/dist/primitives/empty-state.js +6 -5
- package/dist/primitives/mermaid-renderer.js +1 -1
- package/dist/session/composer/send-stop-control.js +8 -3
- package/dist/session/header/session-site-header.js +3 -1
- package/dist/session/panel/easy/detail-view.js +4 -0
- package/dist/session/panel/mobile-tool-drawer.js +5 -3
- package/dist/session/panel/open-session-quick-view.js +2 -2
- package/dist/session/panel/panel-fallback.js +10 -7
- package/dist/session/panel/session-audit-shared.js +3 -1
- package/dist/session/panel/session-desktop.d.ts +6 -0
- package/dist/session/panel/session-desktop.js +74 -0
- package/dist/session/panel/session-panel-provider.d.ts +1 -0
- package/dist/session/panel/session-panel-provider.js +17 -1
- package/dist/session/panel/shared/use-panel-handoffs.d.ts +2 -2
- package/dist/session/panel/shared/use-panel-handoffs.js +5 -1
- package/dist/session/panel/use-session-panel-layout.js +8 -0
- package/dist/session/participants/session-participants.d.ts +3 -1
- package/dist/session/participants/session-participants.js +2 -2
- package/dist/session/prompts/approval-request.js +2 -0
- package/dist/session/status/session-tab-title.d.ts +1 -1
- package/dist/session/status/session-tab-title.js +1 -1
- package/dist/session/thread/session-chat.js +12 -0
- package/dist/session/thread/session-context-sub-sessions.js +1 -1
- package/dist/session/thread/session-thread.d.ts +13 -1
- package/dist/session/thread/session-thread.js +15 -5
- package/dist/session/thread/session-transcript-class.d.ts +5 -1
- package/dist/session/thread/session-transcript-class.js +5 -1
- package/dist/session/tool/tools/image-gen-tool.d.ts +1 -1
- package/dist/session/tool/tools/image-gen-tool.js +21 -11
- package/dist/session/turn/user-message.d.ts +7 -5
- package/dist/session/turn/user-message.js +14 -8
- package/dist/styles.css +1 -1
- package/dist/workspace/session-workspace.js +24 -5
- package/dist/workspace/sessions/session-label-badges.js +2 -1
- package/dist/workspace/sidebar/footer/project-chrome-nav.d.ts +10 -0
- package/dist/workspace/sidebar/footer/project-chrome-nav.js +56 -0
- package/dist/workspace/sidebar/project-sidebar.js +5 -1
- package/package.json +43 -3
- package/skills/kortix-ui/references/modules.md +1 -1
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* "Apps and extensions": every way to use Kortix outside this tab, in one
|
|
5
|
+
* dialog. Chrome first, then the computer, then the developer surfaces. Each
|
|
6
|
+
* row opens the dialog that already owns the flow (computer, MCP, Settings →
|
|
7
|
+
* Kortix in Chrome); nothing here pairs or connects by itself.
|
|
8
|
+
*
|
|
9
|
+
* It is mounted once, in the project sidebar, next to its only triggers: the
|
|
10
|
+
* workspace menu row, the sidebar row and `?apps=1`. Outside a project there is
|
|
11
|
+
* no workspace menu and no sidebar, so `?apps=1` there does nothing.
|
|
12
|
+
*/
|
|
13
|
+
import { useChromeConnections } from '@kortix/sdk/react';
|
|
14
|
+
import { ArrowUpRightIcon, DownloadSimpleIcon, LaptopIcon, MonitorIcon, PlugsConnectedIcon, TerminalWindowIcon, } from '@phosphor-icons/react';
|
|
15
|
+
import { useEffect, useId } from 'react';
|
|
16
|
+
import { Button } from '../../primitives/button.js';
|
|
17
|
+
import { Modal, ModalContent, ModalDescription, ModalTitle } from '../../primitives/modal.js';
|
|
18
|
+
import { ChromeCopyLink, ChromeInstallActions, ChromeOnlineDot, useChromeConnect, } from '../chrome/chrome-connect.js';
|
|
19
|
+
import { BrowserLogoTile } from '../chrome/browser-icon.js';
|
|
20
|
+
import { ChromeMock } from '../chrome/chrome-mock.js';
|
|
21
|
+
import { chromeCardState } from '../chrome/chrome-card-state.js';
|
|
22
|
+
import { chromeCounts } from '../chrome/chrome-names.js';
|
|
23
|
+
import { SolidCheckIcon } from '../icons/solid-check-icon.js';
|
|
24
|
+
import { detectPlatform, isMobilePlatform } from '../lib/detect-os.js';
|
|
25
|
+
import { useOwnsPairedComputer } from '../tunnel/computer-connect.js';
|
|
26
|
+
import { errorToast } from '../../primitives/toast.js';
|
|
27
|
+
import { useCopy } from '../hooks/use-copy.js';
|
|
28
|
+
import { useTranslations } from '../../i18n/index.js';
|
|
29
|
+
import { desktopDownloadUrl, isDesktop } from '../lib/desktop.js';
|
|
30
|
+
import { useUiHostConfig, useUiPathname, useUiRouter, useUiSearchParams, } from '../../providers/ui-provider.js';
|
|
31
|
+
import { useDeploymentCliInstallCommand } from '../lib/use-deployment-cli-install-command.js';
|
|
32
|
+
import { useAppsModalStore } from '../stores/apps-modal-store.js';
|
|
33
|
+
import { useSettingsPanelStore } from '../stores/settings-panel-store.js';
|
|
34
|
+
/** Opens the dialog from `?apps=1` and removes the parameter. */
|
|
35
|
+
export function useAppsDeepLink() {
|
|
36
|
+
const params = useUiSearchParams();
|
|
37
|
+
const pathname = useUiPathname();
|
|
38
|
+
const router = useUiRouter();
|
|
39
|
+
const wanted = params.get('apps') === '1';
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (!wanted)
|
|
42
|
+
return;
|
|
43
|
+
useAppsModalStore.getState().setOpen(true);
|
|
44
|
+
const next = new URLSearchParams(params.toString());
|
|
45
|
+
next.delete('apps');
|
|
46
|
+
const query = next.toString();
|
|
47
|
+
router.replace(query ? `${pathname}?${query}` : pathname, { scroll: false });
|
|
48
|
+
}, [wanted, params, pathname, router]);
|
|
49
|
+
}
|
|
50
|
+
export function AppsModal({ chrome, onOpenComputer, onOpenMcp, }) {
|
|
51
|
+
const t = useTranslations('appsModal');
|
|
52
|
+
const open = useAppsModalStore((state) => state.open);
|
|
53
|
+
const setOpen = useAppsModalStore((state) => state.setOpen);
|
|
54
|
+
useAppsDeepLink();
|
|
55
|
+
// Close first, then hand over: one dialog on screen at a time.
|
|
56
|
+
const hand = (next) => () => {
|
|
57
|
+
setOpen(false);
|
|
58
|
+
next();
|
|
59
|
+
};
|
|
60
|
+
return (_jsx(Modal, { open: open, onOpenChange: setOpen, children: _jsx(ModalContent, { className: "max-h-[90dvh] space-y-0 overflow-y-auto lg:max-w-2xl", children: _jsxs("div", { className: "flex flex-col gap-6 p-5 lg:p-8", children: [_jsxs("header", { className: "space-y-1.5 pr-10", children: [_jsx(ModalTitle, { className: "text-xl font-medium text-balance", children: t('title') }), _jsx(ModalDescription, { className: "text-muted-foreground text-sm text-pretty", children: t('subtitle') })] }), open ? (_jsxs(_Fragment, { children: [chrome ? _jsx(ChromeSection, { onManage: () => setOpen(false) }) : null, _jsx(ComputerSection, { onOpenComputer: hand(onOpenComputer) }), _jsx(DevelopersSection, { onOpenMcp: hand(onOpenMcp) })] })) : null] }) }) }));
|
|
61
|
+
}
|
|
62
|
+
/** The hero decides from THIS browser first. Other browsers are one line plus Manage. */
|
|
63
|
+
function ChromeSection({ onManage }) {
|
|
64
|
+
const t = useTranslations('appsModal.chrome');
|
|
65
|
+
const state = useChromeConnect();
|
|
66
|
+
const { environment, checking, installed, signedIn, connected } = state;
|
|
67
|
+
const connections = useChromeConnections();
|
|
68
|
+
const { count, online } = chromeCounts(connections.data ?? []);
|
|
69
|
+
const cardState = chromeCardState({
|
|
70
|
+
thisBrowserConnected: Boolean(connected),
|
|
71
|
+
environment,
|
|
72
|
+
checking,
|
|
73
|
+
installed: installed || signedIn,
|
|
74
|
+
});
|
|
75
|
+
const manage = (_jsx(Button, { size: "sm", variant: "secondary", "aria-label": t('manageLabel'), onClick: () => {
|
|
76
|
+
onManage();
|
|
77
|
+
useSettingsPanelStore.getState().openSettings('chrome');
|
|
78
|
+
}, children: t('manage') }));
|
|
79
|
+
return (_jsxs("section", { "aria-labelledby": "apps-modal-browser", className: "space-y-3", children: [_jsx("h3", { id: "apps-modal-browser", className: "text-muted-foreground text-xs font-medium", children: t('group') }), _jsxs("div", { className: "grid items-center gap-4 sm:grid-cols-2", children: [_jsxs("div", { className: "flex min-w-0 flex-col gap-4", children: [_jsxs("div", { className: "space-y-1.5", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(BrowserLogoTile, { brand: "chrome", className: "size-7", iconClassName: "size-4" }), _jsx("h4", { className: "text-base font-medium", children: t('name') })] }), _jsx("p", { className: "text-muted-foreground text-sm text-pretty", children: t('description') })] }), cardState === 'connected' && connected ? (_jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-2", children: [manage, _jsxs("span", { className: "flex min-w-0 items-center gap-1.5 text-xs", children: [_jsx(ChromeOnlineDot, { online: connected.online }), _jsx("span", { className: "truncate", children: connected.name }), count > 1 ? (_jsxs("span", { className: "text-muted-foreground shrink-0", children: ["\u00B7 ", t('summary', { count, online })] })) : null] })] })) : (_jsxs(_Fragment, { children: [cardState === 'unsupported' ? (_jsxs("div", { className: "space-y-2", children: [_jsx("p", { className: "text-muted-foreground text-xs text-pretty", children: t('unsupported') }), _jsx(ChromeCopyLink, {})] })) : (_jsx(ChromeInstallActions, { state: state })), count > 0 ? (_jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-2", children: [_jsx("span", { className: "text-muted-foreground text-xs", children: t('summary', { count, online }) }), manage] })) : null] }))] }), _jsx(ChromeMock, {})] })] }));
|
|
80
|
+
}
|
|
81
|
+
function ComputerSection({ onOpenComputer }) {
|
|
82
|
+
const t = useTranslations('appsModal.computer');
|
|
83
|
+
const { owned } = useOwnsPairedComputer();
|
|
84
|
+
const live = owned.some((machine) => machine.isLive);
|
|
85
|
+
const inShell = typeof window !== 'undefined' && isDesktop();
|
|
86
|
+
const platform = typeof navigator === 'undefined' ? null : detectPlatform(navigator.userAgent);
|
|
87
|
+
const download = platform && !isMobilePlatform(platform) ? desktopDownloadUrl(platform) : desktopDownloadUrl();
|
|
88
|
+
return (_jsxs(Group, { title: t('title'), children: [_jsx(AppRow, { icon: LaptopIcon, name: t('thisComputer'), description: live ? (_jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx(ChromeOnlineDot, { online: true }), t('connected')] })) : (t('thisComputerDescription')), action: _jsx(Button, { size: "sm", variant: "secondary", "aria-label": t(live ? 'manageLabel' : 'connectLabel'), onClick: onOpenComputer, children: live ? t('manage') : t('connect') }) }), _jsx(AppRow, { icon: MonitorIcon, name: t('desktop'), description: inShell ? t('desktopRunning') : t('desktopDescription'), action: inShell ? null : (_jsx(Button, { asChild: true, size: "sm", variant: "secondary", className: "gap-1.5", children: _jsxs("a", { href: download, children: [_jsx(DownloadSimpleIcon, { className: "size-3.5 shrink-0" }), t('download')] }) })) })] }));
|
|
89
|
+
}
|
|
90
|
+
function DevelopersSection({ onOpenMcp }) {
|
|
91
|
+
const t = useTranslations('appsModal.developers');
|
|
92
|
+
const command = useDeploymentCliInstallCommand(useUiHostConfig().deploymentVersion);
|
|
93
|
+
const { copy, copied } = useCopy({ toast: false });
|
|
94
|
+
const copyCommand = async () => {
|
|
95
|
+
if (!(await copy(command)))
|
|
96
|
+
errorToast(t('copyFailed'));
|
|
97
|
+
};
|
|
98
|
+
return (_jsxs(Group, { title: t('title'), children: [_jsx(AppRow, { icon: TerminalWindowIcon, name: t('cli'), description: t('cliDescription'), action: _jsxs(Button, { size: "sm", variant: "secondary", className: "gap-1.5", onClick: copyCommand, children: [copied ? _jsx(SolidCheckIcon, { className: "size-3.5" }) : null, copied ? t('copied') : t('copy')] }) }), _jsx(AppRow, { icon: PlugsConnectedIcon, name: t('mcp'), description: t('mcpDescription'), action: _jsxs(Button, { size: "sm", variant: "secondary", className: "gap-1.5", "aria-label": t('connectLabel'), onClick: onOpenMcp, children: [t('connect'), _jsx(ArrowUpRightIcon, { className: "size-3.5 shrink-0" })] }) })] }));
|
|
99
|
+
}
|
|
100
|
+
/** A muted label over plain rows: no card, no border, one rule between rows. */
|
|
101
|
+
function Group({ title, children }) {
|
|
102
|
+
const id = useId();
|
|
103
|
+
return (_jsxs("section", { "aria-labelledby": id, className: "min-w-0 space-y-1", children: [_jsx("h3", { id: id, className: "text-muted-foreground text-xs font-medium", children: title }), _jsx("ul", { className: "divide-border divide-y", children: children })] }));
|
|
104
|
+
}
|
|
105
|
+
function AppRow({ icon: RowIcon, name, description, action, }) {
|
|
106
|
+
return (_jsxs("li", { className: "flex items-center gap-3 py-3", children: [_jsx("span", { className: "bg-muted flex size-9 shrink-0 items-center justify-center rounded-sm", children: _jsx(RowIcon, { className: "size-5" }) }), _jsxs("div", { className: "min-w-0 flex-1 space-y-0.5", children: [_jsx("p", { className: "text-sm font-medium", children: name }), _jsx("div", { className: "text-muted-foreground line-clamp-2 text-xs text-pretty", children: description })] }), action] }));
|
|
107
|
+
}
|
|
@@ -90,8 +90,8 @@ const originals = new WeakMap();
|
|
|
90
90
|
* trade-off (host-based tenancy would be the way to remove it).
|
|
91
91
|
*
|
|
92
92
|
* Title: Next writes `<title>` on every navigation from the route's metadata:
|
|
93
|
-
* the site default (`Kortix –
|
|
94
|
-
* routes with no title of their own, `<page> | Kortix` elsewhere. A
|
|
93
|
+
* the site default (`Kortix CompanyOS – the open-source operating system for your company`) on
|
|
94
|
+
* routes with no title of their own, `<page> | Kortix CompanyOS` elsewhere. A
|
|
95
95
|
* `MutationObserver` on `<head>` rewrites whatever Next just wrote, so the
|
|
96
96
|
* swap survives navigation without touching every page's metadata: the site
|
|
97
97
|
* default collapses to the org name alone (its tagline is Kortix's, not
|
|
@@ -190,7 +190,7 @@ function BrandingDocumentEffect({ branding }) {
|
|
|
190
190
|
const current = document.title;
|
|
191
191
|
if (!current.includes('Kortix'))
|
|
192
192
|
return;
|
|
193
|
-
const next = current === siteMetadata.title ? appName : current.replace(/\bKortix
|
|
193
|
+
const next = current === siteMetadata.title ? appName : current.replace(/\bKortix(?: CompanyOS)?\b/g, appName);
|
|
194
194
|
if (next !== current)
|
|
195
195
|
document.title = next;
|
|
196
196
|
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** @internal Kortix apps only. No semver: it may change in any release. */
|
|
2
|
+
type BrowserKey = 'chrome' | 'brave' | 'arc' | 'opera' | 'vivaldi' | 'edge';
|
|
3
|
+
/** Maps the API's `brand` string (Chrome, Edge, Brave, Arc, Dia, …) to a mark. Any other Chromium browser gets Chrome. */
|
|
4
|
+
export declare function browserKey(brand: string | null | undefined): BrowserKey;
|
|
5
|
+
/**
|
|
6
|
+
* The browser's mark. `tone="brand"` paints the official color; `tone="current"`
|
|
7
|
+
* paints `currentColor`, for the monochrome sidebar and settings rail.
|
|
8
|
+
*/
|
|
9
|
+
export declare function BrowserIcon({ brand, tone, className, }: {
|
|
10
|
+
brand?: string | null;
|
|
11
|
+
tone?: 'brand' | 'current';
|
|
12
|
+
className?: string;
|
|
13
|
+
}): import("react").JSX.Element;
|
|
14
|
+
/** The browser mark on a white tile. `className` sets the tile size and radius. */
|
|
15
|
+
export declare function BrowserLogoTile({ brand, className, iconClassName, }: {
|
|
16
|
+
brand?: string | null;
|
|
17
|
+
className?: string;
|
|
18
|
+
iconClassName?: string;
|
|
19
|
+
}): import("react").JSX.Element;
|
|
20
|
+
/** Monochrome Chrome mark with the `Icon` call shape the settings rail expects. */
|
|
21
|
+
export declare function ChromeRailIcon({ className }: {
|
|
22
|
+
className?: string;
|
|
23
|
+
}): import("react").JSX.Element;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Edge } from '../icons/edge.js';
|
|
3
|
+
import { cn } from '../lib/utils.js';
|
|
4
|
+
/** A third-party logo tile is white in both themes (color.md). */
|
|
5
|
+
const LOGO_TILE_BACKGROUND = 'bg-white';
|
|
6
|
+
/**
|
|
7
|
+
* Official brand colors, from simple-icons (CC0). The SVG files in
|
|
8
|
+
* `public/browser-icons/` are the same set. Edge is not in simple-icons 13:
|
|
9
|
+
* it renders the house full-colour `Edge` mark instead.
|
|
10
|
+
*/
|
|
11
|
+
const CHROME_BLUE = '#4285F4';
|
|
12
|
+
const BRAVE_ORANGE = '#FB542B';
|
|
13
|
+
const ARC_PINK = '#FCBFBD';
|
|
14
|
+
const OPERA_RED = '#FF1B2D';
|
|
15
|
+
const VIVALDI_RED = '#EF3939';
|
|
16
|
+
const BROWSER_BRAND_HEX = {
|
|
17
|
+
chrome: CHROME_BLUE,
|
|
18
|
+
brave: BRAVE_ORANGE,
|
|
19
|
+
arc: ARC_PINK,
|
|
20
|
+
opera: OPERA_RED,
|
|
21
|
+
vivaldi: VIVALDI_RED,
|
|
22
|
+
};
|
|
23
|
+
const BROWSER_KEYS = ['brave', 'arc', 'opera', 'vivaldi', 'edge', 'chrome'];
|
|
24
|
+
/** Maps the API's `brand` string (Chrome, Edge, Brave, Arc, Dia, …) to a mark. Any other Chromium browser gets Chrome. */
|
|
25
|
+
export function browserKey(brand) {
|
|
26
|
+
const name = (brand ?? '').toLowerCase();
|
|
27
|
+
return BROWSER_KEYS.find((key) => name.includes(key)) ?? 'chrome';
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The browser's mark. `tone="brand"` paints the official color; `tone="current"`
|
|
31
|
+
* paints `currentColor`, for the monochrome sidebar and settings rail.
|
|
32
|
+
*/
|
|
33
|
+
export function BrowserIcon({ brand, tone = 'brand', className, }) {
|
|
34
|
+
const key = browserKey(brand);
|
|
35
|
+
if (key === 'edge' && tone === 'brand')
|
|
36
|
+
return _jsx(Edge, { className: className });
|
|
37
|
+
const file = key === 'edge' ? 'chrome' : key;
|
|
38
|
+
const url = `url(/browser-icons/${file}.svg)`;
|
|
39
|
+
return (_jsx("span", { "aria-hidden": "true", className: cn('inline-block size-4 shrink-0', tone === 'current' && 'bg-current', className), style: {
|
|
40
|
+
backgroundColor: tone === 'brand' && key !== 'edge' ? BROWSER_BRAND_HEX[key] : undefined,
|
|
41
|
+
maskImage: url,
|
|
42
|
+
WebkitMaskImage: url,
|
|
43
|
+
maskSize: 'contain',
|
|
44
|
+
WebkitMaskSize: 'contain',
|
|
45
|
+
maskRepeat: 'no-repeat',
|
|
46
|
+
WebkitMaskRepeat: 'no-repeat',
|
|
47
|
+
maskPosition: 'center',
|
|
48
|
+
WebkitMaskPosition: 'center',
|
|
49
|
+
} }));
|
|
50
|
+
}
|
|
51
|
+
/** The browser mark on a white tile. `className` sets the tile size and radius. */
|
|
52
|
+
export function BrowserLogoTile({ brand, className, iconClassName, }) {
|
|
53
|
+
return (_jsx("span", { className: cn(LOGO_TILE_BACKGROUND, 'ring-border flex size-9 shrink-0 items-center justify-center rounded-sm ring-1 ring-inset', className), children: _jsx(BrowserIcon, { brand: brand, className: cn('size-5', iconClassName) }) }));
|
|
54
|
+
}
|
|
55
|
+
/** Monochrome Chrome mark with the `Icon` call shape the settings rail expects. */
|
|
56
|
+
export function ChromeRailIcon({ className }) {
|
|
57
|
+
return _jsx(BrowserIcon, { brand: "chrome", tone: "current", className: className });
|
|
58
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** @internal Kortix apps only. No semver: it may change in any release. */
|
|
2
|
+
/**
|
|
3
|
+
* Which call to action the Chrome card shows. Pure, so the three states
|
|
4
|
+
* (not installed, installed, connected) are testable without a browser.
|
|
5
|
+
*/
|
|
6
|
+
export type ChromeCardState = 'connected' | 'unsupported' | 'checking' | 'installed' | 'notInstalled';
|
|
7
|
+
export declare function chromeCardState(input: {
|
|
8
|
+
/** THIS browser is connected. Other connected browsers do not count. */
|
|
9
|
+
thisBrowserConnected: boolean;
|
|
10
|
+
/** `unsupported`: Safari, Firefox, the desktop shell. */
|
|
11
|
+
environment: 'checking' | 'supported' | 'unsupported';
|
|
12
|
+
/** The first ping has not answered yet. */
|
|
13
|
+
checking: boolean;
|
|
14
|
+
/** The extension answered a ping, or a browser connected. */
|
|
15
|
+
installed: boolean;
|
|
16
|
+
}): ChromeCardState;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function chromeCardState(input) {
|
|
2
|
+
if (input.thisBrowserConnected)
|
|
3
|
+
return 'connected';
|
|
4
|
+
if (input.environment === 'unsupported')
|
|
5
|
+
return 'unsupported';
|
|
6
|
+
if (input.checking)
|
|
7
|
+
return 'checking';
|
|
8
|
+
return input.installed ? 'installed' : 'notInstalled';
|
|
9
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** @internal Kortix apps only. No semver: it may change in any release. */
|
|
2
|
+
import { type ChromeConnection } from '@kortix/sdk';
|
|
3
|
+
type Environment = 'checking' | 'supported' | 'unsupported';
|
|
4
|
+
/**
|
|
5
|
+
* The live state of the three steps: the extension is installed (ping every
|
|
6
|
+
* 2 s), the extension is signed in, and the new browser shows in the list
|
|
7
|
+
* (poll every 3 s). Mount it only while the steps are on screen.
|
|
8
|
+
*/
|
|
9
|
+
export type ChromeConnectState = ReturnType<typeof useChromeConnect>;
|
|
10
|
+
/**
|
|
11
|
+
* `fresh`: the person adds ANOTHER browser. This browser's own extension state
|
|
12
|
+
* does not count: no ping, and only a browser that was not in the list when the
|
|
13
|
+
* dialog opened completes it.
|
|
14
|
+
*/
|
|
15
|
+
export declare function useChromeConnect({ fresh }?: {
|
|
16
|
+
fresh?: boolean;
|
|
17
|
+
}): {
|
|
18
|
+
environment: Environment;
|
|
19
|
+
/** The first ping has not answered yet. */
|
|
20
|
+
checking: boolean;
|
|
21
|
+
installed: boolean;
|
|
22
|
+
signedIn: boolean;
|
|
23
|
+
connected: ChromeConnection | undefined;
|
|
24
|
+
connect: import("@tanstack/react-query").UseMutationResult<import("./extension.js").ExtensionConnectResult, Error, void, unknown>;
|
|
25
|
+
};
|
|
26
|
+
/** The Chrome mark on a small white tile, for a button label. */
|
|
27
|
+
export declare function ChromeButtonMark(): import("react").JSX.Element;
|
|
28
|
+
export declare function ChromeOnlineDot({ online, className }: {
|
|
29
|
+
online: boolean;
|
|
30
|
+
className?: string;
|
|
31
|
+
}): import("react").JSX.Element;
|
|
32
|
+
/** Copies the link that opens Settings → Kortix in Chrome, for a browser that cannot run the extension. */
|
|
33
|
+
export declare function ChromeCopyLink({ size, className }: {
|
|
34
|
+
size?: 'sm' | 'lg';
|
|
35
|
+
className?: string;
|
|
36
|
+
}): import("react").JSX.Element;
|
|
37
|
+
/**
|
|
38
|
+
* The action that moves this browser forward: Connect once the extension is
|
|
39
|
+
* there, otherwise Connect plus "How to install" (or "Add to Chrome" once the
|
|
40
|
+
* Web Store listing is public). Renders nothing for a connected browser.
|
|
41
|
+
*/
|
|
42
|
+
export declare function ChromeInstallActions({ state, size, className, showGuide, }: {
|
|
43
|
+
state: ChromeConnectState;
|
|
44
|
+
size?: 'sm' | 'lg';
|
|
45
|
+
className?: string;
|
|
46
|
+
/** The "How to install" link. Off where the page already links the docs. */
|
|
47
|
+
showGuide?: boolean;
|
|
48
|
+
}): import("react").JSX.Element | null;
|
|
49
|
+
/**
|
|
50
|
+
* The three steps and the one action that moves them. Used in the connect
|
|
51
|
+
* dialog and, with no browser connected yet, inline in Settings → Kortix in
|
|
52
|
+
* Chrome (`showActions={false}`: the page header holds the action there).
|
|
53
|
+
*/
|
|
54
|
+
export declare function ChromeConnectSteps({ state, onDone, showActions, }: {
|
|
55
|
+
state: ChromeConnectState;
|
|
56
|
+
onDone?: () => void;
|
|
57
|
+
showActions?: boolean;
|
|
58
|
+
}): import("react").JSX.Element;
|
|
59
|
+
/**
|
|
60
|
+
* "Add Kortix to Chrome": add the Kortix extension to Chrome, sign it in, and
|
|
61
|
+
* watch the browser appear. `fresh`: add ANOTHER browser. Same frame as
|
|
62
|
+
* `ComputerConnectModal`.
|
|
63
|
+
*/
|
|
64
|
+
export declare function ChromeConnectModal({ open, onOpenChange, fresh, }: {
|
|
65
|
+
open: boolean;
|
|
66
|
+
onOpenChange: (open: boolean) => void;
|
|
67
|
+
fresh?: boolean;
|
|
68
|
+
}): import("react").JSX.Element;
|
|
69
|
+
export {};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { platformConfig } from '@kortix/sdk';
|
|
4
|
+
import { useChromeConnections } from '@kortix/sdk/react';
|
|
5
|
+
import { AppWindowIcon, ArrowUpRightIcon, LinkSimpleIcon, PuzzlePieceIcon, SignInIcon, } from '@phosphor-icons/react';
|
|
6
|
+
import { useMutation, useQuery } from '@tanstack/react-query';
|
|
7
|
+
import { useState, useSyncExternalStore } from 'react';
|
|
8
|
+
import { Button } from '../../primitives/button.js';
|
|
9
|
+
import Loading from '../../primitives/loading.js';
|
|
10
|
+
import { Modal, ModalContent, ModalDescription, ModalTitle } from '../../primitives/modal.js';
|
|
11
|
+
import { BeamsShader } from '../../primitives/paper-wallpaper-shaders.js';
|
|
12
|
+
import { errorToast } from '../../primitives/toast.js';
|
|
13
|
+
import { SolidCheckIcon } from '../icons/solid-check-icon.js';
|
|
14
|
+
import { useCopy } from '../hooks/use-copy.js';
|
|
15
|
+
import { useTranslations } from '../../i18n/index.js';
|
|
16
|
+
import { isDesktop } from '../lib/desktop.js';
|
|
17
|
+
import { cn } from '../lib/utils.js';
|
|
18
|
+
import { BrowserIcon, BrowserLogoTile } from './browser-icon.js';
|
|
19
|
+
import { apiOrigin, CHROME_INSTALL_DOCS_PATH, CHROME_STORE_LISTED, chromeSettingsLink, connectExtension, isChromiumBrowser, KORTIX_EXTENSION_STORE_URL, pingExtension, } from './extension.js';
|
|
20
|
+
const subscribeNever = () => () => { };
|
|
21
|
+
function detectEnvironment() {
|
|
22
|
+
return isChromiumBrowser(navigator.userAgent) && !isDesktop() ? 'supported' : 'unsupported';
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* `fresh`: the person adds ANOTHER browser. This browser's own extension state
|
|
26
|
+
* does not count: no ping, and only a browser that was not in the list when the
|
|
27
|
+
* dialog opened completes it.
|
|
28
|
+
*/
|
|
29
|
+
export function useChromeConnect({ fresh = false } = {}) {
|
|
30
|
+
const t = useTranslations('chrome');
|
|
31
|
+
const environment = useSyncExternalStore(subscribeNever, detectEnvironment, () => 'checking');
|
|
32
|
+
const ping = useQuery({
|
|
33
|
+
queryKey: ['chrome-extension-ping'],
|
|
34
|
+
queryFn: () => pingExtension(),
|
|
35
|
+
enabled: environment === 'supported' && !fresh,
|
|
36
|
+
refetchInterval: 2_000,
|
|
37
|
+
staleTime: 0,
|
|
38
|
+
gcTime: 0,
|
|
39
|
+
});
|
|
40
|
+
const connections = useChromeConnections({ refetchInterval: 3_000 });
|
|
41
|
+
// The browsers that existed when the steps appeared. A browser outside it is
|
|
42
|
+
// the one this person is connecting now.
|
|
43
|
+
const [baseline, setBaseline] = useState(null);
|
|
44
|
+
if (baseline === null && connections.isSuccess) {
|
|
45
|
+
setBaseline(new Set(connections.data.map((chrome) => chrome.id)));
|
|
46
|
+
}
|
|
47
|
+
const status = fresh ? null : (ping.data ?? null);
|
|
48
|
+
const connected = connections.data?.find((chrome) => baseline && !baseline.has(chrome.id)) ??
|
|
49
|
+
// The extension keeps its browser id after a sign-out: trust it only while signed in.
|
|
50
|
+
(status?.connected && status.chromeId
|
|
51
|
+
? connections.data?.find((chrome) => chrome.id === status.chromeId && chrome.online)
|
|
52
|
+
: undefined);
|
|
53
|
+
const connect = useMutation({
|
|
54
|
+
// A retry would open the sign-in window again.
|
|
55
|
+
retry: false,
|
|
56
|
+
mutationFn: () => connectExtension(apiOrigin(platformConfig().backendUrl || '', window.location.origin)),
|
|
57
|
+
onSuccess: (result) => {
|
|
58
|
+
if (!result.ok) {
|
|
59
|
+
// `unavailable`: the page could not reach the extension.
|
|
60
|
+
errorToast(t(result.error === 'unavailable' ? 'extensionMissing' : 'connectFailed'));
|
|
61
|
+
}
|
|
62
|
+
void ping.refetch();
|
|
63
|
+
void connections.refetch();
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
return {
|
|
67
|
+
environment,
|
|
68
|
+
/** The first ping has not answered yet. */
|
|
69
|
+
checking: environment === 'checking' || (environment === 'supported' && !fresh && !ping.isFetched),
|
|
70
|
+
// A page opened before the install cannot reach the extension until it
|
|
71
|
+
// reloads. The extension's own tab finishes the sign-in, and the browser
|
|
72
|
+
// then shows up in the list, so a connected browser completes every step.
|
|
73
|
+
installed: status !== null || Boolean(connected),
|
|
74
|
+
signedIn: status?.connected === true || Boolean(connected),
|
|
75
|
+
connected,
|
|
76
|
+
connect,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** The Chrome mark on a small white tile, for a button label. */
|
|
80
|
+
export function ChromeButtonMark() {
|
|
81
|
+
return _jsx(BrowserLogoTile, { brand: "chrome", className: "size-5 shrink-0 ring-0", iconClassName: "size-3.5" });
|
|
82
|
+
}
|
|
83
|
+
function StepTile({ done, icon: StepIcon, brand }) {
|
|
84
|
+
// The last step shows the connected browser's own mark.
|
|
85
|
+
if (done && brand)
|
|
86
|
+
return _jsx(BrowserLogoTile, { brand: brand, className: "size-10" });
|
|
87
|
+
return (_jsx("span", { className: cn('flex size-10 shrink-0 items-center justify-center rounded-sm', done ? 'bg-kortix-green/15' : 'bg-muted text-muted-foreground'), children: done ? _jsx(SolidCheckIcon, { className: "text-kortix-green size-5" }) : _jsx(StepIcon, { className: "size-5" }) }));
|
|
88
|
+
}
|
|
89
|
+
export function ChromeOnlineDot({ online, className }) {
|
|
90
|
+
const t = useTranslations('chrome');
|
|
91
|
+
return (_jsx("span", { role: "img", "aria-label": online ? t('online') : t('offline'), className: cn('inline-block size-2 shrink-0 rounded-full', online ? 'bg-kortix-green' : 'bg-muted-foreground', className) }));
|
|
92
|
+
}
|
|
93
|
+
/** Copies the link that opens Settings → Kortix in Chrome, for a browser that cannot run the extension. */
|
|
94
|
+
export function ChromeCopyLink({ size = 'sm', className }) {
|
|
95
|
+
const t = useTranslations('chrome');
|
|
96
|
+
const { copy, copied } = useCopy({ toast: false });
|
|
97
|
+
return (_jsxs(Button, { variant: "secondary", size: size, className: cn('gap-1.5', className), onClick: async () => {
|
|
98
|
+
if (!(await copy(chromeSettingsLink(window.location.origin))))
|
|
99
|
+
errorToast(t('copyFailed'));
|
|
100
|
+
}, children: [copied ? _jsx(SolidCheckIcon, { className: "size-3.5 shrink-0" }) : _jsx(LinkSimpleIcon, { className: "size-3.5 shrink-0" }), copied ? t('linkCopied') : t('copyLink')] }));
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The action that moves this browser forward: Connect once the extension is
|
|
104
|
+
* there, otherwise Connect plus "How to install" (or "Add to Chrome" once the
|
|
105
|
+
* Web Store listing is public). Renders nothing for a connected browser.
|
|
106
|
+
*/
|
|
107
|
+
export function ChromeInstallActions({ state, size = 'sm', className, showGuide = true, }) {
|
|
108
|
+
const t = useTranslations('chrome');
|
|
109
|
+
const { checking, installed, connected, connect } = state;
|
|
110
|
+
const [openedStore, setOpenedStore] = useState(false);
|
|
111
|
+
if (connected)
|
|
112
|
+
return null;
|
|
113
|
+
const full = size === 'lg' ? 'w-full' : '';
|
|
114
|
+
const connectButton = (_jsxs(Button, { size: size, className: full, disabled: connect.isPending, onClick: () => connect.mutate(), children: [connect.isPending ? _jsx(Loading, { className: "size-4 shrink-0" }) : null, connect.isPending ? t('connecting') : t('connect')] }));
|
|
115
|
+
if (installed)
|
|
116
|
+
return _jsx("div", { className: className, children: connectButton });
|
|
117
|
+
if (CHROME_STORE_LISTED) {
|
|
118
|
+
return (_jsxs("div", { className: cn('space-y-2', className), children: [_jsx(Button, { asChild: true, size: size, className: full, "aria-disabled": checking, children: _jsxs("a", { href: KORTIX_EXTENSION_STORE_URL, target: "_blank", rel: "noreferrer", onClick: () => setOpenedStore(true), children: [_jsx(ChromeButtonMark, {}), t('addToChrome')] }) }), openedStore ? (_jsx("p", { className: "text-muted-foreground text-center text-xs text-pretty", children: t('finishInChrome') })) : null] }));
|
|
119
|
+
}
|
|
120
|
+
return (_jsxs("div", { className: cn(size === 'lg' ? 'flex flex-col gap-2' : 'flex flex-wrap items-center gap-x-3 gap-y-2', className), children: [connectButton, showGuide ? (_jsx(Button, { asChild: true, variant: "ghost", size: size, className: cn('gap-1.5', size === 'sm' && 'text-muted-foreground'), children: _jsxs("a", { href: CHROME_INSTALL_DOCS_PATH, target: "_blank", rel: "noreferrer", children: [t('howToInstall'), _jsx(ArrowUpRightIcon, { className: "size-3.5 shrink-0" })] }) })) : null] }));
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The three steps and the one action that moves them. Used in the connect
|
|
124
|
+
* dialog and, with no browser connected yet, inline in Settings → Kortix in
|
|
125
|
+
* Chrome (`showActions={false}`: the page header holds the action there).
|
|
126
|
+
*/
|
|
127
|
+
export function ChromeConnectSteps({ state, onDone, showActions = true, }) {
|
|
128
|
+
const t = useTranslations('chrome');
|
|
129
|
+
const { environment, installed, signedIn, connected } = state;
|
|
130
|
+
if (environment === 'unsupported') {
|
|
131
|
+
return (_jsxs("div", { className: "mt-auto space-y-3", children: [_jsx("p", { className: "text-muted-foreground text-sm text-pretty", children: t('unsupported') }), showActions ? _jsx(ChromeCopyLink, { size: "lg", className: "w-full" }) : null] }));
|
|
132
|
+
}
|
|
133
|
+
const steps = [
|
|
134
|
+
{
|
|
135
|
+
key: 'install',
|
|
136
|
+
icon: PuzzlePieceIcon,
|
|
137
|
+
done: installed,
|
|
138
|
+
description: installed ? (t('steps.install.done')) : CHROME_STORE_LISTED ? (t('steps.install.description')) : (_jsxs(_Fragment, { children: [t('steps.install.descriptionUnlisted'), showActions ? null : (_jsxs(_Fragment, { children: [' ', _jsx("a", { href: CHROME_INSTALL_DOCS_PATH, target: "_blank", rel: "noreferrer", className: "text-foreground underline underline-offset-2", children: t('howToInstall') })] }))] })),
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
key: 'connect',
|
|
142
|
+
icon: SignInIcon,
|
|
143
|
+
done: signedIn,
|
|
144
|
+
description: t(signedIn ? 'steps.connect.done' : 'steps.connect.description'),
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
key: 'done',
|
|
148
|
+
icon: AppWindowIcon,
|
|
149
|
+
done: Boolean(connected),
|
|
150
|
+
brand: connected?.brand,
|
|
151
|
+
description: connected ? (_jsxs("span", { className: "flex min-w-0 items-center gap-1.5", children: [_jsx(ChromeOnlineDot, { online: connected.online }), _jsx("span", { className: "truncate", children: connected.name })] })) : (t('steps.done.description')),
|
|
152
|
+
},
|
|
153
|
+
];
|
|
154
|
+
return (_jsxs(_Fragment, { children: [_jsx("ol", { className: "divide-border divide-y", children: steps.map(({ key, icon, done, brand, description }) => (_jsxs("li", { className: "flex items-center gap-3 py-3", children: [_jsx(StepTile, { done: done, icon: icon, brand: brand }), _jsxs("div", { className: "min-w-0 flex-1 space-y-0.5", children: [_jsx("p", { className: "text-sm font-medium", children: t(`steps.${key}.title`) }), _jsx("div", { className: "text-muted-foreground line-clamp-2 text-xs text-pretty", children: description })] })] }, key))) }), showActions || connected ? (_jsx("div", { className: "mt-auto space-y-2", children: connected ? (onDone ? (_jsx(Button, { size: "lg", className: "w-full", onClick: onDone, children: t('done') })) : null) : (_jsx(ChromeInstallActions, { state: state, size: "lg" })) })) : null] }));
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* "Add another browser": the other browser, or another Chrome profile, signs
|
|
158
|
+
* itself in. This page cannot do that for it, so the dialog says where to go
|
|
159
|
+
* and waits for the new browser to show up in the list.
|
|
160
|
+
*/
|
|
161
|
+
function ChromeAddAnother({ onDone }) {
|
|
162
|
+
const t = useTranslations('chrome');
|
|
163
|
+
const { connected } = useChromeConnect({ fresh: true });
|
|
164
|
+
return (_jsxs(_Fragment, { children: [_jsx("ol", { className: "divide-border divide-y", children: _jsxs("li", { className: "flex items-center gap-3 py-3", children: [_jsx(StepTile, { done: Boolean(connected), icon: AppWindowIcon, brand: connected?.brand }), _jsxs("div", { className: "min-w-0 flex-1 space-y-0.5", children: [_jsx("p", { className: "text-sm font-medium", children: t('another.title') }), _jsx("div", { className: "text-muted-foreground line-clamp-2 text-xs text-pretty", children: connected ? (_jsxs("span", { className: "flex min-w-0 items-center gap-1.5", children: [_jsx(ChromeOnlineDot, { online: connected.online }), _jsx("span", { className: "truncate", children: connected.name })] })) : (t('another.waiting')) })] })] }) }), _jsx("div", { className: "mt-auto space-y-2", children: connected ? (onDone ? (_jsx(Button, { size: "lg", className: "w-full", onClick: onDone, children: t('done') })) : null) : (_jsx(ChromeCopyLink, { size: "lg", className: "w-full" })) })] }));
|
|
165
|
+
}
|
|
166
|
+
function ChromeConnectBody({ onDone }) {
|
|
167
|
+
const state = useChromeConnect();
|
|
168
|
+
return _jsx(ChromeConnectSteps, { state: state, onDone: onDone });
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* "Add Kortix to Chrome": add the Kortix extension to Chrome, sign it in, and
|
|
172
|
+
* watch the browser appear. `fresh`: add ANOTHER browser. Same frame as
|
|
173
|
+
* `ComputerConnectModal`.
|
|
174
|
+
*/
|
|
175
|
+
export function ChromeConnectModal({ open, onOpenChange, fresh = false, }) {
|
|
176
|
+
const t = useTranslations('chrome');
|
|
177
|
+
return (_jsx(Modal, { open: open, onOpenChange: onOpenChange, children: _jsx(ModalContent, { className: "space-y-0 lg:max-w-3xl", closeClassName: "max-sm:bg-background border-0 overflow-hidden", children: _jsxs("div", { className: "grid sm:grid-cols-5 lg:min-h-128", children: [_jsxs("div", { "aria-hidden": "true", className: "dark bg-background relative isolate flex h-48 items-center justify-center overflow-hidden rounded-t-xl border-b sm:col-span-2 sm:h-auto sm:rounded-tr-none sm:border-r sm:border-b-0 lg:rounded-bl-xl", children: [_jsx(BeamsShader, {}), _jsxs("span", { className: "bg-foreground text-background relative flex items-center gap-2 rounded-full px-4 py-2 text-sm font-medium", children: [_jsx(BrowserIcon, { brand: "chrome", tone: "current", className: "size-4" }), t('pill')] })] }), _jsxs("div", { className: "flex min-w-0 flex-col gap-5 p-5 sm:col-span-3 lg:p-8", children: [_jsxs("header", { className: "space-y-1.5 pr-10", children: [_jsx(ModalTitle, { className: "text-lg font-medium text-balance", children: t('connectTitle') }), _jsx(ModalDescription, { className: "text-pretty", children: t(fresh ? 'another.description' : 'connectDescription') })] }), open ? (fresh ? (_jsx(ChromeAddAnother, { onDone: () => onOpenChange(false) })) : (_jsx(ChromeConnectBody, { onDone: () => onOpenChange(false) }))) : null] })] }) }) }));
|
|
178
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { CursorIcon } from '@phosphor-icons/react';
|
|
3
|
+
import { useTranslations } from '../../i18n/index.js';
|
|
4
|
+
import { cn } from '../lib/utils.js';
|
|
5
|
+
/**
|
|
6
|
+
* What the person sees while a session works in their chrome: the Kortix tab
|
|
7
|
+
* group, the page with a halo, the agent cursor and the control bar. Static,
|
|
8
|
+
* decorative, built from tokens. Blue marks agent state only. It mirrors the
|
|
9
|
+
* extension's welcome page (`apps/chrome-extension/src/pages/welcome.html`).
|
|
10
|
+
*/
|
|
11
|
+
const MOCK_URL = 'shop.example.com/returns';
|
|
12
|
+
export function ChromeMock({ className }) {
|
|
13
|
+
const t = useTranslations('appsModal.mock');
|
|
14
|
+
return (_jsx("div", { "aria-hidden": "true", className: cn('bg-muted rounded-md p-3 sm:p-4', className), children: _jsxs("div", { className: "bg-background overflow-hidden rounded-sm border", children: [_jsx("div", { className: "bg-card flex items-center gap-2 border-b px-2 py-1.5", children: _jsxs("span", { className: "bg-kortix-base/15 flex min-w-0 items-center gap-1.5 rounded-sm px-2 py-0.5 text-xs", children: [_jsx("span", { className: "bg-kortix-base size-1.5 shrink-0 rounded-full" }), _jsx("span", { className: "truncate", children: t('group') })] }) }), _jsx("div", { className: "border-b px-2 py-1.5", children: _jsx("span", { className: "bg-muted text-muted-foreground block truncate rounded-sm px-2 py-0.5 font-mono text-xs", children: MOCK_URL }) }), _jsxs("div", { className: "relative space-y-3 px-3 pt-3 pb-14 sm:px-4 sm:pt-4", children: [_jsx("span", { className: "bg-muted block h-2 w-24 rounded-sm" }), _jsxs("div", { className: "flex items-center justify-between gap-3 rounded-sm border p-3", children: [_jsxs("span", { className: "space-y-1.5", children: [_jsx("span", { className: "bg-muted block h-2 w-20 rounded-sm" }), _jsx("span", { className: "bg-muted block h-2 w-12 rounded-sm" })] }), _jsxs("span", { className: "border-ring ring-ring/15 relative rounded-sm border px-2 py-1 text-xs ring-3", children: [t('button'), _jsxs("span", { className: "absolute top-full right-1 -mt-0.5 flex flex-col items-end", children: [_jsx(CursorIcon, { weight: "fill", className: "text-kortix-base size-4" }), _jsx("span", { className: "bg-popover -mt-0.5 rounded-sm border px-1.5 py-0.5 text-xs", children: "Kortix" })] })] })] }), _jsxs("span", { className: "bg-popover absolute bottom-3 left-1/2 flex -translate-x-1/2 items-center gap-2 rounded-full border px-3 py-1 text-xs whitespace-nowrap", children: [_jsx("span", { className: "bg-kortix-base size-1.5 rounded-full" }), t('working'), _jsx("span", { className: "text-muted-foreground", children: t('takeOver') }), _jsx("span", { className: "text-muted-foreground", children: t('stop') })] })] })] }) }));
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** @internal Kortix apps only. No semver: it may change in any release. */
|
|
2
|
+
import type { ChromeConnection } from '@kortix/sdk';
|
|
3
|
+
/**
|
|
4
|
+
* Two browsers can share a default name ("Chromium on macOS"). Where they do,
|
|
5
|
+
* the list tells them apart: the profile label when it has one, else a count
|
|
6
|
+
* in connection order. The stored name never changes; this is display only.
|
|
7
|
+
*/
|
|
8
|
+
export declare function chromeDisplayNames(list: readonly ChromeConnection[]): Map<string, string>;
|
|
9
|
+
/** "N browsers · M online" for the Apps modal. */
|
|
10
|
+
export declare function chromeCounts(list: readonly ChromeConnection[]): {
|
|
11
|
+
count: number;
|
|
12
|
+
online: number;
|
|
13
|
+
};
|
|
14
|
+
/** Offline for longer than `thresholdMs`: only then is "last seen" worth saying. */
|
|
15
|
+
export declare function offlineLongerThan(lastSeenAt: string | null, thresholdMs: number, now?: number): boolean;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two browsers can share a default name ("Chromium on macOS"). Where they do,
|
|
3
|
+
* the list tells them apart: the profile label when it has one, else a count
|
|
4
|
+
* in connection order. The stored name never changes; this is display only.
|
|
5
|
+
*/
|
|
6
|
+
export function chromeDisplayNames(list) {
|
|
7
|
+
const byName = new Map();
|
|
8
|
+
for (const chrome of list)
|
|
9
|
+
byName.set(chrome.name, [...(byName.get(chrome.name) ?? []), chrome]);
|
|
10
|
+
const out = new Map();
|
|
11
|
+
for (const chrome of list) {
|
|
12
|
+
const same = byName.get(chrome.name) ?? [];
|
|
13
|
+
if (same.length < 2) {
|
|
14
|
+
out.set(chrome.id, chrome.name);
|
|
15
|
+
}
|
|
16
|
+
else if (chrome.profileLabel && !chrome.name.includes(chrome.profileLabel)) {
|
|
17
|
+
out.set(chrome.id, `${chrome.name} · ${chrome.profileLabel}`);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
out.set(chrome.id, `${chrome.name} #${same.indexOf(chrome) + 1}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
/** "N browsers · M online" for the Apps modal. */
|
|
26
|
+
export function chromeCounts(list) {
|
|
27
|
+
return { count: list.length, online: list.filter((chrome) => chrome.online).length };
|
|
28
|
+
}
|
|
29
|
+
/** Offline for longer than `thresholdMs`: only then is "last seen" worth saying. */
|
|
30
|
+
export function offlineLongerThan(lastSeenAt, thresholdMs, now = Date.now()) {
|
|
31
|
+
if (!lastSeenAt)
|
|
32
|
+
return false;
|
|
33
|
+
const seen = Date.parse(lastSeenAt);
|
|
34
|
+
return Number.isFinite(seen) && now - seen > thresholdMs;
|
|
35
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** @internal Kortix apps only. No semver: it may change in any release. */
|
|
2
|
+
/**
|
|
3
|
+
* The Kortix Chrome extension, seen from the web app.
|
|
4
|
+
*
|
|
5
|
+
* A page talks to the extension through `chrome.runtime.sendMessage(id, …)`
|
|
6
|
+
* (`externally_connectable`). Chrome exposes `chrome.runtime` to a page only
|
|
7
|
+
* when the extension is installed and lists the page's origin. Safari, Firefox
|
|
8
|
+
* and the desktop shell never expose it, so `pingExtension` answers `null`.
|
|
9
|
+
*/
|
|
10
|
+
export declare const KORTIX_EXTENSION_ID: string;
|
|
11
|
+
export declare const KORTIX_EXTENSION_STORE_URL: string;
|
|
12
|
+
/**
|
|
13
|
+
* The Web Store listing is public. While `false`, no surface offers "Add to
|
|
14
|
+
* Chrome": each one shows "Connect" and a "How to install" link to the docs.
|
|
15
|
+
* Flip it with the listing, nothing else changes.
|
|
16
|
+
*/
|
|
17
|
+
export declare const CHROME_STORE_LISTED = false;
|
|
18
|
+
/** Where "How to install" points: the docs page that explains Load unpacked. */
|
|
19
|
+
export declare const CHROME_INSTALL_DOCS_PATH = "/docs/connect/chrome";
|
|
20
|
+
/** Opens Settings → Kortix in Chrome. The link a person copies to open it in Chrome. */
|
|
21
|
+
export declare function chromeSettingsLink(origin: string): string;
|
|
22
|
+
export interface ExtensionStatus {
|
|
23
|
+
version: string;
|
|
24
|
+
/** The extension holds a Kortix sign-in and a browser id. */
|
|
25
|
+
connected: boolean;
|
|
26
|
+
apiUrl: string | null;
|
|
27
|
+
chromeId: string | null;
|
|
28
|
+
}
|
|
29
|
+
export type ExtensionConnectResult = {
|
|
30
|
+
ok: true;
|
|
31
|
+
chromeId: string;
|
|
32
|
+
} | {
|
|
33
|
+
ok: false;
|
|
34
|
+
error: string;
|
|
35
|
+
};
|
|
36
|
+
/** Chrome, Edge, Brave, Arc and Opera on a computer. Not Electron, not Chrome on iOS. */
|
|
37
|
+
export declare function isChromiumBrowser(userAgent: string): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* The API origin the extension signs in to. The web app's backend URL can be
|
|
40
|
+
* root-relative (`/v1`, same-origin deployments), so resolve it against the
|
|
41
|
+
* page. The extension appends `/v1/...` itself. `''` when there is none.
|
|
42
|
+
*/
|
|
43
|
+
export declare function apiOrigin(backendUrl: string, pageOrigin: string): string;
|
|
44
|
+
/** The extension's state, or `null`: not installed, not Chromium, or no answer in 1.5 s. */
|
|
45
|
+
export declare function pingExtension(timeoutMs?: number): Promise<ExtensionStatus | null>;
|
|
46
|
+
/** Starts the extension's Kortix sign-in. Resolves when the person finishes it. */
|
|
47
|
+
export declare function connectExtension(apiUrl: string): Promise<ExtensionConnectResult>;
|