@astryxdesign/core 0.1.0 → 0.1.1-canary.080d887

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 (170) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +119 -1
  3. package/dist/AvatarGroup/AvatarGroupOverflow.d.ts +1 -1
  4. package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
  5. package/dist/AvatarGroup/AvatarGroupOverflow.js +4 -1
  6. package/dist/Banner/Banner.d.ts +7 -0
  7. package/dist/Banner/Banner.d.ts.map +1 -1
  8. package/dist/Banner/Banner.js +9 -2
  9. package/dist/Button/Button.d.ts +12 -2
  10. package/dist/Button/Button.d.ts.map +1 -1
  11. package/dist/Button/Button.js +51 -13
  12. package/dist/Chat/ChatLayoutScrollButton.d.ts.map +1 -1
  13. package/dist/Chat/ChatLayoutScrollButton.js +5 -1
  14. package/dist/ContextMenu/ContextMenu.js +2 -2
  15. package/dist/DropdownMenu/DropdownMenu.js +2 -2
  16. package/dist/DropdownMenu/{renderXDSDropdownItems.d.ts → renderDropdownItems.d.ts} +3 -3
  17. package/dist/DropdownMenu/renderDropdownItems.d.ts.map +1 -0
  18. package/dist/DropdownMenu/{renderXDSDropdownItems.js → renderDropdownItems.js} +2 -2
  19. package/dist/EmptyState/EmptyState.d.ts.map +1 -1
  20. package/dist/EmptyState/EmptyState.js +7 -1
  21. package/dist/HoverCard/HoverCard.d.ts +2 -2
  22. package/dist/HoverCard/HoverCard.d.ts.map +1 -1
  23. package/dist/HoverCard/HoverCard.js +18 -6
  24. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  25. package/dist/HoverCard/useHoverCard.js +6 -3
  26. package/dist/Kbd/Kbd.d.ts.map +1 -1
  27. package/dist/Kbd/Kbd.js +1 -1
  28. package/dist/Layer/useLayer.d.ts +13 -0
  29. package/dist/Layer/useLayer.d.ts.map +1 -1
  30. package/dist/Layer/useLayer.js +7 -2
  31. package/dist/Layout/Layout.d.ts +10 -1
  32. package/dist/Layout/Layout.d.ts.map +1 -1
  33. package/dist/Layout/Layout.js +5 -1
  34. package/dist/Markdown/Markdown.d.ts.map +1 -1
  35. package/dist/Markdown/Markdown.js +13 -3
  36. package/dist/MobileNav/MobileNav.d.ts.map +1 -1
  37. package/dist/MobileNav/MobileNav.js +13 -0
  38. package/dist/Outline/Outline.d.ts +3 -2
  39. package/dist/Outline/Outline.d.ts.map +1 -1
  40. package/dist/Outline/Outline.js +23 -4
  41. package/dist/Outline/useScrollSpy.d.ts +14 -1
  42. package/dist/Outline/useScrollSpy.d.ts.map +1 -1
  43. package/dist/Outline/useScrollSpy.js +161 -50
  44. package/dist/Pagination/Pagination.d.ts.map +1 -1
  45. package/dist/Pagination/Pagination.js +31 -27
  46. package/dist/Resizable/useResizable.d.ts.map +1 -1
  47. package/dist/Resizable/useResizable.js +1 -5
  48. package/dist/Selector/Selector.d.ts.map +1 -1
  49. package/dist/Selector/Selector.js +1 -1
  50. package/dist/Slider/Slider.js +2 -2
  51. package/dist/Table/BaseTable.d.ts.map +1 -1
  52. package/dist/Table/BaseTable.js +26 -8
  53. package/dist/Table/Table.d.ts.map +1 -1
  54. package/dist/Table/Table.js +30 -7
  55. package/dist/Table/index.d.ts +3 -1
  56. package/dist/Table/index.d.ts.map +1 -1
  57. package/dist/Table/index.js +1 -0
  58. package/dist/Table/plugins/stickyColumns/index.d.ts +3 -0
  59. package/dist/Table/plugins/stickyColumns/index.d.ts.map +1 -0
  60. package/dist/Table/plugins/stickyColumns/index.js +3 -0
  61. package/dist/Table/plugins/stickyColumns/useTableStickyColumns.d.ts +25 -0
  62. package/dist/Table/plugins/stickyColumns/useTableStickyColumns.d.ts.map +1 -0
  63. package/dist/Table/plugins/stickyColumns/useTableStickyColumns.js +376 -0
  64. package/dist/Table/types.d.ts +90 -5
  65. package/dist/Table/types.d.ts.map +1 -1
  66. package/dist/Table/useBaseTablePlugins.d.ts.map +1 -1
  67. package/dist/Table/useBaseTablePlugins.js +1 -1
  68. package/dist/Timestamp/Timestamp.d.ts +1 -1
  69. package/dist/Timestamp/Timestamp.d.ts.map +1 -1
  70. package/dist/Timestamp/Timestamp.js +28 -3
  71. package/dist/ToggleButton/ToggleButton.d.ts +10 -5
  72. package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
  73. package/dist/ToggleButton/ToggleButton.js +40 -17
  74. package/dist/astryx.css +20 -0
  75. package/dist/astryx.umd.js +147 -0
  76. package/dist/astryx.umd.js.map +7 -0
  77. package/dist/theme/Theme.js +1 -1
  78. package/dist/theme/defineTheme.d.ts +1 -1
  79. package/dist/theme/defineTheme.d.ts.map +1 -1
  80. package/dist/theme/defineTheme.js +1 -1
  81. package/dist/theme/index.d.ts +1 -1
  82. package/dist/theme/index.d.ts.map +1 -1
  83. package/dist/theme/index.js +1 -1
  84. package/dist/theme/syntax/defineSyntaxTheme.js +1 -1
  85. package/dist/theme/tokens.d.ts +1 -1
  86. package/dist/theme/tokens.js +4 -4
  87. package/dist/theme/useTheme.d.ts +2 -2
  88. package/dist/utils/dateParser.d.ts.map +1 -1
  89. package/dist/utils/dateParser.js +15 -2
  90. package/package.json +7 -3
  91. package/src/AvatarGroup/AvatarGroupOverflow.tsx +3 -0
  92. package/src/Banner/Banner.test.tsx +16 -7
  93. package/src/Banner/Banner.tsx +9 -2
  94. package/src/Button/Button.doc.mjs +6 -0
  95. package/src/Button/Button.test.tsx +105 -11
  96. package/src/Button/Button.tsx +73 -5
  97. package/src/Chat/ChatLayoutScrollButton.tsx +7 -1
  98. package/src/Collapsible/useCollapsible.doc.mjs +2 -2
  99. package/src/ContextMenu/ContextMenu.tsx +2 -2
  100. package/src/DateInput/DateInput.test.tsx +68 -20
  101. package/src/Divider/Divider.doc.mjs +1 -1
  102. package/src/DropdownMenu/DropdownMenu.tsx +2 -2
  103. package/src/DropdownMenu/{renderXDSDropdownItems.tsx → renderDropdownItems.tsx} +2 -2
  104. package/src/EmptyState/EmptyState.test.tsx +4 -2
  105. package/src/EmptyState/EmptyState.tsx +6 -2
  106. package/src/FormLayout/FormLayout.doc.mjs +3 -3
  107. package/src/HoverCard/HoverCard.doc.mjs +3 -0
  108. package/src/HoverCard/HoverCard.test.tsx +178 -2
  109. package/src/HoverCard/HoverCard.tsx +20 -16
  110. package/src/HoverCard/useHoverCard.tsx +12 -10
  111. package/src/Icon/Icon.doc.mjs +12 -4
  112. package/src/Item/Item.doc.mjs +2 -2
  113. package/src/Kbd/Kbd.tsx +2 -9
  114. package/src/Layer/useLayer.doc.mjs +7 -2
  115. package/src/Layer/useLayer.tsx +19 -2
  116. package/src/Layout/Layout.doc.mjs +2 -1
  117. package/src/Layout/Layout.tsx +15 -1
  118. package/src/Layout/__tests__/childrenAsContent.test.tsx +59 -0
  119. package/src/Lightbox/Lightbox.doc.mjs +0 -2
  120. package/src/Link/Link.doc.mjs +3 -3
  121. package/src/Link/LinkProvider.doc.mjs +3 -3
  122. package/src/Markdown/Markdown.doc.mjs +6 -4
  123. package/src/Markdown/Markdown.test.tsx +17 -26
  124. package/src/Markdown/Markdown.tsx +16 -6
  125. package/src/MobileNav/MobileNav.doc.mjs +8 -8
  126. package/src/MobileNav/MobileNav.tsx +13 -0
  127. package/src/MobileNav/MobileNavReopen.test.tsx +118 -0
  128. package/src/Outline/Outline.doc.mjs +1 -1
  129. package/src/Outline/Outline.test.tsx +76 -38
  130. package/src/Outline/Outline.tsx +23 -4
  131. package/src/Outline/useScrollSpy.ts +196 -63
  132. package/src/Pagination/Pagination.test.tsx +137 -13
  133. package/src/Pagination/Pagination.tsx +33 -28
  134. package/src/Resizable/Resizable.doc.mjs +3 -3
  135. package/src/Resizable/useResizable.ts +1 -7
  136. package/src/Selector/Selector.doc.mjs +4 -0
  137. package/src/Selector/Selector.tsx +5 -6
  138. package/src/Skeleton/Skeleton.doc.mjs +11 -1
  139. package/src/Slider/Slider.doc.mjs +4 -0
  140. package/src/Slider/Slider.test.tsx +3 -16
  141. package/src/Slider/Slider.tsx +1 -1
  142. package/src/Table/BaseTable.tsx +50 -24
  143. package/src/Table/Table.doc.mjs +15 -3
  144. package/src/Table/Table.tsx +22 -1
  145. package/src/Table/index.ts +3 -0
  146. package/src/Table/plugins/stickyColumns/index.ts +4 -0
  147. package/src/Table/plugins/stickyColumns/useTableStickyColumns.test.tsx +163 -0
  148. package/src/Table/plugins/stickyColumns/useTableStickyColumns.tsx +414 -0
  149. package/src/Table/types.ts +96 -4
  150. package/src/Table/useBaseTablePlugins.ts +1 -0
  151. package/src/Text/Heading.doc.mjs +13 -0
  152. package/src/Timestamp/Timestamp.test.tsx +44 -18
  153. package/src/Timestamp/Timestamp.tsx +30 -10
  154. package/src/ToggleButton/ToggleButton.doc.mjs +4 -4
  155. package/src/ToggleButton/ToggleButton.test.tsx +172 -9
  156. package/src/ToggleButton/ToggleButton.tsx +57 -31
  157. package/src/Toolbar/Toolbar.doc.mjs +1 -1
  158. package/src/hooks/useEntryAnimation.doc.mjs +3 -3
  159. package/src/hooks/useMediaQuery.doc.mjs +2 -2
  160. package/src/hooks/useStreamingText.doc.mjs +3 -3
  161. package/src/theme/Theme.doc.mjs +2 -2
  162. package/src/theme/Theme.tsx +1 -1
  163. package/src/theme/defineTheme.ts +1 -1
  164. package/src/theme/index.ts +1 -1
  165. package/src/theme/syntax/defineSyntaxTheme.ts +1 -1
  166. package/src/theme/tokens.ts +4 -4
  167. package/src/theme/useTheme.ts +2 -2
  168. package/src/utils/dateParser.test.ts +26 -0
  169. package/src/utils/dateParser.ts +16 -2
  170. package/dist/DropdownMenu/renderXDSDropdownItems.d.ts.map +0 -1
@@ -7,6 +7,14 @@ export const docs = {
7
7
  displayName: 'Icon',
8
8
  category: 'Content',
9
9
  keywords: ["icon","svg","glyph","symbol","pictogram","graphic","vector"],
10
+ playground: {
11
+ // `icon` is required and its type can't be auto-generated, so the
12
+ // properties-tab preview showed "Missing: icon". Seed a valid semantic
13
+ // icon name so the interactive preview renders.
14
+ defaults: {
15
+ icon: 'search',
16
+ },
17
+ },
10
18
  props: [
11
19
  {
12
20
  name: 'icon',
@@ -17,7 +25,7 @@ export const docs = {
17
25
  {
18
26
  name: 'color',
19
27
  type: "'primary' | 'secondary' | 'tertiary' | 'disabled' | 'accent' | 'success' | 'error' | 'warning' | 'inherit'",
20
- description: 'Color variant mapped to XDS icon color tokens.',
28
+ description: 'Color variant mapped to Astryx icon color tokens.',
21
29
  default: "'inherit'",
22
30
  },
23
31
  {
@@ -62,7 +70,7 @@ export const docsZh = {
62
70
  {
63
71
  name: 'color',
64
72
  type: "'primary' | 'secondary' | 'tertiary' | 'disabled' | 'accent' | 'success' | 'error' | 'warning' | 'inherit'",
65
- description: '映射到 XDS 图标颜色令牌的颜色变体。',
73
+ description: '映射到 Astryx 图标颜色令牌的颜色变体。',
66
74
  default: "'inherit'",
67
75
  },
68
76
  {
@@ -96,7 +104,7 @@ export const docsZh = {
96
104
  /** @type {import('../docs-types').TranslationDoc} */
97
105
  export const docsDense = {
98
106
  description:
99
- 'Renders icons w/ XDS design system colors + sizes. Supports direct SVG icon components + semantic icon names that adapt to active theme.',
107
+ 'Renders icons w/ Astryx design system colors + sizes. Supports direct SVG icon components + semantic icon names that adapt to active theme.',
100
108
  usage: {
101
109
  description: 'Icons are small visual symbols that represent actions, objects, or concepts. They improve scannability and reinforce meaning alongside text. Supports both direct SVG components and semantic icon names that adapt to the active theme.',
102
110
  bestPractices: [
@@ -113,7 +121,7 @@ export const docsDense = {
113
121
  },
114
122
  propDescriptions: {
115
123
  icon: 'Semantic icon name or SVG component. Valid names: close, chevronDown, chevronLeft, chevronRight, check, success, error, warning, info, calendar, clock, externalLink, menu, moreHorizontal, search, arrowUp, arrowDown, arrowsUpDown, funnel, eyeSlash, viewColumns, copy, checkDouble, wrench, stop, microphone. For others, pass an SVG component.',
116
- color: 'Color variant mapped to XDS icon color tokens.',
124
+ color: 'Color variant mapped to Astryx icon color tokens.',
117
125
  size: 'Icon size.',
118
126
  },
119
127
  };
@@ -51,7 +51,7 @@ export const docs = {
51
51
  ],
52
52
  usage: {
53
53
  description:
54
- 'A single, flexible item primitive that unifies the "start content + label + description + end content" pattern across XDS. Use it wherever you need a structured row: dropdown menus, selectors, contact lists, notifications, file browsers, and activity feeds.',
54
+ 'A single, flexible item primitive that unifies the "start content + label + description + end content" pattern across Astryx. Use it wherever you need a structured row: dropdown menus, selectors, contact lists, notifications, file browsers, and activity feeds.',
55
55
  bestPractices: [
56
56
  {guidance: true, description: 'Use named slots (startContent, label, description, endContent) for the common layout. These cover the 80% case.'},
57
57
  {guidance: true, description: 'Use density="compact" for menus and dense lists, "balanced" for standard rows, and "spacious" for roomier layouts.'},
@@ -104,7 +104,7 @@ export const docsZh = {
104
104
  ],
105
105
  usage: {
106
106
  description:
107
- '通用项目原语,统一 XDS 中 "起始内容 + 标签 + 描述 + 结束内容" 的布局模式。适用于下拉菜单、选择器、联系人列表、通知、文件浏览器和活动流等场景。',
107
+ '通用项目原语,统一 Astryx 中 "起始内容 + 标签 + 描述 + 结束内容" 的布局模式。适用于下拉菜单、选择器、联系人列表、通知、文件浏览器和活动流等场景。',
108
108
  bestPractices: [
109
109
  {guidance: true, description: '使用命名插槽(startContent、label、description、endContent)处理常见布局。'},
110
110
  {guidance: true, description: '菜单和密集列表使用 density="compact",标准行使用 "balanced",宽松布局使用 "spacious"。'},
package/src/Kbd/Kbd.tsx CHANGED
@@ -45,7 +45,7 @@ const styles = stylex.create({
45
45
  backgroundColor: colorVars['--color-neutral'],
46
46
  borderBottomWidth: '2px',
47
47
  borderBottomStyle: 'solid',
48
- borderBottomColor: colorVars['--color-border'],
48
+ borderBottomColor: colorVars['--color-border-emphasized'],
49
49
  color: colorVars['--color-text-secondary'],
50
50
  fontFamily: typographyVars['--font-family-body'],
51
51
  fontSize: typeScaleVars['--text-supporting-size'],
@@ -146,14 +146,7 @@ export interface KbdProps extends BaseProps<HTMLSpanElement> {
146
146
  * <Kbd keys="mod+k" />
147
147
  * ```
148
148
  */
149
- export function Kbd({
150
- keys,
151
- ref,
152
- xstyle,
153
- className,
154
- style,
155
- ...rest
156
- }: KbdProps) {
149
+ export function Kbd({keys, ref, xstyle, className, style, ...rest}: KbdProps) {
157
150
  const isMac = useSyncExternalStore(
158
151
  subscribeToPlatformChanges,
159
152
  detectMac,
@@ -78,7 +78,7 @@ export const docs = {
78
78
  name: 'render',
79
79
  type: '(children: ReactNode, props: ContextRenderProps | FixedRenderProps) => ReactNode',
80
80
  description:
81
- 'Render function for the popover element. Pass placement/alignment in context mode or x/y in fixed mode.',
81
+ 'Render function for the popover element. Pass placement/alignment in context mode or x/y in fixed mode. In context mode, pass `as: "span"` to render an inline-safe layer (e.g. inside a paragraph). The layer renders inline in the React tree — the Popover API promotes it to the top layer when shown, so it escapes ancestor clipping and stacking without a portal.',
82
82
  },
83
83
  ],
84
84
  usage: {
@@ -95,6 +95,11 @@ export const docs = {
95
95
  description:
96
96
  'Build on higher-level components like Popover, HoverCard, and Tooltip for common overlay patterns.',
97
97
  },
98
+ {
99
+ guidance: true,
100
+ description:
101
+ 'Rely on the Popover API top layer to escape ancestor clipping and stacking — render the layer inline (no portal) so it inherits the trigger\u2019s theme cascade and keeps a natural focus order. Use `as: "span"` when the layer must be valid inside inline contexts like a paragraph.',
102
+ },
98
103
  {
99
104
  guidance: false,
100
105
  description:
@@ -125,7 +130,7 @@ export const docsDense = {
125
130
  hide: 'hide layer.',
126
131
  isOpen: 'whether layer is open.',
127
132
  id: 'unique ARIA id.',
128
- render: 'renders popover element; pass placement/alignment or x/y.',
133
+ render: 'renders popover element; pass placement/alignment or x/y. Context mode accepts `as: "span"` for inline-safe layers. Renders inline; the Popover API top layer escapes clipping/stacking without a portal.',
129
134
  },
130
135
  usage: {
131
136
  description:
@@ -79,6 +79,19 @@ export interface ContextRenderProps {
79
79
  * Merged after StyleX and anchor positioning styles.
80
80
  */
81
81
  style?: React.CSSProperties;
82
+ /**
83
+ * HTML tag to render the popover container as.
84
+ *
85
+ * Defaults to `'div'`. Pass `'span'` when the layer must render inline-safe
86
+ * markup — e.g. a `HoverCard` wrapping inline text inside a `<p>`. A `<span>`
87
+ * is phrasing content, so it stays put in the DOM tree instead of being
88
+ * reparented out of a paragraph by the HTML parser, which keeps server and
89
+ * client markup identical. The Popover API and CSS anchor positioning work
90
+ * the same on either tag.
91
+ *
92
+ * @default 'div'
93
+ */
94
+ as?: 'div' | 'span';
82
95
  }
83
96
 
84
97
  /**
@@ -369,6 +382,7 @@ export function useLayer(
369
382
  xstyle,
370
383
  className: extraClassName,
371
384
  style: extraStyle,
385
+ as: Container = 'div',
372
386
  } = props || {};
373
387
 
374
388
  // CSS anchor positioning (dynamic, not in StyleX)
@@ -383,15 +397,18 @@ export function useLayer(
383
397
  ? `${extraClassName} ${stylexResult.className ?? ''}`
384
398
  : stylexResult.className;
385
399
 
400
+ // Render as the requested tag. A `span` keeps the layer phrasing content
401
+ // so it is valid (and stays put on hydration) inside inline contexts like
402
+ // a `<p>`; `div` remains the default for block layers.
386
403
  return (
387
- <div
404
+ <Container
388
405
  ref={popoverRefCallback}
389
406
  id={id}
390
407
  popover={lightDismiss ? 'auto' : 'manual'}
391
408
  className={combinedClassName}
392
409
  style={{...stylexResult.style, ...anchorStyle, ...extraStyle}}>
393
410
  {children}
394
- </div>
411
+ </Container>
395
412
  );
396
413
  },
397
414
  [anchorId, id, lightDismiss, popoverRefCallback],
@@ -29,7 +29,8 @@ export const docs = {
29
29
  {
30
30
  name: 'content',
31
31
  type: 'ReactNode',
32
- description: 'Main content area (center).',
32
+ description:
33
+ 'Main content area (center). Children passed to `<Layout>` render here too — `<Layout>{main}</Layout>` is shorthand for `<Layout content={main} />`.',
33
34
  slotElements: [
34
35
  {
35
36
  __element: 'LayoutContent',
@@ -157,6 +157,16 @@ export interface LayoutProps extends Omit<BaseProps, 'content'> {
157
157
  * Inline styles to apply to the root element.
158
158
  */
159
159
  style?: React.CSSProperties;
160
+
161
+ /**
162
+ * Children are a shorthand for the `content` slot:
163
+ * `<Layout>{main}</Layout>` is equivalent to `<Layout content={main} />`.
164
+ * The surrounding zones (`header`/`start`/`end`/`footer`) stay explicit
165
+ * props. If both `content` and `children` are provided, `content` wins.
166
+ * Accepting children keeps the natural `<Layout>…</Layout>` form from
167
+ * rendering a blank shell.
168
+ */
169
+ children?: ReactNode;
160
170
  }
161
171
 
162
172
  /**
@@ -222,6 +232,7 @@ function AreaProvider({
222
232
  * ```
223
233
  */
224
234
  export function Layout({
235
+ children,
225
236
  content,
226
237
  contentWidth,
227
238
  defaultHasDividers,
@@ -237,6 +248,9 @@ export function Layout({
237
248
  style,
238
249
  }: LayoutProps) {
239
250
  const isFill = height === 'fill';
251
+ // Children are a shorthand for the content slot; an explicit `content` prop
252
+ // wins when both are provided.
253
+ const resolvedContent = content ?? children;
240
254
 
241
255
  const dividerCtxValue = useMemo(
242
256
  () => (defaultHasDividers != null ? {defaultHasDividers} : null),
@@ -287,7 +301,7 @@ export function Layout({
287
301
  )}>
288
302
  <AreaProvider area="start">{start}</AreaProvider>
289
303
  <div {...stylex.props(...stackItem({size: 'fill'}))}>
290
- <AreaProvider area="content">{content}</AreaProvider>
304
+ <AreaProvider area="content">{resolvedContent}</AreaProvider>
291
305
  </div>
292
306
  <AreaProvider area="end">{end}</AreaProvider>
293
307
  </div>
@@ -0,0 +1,59 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file childrenAsContent.test.tsx
5
+ * @input Uses vitest, @testing-library/react
6
+ * @output Verifies Layout renders children as a shorthand for the content slot
7
+ * (so the natural `<Layout>…</Layout>` form never renders a blank shell), with
8
+ * an explicit `content` prop taking precedence.
9
+ */
10
+
11
+ import {describe, it, expect} from 'vitest';
12
+ import {render, screen} from '@testing-library/react';
13
+ import {Layout} from '../Layout';
14
+ import {LayoutContent} from '../LayoutContent';
15
+
16
+ describe('Layout children-as-content', () => {
17
+ it('renders nested children in the content slot', () => {
18
+ render(
19
+ <Layout>
20
+ <LayoutContent>
21
+ <span data-testid="body">Body</span>
22
+ </LayoutContent>
23
+ </Layout>,
24
+ );
25
+ expect(screen.getByTestId('body')).toBeInTheDocument();
26
+ });
27
+
28
+ it('renders bare children (no LayoutContent wrapper) too', () => {
29
+ render(
30
+ <Layout>
31
+ <span data-testid="bare">Bare</span>
32
+ </Layout>,
33
+ );
34
+ expect(screen.getByTestId('bare')).toBeInTheDocument();
35
+ });
36
+
37
+ it('lets an explicit content prop win over children', () => {
38
+ render(
39
+ <Layout content={<span data-testid="slot">Slot</span>}>
40
+ <span data-testid="child">Child</span>
41
+ </Layout>,
42
+ );
43
+ expect(screen.getByTestId('slot')).toBeInTheDocument();
44
+ expect(screen.queryByTestId('child')).not.toBeInTheDocument();
45
+ });
46
+
47
+ it('still supports the canonical slot-only API', () => {
48
+ render(
49
+ <Layout
50
+ content={
51
+ <LayoutContent>
52
+ <span data-testid="canon">Canonical</span>
53
+ </LayoutContent>
54
+ }
55
+ />,
56
+ );
57
+ expect(screen.getByTestId('canon')).toBeInTheDocument();
58
+ });
59
+ });
@@ -30,7 +30,6 @@ export const docs = {
30
30
  name: 'index',
31
31
  type: 'number',
32
32
  description: 'Current index in gallery mode (when media is an array).',
33
- default: '0',
34
33
  },
35
34
  {
36
35
  name: 'onIndexChange',
@@ -94,7 +93,6 @@ export const docsZh = {
94
93
  name: 'index',
95
94
  type: 'number',
96
95
  description: '画廊模式中当前索引。',
97
- default: '0',
98
96
  },
99
97
  {
100
98
  name: 'onIndexChange',
@@ -99,7 +99,7 @@ export const docs = {
99
99
  isHiddenFromOverview: true,
100
100
  displayName: 'Link Provider',
101
101
  description:
102
- 'Provider that sets the default link component for all XDS link-rendering components in the subtree. ' +
102
+ 'Provider that sets the default link component for all Astryx link-rendering components in the subtree. ' +
103
103
  'Wrap your app root to replace native <a> elements with your framework router (Next.js Link, React Router Link, etc.).',
104
104
  props: [
105
105
  {
@@ -226,7 +226,7 @@ export const docsZh = {
226
226
  isHiddenFromOverview: true,
227
227
  displayName: 'Link Provider',
228
228
  description:
229
- '为子树中所有 XDS 链接组件设置默认链接组件的 Provider。',
229
+ '为子树中所有 Astryx 链接组件设置默认链接组件的 Provider。',
230
230
  props: [
231
231
  {
232
232
  name: 'component',
@@ -310,7 +310,7 @@ export const docsDense = {
310
310
  isHiddenFromOverview: true,
311
311
  displayName: 'Link Provider',
312
312
  description:
313
- 'Provider setting default link component for all XDS links in subtree.',
313
+ 'Provider setting default link component for all Astryx links in subtree.',
314
314
  propDescriptions: {
315
315
  component: 'Component for all link elements',
316
316
  children: 'Subtree',
@@ -10,7 +10,7 @@ export const docs = {
10
10
  isHiddenFromOverview: true,
11
11
  keywords: ['link', 'provider', 'router', 'nextjs', 'client-side-routing'],
12
12
  usage: {
13
- description: 'Wraps your app to replace the default <a> tag with a framework-specific link component (e.g. Next.js Link) for client-side routing across all XDS components.',
13
+ description: 'Wraps your app to replace the default <a> tag with a framework-specific link component (e.g. Next.js Link) for client-side routing across all Astryx components.',
14
14
  },
15
15
  props: [
16
16
  {name: 'component', type: 'LinkComponentType', required: true, description: 'Link component to use for all link elements in the subtree (e.g. Next.js Link).'},
@@ -20,9 +20,9 @@ export const docs = {
20
20
 
21
21
  /** @type {import('../docs-types').TranslationDoc} */
22
22
  export const docsDense = {
23
- description: 'Wraps app to replace default <a> tag w/ framework-specific link component (e.g. Next.js Link) for client-side routing across all XDS components.',
23
+ description: 'Wraps app to replace default <a> tag w/ framework-specific link component (e.g. Next.js Link) for client-side routing across all Astryx components.',
24
24
  usage: {
25
- description: 'Wraps app to replace default <a> tag w/ framework-specific link component (e.g. Next.js Link) for client-side routing across all XDS components.',
25
+ description: 'Wraps app to replace default <a> tag w/ framework-specific link component (e.g. Next.js Link) for client-side routing across all Astryx components.',
26
26
  },
27
27
  propDescriptions: {
28
28
  component: 'link component for all link elements in subtree (e.g. Next.js Link)',
@@ -75,6 +75,7 @@ export const docs = {
75
75
  type: 'number | string',
76
76
  description:
77
77
  'Max width for prose content (paragraphs, headings, lists, blockquotes). Tables and code blocks are unconstrained and can expand to the full container width. Use for readable line lengths in wide layouts.',
78
+ default: '680',
78
79
  },
79
80
  {
80
81
  name: 'contentAlign',
@@ -131,7 +132,7 @@ export const docs = {
131
132
  },
132
133
  usage: {
133
134
  description:
134
- 'Renders a markdown string as XDS-styled components. Use Markdown for user-generated content, AI responses, and documentation; it handles headings, lists, tables, code blocks, and citations with consistent styling.',
135
+ 'Renders a markdown string as Astryx-styled components. Use Markdown for user-generated content, AI responses, and documentation; it handles headings, lists, tables, code blocks, and citations with consistent styling.',
135
136
  bestPractices: [
136
137
  { guidance: true, description: 'Set headingLevelStart to match the page hierarchy, e.g. start at 3 if the markdown sits inside an h2 section.' },
137
138
  { guidance: true, description: 'Use contentWidth to keep prose at a readable line length in wide layouts.' },
@@ -245,6 +246,7 @@ export const docsZh = {
245
246
  type: 'number | string',
246
247
  description:
247
248
  '正文内容的最大宽度(段落、标题、列表、引用块)。表格和代码块不受限制,可扩展到完整容器宽度。用于在宽布局中保持可读行长。',
249
+ default: '680',
248
250
  },
249
251
  {
250
252
  name: 'contentAlign',
@@ -294,7 +296,7 @@ export const docsZh = {
294
296
  },
295
297
  usage: {
296
298
  description:
297
- 'Renders a markdown string as XDS-styled components. Use Markdown for user-generated content, AI responses, and documentation; it handles headings, lists, tables, code blocks, and citations with consistent styling.',
299
+ 'Renders a markdown string as Astryx-styled components. Use Markdown for user-generated content, AI responses, and documentation; it handles headings, lists, tables, code blocks, and citations with consistent styling.',
298
300
  bestPractices: [
299
301
  { guidance: true, description: 'Set headingLevelStart to match the page hierarchy, e.g. start at 3 if the markdown sits inside an h2 section.' },
300
302
  { guidance: true, description: 'Use contentWidth to keep prose at a readable line length in wide layouts.' },
@@ -306,10 +308,10 @@ export const docsZh = {
306
308
 
307
309
  export const docsDense = {
308
310
  description:
309
- 'Renders markdown string as XDS-styled components. Use for user-generated content, AI responses, docs. Headings, lists, tables, code, citations w/ consistent styling.',
311
+ 'Renders markdown string as Astryx-styled components. Use for user-generated content, AI responses, docs. Headings, lists, tables, code, citations w/ consistent styling.',
310
312
  usage: {
311
313
  description:
312
- 'Renders a markdown string as XDS-styled components. Use Markdown for user-generated content, AI responses, and documentation; it handles headings, lists, tables, code blocks, and citations with consistent styling.',
314
+ 'Renders a markdown string as Astryx-styled components. Use Markdown for user-generated content, AI responses, and documentation; it handles headings, lists, tables, code blocks, and citations with consistent styling.',
313
315
  bestPractices: [
314
316
  { guidance: true, description: 'Set headingLevelStart to match the page hierarchy, e.g. start at 3 if the markdown sits inside an h2 section.' },
315
317
  { guidance: true, description: 'Use contentWidth to keep prose at a readable line length in wide layouts.' },
@@ -22,9 +22,16 @@ describe('Markdown', () => {
22
22
  expect(screen.getByText('Heading 2').tagName).toBe('H2');
23
23
  });
24
24
 
25
- it('renders paragraphs', () => {
25
+ it('renders paragraphs as block <div> (never <p>) for composition safety', () => {
26
26
  render(<Markdown>{'Hello world'}</Markdown>);
27
- expect(screen.getByText('Hello world').tagName).toBe('P');
27
+ // Markdown paragraphs render as <div> so block-level inline content
28
+ // (images, custom inline components) never trips the phrasing-content
29
+ // trap that a <p> would impose. role="paragraph" re-exposes the paragraph
30
+ // role to assistive tech without the <p> hazard. Consumers who want a real
31
+ // <p> element can pass `components={{paragraph: 'p'}}`.
32
+ const para = screen.getByText('Hello world');
33
+ expect(para.tagName).toBe('DIV');
34
+ expect(para).toHaveAttribute('role', 'paragraph');
28
35
  });
29
36
 
30
37
  it('renders inline display without block wrappers', () => {
@@ -201,9 +208,7 @@ describe('Markdown', () => {
201
208
  });
202
209
 
203
210
  it('shows streaming cursor when isStreaming is true', () => {
204
- const {container} = render(
205
- <Markdown isStreaming>{'Hello'}</Markdown>,
206
- );
211
+ const {container} = render(<Markdown isStreaming>{'Hello'}</Markdown>);
207
212
  // Streaming mode parses incrementally but no cursor element
208
213
  expect(container.querySelector('[role="document"]')).toBeInTheDocument();
209
214
  });
@@ -243,9 +248,7 @@ describe('Markdown', () => {
243
248
 
244
249
  it('sanitizes data: URLs in images', () => {
245
250
  const {container} = render(
246
- <Markdown>
247
- {'![xss](data:text/html,<script>alert(1)</script>)'}
248
- </Markdown>,
251
+ <Markdown>{'![xss](data:text/html,<script>alert(1)</script>)'}</Markdown>,
249
252
  );
250
253
  const img = container.querySelector('img');
251
254
  expect(img).toBeNull();
@@ -435,9 +438,7 @@ describe('inlinePlugins', () => {
435
438
  },
436
439
  };
437
440
  const {container} = render(
438
- <Markdown inlinePlugins={[plugin]}>
439
- {'See TAG:important here'}
440
- </Markdown>,
441
+ <Markdown inlinePlugins={[plugin]}>{'See TAG:important here'}</Markdown>,
441
442
  );
442
443
  const tag = container.querySelector('[data-testid="tag-match"]');
443
444
  expect(tag).toBeInTheDocument();
@@ -446,9 +447,7 @@ describe('inlinePlugins', () => {
446
447
 
447
448
  it('renders identically when no inlinePlugins are provided', () => {
448
449
  const withPlugins = render(
449
- <Markdown inlinePlugins={[]}>
450
- {'Hello **world** and `code`'}
451
- </Markdown>,
450
+ <Markdown inlinePlugins={[]}>{'Hello **world** and `code`'}</Markdown>,
452
451
  );
453
452
  const withoutPlugins = render(
454
453
  <Markdown>{'Hello **world** and `code`'}</Markdown>,
@@ -481,9 +480,7 @@ describe('inlinePlugins', () => {
481
480
 
482
481
  it('renders bare https URLs as links when autolink="gfm"', () => {
483
482
  const {container} = render(
484
- <Markdown autolink="gfm">
485
- {'see https://example.com here'}
486
- </Markdown>,
483
+ <Markdown autolink="gfm">{'see https://example.com here'}</Markdown>,
487
484
  );
488
485
  const link = container.querySelector('a');
489
486
  expect(link).not.toBeNull();
@@ -503,9 +500,7 @@ describe('inlinePlugins', () => {
503
500
 
504
501
  it('renders bare emails with mailto: href', () => {
505
502
  const {container} = render(
506
- <Markdown autolink="gfm">
507
- {'ping user@example.com please'}
508
- </Markdown>,
503
+ <Markdown autolink="gfm">{'ping user@example.com please'}</Markdown>,
509
504
  );
510
505
  const link = container.querySelector('a');
511
506
  expect(link).not.toBeNull();
@@ -515,9 +510,7 @@ describe('inlinePlugins', () => {
515
510
 
516
511
  it('does not autolink URLs inside code spans', () => {
517
512
  const {container} = render(
518
- <Markdown autolink="gfm">
519
- {'try `https://example.com` here'}
520
- </Markdown>,
513
+ <Markdown autolink="gfm">{'try `https://example.com` here'}</Markdown>,
521
514
  );
522
515
  expect(container.querySelector('a')).toBeNull();
523
516
  expect(container.querySelector('code')).not.toBeNull();
@@ -525,9 +518,7 @@ describe('inlinePlugins', () => {
525
518
 
526
519
  it('does not autolink URLs inside code blocks', () => {
527
520
  const {container} = render(
528
- <Markdown autolink="gfm">
529
- {'```\nhttps://example.com\n```'}
530
- </Markdown>,
521
+ <Markdown autolink="gfm">{'```\nhttps://example.com\n```'}</Markdown>,
531
522
  );
532
523
  expect(container.querySelector('a')).toBeNull();
533
524
  expect(container.querySelector('pre')).not.toBeNull();
@@ -1113,9 +1113,19 @@ function renderBlock(
1113
1113
  if (ParagraphComp) {
1114
1114
  return <ParagraphComp key={index}>{paraChildren}</ParagraphComp>;
1115
1115
  }
1116
+ // Markdown paragraphs render as <div>, not <p>: inline content can
1117
+ // include block-level nodes (images, custom inline components), and a
1118
+ // <p> would reparent them, desyncing SSR markup from the hydrated DOM.
1119
+ // Block spacing comes from token-based StyleX margins, so the rendered
1120
+ // appearance is unchanged. role="paragraph" re-exposes the paragraph
1121
+ // role in the accessibility tree (a pure ARIA hint — it does not trigger
1122
+ // the parser's block-child reparenting) so prose semantics are preserved
1123
+ // without the <p> composition hazard. Consumers who want a real <p>
1124
+ // element can still pass components={{paragraph: 'p'}}.
1116
1125
  return (
1117
- <p
1126
+ <div
1118
1127
  key={index}
1128
+ role="paragraph"
1119
1129
  {...stylex.props(
1120
1130
  spacing,
1121
1131
  contentWidthValue != null
@@ -1128,7 +1138,7 @@ function renderBlock(
1128
1138
  isLast && styles.noMarginBlockEnd,
1129
1139
  )}>
1130
1140
  {paraChildren}
1131
- </p>
1141
+ </div>
1132
1142
  );
1133
1143
  }
1134
1144
  case 'codeblock': {
@@ -1487,7 +1497,7 @@ function renderBlock(
1487
1497
  const safeSrc = sanitizeUrl(node.src);
1488
1498
  if (safeSrc == null) {
1489
1499
  return (
1490
- <p
1500
+ <div
1491
1501
  key={index}
1492
1502
  {...stylex.props(
1493
1503
  spacing,
@@ -1495,11 +1505,11 @@ function renderBlock(
1495
1505
  isLast && styles.noMarginBlockEnd,
1496
1506
  )}>
1497
1507
  [{node.alt}]
1498
- </p>
1508
+ </div>
1499
1509
  );
1500
1510
  }
1501
1511
  return (
1502
- <p
1512
+ <div
1503
1513
  key={index}
1504
1514
  {...stylex.props(
1505
1515
  spacing,
@@ -1507,7 +1517,7 @@ function renderBlock(
1507
1517
  isLast && styles.noMarginBlockEnd,
1508
1518
  )}>
1509
1519
  <img src={safeSrc} alt={node.alt} {...stylex.props(styles.image)} />
1510
- </p>
1520
+ </div>
1511
1521
  );
1512
1522
  }
1513
1523
  }
@@ -43,14 +43,14 @@ export const docs = {
43
43
  type: 'number',
44
44
  description:
45
45
  'Drawer width in pixels. Capped at 85vw to prevent overflow on small screens.',
46
- default: '280',
46
+ default: '320',
47
47
  },
48
48
  {
49
49
  name: 'side',
50
- type: "'start' | 'end'",
50
+ type: "'start' | 'end' | 'auto'",
51
51
  description:
52
- 'Which side the drawer slides from. Start is left in LTR, right in RTL.',
53
- default: "'start'",
52
+ 'Which side the drawer slides from. Start is left in LTR, right in RTL. Auto picks a side based on the trigger position.',
53
+ default: "'auto'",
54
54
  },
55
55
  ],
56
56
  },
@@ -124,14 +124,14 @@ export const docsZh = {
124
124
  type: 'number',
125
125
  description:
126
126
  '抽屉宽度(像素)。上限为 85vw 以防止在小屏幕上溢出。',
127
- default: '280',
127
+ default: '320',
128
128
  },
129
129
  {
130
130
  name: 'side',
131
- type: "'start' | 'end'",
131
+ type: "'start' | 'end' | 'auto'",
132
132
  description:
133
- '抽屉滑出的方向。在 LTR 布局中 start 为左侧,在 RTL 布局中为右侧。',
134
- default: "'start'",
133
+ '抽屉滑出的方向。在 LTR 布局中 start 为左侧,在 RTL 布局中为右侧。auto 根据触发元素的位置自动选择方向。',
134
+ default: "'auto'",
135
135
  },
136
136
  ],
137
137
  theming: {
@@ -363,8 +363,21 @@ export function MobileNav({
363
363
  return () => {
364
364
  if (closeTimeoutRef.current) {
365
365
  clearTimeout(closeTimeoutRef.current);
366
+ closeTimeoutRef.current = null;
366
367
  }
367
368
  document.documentElement.style.overflow = '';
369
+ // Close the native dialog on teardown if it's still open. Inside AppShell
370
+ // the drawer is mounted in an <Activity> that switches to mode="hidden"
371
+ // when the drawer closes; React then runs this cleanup (with a stale
372
+ // isOpen) instead of re-running the effect with isOpen=false, so the
373
+ // close branch above never fires. If we leave the <dialog> `open` here,
374
+ // showModal() is skipped on the next open (the dialog is already open in
375
+ // the hidden tree) and the drawer can never be re-opened. Closing it
376
+ // unconditionally on teardown keeps the native dialog state in sync so a
377
+ // subsequent open cleanly calls showModal() again.
378
+ if (dialog.open) {
379
+ dialog.close();
380
+ }
368
381
  };
369
382
  }, [isOpen, side]);
370
383