@astryxdesign/core 0.4.4 → 0.4.5-canary.1dc3e5d
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.
- package/CHANGELOG.md +39 -0
- package/dist/BottomSheet/BottomSheet.d.ts +11 -2
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheet.js +4 -0
- package/dist/BottomSheet/BottomSheetPanel.d.ts +5 -2
- package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetPanel.js +56 -13
- package/dist/BottomSheet/index.d.ts +1 -1
- package/dist/BottomSheet/index.d.ts.map +1 -1
- package/dist/BottomSheet/snapOffsets.d.ts +57 -20
- package/dist/BottomSheet/snapOffsets.d.ts.map +1 -1
- package/dist/BottomSheet/snapOffsets.js +106 -30
- package/dist/BottomSheet/useSheetGestures.d.ts +13 -8
- package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -1
- package/dist/BottomSheet/useSheetGestures.js +160 -64
- package/dist/Calendar/Calendar.d.ts +17 -0
- package/dist/Calendar/Calendar.d.ts.map +1 -1
- package/dist/Calendar/Calendar.js +34 -3
- package/dist/Calendar/getStandaloneShortWeekdayNames.d.ts +14 -0
- package/dist/Calendar/getStandaloneShortWeekdayNames.d.ts.map +1 -0
- package/dist/Calendar/getStandaloneShortWeekdayNames.js +26 -0
- package/dist/Calendar/hooks/useCalendarConstraints.d.ts +22 -1
- package/dist/Calendar/hooks/useCalendarConstraints.d.ts.map +1 -1
- package/dist/Calendar/hooks/useCalendarConstraints.js +26 -4
- package/dist/Calendar/hooks/useCalendarDays.d.ts.map +1 -1
- package/dist/Calendar/hooks/useCalendarDays.js +14 -10
- package/dist/Calendar/standaloneShortWeekdayNames.generated.d.ts +47 -0
- package/dist/Calendar/standaloneShortWeekdayNames.generated.d.ts.map +1 -0
- package/dist/Calendar/standaloneShortWeekdayNames.generated.js +40 -0
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +9 -0
- package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
- package/dist/ComplexSelector/ComplexSelector.js +6 -1
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +6 -1
- package/dist/DateRangeInput/DateRangeInput.d.ts +23 -1
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +38 -3
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +7 -2
- package/dist/Field/FieldLabel.d.ts +2 -1
- package/dist/Field/FieldLabel.d.ts.map +1 -1
- package/dist/Field/FieldLabel.js +21 -3
- package/dist/Field/InputClearButton.d.ts +2 -1
- package/dist/Field/InputClearButton.d.ts.map +1 -1
- package/dist/Field/InputClearButton.js +6 -1
- package/dist/FormLayout/FormLayout.d.ts +24 -2
- package/dist/FormLayout/FormLayout.d.ts.map +1 -1
- package/dist/FormLayout/FormLayout.js +7 -2
- package/dist/FormLayout/FormLayoutContext.d.ts +15 -2
- package/dist/FormLayout/FormLayoutContext.d.ts.map +1 -1
- package/dist/FormLayout/FormLayoutContext.js +19 -4
- package/dist/FormLayout/index.d.ts +1 -1
- package/dist/FormLayout/index.d.ts.map +1 -1
- package/dist/Markdown/Markdown.d.ts +8 -0
- package/dist/Markdown/Markdown.d.ts.map +1 -1
- package/dist/Markdown/Markdown.js +31 -3
- package/dist/Markdown/parser.d.ts +14 -2
- package/dist/Markdown/parser.d.ts.map +1 -1
- package/dist/Markdown/parser.js +50 -2
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +6 -1
- package/dist/NumberInput/NumberInput.d.ts.map +1 -1
- package/dist/NumberInput/NumberInput.js +6 -1
- package/dist/Outline/parseOutlineFromMarkdown.d.ts +2 -0
- package/dist/Outline/parseOutlineFromMarkdown.d.ts.map +1 -1
- package/dist/Outline/parseOutlineFromMarkdown.js +5 -31
- package/dist/RadioList/RadioList.d.ts.map +1 -1
- package/dist/RadioList/RadioList.js +12 -1
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +6 -1
- package/dist/StatusDot/StatusDot.d.ts +43 -3
- package/dist/StatusDot/StatusDot.d.ts.map +1 -1
- package/dist/StatusDot/StatusDot.js +42 -5
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +9 -0
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +7 -2
- package/dist/TextArea/TextArea.d.ts.map +1 -1
- package/dist/TextArea/TextArea.js +6 -1
- package/dist/TextInput/TextInput.d.ts.map +1 -1
- package/dist/TextInput/TextInput.js +6 -1
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +6 -1
- package/dist/hooks/useResolvedRequired.d.ts +19 -0
- package/dist/hooks/useResolvedRequired.d.ts.map +1 -0
- package/dist/hooks/useResolvedRequired.js +40 -0
- package/dist/utils/plainDate.d.ts +6 -0
- package/dist/utils/plainDate.d.ts.map +1 -1
- package/dist/utils/plainDate.js +12 -0
- package/locales/af-ZA.json +958 -0
- package/locales/ar-SA.json +998 -0
- package/locales/ca-ES.json +974 -0
- package/locales/cs-CZ.json +990 -0
- package/locales/da-DK.json +966 -0
- package/locales/de-DE.json +966 -0
- package/locales/el-GR.json +990 -0
- package/locales/en.json +4 -0
- package/locales/es-ES.json +978 -0
- package/locales/fi-FI.json +990 -0
- package/locales/fr-FR.json +928 -0
- package/locales/he-IL.json +990 -0
- package/locales/hu-HU.json +990 -0
- package/locales/it-IT.json +978 -0
- package/locales/ja-JP.json +998 -0
- package/locales/ko-KR.json +990 -0
- package/locales/nl-NL.json +954 -0
- package/locales/no-NO.json +978 -0
- package/locales/pl-PL.json +982 -0
- package/locales/pseudo.json +3 -0
- package/locales/pt-BR.json +982 -0
- package/locales/pt-PT.json +982 -0
- package/locales/ro-RO.json +982 -0
- package/locales/ru-RU.json +990 -0
- package/locales/sr-SP.json +990 -0
- package/locales/sv-SE.json +986 -0
- package/locales/tr-TR.json +990 -0
- package/locales/uk-UA.json +990 -0
- package/locales/vi-VN.json +986 -0
- package/locales/zh-CN.json +998 -0
- package/locales/zh-TW.json +998 -0
- package/package.json +3 -3
- package/src/BottomSheet/BottomSheet.doc.mjs +36 -3
- package/src/BottomSheet/BottomSheet.test.tsx +153 -2
- package/src/BottomSheet/BottomSheet.tsx +16 -2
- package/src/BottomSheet/BottomSheetPanel.tsx +67 -13
- package/src/BottomSheet/index.ts +5 -1
- package/src/BottomSheet/snapOffsets.test.ts +114 -47
- package/src/BottomSheet/snapOffsets.ts +119 -33
- package/src/BottomSheet/useSheetGestures.test.ts +113 -15
- package/src/BottomSheet/useSheetGestures.ts +196 -100
- package/src/Calendar/Calendar.doc.mjs +16 -0
- package/src/Calendar/Calendar.test.tsx +137 -0
- package/src/Calendar/Calendar.tsx +65 -2
- package/src/Calendar/getStandaloneShortWeekdayNames.test.ts +54 -0
- package/src/Calendar/getStandaloneShortWeekdayNames.ts +38 -0
- package/src/Calendar/hooks/useCalendarConstraints.ts +54 -3
- package/src/Calendar/hooks/useCalendarDays.ts +13 -10
- package/src/Calendar/standaloneShortWeekdayNames.generated.ts +50 -0
- package/src/CheckboxInput/CheckboxInput.tsx +6 -0
- package/src/ComplexSelector/ComplexSelector.tsx +3 -1
- package/src/DateInput/DateInput.tsx +3 -1
- package/src/DateRangeInput/DateRangeInput.doc.mjs +16 -0
- package/src/DateRangeInput/DateRangeInput.test.tsx +81 -1
- package/src/DateRangeInput/DateRangeInput.tsx +72 -1
- package/src/DateTimeInput/DateTimeInput.tsx +4 -2
- package/src/Field/Field.doc.mjs +1 -0
- package/src/Field/FieldLabel.tsx +20 -4
- package/src/Field/InputClearButton.test.tsx +25 -0
- package/src/Field/InputClearButton.tsx +4 -1
- package/src/FormLayout/FormLayout.doc.mjs +13 -0
- package/src/FormLayout/FormLayout.test.tsx +181 -4
- package/src/FormLayout/FormLayout.tsx +33 -2
- package/src/FormLayout/FormLayoutContext.ts +22 -7
- package/src/FormLayout/index.ts +1 -1
- package/src/Markdown/Markdown.doc.mjs +3 -0
- package/src/Markdown/Markdown.test.tsx +78 -0
- package/src/Markdown/Markdown.tsx +41 -1
- package/src/Markdown/parser.ts +60 -2
- package/src/MultiSelector/MultiSelector.tsx +3 -1
- package/src/NumberInput/NumberInput.tsx +3 -1
- package/src/Outline/Outline.doc.mjs +2 -0
- package/src/Outline/parseOutlineFromMarkdown.ts +10 -40
- package/src/RadioList/RadioList.tsx +9 -1
- package/src/Selector/Selector.tsx +3 -1
- package/src/StatusDot/StatusDot.doc.mjs +13 -0
- package/src/StatusDot/StatusDot.test.tsx +115 -8
- package/src/StatusDot/StatusDot.tsx +74 -9
- package/src/Switch/Switch.tsx +6 -0
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +20 -2
- package/src/TextArea/TextArea.tsx +3 -1
- package/src/TextInput/TextInput.tsx +3 -1
- package/src/TimeInput/TimeInput.tsx +3 -1
- package/src/hooks/useResolvedRequired.ts +42 -0
- package/src/utils/plainDate.test.ts +50 -0
- package/src/utils/plainDate.ts +12 -0
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
*
|
|
11
11
|
* SYNC: When modified, update these files to stay in sync:
|
|
12
12
|
* - /packages/core/src/FormLayout/FormLayout.test.tsx (tests for new/changed behavior)
|
|
13
|
+
* - /packages/core/src/FormLayout/FormLayoutContext.ts (context value shape)
|
|
14
|
+
* - /packages/core/src/FormLayout/FormLayout.doc.mjs (props table)
|
|
13
15
|
* - /packages/core/src/FormLayout/index.ts (exports if types change)
|
|
16
|
+
* - /packages/core/src/Field/FieldLabel.tsx (defaultOptionality indicator resolution)
|
|
14
17
|
* - /apps/storybook/stories/FormLayout.stories.tsx (storybook stories)
|
|
15
18
|
* - /packages/cli/assets/templates/blocks/components/FormLayout/ (showcase blocks)
|
|
16
19
|
*/
|
|
@@ -19,7 +22,11 @@ import {useMemo, type ReactNode} from 'react';
|
|
|
19
22
|
import type {BaseProps} from '../BaseProps';
|
|
20
23
|
import * as stylex from '@stylexjs/stylex';
|
|
21
24
|
import {spacingVars} from '../theme/tokens.stylex';
|
|
22
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
FormLayoutContext,
|
|
27
|
+
type FormLayoutDirection,
|
|
28
|
+
type FormOptionality,
|
|
29
|
+
} from './FormLayoutContext';
|
|
23
30
|
import {mergeProps} from '../utils';
|
|
24
31
|
import {themeProps} from '../utils/themeProps';
|
|
25
32
|
|
|
@@ -82,6 +89,26 @@ export interface FormLayoutProps extends BaseProps<HTMLDivElement> {
|
|
|
82
89
|
* @default 'vertical'
|
|
83
90
|
*/
|
|
84
91
|
direction?: FormLayoutDirection;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Which state the form treats as its default, so only the *exception*
|
|
95
|
+
* carries a visible optional/required indicator. It also resolves each
|
|
96
|
+
* field's `aria-required` so the unmarked majority is still announced
|
|
97
|
+
* correctly — but only `aria-required`, never the native `required`
|
|
98
|
+
* attribute, so a layout default can't switch on browser validation.
|
|
99
|
+
*
|
|
100
|
+
* - `'optional'` — fields read as optional; only a field with `isRequired`
|
|
101
|
+
* shows an indicator (the "required" one).
|
|
102
|
+
* - `'required'` — fields read as required; only a field with `isOptional`
|
|
103
|
+
* shows an indicator (the "optional" one). Fields without `isOptional`
|
|
104
|
+
* expose `aria-required` even though they show no indicator.
|
|
105
|
+
* - unset — today's behavior: `isRequired` and `isOptional` each show their
|
|
106
|
+
* own indicator independently.
|
|
107
|
+
*
|
|
108
|
+
* A field that merely restates the default (e.g. `isOptional` under
|
|
109
|
+
* `'optional'`) shows nothing. An inner `FormLayout` shadows an outer one.
|
|
110
|
+
*/
|
|
111
|
+
defaultOptionality?: FormOptionality;
|
|
85
112
|
}
|
|
86
113
|
|
|
87
114
|
// =============================================================================
|
|
@@ -109,13 +136,17 @@ export interface FormLayoutProps extends BaseProps<HTMLDivElement> {
|
|
|
109
136
|
export function FormLayout({
|
|
110
137
|
children,
|
|
111
138
|
direction = 'vertical',
|
|
139
|
+
defaultOptionality,
|
|
112
140
|
xstyle,
|
|
113
141
|
className,
|
|
114
142
|
style,
|
|
115
143
|
ref,
|
|
116
144
|
...props
|
|
117
145
|
}: FormLayoutProps) {
|
|
118
|
-
const contextValue = useMemo(
|
|
146
|
+
const contextValue = useMemo(
|
|
147
|
+
() => ({direction, defaultOptionality}),
|
|
148
|
+
[direction, defaultOptionality],
|
|
149
|
+
);
|
|
119
150
|
|
|
120
151
|
return (
|
|
121
152
|
<FormLayoutContext value={contextValue}>
|
|
@@ -5,10 +5,13 @@
|
|
|
5
5
|
/**
|
|
6
6
|
* @file FormLayoutContext.ts
|
|
7
7
|
* @input Uses React createContext
|
|
8
|
-
* @output Exports FormLayoutContext and
|
|
9
|
-
* @position Context for form layout direction detection
|
|
8
|
+
* @output Exports FormLayoutContext, FormLayoutDirection, and FormOptionality types
|
|
9
|
+
* @position Context for form layout direction + default-optionality detection
|
|
10
10
|
*
|
|
11
11
|
* SYNC: When modified, update these files to stay in sync:
|
|
12
|
+
* - /packages/core/src/FormLayout/FormLayout.tsx (prop + context value)
|
|
13
|
+
* - /packages/core/src/Field/FieldLabel.tsx (indicator resolution)
|
|
14
|
+
* - /packages/core/src/FormLayout/index.ts (exports if types change)
|
|
12
15
|
*/
|
|
13
16
|
|
|
14
17
|
import {createContext} from 'react';
|
|
@@ -22,15 +25,27 @@ import {createContext} from 'react';
|
|
|
22
25
|
* of their inputs (settings/admin panel pattern).
|
|
23
26
|
*/
|
|
24
27
|
export type FormLayoutDirection =
|
|
25
|
-
| '
|
|
26
|
-
| 'horizontal'
|
|
27
|
-
| 'horizontal-labels';
|
|
28
|
+
'vertical' | 'horizontal' | 'horizontal-labels';
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
31
|
+
* Which state a form treats as its default, so only the *exception* carries a
|
|
32
|
+
* visible optional/required indicator.
|
|
33
|
+
*
|
|
34
|
+
* - `'optional'` — fields are optional unless a field opts into `isRequired`;
|
|
35
|
+
* only required fields show an indicator.
|
|
36
|
+
* - `'required'` — fields are required unless a field opts into `isOptional`;
|
|
37
|
+
* only optional fields show an indicator.
|
|
38
|
+
*/
|
|
39
|
+
export type FormOptionality = 'optional' | 'required';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Context for detecting which form layout a component is rendered in. Children
|
|
43
|
+
* can use this to adapt their rendering based on the parent layout — direction
|
|
44
|
+
* for spatial arrangement, and `defaultOptionality` so a field can suppress the
|
|
45
|
+
* indicator that merely restates the form-wide default.
|
|
32
46
|
*/
|
|
33
47
|
export const FormLayoutContext = createContext<{
|
|
34
48
|
direction: FormLayoutDirection;
|
|
49
|
+
defaultOptionality?: FormOptionality;
|
|
35
50
|
}>({direction: 'vertical'});
|
|
36
51
|
FormLayoutContext.displayName = 'FormLayoutContext';
|
package/src/FormLayout/index.ts
CHANGED
|
@@ -13,5 +13,5 @@
|
|
|
13
13
|
|
|
14
14
|
export {FormLayout} from './FormLayout';
|
|
15
15
|
export type {FormLayoutProps} from './FormLayout';
|
|
16
|
-
export type {FormLayoutDirection} from './FormLayoutContext';
|
|
16
|
+
export type {FormLayoutDirection, FormOptionality} from './FormLayoutContext';
|
|
17
17
|
export {FormLayoutContext} from './FormLayoutContext';
|
|
@@ -169,6 +169,7 @@ export const docs = {
|
|
|
169
169
|
{ guidance: true, description: 'Set headingLevelStart to match the page hierarchy, e.g. start at 3 if the markdown sits inside an h2 section.' },
|
|
170
170
|
{ guidance: true, description: 'Use contentWidth to keep prose at a readable line length in wide layouts.' },
|
|
171
171
|
{ guidance: true, description: 'Use inlinePlugins for custom shorthand patterns like issue refs, diff refs, and mentions instead of preprocessing the markdown string.' },
|
|
172
|
+
{ guidance: true, description: 'Pair with Outline and useOutlineFromMarkdown for section navigation: headings render generated id attributes that match the outline item ids, so hash links scroll to their target.' },
|
|
172
173
|
{ guidance: false, description: 'Use Markdown for hand-authored layouts; use Text and Heading directly when you control the content.' },
|
|
173
174
|
],
|
|
174
175
|
},
|
|
@@ -381,6 +382,7 @@ export const docsZh = {
|
|
|
381
382
|
{ guidance: true, description: 'Set headingLevelStart to match the page hierarchy, e.g. start at 3 if the markdown sits inside an h2 section.' },
|
|
382
383
|
{ guidance: true, description: 'Use contentWidth to keep prose at a readable line length in wide layouts.' },
|
|
383
384
|
{ guidance: true, description: 'Use inlinePlugins for custom shorthand patterns like issue refs, diff refs, and mentions instead of preprocessing the markdown string.' },
|
|
385
|
+
{ guidance: true, description: 'Pair with Outline and useOutlineFromMarkdown for section navigation: headings render generated id attributes that match the outline item ids, so hash links scroll to their target.' },
|
|
384
386
|
{ guidance: false, description: 'Use Markdown for hand-authored layouts; use Text and Heading directly when you control the content.' },
|
|
385
387
|
],
|
|
386
388
|
},
|
|
@@ -396,6 +398,7 @@ export const docsDense = {
|
|
|
396
398
|
{ guidance: true, description: 'Set headingLevelStart to match the page hierarchy, e.g. start at 3 if the markdown sits inside an h2 section.' },
|
|
397
399
|
{ guidance: true, description: 'Use contentWidth to keep prose at a readable line length in wide layouts.' },
|
|
398
400
|
{ guidance: true, description: 'Use inlinePlugins for custom shorthand patterns (issue refs, diff refs, mentions) instead of preprocessing the markdown string.' },
|
|
401
|
+
{ guidance: true, description: 'Headings render id attributes matching useOutlineFromMarkdown ids; pair with Outline for hash navigation.' },
|
|
399
402
|
{ guidance: false, description: 'Use Markdown for hand-authored layouts; use Text and Heading directly when you control the content.' },
|
|
400
403
|
],
|
|
401
404
|
},
|
|
@@ -5,6 +5,7 @@ import {render, screen, fireEvent} from '@testing-library/react';
|
|
|
5
5
|
import type {ReactNode} from 'react';
|
|
6
6
|
import {Markdown} from './Markdown';
|
|
7
7
|
import type {MarkdownInlinePlugin} from './Markdown';
|
|
8
|
+
import {parseOutlineFromMarkdown} from '../Outline/parseOutlineFromMarkdown';
|
|
8
9
|
|
|
9
10
|
describe('Markdown', () => {
|
|
10
11
|
it('renders with role="document"', () => {
|
|
@@ -23,6 +24,83 @@ describe('Markdown', () => {
|
|
|
23
24
|
expect(screen.getByText('Heading 2').tagName).toBe('H2');
|
|
24
25
|
});
|
|
25
26
|
|
|
27
|
+
describe('heading ids', () => {
|
|
28
|
+
// Outline's documented contract: an outline item id "should match the
|
|
29
|
+
// target heading element id". Markdown renders the ids that
|
|
30
|
+
// useOutlineFromMarkdown derives, so hash navigation resolves.
|
|
31
|
+
it('renders generated id attributes on headings', () => {
|
|
32
|
+
render(<Markdown>{'# Overview\n\ncontent\n\n# Installation'}</Markdown>);
|
|
33
|
+
expect(screen.getByText('Overview')).toHaveAttribute('id', 'overview');
|
|
34
|
+
expect(screen.getByText('Installation')).toHaveAttribute(
|
|
35
|
+
'id',
|
|
36
|
+
'installation',
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('disambiguates duplicate headings with numeric suffixes', () => {
|
|
41
|
+
render(<Markdown>{'# Setup\n\n# Setup\n\n# Setup'}</Markdown>);
|
|
42
|
+
const ids = screen.getAllByText('Setup').map(el => el.id);
|
|
43
|
+
expect(ids).toEqual(['setup', 'setup-1', 'setup-2']);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('renders ids matching parseOutlineFromMarkdown for the same source', () => {
|
|
47
|
+
// Parity invariant: every id the outline derives must resolve to a
|
|
48
|
+
// rendered heading with that exact id — including slugified formatting,
|
|
49
|
+
// duplicate numbering, the empty-slug fallback, and code-fence decoys.
|
|
50
|
+
const source = [
|
|
51
|
+
'# **Bold** and _italic_ text',
|
|
52
|
+
'## Setup',
|
|
53
|
+
'## Setup',
|
|
54
|
+
'### !!!',
|
|
55
|
+
'```',
|
|
56
|
+
'# not a heading',
|
|
57
|
+
'```',
|
|
58
|
+
'## The `useState` hook',
|
|
59
|
+
].join('\n\n');
|
|
60
|
+
const {container} = render(<Markdown>{source}</Markdown>);
|
|
61
|
+
const outline = parseOutlineFromMarkdown(source);
|
|
62
|
+
expect(outline.length).toBe(5);
|
|
63
|
+
for (const item of outline) {
|
|
64
|
+
const target = container.querySelector(`[id="${item.id}"]`);
|
|
65
|
+
expect(target, `no rendered heading with id "${item.id}"`).not.toBe(
|
|
66
|
+
null,
|
|
67
|
+
);
|
|
68
|
+
expect(target!.tagName).toMatch(/^H[1-6]$/);
|
|
69
|
+
expect(target!.textContent?.trim()).toBe(item.label);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('passes the generated id to a custom heading component', () => {
|
|
74
|
+
const received: (string | undefined)[] = [];
|
|
75
|
+
render(
|
|
76
|
+
<Markdown
|
|
77
|
+
components={{
|
|
78
|
+
heading: ({children, id}: {children: ReactNode; id?: string}) => {
|
|
79
|
+
received.push(id);
|
|
80
|
+
return <h2 id={id}>{children}</h2>;
|
|
81
|
+
},
|
|
82
|
+
}}>
|
|
83
|
+
{'# Overview\n\n# Overview'}
|
|
84
|
+
</Markdown>,
|
|
85
|
+
);
|
|
86
|
+
expect(received).toEqual(['overview', 'overview-1']);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('does not assign ids to headings nested inside blockquotes', () => {
|
|
90
|
+
// parseOutlineFromMarkdown only lists top-level headings. If nested
|
|
91
|
+
// headings consumed slugs too, duplicate numbering would drift and
|
|
92
|
+
// outline links would land on the wrong heading.
|
|
93
|
+
const source = '> # Quoted\n\n# Quoted';
|
|
94
|
+
const {container} = render(<Markdown>{source}</Markdown>);
|
|
95
|
+
const outline = parseOutlineFromMarkdown(source);
|
|
96
|
+
expect(outline.map(i => i.id)).toEqual(['quoted']);
|
|
97
|
+
const [nested, topLevel] = screen.getAllByText('Quoted');
|
|
98
|
+
expect(container.querySelector('blockquote')).toContainElement(nested);
|
|
99
|
+
expect(nested).not.toHaveAttribute('id');
|
|
100
|
+
expect(topLevel).toHaveAttribute('id', 'quoted');
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
26
104
|
it('renders paragraphs as block <div> (never <p>) for composition safety', () => {
|
|
27
105
|
render(<Markdown>{'Hello world'}</Markdown>);
|
|
28
106
|
// Markdown paragraphs render as <div> so block-level inline content
|
|
@@ -53,6 +53,9 @@ import {
|
|
|
53
53
|
parseMarkdownIncremental,
|
|
54
54
|
createIncrementalState,
|
|
55
55
|
trimStreamingArtifacts,
|
|
56
|
+
inlineText,
|
|
57
|
+
slugify,
|
|
58
|
+
uniqueSlug,
|
|
56
59
|
} from './parser';
|
|
57
60
|
import type {BlockNode, InlineNode, IncrementalState} from './parser';
|
|
58
61
|
import {themeProps} from '../utils/themeProps';
|
|
@@ -108,6 +111,14 @@ export interface MarkdownComponents {
|
|
|
108
111
|
heading?: React.ComponentType<{
|
|
109
112
|
level: 1 | 2 | 3 | 4 | 5 | 6;
|
|
110
113
|
children: React.ReactNode;
|
|
114
|
+
/**
|
|
115
|
+
* Generated slug for this heading, matching the ids produced by
|
|
116
|
+
* useOutlineFromMarkdown / parseOutlineFromMarkdown. Render it as the
|
|
117
|
+
* element's `id` to keep Outline hash navigation working. Undefined for
|
|
118
|
+
* headings nested inside blockquotes or list items (the outline only
|
|
119
|
+
* lists top-level headings).
|
|
120
|
+
*/
|
|
121
|
+
id?: string;
|
|
111
122
|
}>;
|
|
112
123
|
paragraph?: React.ComponentType<{children: React.ReactNode}>;
|
|
113
124
|
image?: React.ComponentType<{src: string; alt: string}>;
|
|
@@ -1045,6 +1056,7 @@ function renderBlock(
|
|
|
1045
1056
|
inlinePlugins: MarkdownInlinePlugin[] | undefined,
|
|
1046
1057
|
components: Partial<MarkdownComponents> | undefined,
|
|
1047
1058
|
t: TranslatorFn,
|
|
1059
|
+
headingIdMap?: ReadonlyMap<BlockNode, string>,
|
|
1048
1060
|
): SyncReactNode {
|
|
1049
1061
|
const blockAlignMargin = BLOCK_ALIGN_MARGIN[contentAlign];
|
|
1050
1062
|
const blockAlignStyle =
|
|
@@ -1071,10 +1083,15 @@ function renderBlock(
|
|
|
1071
1083
|
components,
|
|
1072
1084
|
),
|
|
1073
1085
|
);
|
|
1086
|
+
// Only top-level headings get an id: the map is built from the same
|
|
1087
|
+
// traversal parseOutlineFromMarkdown uses (which skips headings nested
|
|
1088
|
+
// in blockquotes / list items), so rendered ids and outline ids stay
|
|
1089
|
+
// identical — including duplicate-slug numbering.
|
|
1090
|
+
const headingId = headingIdMap?.get(node);
|
|
1074
1091
|
const HeadingComp = components?.heading;
|
|
1075
1092
|
if (HeadingComp) {
|
|
1076
1093
|
return (
|
|
1077
|
-
<HeadingComp key={index} level={level}>
|
|
1094
|
+
<HeadingComp key={index} level={level} id={headingId}>
|
|
1078
1095
|
{headingChildren}
|
|
1079
1096
|
</HeadingComp>
|
|
1080
1097
|
);
|
|
@@ -1083,6 +1100,7 @@ function renderBlock(
|
|
|
1083
1100
|
return (
|
|
1084
1101
|
<Tag
|
|
1085
1102
|
key={index}
|
|
1103
|
+
id={headingId}
|
|
1086
1104
|
{...mergeProps(
|
|
1087
1105
|
themeProps('markdown-heading', {density, level}),
|
|
1088
1106
|
stylex.props(
|
|
@@ -1652,6 +1670,27 @@ export function Markdown({
|
|
|
1652
1670
|
return parseMarkdown(children, parseOptions);
|
|
1653
1671
|
}, [display, smoothedText, children, isStreaming, parseOptions]);
|
|
1654
1672
|
|
|
1673
|
+
// Assign each top-level heading the slug that parseOutlineFromMarkdown
|
|
1674
|
+
// would derive for it, so Outline hash links built from the same source
|
|
1675
|
+
// always find a matching DOM id. Mirrors that function's traversal exactly:
|
|
1676
|
+
// top-level blocks only, one shared duplicate-numbering sequence.
|
|
1677
|
+
// NOTE: must stay above the `display === 'inline'` early return below —
|
|
1678
|
+
// hooks cannot be conditional.
|
|
1679
|
+
const headingIdMap = useMemo(() => {
|
|
1680
|
+
if (display === 'inline' || blocks.length === 0) {
|
|
1681
|
+
return undefined;
|
|
1682
|
+
}
|
|
1683
|
+
const map = new Map<BlockNode, string>();
|
|
1684
|
+
const counts = new Map<string, number>();
|
|
1685
|
+
for (const block of blocks) {
|
|
1686
|
+
if (block.type === 'heading') {
|
|
1687
|
+
const label = inlineText(block.children).trim();
|
|
1688
|
+
map.set(block, uniqueSlug(slugify(label), counts));
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
return map;
|
|
1692
|
+
}, [display, blocks]);
|
|
1693
|
+
|
|
1655
1694
|
const inlineNodes = useMemo(() => {
|
|
1656
1695
|
if (display !== 'inline') {
|
|
1657
1696
|
return [];
|
|
@@ -1765,6 +1804,7 @@ export function Markdown({
|
|
|
1765
1804
|
inlinePlugins,
|
|
1766
1805
|
components,
|
|
1767
1806
|
t,
|
|
1807
|
+
headingIdMap,
|
|
1768
1808
|
),
|
|
1769
1809
|
)}
|
|
1770
1810
|
</div>
|
package/src/Markdown/parser.ts
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* @file parser.ts
|
|
5
5
|
* @input Markdown string
|
|
6
|
-
* @output Array of MarkdownNode AST nodes
|
|
7
|
-
*
|
|
6
|
+
* @output Array of MarkdownNode AST nodes; heading slug helpers
|
|
7
|
+
* (inlineText, slugify, uniqueSlug) shared by Markdown rendering and
|
|
8
|
+
* Outline's parseOutlineFromMarkdown
|
|
9
|
+
* @position Core parser; consumed by Markdown.tsx and Outline
|
|
8
10
|
*/
|
|
9
11
|
|
|
10
12
|
// ---------------------------------------------------------------------------
|
|
@@ -1815,3 +1817,59 @@ export function parseMarkdownIncremental(
|
|
|
1815
1817
|
|
|
1816
1818
|
return mergeSettledBlocks(settledBlocks, unsettledBlocks);
|
|
1817
1819
|
}
|
|
1820
|
+
|
|
1821
|
+
// ---------------------------------------------------------------------------
|
|
1822
|
+
// Heading slugs
|
|
1823
|
+
// ---------------------------------------------------------------------------
|
|
1824
|
+
// Single source of truth for the heading id contract: Markdown renders these
|
|
1825
|
+
// slugs as `id` attributes on h1–h6, and Outline's parseOutlineFromMarkdown
|
|
1826
|
+
// derives its item ids from the same functions, so outline hash links always
|
|
1827
|
+
// resolve to a rendered heading by construction.
|
|
1828
|
+
|
|
1829
|
+
/** Flatten inline nodes into their plain text content. */
|
|
1830
|
+
export function inlineText(nodes: InlineNode[]): string {
|
|
1831
|
+
return nodes
|
|
1832
|
+
.map(node => {
|
|
1833
|
+
switch (node.type) {
|
|
1834
|
+
case 'text':
|
|
1835
|
+
case 'code':
|
|
1836
|
+
return node.content;
|
|
1837
|
+
case 'bold':
|
|
1838
|
+
case 'italic':
|
|
1839
|
+
case 'strikethrough':
|
|
1840
|
+
case 'link':
|
|
1841
|
+
return inlineText(node.children);
|
|
1842
|
+
case 'image':
|
|
1843
|
+
return node.alt;
|
|
1844
|
+
case 'citation':
|
|
1845
|
+
case 'break':
|
|
1846
|
+
return '';
|
|
1847
|
+
}
|
|
1848
|
+
})
|
|
1849
|
+
.join('');
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
/** Turn heading text into a URL-safe slug (lowercase, hyphen-separated). */
|
|
1853
|
+
export function slugify(value: string): string {
|
|
1854
|
+
return value
|
|
1855
|
+
.trim()
|
|
1856
|
+
.toLowerCase()
|
|
1857
|
+
.replace(/['"]/g, '')
|
|
1858
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
1859
|
+
.replace(/^-+|-+$/g, '');
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
/**
|
|
1863
|
+
* Disambiguate repeated slugs with a numeric suffix (`setup`, `setup-1`, …).
|
|
1864
|
+
* Empty slugs fall back to `section`. The caller owns the counts map so one
|
|
1865
|
+
* document shares a single numbering sequence.
|
|
1866
|
+
*/
|
|
1867
|
+
export function uniqueSlug(
|
|
1868
|
+
baseSlug: string,
|
|
1869
|
+
counts: Map<string, number>,
|
|
1870
|
+
): string {
|
|
1871
|
+
const fallbackSlug = baseSlug || 'section';
|
|
1872
|
+
const count = counts.get(fallbackSlug) ?? 0;
|
|
1873
|
+
counts.set(fallbackSlug, count + 1);
|
|
1874
|
+
return count === 0 ? fallbackSlug : `${fallbackSlug}-${count}`;
|
|
1875
|
+
}
|
|
@@ -73,6 +73,7 @@ import {
|
|
|
73
73
|
import {useMultiCombobox} from './hooks';
|
|
74
74
|
import {getInputARIA, isImeKeyEvent, mergeProps} from '../utils';
|
|
75
75
|
import {useAnnounce} from '../hooks/useAnnounce';
|
|
76
|
+
import {useResolvedRequired} from '../hooks/useResolvedRequired';
|
|
76
77
|
import type {BaseProps} from '../BaseProps';
|
|
77
78
|
import type {SizeValue} from '../utils/types';
|
|
78
79
|
import {useSize} from '../SizeContext/SizeContext';
|
|
@@ -706,6 +707,7 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
|
|
|
706
707
|
style,
|
|
707
708
|
}: MultiSelectorProps<T>) {
|
|
708
709
|
const t = useTranslator();
|
|
710
|
+
const isEffectivelyRequired = useResolvedRequired({isRequired, isOptional});
|
|
709
711
|
const placeholder =
|
|
710
712
|
placeholderFromProps ?? t('@astryx.multiSelector.selectPlaceholder');
|
|
711
713
|
const selectAllLabel =
|
|
@@ -1526,7 +1528,7 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
|
|
|
1526
1528
|
}
|
|
1527
1529
|
aria-describedby={ariaDescribedBy}
|
|
1528
1530
|
aria-labelledby={ariaLabelledBy}
|
|
1529
|
-
aria-required={
|
|
1531
|
+
aria-required={isEffectivelyRequired ? 'true' : undefined}
|
|
1530
1532
|
aria-invalid={status?.type === 'error' ? 'true' : undefined}
|
|
1531
1533
|
aria-busy={isBusy || undefined}
|
|
1532
1534
|
// With a disabledMessage the trigger keeps focusability via
|
|
@@ -53,6 +53,7 @@ import {getInputARIA} from '../utils';
|
|
|
53
53
|
import {useSize} from '../SizeContext/SizeContext';
|
|
54
54
|
import {useInputContainer} from '../hooks/useInputContainer';
|
|
55
55
|
import {useInputStatusIcon} from '../hooks/useInputStatusIcon';
|
|
56
|
+
import {useResolvedRequired} from '../hooks/useResolvedRequired';
|
|
56
57
|
import {useInputGroup} from '../InputGroup/InputGroupContext';
|
|
57
58
|
|
|
58
59
|
const styles = stylex.create({
|
|
@@ -559,6 +560,7 @@ export function NumberInput({
|
|
|
559
560
|
...rest
|
|
560
561
|
}: NumberInputProps) {
|
|
561
562
|
const t = useTranslator();
|
|
563
|
+
const isEffectivelyRequired = useResolvedRequired({isRequired, isOptional});
|
|
562
564
|
const size = useSize(sizeProp, 'md');
|
|
563
565
|
const id = useId();
|
|
564
566
|
const inputLabelID = useId();
|
|
@@ -927,7 +929,7 @@ export function NumberInput({
|
|
|
927
929
|
value == null || !formatValue ? undefined : formattedValue
|
|
928
930
|
}
|
|
929
931
|
aria-describedby={ariaDescribedBy}
|
|
930
|
-
aria-required={
|
|
932
|
+
aria-required={isEffectivelyRequired ? 'true' : undefined}
|
|
931
933
|
aria-invalid={
|
|
932
934
|
status?.type === 'error' || !isInputValid ? 'true' : undefined
|
|
933
935
|
}
|
|
@@ -166,6 +166,8 @@ import {Outline, useOutlineFromMarkdown} from '@astryxdesign/core/Outline';
|
|
|
166
166
|
|
|
167
167
|
function MarkdownOutline({markdown}) {
|
|
168
168
|
// Derives {id, label, level} items from headings in the source.
|
|
169
|
+
// The Markdown component renders the same generated ids on its
|
|
170
|
+
// headings, so outline links scroll without any extra wiring.
|
|
169
171
|
const items = useOutlineFromMarkdown(markdown);
|
|
170
172
|
return <Outline items={items} />;
|
|
171
173
|
}
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @file parseOutlineFromMarkdown.ts
|
|
5
|
-
* @input Uses Markdown parser internals
|
|
5
|
+
* @input Uses Markdown parser internals (parseMarkdown + heading slug
|
|
6
|
+
* helpers) and OutlineItem type
|
|
6
7
|
* @output Exports parseOutlineFromMarkdown for extracting heading outlines from Markdown
|
|
7
8
|
* @position Pure utility; consumed by useOutlineFromMarkdown and public exports
|
|
8
9
|
*
|
|
@@ -11,52 +12,21 @@
|
|
|
11
12
|
* - /packages/core/src/Outline/index.ts
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
parseMarkdown,
|
|
17
|
+
inlineText,
|
|
18
|
+
slugify,
|
|
19
|
+
uniqueSlug,
|
|
20
|
+
} from '../Markdown/parser';
|
|
15
21
|
import type {OutlineItem} from './types';
|
|
16
22
|
|
|
17
|
-
function inlineText(nodes: InlineNode[]): string {
|
|
18
|
-
return nodes
|
|
19
|
-
.map(node => {
|
|
20
|
-
switch (node.type) {
|
|
21
|
-
case 'text':
|
|
22
|
-
case 'code':
|
|
23
|
-
return node.content;
|
|
24
|
-
case 'bold':
|
|
25
|
-
case 'italic':
|
|
26
|
-
case 'strikethrough':
|
|
27
|
-
case 'link':
|
|
28
|
-
return inlineText(node.children);
|
|
29
|
-
case 'image':
|
|
30
|
-
return node.alt;
|
|
31
|
-
case 'citation':
|
|
32
|
-
case 'break':
|
|
33
|
-
return '';
|
|
34
|
-
}
|
|
35
|
-
})
|
|
36
|
-
.join('');
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function slugify(value: string): string {
|
|
40
|
-
return value
|
|
41
|
-
.trim()
|
|
42
|
-
.toLowerCase()
|
|
43
|
-
.replace(/['"]/g, '')
|
|
44
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
45
|
-
.replace(/^-+|-+$/g, '');
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function uniqueSlug(baseSlug: string, counts: Map<string, number>): string {
|
|
49
|
-
const fallbackSlug = baseSlug || 'section';
|
|
50
|
-
const count = counts.get(fallbackSlug) ?? 0;
|
|
51
|
-
counts.set(fallbackSlug, count + 1);
|
|
52
|
-
return count === 0 ? fallbackSlug : `${fallbackSlug}-${count}`;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
23
|
/**
|
|
56
24
|
* Extract heading items from a Markdown string.
|
|
57
25
|
*
|
|
58
26
|
* Uses Markdown's parser so fenced code blocks, tables, lists, and inline
|
|
59
27
|
* formatting are interpreted consistently with rendered Markdown output.
|
|
28
|
+
* Ids come from the parser's shared slug helpers, so they always match the
|
|
29
|
+
* `id` attributes Markdown renders on its headings.
|
|
60
30
|
*/
|
|
61
31
|
export function parseOutlineFromMarkdown(markdown: string): OutlineItem[] {
|
|
62
32
|
const counts = new Map<string, number>();
|
|
@@ -29,6 +29,7 @@ import {spacingVars} from '../theme/tokens.stylex';
|
|
|
29
29
|
import {Field} from '../Field/Field';
|
|
30
30
|
import type {InputStatus} from '../Field/types';
|
|
31
31
|
import {useTooltip} from '../Tooltip';
|
|
32
|
+
import {useResolvedRequired} from '../hooks/useResolvedRequired';
|
|
32
33
|
import {mergeProps} from '../utils';
|
|
33
34
|
import type {BaseProps} from '../BaseProps';
|
|
34
35
|
import type {SizeValue} from '../utils/types';
|
|
@@ -218,6 +219,13 @@ export function RadioList({
|
|
|
218
219
|
|
|
219
220
|
const groupRef = useRef<HTMLDivElement>(null);
|
|
220
221
|
|
|
222
|
+
// The radiogroup exposes the *effective* required state so a form-wide
|
|
223
|
+
// `defaultOptionality="required"` is announced even when the group carries no
|
|
224
|
+
// visible indicator. Individual radios keep their native `required` bound to
|
|
225
|
+
// the explicit `isRequired` (via context), so a layout default never switches
|
|
226
|
+
// on browser validation for the group.
|
|
227
|
+
const isEffectivelyRequired = useResolvedRequired({isRequired, isOptional});
|
|
228
|
+
|
|
221
229
|
// Disabled-reason tooltip. Applies to the whole-group disabled state. Disabled
|
|
222
230
|
// controls swallow pointer events, so the tooltip listeners attach to the
|
|
223
231
|
// radiogroup container and the radios stay perceivable via aria-disabled
|
|
@@ -387,7 +395,7 @@ export function RadioList({
|
|
|
387
395
|
.join(' ') || undefined
|
|
388
396
|
}
|
|
389
397
|
aria-invalid={status?.type === 'error' ? true : undefined}
|
|
390
|
-
aria-required={
|
|
398
|
+
aria-required={isEffectivelyRequired || undefined}
|
|
391
399
|
{...mergeProps(
|
|
392
400
|
themeProps('radio-list', {orientation, size}),
|
|
393
401
|
stylex.props(
|
|
@@ -71,6 +71,7 @@ import {
|
|
|
71
71
|
} from './utils';
|
|
72
72
|
import {useCombobox, useSelectedItemOffset} from './hooks';
|
|
73
73
|
import {useTypeahead} from '../hooks/useTypeahead';
|
|
74
|
+
import {useResolvedRequired} from '../hooks/useResolvedRequired';
|
|
74
75
|
import {SelectorOption} from './SelectorOption';
|
|
75
76
|
import {getInputARIA, isImeKeyEvent, mergeProps} from '../utils';
|
|
76
77
|
import {useSize} from '../SizeContext/SizeContext';
|
|
@@ -699,6 +700,7 @@ export function Selector<T extends SelectorOptionType>(
|
|
|
699
700
|
hasClear: hasClearProp,
|
|
700
701
|
...rest
|
|
701
702
|
} = props as SelectorPropsClearable<T>;
|
|
703
|
+
const isEffectivelyRequired = useResolvedRequired({isRequired, isOptional});
|
|
702
704
|
const placeholder = placeholderFromProps ?? t('@astryx.selector.placeholder');
|
|
703
705
|
const searchPlaceholder =
|
|
704
706
|
searchPlaceholderFromProps ?? t('@astryx.selector.searchPlaceholder');
|
|
@@ -1345,7 +1347,7 @@ export function Selector<T extends SelectorOptionType>(
|
|
|
1345
1347
|
}
|
|
1346
1348
|
aria-describedby={ariaDescribedBy}
|
|
1347
1349
|
aria-labelledby={ariaLabelledBy}
|
|
1348
|
-
aria-required={
|
|
1350
|
+
aria-required={isEffectivelyRequired ? 'true' : undefined}
|
|
1349
1351
|
aria-invalid={status?.type === 'error' ? 'true' : undefined}
|
|
1350
1352
|
aria-busy={isBusy || undefined}
|
|
1351
1353
|
// With a disabledMessage the trigger keeps focusability via
|
|
@@ -33,6 +33,12 @@ export const docs = {
|
|
|
33
33
|
description:
|
|
34
34
|
'Tooltip text shown on hover to explain the status meaning.',
|
|
35
35
|
},
|
|
36
|
+
{
|
|
37
|
+
name: 'icon',
|
|
38
|
+
type: 'ReactNode',
|
|
39
|
+
description:
|
|
40
|
+
'Optional icon rendered centered inside the dot, painted in currentColor (the variant\'s ink). Gives the status a non-color mark, so use a different icon per status. Booleans and empty strings are ignored, so `cond && <Icon />` is safe. Same contract as AvatarStatusDot.',
|
|
41
|
+
},
|
|
36
42
|
{
|
|
37
43
|
name: 'xstyle',
|
|
38
44
|
type: 'StyleXStyles',
|
|
@@ -84,6 +90,12 @@ export const docsZh = {
|
|
|
84
90
|
'启用脉冲动画;尊重 prefers-reduced-motion: reduce 设置。',
|
|
85
91
|
default: 'false',
|
|
86
92
|
},
|
|
93
|
+
{
|
|
94
|
+
name: 'icon',
|
|
95
|
+
type: 'ReactNode',
|
|
96
|
+
description:
|
|
97
|
+
'可选图标,居中渲染于圆点内,以 currentColor(变体的前景色)着色。为状态提供非颜色标记,请为每个状态使用不同图标。布尔值和空字符串会被忽略,因此 `cond && <Icon />` 是安全的。与 AvatarStatusDot 的契约一致。',
|
|
98
|
+
},
|
|
87
99
|
{
|
|
88
100
|
name: 'xstyle',
|
|
89
101
|
type: 'StyleXStyles',
|
|
@@ -132,6 +144,7 @@ export const docsDense = {
|
|
|
132
144
|
label: 'Accessible label via aria-label.',
|
|
133
145
|
isPulsing: 'Pulse animation; respects prefers-reduced-motion: reduce.',
|
|
134
146
|
tooltip: 'Tooltip text on hover to explain status meaning.',
|
|
147
|
+
icon: 'Optional ReactNode rendered centered in the dot (currentColor ink); a non-color mark for the status, use a different icon per status. Booleans/empty strings ignored (safe for cond && <Icon/>). Same contract as AvatarStatusDot.',
|
|
135
148
|
xstyle: 'StyleX layout styles; must be stylex.create() value.',
|
|
136
149
|
},
|
|
137
150
|
};
|