hlassiege/darylpublic Fork
⑂ 3b4d23a
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

feat(web): add a reusable modal component

hlassiege committed 2026-10-11T21:55:00+02:00 Browse files
3b4d23a parent: 5028966
added web/src/components/Modal.vue +54 -0
new file mode 100644
@@ -0,0 +1,54 @@
1+<script setup lang="ts">
2+/**
3+ * Une popup centrée sur un fond assombri : un clic à côté ou Échap la ferme.
4+ * Avec le slot `nav`, une colonne grise à gauche (les rubriques) ; le slot par défaut défile sous l'en-tête.
5+ */
6+import { onMounted, onUnmounted } from "vue";
7+import { useI18n } from "vue-i18n";
8+
9+withDefaults(defineProps<{ title?: string; width?: string; height?: string }>(), { title: "", width: "1120px", height: "760px" });
10+const emit = defineEmits<{ close: [] }>();
11+const { t } = useI18n();
12+
13+function onKey(e: KeyboardEvent) {
14+ if (e.key === "Escape") emit("close");
15+}
16+onMounted(() => window.addEventListener("keydown", onKey));
17+onUnmounted(() => window.removeEventListener("keydown", onKey));
18+</script>
19+
20+<template>
21+ <div class="overlay" @click.self="emit('close')">
22+ <div class="modal" :class="{ 'with-nav': $slots.nav }" :style="{ maxWidth: width, height: `min(${height}, 100%)` }" role="dialog" aria-modal="true">
23+ <aside v-if="$slots.nav" class="nav"><slot name="nav" /></aside>
24+ <div class="main">
25+ <div class="head">
26+ <span class="title">{{ title }}</span>
27+ <slot name="head" />
28+ <span class="spacer" />
29+ <button class="close" @click="emit('close')">{{ t("common.close") }} ×</button>
30+ </div>
31+ <div class="body"><slot /></div>
32+ </div>
33+ </div>
34+ </div>
35+</template>
36+
37+<style scoped>
38+.overlay {
39+ position: fixed; inset: 0; z-index: 40; background: rgba(24, 25, 27, .32);
40+ display: flex; align-items: center; justify-content: center; padding: 32px;
41+}
42+.modal {
43+ width: 100%; background: #fff; border-radius: 6px; box-shadow: 0 24px 64px rgba(0, 0, 0, .22);
44+ display: grid; grid-template-columns: minmax(0, 1fr); overflow: hidden;
45+}
46+.modal.with-nav { grid-template-columns: 220px minmax(0, 1fr); }
47+.nav { display: flex; flex-direction: column; gap: 2px; padding: 20px 12px; background: #F7F7F5; border-right: 1px solid var(--line); min-height: 0; }
48+.main { display: flex; flex-direction: column; min-width: 0; min-height: 0; }
49+.head { display: flex; align-items: center; gap: 12px; padding: 16px 24px; border-bottom: 1px solid var(--line); }
50+.title { font-size: 15px; font-weight: 600; }
51+.close { border: none; background: none; padding: 0; font-size: 13px; color: var(--muted); cursor: pointer; }
52+.close:hover { color: var(--ink); }
53+.body { flex: 1; overflow: auto; padding: 20px 24px 28px; display: flex; flex-direction: column; gap: 18px; }
54+</style>
new file mode 100644
@@ -0,0 +1,54 @@
1+<script setup lang="ts">
2+/**
3+ * Une popup centrée sur un fond assombri : un clic à côté ou Échap la ferme.
4+ * Avec le slot `nav`, une colonne grise à gauche (les rubriques) ; le slot par défaut défile sous l'en-tête.
5+ */
6+import { onMounted, onUnmounted } from "vue";
7+import { useI18n } from "vue-i18n";
8+
9+withDefaults(defineProps<{ title?: string; width?: string; height?: string }>(), { title: "", width: "1120px", height: "760px" });
10+const emit = defineEmits<{ close: [] }>();
11+const { t } = useI18n();
12+
13+function onKey(e: KeyboardEvent) {
14+ if (e.key === "Escape") emit("close");
15+}
16+onMounted(() => window.addEventListener("keydown", onKey));
17+onUnmounted(() => window.removeEventListener("keydown", onKey));
18+</script>
19+
20+<template>
21+ <div class="overlay" @click.self="emit('close')">
22+ <div class="modal" :class="{ 'with-nav': $slots.nav }" :style="{ maxWidth: width, height: `min(${height}, 100%)` }" role="dialog" aria-modal="true">
23+ <aside v-if="$slots.nav" class="nav"><slot name="nav" /></aside>
24+ <div class="main">
25+ <div class="head">
26+ <span class="title">{{ title }}</span>
27+ <slot name="head" />
28+ <span class="spacer" />
29+ <button class="close" @click="emit('close')">{{ t("common.close") }} ×</button>
30+ </div>
31+ <div class="body"><slot /></div>
32+ </div>
33+ </div>
34+ </div>
35+</template>
36+
37+<style scoped>
38+.overlay {
39+ position: fixed; inset: 0; z-index: 40; background: rgba(24, 25, 27, .32);
40+ display: flex; align-items: center; justify-content: center; padding: 32px;
41+}
42+.modal {
43+ width: 100%; background: #fff; border-radius: 6px; box-shadow: 0 24px 64px rgba(0, 0, 0, .22);
44+ display: grid; grid-template-columns: minmax(0, 1fr); overflow: hidden;
45+}
46+.modal.with-nav { grid-template-columns: 220px minmax(0, 1fr); }
47+.nav { display: flex; flex-direction: column; gap: 2px; padding: 20px 12px; background: #F7F7F5; border-right: 1px solid var(--line); min-height: 0; }
48+.main { display: flex; flex-direction: column; min-width: 0; min-height: 0; }
49+.head { display: flex; align-items: center; gap: 12px; padding: 16px 24px; border-bottom: 1px solid var(--line); }
50+.title { font-size: 15px; font-weight: 600; }
51+.close { border: none; background: none; padding: 0; font-size: 13px; color: var(--muted); cursor: pointer; }
52+.close:hover { color: var(--ink); }
53+.body { flex: 1; overflow: auto; padding: 20px 24px 28px; display: flex; flex-direction: column; gap: 18px; }
54+</style>