@elyndra/astro-frames 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,60 @@
1
+ {
2
+ "name": "@elyndra/astro-frames",
3
+ "version": "1.2.0",
4
+ "sideEffects": false,
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "description": "Elyndra frames 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
+ "./FrameBase": "./src/FrameBase/FrameBase.astro",
35
+ "./FrameCircle": "./src/FrameCircle/FrameCircle.astro",
36
+ "./FrameCorners": "./src/FrameCorners/FrameCorners.astro",
37
+ "./FrameHeader": "./src/FrameHeader/FrameHeader.astro",
38
+ "./FrameKranox": "./src/FrameKranox/FrameKranox.astro",
39
+ "./FrameLines": "./src/FrameLines/FrameLines.astro",
40
+ "./FrameNefrex": "./src/FrameNefrex/FrameNefrex.astro",
41
+ "./FrameNero": "./src/FrameNero/FrameNero.astro",
42
+ "./FrameOctagon": "./src/FrameOctagon/FrameOctagon.astro",
43
+ "./FrameUnderline": "./src/FrameUnderline/FrameUnderline.astro",
44
+ "./client": "./src/client/frameClient.ts"
45
+ },
46
+ "types": "./src/index.ts",
47
+ "module": "./src/index.ts",
48
+ "main": "./src/index.ts",
49
+ "peerDependencies": {
50
+ "astro": "7.3.2"
51
+ },
52
+ "dependencies": {
53
+ "@elyndra/frames": "^1.2.0",
54
+ "@elyndra/tools": "^1.2.0",
55
+ "tslib": "2.8.1"
56
+ },
57
+ "scripts": {
58
+ "build": "tsc --noEmit -p tsconfig.json"
59
+ }
60
+ }
@@ -0,0 +1,33 @@
1
+ ---
2
+ import type { FrameBaseElementProps } from '../types.js';
3
+
4
+ interface Props extends FrameBaseElementProps {}
5
+
6
+ const { className, preserveAspectRatio = 'none', ...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-frame-base-${Math.random().toString(36).slice(2)}`;
11
+ const serializable = { ...settings };
12
+ ---
13
+
14
+ <svg
15
+ id={nodeId}
16
+ data-elyndra-frame-base
17
+ role="presentation"
18
+ class={className}
19
+ style="position: absolute; inset: 0; display: block; border: 0; margin: 0; padding: 0; width: 100%; height: 100%;"
20
+ xmlns="http://www.w3.org/2000/svg"
21
+ preserveAspectRatio={preserveAspectRatio}
22
+ >
23
+ <slot />
24
+ </svg>
25
+
26
+ <script define:vars={{ nodeId, serializable }}>
27
+ import { initFrameBaseElement } from '../client/frameClient.js';
28
+
29
+ const element = document.getElementById(nodeId);
30
+ if (element instanceof SVGSVGElement) {
31
+ initFrameBaseElement(element, serializable);
32
+ }
33
+ </script>
@@ -0,0 +1,15 @@
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 FrameBase from './FrameBase.astro';
7
+
8
+ test('Should render children inside the frame base shell', async () => {
9
+ const html = await renderComponent(FrameBase, {
10
+ slots: { default: '<g><circle /></g>' },
11
+ });
12
+
13
+ expect(html).toContain('data-elyndra-frame-base');
14
+ expect(html).toContain('initFrameBaseElement');
15
+ });
@@ -0,0 +1,33 @@
1
+ ---
2
+ import type { FrameCircleElementProps } from '../types.js';
3
+
4
+ interface Props extends FrameCircleElementProps {}
5
+
6
+ const { className, preserveAspectRatio = 'none', ...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-frame-circle-${Math.random().toString(36).slice(2)}`;
11
+ const serializable = { ...settings };
12
+ ---
13
+
14
+ <svg
15
+ id={nodeId}
16
+ data-elyndra-frame-circle
17
+ role="presentation"
18
+ class={className}
19
+ style="position: absolute; inset: 0; display: block; border: 0; margin: 0; padding: 0; width: 100%; height: 100%;"
20
+ xmlns="http://www.w3.org/2000/svg"
21
+ preserveAspectRatio={preserveAspectRatio}
22
+ >
23
+ <slot />
24
+ </svg>
25
+
26
+ <script define:vars={{ nodeId, serializable }}>
27
+ import { initFrameCircleElement } from '../client/frameClient.js';
28
+
29
+ const element = document.getElementById(nodeId);
30
+ if (element instanceof SVGSVGElement) {
31
+ initFrameCircleElement(element, serializable);
32
+ }
33
+ </script>
@@ -0,0 +1,15 @@
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 FrameCircle from './FrameCircle.astro';
7
+
8
+ test('Should render the frame circle shell', async () => {
9
+ const html = await renderComponent(FrameCircle, {
10
+ props: { padding: 4 },
11
+ });
12
+
13
+ expect(html).toContain('data-elyndra-frame-circle');
14
+ expect(html).toContain('initFrameCircleElement');
15
+ });
@@ -0,0 +1,33 @@
1
+ ---
2
+ import type { FrameCornersElementProps } from '../types.js';
3
+
4
+ interface Props extends FrameCornersElementProps {}
5
+
6
+ const { className, preserveAspectRatio = 'none', ...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-frame-corners-${Math.random().toString(36).slice(2)}`;
11
+ const serializable = { ...settings };
12
+ ---
13
+
14
+ <svg
15
+ id={nodeId}
16
+ data-elyndra-frame-corners
17
+ role="presentation"
18
+ class={className}
19
+ style="position: absolute; inset: 0; display: block; border: 0; margin: 0; padding: 0; width: 100%; height: 100%;"
20
+ xmlns="http://www.w3.org/2000/svg"
21
+ preserveAspectRatio={preserveAspectRatio}
22
+ >
23
+ <slot />
24
+ </svg>
25
+
26
+ <script define:vars={{ nodeId, serializable }}>
27
+ import { initFrameCornersElement } from '../client/frameClient.js';
28
+
29
+ const element = document.getElementById(nodeId);
30
+ if (element instanceof SVGSVGElement) {
31
+ initFrameCornersElement(element, serializable);
32
+ }
33
+ </script>
@@ -0,0 +1,15 @@
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 FrameCorners from './FrameCorners.astro';
7
+
8
+ test('Should render the frame corners shell', async () => {
9
+ const html = await renderComponent(FrameCorners, {
10
+ props: { cornerLength: 8 },
11
+ });
12
+
13
+ expect(html).toContain('data-elyndra-frame-corners');
14
+ expect(html).toContain('initFrameCornersElement');
15
+ });
@@ -0,0 +1,33 @@
1
+ ---
2
+ import type { FrameHeaderElementProps } from '../types.js';
3
+
4
+ interface Props extends FrameHeaderElementProps {}
5
+
6
+ const { className, preserveAspectRatio = 'none', ...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-frame-header-${Math.random().toString(36).slice(2)}`;
11
+ const serializable = { ...settings };
12
+ ---
13
+
14
+ <svg
15
+ id={nodeId}
16
+ data-elyndra-frame-header
17
+ role="presentation"
18
+ class={className}
19
+ style="position: absolute; inset: 0; display: block; border: 0; margin: 0; padding: 0; width: 100%; height: 100%;"
20
+ xmlns="http://www.w3.org/2000/svg"
21
+ preserveAspectRatio={preserveAspectRatio}
22
+ >
23
+ <slot />
24
+ </svg>
25
+
26
+ <script define:vars={{ nodeId, serializable }}>
27
+ import { initFrameHeaderElement } from '../client/frameClient.js';
28
+
29
+ const element = document.getElementById(nodeId);
30
+ if (element instanceof SVGSVGElement) {
31
+ initFrameHeaderElement(element, serializable);
32
+ }
33
+ </script>
@@ -0,0 +1,15 @@
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 FrameHeader from './FrameHeader.astro';
7
+
8
+ test('Should render the frame header shell', async () => {
9
+ const html = await renderComponent(FrameHeader, {
10
+ props: { direction: 'vertical' },
11
+ });
12
+
13
+ expect(html).toContain('data-elyndra-frame-header');
14
+ expect(html).toContain('initFrameHeaderElement');
15
+ });
@@ -0,0 +1,33 @@
1
+ ---
2
+ import type { FrameKranoxElementProps } from '../types.js';
3
+
4
+ interface Props extends FrameKranoxElementProps {}
5
+
6
+ const { className, preserveAspectRatio = 'none', ...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-frame-kranox-${Math.random().toString(36).slice(2)}`;
11
+ const serializable = { ...settings };
12
+ ---
13
+
14
+ <svg
15
+ id={nodeId}
16
+ data-elyndra-frame-kranox
17
+ role="presentation"
18
+ class={className}
19
+ style="position: absolute; inset: 0; display: block; border: 0; margin: 0; padding: 0; width: 100%; height: 100%;"
20
+ xmlns="http://www.w3.org/2000/svg"
21
+ preserveAspectRatio={preserveAspectRatio}
22
+ >
23
+ <slot />
24
+ </svg>
25
+
26
+ <script define:vars={{ nodeId, serializable }}>
27
+ import { initFrameKranoxElement } from '../client/frameClient.js';
28
+
29
+ const element = document.getElementById(nodeId);
30
+ if (element instanceof SVGSVGElement) {
31
+ initFrameKranoxElement(element, serializable);
32
+ }
33
+ </script>
@@ -0,0 +1,15 @@
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 FrameKranox from './FrameKranox.astro';
7
+
8
+ test('Should render the frame kranox shell', async () => {
9
+ const html = await renderComponent(FrameKranox, {
10
+ props: { squareSize: 8 },
11
+ });
12
+
13
+ expect(html).toContain('data-elyndra-frame-kranox');
14
+ expect(html).toContain('initFrameKranoxElement');
15
+ });
@@ -0,0 +1,33 @@
1
+ ---
2
+ import type { FrameLinesElementProps } from '../types.js';
3
+
4
+ interface Props extends FrameLinesElementProps {}
5
+
6
+ const { className, preserveAspectRatio = 'none', ...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-frame-lines-${Math.random().toString(36).slice(2)}`;
11
+ const serializable = { ...settings };
12
+ ---
13
+
14
+ <svg
15
+ id={nodeId}
16
+ data-elyndra-frame-lines
17
+ role="presentation"
18
+ class={className}
19
+ style="position: absolute; inset: 0; display: block; border: 0; margin: 0; padding: 0; width: 100%; height: 100%;"
20
+ xmlns="http://www.w3.org/2000/svg"
21
+ preserveAspectRatio={preserveAspectRatio}
22
+ >
23
+ <slot />
24
+ </svg>
25
+
26
+ <script define:vars={{ nodeId, serializable }}>
27
+ import { initFrameLinesElement } from '../client/frameClient.js';
28
+
29
+ const element = document.getElementById(nodeId);
30
+ if (element instanceof SVGSVGElement) {
31
+ initFrameLinesElement(element, serializable);
32
+ }
33
+ </script>
@@ -0,0 +1,15 @@
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 FrameLines from './FrameLines.astro';
7
+
8
+ test('Should render the frame lines shell', async () => {
9
+ const html = await renderComponent(FrameLines, {
10
+ props: { smallLineLength: 8 },
11
+ });
12
+
13
+ expect(html).toContain('data-elyndra-frame-lines');
14
+ expect(html).toContain('initFrameLinesElement');
15
+ });
@@ -0,0 +1,33 @@
1
+ ---
2
+ import type { FrameNefrexElementProps } from '../types.js';
3
+
4
+ interface Props extends FrameNefrexElementProps {}
5
+
6
+ const { className, preserveAspectRatio = 'none', ...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-frame-nefrex-${Math.random().toString(36).slice(2)}`;
11
+ const serializable = { ...settings };
12
+ ---
13
+
14
+ <svg
15
+ id={nodeId}
16
+ data-elyndra-frame-nefrex
17
+ role="presentation"
18
+ class={className}
19
+ style="position: absolute; inset: 0; display: block; border: 0; margin: 0; padding: 0; width: 100%; height: 100%;"
20
+ xmlns="http://www.w3.org/2000/svg"
21
+ preserveAspectRatio={preserveAspectRatio}
22
+ >
23
+ <slot />
24
+ </svg>
25
+
26
+ <script define:vars={{ nodeId, serializable }}>
27
+ import { initFrameNefrexElement } from '../client/frameClient.js';
28
+
29
+ const element = document.getElementById(nodeId);
30
+ if (element instanceof SVGSVGElement) {
31
+ initFrameNefrexElement(element, serializable);
32
+ }
33
+ </script>
@@ -0,0 +1,15 @@
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 FrameNefrex from './FrameNefrex.astro';
7
+
8
+ test('Should render the frame nefrex shell', async () => {
9
+ const html = await renderComponent(FrameNefrex, {
10
+ props: { leftTop: false },
11
+ });
12
+
13
+ expect(html).toContain('data-elyndra-frame-nefrex');
14
+ expect(html).toContain('initFrameNefrexElement');
15
+ });
@@ -0,0 +1,33 @@
1
+ ---
2
+ import type { FrameNeroElementProps } from '../types.js';
3
+
4
+ interface Props extends FrameNeroElementProps {}
5
+
6
+ const { className, preserveAspectRatio = 'none', ...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-frame-nero-${Math.random().toString(36).slice(2)}`;
11
+ const serializable = { ...settings };
12
+ ---
13
+
14
+ <svg
15
+ id={nodeId}
16
+ data-elyndra-frame-nero
17
+ role="presentation"
18
+ class={className}
19
+ style="position: absolute; inset: 0; display: block; border: 0; margin: 0; padding: 0; width: 100%; height: 100%;"
20
+ xmlns="http://www.w3.org/2000/svg"
21
+ preserveAspectRatio={preserveAspectRatio}
22
+ >
23
+ <slot />
24
+ </svg>
25
+
26
+ <script define:vars={{ nodeId, serializable }}>
27
+ import { initFrameNeroElement } from '../client/frameClient.js';
28
+
29
+ const element = document.getElementById(nodeId);
30
+ if (element instanceof SVGSVGElement) {
31
+ initFrameNeroElement(element, serializable);
32
+ }
33
+ </script>
@@ -0,0 +1,15 @@
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 FrameNero from './FrameNero.astro';
7
+
8
+ test('Should render the frame nero shell', async () => {
9
+ const html = await renderComponent(FrameNero, {
10
+ props: { cornerLength: 8 },
11
+ });
12
+
13
+ expect(html).toContain('data-elyndra-frame-nero');
14
+ expect(html).toContain('initFrameNeroElement');
15
+ });
@@ -0,0 +1,33 @@
1
+ ---
2
+ import type { FrameOctagonElementProps } from '../types.js';
3
+
4
+ interface Props extends FrameOctagonElementProps {}
5
+
6
+ const { className, preserveAspectRatio = 'none', ...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-frame-octagon-${Math.random().toString(36).slice(2)}`;
11
+ const serializable = { ...settings };
12
+ ---
13
+
14
+ <svg
15
+ id={nodeId}
16
+ data-elyndra-frame-octagon
17
+ role="presentation"
18
+ class={className}
19
+ style="position: absolute; inset: 0; display: block; border: 0; margin: 0; padding: 0; width: 100%; height: 100%;"
20
+ xmlns="http://www.w3.org/2000/svg"
21
+ preserveAspectRatio={preserveAspectRatio}
22
+ >
23
+ <slot />
24
+ </svg>
25
+
26
+ <script define:vars={{ nodeId, serializable }}>
27
+ import { initFrameOctagonElement } from '../client/frameClient.js';
28
+
29
+ const element = document.getElementById(nodeId);
30
+ if (element instanceof SVGSVGElement) {
31
+ initFrameOctagonElement(element, serializable);
32
+ }
33
+ </script>
@@ -0,0 +1,15 @@
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 FrameOctagon from './FrameOctagon.astro';
7
+
8
+ test('Should render the frame octagon shell', async () => {
9
+ const html = await renderComponent(FrameOctagon, {
10
+ props: { squareSize: 8 },
11
+ });
12
+
13
+ expect(html).toContain('data-elyndra-frame-octagon');
14
+ expect(html).toContain('initFrameOctagonElement');
15
+ });
@@ -0,0 +1,33 @@
1
+ ---
2
+ import type { FrameUnderlineElementProps } from '../types.js';
3
+
4
+ interface Props extends FrameUnderlineElementProps {}
5
+
6
+ const { className, preserveAspectRatio = 'none', ...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-frame-underline-${Math.random().toString(36).slice(2)}`;
11
+ const serializable = { ...settings };
12
+ ---
13
+
14
+ <svg
15
+ id={nodeId}
16
+ data-elyndra-frame-underline
17
+ role="presentation"
18
+ class={className}
19
+ style="position: absolute; inset: 0; display: block; border: 0; margin: 0; padding: 0; width: 100%; height: 100%;"
20
+ xmlns="http://www.w3.org/2000/svg"
21
+ preserveAspectRatio={preserveAspectRatio}
22
+ >
23
+ <slot />
24
+ </svg>
25
+
26
+ <script define:vars={{ nodeId, serializable }}>
27
+ import { initFrameUnderlineElement } from '../client/frameClient.js';
28
+
29
+ const element = document.getElementById(nodeId);
30
+ if (element instanceof SVGSVGElement) {
31
+ initFrameUnderlineElement(element, serializable);
32
+ }
33
+ </script>
@@ -0,0 +1,15 @@
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 FrameUnderline from './FrameUnderline.astro';
7
+
8
+ test('Should render the frame underline shell', async () => {
9
+ const html = await renderComponent(FrameUnderline, {
10
+ props: { strokeWidth: 2 },
11
+ });
12
+
13
+ expect(html).toContain('data-elyndra-frame-underline');
14
+ expect(html).toContain('initFrameUnderlineElement');
15
+ });
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,92 @@
1
+ import { beforeEach, expect, test, vi } from 'vitest';
2
+ import {
3
+ initFrameAssemblerElement,
4
+ initFrameBaseElement,
5
+ initFrameCircleElement,
6
+ initFrameCornersElement,
7
+ initFrameHeaderElement,
8
+ initFrameKranoxElement,
9
+ initFrameLinesElement,
10
+ initFrameNefrexElement,
11
+ initFrameNeroElement,
12
+ initFrameOctagonElement,
13
+ initFrameUnderlineElement,
14
+ } from './frameClient.js';
15
+
16
+ const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
17
+
18
+ beforeEach(() => {
19
+ document.body.innerHTML = '';
20
+ vi.unstubAllGlobals();
21
+ // jsdom has no ResizeObserver: the vanilla frame observes resizes, so the
22
+ // tests stub the constructor (observe/disconnect no-ops).
23
+ vi.stubGlobal(
24
+ 'ResizeObserver',
25
+ class {
26
+ observe(): void {}
27
+ disconnect(): void {}
28
+ },
29
+ );
30
+ });
31
+
32
+ const mountSVG = (): SVGSVGElement => {
33
+ const element = document.createElementNS(SVG_NAMESPACE, 'svg');
34
+ document.body.appendChild(element);
35
+ return element;
36
+ };
37
+
38
+ test('Should render a static base frame and mark the node', () => {
39
+ const element = mountSVG();
40
+
41
+ const handle = initFrameBaseElement(element, { elements: [] });
42
+
43
+ expect(element.hasAttribute('data-elyndra-frame-node')).toBe(true);
44
+ expect(element.querySelector('[data-frame]')).not.toBeNull();
45
+
46
+ handle.dispose();
47
+ handle.dispose();
48
+
49
+ expect(element.hasAttribute('data-elyndra-frame-node')).toBe(false);
50
+ });
51
+
52
+ test('Should render every named frame statically and unmark on dispose', () => {
53
+ const inits = [
54
+ () => initFrameCircleElement(mountSVG(), { animated: false }),
55
+ () => initFrameCornersElement(mountSVG(), { animated: false }),
56
+ () => initFrameHeaderElement(mountSVG(), { animated: false }),
57
+ () => initFrameKranoxElement(mountSVG(), { animated: false }),
58
+ () => initFrameLinesElement(mountSVG(), { animated: false }),
59
+ () => initFrameNefrexElement(mountSVG(), { animated: false }),
60
+ () => initFrameNeroElement(mountSVG(), { animated: false }),
61
+ () => initFrameOctagonElement(mountSVG(), { animated: false }),
62
+ () => initFrameUnderlineElement(mountSVG(), { animated: false }),
63
+ ];
64
+
65
+ for (const init of inits) {
66
+ const handle = init();
67
+ const svg = document.body.lastElementChild;
68
+ expect(svg?.hasAttribute('data-elyndra-frame-node')).toBe(true);
69
+ handle.dispose();
70
+ expect(svg?.hasAttribute('data-elyndra-frame-node')).toBe(false);
71
+ }
72
+ });
73
+
74
+ test('Should dispose the previous frame on second init', () => {
75
+ const element = mountSVG();
76
+
77
+ initFrameCircleElement(element, { animated: false });
78
+ initFrameCircleElement(element, { animated: false });
79
+
80
+ expect(element.hasAttribute('data-elyndra-frame-node')).toBe(true);
81
+ });
82
+
83
+ test('Should run one assembly and cancel on dispose', () => {
84
+ const element = mountSVG();
85
+
86
+ const handle = initFrameAssemblerElement(element, { duration: 0.05, isEntering: true });
87
+
88
+ handle.dispose();
89
+ handle.dispose();
90
+
91
+ expect(element.hasAttribute('data-elyndra-frame-node')).toBe(false);
92
+ });
@@ -0,0 +1,186 @@
1
+ import {
2
+ type Frame,
3
+ animateFrameAssembler,
4
+ createFrame,
5
+ createFrameCircleSettings,
6
+ createFrameCornersSettings,
7
+ createFrameHeaderSettings,
8
+ createFrameKranoxSettings,
9
+ createFrameLinesSettings,
10
+ createFrameNefrexSettings,
11
+ createFrameNeroSettings,
12
+ createFrameOctagonSettings,
13
+ createFrameUnderlineSettings,
14
+ } from '@elyndra/frames';
15
+ import { filterProps } from '@elyndra/tools';
16
+ import type {
17
+ FrameAssemblerSerializableSettings,
18
+ FrameBaseSerializableSettings,
19
+ FrameCircleSerializableSettings,
20
+ FrameCornersSerializableSettings,
21
+ FrameHeaderSerializableSettings,
22
+ FrameKranoxSerializableSettings,
23
+ FrameLinesSerializableSettings,
24
+ FrameNefrexSerializableSettings,
25
+ FrameNeroSerializableSettings,
26
+ FrameOctagonSerializableSettings,
27
+ FrameUnderlineSerializableSettings,
28
+ } from '../types.js';
29
+
30
+ const FRAME_NODE_MARKER = 'data-elyndra-frame-node';
31
+
32
+ interface FrameHandle {
33
+ readonly frame: Frame;
34
+ readonly dispose: () => void;
35
+ }
36
+
37
+ interface FrameAssemblerHandle {
38
+ readonly dispose: () => void;
39
+ }
40
+
41
+ const framesByElement = new WeakMap<Element, { dispose: () => void }>();
42
+
43
+ const initFrameElement = (element: SVGSVGElement, create: () => Frame): FrameHandle => {
44
+ framesByElement.get(element)?.dispose();
45
+
46
+ const frame = create();
47
+
48
+ element.setAttribute(FRAME_NODE_MARKER, '');
49
+
50
+ const handle: FrameHandle = {
51
+ frame,
52
+ dispose: () => {
53
+ if (framesByElement.get(element) !== handle) {
54
+ return;
55
+ }
56
+ framesByElement.delete(element);
57
+ element.removeAttribute(FRAME_NODE_MARKER);
58
+ frame.remove();
59
+ },
60
+ };
61
+
62
+ framesByElement.set(element, handle);
63
+
64
+ return handle;
65
+ };
66
+
67
+ /**
68
+ * Hydrates one base frame: the shell `<svg>` itself is the vanilla target,
69
+ * islands run standalone (no animator join). Idempotent: re-running removes
70
+ * the previous frame first.
71
+ */
72
+ const initFrameBaseElement = (
73
+ element: SVGSVGElement,
74
+ props: FrameBaseSerializableSettings,
75
+ ): FrameHandle =>
76
+ initFrameElement(element, () =>
77
+ createFrame(element, {
78
+ elements: props.elements ?? [],
79
+ ...filterProps({ contexts: props.contexts }),
80
+ }),
81
+ );
82
+
83
+ const initFrameCircleElement = (
84
+ element: SVGSVGElement,
85
+ props: FrameCircleSerializableSettings,
86
+ ): FrameHandle =>
87
+ initFrameElement(element, () => createFrame(element, createFrameCircleSettings(filterProps(props))));
88
+
89
+ const initFrameCornersElement = (
90
+ element: SVGSVGElement,
91
+ props: FrameCornersSerializableSettings,
92
+ ): FrameHandle =>
93
+ initFrameElement(element, () => createFrame(element, createFrameCornersSettings(filterProps(props))));
94
+
95
+ const initFrameHeaderElement = (
96
+ element: SVGSVGElement,
97
+ props: FrameHeaderSerializableSettings,
98
+ ): FrameHandle =>
99
+ initFrameElement(element, () => createFrame(element, createFrameHeaderSettings(filterProps(props))));
100
+
101
+ const initFrameKranoxElement = (
102
+ element: SVGSVGElement,
103
+ props: FrameKranoxSerializableSettings,
104
+ ): FrameHandle =>
105
+ initFrameElement(element, () => createFrame(element, createFrameKranoxSettings(filterProps(props))));
106
+
107
+ const initFrameLinesElement = (
108
+ element: SVGSVGElement,
109
+ props: FrameLinesSerializableSettings,
110
+ ): FrameHandle =>
111
+ initFrameElement(element, () => createFrame(element, createFrameLinesSettings(filterProps(props))));
112
+
113
+ const initFrameNefrexElement = (
114
+ element: SVGSVGElement,
115
+ props: FrameNefrexSerializableSettings,
116
+ ): FrameHandle =>
117
+ initFrameElement(element, () => createFrame(element, createFrameNefrexSettings(filterProps(props))));
118
+
119
+ const initFrameNeroElement = (
120
+ element: SVGSVGElement,
121
+ props: FrameNeroSerializableSettings,
122
+ ): FrameHandle =>
123
+ initFrameElement(element, () => createFrame(element, createFrameNeroSettings(filterProps(props))));
124
+
125
+ const initFrameOctagonElement = (
126
+ element: SVGSVGElement,
127
+ props: FrameOctagonSerializableSettings,
128
+ ): FrameHandle =>
129
+ initFrameElement(element, () => createFrame(element, createFrameOctagonSettings(filterProps(props))));
130
+
131
+ const initFrameUnderlineElement = (
132
+ element: SVGSVGElement,
133
+ props: FrameUnderlineSerializableSettings,
134
+ ): FrameHandle =>
135
+ initFrameElement(element, () =>
136
+ createFrame(element, createFrameUnderlineSettings(filterProps(props))),
137
+ );
138
+
139
+ /**
140
+ * Runs one frame assembly on an element (the island replacement for the
141
+ * `useFrameAssembler` hook: a single run instead of an animator
142
+ * subscription). Idempotent: re-running cancels the previous assembly
143
+ * first.
144
+ */
145
+ const initFrameAssemblerElement = (
146
+ element: HTMLElement | SVGElement,
147
+ props: FrameAssemblerSerializableSettings,
148
+ ): FrameAssemblerHandle => {
149
+ framesByElement.get(element)?.dispose();
150
+
151
+ const animation = animateFrameAssembler({
152
+ element,
153
+ duration: props.duration ?? 0.3,
154
+ isEntering: props.isEntering ?? true,
155
+ });
156
+
157
+ const handle: FrameAssemblerHandle = {
158
+ dispose: () => {
159
+ if (framesByElement.get(element) !== handle) {
160
+ return;
161
+ }
162
+ framesByElement.delete(element);
163
+ animation.cancel();
164
+ },
165
+ };
166
+
167
+ framesByElement.set(element, handle);
168
+
169
+ return handle;
170
+ };
171
+
172
+ export {
173
+ FRAME_NODE_MARKER,
174
+ initFrameAssemblerElement,
175
+ initFrameBaseElement,
176
+ initFrameCircleElement,
177
+ initFrameCornersElement,
178
+ initFrameHeaderElement,
179
+ initFrameKranoxElement,
180
+ initFrameLinesElement,
181
+ initFrameNefrexElement,
182
+ initFrameNeroElement,
183
+ initFrameOctagonElement,
184
+ initFrameUnderlineElement,
185
+ };
186
+ export type { FrameAssemblerHandle, FrameHandle };
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './client/frameClient.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,100 @@
1
+ import type {
2
+ CreateFrameCircleSettingsProps,
3
+ CreateFrameCornersSettingsProps,
4
+ CreateFrameHeaderSettingsProps,
5
+ CreateFrameKranoxSettingsProps,
6
+ CreateFrameLinesSettingsProps,
7
+ CreateFrameNefrexSettingsProps,
8
+ CreateFrameNeroSettingsProps,
9
+ CreateFrameOctagonSettingsProps,
10
+ CreateFrameUnderlineSettingsProps,
11
+ FrameSettings,
12
+ } from '@elyndra/frames';
13
+
14
+ /**
15
+ * Serializable subsets of the frame settings. Only JSON crosses the Astro
16
+ * server/client boundary (`define:vars`):
17
+ * - `FrameBase` takes plain `elements`/`contexts`: `draw`/`path`/context
18
+ * `animate` function forms are excluded (static settings only).
19
+ * - Variant props (`CreateFrame*SettingsProps`) are already scalar-only.
20
+ * - Excluded everywhere: `animator` nodes (islands run standalone),
21
+ * `container` instances (the vanilla container is created internally),
22
+ * `elementRef` (the shell `<svg>` itself is hydrated), `useFrameAssembler`
23
+ * (hook: use `initFrameAssemblerElement` from the client entry instead).
24
+ */
25
+ interface FrameBaseSerializableSettings {
26
+ elements?: FrameSettings['elements'];
27
+ contexts?: Record<string, string>;
28
+ }
29
+
30
+ interface FrameBaseElementProps extends FrameBaseSerializableSettings {
31
+ className?: string;
32
+ preserveAspectRatio?: string;
33
+ }
34
+
35
+ interface FrameShellProps {
36
+ className?: string;
37
+ preserveAspectRatio?: string;
38
+ }
39
+
40
+ type FrameCircleSerializableSettings = CreateFrameCircleSettingsProps;
41
+ interface FrameCircleElementProps extends FrameCircleSerializableSettings, FrameShellProps {}
42
+
43
+ type FrameCornersSerializableSettings = CreateFrameCornersSettingsProps;
44
+ interface FrameCornersElementProps extends FrameCornersSerializableSettings, FrameShellProps {}
45
+
46
+ type FrameHeaderSerializableSettings = CreateFrameHeaderSettingsProps;
47
+ interface FrameHeaderElementProps extends FrameHeaderSerializableSettings, FrameShellProps {}
48
+
49
+ type FrameKranoxSerializableSettings = CreateFrameKranoxSettingsProps;
50
+ interface FrameKranoxElementProps extends FrameKranoxSerializableSettings, FrameShellProps {}
51
+
52
+ type FrameLinesSerializableSettings = CreateFrameLinesSettingsProps;
53
+ interface FrameLinesElementProps extends FrameLinesSerializableSettings, FrameShellProps {}
54
+
55
+ type FrameNefrexSerializableSettings = CreateFrameNefrexSettingsProps;
56
+ interface FrameNefrexElementProps extends FrameNefrexSerializableSettings, FrameShellProps {}
57
+
58
+ type FrameNeroSerializableSettings = CreateFrameNeroSettingsProps;
59
+ interface FrameNeroElementProps extends FrameNeroSerializableSettings, FrameShellProps {}
60
+
61
+ type FrameOctagonSerializableSettings = CreateFrameOctagonSettingsProps;
62
+ interface FrameOctagonElementProps extends FrameOctagonSerializableSettings, FrameShellProps {}
63
+
64
+ type FrameUnderlineSerializableSettings = CreateFrameUnderlineSettingsProps;
65
+ interface FrameUnderlineElementProps extends FrameUnderlineSerializableSettings, FrameShellProps {}
66
+
67
+ /**
68
+ * Serializable settings for the assembler runner (the island replacement
69
+ * for the `useFrameAssembler` hook): a single entering/exiting assembly
70
+ * instead of an animator subscription.
71
+ */
72
+ interface FrameAssemblerSerializableSettings {
73
+ duration?: number;
74
+ isEntering?: boolean;
75
+ }
76
+
77
+ export type {
78
+ FrameAssemblerSerializableSettings,
79
+ FrameBaseElementProps,
80
+ FrameBaseSerializableSettings,
81
+ FrameCircleElementProps,
82
+ FrameCircleSerializableSettings,
83
+ FrameCornersElementProps,
84
+ FrameCornersSerializableSettings,
85
+ FrameHeaderElementProps,
86
+ FrameHeaderSerializableSettings,
87
+ FrameKranoxElementProps,
88
+ FrameKranoxSerializableSettings,
89
+ FrameLinesElementProps,
90
+ FrameLinesSerializableSettings,
91
+ FrameNefrexElementProps,
92
+ FrameNefrexSerializableSettings,
93
+ FrameNeroElementProps,
94
+ FrameNeroSerializableSettings,
95
+ FrameOctagonElementProps,
96
+ FrameOctagonSerializableSettings,
97
+ FrameShellProps,
98
+ FrameUnderlineElementProps,
99
+ FrameUnderlineSerializableSettings,
100
+ };