@elyndra/svelte-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.
Files changed (37) hide show
  1. package/package.json +51 -0
  2. package/src/FrameBase/FrameBase.svelte +74 -0
  3. package/src/FrameBase/index.ts +2 -0
  4. package/src/FrameBase/types.ts +17 -0
  5. package/src/FrameCircle/FrameCircle.svelte +32 -0
  6. package/src/FrameCircle/index.ts +2 -0
  7. package/src/FrameCircle/types.ts +6 -0
  8. package/src/FrameCorners/FrameCorners.svelte +31 -0
  9. package/src/FrameCorners/index.ts +2 -0
  10. package/src/FrameCorners/types.ts +6 -0
  11. package/src/FrameHeader/FrameHeader.svelte +34 -0
  12. package/src/FrameHeader/index.ts +2 -0
  13. package/src/FrameHeader/types.ts +6 -0
  14. package/src/FrameKranox/FrameKranox.svelte +34 -0
  15. package/src/FrameKranox/index.ts +2 -0
  16. package/src/FrameKranox/types.ts +6 -0
  17. package/src/FrameLines/FrameLines.svelte +32 -0
  18. package/src/FrameLines/index.ts +2 -0
  19. package/src/FrameLines/types.ts +6 -0
  20. package/src/FrameNefrex/FrameNefrex.svelte +37 -0
  21. package/src/FrameNefrex/index.ts +2 -0
  22. package/src/FrameNefrex/types.ts +6 -0
  23. package/src/FrameNero/FrameNero.svelte +31 -0
  24. package/src/FrameNero/index.ts +2 -0
  25. package/src/FrameNero/types.ts +6 -0
  26. package/src/FrameOctagon/FrameOctagon.svelte +35 -0
  27. package/src/FrameOctagon/index.ts +2 -0
  28. package/src/FrameOctagon/types.ts +6 -0
  29. package/src/FrameUnderline/FrameUnderline.svelte +30 -0
  30. package/src/FrameUnderline/FrameUnderline.test.ts +28 -0
  31. package/src/FrameUnderline/index.ts +2 -0
  32. package/src/FrameUnderline/types.ts +6 -0
  33. package/src/index.ts +11 -0
  34. package/src/internal/styles.ts +54 -0
  35. package/src/svelte.d.ts +7 -0
  36. package/src/useFrameAssembler/index.ts +1 -0
  37. package/src/useFrameAssembler/useFrameAssembler.svelte.ts +55 -0
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@elyndra/svelte-frames",
3
+ "version": "1.2.0",
4
+ "sideEffects": false,
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "description": "Elyndra frames components for Svelte",
9
+ "keywords": [
10
+ "elyndra",
11
+ "ui",
12
+ "front-end",
13
+ "framework",
14
+ "scifi",
15
+ "sci-fi",
16
+ "science-fiction",
17
+ "svelte"
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
+ },
35
+ "types": "./src/index.ts",
36
+ "module": "./src/index.ts",
37
+ "main": "./src/index.ts",
38
+ "peerDependencies": {
39
+ "svelte": "5.57.0"
40
+ },
41
+ "dependencies": {
42
+ "@elyndra/frames": "^1.2.0",
43
+ "@elyndra/svelte-animator": "^1.2.0",
44
+ "@elyndra/svelte-tools": "^1.2.0",
45
+ "@elyndra/tools": "^1.2.0",
46
+ "tslib": "2.8.1"
47
+ },
48
+ "scripts": {
49
+ "build": "tsc --noEmit -p tsconfig.json && svelte-check --tsconfig tsconfig.json --output machine"
50
+ }
51
+ }
@@ -0,0 +1,74 @@
1
+ <script lang="ts">
2
+ import { createFrame, type Frame, type FrameSettings } from '@elyndra/frames';
3
+ import { useAnimator } from '@elyndra/svelte-animator';
4
+ import { useUpdateEffect } from '@elyndra/svelte-tools';
5
+ import { cx } from '@elyndra/tools';
6
+ import { positionedStyleText, toStyleText } from '../internal/styles.js';
7
+ import type { FrameBaseProps } from './types.js';
8
+
9
+ // No destructured rest: only direct `props.*` reads stay reactive.
10
+ const props: FrameBaseProps = $props();
11
+
12
+ const getAnimator = useAnimator();
13
+ let svg: SVGSVGElement | null = $state(null);
14
+ let frame: Frame | null = null;
15
+ // Intentionally captures the initial settings object; updates flow through
16
+ // the `$effect` + `useUpdateEffect` below (Solid does the same).
17
+ // svelte-ignore state_referenced_locally
18
+ const settingsRef: { current: FrameSettings } = { current: props.settings };
19
+
20
+ $effect(() => {
21
+ Object.assign(settingsRef.current, props.settings, { animator: getAnimator?.()?.node });
22
+ });
23
+
24
+ $effect(() => {
25
+ // Rebuild the frame when the animator interface changes, full teardown +
26
+ // rebuild. Settings-only changes take the light path below, so animator
27
+ // changes must NOT join those deps or they would render() a
28
+ // just-removed frame.
29
+ const animator = getAnimator?.();
30
+ void animator;
31
+
32
+ const target = svg;
33
+
34
+ if (!target) {
35
+ return;
36
+ }
37
+
38
+ frame = createFrame(target, settingsRef.current);
39
+
40
+ return () => frame?.remove();
41
+ });
42
+
43
+ // NOTE: animator changes must NOT join these deps. They are already handled
44
+ // above with full teardown + rebuild; adding them here would render() a
45
+ // just-removed frame. Settings changes take the light path only.
46
+ useUpdateEffect(() => {
47
+ frame?.render();
48
+ }, [() => props.settings]);
49
+
50
+ $effect(() => {
51
+ const target = svg;
52
+ const ref = props.elementRef;
53
+
54
+ if (typeof ref === 'function') {
55
+ ref(target);
56
+ } else if (ref) {
57
+ ref.current = target;
58
+ }
59
+ });
60
+
61
+ const styleText = $derived(toStyleText((props.positioned ?? true) ? positionedStyleText : undefined, props.style));
62
+ </script>
63
+
64
+ <svg
65
+ role="presentation"
66
+ bind:this={svg}
67
+ id={props.id}
68
+ class={cx('elyndra-frames-frame', props.className)}
69
+ style={styleText}
70
+ xmlns="http://www.w3.org/2000/svg"
71
+ preserveAspectRatio={props.preserveAspectRatio ?? 'none'}
72
+ >
73
+ {@render props.children?.()}
74
+ </svg>
@@ -0,0 +1,2 @@
1
+ export { default as FrameBase } from './FrameBase.svelte';
2
+ export type * from './types.js';
@@ -0,0 +1,17 @@
1
+ import type { FrameSettings } from '@elyndra/frames';
2
+ import type { Snippet } from 'svelte';
3
+
4
+ type ElementRef<E> = ((element: E | null) => void) | { current: E | null };
5
+
6
+ type FrameBaseProps = {
7
+ elementRef?: ElementRef<SVGSVGElement>;
8
+ positioned?: boolean;
9
+ settings: FrameSettings;
10
+ className?: string;
11
+ style?: string | Record<string, string | number | undefined>;
12
+ preserveAspectRatio?: string;
13
+ id?: string;
14
+ children?: Snippet;
15
+ };
16
+
17
+ export type { FrameBaseProps };
@@ -0,0 +1,32 @@
1
+ <script lang="ts">
2
+ import { createFrameCircleSettings } from '@elyndra/frames';
3
+ import { cx } from '@elyndra/tools';
4
+ import FrameBase from '../FrameBase/FrameBase.svelte';
5
+ import type { FrameCircleProps } from './types.js';
6
+
7
+ // No destructured rest: only direct `props.*` reads stay reactive.
8
+ const props: FrameCircleProps = $props();
9
+
10
+ const settings = $derived.by(() =>
11
+ createFrameCircleSettings({
12
+ styled: props.styled,
13
+ animated: props.animated,
14
+ padding: props.padding,
15
+ strokeWidth: props.strokeWidth,
16
+ separation: props.separation,
17
+ sideWidth: props.sideWidth,
18
+ }),
19
+ );
20
+ </script>
21
+
22
+ <FrameBase
23
+ elementRef={props.elementRef}
24
+ positioned={props.positioned}
25
+ {settings}
26
+ className={cx('elyndra-frames-framecircle', props.className)}
27
+ style={props.style}
28
+ preserveAspectRatio={props.preserveAspectRatio}
29
+ id={props.id}
30
+ >
31
+ {@render props.children?.()}
32
+ </FrameBase>
@@ -0,0 +1,2 @@
1
+ export { default as FrameCircle } from './FrameCircle.svelte';
2
+ export type * from './types.js';
@@ -0,0 +1,6 @@
1
+ import type { CreateFrameCircleSettingsProps } from '@elyndra/frames';
2
+ import type { FrameBaseProps } from '../FrameBase/index.js';
3
+
4
+ type FrameCircleProps = Omit<FrameBaseProps, 'settings'> & CreateFrameCircleSettingsProps;
5
+
6
+ export type { FrameCircleProps };
@@ -0,0 +1,31 @@
1
+ <script lang="ts">
2
+ import { createFrameCornersSettings } from '@elyndra/frames';
3
+ import { cx } from '@elyndra/tools';
4
+ import FrameBase from '../FrameBase/FrameBase.svelte';
5
+ import type { FrameCornersProps } from './types.js';
6
+
7
+ // No destructured rest: only direct `props.*` reads stay reactive.
8
+ const props: FrameCornersProps = $props();
9
+
10
+ const settings = $derived.by(() =>
11
+ createFrameCornersSettings({
12
+ styled: props.styled,
13
+ animated: props.animated,
14
+ padding: props.padding,
15
+ strokeWidth: props.strokeWidth,
16
+ cornerLength: props.cornerLength,
17
+ }),
18
+ );
19
+ </script>
20
+
21
+ <FrameBase
22
+ elementRef={props.elementRef}
23
+ positioned={props.positioned}
24
+ {settings}
25
+ className={cx('elyndra-frames-framecorners', props.className)}
26
+ style={props.style}
27
+ preserveAspectRatio={props.preserveAspectRatio}
28
+ id={props.id}
29
+ >
30
+ {@render props.children?.()}
31
+ </FrameBase>
@@ -0,0 +1,2 @@
1
+ export { default as FrameCorners } from './FrameCorners.svelte';
2
+ export type * from './types.js';
@@ -0,0 +1,6 @@
1
+ import type { CreateFrameCornersSettingsProps } from '@elyndra/frames';
2
+ import type { FrameBaseProps } from '../FrameBase/index.js';
3
+
4
+ type FrameCornersProps = Omit<FrameBaseProps, 'settings'> & CreateFrameCornersSettingsProps;
5
+
6
+ export type { FrameCornersProps };
@@ -0,0 +1,34 @@
1
+ <script lang="ts">
2
+ import { createFrameHeaderSettings } from '@elyndra/frames';
3
+ import { cx } from '@elyndra/tools';
4
+ import FrameBase from '../FrameBase/FrameBase.svelte';
5
+ import type { FrameHeaderProps } from './types.js';
6
+
7
+ // No destructured rest: only direct `props.*` reads stay reactive.
8
+ const props: FrameHeaderProps = $props();
9
+
10
+ const settings = $derived.by(() =>
11
+ createFrameHeaderSettings({
12
+ styled: props.styled,
13
+ animated: props.animated,
14
+ padding: props.padding,
15
+ strokeWidth: props.strokeWidth,
16
+ decoWidth: props.decoWidth,
17
+ direction: props.direction,
18
+ align: props.align,
19
+ contentLength: props.contentLength,
20
+ }),
21
+ );
22
+ </script>
23
+
24
+ <FrameBase
25
+ elementRef={props.elementRef}
26
+ positioned={props.positioned}
27
+ {settings}
28
+ className={cx('elyndra-frames-frameheader', props.className)}
29
+ style={props.style}
30
+ preserveAspectRatio={props.preserveAspectRatio}
31
+ id={props.id}
32
+ >
33
+ {@render props.children?.()}
34
+ </FrameBase>
@@ -0,0 +1,2 @@
1
+ export { default as FrameHeader } from './FrameHeader.svelte';
2
+ export type * from './types.js';
@@ -0,0 +1,6 @@
1
+ import type { CreateFrameHeaderSettingsProps } from '@elyndra/frames';
2
+ import type { FrameBaseProps } from '../FrameBase/index.js';
3
+
4
+ type FrameHeaderProps = Omit<FrameBaseProps, 'settings'> & CreateFrameHeaderSettingsProps;
5
+
6
+ export type { FrameHeaderProps };
@@ -0,0 +1,34 @@
1
+ <script lang="ts">
2
+ import { createFrameKranoxSettings } from '@elyndra/frames';
3
+ import { cx } from '@elyndra/tools';
4
+ import FrameBase from '../FrameBase/FrameBase.svelte';
5
+ import type { FrameKranoxProps } from './types.js';
6
+
7
+ // No destructured rest: only direct `props.*` reads stay reactive.
8
+ const props: FrameKranoxProps = $props();
9
+
10
+ const settings = $derived.by(() =>
11
+ createFrameKranoxSettings({
12
+ styled: props.styled,
13
+ animated: props.animated,
14
+ padding: props.padding,
15
+ strokeWidth: props.strokeWidth,
16
+ bgStrokeWidth: props.bgStrokeWidth,
17
+ squareSize: props.squareSize,
18
+ smallLineLength: props.smallLineLength,
19
+ largeLineLength: props.largeLineLength,
20
+ }),
21
+ );
22
+ </script>
23
+
24
+ <FrameBase
25
+ elementRef={props.elementRef}
26
+ positioned={props.positioned}
27
+ {settings}
28
+ className={cx('elyndra-frames-framekranox', props.className)}
29
+ style={props.style}
30
+ preserveAspectRatio={props.preserveAspectRatio}
31
+ id={props.id}
32
+ >
33
+ {@render props.children?.()}
34
+ </FrameBase>
@@ -0,0 +1,2 @@
1
+ export { default as FrameKranox } from './FrameKranox.svelte';
2
+ export type * from './types.js';
@@ -0,0 +1,6 @@
1
+ import type { CreateFrameKranoxSettingsProps } from '@elyndra/frames';
2
+ import type { FrameBaseProps } from '../FrameBase/index.js';
3
+
4
+ type FrameKranoxProps = Omit<FrameBaseProps, 'settings'> & CreateFrameKranoxSettingsProps;
5
+
6
+ export type { FrameKranoxProps };
@@ -0,0 +1,32 @@
1
+ <script lang="ts">
2
+ import { createFrameLinesSettings } from '@elyndra/frames';
3
+ import { cx } from '@elyndra/tools';
4
+ import FrameBase from '../FrameBase/FrameBase.svelte';
5
+ import type { FrameLinesProps } from './types.js';
6
+
7
+ // No destructured rest: only direct `props.*` reads stay reactive.
8
+ const props: FrameLinesProps = $props();
9
+
10
+ const settings = $derived.by(() =>
11
+ createFrameLinesSettings({
12
+ styled: props.styled,
13
+ animated: props.animated,
14
+ padding: props.padding,
15
+ largeLineWidth: props.largeLineWidth,
16
+ smallLineWidth: props.smallLineWidth,
17
+ smallLineLength: props.smallLineLength,
18
+ }),
19
+ );
20
+ </script>
21
+
22
+ <FrameBase
23
+ elementRef={props.elementRef}
24
+ positioned={props.positioned}
25
+ {settings}
26
+ className={cx('elyndra-frames-framelines', props.className)}
27
+ style={props.style}
28
+ preserveAspectRatio={props.preserveAspectRatio}
29
+ id={props.id}
30
+ >
31
+ {@render props.children?.()}
32
+ </FrameBase>
@@ -0,0 +1,2 @@
1
+ export { default as FrameLines } from './FrameLines.svelte';
2
+ export type * from './types.js';
@@ -0,0 +1,6 @@
1
+ import type { CreateFrameLinesSettingsProps } from '@elyndra/frames';
2
+ import type { FrameBaseProps } from '../FrameBase/index.js';
3
+
4
+ type FrameLinesProps = Omit<FrameBaseProps, 'settings'> & CreateFrameLinesSettingsProps;
5
+
6
+ export type { FrameLinesProps };
@@ -0,0 +1,37 @@
1
+ <script lang="ts">
2
+ import { createFrameNefrexSettings } from '@elyndra/frames';
3
+ import { cx } from '@elyndra/tools';
4
+ import FrameBase from '../FrameBase/FrameBase.svelte';
5
+ import type { FrameNefrexProps } from './types.js';
6
+
7
+ // No destructured rest: only direct `props.*` reads stay reactive.
8
+ const props: FrameNefrexProps = $props();
9
+
10
+ const settings = $derived.by(() =>
11
+ createFrameNefrexSettings({
12
+ styled: props.styled,
13
+ animated: props.animated,
14
+ padding: props.padding,
15
+ leftTop: props.leftTop,
16
+ leftBottom: props.leftBottom,
17
+ rightTop: props.rightTop,
18
+ rightBottom: props.rightBottom,
19
+ squareSize: props.squareSize,
20
+ strokeWidth: props.strokeWidth,
21
+ smallLineLength: props.smallLineLength,
22
+ largeLineLength: props.largeLineLength,
23
+ }),
24
+ );
25
+ </script>
26
+
27
+ <FrameBase
28
+ elementRef={props.elementRef}
29
+ positioned={props.positioned}
30
+ {settings}
31
+ className={cx('elyndra-frames-framenefrex', props.className)}
32
+ style={props.style}
33
+ preserveAspectRatio={props.preserveAspectRatio}
34
+ id={props.id}
35
+ >
36
+ {@render props.children?.()}
37
+ </FrameBase>
@@ -0,0 +1,2 @@
1
+ export { default as FrameNefrex } from './FrameNefrex.svelte';
2
+ export type * from './types.js';
@@ -0,0 +1,6 @@
1
+ import type { CreateFrameNefrexSettingsProps } from '@elyndra/frames';
2
+ import type { FrameBaseProps } from '../FrameBase/index.js';
3
+
4
+ type FrameNefrexProps = Omit<FrameBaseProps, 'settings'> & CreateFrameNefrexSettingsProps;
5
+
6
+ export type { FrameNefrexProps };
@@ -0,0 +1,31 @@
1
+ <script lang="ts">
2
+ import { createFrameNeroSettings } from '@elyndra/frames';
3
+ import { cx } from '@elyndra/tools';
4
+ import FrameBase from '../FrameBase/FrameBase.svelte';
5
+ import type { FrameNeroProps } from './types.js';
6
+
7
+ // No destructured rest: only direct `props.*` reads stay reactive.
8
+ const props: FrameNeroProps = $props();
9
+
10
+ const settings = $derived.by(() =>
11
+ createFrameNeroSettings({
12
+ styled: props.styled,
13
+ animated: props.animated,
14
+ padding: props.padding,
15
+ cornerLength: props.cornerLength,
16
+ cornerWidth: props.cornerWidth,
17
+ }),
18
+ );
19
+ </script>
20
+
21
+ <FrameBase
22
+ elementRef={props.elementRef}
23
+ positioned={props.positioned}
24
+ {settings}
25
+ className={cx('elyndra-frames-framenero', props.className)}
26
+ style={props.style}
27
+ preserveAspectRatio={props.preserveAspectRatio}
28
+ id={props.id}
29
+ >
30
+ {@render props.children?.()}
31
+ </FrameBase>
@@ -0,0 +1,2 @@
1
+ export { default as FrameNero } from './FrameNero.svelte';
2
+ export type * from './types.js';
@@ -0,0 +1,6 @@
1
+ import type { CreateFrameNeroSettingsProps } from '@elyndra/frames';
2
+ import type { FrameBaseProps } from '../FrameBase/index.js';
3
+
4
+ type FrameNeroProps = Omit<FrameBaseProps, 'settings'> & CreateFrameNeroSettingsProps;
5
+
6
+ export type { FrameNeroProps };
@@ -0,0 +1,35 @@
1
+ <script lang="ts">
2
+ import { createFrameOctagonSettings } from '@elyndra/frames';
3
+ import { cx } from '@elyndra/tools';
4
+ import FrameBase from '../FrameBase/FrameBase.svelte';
5
+ import type { FrameOctagonProps } from './types.js';
6
+
7
+ // No destructured rest: only direct `props.*` reads stay reactive.
8
+ const props: FrameOctagonProps = $props();
9
+
10
+ const settings = $derived.by(() =>
11
+ createFrameOctagonSettings({
12
+ styled: props.styled,
13
+ animated: props.animated,
14
+ padding: props.padding,
15
+ leftTop: props.leftTop,
16
+ rightTop: props.rightTop,
17
+ rightBottom: props.rightBottom,
18
+ leftBottom: props.leftBottom,
19
+ squareSize: props.squareSize,
20
+ strokeWidth: props.strokeWidth,
21
+ }),
22
+ );
23
+ </script>
24
+
25
+ <FrameBase
26
+ elementRef={props.elementRef}
27
+ positioned={props.positioned}
28
+ {settings}
29
+ className={cx('elyndra-frames-frameoctagon', props.className)}
30
+ style={props.style}
31
+ preserveAspectRatio={props.preserveAspectRatio}
32
+ id={props.id}
33
+ >
34
+ {@render props.children?.()}
35
+ </FrameBase>
@@ -0,0 +1,2 @@
1
+ export { default as FrameOctagon } from './FrameOctagon.svelte';
2
+ export type * from './types.js';
@@ -0,0 +1,6 @@
1
+ import type { CreateFrameOctagonSettingsProps } from '@elyndra/frames';
2
+ import type { FrameBaseProps } from '../FrameBase/index.js';
3
+
4
+ type FrameOctagonProps = Omit<FrameBaseProps, 'settings'> & CreateFrameOctagonSettingsProps;
5
+
6
+ export type { FrameOctagonProps };
@@ -0,0 +1,30 @@
1
+ <script lang="ts">
2
+ import { createFrameUnderlineSettings } from '@elyndra/frames';
3
+ import { cx } from '@elyndra/tools';
4
+ import FrameBase from '../FrameBase/FrameBase.svelte';
5
+ import type { FrameUnderlineProps } from './types.js';
6
+
7
+ // No destructured rest: only direct `props.*` reads stay reactive.
8
+ const props: FrameUnderlineProps = $props();
9
+
10
+ const settings = $derived.by(() =>
11
+ createFrameUnderlineSettings({
12
+ styled: props.styled,
13
+ squareSize: props.squareSize,
14
+ strokeWidth: props.strokeWidth,
15
+ padding: props.padding,
16
+ }),
17
+ );
18
+ </script>
19
+
20
+ <FrameBase
21
+ elementRef={props.elementRef}
22
+ positioned={props.positioned}
23
+ {settings}
24
+ className={cx('elyndra-frames-frameunderline', props.className)}
25
+ style={props.style}
26
+ preserveAspectRatio={props.preserveAspectRatio}
27
+ id={props.id}
28
+ >
29
+ {@render props.children?.()}
30
+ </FrameBase>
@@ -0,0 +1,28 @@
1
+ import { render } from '@testing-library/svelte';
2
+ import type { Component } from 'svelte';
3
+ import { afterEach, beforeEach, expect, test } from 'vitest';
4
+ import FrameUnderline from './FrameUnderline.svelte';
5
+
6
+ beforeEach(() => {
7
+ class ResizeObserver {
8
+ observe(): void {}
9
+ unobserve(): void {}
10
+ disconnect(): void {}
11
+ }
12
+ window.ResizeObserver = ResizeObserver as unknown as typeof window.ResizeObserver;
13
+ });
14
+
15
+ afterEach(() => {
16
+ window.ResizeObserver = undefined as unknown as typeof window.ResizeObserver;
17
+ });
18
+
19
+ // `.svelte` components are `unknown` in-repo (ambient decl, tests only).
20
+ const frameUnderline = FrameUnderline as Component;
21
+
22
+ test('Should render svg element with elyndra classes', () => {
23
+ const { container } = render(frameUnderline);
24
+ const element = container.querySelector('svg') as Element;
25
+ expect(element.tagName).toBe('svg');
26
+ expect(element.classList.contains('elyndra-frames-frame')).toBe(true);
27
+ expect(element.classList.contains('elyndra-frames-frameunderline')).toBe(true);
28
+ });
@@ -0,0 +1,2 @@
1
+ export { default as FrameUnderline } from './FrameUnderline.svelte';
2
+ export type * from './types.js';
@@ -0,0 +1,6 @@
1
+ import type { CreateFrameUnderlineSettingsProps } from '@elyndra/frames';
2
+ import type { FrameBaseProps } from '../FrameBase/index.js';
3
+
4
+ type FrameUnderlineProps = Omit<FrameBaseProps, 'settings'> & CreateFrameUnderlineSettingsProps;
5
+
6
+ export type { FrameUnderlineProps };
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ export * from './FrameBase/index.js';
2
+ export * from './FrameCircle/index.js';
3
+ export * from './FrameCorners/index.js';
4
+ export * from './FrameHeader/index.js';
5
+ export * from './FrameKranox/index.js';
6
+ export * from './FrameLines/index.js';
7
+ export * from './FrameNefrex/index.js';
8
+ export * from './FrameNero/index.js';
9
+ export * from './FrameOctagon/index.js';
10
+ export * from './FrameUnderline/index.js';
11
+ export * from './useFrameAssembler/index.js';
@@ -0,0 +1,54 @@
1
+ type StyleValue = string | number | undefined;
2
+ type StyleInput = string | Record<string, StyleValue> | undefined;
3
+
4
+ const toKebabCase = (key: string): string => key.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
5
+
6
+ const stringifyStyleObject = (style: Record<string, StyleValue>): string => {
7
+ const parts: string[] = [];
8
+
9
+ for (const [key, value] of Object.entries(style)) {
10
+ if (value === undefined || value === null || value === '') {
11
+ continue;
12
+ }
13
+
14
+ parts.push(`${toKebabCase(key)}: ${value}`);
15
+ }
16
+
17
+ return parts.join('; ');
18
+ };
19
+
20
+ /**
21
+ * Serializes Svelte `style` props to CSS text. Accepts the React-style
22
+ * object (camelCase keys) for API parity plus plain CSS strings.
23
+ */
24
+ const toStyleText = (...styles: StyleInput[]): string | undefined => {
25
+ const parts: string[] = [];
26
+
27
+ for (const style of styles) {
28
+ if (!style) {
29
+ continue;
30
+ }
31
+
32
+ if (typeof style === 'string') {
33
+ const trimmed = style.trim().replace(/;$/, '');
34
+
35
+ if (trimmed) {
36
+ parts.push(trimmed);
37
+ }
38
+ } else {
39
+ const text = stringifyStyleObject(style);
40
+
41
+ if (text) {
42
+ parts.push(text);
43
+ }
44
+ }
45
+ }
46
+
47
+ return parts.length ? parts.join('; ') : undefined;
48
+ };
49
+
50
+ const positionedStyleText =
51
+ 'position: absolute; inset: 0; display: block; border: 0; margin: 0; padding: 0; width: 100%; height: 100%';
52
+
53
+ export { positionedStyleText, toStyleText };
54
+ export type { StyleInput };
@@ -0,0 +1,7 @@
1
+ // In-repo ambient types for `.svelte` imports (tests only). The published
2
+ // package ships the `.svelte` sources and consumers get full prop inference
3
+ // from the `$props()` interfaces via their own Svelte setup.
4
+ declare module '*.svelte' {
5
+ const Component: unknown;
6
+ export default Component;
7
+ }
@@ -0,0 +1 @@
1
+ export * from './useFrameAssembler.svelte.js';
@@ -0,0 +1,55 @@
1
+ import { animateFrameAssembler } from '@elyndra/frames';
2
+ import { useAnimator } from '@elyndra/svelte-animator';
3
+ import type { AnimationPlaybackControlsWithThen } from 'motion';
4
+
5
+ /**
6
+ * Mechanical Svelte port of the React/Preact `useFrameAssembler` hook. The
7
+ * element is an accessor so it stays tracked (same convention as
8
+ * `Animator`'s `refreshOn`); `$effect` + cleanup replace `useEffect`.
9
+ * Must be called during component init.
10
+ */
11
+ const useFrameAssembler = (svg: () => SVGElement | HTMLElement | null | undefined): void => {
12
+ const getAnimator = useAnimator();
13
+
14
+ $effect(() => {
15
+ const animator = getAnimator?.();
16
+ const container = svg();
17
+
18
+ if (!animator || !container) {
19
+ return;
20
+ }
21
+
22
+ let animation: AnimationPlaybackControlsWithThen | undefined;
23
+
24
+ const unsubscribe = animator.node.subscribe((node) => {
25
+ switch (node.state) {
26
+ case 'entering': {
27
+ animation?.cancel();
28
+ animation = animateFrameAssembler({
29
+ element: container,
30
+ duration: node.settings.duration.enter,
31
+ isEntering: true,
32
+ });
33
+ break;
34
+ }
35
+
36
+ case 'exiting': {
37
+ animation?.cancel();
38
+ animation = animateFrameAssembler({
39
+ element: container,
40
+ duration: node.settings.duration.exit,
41
+ isEntering: false,
42
+ });
43
+ break;
44
+ }
45
+ }
46
+ });
47
+
48
+ return () => {
49
+ animation?.cancel();
50
+ unsubscribe();
51
+ };
52
+ });
53
+ };
54
+
55
+ export { useFrameAssembler };