@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.
- package/CHANGELOG.md +82 -0
- package/LICENSE +21 -0
- package/README.md +122 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/clover.xml +39 -0
- package/coverage/coverage-final.json +5 -0
- package/coverage/dist/src/FzThumbnail.vue.d.ts.html +172 -0
- package/coverage/dist/src/index.html +131 -0
- package/coverage/dist/src/types.d.ts.html +367 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +131 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/coverage/src/FzThumbnail.vue.html +619 -0
- package/coverage/src/index.html +131 -0
- package/coverage/src/types.ts.html +379 -0
- package/dist/index.d.ts +2 -0
- package/dist/src/FzThumbnail.vue.d.ts +29 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/types.d.ts +94 -0
- package/dist/thumbnail.css +2 -0
- package/dist/thumbnail.js +94 -0
- package/dist/thumbnail.umd.cjs +1 -0
- package/eslintrc.cjs +18 -0
- package/package.json +41 -0
- package/prettierrc.js +5 -0
- package/src/FzThumbnail.vue +178 -0
- package/src/__tests__/FzThumbnail.spec.ts +396 -0
- package/src/__tests__/__snapshots__/FzThumbnail.spec.ts.snap +47 -0
- package/src/index.ts +2 -0
- package/src/types.ts +98 -0
- package/tsconfig.json +4 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vite.config.ts +34 -0
- package/vitest.config.ts +26 -0
|
@@ -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 [&>*]:pointer-events-auto items-end justify-end" data-testid="fz-thumbnail-overlay"><button>Scarica</button></div>
|
|
46
|
+
</div>"
|
|
47
|
+
`;
|
package/src/index.ts
ADDED
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 @@
|
|
|
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
|
+
})
|
package/vitest.config.ts
ADDED
|
@@ -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
|
+
)
|