refactor(events): turn EventSlideOver into a centered modal with auto-save
User feedback: the right-edge slide-over panel pinned its action buttons
to a thin floor band where the destructive ghost-style Delete button was
functionally invisible against the dark surface. Save / Cancel / Delete
all sat in the same floor strip, isolated from the form content.
This refactor changes the surface and the commit model.
## Centered modal, not slide-over
Backdrop dim covers the whole viewport; the panel sits centered with a
12px corner radius and a soft shadow. The form scrolls internally when
content overflows the viewport (max-height: calc(100vh - 2.5rem)).
File kept as `EventSlideOver.vue` to avoid touching the three consumers
(CalendarView, HomeView, ToolCallCard).
## Action buttons removed; close = save
- Save button: gone. Auto-save fires when the user closes via X, Esc,
backdrop click, or pressing Enter inside a text field.
- Cancel button: gone. Esc / X / backdrop click already cover dismiss;
a labeled "Cancel" was redundant.
- Delete button: moved to the header as a Trash2 icon (edit mode only).
Click → header swaps to inline confirm "Delete this event? [Yes,
delete] [No]" — same two-step flow, just relocated. Esc during the
confirm cancels back to edit mode rather than closing the modal,
giving the user a clear way out of the destructive prompt.
## Validity-aware close
All exit paths funnel through `attemptClose`:
- Form valid → save (POST or PATCH), then close.
- Form invalid in EDIT mode → discard the in-memory change and
close, with a toast naming the missing field
("Title required — change discarded"). Keeps the user from
silently corrupting an event.
- Form invalid in CREATE mode → close silently. Nothing was
committed; calling that out adds noise.
Emit signature unchanged (close / created / updated / deleted), so the
three consumers continue to work without edits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { X } from "lucide-vue-next";
|
||||
import { Trash2, X } from "lucide-vue-next";
|
||||
import { ref, computed, watch, onMounted, onUnmounted } from "vue";
|
||||
import { createEvent, updateEvent, deleteEvent, type EventEntry, type EventCreatePayload, type EventUpdatePayload } from "@/api/client";
|
||||
import ProjectSelector from "@/components/ProjectSelector.vue";
|
||||
@@ -177,26 +177,69 @@ watch(() => props.event, resetForm, { immediate: true });
|
||||
watch(() => props.initialDate, resetForm);
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") emit("close");
|
||||
if (e.key === "Escape") {
|
||||
if (deleteConfirm.value) {
|
||||
// Esc cancels the delete-confirm rather than closing the modal —
|
||||
// gives the user a clear way out of the destructive prompt.
|
||||
deleteConfirm.value = false;
|
||||
return;
|
||||
}
|
||||
attemptClose();
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => document.addEventListener("keydown", handleKeydown));
|
||||
onUnmounted(() => document.removeEventListener("keydown", handleKeydown));
|
||||
|
||||
async function save() {
|
||||
// ── Close / save flow ─────────────────────────────────────────────────────────
|
||||
//
|
||||
// All exit paths (X button, Esc, backdrop click) funnel through `attemptClose`.
|
||||
// The Save button is gone — explicit-commit is replaced with auto-save-on-close.
|
||||
//
|
||||
// Validity-aware behavior:
|
||||
// - Form valid → save (PATCH for edit, POST for create), then close.
|
||||
// - Form invalid in EDIT mode → discard the in-memory change and close.
|
||||
// A toast tells the user what happened so they don't think their edit
|
||||
// silently landed.
|
||||
// - Form invalid in CREATE mode → close silently (nothing existed to begin
|
||||
// with; no need to call this out).
|
||||
|
||||
function isFormValid(): { valid: boolean; reason?: string } {
|
||||
if (!title.value.trim()) {
|
||||
toast.show("Title is required", "error");
|
||||
return;
|
||||
return { valid: false, reason: "Title required" };
|
||||
}
|
||||
if (!startDate.value) {
|
||||
toast.show("Start date is required", "error");
|
||||
return;
|
||||
return { valid: false, reason: "Start date required" };
|
||||
}
|
||||
if (!allDay.value && !startTime.value) {
|
||||
toast.show("Start time is required", "error");
|
||||
return;
|
||||
return { valid: false, reason: "Start time required" };
|
||||
}
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
let _closing = false;
|
||||
|
||||
async function attemptClose() {
|
||||
if (_closing) return;
|
||||
_closing = true;
|
||||
try {
|
||||
const validity = isFormValid();
|
||||
if (!validity.valid) {
|
||||
if (isEditMode.value) {
|
||||
toast.show(`${validity.reason} — change discarded`, "warning");
|
||||
}
|
||||
// Create mode + invalid: silent close. Nothing was committed.
|
||||
emit("close");
|
||||
return;
|
||||
}
|
||||
await save();
|
||||
emit("close");
|
||||
} finally {
|
||||
_closing = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function save() {
|
||||
const start_dt = allDay.value ? `${startDate.value}T00:00:00` : toIso(startDate.value, startTime.value);
|
||||
const end_dt = endDate.value
|
||||
? (allDay.value ? `${endDate.value}T00:00:00` : toIso(endDate.value, endTime.value))
|
||||
@@ -216,7 +259,6 @@ async function save() {
|
||||
project_id: projectId.value ?? undefined,
|
||||
};
|
||||
const updated = await updateEvent(props.event.id, payload);
|
||||
toast.show("Event updated", "success");
|
||||
emit("updated", updated);
|
||||
} else {
|
||||
const payload: EventCreatePayload = {
|
||||
@@ -230,7 +272,6 @@ async function save() {
|
||||
project_id: projectId.value ?? undefined,
|
||||
};
|
||||
const created = await createEvent(payload);
|
||||
toast.show("Event created", "success");
|
||||
emit("created", created);
|
||||
}
|
||||
} catch {
|
||||
@@ -256,23 +297,57 @@ async function doDelete() {
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div class="slide-over-backdrop" @click.self="emit('close')">
|
||||
<div class="slide-over-panel" role="dialog" aria-modal="true">
|
||||
<div class="so-header">
|
||||
<h2 class="so-title">{{ isEditMode ? "Edit Event" : "New Event" }}</h2>
|
||||
<button class="so-close" @click="emit('close')" aria-label="Close"><X :size="16" /></button>
|
||||
<div class="modal-backdrop" @click.self="attemptClose">
|
||||
<div class="modal-panel" role="dialog" aria-modal="true">
|
||||
<!-- Header: trash + close (or inline delete-confirm) -->
|
||||
<div class="modal-header">
|
||||
<template v-if="!deleteConfirm">
|
||||
<h2 class="modal-title">{{ isEditMode ? "Edit Event" : "New Event" }}</h2>
|
||||
<div class="header-actions">
|
||||
<button
|
||||
v-if="isEditMode"
|
||||
class="header-btn header-btn-danger"
|
||||
@click="deleteConfirm = true"
|
||||
title="Delete event"
|
||||
aria-label="Delete event"
|
||||
><Trash2 :size="16" /></button>
|
||||
<button
|
||||
class="header-btn"
|
||||
@click="attemptClose"
|
||||
title="Close"
|
||||
aria-label="Close"
|
||||
><X :size="16" /></button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="delete-confirm-prompt">Delete this event?</span>
|
||||
<div class="header-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="btn-danger"
|
||||
:disabled="deleting"
|
||||
@click="doDelete"
|
||||
>{{ deleting ? "Deleting…" : "Yes, delete" }}</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-confirm-cancel"
|
||||
@click="deleteConfirm = false"
|
||||
>No</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<form class="so-form" @submit.prevent="save">
|
||||
<!-- Body: form (scrolls if it gets long) -->
|
||||
<form class="modal-form" @submit.prevent="attemptClose">
|
||||
<!-- Title -->
|
||||
<div class="so-field">
|
||||
<label class="so-label">Title <span class="required">*</span></label>
|
||||
<input v-model="title" class="so-input" placeholder="Event title" autofocus />
|
||||
<div class="form-field">
|
||||
<label class="form-label">Title <span class="required">*</span></label>
|
||||
<input v-model="title" class="form-input" placeholder="Event title" autofocus />
|
||||
</div>
|
||||
|
||||
<!-- All-day toggle -->
|
||||
<div class="so-field so-field-row">
|
||||
<label class="so-label so-label-inline">All day</label>
|
||||
<div class="form-field form-field-row">
|
||||
<label class="form-label form-label-inline">All day</label>
|
||||
<button
|
||||
type="button"
|
||||
:class="['toggle-btn', { active: allDay }]"
|
||||
@@ -281,74 +356,55 @@ async function doDelete() {
|
||||
</div>
|
||||
|
||||
<!-- Start -->
|
||||
<div class="so-field">
|
||||
<label class="so-label">Start <span class="required">*</span></label>
|
||||
<div class="form-field">
|
||||
<label class="form-label">Start <span class="required">*</span></label>
|
||||
<div class="dt-row">
|
||||
<input v-model="startDate" type="date" class="so-input dt-date" required />
|
||||
<input v-if="!allDay" v-model="startTime" type="time" class="so-input dt-time" required />
|
||||
<input v-model="startDate" type="date" class="form-input dt-date" required />
|
||||
<input v-if="!allDay" v-model="startTime" type="time" class="form-input dt-time" required />
|
||||
</div>
|
||||
<p v-if="isPastEvent" class="so-past-hint">This event is in the past</p>
|
||||
<p v-if="isPastEvent" class="form-past-hint">This event is in the past</p>
|
||||
</div>
|
||||
|
||||
<!-- End -->
|
||||
<div class="so-field">
|
||||
<label class="so-label">End</label>
|
||||
<div class="form-field">
|
||||
<label class="form-label">End</label>
|
||||
<div class="dt-row">
|
||||
<input v-model="endDate" type="date" class="so-input dt-date" :min="startDate" />
|
||||
<input v-if="!allDay" v-model="endTime" type="time" class="so-input dt-time" />
|
||||
<input v-model="endDate" type="date" class="form-input dt-date" :min="startDate" />
|
||||
<input v-if="!allDay" v-model="endTime" type="time" class="form-input dt-time" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Location -->
|
||||
<div class="so-field">
|
||||
<label class="so-label">Location <span class="so-hint">(optional)</span></label>
|
||||
<input v-model="location" class="so-input" placeholder="Location" />
|
||||
<div class="form-field">
|
||||
<label class="form-label">Location <span class="form-hint">(optional)</span></label>
|
||||
<input v-model="location" class="form-input" placeholder="Location" />
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="so-field">
|
||||
<label class="so-label">Description <span class="so-hint">(optional)</span></label>
|
||||
<textarea v-model="description" class="so-input so-textarea" placeholder="Description" rows="3" />
|
||||
<div class="form-field">
|
||||
<label class="form-label">Description <span class="form-hint">(optional)</span></label>
|
||||
<textarea v-model="description" class="form-input form-textarea" placeholder="Description" rows="3" />
|
||||
</div>
|
||||
|
||||
<!-- Color -->
|
||||
<div class="so-field so-field-row">
|
||||
<label class="so-label so-label-inline">Color</label>
|
||||
<div class="form-field form-field-row">
|
||||
<label class="form-label form-label-inline">Color</label>
|
||||
<div class="color-row">
|
||||
<input v-model="color" type="color" class="color-picker" title="Pick event color" />
|
||||
<input v-model="color" class="so-input color-hex" placeholder="#5B4A8A" />
|
||||
<input v-model="color" class="form-input color-hex" placeholder="#5B4A8A" />
|
||||
<button v-if="color" type="button" class="btn-clear-color" @click="color = ''"><X :size="16" /></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Project -->
|
||||
<div class="so-field">
|
||||
<label class="so-label">Project <span class="so-hint">(optional)</span></label>
|
||||
<div class="form-field">
|
||||
<label class="form-label">Project <span class="form-hint">(optional)</span></label>
|
||||
<ProjectSelector v-model="projectId" />
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="so-actions">
|
||||
<button type="submit" class="btn-primary" :disabled="saving">
|
||||
{{ saving ? "Saving…" : (isEditMode ? "Save" : "Create") }}
|
||||
</button>
|
||||
<button type="button" class="btn-secondary" @click="emit('close')">Cancel</button>
|
||||
<template v-if="isEditMode">
|
||||
<button
|
||||
v-if="!deleteConfirm"
|
||||
type="button"
|
||||
class="btn-danger-ghost"
|
||||
@click="deleteConfirm = true"
|
||||
>Delete</button>
|
||||
<template v-else>
|
||||
<span class="delete-confirm-label">Delete this event?</span>
|
||||
<button type="button" class="btn-danger" :disabled="deleting" @click="doDelete">
|
||||
{{ deleting ? "Deleting…" : "Yes, delete" }}
|
||||
</button>
|
||||
<button type="button" class="btn-secondary" @click="deleteConfirm = false">No</button>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
<!-- A hidden submit so Enter inside text inputs triggers attemptClose,
|
||||
matching the no-explicit-Save-button intent: Enter commits. -->
|
||||
<button type="submit" class="hidden-submit" :disabled="saving" tabindex="-1" aria-hidden="true" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -356,81 +412,110 @@ async function doDelete() {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.slide-over-backdrop {
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
z-index: 200;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.slide-over-panel {
|
||||
.modal-panel {
|
||||
background: var(--color-surface, #1a1b1e);
|
||||
border-left: 1px solid var(--color-border, #2a2b30);
|
||||
width: min(440px, 100vw);
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--color-border, #2a2b30);
|
||||
border-radius: 12px;
|
||||
width: min(480px, 100%);
|
||||
max-height: calc(100vh - 2.5rem);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: -4px 0 24px rgba(0, 0, 0, 0.4);
|
||||
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.5);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.so-header {
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1.25rem 1.5rem;
|
||||
gap: 0.75rem;
|
||||
padding: 0.85rem 1rem 0.85rem 1.5rem;
|
||||
border-bottom: 1px solid var(--color-border, #2a2b30);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--color-surface, #1a1b1e);
|
||||
z-index: 1;
|
||||
min-height: 3rem;
|
||||
}
|
||||
|
||||
.so-title {
|
||||
.modal-title {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
color: var(--color-text, #e8e9f0);
|
||||
}
|
||||
|
||||
.so-close {
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.header-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-text-muted, #888);
|
||||
cursor: pointer;
|
||||
font-size: 1.1rem;
|
||||
padding: 0.25rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
padding: 0.4rem;
|
||||
border-radius: 6px;
|
||||
line-height: 1;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
.header-btn:hover {
|
||||
background: var(--color-hover, rgba(255,255,255,0.06));
|
||||
color: var(--color-text, #e8e9f0);
|
||||
}
|
||||
.so-close:hover { background: var(--color-hover, rgba(255,255,255,0.06)); }
|
||||
|
||||
.so-form {
|
||||
padding: 1.25rem 1.5rem;
|
||||
/* Trash in header: subtle until hover, then Oxblood. Lower visual weight
|
||||
than Save would have been — destructive actions shouldn't loom. */
|
||||
.header-btn-danger:hover {
|
||||
background: var(--color-action-destructive);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Inline delete-confirm prompt replaces the title row */
|
||||
.delete-confirm-prompt {
|
||||
font-size: 0.95rem;
|
||||
color: var(--color-text, #e8e9f0);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Form scrolls inside the panel when content overflows */
|
||||
.modal-form {
|
||||
padding: 1.25rem 1.5rem 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.1rem;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.so-field { display: flex; flex-direction: column; gap: 0.35rem; }
|
||||
.so-field-row { flex-direction: row; align-items: center; gap: 0.75rem; }
|
||||
.form-field { display: flex; flex-direction: column; gap: 0.35rem; }
|
||||
.form-field-row { flex-direction: row; align-items: center; gap: 0.75rem; }
|
||||
|
||||
.so-label {
|
||||
.form-label {
|
||||
font-size: 0.78rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-muted, #888);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.so-label-inline { flex-shrink: 0; margin: 0; }
|
||||
.so-hint { font-weight: 400; text-transform: none; letter-spacing: 0; opacity: 0.7; }
|
||||
.form-label-inline { flex-shrink: 0; margin: 0; }
|
||||
.form-hint { font-weight: 400; text-transform: none; letter-spacing: 0; opacity: 0.7; }
|
||||
|
||||
.required { color: #f87171; }
|
||||
|
||||
.so-input {
|
||||
.form-input {
|
||||
background: var(--color-input-bg, #111113);
|
||||
border: 1px solid var(--color-border, #2a2b30);
|
||||
color: var(--color-text, #e8e9f0);
|
||||
@@ -441,14 +526,14 @@ async function doDelete() {
|
||||
box-sizing: border-box;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
.so-input:focus { outline: none; border-color: var(--color-primary); }
|
||||
.form-input:focus { outline: none; border-color: var(--color-primary); }
|
||||
|
||||
.so-textarea { resize: vertical; min-height: 5rem; font-family: inherit; }
|
||||
.form-textarea { resize: vertical; min-height: 5rem; font-family: inherit; }
|
||||
|
||||
.dt-row { display: flex; gap: 0.5rem; }
|
||||
.dt-date { flex: 1; }
|
||||
.dt-time { width: 7.5rem; flex-shrink: 0; }
|
||||
.so-past-hint {
|
||||
.form-past-hint {
|
||||
margin: 4px 0 0;
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-secondary);
|
||||
@@ -482,65 +567,14 @@ async function doDelete() {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.so-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
padding-top: 0.5rem;
|
||||
border-top: 1px solid var(--color-border, #2a2b30);
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
/* Save (in slide-over): Moss action-primary */
|
||||
.btn-primary {
|
||||
background: var(--color-action-primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 0.55rem 1.2rem;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.btn-primary:hover:not(:disabled) { background: var(--color-action-primary-hover); }
|
||||
|
||||
/* Cancel: Bronze action-secondary */
|
||||
.btn-secondary {
|
||||
background: var(--color-action-secondary);
|
||||
border: none;
|
||||
color: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 0.55rem 1rem;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.btn-secondary:hover { background: var(--color-action-secondary-hover); }
|
||||
|
||||
/* Delete (entry-point): Oxblood action-destructive ghost */
|
||||
.btn-danger-ghost {
|
||||
margin-left: auto;
|
||||
background: none;
|
||||
border: 1px solid var(--color-action-destructive);
|
||||
color: var(--color-action-destructive);
|
||||
border-radius: 8px;
|
||||
padding: 0.55rem 1rem;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
.btn-danger-ghost:hover { background: var(--color-action-destructive); color: #fff; }
|
||||
|
||||
/* Confirm-delete: Oxblood filled */
|
||||
/* Confirm-delete buttons (only shown during the inline confirm flow) */
|
||||
.btn-danger {
|
||||
background: var(--color-action-destructive);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 0.55rem 1rem;
|
||||
font-size: 0.9rem;
|
||||
border-radius: 6px;
|
||||
padding: 0.4rem 0.85rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
@@ -548,9 +582,28 @@ async function doDelete() {
|
||||
.btn-danger:hover:not(:disabled) { background: var(--color-action-destructive-hover); }
|
||||
.btn-danger:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
|
||||
.delete-confirm-label {
|
||||
.btn-confirm-cancel {
|
||||
background: var(--color-action-secondary);
|
||||
border: none;
|
||||
color: #fff;
|
||||
border-radius: 6px;
|
||||
padding: 0.4rem 0.85rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-text-muted, #888);
|
||||
align-self: center;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.btn-confirm-cancel:hover { background: var(--color-action-secondary-hover); }
|
||||
|
||||
/* Hidden submit lets Enter-in-text-input trigger the same close-with-save
|
||||
path as X / Esc / backdrop. No visible Save button needed. */
|
||||
.hidden-submit {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0,0,0,0);
|
||||
border: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user