@astronox/core 0.2.2 → 0.2.4

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 (59) hide show
  1. package/package.json +1 -1
  2. package/src/admin/cache.ts +217 -0
  3. package/src/admin/index.ts +1 -0
  4. package/src/auth/firebase/client.ts +37 -7
  5. package/src/client/formSubmission.ts +177 -0
  6. package/src/client/index.ts +1 -0
  7. package/src/components/actions/Button/Button.css +1 -1
  8. package/src/components/actions/CardAction/CardAction.css +3 -3
  9. package/src/components/actions/ScrollToTop/ScrollToTop.css +11 -0
  10. package/src/components/actions/ThemeToggle/ThemeToggle.astro +59 -0
  11. package/src/components/actions/ThemeToggle/ThemeToggle.css +83 -0
  12. package/src/components/actions/ThemeToggle/themeToggleController.ts +111 -0
  13. package/src/components/content/CardBody/CardBody.css +6 -2
  14. package/src/components/content/CardFooter/CardFooter.astro +9 -0
  15. package/src/components/content/CardFooter/CardFooter.css +45 -3
  16. package/src/components/content/CardLink/CardLink.astro +12 -3
  17. package/src/components/content/CardLink/CardLink.css +32 -0
  18. package/src/components/content/MarkdownContent/MarkdownContent.css +14 -4
  19. package/src/components/content/MarkdownContent/MarkdownInline.astro +1 -1
  20. package/src/components/content/MarkdownContent/contracts.ts +1 -1
  21. package/src/components/content/MarkdownContent/markdownContentRenderer.ts +7 -4
  22. package/src/components/data-display/TextPair/TextPair.css +8 -2
  23. package/src/components/feedback/ThemeBootstrap/ThemeBootstrap.astro +31 -0
  24. package/src/components/forms/ChoiceGroup/ChoiceGroup.css +1 -1
  25. package/src/components/forms/PreparedFormBootstrap/PreparedFormBootstrap.astro +203 -0
  26. package/src/components/forms/PreparedFormBootstrap/contracts.ts +15 -0
  27. package/src/components/forms/Textarea/Textarea.astro +3 -0
  28. package/src/components/forms/Textarea/Textarea.css +8 -0
  29. package/src/components/icons/CircleSymbolIcon.css +3 -3
  30. package/src/components/index.ts +10 -0
  31. package/src/components/navigation/Breadcrumbs/Breadcrumbs.astro +5 -1
  32. package/src/components/navigation/Breadcrumbs/Breadcrumbs.css +12 -17
  33. package/src/components/navigation/DesktopNavigation.css +4 -1
  34. package/src/components/navigation/Footer/Footer.astro +102 -0
  35. package/src/components/navigation/Footer/Footer.css +155 -0
  36. package/src/components/navigation/Footer/contracts.ts +17 -0
  37. package/src/components/navigation/Header.astro +20 -2
  38. package/src/components/navigation/MobileNavigation.astro +107 -43
  39. package/src/components/navigation/MobileNavigation.css +109 -24
  40. package/src/components/navigation/NavigationBrand.css +1 -1
  41. package/src/components/navigation/NavigationControl.css +16 -2
  42. package/src/components/navigation/NavigationItemContent.astro +6 -1
  43. package/src/components/navigation/mobileNavigationController.ts +0 -16
  44. package/src/components/navigation/navigationTypes.ts +9 -1
  45. package/src/composition/Grid/Grid.css +16 -0
  46. package/src/data/firestore/client/contracts.ts +23 -0
  47. package/src/data/firestore/client/firebase.ts +45 -1
  48. package/src/display/contracts.ts +5 -1
  49. package/src/layouts/BaseLayout.astro +13 -1
  50. package/src/patterns/Combobox/comboboxController.ts +69 -0
  51. package/src/patterns/MapViewport/MapViewport.css +3 -3
  52. package/src/patterns/Table/TableBody.css +3 -3
  53. package/src/patterns/Table/TableCell.css +5 -1
  54. package/src/styles/base.css +24 -7
  55. package/src/styles/style.css +2 -0
  56. package/src/styles/tokens.css +60 -15
  57. package/src/theme/contracts.ts +2 -0
  58. package/src/theme/index.ts +1 -0
  59. package/src/theme/storage.ts +1 -0
@@ -0,0 +1,111 @@
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 ??
15
+ defaultThemeStorageKey
16
+ );
17
+ }
18
+
19
+ function readStoredTheme(): ColorTheme | null {
20
+ try {
21
+ const value = window.localStorage.getItem(getStorageKey());
22
+ return isColorTheme(value) ? value : null;
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ function getSystemTheme(): ColorTheme {
29
+ return window.matchMedia(systemThemeQuery).matches ? 'dark' : 'light';
30
+ }
31
+
32
+ function getCurrentTheme(): ColorTheme {
33
+ const value = document.documentElement.dataset.theme ?? null;
34
+ return isColorTheme(value) ? value : readStoredTheme() ?? getSystemTheme();
35
+ }
36
+
37
+ function updateToggle(toggle: HTMLElement, theme: ColorTheme): void {
38
+ const label =
39
+ theme === 'light'
40
+ ? toggle.dataset.themeToggleDarkLabel
41
+ : toggle.dataset.themeToggleLightLabel;
42
+
43
+ if (label) {
44
+ toggle.setAttribute('aria-label', label);
45
+ toggle.setAttribute('title', label);
46
+ }
47
+ }
48
+
49
+ function updateToggles(theme: ColorTheme): void {
50
+ for (const toggle of document.querySelectorAll<HTMLElement>(
51
+ '[data-theme-toggle]',
52
+ )) {
53
+ updateToggle(toggle, theme);
54
+ }
55
+ }
56
+
57
+ function applyTheme(theme: ColorTheme, persist: boolean): void {
58
+ document.documentElement.dataset.theme = theme;
59
+
60
+ if (persist) {
61
+ try {
62
+ window.localStorage.setItem(getStorageKey(), theme);
63
+ } catch {
64
+ // The selected theme remains active for the current document.
65
+ }
66
+ }
67
+
68
+ updateToggles(theme);
69
+ document.dispatchEvent(
70
+ new CustomEvent('astronox:theme-change', { detail: { theme } }),
71
+ );
72
+ }
73
+
74
+ function initializeGlobalListeners(): void {
75
+ if (globalListenersInitialized) return;
76
+ globalListenersInitialized = true;
77
+
78
+ const media = window.matchMedia(systemThemeQuery);
79
+ media.addEventListener('change', (event) => {
80
+ if (readStoredTheme() === null) {
81
+ applyTheme(event.matches ? 'dark' : 'light', false);
82
+ }
83
+ });
84
+
85
+ window.addEventListener('storage', (event) => {
86
+ if (event.key !== getStorageKey()) return;
87
+
88
+ const theme = isColorTheme(event.newValue)
89
+ ? event.newValue
90
+ : getSystemTheme();
91
+ applyTheme(theme, false);
92
+ });
93
+ }
94
+
95
+ export function initThemeToggle(toggle: HTMLElement): () => void {
96
+ if (initializedToggles.has(toggle)) return () => {};
97
+ initializedToggles.add(toggle);
98
+ initializeGlobalListeners();
99
+ updateToggle(toggle, getCurrentTheme());
100
+
101
+ const handleClick = (): void => {
102
+ applyTheme(getCurrentTheme() === 'light' ? 'dark' : 'light', true);
103
+ };
104
+
105
+ toggle.addEventListener('click', handleClick);
106
+
107
+ return () => {
108
+ toggle.removeEventListener('click', handleClick);
109
+ initializedToggles.delete(toggle);
110
+ };
111
+ }
@@ -56,8 +56,7 @@
56
56
  }
57
57
 
58
58
  .card-body a {
59
- color: var(--nox-link);
60
- text-underline-offset: 0.2em;
59
+ color: var(--nox-content-link-color);
61
60
  }
62
61
 
63
62
  .card-body strong {
@@ -65,6 +64,11 @@
65
64
  font-weight: var(--nox-font-weight-strong);
66
65
  }
67
66
 
67
+ .card-body a strong {
68
+ color: inherit;
69
+ font-weight: var(--nox-content-link-emphasis-font-weight);
70
+ }
71
+
68
72
  @container (width < 32rem) {
69
73
  .card-body__content {
70
74
  --nox-content-padding: var(--nox-space-4);
@@ -18,6 +18,7 @@ const {
18
18
  }: Props = Astro.props;
19
19
  const resolvedTone = tone ?? dataTone;
20
20
  const hasAction = Astro.slots.has('action');
21
+ const hasLeadingAction = Astro.slots.has('leading-action');
21
22
  const hasMedia = Boolean(media);
22
23
  ---
23
24
 
@@ -28,10 +29,18 @@ const hasMedia = Boolean(media);
28
29
  data-surface
29
30
  data-surface-sizing="content"
30
31
  data-has-action={hasAction ? true : undefined}
32
+ data-has-leading-action={hasLeadingAction ? true : undefined}
31
33
  data-has-media={hasMedia ? true : undefined}
32
34
  data-tone={resolvedTone}
33
35
  >
34
36
  <div class="card-footer__layout">
37
+ {
38
+ hasLeadingAction ? (
39
+ <div class="card-footer__leading-action">
40
+ <slot name="leading-action" />
41
+ </div>
42
+ ) : null
43
+ }
35
44
  {
36
45
  media ? (
37
46
  <div class="card-footer__media" aria-hidden="true">
@@ -31,6 +31,10 @@
31
31
  grid-template-columns: minmax(0, 1fr) var(--card-footer-size);
32
32
  }
33
33
 
34
+ .card-footer[data-has-leading-action] .card-footer__layout {
35
+ grid-template-columns: var(--card-footer-size) minmax(0, 1fr);
36
+ }
37
+
34
38
  .card-footer[data-has-media][data-has-action] .card-footer__layout {
35
39
  grid-template-columns:
36
40
  var(--card-footer-size)
@@ -38,6 +42,31 @@
38
42
  var(--card-footer-size);
39
43
  }
40
44
 
45
+ .card-footer[data-has-leading-action][data-has-action]
46
+ .card-footer__layout {
47
+ grid-template-columns:
48
+ var(--card-footer-size)
49
+ minmax(0, 1fr)
50
+ var(--card-footer-size);
51
+ }
52
+
53
+ .card-footer[data-has-leading-action][data-has-media]
54
+ .card-footer__layout {
55
+ grid-template-columns:
56
+ var(--card-footer-size)
57
+ var(--card-footer-size)
58
+ minmax(0, 1fr);
59
+ }
60
+
61
+ .card-footer[data-has-leading-action][data-has-media][data-has-action]
62
+ .card-footer__layout {
63
+ grid-template-columns:
64
+ var(--card-footer-size)
65
+ var(--card-footer-size)
66
+ minmax(0, 1fr)
67
+ var(--card-footer-size);
68
+ }
69
+
41
70
  .card-footer__media {
42
71
  display: grid;
43
72
  min-width: 0;
@@ -64,29 +93,42 @@
64
93
  align-items: center;
65
94
  padding: var(--nox-space-3) var(--nox-space-5);
66
95
  font-size: var(--nox-font-size-sm);
67
- font-weight: var(--nox-font-weight-medium);
96
+ font-weight: var(--nox-font-weight-regular);
68
97
  line-height: var(--nox-line-height-tight);
69
98
  overflow-wrap: anywhere;
70
99
  }
71
100
 
72
101
  .card-footer__content a {
73
- color: var(--nox-link);
74
- text-underline-offset: 0.2em;
102
+ color: var(--nox-content-link-color);
103
+ font-weight: var(--nox-content-link-emphasis-font-weight);
75
104
  }
76
105
 
77
106
  .card-footer__content strong {
78
107
  font-weight: var(--nox-font-weight-strong);
79
108
  }
80
109
 
110
+ .card-footer__content a strong {
111
+ font-weight: var(--nox-content-link-emphasis-font-weight);
112
+ }
113
+
114
+ .card-footer__leading-action,
81
115
  .card-footer__action {
82
116
  display: grid;
83
117
  min-width: 0;
84
118
  min-height: var(--card-footer-size);
85
119
  place-items: stretch;
86
120
  overflow: hidden;
121
+ }
122
+
123
+ .card-footer__leading-action {
124
+ border-inline-end: var(--nox-border-width) solid var(--surface-border-color);
125
+ }
126
+
127
+ .card-footer__action {
87
128
  border-inline-start: var(--nox-border-width) solid var(--surface-border-color);
88
129
  }
89
130
 
131
+ .card-footer__leading-action > .nox-button[data-button-borderless],
90
132
  .card-footer__action > .nox-button[data-button-borderless] {
91
133
  --nox-button-background: transparent;
92
134
  }
@@ -20,6 +20,7 @@ const {
20
20
  compactContent,
21
21
  compactText,
22
22
  compactVisual = 'show',
23
+ density = 'default',
23
24
  disabled = false,
24
25
  direction,
25
26
  full = false,
@@ -32,11 +33,15 @@ const {
32
33
  mediaSize = 'default',
33
34
  meta,
34
35
  mode = 'link',
36
+ textLayout = 'inline',
37
+ textTreatment = 'default',
35
38
  tone,
36
39
  title,
37
40
  ...attrs
38
41
  }: Props = Astro.props;
39
- const accessibleLabel = `${label} : ${title}${meta !== undefined ? ` ${meta}` : ''}`;
42
+ const accessibleLabel = [label, title, meta]
43
+ .filter((value) => value !== undefined && value !== '')
44
+ .join(' ');
40
45
  const isDisabled = disabled || (mode === 'link' && !href);
41
46
  const isInteractive = mode === 'link' && !isDisabled;
42
47
  const Root = isInteractive ? 'a' : 'span';
@@ -65,6 +70,7 @@ const resolvedTone = tone ?? dataTone;
65
70
  data-collapse={collapse}
66
71
  data-compact-content={resolvedCompactContent}
67
72
  data-compact-visual={compactVisual}
73
+ data-density={density}
68
74
  data-disabled={isDisabled ? 'true' : 'false'}
69
75
  data-direction={direction}
70
76
  data-full={full ? 'true' : 'false'}
@@ -74,6 +80,7 @@ const resolvedTone = tone ?? dataTone;
74
80
  data-media-position={hasVisual ? mediaPosition : undefined}
75
81
  data-media-size={hasVisual ? mediaSize : undefined}
76
82
  data-mode={mode}
83
+ data-text-treatment={textTreatment}
77
84
  data-tone={resolvedTone}
78
85
  >
79
86
  <span class="card-link__layout">
@@ -109,12 +116,14 @@ const resolvedTone = tone ?? dataTone;
109
116
  <span class="card-link__compact-text" aria-hidden="true">
110
117
  {compactText}
111
118
  </span>
112
- <span class="card-link__label">{label}</span>
119
+ {label ? <span class="card-link__label">{label}</span> : null}
113
120
  <TextPair
114
121
  class="card-link__text"
115
- layout="inline"
122
+ layout={textLayout}
116
123
  primary={title}
124
+ primaryWeight={isInteractive ? 'medium' : 'strong'}
117
125
  secondary={meta}
126
+ secondaryWeight={textTreatment === 'heading' ? 'regular' : 'medium'}
118
127
  />
119
128
  </span>
120
129
  </span>
@@ -202,6 +202,20 @@
202
202
  gap: var(--nox-space-1);
203
203
  }
204
204
 
205
+ .card-link[data-density='relaxed'] .card-link__layout {
206
+ --card-link-size: calc(
207
+ var(--nox-navigation-block-size) + var(--nox-space-5)
208
+ );
209
+ }
210
+
211
+ .card-link[data-density='relaxed'] .card-link__content {
212
+ padding: var(--nox-space-5) var(--nox-space-6);
213
+ }
214
+
215
+ .card-link[data-density='relaxed'] .card-link__text {
216
+ gap: var(--nox-space-3);
217
+ }
218
+
205
219
  .card-link__compact-text {
206
220
  display: none;
207
221
  min-width: 0;
@@ -233,6 +247,14 @@
233
247
  align-items: center;
234
248
  }
235
249
 
250
+ .card-link[data-text-treatment='heading'] .card-link__text {
251
+ --text-pair-primary-font-size: var(--nox-font-size-lg);
252
+ --text-pair-primary-letter-spacing: var(--nox-letter-spacing-wide);
253
+ --text-pair-primary-text-transform: uppercase;
254
+ --text-pair-secondary-font-size: var(--nox-font-size-md);
255
+ line-height: var(--nox-line-height-tight);
256
+ }
257
+
236
258
  .card-link[data-interactive='true']:active {
237
259
  background: var(--card-link-active-background);
238
260
  }
@@ -287,6 +309,16 @@
287
309
  }
288
310
 
289
311
  @media (width < 48rem) {
312
+ .card-link[data-density='relaxed'] .card-link__layout {
313
+ --card-link-size: calc(
314
+ var(--nox-navigation-block-size) + var(--nox-space-4)
315
+ );
316
+ }
317
+
318
+ .card-link[data-density='relaxed'] .card-link__content {
319
+ padding: var(--nox-space-4) var(--nox-space-5);
320
+ }
321
+
290
322
  .card-link[data-compact-content] .card-link__layout {
291
323
  --card-link-size: var(--nox-navigation-compact-block-size);
292
324
  --card-link-media-size: 2.125rem;
@@ -171,7 +171,7 @@
171
171
 
172
172
  .markdown-content__resource-title {
173
173
  font-size: var(--nox-font-size-md);
174
- font-weight: var(--nox-font-weight-strong);
174
+ font-weight: var(--nox-font-weight-regular);
175
175
  line-height: var(--nox-line-height-tight);
176
176
  }
177
177
 
@@ -298,13 +298,23 @@
298
298
  }
299
299
 
300
300
  .markdown-content__link {
301
- color: var(--nox-tone-color);
301
+ color: var(--nox-content-link-color);
302
302
  overflow-wrap: anywhere;
303
303
  text-decoration: none;
304
304
  }
305
305
 
306
- .markdown-content__link[data-markdown-link-treatment='reference'] {
307
- font-weight: var(--nox-font-weight-strong);
306
+ .markdown-content__link[data-markdown-link-treatment='primary'] {
307
+ font-weight: var(--nox-content-link-emphasis-font-weight);
308
+ }
309
+
310
+ .markdown-content .markdown-content__strong {
311
+ font-weight: var(--nox-font-weight-medium);
312
+ }
313
+
314
+ @media (hover: hover) and (pointer: fine) {
315
+ .markdown-content__link:hover {
316
+ text-decoration-line: underline;
317
+ }
308
318
  }
309
319
 
310
320
  .markdown-content [data-tone] {
@@ -19,7 +19,7 @@ const { node }: Props = Astro.props;
19
19
  ) : node.type === 'symbol' ? (
20
20
  <MarkdownSymbol node={node} />
21
21
  ) : node.type === 'strong' ? (
22
- <strong>
22
+ <strong class="markdown-content__strong">
23
23
  {node.children.map((child) => <Astro.self node={child} />)}
24
24
  </strong>
25
25
  ) : node.type === 'emphasis' ? (
@@ -49,7 +49,7 @@ export type MarkdownLink = {
49
49
  external: boolean;
50
50
  href: string;
51
51
  lang?: string;
52
- treatment?: 'reference';
52
+ treatment?: 'primary';
53
53
  };
54
54
 
55
55
  export type MarkdownBreak = {
@@ -259,10 +259,13 @@ function renderInline(node: MarkdownInline): Node {
259
259
  return text;
260
260
  }
261
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
- );
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');
266
269
  appendInline(element, node.children);
267
270
  return element;
268
271
  }
@@ -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
  }
@@ -0,0 +1,31 @@
1
+ ---
2
+ import { defaultThemeStorageKey } from '../../../theme/storage';
3
+
4
+ type Props = {
5
+ storageKey?: string;
6
+ };
7
+
8
+ const { storageKey = defaultThemeStorageKey }: Props = Astro.props;
9
+ ---
10
+
11
+ <script is:inline define:vars={{ storageKey }}>
12
+ (() => {
13
+ const root = document.documentElement;
14
+ let storedTheme = null;
15
+
16
+ try {
17
+ const value = window.localStorage.getItem(storageKey);
18
+ if (value === 'light' || value === 'dark') storedTheme = value;
19
+ } catch {
20
+ // The system preference remains available without local storage.
21
+ }
22
+
23
+ const systemTheme = window.matchMedia?.('(prefers-color-scheme: dark)')
24
+ .matches
25
+ ? 'dark'
26
+ : 'light';
27
+
28
+ root.dataset.theme = storedTheme ?? systemTheme;
29
+ root.dataset.themeStorageKey = storageKey;
30
+ })();
31
+ </script>
@@ -81,7 +81,7 @@
81
81
  background: color-mix(
82
82
  in srgb,
83
83
  var(--nox-tone-seed) 12%,
84
- var(--nox-mix-light)
84
+ var(--nox-theme-surface-base)
85
85
  );
86
86
  color: var(--nox-tone-text);
87
87
  }