@fiscozen/thumbnail 0.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.
@@ -0,0 +1,396 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import { FzThumbnail } from '..'
4
+
5
+ const SRC = 'https://example.com/receipt.jpg'
6
+
7
+ const createWrapper = (props = {}, options = {}) =>
8
+ mount(FzThumbnail, {
9
+ props: { src: SRC, alt: 'Scontrino di marzo', ...props },
10
+ global: { stubs: { FzIcon: true } },
11
+ ...options
12
+ })
13
+
14
+ const PLACEHOLDER = '[data-testid="fz-thumbnail-placeholder"]'
15
+ const SCRIM = '[data-testid="fz-thumbnail-scrim"]'
16
+ const OVERLAY = '[data-testid="fz-thumbnail-overlay"]'
17
+
18
+ describe('FzThumbnail', () => {
19
+ // ============================================
20
+ // RENDERING TESTS
21
+ // ============================================
22
+ describe('Rendering', () => {
23
+ it('should render the image from src', () => {
24
+ const wrapper = createWrapper()
25
+ const img = wrapper.find('img')
26
+ expect(img.exists()).toBe(true)
27
+ expect(img.attributes('src')).toBe(SRC)
28
+ })
29
+
30
+ it('should not render the placeholder while the image is fine', () => {
31
+ expect(createWrapper().find(PLACEHOLDER).exists()).toBe(false)
32
+ })
33
+
34
+ it('should render the placeholder instead of an image when src is empty', () => {
35
+ const wrapper = createWrapper({ src: '' })
36
+ expect(wrapper.find('img').exists()).toBe(false)
37
+ expect(wrapper.find(PLACEHOLDER).exists()).toBe(true)
38
+ })
39
+
40
+ it('should crop rather than distort the image', () => {
41
+ expect(createWrapper().find('img').classes()).toContain('object-cover')
42
+ })
43
+
44
+ it('should declare no width or height of its own, so the caller owns the box', () => {
45
+ // The whole point of the component: it must not repeat FzAvatar's hardwired
46
+ // sizes. Nothing on the root may set a dimension.
47
+ const wrapper = createWrapper()
48
+ const sizing = wrapper
49
+ .classes()
50
+ .filter((c) => /^(w-|h-|min-w-|min-h-|max-w-|max-h-|size-|aspect-)/.test(c))
51
+ expect(sizing).toEqual([])
52
+ expect(wrapper.attributes('style')).toBeUndefined()
53
+ })
54
+ })
55
+
56
+ // ============================================
57
+ // SIZING
58
+ // ============================================
59
+ describe('Sizing', () => {
60
+ // The dimensions are props rather than classes because the consuming apps
61
+ // forbid `class` at the organism/template/page layers and `style` above an
62
+ // atom — a class-only contract would make the component unusable there.
63
+ it('should apply width and height as an inline style', () => {
64
+ const wrapper = createWrapper({ width: '158px', height: '108px' })
65
+ const style = wrapper.attributes('style')
66
+ expect(style).toContain('width: 158px')
67
+ expect(style).toContain('height: 108px')
68
+ })
69
+
70
+ it('should accept any CSS length, not just px', () => {
71
+ const wrapper = createWrapper({ width: '100%', height: '12rem' })
72
+ const style = wrapper.attributes('style')
73
+ expect(style).toContain('width: 100%')
74
+ expect(style).toContain('height: 12rem')
75
+ })
76
+
77
+ it('should apply aspectRatio for a box with only one known dimension', () => {
78
+ const wrapper = createWrapper({ width: '100%', aspectRatio: '16 / 9' })
79
+ const style = wrapper.attributes('style')
80
+ expect(style).toContain('aspect-ratio: 16 / 9')
81
+ expect(style).not.toContain('height')
82
+ })
83
+
84
+ it('should leave an unset dimension out of the style attribute entirely', () => {
85
+ // Not `width: undefined`, which would be a live declaration a caller class
86
+ // then has to fight.
87
+ const style = createWrapper({ height: '168px' }).attributes('style')
88
+ expect(style).toBe('height: 168px;')
89
+ })
90
+ })
91
+
92
+ // ============================================
93
+ // PROPS TESTS
94
+ // ============================================
95
+ describe('Props', () => {
96
+ it('should apply the 4px radius by default', () => {
97
+ expect(createWrapper().classes()).toContain('rounded')
98
+ })
99
+
100
+ it.each([
101
+ ['none', 'rounded-none'],
102
+ ['sm', 'rounded-sm'],
103
+ ['base', 'rounded'],
104
+ ['lg', 'rounded-lg'],
105
+ ['xl', 'rounded-xl']
106
+ ] as const)('should map radius "%s" to %s', (radius, expected) => {
107
+ expect(createWrapper({ radius }).classes()).toContain(expected)
108
+ })
109
+
110
+ it('should draw no border by default', () => {
111
+ expect(createWrapper().classes()).not.toContain('border-1')
112
+ })
113
+
114
+ it('should draw a grey border when bordered', () => {
115
+ const classes = createWrapper({ bordered: true }).classes()
116
+ expect(classes).toContain('border-1')
117
+ expect(classes).toContain('border-grey-100')
118
+ })
119
+
120
+ it('should draw no scrim by default', () => {
121
+ expect(createWrapper().find(SCRIM).exists()).toBe(false)
122
+ })
123
+
124
+ it('should draw the scrim over the image when asked', () => {
125
+ expect(createWrapper({ scrim: true }).find(SCRIM).exists()).toBe(true)
126
+ })
127
+
128
+ it('should lazy-load by default and allow eager', () => {
129
+ expect(createWrapper().find('img').attributes('loading')).toBe('lazy')
130
+ expect(createWrapper({ loading: 'eager' }).find('img').attributes('loading')).toBe('eager')
131
+ })
132
+
133
+ it('should apply imgProps to the image, not the root box', () => {
134
+ // A fallthrough attribute would land on the wrapping div, where
135
+ // referrerpolicy means nothing — hence the explicit prop.
136
+ const wrapper = createWrapper({
137
+ imgProps: { referrerpolicy: 'no-referrer', decoding: 'async' }
138
+ })
139
+ const img = wrapper.find('img')
140
+ expect(img.attributes('referrerpolicy')).toBe('no-referrer')
141
+ expect(img.attributes('decoding')).toBe('async')
142
+ expect(wrapper.attributes('referrerpolicy')).toBeUndefined()
143
+ })
144
+
145
+ it("should not let imgProps override the component's own contract", () => {
146
+ // These four keys are `Omit`ted from `imgProps`' type, so a real call site
147
+ // cannot name them at all. `createWrapper` takes untyped props, which is
148
+ // what lets this assert the runtime behaviour behind that type.
149
+ const wrapper = createWrapper({
150
+ imgProps: { src: '/hijacked.jpg', alt: 'hijacked', loading: 'eager' }
151
+ })
152
+ const img = wrapper.find('img')
153
+ expect(img.attributes('src')).toBe(SRC)
154
+ expect(img.attributes('alt')).toBe('Scontrino di marzo')
155
+ expect(img.attributes('loading')).toBe('lazy')
156
+ })
157
+
158
+ it('should chain, not replace, an error handler smuggled through imgProps', () => {
159
+ // Why `onError` is *excluded from the type* rather than merely documented
160
+ // as losing to the component's own: it does not lose. Vue's `mergeProps`
161
+ // collects `onXxx` into an array, so a handler named here fires alongside
162
+ // the internal one. The component still fails over to the placeholder —
163
+ // nothing is broken — but "the component's handler wins" would be a lie,
164
+ // and the type is what makes it moot.
165
+ const hijack = vi.fn()
166
+ const wrapper = createWrapper({ imgProps: { onError: hijack } })
167
+ wrapper.find('img').trigger('error')
168
+ expect(hijack).toHaveBeenCalledTimes(1)
169
+ })
170
+
171
+ it('should pass placeholderIcon through to FzIcon', async () => {
172
+ const wrapper = createWrapper({
173
+ src: '',
174
+ placeholderIcon: 'triangle-exclamation'
175
+ })
176
+ expect(wrapper.find(PLACEHOLDER).html()).toContain('triangle-exclamation')
177
+ })
178
+ })
179
+
180
+ // ============================================
181
+ // EVENTS TESTS
182
+ // ============================================
183
+ describe('Events', () => {
184
+ it('should swap to the placeholder when the image fails to load', async () => {
185
+ const wrapper = createWrapper()
186
+ await wrapper.find('img').trigger('error')
187
+ expect(wrapper.find('img').exists()).toBe(false)
188
+ expect(wrapper.find(PLACEHOLDER).exists()).toBe(true)
189
+ })
190
+
191
+ it('should emit error with the failing src', async () => {
192
+ const wrapper = createWrapper()
193
+ await wrapper.find('img').trigger('error')
194
+ expect(wrapper.emitted('error')).toEqual([[SRC]])
195
+ })
196
+
197
+ it('should retry when src changes after a failure', async () => {
198
+ const wrapper = createWrapper()
199
+ await wrapper.find('img').trigger('error')
200
+ expect(wrapper.find('img').exists()).toBe(false)
201
+
202
+ await wrapper.setProps({ src: 'https://example.com/other.jpg' })
203
+ expect(wrapper.find('img').exists()).toBe(true)
204
+ expect(wrapper.find(PLACEHOLDER).exists()).toBe(false)
205
+ })
206
+
207
+ it('should hide the scrim once the image has failed', async () => {
208
+ const wrapper = createWrapper({ scrim: true })
209
+ expect(wrapper.find(SCRIM).exists()).toBe(true)
210
+ await wrapper.find('img').trigger('error')
211
+ expect(wrapper.find(SCRIM).exists()).toBe(false)
212
+ })
213
+ })
214
+
215
+ // ============================================
216
+ // SLOTS TESTS
217
+ // ============================================
218
+ describe('Slots', () => {
219
+ it('should render no overlay layer when the slot is unused', () => {
220
+ expect(createWrapper().find(OVERLAY).exists()).toBe(false)
221
+ })
222
+
223
+ it('should render the overlay slot above the image', () => {
224
+ const wrapper = createWrapper({}, { slots: { overlay: '<button>Scarica</button>' } })
225
+ const overlay = wrapper.find(OVERLAY)
226
+ expect(overlay.exists()).toBe(true)
227
+ expect(overlay.find('button').text()).toBe('Scarica')
228
+ })
229
+
230
+ it('should keep the overlay layer inert but its children clickable', () => {
231
+ // Otherwise the layer covers the whole image and eats every click that was
232
+ // meant for the thumbnail itself.
233
+ const wrapper = createWrapper({}, { slots: { overlay: '<button>Scarica</button>' } })
234
+ const classes = wrapper.find(OVERLAY).classes()
235
+ expect(classes).toContain('pointer-events-none')
236
+ expect(classes).toContain('[&>*]:pointer-events-auto')
237
+ })
238
+
239
+ it('should pin the overlay bottom-end by default', () => {
240
+ const wrapper = createWrapper({}, { slots: { overlay: '<button>Scarica</button>' } })
241
+ const classes = wrapper.find(OVERLAY).classes()
242
+ expect(classes).toContain('items-end')
243
+ expect(classes).toContain('justify-end')
244
+ })
245
+
246
+ it.each([
247
+ ['top-start', ['items-start', 'justify-start']],
248
+ ['top-end', ['items-start', 'justify-end']],
249
+ ['bottom-start', ['items-end', 'justify-start']],
250
+ ['bottom-end', ['items-end', 'justify-end']],
251
+ ['center', ['items-center', 'justify-center']]
252
+ ] as const)(
253
+ 'should place the overlay at %s without the call site writing a class',
254
+ (overlayPosition, expected) => {
255
+ const wrapper = createWrapper(
256
+ { overlayPosition },
257
+ { slots: { overlay: '<button>Scarica</button>' } }
258
+ )
259
+ const classes = wrapper.find(OVERLAY).classes()
260
+ for (const c of expected) expect(classes).toContain(c)
261
+ }
262
+ )
263
+
264
+ it('should still render the overlay over the placeholder', async () => {
265
+ const wrapper = createWrapper({ src: '' }, { slots: { overlay: '<button>Rimuovi</button>' } })
266
+ expect(wrapper.find(OVERLAY).exists()).toBe(true)
267
+ })
268
+ })
269
+
270
+ // ============================================
271
+ // ACCESSIBILITY TESTS
272
+ // ============================================
273
+ describe('Accessibility', () => {
274
+ it('should put alt on the image', () => {
275
+ expect(createWrapper().find('img').attributes('alt')).toBe('Scontrino di marzo')
276
+ })
277
+
278
+ it('should honour an explicitly empty alt for a decorative image', () => {
279
+ // `alt` is required by the type, so `alt=""` is a decision the call site
280
+ // had to make. It must reach the DOM as an empty attribute, not be dropped:
281
+ // a missing alt makes a screen reader read the URL out loud.
282
+ const wrapper = createWrapper({ alt: '' })
283
+ expect(wrapper.find('img').attributes('alt')).toBe('')
284
+ })
285
+
286
+ it('should give the empty-src placeholder no accessible name of its own', () => {
287
+ // FzIcon is role="presentation"; with no src there was never an image to
288
+ // name, so the placeholder must not invent a label the caller did not write.
289
+ const wrapper = createWrapper({ src: '' })
290
+ const placeholder = wrapper.find(PLACEHOLDER)
291
+ expect(placeholder.attributes('aria-label')).toBeUndefined()
292
+ expect(placeholder.attributes('role')).toBeUndefined()
293
+ })
294
+
295
+ it('should keep the accessible name when a named image fails to load', async () => {
296
+ // A native <img alt="..."> whose resource 404s still exposes its alt in the
297
+ // accessibility tree — the element survives, only the pixels are missing.
298
+ // Because this component swaps the <img> out for a div, it has to carry the
299
+ // name across itself or the screen-reader user ends up worse off than the
300
+ // sighted one, who at least sees the placeholder icon.
301
+ const wrapper = createWrapper()
302
+ await wrapper.find('img').trigger('error')
303
+
304
+ const placeholder = wrapper.find(PLACEHOLDER)
305
+ expect(placeholder.attributes('role')).toBe('img')
306
+ expect(placeholder.attributes('aria-label')).toBe('Scontrino di marzo')
307
+ })
308
+
309
+ it('should stay silent when a decorative image fails to load', async () => {
310
+ // alt="" is the caller saying "this carries no information". A failure does
311
+ // not turn it into something worth announcing.
312
+ const wrapper = createWrapper({ alt: '' })
313
+ await wrapper.find('img').trigger('error')
314
+
315
+ const placeholder = wrapper.find(PLACEHOLDER)
316
+ expect(placeholder.attributes('role')).toBeUndefined()
317
+ expect(placeholder.attributes('aria-label')).toBeUndefined()
318
+ })
319
+
320
+ it('should drop the accessible name again once a new src retries', async () => {
321
+ const wrapper = createWrapper()
322
+ await wrapper.find('img').trigger('error')
323
+ expect(wrapper.find(PLACEHOLDER).attributes('role')).toBe('img')
324
+
325
+ await wrapper.setProps({ src: '/another.jpg' })
326
+ expect(wrapper.find('img').exists()).toBe(true)
327
+ expect(wrapper.find(PLACEHOLDER).exists()).toBe(false)
328
+ })
329
+ })
330
+
331
+ // ============================================
332
+ // EDGE CASES
333
+ // ============================================
334
+ describe('Edge Cases', () => {
335
+ it('should clip whatever overflows the rounded box', () => {
336
+ expect(createWrapper().classes()).toContain('overflow-hidden')
337
+ })
338
+
339
+ it('should establish a positioning context for the overlay', () => {
340
+ expect(createWrapper().classes()).toContain('relative')
341
+ })
342
+
343
+ it('should keep two instances independent after one fails', async () => {
344
+ const a = createWrapper()
345
+ const b = createWrapper()
346
+ await a.find('img').trigger('error')
347
+ expect(a.find('img').exists()).toBe(false)
348
+ expect(b.find('img').exists()).toBe(true)
349
+ })
350
+
351
+ it('should give the placeholder no size of its own either', () => {
352
+ // The placeholder fills the box and adds nothing to it — no min-height, no
353
+ // aspect ratio. That is why an *unsized* box collapses on a load failure
354
+ // while a loaded image keeps its natural ratio: `height: 100%` against an
355
+ // auto-height parent resolves to `auto`, and unlike an image the
356
+ // placeholder has no intrinsic ratio to fill it with.
357
+ //
358
+ // jsdom has no layout engine, so the measurements themselves are in the
359
+ // `Unsized` Storybook story. This locks the classes they follow from, so
360
+ // that adding a min-height here has to be a deliberate decision — it would
361
+ // hand the component a size of its own, which is the thing it refuses.
362
+ const classes = createWrapper({ src: '' }).find(PLACEHOLDER).classes()
363
+ expect(classes).toContain('size-full')
364
+ expect(classes.filter((c) => /^(min-h-|min-w-|aspect-|h-|w-)/.test(c))).toEqual([])
365
+ })
366
+
367
+ it('should forward a caller class alongside its own', () => {
368
+ const wrapper = createWrapper({}, { attrs: { class: 'h-[168px] w-full' } })
369
+ const classes = wrapper.classes()
370
+ expect(classes).toContain('h-[168px]')
371
+ expect(classes).toContain('w-full')
372
+ expect(classes).toContain('fz-thumbnail')
373
+ })
374
+ })
375
+
376
+ // ============================================
377
+ // SNAPSHOTS
378
+ // ============================================
379
+ describe('Snapshots', () => {
380
+ it('should match the default snapshot', () => {
381
+ expect(createWrapper().html()).toMatchSnapshot()
382
+ })
383
+
384
+ it('should match the scrim + border + overlay snapshot', () => {
385
+ const wrapper = createWrapper(
386
+ { scrim: true, bordered: true },
387
+ { slots: { overlay: '<button>Scarica</button>' } }
388
+ )
389
+ expect(wrapper.html()).toMatchSnapshot()
390
+ })
391
+
392
+ it('should match the placeholder snapshot', () => {
393
+ expect(createWrapper({ src: '' }).html()).toMatchSnapshot()
394
+ })
395
+ })
396
+ })
@@ -0,0 +1,47 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`FzThumbnail > Snapshots > should match the default snapshot 1`] = `
4
+ "<div data-v-40132b1d="" class="fz-thumbnail bg-core-white relative overflow-hidden rounded"><img data-v-40132b1d="" src="https://example.com/receipt.jpg" alt="Scontrino di marzo" loading="lazy" class="block size-full object-cover">
5
+ <!--v-if-->
6
+ <!-- Overlay layer. Inert itself so it does not swallow clicks meant for the
7
+ image, with pointer events handed back to whatever the caller puts in
8
+ it. A flex box rather than bare \`absolute inset-0\`, so that dropping a
9
+ button in the slot places it without the call site writing a class —
10
+ which the organism, template and page layers may not do. -->
11
+ <!--v-if-->
12
+ </div>"
13
+ `;
14
+
15
+ exports[`FzThumbnail > Snapshots > should match the placeholder snapshot 1`] = `
16
+ "<div data-v-40132b1d="" class="fz-thumbnail bg-core-white relative overflow-hidden rounded">
17
+ <!-- Placeholder. Fills the same box, so a broken URL leaves the layout
18
+ alone instead of collapsing the row around it — as long as the box has
19
+ a size of its own. An unsized box is only as tall as its content, and
20
+ a placeholder, unlike an image, has no intrinsic ratio to supply that
21
+ height with, so it collapses to the icon. Deliberately not given a
22
+ min-height: that would hand the component a size of its own, which is
23
+ the thing it refuses. Measured in the \`Unsized\` story. -->
24
+ <div data-v-40132b1d="" class="bg-grey-100 grid size-full place-content-center" data-testid="fz-thumbnail-placeholder">
25
+ <fz-icon-stub data-v-40132b1d="" name="file" size="lg" variant="far" spin="false" class="text-grey-300"></fz-icon-stub>
26
+ </div>
27
+ <!--v-if-->
28
+ <!-- Overlay layer. Inert itself so it does not swallow clicks meant for the
29
+ image, with pointer events handed back to whatever the caller puts in
30
+ it. A flex box rather than bare \`absolute inset-0\`, so that dropping a
31
+ button in the slot places it without the call site writing a class —
32
+ which the organism, template and page layers may not do. -->
33
+ <!--v-if-->
34
+ </div>"
35
+ `;
36
+
37
+ exports[`FzThumbnail > Snapshots > should match the scrim + border + overlay snapshot 1`] = `
38
+ "<div data-v-40132b1d="" class="fz-thumbnail bg-core-white relative overflow-hidden rounded border-1 border-grey-100 border-solid"><img data-v-40132b1d="" src="https://example.com/receipt.jpg" alt="Scontrino di marzo" loading="lazy" class="block size-full object-cover">
39
+ <div data-v-40132b1d="" class="fz-thumbnail__scrim absolute inset-0" data-testid="fz-thumbnail-scrim"></div>
40
+ <!-- Overlay layer. Inert itself so it does not swallow clicks meant for the
41
+ image, with pointer events handed back to whatever the caller puts in
42
+ it. A flex box rather than bare \`absolute inset-0\`, so that dropping a
43
+ button in the slot places it without the call site writing a class —
44
+ which the organism, template and page layers may not do. -->
45
+ <div data-v-40132b1d="" class="pointer-events-none absolute inset-0 flex p-8 [&amp;>*]:pointer-events-auto items-end justify-end" data-testid="fz-thumbnail-overlay"><button>Scarica</button></div>
46
+ </div>"
47
+ `;
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { default as FzThumbnail } from './FzThumbnail.vue'
2
+ export type * from './types'
package/src/types.ts ADDED
@@ -0,0 +1,98 @@
1
+ import type { ImgHTMLAttributes } from 'vue'
2
+
3
+ /**
4
+ * Corner radius, named after the design system's radius tokens.
5
+ * `base` is 4px — the value the designs use for an image in a feed.
6
+ */
7
+ type FzThumbnailRadius = 'none' | 'sm' | 'base' | 'lg' | 'xl'
8
+
9
+ /**
10
+ * Corner the `overlay` slot's content is pinned to.
11
+ */
12
+ type FzThumbnailOverlayPosition = 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end' | 'center'
13
+
14
+ /**
15
+ * Props for FzThumbnail.
16
+ */
17
+ export interface FzThumbnailProps {
18
+ /** URL of the image to show. */
19
+ src: string
20
+ /**
21
+ * The image's accessible name. Required on purpose: pass an explicit empty
22
+ * string for a decorative image, so that the decision is visible at the call
23
+ * site rather than forgotten.
24
+ */
25
+ alt: string
26
+ /**
27
+ * Box width, as any CSS length (`'158px'`, `'100%'`, `'12rem'`).
28
+ *
29
+ * A prop rather than a class on purpose. This component hardwires no size, so
30
+ * the caller has to supply one — but the consuming apps forbid `class` outright
31
+ * at the organism, template and page layers, and `style` at every layer above
32
+ * an atom. Deciding it inside the component is the only way those layers can
33
+ * size a thumbnail at all. Same reasoning as `FzNavbar`'s `elevation`.
34
+ *
35
+ * A Tailwind class on the call site still works where the layer permits one,
36
+ * and wins over this prop only if it beats an inline style — so pick one.
37
+ */
38
+ width?: string
39
+ /** Box height, as any CSS length. See `width`. */
40
+ height?: string
41
+ /**
42
+ * Box aspect ratio (`'16 / 9'`, `'1'`), for when only one dimension is known —
43
+ * a thumbnail filling a column of unknown width, say. Ignored if both `width`
44
+ * and `height` are set.
45
+ */
46
+ aspectRatio?: string
47
+ /** Corner radius. Defaults to `base` (4px). */
48
+ radius?: FzThumbnailRadius
49
+ /** Draws a 1px `grey-100` border around the box. */
50
+ bordered?: boolean
51
+ /**
52
+ * Lays a translucent scrim over the image, so an action rendered in the
53
+ * `overlay` slot stays legible on a light photo. Not drawn over the
54
+ * placeholder, which is already a flat light surface.
55
+ */
56
+ scrim?: boolean
57
+ /**
58
+ * Where the `overlay` slot's content sits inside the box, 8px in from the
59
+ * chosen corner. `bottom-end` by default — the download button's place in the
60
+ * chat feed; `top-end` is the composer's remove control.
61
+ *
62
+ * A prop for the same reason the dimensions are: the layers that may not write
63
+ * a `class` would otherwise have no way to place an overlaid action. Content
64
+ * that needs finer placement can still position itself, from a layer that
65
+ * permits it.
66
+ */
67
+ overlayPosition?: FzThumbnailOverlayPosition
68
+ /**
69
+ * Icon shown in place of the image when it fails to load. Any name in the
70
+ * Font Awesome kit; `file` by default, since the kit has no image glyph.
71
+ */
72
+ placeholderIcon?: string
73
+ /** Native loading hint. `lazy` by default — a feed of images is the use case. */
74
+ loading?: 'lazy' | 'eager'
75
+ /**
76
+ * Extra attributes for the `<img>` itself — `referrerpolicy`, `crossorigin`,
77
+ * `decoding`, `fetchpriority`, `srcset`, `sizes`.
78
+ *
79
+ * Needed because a fallthrough attribute lands on the root box, not the image:
80
+ * the component has one root element, so `<FzThumbnail referrerpolicy="...">`
81
+ * would set it on the wrapping `<div>`, where it means nothing. That keeps
82
+ * `class` and listeners on the box, which is what a caller sizing or clicking
83
+ * the thumbnail wants — but it leaves the image itself out of reach without
84
+ * this.
85
+ *
86
+ * `src`, `alt`, `loading` and `onError` are the component's own contract, so
87
+ * the type excludes them: naming one is a compile error rather than a silent
88
+ * no-op. `onError` has to be *excluded* rather than merely documented —
89
+ * Vue's `mergeProps` chains listeners, so a handler named here would fire
90
+ * alongside the component's own rather than losing to it.
91
+ *
92
+ * @example Do not send a Referer to a third-party image host.
93
+ * :imgProps="{ referrerpolicy: 'no-referrer' }"
94
+ */
95
+ imgProps?: Omit<ImgHTMLAttributes, 'src' | 'alt' | 'loading' | 'onError'>
96
+ }
97
+
98
+ export type { FzThumbnailRadius, FzThumbnailOverlayPosition }
package/tsconfig.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "@fiscozen/tsconfig",
3
+ "exclude": ["src/__tests__", "vite.config.ts", "vitest.config.ts"]
4
+ }
@@ -0,0 +1 @@
1
+ {"fileNames":["../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.es5.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.es2015.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.es2016.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.es2017.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.es2018.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.es2019.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.es2020.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.dom.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.dom.iterable.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.es2015.core.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.es2015.collection.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.es2015.generator.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.es2015.promise.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.es2016.intl.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.es2017.date.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.es2017.object.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.es2017.string.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.es2017.intl.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.es2018.intl.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.es2018.promise.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.es2019.array.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.es2019.object.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.es2019.string.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.es2019.symbol.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.es2019.intl.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.es2020.date.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.es2020.promise.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.es2020.string.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.es2020.intl.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.es2020.number.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.decorators.d.ts","../../node_modules/.pnpm/typescript@5.7.3/node_modules/typescript/lib/lib.decorators.legacy.d.ts","../../node_modules/.pnpm/@vue+shared@3.5.26/node_modules/@vue/shared/dist/shared.d.ts","../../node_modules/.pnpm/@babel+types@7.29.0/node_modules/@babel/types/lib/index.d.ts","../../node_modules/.pnpm/@babel+types@7.28.6/node_modules/@babel/types/lib/index.d.ts","../../node_modules/.pnpm/@babel+parser@7.28.6/node_modules/@babel/parser/typings/babel-parser.d.ts","../../node_modules/.pnpm/@vue+compiler-core@3.5.26/node_modules/@vue/compiler-core/dist/compiler-core.d.ts","../../node_modules/.pnpm/@vue+compiler-dom@3.5.26/node_modules/@vue/compiler-dom/dist/compiler-dom.d.ts","../../node_modules/.pnpm/@vue+reactivity@3.5.26/node_modules/@vue/reactivity/dist/reactivity.d.ts","../../node_modules/.pnpm/@vue+runtime-core@3.5.26/node_modules/@vue/runtime-core/dist/runtime-core.d.ts","../../node_modules/.pnpm/csstype@3.2.3/node_modules/csstype/index.d.ts","../../node_modules/.pnpm/@vue+runtime-dom@3.5.26/node_modules/@vue/runtime-dom/dist/runtime-dom.d.ts","../../node_modules/.pnpm/vue@3.5.26_typescript@5.7.3/node_modules/vue/dist/vue.d.mts","./dist/src/types.d.ts","./dist/src/fzthumbnail.vue.d.ts","./dist/src/index.d.ts","./dist/index.d.ts","../../node_modules/.pnpm/vue@3.5.26_typescript@5.7.3/node_modules/vue/jsx-runtime/index.d.ts","./node_modules/.vue-global-types/vue_3.5_0_0_0.d.ts","../../node_modules/.pnpm/@fortawesome+fontawesome-common-types@6.7.2/node_modules/@fortawesome/fontawesome-common-types/index.d.ts","../../node_modules/.pnpm/@fortawesome+fontawesome-svg-core@6.7.2/node_modules/@fortawesome/fontawesome-svg-core/index.d.ts","../../node_modules/.pnpm/@awesome.me+kit-8137893ad3@1.0.430/node_modules/@awesome.me/kit-8137893ad3/icons/modules/icon-types.ts","../../node_modules/.pnpm/@awesome.me+kit-8137893ad3@1.0.430/node_modules/@awesome.me/kit-8137893ad3/icons/modules/index.d.ts","../../node_modules/.pnpm/@fortawesome+vue-fontawesome@3.1.3_@fortawesome+fontawesome-svg-core@6.7.2_vue@3.5.26_typescript@5.7.3_/node_modules/@fortawesome/vue-fontawesome/index.d.ts","../icons/src/types.ts","../icons/src/fzicon.vue","../icons/src/fziconbackground.vue","../icons/src/index.ts","./src/types.ts","./src/fzthumbnail.vue","./src/index.ts"],"fileIdsList":[[63,65],[67],[50],[65],[58,66],[48,49,51],[52],[48],[48,54,55,57],[54,55,56,57],[53,57],[57],[58,63,64,68,69,70],[58,63,64,70,71],[58,63,66,68,70,71,72],[63],[61],[58,59],[59,60],[58],[58,63],[58,63,64,73,74],[63,74,75]],"fileInfos":[{"version":"e41c290ef7dd7dab3493e6cbe5909e0148edf4a8dad0271be08edec368a0f7b9","affectsGlobalScope":true,"impliedFormat":1},{"version":"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","impliedFormat":1},{"version":"3facaf05f0c5fc569c5649dd359892c98a85557e3e0c847964caeb67076f4d75","impliedFormat":1},{"version":"e44bb8bbac7f10ecc786703fe0a6a4b952189f908707980ba8f3c8975a760962","impliedFormat":1},{"version":"5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","impliedFormat":1},{"version":"68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","impliedFormat":1},{"version":"5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","impliedFormat":1},{"version":"4fd3f3422b2d2a3dfd5cdd0f387b3a8ec45f006c6ea896a4cb41264c2100bb2c","affectsGlobalScope":true,"impliedFormat":1},{"version":"69e65d976bf166ce4a9e6f6c18f94d2424bf116e90837ace179610dbccad9b42","affectsGlobalScope":true,"impliedFormat":1},{"version":"c57796738e7f83dbc4b8e65132f11a377649c00dd3eee333f672b8f0a6bea671","affectsGlobalScope":true,"impliedFormat":1},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","affectsGlobalScope":true,"impliedFormat":1},{"version":"515d0b7b9bea2e31ea4ec968e9edd2c39d3eebf4a2d5cbd04e88639819ae3b71","affectsGlobalScope":true,"impliedFormat":1},{"version":"62bb211266ee48b2d0edf0d8d1b191f0c24fc379a82bd4c1692a082c540bc6b1","affectsGlobalScope":true,"impliedFormat":1},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true,"impliedFormat":1},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","affectsGlobalScope":true,"impliedFormat":1},{"version":"936e80ad36a2ee83fc3caf008e7c4c5afe45b3cf3d5c24408f039c1d47bdc1df","affectsGlobalScope":true,"impliedFormat":1},{"version":"d15bea3d62cbbdb9797079416b8ac375ae99162a7fba5de2c6c505446486ac0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"68d18b664c9d32a7336a70235958b8997ebc1c3b8505f4f1ae2b7e7753b87618","affectsGlobalScope":true,"impliedFormat":1},{"version":"eb3d66c8327153d8fa7dd03f9c58d351107fe824c79e9b56b462935176cdf12a","affectsGlobalScope":true,"impliedFormat":1},{"version":"38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","affectsGlobalScope":true,"impliedFormat":1},{"version":"69ab18c3b76cd9b1be3d188eaf8bba06112ebbe2f47f6c322b5105a6fbc45a2e","affectsGlobalScope":true,"impliedFormat":1},{"version":"fef8cfad2e2dc5f5b3d97a6f4f2e92848eb1b88e897bb7318cef0e2820bceaab","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true,"impliedFormat":1},{"version":"4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"954296b30da6d508a104a3a0b5d96b76495c709785c1d11610908e63481ee667","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac9538681b19688c8eae65811b329d3744af679e0bdfa5d842d0e32524c73e1c","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a969edff4bd52585473d24995c5ef223f6652d6ef46193309b3921d65dd4376","affectsGlobalScope":true,"impliedFormat":1},{"version":"9e9fbd7030c440b33d021da145d3232984c8bb7916f277e8ffd3dc2e3eae2bdb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","affectsGlobalScope":true,"impliedFormat":1},{"version":"717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","affectsGlobalScope":true,"impliedFormat":1},{"version":"71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","affectsGlobalScope":true,"impliedFormat":1},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true,"impliedFormat":1},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true,"impliedFormat":1},{"version":"74f7fa2d027d5b33eb0471c8e82a6c87216223181ec31247c357a3e8e2fddc5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"f1e2a172204962276504466a6393426d2ca9c54894b1ad0a6c9dad867a65f876","affectsGlobalScope":true,"impliedFormat":1},{"version":"063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","affectsGlobalScope":true,"impliedFormat":1},{"version":"934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","affectsGlobalScope":true,"impliedFormat":1},{"version":"52ada8e0b6e0482b728070b7639ee42e83a9b1c22d205992756fe020fd9f4a47","affectsGlobalScope":true,"impliedFormat":1},{"version":"3bdefe1bfd4d6dee0e26f928f93ccc128f1b64d5d501ff4a8cf3c6371200e5e6","affectsGlobalScope":true,"impliedFormat":1},{"version":"59fb2c069260b4ba00b5643b907ef5d5341b167e7d1dbf58dfd895658bda2867","affectsGlobalScope":true,"impliedFormat":1},{"version":"639e512c0dfc3fad96a84caad71b8834d66329a1f28dc95e3946c9b58176c73a","affectsGlobalScope":true,"impliedFormat":1},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e7f8264d0fb4c5339605a15daadb037bf238c10b654bb3eee14208f860a32ea","affectsGlobalScope":true,"impliedFormat":1},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true,"impliedFormat":1},{"version":"f468b74459f1ad4473b36a36d49f2b255f3c6b5d536c81239c2b2971df089eaf","impliedFormat":1},{"version":"556ccd493ec36c7d7cb130d51be66e147b91cc1415be383d71da0f1e49f742a9","impliedFormat":1},{"version":"511a5f4f77165dc1b73ceae1e28b4a8f78f3443d8e18a1fd43bfafd2b0133bbe","impliedFormat":1},{"version":"95aba78013d782537cc5e23868e736bec5d377b918990e28ed56110e3ae8b958","impliedFormat":1},{"version":"e63d565526fcb1a4cdd35e4c3d6dedc0a967933623bf2316ddab29ad2f62203a","impliedFormat":1},{"version":"ebe84ad8344962b7117a3b95065f47383215020eaf1b626463863b45b4d16e62","impliedFormat":1},{"version":"4ff3cc3b6d36d0b2f8353b88e41c757d40a1e66200fb87415b9e284ed1b19ed9","impliedFormat":1},{"version":"27296e9e4b9a6b8747afb6505b2cc5456343b06fa18cd0453b5c15dfe78d1e6d","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac51dd7d31333793807a6abaa5ae168512b6131bd41d9c5b98477fc3b7800f9f","impliedFormat":1},{"version":"df513895e96e0c3ba6a73d96c762b9fbc4f277f06f855d756e29b4068c9b8aff","impliedFormat":1},{"version":"c0191592be8eb7906f99ac4b8798d80a585b94001ea1a5f50d6ce5b0d13a5c62","impliedFormat":99},"11e5efed0afa03fe370d7493283d3018c95347ebd7181c750cc655e2594d4714","d91ef3f584d94a92a3fb9a89b3bd02740dac543453e81ac3103eeac6a0601ab7","2a77e212731f1d58acd9168628cc3578c5f37f9ddd13674578d55c073c4c0eb3","f0daef1fbc6425eba1104910fadeb40cedfc08863791ea7ef44b88a86d6dd5c4",{"version":"318d19118bf6bf8d088441c948990f53cafc79ed581b78f3d41a0f7a3f5f145c","impliedFormat":1},{"version":"25900318042675aee6d709c82309effd29c995d03f92f8b7a469d38e07c7f846","affectsGlobalScope":true,"impliedFormat":99},{"version":"b3f4d51270e5e21b4ed504eb4f091940d6529acdd10c036cb35e021d438ec168","impliedFormat":1},{"version":"7859ab6422f18d61fd9e9a40d5564ace4651f999e2627f0e06c4d83684697262","impliedFormat":1},{"version":"59555e00e20fbc255aaea83764309dafb70650f7611007b4863f3be672cf888c","impliedFormat":1},{"version":"90f1fd3a86a8989337c044eb12be60f0677ddba65fddcd7da00b1028f9e07abc","impliedFormat":1},{"version":"e6e9cce1a55de876b81d1fa1ce486dd59a2b906c16ba40c582ebc895b50c6a08","impliedFormat":1},"d6cc010ba00a6eb5eb4c06393504812f07a3c445bfc761b4526141bb13e327c2","e13f6c840ddfe391259c5c8cfcf7c3c3bef0b63e4339b414a009a83badc68290","dec0c7622b8c2393440bb7beb02b97b493dddc36a4d9ff19b7ae042052136abf","cd2349d32b2dba6286becb27fce210761c6beec409a247a4093f574df9c539c7",{"version":"04931a6ecb6da26dd34deb4c967de5f59e011d3ad8cdb2f3a66412fc779e195d","signature":"8eb6cfa7ab062382e41885240c2fac5ce650afd058f25c2852f3b6b099547cec"},{"version":"c4c7182959124822942b3530793993b8059a6808d37537de1fc1fd66064689cd","signature":"20040e344efd04c2314119c0ceb798490e1e1820abbf3efd243291faadfc06e4"},{"version":"9ae033b6c6e7dda571cb5365f0b45bddf688059938b429d57132c837958b1d75","signature":"2a77e212731f1d58acd9168628cc3578c5f37f9ddd13674578d55c073c4c0eb3"}],"root":[[59,62],[74,76]],"options":{"composite":true,"esModuleInterop":true,"jsx":1,"jsxImportSource":"vue","module":99,"noImplicitThis":true,"skipLibCheck":true,"strict":true,"target":99,"useDefineForClassFields":true,"verbatimModuleSyntax":false},"referencedMap":[[67,1],[68,2],[51,3],[66,4],[69,5],[52,6],[53,7],[54,8],[55,9],[57,10],[58,11],[63,12],[71,13],[72,14],[73,15],[70,16],[62,17],[60,18],[61,19],[59,20],[64,21],[75,22],[76,23],[74,21]],"affectedFilesPendingEmit":[[75,17],[76,17],[74,17]],"emitSignatures":[74,75,76],"version":"5.7.3"}
package/vite.config.ts ADDED
@@ -0,0 +1,34 @@
1
+ import { fileURLToPath, URL } from 'node:url'
2
+ import { resolve } from 'node:path'
3
+ import { defineConfig } from 'vite'
4
+ import vue from '@vitejs/plugin-vue'
5
+ import dts from 'vite-plugin-dts'
6
+
7
+ // https://vitejs.dev/config/
8
+ export default defineConfig({
9
+ plugins: [
10
+ vue(),
11
+ dts({
12
+ insertTypesEntry: true
13
+ })
14
+ ],
15
+ resolve: {
16
+ alias: {
17
+ '@': fileURLToPath(new URL('./src', import.meta.url))
18
+ }
19
+ },
20
+ build: {
21
+ lib: {
22
+ entry: resolve(__dirname, './src/index.ts'),
23
+ name: 'FzThumbnail'
24
+ },
25
+ rollupOptions: {
26
+ external: ['vue', '@fiscozen/icons'],
27
+ output: {
28
+ globals: {
29
+ vue: 'Vue'
30
+ }
31
+ }
32
+ }
33
+ }
34
+ })
@@ -0,0 +1,26 @@
1
+ import { fileURLToPath } from 'node:url'
2
+ import { mergeConfig, defineConfig, configDefaults } from 'vitest/config'
3
+ import viteConfig from './vite.config'
4
+
5
+ export default mergeConfig(
6
+ viteConfig,
7
+ defineConfig({
8
+ test: {
9
+ environment: 'jsdom',
10
+ exclude: [...configDefaults.exclude, 'e2e/*'],
11
+ root: fileURLToPath(new URL('./', import.meta.url)),
12
+ setupFiles: ['../vitest.setup.ts'],
13
+ coverage: {
14
+ provider: 'v8',
15
+ include: ['**/src/**'],
16
+ exclude: ['**/index.ts', '**/__tests__/**', '**/*.stories.ts'],
17
+ thresholds: {
18
+ statements: 80,
19
+ branches: 75,
20
+ functions: 80,
21
+ lines: 80
22
+ }
23
+ }
24
+ }
25
+ })
26
+ )