@astryxdesign/core 0.6.3-canary.bcf01fe → 0.6.3-canary.c13ef6b

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 (59) hide show
  1. package/dist/Citation/Citation.d.ts +1 -1
  2. package/dist/Citation/Citation.d.ts.map +1 -1
  3. package/dist/Citation/Citation.js +3 -2
  4. package/dist/Link/useLinkComponent.d.ts +5 -15
  5. package/dist/Link/useLinkComponent.d.ts.map +1 -1
  6. package/dist/Link/useLinkComponent.js +34 -51
  7. package/dist/Markdown/Markdown.js +2 -2
  8. package/dist/Markdown/url.d.ts +2 -0
  9. package/dist/Markdown/url.d.ts.map +1 -1
  10. package/dist/Markdown/url.js +8 -2
  11. package/dist/TopNav/TopNavMenu.d.ts +1 -1
  12. package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
  13. package/dist/TopNav/TopNavMenu.js +3 -2
  14. package/dist/hooks/useClickableContainer.d.ts +10 -1
  15. package/dist/hooks/useClickableContainer.d.ts.map +1 -1
  16. package/dist/hooks/useClickableContainer.js +15 -4
  17. package/dist/theme/declarationBoundary.d.ts +17 -0
  18. package/dist/theme/declarationBoundary.d.ts.map +1 -0
  19. package/dist/theme/declarationBoundary.js +343 -0
  20. package/dist/theme/generateThemeRules.d.ts +13 -7
  21. package/dist/theme/generateThemeRules.d.ts.map +1 -1
  22. package/dist/theme/generateThemeRules.js +131 -35
  23. package/dist/utils/safeUrl.d.ts +11 -0
  24. package/dist/utils/safeUrl.d.ts.map +1 -0
  25. package/dist/utils/safeUrl.js +78 -0
  26. package/package.json +3 -3
  27. package/src/Button/Button.doc.mjs +1 -1
  28. package/src/Citation/Citation.doc.mjs +2 -2
  29. package/src/Citation/Citation.test.tsx +17 -0
  30. package/src/Citation/Citation.tsx +4 -2
  31. package/src/ClickableCard/ClickableCard.doc.mjs +2 -2
  32. package/src/Item/Item.doc.mjs +2 -2
  33. package/src/Link/Link.doc.mjs +6 -4
  34. package/src/Link/LinkProvider.doc.mjs +2 -2
  35. package/src/Link/__tests__/Link.navigation.a11y.chromium.spec.ts +261 -0
  36. package/src/Link/useLinkComponent.test.tsx +333 -0
  37. package/src/Link/useLinkComponent.ts +29 -57
  38. package/src/List/ListItem.doc.mjs +2 -2
  39. package/src/Markdown/Markdown.doc.mjs +2 -2
  40. package/src/Markdown/Markdown.renderBoundary.test.tsx +72 -0
  41. package/src/Markdown/Markdown.spec.md +19 -15
  42. package/src/Markdown/Markdown.test.tsx +52 -0
  43. package/src/Markdown/Markdown.tsx +2 -2
  44. package/src/Markdown/parser.test.ts +20 -0
  45. package/src/Markdown/url.ts +9 -3
  46. package/src/Token/Token.doc.mjs +2 -2
  47. package/src/TopNav/TopNavMenu.doc.mjs +2 -2
  48. package/src/TopNav/TopNavMenu.test.tsx +30 -0
  49. package/src/TopNav/TopNavMenu.tsx +7 -2
  50. package/src/hooks/useClickableContainer.test.tsx +236 -0
  51. package/src/hooks/useClickableContainer.ts +20 -4
  52. package/src/theme/Theme.test.tsx +32 -0
  53. package/src/theme/declarationBoundary.test.ts +126 -0
  54. package/src/theme/declarationBoundary.ts +347 -0
  55. package/src/theme/generateThemeRules.test.ts +470 -2
  56. package/src/theme/generateThemeRules.ts +219 -51
  57. package/src/theme/tokenValueCompat.test.ts +4 -1
  58. package/src/utils/safeUrl.test.ts +88 -0
  59. package/src/utils/safeUrl.ts +85 -0
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @file Citation.tsx
3
- * @input Uses React, StyleX, theme tokens
3
+ * @input Uses React, StyleX, theme tokens, and the shared navigation policy
4
4
  * @output Exports Citation component for inline citation references
5
5
  * @position Core implementation; consumed by index.ts
6
6
  *
@@ -1 +1 @@
1
- {"version":3,"file":"Citation.d.ts","sourceRoot":"","sources":["../../src/Citation/Citation.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,OAAO,CAAC;AAarC,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAK5C,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;;;;;;OASG;IACH,IAAI,CAAC,EAAE,SAAS,CAAC;CAClB;AAED,MAAM,WAAW,aAAc,SAAQ,SAAS,CAAC,WAAW,CAAC;IAC3D,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IAC7B,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;CAC9B;AAiHD,wBAAgB,QAAQ,CAAC,EACvB,GAAG,EACH,MAAM,EACN,MAAM,EACN,OAAiB,EACjB,MAAM,EACN,SAAS,EACT,KAAK,EACL,aAAa,EAAE,MAAM,EACrB,GAAG,IAAI,EACR,EAAE,aAAa,GAAG,KAAK,CAAC,YAAY,CA2FpC;yBArGe,QAAQ"}
1
+ {"version":3,"file":"Citation.d.ts","sourceRoot":"","sources":["../../src/Citation/Citation.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,OAAO,CAAC;AAcrC,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAK5C,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;;;;;;OASG;IACH,IAAI,CAAC,EAAE,SAAS,CAAC;CAClB;AAED,MAAM,WAAW,aAAc,SAAQ,SAAS,CAAC,WAAW,CAAC;IAC3D,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IAC7B,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;CAC9B;AAiHD,wBAAgB,QAAQ,CAAC,EACvB,GAAG,EACH,MAAM,EACN,MAAM,EACN,OAAiB,EACjB,MAAM,EACN,SAAS,EACT,KAAK,EACL,aAAa,EAAE,MAAM,EACrB,GAAG,IAAI,EACR,EAAE,aAAa,GAAG,KAAK,CAAC,YAAY,CA4FpC;yBAtGe,QAAQ"}
@@ -4,7 +4,7 @@
4
4
 
5
5
  /**
6
6
  * @file Citation.tsx
7
- * @input Uses React, StyleX, theme tokens
7
+ * @input Uses React, StyleX, theme tokens, and the shared navigation policy
8
8
  * @output Exports Citation component for inline citation references
9
9
  * @position Core implementation; consumed by index.ts
10
10
  *
@@ -19,6 +19,7 @@ import * as stylex from '@stylexjs/stylex';
19
19
  import "../theme/tokens.stylex.js";
20
20
  import { colorVars, spacingVars, radiusVars, typeScaleVars, fontWeightVars, borderVars, durationVars, easeVars } from "../theme/tokens.stylex.js";
21
21
  import { mergeProps } from "../utils/index.js";
22
+ import { isSafeUrl } from "../utils/safeUrl.js";
22
23
  import { themeProps } from "../utils/themeProps.js";
23
24
  import { useTranslator } from "../i18n/index.js";
24
25
  import { renderIconSlot } from "../Icon/index.js";
@@ -103,7 +104,7 @@ export function Citation({
103
104
  }) {
104
105
  const t = useTranslator();
105
106
  const title = source.title ?? String(number);
106
- const href = source.url;
107
+ const href = source.url != null && isSafeUrl(source.url) ? source.url : undefined;
107
108
 
108
109
  // Resolve the source icon. A non-string `icon` node renders as-is (an Astryx
109
110
  // <Icon>, SVG, avatar, etc.). Otherwise fall back to an image URL: `src`, or
@@ -1,23 +1,13 @@
1
1
  import type { LinkComponentType } from './types';
2
2
  /**
3
- * Resolves the link component to use.
4
- *
5
- * Priority: `as` prop > `LinkProvider` context > native `<a>`.
6
- *
7
- * When the resolved component is a custom component (not the native `<a>`),
8
- * it is wrapped to receive both `href` and `to` props set to the same value.
9
- * This allows `to`-based routers (React Router, TanStack Router) to work
10
- * out of the box without a manual adapter.
11
- *
12
- * @param as - Per-component override. If provided, takes highest priority.
13
- * @returns The resolved link component (with `to` injection for custom components).
3
+ * Resolve `as`, then LinkProvider, then a native anchor. Both destination props
4
+ * are checked before rendering; accepted structured values retain identity.
5
+ * Custom components receive `to={href}` unless an explicit `to` is supplied.
14
6
  *
15
7
  * @example
16
8
  * ```
17
- * function MyComponent({ as }: { as?: LinkComponentType }) {
18
- * const LinkComponent = useLinkComponent(as);
19
- * return <LinkComponent href="/foo">Click me</LinkComponent>;
20
- * }
9
+ * const LinkComponent = useLinkComponent(as);
10
+ * return <LinkComponent href="/docs">Documentation</LinkComponent>;
21
11
  * ```
22
12
  */
23
13
  export declare function useLinkComponent(as?: LinkComponentType): LinkComponentType;
@@ -1 +1 @@
1
- {"version":3,"file":"useLinkComponent.d.ts","sourceRoot":"","sources":["../../src/Link/useLinkComponent.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,SAAS,CAAC;AAgC/C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,gBAAgB,CAC9B,EAAE,CAAC,EAAE,iBAAiB,GACrB,iBAAiB,CAYnB"}
1
+ {"version":3,"file":"useLinkComponent.d.ts","sourceRoot":"","sources":["../../src/Link/useLinkComponent.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,SAAS,CAAC;AA+B/C;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,CAAC,EAAE,iBAAiB,GAAG,iBAAiB,CAI1E"}
@@ -4,79 +4,62 @@
4
4
 
5
5
  /**
6
6
  * @file useLinkComponent.ts
7
- * @input React use, useMemo, createElement, forwardRef, LinkContext, LinkComponentType
8
- * @output Exports useLinkComponent hook
9
- * @position Hook for resolving the link component in Astryx components
7
+ * @input Per-component override, LinkProvider context, and navigation props
8
+ * @output Stable link component with destination checks and custom-router `to` injection
9
+ * @position Shared native and custom link boundary; rejected destinations render
10
+ * as destination-less anchors without invoking a router.
10
11
  *
11
- * Resolution order: per-component `as` prop > LinkProvider context > native `<a>`.
12
- *
13
- * When the resolved component is a custom component (not native `<a>`),
14
- * wraps it to pass `to={href}` alongside `href`. This enables compatibility
15
- * with routers that use `to` (React Router, TanStack Router)
16
- * without requiring an adapter component.
17
- *
18
- * SYNC: When modified, update these files to stay in sync:
19
- * - /packages/core/src/Link/index.ts
20
- * - /packages/core/src/Link/Link.doc.mjs
12
+ * SYNC: Link.doc.mjs and LinkProvider.doc.mjs describe this boundary.
21
13
  */
22
14
  import { use, useMemo, createElement } from 'react';
23
15
  import { LinkContext } from "./LinkContext.js";
24
- /**
25
- * Creates a wrapper component that passes both `href` and `to` props
26
- * to the underlying link component. This enables routers that use `to`
27
- * (React Router, TanStack Router) to work without an adapter.
28
- *
29
- * The wrapper is transparent: it forwards refs and all other props unchanged.
30
- * Native `<a>` elements ignore the unknown `to` prop harmlessly.
31
- */
32
- function createLinkWithTo(Component) {
33
- function LinkWithTo({
16
+ import { isSafeDestination } from "../utils/safeUrl.js";
17
+ function createSafeLink(Component) {
18
+ function SafeLink({
34
19
  href,
20
+ to,
35
21
  ref,
36
22
  ...rest
37
23
  }) {
24
+ // An undefined destination can crash a router or resolve to the current
25
+ // route. Reject the whole handoff, not just one prop or a fallback value.
26
+ if (!isSafeDestination(href) || !isSafeDestination(to)) {
27
+ return /*#__PURE__*/createElement('a', {
28
+ ref,
29
+ ...rest
30
+ });
31
+ }
32
+ if (Component === 'a') {
33
+ return /*#__PURE__*/createElement('a', {
34
+ ref,
35
+ ...rest,
36
+ href
37
+ });
38
+ }
38
39
  return /*#__PURE__*/createElement(Component, {
39
40
  ref,
41
+ ...rest,
40
42
  href,
41
- to: href,
42
- ...rest
43
+ to: to ?? href
43
44
  });
44
45
  }
45
- LinkWithTo.displayName = `LinkWithTo(${typeof Component === 'string' ? Component : Component.displayName || Component.name || 'Component'})`;
46
- return LinkWithTo;
46
+ SafeLink.displayName = `SafeLink(${typeof Component === 'string' ? Component : Component.displayName || Component.name || 'Component'})`;
47
+ return SafeLink;
47
48
  }
48
49
 
49
50
  /**
50
- * Resolves the link component to use.
51
- *
52
- * Priority: `as` prop > `LinkProvider` context > native `<a>`.
53
- *
54
- * When the resolved component is a custom component (not the native `<a>`),
55
- * it is wrapped to receive both `href` and `to` props set to the same value.
56
- * This allows `to`-based routers (React Router, TanStack Router) to work
57
- * out of the box without a manual adapter.
58
- *
59
- * @param as - Per-component override. If provided, takes highest priority.
60
- * @returns The resolved link component (with `to` injection for custom components).
51
+ * Resolve `as`, then LinkProvider, then a native anchor. Both destination props
52
+ * are checked before rendering; accepted structured values retain identity.
53
+ * Custom components receive `to={href}` unless an explicit `to` is supplied.
61
54
  *
62
55
  * @example
63
56
  * ```
64
- * function MyComponent({ as }: { as?: LinkComponentType }) {
65
- * const LinkComponent = useLinkComponent(as);
66
- * return <LinkComponent href="/foo">Click me</LinkComponent>;
67
- * }
57
+ * const LinkComponent = useLinkComponent(as);
58
+ * return <LinkComponent href="/docs">Documentation</LinkComponent>;
68
59
  * ```
69
60
  */
70
61
  export function useLinkComponent(as) {
71
62
  const ctx = use(LinkContext);
72
63
  const resolved = as ?? ctx?.component ?? 'a';
73
-
74
- // Memoize the wrapper to maintain referential stability.
75
- // The wrapper is only created for custom components (not native <a>).
76
- return useMemo(() => {
77
- if (resolved === 'a') {
78
- return 'a';
79
- }
80
- return createLinkWithTo(resolved);
81
- }, [resolved]);
64
+ return useMemo(() => createSafeLink(resolved), [resolved]);
82
65
  }
@@ -35,7 +35,7 @@ import { parseInlineAst, parseMarkdownAst, parseMarkdownAstIncremental, createIn
35
35
  import { getMarkdownAstLegacyCodeLanguage, markdownAstText } from "./ast.js";
36
36
  import { applyMarkdownTransforms, getMarkdownExtensionRenderer, markdownExtensionText, prepareMarkdownPlugins, reportMarkdownPluginFailure } from "./plugins/protocol.js";
37
37
  import { getMarkdownFenceProposal } from "./plugins/semanticFence.js";
38
- import { sanitizeMarkdownUrl } from "./url.js";
38
+ import { sanitizeMarkdownLinkUrl, sanitizeMarkdownUrl } from "./url.js";
39
39
  import { themeProps } from "../utils/themeProps.js";
40
40
  import { useTranslator } from "../i18n/index.js";
41
41
 
@@ -668,7 +668,7 @@ function renderInline(node, index, onLinkClick, cursor, citationCtx, linkCompone
668
668
  }
669
669
  case 'link':
670
670
  {
671
- const safeHref = sanitizeMarkdownUrl(node.url);
671
+ const safeHref = sanitizeMarkdownLinkUrl(node.url);
672
672
  if (safeHref == null) {
673
673
  // Unsafe URL — render as plain text
674
674
  return /*#__PURE__*/_jsx("span", {
@@ -1,4 +1,6 @@
1
1
  /** Preserve the released parser policy for no-plugin output compatibility. */
2
2
  export declare function isSafeMarkdownParserUrl(url: string): boolean;
3
+ /** Navigation and image rendering deliberately have different policies. */
4
+ export declare function sanitizeMarkdownLinkUrl(url: string): string | null;
3
5
  export declare function sanitizeMarkdownUrl(url: string): string | null;
4
6
  //# sourceMappingURL=url.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"url.d.ts","sourceRoot":"","sources":["../../src/Markdown/url.ts"],"names":[],"mappings":"AAmBA,8EAA8E;AAC9E,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAE5D;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK9D"}
1
+ {"version":3,"file":"url.d.ts","sourceRoot":"","sources":["../../src/Markdown/url.ts"],"names":[],"mappings":"AAmBA,8EAA8E;AAC9E,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAE5D;AAED,2EAA2E;AAC3E,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGlE;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK9D"}
@@ -7,7 +7,7 @@
7
7
  * @position Shared URL policy for parser, transforms, and renderer
8
8
  */
9
9
 
10
- const PARSER_DANGEROUS_URL_PATTERN = /^(?:javascript:|data:text\/html|vbscript:)/i;
10
+ import { isSafeUrl, sanitizeUrl } from "../utils/safeUrl.js";
11
11
  const RENDER_DANGEROUS_URL_PATTERN = /^(?:javascript:|data:|vbscript:)/i;
12
12
  function normalizeMarkdownUrl(url) {
13
13
  // Browsers ignore embedded controls in schemes, so normalize before checking.
@@ -17,7 +17,13 @@ function normalizeMarkdownUrl(url) {
17
17
 
18
18
  /** Preserve the released parser policy for no-plugin output compatibility. */
19
19
  export function isSafeMarkdownParserUrl(url) {
20
- return !PARSER_DANGEROUS_URL_PATTERN.test(normalizeMarkdownUrl(url));
20
+ return isSafeUrl(url);
21
+ }
22
+
23
+ /** Navigation and image rendering deliberately have different policies. */
24
+ export function sanitizeMarkdownLinkUrl(url) {
25
+ const normalized = sanitizeUrl(url);
26
+ return normalized === '' ? null : normalized;
21
27
  }
22
28
  export function sanitizeMarkdownUrl(url) {
23
29
  const normalized = normalizeMarkdownUrl(url);
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @file TopNavMenu.tsx
3
3
  * @input Uses React, StyleX, usePopover, useMenuHover, useListFocus,
4
- * useTypeahead, TopNavItem tokens
4
+ * useTypeahead, TopNavItem tokens, and the shared navigation policy
5
5
  * @output Exports TopNavMenu component and related types
6
6
  * @position Navigation item with hover-triggered overflow menu for TopNav
7
7
  *
@@ -1 +1 @@
1
- {"version":3,"file":"TopNavMenu.d.ts","sourceRoot":"","sources":["../../src/TopNav/TopNavMenu.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,EAKZ,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AASf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAqM5C;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;OAEG;IACH,IAAI,CAAC,EAAE,SAAS,CAAC;IAEjB;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB;AAMD,MAAM,WAAW,eAAgB,SAAQ,SAAS,CAAC,iBAAiB,CAAC;IACnE,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;IACnC;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,KAAK,EAAE,kBAAkB,EAAE,CAAC;IAE5B;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,wBAAgB,UAAU,CAAC,EACzB,GAAG,EACH,KAAK,EACL,KAAK,EACL,KAAW,EACX,SAAe,EACf,MAAM,EACN,SAAS,EACT,KAAK,EACL,OAAO,EAAE,WAAW,EACpB,YAAY,EAAE,gBAAgB,EAC9B,YAAY,EAAE,gBAAgB,EAC9B,GAAG,IAAI,EACR,EAAE,eAAe,4BAqQjB;yBAlRe,UAAU"}
1
+ {"version":3,"file":"TopNavMenu.d.ts","sourceRoot":"","sources":["../../src/TopNav/TopNavMenu.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,EAKZ,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AASf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAsM5C;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;OAEG;IACH,IAAI,CAAC,EAAE,SAAS,CAAC;IAEjB;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB;AAMD,MAAM,WAAW,eAAgB,SAAQ,SAAS,CAAC,iBAAiB,CAAC;IACnE,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;IACnC;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,KAAK,EAAE,kBAAkB,EAAE,CAAC;IAE5B;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,wBAAgB,UAAU,CAAC,EACzB,GAAG,EACH,KAAK,EACL,KAAK,EACL,KAAW,EACX,SAAe,EACf,MAAM,EACN,SAAS,EACT,KAAK,EACL,OAAO,EAAE,WAAW,EACpB,YAAY,EAAE,gBAAgB,EAC9B,YAAY,EAAE,gBAAgB,EAC9B,GAAG,IAAI,EACR,EAAE,eAAe,4BAyQjB;yBAtRe,UAAU"}
@@ -5,7 +5,7 @@
5
5
  /**
6
6
  * @file TopNavMenu.tsx
7
7
  * @input Uses React, StyleX, usePopover, useMenuHover, useListFocus,
8
- * useTypeahead, TopNavItem tokens
8
+ * useTypeahead, TopNavItem tokens, and the shared navigation policy
9
9
  * @output Exports TopNavMenu component and related types
10
10
  * @position Navigation item with hover-triggered overflow menu for TopNav
11
11
  *
@@ -29,6 +29,7 @@ import { useTopNavSlot } from "./TopNavContext.js";
29
29
  import { useTopNavRenderMode } from "./TopNavRenderContext.js";
30
30
  import { useAppShellMobile } from "../AppShell/AppShellMobileContext.js";
31
31
  import { useLinkComponent } from "../Link/useLinkComponent.js";
32
+ import { isSafeUrl } from "../utils/safeUrl.js";
32
33
  import { themeProps } from "../utils/themeProps.js";
33
34
  import { focusOutlineProps } from "../utils/focusOutline.stylex.js";
34
35
  import { interactionOverlayStyles } from "../utils/interactionOverlay.stylex.js";
@@ -378,7 +379,7 @@ export function TopNavMenu({
378
379
  // promotes exactly one item to 0.
379
380
  ,
380
381
  tabIndex: -1,
381
- href: item.href,
382
+ href: item.href != null && isSafeUrl(item.href) ? item.href : undefined,
382
383
  onClick: item.onClick,
383
384
  ...focusOutlineProps.focusVisible(styles.menuItem),
384
385
  children: [/*#__PURE__*/_jsx("div", {
@@ -7,6 +7,11 @@
7
7
  * Solves the "nested interactive elements" problem: when a card is clickable
8
8
  * but contains buttons/links, clicking those should NOT trigger the card's action.
9
9
  *
10
+ * Every navigation exit here (window.open, location.href, and the delegated
11
+ * click on the interactive ref) runs after the shared navigation-destination
12
+ * rule in utils/safeUrl.ts; a blocked href does not navigate by any
13
+ * activation method.
14
+ *
10
15
  * SYNC: When modified, update:
11
16
  * - /packages/core/src/hooks/index.ts (export)
12
17
  */
@@ -30,7 +35,11 @@ export interface UseClickableContainerOptions {
30
35
  interactiveRef?: RefObject<HTMLElement | null>;
31
36
  /** Click handler */
32
37
  onClick?: (event: MouseEvent<HTMLElement>) => void;
33
- /** Navigation URL — when provided, clicking the container navigates */
38
+ /**
39
+ * Navigation URL — when provided, clicking the container navigates. Checked
40
+ * with the shared navigation rule (utils/safeUrl.ts) before any activation
41
+ * method (plain, new-tab, Cmd/Ctrl-click, middle-click, delegated click).
42
+ */
34
43
  href?: string;
35
44
  /** Link target */
36
45
  target?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"useClickableContainer.d.ts","sourceRoot":"","sources":["../../src/hooks/useClickableContainer.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAyB,KAAK,SAAS,EAAE,KAAK,UAAU,EAAC,MAAM,OAAO,CAAC;AAE9E;;;;;;;;;;GAUG;AACH,eAAO,MAAM,qBAAqB,QAmBvB,CAAC;AAmCZ,MAAM,WAAW,4BAA4B;IAC3C,yCAAyC;IACzC,YAAY,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC5C,mEAAmE;IACnE,cAAc,CAAC,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC/C,oBAAoB;IACpB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,CAAC,WAAW,CAAC,KAAK,IAAI,CAAC;IACnD,uEAAuE;IACvE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kBAAkB;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wCAAwC;IACxC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,CAAC,KAAK,EAAE,UAAU,CAAC,WAAW,CAAC,KAAK,IAAI,CAAC;IAClD,SAAS,EAAE,CAAC,KAAK,EAAE,UAAU,CAAC,WAAW,CAAC,KAAK,IAAI,CAAC;CACrD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,qBAAqB,CAAC,EACpC,YAAY,EACZ,cAAc,EACd,OAAO,EAAE,WAAW,EACpB,IAAI,EACJ,MAAM,EACN,QAAgB,GACjB,EAAE,4BAA4B,GAAG,wBAAwB,CA6GzD"}
1
+ {"version":3,"file":"useClickableContainer.d.ts","sourceRoot":"","sources":["../../src/hooks/useClickableContainer.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAyB,KAAK,SAAS,EAAE,KAAK,UAAU,EAAC,MAAM,OAAO,CAAC;AAG9E;;;;;;;;;;GAUG;AACH,eAAO,MAAM,qBAAqB,QAmBvB,CAAC;AAmCZ,MAAM,WAAW,4BAA4B;IAC3C,yCAAyC;IACzC,YAAY,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC5C,mEAAmE;IACnE,cAAc,CAAC,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC/C,oBAAoB;IACpB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,CAAC,WAAW,CAAC,KAAK,IAAI,CAAC;IACnD;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kBAAkB;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wCAAwC;IACxC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,CAAC,KAAK,EAAE,UAAU,CAAC,WAAW,CAAC,KAAK,IAAI,CAAC;IAClD,SAAS,EAAE,CAAC,KAAK,EAAE,UAAU,CAAC,WAAW,CAAC,KAAK,IAAI,CAAC;CACrD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,qBAAqB,CAAC,EACpC,YAAY,EACZ,cAAc,EACd,OAAO,EAAE,WAAW,EACpB,IAAI,EACJ,MAAM,EACN,QAAgB,GACjB,EAAE,4BAA4B,GAAG,wBAAwB,CAmHzD"}
@@ -11,10 +11,16 @@
11
11
  * Solves the "nested interactive elements" problem: when a card is clickable
12
12
  * but contains buttons/links, clicking those should NOT trigger the card's action.
13
13
  *
14
+ * Every navigation exit here (window.open, location.href, and the delegated
15
+ * click on the interactive ref) runs after the shared navigation-destination
16
+ * rule in utils/safeUrl.ts; a blocked href does not navigate by any
17
+ * activation method.
18
+ *
14
19
  * SYNC: When modified, update:
15
20
  * - /packages/core/src/hooks/index.ts (export)
16
21
  */
17
22
  import { useCallback, useEffect } from 'react';
23
+ import { isSafeUrl } from "../utils/safeUrl.js";
18
24
 
19
25
  /**
20
26
  * Canonical list of interactive element selectors — native controls plus
@@ -128,8 +134,12 @@ export function useClickableContainer({
128
134
  return;
129
135
  }
130
136
 
131
- // Navigate if href is provided
132
- if (href != null) {
137
+ // Navigate if href is provided. React DOM vets the hrefs it writes as
138
+ // attributes; nothing below passes through React, so the shared
139
+ // navigation rule decides once, before target, modifier key, or the
140
+ // delegated anchor can select a sink. A blocked href simply does not
141
+ // navigate — the click handler above still ran.
142
+ if (href != null && isSafeUrl(href)) {
133
143
  const shouldOpenNewTab = target === '_blank' || event.ctrlKey || event.metaKey;
134
144
  if (shouldOpenNewTab) {
135
145
  window.open(href, '_blank', 'noopener');
@@ -171,9 +181,10 @@ export function useClickableContainer({
171
181
  return;
172
182
  }
173
183
 
174
- // Middle-click on href opens in new tab
184
+ // Middle-click on href opens in new tab (same scheme rule as onClick —
185
+ // this navigation never passes through React either).
175
186
  const isMiddleClick = event.button === 1;
176
- if (isMiddleClick && href != null && (eventTarget === event.currentTarget || !hasInteractiveAncestor(eventTarget, containerEl))) {
187
+ if (isMiddleClick && href != null && isSafeUrl(href) && (eventTarget === event.currentTarget || !hasInteractiveAncestor(eventTarget, containerEl))) {
177
188
  window.open(href, '_blank', 'noopener');
178
189
  }
179
190
  }, [containerRef, href, disabled]);
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Why `value` cannot be emitted as the value of one declaration, or `null`
3
+ * when it can. Valid CSS a browser keeps inside one declaration always
4
+ * returns `null`; see the file header for the exact rules.
5
+ */
6
+ export declare function checkDeclarationValue(input: string): string | null;
7
+ /**
8
+ * Why `name` cannot be emitted as a declaration's property name, or `null`
9
+ * when it can. A property name is one CSS ident sequence: a standard
10
+ * property (`color`), a vendor-prefixed one (`-webkit-line-clamp`), or a
11
+ * custom property (`--brand-accent`, `--_internal`). Escapes are allowed
12
+ * because an ident allows them. Whether the name means anything to the
13
+ * browser is not this check's concern; only that it cannot end the
14
+ * declaration or leak a second one.
15
+ */
16
+ export declare function checkDeclarationName(input: string): string | null;
17
+ //# sourceMappingURL=declarationBoundary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"declarationBoundary.d.ts","sourceRoot":"","sources":["../../src/theme/declarationBoundary.ts"],"names":[],"mappings":"AAgOA;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAgGlE;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUjE"}