@astronox/core 0.2.3 → 0.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 (127) hide show
  1. package/README.md +16 -1
  2. package/package.json +18 -4
  3. package/src/admin/authController.ts +166 -0
  4. package/src/admin/cache.ts +44 -17
  5. package/src/admin/index.ts +3 -0
  6. package/src/admin/preparedDestination.ts +114 -0
  7. package/src/admin/resourceClient.ts +107 -0
  8. package/src/ads/adsense/index.ts +1 -0
  9. package/src/ads/adsense.ts +112 -0
  10. package/src/ads/contracts.ts +11 -0
  11. package/src/ads/index.ts +2 -0
  12. package/src/ads/slot.ts +16 -0
  13. package/src/auth/contracts.ts +1 -4
  14. package/src/auth/firebase/client.ts +19 -6
  15. package/src/auth/firebase/index.ts +1 -0
  16. package/src/client/formSubmission.ts +6 -1
  17. package/src/client/preferences.ts +8 -4
  18. package/src/components/actions/Button/Button.astro +30 -42
  19. package/src/components/actions/Button/Button.css +1 -1
  20. package/src/components/actions/CardAction/CardAction.css +8 -13
  21. package/src/components/actions/IdentityProviderAction/IdentityProviderAction.astro +6 -8
  22. package/src/components/actions/MediaTrigger/MediaTrigger.astro +1 -1
  23. package/src/components/actions/ScrollToTop/ScrollToTop.astro +2 -1
  24. package/src/components/actions/ScrollToTop/ScrollToTop.css +2 -6
  25. package/src/components/actions/ThemeToggle/ThemeToggle.astro +59 -0
  26. package/src/components/actions/ThemeToggle/ThemeToggle.css +83 -0
  27. package/src/components/actions/ThemeToggle/themeToggleController.ts +110 -0
  28. package/src/components/content/CardFooter/CardFooter.css +2 -4
  29. package/src/components/content/CardHeader/CardHeader.astro +1 -4
  30. package/src/components/content/CardLink/CardLink.astro +3 -15
  31. package/src/components/content/CardLink/CardLink.css +4 -15
  32. package/src/components/content/CodeBlock/CodeBlock.astro +7 -6
  33. package/src/components/content/CodeBlock/CodeBlock.css +2 -5
  34. package/src/components/content/CodeBlock/codeBlockController.ts +1 -2
  35. package/src/components/content/MarkdownContent/MarkdownContent.astro +1 -3
  36. package/src/components/content/MarkdownContent/MarkdownContent.css +7 -2
  37. package/src/components/content/MarkdownContent/MarkdownInline.astro +11 -3
  38. package/src/components/content/MarkdownContent/MarkdownNode.astro +28 -33
  39. package/src/components/content/MarkdownContent/MarkdownSymbol.astro +30 -19
  40. package/src/components/content/MarkdownContent/contracts.ts +15 -15
  41. package/src/components/content/MarkdownContent/markdownContentRenderer.ts +27 -83
  42. package/src/components/feedback/PreferenceBootstrap/PreferenceBootstrap.astro +19 -22
  43. package/src/components/feedback/Spinner/Spinner.astro +1 -2
  44. package/src/components/feedback/ThemeBootstrap/ThemeBootstrap.astro +31 -0
  45. package/src/components/feedback/Tooltip/tooltipController.ts +1 -4
  46. package/src/components/forms/ChoiceGroup/ChoiceGroup.css +2 -5
  47. package/src/components/forms/DateTimeInput/DateTimeInput.astro +2 -1
  48. package/src/components/forms/PreparedFormBootstrap/PreparedFormBootstrap.astro +10 -8
  49. package/src/components/forms/PreparedFormBootstrap/parsePreparedFormState.ts +48 -0
  50. package/src/components/forms/Textarea/Textarea.astro +2 -2
  51. package/src/components/icons/BurnIcon.astro +2 -1
  52. package/src/components/icons/CircleSymbolIcon.css +3 -3
  53. package/src/components/icons/FieldIcon.astro +10 -4
  54. package/src/components/icons/GrassIcon.astro +3 -2
  55. package/src/components/icons/SlowIcon.astro +1 -2
  56. package/src/components/icons/StunIcon.astro +2 -1
  57. package/src/components/icons/WaterIcon.astro +3 -2
  58. package/src/components/index.ts +4 -0
  59. package/src/components/media/CanvasFrame/CanvasFrame.astro +1 -2
  60. package/src/components/media/CanvasFrame/canvasFrameController.ts +1 -4
  61. package/src/components/media/EmbedFrame/EmbedFrame.astro +5 -12
  62. package/src/components/media/EmbedFrame/embedFrameController.ts +4 -4
  63. package/src/components/monetization/AdSlot/AdSlot.astro +33 -0
  64. package/src/components/monetization/AdSlot/AdSlot.css +53 -0
  65. package/src/components/navigation/DesktopNavigation.css +5 -3
  66. package/src/components/navigation/Footer/Footer.css +3 -9
  67. package/src/components/navigation/Header.astro +20 -2
  68. package/src/components/navigation/MobileNavigation.astro +107 -43
  69. package/src/components/navigation/MobileNavigation.css +102 -26
  70. package/src/components/navigation/NavigationBrand.css +1 -1
  71. package/src/components/navigation/NavigationControl.css +16 -2
  72. package/src/components/navigation/NavigationItemContent.astro +6 -1
  73. package/src/components/navigation/mobileNavigationController.ts +0 -16
  74. package/src/components/navigation/navigationTypes.ts +6 -1
  75. package/src/components/seo/SeoHead.astro +1 -6
  76. package/src/composition/SurfaceGroup/SurfaceGroup.css +4 -14
  77. package/src/config/environment.ts +52 -0
  78. package/src/config/index.ts +3 -0
  79. package/src/config/integrations.ts +9 -0
  80. package/src/config/siteConfig.ts +5 -0
  81. package/src/config/storage.ts +16 -0
  82. package/src/content/admin.ts +354 -0
  83. package/src/content/cache.ts +177 -0
  84. package/src/content/catalog.ts +72 -0
  85. package/src/content/contracts.ts +77 -0
  86. package/src/content/drafts.ts +49 -0
  87. package/src/content/index.ts +7 -0
  88. package/src/content/markdown.ts +146 -0
  89. package/src/content/snapshot.ts +139 -0
  90. package/src/data/firestore/client/contracts.ts +10 -2
  91. package/src/data/index.ts +1 -0
  92. package/src/data/memoizedLoader.ts +13 -0
  93. package/src/display/contracts.ts +2 -5
  94. package/src/i18n/config.ts +2 -8
  95. package/src/i18n/coreMessages.ts +2 -9
  96. package/src/i18n/preference.ts +2 -6
  97. package/src/layouts/BaseLayout.astro +5 -0
  98. package/src/patterns/Combobox/Combobox.astro +9 -12
  99. package/src/patterns/Combobox/comboboxController.ts +14 -8
  100. package/src/patterns/Fullscreen/FullscreenControl.astro +5 -1
  101. package/src/patterns/Fullscreen/fullscreenController.ts +1 -3
  102. package/src/patterns/MapViewport/MapViewport.css +3 -3
  103. package/src/patterns/MapViewport/geometry.ts +1 -4
  104. package/src/patterns/Modal/Modal.css +1 -4
  105. package/src/patterns/Modal/ModalOpen.astro +5 -1
  106. package/src/patterns/Table/Table.astro +10 -10
  107. package/src/patterns/Table/TableBody.astro +1 -4
  108. package/src/patterns/Table/TableBody.css +2 -4
  109. package/src/patterns/Table/TablePagination.astro +23 -30
  110. package/src/patterns/Table/TableSkeleton.astro +1 -5
  111. package/src/patterns/Table/tableClientController.ts +2 -2
  112. package/src/patterns/Table/tableFilterController.ts +1 -3
  113. package/src/patterns/Table/tableHeaderOverflowController.ts +1 -4
  114. package/src/patterns/Table/tableProgressiveController.ts +62 -60
  115. package/src/patterns/Table/tableScrollController.ts +3 -2
  116. package/src/primitives/text/TextBlock/TextBlock.astro +1 -3
  117. package/src/publication/contracts.ts +11 -0
  118. package/src/publication/http.ts +31 -0
  119. package/src/publication/index.ts +2 -0
  120. package/src/routing/localized.ts +9 -12
  121. package/src/seo/localizedMeta.ts +2 -6
  122. package/src/styles/interaction.css +7 -7
  123. package/src/styles/style.css +12 -20
  124. package/src/styles/tokens.css +55 -15
  125. package/src/theme/contracts.ts +3 -6
  126. package/src/theme/index.ts +1 -0
  127. package/src/theme/storage.ts +1 -0
@@ -0,0 +1,110 @@
1
+ import type { ColorTheme } from '../../../theme/contracts';
2
+ import { defaultThemeStorageKey } from '../../../theme/storage';
3
+
4
+ const initializedToggles = new WeakSet<HTMLElement>();
5
+ const systemThemeQuery = '(prefers-color-scheme: dark)';
6
+ let globalListenersInitialized = false;
7
+
8
+ function isColorTheme(value: string | null): value is ColorTheme {
9
+ return value === 'light' || value === 'dark';
10
+ }
11
+
12
+ function getStorageKey(): string {
13
+ return (
14
+ document.documentElement.dataset.themeStorageKey ?? defaultThemeStorageKey
15
+ );
16
+ }
17
+
18
+ function readStoredTheme(): ColorTheme | null {
19
+ try {
20
+ const value = window.localStorage.getItem(getStorageKey());
21
+ return isColorTheme(value) ? value : null;
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+
27
+ function getSystemTheme(): ColorTheme {
28
+ return window.matchMedia(systemThemeQuery).matches ? 'dark' : 'light';
29
+ }
30
+
31
+ function getCurrentTheme(): ColorTheme {
32
+ const value = document.documentElement.dataset.theme ?? null;
33
+ return isColorTheme(value) ? value : (readStoredTheme() ?? getSystemTheme());
34
+ }
35
+
36
+ function updateToggle(toggle: HTMLElement, theme: ColorTheme): void {
37
+ const label =
38
+ theme === 'light'
39
+ ? toggle.dataset.themeToggleDarkLabel
40
+ : toggle.dataset.themeToggleLightLabel;
41
+
42
+ if (label) {
43
+ toggle.setAttribute('aria-label', label);
44
+ toggle.setAttribute('title', label);
45
+ }
46
+ }
47
+
48
+ function updateToggles(theme: ColorTheme): void {
49
+ for (const toggle of document.querySelectorAll<HTMLElement>(
50
+ '[data-theme-toggle]',
51
+ )) {
52
+ updateToggle(toggle, theme);
53
+ }
54
+ }
55
+
56
+ function applyTheme(theme: ColorTheme, persist: boolean): void {
57
+ document.documentElement.dataset.theme = theme;
58
+
59
+ if (persist) {
60
+ try {
61
+ window.localStorage.setItem(getStorageKey(), theme);
62
+ } catch {
63
+ // The selected theme remains active for the current document.
64
+ }
65
+ }
66
+
67
+ updateToggles(theme);
68
+ document.dispatchEvent(
69
+ new CustomEvent('astronox:theme-change', { detail: { theme } }),
70
+ );
71
+ }
72
+
73
+ function initializeGlobalListeners(): void {
74
+ if (globalListenersInitialized) return;
75
+ globalListenersInitialized = true;
76
+
77
+ const media = window.matchMedia(systemThemeQuery);
78
+ media.addEventListener('change', (event) => {
79
+ if (readStoredTheme() === null) {
80
+ applyTheme(event.matches ? 'dark' : 'light', false);
81
+ }
82
+ });
83
+
84
+ window.addEventListener('storage', (event) => {
85
+ if (event.key !== getStorageKey()) return;
86
+
87
+ const theme = isColorTheme(event.newValue)
88
+ ? event.newValue
89
+ : getSystemTheme();
90
+ applyTheme(theme, false);
91
+ });
92
+ }
93
+
94
+ export function initThemeToggle(toggle: HTMLElement): () => void {
95
+ if (initializedToggles.has(toggle)) return () => {};
96
+ initializedToggles.add(toggle);
97
+ initializeGlobalListeners();
98
+ updateToggle(toggle, getCurrentTheme());
99
+
100
+ const handleClick = (): void => {
101
+ applyTheme(getCurrentTheme() === 'light' ? 'dark' : 'light', true);
102
+ };
103
+
104
+ toggle.addEventListener('click', handleClick);
105
+
106
+ return () => {
107
+ toggle.removeEventListener('click', handleClick);
108
+ initializedToggles.delete(toggle);
109
+ };
110
+ }
@@ -42,16 +42,14 @@
42
42
  var(--card-footer-size);
43
43
  }
44
44
 
45
- .card-footer[data-has-leading-action][data-has-action]
46
- .card-footer__layout {
45
+ .card-footer[data-has-leading-action][data-has-action] .card-footer__layout {
47
46
  grid-template-columns:
48
47
  var(--card-footer-size)
49
48
  minmax(0, 1fr)
50
49
  var(--card-footer-size);
51
50
  }
52
51
 
53
- .card-footer[data-has-leading-action][data-has-media]
54
- .card-footer__layout {
52
+ .card-footer[data-has-leading-action][data-has-media] .card-footer__layout {
55
53
  grid-template-columns:
56
54
  var(--card-footer-size)
57
55
  var(--card-footer-size)
@@ -2,10 +2,7 @@
2
2
  import type { HTMLAttributes } from 'astro/types';
3
3
  import type { Tone } from '../../../theme/contracts';
4
4
  import type { DisplayMedia, TextWeight } from '../../../display/contracts';
5
- import type {
6
- AstroComponent,
7
- HeadingLevel,
8
- } from '../../widgetTypes';
5
+ import type { AstroComponent, HeadingLevel } from '../../widgetTypes';
9
6
  import Inline from '../../../composition/Inline/Inline.astro';
10
7
  import Media from '../../../primitives/media/Media/Media.astro';
11
8
 
@@ -90,11 +90,7 @@ const resolvedTone = tone ?? dataTone;
90
90
  {Icon ? (
91
91
  <Icon class="card-link__icon" data-tone={iconTone} />
92
92
  ) : media ? (
93
- <Media
94
- media={media}
95
- fit="contain"
96
- size="intrinsic"
97
- />
93
+ <Media media={media} fit="contain" size="intrinsic" />
98
94
  ) : null}
99
95
  </span>
100
96
  ) : null
@@ -104,11 +100,7 @@ const resolvedTone = tone ?? dataTone;
104
100
  {
105
101
  hasContentMedia && media ? (
106
102
  <span class="card-link__content-media" aria-hidden="true">
107
- <Media
108
- media={media}
109
- fit="contain"
110
- size="intrinsic"
111
- />
103
+ <Media media={media} fit="contain" size="intrinsic" />
112
104
  </span>
113
105
  ) : null
114
106
  }
@@ -134,11 +126,7 @@ const resolvedTone = tone ?? dataTone;
134
126
  {Icon ? (
135
127
  <Icon class="card-link__icon" data-tone={iconTone} />
136
128
  ) : media ? (
137
- <Media
138
- media={media}
139
- fit="contain"
140
- size="intrinsic"
141
- />
129
+ <Media media={media} fit="contain" size="intrinsic" />
142
130
  ) : null}
143
131
  </span>
144
132
  ) : null
@@ -154,26 +154,17 @@
154
154
  );
155
155
  }
156
156
 
157
- .card-link:is(
158
- [data-media-size='compact'],
159
- [data-media-size='small']
160
- )
157
+ .card-link:is([data-media-size='compact'], [data-media-size='small'])
161
158
  .card-link__layout {
162
159
  --card-link-artwork-max-size: var(--card-link-resolved-media-size);
163
160
  }
164
161
 
165
- .card-link:is(
166
- [data-media-size='compact'],
167
- [data-media-size='small']
168
- )
162
+ .card-link:is([data-media-size='compact'], [data-media-size='small'])
169
163
  .card-link__content {
170
164
  --card-link-content-media-size: var(--card-link-resolved-media-size);
171
165
  }
172
166
 
173
- .card-link:is(
174
- [data-media-size='compact'],
175
- [data-media-size='small']
176
- )
167
+ .card-link:is([data-media-size='compact'], [data-media-size='small'])
177
168
  :is(.card-link__media, .card-link__content-media)
178
169
  [data-widget='media'] {
179
170
  --media-object-inline-size: 100%;
@@ -203,9 +194,7 @@
203
194
  }
204
195
 
205
196
  .card-link[data-density='relaxed'] .card-link__layout {
206
- --card-link-size: calc(
207
- var(--nox-navigation-block-size) + var(--nox-space-5)
208
- );
197
+ --card-link-size: calc(var(--nox-navigation-block-size) + var(--nox-space-5));
209
198
  }
210
199
 
211
200
  .card-link[data-density='relaxed'] .card-link__content {
@@ -3,10 +3,7 @@ import type { HTMLAttributes } from 'astro/types';
3
3
  import Button from '../../actions/Button/Button.astro';
4
4
  import CopyIcon from '../../icons/CopyIcon.astro';
5
5
  import type { Tone } from '../../../theme/contracts';
6
- import type {
7
- CodeBlockCopy,
8
- CodeBlockPresentation,
9
- } from './contracts';
6
+ import type { CodeBlockCopy, CodeBlockPresentation } from './contracts';
10
7
 
11
8
  type Props = HTMLAttributes<'pre'> & {
12
9
  copy?: CodeBlockCopy;
@@ -42,7 +39,9 @@ const resolvedTone = tone ?? dataTone;
42
39
  data-code-block-content
43
40
  data-code-block-presentation={presentation}
44
41
  data-wrap={wrap ? true : undefined}
45
- >{text}</pre>
42
+ >
43
+ {text}
44
+ </pre>
46
45
  <Button
47
46
  class="code-block__copy"
48
47
  type="button"
@@ -75,7 +74,9 @@ const resolvedTone = tone ?? dataTone;
75
74
  data-code-block-presentation={presentation}
76
75
  data-tone={resolvedTone}
77
76
  data-wrap={wrap ? true : undefined}
78
- >{text}</pre>
77
+ >
78
+ {text}
79
+ </pre>
79
80
  )
80
81
  }
81
82
 
@@ -18,8 +18,7 @@
18
18
  --code-block-light-line-height: 1.125rem;
19
19
  --code-block-copy-size: calc(
20
20
  var(--code-block-light-line-height) + var(--nox-space-3) +
21
- var(--nox-space-3) + var(--nox-border-width) +
22
- var(--nox-border-width)
21
+ var(--nox-space-3) + var(--nox-border-width) + var(--nox-border-width)
23
22
  );
24
23
 
25
24
  position: relative;
@@ -30,9 +29,7 @@
30
29
  .code-block-shell .code-block {
31
30
  box-sizing: border-box;
32
31
  inline-size: 100%;
33
- padding-inline-end: calc(
34
- var(--code-block-copy-size) + var(--nox-space-3)
35
- );
32
+ padding-inline-end: calc(var(--code-block-copy-size) + var(--nox-space-3));
36
33
  }
37
34
 
38
35
  .code-block[data-code-block-presentation='light'] {
@@ -17,8 +17,7 @@ export function initCodeBlock(root: HTMLElement): () => void {
17
17
  root.setAttribute(initializedAttribute, 'true');
18
18
  const copyLabel = copyButton.dataset.copyLabel ?? '';
19
19
  const copiedLabel = copyButton.dataset.copiedLabel ?? copyLabel;
20
- const unavailableLabel =
21
- copyButton.dataset.copyUnavailableLabel ?? copyLabel;
20
+ const unavailableLabel = copyButton.dataset.copyUnavailableLabel ?? copyLabel;
22
21
  let feedbackTimeout: number | undefined;
23
22
 
24
23
  const setButtonLabel = (label: string): void => {
@@ -1,8 +1,6 @@
1
1
  ---
2
2
  import type { HTMLAttributes } from 'astro/types';
3
- import type {
4
- CodeBlockCopy,
5
- } from '../CodeBlock/contracts';
3
+ import type { CodeBlockCopy } from '../CodeBlock/contracts';
6
4
  import MarkdownNode from './MarkdownNode.astro';
7
5
  import type { MarkdownDocument } from './contracts';
8
6
 
@@ -163,8 +163,7 @@
163
163
  gap: 0;
164
164
  }
165
165
 
166
- .markdown-content__list-item[data-markdown-resource]
167
- .markdown-content__list {
166
+ .markdown-content__list-item[data-markdown-resource] .markdown-content__list {
168
167
  padding-inline-start: 0;
169
168
  list-style: none;
170
169
  }
@@ -268,6 +267,12 @@
268
267
  block-size: 1em;
269
268
  }
270
269
 
270
+ .markdown-content__symbol-media {
271
+ block-size: 1.5em;
272
+ inline-size: 1.5em;
273
+ object-fit: contain;
274
+ }
275
+
271
276
  .markdown-content__symbol[data-markdown-symbol^='terrain-']
272
277
  .markdown-content__symbol-icon {
273
278
  inline-size: 1.25em;
@@ -20,10 +20,16 @@ const { node }: Props = Astro.props;
20
20
  <MarkdownSymbol node={node} />
21
21
  ) : node.type === 'strong' ? (
22
22
  <strong class="markdown-content__strong">
23
- {node.children.map((child) => <Astro.self node={child} />)}
23
+ {node.children.map((child) => (
24
+ <Astro.self node={child} />
25
+ ))}
24
26
  </strong>
25
27
  ) : node.type === 'emphasis' ? (
26
- <em>{node.children.map((child) => <Astro.self node={child} />)}</em>
28
+ <em>
29
+ {node.children.map((child) => (
30
+ <Astro.self node={child} />
31
+ ))}
32
+ </em>
27
33
  ) : node.type === 'code' ? (
28
34
  <code class="markdown-content__inline-code">{node.value}</code>
29
35
  ) : node.type === 'link' ? (
@@ -36,7 +42,9 @@ const { node }: Props = Astro.props;
36
42
  rel={node.external ? 'noopener noreferrer' : undefined}
37
43
  target={node.external ? '_blank' : undefined}
38
44
  >
39
- {node.children.map((child) => <Astro.self node={child} />)}
45
+ {node.children.map((child) => (
46
+ <Astro.self node={child} />
47
+ ))}
40
48
  </a>
41
49
  ) : (
42
50
  <br />
@@ -1,8 +1,6 @@
1
1
  ---
2
2
  import CodeBlock from '../CodeBlock/CodeBlock.astro';
3
- import type {
4
- CodeBlockCopy,
5
- } from '../CodeBlock/contracts';
3
+ import type { CodeBlockCopy } from '../CodeBlock/contracts';
6
4
  import Media from '../../../primitives/media/Media/Media.astro';
7
5
  import Separator from '../../../primitives/layout/Separator/Separator.astro';
8
6
  import type { Tone } from '../../../theme/contracts';
@@ -26,14 +24,14 @@ function toHeadingTag(
26
24
  return `h${level}` as 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
27
25
  }
28
26
 
29
- const defaultListMarker = {
27
+ const defaultListMarker: MarkdownListMarker = {
30
28
  symbol: 'bullet',
31
29
  tone: 'neutral',
32
- } as const satisfies MarkdownListMarker;
33
- const nestedListMarker = {
30
+ };
31
+ const nestedListMarker: MarkdownListMarker = {
34
32
  symbol: 'arrow-right',
35
33
  tone: 'neutral',
36
- } as const satisfies MarkdownListMarker;
34
+ };
37
35
 
38
36
  const codeBlockTones = new Set<Tone>([
39
37
  'neutral',
@@ -77,10 +75,7 @@ const listItems =
77
75
 
78
76
  {
79
77
  node.type === 'section' ? (
80
- <section
81
- class="markdown-content__section"
82
- aria-labelledby={node.id}
83
- >
78
+ <section class="markdown-content__section" aria-labelledby={node.id}>
84
79
  <HeadingTag
85
80
  class="markdown-content__section-title"
86
81
  data-markdown-heading-level={node.level}
@@ -89,16 +84,16 @@ const listItems =
89
84
  {node.title}
90
85
  </HeadingTag>
91
86
  <div class="markdown-content__section-body">
92
- {
93
- node.blocks.map((block) => (
94
- <Astro.self node={block} codeBlockCopy={codeBlockCopy} />
95
- ))
96
- }
87
+ {node.blocks.map((block) => (
88
+ <Astro.self node={block} codeBlockCopy={codeBlockCopy} />
89
+ ))}
97
90
  </div>
98
91
  </section>
99
92
  ) : node.type === 'paragraph' ? (
100
93
  <p class="markdown-content__paragraph">
101
- {node.children.map((child) => <MarkdownInline node={child} />)}
94
+ {node.children.map((child) => (
95
+ <MarkdownInline node={child} />
96
+ ))}
102
97
  </p>
103
98
  ) : node.type === 'heading' ? (
104
99
  <HeadingTag
@@ -106,17 +101,23 @@ const listItems =
106
101
  data-markdown-heading-level={node.level}
107
102
  id={node.id}
108
103
  >
109
- {node.children.map((child) => <MarkdownInline node={child} />)}
104
+ {node.children.map((child) => (
105
+ <MarkdownInline node={child} />
106
+ ))}
110
107
  </HeadingTag>
111
108
  ) : node.type === 'change' ? (
112
109
  <div class="markdown-content__change">
113
110
  <span class="markdown-content__change-label">
114
- {node.label.map((child) => <MarkdownInline node={child} />)}
111
+ {node.label.map((child) => (
112
+ <MarkdownInline node={child} />
113
+ ))}
115
114
  <span>:</span>
116
115
  </span>
117
116
  {node.presentation === 'statement' ? (
118
117
  <span class="markdown-content__change-values">
119
- {node.value.map((child) => <MarkdownInline node={child} />)}
118
+ {node.value.map((child) => (
119
+ <MarkdownInline node={child} />
120
+ ))}
120
121
  </span>
121
122
  ) : (
122
123
  <span class="markdown-content__change-values" lang={node.valueLang}>
@@ -187,9 +188,7 @@ const listItems =
187
188
  media={item.resource.media}
188
189
  size="fill"
189
190
  upscale={
190
- item.resource.mediaScale === 'compact'
191
- ? 'prevent'
192
- : undefined
191
+ item.resource.mediaScale === 'compact' ? 'prevent' : undefined
193
192
  }
194
193
  />
195
194
  ) : null}
@@ -242,11 +241,9 @@ const listItems =
242
241
  )}
243
242
  </div>
244
243
  ) : null}
245
- {
246
- item.children.map((block) => (
247
- <Astro.self node={block} codeBlockCopy={codeBlockCopy} />
248
- ))
249
- }
244
+ {item.children.map((block) => (
245
+ <Astro.self node={block} codeBlockCopy={codeBlockCopy} />
246
+ ))}
250
247
  </div>
251
248
  </li>
252
249
  ))}
@@ -256,11 +253,9 @@ const listItems =
256
253
  class="markdown-content__quote"
257
254
  data-tone={node.tone ?? 'neutral'}
258
255
  >
259
- {
260
- node.children.map((block) => (
261
- <Astro.self node={block} codeBlockCopy={codeBlockCopy} />
262
- ))
263
- }
256
+ {node.children.map((block) => (
257
+ <Astro.self node={block} codeBlockCopy={codeBlockCopy} />
258
+ ))}
264
259
  </blockquote>
265
260
  ) : node.type === 'separator' ? (
266
261
  <Separator bleed emphasis={node.emphasis} />
@@ -5,29 +5,24 @@ import ArrowRightIcon from '../../icons/ArrowRightIcon.astro';
5
5
  import ArrowUpIcon from '../../icons/ArrowUpIcon.astro';
6
6
  import BulletIcon from '../../icons/BulletIcon.astro';
7
7
  import EqualIcon from '../../icons/EqualIcon.astro';
8
- import FieldIcon from '../../icons/FieldIcon.astro';
9
- import GrassIcon from '../../icons/GrassIcon.astro';
10
- import MountainIcon from '../../icons/MountainIcon.astro';
11
- import WaterIcon from '../../icons/WaterIcon.astro';
12
- import type { MarkdownSymbol } from './contracts';
8
+ import type { MarkdownBuiltinSymbolName, MarkdownSymbol } from './contracts';
13
9
 
14
10
  type Props = {
15
11
  node: MarkdownSymbol;
16
12
  };
17
13
 
18
14
  const { node }: Props = Astro.props;
19
- const Icon = {
15
+ const icons = {
20
16
  adjustment: ArrowUpDownIcon,
21
17
  'arrow-right': ArrowRightIcon,
22
18
  bullet: BulletIcon,
23
19
  decrease: ArrowDownIcon,
24
20
  increase: ArrowUpIcon,
25
21
  neutral: EqualIcon,
26
- 'terrain-field': FieldIcon,
27
- 'terrain-grass': GrassIcon,
28
- 'terrain-mountain': MountainIcon,
29
- 'terrain-water': WaterIcon,
30
- }[node.name];
22
+ } satisfies Record<MarkdownBuiltinSymbolName, typeof ArrowUpDownIcon>;
23
+ const Icon = node.media ? null : icons[node.name as MarkdownBuiltinSymbolName];
24
+ if (!node.media && !Icon)
25
+ throw new Error(`Unknown Markdown symbol: ${node.name}`);
31
26
  ---
32
27
 
33
28
  <span
@@ -36,12 +31,28 @@ const Icon = {
36
31
  data-tone={node.tone}
37
32
  lang={node.lang}
38
33
  >
39
- <Icon class="markdown-content__symbol-icon" />
40
- {node.label ? (
41
- <span class="markdown-content__symbol-label">{node.label}</span>
42
- ) : node.accessibleLabel ? (
43
- <span class="markdown-content__visually-hidden">
44
- {node.accessibleLabel}
45
- </span>
46
- ) : null}
34
+ {
35
+ node.media ? (
36
+ <img
37
+ class="markdown-content__symbol-media"
38
+ src={node.media.src}
39
+ alt={node.media.alt}
40
+ width={node.media.width}
41
+ height={node.media.height}
42
+ loading={node.media.loading ?? 'lazy'}
43
+ decoding={node.media.decoding ?? 'async'}
44
+ />
45
+ ) : Icon ? (
46
+ <Icon class="markdown-content__symbol-icon" />
47
+ ) : null
48
+ }
49
+ {
50
+ node.label ? (
51
+ <span class="markdown-content__symbol-label">{node.label}</span>
52
+ ) : node.accessibleLabel ? (
53
+ <span class="markdown-content__visually-hidden">
54
+ {node.accessibleLabel}
55
+ </span>
56
+ ) : null
57
+ }
47
58
  </span>
@@ -7,27 +7,27 @@ export type MarkdownText = {
7
7
  value: string;
8
8
  };
9
9
 
10
- export type MarkdownSymbolName =
11
- | 'adjustment'
12
- | 'arrow-right'
13
- | 'bullet'
14
- | 'decrease'
15
- | 'increase'
16
- | 'neutral'
17
- | 'terrain-field'
18
- | 'terrain-grass'
19
- | 'terrain-mountain'
20
- | 'terrain-water';
21
-
22
- export type MarkdownSymbol = {
10
+ export type MarkdownBuiltinSymbolName =
11
+ 'adjustment' | 'arrow-right' | 'bullet' | 'decrease' | 'increase' | 'neutral';
12
+
13
+ type MarkdownSymbolBase = {
23
14
  type: 'symbol';
24
15
  accessibleLabel?: string;
25
16
  lang?: string;
26
17
  label?: string;
27
- name: MarkdownSymbolName;
28
18
  tone?: Tone;
29
19
  };
30
20
 
21
+ export type MarkdownSymbol =
22
+ | (MarkdownSymbolBase & {
23
+ media?: never;
24
+ name: MarkdownBuiltinSymbolName;
25
+ })
26
+ | (MarkdownSymbolBase & {
27
+ media: DisplayMedia;
28
+ name: string;
29
+ });
30
+
31
31
  export type MarkdownStrong = {
32
32
  type: 'strong';
33
33
  children: readonly MarkdownInline[];
@@ -91,7 +91,7 @@ export type MarkdownListMarker = {
91
91
  lang?: string;
92
92
  label?: string;
93
93
  symbol: Extract<
94
- MarkdownSymbolName,
94
+ MarkdownBuiltinSymbolName,
95
95
  | 'adjustment'
96
96
  | 'arrow-right'
97
97
  | 'bullet'