origam 2.37.0 → 2.37.2
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 +19 -0
- package/dist/src/components/Carousel/OrigamCarouselItem.vue +1 -0
- package/dist/src/components/Img/OrigamImg.vue +20 -12
- package/dist/src/components/Responsive/OrigamResponsive.vue +7 -4
- package/dist/src/composables/Responsive/aspect.composable.cjs +6 -1
- package/dist/src/composables/Responsive/aspect.composable.d.ts +13 -0
- package/dist/src/composables/Responsive/aspect.composable.js +7 -2
- package/dist/src/interfaces/Responsive/responsive.interface.d.ts +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -18,6 +18,25 @@ This project follows [Semantic Versioning](https://semver.org).
|
|
|
18
18
|
|
|
19
19
|
## [Unreleased]
|
|
20
20
|
|
|
21
|
+
## [2.37.2] - 2026-10-19
|
|
22
|
+
|
|
23
|
+
### Fixed — `OrigamImg` sous SSR : écart d'hydratation `aspect-ratio`, et image montée côté client jamais chargée (#1192)
|
|
24
|
+
|
|
25
|
+
- **Écart d'hydratation** : sans prop `aspect-ratio`, `useAspectRatio` prend le ratio de la fenêtre (`innerWidth / innerHeight`), une valeur inconnue du serveur. Il était appliqué dès le premier rendu client d'une instance hydratée, d'où un `Hydration style mismatch`. Une instance qui hydrate l'applique désormais après `onMounted`. Une instance montée côté client l'applique tout de suite, comme avant. Un `aspect-ratio` explicite est inchangé.
|
|
26
|
+
- **Image montée côté client jamais chargée** : `v-intersect` était lié avec le tuple `withDirectives` comme valeur. La directive lisait `.handler` sur un tableau, si bien que l'intersection ne déclenchait jamais le chargement. L'`<img>` restait en `display:none` + `loading="lazy"`, que Chromium ne charge pas. La directive est désormais liée en `v-intersect.once="{ handler, options }"`, et l'`<img>` passe en `loading="eager"` dès que son chargement est demandé.
|
|
27
|
+
- La même forme de liaison subsiste dans d'autres composants : suivi en #1205.
|
|
28
|
+
|
|
29
|
+
## [2.37.1] - 2026-10-19
|
|
30
|
+
|
|
31
|
+
### Fixed — `OrigamImg` / `OrigamResponsive` : prop `tag`, `<img>` visible en SSR, fondu en rendu client seul (#1182)
|
|
32
|
+
|
|
33
|
+
Un `<origam-img>` placé dans un `<p>` (contenu prose) produisait un `<div>` invalide dans un paragraphe, que le parseur HTML expulse : DOM serveur et client divergeaient. Et l'`<img>` était servie en `display:none` jusqu'au `load` côté client, donc invisible tant que le JS n'avait pas tourné.
|
|
34
|
+
|
|
35
|
+
- **Nouvelle prop `tag`** (défaut `'div'`) sur `OrigamResponsive` et `OrigamImg` (transmise au Responsive et aux conteneurs placeholder / error / gradient). `tag="span"` garde un DOM valide dans un `<p>`.
|
|
36
|
+
- **`<img>` visible dans le HTML serveur** : elle n'est plus masquée avant le `load`.
|
|
37
|
+
- **Fondu conservé uniquement au montage client** (navigation SPA, `v-if` après hydratation). Une image hydratée depuis le HTML serveur s'affiche sans fondu et sans clignotement.
|
|
38
|
+
- ⚠️ Changement de rendu assumé : en rendu serveur puis hydratation, l'image n'apparaît plus en fondu.
|
|
39
|
+
|
|
21
40
|
## [2.37.0] - 2026-10-19
|
|
22
41
|
|
|
23
42
|
### Added — module Nuxt : options `locale` transmises à `createOrigam` (#1181)
|
|
@@ -76,6 +76,7 @@ const props = defineProps({
|
|
|
76
76
|
id: { type: String, required: false },
|
|
77
77
|
class: { type: [String, Array, Object], required: false },
|
|
78
78
|
style: { type: [String, Array, Object, Boolean, null], required: false, skipCheck: true },
|
|
79
|
+
tag: { type: String, required: false },
|
|
79
80
|
padding: { type: [Boolean, Number, String], required: false },
|
|
80
81
|
paddingTop: { type: [Boolean, Number, String], required: false },
|
|
81
82
|
paddingLeft: { type: [Boolean, Number, String], required: false },
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<origam-responsive
|
|
3
3
|
:id="id"
|
|
4
|
-
v-intersect="intersect"
|
|
4
|
+
v-intersect.once="intersect"
|
|
5
5
|
:aria-label="alt"
|
|
6
6
|
:class="imgClasses"
|
|
7
7
|
:role="alt ? 'img' : undefined"
|
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
>
|
|
11
11
|
<template #additional>
|
|
12
12
|
<origam-transition
|
|
13
|
+
:appear="fadesIn"
|
|
13
14
|
:transition="transition"
|
|
14
|
-
appear
|
|
15
15
|
>
|
|
16
16
|
<img
|
|
17
|
-
v-show="
|
|
17
|
+
v-show="isPictureShown"
|
|
18
18
|
ref="image"
|
|
19
19
|
:alt="alt"
|
|
20
20
|
:class="imgPictureClasses"
|
|
@@ -46,25 +46,28 @@
|
|
|
46
46
|
:transition="transition"
|
|
47
47
|
appear
|
|
48
48
|
>
|
|
49
|
-
<
|
|
49
|
+
<component
|
|
50
|
+
:is="tag"
|
|
50
51
|
v-if="(isLoading || (isError && !slots.error)) && slots.placeholder"
|
|
51
52
|
class="origam-img__placeholder"
|
|
52
53
|
>
|
|
53
54
|
<slot name="placeholder"/>
|
|
54
|
-
</
|
|
55
|
+
</component>
|
|
55
56
|
</origam-transition>
|
|
56
57
|
<origam-transition
|
|
57
58
|
:transition="transition"
|
|
58
59
|
appear
|
|
59
60
|
>
|
|
60
|
-
<
|
|
61
|
+
<component
|
|
62
|
+
:is="tag"
|
|
61
63
|
v-if="isError && slots.error"
|
|
62
64
|
class="origam-img__error"
|
|
63
65
|
>
|
|
64
66
|
<slot name="error"/>
|
|
65
|
-
</
|
|
67
|
+
</component>
|
|
66
68
|
</origam-transition>
|
|
67
|
-
<
|
|
69
|
+
<component
|
|
70
|
+
:is="tag"
|
|
68
71
|
v-if="gradient"
|
|
69
72
|
:style="imgGradientStyles"
|
|
70
73
|
class="origam-img__gradient"
|
|
@@ -133,6 +136,7 @@ const props = defineProps({
|
|
|
133
136
|
id: { type: String, required: false },
|
|
134
137
|
class: { type: [String, Array, Object], required: false },
|
|
135
138
|
style: { type: [String, Array, Object, Boolean, null], required: false, skipCheck: true },
|
|
139
|
+
tag: { type: String, required: false, default: "div" },
|
|
136
140
|
padding: { type: [Boolean, Number, String], required: false },
|
|
137
141
|
paddingTop: { type: [Boolean, Number, String], required: false },
|
|
138
142
|
paddingLeft: { type: [Boolean, Number, String], required: false },
|
|
@@ -219,7 +223,7 @@ const normalisedSrc = computed(() => {
|
|
|
219
223
|
const aspectRatio = computed(() => {
|
|
220
224
|
return normalisedSrc.value.aspectRatio || naturalWidth.value / naturalHeight.value || 0;
|
|
221
225
|
});
|
|
222
|
-
const responsiveProps = computed(() => pick(props, ["aspectRatio", "contentClass", "height", "maxHeight", "maxWidth", "minHeight", "minWidth", "width", "class", "style"]));
|
|
226
|
+
const responsiveProps = computed(() => pick(props, ["tag", "aspectRatio", "contentClass", "height", "maxHeight", "maxWidth", "minHeight", "minWidth", "width", "class", "style"]));
|
|
223
227
|
watch(() => props.src, () => {
|
|
224
228
|
init(state.value !== IMG_STATE.IDLE);
|
|
225
229
|
});
|
|
@@ -323,11 +327,11 @@ const markBooted = (val) => {
|
|
|
323
327
|
};
|
|
324
328
|
stop = watch(aspectRatio, markBooted);
|
|
325
329
|
markBooted(aspectRatio.value);
|
|
326
|
-
const intersect = computed(() =>
|
|
330
|
+
const intersect = computed(() => ({
|
|
327
331
|
handler: init,
|
|
328
332
|
options: props.options
|
|
329
|
-
}
|
|
330
|
-
const nativeLoading = computed(() => props.eager ? "eager" : "lazy");
|
|
333
|
+
}));
|
|
334
|
+
const nativeLoading = computed(() => props.eager || state.value !== IMG_STATE.IDLE ? "eager" : "lazy");
|
|
331
335
|
const isLoaded = computed(() => {
|
|
332
336
|
return state.value === IMG_STATE.LOADED;
|
|
333
337
|
});
|
|
@@ -337,6 +341,10 @@ const isLoading = computed(() => {
|
|
|
337
341
|
const isError = computed(() => {
|
|
338
342
|
return state.value === IMG_STATE.ERROR;
|
|
339
343
|
});
|
|
344
|
+
const fadesIn = IN_BROWSER && !vm.vnode.el;
|
|
345
|
+
const isPictureShown = computed(() => {
|
|
346
|
+
return fadesIn ? isLoaded.value : !isError.value;
|
|
347
|
+
});
|
|
340
348
|
const hasContent = computed(() => {
|
|
341
349
|
return slots.default;
|
|
342
350
|
});
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
2
|
+
<component
|
|
3
|
+
:is="tag"
|
|
3
4
|
:id="id"
|
|
4
5
|
:class="responsiveClasses"
|
|
5
6
|
:style="responsiveStyles"
|
|
6
7
|
>
|
|
7
8
|
<slot name="additional"/>
|
|
8
|
-
<
|
|
9
|
+
<component
|
|
10
|
+
:is="tag"
|
|
9
11
|
v-if="slots.default"
|
|
10
12
|
:class="responsiveContentClasses"
|
|
11
13
|
>
|
|
12
14
|
<slot name="default"/>
|
|
13
|
-
</
|
|
14
|
-
</
|
|
15
|
+
</component>
|
|
16
|
+
</component>
|
|
15
17
|
</template>
|
|
16
18
|
|
|
17
19
|
<script lang="ts" setup>
|
|
@@ -36,6 +38,7 @@ const props = defineProps({
|
|
|
36
38
|
id: { type: String, required: false },
|
|
37
39
|
class: { type: [String, Array, Object], required: false },
|
|
38
40
|
style: { type: [String, Array, Object, Boolean, null], required: false, skipCheck: true },
|
|
41
|
+
tag: { type: String, required: false, default: "div" },
|
|
39
42
|
padding: { type: [Boolean, Number, String], required: false },
|
|
40
43
|
paddingTop: { type: [Boolean, Number, String], required: false },
|
|
41
44
|
paddingLeft: { type: [Boolean, Number, String], required: false },
|
|
@@ -8,6 +8,11 @@ var _vue = require("vue");
|
|
|
8
8
|
var _commons = require("../../consts/Commons/commons.const.cjs");
|
|
9
9
|
const isUsableRatio = value => Number.isFinite(value) && value > 0;
|
|
10
10
|
function useAspectRatio(props) {
|
|
11
|
+
const isHydrating = _commons.IN_BROWSER && !!(0, _vue.getCurrentInstance)()?.vnode.el;
|
|
12
|
+
const isViewportReady = (0, _vue.shallowRef)(!isHydrating);
|
|
13
|
+
if (isHydrating) (0, _vue.onMounted)(() => {
|
|
14
|
+
isViewportReady.value = true;
|
|
15
|
+
});
|
|
11
16
|
const ratio = (0, _vue.computed)(() => {
|
|
12
17
|
if (props.aspectRatio) {
|
|
13
18
|
const raw = String(props.aspectRatio).trim();
|
|
@@ -21,7 +26,7 @@ function useAspectRatio(props) {
|
|
|
21
26
|
const value = Number(raw);
|
|
22
27
|
return isUsableRatio(value) ? String(value) : void 0;
|
|
23
28
|
}
|
|
24
|
-
if (!_commons.IN_BROWSER) return void 0;
|
|
29
|
+
if (!_commons.IN_BROWSER || !isViewportReady.value) return void 0;
|
|
25
30
|
const viewport = window.innerWidth / window.innerHeight;
|
|
26
31
|
return isUsableRatio(viewport) ? String(viewport) : void 0;
|
|
27
32
|
});
|
|
@@ -40,6 +40,19 @@
|
|
|
40
40
|
* runtime, never a design-time default a theme would override, so there is
|
|
41
41
|
* nothing for the token pipeline to carry.
|
|
42
42
|
*
|
|
43
|
+
* Hydration (#1192)
|
|
44
|
+
* ─────────────────
|
|
45
|
+
* The viewport fallback is a CLIENT-ONLY value: the server cannot emit it.
|
|
46
|
+
* Applied from the first render of an instance that is being hydrated, it
|
|
47
|
+
* made the client's first render differ from the server HTML
|
|
48
|
+
* (`style="width:120px;"` vs `style="aspect-ratio:1.77…;width:120px;"`),
|
|
49
|
+
* i.e. a "Hydration style mismatch" that Vue does not rectify in
|
|
50
|
+
* production. So an instance hydrating server HTML (`vnode.el` already set
|
|
51
|
+
* when `setup()` runs) holds the fallback back until `onMounted`; the style
|
|
52
|
+
* is then patched by an ordinary reactive update. An instance created on
|
|
53
|
+
* the client (no server HTML to match) applies it at once, as before.
|
|
54
|
+
* An explicit `aspectRatio` is deterministic and is emitted by both sides.
|
|
55
|
+
*
|
|
43
56
|
* Guarding the value
|
|
44
57
|
* ──────────────────
|
|
45
58
|
* The returned ratio rejects NaN, 0, negatives and Infinity. Any of them
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import { computed } from "vue";
|
|
1
|
+
import { computed, getCurrentInstance, onMounted, shallowRef } from "vue";
|
|
2
2
|
import { IN_BROWSER } from "../../consts/Commons/commons.const.js";
|
|
3
3
|
const isUsableRatio = (value) => Number.isFinite(value) && value > 0;
|
|
4
4
|
export function useAspectRatio(props) {
|
|
5
|
+
const isHydrating = IN_BROWSER && !!getCurrentInstance()?.vnode.el;
|
|
6
|
+
const isViewportReady = shallowRef(!isHydrating);
|
|
7
|
+
if (isHydrating) onMounted(() => {
|
|
8
|
+
isViewportReady.value = true;
|
|
9
|
+
});
|
|
5
10
|
const ratio = computed(() => {
|
|
6
11
|
if (props.aspectRatio) {
|
|
7
12
|
const raw = String(props.aspectRatio).trim();
|
|
@@ -15,7 +20,7 @@ export function useAspectRatio(props) {
|
|
|
15
20
|
const value = Number(raw);
|
|
16
21
|
return isUsableRatio(value) ? String(value) : void 0;
|
|
17
22
|
}
|
|
18
|
-
if (!IN_BROWSER) return void 0;
|
|
23
|
+
if (!IN_BROWSER || !isViewportReady.value) return void 0;
|
|
19
24
|
const viewport = window.innerWidth / window.innerHeight;
|
|
20
25
|
return isUsableRatio(viewport) ? String(viewport) : void 0;
|
|
21
26
|
});
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { IBorderProps } from '../Commons/border.interface';
|
|
2
|
-
import type { ICommonsComponentProps } from '../Commons/commons.interface';
|
|
2
|
+
import type { ICommonsComponentProps, ITagProps } from '../Commons/commons.interface';
|
|
3
3
|
import type { IDimensionProps } from '../Commons/dimension.interface';
|
|
4
4
|
import type { IMarginProps } from '../Commons/margin.interface';
|
|
5
5
|
import type { IPaddingProps } from '../Commons/padding.interface';
|
|
6
6
|
import type { IRoundedProps } from '../Commons/rounded.interface';
|
|
7
|
-
export interface IResponsiveProps extends IDimensionProps, ICommonsComponentProps, IPaddingProps, IMarginProps, IBorderProps, IRoundedProps {
|
|
7
|
+
export interface IResponsiveProps extends IDimensionProps, ICommonsComponentProps, ITagProps, IPaddingProps, IMarginProps, IBorderProps, IRoundedProps {
|
|
8
8
|
aspectRatio?: string | number;
|
|
9
9
|
contentClass?: string;
|
|
10
10
|
}
|