Initial import of hire-me.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+261
@@ -0,0 +1,261 @@
|
||||
const statusEl = document.querySelector("#status");
|
||||
const listEl = document.querySelector("#list");
|
||||
const metaEl = document.querySelector("#meta");
|
||||
const dialog = document.querySelector("#preview");
|
||||
const previewTitle = document.querySelector("#preview-title");
|
||||
const previewFrom = document.querySelector("#preview-from");
|
||||
const previewReply = document.querySelector("#preview-reply");
|
||||
const previewAttach = document.querySelector("#preview-attach");
|
||||
const previewTo = document.querySelector("#preview-to");
|
||||
const previewSubject = document.querySelector("#preview-subject");
|
||||
const previewText = document.querySelector("#preview-text");
|
||||
const previewError = document.querySelector("#preview-error");
|
||||
const previewSend = document.querySelector("#preview-send");
|
||||
const previewRegen = document.querySelector("#preview-regen");
|
||||
|
||||
/** @type {{ offer: any, article: HTMLElement, button: HTMLButtonElement, input: HTMLInputElement, hint: HTMLElement | null, feedback: HTMLElement, language: string } | null} */
|
||||
let active = null;
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value)
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """);
|
||||
}
|
||||
|
||||
function sourceLabel(source) {
|
||||
if (source === "website") return "trouvé sur le site";
|
||||
if (source === "gemini") return "détecté via Gemini";
|
||||
if (source === "guess") return "estimé (careers/recruiting@)";
|
||||
if (source === "description") return "dans l’offre";
|
||||
if (source === "manual") return "saisi";
|
||||
if (source === "cache") return "en cache";
|
||||
return source || "recherche…";
|
||||
}
|
||||
|
||||
function showPreviewError(message) {
|
||||
if (!previewError) return;
|
||||
previewError.hidden = !message;
|
||||
previewError.textContent = message || "";
|
||||
}
|
||||
|
||||
async function loadPreview(forceTo) {
|
||||
if (!active) return;
|
||||
const { offer, button, input, feedback } = active;
|
||||
showPreviewError("");
|
||||
button.disabled = true;
|
||||
button.textContent = "Rédaction…";
|
||||
feedback.hidden = false;
|
||||
feedback.className = "feedback";
|
||||
feedback.textContent = "Gemini rédige le mail…";
|
||||
if (previewRegen) previewRegen.disabled = true;
|
||||
if (previewSend) previewSend.disabled = true;
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/preview", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
id: offer.id,
|
||||
to: forceTo || input.value.trim() || undefined,
|
||||
}),
|
||||
});
|
||||
const data = await response.json();
|
||||
if (!response.ok) throw new Error(data.error || "Impossible de préparer le mail");
|
||||
|
||||
active.language = data.language || "en";
|
||||
previewTitle.textContent = `${data.company} — ${data.title}`;
|
||||
previewFrom.textContent = data.from;
|
||||
previewReply.textContent = data.replyTo;
|
||||
previewAttach.textContent = data.attachment;
|
||||
previewTo.value = data.to;
|
||||
previewSubject.value = data.subject;
|
||||
previewText.value = data.text;
|
||||
input.value = data.to;
|
||||
if (active.hint) active.hint.textContent = sourceLabel(data.emailSource);
|
||||
|
||||
feedback.hidden = true;
|
||||
dialog?.showModal();
|
||||
} catch (error) {
|
||||
feedback.className = "feedback err";
|
||||
feedback.textContent = error instanceof Error ? error.message : "Erreur";
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
button.textContent = "Postuler";
|
||||
if (previewRegen) previewRegen.disabled = false;
|
||||
if (previewSend) previewSend.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function sendPreview() {
|
||||
if (!active) return;
|
||||
const { offer, article, button, input, hint, feedback } = active;
|
||||
const to = previewTo.value.trim();
|
||||
const subject = previewSubject.value.trim();
|
||||
const text = previewText.value.trim();
|
||||
showPreviewError("");
|
||||
|
||||
if (!to || !subject || !text) {
|
||||
showPreviewError("Destinataire, objet et corps sont requis.");
|
||||
return;
|
||||
}
|
||||
|
||||
previewSend.disabled = true;
|
||||
previewRegen.disabled = true;
|
||||
previewSend.textContent = "Envoi…";
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/apply", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
id: offer.id,
|
||||
to,
|
||||
subject,
|
||||
text,
|
||||
language: active.language,
|
||||
}),
|
||||
});
|
||||
const data = await response.json();
|
||||
if (!response.ok) throw new Error(data.error || "Échec de candidature");
|
||||
|
||||
article.classList.add("applied");
|
||||
button.classList.add("done");
|
||||
button.textContent = "Envoyé";
|
||||
button.disabled = true;
|
||||
input.value = data.to;
|
||||
input.disabled = true;
|
||||
if (hint) hint.textContent = sourceLabel(data.emailSource);
|
||||
feedback.hidden = false;
|
||||
feedback.className = "feedback ok";
|
||||
feedback.textContent = `Envoyé à ${data.to} — ${data.subject}`;
|
||||
dialog?.close();
|
||||
} catch (error) {
|
||||
showPreviewError(error instanceof Error ? error.message : "Erreur");
|
||||
} finally {
|
||||
previewSend.disabled = false;
|
||||
previewRegen.disabled = false;
|
||||
previewSend.textContent = "Envoyer";
|
||||
}
|
||||
}
|
||||
|
||||
function renderOffer(offer) {
|
||||
const applied = Boolean(offer.appliedAt);
|
||||
const email = offer.emails[0] ?? "";
|
||||
const article = document.createElement("article");
|
||||
article.className = `offer${applied ? " applied" : ""}`;
|
||||
article.dataset.id = offer.id;
|
||||
article.innerHTML = `
|
||||
<div class="offer-head">
|
||||
<div>
|
||||
<p class="company">${escapeHtml(offer.company)}</p>
|
||||
<h2 class="title">${escapeHtml(offer.title)}</h2>
|
||||
</div>
|
||||
<span class="score">score ${escapeHtml(offer.score)}</span>
|
||||
</div>
|
||||
<p class="meta-row">
|
||||
<span>${escapeHtml(offer.location)}</span>
|
||||
<span>·</span>
|
||||
<span>${escapeHtml(offer.source)}</span>
|
||||
<span>·</span>
|
||||
<a href="${escapeHtml(offer.url)}" target="_blank" rel="noreferrer">voir l’offre</a>
|
||||
</p>
|
||||
<ul class="reasons">
|
||||
${offer.reasons.map((reason) => `<li>${escapeHtml(reason)}</li>`).join("")}
|
||||
</ul>
|
||||
<p class="excerpt">${escapeHtml(offer.excerpt)}</p>
|
||||
<div class="actions">
|
||||
<label>
|
||||
Email destinataire
|
||||
<input
|
||||
type="email"
|
||||
name="to"
|
||||
value="${escapeHtml(email)}"
|
||||
placeholder="${email ? "" : "Recherche auto en cours…"}"
|
||||
${applied ? "disabled" : ""}
|
||||
/>
|
||||
<span class="email-hint">${
|
||||
email
|
||||
? escapeHtml(sourceLabel(offer.emailSource))
|
||||
: "sera récupéré automatiquement au clic si besoin"
|
||||
}</span>
|
||||
</label>
|
||||
<button type="button" ${applied ? "disabled" : ""}>
|
||||
${applied ? "Déjà postulé" : "Postuler"}
|
||||
</button>
|
||||
</div>
|
||||
<p class="feedback" hidden></p>
|
||||
`;
|
||||
|
||||
const button = article.querySelector("button");
|
||||
const input = article.querySelector('input[name="to"]');
|
||||
const hint = article.querySelector(".email-hint");
|
||||
const feedback = article.querySelector(".feedback");
|
||||
|
||||
button?.addEventListener("click", async () => {
|
||||
if (!button || !input || !feedback) return;
|
||||
active = { offer, article, button, input, hint, feedback, language: "en" };
|
||||
await loadPreview(input.value.trim());
|
||||
});
|
||||
|
||||
return article;
|
||||
}
|
||||
|
||||
previewSend?.addEventListener("click", () => {
|
||||
void sendPreview();
|
||||
});
|
||||
|
||||
previewRegen?.addEventListener("click", () => {
|
||||
void loadPreview(previewTo?.value.trim());
|
||||
});
|
||||
|
||||
async function boot() {
|
||||
try {
|
||||
const response = await fetch("/api/offers");
|
||||
const data = await response.json();
|
||||
if (!response.ok) throw new Error(data.error || "Impossible de charger les offres");
|
||||
|
||||
statusEl.hidden = true;
|
||||
listEl.hidden = false;
|
||||
metaEl.hidden = false;
|
||||
const withEmail = data.offers.filter((o) => o.emails?.length).length;
|
||||
metaEl.textContent = `${data.count} offre(s) — ${withEmail} email(s) trouvés. Postuler ouvre d’abord un aperçu.`;
|
||||
|
||||
if (!data.offers.length) {
|
||||
statusEl.hidden = false;
|
||||
statusEl.textContent = "Aucune offre. Lance d’abord `npm run find`.";
|
||||
return;
|
||||
}
|
||||
|
||||
for (const offer of data.offers) {
|
||||
listEl.append(renderOffer(offer));
|
||||
}
|
||||
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const again = await fetch("/api/offers");
|
||||
const next = await again.json();
|
||||
if (!again.ok) return;
|
||||
for (const offer of next.offers) {
|
||||
const article = listEl.querySelector(`[data-id="${offer.id}"]`);
|
||||
const input = article?.querySelector('input[name="to"]');
|
||||
const hint = article?.querySelector(".email-hint");
|
||||
if (input && !input.disabled && offer.emails?.[0] && !input.value) {
|
||||
input.value = offer.emails[0];
|
||||
if (hint) hint.textContent = sourceLabel(offer.emailSource);
|
||||
}
|
||||
}
|
||||
const filled = next.offers.filter((o) => o.emails?.length).length;
|
||||
metaEl.textContent = `${next.count} offre(s) — ${filled} email(s) trouvés. Postuler ouvre d’abord un aperçu.`;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}, 8000);
|
||||
} catch (error) {
|
||||
statusEl.classList.add("error");
|
||||
statusEl.textContent = error instanceof Error ? error.message : "Erreur de chargement";
|
||||
}
|
||||
}
|
||||
|
||||
boot();
|
||||
@@ -0,0 +1,75 @@
|
||||
<!doctype html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>hire-me — candidatures</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400&family=Instrument+Serif:ital@0;1&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link rel="stylesheet" href="/styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="bg"></div>
|
||||
<main>
|
||||
<header class="top">
|
||||
<p class="brand">hire-me</p>
|
||||
<h1>Offres alignées sur ton profil</h1>
|
||||
<p class="lede">
|
||||
<strong>Postuler</strong> prépare le mail (destinataire + texte Gemini + CV). Tu
|
||||
previews, tu ajustes, puis tu envoies.
|
||||
</p>
|
||||
<p id="meta" class="meta" hidden></p>
|
||||
</header>
|
||||
|
||||
<section id="status" class="status" aria-live="polite">Chargement des offres…</section>
|
||||
<section id="list" class="list" hidden></section>
|
||||
</main>
|
||||
|
||||
<dialog id="preview" class="preview">
|
||||
<form method="dialog" id="preview-form" class="preview-sheet">
|
||||
<header class="preview-top">
|
||||
<div>
|
||||
<p class="preview-kicker">Aperçu avant envoi</p>
|
||||
<h2 id="preview-title">Candidature</h2>
|
||||
</div>
|
||||
<button type="submit" value="cancel" class="ghost" id="preview-close">Fermer</button>
|
||||
</header>
|
||||
|
||||
<div class="preview-meta">
|
||||
<p><span>De</span> <strong id="preview-from"></strong></p>
|
||||
<p><span>Reply-To</span> <strong id="preview-reply"></strong></p>
|
||||
<p><span>Pièce jointe</span> <strong id="preview-attach"></strong></p>
|
||||
</div>
|
||||
|
||||
<label>
|
||||
Destinataire
|
||||
<input type="email" id="preview-to" required />
|
||||
</label>
|
||||
<label>
|
||||
Objet
|
||||
<input type="text" id="preview-subject" required />
|
||||
</label>
|
||||
<label>
|
||||
Corps du mail
|
||||
<textarea id="preview-text" rows="14" required></textarea>
|
||||
</label>
|
||||
|
||||
<p id="preview-error" class="feedback err" hidden></p>
|
||||
|
||||
<footer class="preview-actions">
|
||||
<button type="button" class="ghost" id="preview-regen">Regénérer</button>
|
||||
<div class="preview-actions-right">
|
||||
<button type="submit" value="cancel" class="ghost">Annuler</button>
|
||||
<button type="button" id="preview-send">Envoyer</button>
|
||||
</div>
|
||||
</footer>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<script type="module" src="/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,407 @@
|
||||
:root {
|
||||
--ink: #14201a;
|
||||
--muted: #5a6b62;
|
||||
--line: rgba(20, 32, 26, 0.12);
|
||||
--paper: #f3efe6;
|
||||
--panel: rgba(255, 252, 246, 0.78);
|
||||
--accent: #0f6b4c;
|
||||
--accent-ink: #f7fff9;
|
||||
--warn: #8a4b16;
|
||||
--danger: #8f2d2d;
|
||||
--ok: #1f6b3a;
|
||||
--shadow: 0 18px 50px rgba(20, 32, 26, 0.08);
|
||||
--radius: 18px;
|
||||
--font: "DM Sans", system-ui, sans-serif;
|
||||
--display: "Instrument Serif", Georgia, serif;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font);
|
||||
color: var(--ink);
|
||||
background: var(--paper);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.bg {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
background:
|
||||
radial-gradient(900px 480px at 12% -10%, rgba(15, 107, 76, 0.18), transparent 60%),
|
||||
radial-gradient(700px 420px at 100% 0%, rgba(138, 75, 22, 0.12), transparent 55%),
|
||||
linear-gradient(180deg, #f7f3ea 0%, #ebe4d6 100%);
|
||||
}
|
||||
|
||||
main {
|
||||
width: min(920px, calc(100% - 2rem));
|
||||
margin: 0 auto;
|
||||
padding: 2.5rem 0 4rem;
|
||||
}
|
||||
|
||||
.top {
|
||||
margin-bottom: 1.75rem;
|
||||
}
|
||||
|
||||
.brand {
|
||||
margin: 0 0 0.75rem;
|
||||
font-size: 0.85rem;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-family: var(--display);
|
||||
font-weight: 400;
|
||||
font-size: clamp(2.2rem, 5vw, 3.4rem);
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.05;
|
||||
}
|
||||
|
||||
.lede {
|
||||
margin: 0.85rem 0 0;
|
||||
max-width: 38rem;
|
||||
color: var(--muted);
|
||||
font-size: 1.02rem;
|
||||
}
|
||||
|
||||
.meta {
|
||||
margin: 0.85rem 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 1rem 1.15rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
background: var(--panel);
|
||||
backdrop-filter: blur(10px);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.status.error {
|
||||
color: var(--danger);
|
||||
border-color: rgba(143, 45, 45, 0.25);
|
||||
}
|
||||
|
||||
.list {
|
||||
display: grid;
|
||||
gap: 0.9rem;
|
||||
}
|
||||
|
||||
.offer {
|
||||
display: grid;
|
||||
gap: 0.95rem;
|
||||
padding: 1.15rem 1.2rem 1.2rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
background: var(--panel);
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: var(--shadow);
|
||||
transition: transform 160ms ease, border-color 160ms ease;
|
||||
}
|
||||
|
||||
.offer:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: rgba(15, 107, 76, 0.28);
|
||||
}
|
||||
|
||||
.offer.applied {
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.offer-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.company {
|
||||
margin: 0;
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0.25rem 0 0;
|
||||
font-family: var(--display);
|
||||
font-size: clamp(1.35rem, 2.5vw, 1.7rem);
|
||||
line-height: 1.15;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.score {
|
||||
flex: none;
|
||||
padding: 0.35rem 0.65rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(15, 107, 76, 0.1);
|
||||
color: var(--accent);
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.meta-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.45rem 0.75rem;
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.meta-row a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.reasons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.reasons li {
|
||||
padding: 0.22rem 0.55rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(20, 32, 26, 0.05);
|
||||
color: var(--muted);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.excerpt {
|
||||
margin: 0;
|
||||
color: var(--ink);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 0.65rem;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
label {
|
||||
display: grid;
|
||||
gap: 0.3rem;
|
||||
font-size: 0.78rem;
|
||||
color: var(--muted);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.email-hint {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: var(--muted);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
input[type="email"] {
|
||||
width: 100%;
|
||||
padding: 0.7rem 0.8rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
color: var(--ink);
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
input[type="email"]:focus {
|
||||
outline: 2px solid rgba(15, 107, 76, 0.35);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
button {
|
||||
appearance: none;
|
||||
border: 0;
|
||||
border-radius: 12px;
|
||||
padding: 0.78rem 1.15rem;
|
||||
background: var(--accent);
|
||||
color: var(--accent-ink);
|
||||
font: inherit;
|
||||
font-weight: 650;
|
||||
cursor: pointer;
|
||||
transition: transform 140ms ease, opacity 140ms ease, background 140ms ease;
|
||||
}
|
||||
|
||||
button:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
button.done {
|
||||
background: var(--ok);
|
||||
}
|
||||
|
||||
.feedback {
|
||||
margin: 0;
|
||||
font-size: 0.88rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.feedback.ok {
|
||||
color: var(--ok);
|
||||
}
|
||||
|
||||
.feedback.err {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.preview {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
max-width: min(720px, calc(100vw - 1.5rem));
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.preview::backdrop {
|
||||
background: rgba(18, 28, 22, 0.45);
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
.preview-sheet {
|
||||
display: grid;
|
||||
gap: 0.85rem;
|
||||
margin: 0;
|
||||
padding: 1.2rem 1.25rem 1.25rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 22px;
|
||||
background: #fffcf6;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.preview-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.preview-kicker {
|
||||
margin: 0;
|
||||
font-size: 0.78rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.preview-top h2 {
|
||||
margin: 0.2rem 0 0;
|
||||
font-family: var(--display);
|
||||
font-size: clamp(1.35rem, 3vw, 1.75rem);
|
||||
font-weight: 400;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.preview-meta {
|
||||
display: grid;
|
||||
gap: 0.25rem;
|
||||
padding: 0.75rem 0.85rem;
|
||||
border-radius: 14px;
|
||||
background: rgba(20, 32, 26, 0.04);
|
||||
color: var(--muted);
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.preview-meta p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.preview-meta span {
|
||||
display: inline-block;
|
||||
min-width: 5.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.preview-sheet input[type="text"],
|
||||
.preview-sheet input[type="email"],
|
||||
.preview-sheet textarea {
|
||||
width: 100%;
|
||||
padding: 0.7rem 0.8rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
color: var(--ink);
|
||||
font: inherit;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.preview-sheet textarea {
|
||||
line-height: 1.5;
|
||||
min-height: 220px;
|
||||
}
|
||||
|
||||
.preview-sheet input:focus,
|
||||
.preview-sheet textarea:focus {
|
||||
outline: 2px solid rgba(15, 107, 76, 0.35);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.preview-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.preview-actions-right {
|
||||
display: flex;
|
||||
gap: 0.55rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
button.ghost {
|
||||
background: transparent;
|
||||
color: var(--ink);
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
|
||||
button.ghost:hover:not(:disabled) {
|
||||
background: rgba(20, 32, 26, 0.04);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.actions {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.preview-actions,
|
||||
.preview-actions-right {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.preview-actions-right button {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user