@furystack/shades-common-components 12.1.0 → 12.3.0

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 (123) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/esm/components/avatar.d.ts.map +1 -1
  3. package/esm/components/avatar.js +3 -1
  4. package/esm/components/avatar.js.map +1 -1
  5. package/esm/components/avatar.spec.js +4 -4
  6. package/esm/components/avatar.spec.js.map +1 -1
  7. package/esm/components/icons/icon-definitions.d.ts +82 -0
  8. package/esm/components/icons/icon-definitions.d.ts.map +1 -1
  9. package/esm/components/icons/icon-definitions.js +717 -0
  10. package/esm/components/icons/icon-definitions.js.map +1 -1
  11. package/esm/components/icons/icon-definitions.spec.js +22 -2
  12. package/esm/components/icons/icon-definitions.spec.js.map +1 -1
  13. package/esm/components/icons/icon-types.d.ts +10 -0
  14. package/esm/components/icons/icon-types.d.ts.map +1 -1
  15. package/esm/components/icons/index.d.ts +1 -1
  16. package/esm/components/icons/index.d.ts.map +1 -1
  17. package/esm/components/index.d.ts +1 -0
  18. package/esm/components/index.d.ts.map +1 -1
  19. package/esm/components/index.js +1 -0
  20. package/esm/components/index.js.map +1 -1
  21. package/esm/components/markdown/index.d.ts +5 -0
  22. package/esm/components/markdown/index.d.ts.map +1 -0
  23. package/esm/components/markdown/index.js +5 -0
  24. package/esm/components/markdown/index.js.map +1 -0
  25. package/esm/components/markdown/markdown-display.d.ts +19 -0
  26. package/esm/components/markdown/markdown-display.d.ts.map +1 -0
  27. package/esm/components/markdown/markdown-display.js +149 -0
  28. package/esm/components/markdown/markdown-display.js.map +1 -0
  29. package/esm/components/markdown/markdown-display.spec.d.ts +2 -0
  30. package/esm/components/markdown/markdown-display.spec.d.ts.map +1 -0
  31. package/esm/components/markdown/markdown-display.spec.js +191 -0
  32. package/esm/components/markdown/markdown-display.spec.js.map +1 -0
  33. package/esm/components/markdown/markdown-editor.d.ts +25 -0
  34. package/esm/components/markdown/markdown-editor.d.ts.map +1 -0
  35. package/esm/components/markdown/markdown-editor.js +113 -0
  36. package/esm/components/markdown/markdown-editor.js.map +1 -0
  37. package/esm/components/markdown/markdown-editor.spec.d.ts +2 -0
  38. package/esm/components/markdown/markdown-editor.spec.d.ts.map +1 -0
  39. package/esm/components/markdown/markdown-editor.spec.js +111 -0
  40. package/esm/components/markdown/markdown-editor.spec.js.map +1 -0
  41. package/esm/components/markdown/markdown-input.d.ts +29 -0
  42. package/esm/components/markdown/markdown-input.d.ts.map +1 -0
  43. package/esm/components/markdown/markdown-input.js +100 -0
  44. package/esm/components/markdown/markdown-input.js.map +1 -0
  45. package/esm/components/markdown/markdown-input.spec.d.ts +2 -0
  46. package/esm/components/markdown/markdown-input.spec.d.ts.map +1 -0
  47. package/esm/components/markdown/markdown-input.spec.js +215 -0
  48. package/esm/components/markdown/markdown-input.spec.js.map +1 -0
  49. package/esm/components/markdown/markdown-parser.d.ts +82 -0
  50. package/esm/components/markdown/markdown-parser.d.ts.map +1 -0
  51. package/esm/components/markdown/markdown-parser.js +274 -0
  52. package/esm/components/markdown/markdown-parser.js.map +1 -0
  53. package/esm/components/markdown/markdown-parser.spec.d.ts +2 -0
  54. package/esm/components/markdown/markdown-parser.spec.d.ts.map +1 -0
  55. package/esm/components/markdown/markdown-parser.spec.js +229 -0
  56. package/esm/components/markdown/markdown-parser.spec.js.map +1 -0
  57. package/esm/components/page-container/index.d.ts +1 -1
  58. package/esm/components/page-container/index.js +1 -1
  59. package/esm/components/page-container/page-header.d.ts +5 -5
  60. package/esm/components/page-container/page-header.d.ts.map +1 -1
  61. package/esm/components/page-container/page-header.js +3 -3
  62. package/esm/components/styles.d.ts +1 -0
  63. package/esm/components/styles.d.ts.map +1 -1
  64. package/esm/components/styles.js.map +1 -1
  65. package/esm/components/suggest/index.d.ts +1 -1
  66. package/esm/components/suggest/index.d.ts.map +1 -1
  67. package/esm/components/typography.d.ts.map +1 -1
  68. package/esm/components/typography.js +26 -14
  69. package/esm/components/typography.js.map +1 -1
  70. package/esm/services/css-variable-theme.d.ts +3 -0
  71. package/esm/services/css-variable-theme.d.ts.map +1 -1
  72. package/esm/services/css-variable-theme.js +3 -0
  73. package/esm/services/css-variable-theme.js.map +1 -1
  74. package/esm/services/css-variable-theme.spec.js +3 -0
  75. package/esm/services/css-variable-theme.spec.js.map +1 -1
  76. package/esm/services/default-dark-palette.d.ts +8 -0
  77. package/esm/services/default-dark-palette.d.ts.map +1 -0
  78. package/esm/services/default-dark-palette.js +56 -0
  79. package/esm/services/default-dark-palette.js.map +1 -0
  80. package/esm/services/default-dark-theme.d.ts +3 -0
  81. package/esm/services/default-dark-theme.d.ts.map +1 -1
  82. package/esm/services/default-dark-theme.js +7 -4
  83. package/esm/services/default-dark-theme.js.map +1 -1
  84. package/esm/services/default-light-theme.d.ts +3 -0
  85. package/esm/services/default-light-theme.d.ts.map +1 -1
  86. package/esm/services/default-light-theme.js +3 -0
  87. package/esm/services/default-light-theme.js.map +1 -1
  88. package/esm/services/index.d.ts +1 -0
  89. package/esm/services/index.d.ts.map +1 -1
  90. package/esm/services/index.js +1 -0
  91. package/esm/services/index.js.map +1 -1
  92. package/esm/services/theme-provider-service.d.ts +10 -1
  93. package/esm/services/theme-provider-service.d.ts.map +1 -1
  94. package/esm/services/theme-provider-service.js.map +1 -1
  95. package/package.json +3 -3
  96. package/src/components/avatar.spec.tsx +4 -4
  97. package/src/components/avatar.tsx +3 -1
  98. package/src/components/icons/icon-definitions.spec.ts +28 -2
  99. package/src/components/icons/icon-definitions.ts +759 -0
  100. package/src/components/icons/icon-types.ts +12 -0
  101. package/src/components/icons/index.ts +1 -1
  102. package/src/components/index.ts +1 -0
  103. package/src/components/markdown/index.ts +4 -0
  104. package/src/components/markdown/markdown-display.spec.tsx +243 -0
  105. package/src/components/markdown/markdown-display.tsx +202 -0
  106. package/src/components/markdown/markdown-editor.spec.tsx +142 -0
  107. package/src/components/markdown/markdown-editor.tsx +167 -0
  108. package/src/components/markdown/markdown-input.spec.tsx +274 -0
  109. package/src/components/markdown/markdown-input.tsx +143 -0
  110. package/src/components/markdown/markdown-parser.spec.ts +258 -0
  111. package/src/components/markdown/markdown-parser.ts +333 -0
  112. package/src/components/page-container/index.tsx +1 -1
  113. package/src/components/page-container/page-header.tsx +5 -5
  114. package/src/components/styles.tsx +1 -0
  115. package/src/components/suggest/index.tsx +1 -1
  116. package/src/components/typography.tsx +28 -15
  117. package/src/services/css-variable-theme.spec.ts +3 -0
  118. package/src/services/css-variable-theme.ts +3 -0
  119. package/src/services/default-dark-palette.ts +57 -0
  120. package/src/services/default-dark-theme.ts +7 -4
  121. package/src/services/default-light-theme.ts +3 -0
  122. package/src/services/index.ts +1 -0
  123. package/src/services/theme-provider-service.ts +7 -1
@@ -0,0 +1,258 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { parseInline, parseMarkdown, toggleCheckbox } from './markdown-parser.js'
3
+
4
+ describe('parseInline', () => {
5
+ it('should parse plain text', () => {
6
+ const result = parseInline('hello world')
7
+ expect(result).toEqual([{ type: 'text', content: 'hello world' }])
8
+ })
9
+
10
+ it('should parse inline code', () => {
11
+ const result = parseInline('use `const x = 1` here')
12
+ expect(result).toEqual([
13
+ { type: 'text', content: 'use ' },
14
+ { type: 'code', content: 'const x = 1' },
15
+ { type: 'text', content: ' here' },
16
+ ])
17
+ })
18
+
19
+ it('should parse bold with **', () => {
20
+ const result = parseInline('this is **bold** text')
21
+ expect(result).toEqual([
22
+ { type: 'text', content: 'this is ' },
23
+ { type: 'bold', children: [{ type: 'text', content: 'bold' }] },
24
+ { type: 'text', content: ' text' },
25
+ ])
26
+ })
27
+
28
+ it('should parse italic with *', () => {
29
+ const result = parseInline('this is *italic* text')
30
+ expect(result).toEqual([
31
+ { type: 'text', content: 'this is ' },
32
+ { type: 'italic', children: [{ type: 'text', content: 'italic' }] },
33
+ { type: 'text', content: ' text' },
34
+ ])
35
+ })
36
+
37
+ it('should parse bold+italic with ***', () => {
38
+ const result = parseInline('this is ***bold italic*** text')
39
+ expect(result).toEqual([
40
+ { type: 'text', content: 'this is ' },
41
+ { type: 'bold', children: [{ type: 'italic', children: [{ type: 'text', content: 'bold italic' }] }] },
42
+ { type: 'text', content: ' text' },
43
+ ])
44
+ })
45
+
46
+ it('should parse links', () => {
47
+ const result = parseInline('click [here](https://example.com) now')
48
+ expect(result).toEqual([
49
+ { type: 'text', content: 'click ' },
50
+ { type: 'link', href: 'https://example.com', children: [{ type: 'text', content: 'here' }] },
51
+ { type: 'text', content: ' now' },
52
+ ])
53
+ })
54
+
55
+ it('should parse images', () => {
56
+ const result = parseInline('see ![alt text](image.png) here')
57
+ expect(result).toEqual([
58
+ { type: 'text', content: 'see ' },
59
+ { type: 'image', src: 'image.png', alt: 'alt text' },
60
+ { type: 'text', content: ' here' },
61
+ ])
62
+ })
63
+
64
+ it('should parse nested bold in link', () => {
65
+ const result = parseInline('[**bold link**](url)')
66
+ expect(result).toEqual([
67
+ {
68
+ type: 'link',
69
+ href: 'url',
70
+ children: [{ type: 'bold', children: [{ type: 'text', content: 'bold link' }] }],
71
+ },
72
+ ])
73
+ })
74
+
75
+ it('should handle empty string', () => {
76
+ expect(parseInline('')).toEqual([])
77
+ })
78
+ })
79
+
80
+ describe('parseMarkdown', () => {
81
+ it('should return empty array for empty input', () => {
82
+ expect(parseMarkdown('')).toEqual([])
83
+ })
84
+
85
+ it('should parse headings level 1–6', () => {
86
+ const md = '# H1\n## H2\n### H3\n#### H4\n##### H5\n###### H6'
87
+ const result = parseMarkdown(md)
88
+ expect(result).toHaveLength(6)
89
+ for (let i = 0; i < 6; i++) {
90
+ expect(result[i]).toMatchObject({ type: 'heading', level: i + 1 })
91
+ }
92
+ })
93
+
94
+ it('should parse a paragraph', () => {
95
+ const result = parseMarkdown('Hello world\nthis is a paragraph.')
96
+ expect(result).toEqual([
97
+ {
98
+ type: 'paragraph',
99
+ children: [{ type: 'text', content: 'Hello world this is a paragraph.' }],
100
+ },
101
+ ])
102
+ })
103
+
104
+ it('should split paragraphs on blank lines', () => {
105
+ const result = parseMarkdown('First paragraph.\n\nSecond paragraph.')
106
+ expect(result).toHaveLength(2)
107
+ expect(result[0]).toMatchObject({ type: 'paragraph' })
108
+ expect(result[1]).toMatchObject({ type: 'paragraph' })
109
+ })
110
+
111
+ it('should parse fenced code blocks', () => {
112
+ const md = '```typescript\nconst x = 1\nconsole.log(x)\n```'
113
+ const result = parseMarkdown(md)
114
+ expect(result).toEqual([
115
+ {
116
+ type: 'codeBlock',
117
+ language: 'typescript',
118
+ content: 'const x = 1\nconsole.log(x)',
119
+ },
120
+ ])
121
+ })
122
+
123
+ it('should parse fenced code blocks without language', () => {
124
+ const md = '```\nhello\n```'
125
+ const result = parseMarkdown(md)
126
+ expect(result).toEqual([
127
+ {
128
+ type: 'codeBlock',
129
+ language: undefined,
130
+ content: 'hello',
131
+ },
132
+ ])
133
+ })
134
+
135
+ it('should parse horizontal rules', () => {
136
+ for (const rule of ['---', '***', '___', '----', '****']) {
137
+ const result = parseMarkdown(rule)
138
+ expect(result).toEqual([{ type: 'horizontalRule' }])
139
+ }
140
+ })
141
+
142
+ it('should parse unordered lists', () => {
143
+ const md = '- Item 1\n- Item 2\n- Item 3'
144
+ const result = parseMarkdown(md)
145
+ expect(result).toHaveLength(1)
146
+ expect(result[0]).toMatchObject({ type: 'list', ordered: false })
147
+ const list = result[0] as { type: 'list'; items: unknown[] }
148
+ expect(list.items).toHaveLength(3)
149
+ })
150
+
151
+ it('should parse ordered lists', () => {
152
+ const md = '1. First\n2. Second\n3. Third'
153
+ const result = parseMarkdown(md)
154
+ expect(result).toHaveLength(1)
155
+ expect(result[0]).toMatchObject({ type: 'list', ordered: true })
156
+ const list = result[0] as { type: 'list'; items: unknown[] }
157
+ expect(list.items).toHaveLength(3)
158
+ })
159
+
160
+ it('should parse checkboxes in unordered lists', () => {
161
+ const md = '- [x] Done task\n- [ ] Todo task\n- Regular item'
162
+ const result = parseMarkdown(md)
163
+ expect(result).toHaveLength(1)
164
+ const list = result[0] as { type: 'list'; items: Array<{ checkbox?: string }> }
165
+ expect(list.items[0].checkbox).toBe('checked')
166
+ expect(list.items[1].checkbox).toBe('unchecked')
167
+ expect(list.items[2].checkbox).toBeUndefined()
168
+ })
169
+
170
+ it('should track sourceLineIndex on list items', () => {
171
+ const md = '- [x] Done\n- [ ] Todo'
172
+ const result = parseMarkdown(md)
173
+ const list = result[0] as { type: 'list'; items: Array<{ sourceLineIndex: number }> }
174
+ expect(list.items[0].sourceLineIndex).toBe(0)
175
+ expect(list.items[1].sourceLineIndex).toBe(1)
176
+ })
177
+
178
+ it('should parse blockquotes', () => {
179
+ const md = '> This is a quote\n> Second line'
180
+ const result = parseMarkdown(md)
181
+ expect(result).toHaveLength(1)
182
+ expect(result[0]).toMatchObject({ type: 'blockquote' })
183
+ const bq = result[0] as { type: 'blockquote'; children: unknown[] }
184
+ expect(bq.children).toHaveLength(1)
185
+ expect(bq.children[0]).toMatchObject({ type: 'paragraph' })
186
+ })
187
+
188
+ it('should parse a complex document', () => {
189
+ const md = [
190
+ '# Title',
191
+ '',
192
+ 'A paragraph with **bold** and *italic*.',
193
+ '',
194
+ '## List Section',
195
+ '',
196
+ '- [x] Task done',
197
+ '- [ ] Task pending',
198
+ '',
199
+ '> A blockquote',
200
+ '',
201
+ '---',
202
+ '',
203
+ '```js',
204
+ 'console.log("hi")',
205
+ '```',
206
+ ].join('\n')
207
+
208
+ const result = parseMarkdown(md)
209
+ expect(result[0]).toMatchObject({ type: 'heading', level: 1 })
210
+ expect(result[1]).toMatchObject({ type: 'paragraph' })
211
+ expect(result[2]).toMatchObject({ type: 'heading', level: 2 })
212
+ expect(result[3]).toMatchObject({ type: 'list', ordered: false })
213
+ expect(result[4]).toMatchObject({ type: 'blockquote' })
214
+ expect(result[5]).toMatchObject({ type: 'horizontalRule' })
215
+ expect(result[6]).toMatchObject({ type: 'codeBlock', language: 'js' })
216
+ })
217
+ })
218
+
219
+ describe('toggleCheckbox', () => {
220
+ it('should toggle an unchecked checkbox to checked', () => {
221
+ const source = '- [ ] Todo item'
222
+ const result = toggleCheckbox(source, 0)
223
+ expect(result).toBe('- [x] Todo item')
224
+ })
225
+
226
+ it('should toggle a checked checkbox to unchecked', () => {
227
+ const source = '- [x] Done item'
228
+ const result = toggleCheckbox(source, 0)
229
+ expect(result).toBe('- [ ] Done item')
230
+ })
231
+
232
+ it('should toggle the correct line in a multi-line document', () => {
233
+ const source = '- [x] Done\n- [ ] Todo\n- [ ] Another'
234
+ const result = toggleCheckbox(source, 1)
235
+ expect(result).toBe('- [x] Done\n- [x] Todo\n- [ ] Another')
236
+ })
237
+
238
+ it('should return original string for out-of-bounds index', () => {
239
+ const source = '- [ ] Todo'
240
+ expect(toggleCheckbox(source, 5)).toBe(source)
241
+ expect(toggleCheckbox(source, -1)).toBe(source)
242
+ })
243
+
244
+ it('should return original string for non-checkbox line', () => {
245
+ const source = 'Regular text'
246
+ expect(toggleCheckbox(source, 0)).toBe(source)
247
+ })
248
+
249
+ it('should not toggle [ ] that appears outside a list item', () => {
250
+ const source = 'This line has [ ] in it but is not a list item'
251
+ expect(toggleCheckbox(source, 0)).toBe(source)
252
+ })
253
+
254
+ it('should toggle checkboxes with * list marker', () => {
255
+ const source = '* [ ] Star item'
256
+ expect(toggleCheckbox(source, 0)).toBe('* [x] Star item')
257
+ })
258
+ })
@@ -0,0 +1,333 @@
1
+ /**
2
+ * Zero-dependency Markdown parser that converts a Markdown string into an AST.
3
+ */
4
+
5
+ export type MarkdownNode = HeadingNode | ParagraphNode | CodeBlockNode | BlockquoteNode | ListNode | HorizontalRuleNode
6
+
7
+ export type HeadingNode = {
8
+ type: 'heading'
9
+ level: 1 | 2 | 3 | 4 | 5 | 6
10
+ children: InlineNode[]
11
+ }
12
+
13
+ export type ParagraphNode = {
14
+ type: 'paragraph'
15
+ children: InlineNode[]
16
+ }
17
+
18
+ export type CodeBlockNode = {
19
+ type: 'codeBlock'
20
+ language?: string
21
+ content: string
22
+ }
23
+
24
+ export type BlockquoteNode = {
25
+ type: 'blockquote'
26
+ children: MarkdownNode[]
27
+ }
28
+
29
+ export type ListNode = {
30
+ type: 'list'
31
+ ordered: boolean
32
+ items: ListItemNode[]
33
+ }
34
+
35
+ export type HorizontalRuleNode = {
36
+ type: 'horizontalRule'
37
+ }
38
+
39
+ export type ListItemNode = {
40
+ children: InlineNode[]
41
+ checkbox?: 'checked' | 'unchecked'
42
+ sourceLineIndex: number
43
+ }
44
+
45
+ export type InlineNode = TextNode | BoldNode | ItalicNode | InlineCodeNode | LinkNode | ImageNode
46
+
47
+ export type TextNode = { type: 'text'; content: string }
48
+ export type BoldNode = { type: 'bold'; children: InlineNode[] }
49
+ export type ItalicNode = { type: 'italic'; children: InlineNode[] }
50
+ export type InlineCodeNode = { type: 'code'; content: string }
51
+ export type LinkNode = { type: 'link'; href: string; children: InlineNode[] }
52
+ export type ImageNode = { type: 'image'; src: string; alt: string }
53
+
54
+ const HORIZONTAL_RULE_RE = /^(\*{3,}|-{3,}|_{3,})\s*$/
55
+ const HEADING_RE = /^(#{1,6})\s+(.*)/
56
+ const FENCED_CODE_OPEN_RE = /^```(\w*)\s*$/
57
+ const FENCED_CODE_CLOSE_RE = /^```\s*$/
58
+ const UNORDERED_LIST_RE = /^(\s*)[-*]\s+(.*)/
59
+ const ORDERED_LIST_RE = /^(\s*)\d+\.\s+(.*)/
60
+ const BLOCKQUOTE_RE = /^>\s?(.*)/
61
+ const CHECKBOX_UNCHECKED_RE = /^\[[ ]\]\s+(.*)/
62
+ const CHECKBOX_CHECKED_RE = /^\[[xX]\]\s+(.*)/
63
+
64
+ /**
65
+ * Parse inline Markdown formatting into an array of InlineNodes.
66
+ *
67
+ * Known limitations:
68
+ * - Backslash escapes (e.g. `\*`) are not supported; special characters are always interpreted.
69
+ * - Underscore `_` markers are not restricted to word boundaries, so identifiers like
70
+ * `some_variable_name` may produce false italic/bold matches.
71
+ */
72
+ export const parseInline = (text: string): InlineNode[] => {
73
+ const nodes: InlineNode[] = []
74
+ let pos = 0
75
+
76
+ while (pos < text.length) {
77
+ // Inline code
78
+ if (text[pos] === '`') {
79
+ const closeIdx = text.indexOf('`', pos + 1)
80
+ if (closeIdx !== -1) {
81
+ nodes.push({ type: 'code', content: text.slice(pos + 1, closeIdx) })
82
+ pos = closeIdx + 1
83
+ continue
84
+ }
85
+ }
86
+
87
+ // Image ![alt](src)
88
+ if (text[pos] === '!' && text[pos + 1] === '[') {
89
+ const altClose = text.indexOf(']', pos + 2)
90
+ if (altClose !== -1 && text[altClose + 1] === '(') {
91
+ const srcClose = text.indexOf(')', altClose + 2)
92
+ if (srcClose !== -1) {
93
+ const alt = text.slice(pos + 2, altClose)
94
+ const src = text.slice(altClose + 2, srcClose)
95
+ nodes.push({ type: 'image', src, alt })
96
+ pos = srcClose + 1
97
+ continue
98
+ }
99
+ }
100
+ }
101
+
102
+ // Link [text](href)
103
+ if (text[pos] === '[') {
104
+ const textClose = text.indexOf(']', pos + 1)
105
+ if (textClose !== -1 && text[textClose + 1] === '(') {
106
+ const hrefClose = text.indexOf(')', textClose + 2)
107
+ if (hrefClose !== -1) {
108
+ const linkText = text.slice(pos + 1, textClose)
109
+ const href = text.slice(textClose + 2, hrefClose)
110
+ nodes.push({ type: 'link', href, children: parseInline(linkText) })
111
+ pos = hrefClose + 1
112
+ continue
113
+ }
114
+ }
115
+ }
116
+
117
+ // Bold+Italic (***text***) or Bold (**text**) or Italic (*text*)
118
+ if (text[pos] === '*' || text[pos] === '_') {
119
+ const marker = text[pos]
120
+
121
+ // Count consecutive markers
122
+ let markerCount = 0
123
+ while (pos + markerCount < text.length && text[pos + markerCount] === marker) {
124
+ markerCount++
125
+ }
126
+
127
+ if (markerCount >= 3) {
128
+ const closeIdx = text.indexOf(marker.repeat(3), pos + 3)
129
+ if (closeIdx !== -1) {
130
+ const inner = text.slice(pos + 3, closeIdx)
131
+ nodes.push({ type: 'bold', children: [{ type: 'italic', children: parseInline(inner) }] })
132
+ pos = closeIdx + 3
133
+ continue
134
+ }
135
+ }
136
+
137
+ if (markerCount >= 2) {
138
+ const closeIdx = text.indexOf(marker.repeat(2), pos + 2)
139
+ if (closeIdx !== -1) {
140
+ const inner = text.slice(pos + 2, closeIdx)
141
+ nodes.push({ type: 'bold', children: parseInline(inner) })
142
+ pos = closeIdx + 2
143
+ continue
144
+ }
145
+ }
146
+
147
+ if (markerCount >= 1) {
148
+ const closeIdx = text.indexOf(marker, pos + 1)
149
+ if (closeIdx !== -1) {
150
+ const inner = text.slice(pos + 1, closeIdx)
151
+ nodes.push({ type: 'italic', children: parseInline(inner) })
152
+ pos = closeIdx + 1
153
+ continue
154
+ }
155
+ }
156
+ }
157
+
158
+ // Plain text — consume until the next special character
159
+ let end = pos + 1
160
+ while (end < text.length && !['`', '!', '[', '*', '_'].includes(text[end])) {
161
+ end++
162
+ }
163
+ const content = text.slice(pos, end)
164
+ const lastNode = nodes[nodes.length - 1]
165
+ if (lastNode?.type === 'text') {
166
+ lastNode.content += content
167
+ } else {
168
+ nodes.push({ type: 'text', content })
169
+ }
170
+ pos = end
171
+ }
172
+
173
+ return nodes
174
+ }
175
+
176
+ /**
177
+ * Parse a Markdown string into an array of block-level MarkdownNodes.
178
+ */
179
+ export const parseMarkdown = (source: string): MarkdownNode[] => {
180
+ const lines = source.split('\n')
181
+ const nodes: MarkdownNode[] = []
182
+ let i = 0
183
+
184
+ while (i < lines.length) {
185
+ const line = lines[i]
186
+
187
+ // Blank lines — skip
188
+ if (line.trim() === '') {
189
+ i++
190
+ continue
191
+ }
192
+
193
+ // Fenced code block
194
+ const codeMatch = FENCED_CODE_OPEN_RE.exec(line)
195
+ if (codeMatch) {
196
+ const language = codeMatch[1] || undefined
197
+ const codeLines: string[] = []
198
+ i++
199
+ while (i < lines.length && !FENCED_CODE_CLOSE_RE.test(lines[i])) {
200
+ codeLines.push(lines[i])
201
+ i++
202
+ }
203
+ nodes.push({ type: 'codeBlock', language, content: codeLines.join('\n') })
204
+ i++ // skip closing ```
205
+ continue
206
+ }
207
+
208
+ // Horizontal rule
209
+ if (HORIZONTAL_RULE_RE.test(line)) {
210
+ nodes.push({ type: 'horizontalRule' })
211
+ i++
212
+ continue
213
+ }
214
+
215
+ // Heading
216
+ const headingMatch = HEADING_RE.exec(line)
217
+ if (headingMatch) {
218
+ const level = headingMatch[1].length as 1 | 2 | 3 | 4 | 5 | 6
219
+ nodes.push({ type: 'heading', level, children: parseInline(headingMatch[2]) })
220
+ i++
221
+ continue
222
+ }
223
+
224
+ // Blockquote
225
+ const bqMatch = BLOCKQUOTE_RE.exec(line)
226
+ if (bqMatch) {
227
+ const bqLines: string[] = []
228
+ while (i < lines.length) {
229
+ const bqLineMatch = BLOCKQUOTE_RE.exec(lines[i])
230
+ if (bqLineMatch) {
231
+ bqLines.push(bqLineMatch[1])
232
+ i++
233
+ } else {
234
+ break
235
+ }
236
+ }
237
+ nodes.push({ type: 'blockquote', children: parseMarkdown(bqLines.join('\n')) })
238
+ continue
239
+ }
240
+
241
+ // Unordered list
242
+ const ulMatch = UNORDERED_LIST_RE.exec(line)
243
+ if (ulMatch) {
244
+ const items: ListItemNode[] = []
245
+ while (i < lines.length) {
246
+ const itemMatch = UNORDERED_LIST_RE.exec(lines[i])
247
+ if (!itemMatch) break
248
+ const itemText = itemMatch[2]
249
+ const checkedMatch = CHECKBOX_CHECKED_RE.exec(itemText)
250
+ const uncheckedMatch = CHECKBOX_UNCHECKED_RE.exec(itemText)
251
+ if (checkedMatch) {
252
+ items.push({
253
+ children: parseInline(checkedMatch[1]),
254
+ checkbox: 'checked',
255
+ sourceLineIndex: i,
256
+ })
257
+ } else if (uncheckedMatch) {
258
+ items.push({
259
+ children: parseInline(uncheckedMatch[1]),
260
+ checkbox: 'unchecked',
261
+ sourceLineIndex: i,
262
+ })
263
+ } else {
264
+ items.push({
265
+ children: parseInline(itemText),
266
+ sourceLineIndex: i,
267
+ })
268
+ }
269
+ i++
270
+ }
271
+ nodes.push({ type: 'list', ordered: false, items })
272
+ continue
273
+ }
274
+
275
+ // Ordered list
276
+ const olMatch = ORDERED_LIST_RE.exec(line)
277
+ if (olMatch) {
278
+ const items: ListItemNode[] = []
279
+ while (i < lines.length) {
280
+ const itemMatch = ORDERED_LIST_RE.exec(lines[i])
281
+ if (!itemMatch) break
282
+ items.push({
283
+ children: parseInline(itemMatch[2]),
284
+ sourceLineIndex: i,
285
+ })
286
+ i++
287
+ }
288
+ nodes.push({ type: 'list', ordered: true, items })
289
+ continue
290
+ }
291
+
292
+ // Paragraph — collect consecutive non-blank, non-block-start lines
293
+ const paraLines: string[] = []
294
+ while (i < lines.length) {
295
+ const pLine = lines[i]
296
+ if (pLine.trim() === '') break
297
+ if (HEADING_RE.test(pLine)) break
298
+ if (FENCED_CODE_OPEN_RE.test(pLine)) break
299
+ if (HORIZONTAL_RULE_RE.test(pLine)) break
300
+ if (BLOCKQUOTE_RE.test(pLine)) break
301
+ if (UNORDERED_LIST_RE.test(pLine)) break
302
+ if (ORDERED_LIST_RE.test(pLine)) break
303
+ paraLines.push(pLine)
304
+ i++
305
+ }
306
+ if (paraLines.length > 0) {
307
+ nodes.push({ type: 'paragraph', children: parseInline(paraLines.join(' ')) })
308
+ }
309
+ }
310
+
311
+ return nodes
312
+ }
313
+
314
+ const TOGGLE_UNCHECKED_RE = /^(\s*[-*]\s+)\[ \]/
315
+ const TOGGLE_CHECKED_RE = /^(\s*[-*]\s+)\[[xX]\]/
316
+
317
+ /**
318
+ * Toggle a checkbox at the given source line index in the raw Markdown string.
319
+ * Only matches checkboxes in unordered list items (`- [ ]` or `* [x]`).
320
+ * Returns the updated string.
321
+ */
322
+ export const toggleCheckbox = (source: string, sourceLineIndex: number): string => {
323
+ const lines = source.split('\n')
324
+ if (sourceLineIndex < 0 || sourceLineIndex >= lines.length) return source
325
+
326
+ const line = lines[sourceLineIndex]
327
+ if (TOGGLE_UNCHECKED_RE.test(line)) {
328
+ lines[sourceLineIndex] = line.replace(TOGGLE_UNCHECKED_RE, '$1[x]')
329
+ } else if (TOGGLE_CHECKED_RE.test(line)) {
330
+ lines[sourceLineIndex] = line.replace(TOGGLE_CHECKED_RE, '$1[ ]')
331
+ }
332
+ return lines.join('\n')
333
+ }
@@ -30,7 +30,7 @@ export type PageContainerProps = {
30
30
  * ```tsx
31
31
  * <PageContainer maxWidth="800px" centered padding="48px" gap="24px">
32
32
  * <PageHeader
33
- * icon="👥"
33
+ * icon={<Icon icon={icons.users} />}
34
34
  * title="Users"
35
35
  * description="Manage user accounts and their roles."
36
36
  * />
@@ -6,8 +6,8 @@ import { Paper } from '../paper.js'
6
6
  * Props for the PageHeader component.
7
7
  */
8
8
  export type PageHeaderProps = {
9
- /** Optional icon (emoji or single character) displayed before the title */
10
- icon?: string
9
+ /** Optional icon displayed before the title (string or JSX element such as an Icon component) */
10
+ icon?: JSX.Element | string
11
11
  /** The page title */
12
12
  title: string
13
13
  /** Optional description text displayed below the title */
@@ -27,9 +27,9 @@ export type PageHeaderProps = {
27
27
  *
28
28
  * @example
29
29
  * ```tsx
30
- * // Simple header with icon and description
30
+ * // Header with an Icon component
31
31
  * <PageHeader
32
- * icon="👥"
32
+ * icon={<Icon icon={icons.users} />}
33
33
  * title="Users"
34
34
  * description="Manage user accounts and their roles."
35
35
  * />
@@ -39,7 +39,7 @@ export type PageHeaderProps = {
39
39
  * ```tsx
40
40
  * // Header with action buttons
41
41
  * <PageHeader
42
- * icon="📁"
42
+ * icon={<Icon icon={icons.folder} />}
43
43
  * title="Projects"
44
44
  * description="View and manage your projects."
45
45
  * actions={
@@ -3,6 +3,7 @@ import { cssVariableTheme } from '../services/css-variable-theme.js'
3
3
  declare global {
4
4
  interface CSSStyleDeclaration {
5
5
  backdropFilter: string
6
+ fieldSizing: string
6
7
  }
7
8
  }
8
9
 
@@ -16,7 +16,7 @@ export * from './suggestion-list.js'
16
16
  export * from './suggestion-result.js'
17
17
 
18
18
  export interface SuggestProps<T> {
19
- defaultPrefix: string
19
+ defaultPrefix: JSX.Element | string
20
20
  getEntries: (term: string) => Promise<T[]>
21
21
  getSuggestionEntry: (entry: T) => SuggestionResult
22
22
  onSelectSuggestion: (entry: T) => void