/* ui.jsx — мелкие примитивы и хелперы. Экспортируются в window. * Категории теперь динамические — берутся из window.Store (см. store.jsx). * Здесь только catFrom() как безопасный геттер на случай раннего рендера. */ const catFrom = (k) => (window.catOf ? window.catOf(k) : { key: k || "other", label: "Прочее", color: "gray" }); const colorClassFrom = (c) => (window.colorClass ? window.colorClass(c) : "cat-c-gray"); /* ----------------------------------------------------------- даты (рус.) */ const MONTHS = ["янв.", "февр.", "марта", "апр.", "мая", "июня", "июля", "авг.", "сент.", "окт.", "нояб.", "дек."]; const MONTHS_FULL = ["января","февраля","марта","апреля","мая","июня","июля","августа","сентября","октября","ноября","декабря"]; function startOfDay(d) { const x = new Date(d); x.setHours(0,0,0,0); return x; } function dayDiff(a, b) { return Math.round((startOfDay(a) - startOfDay(b)) / 86400e3); } function hhmm(d) { return new Date(d).toLocaleTimeString("ru-RU", { hour: "2-digit", minute: "2-digit" }); } // «сегодня 14:30», «вчера», «3 июня» function formatCreated(iso) { const d = new Date(iso); const diff = dayDiff(new Date(), d); if (diff === 0) return "сегодня " + hhmm(d); if (diff === 1) return "вчера"; if (diff > 1 && diff < 7) return diff + " дн. назад"; return d.getDate() + " " + MONTHS[d.getMonth()] + (d.getFullYear() !== new Date().getFullYear() ? " " + d.getFullYear() : ""); } // для напоминаний: «сегодня 10:00», «завтра 09:00», «просрочено», «через 3 дня» function formatRemind(iso) { const d = new Date(iso); const diff = dayDiff(d, new Date()); const past = d < new Date(); let when; if (diff === 0) when = "сегодня " + hhmm(d); else if (diff === 1) when = "завтра " + hhmm(d); else if (diff === -1) when = "вчера " + hhmm(d); else when = d.getDate() + " " + MONTHS_FULL[d.getMonth()] + ", " + hhmm(d); let tone = "future"; if (past) tone = "overdue"; else if (diff === 0) tone = "today"; else if (diff === 1) tone = "tomorrow"; return { text: when, tone }; } // значение для function toLocalInput(iso) { if (!iso) return ""; const d = new Date(iso); const pad = (n) => String(n).padStart(2, "0"); return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`; } function fromLocalInput(v) { return v ? new Date(v).toISOString() : null; } /* ----------------------------------------------------------- иконки (24px stroke) */ function Icon({ name, size = 20, className = "", strokeWidth = 1.75 }) { const p = { notes: <>, search: <>, bell: <>, plus: <>, close: <>, trash: <>, sun: <>, moon: <>, telegram: <>, web: <>, chevron: <>, logout: <>, sparkles: <>, tag: <>, check: <>, edit: <>, menu: <>, calendar: <>, arrowRight: <>, inbox: <>, lock: <>, key: <>, eye: <>, eyeOff: <>, copy: <>, settings: <>, paperclip: <>, image: <>, video: <>, link: <>, external: <>, refresh: <>, star: <>, grid: <>, rows: <>, mic: <>, }[name]; return ( ); } /* ----------------------------------------------------------- бейдж категории */ function CategoryBadge({ category, size = "md" }) { const c = catFrom(category); const pad = size === "sm" ? "px-2 py-0.5 text-[11px]" : "px-2.5 py-1 text-xs"; return ( {c.label} ); } /* ----------------------------------------------------------- копирование */ function pluralRu(n, one, few, many) { const m10 = n % 10, m100 = n % 100; if (m10 === 1 && m100 !== 11) return one; if (m10 >= 2 && m10 <= 4 && (m100 < 10 || m100 >= 20)) return few; return many; } function copyToClipboard(text) { if (navigator.clipboard && navigator.clipboard.writeText) return navigator.clipboard.writeText(text); return new Promise((res) => { const ta = document.createElement("textarea"); ta.value = text; ta.style.position = "fixed"; ta.style.opacity = "0"; document.body.appendChild(ta); ta.select(); try { document.execCommand("copy"); } catch (e) {} document.body.removeChild(ta); res(); }); } /* ----------------------------------------------------------- генератор паролей */ function generatePassword({ length = 16, upper = true, digits = true, symbols = true } = {}) { const lo = "abcdefghijkmnpqrstuvwxyz"; const up = "ABCDEFGHJKLMNPQRSTUVWXYZ"; const di = "23456789"; const sy = "!@#$%^&*-_=+?"; let pool = lo; const must = [pick(lo)]; if (upper) { pool += up; must.push(pick(up)); } if (digits) { pool += di; must.push(pick(di)); } if (symbols) { pool += sy; must.push(pick(sy)); } const out = [...must]; for (let i = out.length; i < length; i++) out.push(pick(pool)); // перемешать (Fisher–Yates с crypto) for (let i = out.length - 1; i > 0; i--) { const j = rnd(i + 1); [out[i], out[j]] = [out[j], out[i]]; } return out.join(""); function pick(s) { return s[rnd(s.length)]; } function rnd(n) { if (window.crypto && window.crypto.getRandomValues) { const a = new Uint32Array(1); window.crypto.getRandomValues(a); return a[0] % n; } return Math.floor(Math.random() * n); } } // сила пароля → {score 0..4, label, tone} function passwordStrength(pw) { if (!pw) return { score: 0, label: "—", tone: "gray" }; let s = 0; if (pw.length >= 8) s++; if (pw.length >= 14) s++; if (/[a-z]/.test(pw) && /[A-Z]/.test(pw)) s++; if (/\d/.test(pw)) s++; if (/[^A-Za-z0-9]/.test(pw)) s++; const score = Math.min(4, s); const map = [ { label: "Слабый", tone: "red" }, { label: "Слабый", tone: "red" }, { label: "Средний", tone: "amber" }, { label: "Хороший", tone: "teal" }, { label: "Надёжный", tone: "green" }, ]; return { score, ...map[score] }; } /* ----------------------------------------------------------- пилюля тега */ function TagPill({ children, onRemove }) { return ( #{children} {onRemove && ( )} ); } /* ----------------------------------------------------------- источник */ function SourceMark({ source }) { const settings = window.Store ? Store.useSettings() : { showSource: true }; if (settings.showSource === false) return null; const tg = source === "telegram"; return ( ); } /* ----------------------------------------------------------- кнопки */ function Button({ children, variant = "primary", size = "md", className = "", ...rest }) { const sizes = { sm: "h-8 px-3 text-[13px]", md: "h-10 px-4 text-sm", lg: "h-11 px-5 text-sm" }; const variants = { primary: "btn-primary", ghost: "btn-ghost", soft: "btn-soft", danger: "btn-danger", }; return ( ); } /* ----------------------------------------------------------- спиннер / скелетон */ function Spinner({ size = 18 }) { return ; } function SkeletonCard() { return (
); } Object.assign(window, { catFrom, colorClassFrom, formatCreated, formatRemind, toLocalInput, fromLocalInput, hhmm, MONTHS_FULL, Icon, CategoryBadge, TagPill, SourceMark, Button, Spinner, SkeletonCard, copyToClipboard, generatePassword, passwordStrength, pluralRu, });