@elyndra/vue-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.test.ts +31 -0
- package/src/FrameBase/FrameBase.vue +76 -0
- package/src/FrameBase/index.ts +1 -0
- package/src/FrameCircle/FrameCircle.vue +40 -0
- package/src/FrameCircle/index.ts +1 -0
- package/src/FrameCorners/FrameCorners.vue +39 -0
- package/src/FrameCorners/index.ts +1 -0
- package/src/FrameHeader/FrameHeader.vue +42 -0
- package/src/FrameHeader/index.ts +1 -0
- package/src/FrameKranox/FrameKranox.vue +42 -0
- package/src/FrameKranox/index.ts +1 -0
- package/src/FrameLines/FrameLines.vue +40 -0
- package/src/FrameLines/index.ts +1 -0
- package/src/FrameNefrex/FrameNefrex.vue +45 -0
- package/src/FrameNefrex/index.ts +1 -0
- package/src/FrameNero/FrameNero.vue +39 -0
- package/src/FrameNero/index.ts +1 -0
- package/src/FrameOctagon/FrameOctagon.vue +43 -0
- package/src/FrameOctagon/index.ts +1 -0
- package/src/FrameUnderline/FrameUnderline.test.ts +27 -0
- package/src/FrameUnderline/FrameUnderline.vue +38 -0
- package/src/FrameUnderline/index.ts +1 -0
- package/src/index.ts +12 -0
- package/src/internal/styles.ts +12 -0
- package/src/types.ts +47 -0
- package/src/useFrameAssembler/index.ts +1 -0
- package/src/useFrameAssembler/useFrameAssembler.test.ts +40 -0
- package/src/useFrameAssembler/useFrameAssembler.ts +57 -0
- package/src/vue.d.ts +8 -0
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@elyndra/vue-frames",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"sideEffects": false,
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"description": "Elyndra frames components for Vue",
|
|
9
|
+
"keywords": [
|
|
10
|
+
"elyndra",
|
|
11
|
+
"ui",
|
|
12
|
+
"front-end",
|
|
13
|
+
"framework",
|
|
14
|
+
"scifi",
|
|
15
|
+
"sci-fi",
|
|
16
|
+
"science-fiction",
|
|
17
|
+
"vue"
|
|
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
|
+
"vue": "3"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@elyndra/frames": "^1.2.0",
|
|
43
|
+
"@elyndra/tools": "^1.2.0",
|
|
44
|
+
"@elyndra/vue-animator": "^1.2.0",
|
|
45
|
+
"@elyndra/vue-tools": "^1.2.0",
|
|
46
|
+
"tslib": "2.8.1"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "tsc --noEmit -p tsconfig.json && vue-tsc --noEmit -p tsconfig.json"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { flushPromises, mount } from '@vue/test-utils';
|
|
2
|
+
import { afterEach, beforeEach, expect, test } from 'vitest';
|
|
3
|
+
import { createFrameOctagonSettings } from '@elyndra/frames';
|
|
4
|
+
import { FrameBase } from './index.js';
|
|
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
|
+
test('Should render svg element with elyndra class and children', async () => {
|
|
20
|
+
const wrapper = mount(FrameBase, {
|
|
21
|
+
props: { settings: createFrameOctagonSettings({}) },
|
|
22
|
+
slots: { default: () => 'hello' },
|
|
23
|
+
});
|
|
24
|
+
await flushPromises();
|
|
25
|
+
|
|
26
|
+
const element = wrapper.element as HTMLElement;
|
|
27
|
+
expect(element.tagName).toBe('svg');
|
|
28
|
+
expect(element.classList.contains('elyndra-frames-frame')).toBe(true);
|
|
29
|
+
expect(element.textContent).toBe('hello');
|
|
30
|
+
wrapper.unmount();
|
|
31
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { type Frame, type FrameSettings, createFrame } from '@elyndra/frames';
|
|
3
|
+
import { cx } from '@elyndra/tools';
|
|
4
|
+
import { useAnimator } from '@elyndra/vue-animator';
|
|
5
|
+
import { mergeRefs, useUpdateEffect } from '@elyndra/vue-tools';
|
|
6
|
+
import { computed, watch, watchEffect, type CSSProperties } from 'vue';
|
|
7
|
+
import { positionedStyle } from '../internal/styles.js';
|
|
8
|
+
import type { FrameBaseProps } from '../types.js';
|
|
9
|
+
|
|
10
|
+
const props = withDefaults(defineProps<FrameBaseProps>(), {
|
|
11
|
+
// Vue boolean-casts absent props with a Boolean type to `false`.
|
|
12
|
+
// `positioned` defaults to `true`, so it gets an `undefined` default
|
|
13
|
+
// which skips the cast.
|
|
14
|
+
positioned: undefined,
|
|
15
|
+
preserveAspectRatio: 'none',
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const getAnimator = useAnimator();
|
|
19
|
+
const svgBox: { current: SVGSVGElement | null } = { current: null };
|
|
20
|
+
let frame: Frame | null = null;
|
|
21
|
+
const settingsRef: { current: FrameSettings } = { current: props.settings };
|
|
22
|
+
|
|
23
|
+
const setSvgRef = (value: unknown): void => {
|
|
24
|
+
const element = value as SVGSVGElement | null;
|
|
25
|
+
svgBox.current = element;
|
|
26
|
+
mergeRefs(props.elementRef)(element);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
watchEffect(() => {
|
|
30
|
+
Object.assign(settingsRef.current, props.settings, { animator: getAnimator?.()?.node });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
watch(
|
|
34
|
+
() => getAnimator?.(),
|
|
35
|
+
(animator, _previous, onCleanup) => {
|
|
36
|
+
// Rebuild the frame when the animator interface changes, full teardown +
|
|
37
|
+
// rebuild. Settings-only changes take the light path below, so animator
|
|
38
|
+
// changes must NOT join those deps or they would render() a
|
|
39
|
+
// just-removed frame.
|
|
40
|
+
void animator;
|
|
41
|
+
|
|
42
|
+
const svg = svgBox.current;
|
|
43
|
+
|
|
44
|
+
if (!svg) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
frame = createFrame(svg, settingsRef.current);
|
|
49
|
+
|
|
50
|
+
onCleanup(() => frame?.remove());
|
|
51
|
+
},
|
|
52
|
+
{ immediate: true, flush: 'post' },
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
useUpdateEffect(() => {
|
|
56
|
+
frame?.render();
|
|
57
|
+
}, [() => props.settings]);
|
|
58
|
+
|
|
59
|
+
const svgStyle = computed<CSSProperties>(() => ({
|
|
60
|
+
...((props.positioned ?? true) ? positionedStyle : null),
|
|
61
|
+
...props.style,
|
|
62
|
+
}));
|
|
63
|
+
</script>
|
|
64
|
+
|
|
65
|
+
<template>
|
|
66
|
+
<svg
|
|
67
|
+
role="presentation"
|
|
68
|
+
:ref="setSvgRef"
|
|
69
|
+
:class="cx('elyndra-frames-frame', props.className)"
|
|
70
|
+
:style="svgStyle"
|
|
71
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
72
|
+
:preserveAspectRatio="props.preserveAspectRatio"
|
|
73
|
+
>
|
|
74
|
+
<slot />
|
|
75
|
+
</svg>
|
|
76
|
+
</template>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as FrameBase } from './FrameBase.vue';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { createFrameCircleSettings } from '@elyndra/frames';
|
|
3
|
+
import { cx } from '@elyndra/tools';
|
|
4
|
+
import { computed } from 'vue';
|
|
5
|
+
import { FrameBase } from '../FrameBase/index.js';
|
|
6
|
+
import type { FrameCircleProps } from '../types.js';
|
|
7
|
+
|
|
8
|
+
const props = withDefaults(defineProps<FrameCircleProps>(), {
|
|
9
|
+
// The vanilla settings default `styled`/`animated` to `true`; Vue
|
|
10
|
+
// boolean-casts absent Boolean props to `false`, so each gets an
|
|
11
|
+
// `undefined` default which skips the cast. Same for `positioned`.
|
|
12
|
+
styled: undefined,
|
|
13
|
+
animated: undefined,
|
|
14
|
+
positioned: undefined,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const settings = computed(() =>
|
|
18
|
+
createFrameCircleSettings({
|
|
19
|
+
styled: props.styled,
|
|
20
|
+
animated: props.animated,
|
|
21
|
+
padding: props.padding,
|
|
22
|
+
strokeWidth: props.strokeWidth,
|
|
23
|
+
separation: props.separation,
|
|
24
|
+
sideWidth: props.sideWidth,
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<template>
|
|
30
|
+
<FrameBase
|
|
31
|
+
:elementRef="props.elementRef"
|
|
32
|
+
:positioned="props.positioned"
|
|
33
|
+
:style="props.style"
|
|
34
|
+
:preserveAspectRatio="props.preserveAspectRatio"
|
|
35
|
+
:class="cx('elyndra-frames-framecircle', props.className)"
|
|
36
|
+
:settings="settings"
|
|
37
|
+
>
|
|
38
|
+
<slot />
|
|
39
|
+
</FrameBase>
|
|
40
|
+
</template>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as FrameCircle } from './FrameCircle.vue';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { createFrameCornersSettings } from '@elyndra/frames';
|
|
3
|
+
import { cx } from '@elyndra/tools';
|
|
4
|
+
import { computed } from 'vue';
|
|
5
|
+
import { FrameBase } from '../FrameBase/index.js';
|
|
6
|
+
import type { FrameCornersProps } from '../types.js';
|
|
7
|
+
|
|
8
|
+
const props = withDefaults(defineProps<FrameCornersProps>(), {
|
|
9
|
+
// The vanilla settings default `styled`/`animated` to `true`; Vue
|
|
10
|
+
// boolean-casts absent Boolean props to `false`, so each gets an
|
|
11
|
+
// `undefined` default which skips the cast. Same for `positioned`.
|
|
12
|
+
styled: undefined,
|
|
13
|
+
animated: undefined,
|
|
14
|
+
positioned: undefined,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const settings = computed(() =>
|
|
18
|
+
createFrameCornersSettings({
|
|
19
|
+
styled: props.styled,
|
|
20
|
+
animated: props.animated,
|
|
21
|
+
padding: props.padding,
|
|
22
|
+
strokeWidth: props.strokeWidth,
|
|
23
|
+
cornerLength: props.cornerLength,
|
|
24
|
+
}),
|
|
25
|
+
);
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<template>
|
|
29
|
+
<FrameBase
|
|
30
|
+
:elementRef="props.elementRef"
|
|
31
|
+
:positioned="props.positioned"
|
|
32
|
+
:style="props.style"
|
|
33
|
+
:preserveAspectRatio="props.preserveAspectRatio"
|
|
34
|
+
:class="cx('elyndra-frames-framecorners', props.className)"
|
|
35
|
+
:settings="settings"
|
|
36
|
+
>
|
|
37
|
+
<slot />
|
|
38
|
+
</FrameBase>
|
|
39
|
+
</template>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as FrameCorners } from './FrameCorners.vue';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { createFrameHeaderSettings } from '@elyndra/frames';
|
|
3
|
+
import { cx } from '@elyndra/tools';
|
|
4
|
+
import { computed } from 'vue';
|
|
5
|
+
import { FrameBase } from '../FrameBase/index.js';
|
|
6
|
+
import type { FrameHeaderProps } from '../types.js';
|
|
7
|
+
|
|
8
|
+
const props = withDefaults(defineProps<FrameHeaderProps>(), {
|
|
9
|
+
// The vanilla settings default `styled`/`animated` to `true`; Vue
|
|
10
|
+
// boolean-casts absent Boolean props to `false`, so each gets an
|
|
11
|
+
// `undefined` default which skips the cast. Same for `positioned`.
|
|
12
|
+
styled: undefined,
|
|
13
|
+
animated: undefined,
|
|
14
|
+
positioned: undefined,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const settings = computed(() =>
|
|
18
|
+
createFrameHeaderSettings({
|
|
19
|
+
styled: props.styled,
|
|
20
|
+
animated: props.animated,
|
|
21
|
+
padding: props.padding,
|
|
22
|
+
strokeWidth: props.strokeWidth,
|
|
23
|
+
decoWidth: props.decoWidth,
|
|
24
|
+
direction: props.direction,
|
|
25
|
+
align: props.align,
|
|
26
|
+
contentLength: props.contentLength,
|
|
27
|
+
}),
|
|
28
|
+
);
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<FrameBase
|
|
33
|
+
:elementRef="props.elementRef"
|
|
34
|
+
:positioned="props.positioned"
|
|
35
|
+
:style="props.style"
|
|
36
|
+
:preserveAspectRatio="props.preserveAspectRatio"
|
|
37
|
+
:class="cx('elyndra-frames-frameheader', props.className)"
|
|
38
|
+
:settings="settings"
|
|
39
|
+
>
|
|
40
|
+
<slot />
|
|
41
|
+
</FrameBase>
|
|
42
|
+
</template>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as FrameHeader } from './FrameHeader.vue';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { createFrameKranoxSettings } from '@elyndra/frames';
|
|
3
|
+
import { cx } from '@elyndra/tools';
|
|
4
|
+
import { computed } from 'vue';
|
|
5
|
+
import { FrameBase } from '../FrameBase/index.js';
|
|
6
|
+
import type { FrameKranoxProps } from '../types.js';
|
|
7
|
+
|
|
8
|
+
const props = withDefaults(defineProps<FrameKranoxProps>(), {
|
|
9
|
+
// The vanilla settings default `styled`/`animated` to `true`; Vue
|
|
10
|
+
// boolean-casts absent Boolean props to `false`, so each gets an
|
|
11
|
+
// `undefined` default which skips the cast. Same for `positioned`.
|
|
12
|
+
styled: undefined,
|
|
13
|
+
animated: undefined,
|
|
14
|
+
positioned: undefined,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const settings = computed(() =>
|
|
18
|
+
createFrameKranoxSettings({
|
|
19
|
+
styled: props.styled,
|
|
20
|
+
animated: props.animated,
|
|
21
|
+
padding: props.padding,
|
|
22
|
+
strokeWidth: props.strokeWidth,
|
|
23
|
+
bgStrokeWidth: props.bgStrokeWidth,
|
|
24
|
+
squareSize: props.squareSize,
|
|
25
|
+
smallLineLength: props.smallLineLength,
|
|
26
|
+
largeLineLength: props.largeLineLength,
|
|
27
|
+
}),
|
|
28
|
+
);
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<FrameBase
|
|
33
|
+
:elementRef="props.elementRef"
|
|
34
|
+
:positioned="props.positioned"
|
|
35
|
+
:style="props.style"
|
|
36
|
+
:preserveAspectRatio="props.preserveAspectRatio"
|
|
37
|
+
:class="cx('elyndra-frames-framekranox', props.className)"
|
|
38
|
+
:settings="settings"
|
|
39
|
+
>
|
|
40
|
+
<slot />
|
|
41
|
+
</FrameBase>
|
|
42
|
+
</template>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as FrameKranox } from './FrameKranox.vue';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { createFrameLinesSettings } from '@elyndra/frames';
|
|
3
|
+
import { cx } from '@elyndra/tools';
|
|
4
|
+
import { computed } from 'vue';
|
|
5
|
+
import { FrameBase } from '../FrameBase/index.js';
|
|
6
|
+
import type { FrameLinesProps } from '../types.js';
|
|
7
|
+
|
|
8
|
+
const props = withDefaults(defineProps<FrameLinesProps>(), {
|
|
9
|
+
// The vanilla settings default `styled`/`animated` to `true`; Vue
|
|
10
|
+
// boolean-casts absent Boolean props to `false`, so each gets an
|
|
11
|
+
// `undefined` default which skips the cast. Same for `positioned`.
|
|
12
|
+
styled: undefined,
|
|
13
|
+
animated: undefined,
|
|
14
|
+
positioned: undefined,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const settings = computed(() =>
|
|
18
|
+
createFrameLinesSettings({
|
|
19
|
+
styled: props.styled,
|
|
20
|
+
animated: props.animated,
|
|
21
|
+
padding: props.padding,
|
|
22
|
+
largeLineWidth: props.largeLineWidth,
|
|
23
|
+
smallLineWidth: props.smallLineWidth,
|
|
24
|
+
smallLineLength: props.smallLineLength,
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<template>
|
|
30
|
+
<FrameBase
|
|
31
|
+
:elementRef="props.elementRef"
|
|
32
|
+
:positioned="props.positioned"
|
|
33
|
+
:style="props.style"
|
|
34
|
+
:preserveAspectRatio="props.preserveAspectRatio"
|
|
35
|
+
:class="cx('elyndra-frames-framelines', props.className)"
|
|
36
|
+
:settings="settings"
|
|
37
|
+
>
|
|
38
|
+
<slot />
|
|
39
|
+
</FrameBase>
|
|
40
|
+
</template>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as FrameLines } from './FrameLines.vue';
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { createFrameNefrexSettings } from '@elyndra/frames';
|
|
3
|
+
import { cx } from '@elyndra/tools';
|
|
4
|
+
import { computed } from 'vue';
|
|
5
|
+
import { FrameBase } from '../FrameBase/index.js';
|
|
6
|
+
import type { FrameNefrexProps } from '../types.js';
|
|
7
|
+
|
|
8
|
+
const props = withDefaults(defineProps<FrameNefrexProps>(), {
|
|
9
|
+
// The vanilla settings default `styled`/`animated` to `true`; Vue
|
|
10
|
+
// boolean-casts absent Boolean props to `false`, so each gets an
|
|
11
|
+
// `undefined` default which skips the cast. Same for `positioned`.
|
|
12
|
+
styled: undefined,
|
|
13
|
+
animated: undefined,
|
|
14
|
+
positioned: undefined,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const settings = computed(() =>
|
|
18
|
+
createFrameNefrexSettings({
|
|
19
|
+
styled: props.styled,
|
|
20
|
+
animated: props.animated,
|
|
21
|
+
padding: props.padding,
|
|
22
|
+
leftTop: props.leftTop,
|
|
23
|
+
leftBottom: props.leftBottom,
|
|
24
|
+
rightTop: props.rightTop,
|
|
25
|
+
rightBottom: props.rightBottom,
|
|
26
|
+
squareSize: props.squareSize,
|
|
27
|
+
strokeWidth: props.strokeWidth,
|
|
28
|
+
smallLineLength: props.smallLineLength,
|
|
29
|
+
largeLineLength: props.largeLineLength,
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<template>
|
|
35
|
+
<FrameBase
|
|
36
|
+
:elementRef="props.elementRef"
|
|
37
|
+
:positioned="props.positioned"
|
|
38
|
+
:style="props.style"
|
|
39
|
+
:preserveAspectRatio="props.preserveAspectRatio"
|
|
40
|
+
:class="cx('elyndra-frames-framenefrex', props.className)"
|
|
41
|
+
:settings="settings"
|
|
42
|
+
>
|
|
43
|
+
<slot />
|
|
44
|
+
</FrameBase>
|
|
45
|
+
</template>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as FrameNefrex } from './FrameNefrex.vue';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { createFrameNeroSettings } from '@elyndra/frames';
|
|
3
|
+
import { cx } from '@elyndra/tools';
|
|
4
|
+
import { computed } from 'vue';
|
|
5
|
+
import { FrameBase } from '../FrameBase/index.js';
|
|
6
|
+
import type { FrameNeroProps } from '../types.js';
|
|
7
|
+
|
|
8
|
+
const props = withDefaults(defineProps<FrameNeroProps>(), {
|
|
9
|
+
// The vanilla settings default `styled`/`animated` to `true`; Vue
|
|
10
|
+
// boolean-casts absent Boolean props to `false`, so each gets an
|
|
11
|
+
// `undefined` default which skips the cast. Same for `positioned`.
|
|
12
|
+
styled: undefined,
|
|
13
|
+
animated: undefined,
|
|
14
|
+
positioned: undefined,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const settings = computed(() =>
|
|
18
|
+
createFrameNeroSettings({
|
|
19
|
+
styled: props.styled,
|
|
20
|
+
animated: props.animated,
|
|
21
|
+
padding: props.padding,
|
|
22
|
+
cornerLength: props.cornerLength,
|
|
23
|
+
cornerWidth: props.cornerWidth,
|
|
24
|
+
}),
|
|
25
|
+
);
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<template>
|
|
29
|
+
<FrameBase
|
|
30
|
+
:elementRef="props.elementRef"
|
|
31
|
+
:positioned="props.positioned"
|
|
32
|
+
:style="props.style"
|
|
33
|
+
:preserveAspectRatio="props.preserveAspectRatio"
|
|
34
|
+
:class="cx('elyndra-frames-framenero', props.className)"
|
|
35
|
+
:settings="settings"
|
|
36
|
+
>
|
|
37
|
+
<slot />
|
|
38
|
+
</FrameBase>
|
|
39
|
+
</template>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as FrameNero } from './FrameNero.vue';
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { createFrameOctagonSettings } from '@elyndra/frames';
|
|
3
|
+
import { cx } from '@elyndra/tools';
|
|
4
|
+
import { computed } from 'vue';
|
|
5
|
+
import { FrameBase } from '../FrameBase/index.js';
|
|
6
|
+
import type { FrameOctagonProps } from '../types.js';
|
|
7
|
+
|
|
8
|
+
const props = withDefaults(defineProps<FrameOctagonProps>(), {
|
|
9
|
+
// The vanilla settings default `styled`/`animated` to `true`; Vue
|
|
10
|
+
// boolean-casts absent Boolean props to `false`, so each gets an
|
|
11
|
+
// `undefined` default which skips the cast. Same for `positioned`.
|
|
12
|
+
styled: undefined,
|
|
13
|
+
animated: undefined,
|
|
14
|
+
positioned: undefined,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const settings = computed(() =>
|
|
18
|
+
createFrameOctagonSettings({
|
|
19
|
+
styled: props.styled,
|
|
20
|
+
animated: props.animated,
|
|
21
|
+
padding: props.padding,
|
|
22
|
+
leftTop: props.leftTop,
|
|
23
|
+
rightTop: props.rightTop,
|
|
24
|
+
rightBottom: props.rightBottom,
|
|
25
|
+
leftBottom: props.leftBottom,
|
|
26
|
+
squareSize: props.squareSize,
|
|
27
|
+
strokeWidth: props.strokeWidth,
|
|
28
|
+
}),
|
|
29
|
+
);
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<template>
|
|
33
|
+
<FrameBase
|
|
34
|
+
:elementRef="props.elementRef"
|
|
35
|
+
:positioned="props.positioned"
|
|
36
|
+
:style="props.style"
|
|
37
|
+
:preserveAspectRatio="props.preserveAspectRatio"
|
|
38
|
+
:class="cx('elyndra-frames-frameoctagon', props.className)"
|
|
39
|
+
:settings="settings"
|
|
40
|
+
>
|
|
41
|
+
<slot />
|
|
42
|
+
</FrameBase>
|
|
43
|
+
</template>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as FrameOctagon } from './FrameOctagon.vue';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { flushPromises, mount } from '@vue/test-utils';
|
|
2
|
+
import { afterEach, beforeEach, expect, test } from 'vitest';
|
|
3
|
+
import { FrameUnderline } from './index.js';
|
|
4
|
+
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
class ResizeObserver {
|
|
7
|
+
observe(): void {}
|
|
8
|
+
unobserve(): void {}
|
|
9
|
+
disconnect(): void {}
|
|
10
|
+
}
|
|
11
|
+
window.ResizeObserver = ResizeObserver as unknown as typeof window.ResizeObserver;
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
window.ResizeObserver = undefined as unknown as typeof window.ResizeObserver;
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('Should render svg element with elyndra classes', async () => {
|
|
19
|
+
const wrapper = mount(FrameUnderline);
|
|
20
|
+
await flushPromises();
|
|
21
|
+
|
|
22
|
+
const element = wrapper.element as HTMLElement;
|
|
23
|
+
expect(element.tagName).toBe('svg');
|
|
24
|
+
expect(element.classList.contains('elyndra-frames-frame')).toBe(true);
|
|
25
|
+
expect(element.classList.contains('elyndra-frames-frameunderline')).toBe(true);
|
|
26
|
+
wrapper.unmount();
|
|
27
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { createFrameUnderlineSettings } from '@elyndra/frames';
|
|
3
|
+
import { cx } from '@elyndra/tools';
|
|
4
|
+
import { computed } from 'vue';
|
|
5
|
+
import { FrameBase } from '../FrameBase/index.js';
|
|
6
|
+
import type { FrameUnderlineProps } from '../types.js';
|
|
7
|
+
|
|
8
|
+
const props = withDefaults(defineProps<FrameUnderlineProps>(), {
|
|
9
|
+
// The vanilla settings default `styled` to `true`; Vue boolean-casts
|
|
10
|
+
// absent Boolean props to `false`, so it gets an `undefined` default
|
|
11
|
+
// which skips the cast. Same for `positioned`. (`animated` is accepted
|
|
12
|
+
// by vanilla but intentionally not forwarded, same as React/Solid.)
|
|
13
|
+
styled: undefined,
|
|
14
|
+
positioned: undefined,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const settings = computed(() =>
|
|
18
|
+
createFrameUnderlineSettings({
|
|
19
|
+
styled: props.styled,
|
|
20
|
+
squareSize: props.squareSize,
|
|
21
|
+
strokeWidth: props.strokeWidth,
|
|
22
|
+
padding: props.padding,
|
|
23
|
+
}),
|
|
24
|
+
);
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<template>
|
|
28
|
+
<FrameBase
|
|
29
|
+
:elementRef="props.elementRef"
|
|
30
|
+
:positioned="props.positioned"
|
|
31
|
+
:style="props.style"
|
|
32
|
+
:preserveAspectRatio="props.preserveAspectRatio"
|
|
33
|
+
:class="cx('elyndra-frames-frameunderline', props.className)"
|
|
34
|
+
:settings="settings"
|
|
35
|
+
>
|
|
36
|
+
<slot />
|
|
37
|
+
</FrameBase>
|
|
38
|
+
</template>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as FrameUnderline } from './FrameUnderline.vue';
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
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';
|
|
12
|
+
export type * from './types.js';
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { CSSProperties } from 'vue';
|
|
2
|
+
import type {
|
|
3
|
+
CreateFrameCircleSettingsProps,
|
|
4
|
+
CreateFrameCornersSettingsProps,
|
|
5
|
+
CreateFrameHeaderSettingsProps,
|
|
6
|
+
CreateFrameKranoxSettingsProps,
|
|
7
|
+
CreateFrameLinesSettingsProps,
|
|
8
|
+
CreateFrameNefrexSettingsProps,
|
|
9
|
+
CreateFrameNeroSettingsProps,
|
|
10
|
+
CreateFrameOctagonSettingsProps,
|
|
11
|
+
CreateFrameUnderlineSettingsProps,
|
|
12
|
+
FrameSettings,
|
|
13
|
+
} from '@elyndra/frames';
|
|
14
|
+
import type { ElementRef } from '@elyndra/vue-tools';
|
|
15
|
+
|
|
16
|
+
type FrameBaseProps = {
|
|
17
|
+
elementRef?: ElementRef<SVGSVGElement>;
|
|
18
|
+
positioned?: boolean;
|
|
19
|
+
settings: FrameSettings;
|
|
20
|
+
className?: string;
|
|
21
|
+
style?: CSSProperties;
|
|
22
|
+
preserveAspectRatio?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type FrameCircleProps = Omit<FrameBaseProps, 'settings'> & CreateFrameCircleSettingsProps;
|
|
26
|
+
type FrameCornersProps = Omit<FrameBaseProps, 'settings'> & CreateFrameCornersSettingsProps;
|
|
27
|
+
type FrameHeaderProps = Omit<FrameBaseProps, 'settings'> & CreateFrameHeaderSettingsProps;
|
|
28
|
+
type FrameKranoxProps = Omit<FrameBaseProps, 'settings'> & CreateFrameKranoxSettingsProps;
|
|
29
|
+
type FrameLinesProps = Omit<FrameBaseProps, 'settings'> & CreateFrameLinesSettingsProps;
|
|
30
|
+
type FrameNefrexProps = Omit<FrameBaseProps, 'settings'> & CreateFrameNefrexSettingsProps;
|
|
31
|
+
type FrameNeroProps = Omit<FrameBaseProps, 'settings'> & CreateFrameNeroSettingsProps;
|
|
32
|
+
type FrameOctagonProps = Omit<FrameBaseProps, 'settings'> & CreateFrameOctagonSettingsProps;
|
|
33
|
+
type FrameUnderlineProps = Omit<FrameBaseProps, 'settings'> & CreateFrameUnderlineSettingsProps;
|
|
34
|
+
|
|
35
|
+
export type {
|
|
36
|
+
ElementRef,
|
|
37
|
+
FrameBaseProps,
|
|
38
|
+
FrameCircleProps,
|
|
39
|
+
FrameCornersProps,
|
|
40
|
+
FrameHeaderProps,
|
|
41
|
+
FrameKranoxProps,
|
|
42
|
+
FrameLinesProps,
|
|
43
|
+
FrameNefrexProps,
|
|
44
|
+
FrameNeroProps,
|
|
45
|
+
FrameOctagonProps,
|
|
46
|
+
FrameUnderlineProps,
|
|
47
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './useFrameAssembler.js';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { flushPromises, mount } from '@vue/test-utils';
|
|
2
|
+
import { defineComponent, h, ref } from 'vue';
|
|
3
|
+
import { expect, test } from 'vitest';
|
|
4
|
+
import { Animator } from '@elyndra/vue-animator';
|
|
5
|
+
import { useFrameAssembler } from './index.js';
|
|
6
|
+
|
|
7
|
+
const flush = async (): Promise<void> => {
|
|
8
|
+
await flushPromises();
|
|
9
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
10
|
+
await flushPromises();
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
test('Should subscribe to the animator without throwing', async () => {
|
|
14
|
+
const svgBox = { current: null as SVGSVGElement | null };
|
|
15
|
+
|
|
16
|
+
const Harness = defineComponent({
|
|
17
|
+
setup() {
|
|
18
|
+
const svg = ref<SVGSVGElement | null>(null);
|
|
19
|
+
useFrameAssembler(() => svg.value);
|
|
20
|
+
return () =>
|
|
21
|
+
h('svg', {
|
|
22
|
+
ref: (value: unknown) => {
|
|
23
|
+
const element = value as SVGSVGElement | null;
|
|
24
|
+
svg.value = element;
|
|
25
|
+
svgBox.current = element;
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const wrapper = mount(Animator, {
|
|
32
|
+
props: { reducedMotion: true },
|
|
33
|
+
slots: { default: () => h(Harness) },
|
|
34
|
+
});
|
|
35
|
+
await flush();
|
|
36
|
+
|
|
37
|
+
expect(svgBox.current).not.toBeNull();
|
|
38
|
+
expect(svgBox.current?.tagName).toBe('svg');
|
|
39
|
+
wrapper.unmount();
|
|
40
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { animateFrameAssembler } from '@elyndra/frames';
|
|
2
|
+
import { useAnimator } from '@elyndra/vue-animator';
|
|
3
|
+
import type { AnimationPlaybackControlsWithThen } from 'motion';
|
|
4
|
+
import { watch } from 'vue';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Mechanical Vue port of the React/Preact/Solid `useFrameAssembler` hook.
|
|
8
|
+
* The element is a source function so it stays tracked (same convention as
|
|
9
|
+
* `Animator`'s `refreshOn`); `watch` + `watch` cleanup replace
|
|
10
|
+
* `useEffect`/`createEffect` + `onCleanup`. Must be called inside `setup`.
|
|
11
|
+
*/
|
|
12
|
+
const useFrameAssembler = (svg: () => SVGElement | HTMLElement | null | undefined): void => {
|
|
13
|
+
const getAnimator = useAnimator();
|
|
14
|
+
|
|
15
|
+
watch(
|
|
16
|
+
() => [getAnimator?.(), svg()] as const,
|
|
17
|
+
([animator, container], _previous, onCleanup) => {
|
|
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
|
+
onCleanup(() => {
|
|
49
|
+
animation?.cancel();
|
|
50
|
+
unsubscribe();
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
{ immediate: true, flush: 'post' },
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export { useFrameAssembler };
|
package/src/vue.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// In-repo ambient types for `.vue` imports (tests only). The published
|
|
2
|
+
// package ships the `.vue` sources and consumers get full prop inference
|
|
3
|
+
// from their own Vue setup.
|
|
4
|
+
declare module '*.vue' {
|
|
5
|
+
import type { DefineComponent } from 'vue';
|
|
6
|
+
const Component: DefineComponent<Record<string, unknown>, Record<string, unknown>, unknown>;
|
|
7
|
+
export default Component;
|
|
8
|
+
}
|