init
This commit is contained in:
341
frontend/src/components/FeatureKarte.vue
Normal file
341
frontend/src/components/FeatureKarte.vue
Normal file
@@ -0,0 +1,341 @@
|
||||
<script setup>
|
||||
import { nextTick, onMounted, ref, watch } from 'vue'
|
||||
import { marked } from 'marked'
|
||||
import { featureSpeichern, featureLoeschen } from '../api.js'
|
||||
|
||||
const props = defineProps({
|
||||
projekt: { type: String, required: true },
|
||||
feature: { type: Object, required: true },
|
||||
titelBearbeiten: { type: Boolean, default: false }, // frisch angelegt: Titel direkt öffnen
|
||||
})
|
||||
const emit = defineEmits(['aktualisiert', 'geloescht'])
|
||||
|
||||
function kopie(sekt) {
|
||||
return {
|
||||
titel: sekt.titel,
|
||||
art: sekt.art,
|
||||
ziel: sekt.ziel,
|
||||
funktionen: sekt.funktionen.map(f => ({ text: f.text, regeln: [...f.regeln] })),
|
||||
sonstiges: sekt.sonstiges,
|
||||
}
|
||||
}
|
||||
|
||||
const s = ref(kopie(props.feature.sektionen))
|
||||
watch(() => props.feature, f => { s.value = kopie(f.sektionen) })
|
||||
|
||||
const aktiv = ref(null) // {feld, i, j, original} — das eine gerade offene Eingabefeld
|
||||
const entwurf = ref('')
|
||||
const meldung = ref('')
|
||||
const feldRef = ref(null)
|
||||
|
||||
// Lokale, selbst geschriebene Daten — kein Sanitizing nötig
|
||||
const md = (text) => marked.parse(text)
|
||||
|
||||
function istAktiv(feld, i, j) {
|
||||
const a = aktiv.value
|
||||
return !!a && a.feld === feld && a.i === i && a.j === j
|
||||
}
|
||||
|
||||
function wert(feld, i, j) {
|
||||
if (feld === 'titel') return s.value.titel
|
||||
if (feld === 'ziel') return s.value.ziel
|
||||
if (feld === 'sonstiges') return s.value.sonstiges
|
||||
if (feld === 'funktion') return s.value.funktionen[i].text
|
||||
return s.value.funktionen[i].regeln[j]
|
||||
}
|
||||
|
||||
async function starte(feld, i, j) {
|
||||
const original = wert(feld, i, j)
|
||||
aktiv.value = { feld, i, j, original }
|
||||
entwurf.value = original
|
||||
await nextTick()
|
||||
const el = Array.isArray(feldRef.value) ? feldRef.value[0] : feldRef.value
|
||||
el?.focus()
|
||||
if (feld === 'titel') el?.select()
|
||||
}
|
||||
|
||||
function abbrechen() {
|
||||
const a = aktiv.value
|
||||
aktiv.value = null
|
||||
// frisch angelegte, noch leere Einträge wieder entfernen
|
||||
if (a && !a.original) {
|
||||
if (a.feld === 'funktion') s.value.funktionen.splice(a.i, 1)
|
||||
if (a.feld === 'regel') s.value.funktionen[a.i].regeln.splice(a.j, 1)
|
||||
}
|
||||
}
|
||||
|
||||
async function fertig() {
|
||||
const a = aktiv.value
|
||||
if (!a) return
|
||||
aktiv.value = null
|
||||
const neu = entwurf.value.trim()
|
||||
|
||||
if (neu === a.original) { abbrechenLeer(a); return }
|
||||
|
||||
if (a.feld === 'titel') {
|
||||
if (!neu) return // Titel ist Pflicht — leere Eingabe verwerfen
|
||||
s.value.titel = neu
|
||||
} else if (a.feld === 'ziel') {
|
||||
s.value.ziel = neu
|
||||
} else if (a.feld === 'sonstiges') {
|
||||
s.value.sonstiges = neu
|
||||
} else if (a.feld === 'funktion') {
|
||||
if (!neu) s.value.funktionen.splice(a.i, 1) // leeren = löschen
|
||||
else s.value.funktionen[a.i].text = neu
|
||||
} else if (a.feld === 'regel') {
|
||||
if (!neu) s.value.funktionen[a.i].regeln.splice(a.j, 1)
|
||||
else s.value.funktionen[a.i].regeln[a.j] = neu
|
||||
}
|
||||
await speichern()
|
||||
}
|
||||
|
||||
function abbrechenLeer(a) {
|
||||
if (a.original) return
|
||||
if (a.feld === 'funktion') s.value.funktionen.splice(a.i, 1)
|
||||
if (a.feld === 'regel') s.value.funktionen[a.i].regeln.splice(a.j, 1)
|
||||
}
|
||||
|
||||
function neueFunktion() {
|
||||
s.value.funktionen.push({ text: '', regeln: [] })
|
||||
starte('funktion', s.value.funktionen.length - 1)
|
||||
}
|
||||
|
||||
function neueRegel(i) {
|
||||
s.value.funktionen[i].regeln.push('')
|
||||
starte('regel', i, s.value.funktionen[i].regeln.length - 1)
|
||||
}
|
||||
|
||||
function entferneFunktion(i) {
|
||||
s.value.funktionen.splice(i, 1)
|
||||
speichern()
|
||||
}
|
||||
|
||||
function entferneRegel(i, j) {
|
||||
s.value.funktionen[i].regeln.splice(j, 1)
|
||||
speichern()
|
||||
}
|
||||
|
||||
function wechsleArt() {
|
||||
s.value.art = s.value.art === 'kern' ? 'optional' : 'kern'
|
||||
speichern()
|
||||
}
|
||||
|
||||
const TRENNER = '-'.repeat(40)
|
||||
|
||||
// Aus dem Karten-Zustand wird die Markdown-Datei zusammengesetzt — sie bleibt die Wahrheit
|
||||
function baueRoh() {
|
||||
const teile = [
|
||||
`<!-- titel -->\n${s.value.titel}`,
|
||||
`<!-- art -->\n${s.value.art}`,
|
||||
`<!-- ziel -->\n${s.value.ziel}`,
|
||||
]
|
||||
const funktionen = s.value.funktionen
|
||||
.map(f => [`<!-- funktion -->\n${f.text}`, ...f.regeln.map(r => `<!-- regel -->\n${r}`)].join('\n'))
|
||||
.join('\n')
|
||||
if (funktionen) teile.push(funktionen)
|
||||
teile.push(`<!-- sonstiges -->\n${s.value.sonstiges}`)
|
||||
return teile.join(`\n${TRENNER}\n`) + '\n'
|
||||
}
|
||||
|
||||
async function speichern() {
|
||||
meldung.value = ''
|
||||
try {
|
||||
const f = await featureSpeichern(props.projekt, props.feature.id, baueRoh())
|
||||
emit('aktualisiert', props.feature.id, f) // id kann sich durch Titeländerung ändern
|
||||
} catch (e) {
|
||||
meldung.value = e.message
|
||||
}
|
||||
}
|
||||
|
||||
async function loeschen() {
|
||||
if (!confirm(`Feature „${s.value.titel}“ löschen?`)) return
|
||||
try {
|
||||
await featureLoeschen(props.projekt, props.feature.id)
|
||||
emit('geloescht')
|
||||
} catch (e) {
|
||||
meldung.value = e.message
|
||||
}
|
||||
}
|
||||
|
||||
function zeilen(text) {
|
||||
return Math.max(2, text.split('\n').length)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.titelBearbeiten) starte('titel')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<article class="karte" :class="s.art">
|
||||
<header>
|
||||
<input
|
||||
v-if="istAktiv('titel')"
|
||||
ref="feldRef"
|
||||
v-model="entwurf"
|
||||
class="titel-feld"
|
||||
@blur="fertig"
|
||||
@keyup.enter="$event.target.blur()"
|
||||
@keydown.esc="abbrechen"
|
||||
>
|
||||
<h3 v-else :title="'Klicken zum Bearbeiten'" @click="starte('titel')">{{ s.titel }}</h3>
|
||||
<button
|
||||
class="badge"
|
||||
:class="s.art === 'kern' ? 'blau' : 'gruen'"
|
||||
title="Klicken zum Umschalten"
|
||||
@click="wechsleArt"
|
||||
>{{ s.art }}</button>
|
||||
<button class="icon-knopf rot werkzeug" title="Feature löschen" @click="loeschen">✕</button>
|
||||
</header>
|
||||
|
||||
<section :class="{ leer: !s.ziel && !istAktiv('ziel') }">
|
||||
<p class="gruppe">Ziel</p>
|
||||
<textarea
|
||||
v-if="istAktiv('ziel')"
|
||||
ref="feldRef"
|
||||
v-model="entwurf"
|
||||
:rows="zeilen(entwurf)"
|
||||
@blur="fertig"
|
||||
@keydown.esc="abbrechen"
|
||||
/>
|
||||
<div v-else-if="s.ziel" class="inhalt klickbar" v-html="md(s.ziel)" @click="starte('ziel')" />
|
||||
<p v-else class="hinweis" @click="starte('ziel')">Ziel ergänzen…</p>
|
||||
</section>
|
||||
|
||||
<section :class="{ leer: !s.funktionen.length }">
|
||||
<p class="gruppe">Funktionen</p>
|
||||
<div v-for="(f, i) in s.funktionen" :key="i" class="funktion">
|
||||
<div class="zeile">
|
||||
<textarea
|
||||
v-if="istAktiv('funktion', i)"
|
||||
ref="feldRef"
|
||||
v-model="entwurf"
|
||||
:rows="zeilen(entwurf)"
|
||||
@blur="fertig"
|
||||
@keydown.esc="abbrechen"
|
||||
/>
|
||||
<div v-else class="inhalt klickbar" v-html="md(f.text)" @click="starte('funktion', i)" />
|
||||
<button class="icon-knopf rot werkzeug" title="Funktion entfernen" @click="entferneFunktion(i)">✕</button>
|
||||
</div>
|
||||
<div class="regeln">
|
||||
<div v-for="(r, j) in f.regeln" :key="j" class="zeile">
|
||||
<textarea
|
||||
v-if="istAktiv('regel', i, j)"
|
||||
ref="feldRef"
|
||||
v-model="entwurf"
|
||||
:rows="zeilen(entwurf)"
|
||||
@blur="fertig"
|
||||
@keydown.esc="abbrechen"
|
||||
/>
|
||||
<div v-else class="inhalt klickbar" v-html="md(r)" @click="starte('regel', i, j)" />
|
||||
<button class="icon-knopf rot werkzeug" title="Regel entfernen" @click="entferneRegel(i, j)">✕</button>
|
||||
</div>
|
||||
<button class="hinzu werkzeug" @click="neueRegel(i)">+ Regel</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="hinzu werkzeug" @click="neueFunktion">+ Funktion</button>
|
||||
</section>
|
||||
|
||||
<section :class="{ leer: !s.sonstiges && !istAktiv('sonstiges') }">
|
||||
<p class="gruppe">Sonstiges</p>
|
||||
<textarea
|
||||
v-if="istAktiv('sonstiges')"
|
||||
ref="feldRef"
|
||||
v-model="entwurf"
|
||||
:rows="zeilen(entwurf)"
|
||||
@blur="fertig"
|
||||
@keydown.esc="abbrechen"
|
||||
/>
|
||||
<div v-else-if="s.sonstiges" class="inhalt klickbar" v-html="md(s.sonstiges)" @click="starte('sonstiges')" />
|
||||
<p v-else class="hinweis" @click="starte('sonstiges')">Sonstiges ergänzen…</p>
|
||||
</section>
|
||||
|
||||
<p v-if="meldung" class="meldung">{{ meldung }}</p>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.karte.kern { border-color: var(--akzent); }
|
||||
.karte.optional { border-color: var(--gruen); }
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.5rem;
|
||||
padding-bottom: 0.55rem;
|
||||
border-bottom: 1px solid var(--rand);
|
||||
}
|
||||
h3 { margin: 0; font-size: 1rem; cursor: text; flex: 1; }
|
||||
.badge { cursor: pointer; }
|
||||
.titel-feld { flex: 1; font-weight: 600; padding: 0.1rem 0.4rem; }
|
||||
|
||||
section { padding: 0.6rem 0; }
|
||||
section + section { border-top: 1px solid var(--rand); }
|
||||
.gruppe {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--akzent);
|
||||
margin: 0 0 0.15rem;
|
||||
}
|
||||
|
||||
/* Leere Sektionen und Werkzeuge erscheinen erst beim Hover über die Karte */
|
||||
section.leer { display: none; }
|
||||
.karte:hover section.leer { display: block; }
|
||||
.werkzeug { opacity: 0; transition: opacity 0.1s; }
|
||||
.karte:hover .werkzeug, .werkzeug:focus { opacity: 1; }
|
||||
/* „+ Funktion"/„+ Regel" belegen ohne Hover auch keinen Platz */
|
||||
.hinzu.werkzeug { display: none; }
|
||||
.karte:hover .hinzu.werkzeug { display: block; }
|
||||
|
||||
.hinweis {
|
||||
color: var(--dezent);
|
||||
font-size: 0.85rem;
|
||||
margin: 0.1rem 0;
|
||||
cursor: text;
|
||||
}
|
||||
.hinzu {
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--dezent);
|
||||
font-size: 0.8rem;
|
||||
padding: 0.1rem 0;
|
||||
}
|
||||
.hinzu:hover { color: var(--akzent); }
|
||||
|
||||
.zeile {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
.zeile .inhalt, .zeile textarea { flex: 1; min-width: 0; }
|
||||
.zeile .icon-knopf { font-size: 0.8rem; }
|
||||
|
||||
.funktion + .funktion {
|
||||
margin-top: 0.5rem;
|
||||
padding-top: 0.5rem;
|
||||
border-top: 1px dashed var(--rand);
|
||||
}
|
||||
.regeln {
|
||||
margin: 0.25rem 0 0 0.3rem;
|
||||
padding-left: 0.6rem;
|
||||
border-left: 2px solid var(--rand);
|
||||
color: var(--dezent);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
resize: vertical;
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.klickbar { cursor: text; border-radius: 4px; }
|
||||
.klickbar:hover { background: var(--code-bg); }
|
||||
|
||||
.inhalt :deep(p) { margin: 0.2rem 0; }
|
||||
.inhalt :deep(ul) { margin: 0.2rem 0; padding-left: 1.2rem; }
|
||||
.inhalt :deep(li) { margin: 0.1rem 0; }
|
||||
</style>
|
||||
87
frontend/src/components/FeatureListe.vue
Normal file
87
frontend/src/components/FeatureListe.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { fetchFeatures, featureAnlegen } from '../api.js'
|
||||
import FeatureKarte from './FeatureKarte.vue'
|
||||
|
||||
const props = defineProps({
|
||||
projekt: { type: String, required: true },
|
||||
})
|
||||
const emit = defineEmits(['geaendert'])
|
||||
|
||||
const features = ref([])
|
||||
const meldung = ref('')
|
||||
const neuId = ref(null) // frisch angelegte Karte: Titel direkt bearbeiten
|
||||
|
||||
async function laden() {
|
||||
meldung.value = ''
|
||||
neuId.value = null
|
||||
try {
|
||||
features.value = await fetchFeatures(props.projekt)
|
||||
} catch (e) {
|
||||
features.value = []
|
||||
meldung.value = e.message
|
||||
}
|
||||
}
|
||||
|
||||
async function neuesFeature() {
|
||||
meldung.value = ''
|
||||
try {
|
||||
const f = await featureAnlegen(props.projekt, '<!-- titel -->\nNeues Feature\n')
|
||||
features.value.push(f)
|
||||
neuId.value = f.id
|
||||
emit('geaendert') // Anzahl in der Seitenleiste aktualisieren
|
||||
} catch (e) {
|
||||
meldung.value = e.message
|
||||
}
|
||||
}
|
||||
|
||||
function aktualisiert(altId, f) {
|
||||
const idx = features.value.findIndex(x => x.id === altId)
|
||||
if (idx >= 0) features.value[idx] = f
|
||||
if (neuId.value === altId) neuId.value = null
|
||||
}
|
||||
|
||||
function geloescht(id) {
|
||||
features.value = features.value.filter(f => f.id !== id)
|
||||
emit('geaendert')
|
||||
}
|
||||
|
||||
watch(() => props.projekt, laden, { immediate: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<p v-if="meldung" class="meldung">{{ meldung }}</p>
|
||||
|
||||
<div class="gitter">
|
||||
<FeatureKarte
|
||||
v-for="f in features"
|
||||
:key="f.id"
|
||||
:projekt="projekt"
|
||||
:feature="f"
|
||||
:titel-bearbeiten="f.id === neuId"
|
||||
@aktualisiert="aktualisiert"
|
||||
@geloescht="geloescht(f.id)"
|
||||
/>
|
||||
<button class="karte neu" @click="neuesFeature">+ Feature</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.gitter {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 0.9rem;
|
||||
align-items: start;
|
||||
}
|
||||
/* Geisterkarte statt Kopfzeilen-Knopf */
|
||||
.neu {
|
||||
border: 1px dashed var(--rand);
|
||||
background: none;
|
||||
color: var(--dezent);
|
||||
min-height: 90px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.neu:hover { color: var(--akzent); border-color: var(--akzent); }
|
||||
</style>
|
||||
156
frontend/src/components/SeitenLeiste.vue
Normal file
156
frontend/src/components/SeitenLeiste.vue
Normal file
@@ -0,0 +1,156 @@
|
||||
<script setup>
|
||||
import { nextTick, ref } from 'vue'
|
||||
import { projektAnlegen, projektUmbenennen, projektLoeschen } from '../api.js'
|
||||
|
||||
const props = defineProps({
|
||||
projekte: { type: Array, required: true },
|
||||
aktiv: { type: String, default: null },
|
||||
})
|
||||
const emit = defineEmits(['auswaehlen', 'geaendert', 'umbenannt'])
|
||||
|
||||
const neuerName = ref('')
|
||||
const meldung = ref('')
|
||||
const bearbeitet = ref(null) // Projektname, der gerade umbenannt wird
|
||||
const umbenennung = ref('')
|
||||
const umbenennFeld = ref(null)
|
||||
|
||||
async function anlegen() {
|
||||
meldung.value = ''
|
||||
const name = neuerName.value.trim()
|
||||
if (!name) return
|
||||
try {
|
||||
await projektAnlegen(name)
|
||||
neuerName.value = ''
|
||||
emit('geaendert')
|
||||
emit('auswaehlen', name)
|
||||
} catch (e) {
|
||||
meldung.value = e.message
|
||||
}
|
||||
}
|
||||
|
||||
async function starteUmbenennen(name) {
|
||||
meldung.value = ''
|
||||
bearbeitet.value = name
|
||||
umbenennung.value = name
|
||||
await nextTick()
|
||||
umbenennFeld.value?.[0]?.focus()
|
||||
}
|
||||
|
||||
async function umbenennen(alt) {
|
||||
const neu = umbenennung.value.trim()
|
||||
bearbeitet.value = null
|
||||
if (!neu || neu === alt) return
|
||||
try {
|
||||
await projektUmbenennen(alt, neu)
|
||||
emit('umbenannt', alt, neu)
|
||||
} catch (e) {
|
||||
meldung.value = e.message
|
||||
}
|
||||
}
|
||||
|
||||
async function loeschen(name) {
|
||||
meldung.value = ''
|
||||
if (!confirm(`Projekt „${name}“ mit allen Features löschen?`)) return
|
||||
try {
|
||||
await projektLoeschen(name)
|
||||
emit('geaendert')
|
||||
} catch (e) {
|
||||
meldung.value = e.message
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside>
|
||||
<header>
|
||||
<p class="gruppe">Projekte</p>
|
||||
<slot />
|
||||
</header>
|
||||
<ul>
|
||||
<li
|
||||
v-for="p in projekte"
|
||||
:key="p.name"
|
||||
:class="{ aktiv: p.name === aktiv }"
|
||||
@click="emit('auswaehlen', p.name)"
|
||||
>
|
||||
<template v-if="bearbeitet === p.name">
|
||||
<input
|
||||
ref="umbenennFeld"
|
||||
v-model="umbenennung"
|
||||
@keyup.enter="umbenennen(p.name)"
|
||||
@keyup.escape="bearbeitet = null"
|
||||
@blur="umbenennen(p.name)"
|
||||
@click.stop
|
||||
>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="name">{{ p.name }}</span>
|
||||
<span class="anzahl">{{ p.anzahl }}</span>
|
||||
<span class="aktionen" @click.stop>
|
||||
<button class="icon-knopf" title="Umbenennen" @click="starteUmbenennen(p.name)">✎</button>
|
||||
<button class="icon-knopf rot" title="Löschen" @click="loeschen(p.name)">✕</button>
|
||||
</span>
|
||||
</template>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<form class="neu" @submit.prevent="anlegen">
|
||||
<input v-model="neuerName" placeholder="Neues Projekt">
|
||||
<button class="primaer" :disabled="!neuerName.trim()">+</button>
|
||||
</form>
|
||||
<p v-if="meldung" class="meldung">{{ meldung }}</p>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
aside {
|
||||
background: var(--panel);
|
||||
border-right: 1px solid var(--rand);
|
||||
padding: 1rem 0.8rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
.gruppe {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--dezent);
|
||||
margin: 0;
|
||||
}
|
||||
ul { list-style: none; margin: 0; padding: 0; }
|
||||
li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
padding: 0.3rem 0.5rem;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
li:hover { background: var(--bg); }
|
||||
li.aktiv { background: var(--bg); font-weight: 600; }
|
||||
.name {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.anzahl { color: var(--dezent); font-size: 0.78rem; }
|
||||
.aktionen { display: none; }
|
||||
li:hover .aktionen { display: inline-flex; }
|
||||
li:hover .anzahl { display: none; }
|
||||
li input { width: 100%; padding: 0.1rem 0.4rem; }
|
||||
.neu {
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
margin-top: 0.4rem;
|
||||
}
|
||||
.neu input { flex: 1; min-width: 0; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user