update kirby to v5 and add refresh cache panel view button

This commit is contained in:
isUnknown 2025-09-10 14:28:38 +02:00
commit 9a86d41254
466 changed files with 19960 additions and 10497 deletions

View file

@ -90,36 +90,41 @@
margin-right: var(--dialog-comments-w, 20rem);
}
.dialog__inner .tracks-header {
height: 3.625rem;
background: var(--color-white-50);
height: 3.75rem;
border-radius: var(--rounded-xl);
padding-inline: var(--space-8);
border: var(--border-width) solid var(--color-grey-200);
padding-right: var(--space-8);
flex-wrap: nowrap;
gap: var(--space-8);
position: sticky;
top: 0;
z-index: 1;
}
.dialog__inner .tracks-header .btn::before {
content: '';
display: block;
position: absolute;
top: -.75rem;
left: -3rem;
bottom: -.75rem;
width: 2.5rem;
background: linear-gradient(to right, transparent, var(--dialog-inner-background, #f7f7f7));
}
.dialog__inner .tracks {
display: flex;
gap: var(--space-8);
overflow-x: auto;
padding: var(--space-8) 1.5rem var(--space-8) var(--space-8);
margin: calc(-1 * var(--space-8));
-webkit-mask-image: linear-gradient(
to left,
rgba(0, 0, 0, 0),
rgba(0, 0, 0, 1) 3rem
);
mask-image: linear-gradient(to left, rgba(0, 0, 0, 0), rgba(0, 0, 0, 1) 3rem);
width: 100%;
margin-left: calc(-1 * var(--space-16));
padding: var(--space-16);
overflow-x: scroll;
overflow-y: hidden;
position: relative;
}
/* Windows fix */
[data-browser*='Windows'] .dialog__inner .tracks {
transform: translateY(0.46875rem); /* 7.5px */
}
/*[data-browser*='Windows'] .dialog__inner .tracks {
transform: translateY(0.46875rem);
}*/
.dialog__inner .tracks > *:last-child {
margin-right: 1rem;
margin-right: var(--space-8);
}
.dialog [data-pc-section='content'] [role='tablist'] {
height: 3.5rem;

338
src/components/Selector.vue Normal file
View file

@ -0,0 +1,338 @@
<template>
<div
id="selector-dropdown"
class="flex flex-col"
:style="'--image: url(\'' + getFrontViewUrl(currentValue) + '\')'"
>
<label for="selector-select" class="text-sm">{{ label }}</label>
<MultiSelect
v-if="isCompareModeEnabled"
id="selector-multiselect"
v-model="currentValue"
:options="items"
optionLabel="title"
:placeholder="label"
:maxSelectedLabels="3"
class="font-serif"
:class="{ active: currentValue }"
data-icon="chevron-single-down"
checkmark
>
<template #option="slotProps">
<img
alt=""
:src="getFrontViewUrl(slotProps.option)"
width="28"
height="28"
/>
<p>{{ slotProps.option.title }}</p>
</template>
</MultiSelect>
<Select
v-else
id="selector-select"
v-model="currentValue"
:options="items"
optionLabel="title"
class="font-serif"
:class="{ active: currentValue }"
data-icon="chevron-single-down"
checkmark
>
<template #value="slotProps">
<p v-if="currentValue">
{{ currentValue.title }}
</p>
<p v-else>Selectionnez une déclinaison</p>
</template>
<template #option="slotProps">
<img
alt=""
:src="getFrontViewUrl(slotProps.option)"
width="28"
height="28"
/>
<p>{{ slotProps.option.title }}</p>
</template>
</Select>
</div>
</template>
<script setup>
import { onBeforeMount, ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { useDialogStore } from '../stores/dialog';
// Props
const { items, label, isCompareModeEnabled, index } = defineProps({
label: String,
items: Array,
isCompareModeEnabled: {
type: Boolean,
default: false,
},
index: Number,
});
// Variables
const currentValue = ref(null);
// Stores
const { activeTracks } = storeToRefs(useDialogStore());
// Hooks
onBeforeMount(() => {
if (index === 0) {
currentValue.value = items[0];
}
});
// Watchers
watch(
() => isCompareModeEnabled,
(newValue) => {
if (newValue) {
currentValue.value = currentValue.value ? [currentValue.value] : [];
}
}
);
watch(currentValue, (newValue) => {
if (
newValue !== null &&
(Array.isArray(newValue) ? newValue.length > 0 : true)
) {
selectTrack(newValue);
}
});
watch(
activeTracks,
(newValue) => {
if (!isCompareModeEnabled) return;
currentValue.value.forEach((item) => {
if (!newValue.includes(item)) {
currentValue.value = currentValue.value.filter((el) => el !== item);
}
});
},
{ deep: true }
);
function selectTrack(track) {
if (!isCompareModeEnabled) {
activeTracks.value = [track];
return;
}
const lastVariation = track[track.length - 1];
if (
activeTracks.value.length === 1 &&
!activeTracks.value.includes(lastVariation)
) {
activeTracks.value.push(lastVariation);
return;
}
if (activeTracks.value.length === 2) {
if (activeTracks.value.includes(lastVariation)) {
removeTrack(track);
} else {
activeTracks.value.pop();
activeTracks.value.push(lastVariation);
}
}
}
function removeTrack(track) {
activeTracks.value = activeTracks.value.filter(
(activeTrack) => activeTrack.title !== track.title
);
}
watch(activeTracks, (newValue) => {
if (newValue[newValue.length - 1] !== currentValue.value) {
currentValue.value = isCompareModeEnabled ? [] : null;
}
});
// Functions
function getFrontViewUrl(item) {
if (!item) return '';
if (Array.isArray(item)) {
return item.length > 0 ? getFrontViewUrl(item[0]) : '';
}
if (item.files.length > 1) {
return item.files[7]?.url || item.files[0].url;
} else {
return item.files[0].url;
}
}
</script>
<style>
#selector-dropdown {
--selector-width: 21rem;
--row-gap: 0;
align-items: flex-start;
position: relative;
background: var(--color-background);
border-radius: var(--rounded-lg);
height: 3.75rem;
min-width: var(--selector-width, 21rem);
padding: var(--space-8) var(--space-48) var(--space-8) var(--space-64);
}
#selector-dropdown::before {
content: '';
position: absolute;
left: var(--space-8);
width: 2.75rem;
height: 2.75rem;
border-radius: var(--rounded-md);
background-color: var(--dialog-inner-background, #f7f7f7);
background-image: var(--image);
background-repeat: no-repeat;
background-size: cover;
background-position: center;
}
[role="combobox"],
[id*="select_list"] {
border: 1px solid var(--color-grey-200);
}
[role="combobox"]:hover {
outline: 0px solid var(--color-grey-400);
border-color: var(--color-grey-400);
}
[role="combobox"][aria-expanded="true"] {
outline: 2px solid var(--color-focus-ring);
outline-offset: -2px;
border-color: transparent;
}
#selector-select,
#selector-multiselect,
[role="combobox"] {
position: absolute;
inset: 0;
border-radius: inherit;
padding: 1.875rem var(--space-48) var(--space-8) var(--space-64);
cursor: pointer;
}
[role="combobox"] p {
max-height: 1lh;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#selector-select.active [role="combobox"] {
border-color: var(--color-focus-ring);
}
/* Icon */
[data-pc-section="dropdown"] {
display: none; /* Hide default component svg */
}
#selector-dropdown [data-icon]::before {
--icon-color: var(--color-grey-700);
position: absolute;
right: var(--space-8);
top: 0.625rem;
width: 2.5rem;
height: 2.5rem;
padding: 0.625rem;
}
#selector-dropdown label {
color: var(--color-grey-700);
letter-spacing: var(--tracking-wider);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
height: 1lh;
padding-right: 1em;
}
/* Options */
[id*="select_list"] {
margin-top: var(--space-4);
border-radius: var(--rounded-md);
background: var(--color-background);
box-shadow: var(--shadow);
padding: var(--space-8);
}
[id*="select_list"] > * {
font-family: var(--font-serif);
padding: var(--space-8) var(--space-8) var(--space-8) var(--space-48);
border-radius: var(--rounded-sm);
position: relative;
display: flex;
align-items: center;
height: 2.75rem;
cursor: pointer;
}
[id*="select_list"] > * + * {
margin-top: var(--space-4);
}
[id*="select_list"] > *::before {
content: '';
position: absolute;
left: var(--space-8);
width: 1.75rem;
height: 1.75rem;
border-radius: var(--rounded-sm);
background-image: var(--image);
background-color: var(--dialog-inner-background, #f7f7f7);
background-repeat: no-repeat;
background-size: cover;
background-position: center;
}
[id*="select_list"] > *:hover {
background-color: var(--color-grey-50);
}
[id*="select_list"] > *:focus,
[id*="select_list"] > *:focus-visible,
[id*="select_list"] > [data-p-focused="true"] {
outline: 2px solid var(--color-focus-ring);
}
/* Check */
#selector-multiselect_list input[type="checkbox"] {
position: absolute;
left: var(--space-4);
top: calc(var(--space-4) + 1px);
width: 1.75rem;
height: 1.75rem;
border-radius: var(--rounded-sm);
}
#selector-multiselect_list input[type="checkbox"]:checked {
--icon-color: var(--color-focus-ring);
}
[id*="select_list"] > * > svg {
position: absolute;
left: 0.875rem;
width: 1rem;
height: 1rem;
color: var(--color-grey-700);
}
[id*="select_list"] img {
position: absolute;
left: 0.5rem;
width: 1.75rem;
height: 1.75rem;
mix-blend-mode: multiply;
}
[id*="select_list"] [aria-selected="true"] img,
#selector-multiselect_list input[type="checkbox"] + [data-pc-section="box"] {
display: none;
}
#selector-multiselect_list [aria-selected="false"] input[type="checkbox"]::before {
--icon-color: transparent;
}
/* Overlay */
[data-pc-section="overlay"] [data-pc-section="header"] {
display: none;
}
</style>

View file

@ -1,5 +1,4 @@
<template>
<!-- TODO: dynamiser la valeur de l'attribut style: url('inspiration.cover') -->
<div
id="inspirations-dropdown"
class="flex flex-col"
@ -21,7 +20,7 @@
</template>
<script setup>
import { ref, watch } from "vue";
import { ref, watch } from 'vue';
const { all } = defineProps({
all: Array,
@ -33,10 +32,10 @@ watch(current, (newValue) => {
changeInspiration(newValue);
});
const emit = defineEmits(["update:currentInspiration"]);
const emit = defineEmits(['update:currentInspiration']);
function changeInspiration(inspiration) {
emit("update:currentInspiration", inspiration);
emit('update:currentInspiration', inspiration);
}
</script>
@ -52,7 +51,7 @@ function changeInspiration(inspiration) {
padding: var(--space-8) var(--space-48) var(--space-8) var(--space-64);
}
#inspirations-dropdown::before {
content: "";
content: '';
position: absolute;
left: var(--space-8);
width: 2.75rem;
@ -64,21 +63,21 @@ function changeInspiration(inspiration) {
background-position: center;
}
[role="combobox"],
[role='combobox'],
#inspirations-select_list {
border: 1px solid var(--color-grey-200);
}
[role="combobox"]:hover {
[role='combobox']:hover {
outline: 1px solid var(--color-grey-400);
border-color: var(--color-background);
}
[role="combobox"][aria-expanded="true"] {
[role='combobox'][aria-expanded='true'] {
outline: 2px solid var(--color-focus-ring);
outline-offset: -2px;
border-color: transparent;
}
#inspirations-select,
[role="combobox"] {
[role='combobox'] {
position: absolute;
inset: 0;
border-radius: inherit;
@ -126,7 +125,7 @@ function changeInspiration(inspiration) {
margin-top: var(--space-4);
}
#inspirations-select_list > *::before {
content: "";
content: '';
position: absolute;
left: var(--space-8);
width: 1.75rem;
@ -143,7 +142,7 @@ function changeInspiration(inspiration) {
}
#inspirations-select_list > *:focus,
#inspirations-select_list > *:focus-visible,
#inspirations-select_list > [data-p-focused="true"] {
#inspirations-select_list > [data-p-focused='true'] {
outline: 2px solid var(--color-focus-ring);
}
/* Check */

View file

@ -11,10 +11,7 @@
:data-count="images.length"
:data-plus="images.length > 3 ? images.length - 3 : undefined"
>
<template
v-for="(image, index) in images.slice(0, 3)"
:key="'image-' + index"
>
<template v-for="image in images.slice(0, 3)" :key="image.uuid">
<img
v-if="image.url"
:src="image.url"
@ -43,6 +40,7 @@
<script setup>
import DateTime from './DateTime.vue';
import { useDesignToLightStore } from '../../../stores/designToLight';
import { useVirtualSampleStore } from '../../../stores/virtualSample';
import { computed } from 'vue';
const { images, step, uri } = defineProps({
@ -52,6 +50,7 @@ const { images, step, uri } = defineProps({
});
const { isDesignToLightStep } = useDesignToLightStore();
const { allVariations } = useVirtualSampleStore();
const commentsCount = computed(() => {
let count = 0;
@ -62,8 +61,8 @@ const commentsCount = computed(() => {
}
} else {
if (step.files?.dynamic) {
for (const track of step.files.dynamic) {
for (const file of track.files) {
for (const variation of allVariations) {
for (const file of variation.files) {
count += file?.comments?.length || 0;
}
}

View file

@ -9,8 +9,13 @@
<script setup>
import Images from './Images.vue';
import { computed } from 'vue';
import { storeToRefs } from 'pinia';
import { useVirtualSampleStore } from '../../../stores/virtualSample';
const { step } = defineProps({ step: Object });
const { allVariations } = storeToRefs(useVirtualSampleStore());
const images = computed(() => {
if (!step.files.dynamic) {
return [
@ -19,20 +24,20 @@ const images = computed(() => {
},
];
}
return step.files?.dynamic?.map((track) => getFrontView(track)) ?? [];
return allVariations.value.map((variation) => getFrontView(variation)) ?? [];
});
const uri = '/' + step.uri;
function getFrontView(track) {
if (track.files.length === 1) return track.files[0];
function getFrontView(variation) {
if (variation.files.length === 1) return variation.files[0];
const xMax = parseInt(
track.files[track.files.length - 1].name.split('_')[1].split('.')[0]
variation.files[variation.files.length - 1].name.split('_')[1].split('.')[0]
);
const xFrontView = Math.round((xMax + 1) / 2);
const extension = track.files[0].name.split('.')[1];
const xFrontView = (xMax + 1) / 2;
const extension = variation.files[0].name.split('.')[1];
const frontViewName = '0_' + xFrontView + '.' + extension;
const frontView = track.files.find((file) => file.name === frontViewName);
const frontView = variation.files.find((file) => file.name === frontViewName);
return frontView;
}
</script>

View file

@ -2,18 +2,15 @@
<div class="dialog__inner">
<header class="tracks-header | flex">
<div class="tracks">
<button
<Selector
v-for="(track, index) in tracks"
class="btn btn--image"
:aria-pressed="activeTracks.includes(track) ? true : false"
:aria-controls="track.slug"
:id="'track--' + track.slug"
:style="`--btn-image: url(${getFrontViewUrl(track)});`"
@click="selectTrack(track)"
:data-comments="getCommentsCount(track)"
>
<span>{{ track.title }}</span>
</button>
:key="track.slug"
:label="track.title"
:items="track.variations"
:isCompareModeEnabled="isCompareModeEnabled"
:index="index"
@update:selectedItems="selectTrack"
/>
</div>
<button
v-if="tracks.length > 1"
@ -45,13 +42,13 @@
v-if="isCompareModeEnabled && activeTracks.length < 2"
class="track-empty | bg-white rounded-xl w-full p-32"
>
<p>Cliquez sur la piste que vous souhaitez comparer</p>
<p>Sélectionnez sur la piste que vous souhaitez comparer</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted, onBeforeMount } from 'vue';
import { computed, watch, onMounted, onBeforeMount } from 'vue';
import { storeToRefs } from 'pinia';
import { usePageStore } from '../../../stores/page';
import { useDialogStore } from '../../../stores/dialog';
@ -59,6 +56,8 @@ import { useVirtualSampleStore } from '../../../stores/virtualSample';
import { useRoute } from 'vue-router';
import Interactive360 from './Interactive360.vue';
import SingleImage from './SingleImage.vue';
import Selector from '../../Selector.vue';
import slugify from 'slugify';
const route = useRoute();
@ -68,15 +67,24 @@ const { isCommentsOpen, isCommentPanelEnabled, activeTracks, openedFile } =
const { isCompareModeEnabled } = storeToRefs(useVirtualSampleStore());
const rawTracks = page.value.steps.find(
(step) => step.slug === 'virtual-sample'
).files.dynamic;
onBeforeMount(() => {
const trackToOpen = tracks.value.find(
(track) => track.slug === route.hash.substring(1)
);
if (trackToOpen) {
activeTracks.value = [trackToOpen];
} else {
activeTracks.value = [tracks.value[0]];
}
const firstTrack = rawTracks[Object.keys(rawTracks)[0]];
const firstVariation = firstTrack[0];
activeTracks.value = [firstVariation];
// TO RE-ENABLE
// if (route.hash.length > 0) {
// const trackToOpen = tracks.value.find(
// (track) => track.slug === route.hash.substring(1)
// );
// activeTracks.value = [trackToOpen];
// } else {
// activeTracks.value = [tracks.value[0]];
// }
});
onMounted(() => {
@ -89,16 +97,28 @@ onMounted(() => {
}
});
const tracks = computed(
() =>
page.value.steps.find((step) => step.slug === 'virtual-sample').files
.dynamic
);
const tracks = computed(() => {
const rawTracks = page.value.steps.find(
(step) => step.slug === 'virtual-sample'
).files.dynamic;
const tracks = [];
for (const key in rawTracks) {
tracks.push({
title: key,
slug: slugify(key),
variations: rawTracks[key],
});
}
return tracks;
});
const isSingleImage = computed(() => {
return (
activeTracks.value?.length === 1 &&
activeTracks.value[0]?.files.length === 1
activeTracks.value[0]?.files?.length === 1
);
});
@ -129,41 +149,6 @@ watch(isCompareModeEnabled, (newValue) => {
}
});
function getFrontViewUrl(track) {
if (track.files.length > 1) {
return track.files[7].url;
} else {
return track.files[0].url;
}
}
function selectTrack(track) {
if (!isCompareModeEnabled.value) {
activeTracks.value = [track];
return;
}
if (activeTracks.value.length === 1 && !activeTracks.value.includes(track)) {
activeTracks.value.push(track);
return;
}
if (activeTracks.value.length === 2) {
if (activeTracks.value.includes(track)) {
removeTrack(track);
} else {
activeTracks.value.pop();
activeTracks.value.push(track);
}
}
}
function removeTrack(track) {
activeTracks.value = activeTracks.value.filter(
(activeTrack) => activeTrack.title !== track.title
);
}
function getCommentsCount(track) {
let count = 0;
for (const file of track.files) {

View file

@ -1,11 +1,12 @@
import { createApp } from "vue";
import "./assets/css/index.css";
import App from "./App.vue";
import { createPinia } from "pinia";
import PrimeVue from "primevue/config";
import ToastService from "primevue/toastservice";
import Select from "primevue/select";
import { router } from "./router/router.js";
import { createApp } from 'vue';
import './assets/css/index.css';
import App from './App.vue';
import { createPinia } from 'pinia';
import PrimeVue from 'primevue/config';
import ToastService from 'primevue/toastservice';
import Select from 'primevue/select';
import MultiSelect from 'primevue/multiselect';
import { router } from './router/router.js';
const app = createApp(App);
const pinia = createPinia();
@ -16,5 +17,6 @@ app.use(PrimeVue, {
});
app.use(ToastService);
app.use(router);
app.component("Select", Select);
app.mount("#app");
app.component('Select', Select);
app.component('MultiSelect', MultiSelect);
app.mount('#app');

View file

@ -130,7 +130,6 @@ export const useDialogStore = defineStore('dialog', () => {
}
function removeCommentMarkers() {
console.log('remove comment markers');
document.querySelectorAll('.comment-marker').forEach((bubble) => {
bubble.parentNode.removeChild(bubble);
});

View file

@ -6,22 +6,31 @@ import { useDialogStore } from './dialog';
export const useVirtualSampleStore = defineStore('virtual-sample', () => {
const { page } = storeToRefs(usePageStore());
const { openedFile } = storeToRefs(useDialogStore());
const step = computed(() => {
return page.value.steps.find((step) => step.id === 'virtualSample');
});
const isCompareModeEnabled = ref(false);
const activeTab = ref(step.value.files.dynamic ? 'dynamic' : 'static');
const isCompareModeEnabled = ref(false);
const currentFile = ref(null);
const isLoopAnimationEnabled = ref(false);
const isDownloadTriggered = ref(false);
const step = computed(() => {
return page.value.steps.find((step) => step.id === 'virtualSample');
});
const activeTab = computed(() =>
step.value.files.dynamic ? 'dynamic' : 'static'
);
const allVariations = computed(() =>
Object.values(step.value.files?.dynamic).flat(1)
);
watch(activeTab, () => (currentFile.value = null));
return {
activeTab,
currentFile,
step,
allVariations,
isLoopAnimationEnabled,
isCompareModeEnabled,
isDownloadTriggered,