hlassiege/darylpublic Fork
⑂ c803802
Commits
⬇ Clone ▾
git clone https://git.rickub.com/hlassiege/daryl.git
git clone ssh://git@rickub.com/hlassiege/daryl.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

fix(vignette): paginate the reference picker so cells stop overlapping

The grid scrolled inside the flex modal, which squashed its rows while
the square cells kept their height. Show 15 images per page instead.
hlassiege committed 2026-10-13T14:46:59+02:00 Browse files
c803802 parent: 9fc5f05
modified web/src/i18n/locales/en/thumbnail.ts +1 -0
@@ -76,6 +76,7 @@ export default {
7676 all: "All · {n}",
7777 noMatch: "No matching images.",
7878 emptyCategory: "No images in this category yet.",
79+ page: "Page {n} of {total}",
7980 uploading: "Uploading · {n}%",
8081 drop: "Drop images here to add them to the library",
8182 },
@@ -76,6 +76,7 @@ export default {
76 all: "All · {n}",76 all: "All · {n}",
77 noMatch: "No matching images.",77 noMatch: "No matching images.",
78 emptyCategory: "No images in this category yet.",78 emptyCategory: "No images in this category yet.",
79+ page: "Page {n} of {total}",
79 uploading: "Uploading · {n}%",80 uploading: "Uploading · {n}%",
80 drop: "Drop images here to add them to the library",81 drop: "Drop images here to add them to the library",
81 },82 },
modified web/src/i18n/locales/fr/thumbnail.ts +1 -0
@@ -76,6 +76,7 @@ export default {
7676 all: "Toutes · {n}",
7777 noMatch: "Aucune image ne correspond.",
7878 emptyCategory: "Aucune image dans cette catégorie pour l’instant.",
79+ page: "Page {n} / {total}",
7980 uploading: "Envoi · {n} %",
8081 drop: "Glisse des images ici pour les ajouter à la bibliothèque",
8182 },
@@ -76,6 +76,7 @@ export default {
76 all: "Toutes · {n}",76 all: "Toutes · {n}",
77 noMatch: "Aucune image ne correspond.",77 noMatch: "Aucune image ne correspond.",
78 emptyCategory: "Aucune image dans cette catégorie pour l’instant.",78 emptyCategory: "Aucune image dans cette catégorie pour l’instant.",
79+ page: "Page {n} / {total}",
79 uploading: "Envoi · {n} %",80 uploading: "Envoi · {n} %",
80 drop: "Glisse des images ici pour les ajouter à la bibliothèque",81 drop: "Glisse des images ici pour les ajouter à la bibliothèque",
81 },82 },
modified web/src/project/steps/vignette/LibraryPicker.vue +21 -6
@@ -4,7 +4,7 @@
44 * (assets et fichiers ajoutés) ; on peut y glisser de nouvelles images, rangées dans la catégorie et
55 * cochées d'emblée.
66 */
7-import { computed, onMounted, ref } from "vue";
7+import { computed, onMounted, ref, watch } from "vue";
88 import { useI18n } from "vue-i18n";
99 import { api } from "../../../lib/api";
1010 import { toast } from "../../../lib/toast";
@@ -41,6 +41,13 @@ const shown = computed(() => {
4141 && (!q || `${x.name} ${x.project ?? ""} ${x.type}`.toLowerCase().includes(q)));
4242 });
4343
44+// trois lignes de cinq : la page tient dans la fenêtre sans défiler
45+const PAGE = 15;
46+const page = ref(0);
47+const pages = computed(() => Math.max(1, Math.ceil(shown.value.length / PAGE)));
48+const paged = computed(() => shown.value.slice(page.value * PAGE, (page.value + 1) * PAGE));
49+watch([filter, query], () => (page.value = 0));
50+
4451 // les médias du projet, comme le panneau Médias du script
4552 const PROJECT_GROUPS = ["Assets", "Fichiers ajoutés"];
4653
@@ -101,8 +108,8 @@ async function onDrop(e: DragEvent) {
101108 {{ c.type }} · {{ c.n }}
102109 </button>
103110 </div>
104- <div class="grid scroll">
105- <button v-for="x in shown" :key="x.rel" class="cell" :class="{ on: picked.includes(x.rel) }" :title="`${x.type} · ${x.name}`" @click="toggle(x.rel)">
111+ <div class="grid">
112+ <button v-for="x in paged" :key="x.rel" class="cell" :class="{ on: picked.includes(x.rel) }" :title="`${x.type} · ${x.name}`" @click="toggle(x.rel)">
106113 <img :src="refUrl(pid, x.rel)" alt="">
107114 <span v-if="picked.includes(x.rel)" class="n">{{ picked.indexOf(x.rel) + 1 }}</span>
108115 </button>
@@ -110,6 +117,11 @@ async function onDrop(e: DragEvent) {
110117 {{ query ? t("thumbnail.picker.noMatch") : t("thumbnail.picker.emptyCategory") }}
111118 </span>
112119 </div>
120+ <div v-if="pages > 1" class="pager">
121+ <button class="btn" :disabled="page === 0" @click="page--">‹</button>
122+ <span class="mono">{{ t("thumbnail.picker.page", { n: page + 1, total: pages }) }}</span>
123+ <button class="btn" :disabled="page >= pages - 1" @click="page++">›</button>
124+ </div>
113125 <div class="drop-zone" :class="{ over }">
114126 <template v-if="uploading != null">{{ t("thumbnail.picker.uploading", { n: uploading }) }}</template>
115127 <template v-else>{{ t("thumbnail.picker.drop") }}</template>
@@ -126,7 +138,7 @@ async function onDrop(e: DragEvent) {
126138 .picker {
127139 position: fixed; z-index: 41; top: 8vh; left: 50%; transform: translateX(-50%); width: min(880px, 92vw);
128140 max-height: 84vh; background: #fff; border-radius: 6px; box-shadow: 0 16px 48px rgba(0, 0, 0, .2);
129- padding: 20px 22px; display: flex; flex-direction: column; gap: 14px;
141+ padding: 20px 22px; display: flex; flex-direction: column; gap: 14px; overflow-y: auto;
130142 }
131143 .head { display: flex; align-items: baseline; gap: 12px; }
132144 .h { font-size: 16px; font-weight: 600; }
@@ -135,18 +147,21 @@ async function onDrop(e: DragEvent) {
135147 .chip { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 4px 9px; font-size: 12px; cursor: pointer; }
136148 .chip:hover { border-color: var(--border-hover); }
137149 .chip.on { background: var(--ink); border-color: var(--ink); color: #fff; }
138-.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 10px; align-content: start; }
150+.grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px; }
139151 .cell {
140152 position: relative; aspect-ratio: 1; padding: 0; border: none; border-radius: 4px; overflow: hidden;
141153 cursor: pointer; background: var(--bg-soft); outline-offset: 2px;
142154 }
143-.cell img { width: 100%; height: 100%; object-fit: cover; display: block; }
155+/* hors du flux : une capture très haute ne doit pas agrandir sa cellule et déborder sur la ligne suivante */
156+.cell img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: top; display: block; }
144157 .cell.on { outline: 2px solid var(--red); }
145158 .cell .n {
146159 position: absolute; top: 6px; right: 6px; min-width: 20px; height: 20px; border-radius: 10px; padding: 0 5px;
147160 background: var(--red); color: #fff; font-family: var(--mono); font-size: 11px; line-height: 20px;
148161 }
149162 .nothing { grid-column: 1 / -1; padding: 10px 0; font-size: 13.5px; color: var(--faint); }
163+.pager { display: flex; align-items: center; justify-content: center; gap: 12px; font-size: 12px; color: var(--muted); }
164+.pager .btn { padding: 2px 10px; }
150165 .drop-zone { padding: 14px; }
151166 .foot { display: flex; justify-content: flex-end; gap: 8px; }
152167 </style>
@@ -4,7 +4,7 @@
4 * (assets et fichiers ajoutés) ; on peut y glisser de nouvelles images, rangées dans la catégorie et4 * (assets et fichiers ajoutés) ; on peut y glisser de nouvelles images, rangées dans la catégorie et
5 * cochées d'emblée.5 * cochées d'emblée.
6 */6 */
7-import { computed, onMounted, ref } from "vue";7+import { computed, onMounted, ref, watch } from "vue";
8 import { useI18n } from "vue-i18n";8 import { useI18n } from "vue-i18n";
9 import { api } from "../../../lib/api";9 import { api } from "../../../lib/api";
10 import { toast } from "../../../lib/toast";10 import { toast } from "../../../lib/toast";
@@ -41,6 +41,13 @@ const shown = computed(() => {
41 && (!q || `${x.name} ${x.project ?? ""} ${x.type}`.toLowerCase().includes(q)));41 && (!q || `${x.name} ${x.project ?? ""} ${x.type}`.toLowerCase().includes(q)));
42 });42 });
43 43
44+// trois lignes de cinq : la page tient dans la fenêtre sans défiler
45+const PAGE = 15;
46+const page = ref(0);
47+const pages = computed(() => Math.max(1, Math.ceil(shown.value.length / PAGE)));
48+const paged = computed(() => shown.value.slice(page.value * PAGE, (page.value + 1) * PAGE));
49+watch([filter, query], () => (page.value = 0));
50+
44 // les médias du projet, comme le panneau Médias du script51 // les médias du projet, comme le panneau Médias du script
45 const PROJECT_GROUPS = ["Assets", "Fichiers ajoutés"];52 const PROJECT_GROUPS = ["Assets", "Fichiers ajoutés"];
46 53
@@ -101,8 +108,8 @@ async function onDrop(e: DragEvent) {
101 {{ c.type }} · {{ c.n }}108 {{ c.type }} · {{ c.n }}
102 </button>109 </button>
103 </div>110 </div>
104- <div class="grid scroll">111+ <div class="grid">
105- <button v-for="x in shown" :key="x.rel" class="cell" :class="{ on: picked.includes(x.rel) }" :title="`${x.type} · ${x.name}`" @click="toggle(x.rel)">112+ <button v-for="x in paged" :key="x.rel" class="cell" :class="{ on: picked.includes(x.rel) }" :title="`${x.type} · ${x.name}`" @click="toggle(x.rel)">
106 <img :src="refUrl(pid, x.rel)" alt="">113 <img :src="refUrl(pid, x.rel)" alt="">
107 <span v-if="picked.includes(x.rel)" class="n">{{ picked.indexOf(x.rel) + 1 }}</span>114 <span v-if="picked.includes(x.rel)" class="n">{{ picked.indexOf(x.rel) + 1 }}</span>
108 </button>115 </button>
@@ -110,6 +117,11 @@ async function onDrop(e: DragEvent) {
110 {{ query ? t("thumbnail.picker.noMatch") : t("thumbnail.picker.emptyCategory") }}117 {{ query ? t("thumbnail.picker.noMatch") : t("thumbnail.picker.emptyCategory") }}
111 </span>118 </span>
112 </div>119 </div>
120+ <div v-if="pages > 1" class="pager">
121+ <button class="btn" :disabled="page === 0" @click="page--">‹</button>
122+ <span class="mono">{{ t("thumbnail.picker.page", { n: page + 1, total: pages }) }}</span>
123+ <button class="btn" :disabled="page >= pages - 1" @click="page++">›</button>
124+ </div>
113 <div class="drop-zone" :class="{ over }">125 <div class="drop-zone" :class="{ over }">
114 <template v-if="uploading != null">{{ t("thumbnail.picker.uploading", { n: uploading }) }}</template>126 <template v-if="uploading != null">{{ t("thumbnail.picker.uploading", { n: uploading }) }}</template>
115 <template v-else>{{ t("thumbnail.picker.drop") }}</template>127 <template v-else>{{ t("thumbnail.picker.drop") }}</template>
@@ -126,7 +138,7 @@ async function onDrop(e: DragEvent) {
126 .picker {138 .picker {
127 position: fixed; z-index: 41; top: 8vh; left: 50%; transform: translateX(-50%); width: min(880px, 92vw);139 position: fixed; z-index: 41; top: 8vh; left: 50%; transform: translateX(-50%); width: min(880px, 92vw);
128 max-height: 84vh; background: #fff; border-radius: 6px; box-shadow: 0 16px 48px rgba(0, 0, 0, .2);140 max-height: 84vh; background: #fff; border-radius: 6px; box-shadow: 0 16px 48px rgba(0, 0, 0, .2);
129- padding: 20px 22px; display: flex; flex-direction: column; gap: 14px;141+ padding: 20px 22px; display: flex; flex-direction: column; gap: 14px; overflow-y: auto;
130 }142 }
131 .head { display: flex; align-items: baseline; gap: 12px; }143 .head { display: flex; align-items: baseline; gap: 12px; }
132 .h { font-size: 16px; font-weight: 600; }144 .h { font-size: 16px; font-weight: 600; }
@@ -135,18 +147,21 @@ async function onDrop(e: DragEvent) {
135 .chip { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 4px 9px; font-size: 12px; cursor: pointer; }147 .chip { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 4px 9px; font-size: 12px; cursor: pointer; }
136 .chip:hover { border-color: var(--border-hover); }148 .chip:hover { border-color: var(--border-hover); }
137 .chip.on { background: var(--ink); border-color: var(--ink); color: #fff; }149 .chip.on { background: var(--ink); border-color: var(--ink); color: #fff; }
138-.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 10px; align-content: start; }150+.grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px; }
139 .cell {151 .cell {
140 position: relative; aspect-ratio: 1; padding: 0; border: none; border-radius: 4px; overflow: hidden;152 position: relative; aspect-ratio: 1; padding: 0; border: none; border-radius: 4px; overflow: hidden;
141 cursor: pointer; background: var(--bg-soft); outline-offset: 2px;153 cursor: pointer; background: var(--bg-soft); outline-offset: 2px;
142 }154 }
143-.cell img { width: 100%; height: 100%; object-fit: cover; display: block; }155+/* hors du flux : une capture très haute ne doit pas agrandir sa cellule et déborder sur la ligne suivante */
156+.cell img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: top; display: block; }
144 .cell.on { outline: 2px solid var(--red); }157 .cell.on { outline: 2px solid var(--red); }
145 .cell .n {158 .cell .n {
146 position: absolute; top: 6px; right: 6px; min-width: 20px; height: 20px; border-radius: 10px; padding: 0 5px;159 position: absolute; top: 6px; right: 6px; min-width: 20px; height: 20px; border-radius: 10px; padding: 0 5px;
147 background: var(--red); color: #fff; font-family: var(--mono); font-size: 11px; line-height: 20px;160 background: var(--red); color: #fff; font-family: var(--mono); font-size: 11px; line-height: 20px;
148 }161 }
149 .nothing { grid-column: 1 / -1; padding: 10px 0; font-size: 13.5px; color: var(--faint); }162 .nothing { grid-column: 1 / -1; padding: 10px 0; font-size: 13.5px; color: var(--faint); }
163+.pager { display: flex; align-items: center; justify-content: center; gap: 12px; font-size: 12px; color: var(--muted); }
164+.pager .btn { padding: 2px 10px; }
150 .drop-zone { padding: 14px; }165 .drop-zone { padding: 14px; }
151 .foot { display: flex; justify-content: flex-end; gap: 8px; }166 .foot { display: flex; justify-content: flex-end; gap: 8px; }
152 </style>167 </style>