Skip to content

Commit 2cfe153

Browse files
author
fureev
committed
feat(GrImageViewer): expose real image metrics in toolbar slots
- Added `naturalWidth`, `naturalHeight`, `renderedWidth`, `renderedHeight`, `realScale`, and `realScalePercent` properties to toolbar slots. - Incorporated `ResizeObserver` for automatic tracking of layout changes. - Metrics reset on index or `urlList` change. - Added a new example showcasing real image size in the toolbar. - Updated documentation, tests, and bumped version to 0.9.3.
1 parent b9a34d4 commit 2cfe153

9 files changed

Lines changed: 234 additions & 24 deletions

File tree

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,20 @@ All notable changes to the [`@feugene/granularity`](./packages/granularity) pack
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/).
77

8+
## [v0.9.3] 2026-06-09
9+
10+
### Added
11+
12+
- `GrImageViewer`: toolbar slots (`#toolbar` / `#toolbar-actions`) now expose the
13+
real image metrics, so consumers no longer have to read the DOM manually
14+
(`querySelector` + `requestAnimationFrame`): `naturalWidth` / `naturalHeight`
15+
(intrinsic image size), `renderedWidth` / `renderedHeight` (actual on-screen
16+
footprint with `scale` applied), and `realScale` / `realScalePercent` (the
17+
true scale relative to the natural size). Natural size is read on `@load`,
18+
the fitted (`object-contain`) layout size is tracked via `ResizeObserver`,
19+
and the real scale is a derived `computed` (rotation-independent). Metrics
20+
reset on index / `urlList` change.
21+
822
## [v0.9.2] 2026-06-08
923

1024
### Fixed

apps/showcase/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"type": "module",
66
"dependencies": {
77
"@feugene/fint-i18n": "^0.2.3",
8-
"@feugene/granularity": "^0.9.0",
8+
"@feugene/granularity": "^0.9.3",
99
"vue": "^3.5.32",
1010
"vue-router": "^4.6.3",
1111
"@feugene/unocss-preset-granular": "^0.3.0",

apps/showcase/src/content/__tests__/componentDocs.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,7 @@ describe('component docs metadata', () => {
362362

363363
const imageViewerDoc = getShowcaseComponentDoc(imageViewerEntity!)
364364

365-
expect(imageViewerDoc.examples).toHaveLength(3)
365+
expect(imageViewerDoc.examples).toHaveLength(4)
366366
expect(imageViewerDoc.examples.every(example => example.status === 'ready')).toBe(true)
367367
expect(imageViewerDoc.examples.every(example => example.previewKey?.startsWith('ds-image-viewer'))).toBe(true)
368368
})

apps/showcase/src/content/component-docs/examples/GrImageViewer.examples.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,34 @@ const open = ref(false)
4949
</div>
5050
</template>
5151
</GrImageViewer>
52+
</template>`,
53+
},
54+
{
55+
id: 'image-viewer-real-size',
56+
title: 'Real image size in toolbar',
57+
description: 'Картинка фиксированного размера (1000×1500): компонент сам отдаёт в slot natural-размер, фактический rendered-размер и реальный масштаб (`realScalePercent`), поэтому не нужно вручную читать DOM.',
58+
status: 'ready',
59+
previewKey: 'ds-image-viewer-real-size',
60+
code: `<script setup lang="ts">
61+
import { ref } from 'vue'
62+
63+
import { GrImageViewer } from '@feugene/granularity'
64+
65+
const slides = ['https://picsum.photos/id/1015/1000/1500']
66+
const open = ref(false)
67+
</script>
68+
69+
<template>
70+
<GrImageViewer v-model="open" :url-list="slides" :show-zoom-value="false">
71+
<template #toolbar="{ scale, naturalWidth, naturalHeight, renderedWidth, renderedHeight, realScalePercent }">
72+
<div>
73+
<span>Natural: {{ naturalWidth }} × {{ naturalHeight }} px</span>
74+
<span>Rendered: {{ renderedWidth }} × {{ renderedHeight }} px</span>
75+
<span>Nominal scale: {{ Math.round(scale * 100) }}%</span>
76+
<span>Real scale: {{ realScalePercent }}%</span>
77+
</div>
78+
</template>
79+
</GrImageViewer>
5280
</template>`,
5381
},
5482
{

apps/showcase/src/content/generated/componentApi.generated.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2145,12 +2145,12 @@
21452145
{
21462146
"name": "toolbar",
21472147
"description": "",
2148-
"type": "{ index: number; displayIndex: number; total: number; scale: number; rotation: number; actions: GrImageViewerToolbarActions; }"
2148+
"type": "{ index: number; displayIndex: number; total: number; scale: number; rotation: number; naturalWidth: number; naturalHeight: number; renderedWidth: number; renderedHeight: number; realScale: number; realScalePercent: string; actions: GrImageViewerToolbarActions; }"
21492149
},
21502150
{
21512151
"name": "toolbar-actions",
21522152
"description": "",
2153-
"type": "{ index: number; displayIndex: number; total: number; scale: number; rotation: number; actions: GrImageViewerToolbarActions; }"
2153+
"type": "{ index: number; displayIndex: number; total: number; scale: number; rotation: number; naturalWidth: number; naturalHeight: number; renderedWidth: number; renderedHeight: number; realScale: number; realScalePercent: string; actions: GrImageViewerToolbarActions; }"
21542154
}
21552155
]
21562156
},
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<script setup lang="ts">
2+
import { ref } from 'vue'
3+
4+
import { GrButton, GrImageViewer } from '@feugene/granularity'
5+
6+
// Картинка гарантированного размера: picsum возвращает ровно указанные width/height.
7+
const IMAGE_WIDTH = 1000
8+
const IMAGE_HEIGHT = 1500
9+
10+
const slides = [`https://picsum.photos/id/1015/${IMAGE_WIDTH}/${IMAGE_HEIGHT}`]
11+
12+
const open = ref(false)
13+
</script>
14+
15+
<template>
16+
<div class="grid gap-3">
17+
<div class="text-sm text-[var(--muted-fg)]">
18+
Картинка гарантированного размера {{ IMAGE_WIDTH }}×{{ IMAGE_HEIGHT }}. Номинальный `scale` считается относительно
19+
вписанного в окно изображения (`object-contain`), поэтому «100%» — это не натуральный размер. Компонент сам отдаёт
20+
в slot natural-размер, фактический rendered-размер и реальный масштаб — без ручного чтения DOM.
21+
</div>
22+
23+
<div>
24+
<GrButton size="sm" @click="open = true">
25+
Open real-size experiment
26+
</GrButton>
27+
</div>
28+
29+
<GrImageViewer
30+
v-model="open"
31+
:url-list="slides"
32+
show-progress
33+
:show-zoom-value="false"
34+
>
35+
<template #toolbar="{ scale, rotation, naturalWidth, naturalHeight, renderedWidth, renderedHeight, realScalePercent, actions }">
36+
<div class="flex flex-col gap-2 rounded-2xl border border-[color-mix(in_srgb,var(--bg)_20%,transparent)] bg-[color-mix(in_srgb,var(--fg)_55%,transparent)] px-3 py-2 text-[var(--bg)] backdrop-blur-sm">
37+
<div class="flex items-center justify-center gap-2">
38+
<button type="button" class="rounded-full px-3 py-1 text-xs transition-colors hover:bg-[color-mix(in_srgb,var(--bg)_10%,transparent)]" @click="actions.zoomOut">−</button>
39+
<button type="button" class="rounded-full px-3 py-1 text-xs transition-colors hover:bg-[color-mix(in_srgb,var(--bg)_10%,transparent)]" @click="actions.reset">Reset</button>
40+
<button type="button" class="rounded-full px-3 py-1 text-xs transition-colors hover:bg-[color-mix(in_srgb,var(--bg)_10%,transparent)]" @click="actions.zoomIn">+</button>
41+
</div>
42+
43+
<div class="grid gap-0.5 text-[11px] leading-tight font-500">
44+
<span>Natural: {{ naturalWidth }} × {{ naturalHeight }} px</span>
45+
<span>Rendered: {{ renderedWidth }} × {{ renderedHeight }} px</span>
46+
<span>Nominal scale: {{ Math.round(scale * 100) }}% · rotation {{ rotation }}°</span>
47+
<span>Real scale: {{ realScalePercent }}%</span>
48+
</div>
49+
</div>
50+
</template>
51+
</GrImageViewer>
52+
</div>
53+
</template>

apps/showcase/src/pages/ComponentDetailPage.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ const previewRegistry = {
108108
'ds-icon-status-card': defineAsyncComponent(() => import('../demos/components/ds-icon/GrIconStatusCardDemo.vue')),
109109
'ds-image-viewer-async-media': defineAsyncComponent(() => import('../demos/components/ds-image-viewer/GrImageViewerAsyncMediaDemo.vue')),
110110
'ds-image-viewer-gallery': defineAsyncComponent(() => import('../demos/components/ds-image-viewer/GrImageViewerGalleryDemo.vue')),
111+
'ds-image-viewer-real-size': defineAsyncComponent(() => import('../demos/components/ds-image-viewer/GrImageViewerRealSizeDemo.vue')),
111112
'ds-image-viewer-toolbar-slot': defineAsyncComponent(() => import('../demos/components/ds-image-viewer/GrImageViewerToolbarSlotDemo.vue')),
112113
'ds-input-addons-width-guards': defineAsyncComponent(() => import('../demos/components/ds-input/GrInputAddonsDemo.vue')),
113114
'ds-input-size-and-alignment': defineAsyncComponent(() => import('../demos/components/ds-input/GrInputSizingDemo.vue')),

packages/granularity/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@feugene/granularity",
33
"description": "Granularity design system package with Vue 3 components, consumed via the `@feugene/unocss-preset-granular` preset.",
4-
"version": "0.9.2",
4+
"version": "0.9.3",
55
"license": "SEE LICENSE IN LICENSE",
66
"author": {
77
"name": "Evgeniy Fureev",

0 commit comments

Comments
 (0)