@astronox/core 0.2.1 → 0.2.2

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 (64) hide show
  1. package/package.json +7 -1
  2. package/src/admin/contracts.ts +28 -0
  3. package/src/admin/index.ts +2 -0
  4. package/src/admin/module.ts +28 -0
  5. package/src/auth/contracts.ts +53 -0
  6. package/src/auth/firebase/client.ts +53 -0
  7. package/src/auth/firebase/contracts.ts +8 -0
  8. package/src/auth/firebase/index.ts +1 -0
  9. package/src/auth/index.ts +2 -0
  10. package/src/auth/module.ts +99 -0
  11. package/src/client/index.ts +3 -0
  12. package/src/client/preferences.ts +400 -0
  13. package/src/components/actions/Button/Button.astro +42 -6
  14. package/src/components/actions/Button/Button.css +19 -0
  15. package/src/components/actions/IdentityProviderAction/IdentityProviderAction.astro +35 -0
  16. package/src/components/content/CardBody/CardBody.astro +3 -0
  17. package/src/components/content/CardBody/CardBody.css +10 -0
  18. package/src/components/content/MarkdownContent/markdownContentRenderer.ts +549 -0
  19. package/src/components/feedback/PreferenceBootstrap/PreferenceBootstrap.astro +403 -0
  20. package/src/components/forms/Checkbox/Checkbox.astro +6 -0
  21. package/src/components/forms/ChoiceGroup/ChoiceGroup.astro +57 -0
  22. package/src/components/forms/ChoiceGroup/ChoiceGroup.css +108 -0
  23. package/src/components/forms/ChoiceGroup/contracts.ts +9 -0
  24. package/src/components/forms/DateTimeInput/DateTimeInput.astro +14 -0
  25. package/src/components/forms/TextInput/TextInput.astro +7 -1
  26. package/src/components/forms/Textarea/Textarea.astro +32 -0
  27. package/src/components/forms/Textarea/Textarea.css +54 -0
  28. package/src/components/icons/BackIcon.astro +26 -0
  29. package/src/components/icons/DeleteIcon.astro +22 -0
  30. package/src/components/icons/StatusIndicatorIcon.astro +16 -0
  31. package/src/components/index.ts +10 -0
  32. package/src/components/media/CanvasFrame/CanvasFrame.astro +1 -17
  33. package/src/components/media/EmbedFrame/EmbedFrame.astro +1 -19
  34. package/src/components/media/EmbedFrame/EmbedFrame.css +0 -2
  35. package/src/components/navigation/SubNavigation/SubNavigation.css +0 -5
  36. package/src/composition/SurfaceGroup/SurfaceGroup.css +21 -7
  37. package/src/composition/SurfaceGroup/stickySurfaceGroupController.ts +3 -0
  38. package/src/data/firestore/client/contracts.ts +55 -0
  39. package/src/data/firestore/client/firebase.ts +12 -0
  40. package/src/data/firestore/client/index.ts +3 -0
  41. package/src/data/firestore/client/repository.ts +111 -0
  42. package/src/display/contracts.ts +40 -0
  43. package/src/display/index.ts +1 -0
  44. package/src/display/tableProgressive.ts +62 -0
  45. package/src/patterns/Combobox/Combobox.astro +23 -0
  46. package/src/patterns/Fullscreen/FullscreenControl.astro +4 -14
  47. package/src/patterns/Fullscreen/FullscreenControl.css +0 -26
  48. package/src/patterns/Modal/Modal.astro +1 -1
  49. package/src/patterns/Modal/Modal.css +38 -1
  50. package/src/patterns/Table/Table.astro +122 -7
  51. package/src/patterns/Table/TableBody.astro +8 -0
  52. package/src/patterns/Table/TableCell.css +6 -0
  53. package/src/patterns/Table/TableCellValue.astro +31 -0
  54. package/src/patterns/Table/TableFooter.astro +3 -1
  55. package/src/patterns/Table/TableHeader.astro +1 -0
  56. package/src/patterns/Table/TablePagination.astro +63 -47
  57. package/src/patterns/Table/TableSkeleton.astro +18 -22
  58. package/src/patterns/Table/TableSkeleton.css +30 -5
  59. package/src/patterns/Table/tableClientController.ts +253 -0
  60. package/src/patterns/Table/tableFilterController.ts +23 -31
  61. package/src/patterns/Table/tableProgressiveController.ts +615 -227
  62. package/src/patterns/Table/tableSortController.ts +25 -7
  63. package/src/styles/interaction.css +16 -0
  64. package/src/styles/style.css +2 -0
@@ -0,0 +1,549 @@
1
+ import type { DisplayMedia } from '../../../display/contracts';
2
+ import type { Tone } from '../../../theme/contracts';
3
+ import { initCodeBlock } from '../CodeBlock/codeBlockController';
4
+ import type { CodeBlockCopy } from '../CodeBlock/contracts';
5
+ import type {
6
+ MarkdownBlock,
7
+ MarkdownDocument,
8
+ MarkdownInline,
9
+ MarkdownListItem,
10
+ MarkdownListMarker,
11
+ MarkdownSection,
12
+ MarkdownSymbol,
13
+ } from './contracts';
14
+
15
+ type RenderOptions = {
16
+ codeBlockCopy?: CodeBlockCopy;
17
+ };
18
+
19
+ const svgNamespace = 'http://www.w3.org/2000/svg';
20
+ const codeBlockTones = new Set<Tone>([
21
+ 'neutral',
22
+ 'info',
23
+ 'success',
24
+ 'warning',
25
+ 'danger',
26
+ ]);
27
+ const defaultListMarker = {
28
+ symbol: 'bullet',
29
+ tone: 'neutral',
30
+ } as const satisfies MarkdownListMarker;
31
+ const nestedListMarker = {
32
+ symbol: 'arrow-right',
33
+ tone: 'neutral',
34
+ } as const satisfies MarkdownListMarker;
35
+
36
+ export function renderMarkdownDocument(
37
+ root: HTMLElement,
38
+ document: MarkdownDocument,
39
+ options: RenderOptions = {},
40
+ ): void {
41
+ const fragment = window.document.createDocumentFragment();
42
+ document.blocks.forEach((block) => {
43
+ fragment.append(renderNode(block, options));
44
+ });
45
+ root.replaceChildren(fragment);
46
+ }
47
+
48
+ function renderNode(
49
+ node: MarkdownBlock | MarkdownSection,
50
+ options: RenderOptions,
51
+ ): HTMLElement {
52
+ if (node.type === 'section') {
53
+ const section = createElement('section', 'markdown-content__section');
54
+ section.setAttribute('aria-labelledby', node.id);
55
+ const heading = createHeading(
56
+ node.level,
57
+ 'markdown-content__section-title',
58
+ );
59
+ heading.id = node.id;
60
+ heading.textContent = node.title;
61
+ const body = createElement('div', 'markdown-content__section-body');
62
+ node.blocks.forEach((block) => body.append(renderNode(block, options)));
63
+ section.append(heading, body);
64
+ return section;
65
+ }
66
+
67
+ if (node.type === 'paragraph') {
68
+ const paragraph = createElement('p', 'markdown-content__paragraph');
69
+ appendInline(paragraph, node.children);
70
+ return paragraph;
71
+ }
72
+
73
+ if (node.type === 'heading') {
74
+ const heading = createHeading(node.level, 'markdown-content__heading');
75
+ heading.id = node.id;
76
+ appendInline(heading, node.children);
77
+ return heading;
78
+ }
79
+
80
+ if (node.type === 'change') {
81
+ const change = createElement('div', 'markdown-content__change');
82
+ const label = createElement('span', 'markdown-content__change-label');
83
+ appendInline(label, node.label);
84
+ label.append(':');
85
+ const values = createElement('span', 'markdown-content__change-values');
86
+
87
+ if (node.presentation === 'statement') {
88
+ appendInline(values, node.value);
89
+ } else {
90
+ if (node.valueLang) values.lang = node.valueLang;
91
+ const transition = createElement(
92
+ 'span',
93
+ 'markdown-content__change-transition',
94
+ );
95
+ transition.append(
96
+ document.createTextNode(node.before),
97
+ renderSymbol({
98
+ type: 'symbol',
99
+ name: 'arrow-right',
100
+ accessibleLabel: node.transitionLabel,
101
+ lang: node.valueLang,
102
+ }),
103
+ document.createTextNode(node.after),
104
+ );
105
+ if (node.delta) {
106
+ const delta = createElement(
107
+ 'span',
108
+ 'markdown-content__change-delta',
109
+ );
110
+ delta.dataset.tone = node.delta.tone;
111
+ delta.append('(');
112
+ if (node.delta.sign !== 'neutral') {
113
+ const accessible = createElement(
114
+ 'span',
115
+ 'markdown-content__visually-hidden',
116
+ );
117
+ accessible.textContent = node.delta.accessibleLabel;
118
+ const sign = document.createElement('span');
119
+ sign.ariaHidden = 'true';
120
+ sign.textContent = node.delta.sign === 'plus' ? '+' : '-';
121
+ delta.append(accessible, sign);
122
+ }
123
+ delta.append(node.delta.value, ')');
124
+ transition.append(delta);
125
+ }
126
+ values.append(transition);
127
+ if (node.suffix?.length) {
128
+ const suffix = createElement(
129
+ 'span',
130
+ 'markdown-content__change-suffix',
131
+ );
132
+ appendInline(suffix, node.suffix);
133
+ values.append(suffix);
134
+ }
135
+ }
136
+
137
+ change.append(label, values);
138
+ return change;
139
+ }
140
+
141
+ if (node.type === 'list') {
142
+ const list = createElement('ul', 'markdown-content__list');
143
+ if (node.items.some((item) => item.heading)) {
144
+ list.dataset.markdownHasHeadings = 'true';
145
+ }
146
+ if (node.items.some((item) => item.resource)) {
147
+ list.dataset.markdownHasResources = 'true';
148
+ }
149
+ node.items.forEach((item) => list.append(renderListItem(item, options)));
150
+ return list;
151
+ }
152
+
153
+ if (node.type === 'quote') {
154
+ const quote = createElement('blockquote', 'markdown-content__quote');
155
+ quote.dataset.tone = node.tone ?? 'neutral';
156
+ node.children.forEach((block) => quote.append(renderNode(block, options)));
157
+ return quote;
158
+ }
159
+
160
+ if (node.type === 'separator') {
161
+ const separator = createElement('hr', 'separator');
162
+ separator.dataset.widget = 'separator';
163
+ separator.dataset.separatorBleed = 'true';
164
+ separator.dataset.separatorEmphasis = node.emphasis ?? 'muted';
165
+ return separator;
166
+ }
167
+
168
+ return renderCodeBlock(node.language, node.value, options.codeBlockCopy);
169
+ }
170
+
171
+ function renderListItem(
172
+ item: MarkdownListItem,
173
+ options: RenderOptions,
174
+ ): HTMLLIElement {
175
+ const listItem = createElement('li', 'markdown-content__list-item');
176
+ const marker =
177
+ item.marker ??
178
+ (item.heading
179
+ ? nestedListMarker
180
+ : item.resource
181
+ ? undefined
182
+ : defaultListMarker);
183
+
184
+ if (marker) listItem.dataset.markdownHasMarker = 'true';
185
+ if (item.resource) {
186
+ listItem.dataset.markdownResource = 'true';
187
+ listItem.dataset.markdownResourceLayout = item.resource.layout;
188
+ listItem.dataset.markdownResourceMedia = item.resource.mediaPlacement;
189
+ listItem.dataset.markdownResourceMediaScale = item.resource.mediaScale;
190
+ }
191
+ if (item.resource?.mediaPlacement === 'lead') {
192
+ listItem.append(
193
+ renderMedia(
194
+ item.resource.media,
195
+ 'markdown-content__lead-media',
196
+ 'fill',
197
+ item.resource.mediaScale === 'compact',
198
+ ),
199
+ );
200
+ }
201
+ if (marker) {
202
+ listItem.append(
203
+ renderSymbol({
204
+ type: 'symbol',
205
+ name: marker.symbol,
206
+ accessibleLabel: marker.label,
207
+ lang: marker.lang,
208
+ tone: marker.tone,
209
+ }),
210
+ );
211
+ }
212
+
213
+ const body = createElement('div', 'markdown-content__list-item-body');
214
+ if (item.heading) {
215
+ const heading = createHeading(5, 'markdown-content__heading');
216
+ appendInline(heading, item.heading);
217
+ body.append(heading);
218
+ } else if (item.resource) {
219
+ const heading = createElement(
220
+ 'div',
221
+ 'markdown-content__resource-heading',
222
+ );
223
+ if (item.resource.mediaPlacement === 'inline') {
224
+ heading.append(
225
+ renderMedia(
226
+ item.resource.media,
227
+ 'markdown-content__inline-media',
228
+ 'intrinsic',
229
+ item.resource.mediaScale === 'compact',
230
+ ),
231
+ );
232
+ }
233
+ const title = createElement(
234
+ item.resource.layout === 'detailed' ? 'h3' : 'span',
235
+ 'markdown-content__resource-title',
236
+ );
237
+ appendInline(title, item.resource.title);
238
+ heading.append(title);
239
+ body.append(heading);
240
+ }
241
+ item.children.forEach((block) => body.append(renderNode(block, options)));
242
+ listItem.append(body);
243
+ return listItem;
244
+ }
245
+
246
+ function appendInline(
247
+ parent: HTMLElement,
248
+ nodes: readonly MarkdownInline[],
249
+ ): void {
250
+ nodes.forEach((node) => parent.append(renderInline(node)));
251
+ }
252
+
253
+ function renderInline(node: MarkdownInline): Node {
254
+ if (node.type === 'text') {
255
+ if (!node.lang) return document.createTextNode(node.value);
256
+ const text = document.createElement('span');
257
+ text.lang = node.lang;
258
+ text.textContent = node.value;
259
+ return text;
260
+ }
261
+ if (node.type === 'symbol') return renderSymbol(node);
262
+ if (node.type === 'strong' || node.type === 'emphasis') {
263
+ const element = document.createElement(
264
+ node.type === 'strong' ? 'strong' : 'em',
265
+ );
266
+ appendInline(element, node.children);
267
+ return element;
268
+ }
269
+ if (node.type === 'code') {
270
+ const code = createElement('code', 'markdown-content__inline-code');
271
+ code.textContent = node.value;
272
+ return code;
273
+ }
274
+ if (node.type === 'link') {
275
+ const link = createElement('a', 'markdown-content__link');
276
+ link.href = node.href;
277
+ link.dataset.tone = 'info';
278
+ if (node.lang) link.lang = node.lang;
279
+ if (node.treatment) link.dataset.markdownLinkTreatment = node.treatment;
280
+ if (node.external) {
281
+ link.target = '_blank';
282
+ link.rel = 'noopener noreferrer';
283
+ }
284
+ appendInline(link, node.children);
285
+ return link;
286
+ }
287
+ return document.createElement('br');
288
+ }
289
+
290
+ function renderSymbol(node: MarkdownSymbol): HTMLSpanElement {
291
+ const symbol = createElement('span', 'markdown-content__symbol');
292
+ symbol.dataset.markdownSymbol = node.name;
293
+ if (node.tone) symbol.dataset.tone = node.tone;
294
+ if (node.lang) symbol.lang = node.lang;
295
+ symbol.append(createSymbolIcon(node.name));
296
+
297
+ if (node.label) {
298
+ const label = createElement('span', 'markdown-content__symbol-label');
299
+ label.textContent = node.label;
300
+ symbol.append(label);
301
+ } else if (node.accessibleLabel) {
302
+ const label = createElement(
303
+ 'span',
304
+ 'markdown-content__visually-hidden',
305
+ );
306
+ label.textContent = node.accessibleLabel;
307
+ symbol.append(label);
308
+ }
309
+ return symbol;
310
+ }
311
+
312
+ function createSymbolIcon(name: MarkdownSymbol['name']): SVGSVGElement {
313
+ const svg = document.createElementNS(svgNamespace, 'svg');
314
+ svg.setAttribute('viewBox', '0 0 32 32');
315
+ svg.setAttribute('fill', 'none');
316
+ svg.setAttribute('aria-hidden', 'true');
317
+ svg.setAttribute('focusable', 'false');
318
+ svg.classList.add('markdown-content__symbol-icon');
319
+
320
+ if (name.startsWith('terrain-')) {
321
+ svg.classList.add('circle-symbol-icon');
322
+ svg.dataset.terrainIcon = name.slice('terrain-'.length);
323
+ appendTerrainIcon(svg, name);
324
+ return svg;
325
+ }
326
+
327
+ if (name === 'bullet') {
328
+ const circle = document.createElementNS(svgNamespace, 'circle');
329
+ circle.setAttribute('cx', '16');
330
+ circle.setAttribute('cy', '16');
331
+ circle.setAttribute('r', '4.5');
332
+ circle.setAttribute('fill', 'currentColor');
333
+ svg.append(circle);
334
+ return svg;
335
+ }
336
+
337
+ const path = document.createElementNS(svgNamespace, 'path');
338
+ path.setAttribute('stroke', 'currentColor');
339
+ path.setAttribute('stroke-linecap', 'round');
340
+ path.setAttribute('stroke-linejoin', 'round');
341
+ path.setAttribute('stroke-width', name === 'neutral' ? '3' : '2.4');
342
+ path.setAttribute(
343
+ 'd',
344
+ name === 'increase'
345
+ ? 'm7.5 19.5 8.5-8.5 8.5 8.5'
346
+ : name === 'decrease'
347
+ ? 'm7.5 12.5 8.5 8.5 8.5-8.5'
348
+ : name === 'adjustment'
349
+ ? 'M11 24V8m0 0-4 4m4-4 4 4m6-4v16m0 0-4-4m4 4 4-4'
350
+ : name === 'neutral'
351
+ ? 'M6 11.5h20M6 20.5h20'
352
+ : 'm12.5 7.5 8.5 8.5-8.5 8.5',
353
+ );
354
+ svg.append(path);
355
+ return svg;
356
+ }
357
+
358
+ function appendTerrainIcon(
359
+ svg: SVGSVGElement,
360
+ name: Extract<MarkdownSymbol['name'], `terrain-${string}`>,
361
+ ): void {
362
+ const surface = document.createElementNS(svgNamespace, 'circle');
363
+ surface.setAttribute('cx', '16');
364
+ surface.setAttribute('cy', '16');
365
+ surface.setAttribute('r', '13');
366
+ surface.classList.add('circle-symbol-icon__surface');
367
+ svg.append(surface);
368
+
369
+ if (name === 'terrain-field') {
370
+ const rectangle = document.createElementNS(svgNamespace, 'rect');
371
+ rectangle.setAttribute('x', '9');
372
+ rectangle.setAttribute('y', '9');
373
+ rectangle.setAttribute('width', '14');
374
+ rectangle.setAttribute('height', '14');
375
+ rectangle.setAttribute('rx', '1');
376
+ rectangle.classList.add('circle-symbol-icon__mark');
377
+ const grid = document.createElementNS(svgNamespace, 'path');
378
+ grid.setAttribute('d', 'M13.7 9v14m4.6-14v14M9 13.7h14M9 18.3h14');
379
+ grid.classList.add('circle-symbol-icon__mark');
380
+ svg.append(rectangle, grid);
381
+ return;
382
+ }
383
+
384
+ const fill = document.createElementNS(svgNamespace, 'path');
385
+ fill.classList.add('circle-symbol-icon__fill');
386
+ fill.setAttribute(
387
+ 'd',
388
+ name === 'terrain-grass'
389
+ ? 'M23.5 8.5c.2 8.4-3.7 14-11.7 15.2-3.1-6.8.2-13.5 11.7-15.2Z'
390
+ : name === 'terrain-water'
391
+ ? 'M16 5.5c3.1 4.2 7.5 9 7.5 13.3A7.5 7.5 0 1 1 8.5 18.8C8.5 14.5 12.9 9.7 16 5.5Z'
392
+ : 'm5.5 24 7.3-13 3.1 5.1 3.4-7.1 7.2 15H5.5Z',
393
+ );
394
+ const highlight = document.createElementNS(svgNamespace, 'path');
395
+ highlight.classList.add('circle-symbol-icon__highlight');
396
+ highlight.setAttribute(
397
+ 'd',
398
+ name === 'terrain-grass'
399
+ ? 'M10.4 23.5c2.2-5.3 5.4-8.7 10.2-11.6'
400
+ : name === 'terrain-water'
401
+ ? 'M12.2 19.2a4.5 4.5 0 0 0 4.5 4.1'
402
+ : 'm10.8 14.6 2-3.6 3.1 5.1 1.4-2.9 2.4 3.6-3.5-1.1-2.5 2.1-2.9-3.2Z',
403
+ );
404
+ svg.append(fill, highlight);
405
+ }
406
+
407
+ function renderMedia(
408
+ media: DisplayMedia,
409
+ className: string,
410
+ size: 'fill' | 'intrinsic',
411
+ preventUpscale: boolean,
412
+ ): HTMLElement {
413
+ const figure = createElement('figure', `media ${className}`);
414
+ figure.dataset.widget = 'media';
415
+ figure.dataset.mediaFit = media.fit ?? 'contain';
416
+ figure.dataset.mediaFraming = media.framing ?? 'asset';
417
+ figure.dataset.mediaRendering = media.rendering ?? 'auto';
418
+ figure.dataset.mediaShape = media.shape ?? 'default';
419
+ figure.dataset.mediaSize = size;
420
+ figure.dataset.mediaUpscale = preventUpscale
421
+ ? 'prevent'
422
+ : (media.upscale ?? 'allow');
423
+ figure.dataset.mediaVisualSize = media.visualSize ?? 'default';
424
+ figure.style.setProperty(
425
+ '--media-aspect-ratio',
426
+ `${media.width} / ${media.height}`,
427
+ );
428
+ figure.style.setProperty('--media-source-inline-size', `${media.width}px`);
429
+ figure.style.setProperty('--media-source-block-size', `${media.height}px`);
430
+
431
+ const frame = createElement('div', 'media__frame');
432
+ const image = document.createElement('img');
433
+ image.src = media.src;
434
+ image.alt = media.alt;
435
+ image.width = media.width;
436
+ image.height = media.height;
437
+ image.loading = media.loading ?? 'lazy';
438
+ image.decoding = media.decoding ?? 'async';
439
+ if (media.sources?.length) {
440
+ image.srcset = media.sources
441
+ .map((source) => `${source.src} ${source.width}w`)
442
+ .join(', ');
443
+ }
444
+ if (media.sizes) image.sizes = media.sizes;
445
+ frame.append(image);
446
+ figure.append(frame);
447
+ if (media.caption) {
448
+ const caption = document.createElement('figcaption');
449
+ caption.textContent = media.caption;
450
+ figure.append(caption);
451
+ }
452
+ return figure;
453
+ }
454
+
455
+ function renderCodeBlock(
456
+ language: string | undefined,
457
+ value: string,
458
+ copy: CodeBlockCopy | undefined,
459
+ ): HTMLElement {
460
+ const normalized = language?.trim().toLowerCase() || undefined;
461
+ const presentation = normalized === 'markdown' ? 'light' : 'default';
462
+ const tone =
463
+ normalized && codeBlockTones.has(normalized as Tone)
464
+ ? (normalized as Tone)
465
+ : 'neutral';
466
+ const content = createElement('pre', 'code-block');
467
+ content.dataset.codeBlockPresentation = presentation;
468
+ content.dataset.wrap = 'false';
469
+ content.textContent = value;
470
+ if (!copy) {
471
+ content.dataset.widget = 'code-block';
472
+ content.dataset.tone = tone;
473
+ return content;
474
+ }
475
+
476
+ const shell = createElement('nox-code-block', 'code-block-shell');
477
+ shell.dataset.widget = 'code-block';
478
+ shell.dataset.tone = tone;
479
+ content.dataset.codeBlockContent = '';
480
+ const button = createElement('button', 'nox-button code-block__copy');
481
+ button.type = 'button';
482
+ button.dataset.buttonBorderless = 'true';
483
+ button.dataset.buttonHoverTone = 'info';
484
+ button.dataset.buttonIconOnly = 'true';
485
+ button.dataset.buttonSize = 'sm';
486
+ button.dataset.codeBlockCopy = '';
487
+ button.dataset.copyLabel = copy.copyLabel;
488
+ button.dataset.copiedLabel = copy.copiedLabel;
489
+ button.dataset.copyUnavailableLabel = copy.unavailableLabel;
490
+ button.setAttribute('aria-label', copy.copyLabel);
491
+ button.title = copy.copyLabel;
492
+ if (copy.lang) button.lang = copy.lang;
493
+ button.append(createCopyIcon());
494
+ const status = createElement('span', 'code-block__status');
495
+ status.dataset.codeBlockStatus = '';
496
+ status.setAttribute('aria-live', 'polite');
497
+ if (copy.lang) status.lang = copy.lang;
498
+ shell.append(content, button, status);
499
+ initCodeBlock(shell);
500
+ return shell;
501
+ }
502
+
503
+ function createCopyIcon(): SVGSVGElement {
504
+ const svg = document.createElementNS(svgNamespace, 'svg');
505
+ svg.setAttribute('viewBox', '0 0 32 32');
506
+ svg.setAttribute('fill', 'none');
507
+ svg.setAttribute('aria-hidden', 'true');
508
+ svg.setAttribute('focusable', 'false');
509
+ const rectangle = document.createElementNS(svgNamespace, 'rect');
510
+ rectangle.setAttribute('x', '11');
511
+ rectangle.setAttribute('y', '11');
512
+ rectangle.setAttribute('width', '14');
513
+ rectangle.setAttribute('height', '14');
514
+ rectangle.setAttribute('rx', '2');
515
+ rectangle.setAttribute('stroke', 'currentColor');
516
+ rectangle.setAttribute('stroke-width', '2.5');
517
+ const path = document.createElementNS(svgNamespace, 'path');
518
+ path.setAttribute(
519
+ 'd',
520
+ 'M21 11V9C21 7.89543 20.1046 7 19 7H9C7.89543 7 7 7.89543 7 9V19C7 20.1046 7.89543 21 9 21H11',
521
+ );
522
+ path.setAttribute('stroke', 'currentColor');
523
+ path.setAttribute('stroke-width', '2.5');
524
+ path.setAttribute('stroke-linecap', 'round');
525
+ path.setAttribute('stroke-linejoin', 'round');
526
+ svg.append(rectangle, path);
527
+ return svg;
528
+ }
529
+
530
+ function createHeading(
531
+ level: number,
532
+ className: string,
533
+ ): HTMLHeadingElement {
534
+ const heading = createElement(
535
+ `h${Math.min(6, Math.max(2, level))}` as keyof HTMLElementTagNameMap,
536
+ className,
537
+ ) as HTMLHeadingElement;
538
+ heading.dataset.markdownHeadingLevel = String(level);
539
+ return heading;
540
+ }
541
+
542
+ function createElement<K extends keyof HTMLElementTagNameMap>(
543
+ tag: K,
544
+ className?: string,
545
+ ): HTMLElementTagNameMap[K] {
546
+ const element = document.createElement(tag);
547
+ if (className) element.className = className;
548
+ return element;
549
+ }