@backstage/plugin-app 0.4.1-next.2 → 0.4.2

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 (54) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/dist/apis/PluginHeaderActionsApi/DefaultPluginHeaderActionsApi.esm.js +8 -1
  3. package/dist/apis/PluginHeaderActionsApi/DefaultPluginHeaderActionsApi.esm.js.map +1 -1
  4. package/dist/apis/PluginWrapperApi/DefaultPluginWrapperApi.esm.js +134 -15
  5. package/dist/apis/PluginWrapperApi/DefaultPluginWrapperApi.esm.js.map +1 -1
  6. package/dist/apis/ToastApiForwarder.esm.js +94 -0
  7. package/dist/apis/ToastApiForwarder.esm.js.map +1 -0
  8. package/dist/apis/toastApiForwarderRef.esm.js +8 -0
  9. package/dist/apis/toastApiForwarderRef.esm.js.map +1 -0
  10. package/dist/components/Toast/Toast.esm.js +156 -0
  11. package/dist/components/Toast/Toast.esm.js.map +1 -0
  12. package/dist/components/Toast/Toast.module.css.esm.js +8 -0
  13. package/dist/components/Toast/Toast.module.css.esm.js.map +1 -0
  14. package/dist/components/Toast/ToastContainer.esm.js +90 -0
  15. package/dist/components/Toast/ToastContainer.esm.js.map +1 -0
  16. package/dist/components/Toast/ToastDisplay.esm.js +51 -0
  17. package/dist/components/Toast/ToastDisplay.esm.js.map +1 -0
  18. package/dist/defaultApis.esm.js +19 -1
  19. package/dist/defaultApis.esm.js.map +1 -1
  20. package/dist/extensions/AppNav.esm.js +35 -6
  21. package/dist/extensions/AppNav.esm.js.map +1 -1
  22. package/dist/extensions/AppRoot.esm.js +40 -34
  23. package/dist/extensions/AppRoot.esm.js.map +1 -1
  24. package/dist/extensions/IconsApi.esm.js.map +1 -1
  25. package/dist/extensions/PluginWrapperApi.esm.js +1 -2
  26. package/dist/extensions/PluginWrapperApi.esm.js.map +1 -1
  27. package/dist/extensions/components.esm.js +30 -18
  28. package/dist/extensions/components.esm.js.map +1 -1
  29. package/dist/extensions/elements.esm.js +4 -2
  30. package/dist/extensions/elements.esm.js.map +1 -1
  31. package/dist/hooks/useInvertedThemeMode.esm.js +41 -0
  32. package/dist/hooks/useInvertedThemeMode.esm.js.map +1 -0
  33. package/dist/index.d.ts +20 -6
  34. package/dist/node_modules_dist/style-inject/dist/style-inject.es.esm.js +29 -0
  35. package/dist/node_modules_dist/style-inject/dist/style-inject.es.esm.js.map +1 -0
  36. package/dist/packages/core-app-api/src/apis/implementations/AlertApi/AlertApiForwarder.esm.js +7 -0
  37. package/dist/packages/core-app-api/src/apis/implementations/AlertApi/AlertApiForwarder.esm.js.map +1 -1
  38. package/dist/packages/core-app-api/src/apis/implementations/auth/saml/types.esm.js +1 -1
  39. package/dist/packages/core-app-api/src/apis/implementations/auth/saml/types.esm.js.map +1 -1
  40. package/dist/packages/core-app-api/src/app/AppRouter.esm.js +11 -10
  41. package/dist/packages/core-app-api/src/app/AppRouter.esm.js.map +1 -1
  42. package/dist/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.esm.js +39 -3
  43. package/dist/packages/frontend-app-api/src/apis/implementations/IconsApi/DefaultIconsApi.esm.js.map +1 -1
  44. package/dist/packages/frontend-internal/src/wiring/InternalExtensionDefinition.esm.js.map +1 -1
  45. package/dist/packages/frontend-internal/src/wiring/InternalFrontendPlugin.esm.js.map +1 -1
  46. package/dist/packages/ui/src/hooks/useBg.esm.js +19 -0
  47. package/dist/packages/ui/src/hooks/useBg.esm.js.map +1 -0
  48. package/dist/packages/ui/src/hooks/useBreakpoint.esm.js +17 -0
  49. package/dist/packages/ui/src/hooks/useBreakpoint.esm.js.map +1 -0
  50. package/dist/packages/ui/src/hooks/useDefinition/helpers.esm.js +4 -0
  51. package/dist/packages/ui/src/hooks/useDefinition/helpers.esm.js.map +1 -0
  52. package/dist/plugins/app/package.json.esm.js +9 -2
  53. package/dist/plugins/app/package.json.esm.js.map +1 -1
  54. package/package.json +24 -17
package/CHANGELOG.md CHANGED
@@ -1,5 +1,71 @@
1
1
  # @backstage/plugin-app
2
2
 
3
+ ## 0.4.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 5783b63: Updated the `PageLayout` swap to pass a clickable `titleLink` on the `PluginHeader`, resolved from the plugin's root route ref.
8
+ - Updated dependencies
9
+ - @backstage/ui@0.13.1
10
+ - @backstage/frontend-plugin-api@0.15.1
11
+
12
+ ## 0.4.1
13
+
14
+ ### Patch Changes
15
+
16
+ - 5f3f5d2: `NavContentBlueprint` nav item collections now keep previously collected `rest()` results in sync when additional items are taken later in the same render, making it easier to place items across multiple sidebar sections.
17
+ - aa29b50: Pages created with `PageBlueprint` now render the plugin header by default in the new frontend system.
18
+ - c0ab376: The app nav now falls back to `plugin.icon` for navigation items that don't have an explicit icon set.
19
+ - 12d8afe: Added `BUIProvider` from `@backstage/ui` to the app root, enabling BUI components to fire analytics events through the Backstage analytics system.
20
+ - 5fec07d: Updated the default app root to better support phased app preparation by allowing the app layout to be absent during bootstrap, routing bootstrap failures through the app root boundary, and avoiding installation of a guest identity in protected apps that do not provide a sign-in page.
21
+ - 9508514: Updated the default `PluginWrapperApi` implementation to support the new `useWrapperValue` hook and root wrapper. The root wrapper is now rendered in the app root to manage shared hook state across plugin wrapper instances.
22
+ - a49a40d: Updated dependency `zod` to `^3.25.76 || ^4.0.0` & migrated to `/v3` or `/v4` imports.
23
+ - 42f8c9b: Moved `BUIProvider` inside the app router to enable automatic client-side routing for all BUI components.
24
+ - 909c742: Switched translation API imports (`translationApiRef`, `appLanguageApiRef`) from the alpha `@backstage/core-plugin-api/alpha` path to the stable `@backstage/frontend-plugin-api` export. This has no effect on runtime behavior.
25
+ - 7e743f4: Introduced a new `ToastApi` for displaying rich toast notifications in the new frontend system.
26
+
27
+ The new `ToastApi` provides enhanced notification capabilities compared to the existing `AlertApi`:
28
+
29
+ - **Title and Description**: Toasts support both a title and an optional description
30
+ - **Custom Timeouts**: Each toast can specify its own timeout duration
31
+ - **Links**: Toasts can include action links
32
+ - **Status Variants**: Support for neutral, info, success, warning, and danger statuses
33
+ - **Programmatic Dismiss**: Toasts can be dismissed programmatically using the `close()` handle returned from `post()`
34
+
35
+ **Usage:**
36
+
37
+ ```typescript
38
+ import { toastApiRef, useApi } from '@backstage/frontend-plugin-api';
39
+
40
+ const toastApi = useApi(toastApiRef);
41
+
42
+ // Full-featured toast
43
+ toastApi.post({
44
+ title: 'Entity saved',
45
+ description: 'Your changes have been saved successfully.',
46
+ status: 'success',
47
+ timeout: 5000,
48
+ links: [{ label: 'View entity', href: '/catalog/entity' }],
49
+ });
50
+
51
+ // Programmatic dismiss
52
+ const { close } = toastApi.post({ title: 'Uploading...', status: 'info' });
53
+ // Later...
54
+ close();
55
+ ```
56
+
57
+ The `ToastDisplay` component subscribes to both `ToastApi` and `AlertApi`, providing a migration path where both systems work side by side until `AlertApi` is fully deprecated.
58
+
59
+ - Updated dependencies
60
+ - @backstage/ui@0.13.0
61
+ - @backstage/core-plugin-api@1.12.4
62
+ - @backstage/core-components@0.18.8
63
+ - @backstage/frontend-plugin-api@0.15.0
64
+ - @backstage/plugin-app-react@0.2.1
65
+ - @backstage/plugin-permission-react@0.4.41
66
+ - @backstage/filter-predicates@0.1.1
67
+ - @backstage/integration-react@1.2.16
68
+
3
69
  ## 0.4.1-next.2
4
70
 
5
71
  ### Patch Changes
@@ -1,3 +1,5 @@
1
+ import { cloneElement } from 'react';
2
+
1
3
  const EMPTY_ACTIONS = new Array();
2
4
  class DefaultPluginHeaderActionsApi {
3
5
  constructor(actionsByPlugin) {
@@ -14,7 +16,12 @@ class DefaultPluginHeaderActionsApi {
14
16
  pluginActions = [];
15
17
  actionsByPlugin.set(action.pluginId, pluginActions);
16
18
  }
17
- pluginActions.push(action.element);
19
+ const index = pluginActions.length;
20
+ pluginActions.push(
21
+ cloneElement(action.element, {
22
+ key: action.element.key ?? `plugin-header-action-${action.pluginId}-${index}`
23
+ })
24
+ );
18
25
  }
19
26
  return new DefaultPluginHeaderActionsApi(actionsByPlugin);
20
27
  }
@@ -1 +1 @@
1
- {"version":3,"file":"DefaultPluginHeaderActionsApi.esm.js","sources":["../../../src/apis/PluginHeaderActionsApi/DefaultPluginHeaderActionsApi.tsx"],"sourcesContent":["/*\n * Copyright 2026 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { JSX } from 'react';\nimport { type PluginHeaderActionsApi } from '@backstage/frontend-plugin-api';\n\n// Stable reference\nconst EMPTY_ACTIONS = new Array<JSX.Element | null>();\n\ntype ActionInput = {\n element: JSX.Element;\n pluginId: string;\n};\n\n/**\n * Default implementation of PluginHeaderActionsApi.\n *\n * @internal\n */\nexport class DefaultPluginHeaderActionsApi implements PluginHeaderActionsApi {\n constructor(\n private readonly actionsByPlugin: Map<string, Array<JSX.Element | null>>,\n ) {}\n\n getPluginHeaderActions(pluginId: string): Array<JSX.Element | null> {\n return this.actionsByPlugin.get(pluginId) ?? EMPTY_ACTIONS;\n }\n\n static fromActions(\n actions: Array<ActionInput>,\n ): DefaultPluginHeaderActionsApi {\n const actionsByPlugin = new Map<string, Array<JSX.Element | null>>();\n\n for (const action of actions) {\n let pluginActions = actionsByPlugin.get(action.pluginId);\n if (!pluginActions) {\n pluginActions = [];\n actionsByPlugin.set(action.pluginId, pluginActions);\n }\n\n pluginActions.push(action.element);\n }\n\n return new DefaultPluginHeaderActionsApi(actionsByPlugin);\n }\n}\n"],"names":[],"mappings":"AAoBA,MAAM,aAAA,GAAgB,IAAI,KAAA,EAA0B;AAY7C,MAAM,6BAAA,CAAgE;AAAA,EAC3E,YACmB,eAAA,EACjB;AADiB,IAAA,IAAA,CAAA,eAAA,GAAA,eAAA;AAAA,EAChB;AAAA,EAEH,uBAAuB,QAAA,EAA6C;AAClE,IAAA,OAAO,IAAA,CAAK,eAAA,CAAgB,GAAA,CAAI,QAAQ,CAAA,IAAK,aAAA;AAAA,EAC/C;AAAA,EAEA,OAAO,YACL,OAAA,EAC+B;AAC/B,IAAA,MAAM,eAAA,uBAAsB,GAAA,EAAuC;AAEnE,IAAA,KAAA,MAAW,UAAU,OAAA,EAAS;AAC5B,MAAA,IAAI,aAAA,GAAgB,eAAA,CAAgB,GAAA,CAAI,MAAA,CAAO,QAAQ,CAAA;AACvD,MAAA,IAAI,CAAC,aAAA,EAAe;AAClB,QAAA,aAAA,GAAgB,EAAC;AACjB,QAAA,eAAA,CAAgB,GAAA,CAAI,MAAA,CAAO,QAAA,EAAU,aAAa,CAAA;AAAA,MACpD;AAEA,MAAA,aAAA,CAAc,IAAA,CAAK,OAAO,OAAO,CAAA;AAAA,IACnC;AAEA,IAAA,OAAO,IAAI,8BAA8B,eAAe,CAAA;AAAA,EAC1D;AACF;;;;"}
1
+ {"version":3,"file":"DefaultPluginHeaderActionsApi.esm.js","sources":["../../../src/apis/PluginHeaderActionsApi/DefaultPluginHeaderActionsApi.tsx"],"sourcesContent":["/*\n * Copyright 2026 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { cloneElement, JSX } from 'react';\nimport { type PluginHeaderActionsApi } from '@backstage/frontend-plugin-api';\n\n// Stable reference\nconst EMPTY_ACTIONS = new Array<JSX.Element | null>();\n\ntype ActionInput = {\n element: JSX.Element;\n pluginId: string;\n};\n\n/**\n * Default implementation of PluginHeaderActionsApi.\n *\n * @internal\n */\nexport class DefaultPluginHeaderActionsApi implements PluginHeaderActionsApi {\n constructor(\n private readonly actionsByPlugin: Map<string, Array<JSX.Element | null>>,\n ) {}\n\n getPluginHeaderActions(pluginId: string): Array<JSX.Element | null> {\n return this.actionsByPlugin.get(pluginId) ?? EMPTY_ACTIONS;\n }\n\n static fromActions(\n actions: Array<ActionInput>,\n ): DefaultPluginHeaderActionsApi {\n const actionsByPlugin = new Map<string, Array<JSX.Element | null>>();\n\n for (const action of actions) {\n let pluginActions = actionsByPlugin.get(action.pluginId);\n if (!pluginActions) {\n pluginActions = [];\n actionsByPlugin.set(action.pluginId, pluginActions);\n }\n\n const index = pluginActions.length;\n pluginActions.push(\n cloneElement(action.element, {\n key:\n action.element.key ??\n `plugin-header-action-${action.pluginId}-${index}`,\n }),\n );\n }\n\n return new DefaultPluginHeaderActionsApi(actionsByPlugin);\n }\n}\n"],"names":[],"mappings":";;AAoBA,MAAM,aAAA,GAAgB,IAAI,KAAA,EAA0B;AAY7C,MAAM,6BAAA,CAAgE;AAAA,EAC3E,YACmB,eAAA,EACjB;AADiB,IAAA,IAAA,CAAA,eAAA,GAAA,eAAA;AAAA,EAChB;AAAA,EAEH,uBAAuB,QAAA,EAA6C;AAClE,IAAA,OAAO,IAAA,CAAK,eAAA,CAAgB,GAAA,CAAI,QAAQ,CAAA,IAAK,aAAA;AAAA,EAC/C;AAAA,EAEA,OAAO,YACL,OAAA,EAC+B;AAC/B,IAAA,MAAM,eAAA,uBAAsB,GAAA,EAAuC;AAEnE,IAAA,KAAA,MAAW,UAAU,OAAA,EAAS;AAC5B,MAAA,IAAI,aAAA,GAAgB,eAAA,CAAgB,GAAA,CAAI,MAAA,CAAO,QAAQ,CAAA;AACvD,MAAA,IAAI,CAAC,aAAA,EAAe;AAClB,QAAA,aAAA,GAAgB,EAAC;AACjB,QAAA,eAAA,CAAgB,GAAA,CAAI,MAAA,CAAO,QAAA,EAAU,aAAa,CAAA;AAAA,MACpD;AAEA,MAAA,MAAM,QAAQ,aAAA,CAAc,MAAA;AAC5B,MAAA,aAAA,CAAc,IAAA;AAAA,QACZ,YAAA,CAAa,OAAO,OAAA,EAAS;AAAA,UAC3B,GAAA,EACE,OAAO,OAAA,CAAQ,GAAA,IACf,wBAAwB,MAAA,CAAO,QAAQ,IAAI,KAAK,CAAA;AAAA,SACnD;AAAA,OACH;AAAA,IACF;AAEA,IAAA,OAAO,IAAI,8BAA8B,eAAe,CAAA;AAAA,EAC1D;AACF;;;;"}
@@ -1,10 +1,17 @@
1
- import { jsx } from 'react/jsx-runtime';
2
- import { useState, useEffect, useMemo } from 'react';
1
+ import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
2
+ import { createContext, useState, useEffect, useSyncExternalStore, useContext, useMemo } from 'react';
3
3
 
4
+ const HookRegistryContext = createContext(
5
+ void 0
6
+ );
4
7
  class DefaultPluginWrapperApi {
5
- constructor(pluginWrappers) {
8
+ constructor(rootWrapper, pluginWrappers) {
9
+ this.rootWrapper = rootWrapper;
6
10
  this.pluginWrappers = pluginWrappers;
7
11
  }
12
+ getRootWrapper() {
13
+ return this.rootWrapper;
14
+ }
8
15
  getPluginWrapper(pluginId) {
9
16
  return this.pluginWrappers.get(pluginId);
10
17
  }
@@ -23,31 +30,143 @@ class DefaultPluginWrapperApi {
23
30
  if (loaders.length === 0) {
24
31
  continue;
25
32
  }
33
+ const WrapperWithState = ({
34
+ loader,
35
+ component: WrapperComponent,
36
+ useWrapperValue,
37
+ children
38
+ }) => {
39
+ const hookContext = useContext(HookRegistryContext);
40
+ if (!hookContext) {
41
+ throw new Error(
42
+ "Attempted to render a wrapped plugin component without a root wrapper context"
43
+ );
44
+ }
45
+ const store = useMemo(() => {
46
+ return hookContext.registerHook(loader, useWrapperValue);
47
+ }, [hookContext, loader, useWrapperValue]);
48
+ const container = useSyncExternalStore(
49
+ store.subscribe,
50
+ store.getSnapshot
51
+ );
52
+ if (!container) {
53
+ return null;
54
+ }
55
+ return /* @__PURE__ */ jsx(WrapperComponent, { value: container.value, children });
56
+ };
26
57
  const ComposedWrapper = (props) => {
27
58
  const [loadedWrappers, setLoadedWrappers] = useState(void 0);
28
59
  const [error, setError] = useState(void 0);
29
60
  useEffect(() => {
30
61
  Promise.all(loaders.map((loader) => loader())).then((results) => {
31
- setLoadedWrappers(results.map((r) => r.component));
62
+ const normalizedResults = results.map(
63
+ ({ component, useWrapperValue }, index) => {
64
+ const loader = loaders[index];
65
+ if (!useWrapperValue) {
66
+ return component;
67
+ }
68
+ return ({ children }) => /* @__PURE__ */ jsx(
69
+ WrapperWithState,
70
+ {
71
+ loader,
72
+ component,
73
+ useWrapperValue,
74
+ children
75
+ }
76
+ );
77
+ }
78
+ );
79
+ setLoadedWrappers(normalizedResults);
32
80
  }).catch(setError);
33
81
  }, []);
34
82
  if (error) {
35
83
  throw error;
36
84
  }
37
- return useMemo(() => {
38
- if (!loadedWrappers) {
39
- return null;
40
- }
41
- let current = props.children;
42
- for (const Wrapper of loadedWrappers) {
43
- current = /* @__PURE__ */ jsx(Wrapper, { children: current });
44
- }
45
- return current;
46
- }, [loadedWrappers, props.children]);
85
+ if (!loadedWrappers) {
86
+ return null;
87
+ }
88
+ let content = props.children;
89
+ for (const Wrapper of loadedWrappers) {
90
+ content = /* @__PURE__ */ jsx(Wrapper, { children: content });
91
+ }
92
+ return /* @__PURE__ */ jsx(Fragment, { children: content });
47
93
  };
48
94
  composedWrappers.set(pluginId, ComposedWrapper);
49
95
  }
50
- return new DefaultPluginWrapperApi(composedWrappers);
96
+ return new DefaultPluginWrapperApi(
97
+ DefaultPluginWrapperApi.createRootWrapper(),
98
+ composedWrappers
99
+ );
100
+ }
101
+ /**
102
+ * Creates the root wrapper component that is responsible for rendering and
103
+ * forwarding the values of the common `useWrapperValue` hooks.
104
+ */
105
+ static createRootWrapper() {
106
+ const renderers = /* @__PURE__ */ new Map();
107
+ const renderUpdateListeners = /* @__PURE__ */ new Set();
108
+ let renderElements = new Array();
109
+ const createHookRenderer = (hook) => {
110
+ const listeners = /* @__PURE__ */ new Set();
111
+ let container = void 0;
112
+ const HookRenderer = () => {
113
+ container = { value: hook() };
114
+ useEffect(() => {
115
+ for (const listener of listeners) {
116
+ listener();
117
+ }
118
+ });
119
+ return null;
120
+ };
121
+ renderElements = [
122
+ ...renderElements,
123
+ /* @__PURE__ */ jsx(HookRenderer, {}, `hook-renderer-${renderElements.length + 1}`)
124
+ ];
125
+ return {
126
+ getSnapshot: () => container,
127
+ subscribe(listener) {
128
+ listeners.add(listener);
129
+ return () => listeners.delete(listener);
130
+ }
131
+ };
132
+ };
133
+ const registerHook = (key, hook) => {
134
+ let renderer = renderers.get(key);
135
+ if (!renderer) {
136
+ renderer = createHookRenderer(hook);
137
+ renderers.set(key, renderer);
138
+ queueMicrotask(() => {
139
+ for (const listener of renderUpdateListeners) {
140
+ listener();
141
+ }
142
+ });
143
+ }
144
+ return renderer;
145
+ };
146
+ const subscribeToRenderUpdates = (listener) => {
147
+ renderUpdateListeners.add(listener);
148
+ return () => renderUpdateListeners.delete(listener);
149
+ };
150
+ const getRenderElements = () => renderElements;
151
+ const RootWrapper = (props) => {
152
+ const elements = useSyncExternalStore(
153
+ subscribeToRenderUpdates,
154
+ getRenderElements
155
+ );
156
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
157
+ /* @__PURE__ */ jsx(Fragment, { children: elements }),
158
+ /* @__PURE__ */ jsx(
159
+ HookRegistryContext.Provider,
160
+ {
161
+ value: {
162
+ registerHook
163
+ },
164
+ children: props.children
165
+ }
166
+ )
167
+ ] });
168
+ };
169
+ return RootWrapper;
51
170
  }
52
171
  }
53
172
 
@@ -1 +1 @@
1
- {"version":3,"file":"DefaultPluginWrapperApi.esm.js","sources":["../../../src/apis/PluginWrapperApi/DefaultPluginWrapperApi.tsx"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { PluginWrapperApi } from '@backstage/frontend-plugin-api/alpha';\nimport { ComponentType, ReactNode, useEffect, useMemo, useState } from 'react';\n\ntype WrapperInput = {\n loader: () => Promise<{ component: ComponentType<{ children: ReactNode }> }>;\n pluginId: string;\n};\n\n/**\n * Default implementation of PluginWrapperApi.\n *\n * @internal\n */\nexport class DefaultPluginWrapperApi implements PluginWrapperApi {\n constructor(\n private readonly pluginWrappers: Map<\n string,\n ComponentType<{ children: ReactNode }>\n >,\n ) {}\n\n getPluginWrapper(\n pluginId: string,\n ): ComponentType<{ children: ReactNode }> | undefined {\n return this.pluginWrappers.get(pluginId);\n }\n\n static fromWrappers(wrappers: Array<WrapperInput>): DefaultPluginWrapperApi {\n const loadersByPlugin = new Map<\n string,\n Array<\n () => Promise<{ component: ComponentType<{ children: ReactNode }> }>\n >\n >();\n\n for (const wrapper of wrappers) {\n let loaders = loadersByPlugin.get(wrapper.pluginId);\n if (!loaders) {\n loaders = [];\n loadersByPlugin.set(wrapper.pluginId, loaders);\n }\n loaders.push(wrapper.loader);\n }\n\n const composedWrappers = new Map<\n string,\n ComponentType<{ children: ReactNode }>\n >();\n\n for (const [pluginId, loaders] of loadersByPlugin) {\n if (loaders.length === 0) {\n continue;\n }\n\n const ComposedWrapper = (props: { children: ReactNode }) => {\n const [loadedWrappers, setLoadedWrappers] = useState<\n Array<ComponentType<{ children: ReactNode }>> | undefined\n >(undefined);\n const [error, setError] = useState<Error | undefined>(undefined);\n\n useEffect(() => {\n Promise.all(loaders.map(loader => loader()))\n .then(results => {\n setLoadedWrappers(results.map(r => r.component));\n })\n .catch(setError);\n }, []);\n\n if (error) {\n throw error;\n }\n\n return useMemo(() => {\n if (!loadedWrappers) {\n return null;\n }\n\n let current = props.children;\n\n for (const Wrapper of loadedWrappers) {\n current = <Wrapper>{current}</Wrapper>;\n }\n\n return current;\n }, [loadedWrappers, props.children]);\n };\n\n composedWrappers.set(pluginId, ComposedWrapper);\n }\n\n return new DefaultPluginWrapperApi(composedWrappers);\n }\n}\n"],"names":[],"mappings":";;;AA6BO,MAAM,uBAAA,CAAoD;AAAA,EAC/D,YACmB,cAAA,EAIjB;AAJiB,IAAA,IAAA,CAAA,cAAA,GAAA,cAAA;AAAA,EAIhB;AAAA,EAEH,iBACE,QAAA,EACoD;AACpD,IAAA,OAAO,IAAA,CAAK,cAAA,CAAe,GAAA,CAAI,QAAQ,CAAA;AAAA,EACzC;AAAA,EAEA,OAAO,aAAa,QAAA,EAAwD;AAC1E,IAAA,MAAM,eAAA,uBAAsB,GAAA,EAK1B;AAEF,IAAA,KAAA,MAAW,WAAW,QAAA,EAAU;AAC9B,MAAA,IAAI,OAAA,GAAU,eAAA,CAAgB,GAAA,CAAI,OAAA,CAAQ,QAAQ,CAAA;AAClD,MAAA,IAAI,CAAC,OAAA,EAAS;AACZ,QAAA,OAAA,GAAU,EAAC;AACX,QAAA,eAAA,CAAgB,GAAA,CAAI,OAAA,CAAQ,QAAA,EAAU,OAAO,CAAA;AAAA,MAC/C;AACA,MAAA,OAAA,CAAQ,IAAA,CAAK,QAAQ,MAAM,CAAA;AAAA,IAC7B;AAEA,IAAA,MAAM,gBAAA,uBAAuB,GAAA,EAG3B;AAEF,IAAA,KAAA,MAAW,CAAC,QAAA,EAAU,OAAO,CAAA,IAAK,eAAA,EAAiB;AACjD,MAAA,IAAI,OAAA,CAAQ,WAAW,CAAA,EAAG;AACxB,QAAA;AAAA,MACF;AAEA,MAAA,MAAM,eAAA,GAAkB,CAAC,KAAA,KAAmC;AAC1D,QAAA,MAAM,CAAC,cAAA,EAAgB,iBAAiB,CAAA,GAAI,SAE1C,MAAS,CAAA;AACX,QAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAI,SAA4B,MAAS,CAAA;AAE/D,QAAA,SAAA,CAAU,MAAM;AACd,UAAA,OAAA,CAAQ,GAAA,CAAI,QAAQ,GAAA,CAAI,CAAA,MAAA,KAAU,QAAQ,CAAC,CAAA,CACxC,IAAA,CAAK,CAAA,OAAA,KAAW;AACf,YAAA,iBAAA,CAAkB,OAAA,CAAQ,GAAA,CAAI,CAAA,CAAA,KAAK,CAAA,CAAE,SAAS,CAAC,CAAA;AAAA,UACjD,CAAC,CAAA,CACA,KAAA,CAAM,QAAQ,CAAA;AAAA,QACnB,CAAA,EAAG,EAAE,CAAA;AAEL,QAAA,IAAI,KAAA,EAAO;AACT,UAAA,MAAM,KAAA;AAAA,QACR;AAEA,QAAA,OAAO,QAAQ,MAAM;AACnB,UAAA,IAAI,CAAC,cAAA,EAAgB;AACnB,YAAA,OAAO,IAAA;AAAA,UACT;AAEA,UAAA,IAAI,UAAU,KAAA,CAAM,QAAA;AAEpB,UAAA,KAAA,MAAW,WAAW,cAAA,EAAgB;AACpC,YAAA,OAAA,mBAAU,GAAA,CAAC,WAAS,QAAA,EAAA,OAAA,EAAQ,CAAA;AAAA,UAC9B;AAEA,UAAA,OAAO,OAAA;AAAA,QACT,CAAA,EAAG,CAAC,cAAA,EAAgB,KAAA,CAAM,QAAQ,CAAC,CAAA;AAAA,MACrC,CAAA;AAEA,MAAA,gBAAA,CAAiB,GAAA,CAAI,UAAU,eAAe,CAAA;AAAA,IAChD;AAEA,IAAA,OAAO,IAAI,wBAAwB,gBAAgB,CAAA;AAAA,EACrD;AACF;;;;"}
1
+ {"version":3,"file":"DefaultPluginWrapperApi.esm.js","sources":["../../../src/apis/PluginWrapperApi/DefaultPluginWrapperApi.tsx"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n PluginWrapperApi,\n PluginWrapperDefinition,\n} from '@backstage/frontend-plugin-api';\nimport {\n ComponentType,\n ReactNode,\n createContext,\n useContext,\n useEffect,\n useMemo,\n useState,\n useSyncExternalStore,\n} from 'react';\n\ninterface HookStore {\n getSnapshot: () => { value: unknown } | undefined;\n subscribe: (listener: () => void) => () => void;\n}\n\ninterface HookRegistryContextValue {\n registerHook: (key: any, hook: () => unknown) => HookStore;\n}\n\nconst HookRegistryContext = createContext<HookRegistryContextValue | undefined>(\n undefined,\n);\n\ntype WrapperInput = {\n loader: () => Promise<PluginWrapperDefinition<any>>;\n pluginId: string;\n};\n\n/**\n * Default implementation of PluginWrapperApi.\n *\n * @internal\n */\nexport class DefaultPluginWrapperApi implements PluginWrapperApi {\n constructor(\n private readonly rootWrapper: ComponentType<{ children: ReactNode }>,\n private readonly pluginWrappers: Map<\n string,\n ComponentType<{ children: ReactNode }>\n >,\n ) {}\n\n getRootWrapper(): ComponentType<{ children: ReactNode }> {\n return this.rootWrapper;\n }\n\n getPluginWrapper(\n pluginId: string,\n ): ComponentType<{ children: ReactNode }> | undefined {\n return this.pluginWrappers.get(pluginId);\n }\n\n static fromWrappers(wrappers: Array<WrapperInput>): DefaultPluginWrapperApi {\n const loadersByPlugin = new Map<\n string,\n Array<() => Promise<PluginWrapperDefinition<any>>>\n >();\n\n for (const wrapper of wrappers) {\n let loaders = loadersByPlugin.get(wrapper.pluginId);\n if (!loaders) {\n loaders = [];\n loadersByPlugin.set(wrapper.pluginId, loaders);\n }\n loaders.push(wrapper.loader);\n }\n\n const composedWrappers = new Map<\n string,\n ComponentType<{ children: ReactNode }>\n >();\n\n for (const [pluginId, loaders] of loadersByPlugin) {\n if (loaders.length === 0) {\n continue;\n }\n\n const WrapperWithState = ({\n loader,\n component: WrapperComponent,\n useWrapperValue,\n children,\n }: {\n loader: () => Promise<PluginWrapperDefinition>;\n component: ComponentType<{\n children: ReactNode;\n value: unknown;\n }>;\n useWrapperValue: () => unknown;\n children: ReactNode;\n }) => {\n const hookContext = useContext(HookRegistryContext);\n if (!hookContext) {\n throw new Error(\n 'Attempted to render a wrapped plugin component without a root wrapper context',\n );\n }\n const store = useMemo(() => {\n return hookContext.registerHook(loader, useWrapperValue);\n }, [hookContext, loader, useWrapperValue]);\n const container = useSyncExternalStore(\n store.subscribe,\n store.getSnapshot,\n );\n\n if (!container) {\n return null;\n }\n\n return (\n <WrapperComponent value={container.value}>\n {children}\n </WrapperComponent>\n );\n };\n\n const ComposedWrapper = (props: { children: ReactNode }) => {\n const [loadedWrappers, setLoadedWrappers] = useState<\n Array<ComponentType<{ children: ReactNode }>> | undefined\n >(undefined);\n const [error, setError] = useState<Error | undefined>(undefined);\n\n useEffect(() => {\n Promise.all(loaders.map(loader => loader()))\n .then(results => {\n const normalizedResults = results.map(\n ({ component, useWrapperValue }, index) => {\n const loader = loaders[index];\n\n if (!useWrapperValue) {\n return component as ComponentType<{ children: ReactNode }>;\n }\n\n return ({ children }: { children: ReactNode }) => (\n <WrapperWithState\n loader={loader}\n component={component}\n useWrapperValue={useWrapperValue}\n >\n {children}\n </WrapperWithState>\n );\n },\n );\n\n setLoadedWrappers(normalizedResults);\n })\n .catch(setError);\n }, []);\n\n if (error) {\n throw error;\n }\n\n if (!loadedWrappers) {\n return null;\n }\n\n let content = props.children;\n\n for (const Wrapper of loadedWrappers) {\n content = <Wrapper>{content}</Wrapper>;\n }\n\n return <>{content}</>;\n };\n\n composedWrappers.set(pluginId, ComposedWrapper);\n }\n\n return new DefaultPluginWrapperApi(\n DefaultPluginWrapperApi.createRootWrapper(),\n composedWrappers,\n );\n }\n\n /**\n * Creates the root wrapper component that is responsible for rendering and\n * forwarding the values of the common `useWrapperValue` hooks.\n */\n static createRootWrapper() {\n const renderers = new Map<any, HookStore>();\n const renderUpdateListeners = new Set<() => void>();\n\n let renderElements = new Array<JSX.Element>();\n\n const createHookRenderer = (hook: () => unknown): HookStore => {\n const listeners = new Set<() => void>();\n let container: { value: unknown } | undefined = undefined;\n\n const HookRenderer = () => {\n container = { value: hook() };\n useEffect(() => {\n for (const listener of listeners) {\n listener();\n }\n });\n return null;\n };\n\n renderElements = [\n ...renderElements,\n <HookRenderer key={`hook-renderer-${renderElements.length + 1}`} />,\n ];\n\n return {\n getSnapshot: () => container,\n subscribe(listener: () => void) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n };\n };\n\n const registerHook = (key: any, hook: () => unknown) => {\n let renderer = renderers.get(key);\n if (!renderer) {\n renderer = createHookRenderer(hook);\n renderers.set(key, renderer);\n\n queueMicrotask(() => {\n for (const listener of renderUpdateListeners) {\n listener();\n }\n });\n }\n return renderer;\n };\n\n const subscribeToRenderUpdates = (listener: () => void) => {\n renderUpdateListeners.add(listener);\n return () => renderUpdateListeners.delete(listener);\n };\n const getRenderElements = () => renderElements;\n\n const RootWrapper = (props: { children: ReactNode }) => {\n const elements = useSyncExternalStore(\n subscribeToRenderUpdates,\n getRenderElements,\n );\n\n return (\n <>\n <>{elements}</>\n <HookRegistryContext.Provider\n value={{\n registerHook,\n }}\n >\n {props.children}\n </HookRegistryContext.Provider>\n </>\n );\n };\n\n return RootWrapper;\n }\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,94 @@
1
+ import ObservableImpl from 'zen-observable';
2
+
3
+ let toastKeyCounter = 0;
4
+ function generateToastKey() {
5
+ toastKeyCounter += 1;
6
+ return `toast-${toastKeyCounter}-${Date.now()}`;
7
+ }
8
+ class PublishSubject {
9
+ subscribers = /* @__PURE__ */ new Set();
10
+ isClosed = false;
11
+ observable = new ObservableImpl((subscriber) => {
12
+ if (this.isClosed) {
13
+ subscriber.complete();
14
+ return () => {
15
+ };
16
+ }
17
+ this.subscribers.add(subscriber);
18
+ return () => {
19
+ this.subscribers.delete(subscriber);
20
+ };
21
+ });
22
+ next(value) {
23
+ if (this.isClosed) {
24
+ throw new Error("PublishSubject is closed");
25
+ }
26
+ this.subscribers.forEach((subscriber) => subscriber.next(value));
27
+ }
28
+ subscribe(observer) {
29
+ return this.observable.subscribe(observer);
30
+ }
31
+ /**
32
+ * Creates an Observable that replays buffered values and then subscribes to live updates.
33
+ */
34
+ asObservable(replayBuffer = []) {
35
+ return new ObservableImpl((subscriber) => {
36
+ for (const value of replayBuffer) {
37
+ subscriber.next(value);
38
+ }
39
+ return this.subscribe(subscriber);
40
+ });
41
+ }
42
+ }
43
+ class ToastApiForwarder {
44
+ subject = new PublishSubject();
45
+ recentToasts = [];
46
+ closedKeys = /* @__PURE__ */ new Set();
47
+ maxBufferSize = 10;
48
+ post(toast) {
49
+ const key = generateToastKey();
50
+ const closeCallbacks = [];
51
+ let closed = false;
52
+ const close = () => {
53
+ if (closed) return;
54
+ closed = true;
55
+ this.closedKeys.add(key);
56
+ const index = this.recentToasts.findIndex((t) => t.key === key);
57
+ if (index !== -1) {
58
+ this.recentToasts.splice(index, 1);
59
+ }
60
+ if (this.recentToasts.length === 0) {
61
+ this.closedKeys.clear();
62
+ }
63
+ closeCallbacks.forEach((fn) => fn());
64
+ };
65
+ const onClose = (callback) => {
66
+ if (closed) {
67
+ callback();
68
+ } else {
69
+ closeCallbacks.push(callback);
70
+ }
71
+ };
72
+ const toastWithKey = {
73
+ ...toast,
74
+ key,
75
+ close,
76
+ onClose
77
+ };
78
+ this.recentToasts.push(toastWithKey);
79
+ if (this.recentToasts.length > this.maxBufferSize) {
80
+ this.recentToasts.shift();
81
+ }
82
+ this.subject.next(toastWithKey);
83
+ return { close };
84
+ }
85
+ toast$() {
86
+ const activeToasts = this.recentToasts.filter(
87
+ (t) => !this.closedKeys.has(t.key)
88
+ );
89
+ return this.subject.asObservable(activeToasts);
90
+ }
91
+ }
92
+
93
+ export { ToastApiForwarder };
94
+ //# sourceMappingURL=ToastApiForwarder.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ToastApiForwarder.esm.js","sources":["../../src/apis/ToastApiForwarder.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n ToastApiMessage,\n ToastApiPostResult,\n} from '@backstage/frontend-plugin-api';\nimport { Observable } from '@backstage/types';\nimport ObservableImpl from 'zen-observable';\nimport {\n ToastApiForwarderApi,\n ToastApiForwarderMessage,\n} from './toastApiForwarderRef';\n\nlet toastKeyCounter = 0;\n\n/**\n * Generates a unique key for a toast message.\n */\nfunction generateToastKey(): string {\n toastKeyCounter += 1;\n return `toast-${toastKeyCounter}-${Date.now()}`;\n}\n\n/**\n * A simple publish subject for broadcasting values to subscribers.\n */\nclass PublishSubject<T> {\n private subscribers = new Set<ZenObservable.SubscriptionObserver<T>>();\n private isClosed = false;\n\n private readonly observable = new ObservableImpl<T>(subscriber => {\n if (this.isClosed) {\n subscriber.complete();\n return () => {};\n }\n this.subscribers.add(subscriber);\n return () => {\n this.subscribers.delete(subscriber);\n };\n });\n\n next(value: T) {\n if (this.isClosed) {\n throw new Error('PublishSubject is closed');\n }\n this.subscribers.forEach(subscriber => subscriber.next(value));\n }\n\n subscribe(observer: ZenObservable.Observer<T>): ZenObservable.Subscription {\n return this.observable.subscribe(observer);\n }\n\n /**\n * Creates an Observable that replays buffered values and then subscribes to live updates.\n */\n asObservable(replayBuffer: T[] = []): Observable<T> {\n return new ObservableImpl<T>(subscriber => {\n // Replay buffered values\n for (const value of replayBuffer) {\n subscriber.next(value);\n }\n // Subscribe to live updates\n return this.subscribe(subscriber);\n });\n }\n}\n\n/**\n * Base implementation for the ToastApi that forwards toast messages to consumers.\n *\n * Recent toasts are buffered and replayed to new subscribers to prevent\n * missing toasts that were posted before subscription.\n *\n * @internal\n */\nexport class ToastApiForwarder implements ToastApiForwarderApi {\n private readonly subject = new PublishSubject<ToastApiForwarderMessage>();\n private readonly recentToasts: ToastApiForwarderMessage[] = [];\n private readonly closedKeys = new Set<string>();\n private readonly maxBufferSize = 10;\n\n post(toast: ToastApiMessage): ToastApiPostResult {\n const key = generateToastKey();\n const closeCallbacks: Array<() => void> = [];\n let closed = false;\n\n const close = () => {\n if (closed) return;\n closed = true;\n\n // Track closed keys to prevent replaying dismissed toasts\n this.closedKeys.add(key);\n\n // Remove from recent buffer if still there\n const index = this.recentToasts.findIndex(t => t.key === key);\n if (index !== -1) {\n this.recentToasts.splice(index, 1);\n }\n\n // Clean up old closed keys when buffer is cleared\n if (this.recentToasts.length === 0) {\n this.closedKeys.clear();\n }\n\n // Notify registered listeners (e.g. the toast display)\n closeCallbacks.forEach(fn => fn());\n };\n\n const onClose = (callback: () => void) => {\n if (closed) {\n callback();\n } else {\n closeCallbacks.push(callback);\n }\n };\n\n const toastWithKey: ToastApiForwarderMessage = {\n ...toast,\n key,\n close,\n onClose,\n };\n\n this.recentToasts.push(toastWithKey);\n if (this.recentToasts.length > this.maxBufferSize) {\n this.recentToasts.shift();\n }\n this.subject.next(toastWithKey);\n\n return { close };\n }\n\n toast$(): Observable<ToastApiForwarderMessage> {\n // Filter out any toasts that were closed to handle race conditions\n const activeToasts = this.recentToasts.filter(\n t => !this.closedKeys.has(t.key),\n );\n return this.subject.asObservable(activeToasts);\n }\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import { createApiRef } from '@backstage/frontend-plugin-api';
2
+
3
+ const toastApiForwarderRef = createApiRef({
4
+ id: "app.toast.internal-forwarder"
5
+ });
6
+
7
+ export { toastApiForwarderRef };
8
+ //# sourceMappingURL=toastApiForwarderRef.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toastApiForwarderRef.esm.js","sources":["../../src/apis/toastApiForwarderRef.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n ToastApi,\n ToastApiMessage,\n createApiRef,\n} from '@backstage/frontend-plugin-api';\nimport { Observable } from '@backstage/types';\n\n/**\n * Internal enriched toast message emitted by the forwarder's observable.\n *\n * @internal\n */\nexport type ToastApiForwarderMessage = ToastApiMessage & {\n /** Unique key for tracking this toast */\n key: string;\n /** Dismiss this toast programmatically */\n close(): void;\n /**\n * Register a callback that fires when this toast is closed.\n * Used by the toast display to sync with the rendering queue.\n */\n onClose(callback: () => void): void;\n};\n\n/**\n * Internal extension of the public ToastApi that adds an observable\n * for the toast display to subscribe to. This interface is only used\n * within the app plugin and can be freely evolved.\n *\n * @internal\n */\nexport type ToastApiForwarderApi = ToastApi & {\n /** Observe toasts posted by other parts of the application. */\n toast$(): Observable<ToastApiForwarderMessage>;\n};\n\n/**\n * Internal API ref for the toast forwarder. The public `toastApiRef`\n * delegates to this, and the toast display subscribes to it directly.\n *\n * @internal\n */\nexport const toastApiForwarderRef = createApiRef<ToastApiForwarderApi>({\n id: 'app.toast.internal-forwarder',\n});\n"],"names":[],"mappings":";;AA0DO,MAAM,uBAAuB,YAAA,CAAmC;AAAA,EACrE,EAAA,EAAI;AACN,CAAC;;;;"}
@@ -0,0 +1,156 @@
1
+ import { jsx, jsxs } from 'react/jsx-runtime';
2
+ import { forwardRef, useRef, useState, useLayoutEffect } from 'react';
3
+ import { useToast } from '@react-aria/toast';
4
+ import { useButton } from '@react-aria/button';
5
+ import { motion } from 'motion/react';
6
+ import { Box } from '@backstage/ui';
7
+ import { RiCloseLine, RiInformationLine, RiAlertLine, RiErrorWarningLine, RiCheckLine } from '@remixicon/react';
8
+ import styles from './Toast.module.css.esm.js';
9
+ import { BgReset } from '../../packages/ui/src/hooks/useBg.esm.js';
10
+
11
+ const manuallyClosingToasts = /* @__PURE__ */ new Set();
12
+ const Toast = forwardRef(
13
+ (props, ref) => {
14
+ const {
15
+ toast,
16
+ state,
17
+ index = 0,
18
+ isExpanded = false,
19
+ onClose,
20
+ status,
21
+ expandedY: expandedYProp = 0,
22
+ collapsedHeight,
23
+ naturalHeight,
24
+ onHeightChange
25
+ } = props;
26
+ const internalRef = useRef(null);
27
+ const toastRef = ref || internalRef;
28
+ const { toastProps, titleProps, closeButtonProps } = useToast(
29
+ { toast },
30
+ state,
31
+ toastRef
32
+ );
33
+ const closeButtonRef = useRef(null);
34
+ const ariaProps = {
35
+ role: toastProps.role,
36
+ tabIndex: toastProps.tabIndex,
37
+ "aria-label": toastProps["aria-label"],
38
+ "aria-labelledby": toastProps["aria-labelledby"],
39
+ "aria-describedby": toastProps["aria-describedby"],
40
+ "aria-posinset": toastProps["aria-posinset"],
41
+ "aria-setsize": toastProps["aria-setsize"]
42
+ };
43
+ const [hasMeasured, setHasMeasured] = useState(false);
44
+ const naturalHeightRef = useRef(null);
45
+ useLayoutEffect(() => {
46
+ if (!onHeightChange) return;
47
+ if (naturalHeightRef.current) return;
48
+ const element = toastRef.current;
49
+ if (!element) return;
50
+ const height = element.getBoundingClientRect().height;
51
+ if (height > 0) {
52
+ naturalHeightRef.current = height;
53
+ onHeightChange(toast.key, height);
54
+ setHasMeasured(true);
55
+ }
56
+ }, [toast.key, onHeightChange, toastRef]);
57
+ const handleClose = () => {
58
+ manuallyClosingToasts.add(toast.key);
59
+ onClose?.();
60
+ state.close(toast.key);
61
+ };
62
+ const { buttonProps } = useButton(
63
+ {
64
+ "aria-label": closeButtonProps["aria-label"],
65
+ onPress: handleClose
66
+ },
67
+ closeButtonRef
68
+ );
69
+ const content = toast.content;
70
+ const finalStatus = status || content.status || "info";
71
+ const getStatusIcon = () => {
72
+ switch (finalStatus) {
73
+ case "neutral":
74
+ return null;
75
+ case "success":
76
+ return /* @__PURE__ */ jsx(RiCheckLine, { "aria-hidden": "true" });
77
+ case "warning":
78
+ return /* @__PURE__ */ jsx(RiErrorWarningLine, { "aria-hidden": "true" });
79
+ case "danger":
80
+ return /* @__PURE__ */ jsx(RiAlertLine, { "aria-hidden": "true" });
81
+ case "info":
82
+ default:
83
+ return /* @__PURE__ */ jsx(RiInformationLine, { "aria-hidden": "true" });
84
+ }
85
+ };
86
+ const statusIcon = getStatusIcon();
87
+ const collapsedScale = Math.max(0.85, 1 - index * 0.05);
88
+ const collapsedY = -index * 12;
89
+ const animateY = isExpanded ? expandedYProp : collapsedY;
90
+ const animateScale = isExpanded ? 1 : collapsedScale;
91
+ const stackZIndex = 1e3 - index;
92
+ const isManualClose = manuallyClosingToasts.has(toast.key);
93
+ const exitAnimation = isManualClose ? { opacity: 0, y: 100, scale: 1, zIndex: 2e3 } : {
94
+ opacity: 0,
95
+ y: animateY + 50,
96
+ scale: animateScale,
97
+ zIndex: stackZIndex
98
+ };
99
+ const measuredHeight = naturalHeight || naturalHeightRef.current;
100
+ const isBackToast = index > 0;
101
+ const hasValidMeasurements = hasMeasured && collapsedHeight && measuredHeight;
102
+ const animateProps = {
103
+ opacity: 1,
104
+ y: animateY,
105
+ scale: animateScale,
106
+ zIndex: stackZIndex,
107
+ ...isBackToast && hasValidMeasurements ? { height: isExpanded ? measuredHeight : collapsedHeight } : {}
108
+ };
109
+ const shouldClipContent = isBackToast && hasValidMeasurements && !isExpanded;
110
+ return /* @__PURE__ */ jsx(
111
+ motion.div,
112
+ {
113
+ ...ariaProps,
114
+ ref: toastRef,
115
+ className: styles.toast,
116
+ style: {
117
+ "--toast-index": index,
118
+ overflow: shouldClipContent ? "hidden" : void 0
119
+ },
120
+ initial: { opacity: 0, y: 100, scale: 1 },
121
+ animate: animateProps,
122
+ exit: exitAnimation,
123
+ onAnimationComplete: (definition) => {
124
+ if (definition === "exit") {
125
+ manuallyClosingToasts.delete(toast.key);
126
+ }
127
+ },
128
+ transition: { type: "spring", stiffness: 400, damping: 35 },
129
+ "data-status": finalStatus,
130
+ children: /* @__PURE__ */ jsx(BgReset, { children: /* @__PURE__ */ jsxs(Box, { className: styles.surface, children: [
131
+ /* @__PURE__ */ jsxs("div", { className: styles.wrapper, children: [
132
+ statusIcon && /* @__PURE__ */ jsx("div", { className: styles.icon, children: statusIcon }),
133
+ /* @__PURE__ */ jsxs("div", { className: styles.content, children: [
134
+ /* @__PURE__ */ jsx("div", { ...titleProps, className: styles.title, children: content.title }),
135
+ content.description && /* @__PURE__ */ jsx("div", { className: styles.description, children: content.description }),
136
+ content.links && content.links.length > 0 && /* @__PURE__ */ jsx("div", { className: styles.links, children: content.links.map((link) => /* @__PURE__ */ jsx("a", { href: link.href, children: link.label }, link.href)) })
137
+ ] })
138
+ ] }),
139
+ /* @__PURE__ */ jsx(
140
+ "button",
141
+ {
142
+ ...buttonProps,
143
+ ref: closeButtonRef,
144
+ className: styles.closeButton,
145
+ children: /* @__PURE__ */ jsx(RiCloseLine, { "aria-hidden": "true" })
146
+ }
147
+ )
148
+ ] }) })
149
+ }
150
+ );
151
+ }
152
+ );
153
+ Toast.displayName = "Toast";
154
+
155
+ export { Toast };
156
+ //# sourceMappingURL=Toast.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Toast.esm.js","sources":["../../../src/components/Toast/Toast.tsx"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { forwardRef, Ref, useRef, useLayoutEffect, useState } from 'react';\nimport { useToast } from '@react-aria/toast';\nimport { useButton } from '@react-aria/button';\nimport { motion } from 'motion/react';\nimport { Box } from '@backstage/ui';\nimport {\n RiInformationLine,\n RiCheckLine,\n RiErrorWarningLine,\n RiAlertLine,\n RiCloseLine,\n} from '@remixicon/react';\nimport type { ToastApiMessageProps } from './types';\nimport styles from './Toast.module.css';\n// eslint-disable-next-line @backstage/no-relative-monorepo-imports\nimport { BgReset } from '../../../../../packages/ui/src/hooks/useBg';\n\n// Track which toasts are being manually closed (vs auto-timeout)\n// This allows different exit animations for each case\nconst manuallyClosingToasts = new Set<string>();\n\n/**\n * A Toast displays a brief, temporary notification of actions, errors, or other events in an application.\n *\n * @remarks\n * The Toast component is used internally by ToastContainer and managed by a ToastQueue.\n * It supports multiple status variants (neutral, info, success, warning, danger) and can display\n * a title and description. Toasts can be dismissed manually or automatically.\n *\n * @internal\n */\nexport const Toast = forwardRef(\n (props: ToastApiMessageProps, ref: Ref<HTMLDivElement>) => {\n const {\n toast,\n state,\n index = 0,\n isExpanded = false,\n onClose,\n status,\n expandedY: expandedYProp = 0,\n collapsedHeight,\n naturalHeight,\n onHeightChange,\n } = props;\n\n // Use internal ref if none provided\n const internalRef = useRef<HTMLDivElement>(null);\n const toastRef = (ref as React.RefObject<HTMLDivElement>) || internalRef;\n\n // Get ARIA props from useToast hook\n const { toastProps, titleProps, closeButtonProps } = useToast(\n { toast },\n state,\n toastRef,\n );\n\n // Close button ref for useButton hook\n const closeButtonRef = useRef<HTMLButtonElement>(null);\n\n // Extract only ARIA and accessibility props from toastProps to avoid\n // conflicts with motion.div's event handler types (motion has its own drag API)\n const ariaProps = {\n role: toastProps.role,\n tabIndex: toastProps.tabIndex,\n 'aria-label': toastProps['aria-label'],\n 'aria-labelledby': toastProps['aria-labelledby'],\n 'aria-describedby': toastProps['aria-describedby'],\n 'aria-posinset': toastProps['aria-posinset'],\n 'aria-setsize': toastProps['aria-setsize'],\n };\n\n // Track whether we've measured this toast's natural height\n const [hasMeasured, setHasMeasured] = useState(false);\n // Store the measured natural height locally to avoid re-measurement issues\n const naturalHeightRef = useRef<number | null>(null);\n\n // Measure this toast's natural height on mount (before paint)\n // Using useLayoutEffect ensures we measure before the browser paints\n useLayoutEffect(() => {\n if (!onHeightChange) return;\n if (naturalHeightRef.current) return; // Already measured\n\n const element = toastRef.current;\n if (!element) return;\n\n // Measure immediately - useLayoutEffect runs before paint\n const height = element.getBoundingClientRect().height;\n if (height > 0) {\n naturalHeightRef.current = height;\n onHeightChange(toast.key, height);\n setHasMeasured(true);\n }\n }, [toast.key, onHeightChange, toastRef]);\n\n // Close button handler\n const handleClose = () => {\n // Mark this toast as manually closed for exit animation\n manuallyClosingToasts.add(toast.key);\n onClose?.();\n state.close(toast.key);\n };\n\n // Get button props from useButton hook\n const { buttonProps } = useButton(\n {\n 'aria-label': closeButtonProps['aria-label'],\n onPress: handleClose,\n },\n closeButtonRef,\n );\n\n // Get content from toast\n const content = toast.content;\n const finalStatus = status || content.status || 'info';\n\n // Determine which icon to render based on status\n const getStatusIcon = () => {\n switch (finalStatus) {\n case 'neutral':\n // Neutral status has no icon\n return null;\n case 'success':\n return <RiCheckLine aria-hidden=\"true\" />;\n case 'warning':\n return <RiErrorWarningLine aria-hidden=\"true\" />;\n case 'danger':\n return <RiAlertLine aria-hidden=\"true\" />;\n case 'info':\n default:\n return <RiInformationLine aria-hidden=\"true\" />;\n }\n };\n\n const statusIcon = getStatusIcon();\n\n // Calculate stacking values based on index\n // Collapsed: each toast behind scales down 5% and peeks up 12px\n const collapsedScale = Math.max(0.85, 1 - index * 0.05);\n const collapsedY = -index * 12;\n\n // Use expanded or collapsed values based on hover state\n // expandedYProp is pre-calculated based on actual toast heights\n const animateY = isExpanded ? expandedYProp : collapsedY;\n const animateScale = isExpanded ? 1 : collapsedScale;\n const stackZIndex = 1000 - index;\n\n // Check if this toast is being manually closed\n const isManualClose = manuallyClosingToasts.has(toast.key);\n\n // Different exit animations for manual close vs auto-timeout\n // Manual close: slide down from front, stay on top\n // Auto-timeout: fade out in place, stay in stack position\n const exitAnimation = isManualClose\n ? { opacity: 0, y: 100, scale: 1, zIndex: 2000 }\n : {\n opacity: 0,\n y: animateY + 50,\n scale: animateScale,\n zIndex: stackZIndex,\n };\n\n // Height animation for back toasts:\n // - Front toast (index 0): never set height, uses natural CSS height\n // - Back toasts: animate between collapsedHeight and their own naturalHeight\n const measuredHeight = naturalHeight || naturalHeightRef.current;\n const isBackToast = index > 0;\n const hasValidMeasurements =\n hasMeasured && collapsedHeight && measuredHeight;\n\n // For back toasts with valid measurements, calculate target height\n // Otherwise, let CSS handle it naturally\n const animateProps: {\n opacity: number;\n y: number;\n scale: number;\n zIndex: number;\n height?: number;\n } = {\n opacity: 1,\n y: animateY,\n scale: animateScale,\n zIndex: stackZIndex,\n ...(isBackToast && hasValidMeasurements\n ? { height: isExpanded ? measuredHeight : collapsedHeight }\n : {}),\n };\n\n const shouldClipContent =\n isBackToast && hasValidMeasurements && !isExpanded;\n\n return (\n <motion.div\n {...ariaProps}\n ref={toastRef}\n className={styles.toast}\n style={\n {\n '--toast-index': index,\n overflow: shouldClipContent ? 'hidden' : undefined,\n } as React.CSSProperties\n }\n initial={{ opacity: 0, y: 100, scale: 1 }}\n animate={animateProps}\n exit={exitAnimation}\n onAnimationComplete={definition => {\n // Clean up the manual close tracking after exit animation\n if (definition === 'exit') {\n manuallyClosingToasts.delete(toast.key);\n }\n }}\n transition={{ type: 'spring', stiffness: 400, damping: 35 }}\n data-status={finalStatus}\n >\n <BgReset>\n <Box className={styles.surface}>\n <div className={styles.wrapper}>\n {statusIcon && <div className={styles.icon}>{statusIcon}</div>}\n <div className={styles.content}>\n <div {...titleProps} className={styles.title}>\n {content.title}\n </div>\n {content.description && (\n <div className={styles.description}>\n {content.description}\n </div>\n )}\n {content.links && content.links.length > 0 && (\n <div className={styles.links}>\n {content.links.map(link => (\n <a key={link.href} href={link.href}>\n {link.label}\n </a>\n ))}\n </div>\n )}\n </div>\n </div>\n {/* eslint-disable-next-line react/forbid-elements */}\n <button\n {...buttonProps}\n ref={closeButtonRef}\n className={styles.closeButton}\n >\n <RiCloseLine aria-hidden=\"true\" />\n </button>\n </Box>\n </BgReset>\n </motion.div>\n );\n },\n);\n\nToast.displayName = 'Toast';\n"],"names":[],"mappings":"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"}