@ekanos/sdk 0.3.0 → 0.4.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.
Files changed (60) hide show
  1. package/README.md +113 -33
  2. package/api-report.md +7 -3
  3. package/dist/components/base-activation-dialog.d.ts +15 -5
  4. package/dist/components/base-activation-dialog.js +31 -5
  5. package/dist/components/base-activation-dialog.js.map +1 -1
  6. package/dist/components/base-activation-form.d.ts +20 -5
  7. package/dist/components/base-activation-form.js +12 -5
  8. package/dist/components/base-activation-form.js.map +1 -1
  9. package/dist/components/base-marketplace-tile.js +12 -10
  10. package/dist/components/base-marketplace-tile.js.map +1 -1
  11. package/dist/components/index.d.ts +1 -1
  12. package/dist/components/index.js.map +1 -1
  13. package/dist/components/oauth-activation-form.d.ts +23 -16
  14. package/dist/components/oauth-activation-form.js +34 -20
  15. package/dist/components/oauth-activation-form.js.map +1 -1
  16. package/dist/components/widgets/use-relative-time.d.ts +7 -0
  17. package/dist/components/widgets/use-relative-time.js +55 -0
  18. package/dist/components/widgets/use-relative-time.js.map +1 -0
  19. package/dist/components/widgets/widget-ask-bar.js.map +1 -1
  20. package/dist/components/widgets/widget-context.d.ts +21 -0
  21. package/dist/components/widgets/widget-context.js.map +1 -1
  22. package/dist/components/widgets/widget-preview-provider.d.ts +1 -1
  23. package/dist/components/widgets/widget-preview-provider.js +1 -1
  24. package/dist/components/widgets/widget-preview-provider.js.map +1 -1
  25. package/dist/components/widgets/widget-state-components.d.ts +4 -1
  26. package/dist/components/widgets/widget-state-components.js +6 -4
  27. package/dist/components/widgets/widget-state-components.js.map +1 -1
  28. package/dist/components/widgets/widget.d.ts +47 -2
  29. package/dist/components/widgets/widget.js +111 -23
  30. package/dist/components/widgets/widget.js.map +1 -1
  31. package/dist/context/egress.js.map +1 -1
  32. package/dist/context/storage-validation.d.ts +24 -4
  33. package/dist/context/storage-validation.js +5 -2
  34. package/dist/context/storage-validation.js.map +1 -1
  35. package/dist/hooks/ask-assistant-context.js.map +1 -1
  36. package/dist/hooks/fetch-integration-storage.js.map +1 -1
  37. package/dist/hooks/partner-oauth.js +3 -0
  38. package/dist/hooks/partner-oauth.js.map +1 -1
  39. package/dist/hooks/use-integration-action.js.map +1 -1
  40. package/dist/hooks/use-oauth-connection-status.d.ts +78 -78
  41. package/dist/hooks/use-oauth-connection-status.js +17 -10
  42. package/dist/hooks/use-oauth-connection-status.js.map +1 -1
  43. package/dist/hooks/widget-event-context.js.map +1 -1
  44. package/dist/integration/event-surfaces.js.map +1 -1
  45. package/dist/integration/index.d.ts +2 -2
  46. package/dist/integration/index.js +1 -1
  47. package/dist/integration/index.js.map +1 -1
  48. package/dist/testing/invoke.js +9 -5
  49. package/dist/testing/invoke.js.map +1 -1
  50. package/dist/testing/mock-context.js +8 -7
  51. package/dist/testing/mock-context.js.map +1 -1
  52. package/dist/testing/mock-fetch-provider.js.map +1 -1
  53. package/dist/types/integration.d.ts +27 -21
  54. package/dist/types/integration.js.map +1 -1
  55. package/dist/types/widget-ask-context.d.ts +9 -0
  56. package/dist/types/widget-ask-context.js.map +1 -1
  57. package/dist/types/workspace-target.d.ts +3 -2
  58. package/dist/types/workspace-target.js.map +1 -1
  59. package/eslint.preset.mjs +9 -8
  60. package/package.json +37 -41
@@ -1 +1 @@
1
- {"version":3,"file":"widget-preview-provider.js","sourceRoot":"","sources":["../../../src/components/widgets/widget-preview-provider.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAkB,WAAW,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAGvE,OAAO,EACL,aAAa,GAId,MAAM,kBAAkB,CAAC;AAqB1B;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,qBAAqB,CAAC,EACpC,QAAQ,EACR,KAAK,EACL,QAAQ,EACR,WAAW,EACX,SAAS,EACT,KAAK,GAAG,QAAQ,EAChB,aAAa,GAAG,KAAK,EACrB,UAAU,GAAG,KAAK,EAClB,gBAAgB,GAAG,KAAK,EACxB,aAAa,GAAG,KAAK,EACrB,gBAAgB,EAChB,UAAU,EACV,eAAe,GAAG,KAAK,EACvB,MAAM,EACN,QAAQ,GACmB;IAC3B,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAC,gBAAgB,CAAC,CAAC;IAC7D,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAC,aAAa,CAAC,CAAC;IAEpD,MAAM,eAAe,GAAG,WAAW,CACjC,GAAG,EAAE,CAAC,YAAY,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,EAC3C,EAAE,CACH,CAAC;IAEF,MAAM,YAAY,GAAG,WAAW,CAC9B,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,EACxC,EAAE,CACH,CAAC;IAEF,MAAM,KAAK,GAAG,OAAO,CACnB,GAAG,EAAE,CAAC,CAAC;QACL,KAAK,EAAE;YACL,KAAK;YACL,SAAS;YACT,MAAM;YACN,eAAe,EAAE,KAAK;YACtB,UAAU,EAAE,KAAK;SAClB;QACD,OAAO,EAAE,EAAE,eAAe,EAAE,YAAY,EAAE;QAC1C,IAAI,EAAE;YACJ,QAAQ;YACR,KAAK;YACL,QAAQ;YACR,WAAW;YACX,SAAS;YACT,gBAAgB;YAChB,aAAa;YACb,UAAU;YACV,UAAU;YACV,eAAe;YACf,MAAM;SACP;KACF,CAAC,EACF;QACE,eAAe;QACf,UAAU;QACV,SAAS;QACT,SAAS;QACT,WAAW;QACX,MAAM;QACN,aAAa;QACb,UAAU;QACV,gBAAgB;QAChB,MAAM;QACN,KAAK;QACL,QAAQ;QACR,KAAK;QACL,eAAe;QACf,YAAY;QACZ,QAAQ;KACT,CACF,CAAC;IAEF,OAAO,KAAC,aAAa,IAAC,KAAK,EAAE,KAAK,YAAG,QAAQ,GAAiB,CAAC;AACjE,CAAC","sourcesContent":["'use client';\n\nimport { type ReactNode, useCallback, useMemo, useState } from 'react';\n\nimport type { WidgetAskContext } from '../../types/widget-ask-context';\nimport {\n WidgetContext,\n type WidgetContextValue,\n type WidgetHealth,\n type WidgetRenderState,\n} from './widget-context';\n\nexport interface WidgetPreviewProviderProps {\n widgetId: string;\n title?: string;\n subtitle?: string;\n description?: string;\n className?: string;\n /** Defaults to `'active'` — the state you almost always want to look at. */\n state?: WidgetRenderState;\n isCollapsible?: boolean;\n isPinnable?: boolean;\n defaultCollapsed?: boolean;\n defaultPinned?: boolean;\n maxContentHeight?: string;\n askContext?: WidgetAskContext;\n aiFooterEnabled?: boolean;\n health?: WidgetHealth;\n children: ReactNode;\n}\n\n/**\n * Renders a widget outside the Fusion dashboard.\n *\n * `Widget.*` reads everything — render state, collapse/pin, title — from\n * `WidgetContext`, which in production is filled by the host's\n * `DashboardWidgetProvider` (config hierarchy, persistence callbacks, health\n * resolution). None of that exists in a partner's own app, a Storybook story,\n * or a component test, and without SOME provider every `Widget.*` throws. This\n * is that provider: same context value, local state only, nothing persisted.\n *\n * ```tsx\n * <WidgetPreviewProvider widgetId=\"my-widget\" title=\"My widget\">\n * <MyWidget accountId={accountId} productSlug=\"my-integration\" />\n * </WidgetPreviewProvider>\n * ```\n *\n * Flip `state` to walk every branch your widget renders (`'loading'`,\n * `'error'`, `'inactive'`, `'disabled'`) without faking a network condition.\n */\nexport function WidgetPreviewProvider({\n widgetId,\n title,\n subtitle,\n description,\n className,\n state = 'active',\n isCollapsible = false,\n isPinnable = false,\n defaultCollapsed = false,\n defaultPinned = false,\n maxContentHeight,\n askContext,\n aiFooterEnabled = false,\n health,\n children,\n}: WidgetPreviewProviderProps) {\n const [collapsed, setCollapsed] = useState(defaultCollapsed);\n const [pinned, setPinned] = useState(defaultPinned);\n\n const toggleCollapsed = useCallback(\n () => setCollapsed((previous) => !previous),\n [],\n );\n\n const togglePinned = useCallback(\n () => setPinned((previous) => !previous),\n [],\n );\n\n const value = useMemo<WidgetContextValue>(\n () => ({\n state: {\n state,\n collapsed,\n pinned,\n pendingCollapse: false,\n pendingPin: false,\n },\n actions: { toggleCollapsed, togglePinned },\n meta: {\n widgetId,\n title,\n subtitle,\n description,\n className,\n maxContentHeight,\n isCollapsible,\n isPinnable,\n askContext,\n aiFooterEnabled,\n health,\n },\n }),\n [\n aiFooterEnabled,\n askContext,\n className,\n collapsed,\n description,\n health,\n isCollapsible,\n isPinnable,\n maxContentHeight,\n pinned,\n state,\n subtitle,\n title,\n toggleCollapsed,\n togglePinned,\n widgetId,\n ],\n );\n\n return <WidgetContext value={value}>{children}</WidgetContext>;\n}\n"]}
1
+ {"version":3,"file":"widget-preview-provider.js","sourceRoot":"","sources":["../../../src/components/widgets/widget-preview-provider.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAkB,WAAW,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAGvE,OAAO,EACL,aAAa,GAId,MAAM,kBAAkB,CAAC;AAqB1B;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,qBAAqB,CAAC,EACpC,QAAQ,EACR,KAAK,EACL,QAAQ,EACR,WAAW,EACX,SAAS,EACT,KAAK,GAAG,QAAQ,EAChB,aAAa,GAAG,KAAK,EACrB,UAAU,GAAG,KAAK,EAClB,gBAAgB,GAAG,KAAK,EACxB,aAAa,GAAG,KAAK,EACrB,gBAAgB,EAChB,UAAU,EACV,eAAe,GAAG,KAAK,EACvB,MAAM,EACN,QAAQ,GACmB;IAC3B,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAC,gBAAgB,CAAC,CAAC;IAC7D,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAC,aAAa,CAAC,CAAC;IAEpD,MAAM,eAAe,GAAG,WAAW,CACjC,GAAG,EAAE,CAAC,YAAY,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,EAC3C,EAAE,CACH,CAAC;IAEF,MAAM,YAAY,GAAG,WAAW,CAC9B,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,EACxC,EAAE,CACH,CAAC;IAEF,MAAM,KAAK,GAAG,OAAO,CACnB,GAAG,EAAE,CAAC,CAAC;QACL,KAAK,EAAE;YACL,KAAK;YACL,SAAS;YACT,MAAM;YACN,eAAe,EAAE,KAAK;YACtB,UAAU,EAAE,KAAK;SAClB;QACD,OAAO,EAAE,EAAE,eAAe,EAAE,YAAY,EAAE;QAC1C,IAAI,EAAE;YACJ,QAAQ;YACR,KAAK;YACL,QAAQ;YACR,WAAW;YACX,SAAS;YACT,gBAAgB;YAChB,aAAa;YACb,UAAU;YACV,UAAU;YACV,eAAe;YACf,MAAM;SACP;KACF,CAAC,EACF;QACE,eAAe;QACf,UAAU;QACV,SAAS;QACT,SAAS;QACT,WAAW;QACX,MAAM;QACN,aAAa;QACb,UAAU;QACV,gBAAgB;QAChB,MAAM;QACN,KAAK;QACL,QAAQ;QACR,KAAK;QACL,eAAe;QACf,YAAY;QACZ,QAAQ;KACT,CACF,CAAC;IAEF,OAAO,KAAC,aAAa,IAAC,KAAK,EAAE,KAAK,YAAG,QAAQ,GAAiB,CAAC;AACjE,CAAC","sourcesContent":["'use client';\n\nimport { type ReactNode, useCallback, useMemo, useState } from 'react';\n\nimport type { WidgetAskContext } from '../../types/widget-ask-context';\nimport {\n WidgetContext,\n type WidgetContextValue,\n type WidgetHealth,\n type WidgetRenderState,\n} from './widget-context';\n\nexport interface WidgetPreviewProviderProps {\n widgetId: string;\n title?: string;\n subtitle?: string;\n description?: string;\n className?: string;\n /** Defaults to `'active'` — the state you almost always want to look at. */\n state?: WidgetRenderState;\n isCollapsible?: boolean;\n isPinnable?: boolean;\n defaultCollapsed?: boolean;\n defaultPinned?: boolean;\n maxContentHeight?: string;\n askContext?: WidgetAskContext;\n aiFooterEnabled?: boolean;\n health?: WidgetHealth;\n children: ReactNode;\n}\n\n/**\n * Renders a widget outside the Ekanos dashboard.\n *\n * `Widget.*` reads everything — render state, collapse/pin, title — from\n * `WidgetContext`, which in production is filled by the host's\n * `DashboardWidgetProvider` (config hierarchy, persistence callbacks, health\n * resolution). None of that exists in a partner's own app, a Storybook story,\n * or a component test, and without SOME provider every `Widget.*` throws. This\n * is that provider: same context value, local state only, nothing persisted.\n *\n * ```tsx\n * <WidgetPreviewProvider widgetId=\"my-widget\" title=\"My widget\">\n * <MyWidget accountId={accountId} productSlug=\"my-integration\" />\n * </WidgetPreviewProvider>\n * ```\n *\n * Flip `state` to walk every branch your widget renders (`'loading'`,\n * `'error'`, `'inactive'`, `'disabled'`) without faking a network condition.\n */\nexport function WidgetPreviewProvider({\n widgetId,\n title,\n subtitle,\n description,\n className,\n state = 'active',\n isCollapsible = false,\n isPinnable = false,\n defaultCollapsed = false,\n defaultPinned = false,\n maxContentHeight,\n askContext,\n aiFooterEnabled = false,\n health,\n children,\n}: WidgetPreviewProviderProps) {\n const [collapsed, setCollapsed] = useState(defaultCollapsed);\n const [pinned, setPinned] = useState(defaultPinned);\n\n const toggleCollapsed = useCallback(\n () => setCollapsed((previous) => !previous),\n [],\n );\n\n const togglePinned = useCallback(\n () => setPinned((previous) => !previous),\n [],\n );\n\n const value = useMemo<WidgetContextValue>(\n () => ({\n state: {\n state,\n collapsed,\n pinned,\n pendingCollapse: false,\n pendingPin: false,\n },\n actions: { toggleCollapsed, togglePinned },\n meta: {\n widgetId,\n title,\n subtitle,\n description,\n className,\n maxContentHeight,\n isCollapsible,\n isPinnable,\n askContext,\n aiFooterEnabled,\n health,\n },\n }),\n [\n aiFooterEnabled,\n askContext,\n className,\n collapsed,\n description,\n health,\n isCollapsible,\n isPinnable,\n maxContentHeight,\n pinned,\n state,\n subtitle,\n title,\n toggleCollapsed,\n togglePinned,\n widgetId,\n ],\n );\n\n return <WidgetContext value={value}>{children}</WidgetContext>;\n}\n"]}
@@ -1,5 +1,8 @@
1
1
  export declare function WidgetLoadingState(): import("react").JSX.Element;
2
- export declare function WidgetErrorState(): import("react").JSX.Element;
2
+ export declare function WidgetErrorState({ onRetry, retrying, }: {
3
+ onRetry?: () => void;
4
+ retrying?: boolean;
5
+ }): import("react").JSX.Element;
3
6
  export declare function WidgetInactiveState(): import("react").JSX.Element;
4
7
  export declare function WidgetDataLoading(): import("react").JSX.Element;
5
8
  export declare function WidgetDisabledState(): import("react").JSX.Element;
@@ -1,17 +1,19 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Button } from '@ekanos/ui/button';
2
3
  import { Icon } from '@ekanos/ui/icon';
3
4
  import { Trans } from '@ekanos/ui/trans';
5
+ import { cn } from '@ekanos/ui/utils';
4
6
  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..." }) })] }));
7
+ return (_jsxs("div", { className: "flex flex-col items-center justify-center py-8 text-center", children: [_jsx(Icon, { name: "fa-light fa-spinner-third", 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
8
  }
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
+ export function WidgetErrorState({ onRetry, retrying = false, }) {
10
+ return (_jsxs("div", { role: "alert", className: "flex flex-col items-center justify-center gap-3 px-5 py-8 text-center", children: [_jsx("div", { className: "bg-destructive/10 flex h-10 w-10 items-center justify-center rounded-full", children: _jsx(Icon, { name: "fa-light fa-circle-exclamation", className: "text-destructive h-5 w-5", "aria-hidden": true }) }), _jsxs("div", { className: "space-y-1", children: [_jsx("p", { className: "text-foreground text-sm font-medium", children: _jsx(Trans, { i18nKey: "dashboard:widgetErrorTitle", defaults: "Couldn't load this widget" }) }), _jsx("p", { className: "text-muted-foreground text-sm", children: onRetry ? (_jsx(Trans, { i18nKey: "dashboard:widgetErrorBodyRetry", defaults: "Try again, or check back in a few minutes." })) : (_jsx(Trans, { i18nKey: "dashboard:widgetErrorBody", defaults: "Check back in a few minutes." })) })] }), onRetry ? (_jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: onRetry, disabled: retrying, "data-test": "widget-error-retry", children: [_jsx(Icon, { name: "fa-light fa-arrows-rotate", className: cn('h-4 w-4', retrying && 'animate-spin'), "aria-hidden": true }), _jsx(Trans, { i18nKey: "dashboard:widgetRetry", defaults: "Try again" })] })) : null] }));
9
11
  }
10
12
  export function WidgetInactiveState() {
11
13
  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
14
  }
13
15
  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" }) }));
16
+ return (_jsx("div", { className: "flex flex-1 items-center justify-center py-12", children: _jsx(Icon, { name: "fa-light fa-spinner-third", className: "text-muted-foreground h-8 w-8 animate-spin" }) }));
15
17
  }
16
18
  export function WidgetDisabledState() {
17
19
  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" }) })] }));
@@ -1 +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"]}
1
+ {"version":3,"file":"widget-state-components.js","sourceRoot":"","sources":["../../../src/components/widgets/widget-state-components.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC3C,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AACvC,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACzC,OAAO,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAC;AAEtC,MAAM,UAAU,kBAAkB;IAChC,OAAO,CACL,eAAK,SAAS,EAAC,4DAA4D,aACzE,KAAC,IAAI,IACH,IAAI,EAAC,2BAA2B,EAChC,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,CAAC,EAC/B,OAAO,EACP,QAAQ,GAAG,KAAK,GAIjB;IACC,OAAO,CACL,eACE,IAAI,EAAC,OAAO,EACZ,SAAS,EAAC,uEAAuE,aAEjF,cAAK,SAAS,EAAC,2EAA2E,YACxF,KAAC,IAAI,IACH,IAAI,EAAC,gCAAgC,EACrC,SAAS,EAAC,0BAA0B,wBAEpC,GACE,EACN,eAAK,SAAS,EAAC,WAAW,aACxB,YAAG,SAAS,EAAC,qCAAqC,YAChD,KAAC,KAAK,IACJ,OAAO,EAAC,4BAA4B,EACpC,QAAQ,EAAC,2BAA2B,GACpC,GACA,EACJ,YAAG,SAAS,EAAC,+BAA+B,YACzC,OAAO,CAAC,CAAC,CAAC,CACT,KAAC,KAAK,IACJ,OAAO,EAAC,gCAAgC,EACxC,QAAQ,EAAC,4CAA4C,GACrD,CACH,CAAC,CAAC,CAAC,CACF,KAAC,KAAK,IACJ,OAAO,EAAC,2BAA2B,EACnC,QAAQ,EAAC,8BAA8B,GACvC,CACH,GACC,IACA,EACL,OAAO,CAAC,CAAC,CAAC,CACT,MAAC,MAAM,IACL,IAAI,EAAC,QAAQ,EACb,OAAO,EAAC,SAAS,EACjB,IAAI,EAAC,IAAI,EACT,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,QAAQ,eACR,oBAAoB,aAE9B,KAAC,IAAI,IACH,IAAI,EAAC,2BAA2B,EAChC,SAAS,EAAE,EAAE,CAAC,SAAS,EAAE,QAAQ,IAAI,cAAc,CAAC,wBAEpD,EACF,KAAC,KAAK,IAAC,OAAO,EAAC,uBAAuB,EAAC,QAAQ,EAAC,WAAW,GAAG,IACvD,CACV,CAAC,CAAC,CAAC,IAAI,IACJ,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,2BAA2B,EAChC,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 { Button } from '@ekanos/ui/button';\nimport { Icon } from '@ekanos/ui/icon';\nimport { Trans } from '@ekanos/ui/trans';\nimport { cn } from '@ekanos/ui/utils';\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-third\"\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 onRetry,\n retrying = false,\n}: {\n onRetry?: () => void;\n retrying?: boolean;\n}) {\n return (\n <div\n role=\"alert\"\n className=\"flex flex-col items-center justify-center gap-3 px-5 py-8 text-center\"\n >\n <div className=\"bg-destructive/10 flex h-10 w-10 items-center justify-center rounded-full\">\n <Icon\n name=\"fa-light fa-circle-exclamation\"\n className=\"text-destructive h-5 w-5\"\n aria-hidden\n />\n </div>\n <div className=\"space-y-1\">\n <p className=\"text-foreground text-sm font-medium\">\n <Trans\n i18nKey=\"dashboard:widgetErrorTitle\"\n defaults=\"Couldn't load this widget\"\n />\n </p>\n <p className=\"text-muted-foreground text-sm\">\n {onRetry ? (\n <Trans\n i18nKey=\"dashboard:widgetErrorBodyRetry\"\n defaults=\"Try again, or check back in a few minutes.\"\n />\n ) : (\n <Trans\n i18nKey=\"dashboard:widgetErrorBody\"\n defaults=\"Check back in a few minutes.\"\n />\n )}\n </p>\n </div>\n {onRetry ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={onRetry}\n disabled={retrying}\n data-test=\"widget-error-retry\"\n >\n <Icon\n name=\"fa-light fa-arrows-rotate\"\n className={cn('h-4 w-4', retrying && 'animate-spin')}\n aria-hidden\n />\n <Trans i18nKey=\"dashboard:widgetRetry\" defaults=\"Try again\" />\n </Button>\n ) : null}\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-third\"\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"]}
@@ -3,15 +3,35 @@ import type { WidgetAskContext } from '../../types/widget-ask-context.js';
3
3
  declare function WidgetCard({ children }: {
4
4
  children: ReactNode;
5
5
  }): import("react").JSX.Element;
6
- declare function WidgetHeader({ title, subtitle, description, children, }: {
6
+ declare function WidgetHeader({ title, subtitle, description, menu, children, }: {
7
7
  /** Override the provider's title — useful when the rendered name differs from the config name. */
8
8
  title?: string;
9
9
  /** Override the provider's subtitle — useful for widget-computed values. */
10
10
  subtitle?: string;
11
11
  /** Override the provider's description — useful for widget-computed values. */
12
12
  description?: string;
13
+ /**
14
+ * `Widget.MenuItem`s placed at the top of the header's ⋯ menu. Use it for
15
+ * occasional actions — "Open in QuickBooks", "Open in Outlook" — and keep
16
+ * `children` for the controls people reach for every visit, like filters.
17
+ */
18
+ menu?: ReactNode;
13
19
  children?: ReactNode;
14
20
  }): import("react").JSX.Element | null;
21
+ /**
22
+ * One widget-specific entry in the header's ⋯ menu; pass it through
23
+ * `Widget.Header`'s `menu` prop. With `href` it is a link, opened in a new tab
24
+ * when `external`; otherwise it runs `onSelect`.
25
+ */
26
+ declare function WidgetMenuItem({ icon, href, external, onSelect, disabled, children, }: {
27
+ /** A Font Awesome class, e.g. `fa-light fa-envelope`. */
28
+ icon?: string;
29
+ href?: string;
30
+ external?: boolean;
31
+ onSelect?: () => void;
32
+ disabled?: boolean;
33
+ children: ReactNode;
34
+ }): import("react").JSX.Element;
15
35
  /**
16
36
  * The standard padded body for content rendered inside `Widget.Active`.
17
37
  *
@@ -95,8 +115,22 @@ declare function WidgetHealthFooter(): import("react").JSX.Element | null;
95
115
  * </Widget.DataState>
96
116
  * );
97
117
  * ```
118
+ *
119
+ * Pass `onRefresh`, `refreshing` and `updatedAt` to give the widget the
120
+ * shared Refresh menu item, a Try again button on its error view, and an
121
+ * "Updated 3 minutes ago" footer. With TanStack Query:
122
+ *
123
+ * ```tsx
124
+ * <Widget.DataState
125
+ * loading={query.isLoading}
126
+ * error={query.isError}
127
+ * onRefresh={() => void query.refetch()}
128
+ * refreshing={query.isFetching}
129
+ * updatedAt={query.dataUpdatedAt}
130
+ * >
131
+ * ```
98
132
  */
99
- declare function WidgetDataState({ loading, error, inactive, children, }: {
133
+ declare function WidgetDataState({ loading, error, inactive, onRefresh, refreshing, updatedAt, children, }: {
100
134
  loading?: boolean;
101
135
  error?: boolean;
102
136
  /**
@@ -106,11 +140,22 @@ declare function WidgetDataState({ loading, error, inactive, children, }: {
106
140
  * over `loading` / `error`.
107
141
  */
108
142
  inactive?: boolean;
143
+ /** Reloads the widget's data. */
144
+ onRefresh?: () => void;
145
+ /** A reload is in flight. */
146
+ refreshing?: boolean;
147
+ /**
148
+ * When the data on screen last loaded successfully: epoch milliseconds, an
149
+ * ISO string or a `Date`. `0` means never, which TanStack Query's
150
+ * `dataUpdatedAt` reports before the first success.
151
+ */
152
+ updatedAt?: number | string | Date | null;
109
153
  children: ReactNode;
110
154
  }): import("react").JSX.Element;
111
155
  export declare const Widget: {
112
156
  Card: typeof WidgetCard;
113
157
  Header: typeof WidgetHeader;
158
+ MenuItem: typeof WidgetMenuItem;
114
159
  DataState: typeof WidgetDataState;
115
160
  Active: typeof WidgetActive;
116
161
  Body: typeof WidgetBody;
@@ -1,13 +1,16 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { Component, use, useMemo } from 'react';
2
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Component, use, useMemo, } from 'react';
4
+ import Link from 'next/link.js';
4
5
  import { Button } from '@ekanos/ui/button';
5
6
  import { Card, CardContent, CardFooter, CardHeader, CardTitle, } from '@ekanos/ui/card';
7
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@ekanos/ui/dropdown-menu';
6
8
  import { Icon } from '@ekanos/ui/icon';
7
9
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@ekanos/ui/tooltip';
8
10
  import { Trans } from '@ekanos/ui/trans';
9
11
  import { cn } from '@ekanos/ui/utils';
10
12
  import { useAnimatedHeight } from './use-animated-height.js';
13
+ import { useRelativeTime } from './use-relative-time.js';
11
14
  import { WidgetAskBar } from './widget-ask-bar.js';
12
15
  import { WidgetContext } from './widget-context.js';
13
16
  import { WidgetDisabledState, WidgetErrorState, WidgetInactiveState, WidgetLoadingState, } from './widget-state-components.js';
@@ -16,6 +19,11 @@ class WidgetErrorBoundary extends Component {
16
19
  constructor() {
17
20
  super(...arguments);
18
21
  this.state = { hasError: false };
22
+ this.retry = () => {
23
+ var _a, _b;
24
+ (_b = (_a = this.props).onRetry) === null || _b === void 0 ? void 0 : _b.call(_a);
25
+ this.setState({ hasError: false });
26
+ };
19
27
  }
20
28
  static getDerivedStateFromError() {
21
29
  return { hasError: true };
@@ -25,7 +33,7 @@ class WidgetErrorBoundary extends Component {
25
33
  }
26
34
  render() {
27
35
  if (this.state.hasError) {
28
- return (_jsxs("div", { className: "flex flex-1 flex-col items-center justify-center gap-3 p-8", children: [_jsx("div", { className: "bg-destructive/10 flex h-16 w-16 items-center justify-center rounded-full", children: _jsx(Icon, { name: "fa-light fa-circle-exclamation", className: "text-destructive h-8 w-8" }) }), _jsx("p", { className: "text-muted-foreground text-base font-medium", children: _jsx(Trans, { i18nKey: "dashboard:widgetCrashedTitle", defaults: "Something went wrong" }) }), _jsx("p", { className: "text-muted-foreground max-w-xs text-center text-sm", children: _jsx(Trans, { i18nKey: "dashboard:widgetCrashedBody", defaults: "This widget encountered an error. Try refreshing the page." }) })] }));
36
+ return (_jsxs("div", { role: "alert", className: "flex flex-1 flex-col items-center justify-center gap-3 px-5 py-8 text-center", children: [_jsx("div", { className: "bg-destructive/10 flex h-10 w-10 items-center justify-center rounded-full", children: _jsx(Icon, { name: "fa-light fa-circle-exclamation", className: "text-destructive h-5 w-5", "aria-hidden": true }) }), _jsxs("div", { className: "space-y-1", children: [_jsx("p", { className: "text-foreground text-sm font-medium", children: _jsx(Trans, { i18nKey: "dashboard:widgetCrashedTitle", defaults: "This widget ran into a problem" }) }), _jsx("p", { className: "text-muted-foreground max-w-xs text-sm", children: _jsx(Trans, { i18nKey: "dashboard:widgetCrashedBody", defaults: "Try again. If it keeps happening, reload the page." }) })] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: this.retry, "data-test": "widget-crash-retry", children: [_jsx(Icon, { name: "fa-light fa-arrows-rotate", className: "h-4 w-4", "aria-hidden": true }), _jsx(Trans, { i18nKey: "dashboard:widgetRetry", defaults: "Try again" })] })] }));
29
37
  }
30
38
  return this.props.children;
31
39
  }
@@ -35,7 +43,7 @@ function useWidget() {
35
43
  const ctx = use(WidgetContext);
36
44
  if (!ctx) {
37
45
  throw new Error('Widget.* compound components must be rendered inside a widget provider. ' +
38
- 'On the Fusion dashboard the host supplies one. Anywhere else — your own ' +
46
+ 'On the Ekanos dashboard the host supplies one. Anywhere else — your own ' +
39
47
  'app, a story, a component test — wrap the widget in ' +
40
48
  "`WidgetPreviewProvider` from '@ekanos/sdk/components'.");
41
49
  }
@@ -44,10 +52,10 @@ function useWidget() {
44
52
  // ─── Card ───────────────────────────────────────────────────────────────────
45
53
  function WidgetCard({ children }) {
46
54
  const { meta } = useWidget();
47
- return (_jsx(Card, { className: cn(
55
+ return (_jsxs(Card, { className: cn(
48
56
  // Neutralize the shadcn card's own py-6/gap-6 box model — widget
49
57
  // chrome owns all vertical spacing via its header/content/footer.
50
- 'shadow-widget relative flex h-full flex-col gap-0 overflow-hidden rounded-lg border-0 py-0', meta.className), "data-widget-id": meta.widgetId, children: children }));
58
+ 'shadow-widget relative flex h-full flex-col gap-0 overflow-hidden rounded-lg border-0 py-0', meta.className), "data-widget-id": meta.widgetId, children: [children, _jsx(WidgetFreshness, {})] }));
51
59
  }
52
60
  // ─── Header ─────────────────────────────────────────────────────────────────
53
61
  function CollapseButton() {
@@ -55,26 +63,68 @@ function CollapseButton() {
55
63
  const { state, actions, meta } = useWidget();
56
64
  return (_jsx(Button, { variant: "outline", size: "icon", onClick: actions.toggleCollapsed, disabled: state.pendingCollapse, className: "bg-background dark:bg-secondary relative h-6 w-6 rounded-full before:absolute before:top-1/2 before:left-1/2 before:h-11 before:w-11 before:-translate-x-1/2 before:-translate-y-1/2 before:content-['']", "aria-expanded": !state.collapsed, "aria-controls": `widget-content-${meta.widgetId}`, "aria-label": state.collapsed
57
65
  ? `Expand ${(_a = meta.title) !== null && _a !== void 0 ? _a : 'widget'}`
58
- : `Collapse ${(_b = meta.title) !== null && _b !== void 0 ? _b : 'widget'}`, children: _jsx(Icon, { name: "fa-light fa-chevron-right", className: cn('h-4 w-4 transition-transform duration-200 ease-[cubic-bezier(0.25,1,0.5,1)] motion-reduce:transition-none', !state.collapsed && 'rotate-90') }) }));
66
+ : `Collapse ${(_b = meta.title) !== null && _b !== void 0 ? _b : 'widget'}`, children: _jsx(Icon, { name: "fa-light fa-chevron-right", className: cn('ease-out-strong h-4 w-4 transition-transform duration-200 motion-reduce:transition-none', !state.collapsed && 'rotate-90') }) }));
59
67
  }
60
- function WidgetHeader({ title, subtitle, description, children, }) {
68
+ function WidgetHeader({ title, subtitle, description, menu, children, }) {
61
69
  const { meta } = useWidget();
62
70
  const effectiveTitle = title !== null && title !== void 0 ? title : meta.title;
63
71
  const effectiveSubtitle = subtitle !== null && subtitle !== void 0 ? subtitle : meta.subtitle;
64
72
  const effectiveDescription = description !== null && description !== void 0 ? description : meta.description;
65
73
  if (!effectiveTitle)
66
74
  return null;
67
- return (_jsx(CardHeader, { className: "flex-shrink-0 border-b pt-6 pb-4 [.border-b]:pb-4", children: _jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [meta.isCollapsible ? _jsx(CollapseButton, {}) : null, effectiveDescription ? (_jsxs(_Fragment, { children: [_jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { render: _jsx(CardTitle, { className: "cursor-help text-lg font-semibold" }), children: effectiveTitle }), _jsx(TooltipContent, { children: _jsx("p", { children: effectiveDescription }) })] }) }), _jsx("span", { className: "sr-only", children: effectiveDescription })] })) : (_jsx(CardTitle, { className: "text-lg font-semibold", children: effectiveTitle })), effectiveSubtitle && (_jsx("span", { className: "text-muted-foreground text-base font-medium", children: effectiveSubtitle }))] }), children && (_jsx("div", { className: "flex shrink-0 items-center gap-2", children: children }))] }) }));
75
+ return (_jsx(CardHeader, { className: "flex-shrink-0 border-b pt-6 pb-4 [.border-b]:pb-4", children: _jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [meta.isCollapsible ? _jsx(CollapseButton, {}) : null, effectiveDescription ? (_jsxs(_Fragment, { children: [_jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { render: _jsx(CardTitle, { className: "cursor-help text-lg font-semibold" }), children: effectiveTitle }), _jsx(TooltipContent, { children: _jsx("p", { children: effectiveDescription }) })] }) }), _jsx("span", { className: "sr-only", children: effectiveDescription })] })) : (_jsx(CardTitle, { className: "text-lg font-semibold", children: effectiveTitle })), effectiveSubtitle && (_jsx("span", { className: "text-muted-foreground text-base font-medium", children: effectiveSubtitle }))] }), _jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [children, _jsx(WidgetMenu, { items: menu })] })] }) }));
76
+ }
77
+ /**
78
+ * The actions every widget shares, whatever it renders: open its full
79
+ * surface, reload it, pin or unpin it. Each item needs something from the host
80
+ * or the widget's data layer, so the menu shows only what applies — and
81
+ * nothing at all when nothing does.
82
+ */
83
+ function WidgetMenu({ items }) {
84
+ var _a;
85
+ const { state, actions, meta, data } = useWidget();
86
+ const refresh = state.state === 'active' ? data === null || data === void 0 ? void 0 : data.refresh : undefined;
87
+ const hasNavigation = !!items || !!meta.openHref || !!refresh;
88
+ if (!hasNavigation && !meta.isPinnable)
89
+ return null;
90
+ const title = (_a = meta.title) !== null && _a !== void 0 ? _a : 'widget';
91
+ return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { render: _jsx(Button, { variant: "ghost", size: "icon-sm", className: "text-muted-foreground hover:text-foreground", "aria-label": `Actions for ${title}`, "data-test": `widget-menu-${meta.widgetId}` }), children: _jsx(Icon, { name: "fa-light fa-ellipsis", className: "h-4 w-4", "aria-hidden": true }) }), _jsxs(DropdownMenuContent, { align: "end", className: "min-w-48", children: [items, meta.openHref ? (_jsxs(DropdownMenuItem, { render: _jsx(Link, { href: meta.openHref }), children: [_jsx(Icon, { name: "fa-light fa-arrow-up-right-from-square", "aria-hidden": true }), _jsx(Trans, { i18nKey: "dashboard:widgetOpenInWorkspace", defaults: "Open in workspace" })] })) : null, refresh ? (_jsxs(DropdownMenuItem, { onClick: refresh, disabled: data === null || data === void 0 ? void 0 : data.refreshing, children: [_jsx(Icon, { name: "fa-light fa-arrows-rotate", "aria-hidden": true }), _jsx(Trans, { i18nKey: "dashboard:widgetRefresh", defaults: "Refresh" })] })) : null, hasNavigation && meta.isPinnable ? _jsx(DropdownMenuSeparator, {}) : null, meta.isPinnable ? (_jsxs(DropdownMenuItem, { onClick: actions.togglePinned, disabled: state.pendingPin, "data-test": `widget-menu-pin-${meta.widgetId}`, children: [_jsx(Icon, { name: state.pinned
92
+ ? 'fa-light fa-thumbtack-slash'
93
+ : 'fa-light fa-thumbtack', "aria-hidden": true }), state.pinned ? (_jsx(Trans, { i18nKey: "dashboard:widgetUnpin", defaults: "Unpin from board" })) : (_jsx(Trans, { i18nKey: "dashboard:widgetPin", defaults: "Pin to board" }))] })) : null] })] }));
94
+ }
95
+ /**
96
+ * One widget-specific entry in the header's ⋯ menu; pass it through
97
+ * `Widget.Header`'s `menu` prop. With `href` it is a link, opened in a new tab
98
+ * when `external`; otherwise it runs `onSelect`.
99
+ */
100
+ function WidgetMenuItem({ icon, href, external = false, onSelect, disabled, children, }) {
101
+ const link = !href ? undefined : external ? (_jsx("a", { href: href, target: "_blank", rel: "noopener noreferrer" })) : (_jsx(Link, { href: href }));
102
+ return (_jsxs(DropdownMenuItem, { render: link, onClick: onSelect, disabled: disabled, children: [_jsx(Icon, { name: icon !== null && icon !== void 0 ? icon : (external
103
+ ? 'fa-light fa-arrow-up-right-from-square'
104
+ : 'fa-light fa-arrow-right'), "aria-hidden": true }), children] }));
105
+ }
106
+ /**
107
+ * "Updated 3 minutes ago" under an active widget whose data layer reports
108
+ * when it last loaded (see `Widget.DataState`'s `updatedAt`).
109
+ */
110
+ function WidgetFreshness() {
111
+ const { state, data } = useWidget();
112
+ const updated = useRelativeTime(data === null || data === void 0 ? void 0 : data.updatedAt);
113
+ if (state.state !== 'active' || state.collapsed || !updated)
114
+ return null;
115
+ return (_jsx("div", { className: "text-muted-foreground border-border/60 flex shrink-0 items-center gap-1.5 border-t px-5 py-2.5 text-xs", "aria-live": "polite", children: (data === null || data === void 0 ? void 0 : data.refreshing) ? (_jsxs(_Fragment, { children: [_jsx(Icon, { name: "fa-light fa-arrows-rotate", className: "h-3 w-3 animate-spin", "aria-hidden": true }), _jsx(Trans, { i18nKey: "dashboard:widgetRefreshing", defaults: "Refreshing\u2026" })] })) : (_jsx(Trans, { i18nKey: "dashboard:widgetUpdated", defaults: "Updated {{time}}", values: { time: updated } })) }));
68
116
  }
69
117
  // ─── State-gated content ────────────────────────────────────────────────────
70
118
  function CollapsibleBody({ collapsed, maxContentHeight, widgetId, children, }) {
71
- return (_jsx("div", { className: cn('grid min-h-0 flex-1 transition-[grid-template-rows] duration-300 ease-[cubic-bezier(0.25,1,0.5,1)] motion-reduce:transition-none', collapsed ? 'grid-rows-[0fr]' : 'grid-rows-[1fr]'), children: _jsx("div", { className: "min-h-0 overflow-hidden", children: _jsx(CardContent, { id: `widget-content-${widgetId}`, className: cn('flex h-full min-h-0 flex-1 flex-col overflow-y-auto p-0', 'transition-opacity duration-200 ease-[cubic-bezier(0.25,1,0.5,1)] motion-reduce:transition-none', collapsed ? 'opacity-0' : 'opacity-100'), style: maxContentHeight ? { maxHeight: maxContentHeight } : undefined, children: children }) }) }));
119
+ return (_jsx("div", { className: cn('ease-out-strong grid min-h-0 flex-1 transition-all duration-300 motion-reduce:transition-none', collapsed ? 'grid-rows-[0fr]' : 'grid-rows-[1fr]'), children: _jsx("div", { className: "min-h-0 overflow-hidden", children: _jsx(CardContent, { id: `widget-content-${widgetId}`, className: cn('flex h-full min-h-0 flex-1 flex-col overflow-y-auto p-0', 'ease-out-strong transition-opacity duration-200 motion-reduce:transition-none', collapsed ? 'opacity-0' : 'opacity-100', maxContentHeight && 'max-h-(--widget-max-height)'), style: maxContentHeight
120
+ ? { '--widget-max-height': maxContentHeight }
121
+ : undefined, children: children }) }) }));
72
122
  }
73
123
  function StaticBody({ maxContentHeight, children, }) {
74
124
  const { innerRef, height } = useAnimatedHeight();
75
- return (_jsx(CardContent, { className: "flex min-h-0 flex-1 flex-col p-0", children: _jsx("div", { className: "overflow-hidden transition-[height] duration-250 ease-out motion-reduce:transition-none", style: {
76
- height: height === 'auto' ? 'auto' : height,
77
- maxHeight: maxContentHeight !== null && maxContentHeight !== void 0 ? maxContentHeight : undefined,
125
+ return (_jsx(CardContent, { className: "flex min-h-0 flex-1 flex-col p-0", children: _jsx("div", { className: cn('h-(--body-height) overflow-hidden transition-all duration-250 ease-out motion-reduce:transition-none', maxContentHeight && 'max-h-(--widget-max-height)'), style: {
126
+ '--body-height': height === 'auto' ? 'auto' : `${height}px`,
127
+ '--widget-max-height': maxContentHeight,
78
128
  }, children: _jsx("div", { ref: innerRef, children: children }) }) }));
79
129
  }
80
130
  /**
@@ -111,10 +161,10 @@ function WidgetBody({ className, children, }) {
111
161
  * `Widget.Body` rather than hand-rolling a `px-*` wrapper.
112
162
  */
113
163
  function WidgetActive({ children }) {
114
- const { state, meta } = useWidget();
164
+ const { state, meta, data } = useWidget();
115
165
  if (state.state !== 'active')
116
166
  return null;
117
- const body = (_jsx(WidgetErrorBoundary, { widgetId: meta.widgetId, children: children }));
167
+ const body = (_jsx(WidgetErrorBoundary, { widgetId: meta.widgetId, onRetry: data === null || data === void 0 ? void 0 : data.refresh, children: children }));
118
168
  return meta.isCollapsible ? (_jsx(CollapsibleBody, { collapsed: state.collapsed, maxContentHeight: meta.maxContentHeight, widgetId: meta.widgetId, children: body })) : (_jsx(StaticBody, { maxContentHeight: meta.maxContentHeight, children: body }));
119
169
  }
120
170
  function WidgetLoading({ children }) {
@@ -124,10 +174,10 @@ function WidgetLoading({ children }) {
124
174
  return (_jsx(CardContent, { className: "flex min-h-0 flex-1 flex-col p-0", children: children !== null && children !== void 0 ? children : _jsx(WidgetLoadingState, {}) }));
125
175
  }
126
176
  function WidgetError({ children }) {
127
- const { state } = useWidget();
177
+ const { state, data } = useWidget();
128
178
  if (state.state !== 'error')
129
179
  return null;
130
- return (_jsx(CardContent, { className: "flex min-h-0 flex-1 flex-col p-0", children: children !== null && children !== void 0 ? children : _jsx(WidgetErrorState, {}) }));
180
+ return (_jsx(CardContent, { className: "flex min-h-0 flex-1 flex-col p-0", children: children !== null && children !== void 0 ? children : (_jsx(WidgetErrorState, { onRetry: data === null || data === void 0 ? void 0 : data.refresh, retrying: data === null || data === void 0 ? void 0 : data.refreshing })) }));
131
181
  }
132
182
  function WidgetInactive({ children }) {
133
183
  const { state } = useWidget();
@@ -146,7 +196,7 @@ function WidgetFooter({ children }) {
146
196
  const { state, meta } = useWidget();
147
197
  if (state.state !== 'active')
148
198
  return null;
149
- return (_jsx(CardFooter, { className: cn('flex-shrink-0 pb-6 transition-opacity duration-200 ease-[cubic-bezier(0.25,1,0.5,1)] motion-reduce:transition-none', meta.isCollapsible && state.collapsed
199
+ return (_jsx(CardFooter, { className: cn('ease-out-strong flex-shrink-0 pb-6 transition-opacity duration-200 motion-reduce:transition-none', meta.isCollapsible && state.collapsed
150
200
  ? 'pointer-events-none opacity-0'
151
201
  : 'opacity-100'), "aria-hidden": meta.isCollapsible && state.collapsed, children: children }));
152
202
  }
@@ -175,14 +225,22 @@ function WidgetAskFooter({ context }) {
175
225
  * instead, so this only fires for the degraded middle ground.
176
226
  */
177
227
  function WidgetHealthFooter() {
178
- var _a;
228
+ var _a, _b;
179
229
  const { state, meta } = useWidget();
230
+ const lastSuccessAt = toEpochMs((_a = meta.health) === null || _a === void 0 ? void 0 : _a.lastSuccessAt);
231
+ const lastSynced = useRelativeTime(lastSuccessAt);
180
232
  const shouldShow = state.state === 'active' &&
181
233
  !state.collapsed &&
182
- ((_a = meta.health) === null || _a === void 0 ? void 0 : _a.status) === 'degraded';
234
+ ((_b = meta.health) === null || _b === void 0 ? void 0 : _b.status) === 'degraded';
183
235
  if (!shouldShow)
184
236
  return null;
185
- return (_jsxs("div", { className: "text-warning border-warning/30 bg-warning/5 flex items-center gap-2 border-t px-4 py-2 text-xs", children: [_jsx(Icon, { name: "fa-light fa-circle-exclamation", className: "h-4 w-4 shrink-0", "aria-hidden": true }), _jsx("span", { children: _jsx(Trans, { i18nKey: "common:integrationHealth.degradedWidgetNotice", defaults: "This integration is degraded \u2014 data may be stale." }) })] }));
237
+ return (_jsxs("div", { className: "text-warning border-warning/30 bg-warning/5 flex items-center gap-2 border-t px-4 py-2 text-xs", children: [_jsx(Icon, { name: "fa-light fa-circle-exclamation", className: "h-4 w-4 shrink-0", "aria-hidden": true }), _jsx("span", { children: lastSynced ? (_jsx(Trans, { i18nKey: "common:integrationHealth.degradedWidgetNoticeSince", defaults: "Data may be out of date. Last synced {{time}}.", values: { time: lastSynced } })) : (_jsx(Trans, { i18nKey: "common:integrationHealth.degradedWidgetNotice", defaults: "This integration is degraded \u2014 data may be stale." })) })] }));
238
+ }
239
+ function toEpochMs(value) {
240
+ if (value === null || value === undefined)
241
+ return undefined;
242
+ const ms = value instanceof Date ? value.getTime() : new Date(value).getTime();
243
+ return Number.isFinite(ms) && ms > 0 ? ms : undefined;
186
244
  }
187
245
  // ─── Data state override ────────────────────────────────────────────────────
188
246
  /**
@@ -204,8 +262,22 @@ function WidgetHealthFooter() {
204
262
  * </Widget.DataState>
205
263
  * );
206
264
  * ```
265
+ *
266
+ * Pass `onRefresh`, `refreshing` and `updatedAt` to give the widget the
267
+ * shared Refresh menu item, a Try again button on its error view, and an
268
+ * "Updated 3 minutes ago" footer. With TanStack Query:
269
+ *
270
+ * ```tsx
271
+ * <Widget.DataState
272
+ * loading={query.isLoading}
273
+ * error={query.isError}
274
+ * onRefresh={() => void query.refetch()}
275
+ * refreshing={query.isFetching}
276
+ * updatedAt={query.dataUpdatedAt}
277
+ * >
278
+ * ```
207
279
  */
208
- function WidgetDataState({ loading, error, inactive, children, }) {
280
+ function WidgetDataState({ loading, error, inactive, onRefresh, refreshing = false, updatedAt, children, }) {
209
281
  const parent = useWidget();
210
282
  const overrideState = parent.state.state !== 'active'
211
283
  ? parent.state.state
@@ -216,17 +288,33 @@ function WidgetDataState({ loading, error, inactive, children, }) {
216
288
  : error
217
289
  ? 'error'
218
290
  : 'active';
291
+ const updatedAtMs = toEpochMs(updatedAt);
292
+ const reportsData = onRefresh !== undefined || updatedAtMs !== undefined;
219
293
  const value = useMemo(() => ({
220
294
  state: Object.assign(Object.assign({}, parent.state), { state: overrideState }),
221
295
  actions: parent.actions,
222
296
  meta: parent.meta,
223
- }), [parent.state, parent.actions, parent.meta, overrideState]);
297
+ data: reportsData
298
+ ? { updatedAt: updatedAtMs, refreshing, refresh: onRefresh }
299
+ : parent.data,
300
+ }), [
301
+ parent.state,
302
+ parent.actions,
303
+ parent.meta,
304
+ parent.data,
305
+ overrideState,
306
+ reportsData,
307
+ updatedAtMs,
308
+ refreshing,
309
+ onRefresh,
310
+ ]);
224
311
  return _jsx(WidgetContext, { value: value, children: children });
225
312
  }
226
313
  // ─── Compound export ────────────────────────────────────────────────────────
227
314
  export const Widget = {
228
315
  Card: WidgetCard,
229
316
  Header: WidgetHeader,
317
+ MenuItem: WidgetMenuItem,
230
318
  DataState: WidgetDataState,
231
319
  Active: WidgetActive,
232
320
  Body: WidgetBody,