@elyndra/astro-bgs 1.2.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/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@elyndra/astro-bgs",
3
+ "version": "1.2.0",
4
+ "sideEffects": false,
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "description": "Elyndra backgrounds components for Astro (SSR shells with vanilla client hydration)",
9
+ "keywords": [
10
+ "elyndra",
11
+ "ui",
12
+ "front-end",
13
+ "framework",
14
+ "scifi",
15
+ "sci-fi",
16
+ "science-fiction",
17
+ "astro"
18
+ ],
19
+ "homepage": "https://elyndra.dev",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/Gabox301/elyndra.git"
23
+ },
24
+ "bugs": {
25
+ "url": "https://github.com/Gabox301/elyndra/issues"
26
+ },
27
+ "funding": "https://github.com/sponsors/romelperez",
28
+ "license": "MIT",
29
+ "files": [
30
+ "src"
31
+ ],
32
+ "exports": {
33
+ ".": "./src/index.ts",
34
+ "./Dots": "./src/Dots/Dots.astro",
35
+ "./GridLines": "./src/GridLines/GridLines.astro",
36
+ "./MovingLines": "./src/MovingLines/MovingLines.astro",
37
+ "./Puffs": "./src/Puffs/Puffs.astro",
38
+ "./client": "./src/client/bgsClient.ts"
39
+ },
40
+ "types": "./src/index.ts",
41
+ "module": "./src/index.ts",
42
+ "main": "./src/index.ts",
43
+ "peerDependencies": {
44
+ "astro": "7.3.2"
45
+ },
46
+ "dependencies": {
47
+ "@elyndra/bgs": "^1.2.0",
48
+ "tslib": "2.8.1"
49
+ },
50
+ "scripts": {
51
+ "build": "tsc --noEmit -p tsconfig.json"
52
+ }
53
+ }
@@ -0,0 +1,29 @@
1
+ ---
2
+ import type { DotsElementProps } from '../types.js';
3
+
4
+ interface Props extends DotsElementProps {}
5
+
6
+ const { className, ...settings } = Astro.props as Props;
7
+
8
+ // Ids only need page-level uniqueness: the component script consumes them in
9
+ // the same render.
10
+ const nodeId = `elyndra-bgs-dots-${Math.random().toString(36).slice(2)}`;
11
+ const serializable = { ...settings };
12
+ ---
13
+
14
+ <canvas
15
+ id={nodeId}
16
+ data-elyndra-bgs-dots
17
+ aria-hidden="true"
18
+ class={className}
19
+ style="position: absolute; inset: 0; display: block; border: 0; margin: 0; padding: 0; width: 100%; height: 100%;"
20
+ ></canvas>
21
+
22
+ <script define:vars={{ nodeId, serializable }}>
23
+ import { initDotsElement } from '../client/bgsClient.js';
24
+
25
+ const element = document.getElementById(nodeId);
26
+ if (element instanceof HTMLCanvasElement) {
27
+ initDotsElement(element, serializable);
28
+ }
29
+ </script>
@@ -0,0 +1,16 @@
1
+ // @vitest-environment node
2
+ // Render tests assert on SSR strings, so they run in node: under jsdom the
3
+ // Astro plugin compiles `.astro` imports for the browser (client stub).
4
+ import { expect, test } from 'vitest';
5
+ import { renderComponent } from '../testUtils/index.js';
6
+ import Dots from './Dots.astro';
7
+
8
+ test('Should render the dots canvas shell', async () => {
9
+ const html = await renderComponent(Dots, {
10
+ props: { color: '#fff', distance: 20 },
11
+ });
12
+
13
+ expect(html).toContain('data-elyndra-bgs-dots');
14
+ expect(html).toContain('aria-hidden="true"');
15
+ expect(html).toContain('initDotsElement');
16
+ });
@@ -0,0 +1,29 @@
1
+ ---
2
+ import type { GridLinesElementProps } from '../types.js';
3
+
4
+ interface Props extends GridLinesElementProps {}
5
+
6
+ const { className, ...settings } = Astro.props as Props;
7
+
8
+ // Ids only need page-level uniqueness: the component script consumes them in
9
+ // the same render.
10
+ const nodeId = `elyndra-bgs-grid-lines-${Math.random().toString(36).slice(2)}`;
11
+ const serializable = { ...settings };
12
+ ---
13
+
14
+ <canvas
15
+ id={nodeId}
16
+ data-elyndra-bgs-grid-lines
17
+ aria-hidden="true"
18
+ class={className}
19
+ style="position: absolute; inset: 0; display: block; border: 0; margin: 0; padding: 0; width: 100%; height: 100%;"
20
+ ></canvas>
21
+
22
+ <script define:vars={{ nodeId, serializable }}>
23
+ import { initGridLinesElement } from '../client/bgsClient.js';
24
+
25
+ const element = document.getElementById(nodeId);
26
+ if (element instanceof HTMLCanvasElement) {
27
+ initGridLinesElement(element, serializable);
28
+ }
29
+ </script>
@@ -0,0 +1,16 @@
1
+ // @vitest-environment node
2
+ // Render tests assert on SSR strings, so they run in node: under jsdom the
3
+ // Astro plugin compiles `.astro` imports for the browser (client stub).
4
+ import { expect, test } from 'vitest';
5
+ import { renderComponent } from '../testUtils/index.js';
6
+ import GridLines from './GridLines.astro';
7
+
8
+ test('Should render the grid-lines canvas shell', async () => {
9
+ const html = await renderComponent(GridLines, {
10
+ props: { lineColor: '#fff' },
11
+ });
12
+
13
+ expect(html).toContain('data-elyndra-bgs-grid-lines');
14
+ expect(html).toContain('aria-hidden="true"');
15
+ expect(html).toContain('initGridLinesElement');
16
+ });
@@ -0,0 +1,29 @@
1
+ ---
2
+ import type { MovingLinesElementProps } from '../types.js';
3
+
4
+ interface Props extends MovingLinesElementProps {}
5
+
6
+ const { className, ...settings } = Astro.props as Props;
7
+
8
+ // Ids only need page-level uniqueness: the component script consumes them in
9
+ // the same render.
10
+ const nodeId = `elyndra-bgs-moving-lines-${Math.random().toString(36).slice(2)}`;
11
+ const serializable = { ...settings };
12
+ ---
13
+
14
+ <canvas
15
+ id={nodeId}
16
+ data-elyndra-bgs-moving-lines
17
+ aria-hidden="true"
18
+ class={className}
19
+ style="position: absolute; inset: 0; display: block; border: 0; margin: 0; padding: 0; width: 100%; height: 100%;"
20
+ ></canvas>
21
+
22
+ <script define:vars={{ nodeId, serializable }}>
23
+ import { initMovingLinesElement } from '../client/bgsClient.js';
24
+
25
+ const element = document.getElementById(nodeId);
26
+ if (element instanceof HTMLCanvasElement) {
27
+ initMovingLinesElement(element, serializable);
28
+ }
29
+ </script>
@@ -0,0 +1,16 @@
1
+ // @vitest-environment node
2
+ // Render tests assert on SSR strings, so they run in node: under jsdom the
3
+ // Astro plugin compiles `.astro` imports for the browser (client stub).
4
+ import { expect, test } from 'vitest';
5
+ import { renderComponent } from '../testUtils/index.js';
6
+ import MovingLines from './MovingLines.astro';
7
+
8
+ test('Should render the moving-lines canvas shell', async () => {
9
+ const html = await renderComponent(MovingLines, {
10
+ props: { sets: 2 },
11
+ });
12
+
13
+ expect(html).toContain('data-elyndra-bgs-moving-lines');
14
+ expect(html).toContain('aria-hidden="true"');
15
+ expect(html).toContain('initMovingLinesElement');
16
+ });
@@ -0,0 +1,29 @@
1
+ ---
2
+ import type { PuffsElementProps } from '../types.js';
3
+
4
+ interface Props extends PuffsElementProps {}
5
+
6
+ const { className, ...settings } = Astro.props as Props;
7
+
8
+ // Ids only need page-level uniqueness: the component script consumes them in
9
+ // the same render.
10
+ const nodeId = `elyndra-bgs-puffs-${Math.random().toString(36).slice(2)}`;
11
+ const serializable = { ...settings };
12
+ ---
13
+
14
+ <canvas
15
+ id={nodeId}
16
+ data-elyndra-bgs-puffs
17
+ aria-hidden="true"
18
+ class={className}
19
+ style="position: absolute; inset: 0; display: block; border: 0; margin: 0; padding: 0; width: 100%; height: 100%;"
20
+ ></canvas>
21
+
22
+ <script define:vars={{ nodeId, serializable }}>
23
+ import { initPuffsElement } from '../client/bgsClient.js';
24
+
25
+ const element = document.getElementById(nodeId);
26
+ if (element instanceof HTMLCanvasElement) {
27
+ initPuffsElement(element, serializable);
28
+ }
29
+ </script>
@@ -0,0 +1,16 @@
1
+ // @vitest-environment node
2
+ // Render tests assert on SSR strings, so they run in node: under jsdom the
3
+ // Astro plugin compiles `.astro` imports for the browser (client stub).
4
+ import { expect, test } from 'vitest';
5
+ import { renderComponent } from '../testUtils/index.js';
6
+ import Puffs from './Puffs.astro';
7
+
8
+ test('Should render the puffs canvas shell', async () => {
9
+ const html = await renderComponent(Puffs, {
10
+ props: { color: '#fff', quantity: 4 },
11
+ });
12
+
13
+ expect(html).toContain('data-elyndra-bgs-puffs');
14
+ expect(html).toContain('aria-hidden="true"');
15
+ expect(html).toContain('initPuffsElement');
16
+ });
package/src/astro.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ // In-repo ambient types for `.astro` imports (tests only). The published
2
+ // package ships the `.astro` sources and consumers get full prop inference
3
+ // from the frontmatter `Props` interfaces via their own Astro setup.
4
+ declare module '*.astro' {
5
+ const Component: unknown;
6
+ export default Component;
7
+ }
@@ -0,0 +1,76 @@
1
+ import { beforeEach, expect, test } from 'vitest';
2
+ import {
3
+ initDotsElement,
4
+ initGridLinesElement,
5
+ initMovingLinesElement,
6
+ initPuffsElement,
7
+ } from './bgsClient.js';
8
+
9
+ beforeEach(() => {
10
+ document.body.innerHTML = '';
11
+ });
12
+
13
+ const mountCanvas = (): HTMLCanvasElement => {
14
+ const element = document.createElement('canvas');
15
+ document.body.appendChild(element);
16
+ return element;
17
+ };
18
+
19
+ test('Should mark the dots node and cancel on dispose', () => {
20
+ const element = mountCanvas();
21
+
22
+ const handle = initDotsElement(element, { color: '#fff' });
23
+
24
+ // jsdom has no 2d context: the vanilla background is a safe no-op.
25
+ expect(element.hasAttribute('data-elyndra-bgs-dots-node')).toBe(true);
26
+
27
+ handle.dispose();
28
+ handle.dispose();
29
+
30
+ expect(element.hasAttribute('data-elyndra-bgs-dots-node')).toBe(false);
31
+ });
32
+
33
+ test('Should mark the grid-lines node and cancel on dispose', () => {
34
+ const element = mountCanvas();
35
+
36
+ const handle = initGridLinesElement(element, { lineColor: '#fff' });
37
+
38
+ expect(element.hasAttribute('data-elyndra-bgs-grid-lines-node')).toBe(true);
39
+
40
+ handle.dispose();
41
+
42
+ expect(element.hasAttribute('data-elyndra-bgs-grid-lines-node')).toBe(false);
43
+ });
44
+
45
+ test('Should mark the moving-lines node and cancel on dispose', () => {
46
+ const element = mountCanvas();
47
+
48
+ const handle = initMovingLinesElement(element, { sets: 2 });
49
+
50
+ expect(element.hasAttribute('data-elyndra-bgs-moving-lines-node')).toBe(true);
51
+
52
+ handle.dispose();
53
+
54
+ expect(element.hasAttribute('data-elyndra-bgs-moving-lines-node')).toBe(false);
55
+ });
56
+
57
+ test('Should mark the puffs node and cancel on dispose', () => {
58
+ const element = mountCanvas();
59
+
60
+ const handle = initPuffsElement(element, { color: '#fff', quantity: 4 });
61
+
62
+ expect(element.hasAttribute('data-elyndra-bgs-puffs-node')).toBe(true);
63
+
64
+ handle.dispose();
65
+
66
+ expect(element.hasAttribute('data-elyndra-bgs-puffs-node')).toBe(false);
67
+ });
68
+
69
+ test('Should dispose the previous background on second init', () => {
70
+ const element = mountCanvas();
71
+
72
+ initDotsElement(element, {});
73
+ initDotsElement(element, {});
74
+
75
+ expect(element.hasAttribute('data-elyndra-bgs-dots-node')).toBe(true);
76
+ });
@@ -0,0 +1,137 @@
1
+ import {
2
+ type BackgroundDots,
3
+ type BackgroundGridLines,
4
+ type BackgroundMovingLines,
5
+ type BackgroundPuffs,
6
+ createBackgroundDots,
7
+ createBackgroundGridLines,
8
+ createBackgroundMovingLines,
9
+ createBackgroundPuffs,
10
+ } from '@elyndra/bgs';
11
+ import type {
12
+ DotsElementProps,
13
+ DotsSerializableSettings,
14
+ GridLinesElementProps,
15
+ GridLinesSerializableSettings,
16
+ MovingLinesElementProps,
17
+ MovingLinesSerializableSettings,
18
+ PuffsElementProps,
19
+ PuffsSerializableSettings,
20
+ } from '../types.js';
21
+
22
+ const DOTS_NODE_MARKER = 'data-elyndra-bgs-dots-node';
23
+ const GRID_LINES_NODE_MARKER = 'data-elyndra-bgs-grid-lines-node';
24
+ const MOVING_LINES_NODE_MARKER = 'data-elyndra-bgs-moving-lines-node';
25
+ const PUFFS_NODE_MARKER = 'data-elyndra-bgs-puffs-node';
26
+
27
+ interface BackgroundHandle<T> {
28
+ readonly background: T;
29
+ readonly dispose: () => void;
30
+ }
31
+
32
+ const backgroundsByElement = new WeakMap<Element, { dispose: () => void }>();
33
+
34
+ // Presentational shell attrs (`className`) never reach the vanilla settings.
35
+ const toSettings = <P extends { className?: string }, S>(props: P): S => {
36
+ const { className: _presentational, ...settings } = props;
37
+ void _presentational;
38
+ return settings as S;
39
+ };
40
+
41
+ const initBackgroundElement = <T extends { cancel: () => void }>(
42
+ element: HTMLCanvasElement,
43
+ marker: string,
44
+ create: () => T,
45
+ ): BackgroundHandle<T> => {
46
+ backgroundsByElement.get(element)?.dispose();
47
+
48
+ const background = create();
49
+
50
+ element.setAttribute(marker, '');
51
+
52
+ const handle: BackgroundHandle<T> = {
53
+ background,
54
+ dispose: () => {
55
+ if (backgroundsByElement.get(element) !== handle) {
56
+ return;
57
+ }
58
+ backgroundsByElement.delete(element);
59
+ element.removeAttribute(marker);
60
+ background.cancel();
61
+ },
62
+ };
63
+
64
+ backgroundsByElement.set(element, handle);
65
+
66
+ return handle;
67
+ };
68
+
69
+ /**
70
+ * Hydrates one dots background: the shell canvas itself is the vanilla
71
+ * target. Islands run standalone (no animator join). Idempotent: re-running
72
+ * cancels the previous background first.
73
+ */
74
+ const initDotsElement = (
75
+ element: HTMLCanvasElement,
76
+ props: DotsElementProps,
77
+ ): BackgroundHandle<BackgroundDots> =>
78
+ initBackgroundElement(element, DOTS_NODE_MARKER, () =>
79
+ createBackgroundDots({
80
+ canvas: element,
81
+ settingsRef: { current: toSettings<DotsElementProps, DotsSerializableSettings>(props) },
82
+ }),
83
+ );
84
+
85
+ /**
86
+ * Hydrates one grid-lines background (same island semantics as dots).
87
+ */
88
+ const initGridLinesElement = (
89
+ element: HTMLCanvasElement,
90
+ props: GridLinesElementProps,
91
+ ): BackgroundHandle<BackgroundGridLines> =>
92
+ initBackgroundElement(element, GRID_LINES_NODE_MARKER, () =>
93
+ createBackgroundGridLines({
94
+ canvas: element,
95
+ settingsRef: { current: toSettings<GridLinesElementProps, GridLinesSerializableSettings>(props) },
96
+ }),
97
+ );
98
+
99
+ /**
100
+ * Hydrates one moving-lines background (same island semantics as dots).
101
+ */
102
+ const initMovingLinesElement = (
103
+ element: HTMLCanvasElement,
104
+ props: MovingLinesElementProps,
105
+ ): BackgroundHandle<BackgroundMovingLines> =>
106
+ initBackgroundElement(element, MOVING_LINES_NODE_MARKER, () =>
107
+ createBackgroundMovingLines({
108
+ canvas: element,
109
+ settingsRef: { current: toSettings<MovingLinesElementProps, MovingLinesSerializableSettings>(props) },
110
+ }),
111
+ );
112
+
113
+ /**
114
+ * Hydrates one puffs background (same island semantics as dots).
115
+ */
116
+ const initPuffsElement = (
117
+ element: HTMLCanvasElement,
118
+ props: PuffsElementProps,
119
+ ): BackgroundHandle<BackgroundPuffs> =>
120
+ initBackgroundElement(element, PUFFS_NODE_MARKER, () =>
121
+ createBackgroundPuffs({
122
+ canvas: element,
123
+ settingsRef: { current: toSettings<PuffsElementProps, PuffsSerializableSettings>(props) },
124
+ }),
125
+ );
126
+
127
+ export {
128
+ DOTS_NODE_MARKER,
129
+ GRID_LINES_NODE_MARKER,
130
+ initDotsElement,
131
+ initGridLinesElement,
132
+ initMovingLinesElement,
133
+ initPuffsElement,
134
+ MOVING_LINES_NODE_MARKER,
135
+ PUFFS_NODE_MARKER,
136
+ };
137
+ export type { BackgroundHandle };
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './client/bgsClient.js';
2
+ export type * from './types.js';
@@ -0,0 +1 @@
1
+ export * from './renderComponent.js';
@@ -0,0 +1,16 @@
1
+ import {
2
+ type ContainerRenderOptions,
3
+ experimental_AstroContainer as AstroContainer,
4
+ } from 'astro/container';
5
+
6
+ /**
7
+ * Renders an `.astro` component to a string. In-repo `.astro` imports are
8
+ * `unknown` (ambient declaration, tests only), so the factory type is
9
+ * inferred from the container itself — no drift if Astro changes it.
10
+ */
11
+ const renderComponent = async (component: unknown, options?: ContainerRenderOptions): Promise<string> => {
12
+ const container = await AstroContainer.create();
13
+ return container.renderToString(component as Parameters<typeof container.renderToString>[0], options);
14
+ };
15
+
16
+ export { renderComponent };
package/src/types.ts ADDED
@@ -0,0 +1,50 @@
1
+ import type {
2
+ CreateBackgroundDotsSettings,
3
+ CreateBackgroundGridLinesSettings,
4
+ CreateBackgroundMovingLinesSettings,
5
+ CreateBackgroundPuffsSettings,
6
+ } from '@elyndra/bgs';
7
+
8
+ /**
9
+ * Serializable subsets of the background settings. Only JSON crosses the
10
+ * Astro server/client boundary (`define:vars`):
11
+ * - `easing` function forms are excluded: use easing names instead.
12
+ * - `canvas`, `animator` and `settingsRef` are excluded: the shell canvas
13
+ * itself is hydrated and islands run standalone (no animator join).
14
+ * - `elementRef`/`id`/`style`/`positioned` are excluded: the shell carries
15
+ * `className` server-side; consumers style via classes.
16
+ */
17
+ interface DotsSerializableSettings extends CreateBackgroundDotsSettings {}
18
+
19
+ interface DotsElementProps extends DotsSerializableSettings {
20
+ className?: string;
21
+ }
22
+
23
+ interface GridLinesSerializableSettings extends CreateBackgroundGridLinesSettings {}
24
+
25
+ interface GridLinesElementProps extends GridLinesSerializableSettings {
26
+ className?: string;
27
+ }
28
+
29
+ interface MovingLinesSerializableSettings extends CreateBackgroundMovingLinesSettings {}
30
+
31
+ interface MovingLinesElementProps extends MovingLinesSerializableSettings {
32
+ className?: string;
33
+ }
34
+
35
+ interface PuffsSerializableSettings extends CreateBackgroundPuffsSettings {}
36
+
37
+ interface PuffsElementProps extends PuffsSerializableSettings {
38
+ className?: string;
39
+ }
40
+
41
+ export type {
42
+ DotsElementProps,
43
+ DotsSerializableSettings,
44
+ GridLinesElementProps,
45
+ GridLinesSerializableSettings,
46
+ MovingLinesElementProps,
47
+ MovingLinesSerializableSettings,
48
+ PuffsElementProps,
49
+ PuffsSerializableSettings,
50
+ };