@astryxdesign/core 0.1.3 → 0.1.4-canary.02643db
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 +51 -0
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +29 -8
- package/dist/Calendar/Calendar.d.ts.map +1 -1
- package/dist/Calendar/Calendar.js +49 -20
- package/dist/Calendar/dayCellUtils.d.ts +67 -5
- package/dist/Calendar/dayCellUtils.d.ts.map +1 -1
- package/dist/Calendar/dayCellUtils.js +139 -18
- package/dist/CheckboxInput/CheckboxInput.d.ts +6 -1
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +7 -1
- package/dist/Code/Code.d.ts +15 -1
- package/dist/Code/Code.d.ts.map +1 -1
- package/dist/Code/Code.js +32 -2
- package/dist/Code/index.d.ts +1 -1
- package/dist/Code/index.d.ts.map +1 -1
- package/dist/CodeBlock/index.d.ts +1 -1
- package/dist/CodeBlock/index.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteEmpty.d.ts +1 -1
- package/dist/CommandPalette/CommandPaletteEmpty.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteEmpty.js +24 -4
- package/dist/ContextMenu/ContextMenu.d.ts +9 -1
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +6 -3
- package/dist/DateRangeInput/DateRangeInput.js +2 -2
- package/dist/DateTimeInput/DateTimeInput.d.ts +0 -5
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/Divider/Divider.d.ts +0 -22
- package/dist/Divider/Divider.d.ts.map +1 -1
- package/dist/Markdown/parser.d.ts +7 -0
- package/dist/Markdown/parser.d.ts.map +1 -1
- package/dist/Markdown/parser.js +300 -6
- package/dist/MultiSelector/MultiSelector.d.ts +7 -1
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +10 -1
- package/dist/RadioList/RadioList.d.ts +7 -1
- package/dist/RadioList/RadioList.d.ts.map +1 -1
- package/dist/RadioList/RadioList.js +3 -1
- package/dist/RadioList/RadioListItem.d.ts.map +1 -1
- package/dist/RadioList/RadioListItem.js +6 -1
- package/dist/Selector/Selector.d.ts +6 -0
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +9 -0
- package/dist/Slider/Slider.d.ts +6 -0
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +10 -1
- package/dist/Spinner/Spinner.d.ts.map +1 -1
- package/dist/Spinner/Spinner.js +1 -1
- package/dist/Switch/Switch.d.ts +6 -1
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +7 -1
- package/dist/Table/BaseTable.d.ts.map +1 -1
- package/dist/Table/BaseTable.js +8 -1
- package/dist/Table/TableCell.d.ts.map +1 -1
- package/dist/Table/TableCell.js +73 -4
- package/dist/Table/index.d.ts +2 -0
- package/dist/Table/index.d.ts.map +1 -1
- package/dist/Table/index.js +1 -0
- package/dist/Table/plugins/rowExpansion/index.d.ts +3 -0
- package/dist/Table/plugins/rowExpansion/index.d.ts.map +1 -0
- package/dist/Table/plugins/rowExpansion/index.js +3 -0
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts +90 -0
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -0
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +398 -0
- package/dist/Table/tableContextMenu.d.ts +5 -1
- package/dist/Table/tableContextMenu.d.ts.map +1 -1
- package/dist/Table/tableContextMenu.js +10 -3
- package/dist/Text/Text.d.ts +1 -1
- package/dist/Text/Text.d.ts.map +1 -1
- package/dist/Text/Text.js +4 -3
- package/dist/Text/text.stylex.d.ts +35 -0
- package/dist/Text/text.stylex.d.ts.map +1 -1
- package/dist/Text/text.stylex.js +52 -1
- package/dist/Tokenizer/Tokenizer.d.ts +6 -1
- package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
- package/dist/Tokenizer/Tokenizer.js +10 -1
- package/dist/astryx.css +16 -0
- package/dist/astryx.umd.js +51 -49
- package/dist/astryx.umd.js.map +4 -4
- package/dist/docs-types.d.ts +12 -0
- package/dist/docs-types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/AspectRatio/AspectRatio.doc.mjs +1 -1
- package/src/Avatar/Avatar.doc.mjs +2 -2
- package/src/Button/Button.tsx +39 -6
- package/src/ButtonGroup/ButtonGroup.doc.mjs +1 -1
- package/src/Calendar/Calendar.test.tsx +106 -0
- package/src/Calendar/Calendar.tsx +59 -22
- package/src/Calendar/dayCellUtils.test.ts +306 -1
- package/src/Calendar/dayCellUtils.ts +206 -13
- package/src/Card/Card.doc.mjs +1 -1
- package/src/Chat/useChatNewMessages.test.tsx +4 -4
- package/src/CheckboxInput/CheckboxInput.doc.mjs +9 -1
- package/src/CheckboxInput/CheckboxInput.test.tsx +36 -0
- package/src/CheckboxInput/CheckboxInput.tsx +11 -0
- package/src/CheckboxList/CheckboxList.doc.mjs +1 -1
- package/src/Code/Code.test.tsx +54 -0
- package/src/Code/Code.tsx +48 -4
- package/src/Code/index.ts +1 -1
- package/src/CodeBlock/index.ts +1 -1
- package/src/CommandPalette/CommandPaletteEmpty.tsx +15 -1
- package/src/ContextMenu/ContextMenu.tsx +17 -1
- package/src/DateInput/DateInput.doc.mjs +6 -1
- package/src/DateRangeInput/DateRangeInput.doc.mjs +1 -1
- package/src/DateRangeInput/DateRangeInput.tsx +2 -2
- package/src/DateTimeInput/DateTimeInput.doc.mjs +1 -1
- package/src/DateTimeInput/DateTimeInput.tsx +1 -6
- package/src/Divider/Divider.tsx +1 -23
- package/src/DropdownMenu/DropdownMenu.test.tsx +5 -3
- package/src/EmptyState/EmptyState.doc.mjs +1 -1
- package/src/Field/Field.doc.mjs +1 -1
- package/src/FileInput/FileInput.doc.mjs +1 -1
- package/src/FormLayout/__snapshots__/FormLayout.test.tsx.snap +0 -3
- package/src/Grid/Grid.doc.mjs +3 -3
- package/src/Layout/__tests__/edgeCompensation.test.tsx +72 -0
- package/src/Markdown/Markdown.test.tsx +23 -0
- package/src/Markdown/incremental.test.ts +36 -0
- package/src/Markdown/parser.test.ts +210 -0
- package/src/Markdown/parser.ts +301 -6
- package/src/MobileNav/MobileNav.doc.mjs +20 -0
- package/src/MultiSelector/MultiSelector.doc.mjs +14 -1
- package/src/MultiSelector/MultiSelector.test.tsx +33 -0
- package/src/MultiSelector/MultiSelector.tsx +20 -0
- package/src/PowerSearch/PowerSearch.doc.mjs +2 -2
- package/src/RadioList/RadioList.doc.mjs +7 -1
- package/src/RadioList/RadioList.test.tsx +46 -0
- package/src/RadioList/RadioList.tsx +10 -1
- package/src/RadioList/RadioListItem.tsx +4 -0
- package/src/Section/Section.test.tsx +4 -10
- package/src/SegmentedControl/SegmentedControl.doc.mjs +1 -1
- package/src/Selector/Selector.doc.mjs +12 -1
- package/src/Selector/Selector.test.tsx +30 -0
- package/src/Selector/Selector.tsx +18 -0
- package/src/Slider/Slider.doc.mjs +14 -2
- package/src/Slider/Slider.test.tsx +30 -0
- package/src/Slider/Slider.tsx +20 -0
- package/src/Spinner/Spinner.tsx +1 -0
- package/src/Switch/Switch.doc.mjs +14 -2
- package/src/Switch/Switch.test.tsx +38 -0
- package/src/Switch/Switch.tsx +12 -0
- package/src/TabList/TabList.doc.mjs +3 -3
- package/src/Table/BaseTable.tsx +9 -1
- package/src/Table/Table.test.tsx +23 -0
- package/src/Table/TableCell.tsx +87 -8
- package/src/Table/index.ts +5 -0
- package/src/Table/plugins/rowExpansion/index.ts +7 -0
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.test.tsx +155 -0
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +590 -0
- package/src/Table/tableContextMenu.test.tsx +27 -3
- package/src/Table/tableContextMenu.tsx +20 -2
- package/src/Table/useTablePagination.doc.mjs +4 -1
- package/src/Table/useTableRowExpansion.doc.mjs +80 -0
- package/src/Table/useTableSortable.doc.mjs +4 -1
- package/src/Text/Text.doc.mjs +2 -2
- package/src/Text/Text.test.tsx +11 -0
- package/src/Text/Text.tsx +4 -2
- package/src/Text/text.stylex.ts +41 -0
- package/src/TextArea/TextArea.doc.mjs +1 -1
- package/src/TextInput/TextInput.doc.mjs +1 -1
- package/src/TimeInput/TimeInput.doc.mjs +11 -6
- package/src/Tokenizer/Tokenizer.doc.mjs +14 -2
- package/src/Tokenizer/Tokenizer.test.tsx +33 -0
- package/src/Tokenizer/Tokenizer.tsx +19 -0
- package/src/Toolbar/Toolbar.test.tsx +1 -1
- package/src/Typeahead/BaseTypeahead.doc.mjs +10 -0
- package/src/Typeahead/Typeahead.doc.mjs +6 -1
- package/src/docs-types.ts +12 -0
- package/src/hooks/useKeyboardHint.doc.mjs +6 -6
|
@@ -650,3 +650,39 @@ describe('streaming end-to-end: no raw syntax visible', () => {
|
|
|
650
650
|
});
|
|
651
651
|
});
|
|
652
652
|
});
|
|
653
|
+
|
|
654
|
+
describe('parseMarkdownIncremental link reference definitions', () => {
|
|
655
|
+
function firstLinkHref(blocks: BlockNode[]): string | undefined {
|
|
656
|
+
for (const block of blocks) {
|
|
657
|
+
if (block.type === 'paragraph') {
|
|
658
|
+
for (const node of block.children) {
|
|
659
|
+
if (node.type === 'link') {
|
|
660
|
+
return node.href;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return undefined;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
it('resolves a footer reference streamed across chunks (matches full parse)', () => {
|
|
669
|
+
const text = 'See [the docs][d] here.\n\n[d]: https://example.com/d\n';
|
|
670
|
+
const {final} = simulateStreaming(text, 5);
|
|
671
|
+
expect(final).toEqual(parseMarkdown(text));
|
|
672
|
+
expect(firstLinkHref(final)).toBe('https://example.com/d');
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
it('re-resolves an already-settled reference once its definition arrives', () => {
|
|
676
|
+
const state = createIncrementalState();
|
|
677
|
+
// The reference paragraph settles (blank line) before the definition.
|
|
678
|
+
parseMarkdownIncremental('See [d] here.\n\n', state);
|
|
679
|
+
const before = parseMarkdownIncremental('See [d] here.\n\n[d', state);
|
|
680
|
+
expect(firstLinkHref(before)).toBeUndefined();
|
|
681
|
+
// Definition completes — the settled paragraph must pick up the link.
|
|
682
|
+
const after = parseMarkdownIncremental(
|
|
683
|
+
'See [d] here.\n\n[d]: /docs\n',
|
|
684
|
+
state,
|
|
685
|
+
);
|
|
686
|
+
expect(firstLinkHref(after)).toBe('/docs');
|
|
687
|
+
});
|
|
688
|
+
});
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import {describe, it, expect} from 'vitest';
|
|
4
4
|
import {parseMarkdown, parseInline} from './parser';
|
|
5
|
+
import type {InlineNode} from './parser';
|
|
5
6
|
|
|
6
7
|
describe('parseInline', () => {
|
|
7
8
|
it('parses plain text', () => {
|
|
@@ -946,3 +947,212 @@ describe('citation parsing', () => {
|
|
|
946
947
|
});
|
|
947
948
|
});
|
|
948
949
|
});
|
|
950
|
+
|
|
951
|
+
// ---------------------------------------------------------------------------
|
|
952
|
+
// Link reference definitions
|
|
953
|
+
// ---------------------------------------------------------------------------
|
|
954
|
+
|
|
955
|
+
describe('link reference definitions', () => {
|
|
956
|
+
// Collect every link/image node across a block tree for concise assertions.
|
|
957
|
+
function collectLinks(
|
|
958
|
+
nodes: InlineNode[],
|
|
959
|
+
): {type: string; href?: string; src?: string; text: string}[] {
|
|
960
|
+
const out: {type: string; href?: string; src?: string; text: string}[] = [];
|
|
961
|
+
const textOf = (inlineNodes: InlineNode[]): string =>
|
|
962
|
+
inlineNodes
|
|
963
|
+
.map(inline =>
|
|
964
|
+
inline.type === 'text'
|
|
965
|
+
? inline.content
|
|
966
|
+
: 'children' in inline
|
|
967
|
+
? textOf(inline.children)
|
|
968
|
+
: '',
|
|
969
|
+
)
|
|
970
|
+
.join('');
|
|
971
|
+
for (const node of nodes) {
|
|
972
|
+
if (node.type === 'link') {
|
|
973
|
+
out.push({type: 'link', href: node.href, text: textOf(node.children)});
|
|
974
|
+
} else if (node.type === 'image') {
|
|
975
|
+
out.push({type: 'image', src: node.src, text: node.alt});
|
|
976
|
+
} else if ('children' in node) {
|
|
977
|
+
out.push(...collectLinks(node.children));
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
return out;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function paragraphLinks(input: string) {
|
|
984
|
+
const blocks = parseMarkdown(input);
|
|
985
|
+
const links = blocks.flatMap(block =>
|
|
986
|
+
block.type === 'paragraph' ? collectLinks(block.children) : [],
|
|
987
|
+
);
|
|
988
|
+
return {blocks, links};
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
it('resolves a full reference `[text][label]` and drops the definition', () => {
|
|
992
|
+
const {blocks, links} = paragraphLinks(
|
|
993
|
+
'See [the docs][docs] here.\n\n[docs]: https://example.com/docs\n',
|
|
994
|
+
);
|
|
995
|
+
// Only the paragraph survives; the definition line produces no block.
|
|
996
|
+
expect(blocks).toHaveLength(1);
|
|
997
|
+
expect(blocks[0].type).toBe('paragraph');
|
|
998
|
+
expect(links).toEqual([
|
|
999
|
+
{type: 'link', href: 'https://example.com/docs', text: 'the docs'},
|
|
1000
|
+
]);
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
it('resolves a collapsed reference `[text][]`', () => {
|
|
1004
|
+
const {links} = paragraphLinks(
|
|
1005
|
+
'See [the docs][].\n\n[the docs]: https://example.com/docs',
|
|
1006
|
+
);
|
|
1007
|
+
expect(links).toEqual([
|
|
1008
|
+
{type: 'link', href: 'https://example.com/docs', text: 'the docs'},
|
|
1009
|
+
]);
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
it('resolves a shortcut reference `[text]`', () => {
|
|
1013
|
+
const {links} = paragraphLinks(
|
|
1014
|
+
'See [the docs].\n\n[the docs]: https://example.com/docs',
|
|
1015
|
+
);
|
|
1016
|
+
expect(links).toEqual([
|
|
1017
|
+
{type: 'link', href: 'https://example.com/docs', text: 'the docs'},
|
|
1018
|
+
]);
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
it('resolves a reference that appears before its definition', () => {
|
|
1022
|
+
const {links} = paragraphLinks('[foo]\n\n[foo]: /bar');
|
|
1023
|
+
expect(links).toEqual([{type: 'link', href: '/bar', text: 'foo'}]);
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
it('matches labels case-insensitively with collapsed whitespace', () => {
|
|
1027
|
+
const {links} = paragraphLinks(
|
|
1028
|
+
'See [The Docs][DOCS].\n\n[docs]: https://example.com/d',
|
|
1029
|
+
);
|
|
1030
|
+
expect(links).toEqual([
|
|
1031
|
+
{type: 'link', href: 'https://example.com/d', text: 'The Docs'},
|
|
1032
|
+
]);
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
it('supports an angle-bracket destination and a title', () => {
|
|
1036
|
+
const {links} = paragraphLinks(
|
|
1037
|
+
'See [x].\n\n[x]: <https://example.com/x> "the title"',
|
|
1038
|
+
);
|
|
1039
|
+
expect(links).toEqual([
|
|
1040
|
+
{type: 'link', href: 'https://example.com/x', text: 'x'},
|
|
1041
|
+
]);
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
it('absorbs a title on the line after a title-less definition', () => {
|
|
1045
|
+
const {blocks, links} = paragraphLinks(
|
|
1046
|
+
'See [x].\n\n[x]: https://example.com/x\n "the title"\n',
|
|
1047
|
+
);
|
|
1048
|
+
// The continuation title line must not leak as its own paragraph.
|
|
1049
|
+
expect(blocks).toHaveLength(1);
|
|
1050
|
+
expect(links).toEqual([
|
|
1051
|
+
{type: 'link', href: 'https://example.com/x', text: 'x'},
|
|
1052
|
+
]);
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
it('resolves an empty angle-bracket destination to an empty href', () => {
|
|
1056
|
+
const {blocks, links} = paragraphLinks('[foo]\n\n[foo]: <>');
|
|
1057
|
+
expect(blocks).toHaveLength(1);
|
|
1058
|
+
expect(links).toEqual([{type: 'link', href: '', text: 'foo'}]);
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
it('recognizes a definition immediately after a heading (no blank line)', () => {
|
|
1062
|
+
const {blocks, links} = paragraphLinks('# Title\n[x]: /url\n\n[x]');
|
|
1063
|
+
expect(blocks.map(block => block.type)).toEqual(['heading', 'paragraph']);
|
|
1064
|
+
expect(links).toEqual([{type: 'link', href: '/url', text: 'x'}]);
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
it('recognizes a definition immediately after a closed code fence', () => {
|
|
1068
|
+
const {blocks, links} = paragraphLinks('```\ncode\n```\n[x]: /url\n\n[x]');
|
|
1069
|
+
expect(blocks.map(block => block.type)).toEqual(['codeblock', 'paragraph']);
|
|
1070
|
+
expect(links).toEqual([{type: 'link', href: '/url', text: 'x'}]);
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
it('uses the first definition when a label is defined twice', () => {
|
|
1074
|
+
const {links} = paragraphLinks('[a]\n\n[a]: /first\n[a]: /second');
|
|
1075
|
+
expect(links).toEqual([{type: 'link', href: '/first', text: 'a'}]);
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
it('resolves a blockquote-nested definition even with a top-level definition present', () => {
|
|
1079
|
+
const blocks = parseMarkdown('[outer]: /o\n\n> [inner]\n>\n> [inner]: /i');
|
|
1080
|
+
const quote = blocks.find(block => block.type === 'blockquote');
|
|
1081
|
+
expect(quote?.type).toBe('blockquote');
|
|
1082
|
+
if (quote != null && quote.type === 'blockquote') {
|
|
1083
|
+
const links = quote.children.flatMap(block =>
|
|
1084
|
+
block.type === 'paragraph' ? collectLinks(block.children) : [],
|
|
1085
|
+
);
|
|
1086
|
+
expect(links).toEqual([{type: 'link', href: '/i', text: 'inner'}]);
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
it('resolves a reference image `![alt][label]`', () => {
|
|
1091
|
+
const {links} = paragraphLinks('![logo][l]\n\n[l]: /logo.png');
|
|
1092
|
+
expect(links).toEqual([{type: 'image', src: '/logo.png', text: 'logo'}]);
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
it('does not treat a whitespace-only second label as collapsed', () => {
|
|
1096
|
+
// `[foo][ ]` is a full reference to the empty (normalized) label and matches
|
|
1097
|
+
// nothing; `[foo]` still resolves as a shortcut and `[ ]` stays literal.
|
|
1098
|
+
const blocks = parseMarkdown('[foo][ ]\n\n[foo]: /f');
|
|
1099
|
+
expect(blocks[0].type).toBe('paragraph');
|
|
1100
|
+
if (blocks[0].type === 'paragraph') {
|
|
1101
|
+
expect(collectLinks(blocks[0].children)).toEqual([
|
|
1102
|
+
{type: 'link', href: '/f', text: 'foo'},
|
|
1103
|
+
]);
|
|
1104
|
+
const literal = blocks[0].children
|
|
1105
|
+
.map(node => (node.type === 'text' ? node.content : ''))
|
|
1106
|
+
.join('');
|
|
1107
|
+
expect(literal).toContain('[ ]');
|
|
1108
|
+
}
|
|
1109
|
+
});
|
|
1110
|
+
|
|
1111
|
+
it('leaves an unresolved reference as literal text', () => {
|
|
1112
|
+
const {blocks, links} = paragraphLinks('See [the docs][missing].');
|
|
1113
|
+
expect(links).toEqual([]);
|
|
1114
|
+
expect(blocks[0].type).toBe('paragraph');
|
|
1115
|
+
if (blocks[0].type === 'paragraph') {
|
|
1116
|
+
expect(blocks[0].children).toContainEqual({
|
|
1117
|
+
type: 'text',
|
|
1118
|
+
content: 'See [the docs][missing].',
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
it('does not resolve a definition or reference inside a code fence', () => {
|
|
1124
|
+
const blocks = parseMarkdown('```\n[x]\n[x]: /should-not-resolve\n```');
|
|
1125
|
+
expect(blocks).toHaveLength(1);
|
|
1126
|
+
expect(blocks[0].type).toBe('codeblock');
|
|
1127
|
+
if (blocks[0].type === 'codeblock') {
|
|
1128
|
+
expect(blocks[0].content).toBe('[x]\n[x]: /should-not-resolve');
|
|
1129
|
+
}
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
it('does not treat a definition-shaped line as a definition mid-paragraph', () => {
|
|
1133
|
+
// CommonMark: a definition cannot interrupt a paragraph.
|
|
1134
|
+
const {blocks, links} = paragraphLinks('Foo\n[bar]: /baz');
|
|
1135
|
+
expect(links).toEqual([]);
|
|
1136
|
+
expect(blocks).toHaveLength(1);
|
|
1137
|
+
expect(blocks[0].type).toBe('paragraph');
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1140
|
+
it('leaves footnote markers and definitions untouched', () => {
|
|
1141
|
+
const blocks = parseMarkdown(
|
|
1142
|
+
'A claim.[^1]\n\n[^1]: https://example.com/note',
|
|
1143
|
+
);
|
|
1144
|
+
// No link nodes; the `[^1]` marker stays literal and `[^1]:` is not
|
|
1145
|
+
// treated as a link reference definition (footnotes are out of scope).
|
|
1146
|
+
const links = blocks.flatMap(block =>
|
|
1147
|
+
block.type === 'paragraph' ? collectLinks(block.children) : [],
|
|
1148
|
+
);
|
|
1149
|
+
expect(links).toEqual([]);
|
|
1150
|
+
expect(blocks).toHaveLength(2);
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
it('leaves ordinary bracketed text with no definition alone', () => {
|
|
1154
|
+
const {links, blocks} = paragraphLinks('an array like [1, 2, 3] here');
|
|
1155
|
+
expect(links).toEqual([]);
|
|
1156
|
+
expect(blocks[0].type).toBe('paragraph');
|
|
1157
|
+
});
|
|
1158
|
+
});
|
package/src/Markdown/parser.ts
CHANGED
|
@@ -81,6 +81,13 @@ export type ParseOptions = {
|
|
|
81
81
|
type ResolvedOptions = {
|
|
82
82
|
readonly sourceIds: ReadonlySet<string> | undefined;
|
|
83
83
|
readonly autolink: 'gfm' | undefined;
|
|
84
|
+
/**
|
|
85
|
+
* Link reference definitions (`[label]: url`) collected from the whole
|
|
86
|
+
* document, keyed by normalized label. Internal only — populated by the
|
|
87
|
+
* block parser, never by the public `ParseOptions`. Enables `parseInlineImpl`
|
|
88
|
+
* to resolve full/collapsed/shortcut reference links and images.
|
|
89
|
+
*/
|
|
90
|
+
readonly linkDefs?: ReadonlyMap<string, string>;
|
|
84
91
|
};
|
|
85
92
|
|
|
86
93
|
const EMPTY_OPTS: ResolvedOptions = {sourceIds: undefined, autolink: undefined};
|
|
@@ -103,6 +110,228 @@ function resolveOptions(
|
|
|
103
110
|
return {sourceIds: opts.sourceIds, autolink: opts.autolink};
|
|
104
111
|
}
|
|
105
112
|
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// Link reference definitions
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
// A CommonMark link reference definition line: up to 3 leading spaces, a
|
|
118
|
+
// bracketed label, `:`, a destination (bare or `<...>`), and an optional
|
|
119
|
+
// same-line title (captured as group 4 so a title-less definition can absorb a
|
|
120
|
+
// title on the following line). `^`-leading labels (`[^1]:`) are footnote
|
|
121
|
+
// definitions — a separate, unsupported feature — and are excluded so they
|
|
122
|
+
// pass through verbatim.
|
|
123
|
+
const LINK_DEFINITION_RE =
|
|
124
|
+
/^ {0,3}\[([^\]^](?:\\.|[^\]\\])*)\]:[ \t]*(?:<([^<>\n]*)>|(\S+))([ \t]+(?:"[^"\n]*"|'[^'\n]*'|\([^()\n]*\)))?[ \t]*$/;
|
|
125
|
+
|
|
126
|
+
// A line that is nothing but a title — the continuation form allowed when a
|
|
127
|
+
// definition's destination is followed by its title on the next line.
|
|
128
|
+
const LINK_TITLE_ONLY_RE = /^ {0,3}(?:"[^"\n]*"|'[^'\n]*'|\([^()\n]*\))[ \t]*$/;
|
|
129
|
+
|
|
130
|
+
// CommonMark matches reference labels case-insensitively with leading/trailing
|
|
131
|
+
// whitespace stripped and internal whitespace runs collapsed to one space.
|
|
132
|
+
function normalizeLinkLabel(label: string): string {
|
|
133
|
+
return label.trim().replace(/\s+/g, ' ').toLowerCase();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function matchLinkDefinition(
|
|
137
|
+
line: string,
|
|
138
|
+
): {label: string; destination: string; hasTitle: boolean} | null {
|
|
139
|
+
const match = LINK_DEFINITION_RE.exec(line);
|
|
140
|
+
if (match == null) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
const label = normalizeLinkLabel(match[1]);
|
|
144
|
+
// `match[2]` is the `<...>` destination (present but possibly empty, e.g.
|
|
145
|
+
// `<>` → empty href, valid per CommonMark); `match[3]` is the bare
|
|
146
|
+
// destination (always non-empty). One of the two always matches.
|
|
147
|
+
const destination = match[2] != null ? match[2] : match[3];
|
|
148
|
+
if (label === '' || destination == null) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
return {label, destination, hasTitle: match[4] != null};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Collect link reference definitions from the whole document and return the
|
|
156
|
+
* input with the definition lines removed. A definition is recognized at a
|
|
157
|
+
* block boundary — document start, after a blank line, after another
|
|
158
|
+
* definition, or after a self-contained block (heading / thematic break /
|
|
159
|
+
* closed fenced code) — but never inside a fenced code block or as a lazy
|
|
160
|
+
* continuation of a paragraph, honoring CommonMark's rule that a definition
|
|
161
|
+
* cannot interrupt a paragraph. First definition wins, and definitions produce
|
|
162
|
+
* no output so stripping unreferenced ones is correct.
|
|
163
|
+
*
|
|
164
|
+
* Scope limit: definitions are collected at the top level only, and a
|
|
165
|
+
* definition directly following a list, blockquote, or table (with no blank
|
|
166
|
+
* line between) is not recognized. A definition nested inside a blockquote or
|
|
167
|
+
* list item resolves within that container (via the recursive parse) but is
|
|
168
|
+
* not exposed to references elsewhere in the document, unlike full CommonMark
|
|
169
|
+
* where every definition is global. Separating a footer definition block with
|
|
170
|
+
* a blank line — the usual form — always works.
|
|
171
|
+
*/
|
|
172
|
+
function extractLinkDefinitions(input: string): {
|
|
173
|
+
defs: ReadonlyMap<string, string>;
|
|
174
|
+
cleaned: string;
|
|
175
|
+
} {
|
|
176
|
+
const lines = input.split('\n');
|
|
177
|
+
const defs = new Map<string, string>();
|
|
178
|
+
const keep = new Array<boolean>(lines.length).fill(true);
|
|
179
|
+
let atBoundary = true;
|
|
180
|
+
let inFence = false;
|
|
181
|
+
let fenceMarker = '';
|
|
182
|
+
|
|
183
|
+
for (let index = 0; index < lines.length; index++) {
|
|
184
|
+
const line = lines[index];
|
|
185
|
+
if (inFence) {
|
|
186
|
+
if (line.startsWith(fenceMarker)) {
|
|
187
|
+
inFence = false;
|
|
188
|
+
fenceMarker = '';
|
|
189
|
+
// The line after a closed fence begins a new block.
|
|
190
|
+
atBoundary = true;
|
|
191
|
+
} else {
|
|
192
|
+
atBoundary = false;
|
|
193
|
+
}
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
const fenceMatch = line.match(/^(`{3,}|~{3,})/);
|
|
197
|
+
if (fenceMatch) {
|
|
198
|
+
inFence = true;
|
|
199
|
+
fenceMarker = fenceMatch[1];
|
|
200
|
+
atBoundary = false;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (line.trim() === '') {
|
|
204
|
+
atBoundary = true;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (atBoundary) {
|
|
208
|
+
const def = matchLinkDefinition(line);
|
|
209
|
+
if (def != null) {
|
|
210
|
+
if (!defs.has(def.label)) {
|
|
211
|
+
defs.set(def.label, def.destination);
|
|
212
|
+
}
|
|
213
|
+
keep[index] = false;
|
|
214
|
+
// A title-less definition absorbs a title on the following line
|
|
215
|
+
// (CommonMark), which then also produces no output.
|
|
216
|
+
if (
|
|
217
|
+
!def.hasTitle &&
|
|
218
|
+
index + 1 < lines.length &&
|
|
219
|
+
LINK_TITLE_ONLY_RE.test(lines[index + 1])
|
|
220
|
+
) {
|
|
221
|
+
keep[index + 1] = false;
|
|
222
|
+
index++;
|
|
223
|
+
}
|
|
224
|
+
// Consecutive definitions stay at a block boundary.
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// A heading or thematic break is a self-contained single-line block, so
|
|
229
|
+
// the next line begins a new block where a definition may appear.
|
|
230
|
+
atBoundary = /^ {0,3}#{1,6}(?: |\t|$)/.test(line) || isHorizontalRule(line);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (defs.size === 0) {
|
|
234
|
+
return {defs, cleaned: input};
|
|
235
|
+
}
|
|
236
|
+
const cleaned = lines.filter((_, index) => keep[index]).join('\n');
|
|
237
|
+
return {defs, cleaned};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Order-independent signature of a link-definition set, for cache checks. */
|
|
241
|
+
function linkDefsSignature(defs: ReadonlyMap<string, string>): string {
|
|
242
|
+
if (defs.size === 0) {
|
|
243
|
+
return '';
|
|
244
|
+
}
|
|
245
|
+
return [...defs]
|
|
246
|
+
.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
|
|
247
|
+
.map(([label, dest]) => `${label}\u0000${dest}`)
|
|
248
|
+
.join('\u0001');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Resolve a full (`[text][label]`), collapsed (`[text][]`), or shortcut
|
|
253
|
+
* (`[text]`) reference at `start` (which points at `[`) against `linkDefs`.
|
|
254
|
+
* Returns the node plus the index just past the reference, or null when it is
|
|
255
|
+
* not a resolvable reference (caller falls through to literal handling).
|
|
256
|
+
*/
|
|
257
|
+
function matchReferenceLink(
|
|
258
|
+
text: string,
|
|
259
|
+
start: number,
|
|
260
|
+
linkDefs: ReadonlyMap<string, string>,
|
|
261
|
+
opts: ResolvedOptions,
|
|
262
|
+
): {node: InlineNode; end: number} | null {
|
|
263
|
+
const textClose = text.indexOf(']', start + 1);
|
|
264
|
+
if (textClose === -1) {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
const linkText = text.slice(start + 1, textClose);
|
|
268
|
+
// Full `[text][label]` / collapsed `[text][]` — a matching definition wins.
|
|
269
|
+
if (text[textClose + 1] === '[') {
|
|
270
|
+
const labelClose = text.indexOf(']', textClose + 2);
|
|
271
|
+
if (labelClose !== -1) {
|
|
272
|
+
const rawLabel = text.slice(textClose + 2, labelClose);
|
|
273
|
+
// Only truly-empty brackets are the collapsed form; a whitespace-only
|
|
274
|
+
// label (`[ ]`) is a full reference whose normalized label is empty and
|
|
275
|
+
// matches nothing.
|
|
276
|
+
const label = rawLabel === '' ? linkText : rawLabel;
|
|
277
|
+
const href = linkDefs.get(normalizeLinkLabel(label));
|
|
278
|
+
if (href != null) {
|
|
279
|
+
return {
|
|
280
|
+
node: {type: 'link', href, children: parseInlineImpl(linkText, opts)},
|
|
281
|
+
end: labelClose + 1,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
// No match — fall back to a shortcut `[text]` (CommonMark back-off),
|
|
285
|
+
// leaving the trailing `[label]` to be parsed separately.
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// Shortcut: `[text]`.
|
|
289
|
+
if (linkText.trim() === '') {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
const href = linkDefs.get(normalizeLinkLabel(linkText));
|
|
293
|
+
if (href == null) {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
node: {type: 'link', href, children: parseInlineImpl(linkText, opts)},
|
|
298
|
+
end: textClose + 1,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Reference-image equivalent of {@link matchReferenceLink} (`![alt][label]`). */
|
|
303
|
+
function matchReferenceImage(
|
|
304
|
+
text: string,
|
|
305
|
+
start: number,
|
|
306
|
+
linkDefs: ReadonlyMap<string, string>,
|
|
307
|
+
): {node: InlineNode; end: number} | null {
|
|
308
|
+
const altClose = text.indexOf(']', start + 2);
|
|
309
|
+
if (altClose === -1) {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
const alt = text.slice(start + 2, altClose);
|
|
313
|
+
if (text[altClose + 1] === '[') {
|
|
314
|
+
const labelClose = text.indexOf(']', altClose + 2);
|
|
315
|
+
if (labelClose !== -1) {
|
|
316
|
+
const rawLabel = text.slice(altClose + 2, labelClose);
|
|
317
|
+
const label = rawLabel === '' ? alt : rawLabel;
|
|
318
|
+
const src = linkDefs.get(normalizeLinkLabel(label));
|
|
319
|
+
if (src != null) {
|
|
320
|
+
return {node: {type: 'image', src, alt}, end: labelClose + 1};
|
|
321
|
+
}
|
|
322
|
+
// No match — fall back to a shortcut `![alt]`.
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (alt.trim() === '') {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
const src = linkDefs.get(normalizeLinkLabel(alt));
|
|
329
|
+
if (src == null) {
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
return {node: {type: 'image', src, alt}, end: altClose + 1};
|
|
333
|
+
}
|
|
334
|
+
|
|
106
335
|
// ---------------------------------------------------------------------------
|
|
107
336
|
// Inline parser helpers
|
|
108
337
|
// ---------------------------------------------------------------------------
|
|
@@ -260,6 +489,16 @@ function parseInlineImpl(text: string, opts: ResolvedOptions): InlineNode[] {
|
|
|
260
489
|
}
|
|
261
490
|
}
|
|
262
491
|
|
|
492
|
+
// --- Reference image ![alt][label] / ![alt][] / ![alt] ---
|
|
493
|
+
if (opts.linkDefs != null && text[i] === '!' && text[i + 1] === '[') {
|
|
494
|
+
const ref = matchReferenceImage(text, i, opts.linkDefs);
|
|
495
|
+
if (ref) {
|
|
496
|
+
nodes.push(ref.node);
|
|
497
|
+
i = ref.end;
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
263
502
|
// --- Citation: bracket [id] (before link — link requires `(` after `]`) ---
|
|
264
503
|
{
|
|
265
504
|
const citation = matchBracketCitation(text, i, opts);
|
|
@@ -287,6 +526,16 @@ function parseInlineImpl(text: string, opts: ResolvedOptions): InlineNode[] {
|
|
|
287
526
|
}
|
|
288
527
|
}
|
|
289
528
|
|
|
529
|
+
// --- Reference link [text][label] / [text][] / [text] ---
|
|
530
|
+
if (opts.linkDefs != null && text[i] === '[') {
|
|
531
|
+
const ref = matchReferenceLink(text, i, opts.linkDefs, opts);
|
|
532
|
+
if (ref) {
|
|
533
|
+
nodes.push(ref.node);
|
|
534
|
+
i = ref.end;
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
290
539
|
// --- Bold-italic: *** or ___ ---
|
|
291
540
|
if (
|
|
292
541
|
(text[i] === '*' && text[i + 1] === '*' && text[i + 2] === '*') ||
|
|
@@ -971,8 +1220,30 @@ export function parseMarkdown(
|
|
|
971
1220
|
return parseMarkdownImpl(input, resolveOptions(arg));
|
|
972
1221
|
}
|
|
973
1222
|
|
|
974
|
-
function parseMarkdownImpl(
|
|
975
|
-
|
|
1223
|
+
function parseMarkdownImpl(
|
|
1224
|
+
input: string,
|
|
1225
|
+
baseOpts: ResolvedOptions,
|
|
1226
|
+
): BlockNode[] {
|
|
1227
|
+
// Collect this input's link reference definitions and strip their lines,
|
|
1228
|
+
// then merge them with any definitions inherited from an enclosing parse
|
|
1229
|
+
// (the incremental parser passes the whole document's definitions in; a
|
|
1230
|
+
// recursive blockquote/list parse inherits the outer definitions). Inherited
|
|
1231
|
+
// definitions win on conflict, matching CommonMark's first-definition-wins
|
|
1232
|
+
// in document order; locally-nested definitions still resolve within this
|
|
1233
|
+
// parse.
|
|
1234
|
+
const {defs, cleaned} = extractLinkDefinitions(input);
|
|
1235
|
+
const inherited = baseOpts.linkDefs;
|
|
1236
|
+
let linkDefs: ReadonlyMap<string, string> | undefined;
|
|
1237
|
+
if (defs.size === 0) {
|
|
1238
|
+
linkDefs = inherited;
|
|
1239
|
+
} else if (inherited == null) {
|
|
1240
|
+
linkDefs = defs;
|
|
1241
|
+
} else {
|
|
1242
|
+
linkDefs = new Map<string, string>([...defs, ...inherited]);
|
|
1243
|
+
}
|
|
1244
|
+
const opts: ResolvedOptions =
|
|
1245
|
+
linkDefs != null ? {...baseOpts, linkDefs} : baseOpts;
|
|
1246
|
+
const lines = cleaned.split('\n');
|
|
976
1247
|
const blocks: BlockNode[] = [];
|
|
977
1248
|
let index = 0;
|
|
978
1249
|
|
|
@@ -1106,6 +1377,13 @@ export interface IncrementalState {
|
|
|
1106
1377
|
* with newly-arriving content.
|
|
1107
1378
|
*/
|
|
1108
1379
|
autolink?: 'gfm';
|
|
1380
|
+
/**
|
|
1381
|
+
* Signature of the link reference definitions the cached `settledBlocks`
|
|
1382
|
+
* were parsed with. Definitions are document-global and typically arrive
|
|
1383
|
+
* (in a footer) after the references that use them, so when the set changes
|
|
1384
|
+
* the settled cache is invalidated to let earlier references resolve.
|
|
1385
|
+
*/
|
|
1386
|
+
linkDefsKey?: string;
|
|
1109
1387
|
}
|
|
1110
1388
|
|
|
1111
1389
|
export function createIncrementalState(): IncrementalState {
|
|
@@ -1434,16 +1712,33 @@ export function parseMarkdownIncremental(
|
|
|
1434
1712
|
state.settledText = '';
|
|
1435
1713
|
state.settledBlocks = [];
|
|
1436
1714
|
state.settledUpTo = 0;
|
|
1715
|
+
state.linkDefsKey = undefined;
|
|
1437
1716
|
return [];
|
|
1438
1717
|
}
|
|
1439
1718
|
|
|
1719
|
+
// Link reference definitions are document-global and usually stream in
|
|
1720
|
+
// (as a footer) after the references that use them, so collect them from the
|
|
1721
|
+
// whole input and pass them into every slice parse. When the set changes,
|
|
1722
|
+
// invalidate the settled cache so already-settled references re-resolve.
|
|
1723
|
+
const {defs: linkDefs} = extractLinkDefinitions(input);
|
|
1724
|
+
const linkDefsKey = linkDefsSignature(linkDefs);
|
|
1725
|
+
if (state.linkDefsKey !== linkDefsKey) {
|
|
1726
|
+
state.prevInput = '';
|
|
1727
|
+
state.settledText = '';
|
|
1728
|
+
state.settledBlocks = [];
|
|
1729
|
+
state.settledUpTo = 0;
|
|
1730
|
+
state.linkDefsKey = linkDefsKey;
|
|
1731
|
+
}
|
|
1732
|
+
const parseOpts: ResolvedOptions =
|
|
1733
|
+
linkDefs.size > 0 ? {...opts, linkDefs} : opts;
|
|
1734
|
+
|
|
1440
1735
|
const lines = input.split('\n');
|
|
1441
1736
|
const boundary = findSettledBoundary(lines);
|
|
1442
1737
|
|
|
1443
1738
|
if (boundary < 0) {
|
|
1444
1739
|
// Inside an unclosed fence or no blank-line boundary — full re-parse
|
|
1445
1740
|
state.prevInput = input;
|
|
1446
|
-
return parseMarkdownImpl(input,
|
|
1741
|
+
return parseMarkdownImpl(input, parseOpts);
|
|
1447
1742
|
}
|
|
1448
1743
|
|
|
1449
1744
|
const settledText = lines.slice(0, boundary).join('\n');
|
|
@@ -1461,15 +1756,15 @@ export function parseMarkdownIncremental(
|
|
|
1461
1756
|
) {
|
|
1462
1757
|
// Settled portion grew — parse only the new delta
|
|
1463
1758
|
const delta = settledText.slice(state.settledText.length);
|
|
1464
|
-
const deltaBlocks = parseMarkdownImpl(delta,
|
|
1759
|
+
const deltaBlocks = parseMarkdownImpl(delta, parseOpts);
|
|
1465
1760
|
settledBlocks = mergeSettledBlocks(state.settledBlocks, deltaBlocks);
|
|
1466
1761
|
} else {
|
|
1467
1762
|
// Content before the boundary changed — full re-parse of settled portion
|
|
1468
|
-
settledBlocks = parseMarkdownImpl(settledText,
|
|
1763
|
+
settledBlocks = parseMarkdownImpl(settledText, parseOpts);
|
|
1469
1764
|
}
|
|
1470
1765
|
|
|
1471
1766
|
const unsettledBlocks = unsettledText
|
|
1472
|
-
? parseMarkdownImpl(unsettledText,
|
|
1767
|
+
? parseMarkdownImpl(unsettledText, parseOpts)
|
|
1473
1768
|
: [];
|
|
1474
1769
|
|
|
1475
1770
|
state.settledText = settledText;
|
|
@@ -14,6 +14,26 @@ export const docs = {
|
|
|
14
14
|
name: 'MobileNav',
|
|
15
15
|
displayName: 'Mobile Nav',
|
|
16
16
|
description: 'A slide-out drawer for mobile navigation. Accepts SideNav children.',
|
|
17
|
+
// The drawer opens via showModal() and renders nothing while closed —
|
|
18
|
+
// overlay mode gives the Properties preview an open trigger instead of
|
|
19
|
+
// an empty stage (#2706). Declared on this entry (not the directory
|
|
20
|
+
// doc) so MobileNavToggle does not inherit it.
|
|
21
|
+
playground: {
|
|
22
|
+
overlay: true,
|
|
23
|
+
defaults: {
|
|
24
|
+
isOpen: false,
|
|
25
|
+
header: 'Navigation',
|
|
26
|
+
children: {
|
|
27
|
+
__element: 'SideNavSection',
|
|
28
|
+
props: {title: 'Main'},
|
|
29
|
+
children: [
|
|
30
|
+
{__element: 'SideNavItem', props: {label: 'Dashboard', isSelected: true}},
|
|
31
|
+
{__element: 'SideNavItem', props: {label: 'Projects'}},
|
|
32
|
+
{__element: 'SideNavItem', props: {label: 'Settings'}},
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
17
37
|
props: [
|
|
18
38
|
{
|
|
19
39
|
name: 'isOpen',
|