@astronox/core 0.2.1 → 0.2.2
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.
- package/package.json +7 -1
- package/src/admin/contracts.ts +28 -0
- package/src/admin/index.ts +2 -0
- package/src/admin/module.ts +28 -0
- package/src/auth/contracts.ts +53 -0
- package/src/auth/firebase/client.ts +53 -0
- package/src/auth/firebase/contracts.ts +8 -0
- package/src/auth/firebase/index.ts +1 -0
- package/src/auth/index.ts +2 -0
- package/src/auth/module.ts +99 -0
- package/src/client/index.ts +3 -0
- package/src/client/preferences.ts +400 -0
- package/src/components/actions/Button/Button.astro +42 -6
- package/src/components/actions/Button/Button.css +19 -0
- package/src/components/actions/IdentityProviderAction/IdentityProviderAction.astro +35 -0
- package/src/components/content/CardBody/CardBody.astro +3 -0
- package/src/components/content/CardBody/CardBody.css +10 -0
- package/src/components/content/MarkdownContent/markdownContentRenderer.ts +549 -0
- package/src/components/feedback/PreferenceBootstrap/PreferenceBootstrap.astro +403 -0
- package/src/components/forms/Checkbox/Checkbox.astro +6 -0
- package/src/components/forms/ChoiceGroup/ChoiceGroup.astro +57 -0
- package/src/components/forms/ChoiceGroup/ChoiceGroup.css +108 -0
- package/src/components/forms/ChoiceGroup/contracts.ts +9 -0
- package/src/components/forms/DateTimeInput/DateTimeInput.astro +14 -0
- package/src/components/forms/TextInput/TextInput.astro +7 -1
- package/src/components/forms/Textarea/Textarea.astro +32 -0
- package/src/components/forms/Textarea/Textarea.css +54 -0
- package/src/components/icons/BackIcon.astro +26 -0
- package/src/components/icons/DeleteIcon.astro +22 -0
- package/src/components/icons/StatusIndicatorIcon.astro +16 -0
- package/src/components/index.ts +10 -0
- package/src/components/media/CanvasFrame/CanvasFrame.astro +1 -17
- package/src/components/media/EmbedFrame/EmbedFrame.astro +1 -19
- package/src/components/media/EmbedFrame/EmbedFrame.css +0 -2
- package/src/components/navigation/SubNavigation/SubNavigation.css +0 -5
- package/src/composition/SurfaceGroup/SurfaceGroup.css +21 -7
- package/src/composition/SurfaceGroup/stickySurfaceGroupController.ts +3 -0
- package/src/data/firestore/client/contracts.ts +55 -0
- package/src/data/firestore/client/firebase.ts +12 -0
- package/src/data/firestore/client/index.ts +3 -0
- package/src/data/firestore/client/repository.ts +111 -0
- package/src/display/contracts.ts +40 -0
- package/src/display/index.ts +1 -0
- package/src/display/tableProgressive.ts +62 -0
- package/src/patterns/Combobox/Combobox.astro +23 -0
- package/src/patterns/Fullscreen/FullscreenControl.astro +4 -14
- package/src/patterns/Fullscreen/FullscreenControl.css +0 -26
- package/src/patterns/Modal/Modal.astro +1 -1
- package/src/patterns/Modal/Modal.css +38 -1
- package/src/patterns/Table/Table.astro +122 -7
- package/src/patterns/Table/TableBody.astro +8 -0
- package/src/patterns/Table/TableCell.css +6 -0
- package/src/patterns/Table/TableCellValue.astro +31 -0
- package/src/patterns/Table/TableFooter.astro +3 -1
- package/src/patterns/Table/TableHeader.astro +1 -0
- package/src/patterns/Table/TablePagination.astro +63 -47
- package/src/patterns/Table/TableSkeleton.astro +18 -22
- package/src/patterns/Table/TableSkeleton.css +30 -5
- package/src/patterns/Table/tableClientController.ts +253 -0
- package/src/patterns/Table/tableFilterController.ts +23 -31
- package/src/patterns/Table/tableProgressiveController.ts +615 -227
- package/src/patterns/Table/tableSortController.ts +25 -7
- package/src/styles/interaction.css +16 -0
- package/src/styles/style.css +2 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
.textarea {
|
|
2
|
+
display: block;
|
|
3
|
+
width: 100%;
|
|
4
|
+
min-width: 0;
|
|
5
|
+
min-height: 28rem;
|
|
6
|
+
resize: vertical;
|
|
7
|
+
padding: var(--nox-field-control-padding-inline);
|
|
8
|
+
border: var(--nox-border-width) solid var(--nox-field-border);
|
|
9
|
+
border-radius: var(--nox-radius-sm);
|
|
10
|
+
outline: 0;
|
|
11
|
+
background: var(--nox-field-background);
|
|
12
|
+
color: var(--nox-field-color);
|
|
13
|
+
font-family: inherit;
|
|
14
|
+
font-size: var(--nox-field-control-font-size);
|
|
15
|
+
font-weight: var(--nox-font-weight-regular);
|
|
16
|
+
letter-spacing: 0;
|
|
17
|
+
line-height: 1.5;
|
|
18
|
+
transition:
|
|
19
|
+
border-color var(--nox-motion-duration-sm) var(--nox-motion-easing-standard),
|
|
20
|
+
box-shadow var(--nox-motion-duration-sm) var(--nox-motion-easing-standard);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.textarea::placeholder {
|
|
24
|
+
color: var(--nox-field-placeholder);
|
|
25
|
+
opacity: 1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.textarea:focus {
|
|
29
|
+
border-color: var(--nox-field-border-active);
|
|
30
|
+
box-shadow: var(--nox-field-focus-shadow);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.textarea[data-invalid] {
|
|
34
|
+
border-color: var(--nox-field-border-invalid);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.textarea:disabled {
|
|
38
|
+
background: var(--nox-field-background-disabled);
|
|
39
|
+
color: var(--nox-text-disabled);
|
|
40
|
+
cursor: not-allowed;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@media (hover: hover) and (pointer: fine) {
|
|
44
|
+
.textarea:not(:disabled):not(:focus):not([data-invalid]):hover {
|
|
45
|
+
border-color: var(--nox-field-border-hover);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@media (width < 48rem), (hover: none) and (pointer: coarse) {
|
|
50
|
+
.textarea {
|
|
51
|
+
min-height: 20rem;
|
|
52
|
+
font-size: var(--nox-font-size-md);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from 'astro/types';
|
|
3
|
+
|
|
4
|
+
type Props = HTMLAttributes<'svg'>;
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
'aria-hidden': ariaHidden = 'true',
|
|
8
|
+
focusable = 'false',
|
|
9
|
+
...attrs
|
|
10
|
+
}: Props = Astro.props;
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
<svg
|
|
14
|
+
{...attrs}
|
|
15
|
+
viewBox="0 0 32 32"
|
|
16
|
+
fill="none"
|
|
17
|
+
aria-hidden={ariaHidden}
|
|
18
|
+
focusable={focusable}
|
|
19
|
+
>
|
|
20
|
+
<path
|
|
21
|
+
d="M13 8 6 15l7 7M7 15h10c5 0 8 3 8 8"
|
|
22
|
+
stroke="currentColor"
|
|
23
|
+
stroke-linecap="round"
|
|
24
|
+
stroke-linejoin="round"
|
|
25
|
+
stroke-width="2.4"></path>
|
|
26
|
+
</svg>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from 'astro/types';
|
|
3
|
+
|
|
4
|
+
type Props = HTMLAttributes<'svg'>;
|
|
5
|
+
|
|
6
|
+
const attrs: Props = Astro.props;
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<svg
|
|
10
|
+
{...attrs}
|
|
11
|
+
viewBox="0 0 32 32"
|
|
12
|
+
fill="none"
|
|
13
|
+
aria-hidden="true"
|
|
14
|
+
focusable="false"
|
|
15
|
+
>
|
|
16
|
+
<path
|
|
17
|
+
d="M9 10H23M13 10V7H19V10M11 10L12 25H20L21 10M14.5 14V21M17.5 14V21"
|
|
18
|
+
stroke="currentColor"
|
|
19
|
+
stroke-width="2.5"
|
|
20
|
+
stroke-linecap="round"
|
|
21
|
+
stroke-linejoin="round"></path>
|
|
22
|
+
</svg>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from 'astro/types';
|
|
3
|
+
|
|
4
|
+
type Props = HTMLAttributes<'svg'>;
|
|
5
|
+
const attrs: Props = Astro.props;
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<svg
|
|
9
|
+
{...attrs}
|
|
10
|
+
viewBox="0 0 32 32"
|
|
11
|
+
fill="none"
|
|
12
|
+
aria-hidden="true"
|
|
13
|
+
focusable="false"
|
|
14
|
+
>
|
|
15
|
+
<circle cx="16" cy="16" r="8" fill="currentColor"></circle>
|
|
16
|
+
</svg>
|
package/src/components/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { default as ActionListItem } from './actions/ActionListItem/ActionListItem.astro';
|
|
2
2
|
export { default as Button } from './actions/Button/Button.astro';
|
|
3
3
|
export { default as CardAction } from './actions/CardAction/CardAction.astro';
|
|
4
|
+
export { default as IdentityProviderAction } from './actions/IdentityProviderAction/IdentityProviderAction.astro';
|
|
4
5
|
export { default as MediaTrigger } from './actions/MediaTrigger/MediaTrigger.astro';
|
|
5
6
|
export { default as ScrollToTop } from './actions/ScrollToTop/ScrollToTop.astro';
|
|
6
7
|
export { default as CardBody } from './content/CardBody/CardBody.astro';
|
|
@@ -16,11 +17,16 @@ export { default as MetricTile } from './data-display/MetricTile/MetricTile.astr
|
|
|
16
17
|
export { default as Tag } from './data-display/Tag/Tag.astro';
|
|
17
18
|
export { default as TextPair } from './data-display/TextPair/TextPair.astro';
|
|
18
19
|
export { default as Checkbox } from './forms/Checkbox/Checkbox.astro';
|
|
20
|
+
export { default as ChoiceGroup } from './forms/ChoiceGroup/ChoiceGroup.astro';
|
|
21
|
+
export type { ChoiceOption } from './forms/ChoiceGroup/contracts';
|
|
22
|
+
export { default as DateTimeInput } from './forms/DateTimeInput/DateTimeInput.astro';
|
|
19
23
|
export { default as FieldError } from './forms/FieldError/FieldError.astro';
|
|
20
24
|
export { default as FieldGroup } from './forms/FieldGroup/FieldGroup.astro';
|
|
21
25
|
export { default as FieldLabel } from './forms/FieldLabel/FieldLabel.astro';
|
|
22
26
|
export { default as TextInput } from './forms/TextInput/TextInput.astro';
|
|
27
|
+
export { default as Textarea } from './forms/Textarea/Textarea.astro';
|
|
23
28
|
export { default as Spinner } from './feedback/Spinner/Spinner.astro';
|
|
29
|
+
export { default as PreferenceBootstrap } from './feedback/PreferenceBootstrap/PreferenceBootstrap.astro';
|
|
24
30
|
export { default as Tooltip } from './feedback/Tooltip/Tooltip.astro';
|
|
25
31
|
export { default as CanvasFrame } from './media/CanvasFrame/CanvasFrame.astro';
|
|
26
32
|
export { default as EmbedFrame } from './media/EmbedFrame/EmbedFrame.astro';
|
|
@@ -55,12 +61,15 @@ export { default as ArrowLeftIcon } from './icons/ArrowLeftIcon.astro';
|
|
|
55
61
|
export { default as ArrowRightIcon } from './icons/ArrowRightIcon.astro';
|
|
56
62
|
export { default as ArrowUpDownIcon } from './icons/ArrowUpDownIcon.astro';
|
|
57
63
|
export { default as ArrowUpIcon } from './icons/ArrowUpIcon.astro';
|
|
64
|
+
export { default as BackIcon } from './icons/BackIcon.astro';
|
|
58
65
|
export { default as BulletIcon } from './icons/BulletIcon.astro';
|
|
59
66
|
export { default as BurnIcon } from './icons/BurnIcon.astro';
|
|
67
|
+
export { default as CheckIcon } from './icons/CheckIcon.astro';
|
|
60
68
|
export { default as ClearIcon } from './icons/ClearIcon.astro';
|
|
61
69
|
export { default as CloseIcon } from './icons/CloseIcon.astro';
|
|
62
70
|
export { default as CopyIcon } from './icons/CopyIcon.astro';
|
|
63
71
|
export { default as DayIcon } from './icons/DayIcon.astro';
|
|
72
|
+
export { default as DeleteIcon } from './icons/DeleteIcon.astro';
|
|
64
73
|
export { default as DollarIcon } from './icons/DollarIcon.astro';
|
|
65
74
|
export { default as DownloadIcon } from './icons/DownloadIcon.astro';
|
|
66
75
|
export { default as EnterFullscreenIcon } from './icons/EnterFullscreenIcon.astro';
|
|
@@ -80,6 +89,7 @@ export { default as RibbonIcon } from './icons/RibbonIcon.astro';
|
|
|
80
89
|
export { default as SlashIcon } from './icons/SlashIcon.astro';
|
|
81
90
|
export { default as SlowIcon } from './icons/SlowIcon.astro';
|
|
82
91
|
export { default as StarIcon } from './icons/StarIcon.astro';
|
|
92
|
+
export { default as StatusIndicatorIcon } from './icons/StatusIndicatorIcon.astro';
|
|
83
93
|
export { default as StunIcon } from './icons/StunIcon.astro';
|
|
84
94
|
export { default as TableSortIcon } from './icons/TableSortIcon.astro';
|
|
85
95
|
export { default as WaterIcon } from './icons/WaterIcon.astro';
|
|
@@ -3,24 +3,18 @@ import type { HTMLAttributes } from 'astro/types';
|
|
|
3
3
|
import type { Tone } from '../../../theme/contracts';
|
|
4
4
|
import Button from '../../actions/Button/Button.astro';
|
|
5
5
|
import Spinner from '../../feedback/Spinner/Spinner.astro';
|
|
6
|
-
import FullscreenControl from '../../../patterns/Fullscreen/FullscreenControl.astro';
|
|
7
6
|
|
|
8
7
|
type CanvasAspectRatio = '16/9' | '4/3';
|
|
9
8
|
type CanvasActivation = 'immediate' | 'manual';
|
|
10
9
|
type CanvasWheelInput = 'canvas' | 'page';
|
|
11
10
|
|
|
12
|
-
type CanvasFullscreen = {
|
|
13
|
-
enterLabel: string;
|
|
14
|
-
exitLabel: string;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
11
|
type Props = HTMLAttributes<'div'> & {
|
|
18
12
|
activation?: CanvasActivation;
|
|
19
13
|
activationLabel?: string;
|
|
20
14
|
aspectRatio?: CanvasAspectRatio;
|
|
21
15
|
canvasId?: string;
|
|
22
16
|
frameId: string;
|
|
23
|
-
fullscreen?:
|
|
17
|
+
fullscreen?: boolean;
|
|
24
18
|
loadingLabel: string;
|
|
25
19
|
title: string;
|
|
26
20
|
tone?: Tone;
|
|
@@ -95,16 +89,6 @@ const resolvedCanvasId = canvasId ?? `${frameId}-canvas`;
|
|
|
95
89
|
role="alert"
|
|
96
90
|
hidden
|
|
97
91
|
></span>
|
|
98
|
-
{
|
|
99
|
-
fullscreen ? (
|
|
100
|
-
<FullscreenControl
|
|
101
|
-
targetId={frameId}
|
|
102
|
-
enterLabel={fullscreen.enterLabel}
|
|
103
|
-
exitLabel={fullscreen.exitLabel}
|
|
104
|
-
placement="overlay"
|
|
105
|
-
/>
|
|
106
|
-
) : null
|
|
107
|
-
}
|
|
108
92
|
</nox-canvas-frame>
|
|
109
93
|
|
|
110
94
|
<script>
|
|
@@ -3,19 +3,12 @@ import type { HTMLAttributes } from 'astro/types';
|
|
|
3
3
|
import type { Tone } from '../../../theme/contracts';
|
|
4
4
|
import Button from '../../actions/Button/Button.astro';
|
|
5
5
|
import Spinner from '../../feedback/Spinner/Spinner.astro';
|
|
6
|
-
import FullscreenControl from '../../../patterns/Fullscreen/FullscreenControl.astro';
|
|
7
6
|
|
|
8
7
|
type EmbedAspectRatio = '16/9' | '4/3';
|
|
9
8
|
type EmbedActivation = 'immediate' | 'manual';
|
|
10
9
|
type EmbedLoading = 'eager' | 'lazy';
|
|
11
10
|
type EmbedLoadingIndicator = 'none' | 'spinner';
|
|
12
11
|
|
|
13
|
-
type EmbedFullscreen = {
|
|
14
|
-
enterLabel: string;
|
|
15
|
-
exitLabel: string;
|
|
16
|
-
overlayOffset?: 'default' | 'native-controls';
|
|
17
|
-
};
|
|
18
|
-
|
|
19
12
|
type Props = Omit<
|
|
20
13
|
HTMLAttributes<'iframe'>,
|
|
21
14
|
'height' | 'loading' | 'src' | 'title' | 'width'
|
|
@@ -24,7 +17,7 @@ type Props = Omit<
|
|
|
24
17
|
activationLabel?: string;
|
|
25
18
|
aspectRatio?: EmbedAspectRatio;
|
|
26
19
|
frameId?: string;
|
|
27
|
-
fullscreen?:
|
|
20
|
+
fullscreen?: boolean;
|
|
28
21
|
loading?: EmbedLoading;
|
|
29
22
|
loadingIndicator?: EmbedLoadingIndicator;
|
|
30
23
|
loadingLabel: string;
|
|
@@ -111,17 +104,6 @@ const resolvedTone = tone ?? dataTone;
|
|
|
111
104
|
</span>
|
|
112
105
|
) : null
|
|
113
106
|
}
|
|
114
|
-
{
|
|
115
|
-
fullscreen ? (
|
|
116
|
-
<FullscreenControl
|
|
117
|
-
targetId={frameId ?? ''}
|
|
118
|
-
enterLabel={fullscreen.enterLabel}
|
|
119
|
-
exitLabel={fullscreen.exitLabel}
|
|
120
|
-
placement="overlay"
|
|
121
|
-
overlayOffset={fullscreen.overlayOffset}
|
|
122
|
-
/>
|
|
123
|
-
) : null
|
|
124
|
-
}
|
|
125
107
|
</nox-embed-frame>
|
|
126
108
|
|
|
127
109
|
<script>
|
|
@@ -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]:
|
|
53
|
-
|
|
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
|
|
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
|
|
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,55 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { getApps, initializeApp } from 'firebase/app';
|
|
2
|
+
import { getFirestore } from 'firebase/firestore';
|
|
3
|
+
|
|
4
|
+
import type { FirebaseClientConfig } from '../../../auth/firebase/contracts';
|
|
5
|
+
|
|
6
|
+
export function createFirebaseClientFirestore(config: FirebaseClientConfig) {
|
|
7
|
+
const app =
|
|
8
|
+
getApps().find((candidate) => candidate.name === config.projectId) ??
|
|
9
|
+
initializeApp(config, config.projectId);
|
|
10
|
+
|
|
11
|
+
return getFirestore(app);
|
|
12
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addDoc,
|
|
3
|
+
collection,
|
|
4
|
+
deleteDoc,
|
|
5
|
+
doc,
|
|
6
|
+
documentId,
|
|
7
|
+
getDoc,
|
|
8
|
+
getDocs,
|
|
9
|
+
limit as limitQuery,
|
|
10
|
+
orderBy as orderByQuery,
|
|
11
|
+
query,
|
|
12
|
+
startAfter,
|
|
13
|
+
updateDoc,
|
|
14
|
+
type QueryConstraint,
|
|
15
|
+
} from 'firebase/firestore';
|
|
16
|
+
|
|
17
|
+
import type {
|
|
18
|
+
ClientFirestoreRepository,
|
|
19
|
+
ClientFirestoreRepositoryConfig,
|
|
20
|
+
} from './contracts';
|
|
21
|
+
|
|
22
|
+
const defaultPageSize = 25;
|
|
23
|
+
const maximumPageSize = 100;
|
|
24
|
+
|
|
25
|
+
export function createClientFirestoreRepository<
|
|
26
|
+
TDocument,
|
|
27
|
+
TItem,
|
|
28
|
+
TCreateInput,
|
|
29
|
+
TUpdateInput = TCreateInput,
|
|
30
|
+
>({
|
|
31
|
+
collection: collectionName,
|
|
32
|
+
cursor: cursorConfig,
|
|
33
|
+
database,
|
|
34
|
+
mapper,
|
|
35
|
+
orderBy = [],
|
|
36
|
+
schema,
|
|
37
|
+
}: ClientFirestoreRepositoryConfig<
|
|
38
|
+
TDocument,
|
|
39
|
+
TItem,
|
|
40
|
+
TCreateInput,
|
|
41
|
+
TUpdateInput
|
|
42
|
+
>): ClientFirestoreRepository<TItem, TCreateInput, TUpdateInput> {
|
|
43
|
+
const documents = collection(database, collectionName);
|
|
44
|
+
const read = (id: string, value: unknown): TItem =>
|
|
45
|
+
mapper.fromDocument(id, schema.parse(value));
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
async getIndex({ cursor, limit = defaultPageSize } = {}) {
|
|
49
|
+
const pageSize = Math.min(
|
|
50
|
+
maximumPageSize,
|
|
51
|
+
Math.max(1, Math.floor(limit)),
|
|
52
|
+
);
|
|
53
|
+
const constraints: QueryConstraint[] = orderBy.map((order) =>
|
|
54
|
+
orderByQuery(order.field, order.direction ?? 'asc'),
|
|
55
|
+
);
|
|
56
|
+
constraints.push(orderByQuery(documentId(), 'asc'));
|
|
57
|
+
|
|
58
|
+
if (cursor && cursorConfig) {
|
|
59
|
+
constraints.push(startAfter(...cursorConfig.toQueryValues(cursor)));
|
|
60
|
+
} else if (cursor) {
|
|
61
|
+
const cursorDocument = await getDoc(doc(documents, cursor));
|
|
62
|
+
if (cursorDocument.exists()) {
|
|
63
|
+
constraints.push(startAfter(cursorDocument));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
constraints.push(limitQuery(pageSize));
|
|
67
|
+
|
|
68
|
+
const snapshot = await getDocs(query(documents, ...constraints));
|
|
69
|
+
const results = snapshot.docs.map((document) => ({
|
|
70
|
+
document: schema.parse(document.data()),
|
|
71
|
+
id: document.id,
|
|
72
|
+
}));
|
|
73
|
+
const items = results.map(({ document, id }) =>
|
|
74
|
+
mapper.fromDocument(id, document),
|
|
75
|
+
);
|
|
76
|
+
const lastResult = results.at(-1);
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
items,
|
|
80
|
+
nextCursor:
|
|
81
|
+
snapshot.size === pageSize && lastResult
|
|
82
|
+
? cursorConfig
|
|
83
|
+
? cursorConfig.serialize(lastResult)
|
|
84
|
+
: lastResult.id
|
|
85
|
+
: undefined,
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
async getDetail({ id }) {
|
|
90
|
+
const snapshot = await getDoc(doc(documents, id));
|
|
91
|
+
return snapshot.exists() ? read(snapshot.id, snapshot.data()) : null;
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
async create(input) {
|
|
95
|
+
const reference = await addDoc(documents, mapper.toCreate(input));
|
|
96
|
+
const snapshot = await getDoc(reference);
|
|
97
|
+
return read(snapshot.id, snapshot.data());
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
async update({ id, value }) {
|
|
101
|
+
const reference = doc(documents, id);
|
|
102
|
+
await updateDoc(reference, mapper.toUpdate(value));
|
|
103
|
+
const snapshot = await getDoc(reference);
|
|
104
|
+
return read(snapshot.id, snapshot.data());
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
async delete({ id }) {
|
|
108
|
+
await deleteDoc(doc(documents, id));
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
package/src/display/contracts.ts
CHANGED
|
@@ -190,11 +190,33 @@ export type TableColumn = {
|
|
|
190
190
|
sizing?: 'content' | 'fill';
|
|
191
191
|
wrap?: boolean;
|
|
192
192
|
sort?: {
|
|
193
|
+
firstDirection?: 'ascending' | 'descending';
|
|
193
194
|
type: 'number' | 'text';
|
|
194
195
|
tieBreaker?: string;
|
|
195
196
|
};
|
|
196
197
|
};
|
|
197
198
|
|
|
199
|
+
type TableActionBase = {
|
|
200
|
+
borderless?: boolean;
|
|
201
|
+
hoverTone?: Tone;
|
|
202
|
+
icon: AstroComponent;
|
|
203
|
+
iconTone?: Tone;
|
|
204
|
+
label: string;
|
|
205
|
+
tone?: Tone;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
export type TableAction = TableActionBase &
|
|
209
|
+
(
|
|
210
|
+
| {
|
|
211
|
+
href: string;
|
|
212
|
+
modalTargetId?: never;
|
|
213
|
+
}
|
|
214
|
+
| {
|
|
215
|
+
href?: never;
|
|
216
|
+
modalTargetId: string;
|
|
217
|
+
}
|
|
218
|
+
);
|
|
219
|
+
|
|
198
220
|
export type TableCell = {
|
|
199
221
|
tooltip?: string;
|
|
200
222
|
} & (
|
|
@@ -238,6 +260,10 @@ export type TableCell = {
|
|
|
238
260
|
kind: 'tags';
|
|
239
261
|
tags: readonly DisplayTag[];
|
|
240
262
|
}
|
|
263
|
+
| {
|
|
264
|
+
kind: 'actions';
|
|
265
|
+
actions: readonly TableAction[];
|
|
266
|
+
}
|
|
241
267
|
);
|
|
242
268
|
|
|
243
269
|
export type TableRow = {
|
|
@@ -256,6 +282,20 @@ export type TablePaginationModel = {
|
|
|
256
282
|
nextHref?: string;
|
|
257
283
|
};
|
|
258
284
|
|
|
285
|
+
export type TableProgressiveEntry = {
|
|
286
|
+
key: string;
|
|
287
|
+
source?: string;
|
|
288
|
+
filters?: TableRow['filters'];
|
|
289
|
+
sortValues?: TableRow['sortValues'];
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
export type TableProgressiveModel = {
|
|
293
|
+
entries: readonly TableProgressiveEntry[];
|
|
294
|
+
sources?: Readonly<Record<string, string>>;
|
|
295
|
+
batchSize?: number;
|
|
296
|
+
initialOffset?: number;
|
|
297
|
+
};
|
|
298
|
+
|
|
259
299
|
type NavigationItemBase = {
|
|
260
300
|
active?: boolean;
|
|
261
301
|
id?: string;
|
package/src/display/index.ts
CHANGED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
TablePaginationModel,
|
|
3
|
+
TableProgressiveModel,
|
|
4
|
+
TableRow,
|
|
5
|
+
} from './contracts';
|
|
6
|
+
|
|
7
|
+
export const tableProgressiveBatchSize = 25;
|
|
8
|
+
|
|
9
|
+
export type PagedTablePresentation = {
|
|
10
|
+
pagination: TablePaginationModel;
|
|
11
|
+
progressive: TableProgressiveModel;
|
|
12
|
+
rows: readonly TableRow[];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function createPagedTablePresentation(
|
|
16
|
+
allRows: readonly TableRow[],
|
|
17
|
+
page: number,
|
|
18
|
+
getPageHref: (page: number) => string,
|
|
19
|
+
batchSize: number = tableProgressiveBatchSize,
|
|
20
|
+
): PagedTablePresentation {
|
|
21
|
+
const resolvedBatchSize = Math.max(1, Math.floor(batchSize));
|
|
22
|
+
const totalPages = Math.max(1, Math.ceil(allRows.length / resolvedBatchSize));
|
|
23
|
+
if (!Number.isInteger(page) || page < 1 || page > totalPages) {
|
|
24
|
+
throw new RangeError(`Invalid table page: ${page}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const normalizedRows = allRows.map((row, index): TableRow => ({
|
|
28
|
+
...row,
|
|
29
|
+
key: row.key ?? `table-row-${index + 1}`,
|
|
30
|
+
}));
|
|
31
|
+
const keys = new Set(normalizedRows.map((row) => row.key));
|
|
32
|
+
if (keys.size !== normalizedRows.length) {
|
|
33
|
+
throw new Error('Progressive table rows require unique keys');
|
|
34
|
+
}
|
|
35
|
+
const sources = Object.fromEntries(
|
|
36
|
+
Array.from({ length: totalPages }, (_, index) => {
|
|
37
|
+
const sourcePage = index + 1;
|
|
38
|
+
return [String(sourcePage), getPageHref(sourcePage)];
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
const start = (page - 1) * resolvedBatchSize;
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
pagination: {
|
|
45
|
+
page,
|
|
46
|
+
totalPages,
|
|
47
|
+
nextHref: page < totalPages ? getPageHref(page + 1) : undefined,
|
|
48
|
+
},
|
|
49
|
+
progressive: {
|
|
50
|
+
batchSize: resolvedBatchSize,
|
|
51
|
+
entries: normalizedRows.map((row, index) => ({
|
|
52
|
+
key: row.key!,
|
|
53
|
+
source: String(Math.floor(index / resolvedBatchSize) + 1),
|
|
54
|
+
filters: row.filters,
|
|
55
|
+
sortValues: row.sortValues,
|
|
56
|
+
})),
|
|
57
|
+
initialOffset: start,
|
|
58
|
+
sources,
|
|
59
|
+
},
|
|
60
|
+
rows: normalizedRows.slice(start, start + resolvedBatchSize),
|
|
61
|
+
};
|
|
62
|
+
}
|