@backstage/ui 0.17.2-next.0 → 0.18.0-next.1

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 (153) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/analytics/useAnalytics.esm.js +5 -5
  3. package/dist/analytics/useAnalytics.esm.js.map +1 -1
  4. package/dist/components/Accordion/definition.esm.js +3 -1
  5. package/dist/components/Accordion/definition.esm.js.map +1 -1
  6. package/dist/components/Alert/definition.esm.js +3 -1
  7. package/dist/components/Alert/definition.esm.js.map +1 -1
  8. package/dist/components/Avatar/definition.esm.js +3 -1
  9. package/dist/components/Avatar/definition.esm.js.map +1 -1
  10. package/dist/components/Badge/definition.esm.js +3 -1
  11. package/dist/components/Badge/definition.esm.js.map +1 -1
  12. package/dist/components/Box/definition.esm.js +3 -1
  13. package/dist/components/Box/definition.esm.js.map +1 -1
  14. package/dist/components/Button/definition.esm.js +3 -1
  15. package/dist/components/Button/definition.esm.js.map +1 -1
  16. package/dist/components/ButtonIcon/definition.esm.js +3 -1
  17. package/dist/components/ButtonIcon/definition.esm.js.map +1 -1
  18. package/dist/components/ButtonLink/ButtonLink.esm.js +39 -23
  19. package/dist/components/ButtonLink/ButtonLink.esm.js.map +1 -1
  20. package/dist/components/ButtonLink/definition.esm.js +4 -1
  21. package/dist/components/ButtonLink/definition.esm.js.map +1 -1
  22. package/dist/components/Card/Card.esm.js +2 -1
  23. package/dist/components/Card/Card.esm.js.map +1 -1
  24. package/dist/components/Card/definition.esm.js +3 -1
  25. package/dist/components/Card/definition.esm.js.map +1 -1
  26. package/dist/components/Checkbox/definition.esm.js +3 -1
  27. package/dist/components/Checkbox/definition.esm.js.map +1 -1
  28. package/dist/components/CheckboxGroup/definition.esm.js +3 -1
  29. package/dist/components/CheckboxGroup/definition.esm.js.map +1 -1
  30. package/dist/components/Combobox/ComboboxItem.esm.js +37 -11
  31. package/dist/components/Combobox/ComboboxItem.esm.js.map +1 -1
  32. package/dist/components/Combobox/definition.esm.js +4 -1
  33. package/dist/components/Combobox/definition.esm.js.map +1 -1
  34. package/dist/components/Container/definition.esm.js +3 -1
  35. package/dist/components/Container/definition.esm.js.map +1 -1
  36. package/dist/components/DatePicker/definition.esm.js +3 -1
  37. package/dist/components/DatePicker/definition.esm.js.map +1 -1
  38. package/dist/components/DateRangePicker/definition.esm.js +3 -1
  39. package/dist/components/DateRangePicker/definition.esm.js.map +1 -1
  40. package/dist/components/Dialog/definition.esm.js +3 -1
  41. package/dist/components/Dialog/definition.esm.js.map +1 -1
  42. package/dist/components/FieldError/definition.esm.js +3 -1
  43. package/dist/components/FieldError/definition.esm.js.map +1 -1
  44. package/dist/components/FieldLabel/definition.esm.js +3 -1
  45. package/dist/components/FieldLabel/definition.esm.js.map +1 -1
  46. package/dist/components/Flex/definition.esm.js +3 -1
  47. package/dist/components/Flex/definition.esm.js.map +1 -1
  48. package/dist/components/FullPage/definition.esm.js +3 -1
  49. package/dist/components/FullPage/definition.esm.js.map +1 -1
  50. package/dist/components/Grid/definition.esm.js +3 -1
  51. package/dist/components/Grid/definition.esm.js.map +1 -1
  52. package/dist/components/Header/HeaderMetadataStatus.esm.js +3 -1
  53. package/dist/components/Header/HeaderMetadataStatus.esm.js.map +1 -1
  54. package/dist/components/Header/HeaderMetadataUsers.esm.js +3 -1
  55. package/dist/components/Header/HeaderMetadataUsers.esm.js.map +1 -1
  56. package/dist/components/Header/HeaderNav.esm.js +67 -24
  57. package/dist/components/Header/HeaderNav.esm.js.map +1 -1
  58. package/dist/components/Header/HeaderNavDefinition.esm.js +4 -1
  59. package/dist/components/Header/HeaderNavDefinition.esm.js.map +1 -1
  60. package/dist/components/Header/definition.esm.js +3 -1
  61. package/dist/components/Header/definition.esm.js.map +1 -1
  62. package/dist/components/Link/Link.esm.js +64 -23
  63. package/dist/components/Link/Link.esm.js.map +1 -1
  64. package/dist/components/Link/definition.esm.js +4 -1
  65. package/dist/components/Link/definition.esm.js.map +1 -1
  66. package/dist/components/List/List.esm.js +3 -2
  67. package/dist/components/List/List.esm.js.map +1 -1
  68. package/dist/components/List/definition.esm.js +3 -1
  69. package/dist/components/List/definition.esm.js.map +1 -1
  70. package/dist/components/Menu/Menu.esm.js +48 -14
  71. package/dist/components/Menu/Menu.esm.js.map +1 -1
  72. package/dist/components/Menu/definition.esm.js +5 -1
  73. package/dist/components/Menu/definition.esm.js.map +1 -1
  74. package/dist/components/NumberField/definition.esm.js +3 -1
  75. package/dist/components/NumberField/definition.esm.js.map +1 -1
  76. package/dist/components/PasswordField/definition.esm.js +3 -1
  77. package/dist/components/PasswordField/definition.esm.js.map +1 -1
  78. package/dist/components/PluginHeader/PluginHeaderBreadcrumbs.esm.js +3 -1
  79. package/dist/components/PluginHeader/PluginHeaderBreadcrumbs.esm.js.map +1 -1
  80. package/dist/components/PluginHeader/definition.esm.js +3 -1
  81. package/dist/components/PluginHeader/definition.esm.js.map +1 -1
  82. package/dist/components/Popover/definition.esm.js +3 -1
  83. package/dist/components/Popover/definition.esm.js.map +1 -1
  84. package/dist/components/RadioGroup/definition.esm.js +3 -1
  85. package/dist/components/RadioGroup/definition.esm.js.map +1 -1
  86. package/dist/components/SearchAutocomplete/SearchAutocomplete.esm.js +23 -5
  87. package/dist/components/SearchAutocomplete/SearchAutocomplete.esm.js.map +1 -1
  88. package/dist/components/SearchAutocomplete/definition.esm.js +4 -1
  89. package/dist/components/SearchAutocomplete/definition.esm.js.map +1 -1
  90. package/dist/components/SearchField/definition.esm.js +3 -1
  91. package/dist/components/SearchField/definition.esm.js.map +1 -1
  92. package/dist/components/Select/SelectItem.esm.js +17 -3
  93. package/dist/components/Select/SelectItem.esm.js.map +1 -1
  94. package/dist/components/Select/definition.esm.js +4 -1
  95. package/dist/components/Select/definition.esm.js.map +1 -1
  96. package/dist/components/Skeleton/definition.esm.js +3 -1
  97. package/dist/components/Skeleton/definition.esm.js.map +1 -1
  98. package/dist/components/Slider/definition.esm.js +3 -1
  99. package/dist/components/Slider/definition.esm.js.map +1 -1
  100. package/dist/components/Switch/definition.esm.js +3 -1
  101. package/dist/components/Switch/definition.esm.js.map +1 -1
  102. package/dist/components/Table/components/Row.esm.js +9 -9
  103. package/dist/components/Table/components/Row.esm.js.map +1 -1
  104. package/dist/components/Table/components/TableBodySkeleton.esm.js +3 -1
  105. package/dist/components/Table/components/TableBodySkeleton.esm.js.map +1 -1
  106. package/dist/components/Table/components/TableRoot.esm.js +3 -2
  107. package/dist/components/Table/components/TableRoot.esm.js.map +1 -1
  108. package/dist/components/Table/definition.esm.js +3 -2
  109. package/dist/components/Table/definition.esm.js.map +1 -1
  110. package/dist/components/TablePagination/TablePagination.esm.js +3 -1
  111. package/dist/components/TablePagination/TablePagination.esm.js.map +1 -1
  112. package/dist/components/TablePagination/definition.esm.js +3 -1
  113. package/dist/components/TablePagination/definition.esm.js.map +1 -1
  114. package/dist/components/Tabs/Tabs.esm.js +38 -15
  115. package/dist/components/Tabs/Tabs.esm.js.map +1 -1
  116. package/dist/components/Tabs/definition.esm.js +4 -1
  117. package/dist/components/Tabs/definition.esm.js.map +1 -1
  118. package/dist/components/TagGroup/TagGroup.esm.js +12 -12
  119. package/dist/components/TagGroup/TagGroup.esm.js.map +1 -1
  120. package/dist/components/TagGroup/definition.esm.js +3 -2
  121. package/dist/components/TagGroup/definition.esm.js.map +1 -1
  122. package/dist/components/Text/definition.esm.js +3 -1
  123. package/dist/components/Text/definition.esm.js.map +1 -1
  124. package/dist/components/TextAreaField/definition.esm.js +3 -1
  125. package/dist/components/TextAreaField/definition.esm.js.map +1 -1
  126. package/dist/components/TextField/definition.esm.js +3 -1
  127. package/dist/components/TextField/definition.esm.js.map +1 -1
  128. package/dist/components/ToggleButton/definition.esm.js +3 -1
  129. package/dist/components/ToggleButton/definition.esm.js.map +1 -1
  130. package/dist/components/ToggleButtonGroup/definition.esm.js +3 -1
  131. package/dist/components/ToggleButtonGroup/definition.esm.js.map +1 -1
  132. package/dist/components/Tooltip/definition.esm.js +3 -1
  133. package/dist/components/Tooltip/definition.esm.js.map +1 -1
  134. package/dist/components/VisuallyHidden/definition.esm.js +3 -1
  135. package/dist/components/VisuallyHidden/definition.esm.js.map +1 -1
  136. package/dist/hooks/useDefinition/useDefinition.esm.js +8 -13
  137. package/dist/hooks/useDefinition/useDefinition.esm.js.map +1 -1
  138. package/dist/hooks/useDefinition/useDefinitionNavigation.esm.js +50 -0
  139. package/dist/hooks/useDefinition/useDefinitionNavigation.esm.js.map +1 -0
  140. package/dist/index.d.ts +32 -8
  141. package/dist/navigation/BUIRoutingProvider.esm.js +44 -0
  142. package/dist/navigation/BUIRoutingProvider.esm.js.map +1 -0
  143. package/dist/navigation/useNavigation.esm.js +130 -0
  144. package/dist/navigation/useNavigation.esm.js.map +1 -0
  145. package/dist/navigation/useRouting.esm.js +27 -0
  146. package/dist/navigation/useRouting.esm.js.map +1 -0
  147. package/dist/provider/BUIContext.esm.js +6 -0
  148. package/dist/provider/BUIContext.esm.js.map +1 -0
  149. package/dist/provider/BUIProvider.esm.js +5 -13
  150. package/dist/provider/BUIProvider.esm.js.map +1 -1
  151. package/dist/utils/linkUtils.esm.js +6 -12
  152. package/dist/utils/linkUtils.esm.js.map +1 -1
  153. package/package.json +3 -2
package/dist/index.d.ts CHANGED
@@ -1491,7 +1491,7 @@ type TabOwnProps = {
1491
1491
  *
1492
1492
  * @public
1493
1493
  */
1494
- interface TabProps extends TabOwnProps, Omit<TabProps$1, keyof TabOwnProps> {
1494
+ interface TabProps extends TabOwnProps, Omit<TabProps$1, keyof TabOwnProps | 'render'> {
1495
1495
  }
1496
1496
  /**
1497
1497
  * Own props for the TabPanel component.
@@ -1894,6 +1894,9 @@ declare const HeaderNavItemDefinition: {
1894
1894
  readonly root: "bui-HeaderNavItem";
1895
1895
  };
1896
1896
  readonly analytics: true;
1897
+ readonly navigation: {
1898
+ readonly type: "anchor";
1899
+ };
1897
1900
  readonly propDefs: {
1898
1901
  readonly noTrack: {};
1899
1902
  readonly id: {};
@@ -2012,7 +2015,7 @@ type ButtonLinkOwnProps = {
2012
2015
  *
2013
2016
  * @public
2014
2017
  */
2015
- interface ButtonLinkProps extends Omit<LinkProps$1, keyof ButtonLinkOwnProps>, ButtonLinkOwnProps {
2018
+ interface ButtonLinkProps extends Omit<LinkProps$1, keyof ButtonLinkOwnProps | 'render'>, ButtonLinkOwnProps {
2016
2019
  }
2017
2020
 
2018
2021
  /**
@@ -2036,6 +2039,9 @@ declare const ButtonLinkDefinition: {
2036
2039
  };
2037
2040
  readonly bg: "consumer";
2038
2041
  readonly analytics: true;
2042
+ readonly navigation: {
2043
+ readonly type: "anchor";
2044
+ };
2039
2045
  readonly propDefs: {
2040
2046
  readonly noTrack: {};
2041
2047
  readonly size: {
@@ -2413,7 +2419,7 @@ type ComboboxItemOwnProps = {
2413
2419
  className?: string;
2414
2420
  };
2415
2421
  /** @public */
2416
- type ComboboxItemProps<T extends object = object> = ComboboxItemOwnProps & Omit<ListBoxItemProps<T>, keyof ComboboxItemOwnProps>;
2422
+ type ComboboxItemProps<T extends object = object> = ComboboxItemOwnProps & Omit<ListBoxItemProps<T>, keyof ComboboxItemOwnProps | 'render'>;
2417
2423
  /** @public */
2418
2424
  type ComboboxItemTextOwnProps = {
2419
2425
  title: string;
@@ -2560,6 +2566,9 @@ declare const ComboboxItemDefinition: {
2560
2566
  readonly indicator: "bui-ComboboxItemIndicator";
2561
2567
  readonly content: "bui-ComboboxItemContent";
2562
2568
  };
2569
+ readonly navigation: {
2570
+ readonly type: "anchor";
2571
+ };
2563
2572
  readonly propDefs: {
2564
2573
  readonly children: {};
2565
2574
  readonly textValue: {};
@@ -3740,7 +3749,7 @@ type MenuItemOwnProps = {
3740
3749
  className?: string;
3741
3750
  };
3742
3751
  /** @public */
3743
- interface MenuItemProps extends MenuItemOwnProps, Omit<MenuItemProps$1, keyof MenuItemOwnProps> {
3752
+ interface MenuItemProps extends MenuItemOwnProps, Omit<MenuItemProps$1, keyof MenuItemOwnProps | 'render'> {
3744
3753
  }
3745
3754
  /** @public */
3746
3755
  type MenuListBoxItemOwnProps = {
@@ -3748,7 +3757,7 @@ type MenuListBoxItemOwnProps = {
3748
3757
  className?: string;
3749
3758
  };
3750
3759
  /** @public */
3751
- interface MenuListBoxItemProps extends MenuListBoxItemOwnProps, Omit<ListBoxItemProps, keyof MenuListBoxItemOwnProps> {
3760
+ interface MenuListBoxItemProps extends MenuListBoxItemOwnProps, Omit<ListBoxItemProps, keyof MenuListBoxItemOwnProps | 'render'> {
3752
3761
  }
3753
3762
  /** @public */
3754
3763
  type MenuSectionOwnProps = {
@@ -3962,7 +3971,7 @@ type SearchAutocompleteItemOwnProps = {
3962
3971
  className?: string;
3963
3972
  };
3964
3973
  /** @public */
3965
- interface SearchAutocompleteItemProps extends SearchAutocompleteItemOwnProps, Omit<ListBoxItemProps, keyof SearchAutocompleteItemOwnProps> {
3974
+ interface SearchAutocompleteItemProps extends SearchAutocompleteItemOwnProps, Omit<ListBoxItemProps, keyof SearchAutocompleteItemOwnProps | 'render'> {
3966
3975
  }
3967
3976
 
3968
3977
  /**
@@ -4104,7 +4113,7 @@ type LinkOwnProps = {
4104
4113
  className?: string;
4105
4114
  };
4106
4115
  /** @public */
4107
- interface LinkProps extends Omit<LinkProps$1, 'children' | 'className'>, LinkOwnProps {
4116
+ interface LinkProps extends Omit<LinkProps$1, 'children' | 'className' | 'render'>, LinkOwnProps {
4108
4117
  }
4109
4118
 
4110
4119
  /**
@@ -4126,6 +4135,9 @@ declare const LinkDefinition: {
4126
4135
  readonly root: "bui-Link";
4127
4136
  };
4128
4137
  readonly analytics: true;
4138
+ readonly navigation: {
4139
+ readonly type: "anchor";
4140
+ };
4129
4141
  readonly propDefs: {
4130
4142
  readonly noTrack: {};
4131
4143
  readonly variant: {
@@ -4458,7 +4470,7 @@ type SelectItemOwnProps = {
4458
4470
  className?: string;
4459
4471
  };
4460
4472
  /** @public */
4461
- type SelectItemProps<T extends object = object> = SelectItemOwnProps & Omit<ListBoxItemProps<T>, keyof SelectItemOwnProps | 'textValue'> & {
4473
+ type SelectItemProps<T extends object = object> = SelectItemOwnProps & Omit<ListBoxItemProps<T>, keyof SelectItemOwnProps | 'render' | 'textValue'> & {
4462
4474
  textValue: string;
4463
4475
  };
4464
4476
  /** @public */
@@ -4616,6 +4628,9 @@ declare const SelectItemDefinition: {
4616
4628
  readonly indicator: "bui-SelectItemIndicator";
4617
4629
  readonly content: "bui-SelectItemContent";
4618
4630
  };
4631
+ readonly navigation: {
4632
+ readonly type: "anchor";
4633
+ };
4619
4634
  readonly propDefs: {
4620
4635
  readonly children: {};
4621
4636
  readonly showSelectionIndicator: {};
@@ -4972,6 +4987,15 @@ type BUIProviderProps = {
4972
4987
  /**
4973
4988
  * Provides integration capabilities to all descendant BUI components.
4974
4989
  *
4990
+ * When rendered inside the Backstage app router, BUI components use
4991
+ * client-side navigation for internal links. Relative destinations resolve
4992
+ * from the route of the component that renders the link, and the router
4993
+ * basename applies once.
4994
+ *
4995
+ * External and scheme links, downloads, and links with non-self targets use the
4996
+ * browser's native navigation. BUI components rendered outside React Router use
4997
+ * native links without throwing.
4998
+ *
4975
4999
  * @example
4976
5000
  * ```tsx
4977
5001
  * import { BUIProvider } from '@backstage/ui';
@@ -0,0 +1,44 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+ import { useCallback } from 'react';
3
+ import { RouterProvider } from 'react-aria-components';
4
+ import { useResolvedPath, useNavigate, useLocation, useInRouterContext, useHref, Link } from 'react-router-dom';
5
+ import { useResolvedHref } from '../hooks/useResolvedHref.esm.js';
6
+
7
+ const delegatedNavigations = /* @__PURE__ */ new WeakMap();
8
+ const buiRoutingIntegration = {
9
+ Link,
10
+ useHref,
11
+ useInRouterContext,
12
+ useLocation,
13
+ useNavigate,
14
+ useResolvedPath,
15
+ createRouterOptions(action, options) {
16
+ const routerOptions = { ...options };
17
+ delegatedNavigations.set(routerOptions, action);
18
+ return routerOptions;
19
+ }
20
+ };
21
+ function BUIRoutingProvider({ children }) {
22
+ if (!useInRouterContext()) {
23
+ return children;
24
+ }
25
+ return /* @__PURE__ */ jsx(ReactAriaRoutingProvider, { children });
26
+ }
27
+ function ReactAriaRoutingProvider({ children }) {
28
+ const providerNavigate = useNavigate();
29
+ const navigate = useCallback(
30
+ (href, options) => {
31
+ const delegatedNavigation = options ? delegatedNavigations.get(options) : void 0;
32
+ if (delegatedNavigation) {
33
+ delegatedNavigation();
34
+ return;
35
+ }
36
+ providerNavigate(href, options);
37
+ },
38
+ [providerNavigate]
39
+ );
40
+ return /* @__PURE__ */ jsx(RouterProvider, { navigate, useHref: useResolvedHref, children });
41
+ }
42
+
43
+ export { BUIRoutingProvider, buiRoutingIntegration };
44
+ //# sourceMappingURL=BUIRoutingProvider.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BUIRoutingProvider.esm.js","sources":["../../src/navigation/BUIRoutingProvider.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 { useCallback, type ReactNode } from 'react';\nimport { RouterProvider } from 'react-aria-components';\nimport {\n Link,\n useHref,\n useInRouterContext,\n useLocation,\n useNavigate,\n useResolvedPath,\n} from 'react-router-dom';\nimport { useResolvedHref } from '../hooks/useResolvedHref';\nimport type { BUIRoutingIntegration } from './types';\n\n// BUIProvider mounts this provider centrally so older BUI components from the\n// same React Aria module instance keep delegated client-side navigation.\n// Collection roots also mount it locally so their synthetic link items and the\n// provider always use the same React Aria module instance. Preserving each\n// item's href and original activation event lets React Aria choose between\n// client navigation and temporary-anchor activation for native link behavior.\n//\n// The exact options object identifies a component-scoped action. Unregistered\n// objects, including those from older BUI components or direct React Aria\n// usage, use the provider's router context instead. A separate React Aria\n// module instance cannot reach this provider and is handled by the rendered\n// host Link in useNavigation.\nconst delegatedNavigations = new WeakMap<object, () => void>();\n\n/** @internal */\nexport const buiRoutingIntegration: BUIRoutingIntegration = {\n Link,\n useHref,\n useInRouterContext,\n useLocation,\n useNavigate,\n useResolvedPath,\n createRouterOptions(action, options) {\n const routerOptions = { ...options };\n delegatedNavigations.set(routerOptions, action);\n return routerOptions;\n },\n};\n\n/** @internal */\nexport function BUIRoutingProvider({ children }: { children: ReactNode }) {\n if (!useInRouterContext()) {\n return children;\n }\n return <ReactAriaRoutingProvider>{children}</ReactAriaRoutingProvider>;\n}\n\nfunction ReactAriaRoutingProvider({ children }: { children: ReactNode }) {\n const providerNavigate = useNavigate();\n const navigate = useCallback(\n (href: string, options: object | undefined) => {\n const delegatedNavigation = options\n ? delegatedNavigations.get(options)\n : undefined;\n if (delegatedNavigation) {\n delegatedNavigation();\n return;\n }\n providerNavigate(href, options);\n },\n [providerNavigate],\n );\n\n return (\n <RouterProvider navigate={navigate} useHref={useResolvedHref}>\n {children}\n </RouterProvider>\n );\n}\n"],"names":[],"mappings":";;;;;;AAyCA,MAAM,oBAAA,uBAA2B,OAAA,EAA4B;AAGtD,MAAM,qBAAA,GAA+C;AAAA,EAC1D,IAAA;AAAA,EACA,OAAA;AAAA,EACA,kBAAA;AAAA,EACA,WAAA;AAAA,EACA,WAAA;AAAA,EACA,eAAA;AAAA,EACA,mBAAA,CAAoB,QAAQ,OAAA,EAAS;AACnC,IAAA,MAAM,aAAA,GAAgB,EAAE,GAAG,OAAA,EAAQ;AACnC,IAAA,oBAAA,CAAqB,GAAA,CAAI,eAAe,MAAM,CAAA;AAC9C,IAAA,OAAO,aAAA;AAAA,EACT;AACF;AAGO,SAAS,kBAAA,CAAmB,EAAE,QAAA,EAAS,EAA4B;AACxE,EAAA,IAAI,CAAC,oBAAmB,EAAG;AACzB,IAAA,OAAO,QAAA;AAAA,EACT;AACA,EAAA,uBAAO,GAAA,CAAC,4BAA0B,QAAA,EAAS,CAAA;AAC7C;AAEA,SAAS,wBAAA,CAAyB,EAAE,QAAA,EAAS,EAA4B;AACvE,EAAA,MAAM,mBAAmB,WAAA,EAAY;AACrC,EAAA,MAAM,QAAA,GAAW,WAAA;AAAA,IACf,CAAC,MAAc,OAAA,KAAgC;AAC7C,MAAA,MAAM,mBAAA,GAAsB,OAAA,GACxB,oBAAA,CAAqB,GAAA,CAAI,OAAO,CAAA,GAChC,MAAA;AACJ,MAAA,IAAI,mBAAA,EAAqB;AACvB,QAAA,mBAAA,EAAoB;AACpB,QAAA;AAAA,MACF;AACA,MAAA,gBAAA,CAAiB,MAAM,OAAO,CAAA;AAAA,IAChC,CAAA;AAAA,IACA,CAAC,gBAAgB;AAAA,GACnB;AAEA,EAAA,uBACE,GAAA,CAAC,cAAA,EAAA,EAAe,QAAA,EAAoB,OAAA,EAAS,iBAC1C,QAAA,EACH,CAAA;AAEJ;;;;"}
@@ -0,0 +1,130 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+ import { useCallback, useMemo } from 'react';
3
+ import { createPath } from 'react-router-dom';
4
+ import { isBrowserOwnedHref } from '../utils/linkUtils.esm.js';
5
+ import { useRoutingIntegration, fallbackRoutingIntegration } from './useRouting.esm.js';
6
+
7
+ function isNativeNavigation(props) {
8
+ const { href, target, download } = props;
9
+ return Boolean(
10
+ href !== void 0 && isBrowserOwnedHref(href) || target && target !== "_self" || download !== void 0 && download !== false
11
+ );
12
+ }
13
+ function useAnchorNavigation(props) {
14
+ const { href, routerOptions: navigateOptions } = props;
15
+ const routingIntegration = useRoutingIntegration();
16
+ const routing = routingIntegration ?? fallbackRoutingIntegration;
17
+ const navigate = routing.useNavigate();
18
+ const routerHref = href !== void 0 && !isBrowserOwnedHref(href) ? href : ".";
19
+ const relativeOptions = { relative: navigateOptions?.relative };
20
+ const resolvedPath = routing.useResolvedPath(routerHref, relativeOptions);
21
+ const resolvedHref = routing.useHref(routerHref, relativeOptions);
22
+ const navigateWithOptions = useCallback(() => {
23
+ if (href !== void 0) {
24
+ navigate(href, navigateOptions);
25
+ }
26
+ }, [navigate, href, navigateOptions]);
27
+ const delegatedRouterOptions = useMemo(() => {
28
+ if (!routingIntegration || href === void 0) {
29
+ return void 0;
30
+ }
31
+ return routingIntegration.createRouterOptions(
32
+ navigateWithOptions,
33
+ navigateOptions
34
+ );
35
+ }, [routingIntegration, navigateWithOptions, href, navigateOptions]);
36
+ let routerLinkOptions;
37
+ if (navigateOptions) {
38
+ const { flushSync: _flushSync, ...supportedOptions } = navigateOptions;
39
+ routerLinkOptions = supportedOptions;
40
+ }
41
+ const navigateWithFullOptions = navigateOptions?.flushSync === true ? navigateWithOptions : void 0;
42
+ if (href === void 0) {
43
+ return { type: "none", canMatchRoute: false };
44
+ }
45
+ const canMatchRoute = !isBrowserOwnedHref(href);
46
+ if (isNativeNavigation(props)) {
47
+ return {
48
+ type: "native",
49
+ canMatchRoute,
50
+ ariaHref: href,
51
+ browserHref: isBrowserOwnedHref(href) ? href : resolvedHref
52
+ };
53
+ }
54
+ if (routingIntegration) {
55
+ return {
56
+ type: "router",
57
+ canMatchRoute,
58
+ ariaHref: href,
59
+ to: href,
60
+ Link: routingIntegration.Link,
61
+ routerOptions: delegatedRouterOptions,
62
+ routerLinkOptions,
63
+ navigateWithFullOptions
64
+ };
65
+ }
66
+ return {
67
+ type: "router",
68
+ canMatchRoute,
69
+ ariaHref: createPath(resolvedPath),
70
+ to: href,
71
+ Link: fallbackRoutingIntegration.Link,
72
+ routerOptions: navigateOptions,
73
+ routerLinkOptions,
74
+ navigateWithFullOptions
75
+ };
76
+ }
77
+ function handleRouterLinkClick(event, navigation) {
78
+ const navigateWithFullOptions = navigation.type === "router" ? navigation.navigateWithFullOptions : void 0;
79
+ if (!navigateWithFullOptions || event.defaultPrevented || event.button !== 0 || event.currentTarget.target && event.currentTarget.target !== "_self" || event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) {
80
+ return;
81
+ }
82
+ event.preventDefault();
83
+ navigateWithFullOptions();
84
+ }
85
+ function getReactAriaAnchorProps(navigation, props) {
86
+ switch (navigation.type) {
87
+ case "none":
88
+ return {
89
+ href: props.href,
90
+ routerOptions: props.routerOptions,
91
+ render: void 0
92
+ };
93
+ case "router":
94
+ return {
95
+ href: navigation.ariaHref,
96
+ routerOptions: navigation.routerOptions,
97
+ render: (domProps) => {
98
+ const {
99
+ href: _href,
100
+ onClick: reactAriaOnClick,
101
+ ...routerLinkProps
102
+ } = domProps;
103
+ const onClick = navigation.navigateWithFullOptions ? (event) => {
104
+ reactAriaOnClick?.(event);
105
+ handleRouterLinkClick(event, navigation);
106
+ } : reactAriaOnClick;
107
+ return /* @__PURE__ */ jsx(
108
+ navigation.Link,
109
+ {
110
+ ...routerLinkProps,
111
+ ...navigation.routerLinkOptions,
112
+ ...onClick ? { onClick } : {},
113
+ to: navigation.to
114
+ }
115
+ );
116
+ }
117
+ };
118
+ case "native":
119
+ return {
120
+ href: navigation.ariaHref,
121
+ routerOptions: props.routerOptions,
122
+ // React Aria classifies the raw href, while the final DOM anchor needs
123
+ // the router-resolved href so a basename is applied exactly once.
124
+ render: (domProps) => /* @__PURE__ */ jsx("a", { ...domProps, href: navigation.browserHref })
125
+ };
126
+ }
127
+ }
128
+
129
+ export { getReactAriaAnchorProps, handleRouterLinkClick, isBrowserOwnedHref, isNativeNavigation, useAnchorNavigation };
130
+ //# sourceMappingURL=useNavigation.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useNavigation.esm.js","sources":["../../src/navigation/useNavigation.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 {\n useCallback,\n useMemo,\n type HTMLAttributeAnchorTarget,\n type JSX,\n type MouseEvent,\n type ReactElement,\n} from 'react';\nimport { createPath, type NavigateOptions } from 'react-router-dom';\nimport { isBrowserOwnedHref } from '../utils/linkUtils';\nimport type { BUIRoutingIntegration } from './types';\nimport {\n fallbackRoutingIntegration,\n useRoutingIntegration,\n} from './useRouting';\n\n/** @internal */\nexport type NavigationProps = {\n href?: string;\n target?: HTMLAttributeAnchorTarget;\n download?: boolean | string;\n routerOptions?: NavigateOptions;\n};\n\n/** @internal */\nexport type AnchorNavigation = { canMatchRoute: boolean } & (\n | { type: 'none' }\n | { type: 'native'; ariaHref: string; browserHref: string }\n | {\n type: 'router';\n ariaHref: string;\n to: string;\n Link: BUIRoutingIntegration['Link'];\n routerOptions?: NavigateOptions;\n routerLinkOptions: Omit<NavigateOptions, 'flushSync'> | undefined;\n navigateWithFullOptions?: () => void;\n }\n);\n\ntype PolymorphicRenderFunction = (...args: any[]) => ReactElement;\n\nexport { isBrowserOwnedHref } from '../utils/linkUtils';\n\n/** @internal */\nexport function isNativeNavigation(props: NavigationProps): boolean {\n const { href, target, download } = props;\n return Boolean(\n (href !== undefined && isBrowserOwnedHref(href)) ||\n (target && target !== '_self') ||\n (download !== undefined && download !== false),\n );\n}\n\n/**\n * A BUI component may come from a plugin that was built and released\n * independently from the application. We therefore cannot assume that the\n * component and the application's BUIProvider use the same BUI version or the\n * same installed copy of React Aria.\n *\n * These are two separate compatibility concerns:\n *\n * 1. BUIProvider version. A provider with routing support supplies the\n * application's React Router hooks and Link component. An older provider\n * does not, so the component falls back to directly imported React Router\n * APIs. This fallback can be removed when providers without routing support\n * are no longer supported.\n *\n * 2. React Aria package instance. React context is only visible to components\n * using the same React Aria copy. A plugin using another copy cannot see the\n * application's React Aria RouterProvider.\n *\n * Internal links render a React Router Link, either directly or through React\n * Aria's render prop. In both cases, React Aria's generated props, including its\n * click handler, are passed to that Link. React Router runs this supplied click\n * handler before its own navigation handler.\n *\n * - If the component can see the application's React Aria RouterProvider,\n * React Aria prevents the click's default behavior and delegates navigation\n * through that provider. React Router sees the prevented event and does not\n * navigate a second time.\n * - If the component uses a separate React Aria copy, it cannot see that\n * provider. React Aria leaves the event unhandled, so the rendered React\n * Router Link performs the navigation.\n *\n * Navigation options are provided to both paths so they behave consistently.\n * React Router Link does not support `flushSync`, so when that option is\n * enabled, BUI calls `navigate` directly if React Aria did not already handle\n * an ordinary click. Modified clicks and other browser-owned activation still\n * reach the rendered anchor unchanged.\n *\n * This dual click handling exists for compatibility with older BUI components\n * and older BUIProvider implementations. Once supported applications and\n * plugins are required to use both a routing-capable BUIProvider and BUI\n * components that render React Router links, the React Aria RouterProvider and\n * its delegated-navigation path can be removed. The rendered React Router Link\n * can then become the only client-side navigation path for anchors.\n *\n * After selecting the routing integration, this hook determines who should\n * handle the destination. React Router handles ordinary internal links. The\n * browser handles external URLs, custom URL schemes, downloads, and links with\n * a non-self target. Route matching is tracked separately: an internal tab can\n * still match the current route even when its activation is browser-owned. A\n * missing href does not navigate.\n *\n * @internal\n */\nexport function useAnchorNavigation(props: NavigationProps): AnchorNavigation {\n const { href, routerOptions: navigateOptions } = props;\n const routingIntegration = useRoutingIntegration();\n const routing = routingIntegration ?? fallbackRoutingIntegration;\n const navigate = routing.useNavigate();\n // React hooks must be called in the same order on every render. Missing hrefs,\n // external URLs, and custom URL schemes do not need route resolution, so \".\"\n // is used as a harmless placeholder when calling the router hooks.\n const routerHref =\n href !== undefined && !isBrowserOwnedHref(href) ? href : '.';\n const relativeOptions = { relative: navigateOptions?.relative };\n const resolvedPath = routing.useResolvedPath(routerHref, relativeOptions);\n const resolvedHref = routing.useHref(routerHref, relativeOptions);\n const navigateWithOptions = useCallback(() => {\n if (href !== undefined) {\n navigate(href, navigateOptions);\n }\n }, [navigate, href, navigateOptions]);\n const delegatedRouterOptions = useMemo(() => {\n if (!routingIntegration || href === undefined) {\n return undefined;\n }\n return routingIntegration.createRouterOptions(\n navigateWithOptions,\n navigateOptions,\n );\n }, [routingIntegration, navigateWithOptions, href, navigateOptions]);\n let routerLinkOptions: Omit<NavigateOptions, 'flushSync'> | undefined;\n if (navigateOptions) {\n // React Aria accepts all NavigateOptions when it delegates through its\n // RouterProvider. React Router Link accepts the same options except\n // flushSync, so omit that option from the props passed to the rendered Link.\n const { flushSync: _flushSync, ...supportedOptions } = navigateOptions;\n routerLinkOptions = supportedOptions;\n }\n const navigateWithFullOptions =\n navigateOptions?.flushSync === true ? navigateWithOptions : undefined;\n\n if (href === undefined) {\n return { type: 'none', canMatchRoute: false };\n }\n const canMatchRoute = !isBrowserOwnedHref(href);\n if (isNativeNavigation(props)) {\n return {\n type: 'native',\n canMatchRoute,\n ariaHref: href,\n browserHref: isBrowserOwnedHref(href) ? href : resolvedHref,\n };\n }\n if (routingIntegration) {\n return {\n type: 'router',\n canMatchRoute,\n ariaHref: href,\n to: href,\n Link: routingIntegration.Link,\n routerOptions: delegatedRouterOptions,\n routerLinkOptions,\n navigateWithFullOptions,\n };\n }\n return {\n type: 'router',\n canMatchRoute,\n ariaHref: createPath(resolvedPath),\n to: href,\n Link: fallbackRoutingIntegration.Link,\n routerOptions: navigateOptions,\n routerLinkOptions,\n navigateWithFullOptions,\n };\n}\n\n/**\n * Completes an eligible click with the one navigation option that React Router\n * Link cannot represent. Callers invoke this after React Aria's click handler.\n *\n * @internal\n */\nexport function handleRouterLinkClick(\n event: MouseEvent<HTMLAnchorElement>,\n navigation: AnchorNavigation,\n) {\n const navigateWithFullOptions =\n navigation.type === 'router'\n ? navigation.navigateWithFullOptions\n : undefined;\n if (\n !navigateWithFullOptions ||\n event.defaultPrevented ||\n event.button !== 0 ||\n (event.currentTarget.target && event.currentTarget.target !== '_self') ||\n event.metaKey ||\n event.altKey ||\n event.ctrlKey ||\n event.shiftKey\n ) {\n return;\n }\n\n event.preventDefault();\n navigateWithFullOptions();\n}\n\n/**\n * Converts the selected navigation behavior into props understood by React\n * Aria.\n *\n * Internal destinations are rendered as the selected React Router Link.\n * `routerOptions` are used when React Aria delegates the click through its\n * RouterProvider. The equivalent Link props are also applied to the rendered\n * React Router Link for components that cannot see that provider.\n *\n * Destinations handled by the browser are rendered as plain anchors. Their\n * final href includes any React Router basename exactly once.\n *\n * @internal\n */\nexport function getReactAriaAnchorProps(\n navigation: AnchorNavigation,\n props: {\n href?: string;\n routerOptions?: NavigateOptions;\n },\n): {\n href?: string;\n routerOptions?: NavigateOptions;\n render?: PolymorphicRenderFunction;\n} {\n switch (navigation.type) {\n case 'none':\n return {\n href: props.href,\n routerOptions: props.routerOptions,\n render: undefined,\n };\n case 'router':\n return {\n href: navigation.ariaHref,\n routerOptions: navigation.routerOptions,\n render: (domProps: JSX.IntrinsicElements['a']) => {\n const {\n href: _href,\n onClick: reactAriaOnClick,\n ...routerLinkProps\n } = domProps;\n const onClick = navigation.navigateWithFullOptions\n ? (event: MouseEvent<HTMLAnchorElement>) => {\n reactAriaOnClick?.(event);\n handleRouterLinkClick(event, navigation);\n }\n : reactAriaOnClick;\n return (\n <navigation.Link\n {...routerLinkProps}\n {...navigation.routerLinkOptions}\n {...(onClick ? { onClick } : {})}\n to={navigation.to}\n />\n );\n },\n };\n case 'native':\n return {\n href: navigation.ariaHref,\n routerOptions: props.routerOptions,\n // React Aria classifies the raw href, while the final DOM anchor needs\n // the router-resolved href so a basename is applied exactly once.\n render: (domProps: JSX.IntrinsicElements['a']) => (\n <a {...domProps} href={navigation.browserHref} />\n ),\n };\n }\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ import { useVersionedContext } from '@backstage/version-bridge';
2
+ import { useResolvedPath, useNavigate, useLocation, useInRouterContext, useHref, Link } from 'react-router-dom';
3
+
4
+ function useRoutingIntegration(options) {
5
+ const routing = useVersionedContext("bui")?.atVersion(2)?.routing;
6
+ if (routing) {
7
+ return routing;
8
+ }
9
+ if (options?.fallback) {
10
+ return fallbackRoutingIntegration;
11
+ }
12
+ return void 0;
13
+ }
14
+ const fallbackRoutingIntegration = {
15
+ Link,
16
+ useHref,
17
+ useInRouterContext,
18
+ useLocation,
19
+ useNavigate,
20
+ useResolvedPath,
21
+ createRouterOptions(_action, options) {
22
+ return { ...options };
23
+ }
24
+ };
25
+
26
+ export { fallbackRoutingIntegration, useRoutingIntegration };
27
+ //# sourceMappingURL=useRouting.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useRouting.esm.js","sources":["../../src/navigation/useRouting.ts"],"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 { useVersionedContext } from '@backstage/version-bridge';\nimport {\n Link,\n useHref,\n useInRouterContext,\n useLocation,\n useNavigate,\n useResolvedPath,\n} from 'react-router-dom';\nimport type { BUIContextVersions } from '../provider/BUIContext';\nimport type { BUIRoutingIntegration } from './types';\n\n/** @internal */\nexport function useRoutingIntegration(): BUIRoutingIntegration | undefined;\n/** @internal */\nexport function useRoutingIntegration(options: {\n fallback: true;\n}): BUIRoutingIntegration;\n/** @internal */\nexport function useRoutingIntegration(options?: {\n fallback?: true;\n}): BUIRoutingIntegration | undefined {\n const routing =\n useVersionedContext<BUIContextVersions>('bui')?.atVersion(2)?.routing;\n if (routing) {\n return routing;\n }\n if (options?.fallback) {\n return fallbackRoutingIntegration;\n }\n return undefined;\n}\n\n/** @internal */\nexport const fallbackRoutingIntegration: BUIRoutingIntegration = {\n Link,\n useHref,\n useInRouterContext,\n useLocation,\n useNavigate,\n useResolvedPath,\n createRouterOptions(_action, options) {\n return { ...options };\n },\n};\n"],"names":[],"mappings":";;;AAmCO,SAAS,sBAAsB,OAAA,EAEA;AACpC,EAAA,MAAM,UACJ,mBAAA,CAAwC,KAAK,CAAA,EAAG,SAAA,CAAU,CAAC,CAAA,EAAG,OAAA;AAChE,EAAA,IAAI,OAAA,EAAS;AACX,IAAA,OAAO,OAAA;AAAA,EACT;AACA,EAAA,IAAI,SAAS,QAAA,EAAU;AACrB,IAAA,OAAO,0BAAA;AAAA,EACT;AACA,EAAA,OAAO,MAAA;AACT;AAGO,MAAM,0BAAA,GAAoD;AAAA,EAC/D,IAAA;AAAA,EACA,OAAA;AAAA,EACA,kBAAA;AAAA,EACA,WAAA;AAAA,EACA,WAAA;AAAA,EACA,eAAA;AAAA,EACA,mBAAA,CAAoB,SAAS,OAAA,EAAS;AACpC,IAAA,OAAO,EAAE,GAAG,OAAA,EAAQ;AAAA,EACtB;AACF;;;;"}
@@ -0,0 +1,6 @@
1
+ import { createVersionedContext } from '@backstage/version-bridge';
2
+
3
+ const BUIContext = createVersionedContext("bui");
4
+
5
+ export { BUIContext };
6
+ //# sourceMappingURL=BUIContext.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BUIContext.esm.js","sources":["../../src/provider/BUIContext.ts"],"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 { createVersionedContext } from '@backstage/version-bridge';\nimport type { UseAnalyticsFn } from '../analytics/types';\nimport type { BUIRoutingIntegration } from '../navigation/types';\n\n/** @internal */\nexport type BUIContextValueV1 = {\n useAnalytics?: UseAnalyticsFn;\n};\n\n/** @internal */\nexport type BUIContextValueV2 = {\n useAnalytics?: UseAnalyticsFn;\n routing: BUIRoutingIntegration;\n};\n\n/** @internal */\nexport type BUIContextVersions = {\n 1: BUIContextValueV1;\n 2: BUIContextValueV2;\n};\n\n/** @internal */\nexport const BUIContext = createVersionedContext<BUIContextVersions>('bui');\n"],"names":[],"mappings":";;AAsCO,MAAM,UAAA,GAAa,uBAA2C,KAAK;;;;"}
@@ -1,28 +1,20 @@
1
1
  import { jsx } from 'react/jsx-runtime';
2
2
  import { useMemo } from 'react';
3
- import { RouterProvider } from 'react-aria-components';
4
- import { useInRouterContext, useNavigate } from 'react-router-dom';
5
3
  import { createVersionedValueMap } from '@backstage/version-bridge';
6
- import { BUIContext } from '../analytics/useAnalytics.esm.js';
7
- import { useResolvedHref } from '../hooks/useResolvedHref.esm.js';
4
+ import { BUIContext } from './BUIContext.esm.js';
5
+ import { buiRoutingIntegration, BUIRoutingProvider } from '../navigation/BUIRoutingProvider.esm.js';
8
6
 
9
7
  function BUIProvider(props) {
10
8
  const { useAnalytics, children } = props;
11
9
  const value = useMemo(
12
10
  () => createVersionedValueMap({
13
- 1: { useAnalytics }
11
+ 1: { useAnalytics },
12
+ 2: { useAnalytics, routing: buiRoutingIntegration }
14
13
  }),
15
14
  [useAnalytics]
16
15
  );
17
16
  const content = /* @__PURE__ */ jsx(BUIContext.Provider, { value, children });
18
- if (useInRouterContext()) {
19
- return /* @__PURE__ */ jsx(RoutedContent, { children: content });
20
- }
21
- return content;
22
- }
23
- function RoutedContent({ children }) {
24
- const navigate = useNavigate();
25
- return /* @__PURE__ */ jsx(RouterProvider, { navigate, useHref: useResolvedHref, children });
17
+ return /* @__PURE__ */ jsx(BUIRoutingProvider, { children: content });
26
18
  }
27
19
 
28
20
  export { BUIProvider };
@@ -1 +1 @@
1
- {"version":3,"file":"BUIProvider.esm.js","sources":["../../src/provider/BUIProvider.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 { useMemo, type ReactNode } from 'react';\nimport { RouterProvider } from 'react-aria-components';\nimport { useInRouterContext, useNavigate } from 'react-router-dom';\nimport { createVersionedValueMap } from '@backstage/version-bridge';\nimport { BUIContext } from '../analytics/useAnalytics';\nimport { useResolvedHref } from '../hooks/useResolvedHref';\nimport type { UseAnalyticsFn } from '../analytics/types';\n\n/** @public */\nexport type BUIProviderProps = {\n useAnalytics?: UseAnalyticsFn;\n children: ReactNode;\n};\n\n/**\n * Provides integration capabilities to all descendant BUI components.\n *\n * @example\n * ```tsx\n * import { BUIProvider } from '@backstage/ui';\n * import { useAnalytics as useBackstageAnalytics } from '@backstage/core-plugin-api';\n *\n * function App() {\n * return (\n * <BUIProvider useAnalytics={useBackstageAnalytics}>\n * <AppContent />\n * </BUIProvider>\n * );\n * }\n * ```\n *\n * @public\n */\nexport function BUIProvider(props: BUIProviderProps) {\n const { useAnalytics, children } = props;\n const value = useMemo(\n () =>\n createVersionedValueMap({\n 1: { useAnalytics },\n }),\n [useAnalytics],\n );\n\n const content = (\n <BUIContext.Provider value={value}>{children}</BUIContext.Provider>\n );\n\n if (useInRouterContext()) {\n return <RoutedContent>{content}</RoutedContent>;\n }\n\n return content;\n}\n\nfunction RoutedContent({ children }: { children: ReactNode }) {\n const navigate = useNavigate();\n return (\n <RouterProvider navigate={navigate} useHref={useResolvedHref}>\n {children}\n </RouterProvider>\n );\n}\n"],"names":[],"mappings":";;;;;;;;AAiDO,SAAS,YAAY,KAAA,EAAyB;AACnD,EAAA,MAAM,EAAE,YAAA,EAAc,QAAA,EAAS,GAAI,KAAA;AACnC,EAAA,MAAM,KAAA,GAAQ,OAAA;AAAA,IACZ,MACE,uBAAA,CAAwB;AAAA,MACtB,CAAA,EAAG,EAAE,YAAA;AAAa,KACnB,CAAA;AAAA,IACH,CAAC,YAAY;AAAA,GACf;AAEA,EAAA,MAAM,0BACJ,GAAA,CAAC,UAAA,CAAW,QAAA,EAAX,EAAoB,OAAe,QAAA,EAAS,CAAA;AAG/C,EAAA,IAAI,oBAAmB,EAAG;AACxB,IAAA,uBAAO,GAAA,CAAC,iBAAe,QAAA,EAAA,OAAA,EAAQ,CAAA;AAAA,EACjC;AAEA,EAAA,OAAO,OAAA;AACT;AAEA,SAAS,aAAA,CAAc,EAAE,QAAA,EAAS,EAA4B;AAC5D,EAAA,MAAM,WAAW,WAAA,EAAY;AAC7B,EAAA,uBACE,GAAA,CAAC,cAAA,EAAA,EAAe,QAAA,EAAoB,OAAA,EAAS,iBAC1C,QAAA,EACH,CAAA;AAEJ;;;;"}
1
+ {"version":3,"file":"BUIProvider.esm.js","sources":["../../src/provider/BUIProvider.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 { useMemo, type ReactNode } from 'react';\nimport { createVersionedValueMap } from '@backstage/version-bridge';\nimport { BUIContext } from './BUIContext';\nimport type { UseAnalyticsFn } from '../analytics/types';\nimport {\n BUIRoutingProvider,\n buiRoutingIntegration,\n} from '../navigation/BUIRoutingProvider';\n\n/** @public */\nexport type BUIProviderProps = {\n useAnalytics?: UseAnalyticsFn;\n children: ReactNode;\n};\n\n/**\n * Provides integration capabilities to all descendant BUI components.\n *\n * When rendered inside the Backstage app router, BUI components use\n * client-side navigation for internal links. Relative destinations resolve\n * from the route of the component that renders the link, and the router\n * basename applies once.\n *\n * External and scheme links, downloads, and links with non-self targets use the\n * browser's native navigation. BUI components rendered outside React Router use\n * native links without throwing.\n *\n * @example\n * ```tsx\n * import { BUIProvider } from '@backstage/ui';\n * import { useAnalytics as useBackstageAnalytics } from '@backstage/core-plugin-api';\n *\n * function App() {\n * return (\n * <BUIProvider useAnalytics={useBackstageAnalytics}>\n * <AppContent />\n * </BUIProvider>\n * );\n * }\n * ```\n *\n * @public\n */\nexport function BUIProvider(props: BUIProviderProps) {\n const { useAnalytics, children } = props;\n const value = useMemo(\n () =>\n createVersionedValueMap({\n 1: { useAnalytics },\n 2: { useAnalytics, routing: buiRoutingIntegration },\n }),\n [useAnalytics],\n );\n\n const content = (\n <BUIContext.Provider value={value}>{children}</BUIContext.Provider>\n );\n return <BUIRoutingProvider>{content}</BUIRoutingProvider>;\n}\n"],"names":[],"mappings":";;;;;;AA2DO,SAAS,YAAY,KAAA,EAAyB;AACnD,EAAA,MAAM,EAAE,YAAA,EAAc,QAAA,EAAS,GAAI,KAAA;AACnC,EAAA,MAAM,KAAA,GAAQ,OAAA;AAAA,IACZ,MACE,uBAAA,CAAwB;AAAA,MACtB,CAAA,EAAG,EAAE,YAAA,EAAa;AAAA,MAClB,CAAA,EAAG,EAAE,YAAA,EAAc,OAAA,EAAS,qBAAA;AAAsB,KACnD,CAAA;AAAA,IACH,CAAC,YAAY;AAAA,GACf;AAEA,EAAA,MAAM,0BACJ,GAAA,CAAC,UAAA,CAAW,QAAA,EAAX,EAAoB,OAAe,QAAA,EAAS,CAAA;AAE/C,EAAA,uBAAO,GAAA,CAAC,sBAAoB,QAAA,EAAA,OAAA,EAAQ,CAAA;AACtC;;;;"}
@@ -1,19 +1,13 @@
1
+ const ABSOLUTE_DESTINATION = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
2
+ function isBrowserOwnedHref(href) {
3
+ return ABSOLUTE_DESTINATION.test(href);
4
+ }
1
5
  function isExternalLink(href) {
2
- if (!href) return false;
3
- if (href.startsWith("http://") || href.startsWith("https://")) {
4
- return true;
5
- }
6
- if (href.startsWith("//")) {
7
- return true;
8
- }
9
- if (href.startsWith("mailto:") || href.startsWith("tel:")) {
10
- return true;
11
- }
12
- return false;
6
+ return href ? isBrowserOwnedHref(href) : false;
13
7
  }
14
8
  function isInternalLink(href) {
15
9
  return !!href && !isExternalLink(href);
16
10
  }
17
11
 
18
- export { isExternalLink, isInternalLink };
12
+ export { isBrowserOwnedHref, isExternalLink, isInternalLink };
19
13
  //# sourceMappingURL=linkUtils.esm.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"linkUtils.esm.js","sources":["../../src/utils/linkUtils.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\n/**\n * Determines if a link is external.\n * @param href - The href of the link.\n * @returns True if the link is external, false otherwise.\n * @internal\n */\nexport function isExternalLink(href?: string): boolean {\n if (!href) return false;\n\n // Check if it's an absolute URL with protocol\n if (href.startsWith('http://') || href.startsWith('https://')) {\n return true;\n }\n\n // Check if it's a protocol-relative URL\n if (href.startsWith('//')) {\n return true;\n }\n\n // Check if it's a mailto: or tel: link\n if (href.startsWith('mailto:') || href.startsWith('tel:')) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Checks if an href is an internal link (not external and not empty).\n *\n * @internal\n */\nexport function isInternalLink(href: string | undefined): href is string {\n return !!href && !isExternalLink(href);\n}\n"],"names":[],"mappings":"AAsBO,SAAS,eAAe,IAAA,EAAwB;AACrD,EAAA,IAAI,CAAC,MAAM,OAAO,KAAA;AAGlB,EAAA,IAAI,KAAK,UAAA,CAAW,SAAS,KAAK,IAAA,CAAK,UAAA,CAAW,UAAU,CAAA,EAAG;AAC7D,IAAA,OAAO,IAAA;AAAA,EACT;AAGA,EAAA,IAAI,IAAA,CAAK,UAAA,CAAW,IAAI,CAAA,EAAG;AACzB,IAAA,OAAO,IAAA;AAAA,EACT;AAGA,EAAA,IAAI,KAAK,UAAA,CAAW,SAAS,KAAK,IAAA,CAAK,UAAA,CAAW,MAAM,CAAA,EAAG;AACzD,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,OAAO,KAAA;AACT;AAOO,SAAS,eAAe,IAAA,EAA0C;AACvE,EAAA,OAAO,CAAC,CAAC,IAAA,IAAQ,CAAC,eAAe,IAAI,CAAA;AACvC;;;;"}
1
+ {"version":3,"file":"linkUtils.esm.js","sources":["../../src/utils/linkUtils.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\nconst ABSOLUTE_DESTINATION = /^(?:[a-z][a-z0-9+.-]*:|\\/\\/)/i;\n\n/** @internal */\nexport function isBrowserOwnedHref(href: string): boolean {\n return ABSOLUTE_DESTINATION.test(href);\n}\n\n/**\n * Determines if a link is external.\n * @param href - The href of the link.\n * @returns True if the link is external, false otherwise.\n * @internal\n */\nexport function isExternalLink(href?: string): boolean {\n return href ? isBrowserOwnedHref(href) : false;\n}\n\n/**\n * Checks if an href is an internal link (not external and not empty).\n *\n * @internal\n */\nexport function isInternalLink(href: string | undefined): href is string {\n return !!href && !isExternalLink(href);\n}\n"],"names":[],"mappings":"AAgBA,MAAM,oBAAA,GAAuB,+BAAA;AAGtB,SAAS,mBAAmB,IAAA,EAAuB;AACxD,EAAA,OAAO,oBAAA,CAAqB,KAAK,IAAI,CAAA;AACvC;AAQO,SAAS,eAAe,IAAA,EAAwB;AACrD,EAAA,OAAO,IAAA,GAAO,kBAAA,CAAmB,IAAI,CAAA,GAAI,KAAA;AAC3C;AAOO,SAAS,eAAe,IAAA,EAA0C;AACvE,EAAA,OAAO,CAAC,CAAC,IAAA,IAAQ,CAAC,eAAe,IAAI,CAAA;AACvC;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backstage/ui",
3
- "version": "0.17.2-next.0",
3
+ "version": "0.18.0-next.1",
4
4
  "backstage": {
5
5
  "role": "web-library"
6
6
  },
@@ -64,8 +64,9 @@
64
64
  },
65
65
  "devDependencies": {
66
66
  "@backstage/cli": "0.36.6-next.0",
67
- "@backstage/core-components": "0.18.14-next.0",
67
+ "@backstage/core-components": "0.18.14-next.1",
68
68
  "@storybook/react-vite": "^10.4.0",
69
+ "@testing-library/dom": "^10.0.0",
69
70
  "@testing-library/jest-dom": "^6.0.0",
70
71
  "@testing-library/react": "^16.0.0",
71
72
  "@types/react": "^18.0.0",