@moldea.ai/website-ui 1.0.0 → 1.1.0

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/README.md CHANGED
@@ -7,7 +7,7 @@ The package owns the reusable design tokens, global website primitives, interact
7
7
  ## Install after release
8
8
 
9
9
  ```bash
10
- pnpm add @moldea.ai/website-ui@1.0.0
10
+ pnpm add @moldea.ai/website-ui@1.1.0
11
11
  ```
12
12
 
13
13
  The package currently supports Astro `7.2.2` and Tailwind CSS `4.3.3` exactly. Import the shared stylesheet once from the website's global stylesheet:
@@ -38,10 +38,11 @@ Every component has a dedicated public subpath:
38
38
  - `@moldea.ai/website-ui/breadcrumbs`
39
39
  - `@moldea.ai/website-ui/inline-brand-text`
40
40
  - `@moldea.ai/website-ui/local-search`
41
+ - `@moldea.ai/website-ui/navigation-progress`
41
42
  - `@moldea.ai/website-ui/theme-bootstrap`
42
43
  - `@moldea.ai/website-ui/theme-control`
43
44
 
44
- `ThemeBootstrap` belongs in the document head before rendered content. Pass the same app-owned storage key to `ThemeControl`. `BrandLogo` receives app-owned asset paths and labels rather than embedding one site's identity. `LocalSearch` receives app-owned copy, routes, and the generated index URL.
45
+ `ThemeBootstrap` belongs in the document head before rendered content. Pass the same app-owned storage key to `ThemeControl`. Mount `NavigationProgress` once near the start of the document body in websites that use Astro's `ClientRouter`; it reports client navigation preparation without taking ownership of the app's layout. `BrandLogo` receives app-owned asset paths and labels rather than embedding one site's identity. `LocalSearch` receives app-owned copy, routes, and the generated index URL.
45
46
 
46
47
  ## Development
47
48
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moldea.ai/website-ui",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Shared Astro foundations for moldea public websites.",
5
5
  "homepage": "https://github.com/moldea-ai/packages/tree/main/projects/website-ui#readme",
6
6
  "bugs": {
@@ -65,6 +65,9 @@
65
65
  "./local-search": {
66
66
  "import": "./src/components/local-search/local-search.component.astro"
67
67
  },
68
+ "./navigation-progress": {
69
+ "import": "./src/components/navigation-progress/navigation-progress.component.astro"
70
+ },
68
71
  "./theme-bootstrap": {
69
72
  "import": "./src/components/theme-bootstrap/theme-bootstrap.component.astro"
70
73
  },
@@ -0,0 +1,93 @@
1
+ <div
2
+ class="pointer-events-none fixed inset-x-0 top-0 z-[999] h-1 overflow-hidden bg-background"
3
+ data-navigation-progress
4
+ role="progressbar"
5
+ aria-label="Page navigation progress"
6
+ aria-valuetext="Loading next page"
7
+ hidden
8
+ >
9
+ <span
10
+ class="block h-full w-full origin-left animate-navigation-progress bg-foreground will-change-[transform,opacity]"
11
+ data-navigation-progress-indicator
12
+ aria-hidden="true"
13
+ ></span>
14
+ </div>
15
+
16
+ <script>
17
+ import type { TransitionBeforePreparationEvent } from 'astro:transitions/client';
18
+
19
+ type INavigationProgressWindow = Window & {
20
+ __moldeaWebsiteUiNavigationProgressInitialized?: boolean;
21
+ };
22
+
23
+ const pendingPreparations = new Set<symbol>();
24
+
25
+ /** Shows or hides every mounted navigation progress indicator. */
26
+ const setNavigationProgressVisibility = (isVisible: boolean): void => {
27
+ for (const progress of document.querySelectorAll<HTMLElement>('[data-navigation-progress]')) {
28
+ progress.hidden = !isVisible;
29
+ }
30
+ };
31
+
32
+ /** Clears pending navigation state and restores the idle presentation. */
33
+ const resetNavigationProgress = (): void => {
34
+ pendingPreparations.clear();
35
+ setNavigationProgressVisibility(false);
36
+ };
37
+
38
+ /** Completes one preparation without hiding a newer concurrent navigation. */
39
+ const finishPreparation = (preparationId: symbol): void => {
40
+ pendingPreparations.delete(preparationId);
41
+
42
+ if (pendingPreparations.size === 0) {
43
+ setNavigationProgressVisibility(false);
44
+ }
45
+ };
46
+
47
+ /** Starts progress and guarantees cleanup when Astro's loader resolves, rejects, or aborts. */
48
+ const handleBeforePreparation = (event: TransitionBeforePreparationEvent): void => {
49
+ const preparationId = Symbol('navigation-preparation');
50
+ const loader = event.loader;
51
+
52
+ pendingPreparations.add(preparationId);
53
+ setNavigationProgressVisibility(true);
54
+
55
+ event.loader = async (): Promise<void> => {
56
+ try {
57
+ await loader();
58
+ } finally {
59
+ finishPreparation(preparationId);
60
+ }
61
+ };
62
+ };
63
+
64
+ /** Provides an idempotent lifecycle cleanup after successful preparation. */
65
+ const handleAfterPreparation = (): void => {
66
+ if (pendingPreparations.size === 0) {
67
+ setNavigationProgressVisibility(false);
68
+ }
69
+ };
70
+
71
+ const navigationWindow = window as INavigationProgressWindow;
72
+
73
+ if (navigationWindow.__moldeaWebsiteUiNavigationProgressInitialized !== true) {
74
+ navigationWindow.__moldeaWebsiteUiNavigationProgressInitialized = true;
75
+ document.addEventListener('astro:before-preparation', handleBeforePreparation);
76
+ document.addEventListener('astro:after-preparation', handleAfterPreparation);
77
+ document.addEventListener('astro:page-load', resetNavigationProgress);
78
+ window.addEventListener('pageshow', resetNavigationProgress);
79
+ }
80
+
81
+ resetNavigationProgress();
82
+ </script>
83
+
84
+ <style>
85
+ @media (prefers-reduced-motion: reduce) {
86
+ [data-navigation-progress-indicator] {
87
+ animation: none;
88
+ opacity: 1;
89
+ transform: translateX(8%) scaleX(0.32);
90
+ will-change: auto;
91
+ }
92
+ }
93
+ </style>
package/src/tokens.css CHANGED
@@ -56,6 +56,25 @@
56
56
  --shadow-raised: var(--shadow-raised-value);
57
57
  --shadow-surface: var(--shadow-surface-value);
58
58
  --shadow-inset: var(--shadow-inset-value);
59
+
60
+ --animate-navigation-progress: navigation-progress 1.2s ease-in-out infinite;
61
+ }
62
+
63
+ @keyframes navigation-progress {
64
+ 0% {
65
+ opacity: 0;
66
+ transform: translateX(-100%) scaleX(0.2);
67
+ }
68
+
69
+ 40% {
70
+ opacity: 1;
71
+ transform: translateX(-20%) scaleX(0.55);
72
+ }
73
+
74
+ 100% {
75
+ opacity: 0;
76
+ transform: translateX(100%) scaleX(0.2);
77
+ }
59
78
  }
60
79
 
61
80
  :root {