@astronox/core 0.3.1 → 0.4.1

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 (39) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/src/ads/adsense/AdsenseHead.astro +55 -0
  4. package/src/ads/adsense/AdsenseUnit.astro +45 -0
  5. package/src/ads/adsense/index.ts +3 -1
  6. package/src/ads/adsense/provider.ts +41 -0
  7. package/src/ads/adsense/runtime.ts +284 -0
  8. package/src/ads/contracts.ts +67 -5
  9. package/src/components/content/CardHeader/CardHeader.astro +20 -0
  10. package/src/components/content/CardHeader/CardHeader.css +21 -0
  11. package/src/components/feedback/Tooltip/tooltipController.ts +37 -5
  12. package/src/components/monetization/AdSlot/AdSlot.astro +0 -3
  13. package/src/components/monetization/AdSlot/AdSlot.css +5 -28
  14. package/src/components/navigation/Breadcrumbs/Breadcrumbs.astro +2 -0
  15. package/src/components/navigation/Breadcrumbs/Breadcrumbs.css +12 -6
  16. package/src/components/navigation/Footer/Footer.astro +8 -0
  17. package/src/components/navigation/Footer/Footer.css +20 -6
  18. package/src/components/navigation/Footer/contracts.ts +1 -0
  19. package/src/composition/Grid/Grid.css +1 -1
  20. package/src/composition/SurfaceGroup/SurfaceGroup.astro +2 -1
  21. package/src/composition/SurfaceGroup/SurfaceGroup.css +18 -18
  22. package/src/composition/SurfaceGroup/stickySurfaceGroupController.ts +9 -0
  23. package/src/config/environment.ts +66 -2
  24. package/src/layouts/BaseLayout.astro +6 -1
  25. package/src/patterns/Combobox/comboboxController.ts +51 -12
  26. package/src/patterns/Dropdown/dropdownController.ts +17 -6
  27. package/src/patterns/Fullscreen/FullscreenControl.astro +3 -30
  28. package/src/patterns/Fullscreen/fullscreenController.ts +18 -22
  29. package/src/patterns/Table/Table.css +8 -2
  30. package/src/patterns/Table/TableFilterPanel.astro +84 -0
  31. package/src/patterns/Table/TableFilterPanel.css +139 -0
  32. package/src/patterns/Table/TableSkeleton.css +0 -3
  33. package/src/patterns/Table/tableFilterPanelController.ts +91 -0
  34. package/src/patterns/Table/tableProgressiveController.ts +78 -16
  35. package/src/patterns/Table/tableScrollPosition.ts +41 -0
  36. package/src/patterns/Table/tableSortController.ts +6 -3
  37. package/src/patterns/index.ts +2 -0
  38. package/src/styles/style.css +1 -0
  39. package/src/ads/adsense.ts +0 -112
@@ -0,0 +1,91 @@
1
+ const initializedAttribute = 'data-table-filter-panel-initialized';
2
+ const compactAttribute = 'data-table-filter-panel-compact';
3
+ const openAttribute = 'data-table-filter-panel-open';
4
+ const noop = (): void => undefined;
5
+
6
+ const collapseWidths = {
7
+ sm: 24,
8
+ md: 36,
9
+ lg: 44,
10
+ xl: 54,
11
+ } as const;
12
+
13
+ export function initTableFilterPanel(root: HTMLElement): () => void {
14
+ if (root.hasAttribute(initializedAttribute)) return noop;
15
+
16
+ const trigger = root.querySelector<HTMLButtonElement>(
17
+ ':scope > [data-widget="card-header"] [data-card-header-trigger]',
18
+ );
19
+ const content = root.querySelector<HTMLElement>(
20
+ ':scope > [data-table-filter-panel-content]',
21
+ );
22
+ if (!trigger || !content) return noop;
23
+
24
+ const collapseAt = root.dataset.tableFilterPanelCollapseAt;
25
+ const collapseWidth =
26
+ collapseAt && collapseAt in collapseWidths
27
+ ? collapseWidths[collapseAt as keyof typeof collapseWidths]
28
+ : collapseWidths.lg;
29
+ const boundary =
30
+ root.closest<HTMLElement>('.grid-container') ?? root.parentElement ?? root;
31
+ const grid = boundary.querySelector<HTMLElement>(
32
+ ':scope > [data-composition="grid"]',
33
+ );
34
+ let compact = false;
35
+ let open = true;
36
+
37
+ const sync = (): void => {
38
+ root.toggleAttribute(compactAttribute, compact);
39
+ root.toggleAttribute(openAttribute, open);
40
+ trigger.hidden = !compact;
41
+ trigger.tabIndex = compact ? 0 : -1;
42
+ trigger.setAttribute('aria-expanded', String(open));
43
+ content.hidden = !open;
44
+ };
45
+
46
+ const updateMode = (): void => {
47
+ const rootFontSize = Number.parseFloat(
48
+ window.getComputedStyle(document.documentElement).fontSize,
49
+ );
50
+ const renderedColumns = grid
51
+ ? window
52
+ .getComputedStyle(grid)
53
+ .gridTemplateColumns.split(/\s+/)
54
+ .filter(Boolean).length
55
+ : 0;
56
+ const nextCompact = grid
57
+ ? renderedColumns === 1
58
+ : boundary.getBoundingClientRect().width < collapseWidth * rootFontSize;
59
+ if (nextCompact === compact) return;
60
+
61
+ compact = nextCompact;
62
+ open = !compact;
63
+ sync();
64
+ };
65
+
66
+ const handleClick = (): void => {
67
+ if (!compact) return;
68
+ open = !open;
69
+ sync();
70
+ };
71
+
72
+ root.setAttribute(initializedAttribute, '');
73
+ trigger.addEventListener('click', handleClick);
74
+ const resizeObserver =
75
+ 'ResizeObserver' in window ? new ResizeObserver(updateMode) : null;
76
+ resizeObserver?.observe(boundary);
77
+ updateMode();
78
+ sync();
79
+
80
+ return () => {
81
+ resizeObserver?.disconnect();
82
+ trigger.removeEventListener('click', handleClick);
83
+ trigger.hidden = true;
84
+ trigger.tabIndex = -1;
85
+ trigger.setAttribute('aria-expanded', 'true');
86
+ content.hidden = false;
87
+ root.removeAttribute(initializedAttribute);
88
+ root.removeAttribute(compactAttribute);
89
+ root.removeAttribute(openAttribute);
90
+ };
91
+ }
@@ -3,6 +3,7 @@ import type {
3
3
  TableProgressiveModel,
4
4
  } from '../../display/contracts';
5
5
  import { tableProgressiveBatchSize } from '../../display/tableProgressive';
6
+ import { mutateTablePreservingScroll } from './tableScrollPosition';
6
7
 
7
8
  const initializedAttribute = 'data-table-progressive-initialized';
8
9
 
@@ -151,6 +152,8 @@ export function initTableProgressive(root: HTMLElement): () => void {
151
152
  let disposed = false;
152
153
  let generation = 0;
153
154
  let prepared = false;
155
+ let retainedWindowSize = Math.max(batchSize, getRows(activeRowsRoot).length);
156
+ let scheduledRenderFrame: number | null = null;
154
157
 
155
158
  registerRows(getRows(activeRowsRoot));
156
159
 
@@ -181,8 +184,11 @@ export function initTableProgressive(root: HTMLElement): () => void {
181
184
  activeSkeletonTemplate.content.querySelectorAll<HTMLElement>(
182
185
  '[data-table-skeleton-row]',
183
186
  ),
184
- ).slice(0, count);
185
- skeletonRows = candidates.map((row) => document.importNode(row, true));
187
+ );
188
+ if (candidates.length === 0) return;
189
+ skeletonRows = Array.from({ length: count }, (_, index) =>
190
+ document.importNode(candidates[index % candidates.length]!, true),
191
+ );
186
192
  insertBeforeEmpty(activeRowsRoot, empty, skeletonRows);
187
193
  }
188
194
 
@@ -204,6 +210,21 @@ export function initTableProgressive(root: HTMLElement): () => void {
204
210
  );
205
211
  }
206
212
 
213
+ function replaceRows(rows: readonly HTMLElement[]): void {
214
+ for (const row of rows) row.hidden = false;
215
+ mutateTablePreservingScroll(root, () => {
216
+ removeSkeleton();
217
+ detachRenderedRows();
218
+ insertBeforeEmpty(activeRowsRoot, empty, rows);
219
+ });
220
+ root.dispatchEvent(
221
+ new CustomEvent<TableRowsAppendedDetail>('table:rows-appended', {
222
+ bubbles: true,
223
+ detail: { rows },
224
+ }),
225
+ );
226
+ }
227
+
207
228
  function updateEmptyState(): void {
208
229
  if (empty) empty.hidden = results.length > 0;
209
230
  }
@@ -286,10 +307,12 @@ export function initTableProgressive(root: HTMLElement): () => void {
286
307
  TableLoadingState,
287
308
  'pending' | 'refreshing' | 'appending'
288
309
  > = 'appending',
310
+ requestedCount = batchSize,
311
+ replace = false,
289
312
  ): Promise<void> {
290
313
  if (request || disposed || cursor >= results.length) return;
291
314
 
292
- const target = results.slice(cursor, cursor + batchSize);
315
+ const target = results.slice(cursor, cursor + requestedCount);
293
316
  if (target.length === 0) {
294
317
  removeSkeleton();
295
318
  updatePagination();
@@ -305,7 +328,9 @@ export function initTableProgressive(root: HTMLElement): () => void {
305
328
  request = activeRequest;
306
329
  setPaginationState(activePagination, activeStatus, 'loading');
307
330
  updateTableState(loadingState);
308
- addSkeleton(target.length);
331
+ if (loadingState === 'appending' || skeletonRows.length === 0) {
332
+ addSkeleton(target.length);
333
+ }
309
334
 
310
335
  try {
311
336
  await loadSources(missing, activeRequest.signal);
@@ -336,9 +361,16 @@ export function initTableProgressive(root: HTMLElement): () => void {
336
361
  return;
337
362
  }
338
363
 
339
- removeSkeleton();
340
- appendRows(rows as HTMLElement[]);
364
+ if (replace) replaceRows(rows as HTMLElement[]);
365
+ else {
366
+ removeSkeleton();
367
+ appendRows(rows as HTMLElement[]);
368
+ }
341
369
  cursor += target.length;
370
+ retainedWindowSize = Math.max(
371
+ retainedWindowSize,
372
+ getRows(activeRowsRoot).length,
373
+ );
342
374
  updatePagination();
343
375
  updateTableState('ready');
344
376
  }
@@ -403,22 +435,48 @@ export function initTableProgressive(root: HTMLElement): () => void {
403
435
  offset: number,
404
436
  state: Extract<TableLoadingState, 'pending' | 'refreshing'>,
405
437
  ): void {
438
+ retainedWindowSize = Math.max(
439
+ retainedWindowSize,
440
+ getRows(activeRowsRoot).length,
441
+ );
406
442
  generation += 1;
407
443
  request?.abort();
408
444
  request = null;
445
+ if (scheduledRenderFrame !== null) {
446
+ window.cancelAnimationFrame(scheduledRenderFrame);
447
+ scheduledRenderFrame = null;
448
+ }
449
+ observer.disconnect();
409
450
  updateTableState(state);
410
- addSkeleton(batchSize);
411
- detachRenderedRows();
451
+ setPaginationState(activePagination, activeStatus, 'loading');
452
+ if (empty) empty.hidden = true;
453
+ mutateTablePreservingScroll(root, () => {
454
+ detachRenderedRows();
455
+ addSkeleton(retainedWindowSize);
456
+ });
457
+
412
458
  applyQuery();
413
459
  cursor = Math.min(offset, results.length);
414
- updateEmptyState();
415
- if (results.length === 0) {
416
- removeSkeleton();
417
- updatePagination();
418
- updateTableState('empty');
419
- return;
420
- }
421
- void renderNextBatch(state);
460
+ const activeGeneration = generation;
461
+ scheduledRenderFrame = window.requestAnimationFrame(() => {
462
+ scheduledRenderFrame = window.requestAnimationFrame(() => {
463
+ scheduledRenderFrame = null;
464
+ if (disposed || generation !== activeGeneration) return;
465
+
466
+ updateEmptyState();
467
+ if (results.length === 0) {
468
+ replaceRows([]);
469
+ updatePagination();
470
+ updateTableState('empty');
471
+ return;
472
+ }
473
+ void renderNextBatch(
474
+ state,
475
+ Math.min(retainedWindowSize, results.length - cursor),
476
+ true,
477
+ );
478
+ });
479
+ });
422
480
  }
423
481
 
424
482
  function refresh(): void {
@@ -434,6 +492,10 @@ export function initTableProgressive(root: HTMLElement): () => void {
434
492
  destroy() {
435
493
  disposed = true;
436
494
  request?.abort();
495
+ if (scheduledRenderFrame !== null) {
496
+ window.cancelAnimationFrame(scheduledRenderFrame);
497
+ scheduledRenderFrame = null;
498
+ }
437
499
  observer.disconnect();
438
500
  removeSkeleton();
439
501
  activePagination.hidden = !fallbackLink;
@@ -0,0 +1,41 @@
1
+ type ScrollRestoreState = {
2
+ frame: number;
3
+ originalOverflowAnchor: string;
4
+ scrollRoot: HTMLElement;
5
+ scrollTop: number;
6
+ };
7
+
8
+ const restoreStates = new WeakMap<HTMLElement, ScrollRestoreState>();
9
+
10
+ export function mutateTablePreservingScroll(
11
+ root: HTMLElement,
12
+ mutation: () => void,
13
+ ): void {
14
+ const scrollRoot = root.closest<HTMLElement>('[data-site-viewport]');
15
+ if (!scrollRoot) {
16
+ mutation();
17
+ return;
18
+ }
19
+
20
+ const previous = restoreStates.get(root);
21
+ if (previous) window.cancelAnimationFrame(previous.frame);
22
+ const originalOverflowAnchor =
23
+ previous?.originalOverflowAnchor ?? root.style.overflowAnchor;
24
+ const scrollTop = scrollRoot.scrollTop;
25
+
26
+ root.style.overflowAnchor = 'none';
27
+ mutation();
28
+ scrollRoot.scrollTop = scrollTop;
29
+
30
+ const state: ScrollRestoreState = {
31
+ frame: window.requestAnimationFrame(() => {
32
+ scrollRoot.scrollTop = scrollTop;
33
+ root.style.overflowAnchor = originalOverflowAnchor;
34
+ restoreStates.delete(root);
35
+ }),
36
+ originalOverflowAnchor,
37
+ scrollRoot,
38
+ scrollTop,
39
+ };
40
+ restoreStates.set(root, state);
41
+ }
@@ -4,6 +4,7 @@ import {
4
4
  setTableProgressiveSort,
5
5
  updateTableProgressiveEntry,
6
6
  } from './tableProgressiveController';
7
+ import { mutateTablePreservingScroll } from './tableScrollPosition';
7
8
 
8
9
  type SortDirection = 'ascending' | 'descending';
9
10
  type SortType = 'number' | 'text';
@@ -96,9 +97,11 @@ export function initTableSort(root: HTMLElement): () => void {
96
97
  orderDirty = activeState !== null;
97
98
  if (rows.every((row, index) => row === sortedRows[index])) return;
98
99
 
99
- const fragment = document.createDocumentFragment();
100
- for (const row of sortedRows) fragment.append(row);
101
- rowsRoot.prepend(fragment);
100
+ mutateTablePreservingScroll(root, () => {
101
+ const fragment = document.createDocumentFragment();
102
+ for (const row of sortedRows) fragment.append(row);
103
+ rowsRoot.prepend(fragment);
104
+ });
102
105
  };
103
106
 
104
107
  const scheduleSort = (): void => {
@@ -15,6 +15,7 @@ export { default as StageFlow } from './StageFlow/StageFlow.astro';
15
15
  export { default as Table } from './Table/Table.astro';
16
16
  export { default as TableBody } from './Table/TableBody.astro';
17
17
  export { default as TableFooter } from './Table/TableFooter.astro';
18
+ export { default as TableFilterPanel } from './Table/TableFilterPanel.astro';
18
19
  export { default as TableHeader } from './Table/TableHeader.astro';
19
20
  export { default as TablePagination } from './Table/TablePagination.astro';
20
21
  export { default as TableSkeleton } from './Table/TableSkeleton.astro';
@@ -41,6 +42,7 @@ export * from './SpriteProjectilePreview/contracts';
41
42
  export * from './SpriteProjectilePreview/spriteProjectilePreviewController';
42
43
  export * from './Table/tableCellController';
43
44
  export * from './Table/tableFilterController';
45
+ export * from './Table/tableFilterPanelController';
44
46
  export * from './Table/tableLayout';
45
47
  export * from './Table/tableProgressiveController';
46
48
  export * from './Table/tableSortController';
@@ -83,6 +83,7 @@
83
83
  @import '../patterns/Table/TableBody.css' layer(patterns);
84
84
  @import '../patterns/Table/TableCell.css' layer(patterns);
85
85
  @import '../patterns/Table/TableFooter.css' layer(patterns);
86
+ @import '../patterns/Table/TableFilterPanel.css' layer(patterns);
86
87
  @import '../patterns/Table/TableHeader.css' layer(patterns);
87
88
  @import '../patterns/Table/TablePagination.css' layer(patterns);
88
89
  @import '../patterns/Table/TableSkeleton.css' layer(patterns);
@@ -1,112 +0,0 @@
1
- import type { AdConsentProvider, AdProvider } from './contracts';
2
- import { setAdSlotState } from './slot';
3
-
4
- declare global {
5
- interface Window {
6
- adsbygoogle?: Record<string, unknown>[];
7
- }
8
- }
9
-
10
- export function createAdsenseProvider({
11
- clientId,
12
- consent,
13
- loadTimeoutMs,
14
- }: {
15
- clientId: string;
16
- consent: AdConsentProvider;
17
- loadTimeoutMs: number;
18
- }): AdProvider {
19
- let loading: Promise<void> | null = null;
20
-
21
- const load = async (): Promise<void> => {
22
- if (!(await consent.canLoadAds())) {
23
- throw new Error('Advertising consent was not granted');
24
- }
25
- loading ??= loadAdsenseScript(clientId, loadTimeoutMs).catch((reason) => {
26
- loading = null;
27
- throw reason;
28
- });
29
- return loading;
30
- };
31
-
32
- return {
33
- load,
34
- async request(slot) {
35
- setAdSlotState(slot, 'loading');
36
- try {
37
- await load();
38
- window.adsbygoogle ??= [];
39
- window.adsbygoogle.push({});
40
- observeAdsenseSlot(slot);
41
- } catch (reason) {
42
- setAdSlotState(slot, 'blocked');
43
- throw reason;
44
- }
45
- },
46
- };
47
- }
48
-
49
- function loadAdsenseScript(
50
- clientId: string,
51
- loadTimeoutMs: number,
52
- ): Promise<void> {
53
- const existing = document.querySelector<HTMLScriptElement>(
54
- 'script[data-astronox-adsense]',
55
- );
56
- if (existing?.dataset.loaded === 'true') return Promise.resolve();
57
-
58
- return new Promise((resolve, reject) => {
59
- const script = existing ?? document.createElement('script');
60
- script.async = true;
61
- script.crossOrigin = 'anonymous';
62
- script.dataset.astronoxAdsense = '';
63
- script.src = `https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${encodeURIComponent(clientId)}`;
64
- const timeout = window.setTimeout(
65
- () => reject(new Error('AdSense loading timed out')),
66
- loadTimeoutMs,
67
- );
68
- script.addEventListener(
69
- 'load',
70
- () => {
71
- window.clearTimeout(timeout);
72
- script.dataset.loaded = 'true';
73
- resolve();
74
- },
75
- { once: true },
76
- );
77
- script.addEventListener(
78
- 'error',
79
- () => {
80
- window.clearTimeout(timeout);
81
- reject(new Error('AdSense failed to load'));
82
- },
83
- { once: true },
84
- );
85
- if (!existing) document.head.append(script);
86
- });
87
- }
88
-
89
- function observeAdsenseSlot(slot: HTMLElement): void {
90
- const advertisement = slot.querySelector<HTMLElement>('.adsbygoogle');
91
- if (!advertisement) {
92
- setAdSlotState(slot, 'unavailable');
93
- return;
94
- }
95
-
96
- const sync = (): void => {
97
- const status = advertisement.dataset.adStatus;
98
- if (status === 'filled') setAdSlotState(slot, 'filled');
99
- if (status === 'unfilled') setAdSlotState(slot, 'unavailable');
100
- };
101
- sync();
102
- if (advertisement.dataset.adStatus) return;
103
-
104
- const observer = new MutationObserver(() => {
105
- sync();
106
- if (advertisement.dataset.adStatus) observer.disconnect();
107
- });
108
- observer.observe(advertisement, {
109
- attributeFilter: ['data-ad-status'],
110
- attributes: true,
111
- });
112
- }