@astronox/core 0.2.1 → 0.2.3

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 (88) hide show
  1. package/package.json +7 -1
  2. package/src/admin/cache.ts +217 -0
  3. package/src/admin/contracts.ts +28 -0
  4. package/src/admin/index.ts +3 -0
  5. package/src/admin/module.ts +28 -0
  6. package/src/auth/contracts.ts +53 -0
  7. package/src/auth/firebase/client.ts +83 -0
  8. package/src/auth/firebase/contracts.ts +8 -0
  9. package/src/auth/firebase/index.ts +1 -0
  10. package/src/auth/index.ts +2 -0
  11. package/src/auth/module.ts +99 -0
  12. package/src/client/formSubmission.ts +177 -0
  13. package/src/client/index.ts +4 -0
  14. package/src/client/preferences.ts +400 -0
  15. package/src/components/actions/Button/Button.astro +42 -6
  16. package/src/components/actions/Button/Button.css +19 -0
  17. package/src/components/actions/IdentityProviderAction/IdentityProviderAction.astro +35 -0
  18. package/src/components/actions/ScrollToTop/ScrollToTop.css +11 -0
  19. package/src/components/content/CardBody/CardBody.astro +3 -0
  20. package/src/components/content/CardBody/CardBody.css +16 -2
  21. package/src/components/content/CardFooter/CardFooter.astro +9 -0
  22. package/src/components/content/CardFooter/CardFooter.css +45 -3
  23. package/src/components/content/CardLink/CardLink.astro +12 -3
  24. package/src/components/content/CardLink/CardLink.css +32 -0
  25. package/src/components/content/MarkdownContent/MarkdownContent.css +14 -4
  26. package/src/components/content/MarkdownContent/MarkdownInline.astro +1 -1
  27. package/src/components/content/MarkdownContent/contracts.ts +1 -1
  28. package/src/components/content/MarkdownContent/markdownContentRenderer.ts +552 -0
  29. package/src/components/data-display/TextPair/TextPair.css +8 -2
  30. package/src/components/feedback/PreferenceBootstrap/PreferenceBootstrap.astro +403 -0
  31. package/src/components/forms/Checkbox/Checkbox.astro +6 -0
  32. package/src/components/forms/ChoiceGroup/ChoiceGroup.astro +57 -0
  33. package/src/components/forms/ChoiceGroup/ChoiceGroup.css +108 -0
  34. package/src/components/forms/ChoiceGroup/contracts.ts +9 -0
  35. package/src/components/forms/DateTimeInput/DateTimeInput.astro +14 -0
  36. package/src/components/forms/PreparedFormBootstrap/PreparedFormBootstrap.astro +203 -0
  37. package/src/components/forms/PreparedFormBootstrap/contracts.ts +15 -0
  38. package/src/components/forms/TextInput/TextInput.astro +7 -1
  39. package/src/components/forms/Textarea/Textarea.astro +35 -0
  40. package/src/components/forms/Textarea/Textarea.css +62 -0
  41. package/src/components/icons/BackIcon.astro +26 -0
  42. package/src/components/icons/DeleteIcon.astro +22 -0
  43. package/src/components/icons/StatusIndicatorIcon.astro +16 -0
  44. package/src/components/index.ts +18 -0
  45. package/src/components/media/CanvasFrame/CanvasFrame.astro +1 -17
  46. package/src/components/media/EmbedFrame/EmbedFrame.astro +1 -19
  47. package/src/components/media/EmbedFrame/EmbedFrame.css +0 -2
  48. package/src/components/navigation/Breadcrumbs/Breadcrumbs.astro +5 -1
  49. package/src/components/navigation/Breadcrumbs/Breadcrumbs.css +12 -17
  50. package/src/components/navigation/Footer/Footer.astro +102 -0
  51. package/src/components/navigation/Footer/Footer.css +155 -0
  52. package/src/components/navigation/Footer/contracts.ts +17 -0
  53. package/src/components/navigation/SubNavigation/SubNavigation.css +0 -5
  54. package/src/composition/Grid/Grid.css +16 -0
  55. package/src/composition/SurfaceGroup/SurfaceGroup.css +21 -7
  56. package/src/composition/SurfaceGroup/stickySurfaceGroupController.ts +3 -0
  57. package/src/data/firestore/client/contracts.ts +78 -0
  58. package/src/data/firestore/client/firebase.ts +56 -0
  59. package/src/data/firestore/client/index.ts +3 -0
  60. package/src/data/firestore/client/repository.ts +111 -0
  61. package/src/display/contracts.ts +44 -1
  62. package/src/display/index.ts +1 -0
  63. package/src/display/tableProgressive.ts +62 -0
  64. package/src/layouts/BaseLayout.astro +8 -1
  65. package/src/patterns/Combobox/Combobox.astro +23 -0
  66. package/src/patterns/Combobox/comboboxController.ts +69 -0
  67. package/src/patterns/Fullscreen/FullscreenControl.astro +4 -14
  68. package/src/patterns/Fullscreen/FullscreenControl.css +0 -26
  69. package/src/patterns/Modal/Modal.astro +1 -1
  70. package/src/patterns/Modal/Modal.css +38 -1
  71. package/src/patterns/Table/Table.astro +122 -7
  72. package/src/patterns/Table/TableBody.astro +8 -0
  73. package/src/patterns/Table/TableBody.css +3 -3
  74. package/src/patterns/Table/TableCell.css +11 -1
  75. package/src/patterns/Table/TableCellValue.astro +31 -0
  76. package/src/patterns/Table/TableFooter.astro +3 -1
  77. package/src/patterns/Table/TableHeader.astro +1 -0
  78. package/src/patterns/Table/TablePagination.astro +63 -47
  79. package/src/patterns/Table/TableSkeleton.astro +18 -22
  80. package/src/patterns/Table/TableSkeleton.css +30 -5
  81. package/src/patterns/Table/tableClientController.ts +253 -0
  82. package/src/patterns/Table/tableFilterController.ts +23 -31
  83. package/src/patterns/Table/tableProgressiveController.ts +615 -227
  84. package/src/patterns/Table/tableSortController.ts +25 -7
  85. package/src/styles/base.css +24 -7
  86. package/src/styles/interaction.css +16 -0
  87. package/src/styles/style.css +3 -0
  88. package/src/styles/tokens.css +5 -0
@@ -0,0 +1,552 @@
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') {
263
+ const element = createElement('strong', 'markdown-content__strong');
264
+ appendInline(element, node.children);
265
+ return element;
266
+ }
267
+ if (node.type === 'emphasis') {
268
+ const element = document.createElement('em');
269
+ appendInline(element, node.children);
270
+ return element;
271
+ }
272
+ if (node.type === 'code') {
273
+ const code = createElement('code', 'markdown-content__inline-code');
274
+ code.textContent = node.value;
275
+ return code;
276
+ }
277
+ if (node.type === 'link') {
278
+ const link = createElement('a', 'markdown-content__link');
279
+ link.href = node.href;
280
+ link.dataset.tone = 'info';
281
+ if (node.lang) link.lang = node.lang;
282
+ if (node.treatment) link.dataset.markdownLinkTreatment = node.treatment;
283
+ if (node.external) {
284
+ link.target = '_blank';
285
+ link.rel = 'noopener noreferrer';
286
+ }
287
+ appendInline(link, node.children);
288
+ return link;
289
+ }
290
+ return document.createElement('br');
291
+ }
292
+
293
+ function renderSymbol(node: MarkdownSymbol): HTMLSpanElement {
294
+ const symbol = createElement('span', 'markdown-content__symbol');
295
+ symbol.dataset.markdownSymbol = node.name;
296
+ if (node.tone) symbol.dataset.tone = node.tone;
297
+ if (node.lang) symbol.lang = node.lang;
298
+ symbol.append(createSymbolIcon(node.name));
299
+
300
+ if (node.label) {
301
+ const label = createElement('span', 'markdown-content__symbol-label');
302
+ label.textContent = node.label;
303
+ symbol.append(label);
304
+ } else if (node.accessibleLabel) {
305
+ const label = createElement(
306
+ 'span',
307
+ 'markdown-content__visually-hidden',
308
+ );
309
+ label.textContent = node.accessibleLabel;
310
+ symbol.append(label);
311
+ }
312
+ return symbol;
313
+ }
314
+
315
+ function createSymbolIcon(name: MarkdownSymbol['name']): SVGSVGElement {
316
+ const svg = document.createElementNS(svgNamespace, 'svg');
317
+ svg.setAttribute('viewBox', '0 0 32 32');
318
+ svg.setAttribute('fill', 'none');
319
+ svg.setAttribute('aria-hidden', 'true');
320
+ svg.setAttribute('focusable', 'false');
321
+ svg.classList.add('markdown-content__symbol-icon');
322
+
323
+ if (name.startsWith('terrain-')) {
324
+ svg.classList.add('circle-symbol-icon');
325
+ svg.dataset.terrainIcon = name.slice('terrain-'.length);
326
+ appendTerrainIcon(svg, name);
327
+ return svg;
328
+ }
329
+
330
+ if (name === 'bullet') {
331
+ const circle = document.createElementNS(svgNamespace, 'circle');
332
+ circle.setAttribute('cx', '16');
333
+ circle.setAttribute('cy', '16');
334
+ circle.setAttribute('r', '4.5');
335
+ circle.setAttribute('fill', 'currentColor');
336
+ svg.append(circle);
337
+ return svg;
338
+ }
339
+
340
+ const path = document.createElementNS(svgNamespace, 'path');
341
+ path.setAttribute('stroke', 'currentColor');
342
+ path.setAttribute('stroke-linecap', 'round');
343
+ path.setAttribute('stroke-linejoin', 'round');
344
+ path.setAttribute('stroke-width', name === 'neutral' ? '3' : '2.4');
345
+ path.setAttribute(
346
+ 'd',
347
+ name === 'increase'
348
+ ? 'm7.5 19.5 8.5-8.5 8.5 8.5'
349
+ : name === 'decrease'
350
+ ? 'm7.5 12.5 8.5 8.5 8.5-8.5'
351
+ : name === 'adjustment'
352
+ ? 'M11 24V8m0 0-4 4m4-4 4 4m6-4v16m0 0-4-4m4 4 4-4'
353
+ : name === 'neutral'
354
+ ? 'M6 11.5h20M6 20.5h20'
355
+ : 'm12.5 7.5 8.5 8.5-8.5 8.5',
356
+ );
357
+ svg.append(path);
358
+ return svg;
359
+ }
360
+
361
+ function appendTerrainIcon(
362
+ svg: SVGSVGElement,
363
+ name: Extract<MarkdownSymbol['name'], `terrain-${string}`>,
364
+ ): void {
365
+ const surface = document.createElementNS(svgNamespace, 'circle');
366
+ surface.setAttribute('cx', '16');
367
+ surface.setAttribute('cy', '16');
368
+ surface.setAttribute('r', '13');
369
+ surface.classList.add('circle-symbol-icon__surface');
370
+ svg.append(surface);
371
+
372
+ if (name === 'terrain-field') {
373
+ const rectangle = document.createElementNS(svgNamespace, 'rect');
374
+ rectangle.setAttribute('x', '9');
375
+ rectangle.setAttribute('y', '9');
376
+ rectangle.setAttribute('width', '14');
377
+ rectangle.setAttribute('height', '14');
378
+ rectangle.setAttribute('rx', '1');
379
+ rectangle.classList.add('circle-symbol-icon__mark');
380
+ const grid = document.createElementNS(svgNamespace, 'path');
381
+ grid.setAttribute('d', 'M13.7 9v14m4.6-14v14M9 13.7h14M9 18.3h14');
382
+ grid.classList.add('circle-symbol-icon__mark');
383
+ svg.append(rectangle, grid);
384
+ return;
385
+ }
386
+
387
+ const fill = document.createElementNS(svgNamespace, 'path');
388
+ fill.classList.add('circle-symbol-icon__fill');
389
+ fill.setAttribute(
390
+ 'd',
391
+ name === 'terrain-grass'
392
+ ? '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'
393
+ : name === 'terrain-water'
394
+ ? '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'
395
+ : 'm5.5 24 7.3-13 3.1 5.1 3.4-7.1 7.2 15H5.5Z',
396
+ );
397
+ const highlight = document.createElementNS(svgNamespace, 'path');
398
+ highlight.classList.add('circle-symbol-icon__highlight');
399
+ highlight.setAttribute(
400
+ 'd',
401
+ name === 'terrain-grass'
402
+ ? 'M10.4 23.5c2.2-5.3 5.4-8.7 10.2-11.6'
403
+ : name === 'terrain-water'
404
+ ? 'M12.2 19.2a4.5 4.5 0 0 0 4.5 4.1'
405
+ : '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',
406
+ );
407
+ svg.append(fill, highlight);
408
+ }
409
+
410
+ function renderMedia(
411
+ media: DisplayMedia,
412
+ className: string,
413
+ size: 'fill' | 'intrinsic',
414
+ preventUpscale: boolean,
415
+ ): HTMLElement {
416
+ const figure = createElement('figure', `media ${className}`);
417
+ figure.dataset.widget = 'media';
418
+ figure.dataset.mediaFit = media.fit ?? 'contain';
419
+ figure.dataset.mediaFraming = media.framing ?? 'asset';
420
+ figure.dataset.mediaRendering = media.rendering ?? 'auto';
421
+ figure.dataset.mediaShape = media.shape ?? 'default';
422
+ figure.dataset.mediaSize = size;
423
+ figure.dataset.mediaUpscale = preventUpscale
424
+ ? 'prevent'
425
+ : (media.upscale ?? 'allow');
426
+ figure.dataset.mediaVisualSize = media.visualSize ?? 'default';
427
+ figure.style.setProperty(
428
+ '--media-aspect-ratio',
429
+ `${media.width} / ${media.height}`,
430
+ );
431
+ figure.style.setProperty('--media-source-inline-size', `${media.width}px`);
432
+ figure.style.setProperty('--media-source-block-size', `${media.height}px`);
433
+
434
+ const frame = createElement('div', 'media__frame');
435
+ const image = document.createElement('img');
436
+ image.src = media.src;
437
+ image.alt = media.alt;
438
+ image.width = media.width;
439
+ image.height = media.height;
440
+ image.loading = media.loading ?? 'lazy';
441
+ image.decoding = media.decoding ?? 'async';
442
+ if (media.sources?.length) {
443
+ image.srcset = media.sources
444
+ .map((source) => `${source.src} ${source.width}w`)
445
+ .join(', ');
446
+ }
447
+ if (media.sizes) image.sizes = media.sizes;
448
+ frame.append(image);
449
+ figure.append(frame);
450
+ if (media.caption) {
451
+ const caption = document.createElement('figcaption');
452
+ caption.textContent = media.caption;
453
+ figure.append(caption);
454
+ }
455
+ return figure;
456
+ }
457
+
458
+ function renderCodeBlock(
459
+ language: string | undefined,
460
+ value: string,
461
+ copy: CodeBlockCopy | undefined,
462
+ ): HTMLElement {
463
+ const normalized = language?.trim().toLowerCase() || undefined;
464
+ const presentation = normalized === 'markdown' ? 'light' : 'default';
465
+ const tone =
466
+ normalized && codeBlockTones.has(normalized as Tone)
467
+ ? (normalized as Tone)
468
+ : 'neutral';
469
+ const content = createElement('pre', 'code-block');
470
+ content.dataset.codeBlockPresentation = presentation;
471
+ content.dataset.wrap = 'false';
472
+ content.textContent = value;
473
+ if (!copy) {
474
+ content.dataset.widget = 'code-block';
475
+ content.dataset.tone = tone;
476
+ return content;
477
+ }
478
+
479
+ const shell = createElement('nox-code-block', 'code-block-shell');
480
+ shell.dataset.widget = 'code-block';
481
+ shell.dataset.tone = tone;
482
+ content.dataset.codeBlockContent = '';
483
+ const button = createElement('button', 'nox-button code-block__copy');
484
+ button.type = 'button';
485
+ button.dataset.buttonBorderless = 'true';
486
+ button.dataset.buttonHoverTone = 'info';
487
+ button.dataset.buttonIconOnly = 'true';
488
+ button.dataset.buttonSize = 'sm';
489
+ button.dataset.codeBlockCopy = '';
490
+ button.dataset.copyLabel = copy.copyLabel;
491
+ button.dataset.copiedLabel = copy.copiedLabel;
492
+ button.dataset.copyUnavailableLabel = copy.unavailableLabel;
493
+ button.setAttribute('aria-label', copy.copyLabel);
494
+ button.title = copy.copyLabel;
495
+ if (copy.lang) button.lang = copy.lang;
496
+ button.append(createCopyIcon());
497
+ const status = createElement('span', 'code-block__status');
498
+ status.dataset.codeBlockStatus = '';
499
+ status.setAttribute('aria-live', 'polite');
500
+ if (copy.lang) status.lang = copy.lang;
501
+ shell.append(content, button, status);
502
+ initCodeBlock(shell);
503
+ return shell;
504
+ }
505
+
506
+ function createCopyIcon(): SVGSVGElement {
507
+ const svg = document.createElementNS(svgNamespace, 'svg');
508
+ svg.setAttribute('viewBox', '0 0 32 32');
509
+ svg.setAttribute('fill', 'none');
510
+ svg.setAttribute('aria-hidden', 'true');
511
+ svg.setAttribute('focusable', 'false');
512
+ const rectangle = document.createElementNS(svgNamespace, 'rect');
513
+ rectangle.setAttribute('x', '11');
514
+ rectangle.setAttribute('y', '11');
515
+ rectangle.setAttribute('width', '14');
516
+ rectangle.setAttribute('height', '14');
517
+ rectangle.setAttribute('rx', '2');
518
+ rectangle.setAttribute('stroke', 'currentColor');
519
+ rectangle.setAttribute('stroke-width', '2.5');
520
+ const path = document.createElementNS(svgNamespace, 'path');
521
+ path.setAttribute(
522
+ 'd',
523
+ 'M21 11V9C21 7.89543 20.1046 7 19 7H9C7.89543 7 7 7.89543 7 9V19C7 20.1046 7.89543 21 9 21H11',
524
+ );
525
+ path.setAttribute('stroke', 'currentColor');
526
+ path.setAttribute('stroke-width', '2.5');
527
+ path.setAttribute('stroke-linecap', 'round');
528
+ path.setAttribute('stroke-linejoin', 'round');
529
+ svg.append(rectangle, path);
530
+ return svg;
531
+ }
532
+
533
+ function createHeading(
534
+ level: number,
535
+ className: string,
536
+ ): HTMLHeadingElement {
537
+ const heading = createElement(
538
+ `h${Math.min(6, Math.max(2, level))}` as keyof HTMLElementTagNameMap,
539
+ className,
540
+ ) as HTMLHeadingElement;
541
+ heading.dataset.markdownHeadingLevel = String(level);
542
+ return heading;
543
+ }
544
+
545
+ function createElement<K extends keyof HTMLElementTagNameMap>(
546
+ tag: K,
547
+ className?: string,
548
+ ): HTMLElementTagNameMap[K] {
549
+ const element = document.createElement(tag);
550
+ if (className) element.className = className;
551
+ return element;
552
+ }
@@ -1,6 +1,10 @@
1
1
  .text-pair {
2
2
  --text-pair-primary-color: var(--nox-text);
3
+ --text-pair-primary-font-size: inherit;
4
+ --text-pair-primary-letter-spacing: inherit;
5
+ --text-pair-primary-text-transform: none;
3
6
  --text-pair-secondary-color: var(--nox-text-muted);
7
+ --text-pair-secondary-font-size: var(--nox-font-size-sm);
4
8
 
5
9
  display: grid;
6
10
  min-width: 0;
@@ -27,9 +31,11 @@
27
31
  .text-pair__primary {
28
32
  min-width: 0;
29
33
  color: var(--text-pair-primary-color);
30
- font-size: inherit;
34
+ font-size: var(--text-pair-primary-font-size);
35
+ letter-spacing: var(--text-pair-primary-letter-spacing);
31
36
  line-height: inherit;
32
37
  overflow-wrap: anywhere;
38
+ text-transform: var(--text-pair-primary-text-transform);
33
39
  }
34
40
 
35
41
  .text-pair__primary[data-text-color='muted'] {
@@ -58,7 +64,7 @@
58
64
  .text-pair__secondary {
59
65
  min-width: 0;
60
66
  color: var(--text-pair-secondary-color);
61
- font-size: var(--nox-font-size-sm);
67
+ font-size: var(--text-pair-secondary-font-size);
62
68
  line-height: 1.35;
63
69
  overflow-wrap: anywhere;
64
70
  }