@ekanos/sdk 0.1.0
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/LICENSE +21 -0
- package/README.md +78 -0
- package/dist/components/base-activation-dialog.d.ts +41 -0
- package/dist/components/base-activation-dialog.js +41 -0
- package/dist/components/base-activation-dialog.js.map +1 -0
- package/dist/components/base-activation-form.d.ts +36 -0
- package/dist/components/base-activation-form.js +21 -0
- package/dist/components/base-activation-form.js.map +1 -0
- package/dist/components/base-marketplace-tile.d.ts +34 -0
- package/dist/components/base-marketplace-tile.js +40 -0
- package/dist/components/base-marketplace-tile.js.map +1 -0
- package/dist/components/current-return-path.d.ts +19 -0
- package/dist/components/current-return-path.js +26 -0
- package/dist/components/current-return-path.js.map +1 -0
- package/dist/components/index.d.ts +40 -0
- package/dist/components/index.js +38 -0
- package/dist/components/index.js.map +1 -0
- package/dist/components/oauth-activation-form.d.ts +50 -0
- package/dist/components/oauth-activation-form.js +119 -0
- package/dist/components/oauth-activation-form.js.map +1 -0
- package/dist/components/widgets/ai-prompt-chip.d.ts +31 -0
- package/dist/components/widgets/ai-prompt-chip.js +38 -0
- package/dist/components/widgets/ai-prompt-chip.js.map +1 -0
- package/dist/components/widgets/ask-icon.d.ts +3 -0
- package/dist/components/widgets/ask-icon.js +5 -0
- package/dist/components/widgets/ask-icon.js.map +1 -0
- package/dist/components/widgets/use-animated-height.d.ts +12 -0
- package/dist/components/widgets/use-animated-height.js +39 -0
- package/dist/components/widgets/use-animated-height.js.map +1 -0
- package/dist/components/widgets/widget-ask-bar.d.ts +18 -0
- package/dist/components/widgets/widget-ask-bar.js +62 -0
- package/dist/components/widgets/widget-ask-bar.js.map +1 -0
- package/dist/components/widgets/widget-context.d.ts +38 -0
- package/dist/components/widgets/widget-context.js +4 -0
- package/dist/components/widgets/widget-context.js.map +1 -0
- package/dist/components/widgets/widget-state-components.d.ts +5 -0
- package/dist/components/widgets/widget-state-components.js +19 -0
- package/dist/components/widgets/widget-state-components.js.map +1 -0
- package/dist/components/widgets/widget.d.ts +91 -0
- package/dist/components/widgets/widget.js +205 -0
- package/dist/components/widgets/widget.js.map +1 -0
- package/dist/context/egress.d.ts +30 -0
- package/dist/context/egress.js +81 -0
- package/dist/context/egress.js.map +1 -0
- package/dist/context/errors.d.ts +57 -0
- package/dist/context/errors.js +78 -0
- package/dist/context/errors.js.map +1 -0
- package/dist/context/index.d.ts +19 -0
- package/dist/context/index.js +20 -0
- package/dist/context/index.js.map +1 -0
- package/dist/context/require-context.d.ts +26 -0
- package/dist/context/require-context.js +36 -0
- package/dist/context/require-context.js.map +1 -0
- package/dist/context/storage-validation.d.ts +71 -0
- package/dist/context/storage-validation.js +132 -0
- package/dist/context/storage-validation.js.map +1 -0
- package/dist/context/types.d.ts +11 -0
- package/dist/context/types.js +2 -0
- package/dist/context/types.js.map +1 -0
- package/dist/hooks/activation-actions-context.d.ts +80 -0
- package/dist/hooks/activation-actions-context.js +31 -0
- package/dist/hooks/activation-actions-context.js.map +1 -0
- package/dist/hooks/ask-assistant-context.d.ts +32 -0
- package/dist/hooks/ask-assistant-context.js +56 -0
- package/dist/hooks/ask-assistant-context.js.map +1 -0
- package/dist/hooks/fetch-integration-storage.d.ts +48 -0
- package/dist/hooks/fetch-integration-storage.js +68 -0
- package/dist/hooks/fetch-integration-storage.js.map +1 -0
- package/dist/hooks/index.d.ts +28 -0
- package/dist/hooks/index.js +32 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/use-activate-integration.d.ts +15 -0
- package/dist/hooks/use-activate-integration.js +61 -0
- package/dist/hooks/use-activate-integration.js.map +1 -0
- package/dist/hooks/use-oauth-connection-status.d.ts +360 -0
- package/dist/hooks/use-oauth-connection-status.js +108 -0
- package/dist/hooks/use-oauth-connection-status.js.map +1 -0
- package/dist/hooks/use-widget-events.d.ts +20 -0
- package/dist/hooks/use-widget-events.js +51 -0
- package/dist/hooks/use-widget-events.js.map +1 -0
- package/dist/hooks/widget-event-context.d.ts +22 -0
- package/dist/hooks/widget-event-context.js +75 -0
- package/dist/hooks/widget-event-context.js.map +1 -0
- package/dist/hooks/widget-event-types.d.ts +55 -0
- package/dist/hooks/widget-event-types.js +9 -0
- package/dist/hooks/widget-event-types.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/integration/cron.d.ts +29 -0
- package/dist/integration/cron.js +102 -0
- package/dist/integration/cron.js.map +1 -0
- package/dist/integration/define-integration.d.ts +28 -0
- package/dist/integration/define-integration.js +41 -0
- package/dist/integration/define-integration.js.map +1 -0
- package/dist/integration/event-surfaces.d.ts +15 -0
- package/dist/integration/event-surfaces.js +45 -0
- package/dist/integration/event-surfaces.js.map +1 -0
- package/dist/integration/index.d.ts +20 -0
- package/dist/integration/index.js +23 -0
- package/dist/integration/index.js.map +1 -0
- package/dist/integration/types.d.ts +7 -0
- package/dist/integration/types.js +2 -0
- package/dist/integration/types.js.map +1 -0
- package/dist/mcp/guards.d.ts +40 -0
- package/dist/mcp/guards.js +99 -0
- package/dist/mcp/guards.js.map +1 -0
- package/dist/mcp/index.d.ts +22 -0
- package/dist/mcp/index.js +22 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/ownership.d.ts +74 -0
- package/dist/mcp/ownership.js +83 -0
- package/dist/mcp/ownership.js.map +1 -0
- package/dist/mcp/types.d.ts +145 -0
- package/dist/mcp/types.js +2 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/testing/index.d.ts +16 -0
- package/dist/testing/index.js +19 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/invoke.d.ts +77 -0
- package/dist/testing/invoke.js +83 -0
- package/dist/testing/invoke.js.map +1 -0
- package/dist/testing/mock-context.d.ts +79 -0
- package/dist/testing/mock-context.js +206 -0
- package/dist/testing/mock-context.js.map +1 -0
- package/dist/types/icon.d.ts +12 -0
- package/dist/types/icon.js +2 -0
- package/dist/types/icon.js.map +1 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.js +8 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/integration.d.ts +258 -0
- package/dist/types/integration.js +2 -0
- package/dist/types/integration.js.map +1 -0
- package/dist/types/widget-ask-context.d.ts +36 -0
- package/dist/types/widget-ask-context.js +2 -0
- package/dist/types/widget-ask-context.js.map +1 -0
- package/dist/types/workspace-target.d.ts +33 -0
- package/dist/types/workspace-target.js +2 -0
- package/dist/types/workspace-target.js.map +1 -0
- package/package.json +116 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useEffect, useRef, useState, } from 'react';
|
|
4
|
+
import { Icon } from '@ekanos/ui/icon';
|
|
5
|
+
import { useForm } from 'react-hook-form';
|
|
6
|
+
import { useActivateIntegration } from '../hooks/use-activate-integration.js';
|
|
7
|
+
import { useOAuthConnectionStatus, } from '../hooks/use-oauth-connection-status.js';
|
|
8
|
+
import { BaseActivationForm } from './base-activation-form.js';
|
|
9
|
+
import { currentReturnPath } from './current-return-path.js';
|
|
10
|
+
/**
|
|
11
|
+
* Shared activation form for OAuth-backed integrations: shows a "Connect
|
|
12
|
+
* with X" button that redirects to the integration's connect route, then
|
|
13
|
+
* auto-activates once the OAuth redirect returns with a live connection.
|
|
14
|
+
*
|
|
15
|
+
* Integrations with real activation fields (API keys, domains, …) should
|
|
16
|
+
* keep a bespoke form built on BaseActivationForm instead.
|
|
17
|
+
*/
|
|
18
|
+
export function OAuthActivationForm(props) {
|
|
19
|
+
var _a;
|
|
20
|
+
const { config, checkConnection, errorParam, children, accountId, sourceId, teamAccountId, onSuccess, onCancel, isLoading: externalLoading = false, inline = false, } = props;
|
|
21
|
+
const { slug, displayName, title, description, successMessage, successTitle = "You're all set!", connectPath = `/api/integrations/${slug}/connect`, connectedBanner = `${displayName} account connected`, connectButtonText = `Connect with ${displayName}`, activateButtonText = `Activate ${displayName}`, externalIdPrefix = slug, autoActivateOnConnect = true, } = config;
|
|
22
|
+
const [error, setError] = useState(null);
|
|
23
|
+
const [success, setSuccess] = useState(false);
|
|
24
|
+
const { data: connectionStatus, oauthJustCompleted, oauthError, isLoading: isCheckingConnection, } = useOAuthConnectionStatus(accountId, {
|
|
25
|
+
connectionKey: `${slug}-connection`,
|
|
26
|
+
checkConnection,
|
|
27
|
+
errorParam,
|
|
28
|
+
});
|
|
29
|
+
const isConnected = (_a = connectionStatus === null || connectionStatus === void 0 ? void 0 : connectionStatus.isConnected) !== null && _a !== void 0 ? _a : false;
|
|
30
|
+
const form = useForm({ defaultValues: {} });
|
|
31
|
+
const activateIntegration = useActivateIntegration();
|
|
32
|
+
const { mutateAsync: activateMutateAsync } = activateIntegration;
|
|
33
|
+
const startConnect = useCallback(() => {
|
|
34
|
+
setError(null);
|
|
35
|
+
const params = new URLSearchParams({
|
|
36
|
+
accountId,
|
|
37
|
+
returnPath: currentReturnPath(),
|
|
38
|
+
});
|
|
39
|
+
// For a user-scoped integration `accountId` is the PERSONAL account (see
|
|
40
|
+
// the connect modal's scope-resolved id), which has no
|
|
41
|
+
// source_id — so an OAuth callback cannot materialize workspaces from it
|
|
42
|
+
// alone. Carry the team through as well; callbacks that don't read it
|
|
43
|
+
// simply ignore the extra param.
|
|
44
|
+
if (teamAccountId) {
|
|
45
|
+
params.set('teamAccountId', teamAccountId);
|
|
46
|
+
}
|
|
47
|
+
window.location.href = `${connectPath}?${params.toString()}`;
|
|
48
|
+
}, [accountId, teamAccountId, connectPath]);
|
|
49
|
+
const handleActivate = useCallback(async () => {
|
|
50
|
+
try {
|
|
51
|
+
setError(null);
|
|
52
|
+
await activateMutateAsync({
|
|
53
|
+
accountId,
|
|
54
|
+
sourceId,
|
|
55
|
+
teamAccountId,
|
|
56
|
+
integrationSlug: slug,
|
|
57
|
+
activationData: {},
|
|
58
|
+
});
|
|
59
|
+
setSuccess(true);
|
|
60
|
+
onSuccess === null || onSuccess === void 0 ? void 0 : onSuccess({
|
|
61
|
+
success: true,
|
|
62
|
+
configData: form.getValues(),
|
|
63
|
+
externalId: `${externalIdPrefix}_${Date.now()}`,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
const errorMessage = err instanceof Error ? err.message : 'Activation failed';
|
|
68
|
+
setError(errorMessage);
|
|
69
|
+
}
|
|
70
|
+
}, [
|
|
71
|
+
accountId,
|
|
72
|
+
sourceId,
|
|
73
|
+
teamAccountId,
|
|
74
|
+
slug,
|
|
75
|
+
activateMutateAsync,
|
|
76
|
+
onSuccess,
|
|
77
|
+
form,
|
|
78
|
+
]);
|
|
79
|
+
const handleSubmit = useCallback(async () => {
|
|
80
|
+
if (!isConnected) {
|
|
81
|
+
startConnect();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
await handleActivate();
|
|
85
|
+
}, [isConnected, handleActivate, startConnect]);
|
|
86
|
+
// Auto-activate after OAuth return, unless the integration opted out so the
|
|
87
|
+
// user can review its post-connect configuration first.
|
|
88
|
+
const hasAutoActivated = useRef(false);
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (autoActivateOnConnect &&
|
|
91
|
+
oauthJustCompleted &&
|
|
92
|
+
isConnected &&
|
|
93
|
+
!success &&
|
|
94
|
+
!activateIntegration.isPending &&
|
|
95
|
+
!hasAutoActivated.current) {
|
|
96
|
+
hasAutoActivated.current = true;
|
|
97
|
+
const id = requestAnimationFrame(() => {
|
|
98
|
+
void handleActivate();
|
|
99
|
+
});
|
|
100
|
+
return () => cancelAnimationFrame(id);
|
|
101
|
+
}
|
|
102
|
+
}, [
|
|
103
|
+
autoActivateOnConnect,
|
|
104
|
+
oauthJustCompleted,
|
|
105
|
+
isConnected,
|
|
106
|
+
success,
|
|
107
|
+
activateIntegration.isPending,
|
|
108
|
+
handleActivate,
|
|
109
|
+
]);
|
|
110
|
+
const displayError = error || oauthError;
|
|
111
|
+
const buttonText = isConnected ? activateButtonText : connectButtonText;
|
|
112
|
+
const buttonLoadingText = isConnected ? 'Activating...' : 'Connecting...';
|
|
113
|
+
return (_jsxs(BaseActivationForm, { isOpen: true, inline: inline, onClose: () => onCancel === null || onCancel === void 0 ? void 0 : onCancel(), title: title, description: description, form: form, onSubmit: handleSubmit, isLoading: externalLoading || activateIntegration.isPending || isCheckingConnection, error: displayError, success: success, successTitle: successTitle, successMessage: successMessage, onSuccessClose: () => onSuccess === null || onSuccess === void 0 ? void 0 : onSuccess({
|
|
114
|
+
success: true,
|
|
115
|
+
configData: form.getValues(),
|
|
116
|
+
externalId: `${externalIdPrefix}_${Date.now()}`,
|
|
117
|
+
}), submitButtonText: buttonText, submitButtonLoadingText: buttonLoadingText, children: [children, isConnected && !success && (_jsxs("div", { className: "border-success/30 bg-success/10 flex items-center justify-center gap-2 rounded-lg border p-4", children: [_jsx(Icon, { name: "fa-light fa-circle-check", className: "text-success h-5 w-5" }), _jsx("span", { className: "text-success text-sm font-medium", children: connectedBanner })] }))] }));
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=oauth-activation-form.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-activation-form.js","sourceRoot":"","sources":["../../src/components/oauth-activation-form.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport {\n type ReactNode,\n useCallback,\n useEffect,\n useRef,\n useState,\n} from 'react';\n\nimport { Icon } from '@ekanos/ui/icon';\nimport { useForm } from 'react-hook-form';\n\nimport { useActivateIntegration } from '../hooks/use-activate-integration';\nimport {\n type ConnectionCheckAction,\n type OAuthErrorParam,\n useOAuthConnectionStatus,\n} from '../hooks/use-oauth-connection-status';\nimport type { ActivationFormProps } from '../types/integration';\nimport { BaseActivationForm } from './base-activation-form';\nimport { currentReturnPath } from './current-return-path';\n\nexport interface OAuthActivationFormConfig {\n /** Integration slug — used for activation, the connect route, and ids. */\n slug: string;\n /** Human name used in default button/banner copy, e.g. 'QuickBooks'. */\n displayName: string;\n title: string;\n description: string;\n successMessage: string;\n /** Defaults to \"You're all set!\" */\n successTitle?: string;\n /** Defaults to `/api/integrations/<slug>/connect`. */\n connectPath?: string;\n /** Defaults to `<displayName> account connected`. */\n connectedBanner?: string;\n /** Defaults to `Connect with <displayName>`. */\n connectButtonText?: string;\n /** Defaults to `Activate <displayName>`. */\n activateButtonText?: string;\n /** externalId is `<prefix>_<timestamp>`; prefix defaults to the slug. */\n externalIdPrefix?: string;\n /**\n * Whether returning from OAuth should immediately activate (default `true`).\n *\n * Set `false` for integrations that expose post-connect configuration — the\n * user needs a chance to review or change it before activating, which\n * auto-activation skips right past. Connecting then leaves the form on an\n * \"Activate <X>\" button as a deliberate second step.\n */\n autoActivateOnConnect?: boolean;\n}\n\ntype OAuthActivationFormProps = ActivationFormProps & {\n config: OAuthActivationFormConfig;\n checkConnection: ConnectionCheckAction;\n errorParam?: OAuthErrorParam;\n /** Extra content (feature showcases, permission badges) rendered above the connected banner. */\n children?: ReactNode;\n};\n\n/**\n * Shared activation form for OAuth-backed integrations: shows a \"Connect\n * with X\" button that redirects to the integration's connect route, then\n * auto-activates once the OAuth redirect returns with a live connection.\n *\n * Integrations with real activation fields (API keys, domains, …) should\n * keep a bespoke form built on BaseActivationForm instead.\n */\nexport function OAuthActivationForm(props: OAuthActivationFormProps) {\n const {\n config,\n checkConnection,\n errorParam,\n children,\n accountId,\n sourceId,\n teamAccountId,\n onSuccess,\n onCancel,\n isLoading: externalLoading = false,\n inline = false,\n } = props;\n\n const {\n slug,\n displayName,\n title,\n description,\n successMessage,\n successTitle = \"You're all set!\",\n connectPath = `/api/integrations/${slug}/connect`,\n connectedBanner = `${displayName} account connected`,\n connectButtonText = `Connect with ${displayName}`,\n activateButtonText = `Activate ${displayName}`,\n externalIdPrefix = slug,\n autoActivateOnConnect = true,\n } = config;\n\n const [error, setError] = useState<string | null>(null);\n const [success, setSuccess] = useState(false);\n\n const {\n data: connectionStatus,\n oauthJustCompleted,\n oauthError,\n isLoading: isCheckingConnection,\n } = useOAuthConnectionStatus(accountId, {\n connectionKey: `${slug}-connection`,\n checkConnection,\n errorParam,\n });\n\n const isConnected = connectionStatus?.isConnected ?? false;\n\n const form = useForm({ defaultValues: {} });\n\n const activateIntegration = useActivateIntegration();\n const { mutateAsync: activateMutateAsync } = activateIntegration;\n\n const startConnect = useCallback(() => {\n setError(null);\n const params = new URLSearchParams({\n accountId,\n returnPath: currentReturnPath(),\n });\n\n // For a user-scoped integration `accountId` is the PERSONAL account (see\n // the connect modal's scope-resolved id), which has no\n // source_id — so an OAuth callback cannot materialize workspaces from it\n // alone. Carry the team through as well; callbacks that don't read it\n // simply ignore the extra param.\n if (teamAccountId) {\n params.set('teamAccountId', teamAccountId);\n }\n\n window.location.href = `${connectPath}?${params.toString()}`;\n }, [accountId, teamAccountId, connectPath]);\n\n const handleActivate = useCallback(async () => {\n try {\n setError(null);\n await activateMutateAsync({\n accountId,\n sourceId,\n teamAccountId,\n integrationSlug: slug,\n activationData: {},\n });\n setSuccess(true);\n\n onSuccess?.({\n success: true,\n configData: form.getValues(),\n externalId: `${externalIdPrefix}_${Date.now()}`,\n });\n } catch (err) {\n const errorMessage =\n err instanceof Error ? err.message : 'Activation failed';\n setError(errorMessage);\n }\n }, [\n accountId,\n sourceId,\n teamAccountId,\n slug,\n activateMutateAsync,\n onSuccess,\n form,\n ]);\n\n const handleSubmit = useCallback(async () => {\n if (!isConnected) {\n startConnect();\n return;\n }\n\n await handleActivate();\n }, [isConnected, handleActivate, startConnect]);\n\n // Auto-activate after OAuth return, unless the integration opted out so the\n // user can review its post-connect configuration first.\n const hasAutoActivated = useRef(false);\n\n useEffect(() => {\n if (\n autoActivateOnConnect &&\n oauthJustCompleted &&\n isConnected &&\n !success &&\n !activateIntegration.isPending &&\n !hasAutoActivated.current\n ) {\n hasAutoActivated.current = true;\n\n const id = requestAnimationFrame(() => {\n void handleActivate();\n });\n\n return () => cancelAnimationFrame(id);\n }\n }, [\n autoActivateOnConnect,\n oauthJustCompleted,\n isConnected,\n success,\n activateIntegration.isPending,\n handleActivate,\n ]);\n\n const displayError = error || oauthError;\n const buttonText = isConnected ? activateButtonText : connectButtonText;\n const buttonLoadingText = isConnected ? 'Activating...' : 'Connecting...';\n\n return (\n <BaseActivationForm\n isOpen={true}\n inline={inline}\n onClose={() => onCancel?.()}\n title={title}\n description={description}\n form={form}\n onSubmit={handleSubmit}\n isLoading={\n externalLoading || activateIntegration.isPending || isCheckingConnection\n }\n error={displayError}\n success={success}\n successTitle={successTitle}\n successMessage={successMessage}\n onSuccessClose={() =>\n onSuccess?.({\n success: true,\n configData: form.getValues(),\n externalId: `${externalIdPrefix}_${Date.now()}`,\n })\n }\n submitButtonText={buttonText}\n submitButtonLoadingText={buttonLoadingText}\n >\n {children}\n\n {isConnected && !success && (\n <div className=\"border-success/30 bg-success/10 flex items-center justify-center gap-2 rounded-lg border p-4\">\n <Icon\n name=\"fa-light fa-circle-check\"\n className=\"text-success h-5 w-5\"\n />\n <span className=\"text-success text-sm font-medium\">\n {connectedBanner}\n </span>\n </div>\n )}\n </BaseActivationForm>\n );\n}\n"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
type AiPromptChipProps = {
|
|
2
|
+
label: string;
|
|
3
|
+
prompt: string;
|
|
4
|
+
context?: Record<string, unknown>;
|
|
5
|
+
sourceWidget: string;
|
|
6
|
+
/**
|
|
7
|
+
* Human-readable label for the dock's ArrivalChip ("Asking about …").
|
|
8
|
+
* Falls back to the chip's display label when omitted.
|
|
9
|
+
*/
|
|
10
|
+
title?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Fires after the assistant has been invoked. Drawers pass their close
|
|
13
|
+
* handler here so the dock takes over the screen on click.
|
|
14
|
+
*/
|
|
15
|
+
onAfterAsk?: () => void;
|
|
16
|
+
variant?: 'header' | 'inline' | 'footer';
|
|
17
|
+
className?: string;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* AI handoff chip that opens the assistant dock pre-seeded with this
|
|
21
|
+
* widget/drawer's snapshot. Routes through `useAskAssistant` so the dock
|
|
22
|
+
* receives the standard `{widgetId, title, snapshot, suggestedPrompt}`
|
|
23
|
+
* shape used by all AI handoffs in the app.
|
|
24
|
+
*
|
|
25
|
+
* Variants:
|
|
26
|
+
* - header (Type A): Compact icon button for widget headers
|
|
27
|
+
* - inline (Type B): Chip with sparkle icon + text, below data sections
|
|
28
|
+
* - footer (Type C): Full-width chip in drawer footers
|
|
29
|
+
*/
|
|
30
|
+
export declare function AiPromptChip({ label, prompt, context, sourceWidget, title, onAfterAsk, variant, className, }: AiPromptChipProps): import("react").JSX.Element;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Button } from '@ekanos/ui/button';
|
|
4
|
+
import { Icon } from '@ekanos/ui/icon';
|
|
5
|
+
import { cn } from '@ekanos/ui/utils';
|
|
6
|
+
import { useAskAssistant } from '../../hooks/ask-assistant-context.js';
|
|
7
|
+
/**
|
|
8
|
+
* AI handoff chip that opens the assistant dock pre-seeded with this
|
|
9
|
+
* widget/drawer's snapshot. Routes through `useAskAssistant` so the dock
|
|
10
|
+
* receives the standard `{widgetId, title, snapshot, suggestedPrompt}`
|
|
11
|
+
* shape used by all AI handoffs in the app.
|
|
12
|
+
*
|
|
13
|
+
* Variants:
|
|
14
|
+
* - header (Type A): Compact icon button for widget headers
|
|
15
|
+
* - inline (Type B): Chip with sparkle icon + text, below data sections
|
|
16
|
+
* - footer (Type C): Full-width chip in drawer footers
|
|
17
|
+
*/
|
|
18
|
+
export function AiPromptChip({ label, prompt, context, sourceWidget, title, onAfterAsk, variant = 'inline', className, }) {
|
|
19
|
+
const askAssistant = useAskAssistant();
|
|
20
|
+
const handleClick = () => {
|
|
21
|
+
askAssistant({
|
|
22
|
+
widgetId: sourceWidget,
|
|
23
|
+
title: title !== null && title !== void 0 ? title : label,
|
|
24
|
+
snapshot: context !== null && context !== void 0 ? context : {},
|
|
25
|
+
suggestedPrompt: prompt,
|
|
26
|
+
});
|
|
27
|
+
onAfterAsk === null || onAfterAsk === void 0 ? void 0 : onAfterAsk();
|
|
28
|
+
};
|
|
29
|
+
if (variant === 'header') {
|
|
30
|
+
return (_jsx(Button, { variant: "ghost", size: "sm", className: cn('h-8 w-8 p-0', className), onClick: handleClick, "aria-label": `Ask AI: ${label}`, children: _jsx(Icon, { name: "fa-light fa-sparkles", className: "h-4 w-4" }) }));
|
|
31
|
+
}
|
|
32
|
+
if (variant === 'footer') {
|
|
33
|
+
return (_jsxs("button", { type: "button", onClick: handleClick, className: cn('flex w-full items-center gap-2 rounded-lg border border-dashed px-4 py-3', 'border-primary/20 bg-primary/5 text-primary text-sm font-medium', 'hover:bg-primary/10 hover:border-primary/40 transition-colors', 'focus-visible:ring-ring/60 focus-visible:ring-2 focus-visible:outline-none', className), "aria-label": label, children: [_jsx(Icon, { name: "fa-light fa-sparkles", className: "h-4 w-4 shrink-0" }), _jsx("span", { className: "text-left", children: label })] }));
|
|
34
|
+
}
|
|
35
|
+
// inline (Type B)
|
|
36
|
+
return (_jsxs("button", { type: "button", onClick: handleClick, className: cn('inline-flex items-center gap-1.5 rounded-full px-3 py-1.5', 'border-primary/20 bg-primary/5 text-primary border text-xs font-medium', 'hover:bg-primary/10 hover:border-primary/40 transition-colors', 'focus-visible:ring-ring/60 focus-visible:ring-2 focus-visible:outline-none', className), "aria-label": label, children: [_jsx(Icon, { name: "fa-light fa-sparkles", className: "h-3.5 w-3.5 shrink-0" }), _jsx("span", { children: label })] }));
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=ai-prompt-chip.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-prompt-chip.js","sourceRoot":"","sources":["../../../src/components/widgets/ai-prompt-chip.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC3C,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AACvC,OAAO,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAC;AAEtC,OAAO,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAqBpE;;;;;;;;;;GAUG;AACH,MAAM,UAAU,YAAY,CAAC,EAC3B,KAAK,EACL,MAAM,EACN,OAAO,EACP,YAAY,EACZ,KAAK,EACL,UAAU,EACV,OAAO,GAAG,QAAQ,EAClB,SAAS,GACS;IAClB,MAAM,YAAY,GAAG,eAAe,EAAE,CAAC;IAEvC,MAAM,WAAW,GAAG,GAAG,EAAE;QACvB,YAAY,CAAC;YACX,QAAQ,EAAE,YAAY;YACtB,KAAK,EAAE,KAAK,aAAL,KAAK,cAAL,KAAK,GAAI,KAAK;YACrB,QAAQ,EAAE,OAAO,aAAP,OAAO,cAAP,OAAO,GAAI,EAAE;YACvB,eAAe,EAAE,MAAM;SACxB,CAAC,CAAC;QACH,UAAU,aAAV,UAAU,uBAAV,UAAU,EAAI,CAAC;IACjB,CAAC,CAAC;IAEF,IAAI,OAAO,KAAK,QAAQ,EAAE,CAAC;QACzB,OAAO,CACL,KAAC,MAAM,IACL,OAAO,EAAC,OAAO,EACf,IAAI,EAAC,IAAI,EACT,SAAS,EAAE,EAAE,CAAC,aAAa,EAAE,SAAS,CAAC,EACvC,OAAO,EAAE,WAAW,gBACR,WAAW,KAAK,EAAE,YAE9B,KAAC,IAAI,IAAC,IAAI,EAAC,sBAAsB,EAAC,SAAS,EAAC,SAAS,GAAG,GACjD,CACV,CAAC;IACJ,CAAC;IAED,IAAI,OAAO,KAAK,QAAQ,EAAE,CAAC;QACzB,OAAO,CACL,kBACE,IAAI,EAAC,QAAQ,EACb,OAAO,EAAE,WAAW,EACpB,SAAS,EAAE,EAAE,CACX,0EAA0E,EAC1E,iEAAiE,EACjE,+DAA+D,EAC/D,4EAA4E,EAC5E,SAAS,CACV,gBACW,KAAK,aAEjB,KAAC,IAAI,IAAC,IAAI,EAAC,sBAAsB,EAAC,SAAS,EAAC,kBAAkB,GAAG,EACjE,eAAM,SAAS,EAAC,WAAW,YAAE,KAAK,GAAQ,IACnC,CACV,CAAC;IACJ,CAAC;IAED,kBAAkB;IAClB,OAAO,CACL,kBACE,IAAI,EAAC,QAAQ,EACb,OAAO,EAAE,WAAW,EACpB,SAAS,EAAE,EAAE,CACX,2DAA2D,EAC3D,wEAAwE,EACxE,+DAA+D,EAC/D,4EAA4E,EAC5E,SAAS,CACV,gBACW,KAAK,aAEjB,KAAC,IAAI,IAAC,IAAI,EAAC,sBAAsB,EAAC,SAAS,EAAC,sBAAsB,GAAG,EACrE,yBAAO,KAAK,GAAQ,IACb,CACV,CAAC;AACJ,CAAC","sourcesContent":["'use client';\n\nimport { Button } from '@ekanos/ui/button';\nimport { Icon } from '@ekanos/ui/icon';\nimport { cn } from '@ekanos/ui/utils';\n\nimport { useAskAssistant } from '../../hooks/ask-assistant-context';\n\ntype AiPromptChipProps = {\n label: string;\n prompt: string;\n context?: Record<string, unknown>;\n sourceWidget: string;\n /**\n * Human-readable label for the dock's ArrivalChip (\"Asking about …\").\n * Falls back to the chip's display label when omitted.\n */\n title?: string;\n /**\n * Fires after the assistant has been invoked. Drawers pass their close\n * handler here so the dock takes over the screen on click.\n */\n onAfterAsk?: () => void;\n variant?: 'header' | 'inline' | 'footer';\n className?: string;\n};\n\n/**\n * AI handoff chip that opens the assistant dock pre-seeded with this\n * widget/drawer's snapshot. Routes through `useAskAssistant` so the dock\n * receives the standard `{widgetId, title, snapshot, suggestedPrompt}`\n * shape used by all AI handoffs in the app.\n *\n * Variants:\n * - header (Type A): Compact icon button for widget headers\n * - inline (Type B): Chip with sparkle icon + text, below data sections\n * - footer (Type C): Full-width chip in drawer footers\n */\nexport function AiPromptChip({\n label,\n prompt,\n context,\n sourceWidget,\n title,\n onAfterAsk,\n variant = 'inline',\n className,\n}: AiPromptChipProps) {\n const askAssistant = useAskAssistant();\n\n const handleClick = () => {\n askAssistant({\n widgetId: sourceWidget,\n title: title ?? label,\n snapshot: context ?? {},\n suggestedPrompt: prompt,\n });\n onAfterAsk?.();\n };\n\n if (variant === 'header') {\n return (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className={cn('h-8 w-8 p-0', className)}\n onClick={handleClick}\n aria-label={`Ask AI: ${label}`}\n >\n <Icon name=\"fa-light fa-sparkles\" className=\"h-4 w-4\" />\n </Button>\n );\n }\n\n if (variant === 'footer') {\n return (\n <button\n type=\"button\"\n onClick={handleClick}\n className={cn(\n 'flex w-full items-center gap-2 rounded-lg border border-dashed px-4 py-3',\n 'border-primary/20 bg-primary/5 text-primary text-sm font-medium',\n 'hover:bg-primary/10 hover:border-primary/40 transition-colors',\n 'focus-visible:ring-ring/60 focus-visible:ring-2 focus-visible:outline-none',\n className,\n )}\n aria-label={label}\n >\n <Icon name=\"fa-light fa-sparkles\" className=\"h-4 w-4 shrink-0\" />\n <span className=\"text-left\">{label}</span>\n </button>\n );\n }\n\n // inline (Type B)\n return (\n <button\n type=\"button\"\n onClick={handleClick}\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-full px-3 py-1.5',\n 'border-primary/20 bg-primary/5 text-primary border text-xs font-medium',\n 'hover:bg-primary/10 hover:border-primary/40 transition-colors',\n 'focus-visible:ring-ring/60 focus-visible:ring-2 focus-visible:outline-none',\n className,\n )}\n aria-label={label}\n >\n <Icon name=\"fa-light fa-sparkles\" className=\"h-3.5 w-3.5 shrink-0\" />\n <span>{label}</span>\n </button>\n );\n}\n"]}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
export function AskIcon({ className }) {
|
|
3
|
+
return (_jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 640 640", fill: "currentColor", className: className, "aria-hidden": "true", children: _jsx("path", { d: "M352 64L352 32L288 32L288 128L96 128L96 544L544 544L544 128L352 128L352 64zM160 408L240 408L240 456L160 456L160 408zM280 408L360 408L360 456L280 456L280 408zM400 408L480 408L480 456L400 456L400 408zM224 240C250.5 240 272 261.5 272 288C272 314.5 250.5 336 224 336C197.5 336 176 314.5 176 288C176 261.5 197.5 240 224 240zM368 288C368 261.5 389.5 240 416 240C442.5 240 464 261.5 464 288C464 314.5 442.5 336 416 336C389.5 336 368 314.5 368 288zM64 288L64 256L0 256L0 416L64 416L64 288zM640 256L576 256L576 416L640 416L640 256z" }) }));
|
|
4
|
+
}
|
|
5
|
+
//# sourceMappingURL=ask-icon.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ask-icon.js","sourceRoot":"","sources":["../../../src/components/widgets/ask-icon.tsx"],"names":[],"mappings":";AAAA,MAAM,UAAU,OAAO,CAAC,EAAE,SAAS,EAA0B;IAC3D,OAAO,CACL,cACE,KAAK,EAAC,4BAA4B,EAClC,OAAO,EAAC,aAAa,EACrB,IAAI,EAAC,cAAc,EACnB,SAAS,EAAE,SAAS,iBACR,MAAM,YAElB,eAAM,CAAC,EAAC,4gBAA4gB,GAAG,GACnhB,CACP,CAAC;AACJ,CAAC","sourcesContent":["export function AskIcon({ className }: { className?: string }) {\n return (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 640 640\"\n fill=\"currentColor\"\n className={className}\n aria-hidden=\"true\"\n >\n <path d=\"M352 64L352 32L288 32L288 128L96 128L96 544L544 544L544 128L352 128L352 64zM160 408L240 408L240 456L160 456L160 408zM280 408L360 408L360 456L280 456L280 408zM400 408L480 408L480 456L400 456L400 408zM224 240C250.5 240 272 261.5 272 288C272 314.5 250.5 336 224 336C197.5 336 176 314.5 176 288C176 261.5 197.5 240 224 240zM368 288C368 261.5 389.5 240 416 240C442.5 240 464 261.5 464 288C464 314.5 442.5 336 416 336C389.5 336 368 314.5 368 288zM64 288L64 256L0 256L0 416L64 416L64 288zM640 256L576 256L576 416L640 416L640 256z\" />\n </svg>\n );\n}\n"]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Measures inner content height via ResizeObserver and provides
|
|
3
|
+
* a smooth CSS transition on the outer container.
|
|
4
|
+
*
|
|
5
|
+
* Returns:
|
|
6
|
+
* - `innerRef` — attach to the inner (measuring) div
|
|
7
|
+
* - `height` — current height value for the outer (animating) div
|
|
8
|
+
*/
|
|
9
|
+
export declare function useAnimatedHeight(): {
|
|
10
|
+
innerRef: (n: HTMLDivElement | null) => void;
|
|
11
|
+
height: number | "auto";
|
|
12
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* Measures inner content height via ResizeObserver and provides
|
|
5
|
+
* a smooth CSS transition on the outer container.
|
|
6
|
+
*
|
|
7
|
+
* Returns:
|
|
8
|
+
* - `innerRef` — attach to the inner (measuring) div
|
|
9
|
+
* - `height` — current height value for the outer (animating) div
|
|
10
|
+
*/
|
|
11
|
+
export function useAnimatedHeight() {
|
|
12
|
+
const [node, setNode] = useState(null);
|
|
13
|
+
const [height, setHeight] = useState('auto');
|
|
14
|
+
const isInitialRef = useRef(true);
|
|
15
|
+
const innerRef = useCallback((n) => {
|
|
16
|
+
setNode(n);
|
|
17
|
+
}, []);
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (!node)
|
|
20
|
+
return;
|
|
21
|
+
const observer = new ResizeObserver(([entry]) => {
|
|
22
|
+
if (!entry)
|
|
23
|
+
return;
|
|
24
|
+
const h = entry.contentRect.height;
|
|
25
|
+
if (h === 0)
|
|
26
|
+
return; // guard initial zero
|
|
27
|
+
// Skip transition on first measurement — height starts as 'auto'
|
|
28
|
+
// so browser won't animate anyway, but this keeps state clean
|
|
29
|
+
if (isInitialRef.current) {
|
|
30
|
+
isInitialRef.current = false;
|
|
31
|
+
}
|
|
32
|
+
setHeight(h);
|
|
33
|
+
});
|
|
34
|
+
observer.observe(node);
|
|
35
|
+
return () => observer.disconnect();
|
|
36
|
+
}, [node]);
|
|
37
|
+
return { innerRef, height };
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=use-animated-height.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-animated-height.js","sourceRoot":"","sources":["../../../src/components/widgets/use-animated-height.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAEjE;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB;IAC/B,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAwB,IAAI,CAAC,CAAC;IAC9D,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAkB,MAAM,CAAC,CAAC;IAC9D,MAAM,YAAY,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;IAElC,MAAM,QAAQ,GAAG,WAAW,CAAC,CAAC,CAAwB,EAAE,EAAE;QACxD,OAAO,CAAC,CAAC,CAAC,CAAC;IACb,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,IAAI;YAAE,OAAO;QAElB,MAAM,QAAQ,GAAG,IAAI,cAAc,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,EAAE;YAC9C,IAAI,CAAC,KAAK;gBAAE,OAAO;YACnB,MAAM,CAAC,GAAG,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC;YACnC,IAAI,CAAC,KAAK,CAAC;gBAAE,OAAO,CAAC,qBAAqB;YAE1C,iEAAiE;YACjE,8DAA8D;YAC9D,IAAI,YAAY,CAAC,OAAO,EAAE,CAAC;gBACzB,YAAY,CAAC,OAAO,GAAG,KAAK,CAAC;YAC/B,CAAC;YAED,SAAS,CAAC,CAAC,CAAC,CAAC;QACf,CAAC,CAAC,CAAC;QAEH,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACvB,OAAO,GAAG,EAAE,CAAC,QAAQ,CAAC,UAAU,EAAE,CAAC;IACrC,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;IAEX,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;AAC9B,CAAC","sourcesContent":["'use client';\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\n/**\n * Measures inner content height via ResizeObserver and provides\n * a smooth CSS transition on the outer container.\n *\n * Returns:\n * - `innerRef` — attach to the inner (measuring) div\n * - `height` — current height value for the outer (animating) div\n */\nexport function useAnimatedHeight() {\n const [node, setNode] = useState<HTMLDivElement | null>(null);\n const [height, setHeight] = useState<number | 'auto'>('auto');\n const isInitialRef = useRef(true);\n\n const innerRef = useCallback((n: HTMLDivElement | null) => {\n setNode(n);\n }, []);\n\n useEffect(() => {\n if (!node) return;\n\n const observer = new ResizeObserver(([entry]) => {\n if (!entry) return;\n const h = entry.contentRect.height;\n if (h === 0) return; // guard initial zero\n\n // Skip transition on first measurement — height starts as 'auto'\n // so browser won't animate anyway, but this keeps state clean\n if (isInitialRef.current) {\n isInitialRef.current = false;\n }\n\n setHeight(h);\n });\n\n observer.observe(node);\n return () => observer.disconnect();\n }, [node]);\n\n return { innerRef, height };\n}\n"]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { WidgetAskContext } from '../../types/widget-ask-context.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
context: WidgetAskContext;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Widget footer tag + expanding prompt input.
|
|
7
|
+
*
|
|
8
|
+
* - closed: a quiet, always-visible tag row at the bottom of the widget
|
|
9
|
+
* ("Aperture icon · Ask about this · chevron up")
|
|
10
|
+
* - open: clicking the tag reveals an inline `PromptInput` (the canonical
|
|
11
|
+
* AI input component) below it. The tag itself doubles as the
|
|
12
|
+
* close affordance (chevron rotates).
|
|
13
|
+
*
|
|
14
|
+
* Dismisses on click-outside or Escape. Submitting sends the user's typed
|
|
15
|
+
* question (or the widget's suggestedPrompt fallback) to the assistant dock.
|
|
16
|
+
*/
|
|
17
|
+
export declare function WidgetAskBar({ context }: Props): import("react").JSX.Element;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { PromptInput, PromptInputBody, PromptInputFooter, PromptInputSubmit, PromptInputTextarea, PromptInputTools, } from '@ekanos/ui/ai-prompt-input';
|
|
5
|
+
import { Icon } from '@ekanos/ui/icon';
|
|
6
|
+
import { cn } from '@ekanos/ui/utils';
|
|
7
|
+
import { useAskAssistant } from '../../hooks/ask-assistant-context.js';
|
|
8
|
+
import { AskIcon } from './ask-icon.js';
|
|
9
|
+
/**
|
|
10
|
+
* Widget footer tag + expanding prompt input.
|
|
11
|
+
*
|
|
12
|
+
* - closed: a quiet, always-visible tag row at the bottom of the widget
|
|
13
|
+
* ("Aperture icon · Ask about this · chevron up")
|
|
14
|
+
* - open: clicking the tag reveals an inline `PromptInput` (the canonical
|
|
15
|
+
* AI input component) below it. The tag itself doubles as the
|
|
16
|
+
* close affordance (chevron rotates).
|
|
17
|
+
*
|
|
18
|
+
* Dismisses on click-outside or Escape. Submitting sends the user's typed
|
|
19
|
+
* question (or the widget's suggestedPrompt fallback) to the assistant dock.
|
|
20
|
+
*/
|
|
21
|
+
export function WidgetAskBar({ context }) {
|
|
22
|
+
var _a;
|
|
23
|
+
const [isActive, setIsActive] = useState(false);
|
|
24
|
+
const containerRef = useRef(null);
|
|
25
|
+
const askAssistant = useAskAssistant();
|
|
26
|
+
const dismiss = useCallback(() => {
|
|
27
|
+
setIsActive(false);
|
|
28
|
+
}, []);
|
|
29
|
+
const handleSubmit = useCallback((message) => {
|
|
30
|
+
const prompt = message.text.trim() || context.suggestedPrompt;
|
|
31
|
+
askAssistant(Object.assign(Object.assign({}, context), { suggestedPrompt: prompt }));
|
|
32
|
+
dismiss();
|
|
33
|
+
}, [askAssistant, context, dismiss]);
|
|
34
|
+
const handleSuggestion = useCallback((prompt) => {
|
|
35
|
+
askAssistant(Object.assign(Object.assign({}, context), { suggestedPrompt: prompt }));
|
|
36
|
+
dismiss();
|
|
37
|
+
}, [askAssistant, context, dismiss]);
|
|
38
|
+
const suggestions = (_a = context.suggestedPrompts) !== null && _a !== void 0 ? _a : [];
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (!isActive)
|
|
41
|
+
return;
|
|
42
|
+
const onPointerDown = (e) => {
|
|
43
|
+
const el = containerRef.current;
|
|
44
|
+
if (!el)
|
|
45
|
+
return;
|
|
46
|
+
if (el.contains(e.target))
|
|
47
|
+
return;
|
|
48
|
+
dismiss();
|
|
49
|
+
};
|
|
50
|
+
document.addEventListener('pointerdown', onPointerDown);
|
|
51
|
+
return () => document.removeEventListener('pointerdown', onPointerDown);
|
|
52
|
+
}, [dismiss, isActive]);
|
|
53
|
+
return (_jsxs("div", { ref: containerRef, className: "border-border/60 shrink-0 border-t", "data-test": "widget-ask-bar", children: [_jsxs("button", { type: "button", onClick: () => (isActive ? dismiss() : setIsActive(true)), "aria-expanded": isActive, "aria-label": isActive
|
|
54
|
+
? `Close ask input for ${context.title}`
|
|
55
|
+
: `Ask about ${context.title}`, "data-test": "widget-ask-bar-toggle", className: cn('group/tag hover:bg-accent/40 flex w-full items-center gap-2.5 px-4 py-2.5 text-left transition-colors sm:px-5', isActive && 'bg-accent/30'), children: [_jsx(AskIcon, { className: cn('text-primary h-5 w-5 shrink-0 transition-transform duration-200', isActive && 'scale-110') }), _jsx("span", { className: "text-muted-foreground group-hover/tag:text-foreground flex-1 text-base font-medium transition-colors", children: "Ask about this" }), _jsx(Icon, { name: "fa-light fa-chevron-up", className: cn('text-muted-foreground/50 group-hover/tag:text-muted-foreground h-3.5 w-3.5 shrink-0 transition-transform duration-200', isActive && 'rotate-180') })] }), isActive && (_jsxs("div", { className: "border-border/40 space-y-3 border-t p-3 sm:px-5", "data-test": "widget-ask-bar-input", children: [suggestions.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2", "data-test": "widget-ask-bar-suggestions", children: suggestions.map((s) => (_jsxs("button", { type: "button", onClick: () => handleSuggestion(s.prompt), className: cn('inline-flex items-center gap-1.5 rounded-full px-3 py-1.5', 'border-primary/20 bg-primary/5 text-primary border text-xs font-medium', 'hover:bg-primary/10 hover:border-primary/40 transition-colors', 'focus-visible:ring-ring/60 focus-visible:ring-2 focus-visible:outline-none'), "aria-label": s.label, children: [_jsx(Icon, { name: "fa-light fa-sparkles", className: "h-3.5 w-3.5 shrink-0" }), _jsx("span", { className: "text-left", children: s.label })] }, s.label))) })), _jsxs(PromptInput, { onSubmit: handleSubmit, children: [_jsx(PromptInputBody, { children: _jsx(PromptInputTextarea, { autoFocus: true, placeholder: `Ask about ${context.title.toLowerCase()}…`, "aria-label": `Ask about ${context.title}`, onKeyDown: (e) => {
|
|
56
|
+
if (e.key === 'Escape') {
|
|
57
|
+
e.preventDefault();
|
|
58
|
+
dismiss();
|
|
59
|
+
}
|
|
60
|
+
} }) }), _jsxs(PromptInputFooter, { children: [_jsx(PromptInputTools, {}), _jsx(PromptInputSubmit, { "data-test": "widget-ask-bar-send" })] })] })] }))] }));
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=widget-ask-bar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"widget-ask-bar.js","sourceRoot":"","sources":["../../../src/components/widgets/widget-ask-bar.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\nimport {\n PromptInput,\n PromptInputBody,\n PromptInputFooter,\n PromptInputSubmit,\n PromptInputTextarea,\n PromptInputTools,\n} from '@ekanos/ui/ai-prompt-input';\nimport { Icon } from '@ekanos/ui/icon';\nimport { cn } from '@ekanos/ui/utils';\n\nimport { useAskAssistant } from '../../hooks/ask-assistant-context';\nimport type { WidgetAskContext } from '../../types/widget-ask-context';\nimport { AskIcon } from './ask-icon';\n\ninterface Props {\n context: WidgetAskContext;\n}\n\n/**\n * Widget footer tag + expanding prompt input.\n *\n * - closed: a quiet, always-visible tag row at the bottom of the widget\n * (\"Aperture icon · Ask about this · chevron up\")\n * - open: clicking the tag reveals an inline `PromptInput` (the canonical\n * AI input component) below it. The tag itself doubles as the\n * close affordance (chevron rotates).\n *\n * Dismisses on click-outside or Escape. Submitting sends the user's typed\n * question (or the widget's suggestedPrompt fallback) to the assistant dock.\n */\nexport function WidgetAskBar({ context }: Props) {\n const [isActive, setIsActive] = useState(false);\n const containerRef = useRef<HTMLDivElement>(null);\n const askAssistant = useAskAssistant();\n\n const dismiss = useCallback(() => {\n setIsActive(false);\n }, []);\n\n const handleSubmit = useCallback(\n (message: { text: string }) => {\n const prompt = message.text.trim() || context.suggestedPrompt;\n askAssistant({ ...context, suggestedPrompt: prompt });\n dismiss();\n },\n [askAssistant, context, dismiss],\n );\n\n const handleSuggestion = useCallback(\n (prompt: string) => {\n askAssistant({ ...context, suggestedPrompt: prompt });\n dismiss();\n },\n [askAssistant, context, dismiss],\n );\n\n const suggestions = context.suggestedPrompts ?? [];\n\n useEffect(() => {\n if (!isActive) return;\n const onPointerDown = (e: PointerEvent) => {\n const el = containerRef.current;\n if (!el) return;\n if (el.contains(e.target as Node)) return;\n dismiss();\n };\n document.addEventListener('pointerdown', onPointerDown);\n return () => document.removeEventListener('pointerdown', onPointerDown);\n }, [dismiss, isActive]);\n\n return (\n <div\n ref={containerRef}\n className=\"border-border/60 shrink-0 border-t\"\n data-test=\"widget-ask-bar\"\n >\n <button\n type=\"button\"\n onClick={() => (isActive ? dismiss() : setIsActive(true))}\n aria-expanded={isActive}\n aria-label={\n isActive\n ? `Close ask input for ${context.title}`\n : `Ask about ${context.title}`\n }\n data-test=\"widget-ask-bar-toggle\"\n className={cn(\n 'group/tag hover:bg-accent/40 flex w-full items-center gap-2.5 px-4 py-2.5 text-left transition-colors sm:px-5',\n isActive && 'bg-accent/30',\n )}\n >\n <AskIcon\n className={cn(\n 'text-primary h-5 w-5 shrink-0 transition-transform duration-200',\n isActive && 'scale-110',\n )}\n />\n <span className=\"text-muted-foreground group-hover/tag:text-foreground flex-1 text-base font-medium transition-colors\">\n Ask about this\n </span>\n <Icon\n name=\"fa-light fa-chevron-up\"\n className={cn(\n 'text-muted-foreground/50 group-hover/tag:text-muted-foreground h-3.5 w-3.5 shrink-0 transition-transform duration-200',\n isActive && 'rotate-180',\n )}\n />\n </button>\n\n {isActive && (\n <div\n className=\"border-border/40 space-y-3 border-t p-3 sm:px-5\"\n data-test=\"widget-ask-bar-input\"\n >\n {suggestions.length > 0 && (\n <div\n className=\"flex flex-wrap gap-2\"\n data-test=\"widget-ask-bar-suggestions\"\n >\n {suggestions.map((s) => (\n <button\n key={s.label}\n type=\"button\"\n onClick={() => handleSuggestion(s.prompt)}\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-full px-3 py-1.5',\n 'border-primary/20 bg-primary/5 text-primary border text-xs font-medium',\n 'hover:bg-primary/10 hover:border-primary/40 transition-colors',\n 'focus-visible:ring-ring/60 focus-visible:ring-2 focus-visible:outline-none',\n )}\n aria-label={s.label}\n >\n <Icon\n name=\"fa-light fa-sparkles\"\n className=\"h-3.5 w-3.5 shrink-0\"\n />\n <span className=\"text-left\">{s.label}</span>\n </button>\n ))}\n </div>\n )}\n <PromptInput onSubmit={handleSubmit}>\n <PromptInputBody>\n <PromptInputTextarea\n autoFocus\n placeholder={`Ask about ${context.title.toLowerCase()}…`}\n aria-label={`Ask about ${context.title}`}\n onKeyDown={(e) => {\n if (e.key === 'Escape') {\n e.preventDefault();\n dismiss();\n }\n }}\n />\n </PromptInputBody>\n <PromptInputFooter>\n <PromptInputTools />\n <PromptInputSubmit data-test=\"widget-ask-bar-send\" />\n </PromptInputFooter>\n </PromptInput>\n </div>\n )}\n </div>\n );\n}\n"]}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { WidgetAskContext } from '../../types/widget-ask-context.js';
|
|
2
|
+
export type WidgetRenderState = 'active' | 'disabled' | 'loading' | 'error' | 'inactive';
|
|
3
|
+
export interface WidgetState {
|
|
4
|
+
state: WidgetRenderState;
|
|
5
|
+
collapsed: boolean;
|
|
6
|
+
pinned: boolean;
|
|
7
|
+
pendingCollapse: boolean;
|
|
8
|
+
pendingPin: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface WidgetActions {
|
|
11
|
+
toggleCollapsed: () => void;
|
|
12
|
+
togglePinned: () => void;
|
|
13
|
+
}
|
|
14
|
+
export interface WidgetHealth {
|
|
15
|
+
status: 'healthy' | 'degraded' | 'unhealthy' | 'unknown';
|
|
16
|
+
lastSuccessAt?: string | null;
|
|
17
|
+
errorMessage?: string | null;
|
|
18
|
+
}
|
|
19
|
+
export interface WidgetMeta {
|
|
20
|
+
widgetId: string;
|
|
21
|
+
title?: string;
|
|
22
|
+
subtitle?: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
maxContentHeight?: string;
|
|
25
|
+
isCollapsible: boolean;
|
|
26
|
+
isPinnable: boolean;
|
|
27
|
+
askContext?: WidgetAskContext;
|
|
28
|
+
aiFooterEnabled: boolean;
|
|
29
|
+
className?: string;
|
|
30
|
+
/** Integration health for this widget's product, if known. */
|
|
31
|
+
health?: WidgetHealth;
|
|
32
|
+
}
|
|
33
|
+
export interface WidgetContextValue {
|
|
34
|
+
state: WidgetState;
|
|
35
|
+
actions: WidgetActions;
|
|
36
|
+
meta: WidgetMeta;
|
|
37
|
+
}
|
|
38
|
+
export declare const WidgetContext: import("react").Context<WidgetContextValue | null>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"widget-context.js","sourceRoot":"","sources":["../../../src/components/widgets/widget-context.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAmDtC,MAAM,CAAC,MAAM,aAAa,GAAG,aAAa,CAA4B,IAAI,CAAC,CAAC","sourcesContent":["'use client';\n\nimport { createContext } from 'react';\n\nimport type { WidgetAskContext } from '../../types/widget-ask-context';\n\nexport type WidgetRenderState =\n | 'active'\n | 'disabled'\n | 'loading'\n | 'error'\n | 'inactive';\n\nexport interface WidgetState {\n state: WidgetRenderState;\n collapsed: boolean;\n pinned: boolean;\n pendingCollapse: boolean;\n pendingPin: boolean;\n}\n\nexport interface WidgetActions {\n toggleCollapsed: () => void;\n togglePinned: () => void;\n}\n\nexport interface WidgetHealth {\n status: 'healthy' | 'degraded' | 'unhealthy' | 'unknown';\n lastSuccessAt?: string | null;\n errorMessage?: string | null;\n}\n\nexport interface WidgetMeta {\n widgetId: string;\n title?: string;\n subtitle?: string;\n description?: string;\n maxContentHeight?: string;\n isCollapsible: boolean;\n isPinnable: boolean;\n askContext?: WidgetAskContext;\n aiFooterEnabled: boolean;\n className?: string;\n /** Integration health for this widget's product, if known. */\n health?: WidgetHealth;\n}\n\nexport interface WidgetContextValue {\n state: WidgetState;\n actions: WidgetActions;\n meta: WidgetMeta;\n}\n\nexport const WidgetContext = createContext<WidgetContextValue | null>(null);\n"]}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function WidgetLoadingState(): import("react").JSX.Element;
|
|
2
|
+
export declare function WidgetErrorState(): import("react").JSX.Element;
|
|
3
|
+
export declare function WidgetInactiveState(): import("react").JSX.Element;
|
|
4
|
+
export declare function WidgetDataLoading(): import("react").JSX.Element;
|
|
5
|
+
export declare function WidgetDisabledState(): import("react").JSX.Element;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Icon } from '@ekanos/ui/icon';
|
|
3
|
+
import { Trans } from '@ekanos/ui/trans';
|
|
4
|
+
export function WidgetLoadingState() {
|
|
5
|
+
return (_jsxs("div", { className: "flex flex-col items-center justify-center py-8 text-center", children: [_jsx(Icon, { name: "fa-light fa-spinner", className: "text-muted-foreground mb-4 h-8 w-8 animate-spin" }), _jsx("p", { className: "text-muted-foreground text-sm", children: _jsx(Trans, { i18nKey: "dashboard:widgetLoading", defaults: "Loading..." }) })] }));
|
|
6
|
+
}
|
|
7
|
+
export function WidgetErrorState() {
|
|
8
|
+
return (_jsxs("div", { className: "flex flex-col items-center justify-center py-8 text-center", children: [_jsx(Icon, { name: "fa-light fa-circle-exclamation", className: "text-destructive mb-4 h-8 w-8" }), _jsx("p", { className: "text-destructive mb-2 text-sm font-medium", children: _jsx(Trans, { i18nKey: "dashboard:widgetErrorTitle", defaults: "Unable to load data" }) }), _jsx("p", { className: "text-muted-foreground text-xs", children: _jsx(Trans, { i18nKey: "dashboard:widgetErrorBody", defaults: "Please try again later" }) })] }));
|
|
9
|
+
}
|
|
10
|
+
export function WidgetInactiveState() {
|
|
11
|
+
return (_jsx("div", { className: "flex flex-col items-center justify-center py-8 text-center", children: _jsx("p", { className: "text-muted-foreground text-sm", children: _jsx(Trans, { i18nKey: "dashboard:widgetInactive", defaults: "Widget inactive" }) }) }));
|
|
12
|
+
}
|
|
13
|
+
export function WidgetDataLoading() {
|
|
14
|
+
return (_jsx("div", { className: "flex flex-1 items-center justify-center py-12", children: _jsx(Icon, { name: "fa-light fa-spinner", className: "text-muted-foreground h-8 w-8 animate-spin" }) }));
|
|
15
|
+
}
|
|
16
|
+
export function WidgetDisabledState() {
|
|
17
|
+
return (_jsxs("div", { className: "flex flex-col items-center justify-center py-8 text-center", children: [_jsx(Icon, { name: "fa-light fa-ban", className: "text-muted-foreground mb-4 h-8 w-8" }), _jsx("p", { className: "text-muted-foreground text-sm", children: _jsx(Trans, { i18nKey: "dashboard:widgetDisabled", defaults: "Widget Disabled" }) })] }));
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=widget-state-components.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"widget-state-components.js","sourceRoot":"","sources":["../../../src/components/widgets/widget-state-components.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AACvC,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAEzC,MAAM,UAAU,kBAAkB;IAChC,OAAO,CACL,eAAK,SAAS,EAAC,4DAA4D,aACzE,KAAC,IAAI,IACH,IAAI,EAAC,qBAAqB,EAC1B,SAAS,EAAC,iDAAiD,GAC3D,EACF,YAAG,SAAS,EAAC,+BAA+B,YAC1C,KAAC,KAAK,IAAC,OAAO,EAAC,yBAAyB,EAAC,QAAQ,EAAC,YAAY,GAAG,GAC/D,IACA,CACP,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,gBAAgB;IAC9B,OAAO,CACL,eAAK,SAAS,EAAC,4DAA4D,aACzE,KAAC,IAAI,IACH,IAAI,EAAC,gCAAgC,EACrC,SAAS,EAAC,+BAA+B,GACzC,EACF,YAAG,SAAS,EAAC,2CAA2C,YACtD,KAAC,KAAK,IACJ,OAAO,EAAC,4BAA4B,EACpC,QAAQ,EAAC,qBAAqB,GAC9B,GACA,EACJ,YAAG,SAAS,EAAC,+BAA+B,YAC1C,KAAC,KAAK,IACJ,OAAO,EAAC,2BAA2B,EACnC,QAAQ,EAAC,wBAAwB,GACjC,GACA,IACA,CACP,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,mBAAmB;IACjC,OAAO,CACL,cAAK,SAAS,EAAC,4DAA4D,YACzE,YAAG,SAAS,EAAC,+BAA+B,YAC1C,KAAC,KAAK,IAAC,OAAO,EAAC,0BAA0B,EAAC,QAAQ,EAAC,iBAAiB,GAAG,GACrE,GACA,CACP,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,iBAAiB;IAC/B,OAAO,CACL,cAAK,SAAS,EAAC,+CAA+C,YAC5D,KAAC,IAAI,IACH,IAAI,EAAC,qBAAqB,EAC1B,SAAS,EAAC,4CAA4C,GACtD,GACE,CACP,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,mBAAmB;IACjC,OAAO,CACL,eAAK,SAAS,EAAC,4DAA4D,aACzE,KAAC,IAAI,IACH,IAAI,EAAC,iBAAiB,EACtB,SAAS,EAAC,oCAAoC,GAC9C,EACF,YAAG,SAAS,EAAC,+BAA+B,YAC1C,KAAC,KAAK,IAAC,OAAO,EAAC,0BAA0B,EAAC,QAAQ,EAAC,iBAAiB,GAAG,GACrE,IACA,CACP,CAAC;AACJ,CAAC","sourcesContent":["import { Icon } from '@ekanos/ui/icon';\nimport { Trans } from '@ekanos/ui/trans';\n\nexport function WidgetLoadingState() {\n return (\n <div className=\"flex flex-col items-center justify-center py-8 text-center\">\n <Icon\n name=\"fa-light fa-spinner\"\n className=\"text-muted-foreground mb-4 h-8 w-8 animate-spin\"\n />\n <p className=\"text-muted-foreground text-sm\">\n <Trans i18nKey=\"dashboard:widgetLoading\" defaults=\"Loading...\" />\n </p>\n </div>\n );\n}\n\nexport function WidgetErrorState() {\n return (\n <div className=\"flex flex-col items-center justify-center py-8 text-center\">\n <Icon\n name=\"fa-light fa-circle-exclamation\"\n className=\"text-destructive mb-4 h-8 w-8\"\n />\n <p className=\"text-destructive mb-2 text-sm font-medium\">\n <Trans\n i18nKey=\"dashboard:widgetErrorTitle\"\n defaults=\"Unable to load data\"\n />\n </p>\n <p className=\"text-muted-foreground text-xs\">\n <Trans\n i18nKey=\"dashboard:widgetErrorBody\"\n defaults=\"Please try again later\"\n />\n </p>\n </div>\n );\n}\n\nexport function WidgetInactiveState() {\n return (\n <div className=\"flex flex-col items-center justify-center py-8 text-center\">\n <p className=\"text-muted-foreground text-sm\">\n <Trans i18nKey=\"dashboard:widgetInactive\" defaults=\"Widget inactive\" />\n </p>\n </div>\n );\n}\n\nexport function WidgetDataLoading() {\n return (\n <div className=\"flex flex-1 items-center justify-center py-12\">\n <Icon\n name=\"fa-light fa-spinner\"\n className=\"text-muted-foreground h-8 w-8 animate-spin\"\n />\n </div>\n );\n}\n\nexport function WidgetDisabledState() {\n return (\n <div className=\"flex flex-col items-center justify-center py-8 text-center\">\n <Icon\n name=\"fa-light fa-ban\"\n className=\"text-muted-foreground mb-4 h-8 w-8\"\n />\n <p className=\"text-muted-foreground text-sm\">\n <Trans i18nKey=\"dashboard:widgetDisabled\" defaults=\"Widget Disabled\" />\n </p>\n </div>\n );\n}\n"]}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import type { WidgetAskContext } from '../../types/widget-ask-context.js';
|
|
3
|
+
declare function WidgetCard({ children }: {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
}): import("react").JSX.Element;
|
|
6
|
+
declare function WidgetHeader({ title, subtitle, description, children, }: {
|
|
7
|
+
/** Override the provider's title — useful when the rendered name differs from the config name. */
|
|
8
|
+
title?: string;
|
|
9
|
+
/** Override the provider's subtitle — useful for widget-computed values. */
|
|
10
|
+
subtitle?: string;
|
|
11
|
+
/** Override the provider's description — useful for widget-computed values. */
|
|
12
|
+
description?: string;
|
|
13
|
+
children?: ReactNode;
|
|
14
|
+
}): import("react").JSX.Element | null;
|
|
15
|
+
declare function WidgetActive({ children }: {
|
|
16
|
+
children: ReactNode;
|
|
17
|
+
}): import("react").JSX.Element | null;
|
|
18
|
+
declare function WidgetLoading({ children }: {
|
|
19
|
+
children?: ReactNode;
|
|
20
|
+
}): import("react").JSX.Element | null;
|
|
21
|
+
declare function WidgetError({ children }: {
|
|
22
|
+
children?: ReactNode;
|
|
23
|
+
}): import("react").JSX.Element | null;
|
|
24
|
+
declare function WidgetInactive({ children }: {
|
|
25
|
+
children?: ReactNode;
|
|
26
|
+
}): import("react").JSX.Element | null;
|
|
27
|
+
declare function WidgetDisabled({ children }: {
|
|
28
|
+
children?: ReactNode;
|
|
29
|
+
}): import("react").JSX.Element | null;
|
|
30
|
+
declare function WidgetFooter({ children }: {
|
|
31
|
+
children: ReactNode;
|
|
32
|
+
}): import("react").JSX.Element | null;
|
|
33
|
+
declare function WidgetActions({ children }: {
|
|
34
|
+
children: ReactNode;
|
|
35
|
+
}): import("react").JSX.Element;
|
|
36
|
+
declare function WidgetAskFooter({ context }: {
|
|
37
|
+
context?: WidgetAskContext;
|
|
38
|
+
}): import("react").JSX.Element | null;
|
|
39
|
+
/**
|
|
40
|
+
* Renders a subtle "data may be stale" banner when the widget's integration is
|
|
41
|
+
* `degraded` (but still active). Unhealthy integrations route to `disabled`
|
|
42
|
+
* instead, so this only fires for the degraded middle ground.
|
|
43
|
+
*/
|
|
44
|
+
declare function WidgetHealthFooter(): import("react").JSX.Element | null;
|
|
45
|
+
/**
|
|
46
|
+
* Wraps children in a sub-provider that overrides the effective render state
|
|
47
|
+
* with the widget's own data loading / error state. Only overrides when the
|
|
48
|
+
* parent state is `active` — so the integration's `inactive` / `disabled` /
|
|
49
|
+
* `loading` (initial) states still win.
|
|
50
|
+
*
|
|
51
|
+
* Read the parent integration state BEFORE wrapping in this component if you
|
|
52
|
+
* need it to gate data fetching:
|
|
53
|
+
*
|
|
54
|
+
* ```tsx
|
|
55
|
+
* const { state } = use(WidgetContext);
|
|
56
|
+
* const integrationActive = state.state === 'active';
|
|
57
|
+
* const { isLoading, isError } = useMyData(accountId, integrationActive);
|
|
58
|
+
* return (
|
|
59
|
+
* <Widget.DataState loading={isLoading} error={isError}>
|
|
60
|
+
* <Widget.Card>…</Widget.Card>
|
|
61
|
+
* </Widget.DataState>
|
|
62
|
+
* );
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
declare function WidgetDataState({ loading, error, inactive, children, }: {
|
|
66
|
+
loading?: boolean;
|
|
67
|
+
error?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Force the `inactive` render state even though the integration itself is
|
|
70
|
+
* active. Use for widget-level "not connected" states (e.g. the product is
|
|
71
|
+
* activated but the upstream OAuth connection is missing). Takes priority
|
|
72
|
+
* over `loading` / `error`.
|
|
73
|
+
*/
|
|
74
|
+
inactive?: boolean;
|
|
75
|
+
children: ReactNode;
|
|
76
|
+
}): import("react").JSX.Element;
|
|
77
|
+
export declare const Widget: {
|
|
78
|
+
Card: typeof WidgetCard;
|
|
79
|
+
Header: typeof WidgetHeader;
|
|
80
|
+
DataState: typeof WidgetDataState;
|
|
81
|
+
Active: typeof WidgetActive;
|
|
82
|
+
Loading: typeof WidgetLoading;
|
|
83
|
+
Error: typeof WidgetError;
|
|
84
|
+
Inactive: typeof WidgetInactive;
|
|
85
|
+
Disabled: typeof WidgetDisabled;
|
|
86
|
+
Footer: typeof WidgetFooter;
|
|
87
|
+
Actions: typeof WidgetActions;
|
|
88
|
+
AskFooter: typeof WidgetAskFooter;
|
|
89
|
+
HealthFooter: typeof WidgetHealthFooter;
|
|
90
|
+
};
|
|
91
|
+
export {};
|