@dillingerstaffing/strand-vue 0.6.0 → 0.7.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.
@@ -0,0 +1,74 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest'
2
+ import { render } from '@testing-library/vue'
3
+ import ScrollReveal from './ScrollReveal.vue'
4
+
5
+ // Mock IntersectionObserver (not available in jsdom)
6
+ const mockObserve = vi.fn()
7
+ const mockUnobserve = vi.fn()
8
+ const mockDisconnect = vi.fn()
9
+
10
+ beforeEach(() => {
11
+ mockObserve.mockClear()
12
+ mockUnobserve.mockClear()
13
+ mockDisconnect.mockClear()
14
+
15
+ global.IntersectionObserver = vi.fn(() => ({
16
+ observe: mockObserve,
17
+ unobserve: mockUnobserve,
18
+ disconnect: mockDisconnect,
19
+ root: null,
20
+ rootMargin: '',
21
+ thresholds: [],
22
+ takeRecords: () => [],
23
+ })) as unknown as typeof IntersectionObserver
24
+ })
25
+
26
+ describe('ScrollReveal', () => {
27
+ // ── Rendering ──
28
+
29
+ it('renders a div element', () => {
30
+ const { container } = render(ScrollReveal, { slots: { default: 'Content' } })
31
+ expect(container.firstElementChild?.tagName).toBe('DIV')
32
+ })
33
+
34
+ it('renders slot content', () => {
35
+ const { getByText } = render(ScrollReveal, { slots: { default: 'Reveal me' } })
36
+ expect(getByText('Reveal me')).toBeTruthy()
37
+ })
38
+
39
+ // ── Base class ──
40
+
41
+ it('applies strand-reveal class', () => {
42
+ const { container } = render(ScrollReveal, { slots: { default: 'Test' } })
43
+ expect(container.firstElementChild?.className).toContain('strand-reveal')
44
+ })
45
+
46
+ // ── Visible class (not applied until intersection) ──
47
+
48
+ it('does not apply visible class on initial render', () => {
49
+ const { container } = render(ScrollReveal, { slots: { default: 'Test' } })
50
+ expect(container.firstElementChild?.className).not.toContain('strand-reveal--visible')
51
+ })
52
+
53
+ // ── Custom className ──
54
+
55
+ it('merges custom className with component classes', () => {
56
+ const { container } = render(ScrollReveal, {
57
+ props: { className: 'custom' },
58
+ slots: { default: 'Test' },
59
+ })
60
+ const el = container.firstElementChild
61
+ expect(el?.className).toContain('strand-reveal')
62
+ expect(el?.className).toContain('custom')
63
+ })
64
+
65
+ // ── Props forwarding ──
66
+
67
+ it('forwards additional attributes', () => {
68
+ const { container } = render(ScrollReveal, {
69
+ attrs: { id: 'reveal-1', 'data-testid': 'my-reveal' },
70
+ slots: { default: 'Test' },
71
+ })
72
+ expect(container.firstElementChild?.getAttribute('id')).toBe('reveal-1')
73
+ })
74
+ })
@@ -0,0 +1,68 @@
1
+ <!--! Strand Vue | MIT License | dillingerstaffing.com -->
2
+ <script setup lang="ts">
3
+ import { computed, ref, onMounted, onUnmounted } from 'vue'
4
+
5
+ interface Props {
6
+ /** Intersection threshold (0-1) to trigger reveal */
7
+ threshold?: number
8
+ /** Only reveal once (do not hide on exit) */
9
+ once?: boolean
10
+ /** Additional CSS class */
11
+ className?: string
12
+ }
13
+
14
+ const props = withDefaults(defineProps<Props>(), {
15
+ threshold: 0.1,
16
+ once: true,
17
+ className: '',
18
+ })
19
+
20
+ const elRef = ref<HTMLDivElement | null>(null)
21
+ const visible = ref(false)
22
+ let observer: IntersectionObserver | null = null
23
+
24
+ const classes = computed(() =>
25
+ [
26
+ 'strand-reveal',
27
+ visible.value && 'strand-reveal--visible',
28
+ props.className,
29
+ ]
30
+ .filter(Boolean)
31
+ .join(' '),
32
+ )
33
+
34
+ onMounted(() => {
35
+ if (!elRef.value) return
36
+
37
+ observer = new IntersectionObserver(
38
+ (entries) => {
39
+ for (const entry of entries) {
40
+ if (entry.isIntersecting) {
41
+ visible.value = true
42
+ if (props.once && observer && elRef.value) {
43
+ observer.unobserve(elRef.value)
44
+ }
45
+ } else if (!props.once) {
46
+ visible.value = false
47
+ }
48
+ }
49
+ },
50
+ { threshold: props.threshold },
51
+ )
52
+
53
+ observer.observe(elRef.value)
54
+ })
55
+
56
+ onUnmounted(() => {
57
+ if (observer) {
58
+ observer.disconnect()
59
+ observer = null
60
+ }
61
+ })
62
+ </script>
63
+
64
+ <template>
65
+ <div ref="elRef" :class="classes" v-bind="$attrs">
66
+ <slot />
67
+ </div>
68
+ </template>
@@ -0,0 +1 @@
1
+ export { default as ScrollReveal } from './ScrollReveal.vue'
package/src/index.ts CHANGED
@@ -26,6 +26,8 @@ export { default as Grid } from "./components/Grid/Grid.vue";
26
26
  export { default as Container } from "./components/Container/Container.vue";
27
27
  export { default as Divider } from "./components/Divider/Divider.vue";
28
28
  export { default as Section } from "./components/Section/Section.vue";
29
+ export { default as InstrumentViewport } from "./components/InstrumentViewport/InstrumentViewport.vue";
30
+ export { default as ScrollReveal } from "./components/ScrollReveal/ScrollReveal.vue";
29
31
 
30
32
  // Navigation
31
33
  export { default as Link } from "./components/Link/Link.vue";