@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.
- package/package.json +7 -1
- package/src/admin/cache.ts +217 -0
- package/src/admin/contracts.ts +28 -0
- package/src/admin/index.ts +3 -0
- package/src/admin/module.ts +28 -0
- package/src/auth/contracts.ts +53 -0
- package/src/auth/firebase/client.ts +83 -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/formSubmission.ts +177 -0
- package/src/client/index.ts +4 -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/actions/ScrollToTop/ScrollToTop.css +11 -0
- package/src/components/content/CardBody/CardBody.astro +3 -0
- package/src/components/content/CardBody/CardBody.css +16 -2
- package/src/components/content/CardFooter/CardFooter.astro +9 -0
- package/src/components/content/CardFooter/CardFooter.css +45 -3
- package/src/components/content/CardLink/CardLink.astro +12 -3
- package/src/components/content/CardLink/CardLink.css +32 -0
- package/src/components/content/MarkdownContent/MarkdownContent.css +14 -4
- package/src/components/content/MarkdownContent/MarkdownInline.astro +1 -1
- package/src/components/content/MarkdownContent/contracts.ts +1 -1
- package/src/components/content/MarkdownContent/markdownContentRenderer.ts +552 -0
- package/src/components/data-display/TextPair/TextPair.css +8 -2
- 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/PreparedFormBootstrap/PreparedFormBootstrap.astro +203 -0
- package/src/components/forms/PreparedFormBootstrap/contracts.ts +15 -0
- package/src/components/forms/TextInput/TextInput.astro +7 -1
- package/src/components/forms/Textarea/Textarea.astro +35 -0
- package/src/components/forms/Textarea/Textarea.css +62 -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 +18 -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/Breadcrumbs/Breadcrumbs.astro +5 -1
- package/src/components/navigation/Breadcrumbs/Breadcrumbs.css +12 -17
- package/src/components/navigation/Footer/Footer.astro +102 -0
- package/src/components/navigation/Footer/Footer.css +155 -0
- package/src/components/navigation/Footer/contracts.ts +17 -0
- package/src/components/navigation/SubNavigation/SubNavigation.css +0 -5
- package/src/composition/Grid/Grid.css +16 -0
- package/src/composition/SurfaceGroup/SurfaceGroup.css +21 -7
- package/src/composition/SurfaceGroup/stickySurfaceGroupController.ts +3 -0
- package/src/data/firestore/client/contracts.ts +78 -0
- package/src/data/firestore/client/firebase.ts +56 -0
- package/src/data/firestore/client/index.ts +3 -0
- package/src/data/firestore/client/repository.ts +111 -0
- package/src/display/contracts.ts +44 -1
- package/src/display/index.ts +1 -0
- package/src/display/tableProgressive.ts +62 -0
- package/src/layouts/BaseLayout.astro +8 -1
- package/src/patterns/Combobox/Combobox.astro +23 -0
- package/src/patterns/Combobox/comboboxController.ts +69 -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/TableBody.css +3 -3
- package/src/patterns/Table/TableCell.css +11 -1
- 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/base.css +24 -7
- package/src/styles/interaction.css +16 -0
- package/src/styles/style.css +3 -0
- package/src/styles/tokens.css +5 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
export type ClientTableState = 'ready' | 'empty' | 'error';
|
|
2
|
+
|
|
3
|
+
export type ClientTablePage<TItem> = {
|
|
4
|
+
items: readonly TItem[];
|
|
5
|
+
nextCursor?: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type ClientTablePaginationController = {
|
|
9
|
+
destroy(): void;
|
|
10
|
+
remove(key: string): void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type ClientTablePaginationOptions<TItem> = {
|
|
14
|
+
createRow(item: TItem): HTMLElement | null;
|
|
15
|
+
emptyMessage: string;
|
|
16
|
+
errorMessage: string;
|
|
17
|
+
getKey(item: TItem): string;
|
|
18
|
+
loadPage(cursor?: string): Promise<ClientTablePage<TItem>>;
|
|
19
|
+
root: HTMLElement;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function replaceClientTableRows(
|
|
23
|
+
root: HTMLElement,
|
|
24
|
+
rows: readonly HTMLElement[],
|
|
25
|
+
{
|
|
26
|
+
message,
|
|
27
|
+
state = rows.length > 0 ? 'ready' : 'empty',
|
|
28
|
+
}: {
|
|
29
|
+
message?: string;
|
|
30
|
+
state?: ClientTableState;
|
|
31
|
+
} = {},
|
|
32
|
+
): void {
|
|
33
|
+
const rowsRoot = root.querySelector<HTMLElement>('[data-table-rows]');
|
|
34
|
+
if (!rowsRoot) return;
|
|
35
|
+
|
|
36
|
+
rowsRoot
|
|
37
|
+
.querySelectorAll(
|
|
38
|
+
':scope > [data-table-row], :scope > [data-table-skeleton-row]',
|
|
39
|
+
)
|
|
40
|
+
.forEach((row) => row.remove());
|
|
41
|
+
|
|
42
|
+
const stateRow = rowsRoot.querySelector<HTMLElement>(
|
|
43
|
+
':scope > [data-table-state]',
|
|
44
|
+
);
|
|
45
|
+
if (stateRow) {
|
|
46
|
+
stateRow.hidden = state === 'ready';
|
|
47
|
+
stateRow.dataset.tableState = state === 'ready' ? 'empty' : state;
|
|
48
|
+
const content = stateRow.querySelector<HTMLElement>(
|
|
49
|
+
'[data-widget="text-block"]',
|
|
50
|
+
);
|
|
51
|
+
if (content && message !== undefined) content.textContent = message;
|
|
52
|
+
if (content) {
|
|
53
|
+
content.setAttribute('role', state === 'error' ? 'alert' : 'status');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (state === 'ready') {
|
|
58
|
+
const fragment = document.createDocumentFragment();
|
|
59
|
+
rows.forEach((row) => {
|
|
60
|
+
row.hidden = false;
|
|
61
|
+
fragment.append(row);
|
|
62
|
+
});
|
|
63
|
+
rowsRoot.insertBefore(fragment, stateRow);
|
|
64
|
+
root.dispatchEvent(
|
|
65
|
+
new CustomEvent('table:rows-appended', {
|
|
66
|
+
bubbles: true,
|
|
67
|
+
detail: { rows },
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
root.dataset.tableLoadingState = state;
|
|
73
|
+
root.removeAttribute('aria-busy');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function createClientTablePagination<TItem>({
|
|
77
|
+
createRow,
|
|
78
|
+
emptyMessage,
|
|
79
|
+
errorMessage,
|
|
80
|
+
getKey,
|
|
81
|
+
loadPage,
|
|
82
|
+
root,
|
|
83
|
+
}: ClientTablePaginationOptions<TItem>): ClientTablePaginationController {
|
|
84
|
+
const rowsRoot = root.querySelector<HTMLElement>('[data-table-rows]');
|
|
85
|
+
const pagination = root.querySelector<HTMLElement>('[data-table-pagination]');
|
|
86
|
+
const sentinel = root.querySelector<HTMLElement>(
|
|
87
|
+
'[data-table-pagination-sentinel]',
|
|
88
|
+
);
|
|
89
|
+
const skeletonTemplate = root.querySelector<HTMLTemplateElement>(
|
|
90
|
+
'[data-table-skeleton-template]',
|
|
91
|
+
);
|
|
92
|
+
const keys = new Set<string>();
|
|
93
|
+
let nextCursor: string | undefined;
|
|
94
|
+
let loading = false;
|
|
95
|
+
let disposed = false;
|
|
96
|
+
|
|
97
|
+
if (pagination) {
|
|
98
|
+
pagination.hidden = false;
|
|
99
|
+
pagination.dataset.tableProgressiveEnhanced = 'true';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const setPaginationState = (state: 'idle' | 'loading' | 'error'): void => {
|
|
103
|
+
if (!pagination) return;
|
|
104
|
+
pagination.dataset.tableProgressiveState = state;
|
|
105
|
+
const status = pagination.querySelector<HTMLElement>(
|
|
106
|
+
'[data-table-pagination-status]',
|
|
107
|
+
);
|
|
108
|
+
if (!status) return;
|
|
109
|
+
status.textContent =
|
|
110
|
+
state === 'loading'
|
|
111
|
+
? (pagination.dataset.tableLoadingLabel ?? '')
|
|
112
|
+
: state === 'error'
|
|
113
|
+
? (pagination.dataset.tableErrorLabel ?? '')
|
|
114
|
+
: '';
|
|
115
|
+
status.hidden = state === 'idle';
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const appendSkeleton = (): HTMLElement[] => {
|
|
119
|
+
if (!rowsRoot || !skeletonTemplate) return [];
|
|
120
|
+
const skeletonRows = Array.from(
|
|
121
|
+
skeletonTemplate.content.querySelectorAll<HTMLElement>(
|
|
122
|
+
'[data-table-skeleton-row]',
|
|
123
|
+
),
|
|
124
|
+
).map((row) => row.cloneNode(true) as HTMLElement);
|
|
125
|
+
const stateRow = rowsRoot.querySelector<HTMLElement>(
|
|
126
|
+
':scope > [data-table-state]',
|
|
127
|
+
);
|
|
128
|
+
const fragment = document.createDocumentFragment();
|
|
129
|
+
skeletonRows.forEach((row) => fragment.append(row));
|
|
130
|
+
rowsRoot.insertBefore(fragment, stateRow);
|
|
131
|
+
return skeletonRows;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const createPageRows = (page: ClientTablePage<TItem>): HTMLElement[] => {
|
|
135
|
+
const rows: HTMLElement[] = [];
|
|
136
|
+
for (const item of page.items) {
|
|
137
|
+
const key = getKey(item);
|
|
138
|
+
if (keys.has(key)) continue;
|
|
139
|
+
const row = createRow(item);
|
|
140
|
+
if (!row) continue;
|
|
141
|
+
keys.add(key);
|
|
142
|
+
rows.push(row);
|
|
143
|
+
}
|
|
144
|
+
nextCursor = page.nextCursor;
|
|
145
|
+
return rows;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const appendRows = (rows: readonly HTMLElement[]): void => {
|
|
149
|
+
if (!rowsRoot) return;
|
|
150
|
+
const stateRow = rowsRoot.querySelector<HTMLElement>(
|
|
151
|
+
':scope > [data-table-state]',
|
|
152
|
+
);
|
|
153
|
+
const fragment = document.createDocumentFragment();
|
|
154
|
+
rows.forEach((row) => {
|
|
155
|
+
row.hidden = false;
|
|
156
|
+
fragment.append(row);
|
|
157
|
+
});
|
|
158
|
+
rowsRoot.insertBefore(fragment, stateRow);
|
|
159
|
+
root.dispatchEvent(
|
|
160
|
+
new CustomEvent('table:rows-appended', {
|
|
161
|
+
bubbles: true,
|
|
162
|
+
detail: { rows },
|
|
163
|
+
}),
|
|
164
|
+
);
|
|
165
|
+
root.dataset.tableLoadingState = 'ready';
|
|
166
|
+
root.removeAttribute('aria-busy');
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const load = async (append: boolean): Promise<void> => {
|
|
170
|
+
if (loading || disposed || (append && !nextCursor)) return;
|
|
171
|
+
loading = true;
|
|
172
|
+
root.setAttribute('aria-busy', 'true');
|
|
173
|
+
root.dataset.tableLoadingState = append ? 'appending' : 'pending';
|
|
174
|
+
setPaginationState('loading');
|
|
175
|
+
const skeletonRows = append ? appendSkeleton() : [];
|
|
176
|
+
let shouldContinue = false;
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const page = await loadPage(append ? nextCursor : undefined);
|
|
180
|
+
if (disposed) return;
|
|
181
|
+
const rows = createPageRows(page);
|
|
182
|
+
if (append) {
|
|
183
|
+
skeletonRows.forEach((row) => row.remove());
|
|
184
|
+
appendRows(rows);
|
|
185
|
+
} else {
|
|
186
|
+
replaceClientTableRows(root, rows, { message: emptyMessage });
|
|
187
|
+
}
|
|
188
|
+
setPaginationState('idle');
|
|
189
|
+
if (!nextCursor) observer?.disconnect();
|
|
190
|
+
shouldContinue = Boolean(
|
|
191
|
+
nextCursor &&
|
|
192
|
+
sentinel &&
|
|
193
|
+
sentinel.getBoundingClientRect().top <= window.innerHeight + 512,
|
|
194
|
+
);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
if (disposed) return;
|
|
197
|
+
console.error('[table] Failed to load client page', error);
|
|
198
|
+
if (append) {
|
|
199
|
+
skeletonRows.forEach((row) => row.remove());
|
|
200
|
+
root.removeAttribute('aria-busy');
|
|
201
|
+
root.dataset.tableLoadingState = 'ready';
|
|
202
|
+
setPaginationState('error');
|
|
203
|
+
} else {
|
|
204
|
+
replaceClientTableRows(root, [], {
|
|
205
|
+
message: errorMessage,
|
|
206
|
+
state: 'error',
|
|
207
|
+
});
|
|
208
|
+
setPaginationState('error');
|
|
209
|
+
}
|
|
210
|
+
} finally {
|
|
211
|
+
loading = false;
|
|
212
|
+
if (shouldContinue && !disposed) {
|
|
213
|
+
window.requestAnimationFrame(() => void load(true));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const observer =
|
|
219
|
+
sentinel && 'IntersectionObserver' in window
|
|
220
|
+
? new IntersectionObserver(
|
|
221
|
+
(entries) => {
|
|
222
|
+
if (entries.some((entry) => entry.isIntersecting)) void load(true);
|
|
223
|
+
},
|
|
224
|
+
{ rootMargin: '512px 0px' },
|
|
225
|
+
)
|
|
226
|
+
: null;
|
|
227
|
+
|
|
228
|
+
const initialize = async (): Promise<void> => {
|
|
229
|
+
await load(false);
|
|
230
|
+
if (!disposed && nextCursor && sentinel && observer) {
|
|
231
|
+
observer.observe(sentinel);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
void initialize();
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
destroy() {
|
|
239
|
+
disposed = true;
|
|
240
|
+
observer?.disconnect();
|
|
241
|
+
},
|
|
242
|
+
remove(key) {
|
|
243
|
+
if (!keys.delete(key)) return;
|
|
244
|
+
const row = Array.from(
|
|
245
|
+
root.querySelectorAll<HTMLElement>('[data-table-row]'),
|
|
246
|
+
).find((candidate) => candidate.dataset.tableRowKey === key);
|
|
247
|
+
row?.remove();
|
|
248
|
+
if (keys.size === 0) {
|
|
249
|
+
replaceClientTableRows(root, [], { message: emptyMessage });
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|
|
@@ -4,6 +4,10 @@ import {
|
|
|
4
4
|
type ComboboxChangeDetail,
|
|
5
5
|
} from '../Combobox/comboboxController';
|
|
6
6
|
import { resetTableSort } from './tableSortController';
|
|
7
|
+
import {
|
|
8
|
+
hasTableProgressive,
|
|
9
|
+
setTableProgressiveFilters,
|
|
10
|
+
} from './tableProgressiveController';
|
|
7
11
|
|
|
8
12
|
type FilterValues = Readonly<Record<string, readonly string[]>>;
|
|
9
13
|
|
|
@@ -104,23 +108,10 @@ export function initTableFilters(root: HTMLElement): () => void {
|
|
|
104
108
|
}
|
|
105
109
|
};
|
|
106
110
|
|
|
107
|
-
const isLoadingAllRows = (): boolean => {
|
|
108
|
-
const pagination = table.querySelector<HTMLElement>(
|
|
109
|
-
'[data-table-pagination]',
|
|
110
|
-
);
|
|
111
|
-
const state = pagination?.dataset.tableProgressiveState;
|
|
112
|
-
return Boolean(
|
|
113
|
-
pagination &&
|
|
114
|
-
!pagination.hidden &&
|
|
115
|
-
state !== 'complete' &&
|
|
116
|
-
state !== 'error',
|
|
117
|
-
);
|
|
118
|
-
};
|
|
119
|
-
|
|
120
111
|
const updateEmptyState = (visibleRows: number, active: boolean): void => {
|
|
121
112
|
const empty = table.querySelector<HTMLElement>('[data-table-filter-empty]');
|
|
122
113
|
if (!empty) return;
|
|
123
|
-
empty.hidden = !active || visibleRows > 0
|
|
114
|
+
empty.hidden = !active || visibleRows > 0;
|
|
124
115
|
};
|
|
125
116
|
|
|
126
117
|
const applyFilters = (): void => {
|
|
@@ -133,6 +124,16 @@ export function initTableFilters(root: HTMLElement): () => void {
|
|
|
133
124
|
rows.some((row) => row.hasAttribute(exclusionsAttribute));
|
|
134
125
|
const active =
|
|
135
126
|
query.length > 0 || activeSelections.length > 0 || hasExternalFilter;
|
|
127
|
+
|
|
128
|
+
if (
|
|
129
|
+
setTableProgressiveFilters(table, {
|
|
130
|
+
search: search?.value.trim() ?? '',
|
|
131
|
+
selections: Object.fromEntries(activeSelections),
|
|
132
|
+
})
|
|
133
|
+
) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
136
137
|
let visibleRows = 0;
|
|
137
138
|
|
|
138
139
|
for (const row of rows) {
|
|
@@ -153,14 +154,17 @@ export function initTableFilters(root: HTMLElement): () => void {
|
|
|
153
154
|
if (row.hidden !== !visible) row.hidden = !visible;
|
|
154
155
|
if (visible) visibleRows += 1;
|
|
155
156
|
}
|
|
156
|
-
|
|
157
157
|
updateEmptyState(visibleRows, active);
|
|
158
|
-
if (active && isLoadingAllRows()) {
|
|
159
|
-
table.dispatchEvent(new CustomEvent('table:load-all', { bubbles: true }));
|
|
160
|
-
}
|
|
161
158
|
};
|
|
162
159
|
|
|
163
160
|
const scheduleFilters = (): void => {
|
|
161
|
+
if (
|
|
162
|
+
table.dataset.tablePreparation === 'controlled' ||
|
|
163
|
+
hasTableProgressive(table)
|
|
164
|
+
) {
|
|
165
|
+
applyFilters();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
164
168
|
if (scheduledFrame !== null) return;
|
|
165
169
|
scheduledFrame = window.requestAnimationFrame(() => {
|
|
166
170
|
scheduledFrame = null;
|
|
@@ -192,11 +196,7 @@ export function initTableFilters(root: HTMLElement): () => void {
|
|
|
192
196
|
const detail = (event as CustomEvent<{ rows?: readonly HTMLElement[] }>)
|
|
193
197
|
.detail;
|
|
194
198
|
registerRows(detail?.rows ?? []);
|
|
195
|
-
scheduleFilters();
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
const handleProgressiveComplete = (): void => {
|
|
199
|
-
scheduleFilters();
|
|
199
|
+
if (!hasTableProgressive(table)) scheduleFilters();
|
|
200
200
|
};
|
|
201
201
|
|
|
202
202
|
const handleReset = (): void => {
|
|
@@ -226,10 +226,6 @@ export function initTableFilters(root: HTMLElement): () => void {
|
|
|
226
226
|
root.addEventListener('combobox:change', handleComboboxChange);
|
|
227
227
|
root.addEventListener(refreshEvent, scheduleFilters);
|
|
228
228
|
table.addEventListener('table:rows-appended', handleRowsAppended);
|
|
229
|
-
table.addEventListener(
|
|
230
|
-
'table:progressive-complete',
|
|
231
|
-
handleProgressiveComplete,
|
|
232
|
-
);
|
|
233
229
|
reset?.addEventListener('click', handleReset);
|
|
234
230
|
|
|
235
231
|
return () => {
|
|
@@ -241,10 +237,6 @@ export function initTableFilters(root: HTMLElement): () => void {
|
|
|
241
237
|
root.removeEventListener('combobox:change', handleComboboxChange);
|
|
242
238
|
root.removeEventListener(refreshEvent, scheduleFilters);
|
|
243
239
|
table.removeEventListener('table:rows-appended', handleRowsAppended);
|
|
244
|
-
table.removeEventListener(
|
|
245
|
-
'table:progressive-complete',
|
|
246
|
-
handleProgressiveComplete,
|
|
247
|
-
);
|
|
248
240
|
reset?.removeEventListener('click', handleReset);
|
|
249
241
|
for (const row of rows) row.hidden = false;
|
|
250
242
|
updateEmptyState(0, false);
|