@astryxdesign/core 0.6.3-canary.f22695a → 0.6.3

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 (118) hide show
  1. package/dist/Button/Button.d.ts.map +1 -1
  2. package/dist/Button/Button.js +28 -13
  3. package/dist/Chat/ChatLayout.d.ts +0 -19
  4. package/dist/Chat/ChatLayout.d.ts.map +1 -1
  5. package/dist/Chat/ChatLayout.js +0 -19
  6. package/dist/Chat/ChatLayoutScrollButton.d.ts.map +1 -1
  7. package/dist/Chat/ChatLayoutScrollButton.js +34 -44
  8. package/dist/Citation/Citation.d.ts +1 -1
  9. package/dist/Citation/Citation.d.ts.map +1 -1
  10. package/dist/Citation/Citation.js +2 -3
  11. package/dist/CodeBlock/highlightRanges.d.ts.map +1 -1
  12. package/dist/CodeBlock/highlightRanges.js +3 -14
  13. package/dist/Icon/Icon.d.ts +31 -6
  14. package/dist/Icon/Icon.d.ts.map +1 -1
  15. package/dist/Icon/Icon.js +71 -8
  16. package/dist/Link/useLinkComponent.d.ts +15 -5
  17. package/dist/Link/useLinkComponent.d.ts.map +1 -1
  18. package/dist/Link/useLinkComponent.js +51 -34
  19. package/dist/Markdown/Markdown.js +2 -2
  20. package/dist/Markdown/url.d.ts +0 -2
  21. package/dist/Markdown/url.d.ts.map +1 -1
  22. package/dist/Markdown/url.js +2 -8
  23. package/dist/PowerSearch/PowerSearchValueEditor.d.ts.map +1 -1
  24. package/dist/PowerSearch/PowerSearchValueEditor.js +0 -5
  25. package/dist/TopNav/TopNavMenu.d.ts +1 -1
  26. package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
  27. package/dist/TopNav/TopNavMenu.js +2 -3
  28. package/dist/astryx.css +4 -2
  29. package/dist/hooks/useClickableContainer.d.ts +1 -10
  30. package/dist/hooks/useClickableContainer.d.ts.map +1 -1
  31. package/dist/hooks/useClickableContainer.js +4 -15
  32. package/dist/index.d.ts +0 -1
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +0 -3
  35. package/dist/theme/generateThemeRules.d.ts +7 -13
  36. package/dist/theme/generateThemeRules.d.ts.map +1 -1
  37. package/dist/theme/generateThemeRules.js +35 -131
  38. package/package.json +4 -9
  39. package/src/Button/Button.doc.mjs +5 -5
  40. package/src/Button/Button.tsx +13 -15
  41. package/src/Chat/ChatLayout.doc.mjs +4 -12
  42. package/src/Chat/ChatLayout.test.tsx +0 -28
  43. package/src/Chat/ChatLayout.tsx +0 -19
  44. package/src/Chat/ChatLayoutScrollButton.test.tsx +0 -31
  45. package/src/Chat/ChatLayoutScrollButton.tsx +14 -31
  46. package/src/Citation/Citation.doc.mjs +2 -2
  47. package/src/Citation/Citation.test.tsx +0 -17
  48. package/src/Citation/Citation.tsx +2 -4
  49. package/src/ClickableCard/ClickableCard.doc.mjs +2 -2
  50. package/src/CodeBlock/highlightRanges.test.ts +1 -143
  51. package/src/CodeBlock/highlightRanges.ts +5 -15
  52. package/src/Icon/Icon.doc.mjs +5 -7
  53. package/src/Icon/Icon.spec.md +40 -81
  54. package/src/Icon/Icon.test.tsx +3 -6
  55. package/src/Icon/Icon.tsx +68 -20
  56. package/src/IconButton/IconButton.doc.mjs +3 -3
  57. package/src/IconButton/IconButton.test.tsx +0 -40
  58. package/src/Item/Item.doc.mjs +2 -2
  59. package/src/Link/Link.doc.mjs +4 -6
  60. package/src/Link/LinkProvider.doc.mjs +2 -2
  61. package/src/Link/useLinkComponent.test.tsx +0 -333
  62. package/src/Link/useLinkComponent.ts +57 -29
  63. package/src/List/ListItem.doc.mjs +2 -2
  64. package/src/Markdown/Markdown.doc.mjs +2 -2
  65. package/src/Markdown/Markdown.spec.md +15 -19
  66. package/src/Markdown/Markdown.test.tsx +0 -52
  67. package/src/Markdown/Markdown.tsx +2 -2
  68. package/src/Markdown/parser.test.ts +0 -20
  69. package/src/Markdown/url.ts +3 -9
  70. package/src/PowerSearch/PowerSearchValueEditor.test.tsx +0 -51
  71. package/src/PowerSearch/PowerSearchValueEditor.tsx +0 -4
  72. package/src/Token/Token.doc.mjs +2 -2
  73. package/src/TopNav/TopNavMenu.doc.mjs +2 -2
  74. package/src/TopNav/TopNavMenu.test.tsx +0 -30
  75. package/src/TopNav/TopNavMenu.tsx +2 -7
  76. package/src/hooks/useClickableContainer.ts +4 -20
  77. package/src/index.ts +0 -3
  78. package/src/tailwind-theme.css +6 -0
  79. package/src/theme/Theme.test.tsx +0 -32
  80. package/src/theme/generateThemeRules.test.ts +2 -470
  81. package/src/theme/generateThemeRules.ts +51 -219
  82. package/src/theme/tokenValueCompat.test.ts +1 -4
  83. package/dist/Icon/IconDefaultSizeContext.d.ts +0 -4
  84. package/dist/Icon/IconDefaultSizeContext.d.ts.map +0 -1
  85. package/dist/Icon/IconDefaultSizeContext.js +0 -18
  86. package/dist/Icon/IconSize.stylex.d.ts +0 -65
  87. package/dist/Icon/IconSize.stylex.d.ts.map +0 -1
  88. package/dist/Icon/IconSize.stylex.js +0 -75
  89. package/dist/Timer/Timer.d.ts +0 -49
  90. package/dist/Timer/Timer.d.ts.map +0 -1
  91. package/dist/Timer/Timer.js +0 -158
  92. package/dist/Timer/index.d.ts +0 -9
  93. package/dist/Timer/index.d.ts.map +0 -1
  94. package/dist/Timer/index.js +0 -10
  95. package/dist/theme/declarationBoundary.d.ts +0 -17
  96. package/dist/theme/declarationBoundary.d.ts.map +0 -1
  97. package/dist/theme/declarationBoundary.js +0 -343
  98. package/dist/utils/safeUrl.d.ts +0 -11
  99. package/dist/utils/safeUrl.d.ts.map +0 -1
  100. package/dist/utils/safeUrl.js +0 -78
  101. package/src/Chat/ChatLayout.spec.md +0 -271
  102. package/src/Chat/ChatLayoutScrollButton.spec.md +0 -338
  103. package/src/Chat/__tests__/ChatLayoutScrollButton.a11y.chromium.spec.ts +0 -514
  104. package/src/Chat/__tests__/ChatLayoutScrollButtonSurface.a11y.chromium.spec.ts +0 -740
  105. package/src/Icon/IconDefaultSizeContext.ts +0 -23
  106. package/src/Icon/IconSize.stylex.ts +0 -70
  107. package/src/Link/__tests__/Link.navigation.a11y.chromium.spec.ts +0 -261
  108. package/src/Markdown/Markdown.renderBoundary.test.tsx +0 -72
  109. package/src/Timer/Timer.doc.mjs +0 -183
  110. package/src/Timer/Timer.spec.md +0 -215
  111. package/src/Timer/Timer.test.tsx +0 -302
  112. package/src/Timer/Timer.tsx +0 -258
  113. package/src/Timer/index.ts +0 -11
  114. package/src/hooks/useClickableContainer.test.tsx +0 -236
  115. package/src/theme/declarationBoundary.test.ts +0 -126
  116. package/src/theme/declarationBoundary.ts +0 -347
  117. package/src/utils/safeUrl.test.ts +0 -88
  118. package/src/utils/safeUrl.ts +0 -85
@@ -4,61 +4,89 @@
4
4
 
5
5
  /**
6
6
  * @file useLinkComponent.ts
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.
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
11
10
  *
12
- * SYNC: Link.doc.mjs and LinkProvider.doc.mjs describe this boundary.
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
13
21
  */
14
22
 
15
23
  import {use, useMemo, createElement} from 'react';
16
24
  import {LinkContext} from './LinkContext';
17
- import {isSafeDestination} from '../utils/safeUrl';
18
25
  import type {LinkComponentType} from './types';
19
26
 
20
- function createSafeLink(Component: LinkComponentType): LinkComponentType {
21
- function SafeLink({
27
+ /**
28
+ * Creates a wrapper component that passes both `href` and `to` props
29
+ * to the underlying link component. This enables routers that use `to`
30
+ * (React Router, TanStack Router) to work without an adapter.
31
+ *
32
+ * The wrapper is transparent: it forwards refs and all other props unchanged.
33
+ * Native `<a>` elements ignore the unknown `to` prop harmlessly.
34
+ */
35
+ function createLinkWithTo(
36
+ Component: LinkComponentType,
37
+ ): LinkComponentType {
38
+ function LinkWithTo({
22
39
  href,
23
- to,
24
40
  ref,
25
41
  ...rest
26
42
  }: {
27
- href?: unknown;
28
- to?: unknown;
43
+ href?: string;
44
+ to?: string;
29
45
  ref?: React.Ref<unknown>;
30
46
  }) {
31
- // An undefined destination can crash a router or resolve to the current
32
- // route. Reject the whole handoff, not just one prop or a fallback value.
33
- if (!isSafeDestination(href) || !isSafeDestination(to)) {
34
- return createElement('a', {ref, ...rest});
35
- }
36
- if (Component === 'a') {
37
- return createElement('a', {ref, ...rest, href});
38
- }
39
- return createElement(Component, {ref, ...rest, href, to: to ?? href});
47
+ return createElement(Component, {ref, href, to: href, ...rest});
40
48
  }
41
- SafeLink.displayName = `SafeLink(${
49
+ LinkWithTo.displayName = `LinkWithTo(${
42
50
  typeof Component === 'string'
43
51
  ? Component
44
52
  : Component.displayName || Component.name || 'Component'
45
53
  })`;
46
- return SafeLink as LinkComponentType;
54
+ return LinkWithTo as LinkComponentType;
47
55
  }
48
56
 
49
57
  /**
50
- * Resolve `as`, then LinkProvider, then a native anchor. Both destination props
51
- * are checked before rendering; accepted structured values retain identity.
52
- * Custom components receive `to={href}` unless an explicit `to` is supplied.
58
+ * Resolves the link component to use.
59
+ *
60
+ * Priority: `as` prop > `LinkProvider` context > native `<a>`.
61
+ *
62
+ * When the resolved component is a custom component (not the native `<a>`),
63
+ * it is wrapped to receive both `href` and `to` props set to the same value.
64
+ * This allows `to`-based routers (React Router, TanStack Router) to work
65
+ * out of the box without a manual adapter.
66
+ *
67
+ * @param as - Per-component override. If provided, takes highest priority.
68
+ * @returns The resolved link component (with `to` injection for custom components).
53
69
  *
54
70
  * @example
55
71
  * ```
56
- * const LinkComponent = useLinkComponent(as);
57
- * return <LinkComponent href="/docs">Documentation</LinkComponent>;
72
+ * function MyComponent({ as }: { as?: LinkComponentType }) {
73
+ * const LinkComponent = useLinkComponent(as);
74
+ * return <LinkComponent href="/foo">Click me</LinkComponent>;
75
+ * }
58
76
  * ```
59
77
  */
60
- export function useLinkComponent(as?: LinkComponentType): LinkComponentType {
78
+ export function useLinkComponent(
79
+ as?: LinkComponentType,
80
+ ): LinkComponentType {
61
81
  const ctx = use(LinkContext);
62
82
  const resolved = as ?? ctx?.component ?? 'a';
63
- return useMemo(() => createSafeLink(resolved), [resolved]);
83
+
84
+ // Memoize the wrapper to maintain referential stability.
85
+ // The wrapper is only created for custom components (not native <a>).
86
+ return useMemo(() => {
87
+ if (resolved === 'a') {
88
+ return 'a';
89
+ }
90
+ return createLinkWithTo(resolved);
91
+ }, [resolved]);
64
92
  }
@@ -67,7 +67,7 @@ export const docs = {
67
67
  {
68
68
  name: 'href',
69
69
  type: 'string',
70
- description: 'Link URL; enables the invisible anchor pattern. The destination follows the shared navigation rule described on the Link `href` prop.',
70
+ description: 'Link URL; enables the invisible anchor pattern.',
71
71
  },
72
72
  {
73
73
  name: 'target',
@@ -174,7 +174,7 @@ export const docsDense = {
174
174
  onClick: 'Click handler; enables invisible button pattern.',
175
175
  interactiveRef:
176
176
  'Ref to a nested control that owns the item\'s keyboard access/action; row delegates surface clicks to it (useClickableContainer), no invisible button/anchor, no second tab stop (WCAG 4.1.2). Mutually exclusive with onClick/href.',
177
- href: 'Link URL; enables invisible anchor pattern. Follows the shared navigation rule (see Link href).',
177
+ href: 'Link URL; enables invisible anchor pattern.',
178
178
  target: 'Link target attribute, only when href provided. target="_blank" auto-adds noopener noreferrer.',
179
179
  rel: 'Link relationship tokens. noopener noreferrer are merged for target="_blank".',
180
180
  isDisabled: 'Disabled state; sets aria-disabled.',
@@ -112,7 +112,7 @@ export const docs = {
112
112
  name: 'onLinkClick',
113
113
  type: '(href: string, event: MouseEvent) => void | false',
114
114
  description:
115
- 'Handler for link clicks. Return false to prevent the default navigation behavior. Link destinations in the markdown follow the shared navigation rule described on the Link `href` prop: a blocked destination renders as plain text and never reaches this handler or a custom link renderer. Image URLs use a separate, stricter policy (every data: URL is rejected).',
115
+ 'Handler for link clicks. Return false to prevent the default navigation behavior.',
116
116
  },
117
117
  {
118
118
  name: 'sources',
@@ -859,7 +859,7 @@ export const docsDense = {
859
859
  isStreaming:
860
860
  'Incremental parse + fade-in for streamed chunks. Default: false.',
861
861
  onLinkClick:
862
- '(href, event) => void|false. Return false prevents navigation. Link destinations follow the shared navigation rule (see Link href); blocked ones render as text. Image URLs: separate stricter policy.',
862
+ '(href, event) => void|false. Return false prevents navigation.',
863
863
  sources:
864
864
  'Record<string, MarkdownSource>. Citation sources by ID. [id]/【id】 markers render as chips.',
865
865
  citationStyle:
@@ -13,7 +13,6 @@ review_triggers: [api, theming]
13
13
  verified_by:
14
14
  [
15
15
  packages/core/src/Markdown/Markdown.test.tsx,
16
- packages/core/src/Markdown/Markdown.renderBoundary.test.tsx,
17
16
  packages/core/src/Markdown/Markdown.public.test.ts,
18
17
  packages/core/src/Markdown/parser.test.ts,
19
18
  packages/core/src/Markdown/incremental.test.ts,
@@ -317,9 +316,7 @@ and this change preserves the existing spelling exactly.
317
316
  current with the implementation.
318
317
  - `family:navigation-destinations` owns the shared accept/block result for parsed
319
318
  links and every Astryx-owned navigation sink. `spec:AST-005/DEC-1` requires
320
- Markdown parsing and link rendering to preserve the same normalized navigation
321
- decision as other Core links. Rejected links remain inert and do not reach a
322
- custom link renderer; accepted links retain their ordinary behavior.
319
+ Markdown navigation to remain conformant with Core link plumbing.
323
320
  - `spec:AST-005/DEC-2` keeps embedded-resource policy separate. Markdown may
324
321
  reject a broader set of image/resource URLs without narrowing the shared
325
322
  navigation contract.
@@ -335,21 +332,20 @@ and this change preserves the existing spelling exactly.
335
332
 
336
333
  ## Verification map
337
334
 
338
- | Contract | Verification | Representative states | Failure signal |
339
- | ---------------------- | ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
340
- | FR1–FR5 | `Markdown.test.tsx`, theme-target tests, and `scripts/check-knowledge.mjs` | Default block/inline output and all current targets | Existing DOM, target, spacing, or renderer behavior changes. |
341
- | FR6–FR10 | `parser.test.ts` and `Markdown.test.tsx` | Opt-out, inline/display math, escapes, currency, code, links, plugins | A delimiter is claimed without opt-in, TeX is formatted as Markdown, or opaque contexts leak. |
342
- | FR11 | `incremental.test.ts` and `Markdown.test.tsx` | Every-character top-level/list/task-list/blockquote splits, CRLF, source ranges | Streaming diverges from a full parse, shows partial syntax, mistakes a nested closer for an opener, or crosses a closed container. |
343
- | FR12–FR16 | plugin, transform, adapter, performance, and Outline parser tests | omitted/empty lists, immutable transforms, invalid outputs, live updates, one compatible Remark plugin, duplicate headings | Empty behavior forks, input mutates, invalid structure escapes, transforms reparse, adapter loses content, budgets fail, or heading targets diverge. |
344
- | FR17–FR18 | container parse/validation, ownership, and dependency tests | leaf/container declarations, nested containers, foreign read/remove/mint/edit, unmet/misordered dependencies | Plugin-built parsed children, invalid content, lost fallback children, foreign mint/edit, or generic ownership codes. |
345
- | FR19 | diagnostic-channel tests in development and production | every phase, advisory reports, rate suppression, no handler, malformed list, duplicate Core, version skew | A silent production failure, document content in a diagnostic, or one entrypoint throwing where another recovers. |
346
- | FR20–FR22 | canonical/server imports, inference, streaming, preparation, theming tests | server imports, no explicit type args, chunk boundaries, recreated lists, themed/unthemed extensions | Client references, explicit-type workarounds, oscillation, per-render re-preparation, or unreachable opted-in output. |
347
- | FR23 | parser, renderer, nesting, and public option tests | task-only, plain-only, and mixed ordered/unordered lists at top level and nested; autolink omitted/enabled | A mixed list splits or loses order/state, a plain item becomes a checkbox, or `autolink: 'gfm'` changes non-autolink syntax. |
348
- | FR24 | `plugins/frontmatter.test.tsx`, Storybook, and server rendering | complete, malformed, non-leading, LF/CRLF, unfinished streaming, full plugin stack | Metadata syntax renders after completion, unfinished syntax leaks while streaming, typing is lost, or later plugins stop composing. |
349
- | FR25 | `parser.public.test.ts`, plugin SSR tests, and package export checks | server/RSC parsing with plugins, synchronous SSR, suspending renderer streaming boundary | A server import gains `use client`, plugin execution needs serialization, SSR loses fallback, or direct RSC rendering is misrepresented as supported. |
350
- | Public syntax/types | `Markdown.public.test.ts`, core typecheck, and `Markdown.doc.mjs` | Legacy exhaustive switches, math opt-ins, inferred extension-node unions | A released union widens, an enabled union loses nodes, or docs drift from declarations. |
351
- | Navigation contract | `parser.test.ts`, `Markdown.test.tsx`, and `Markdown.renderBoundary.test.tsx` | Parsed and rendered links, including transformed built-in links; accepted ordinary schemes; rejected destinations; links versus images | A blocked destination reaches navigation or a custom link renderer, or resource policy narrows accepted navigation. |
352
- | Security/accessibility | `parser.test.ts`, `Markdown.test.tsx`, and renderer guidance | Inert expression strings and renderer-owned semantics | Astryx executes math as HTML or silently claims renderer-owned accessibility. |
335
+ | Contract | Verification | Representative states | Failure signal |
336
+ | ---------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
337
+ | FR1–FR5 | `Markdown.test.tsx`, theme-target tests, and `scripts/check-knowledge.mjs` | Default block/inline output and all current targets | Existing DOM, target, spacing, or renderer behavior changes. |
338
+ | FR6–FR10 | `parser.test.ts` and `Markdown.test.tsx` | Opt-out, inline/display math, escapes, currency, code, links, plugins | A delimiter is claimed without opt-in, TeX is formatted as Markdown, or opaque contexts leak. |
339
+ | FR11 | `incremental.test.ts` and `Markdown.test.tsx` | Every-character top-level/list/task-list/blockquote splits, CRLF, source ranges | Streaming diverges from a full parse, shows partial syntax, mistakes a nested closer for an opener, or crosses a closed container. |
340
+ | FR12–FR16 | plugin, transform, adapter, performance, and Outline parser tests | omitted/empty lists, immutable transforms, invalid outputs, live updates, one compatible Remark plugin, duplicate headings | Empty behavior forks, input mutates, invalid structure escapes, transforms reparse, adapter loses content, budgets fail, or heading targets diverge. |
341
+ | FR17–FR18 | container parse/validation, ownership, and dependency tests | leaf/container declarations, nested containers, foreign read/remove/mint/edit, unmet/misordered dependencies | Plugin-built parsed children, invalid content, lost fallback children, foreign mint/edit, or generic ownership codes. |
342
+ | FR19 | diagnostic-channel tests in development and production | every phase, advisory reports, rate suppression, no handler, malformed list, duplicate Core, version skew | A silent production failure, document content in a diagnostic, or one entrypoint throwing where another recovers. |
343
+ | FR20–FR22 | canonical/server imports, inference, streaming, preparation, theming tests | server imports, no explicit type args, chunk boundaries, recreated lists, themed/unthemed extensions | Client references, explicit-type workarounds, oscillation, per-render re-preparation, or unreachable opted-in output. |
344
+ | FR23 | parser, renderer, nesting, and public option tests | task-only, plain-only, and mixed ordered/unordered lists at top level and nested; autolink omitted/enabled | A mixed list splits or loses order/state, a plain item becomes a checkbox, or `autolink: 'gfm'` changes non-autolink syntax. |
345
+ | FR24 | `plugins/frontmatter.test.tsx`, Storybook, and server rendering | complete, malformed, non-leading, LF/CRLF, unfinished streaming, full plugin stack | Metadata syntax renders after completion, unfinished syntax leaks while streaming, typing is lost, or later plugins stop composing. |
346
+ | FR25 | `parser.public.test.ts`, plugin SSR tests, and package export checks | server/RSC parsing with plugins, synchronous SSR, suspending renderer streaming boundary | A server import gains `use client`, plugin execution needs serialization, SSR loses fallback, or direct RSC rendering is misrepresented as supported. |
347
+ | Public syntax/types | `Markdown.public.test.ts`, core typecheck, and `Markdown.doc.mjs` | Legacy exhaustive switches, math opt-ins, inferred extension-node unions | A released union widens, an enabled union loses nodes, or docs drift from declarations. |
348
+ | Security/accessibility | `parser.test.ts`, `Markdown.test.tsx`, and renderer guidance | Inert expression strings and renderer-owned semantics | Astryx executes math as HTML or silently claims renderer-owned accessibility. |
353
349
 
354
350
  Focused tests continue to pin all nine current target names and default block
355
351
  placement. Math intentionally adds no target and no default anatomy.
@@ -636,58 +636,6 @@ describe('Markdown', () => {
636
636
  expect(links[1].getAttribute('href')).toBe('/page');
637
637
  });
638
638
 
639
- describe('link destinations follow the shared navigation rule', () => {
640
- // The same matrix Core's link plumbing and imperative navigation apply
641
- // (utils/safeUrl.ts): only executable document schemes are blocked.
642
- const blocked = [
643
- 'javascript:alert(1)',
644
- 'JaVaScRiPt:alert(1)',
645
- 'vbscript:MsgBox(1)',
646
- 'data:text/html,<script>alert(1)</script>',
647
- 'java\nscript:alert(1)',
648
- ];
649
- const accepted = [
650
- 'https://example.com',
651
- '/page',
652
- '#section',
653
- '//example.com/x',
654
- 'mailto:a@example.com',
655
- 'tel:+15555550100',
656
- 'data:image/png;base64,iVBORw0KGgo=',
657
- ];
658
-
659
- it.each(blocked)('renders %s as text, not a link', destination => {
660
- const {container} = render(
661
- <Markdown>{`[click](${destination})`}</Markdown>,
662
- );
663
- expect(container.querySelector('a')).toBeNull();
664
- expect(container.textContent).toContain('click');
665
- });
666
-
667
- it.each(accepted)('renders %s as a link', destination => {
668
- const {container} = render(
669
- <Markdown>{`[click](${destination})`}</Markdown>,
670
- );
671
- const link = container.querySelector('a');
672
- expect(link).not.toBeNull();
673
- expect(link?.getAttribute('href')).toBe(destination);
674
- });
675
-
676
- it('a data:image link is navigation and is accepted, while a data:image image stays rejected by the resource policy', () => {
677
- const {container} = render(
678
- <Markdown>
679
- {
680
- '[view](data:image/png;base64,iVBORw0KGgo=)\n\n![pic](data:image/png;base64,iVBORw0KGgo=)'
681
- }
682
- </Markdown>,
683
- );
684
- expect(container.querySelector('a')?.getAttribute('href')).toBe(
685
- 'data:image/png;base64,iVBORw0KGgo=',
686
- );
687
- expect(container.querySelector('img')).toBeNull();
688
- });
689
- });
690
-
691
639
  it('preserves dollar-delimited text when no math renderer is supplied', () => {
692
640
  const {container} = render(
693
641
  <Markdown>{'Total $5 and formula $x_1 + *y*$.'}</Markdown>,
@@ -76,7 +76,7 @@ import type {
76
76
  MarkdownPluginEntry,
77
77
  PreparedMarkdownPlugins,
78
78
  } from './plugins/protocol';
79
- import {sanitizeMarkdownLinkUrl, sanitizeMarkdownUrl} from './url';
79
+ import {sanitizeMarkdownUrl} from './url';
80
80
  import {themeProps} from '../utils/themeProps';
81
81
  import {useTranslator, type TranslatorFn} from '../i18n';
82
82
 
@@ -961,7 +961,7 @@ function renderInline(
961
961
  return <MathComp key={index} value={node.value} display="inline" />;
962
962
  }
963
963
  case 'link': {
964
- const safeHref = sanitizeMarkdownLinkUrl(node.url);
964
+ const safeHref = sanitizeMarkdownUrl(node.url);
965
965
  if (safeHref == null) {
966
966
  // Unsafe URL — render as plain text
967
967
  return (
@@ -165,26 +165,6 @@ describe('parseInline', () => {
165
165
  }
166
166
  });
167
167
 
168
- it('decides link destinations with the shared navigation rule', () => {
169
- const destinations: [string, boolean][] = [
170
- ['https://example.com', true],
171
- ['/page', true],
172
- ['#section', true],
173
- ['mailto:a@example.com', true],
174
- ['tel:+1234567890', true],
175
- ['custom:document', true],
176
- ['data:image/png;base64,iVBORw0KGgo=', true],
177
- ['javascript:alert(1)', false],
178
- ['vbscript:MsgBox(1)', false],
179
- ['data:text/html,<b>x</b>', false],
180
- ['java\u0000script:alert(1)', false],
181
- ];
182
- for (const [destination, accepted] of destinations) {
183
- const [node] = parseInline(`[t](${destination})`);
184
- expect(node.type === 'link').toBe(accepted);
185
- }
186
- });
187
-
188
168
  it('parses strikethrough', () => {
189
169
  const result = parseInline('~~deleted~~');
190
170
  expect(result[0].type).toBe('strikethrough');
@@ -7,8 +7,8 @@
7
7
  * @position Shared URL policy for parser, transforms, and renderer
8
8
  */
9
9
 
10
- import {isSafeUrl, sanitizeUrl} from '../utils/safeUrl';
11
-
10
+ const PARSER_DANGEROUS_URL_PATTERN =
11
+ /^(?:javascript:|data:text\/html|vbscript:)/i;
12
12
  const RENDER_DANGEROUS_URL_PATTERN = /^(?:javascript:|data:|vbscript:)/i;
13
13
 
14
14
  function normalizeMarkdownUrl(url: string): string {
@@ -19,13 +19,7 @@ function normalizeMarkdownUrl(url: string): string {
19
19
 
20
20
  /** Preserve the released parser policy for no-plugin output compatibility. */
21
21
  export function isSafeMarkdownParserUrl(url: string): boolean {
22
- return isSafeUrl(url);
23
- }
24
-
25
- /** Navigation and image rendering deliberately have different policies. */
26
- export function sanitizeMarkdownLinkUrl(url: string): string | null {
27
- const normalized = sanitizeUrl(url);
28
- return normalized === '' ? null : normalized;
22
+ return !PARSER_DANGEROUS_URL_PATTERN.test(normalizeMarkdownUrl(url));
29
23
  }
30
24
 
31
25
  export function sanitizeMarkdownUrl(url: string): string | null {
@@ -630,54 +630,3 @@ describe('maxMenuItems', () => {
630
630
  await expectCapped({type: 'entity_list', searchSource: source});
631
631
  });
632
632
  });
633
-
634
- // =============================================================================
635
- // EnumListEditor shows the whole closed vocabulary
636
- // =============================================================================
637
-
638
- describe('EnumListEditor', () => {
639
- const values = Array.from({length: 12}, (_, index) => ({
640
- value: `option-${index}`,
641
- label: `Option ${index}`,
642
- }));
643
-
644
- it('shows every value on focus instead of the typeahead default of 10', async () => {
645
- render(
646
- <PowerSearchValueEditor
647
- operatorValue={{type: 'enum_list', values}}
648
- filterValue={undefined}
649
- onChange={vi.fn()}
650
- config={stubConfig}
651
- />,
652
- );
653
-
654
- fireEvent.focus(screen.getByRole('combobox'));
655
- await act(async () => {
656
- await new Promise(resolve => setTimeout(resolve, 50));
657
- });
658
-
659
- expect(screen.getAllByRole('option', {hidden: true})).toHaveLength(12);
660
- expect(screen.getByText('Option 11')).toBeInTheDocument();
661
- });
662
-
663
- it('shows every matching value when searching', async () => {
664
- render(
665
- <PowerSearchValueEditor
666
- operatorValue={{type: 'enum_list', values}}
667
- filterValue={undefined}
668
- onChange={vi.fn()}
669
- config={stubConfig}
670
- />,
671
- );
672
-
673
- await act(async () => {
674
- fireEvent.change(screen.getByRole('combobox'), {
675
- target: {value: 'Option'},
676
- });
677
- await new Promise(resolve => setTimeout(resolve, 200));
678
- });
679
-
680
- expect(screen.getAllByRole('option', {hidden: true})).toHaveLength(12);
681
- expect(screen.getByText('Option 11')).toBeInTheDocument();
682
- });
683
- });
@@ -619,10 +619,6 @@ function EnumListEditor({
619
619
  placeholder={t('@astryx.powersearch.valueEditor.selectValuesPlaceholder')}
620
620
  hasEntriesOnFocus
621
621
  debounceMs={0}
622
- // A closed vocabulary: every value is a valid choice, so the menu shows
623
- // all of them instead of BaseTypeahead's 10-suggestion default, which
624
- // would silently drop valid choices with no way to reach them.
625
- maxMenuItems={items.length}
626
622
  />
627
623
  );
628
624
  }
@@ -55,7 +55,7 @@ export const docs = {
55
55
  name: 'href',
56
56
  type: 'string',
57
57
  description:
58
- 'Link URL. When provided, the token renders as an <a> element. The destination follows the shared navigation rule described on the Link `href` prop.',
58
+ 'Link URL. When provided, the token renders as an <a> element.',
59
59
  },
60
60
  {
61
61
  name: 'description',
@@ -241,7 +241,7 @@ export const docsDense = {
241
241
  isDisabled: 'Reduces opacity, blocks interactions.',
242
242
  onRemove: 'Fired on remove button click. Renders X button when provided.',
243
243
  onClick: 'Click handler. Renders <span> w/ invisible <button> inside for a11y.',
244
- href: 'Link URL. Renders as <a> element. Follows the shared navigation rule (see Link href).',
244
+ href: 'Link URL. Renders as <a> element.',
245
245
  description: 'A11y description via aria-description on root.',
246
246
  endContent: 'Content after label, before remove button.',
247
247
  isLabelHidden: 'Visually hides label w/ screen-reader-only clip; stays accessible.',
@@ -27,7 +27,7 @@ export const docs = {
27
27
  {
28
28
  name: 'items',
29
29
  type: 'TopNavMenuItemData[]',
30
- description: 'Menu items to display in the hover popover. Item href values follow the shared navigation rule described on Link; rejected destinations remain visible without navigation.',
30
+ description: 'Menu items to display in the hover popover.',
31
31
  required: true,
32
32
  },
33
33
  {
@@ -83,7 +83,7 @@ export const docsDense = {
83
83
  description: 'Nav item w/ hover-triggered popover menu containing rich items w/ icon, title, optional description.',
84
84
  propDescriptions: {
85
85
  label: 'Trigger button visible label.',
86
- items: 'Menu items in hover popover. href follows the Link navigation rule; rejected destinations do not navigate.',
86
+ items: 'Menu items in hover popover.',
87
87
  delay: 'Show delay ms on hover.',
88
88
  hideDelay: 'Hide delay ms after mouse leaves.',
89
89
  },
@@ -31,36 +31,6 @@ const mockItems = [
31
31
  ];
32
32
 
33
33
  describe('TopNavMenu', () => {
34
- it.each([
35
- 'javascript:alert(1)',
36
- 'vbscript:MsgBox(1)',
37
- 'data:text/html,<b>x</b>',
38
- ])('does not expose rejected menu destination %s', href => {
39
- const onClick = vi.fn();
40
- render(
41
- <TopNavMenu
42
- label="Menu"
43
- items={[{title: 'Destination', href, onClick}]}
44
- />,
45
- );
46
- const item = screen.getByRole('menuitem', {hidden: true});
47
- expect(item).not.toHaveAttribute('href');
48
- fireEvent.click(item);
49
- expect(onClick).toHaveBeenCalledOnce();
50
- });
51
-
52
- it('preserves an accepted native menu destination', () => {
53
- render(
54
- <TopNavMenu
55
- label="Menu"
56
- items={[{title: 'Destination', href: '/docs'}]}
57
- />,
58
- );
59
- expect(screen.getByRole('menuitem', {hidden: true})).toHaveAttribute(
60
- 'href',
61
- '/docs',
62
- );
63
- });
64
34
  it('renders the trigger button with label', () => {
65
35
  render(<TopNavMenu label="Products" items={mockItems} />);
66
36
  expect(screen.getByRole('button', {name: 'Products'})).toBeInTheDocument();
@@ -5,7 +5,7 @@
5
5
  /**
6
6
  * @file TopNavMenu.tsx
7
7
  * @input Uses React, StyleX, usePopover, useMenuHover, useListFocus,
8
- * useTypeahead, TopNavItem tokens, and the shared navigation policy
8
+ * useTypeahead, TopNavItem tokens
9
9
  * @output Exports TopNavMenu component and related types
10
10
  * @position Navigation item with hover-triggered overflow menu for TopNav
11
11
  *
@@ -37,7 +37,6 @@ import {useTopNavSlot} from './TopNavContext';
37
37
  import {useTopNavRenderMode} from './TopNavRenderContext';
38
38
  import {useAppShellMobile} from '../AppShell/AppShellMobileContext';
39
39
  import {useLinkComponent} from '../Link/useLinkComponent';
40
- import {isSafeUrl} from '../utils/safeUrl';
41
40
  import {themeProps} from '../utils/themeProps';
42
41
  import {focusOutlineProps} from '../utils/focusOutline.stylex';
43
42
  import {interactionOverlayStyles} from '../utils/interactionOverlay.stylex';
@@ -576,11 +575,7 @@ export function TopNavMenu({
576
575
  // Single tab stop: useListFocus owns the roving tabindex and
577
576
  // promotes exactly one item to 0.
578
577
  tabIndex={-1}
579
- href={
580
- item.href != null && isSafeUrl(item.href)
581
- ? item.href
582
- : undefined
583
- }
578
+ href={item.href}
584
579
  onClick={item.onClick}
585
580
  {...focusOutlineProps.focusVisible(styles.menuItem)}>
586
581
  <div {...stylex.props(styles.menuItemIcon)}>{item.icon}</div>
@@ -11,17 +11,11 @@
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
- *
19
14
  * SYNC: When modified, update:
20
15
  * - /packages/core/src/hooks/index.ts (export)
21
16
  */
22
17
 
23
18
  import {useCallback, useEffect, type RefObject, type MouseEvent} from 'react';
24
- import {isSafeUrl} from '../utils/safeUrl';
25
19
 
26
20
  /**
27
21
  * Canonical list of interactive element selectors — native controls plus
@@ -95,11 +89,7 @@ export interface UseClickableContainerOptions {
95
89
  interactiveRef?: RefObject<HTMLElement | null>;
96
90
  /** Click handler */
97
91
  onClick?: (event: MouseEvent<HTMLElement>) => void;
98
- /**
99
- * Navigation URL — when provided, clicking the container navigates. Checked
100
- * with the shared navigation rule (utils/safeUrl.ts) before any activation
101
- * method (plain, new-tab, Cmd/Ctrl-click, middle-click, delegated click).
102
- */
92
+ /** Navigation URL — when provided, clicking the container navigates */
103
93
  href?: string;
104
94
  /** Link target */
105
95
  target?: string;
@@ -190,12 +180,8 @@ export function useClickableContainer({
190
180
  return;
191
181
  }
192
182
 
193
- // Navigate if href is provided. React DOM vets the hrefs it writes as
194
- // attributes; nothing below passes through React, so the shared
195
- // navigation rule decides once, before target, modifier key, or the
196
- // delegated anchor can select a sink. A blocked href simply does not
197
- // navigate — the click handler above still ran.
198
- if (href != null && isSafeUrl(href)) {
183
+ // Navigate if href is provided
184
+ if (href != null) {
199
185
  const shouldOpenNewTab =
200
186
  target === '_blank' || event.ctrlKey || event.metaKey;
201
187
  if (shouldOpenNewTab) {
@@ -244,13 +230,11 @@ export function useClickableContainer({
244
230
  return;
245
231
  }
246
232
 
247
- // Middle-click on href opens in new tab (same scheme rule as onClick —
248
- // this navigation never passes through React either).
233
+ // Middle-click on href opens in new tab
249
234
  const isMiddleClick = event.button === 1;
250
235
  if (
251
236
  isMiddleClick &&
252
237
  href != null &&
253
- isSafeUrl(href) &&
254
238
  (eventTarget === event.currentTarget ||
255
239
  !hasInteractiveAncestor(eventTarget, containerEl))
256
240
  ) {
package/src/index.ts CHANGED
@@ -161,9 +161,6 @@ export * from './Spinner';
161
161
  // Timestamp display
162
162
  export * from './Timestamp';
163
163
 
164
- // Elapsed timer display
165
- export * from './Timer';
166
-
167
164
  // Overlay
168
165
  export * from './Overlay';
169
166
  export * from './Outline';
@@ -182,6 +182,12 @@
182
182
  --text-4xl: var(--font-size-4xl);
183
183
  --text-5xl: var(--font-size-5xl);
184
184
 
185
+ /* --- Font weights --- */
186
+ --font-weight-normal: var(--font-weight-normal);
187
+ --font-weight-medium: var(--font-weight-medium);
188
+ --font-weight-semibold: var(--font-weight-semibold);
189
+ --font-weight-bold: var(--font-weight-bold);
190
+
185
191
  /* =========================================================================
186
192
  * Border radius
187
193
  *