@astronox/core 0.2.4 → 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 (112) 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/CardAction/CardAction.css +5 -10
  20. package/src/components/actions/IdentityProviderAction/IdentityProviderAction.astro +6 -8
  21. package/src/components/actions/MediaTrigger/MediaTrigger.astro +1 -1
  22. package/src/components/actions/ScrollToTop/ScrollToTop.astro +2 -1
  23. package/src/components/actions/ScrollToTop/ScrollToTop.css +2 -6
  24. package/src/components/actions/ThemeToggle/ThemeToggle.astro +3 -3
  25. package/src/components/actions/ThemeToggle/themeToggleController.ts +2 -3
  26. package/src/components/content/CardFooter/CardFooter.css +2 -4
  27. package/src/components/content/CardHeader/CardHeader.astro +1 -4
  28. package/src/components/content/CardLink/CardLink.astro +3 -15
  29. package/src/components/content/CardLink/CardLink.css +4 -15
  30. package/src/components/content/CodeBlock/CodeBlock.astro +7 -6
  31. package/src/components/content/CodeBlock/CodeBlock.css +2 -5
  32. package/src/components/content/CodeBlock/codeBlockController.ts +1 -2
  33. package/src/components/content/MarkdownContent/MarkdownContent.astro +1 -3
  34. package/src/components/content/MarkdownContent/MarkdownContent.css +7 -2
  35. package/src/components/content/MarkdownContent/MarkdownInline.astro +11 -3
  36. package/src/components/content/MarkdownContent/MarkdownNode.astro +28 -33
  37. package/src/components/content/MarkdownContent/MarkdownSymbol.astro +30 -19
  38. package/src/components/content/MarkdownContent/contracts.ts +15 -15
  39. package/src/components/content/MarkdownContent/markdownContentRenderer.ts +27 -83
  40. package/src/components/feedback/PreferenceBootstrap/PreferenceBootstrap.astro +19 -22
  41. package/src/components/feedback/Spinner/Spinner.astro +1 -2
  42. package/src/components/feedback/Tooltip/tooltipController.ts +1 -4
  43. package/src/components/forms/ChoiceGroup/ChoiceGroup.css +1 -4
  44. package/src/components/forms/DateTimeInput/DateTimeInput.astro +2 -1
  45. package/src/components/forms/PreparedFormBootstrap/PreparedFormBootstrap.astro +10 -8
  46. package/src/components/forms/PreparedFormBootstrap/parsePreparedFormState.ts +48 -0
  47. package/src/components/forms/Textarea/Textarea.astro +2 -2
  48. package/src/components/icons/BurnIcon.astro +2 -1
  49. package/src/components/icons/FieldIcon.astro +10 -4
  50. package/src/components/icons/GrassIcon.astro +3 -2
  51. package/src/components/icons/SlowIcon.astro +1 -2
  52. package/src/components/icons/StunIcon.astro +2 -1
  53. package/src/components/icons/WaterIcon.astro +3 -2
  54. package/src/components/index.ts +2 -0
  55. package/src/components/media/CanvasFrame/CanvasFrame.astro +1 -2
  56. package/src/components/media/CanvasFrame/canvasFrameController.ts +1 -4
  57. package/src/components/media/EmbedFrame/EmbedFrame.astro +5 -12
  58. package/src/components/media/EmbedFrame/embedFrameController.ts +4 -4
  59. package/src/components/monetization/AdSlot/AdSlot.astro +33 -0
  60. package/src/components/monetization/AdSlot/AdSlot.css +53 -0
  61. package/src/components/navigation/DesktopNavigation.css +1 -2
  62. package/src/components/navigation/Footer/Footer.css +3 -9
  63. package/src/components/navigation/MobileNavigation.css +3 -12
  64. package/src/components/navigation/navigationTypes.ts +1 -4
  65. package/src/components/seo/SeoHead.astro +1 -6
  66. package/src/composition/SurfaceGroup/SurfaceGroup.css +4 -14
  67. package/src/config/environment.ts +52 -0
  68. package/src/config/index.ts +3 -0
  69. package/src/config/integrations.ts +9 -0
  70. package/src/config/siteConfig.ts +5 -0
  71. package/src/config/storage.ts +16 -0
  72. package/src/content/admin.ts +354 -0
  73. package/src/content/cache.ts +177 -0
  74. package/src/content/catalog.ts +72 -0
  75. package/src/content/contracts.ts +77 -0
  76. package/src/content/drafts.ts +49 -0
  77. package/src/content/index.ts +7 -0
  78. package/src/content/markdown.ts +146 -0
  79. package/src/content/snapshot.ts +139 -0
  80. package/src/data/firestore/client/contracts.ts +10 -2
  81. package/src/data/index.ts +1 -0
  82. package/src/data/memoizedLoader.ts +13 -0
  83. package/src/display/contracts.ts +1 -5
  84. package/src/i18n/config.ts +2 -8
  85. package/src/i18n/coreMessages.ts +2 -9
  86. package/src/i18n/preference.ts +2 -6
  87. package/src/patterns/Combobox/Combobox.astro +9 -12
  88. package/src/patterns/Combobox/comboboxController.ts +14 -8
  89. package/src/patterns/Fullscreen/FullscreenControl.astro +5 -1
  90. package/src/patterns/Fullscreen/fullscreenController.ts +1 -3
  91. package/src/patterns/MapViewport/geometry.ts +1 -4
  92. package/src/patterns/Modal/Modal.css +1 -4
  93. package/src/patterns/Modal/ModalOpen.astro +5 -1
  94. package/src/patterns/Table/Table.astro +10 -10
  95. package/src/patterns/Table/TableBody.astro +1 -4
  96. package/src/patterns/Table/TableBody.css +2 -4
  97. package/src/patterns/Table/TablePagination.astro +23 -30
  98. package/src/patterns/Table/TableSkeleton.astro +1 -5
  99. package/src/patterns/Table/tableClientController.ts +2 -2
  100. package/src/patterns/Table/tableFilterController.ts +1 -3
  101. package/src/patterns/Table/tableHeaderOverflowController.ts +1 -4
  102. package/src/patterns/Table/tableProgressiveController.ts +62 -60
  103. package/src/patterns/Table/tableScrollController.ts +3 -2
  104. package/src/primitives/text/TextBlock/TextBlock.astro +1 -3
  105. package/src/publication/contracts.ts +11 -0
  106. package/src/publication/http.ts +31 -0
  107. package/src/publication/index.ts +2 -0
  108. package/src/routing/localized.ts +9 -12
  109. package/src/seo/localizedMeta.ts +2 -6
  110. package/src/styles/interaction.css +7 -7
  111. package/src/styles/style.css +11 -20
  112. package/src/theme/contracts.ts +1 -6
@@ -3,7 +3,8 @@ import type { HTMLAttributes } from 'astro/types';
3
3
  import type { Tone } from '../../../theme/contracts';
4
4
  import TextInput from '../TextInput/TextInput.astro';
5
5
 
6
- type Props = Omit<HTMLAttributes<'input'>, 'type'> & {
6
+ type Props = Omit<HTMLAttributes<'input'>, 'autofocus' | 'type'> & {
7
+ autofocus?: boolean | 'desktop';
7
8
  invalid?: boolean;
8
9
  tone?: Tone;
9
10
  };
@@ -34,8 +34,9 @@ const { formId, stateKey }: Props = Astro.props;
34
34
  return item ? [item] : [];
35
35
  };
36
36
  const getComboboxRoot = (form, key) =>
37
- [...form.querySelectorAll('[data-prepared-form-combobox]')]
38
- .find((candidate) => candidate.dataset.preparedFormCombobox === key);
37
+ [...form.querySelectorAll('[data-prepared-form-combobox]')].find(
38
+ (candidate) => candidate.dataset.preparedFormCombobox === key,
39
+ );
39
40
  const isComboboxReady = (root) =>
40
41
  root instanceof HTMLElement &&
41
42
  root.querySelector('[data-combobox-options]') instanceof HTMLElement &&
@@ -110,7 +111,10 @@ const { formId, stateKey }: Props = Astro.props;
110
111
  button.type = 'button';
111
112
  button.role = 'option';
112
113
  button.tabIndex = -1;
113
- button.setAttribute('aria-selected', String(selected.has(option.value)));
114
+ button.setAttribute(
115
+ 'aria-selected',
116
+ String(selected.has(option.value)),
117
+ );
114
118
  button.dataset.comboboxOption = option.value;
115
119
  button.dataset.comboboxSearchText = option.label;
116
120
  button.dataset.interactionAccent = 'start';
@@ -194,10 +198,8 @@ const { formId, stateKey }: Props = Astro.props;
194
198
  childList: true,
195
199
  subtree: true,
196
200
  });
197
- document.addEventListener(
198
- 'DOMContentLoaded',
199
- () => observer.disconnect(),
200
- { once: true },
201
- );
201
+ document.addEventListener('DOMContentLoaded', () => observer.disconnect(), {
202
+ once: true,
203
+ });
202
204
  })();
203
205
  </script>
@@ -0,0 +1,48 @@
1
+ import type { PreparedFormState } from './contracts';
2
+
3
+ export function parsePreparedFormState(
4
+ value: unknown,
5
+ ): PreparedFormState | null {
6
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
7
+ const state = value as PreparedFormState;
8
+
9
+ for (const record of [state.values, state.choices]) {
10
+ if (
11
+ record !== undefined &&
12
+ (!record ||
13
+ typeof record !== 'object' ||
14
+ Array.isArray(record) ||
15
+ Object.values(record).some((item) => typeof item !== 'string'))
16
+ ) {
17
+ return null;
18
+ }
19
+ }
20
+
21
+ if (state.comboboxes === undefined) return state;
22
+ if (
23
+ !state.comboboxes ||
24
+ typeof state.comboboxes !== 'object' ||
25
+ Array.isArray(state.comboboxes)
26
+ ) {
27
+ return null;
28
+ }
29
+ for (const combobox of Object.values(state.comboboxes)) {
30
+ if (
31
+ !combobox ||
32
+ typeof combobox !== 'object' ||
33
+ !Array.isArray(combobox.options) ||
34
+ !Array.isArray(combobox.values) ||
35
+ combobox.values.some((item) => typeof item !== 'string') ||
36
+ combobox.options.some(
37
+ (option) =>
38
+ !option ||
39
+ typeof option !== 'object' ||
40
+ typeof option.label !== 'string' ||
41
+ typeof option.value !== 'string',
42
+ )
43
+ ) {
44
+ return null;
45
+ }
46
+ }
47
+ return state;
48
+ }
@@ -31,5 +31,5 @@ const resolvedTone = tone ?? dataTone;
31
31
  data-invalid={resolvedInvalid ? true : undefined}
32
32
  data-size={size}
33
33
  data-tone={resolvedTone}
34
- aria-invalid={resolvedInvalid ? 'true' : ariaInvalid}
35
- >{value}</textarea>
34
+ aria-invalid={resolvedInvalid ? 'true' : ariaInvalid}>{value}</textarea
35
+ >
@@ -20,5 +20,6 @@ const {
20
20
  <circle class="circle-symbol-icon__surface" cx="16" cy="16" r="13"></circle>
21
21
  <path
22
22
  class="circle-symbol-icon__fill"
23
- d="M17 7c1 5-3 6-3 10 0 1.5.8 2.7 2 3.4-.3-2.4 1.3-3.4 2.2-5.4 2.3 2 3.8 4.1 3.8 6.4 0 3.2-2.7 5.6-6 5.6s-6-2.4-6-5.8C10 15.7 15.7 13 17 7Z"></path>
23
+ d="M17 7c1 5-3 6-3 10 0 1.5.8 2.7 2 3.4-.3-2.4 1.3-3.4 2.2-5.4 2.3 2 3.8 4.1 3.8 6.4 0 3.2-2.7 5.6-6 5.6s-6-2.4-6-5.8C10 15.7 15.7 13 17 7Z"
24
+ ></path>
24
25
  </svg>
@@ -21,8 +21,14 @@ const {
21
21
  focusable={focusable}
22
22
  >
23
23
  <circle class="circle-symbol-icon__surface" cx="16" cy="16" r="13"></circle>
24
- <rect class="circle-symbol-icon__mark" x="9" y="9" width="14" height="14" rx="1"
25
- ></rect>
26
- <path class="circle-symbol-icon__mark" d="M13.7 9v14m4.6-14v14M9 13.7h14M9 18.3h14"
27
- ></path>
24
+ <rect
25
+ class="circle-symbol-icon__mark"
26
+ x="9"
27
+ y="9"
28
+ width="14"
29
+ height="14"
30
+ rx="1"></rect>
31
+ <path
32
+ class="circle-symbol-icon__mark"
33
+ d="M13.7 9v14m4.6-14v14M9 13.7h14M9 18.3h14"></path>
28
34
  </svg>
@@ -24,6 +24,7 @@ const {
24
24
  <path
25
25
  class="circle-symbol-icon__fill"
26
26
  d="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"></path>
27
- <path class="circle-symbol-icon__highlight" d="M10.4 23.5c2.2-5.3 5.4-8.7 10.2-11.6"
28
- ></path>
27
+ <path
28
+ class="circle-symbol-icon__highlight"
29
+ d="M10.4 23.5c2.2-5.3 5.4-8.7 10.2-11.6"></path>
29
30
  </svg>
@@ -20,6 +20,5 @@ const {
20
20
  <circle class="circle-symbol-icon__surface" cx="16" cy="16" r="13"></circle>
21
21
  <path
22
22
  class="circle-symbol-icon__mark"
23
- d="M16 8v16M9 12l14 8M9 20l14-8M13 10l3 3 3-3M13 22l3-3 3 3"
24
- ></path>
23
+ d="M16 8v16M9 12l14 8M9 20l14-8M13 10l3 3 3-3M13 22l3-3 3 3"></path>
25
24
  </svg>
@@ -18,5 +18,6 @@ const {
18
18
  focusable={focusable}
19
19
  >
20
20
  <circle class="circle-symbol-icon__surface" cx="16" cy="16" r="13"></circle>
21
- <path class="circle-symbol-icon__fill" d="m18 7-8 11h5l-1 7 8-11h-5l1-7Z"></path>
21
+ <path class="circle-symbol-icon__fill" d="m18 7-8 11h5l-1 7 8-11h-5l1-7Z"
22
+ ></path>
22
23
  </svg>
@@ -25,6 +25,7 @@ const {
25
25
  class="circle-symbol-icon__fill"
26
26
  d="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"
27
27
  ></path>
28
- <path class="circle-symbol-icon__highlight" d="M12.2 19.2a4.5 4.5 0 0 0 4.5 4.1"
29
- ></path>
28
+ <path
29
+ class="circle-symbol-icon__highlight"
30
+ d="M12.2 19.2a4.5 4.5 0 0 0 4.5 4.1"></path>
30
31
  </svg>
@@ -30,6 +30,7 @@ export type {
30
30
  PreparedFormOption,
31
31
  PreparedFormState,
32
32
  } from './forms/PreparedFormBootstrap/contracts';
33
+ export { parsePreparedFormState } from './forms/PreparedFormBootstrap/parsePreparedFormState';
33
34
  export { default as TextInput } from './forms/TextInput/TextInput.astro';
34
35
  export { default as Textarea } from './forms/Textarea/Textarea.astro';
35
36
  export { default as Spinner } from './feedback/Spinner/Spinner.astro';
@@ -39,6 +40,7 @@ export { default as Tooltip } from './feedback/Tooltip/Tooltip.astro';
39
40
  export { default as CanvasFrame } from './media/CanvasFrame/CanvasFrame.astro';
40
41
  export { default as EmbedFrame } from './media/EmbedFrame/EmbedFrame.astro';
41
42
  export { default as MediaFrame } from './media/MediaFrame/MediaFrame.astro';
43
+ export { default as AdSlot } from './monetization/AdSlot/AdSlot.astro';
42
44
  export { default as Breadcrumbs } from './navigation/Breadcrumbs/Breadcrumbs.astro';
43
45
  export { default as DesktopNavigation } from './navigation/DesktopNavigation.astro';
44
46
  export { default as Header } from './navigation/Header.astro';
@@ -87,8 +87,7 @@ const resolvedCanvasId = canvasId ?? `${frameId}-canvas`;
87
87
  class="canvas-frame__feedback"
88
88
  data-canvas-frame-feedback
89
89
  role="alert"
90
- hidden
91
- ></span>
90
+ hidden></span>
92
91
  </nox-canvas-frame>
93
92
 
94
93
  <script>
@@ -53,10 +53,7 @@ export function markCanvasFrameReady(root: HTMLElement): void {
53
53
  root.dataset.canvasFrameReady = 'true';
54
54
  }
55
55
 
56
- export function markCanvasFrameError(
57
- root: HTMLElement,
58
- message: string,
59
- ): void {
56
+ export function markCanvasFrameError(root: HTMLElement, message: string): void {
60
57
  const loading = root.querySelector<HTMLElement>(
61
58
  '[data-canvas-frame-loading]',
62
59
  );
@@ -63,11 +63,9 @@ const resolvedTone = tone ?? dataTone;
63
63
  data-fullscreen-region={fullscreen ? true : undefined}
64
64
  data-widget="embed-frame"
65
65
  data-tone={resolvedTone}
66
- aria-busy={
67
- activation === 'immediate' && loadingIndicator === 'spinner'
68
- ? 'true'
69
- : 'false'
70
- }
66
+ aria-busy={activation === 'immediate' && loadingIndicator === 'spinner'
67
+ ? 'true'
68
+ : 'false'}
71
69
  >
72
70
  <iframe
73
71
  {...attrs}
@@ -78,16 +76,11 @@ const resolvedTone = tone ?? dataTone;
78
76
  width={width}
79
77
  height={height}
80
78
  loading={loading}
81
- allowfullscreen
82
- ></iframe>
79
+ allowfullscreen></iframe>
83
80
  {
84
81
  activation === 'manual' && activationLabel ? (
85
82
  <span class="embed-frame__activation" data-embed-frame-activation-layer>
86
- <Button
87
- type="button"
88
- prominent
89
- data-embed-frame-activate
90
- >
83
+ <Button type="button" prominent data-embed-frame-activate>
91
84
  {activationLabel}
92
85
  </Button>
93
86
  </span>
@@ -1,6 +1,8 @@
1
1
  const activateEvent = 'nox:embed-frame:activate';
2
2
 
3
- export function activateEmbedFrame(root: HTMLElement): HTMLIFrameElement | null {
3
+ export function activateEmbedFrame(
4
+ root: HTMLElement,
5
+ ): HTMLIFrameElement | null {
4
6
  const iframe = root.querySelector<HTMLIFrameElement>(
5
7
  '[data-embed-frame-content]',
6
8
  );
@@ -14,9 +16,7 @@ export const initEmbedFrame = (root: HTMLElement): (() => void) => {
14
16
  const iframe = root.querySelector<HTMLIFrameElement>(
15
17
  '[data-embed-frame-content]',
16
18
  );
17
- const loading = root.querySelector<HTMLElement>(
18
- '[data-embed-frame-loading]',
19
- );
19
+ const loading = root.querySelector<HTMLElement>('[data-embed-frame-loading]');
20
20
  const activationLayer = root.querySelector<HTMLElement>(
21
21
  '[data-embed-frame-activation-layer]',
22
22
  );
@@ -0,0 +1,33 @@
1
+ ---
2
+ import type { HTMLAttributes } from 'astro/types';
3
+ import type { AdSlotState } from '../../../ads/contracts';
4
+
5
+ type Props = Omit<HTMLAttributes<'aside'>, 'aria-label'> & {
6
+ label: string;
7
+ size?: 'compact' | 'standard' | 'tall';
8
+ state?: AdSlotState;
9
+ };
10
+
11
+ const {
12
+ class: className,
13
+ label,
14
+ size = 'standard',
15
+ state = 'idle',
16
+ ...attrs
17
+ }: Props = Astro.props;
18
+ ---
19
+
20
+ <aside
21
+ {...attrs}
22
+ class:list={['ad-slot', className]}
23
+ data-ad-size={size}
24
+ data-ad-state={state}
25
+ data-widget="ad-slot"
26
+ aria-label={label}
27
+ aria-busy={state === 'loading' ? 'true' : 'false'}
28
+ >
29
+ <div class="ad-slot__content"><slot /></div>
30
+ <div class="ad-slot__fallback">
31
+ <slot name="fallback"><div class="ad-slot__fallback-canvas"></div></slot>
32
+ </div>
33
+ </aside>
@@ -0,0 +1,53 @@
1
+ .ad-slot {
2
+ --ad-slot-min-block-size: 6rem;
3
+
4
+ display: grid;
5
+ inline-size: 100%;
6
+ min-block-size: var(--ad-slot-min-block-size);
7
+ overflow: hidden;
8
+ place-items: center;
9
+ }
10
+
11
+ .ad-slot[data-ad-size='compact'] {
12
+ --ad-slot-min-block-size: 4rem;
13
+ }
14
+
15
+ .ad-slot[data-ad-size='tall'] {
16
+ --ad-slot-min-block-size: 15rem;
17
+ }
18
+
19
+ .ad-slot__content,
20
+ .ad-slot__fallback {
21
+ grid-area: 1 / 1;
22
+ inline-size: 100%;
23
+ }
24
+
25
+ .ad-slot__fallback {
26
+ display: none;
27
+ block-size: 100%;
28
+ }
29
+
30
+ .ad-slot__fallback-canvas {
31
+ block-size: 100%;
32
+ min-block-size: var(--ad-slot-min-block-size);
33
+ background:
34
+ linear-gradient(
35
+ 135deg,
36
+ transparent 0 46%,
37
+ color-mix(in srgb, var(--nox-theme-border-base) 55%, transparent) 46% 54%,
38
+ transparent 54% 100%
39
+ ),
40
+ var(--nox-surface-muted);
41
+ border: 1px solid var(--nox-theme-border-base);
42
+ inline-size: 100%;
43
+ }
44
+
45
+ .ad-slot[data-ad-state='blocked'] .ad-slot__content,
46
+ .ad-slot[data-ad-state='unavailable'] .ad-slot__content {
47
+ display: none;
48
+ }
49
+
50
+ .ad-slot[data-ad-state='blocked'] .ad-slot__fallback,
51
+ .ad-slot[data-ad-state='unavailable'] .ad-slot__fallback {
52
+ display: block;
53
+ }
@@ -87,8 +87,7 @@
87
87
  min-width: 4.75rem;
88
88
  }
89
89
 
90
- .site-nav__action
91
- > .dropdown[data-dropdown-trigger-appearance='bar'] {
90
+ .site-nav__action > .dropdown[data-dropdown-trigger-appearance='bar'] {
92
91
  min-width: 0;
93
92
  block-size: 100%;
94
93
  border-radius: 0;
@@ -33,15 +33,11 @@
33
33
  column-gap: var(--nox-space-4);
34
34
  }
35
35
 
36
- .site-footer__content[data-footer-columns='2'][
37
- data-footer-column-separator='divider'
38
- ] {
36
+ .site-footer__content[data-footer-columns='2'][data-footer-column-separator='divider'] {
39
37
  column-gap: 0;
40
38
  }
41
39
 
42
- .site-footer__content[data-footer-columns='2'][
43
- data-footer-column-separator='divider'
44
- ]
40
+ .site-footer__content[data-footer-columns='2'][data-footer-column-separator='divider']
45
41
  > .site-footer__group:nth-child(even)::before {
46
42
  display: inline-grid;
47
43
  width: calc(
@@ -146,9 +142,7 @@
146
142
  grid-column: auto;
147
143
  }
148
144
 
149
- .site-footer__content[data-footer-columns='2'][
150
- data-footer-column-separator='divider'
151
- ]
145
+ .site-footer__content[data-footer-columns='2'][data-footer-column-separator='divider']
152
146
  > .site-footer__group:nth-child(even)::before {
153
147
  content: none;
154
148
  }
@@ -94,8 +94,7 @@
94
94
  border-radius: var(--nox-radius-md);
95
95
  }
96
96
 
97
- .site-nav__mobile-action
98
- > .dropdown[data-dropdown-trigger-appearance='bar'] {
97
+ .site-nav__mobile-action > .dropdown[data-dropdown-trigger-appearance='bar'] {
99
98
  min-width: 0;
100
99
  block-size: var(--nox-site-header-compact-height);
101
100
  border-radius: 0;
@@ -239,16 +238,8 @@ html[data-mobile-navigation-open] body {
239
238
  }
240
239
 
241
240
  .site-nav__mobile-footer-link[data-mobile-navigation-accent]:hover {
242
- border-color: color-mix(
243
- in srgb,
244
- var(--mobile-navigation-accent) 88%,
245
- #000
246
- );
247
- background: color-mix(
248
- in srgb,
249
- var(--mobile-navigation-accent) 88%,
250
- #000
251
- );
241
+ border-color: color-mix(in srgb, var(--mobile-navigation-accent) 88%, #000);
242
+ background: color-mix(in srgb, var(--mobile-navigation-accent) 88%, #000);
252
243
  color: #fff;
253
244
  }
254
245
  }
@@ -1,8 +1,5 @@
1
1
  import type { AstroComponent } from '../widgetTypes';
2
- import type {
3
- DisplayMedia,
4
- NavigationLinkItem,
5
- } from '../../display/contracts';
2
+ import type { DisplayMedia, NavigationLinkItem } from '../../display/contracts';
6
3
 
7
4
  export type MobileNavigationLabels = {
8
5
  closeLabel?: string;
@@ -72,12 +72,7 @@ const jsonLdNodes = [
72
72
  <title>{seo.documentTitle}</title>
73
73
  {
74
74
  site.seo.icons?.map((icon) => (
75
- <link
76
- rel="icon"
77
- href={icon.href}
78
- type={icon.type}
79
- sizes={icon.sizes}
80
- />
75
+ <link rel="icon" href={icon.href} type={icon.type} sizes={icon.sizes} />
81
76
  ))
82
77
  }
83
78
  {
@@ -34,9 +34,7 @@
34
34
  content: '';
35
35
  }
36
36
 
37
- .surface-group:has(> [data-sticky-surface]):not(
38
- [data-sticky-measured]
39
- )::before,
37
+ .surface-group:has(> [data-sticky-surface]):not([data-sticky-measured])::before,
40
38
  .surface-group[data-sticky-active]::before {
41
39
  visibility: visible;
42
40
  }
@@ -52,16 +50,10 @@
52
50
  [data-surface-sizing='content']
53
51
  ),
54
52
  .surface-group
55
- > [data-surface]:not([hidden]):not(
56
- [data-surface-sizing='content']
57
- ):has(
53
+ > [data-surface]:not([hidden]):not([data-surface-sizing='content']):has(
58
54
  ~ [data-surface]:not([hidden])[data-surface-sizing='content']
59
55
  ):not(
60
- :has(
61
- ~ [data-surface]:not([hidden]):not(
62
- [data-surface-sizing='content']
63
- )
64
- )
56
+ :has(~ [data-surface]:not([hidden]):not([data-surface-sizing='content']))
65
57
  ) {
66
58
  flex: 1 1 auto;
67
59
  min-height: 0;
@@ -73,9 +65,7 @@
73
65
  border-end-end-radius: 0;
74
66
  }
75
67
 
76
- .surface-group
77
- > [data-surface]:not([hidden])
78
- ~ [data-surface]:not([hidden]) {
68
+ .surface-group > [data-surface]:not([hidden]) ~ [data-surface]:not([hidden]) {
79
69
  border-block-start-width: 0;
80
70
  border-start-start-radius: 0;
81
71
  border-start-end-radius: 0;
@@ -0,0 +1,52 @@
1
+ export function readRequiredEnvironmentValue(
2
+ name: string,
3
+ value: string | undefined,
4
+ ): string {
5
+ const resolved = value?.trim();
6
+ if (!resolved) throw new Error(`Missing environment variable: ${name}`);
7
+ return resolved;
8
+ }
9
+
10
+ export function readOptionalEnvironmentValue(
11
+ value: string | undefined,
12
+ ): string | undefined {
13
+ const resolved = value?.trim();
14
+ return resolved || undefined;
15
+ }
16
+
17
+ export function readRequiredEnvironmentUrl(
18
+ name: string,
19
+ value: string | undefined,
20
+ ): string {
21
+ const resolved = readRequiredEnvironmentValue(name, value);
22
+
23
+ try {
24
+ const url = new URL(resolved);
25
+ if (url.protocol !== 'http:' && url.protocol !== 'https:')
26
+ throw new Error();
27
+ return url.toString();
28
+ } catch {
29
+ throw new Error(`Invalid environment URL: ${name}`);
30
+ }
31
+ }
32
+
33
+ export function readEnvironmentRecord(
34
+ name: string,
35
+ value: string | undefined,
36
+ ): Readonly<Record<string, string>> {
37
+ const resolved = readRequiredEnvironmentValue(name, value);
38
+
39
+ try {
40
+ const parsed: unknown = JSON.parse(resolved);
41
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
42
+ throw new Error();
43
+ }
44
+
45
+ const entries = Object.entries(parsed);
46
+ if (entries.some(([, entry]) => typeof entry !== 'string'))
47
+ throw new Error();
48
+ return Object.fromEntries(entries) as Readonly<Record<string, string>>;
49
+ } catch {
50
+ throw new Error(`Invalid environment record: ${name}`);
51
+ }
52
+ }
@@ -1,2 +1,5 @@
1
+ export * from './environment';
2
+ export * from './integrations';
1
3
  export * from './pageMeta';
2
4
  export * from './siteConfig';
5
+ export * from './storage';
@@ -0,0 +1,9 @@
1
+ export type IntegrationConfig = Readonly<
2
+ Record<string, Readonly<Record<string, boolean>>>
3
+ >;
4
+
5
+ export function defineIntegrationConfig<
6
+ const TConfig extends IntegrationConfig,
7
+ >(config: TConfig): TConfig {
8
+ return config;
9
+ }
@@ -13,6 +13,7 @@ export type SiteLocaleConfig = {
13
13
  };
14
14
 
15
15
  export type SiteIdentity = {
16
+ id: string;
16
17
  name: string;
17
18
  shortName?: string;
18
19
  logo?: {
@@ -68,6 +69,10 @@ export type SiteConfig = {
68
69
  export function defineSiteConfig<const TConfig extends SiteConfig>(
69
70
  config: TConfig,
70
71
  ): TConfig {
72
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(config.identity.id)) {
73
+ throw new Error(`Invalid site identity: ${config.identity.id}`);
74
+ }
75
+
71
76
  const localeIds = new Set<string>();
72
77
  const urlCodes = new Set<string>();
73
78
 
@@ -0,0 +1,16 @@
1
+ const storageSegmentPattern = /^[a-z0-9][a-z0-9-]*$/;
2
+
3
+ export function createSiteStorageKey(
4
+ siteId: string,
5
+ ...segments: readonly string[]
6
+ ): string {
7
+ const values = [siteId, ...segments];
8
+ if (
9
+ segments.length === 0 ||
10
+ values.some((segment) => !storageSegmentPattern.test(segment))
11
+ ) {
12
+ throw new Error('Invalid site storage key');
13
+ }
14
+
15
+ return values.join(':');
16
+ }