@elyndra/svelte-effects 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 +50 -0
- package/src/Illuminator/Illuminator.svelte +59 -0
- package/src/Illuminator/Illuminator.test.ts +14 -0
- package/src/Illuminator/index.ts +2 -0
- package/src/Illuminator/types.ts +14 -0
- package/src/IlluminatorSVG/IlluminatorSVG.svelte +47 -0
- package/src/IlluminatorSVG/IlluminatorSVG.test.ts +15 -0
- package/src/IlluminatorSVG/index.ts +2 -0
- package/src/IlluminatorSVG/types.ts +17 -0
- package/src/index.ts +2 -0
- package/src/internal/style.ts +51 -0
- package/src/svelte.d.ts +7 -0
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@elyndra/svelte-effects",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"sideEffects": false,
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"description": "Elyndra effects 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/effects": "^1.2.0",
|
|
43
|
+
"@elyndra/svelte-tools": "^1.2.0",
|
|
44
|
+
"@elyndra/tools": "^1.2.0",
|
|
45
|
+
"tslib": "2.8.1"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "tsc --noEmit -p tsconfig.json && svelte-check --tsconfig tsconfig.json --output machine"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { createEffectIlluminator } from '@elyndra/effects';
|
|
3
|
+
import { cx } from '@elyndra/tools';
|
|
4
|
+
import { toStyleText } from '../internal/style.js';
|
|
5
|
+
import type { IlluminatorProps } from './types.js';
|
|
6
|
+
|
|
7
|
+
// No destructured rest: only direct `props.*` reads stay reactive.
|
|
8
|
+
const props: IlluminatorProps = $props();
|
|
9
|
+
|
|
10
|
+
let container: HTMLDivElement | null = $state(null);
|
|
11
|
+
|
|
12
|
+
$effect(() => {
|
|
13
|
+
const color = props.color;
|
|
14
|
+
const size = props.size;
|
|
15
|
+
const target = container;
|
|
16
|
+
|
|
17
|
+
if (!target) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const illuminator = createEffectIlluminator({ container: target, color, size });
|
|
22
|
+
|
|
23
|
+
return () => illuminator.cancel();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
$effect(() => {
|
|
27
|
+
const target = container;
|
|
28
|
+
const ref = props.elementRef;
|
|
29
|
+
|
|
30
|
+
if (typeof ref === 'function') {
|
|
31
|
+
ref(target);
|
|
32
|
+
} else if (ref) {
|
|
33
|
+
ref.current = target;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const styleText = $derived(
|
|
38
|
+
toStyleText(
|
|
39
|
+
{
|
|
40
|
+
position: 'absolute',
|
|
41
|
+
inset: 0,
|
|
42
|
+
overflow: 'hidden',
|
|
43
|
+
width: '100%',
|
|
44
|
+
height: '100%',
|
|
45
|
+
},
|
|
46
|
+
props.style,
|
|
47
|
+
),
|
|
48
|
+
);
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<div
|
|
52
|
+
bind:this={container}
|
|
53
|
+
role="presentation"
|
|
54
|
+
id={props.id}
|
|
55
|
+
class={cx('elyndra-frames-illuminator', props.className)}
|
|
56
|
+
style={styleText}
|
|
57
|
+
>
|
|
58
|
+
{@render props.children?.()}
|
|
59
|
+
</div>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { render } from '@testing-library/svelte';
|
|
2
|
+
import type { Component } from 'svelte';
|
|
3
|
+
import { expect, test } from 'vitest';
|
|
4
|
+
import Illuminator from './Illuminator.svelte';
|
|
5
|
+
|
|
6
|
+
// `.svelte` components are `unknown` in-repo (ambient decl, tests only).
|
|
7
|
+
const illuminator = Illuminator as Component;
|
|
8
|
+
|
|
9
|
+
test('Should render div element with elyndra class', () => {
|
|
10
|
+
const { container } = render(illuminator, { color: 'cyan', size: 300 });
|
|
11
|
+
const element = container.querySelector('div') as HTMLElement;
|
|
12
|
+
expect(element.tagName).toBe('DIV');
|
|
13
|
+
expect(element.classList.contains('elyndra-frames-illuminator')).toBe(true);
|
|
14
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CreateEffectIlluminatorProps } from '@elyndra/effects';
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
type ElementRef<E> = ((element: E | null) => void) | { current: E | null };
|
|
5
|
+
|
|
6
|
+
interface IlluminatorProps extends Omit<CreateEffectIlluminatorProps, 'container' | 'className' | 'style'> {
|
|
7
|
+
elementRef?: ElementRef<HTMLDivElement>;
|
|
8
|
+
className?: string;
|
|
9
|
+
style?: string | Record<string, string | number | undefined>;
|
|
10
|
+
id?: string;
|
|
11
|
+
children?: Snippet;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type { IlluminatorProps };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { createEffectIlluminatorSVG } from '@elyndra/effects';
|
|
3
|
+
import { cx } from '@elyndra/tools';
|
|
4
|
+
import { toStyleText } from '../internal/style.js';
|
|
5
|
+
import type { IlluminatorSVGProps } from './types.js';
|
|
6
|
+
|
|
7
|
+
// No destructured rest: only direct `props.*` reads stay reactive.
|
|
8
|
+
const props: IlluminatorSVGProps = $props();
|
|
9
|
+
|
|
10
|
+
let container: SVGGElement | null = $state(null);
|
|
11
|
+
|
|
12
|
+
$effect(() => {
|
|
13
|
+
const size = props.size;
|
|
14
|
+
const color = props.color;
|
|
15
|
+
const svg = props.svgRef.current;
|
|
16
|
+
const target = container;
|
|
17
|
+
|
|
18
|
+
if (!svg || !target) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const effect = createEffectIlluminatorSVG({ svg, container: target, size, color });
|
|
23
|
+
|
|
24
|
+
return () => effect.cancel();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
$effect(() => {
|
|
28
|
+
const target = container;
|
|
29
|
+
const ref = props.elementRef;
|
|
30
|
+
|
|
31
|
+
if (typeof ref === 'function') {
|
|
32
|
+
ref(target);
|
|
33
|
+
} else if (ref) {
|
|
34
|
+
ref.current = target;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const styleText = $derived(toStyleText({ pointerEvents: 'none' }, props.style));
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<g
|
|
42
|
+
bind:this={container}
|
|
43
|
+
class={cx('elyndra-frames-illuminatorsvg', props.className)}
|
|
44
|
+
style={styleText}
|
|
45
|
+
>
|
|
46
|
+
{@render props.children?.()}
|
|
47
|
+
</g>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { render } from '@testing-library/svelte';
|
|
2
|
+
import type { Component } from 'svelte';
|
|
3
|
+
import { expect, test } from 'vitest';
|
|
4
|
+
import IlluminatorSVG from './IlluminatorSVG.svelte';
|
|
5
|
+
|
|
6
|
+
// `.svelte` components are `unknown` in-repo (ambient decl, tests only).
|
|
7
|
+
const illuminatorSVG = IlluminatorSVG as Component;
|
|
8
|
+
|
|
9
|
+
test('Should render g element with elyndra class', () => {
|
|
10
|
+
const svgRef: { current: SVGSVGElement | null } = { current: null };
|
|
11
|
+
const { container } = render(illuminatorSVG, { color: 'cyan', size: 300, svgRef });
|
|
12
|
+
const element = container.querySelector('g') as Element;
|
|
13
|
+
expect(element.tagName).toBe('g');
|
|
14
|
+
expect(element.classList.contains('elyndra-frames-illuminatorsvg')).toBe(true);
|
|
15
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { CreateEffectIlluminatorSVGProps } from '@elyndra/effects';
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
type ElementRef<E> = ((element: E | null) => void) | { current: E | null };
|
|
5
|
+
|
|
6
|
+
interface IlluminatorSVGProps
|
|
7
|
+
extends Omit<CreateEffectIlluminatorSVGProps, 'svg' | 'container' | 'className' | 'style'> {
|
|
8
|
+
elementRef?: ElementRef<SVGGElement>;
|
|
9
|
+
svgRef: { current: SVGSVGElement | null };
|
|
10
|
+
className?: string;
|
|
11
|
+
style?: string | Record<string, string | number | undefined>;
|
|
12
|
+
color?: string;
|
|
13
|
+
size?: number;
|
|
14
|
+
children?: Snippet;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type { IlluminatorSVGProps };
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
export { toStyleText };
|
|
51
|
+
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
|
+
}
|