refactor(web): extract the design system swatch into a shared component
0606563 parent: 46136b1 added
web/src/components/ThemeSwatch.vue +18 -0 | new file mode 100644 | ||
| @@ -0,0 +1,18 @@ | ||
| 1 | +<script setup lang="ts"> | |
| 2 | +/** L'aperçu d'un design system : « Aa » sur le fond des cartes, dans la couleur du texte, et une pastille d'accent. */ | |
| 3 | +defineProps<{ theme: Record<string, string | number> }>(); | |
| 4 | +</script> | |
| 5 | + | |
| 6 | +<template> | |
| 7 | + <span class="swatch" :style="{ | |
| 8 | + background: String(theme.card), borderColor: String(theme.line), color: String(theme.text), | |
| 9 | + borderRadius: theme.radius === undefined ? undefined : `${Math.min(Number(theme.radius), 12)}px`, | |
| 10 | + }"> | |
| 11 | + Aa<i :style="{ background: String(theme.accent) }" /> | |
| 12 | + </span> | |
| 13 | +</template> | |
| 14 | + | |
| 15 | +<style scoped> | |
| 16 | +.swatch { position: relative; display: grid; place-items: center; flex: none; width: 44px; height: 30px; border: 1px solid; font-size: 13px; font-weight: 600; } | |
| 17 | +.swatch i { position: absolute; right: 4px; bottom: 4px; width: 8px; height: 8px; border-radius: 50%; } | |
| 18 | +</style> | |
| new file mode 100644 | |||
| @@ -0,0 +1,18 @@ | |||
| 1 | +<script setup lang="ts"> | ||
| 2 | +/** L'aperçu d'un design system : « Aa » sur le fond des cartes, dans la couleur du texte, et une pastille d'accent. */ | ||
| 3 | +defineProps<{ theme: Record<string, string | number> }>(); | ||
| 4 | +</script> | ||
| 5 | + | ||
| 6 | +<template> | ||
| 7 | + <span class="swatch" :style="{ | ||
| 8 | + background: String(theme.card), borderColor: String(theme.line), color: String(theme.text), | ||
| 9 | + borderRadius: theme.radius === undefined ? undefined : `${Math.min(Number(theme.radius), 12)}px`, | ||
| 10 | + }"> | ||
| 11 | + Aa<i :style="{ background: String(theme.accent) }" /> | ||
| 12 | + </span> | ||
| 13 | +</template> | ||
| 14 | + | ||
| 15 | +<style scoped> | ||
| 16 | +.swatch { position: relative; display: grid; place-items: center; flex: none; width: 44px; height: 30px; border: 1px solid; font-size: 13px; font-weight: 600; } | ||
| 17 | +.swatch i { position: absolute; right: 4px; bottom: 4px; width: 8px; height: 8px; border-radius: 50%; } | ||
| 18 | +</style> | ||
modified
web/src/pages/library/KitDesign.vue +4 -13 | @@ -8,6 +8,7 @@ import { computed, onMounted, ref } from "vue"; | ||
| 8 | 8 | import { useI18n } from "vue-i18n"; |
| 9 | 9 | import { api, putJson } from "../../lib/api"; |
| 10 | 10 | import { themesFromImage } from "../../lib/palette"; |
| 11 | +import ThemeSwatch from "../../components/ThemeSwatch.vue"; | |
| 11 | 12 | import { toast } from "../../lib/toast"; |
| 12 | 13 | |
| 13 | 14 | type Design = Record<string, string | number>; |
| @@ -35,10 +36,6 @@ const changed = (key: string) => view.value && view.value.design[key] !== view.v | ||
| 35 | 36 | |
| 36 | 37 | // la charte de base en tête des ambiances ; une ambiance est active si tous ses réglages le sont |
| 37 | 38 | const presets = computed<Design[]>(() => view.value ? [{ ...view.value.defaults, id: "base" }, ...view.value.presets] : []); |
| 38 | -const swatch = (p: Design) => ({ | |
| 39 | - background: String(p.card), borderColor: String(p.line), color: String(p.text), | |
| 40 | - borderRadius: p.radius === undefined ? undefined : `${Math.min(Number(p.radius), 12)}px`, | |
| 41 | -}); | |
| 42 | 39 | const active = (values: Design) => !!view.value && Object.entries(values) |
| 43 | 40 | .every(([k, v]) => k === "id" || view.value!.design[k] === v); |
| 44 | 41 | |
| @@ -67,9 +64,7 @@ async function readPhoto(e: Event) { | ||
| 67 | 64 | </div> |
| 68 | 65 | <div class="themes"> |
| 69 | 66 | <button v-for="p in presets" :key="p.id" class="theme" :class="{ active: active(p) }" @click="save(p)"> |
| 70 | - <span class="swatch" :style="swatch(p)"> | |
| 71 | - Aa<i :style="{ background: String(p.accent) }" /> | |
| 72 | - </span> | |
| 67 | + <ThemeSwatch :theme="p" /> | |
| 73 | 68 | <span>{{ t(`library.design.presets.${p.id}`) }}</span> |
| 74 | 69 | </button> |
| 75 | 70 | <button class="theme photo" @click="photo?.click()"> |
| @@ -81,9 +76,7 @@ async function readPhoto(e: Event) { | ||
| 81 | 76 | <div v-if="fromPhoto.length" class="themes"> |
| 82 | 77 | <span class="muted hint">{{ t("library.design.photoHint") }}</span> |
| 83 | 78 | <button v-for="(p, i) in fromPhoto" :key="i" class="theme" :class="{ active: active(p) }" @click="save(p)"> |
| 84 | - <span class="swatch" :style="swatch(p)"> | |
| 85 | - Aa<i :style="{ background: String(p.accent) }" /> | |
| 86 | - </span> | |
| 79 | + <ThemeSwatch :theme="p" /> | |
| 87 | 80 | <span>{{ t("library.design.photoTheme", { n: i + 1 }) }}</span> |
| 88 | 81 | </button> |
| 89 | 82 | </div> |
| @@ -122,9 +115,7 @@ async function readPhoto(e: Event) { | ||
| 122 | 115 | .theme { display: flex; align-items: center; gap: 8px; padding: 4px 10px 4px 4px; border: 1px solid var(--line); border-radius: 6px; background: none; font-size: 13px; color: var(--muted); cursor: pointer; } |
| 123 | 116 | .theme:hover { border-color: var(--line-strong); } |
| 124 | 117 | .theme.active { border-color: var(--ink); color: var(--ink); font-weight: 500; } |
| 125 | -.swatch { position: relative; display: grid; place-items: center; width: 44px; height: 30px; border: 1px solid; font-size: 13px; font-weight: 600; } | |
| 126 | -.swatch i { position: absolute; right: 4px; bottom: 4px; width: 8px; height: 8px; border-radius: 50%; } | |
| 127 | -.swatch.add { border: 1px dashed var(--line-strong); border-radius: 4px; color: var(--muted); font-size: 16px; font-weight: 400; } | |
| 118 | +.swatch.add { display: grid; place-items: center; width: 44px; height: 30px; border: 1px dashed var(--line-strong); border-radius: 4px; color: var(--muted); font-size: 16px; font-weight: 400; } | |
| 128 | 119 | .fields { display: flex; flex-wrap: wrap; gap: 10px 22px; align-items: center; } |
| 129 | 120 | .field { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--muted); cursor: pointer; } |
| 130 | 121 | .field.changed span { color: var(--ink); font-weight: 500; } |
| @@ -8,6 +8,7 @@ import { computed, onMounted, ref } from "vue"; | |||
| 8 | import { useI18n } from "vue-i18n"; | 8 | import { useI18n } from "vue-i18n"; |
| 9 | import { api, putJson } from "../../lib/api"; | 9 | import { api, putJson } from "../../lib/api"; |
| 10 | import { themesFromImage } from "../../lib/palette"; | 10 | import { themesFromImage } from "../../lib/palette"; |
| 11 | +import ThemeSwatch from "../../components/ThemeSwatch.vue"; | ||
| 11 | import { toast } from "../../lib/toast"; | 12 | import { toast } from "../../lib/toast"; |
| 12 | 13 | ||
| 13 | type Design = Record<string, string | number>; | 14 | type Design = Record<string, string | number>; |
| @@ -35,10 +36,6 @@ const changed = (key: string) => view.value && view.value.design[key] !== view.v | |||
| 35 | 36 | ||
| 36 | // la charte de base en tête des ambiances ; une ambiance est active si tous ses réglages le sont | 37 | // la charte de base en tête des ambiances ; une ambiance est active si tous ses réglages le sont |
| 37 | const presets = computed<Design[]>(() => view.value ? [{ ...view.value.defaults, id: "base" }, ...view.value.presets] : []); | 38 | const presets = computed<Design[]>(() => view.value ? [{ ...view.value.defaults, id: "base" }, ...view.value.presets] : []); |
| 38 | -const swatch = (p: Design) => ({ | ||
| 39 | - background: String(p.card), borderColor: String(p.line), color: String(p.text), | ||
| 40 | - borderRadius: p.radius === undefined ? undefined : `${Math.min(Number(p.radius), 12)}px`, | ||
| 41 | -}); | ||
| 42 | const active = (values: Design) => !!view.value && Object.entries(values) | 39 | const active = (values: Design) => !!view.value && Object.entries(values) |
| 43 | .every(([k, v]) => k === "id" || view.value!.design[k] === v); | 40 | .every(([k, v]) => k === "id" || view.value!.design[k] === v); |
| 44 | 41 | ||
| @@ -67,9 +64,7 @@ async function readPhoto(e: Event) { | |||
| 67 | </div> | 64 | </div> |
| 68 | <div class="themes"> | 65 | <div class="themes"> |
| 69 | <button v-for="p in presets" :key="p.id" class="theme" :class="{ active: active(p) }" @click="save(p)"> | 66 | <button v-for="p in presets" :key="p.id" class="theme" :class="{ active: active(p) }" @click="save(p)"> |
| 70 | - <span class="swatch" :style="swatch(p)"> | 67 | + <ThemeSwatch :theme="p" /> |
| 71 | - Aa<i :style="{ background: String(p.accent) }" /> | ||
| 72 | - </span> | ||
| 73 | <span>{{ t(`library.design.presets.${p.id}`) }}</span> | 68 | <span>{{ t(`library.design.presets.${p.id}`) }}</span> |
| 74 | </button> | 69 | </button> |
| 75 | <button class="theme photo" @click="photo?.click()"> | 70 | <button class="theme photo" @click="photo?.click()"> |
| @@ -81,9 +76,7 @@ async function readPhoto(e: Event) { | |||
| 81 | <div v-if="fromPhoto.length" class="themes"> | 76 | <div v-if="fromPhoto.length" class="themes"> |
| 82 | <span class="muted hint">{{ t("library.design.photoHint") }}</span> | 77 | <span class="muted hint">{{ t("library.design.photoHint") }}</span> |
| 83 | <button v-for="(p, i) in fromPhoto" :key="i" class="theme" :class="{ active: active(p) }" @click="save(p)"> | 78 | <button v-for="(p, i) in fromPhoto" :key="i" class="theme" :class="{ active: active(p) }" @click="save(p)"> |
| 84 | - <span class="swatch" :style="swatch(p)"> | 79 | + <ThemeSwatch :theme="p" /> |
| 85 | - Aa<i :style="{ background: String(p.accent) }" /> | ||
| 86 | - </span> | ||
| 87 | <span>{{ t("library.design.photoTheme", { n: i + 1 }) }}</span> | 80 | <span>{{ t("library.design.photoTheme", { n: i + 1 }) }}</span> |
| 88 | </button> | 81 | </button> |
| 89 | </div> | 82 | </div> |
| @@ -122,9 +115,7 @@ async function readPhoto(e: Event) { | |||
| 122 | .theme { display: flex; align-items: center; gap: 8px; padding: 4px 10px 4px 4px; border: 1px solid var(--line); border-radius: 6px; background: none; font-size: 13px; color: var(--muted); cursor: pointer; } | 115 | .theme { display: flex; align-items: center; gap: 8px; padding: 4px 10px 4px 4px; border: 1px solid var(--line); border-radius: 6px; background: none; font-size: 13px; color: var(--muted); cursor: pointer; } |
| 123 | .theme:hover { border-color: var(--line-strong); } | 116 | .theme:hover { border-color: var(--line-strong); } |
| 124 | .theme.active { border-color: var(--ink); color: var(--ink); font-weight: 500; } | 117 | .theme.active { border-color: var(--ink); color: var(--ink); font-weight: 500; } |
| 125 | -.swatch { position: relative; display: grid; place-items: center; width: 44px; height: 30px; border: 1px solid; font-size: 13px; font-weight: 600; } | 118 | +.swatch.add { display: grid; place-items: center; width: 44px; height: 30px; border: 1px dashed var(--line-strong); border-radius: 4px; color: var(--muted); font-size: 16px; font-weight: 400; } |
| 126 | -.swatch i { position: absolute; right: 4px; bottom: 4px; width: 8px; height: 8px; border-radius: 50%; } | ||
| 127 | -.swatch.add { border: 1px dashed var(--line-strong); border-radius: 4px; color: var(--muted); font-size: 16px; font-weight: 400; } | ||
| 128 | .fields { display: flex; flex-wrap: wrap; gap: 10px 22px; align-items: center; } | 119 | .fields { display: flex; flex-wrap: wrap; gap: 10px 22px; align-items: center; } |
| 129 | .field { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--muted); cursor: pointer; } | 120 | .field { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--muted); cursor: pointer; } |
| 130 | .field.changed span { color: var(--ink); font-weight: 500; } | 121 | .field.changed span { color: var(--ink); font-weight: 500; } |