@astronox/core 0.2.0 → 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.
Files changed (81) hide show
  1. package/README.md +9 -30
  2. package/package.json +7 -1
  3. package/src/admin/contracts.ts +28 -0
  4. package/src/admin/index.ts +2 -0
  5. package/src/admin/module.ts +28 -0
  6. package/src/auth/contracts.ts +53 -0
  7. package/src/auth/firebase/client.ts +53 -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/index.ts +3 -0
  13. package/src/client/preferences.ts +400 -0
  14. package/src/components/actions/Button/Button.astro +42 -6
  15. package/src/components/actions/Button/Button.css +19 -0
  16. package/src/components/actions/IdentityProviderAction/IdentityProviderAction.astro +35 -0
  17. package/src/components/actions/MediaTrigger/MediaTrigger.astro +12 -4
  18. package/src/components/actions/MediaTrigger/MediaTrigger.css +1 -0
  19. package/src/components/content/CardBody/CardBody.astro +3 -0
  20. package/src/components/content/CardBody/CardBody.css +10 -0
  21. package/src/components/content/MarkdownContent/MarkdownContent.css +0 -1
  22. package/src/components/content/MarkdownContent/MarkdownNode.astro +1 -0
  23. package/src/components/content/MarkdownContent/markdownContentRenderer.ts +549 -0
  24. package/src/components/feedback/PreferenceBootstrap/PreferenceBootstrap.astro +403 -0
  25. package/src/components/forms/Checkbox/Checkbox.astro +66 -0
  26. package/src/components/forms/Checkbox/Checkbox.css +126 -0
  27. package/src/components/forms/ChoiceGroup/ChoiceGroup.astro +57 -0
  28. package/src/components/forms/ChoiceGroup/ChoiceGroup.css +108 -0
  29. package/src/components/forms/ChoiceGroup/contracts.ts +9 -0
  30. package/src/components/forms/DateTimeInput/DateTimeInput.astro +14 -0
  31. package/src/components/forms/FieldGroup/FieldGroup.astro +34 -0
  32. package/src/components/forms/FieldGroup/FieldGroup.css +30 -0
  33. package/src/components/forms/TextInput/TextInput.astro +7 -1
  34. package/src/components/forms/Textarea/Textarea.astro +32 -0
  35. package/src/components/forms/Textarea/Textarea.css +54 -0
  36. package/src/components/icons/BackIcon.astro +26 -0
  37. package/src/components/icons/CheckIcon.astro +22 -0
  38. package/src/components/icons/DeleteIcon.astro +22 -0
  39. package/src/components/icons/StatusIndicatorIcon.astro +16 -0
  40. package/src/components/index.ts +12 -0
  41. package/src/components/media/CanvasFrame/CanvasFrame.astro +1 -17
  42. package/src/components/media/EmbedFrame/EmbedFrame.astro +1 -19
  43. package/src/components/media/EmbedFrame/EmbedFrame.css +0 -2
  44. package/src/components/navigation/SubNavigation/SubNavigation.css +0 -5
  45. package/src/composition/Grid/Grid.astro +9 -0
  46. package/src/composition/Grid/Grid.css +52 -0
  47. package/src/composition/GridItem/GridItem.astro +9 -0
  48. package/src/composition/GridItem/GridItem.css +12 -0
  49. package/src/composition/SurfaceGroup/SurfaceGroup.css +21 -7
  50. package/src/composition/SurfaceGroup/stickySurfaceGroupController.ts +3 -0
  51. package/src/data/firestore/client/contracts.ts +55 -0
  52. package/src/data/firestore/client/firebase.ts +12 -0
  53. package/src/data/firestore/client/index.ts +3 -0
  54. package/src/data/firestore/client/repository.ts +111 -0
  55. package/src/display/contracts.ts +41 -0
  56. package/src/display/index.ts +1 -0
  57. package/src/display/tableProgressive.ts +62 -0
  58. package/src/patterns/Combobox/Combobox.astro +23 -0
  59. package/src/patterns/Combobox/comboboxController.ts +72 -0
  60. package/src/patterns/Fullscreen/FullscreenControl.astro +4 -14
  61. package/src/patterns/Fullscreen/FullscreenControl.css +0 -26
  62. package/src/patterns/Modal/Modal.astro +1 -1
  63. package/src/patterns/Modal/Modal.css +38 -1
  64. package/src/patterns/Table/Table.astro +122 -7
  65. package/src/patterns/Table/Table.css +2 -1
  66. package/src/patterns/Table/TableBody.astro +11 -1
  67. package/src/patterns/Table/TableBody.css +6 -0
  68. package/src/patterns/Table/TableCell.css +6 -0
  69. package/src/patterns/Table/TableCellValue.astro +31 -0
  70. package/src/patterns/Table/TableFooter.astro +3 -1
  71. package/src/patterns/Table/TableHeader.astro +1 -0
  72. package/src/patterns/Table/TablePagination.astro +63 -47
  73. package/src/patterns/Table/TableSkeleton.astro +18 -22
  74. package/src/patterns/Table/TableSkeleton.css +30 -5
  75. package/src/patterns/Table/tableClientController.ts +253 -0
  76. package/src/patterns/Table/tableFilterController.ts +83 -33
  77. package/src/patterns/Table/tableLayout.ts +10 -3
  78. package/src/patterns/Table/tableProgressiveController.ts +615 -227
  79. package/src/patterns/Table/tableSortController.ts +25 -7
  80. package/src/styles/interaction.css +16 -0
  81. package/src/styles/style.css +4 -0
@@ -1,70 +1,86 @@
1
1
  ---
2
2
  import type { HTMLAttributes } from 'astro/types';
3
- import type {
4
- TableColumn,
5
- TablePaginationModel,
6
- } from '../../display/contracts';
3
+ import type { TablePaginationModel } from '../../display/contracts';
7
4
  import TableFooter from './TableFooter.astro';
8
- import TableSkeleton from './TableSkeleton.astro';
9
5
 
10
6
  type Props = HTMLAttributes<'div'> & {
11
- columns: readonly TableColumn[];
12
7
  errorLabel: string;
13
8
  loadingLabel: string;
14
9
  nextLabel: string;
15
- pagination: TablePaginationModel;
16
- skeletonRows?: number;
10
+ pagination?: TablePaginationModel;
11
+ progressive?: boolean;
17
12
  };
18
13
 
19
14
  const {
20
15
  class: className,
21
- columns,
22
16
  errorLabel,
23
17
  loadingLabel,
24
18
  nextLabel,
25
19
  pagination,
26
- skeletonRows = 3,
20
+ progressive = false,
27
21
  ...attrs
28
22
  }: Props = Astro.props;
29
23
  ---
30
24
 
31
25
  {
32
- pagination.nextHref ? (
33
- <TableFooter
34
- {...attrs}
35
- class:list={['table-pagination', className]}
36
- padding="none"
37
- data-table-pagination
38
- data-table-page={pagination.page}
39
- data-table-total-pages={pagination.totalPages}
40
- data-table-loading-label={loadingLabel}
41
- data-table-error-label={errorLabel}
42
- data-table-progressive-state="idle"
43
- >
44
- <template data-table-skeleton-template>
45
- <TableSkeleton columns={columns} rows={skeletonRows} />
46
- </template>
47
- <div class="table-pagination__content">
48
- <a
49
- class="table-pagination__link"
50
- href={pagination.nextHref}
51
- data-table-pagination-link
52
- >
53
- {nextLabel}
54
- </a>
55
- <span
56
- class="table-pagination__status"
57
- data-table-pagination-status
58
- role="status"
59
- aria-live="polite"
60
- hidden
61
- />
62
- <span
63
- class="table-pagination__sentinel"
64
- data-table-pagination-sentinel
65
- aria-hidden="true"
66
- />
67
- </div>
68
- </TableFooter>
26
+ pagination?.nextHref || progressive ? (
27
+ <>
28
+ <TableFooter
29
+ {...attrs}
30
+ class:list={['table-pagination', className]}
31
+ padding="none"
32
+ surface={!progressive}
33
+ data-table-pagination
34
+ data-table-loading-label={loadingLabel}
35
+ data-table-error-label={errorLabel}
36
+ data-table-progressive-state="idle"
37
+ hidden={progressive || !pagination?.nextHref}
38
+ >
39
+ <div class="table-pagination__content">
40
+ {
41
+ pagination?.nextHref ? (
42
+ <a
43
+ class="table-pagination__link"
44
+ href={pagination.nextHref}
45
+ data-table-pagination-link
46
+ >
47
+ {nextLabel}
48
+ </a>
49
+ ) : null
50
+ }
51
+ <span
52
+ class="table-pagination__status"
53
+ data-table-pagination-status
54
+ role="status"
55
+ aria-live="polite"
56
+ hidden
57
+ />
58
+ <span
59
+ class="table-pagination__sentinel"
60
+ data-table-pagination-sentinel
61
+ aria-hidden="true"
62
+ />
63
+ </div>
64
+ </TableFooter>
65
+ {
66
+ progressive && pagination?.nextHref ? (
67
+ <noscript>
68
+ <TableFooter
69
+ class:list={['table-pagination', className]}
70
+ padding="none"
71
+ >
72
+ <div class="table-pagination__content">
73
+ <a
74
+ class="table-pagination__link"
75
+ href={pagination.nextHref}
76
+ >
77
+ {nextLabel}
78
+ </a>
79
+ </div>
80
+ </TableFooter>
81
+ </noscript>
82
+ ) : null
83
+ }
84
+ </>
69
85
  ) : null
70
86
  }
@@ -11,26 +11,22 @@ const { columns, rows = 3 }: Props = Astro.props;
11
11
  const rowCount = Math.max(1, Math.floor(rows));
12
12
  ---
13
13
 
14
- <table class="table-skeleton" aria-hidden="true">
15
- <tbody>
16
- {
17
- Array.from({ length: rowCount }, () => (
18
- <tr
19
- class="table-skeleton__row"
20
- data-table-skeleton-row
21
- aria-hidden="true"
14
+ {
15
+ Array.from({ length: rowCount }, () => (
16
+ <tr
17
+ class="table-skeleton__row"
18
+ data-table-skeleton-row
19
+ aria-hidden="true"
20
+ >
21
+ {columns.map((column) => (
22
+ <td
23
+ class="table-skeleton__cell"
24
+ data-align={getTableColumnAlignment(column)}
25
+ data-table-column-role={column.role}
22
26
  >
23
- {columns.map((column) => (
24
- <td
25
- class="table-skeleton__cell"
26
- data-align={getTableColumnAlignment(column)}
27
- data-table-column-role={column.role}
28
- >
29
- <span class="table-skeleton__placeholder" />
30
- </td>
31
- ))}
32
- </tr>
33
- ))
34
- }
35
- </tbody>
36
- </table>
27
+ <span class="table-skeleton__placeholder" />
28
+ </td>
29
+ ))}
30
+ </tr>
31
+ ))
32
+ }
@@ -1,16 +1,41 @@
1
- .table-skeleton {
2
- display: none;
3
- }
4
-
5
1
  .table-skeleton__row {
6
- display: grid;
2
+ display: none;
7
3
  min-width: 0;
8
4
  min-height: var(--table-row-min-block-size);
9
5
  grid-column: 1 / -1;
10
6
  grid-template-columns: subgrid;
7
+ border-block-start: 0;
8
+ }
9
+
10
+ .table-skeleton__row + .table-skeleton__row,
11
+ [data-nox-table-enhanced]
12
+ .table[data-table-loading-state='appending']
13
+ .table-body__row:not([hidden])
14
+ + .table-skeleton__row {
11
15
  border-block-start: var(--nox-border-width) solid var(--nox-border);
12
16
  }
13
17
 
18
+ [data-nox-table-enhanced]
19
+ .table[data-table-loading-state='pending']
20
+ .table-body__row[data-table-row],
21
+ [data-nox-table-enhanced]
22
+ .table[data-table-loading-state='refreshing']
23
+ .table-body__row[data-table-row] {
24
+ display: none;
25
+ }
26
+
27
+ [data-nox-table-enhanced]
28
+ .table[data-table-loading-state='pending']
29
+ .table-skeleton__row,
30
+ [data-nox-table-enhanced]
31
+ .table[data-table-loading-state='refreshing']
32
+ .table-skeleton__row,
33
+ [data-nox-table-enhanced]
34
+ .table[data-table-loading-state='appending']
35
+ .table-skeleton__row {
36
+ display: grid;
37
+ }
38
+
14
39
  .table-skeleton__cell {
15
40
  display: grid;
16
41
  min-width: 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
 
@@ -13,8 +17,57 @@ type CachedFilterValues = {
13
17
  };
14
18
 
15
19
  const initializedAttribute = 'data-table-filters-initialized';
20
+ const externalFiltersAttribute = 'data-table-filter-external';
21
+ const exclusionsAttribute = 'data-table-filter-exclusions';
22
+ const refreshEvent = 'table-filter:refresh';
16
23
  const noop = (): void => undefined;
17
24
 
25
+ export function setTableExternalFilterActive(
26
+ root: HTMLElement,
27
+ key: string,
28
+ active: boolean,
29
+ ): void {
30
+ setTokenAttribute(root, externalFiltersAttribute, key, active);
31
+ }
32
+
33
+ export function setTableRowFilterExclusion(
34
+ row: HTMLElement,
35
+ key: string,
36
+ excluded: boolean,
37
+ ): void {
38
+ setTokenAttribute(row, exclusionsAttribute, key, excluded);
39
+ }
40
+
41
+ function setTokenAttribute(
42
+ element: HTMLElement,
43
+ attribute: string,
44
+ key: string,
45
+ enabled: boolean,
46
+ ): void {
47
+ const normalizedKey = key.trim();
48
+ if (!normalizedKey || /\s/.test(normalizedKey)) {
49
+ throw new Error('Table filter keys must be non-empty tokens');
50
+ }
51
+
52
+ const tokens = new Set(
53
+ (element.getAttribute(attribute) ?? '')
54
+ .split(/\s+/)
55
+ .filter(Boolean),
56
+ );
57
+ if (enabled) tokens.add(normalizedKey);
58
+ else tokens.delete(normalizedKey);
59
+
60
+ if (tokens.size > 0) {
61
+ element.setAttribute(attribute, [...tokens].join(' '));
62
+ } else {
63
+ element.removeAttribute(attribute);
64
+ }
65
+ }
66
+
67
+ export function refreshTableFilters(root: HTMLElement): void {
68
+ root.dispatchEvent(new Event(refreshEvent));
69
+ }
70
+
18
71
  export function initTableFilters(root: HTMLElement): () => void {
19
72
  if (root.getAttribute(initializedAttribute) === 'true') return noop;
20
73
 
@@ -55,23 +108,10 @@ export function initTableFilters(root: HTMLElement): () => void {
55
108
  }
56
109
  };
57
110
 
58
- const isLoadingAllRows = (): boolean => {
59
- const pagination = table.querySelector<HTMLElement>(
60
- '[data-table-pagination]',
61
- );
62
- const state = pagination?.dataset.tableProgressiveState;
63
- return Boolean(
64
- pagination &&
65
- !pagination.hidden &&
66
- state !== 'complete' &&
67
- state !== 'error',
68
- );
69
- };
70
-
71
111
  const updateEmptyState = (visibleRows: number, active: boolean): void => {
72
112
  const empty = table.querySelector<HTMLElement>('[data-table-filter-empty]');
73
113
  if (!empty) return;
74
- empty.hidden = !active || visibleRows > 0 || isLoadingAllRows();
114
+ empty.hidden = !active || visibleRows > 0;
75
115
  };
76
116
 
77
117
  const applyFilters = (): void => {
@@ -79,7 +119,21 @@ export function initTableFilters(root: HTMLElement): () => void {
79
119
  const activeSelections = [...selections.entries()].filter(
80
120
  ([, value]) => value.length > 0,
81
121
  );
82
- const active = query.length > 0 || activeSelections.length > 0;
122
+ const hasExternalFilter =
123
+ root.hasAttribute(externalFiltersAttribute) ||
124
+ rows.some((row) => row.hasAttribute(exclusionsAttribute));
125
+ const active =
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
+
83
137
  let visibleRows = 0;
84
138
 
85
139
  for (const row of rows) {
@@ -92,19 +146,25 @@ export function initTableFilters(root: HTMLElement): () => void {
92
146
  ([field, selectedValue]) =>
93
147
  (cached.values[field] ?? []).includes(selectedValue),
94
148
  );
95
- const visible = matchesSearch && matchesSelections;
149
+ const visible =
150
+ matchesSearch &&
151
+ matchesSelections &&
152
+ !row.hasAttribute(exclusionsAttribute);
96
153
 
97
154
  if (row.hidden !== !visible) row.hidden = !visible;
98
155
  if (visible) visibleRows += 1;
99
156
  }
100
-
101
157
  updateEmptyState(visibleRows, active);
102
- if (active && isLoadingAllRows()) {
103
- table.dispatchEvent(new CustomEvent('table:load-all', { bubbles: true }));
104
- }
105
158
  };
106
159
 
107
160
  const scheduleFilters = (): void => {
161
+ if (
162
+ table.dataset.tablePreparation === 'controlled' ||
163
+ hasTableProgressive(table)
164
+ ) {
165
+ applyFilters();
166
+ return;
167
+ }
108
168
  if (scheduledFrame !== null) return;
109
169
  scheduledFrame = window.requestAnimationFrame(() => {
110
170
  scheduledFrame = null;
@@ -136,11 +196,7 @@ export function initTableFilters(root: HTMLElement): () => void {
136
196
  const detail = (event as CustomEvent<{ rows?: readonly HTMLElement[] }>)
137
197
  .detail;
138
198
  registerRows(detail?.rows ?? []);
139
- scheduleFilters();
140
- };
141
-
142
- const handleProgressiveComplete = (): void => {
143
- scheduleFilters();
199
+ if (!hasTableProgressive(table)) scheduleFilters();
144
200
  };
145
201
 
146
202
  const handleReset = (): void => {
@@ -168,11 +224,8 @@ export function initTableFilters(root: HTMLElement): () => void {
168
224
  applyFilters();
169
225
  search?.addEventListener('input', handleSearch);
170
226
  root.addEventListener('combobox:change', handleComboboxChange);
227
+ root.addEventListener(refreshEvent, scheduleFilters);
171
228
  table.addEventListener('table:rows-appended', handleRowsAppended);
172
- table.addEventListener(
173
- 'table:progressive-complete',
174
- handleProgressiveComplete,
175
- );
176
229
  reset?.addEventListener('click', handleReset);
177
230
 
178
231
  return () => {
@@ -182,11 +235,8 @@ export function initTableFilters(root: HTMLElement): () => void {
182
235
  }
183
236
  search?.removeEventListener('input', handleSearch);
184
237
  root.removeEventListener('combobox:change', handleComboboxChange);
238
+ root.removeEventListener(refreshEvent, scheduleFilters);
185
239
  table.removeEventListener('table:rows-appended', handleRowsAppended);
186
- table.removeEventListener(
187
- 'table:progressive-complete',
188
- handleProgressiveComplete,
189
- );
190
240
  reset?.removeEventListener('click', handleReset);
191
241
  for (const row of rows) row.hidden = false;
192
242
  updateEmptyState(0, false);