@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
@@ -1,7 +1,7 @@
1
1
  .breadcrumbs {
2
2
  min-width: 0;
3
3
  overflow: hidden;
4
- color: var(--nox-text-muted);
4
+ color: var(--nox-tone-color);
5
5
  font-size: var(--nox-font-size-sm);
6
6
  font-weight: var(--nox-font-weight-medium);
7
7
  line-height: 1.35;
@@ -25,7 +25,7 @@
25
25
  .breadcrumbs__item {
26
26
  display: inline-flex;
27
27
  flex: 0 0 auto;
28
- align-items: stretch;
28
+ align-items: center;
29
29
  min-width: 0;
30
30
  }
31
31
 
@@ -39,15 +39,13 @@
39
39
  align-items: center;
40
40
  }
41
41
 
42
- .breadcrumbs__item + .breadcrumbs__item::before {
43
- display: inline-flex;
44
- align-items: center;
42
+ .breadcrumbs__separator {
43
+ display: block;
44
+ width: 0.625rem;
45
+ height: 0.625rem;
46
+ flex: 0 0 auto;
45
47
  margin-inline: var(--nox-space-2);
46
- color: color-mix(in srgb, var(--nox-tone-color) 55%, var(--nox-text-subtle));
47
- content: '›';
48
- font-size: var(--nox-font-size-md);
49
- font-weight: var(--nox-font-weight-strong);
50
- line-height: 1;
48
+ color: var(--nox-tone-color);
51
49
  }
52
50
 
53
51
  .breadcrumbs a {
@@ -56,7 +54,8 @@
56
54
  align-items: center;
57
55
  align-self: stretch;
58
56
  border-radius: var(--nox-radius-sm);
59
- color: inherit;
57
+ color: var(--nox-tone-color);
58
+ font-weight: var(--nox-content-link-emphasis-font-weight);
60
59
  margin-inline: -0.1875rem;
61
60
  padding-inline: 0.1875rem;
62
61
  text-decoration: none;
@@ -71,19 +70,15 @@
71
70
 
72
71
  @media (hover: hover) and (pointer: fine) {
73
72
  .breadcrumbs a:hover {
74
- color: var(--nox-tone-text);
75
73
  text-decoration-line: underline;
76
- text-decoration-thickness: 0.125rem;
77
74
  }
78
75
  }
79
76
 
80
77
  .breadcrumbs a:focus-visible {
81
- color: var(--nox-tone-text);
82
- text-decoration-line: underline;
83
- text-decoration-thickness: 0.125rem;
78
+ color: var(--nox-tone-color);
84
79
  }
85
80
 
86
81
  .breadcrumbs [aria-current='page'] {
87
- color: var(--nox-tone-text);
82
+ color: var(--nox-tone-color);
88
83
  font-weight: var(--nox-font-weight-strong);
89
84
  }
@@ -0,0 +1,102 @@
1
+ ---
2
+ import type { HTMLAttributes } from 'astro/types';
3
+ import type { Tone } from '../../../theme/contracts';
4
+ import type { FooterGroup, FooterItem } from './contracts';
5
+ import ArrowRightIcon from '../../icons/ArrowRightIcon.astro';
6
+
7
+ type Props = HTMLAttributes<'footer'> & {
8
+ columnSeparator?: 'divider' | 'none';
9
+ columns?: 1 | 2;
10
+ groups: readonly FooterGroup[];
11
+ label: string;
12
+ tone?: Tone;
13
+ };
14
+
15
+ const {
16
+ class: className,
17
+ columnSeparator = 'none',
18
+ columns = 1,
19
+ 'data-tone': dataTone,
20
+ groups,
21
+ label,
22
+ tone,
23
+ ...attrs
24
+ }: Props = Astro.props;
25
+ const resolvedTone = tone ?? dataTone;
26
+ const visibleGroups = groups.filter(({ items }) => items.length > 0);
27
+ const resolveRel = (target: FooterItem['target'], rel?: FooterItem['rel']) =>
28
+ rel ?? (target === '_blank' ? 'noopener noreferrer' : undefined);
29
+ ---
30
+
31
+ <footer
32
+ {...attrs}
33
+ class:list={['site-footer', className]}
34
+ data-tone={resolvedTone}
35
+ data-widget="site-footer"
36
+ >
37
+ <nav
38
+ class="site-footer__content"
39
+ aria-label={label}
40
+ data-footer-columns={columns}
41
+ data-footer-column-separator={columnSeparator}
42
+ >
43
+ {
44
+ visibleGroups.map(({ items }) => (
45
+ <span class="site-footer__group">
46
+ {items.map(
47
+ (
48
+ {
49
+ href,
50
+ label: itemLabel,
51
+ marker,
52
+ prefix,
53
+ rel,
54
+ separator = 'divider',
55
+ target,
56
+ tone: itemTone,
57
+ },
58
+ index,
59
+ ) => (
60
+ <>
61
+ {index > 0 ? (
62
+ separator === 'divider' ? (
63
+ <span class="site-footer__separator" aria-hidden="true">
64
+ |
65
+ </span>
66
+ ) : (
67
+ <span class="site-footer__spacer" aria-hidden="true" />
68
+ )
69
+ ) : null}
70
+ <span class="site-footer__item">
71
+ {prefix ? (
72
+ <span class="site-footer__prefix">{prefix}</span>
73
+ ) : null}
74
+ {href ? (
75
+ <a
76
+ href={href}
77
+ rel={resolveRel(target, rel)}
78
+ target={target}
79
+ data-tone={itemTone}
80
+ >
81
+ {marker === 'arrow' ? (
82
+ <ArrowRightIcon class="site-footer__marker" />
83
+ ) : null}
84
+ <span>{itemLabel}</span>
85
+ </a>
86
+ ) : (
87
+ <span class="site-footer__label" data-tone={itemTone}>
88
+ {marker === 'arrow' ? (
89
+ <ArrowRightIcon class="site-footer__marker" />
90
+ ) : null}
91
+ <span>{itemLabel}</span>
92
+ </span>
93
+ )}
94
+ </span>
95
+ </>
96
+ ),
97
+ )}
98
+ </span>
99
+ ))
100
+ }
101
+ </nav>
102
+ </footer>
@@ -0,0 +1,155 @@
1
+ .site-footer {
2
+ color: var(--nox-text-muted);
3
+ }
4
+
5
+ .site-footer__content {
6
+ --site-footer-item-gap: var(--nox-space-2);
7
+ --site-footer-marker-gap: 0.0625rem;
8
+
9
+ display: grid;
10
+ width: var(--nox-page-width);
11
+ margin-inline: auto;
12
+ justify-items: center;
13
+ gap: var(--nox-space-1);
14
+ padding-block-end: calc(
15
+ var(--nox-section-compact-block-size) + var(--nox-space-7)
16
+ );
17
+ font-size: var(--nox-font-size-sm);
18
+ line-height: 1.25;
19
+ text-align: center;
20
+ }
21
+
22
+ .site-footer__group {
23
+ display: inline-flex;
24
+ min-width: 0;
25
+ align-items: center;
26
+ justify-content: center;
27
+ flex-wrap: wrap;
28
+ }
29
+
30
+ .site-footer__content[data-footer-columns='2'] {
31
+ grid-template-columns: repeat(2, max-content);
32
+ justify-content: center;
33
+ column-gap: var(--nox-space-4);
34
+ }
35
+
36
+ .site-footer__content[data-footer-columns='2'][
37
+ data-footer-column-separator='divider'
38
+ ] {
39
+ column-gap: 0;
40
+ }
41
+
42
+ .site-footer__content[data-footer-columns='2'][
43
+ data-footer-column-separator='divider'
44
+ ]
45
+ > .site-footer__group:nth-child(even)::before {
46
+ display: inline-grid;
47
+ width: calc(
48
+ var(--site-footer-item-gap) + var(--site-footer-item-gap) + 0.5em
49
+ );
50
+ flex: 0 0 auto;
51
+ place-items: center;
52
+ color: var(--nox-border);
53
+ content: '|';
54
+ line-height: 1;
55
+ }
56
+
57
+ .site-footer__content[data-footer-columns='2']
58
+ > .site-footer__group:last-child:nth-child(odd) {
59
+ grid-column: 1 / -1;
60
+ }
61
+
62
+ .site-footer__item {
63
+ display: inline-flex;
64
+ min-width: 0;
65
+ align-items: center;
66
+ gap: 0;
67
+ }
68
+
69
+ .site-footer__separator {
70
+ display: inline-grid;
71
+ width: calc(
72
+ var(--site-footer-item-gap) + var(--site-footer-item-gap) + 0.5em
73
+ );
74
+ flex: 0 0 auto;
75
+ place-items: center;
76
+ color: var(--nox-border);
77
+ line-height: 1;
78
+ }
79
+
80
+ .site-footer__spacer {
81
+ width: var(--site-footer-item-gap);
82
+ flex: 0 0 auto;
83
+ }
84
+
85
+ .site-footer__prefix + :is(a, .site-footer__label) {
86
+ margin-inline-start: var(--nox-space-1);
87
+ }
88
+
89
+ .site-footer a,
90
+ .site-footer__label {
91
+ display: inline-flex;
92
+ min-width: 0;
93
+ align-items: center;
94
+ gap: var(--site-footer-marker-gap);
95
+ }
96
+
97
+ .site-footer__marker {
98
+ display: block;
99
+ width: 0.625rem;
100
+ height: 0.625rem;
101
+ flex: 0 0 auto;
102
+ color: var(--nox-text-muted);
103
+ }
104
+
105
+ .site-footer a {
106
+ color: var(--nox-content-link-color);
107
+ font-weight: var(--nox-content-link-emphasis-font-weight);
108
+ text-decoration: none;
109
+ }
110
+
111
+ .site-footer a[data-tone],
112
+ .site-footer__label[data-tone] {
113
+ color: var(--nox-tone-color);
114
+ }
115
+
116
+ .site-footer a:focus-visible {
117
+ border-radius: var(--nox-radius-sm);
118
+ outline: var(--nox-focus-ring-width) solid var(--nox-tone-focus-ring);
119
+ outline-offset: var(--nox-focus-ring-offset);
120
+ color: var(--nox-content-link-color);
121
+ }
122
+
123
+ .site-footer a[data-tone]:focus-visible {
124
+ color: var(--nox-tone-color);
125
+ }
126
+
127
+ @media (hover: hover) and (pointer: fine) {
128
+ .site-footer a:hover {
129
+ color: var(--nox-content-link-color);
130
+ text-decoration-line: underline;
131
+ }
132
+
133
+ .site-footer a[data-tone]:hover {
134
+ color: var(--nox-tone-color);
135
+ }
136
+ }
137
+
138
+ @media (width < 48rem) {
139
+ .site-footer__content[data-footer-columns='2'] {
140
+ grid-template-columns: minmax(0, 1fr);
141
+ column-gap: 0;
142
+ }
143
+
144
+ .site-footer__content[data-footer-columns='2']
145
+ > .site-footer__group:last-child:nth-child(odd) {
146
+ grid-column: auto;
147
+ }
148
+
149
+ .site-footer__content[data-footer-columns='2'][
150
+ data-footer-column-separator='divider'
151
+ ]
152
+ > .site-footer__group:nth-child(even)::before {
153
+ content: none;
154
+ }
155
+ }
@@ -0,0 +1,17 @@
1
+ import type { HTMLAttributes } from 'astro/types';
2
+ import type { Tone } from '../../../theme/contracts';
3
+
4
+ export type FooterItem = {
5
+ href?: HTMLAttributes<'a'>['href'];
6
+ label: string;
7
+ marker?: 'arrow';
8
+ prefix?: string;
9
+ rel?: HTMLAttributes<'a'>['rel'];
10
+ separator?: 'divider' | 'none';
11
+ target?: HTMLAttributes<'a'>['target'];
12
+ tone?: Tone;
13
+ };
14
+
15
+ export type FooterGroup = {
16
+ items: readonly FooterItem[];
17
+ };
@@ -194,11 +194,6 @@
194
194
  row-gap: 0;
195
195
  }
196
196
 
197
- .sub-navigation__visual--media {
198
- inline-size: 100%;
199
- block-size: 100%;
200
- }
201
-
202
197
  .sub-navigation__label--with-visual {
203
198
  position: absolute;
204
199
  overflow: hidden;
@@ -115,6 +115,10 @@
115
115
  grid-template-columns: 1fr;
116
116
  }
117
117
 
118
+ .grid[data-layout='fixed'][data-collapse-at='sm'][data-last-row='fill'] > * {
119
+ grid-column: auto;
120
+ }
121
+
118
122
  .grid:is(
119
123
  [data-layout='fixed'],
120
124
  [data-layout='content-sidebar']
@@ -163,6 +167,10 @@
163
167
  grid-template-columns: 1fr;
164
168
  }
165
169
 
170
+ .grid[data-layout='fixed'][data-collapse-at='md'][data-last-row='fill'] > * {
171
+ grid-column: auto;
172
+ }
173
+
166
174
  .grid:is(
167
175
  [data-layout='fixed'],
168
176
  [data-layout='content-sidebar']
@@ -211,6 +219,10 @@
211
219
  grid-template-columns: 1fr;
212
220
  }
213
221
 
222
+ .grid[data-layout='fixed'][data-collapse-at='lg'][data-last-row='fill'] > * {
223
+ grid-column: auto;
224
+ }
225
+
214
226
  .grid:is(
215
227
  [data-layout='fixed'],
216
228
  [data-layout='content-sidebar']
@@ -259,6 +271,10 @@
259
271
  grid-template-columns: 1fr;
260
272
  }
261
273
 
274
+ .grid[data-layout='fixed'][data-collapse-at='xl'][data-last-row='fill'] > * {
275
+ grid-column: auto;
276
+ }
277
+
262
278
  .grid:is(
263
279
  [data-layout='fixed'],
264
280
  [data-layout='content-sidebar']
@@ -34,34 +34,48 @@
34
34
  content: '';
35
35
  }
36
36
 
37
+ .surface-group:has(> [data-sticky-surface]):not(
38
+ [data-sticky-measured]
39
+ )::before,
37
40
  .surface-group[data-sticky-active]::before {
38
41
  visibility: visible;
39
42
  }
40
43
 
41
- .surface-group > [data-surface] {
44
+ .surface-group > [data-surface]:not([hidden]) {
42
45
  flex: 0 0 auto;
43
46
  min-width: 0;
44
47
  }
45
48
 
46
49
  .surface-group
47
- > [data-surface]:not(
48
- :has(~ [data-surface]),
50
+ > [data-surface]:not([hidden]):not(
51
+ :has(~ [data-surface]:not([hidden])),
49
52
  [data-surface-sizing='content']
50
53
  ),
51
54
  .surface-group
52
- > [data-surface]:has(
53
- + [data-surface][data-surface-sizing='content']
55
+ > [data-surface]:not([hidden]):not(
56
+ [data-surface-sizing='content']
57
+ ):has(
58
+ ~ [data-surface]:not([hidden])[data-surface-sizing='content']
59
+ ):not(
60
+ :has(
61
+ ~ [data-surface]:not([hidden]):not(
62
+ [data-surface-sizing='content']
63
+ )
64
+ )
54
65
  ) {
55
66
  flex: 1 1 auto;
56
67
  min-height: 0;
57
68
  }
58
69
 
59
- .surface-group > [data-surface]:has(+ [data-surface]) {
70
+ .surface-group
71
+ > [data-surface]:not([hidden]):has(~ [data-surface]:not([hidden])) {
60
72
  border-end-start-radius: 0;
61
73
  border-end-end-radius: 0;
62
74
  }
63
75
 
64
- .surface-group > [data-surface] + [data-surface] {
76
+ .surface-group
77
+ > [data-surface]:not([hidden])
78
+ ~ [data-surface]:not([hidden]) {
65
79
  border-block-start-width: 0;
66
80
  border-start-start-radius: 0;
67
81
  border-start-end-radius: 0;
@@ -42,6 +42,7 @@ export const initStickySurfaceGroup = (root: HTMLElement): (() => void) => {
42
42
  const observe = () => {
43
43
  resizeFrame = 0;
44
44
  observer?.disconnect();
45
+ root.removeAttribute('data-sticky-measured');
45
46
 
46
47
  const stickyOffset =
47
48
  Number.parseFloat(window.getComputedStyle(stickySurface).top) || 0;
@@ -54,6 +55,7 @@ export const initStickySurfaceGroup = (root: HTMLElement): (() => void) => {
54
55
  'data-sticky-active',
55
56
  entry.boundingClientRect.top <= activationBoundary + 0.5,
56
57
  );
58
+ root.setAttribute('data-sticky-measured', 'true');
57
59
  },
58
60
  {
59
61
  root: scrollParent instanceof Window ? null : scrollParent,
@@ -85,5 +87,6 @@ export const initStickySurfaceGroup = (root: HTMLElement): (() => void) => {
85
87
 
86
88
  root.removeAttribute('data-sticky-active');
87
89
  root.removeAttribute('data-sticky-initialized');
90
+ root.removeAttribute('data-sticky-measured');
88
91
  };
89
92
  };
@@ -0,0 +1,78 @@
1
+ import type { Firestore } from 'firebase/firestore';
2
+
3
+ export type ClientFirestorePage<TItem> = {
4
+ items: readonly TItem[];
5
+ nextCursor?: string;
6
+ };
7
+
8
+ export type ClientFirestoreOrder = {
9
+ direction?: 'asc' | 'desc';
10
+ field: string;
11
+ };
12
+
13
+ export type ClientFirestoreCursor<TDocument> = {
14
+ serialize(input: { document: TDocument; id: string }): string;
15
+ toQueryValues(cursor: string): readonly unknown[];
16
+ };
17
+
18
+ export type ClientFirestoreDocumentMapper<
19
+ TItem,
20
+ TCreateInput,
21
+ TUpdateInput,
22
+ > = {
23
+ fromDocument(id: string, value: unknown): TItem;
24
+ toCreate(input: TCreateInput): Record<string, unknown>;
25
+ toUpdate(input: TUpdateInput): Record<string, unknown>;
26
+ };
27
+
28
+ export type ClientFirestoreSchema<TDocument> = {
29
+ parse(input: unknown): TDocument;
30
+ };
31
+
32
+ export type ClientFirestoreRepository<TItem, TCreateInput, TUpdateInput> = {
33
+ getIndex(input?: {
34
+ cursor?: string;
35
+ limit?: number;
36
+ }): Promise<ClientFirestorePage<TItem>>;
37
+ getDetail(input: { id: string }): Promise<TItem | null>;
38
+ create(input: TCreateInput): Promise<TItem>;
39
+ update(input: { id: string; value: TUpdateInput }): Promise<TItem>;
40
+ delete(input: { id: string }): Promise<void>;
41
+ };
42
+
43
+ export type ClientFirestoreRepositoryConfig<
44
+ TDocument,
45
+ TItem,
46
+ TCreateInput,
47
+ TUpdateInput,
48
+ > = {
49
+ collection: string;
50
+ cursor?: ClientFirestoreCursor<TDocument>;
51
+ database: Firestore;
52
+ mapper: ClientFirestoreDocumentMapper<TItem, TCreateInput, TUpdateInput>;
53
+ orderBy?: readonly ClientFirestoreOrder[];
54
+ schema: ClientFirestoreSchema<TDocument>;
55
+ };
56
+
57
+ export type ClientFirestoreBatchOperation =
58
+ | {
59
+ collection: string;
60
+ id?: string;
61
+ kind: 'create';
62
+ value: Record<string, unknown>;
63
+ }
64
+ | {
65
+ collection: string;
66
+ id: string;
67
+ kind: 'update';
68
+ value: Record<string, unknown>;
69
+ }
70
+ | {
71
+ collection: string;
72
+ id: string;
73
+ kind: 'delete';
74
+ };
75
+
76
+ export type ClientFirestoreBatchResult = {
77
+ createdIds: readonly string[];
78
+ };
@@ -0,0 +1,56 @@
1
+ import { getApps, initializeApp } from 'firebase/app';
2
+ import {
3
+ collection,
4
+ doc,
5
+ getFirestore,
6
+ serverTimestamp,
7
+ writeBatch,
8
+ } from 'firebase/firestore';
9
+
10
+ import type { FirebaseClientConfig } from '../../../auth/firebase/contracts';
11
+ import type {
12
+ ClientFirestoreBatchOperation,
13
+ ClientFirestoreBatchResult,
14
+ } from './contracts';
15
+
16
+ export function createFirebaseClientFirestore(config: FirebaseClientConfig) {
17
+ const app =
18
+ getApps().find((candidate) => candidate.name === config.projectId) ??
19
+ initializeApp(config, config.projectId);
20
+
21
+ return getFirestore(app);
22
+ }
23
+
24
+ export function createClientFirestoreServerTimestamp() {
25
+ return serverTimestamp();
26
+ }
27
+
28
+ export async function commitClientFirestoreBatch(
29
+ database: ReturnType<typeof createFirebaseClientFirestore>,
30
+ operations: readonly ClientFirestoreBatchOperation[],
31
+ ): Promise<ClientFirestoreBatchResult> {
32
+ const batch = writeBatch(database);
33
+ const createdIds: string[] = [];
34
+
35
+ for (const operation of operations) {
36
+ const documents = collection(database, operation.collection);
37
+ if (operation.kind === 'create') {
38
+ const reference = operation.id
39
+ ? doc(documents, operation.id)
40
+ : doc(documents);
41
+ batch.set(reference, operation.value);
42
+ createdIds.push(reference.id);
43
+ continue;
44
+ }
45
+
46
+ const reference = doc(documents, operation.id);
47
+ if (operation.kind === 'update') {
48
+ batch.update(reference, operation.value);
49
+ } else {
50
+ batch.delete(reference);
51
+ }
52
+ }
53
+
54
+ await batch.commit();
55
+ return { createdIds };
56
+ }
@@ -0,0 +1,3 @@
1
+ export * from './contracts';
2
+ export * from './firebase';
3
+ export * from './repository';