@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.
- package/package.json +51 -0
- package/src/FrameBase/FrameBase.svelte +74 -0
- package/src/FrameBase/index.ts +2 -0
- package/src/FrameBase/types.ts +17 -0
- package/src/FrameCircle/FrameCircle.svelte +32 -0
- package/src/FrameCircle/index.ts +2 -0
- package/src/FrameCircle/types.ts +6 -0
- package/src/FrameCorners/FrameCorners.svelte +31 -0
- package/src/FrameCorners/index.ts +2 -0
- package/src/FrameCorners/types.ts +6 -0
- package/src/FrameHeader/FrameHeader.svelte +34 -0
- package/src/FrameHeader/index.ts +2 -0
- package/src/FrameHeader/types.ts +6 -0
- package/src/FrameKranox/FrameKranox.svelte +34 -0
- package/src/FrameKranox/index.ts +2 -0
- package/src/FrameKranox/types.ts +6 -0
- package/src/FrameLines/FrameLines.svelte +32 -0
- package/src/FrameLines/index.ts +2 -0
- package/src/FrameLines/types.ts +6 -0
- package/src/FrameNefrex/FrameNefrex.svelte +37 -0
- package/src/FrameNefrex/index.ts +2 -0
- package/src/FrameNefrex/types.ts +6 -0
- package/src/FrameNero/FrameNero.svelte +31 -0
- package/src/FrameNero/index.ts +2 -0
- package/src/FrameNero/types.ts +6 -0
- package/src/FrameOctagon/FrameOctagon.svelte +35 -0
- package/src/FrameOctagon/index.ts +2 -0
- package/src/FrameOctagon/types.ts +6 -0
- package/src/FrameUnderline/FrameUnderline.svelte +30 -0
- package/src/FrameUnderline/FrameUnderline.test.ts +28 -0
- package/src/FrameUnderline/index.ts +2 -0
- package/src/FrameUnderline/types.ts +6 -0
- package/src/index.ts +11 -0
- package/src/internal/styles.ts +54 -0
- package/src/svelte.d.ts +7 -0
- package/src/useFrameAssembler/index.ts +1 -0
- 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,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,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,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,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,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,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,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,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,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,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 };
|
package/src/svelte.d.ts
ADDED
|
@@ -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 };
|