refactor: update class names for consistency and improve styling
Build & Push Docker Image / build (push) Successful in 4m21s
Build & Push Docker Image / build (push) Successful in 4m21s
- Changed `flex-shrink-0` to `shrink-0` in multiple components for better consistency. - Updated button and checkbox classes to use `rounded-sm` for a more uniform appearance. - Adjusted backdrop blur classes for modals to `backdrop-blur-xs` for a subtler effect. - Removed unused Tailwind CSS configuration file.
This commit is contained in:
Generated
+1372
-1199
File diff suppressed because it is too large
Load Diff
+13
-13
@@ -20,33 +20,33 @@
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.3.1",
|
||||
"exceljs": "^4.4.0",
|
||||
"express": "^4.21.0",
|
||||
"express-rate-limit": "^7.5.1",
|
||||
"express": "^5.2.1",
|
||||
"express-rate-limit": "^8.5.2",
|
||||
"flatpickr": "^4.6.13",
|
||||
"ioredis": "^5.10.0",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"lucide-react": "^0.460.0",
|
||||
"lucide-react": "^1.16.0",
|
||||
"multer": "^2.1.0",
|
||||
"nodemailer": "^8.0.1",
|
||||
"otpauth": "^9.5.0",
|
||||
"pdfkit": "^0.17.2",
|
||||
"pdfkit": "^0.18.0",
|
||||
"pg": "^8.18.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"rate-limit-redis": "^4.3.1",
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0",
|
||||
"rate-limit-redis": "^5.0.0",
|
||||
"react": "^19.2.6",
|
||||
"react-dom": "^19.2.6",
|
||||
"react-hot-toast": "^2.4.0",
|
||||
"react-router-dom": "^6.28.0",
|
||||
"uuid": "^13.0.0",
|
||||
"react-router-dom": "^7.15.1",
|
||||
"uuid": "^14.0.0",
|
||||
"xml2js": "^0.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.0",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@tailwindcss/postcss": "^4.3.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"autoprefixer": "^10.4.0",
|
||||
"postcss": "^8.4.0",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"tailwindcss": "^4.3.0",
|
||||
"vite": "^8.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -1,6 +1,5 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
};
|
||||
|
||||
+1
-1
@@ -100,7 +100,7 @@ async function start() {
|
||||
// Serve static files in production
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
app.use(express.static(path.join(__dirname, '..', 'dist')));
|
||||
app.get('*', (req, res) => {
|
||||
app.get('/*splat', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, '..', 'dist', 'index.html'));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -278,7 +278,7 @@ ${propXml}
|
||||
}
|
||||
|
||||
// ── OPTIONS ────────────────────────────────────────────────────────────────
|
||||
router.options('*', (req, res) => {
|
||||
router.options('/*splat', (req, res) => {
|
||||
setDAVHeaders(res);
|
||||
res.status(200).end();
|
||||
});
|
||||
@@ -529,7 +529,7 @@ router.delete('/:username/calendar/:filename', caldavAuth, validateCalDAVUser, a
|
||||
});
|
||||
|
||||
// ── Fallback ───────────────────────────────────────────────────────────────
|
||||
router.all('*', caldavAuth, (req, res) => {
|
||||
router.all('/*splat', caldavAuth, (req, res) => {
|
||||
setDAVHeaders(res);
|
||||
res.status(405).end();
|
||||
});
|
||||
|
||||
@@ -152,7 +152,7 @@ export default function AnalyticsList({ analytics, onRefresh, isOwner = true })
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => toggleExportMenu(entry.id)}
|
||||
|
||||
@@ -2,9 +2,9 @@ import { Video } from 'lucide-react';
|
||||
import { useBranding } from '../contexts/BrandingContext';
|
||||
|
||||
const sizes = {
|
||||
sm: { box: 'w-8 h-8', h: 'h-8', maxW: 'max-w-[8rem]', rounded: 'rounded-lg', icon: 16, text: 'text-lg' },
|
||||
md: { box: 'w-9 h-9', h: 'h-12', maxW: 'max-w-[10rem]', rounded: 'rounded-lg', icon: 20, text: 'text-xl' },
|
||||
lg: { box: 'w-10 h-10', h: 'h-10', maxW: 'max-w-[12rem]', rounded: 'rounded-xl', icon: 22, text: 'text-2xl' },
|
||||
sm: { box: 'w-8 h-8', h: 'h-8', maxW: 'max-w-32', rounded: 'rounded-lg', icon: 16, text: 'text-lg' },
|
||||
md: { box: 'w-9 h-9', h: 'h-12', maxW: 'max-w-40', rounded: 'rounded-lg', icon: 20, text: 'text-xl' },
|
||||
lg: { box: 'w-10 h-10', h: 'h-10', maxW: 'max-w-48', rounded: 'rounded-xl', icon: 22, text: 'text-2xl' },
|
||||
};
|
||||
|
||||
export default function BrandLogo({ size = 'md', className = '' }) {
|
||||
|
||||
@@ -90,7 +90,7 @@ export default function DateTimePicker({
|
||||
</label>
|
||||
)}
|
||||
<div className="relative">
|
||||
<Icon size={15} className="absolute left-3 top-1/2 -translate-y-1/2 text-th-text-s pointer-events-none z-[1]" />
|
||||
<Icon size={15} className="absolute left-3 top-1/2 -translate-y-1/2 text-th-text-s pointer-events-none z-1" />
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
|
||||
@@ -35,17 +35,17 @@ export default function FederatedRoomCard({ room, onRemove }) {
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<Globe size={14} className="text-th-accent flex-shrink-0" />
|
||||
<Globe size={14} className="text-th-accent shrink-0" />
|
||||
<h3 className="text-base font-semibold text-th-text truncate group-hover:text-th-accent transition-colors">
|
||||
{room.room_name}
|
||||
</h3>
|
||||
{isDeleted ? (
|
||||
<span className="flex-shrink-0 px-2 py-0.5 bg-red-500/15 text-red-500 rounded-full text-xs font-medium flex items-center gap-1">
|
||||
<span className="shrink-0 px-2 py-0.5 bg-red-500/15 text-red-500 rounded-full text-xs font-medium flex items-center gap-1">
|
||||
<AlertTriangle size={10} />
|
||||
{t('federation.roomDeleted')}
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex-shrink-0 px-2 py-0.5 bg-th-accent/15 text-th-accent rounded-full text-xs font-medium">
|
||||
<span className="shrink-0 px-2 py-0.5 bg-th-accent/15 text-th-accent rounded-full text-xs font-medium">
|
||||
{t('federation.federated')}
|
||||
</span>
|
||||
)}
|
||||
@@ -60,12 +60,12 @@ export default function FederatedRoomCard({ room, onRemove }) {
|
||||
<div className="grid grid-cols-2 gap-2 mb-4">
|
||||
{room.meet_id && (
|
||||
<div className="flex items-center gap-1.5 text-xs text-th-text-s">
|
||||
<Hash size={12} className="text-th-accent flex-shrink-0" />
|
||||
<Hash size={12} className="text-th-accent shrink-0" />
|
||||
<span className="truncate font-mono" title={room.meet_id}>{room.meet_id.slice(0, 10)}…</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-1.5 text-xs text-th-text-s">
|
||||
<Users size={12} className="text-th-accent flex-shrink-0" />
|
||||
<Users size={12} className="text-th-accent shrink-0" />
|
||||
<span>
|
||||
{t('federation.maxParticipants')}:{' '}
|
||||
<span className="text-th-text font-medium">
|
||||
@@ -76,12 +76,12 @@ export default function FederatedRoomCard({ room, onRemove }) {
|
||||
<div className="flex items-center gap-1.5 text-xs col-span-2">
|
||||
{recordingOn ? (
|
||||
<>
|
||||
<Video size={12} className="text-amber-500 flex-shrink-0" />
|
||||
<Video size={12} className="text-amber-500 shrink-0" />
|
||||
<span className="text-amber-500 font-medium">{t('federation.recordingOn')}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<VideoOff size={12} className="text-th-text-s flex-shrink-0" />
|
||||
<VideoOff size={12} className="text-th-text-s shrink-0" />
|
||||
<span className="text-th-text-s">{t('federation.recordingOff')}</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -52,7 +52,7 @@ export default function Layout() {
|
||||
{/* Email verification banner */}
|
||||
{user && user.email_verified === 0 && (
|
||||
<div className="bg-amber-500/15 border-b border-amber-500/30 px-4 py-2.5 flex items-center justify-center gap-3 text-sm">
|
||||
<AlertTriangle size={15} className="text-amber-400 flex-shrink-0" />
|
||||
<AlertTriangle size={15} className="text-amber-400 shrink-0" />
|
||||
<span className="text-amber-200">{t('auth.emailVerificationBanner')}</span>
|
||||
<button
|
||||
onClick={handleResendVerification}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { X } from 'lucide-react';
|
||||
export default function Modal({ title, children, onClose, maxWidth = 'max-w-lg' }) {
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm" onClick={onClose} />
|
||||
<div className="fixed inset-0 bg-black/60 backdrop-blur-xs" onClick={onClose} />
|
||||
<div className={`relative bg-th-card rounded-2xl border border-th-border shadow-2xl w-full ${maxWidth}`}>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-th-border rounded-t-2xl">
|
||||
|
||||
@@ -38,7 +38,7 @@ export default function Navbar({ onMenuClick }) {
|
||||
: '?';
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-20 bg-th-nav border-b border-th-border backdrop-blur-sm">
|
||||
<header className="sticky top-0 z-20 bg-th-nav border-b border-th-border backdrop-blur-xs">
|
||||
<div className="flex items-center justify-between h-16 px-4 md:px-6">
|
||||
{/* Left section */}
|
||||
<div className="flex items-center gap-3">
|
||||
|
||||
@@ -148,7 +148,7 @@ export default function NotificationBell() {
|
||||
${n.read ? 'hover:bg-th-hover' : 'bg-th-accent/5 hover:bg-th-accent/10'}`}
|
||||
>
|
||||
{/* Icon */}
|
||||
<span className="text-lg flex-shrink-0 mt-0.5">{notificationIcon(n.type)}</span>
|
||||
<span className="text-lg shrink-0 mt-0.5">{notificationIcon(n.type)}</span>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 min-w-0">
|
||||
@@ -164,7 +164,7 @@ export default function NotificationBell() {
|
||||
</div>
|
||||
|
||||
{/* Right side: unread dot, link icon, delete button */}
|
||||
<div className="flex flex-col items-end gap-1 flex-shrink-0">
|
||||
<div className="flex flex-col items-end gap-1 shrink-0">
|
||||
{!n.read && (
|
||||
<span className="w-2 h-2 rounded-full bg-th-accent mt-1" />
|
||||
)}
|
||||
@@ -173,7 +173,7 @@ export default function NotificationBell() {
|
||||
)}
|
||||
<button
|
||||
onClick={(e) => handleDelete(e, n.id)}
|
||||
className="opacity-0 group-hover:opacity-100 p-0.5 rounded hover:text-th-error transition-all text-th-text-s/50"
|
||||
className="opacity-0 group-hover:opacity-100 p-0.5 rounded-sm hover:text-th-error transition-all text-th-text-s/50"
|
||||
title={t('notifications.delete')}
|
||||
>
|
||||
<X size={13} />
|
||||
|
||||
@@ -133,7 +133,7 @@ export default function RecordingList({ recordings, onRefresh }) {
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<button
|
||||
onClick={() => handlePublish(rec.recordID, !rec.published)}
|
||||
disabled={loading[rec.recordID] === 'publishing'}
|
||||
|
||||
@@ -80,7 +80,7 @@ export default function RoomCard({ room, onDelete }) {
|
||||
<span>Max: {room.max_participants}</span>
|
||||
)}
|
||||
{room.access_code && (
|
||||
<span className="px-1.5 py-0.5 bg-th-warning/15 text-th-warning rounded text-[10px] font-medium">
|
||||
<span className="px-1.5 py-0.5 bg-th-warning/15 text-th-warning rounded-sm text-[10px] font-medium">
|
||||
{t('common.protected')}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -43,7 +43,7 @@ export default function Sidebar({ open, onClose }) {
|
||||
|
||||
const linkClasses = ({ isActive }) =>
|
||||
`flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200 ${isActive
|
||||
? 'bg-th-accent text-th-accent-t shadow-sm'
|
||||
? 'bg-th-accent text-th-accent-t shadow-xs'
|
||||
: 'text-th-text-s hover:text-th-text hover:bg-th-hover'
|
||||
}`;
|
||||
|
||||
@@ -106,7 +106,7 @@ export default function Sidebar({ open, onClose }) {
|
||||
<div className="p-4 border-t border-th-border">
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className="w-9 h-9 rounded-full flex items-center justify-center text-white text-sm font-bold flex-shrink-0 overflow-hidden"
|
||||
className="w-9 h-9 rounded-full flex items-center justify-center text-white text-sm font-bold shrink-0 overflow-hidden"
|
||||
style={{ backgroundColor: user?.avatar_color || '#6366f1' }}
|
||||
>
|
||||
{user?.avatar_image ? (
|
||||
|
||||
@@ -10,7 +10,7 @@ export default function ThemeSelector({ onClose }) {
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm" onClick={onClose} />
|
||||
<div className="fixed inset-0 bg-black/60 backdrop-blur-xs" onClick={onClose} />
|
||||
|
||||
<div className="relative bg-th-card rounded-2xl border border-th-border shadow-2xl w-full max-w-2xl max-h-[80vh] overflow-hidden">
|
||||
{/* Header */}
|
||||
|
||||
+134
-84
@@ -1,6 +1,126 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import 'tailwindcss';
|
||||
|
||||
@theme {
|
||||
--color-th-bg: var(--bg-primary);
|
||||
--color-th-bg-s: var(--bg-secondary);
|
||||
--color-th-bg-t: var(--bg-tertiary);
|
||||
--color-th-text: var(--text-primary);
|
||||
--color-th-text-s: var(--text-secondary);
|
||||
--color-th-accent: var(--accent);
|
||||
--color-th-accent-h: var(--accent-hover);
|
||||
--color-th-accent-t: var(--accent-text);
|
||||
--color-th-border: var(--border);
|
||||
--color-th-card: var(--card-bg);
|
||||
--color-th-input: var(--input-bg);
|
||||
--color-th-input-b: var(--input-border);
|
||||
--color-th-nav: var(--nav-bg);
|
||||
--color-th-side: var(--sidebar-bg);
|
||||
--color-th-hover: var(--hover-bg);
|
||||
--color-th-success: var(--success);
|
||||
--color-th-warning: var(--warning);
|
||||
--color-th-error: var(--error);
|
||||
--color-th-ring: var(--ring);
|
||||
|
||||
--font-sans: Inter, system-ui, -apple-system, sans-serif;
|
||||
|
||||
--shadow-th:
|
||||
0 1px 3px 0 var(--shadow-color), 0 1px 2px -1px var(--shadow-color);
|
||||
--shadow-th-lg:
|
||||
0 10px 15px -3px var(--shadow-color), 0 4px 6px -4px var(--shadow-color);
|
||||
}
|
||||
|
||||
/*
|
||||
The default border color has changed to `currentcolor` in Tailwind CSS v4,
|
||||
so we've added these compatibility styles to make sure everything still
|
||||
looks the same as it did with Tailwind CSS v3.
|
||||
|
||||
If we ever want to remove these styles, we need to add an explicit border
|
||||
color utility to any element that depends on these defaults.
|
||||
*/
|
||||
@layer base {
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
border-color: var(--color-gray-200, currentcolor);
|
||||
}
|
||||
}
|
||||
|
||||
@utility btn-primary {
|
||||
@apply inline-flex items-center justify-center gap-2 px-4 py-2.5 rounded-lg font-medium
|
||||
bg-th-accent text-th-accent-t hover:bg-th-accent-h
|
||||
transition-all duration-200 ease-out
|
||||
focus:outline-hidden focus:ring-2 focus:ring-th-ring focus:ring-offset-2
|
||||
disabled:opacity-50 disabled:cursor-not-allowed;
|
||||
--tw-ring-offset-color: var(--bg-primary);
|
||||
}
|
||||
|
||||
@utility btn-secondary {
|
||||
@apply inline-flex items-center justify-center gap-2 px-4 py-2.5 rounded-lg font-medium
|
||||
bg-th-bg-s text-th-text border border-th-border
|
||||
hover:bg-th-hover transition-all duration-200 ease-out
|
||||
focus:outline-hidden focus:ring-2 focus:ring-th-ring focus:ring-offset-2
|
||||
disabled:opacity-50 disabled:cursor-not-allowed;
|
||||
--tw-ring-offset-color: var(--bg-primary);
|
||||
}
|
||||
|
||||
@utility btn-danger {
|
||||
@apply inline-flex items-center justify-center gap-2 px-4 py-2.5 rounded-lg font-medium
|
||||
bg-th-error text-white hover:opacity-90
|
||||
transition-all duration-200 ease-out
|
||||
focus:outline-hidden focus:ring-2 focus:ring-th-error focus:ring-offset-2
|
||||
disabled:opacity-50 disabled:cursor-not-allowed;
|
||||
--tw-ring-offset-color: var(--bg-primary);
|
||||
}
|
||||
|
||||
@utility btn-ghost {
|
||||
@apply inline-flex items-center justify-center gap-2 px-4 py-2.5 rounded-lg font-medium
|
||||
text-th-text-s hover:bg-th-hover hover:text-th-text
|
||||
transition-all duration-200 ease-out
|
||||
focus:outline-hidden focus:ring-2 focus:ring-th-ring focus:ring-offset-2
|
||||
disabled:opacity-50 disabled:cursor-not-allowed;
|
||||
--tw-ring-offset-color: var(--bg-primary);
|
||||
}
|
||||
|
||||
@utility input-field {
|
||||
@apply w-full px-4 py-2.5 rounded-lg
|
||||
bg-th-input text-th-text placeholder-th-text-s
|
||||
border border-th-input-b
|
||||
focus:outline-hidden focus:ring-2 focus:ring-th-ring focus:border-transparent
|
||||
transition-all duration-200;
|
||||
}
|
||||
|
||||
@utility card {
|
||||
@apply bg-th-card rounded-xl border border-th-border
|
||||
shadow-th transition-all duration-200;
|
||||
}
|
||||
|
||||
@utility card-hover {
|
||||
@apply card hover:shadow-th-lg cursor-pointer;
|
||||
&:hover {
|
||||
border-color: color-mix(in srgb, var(--accent) 30%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
@utility gradient-text {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--gradient-start),
|
||||
var(--gradient-end)
|
||||
);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
@utility gradient-bg {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--gradient-start),
|
||||
var(--gradient-end)
|
||||
);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
/* ===== DEFAULT LIGHT ===== */
|
||||
@@ -444,8 +564,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
/* ===== SCRUNKLY.CAT DARK ===== */
|
||||
[data-theme="scrunkly-cat"] {
|
||||
[data-theme='scrunkly-cat'] {
|
||||
color-scheme: dark;
|
||||
--picker-icon-filter: invert(0.8);
|
||||
--bg-primary: #161924;
|
||||
@@ -472,8 +593,8 @@
|
||||
--gradient-end: #d6336a;
|
||||
}
|
||||
|
||||
/* ===== RED MODULAR LIGHT ===== */
|
||||
[data-theme="red-modular-light"] {
|
||||
/* ===== RED MODULAR LIGHT ===== */
|
||||
[data-theme='red-modular-light'] {
|
||||
color-scheme: light;
|
||||
--picker-icon-filter: none;
|
||||
--bg-primary: #ffffff;
|
||||
@@ -501,7 +622,7 @@
|
||||
}
|
||||
|
||||
/* ===== EVERFOREST DARK ===== */
|
||||
[data-theme="everforest-dark"] {
|
||||
[data-theme='everforest-dark'] {
|
||||
color-scheme: dark;
|
||||
--picker-icon-filter: invert(0.8);
|
||||
--bg-primary: #2d353b;
|
||||
@@ -529,7 +650,7 @@
|
||||
}
|
||||
|
||||
/* ===== EVERFOREST LIGHT ===== */
|
||||
[data-theme="everforest-light"] {
|
||||
[data-theme='everforest-light'] {
|
||||
color-scheme: light;
|
||||
--picker-icon-filter: none;
|
||||
--bg-primary: #fdf6e3;
|
||||
@@ -557,7 +678,7 @@
|
||||
}
|
||||
|
||||
/* ===== KANAGAWA ===== */
|
||||
[data-theme="kanagawa"] {
|
||||
[data-theme='kanagawa'] {
|
||||
color-scheme: dark;
|
||||
--picker-icon-filter: invert(0.8);
|
||||
--bg-primary: #1f1f28;
|
||||
@@ -585,7 +706,7 @@
|
||||
}
|
||||
|
||||
/* ===== AYU DARK ===== */
|
||||
[data-theme="ayu-dark"] {
|
||||
[data-theme='ayu-dark'] {
|
||||
color-scheme: dark;
|
||||
--picker-icon-filter: invert(0.8);
|
||||
--bg-primary: #0d1017;
|
||||
@@ -613,7 +734,7 @@
|
||||
}
|
||||
|
||||
/* ===== MOONLIGHT ===== */
|
||||
[data-theme="moonlight"] {
|
||||
[data-theme='moonlight'] {
|
||||
color-scheme: dark;
|
||||
--picker-icon-filter: invert(0.8);
|
||||
--bg-primary: #212337;
|
||||
@@ -641,7 +762,7 @@
|
||||
}
|
||||
|
||||
/* ===== CYBERPUNK ===== */
|
||||
[data-theme="cyberpunk"] {
|
||||
[data-theme='cyberpunk'] {
|
||||
color-scheme: dark;
|
||||
--picker-icon-filter: invert(0.8);
|
||||
--bg-primary: #0a0a0f;
|
||||
@@ -669,7 +790,7 @@
|
||||
}
|
||||
|
||||
/* ===== COTTON CANDY LIGHT ===== */
|
||||
[data-theme="cotton-candy-light"] {
|
||||
[data-theme='cotton-candy-light'] {
|
||||
color-scheme: light;
|
||||
--picker-icon-filter: none;
|
||||
--bg-primary: #fff5f9;
|
||||
@@ -695,77 +816,6 @@
|
||||
--gradient-start: #ff85a2;
|
||||
--gradient-end: #c084fc;
|
||||
}
|
||||
|
||||
|
||||
@layer components {
|
||||
.btn-primary {
|
||||
@apply inline-flex items-center justify-center gap-2 px-4 py-2.5 rounded-lg font-medium
|
||||
bg-th-accent text-th-accent-t hover:bg-th-accent-h
|
||||
transition-all duration-200 ease-out
|
||||
focus:outline-none focus:ring-2 focus:ring-th-ring focus:ring-offset-2
|
||||
disabled:opacity-50 disabled:cursor-not-allowed;
|
||||
--tw-ring-offset-color: var(--bg-primary);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@apply inline-flex items-center justify-center gap-2 px-4 py-2.5 rounded-lg font-medium
|
||||
bg-th-bg-s text-th-text border border-th-border
|
||||
hover:bg-th-hover transition-all duration-200 ease-out
|
||||
focus:outline-none focus:ring-2 focus:ring-th-ring focus:ring-offset-2
|
||||
disabled:opacity-50 disabled:cursor-not-allowed;
|
||||
--tw-ring-offset-color: var(--bg-primary);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
@apply inline-flex items-center justify-center gap-2 px-4 py-2.5 rounded-lg font-medium
|
||||
bg-th-error text-white hover:opacity-90
|
||||
transition-all duration-200 ease-out
|
||||
focus:outline-none focus:ring-2 focus:ring-th-error focus:ring-offset-2
|
||||
disabled:opacity-50 disabled:cursor-not-allowed;
|
||||
--tw-ring-offset-color: var(--bg-primary);
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
@apply inline-flex items-center justify-center gap-2 px-4 py-2.5 rounded-lg font-medium
|
||||
text-th-text-s hover:bg-th-hover hover:text-th-text
|
||||
transition-all duration-200 ease-out
|
||||
focus:outline-none focus:ring-2 focus:ring-th-ring focus:ring-offset-2
|
||||
disabled:opacity-50 disabled:cursor-not-allowed;
|
||||
--tw-ring-offset-color: var(--bg-primary);
|
||||
}
|
||||
|
||||
.input-field {
|
||||
@apply w-full px-4 py-2.5 rounded-lg
|
||||
bg-th-input text-th-text placeholder-th-text-s
|
||||
border border-th-input-b
|
||||
focus:outline-none focus:ring-2 focus:ring-th-ring focus:border-transparent
|
||||
transition-all duration-200;
|
||||
}
|
||||
|
||||
.card {
|
||||
@apply bg-th-card rounded-xl border border-th-border
|
||||
shadow-th transition-all duration-200;
|
||||
}
|
||||
|
||||
.card-hover {
|
||||
@apply card hover:shadow-th-lg cursor-pointer;
|
||||
}
|
||||
.card-hover:hover {
|
||||
border-color: color-mix(in srgb, var(--accent) 30%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.gradient-text {
|
||||
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.gradient-bg {
|
||||
background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end));
|
||||
}
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
|
||||
+11
-11
@@ -502,7 +502,7 @@ export default function Admin() {
|
||||
type="button"
|
||||
disabled={savingHideAppName}
|
||||
onClick={() => handleHideAppNameToggle(!hideAppName)}
|
||||
className={`relative inline-flex h-5 w-9 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-th-ring focus:ring-offset-1 disabled:opacity-50 ml-4 ${
|
||||
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-hidden focus:ring-2 focus:ring-th-ring focus:ring-offset-1 disabled:opacity-50 ml-4 ${
|
||||
hideAppName ? 'bg-th-accent' : 'bg-th-border'
|
||||
}`}
|
||||
aria-checked={hideAppName}
|
||||
@@ -539,7 +539,7 @@ export default function Admin() {
|
||||
<button
|
||||
onClick={handleDefaultThemeSave}
|
||||
disabled={savingDefaultTheme || editDefaultTheme === (defaultTheme || 'dark')}
|
||||
className="btn-primary text-sm px-4 flex-shrink-0"
|
||||
className="btn-primary text-sm px-4 shrink-0"
|
||||
>
|
||||
{savingDefaultTheme ? <Loader2 size={14} className="animate-spin" /> : t('common.save')}
|
||||
</button>
|
||||
@@ -569,7 +569,7 @@ export default function Admin() {
|
||||
<button
|
||||
onClick={handleImprintUrlSave}
|
||||
disabled={savingImprintUrl || editImprintUrl === (imprintUrl || '')}
|
||||
className="btn-primary text-sm px-4 flex-shrink-0"
|
||||
className="btn-primary text-sm px-4 shrink-0"
|
||||
>
|
||||
{savingImprintUrl ? <Loader2 size={14} className="animate-spin" /> : t('common.save')}
|
||||
</button>
|
||||
@@ -590,7 +590,7 @@ export default function Admin() {
|
||||
<button
|
||||
onClick={handlePrivacyUrlSave}
|
||||
disabled={savingPrivacyUrl || editPrivacyUrl === (privacyUrl || '')}
|
||||
className="btn-primary text-sm px-4 flex-shrink-0"
|
||||
className="btn-primary text-sm px-4 shrink-0"
|
||||
>
|
||||
{savingPrivacyUrl ? <Loader2 size={14} className="animate-spin" /> : t('common.save')}
|
||||
</button>
|
||||
@@ -660,7 +660,7 @@ export default function Admin() {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={sendingInvite || !inviteEmail.trim()}
|
||||
className="btn-primary text-sm px-4 flex-shrink-0"
|
||||
className="btn-primary text-sm px-4 shrink-0"
|
||||
>
|
||||
{sendingInvite ? <Loader2 size={14} className="animate-spin" /> : <Send size={14} />}
|
||||
{t('admin.sendInvite')}
|
||||
@@ -676,7 +676,7 @@ export default function Admin() {
|
||||
return (
|
||||
<div key={inv.id} className="flex items-center justify-between gap-3 p-3 rounded-xl bg-th-bg border border-th-border">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 ${
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center shrink-0 ${
|
||||
isUsed ? 'bg-green-500/15 text-green-400' : isExpired ? 'bg-red-500/15 text-red-400' : 'bg-th-accent/15 text-th-accent'
|
||||
}`}>
|
||||
{isUsed ? <Check size={14} /> : isExpired ? <XIcon size={14} /> : <Clock size={14} />}
|
||||
@@ -693,7 +693,7 @@ export default function Admin() {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
{!isUsed && !isExpired && (
|
||||
<button
|
||||
onClick={() => handleCopyInviteLink(inv.token)}
|
||||
@@ -981,7 +981,7 @@ export default function Admin() {
|
||||
<td className="px-5 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className="w-9 h-9 rounded-full flex items-center justify-center text-white text-sm font-bold flex-shrink-0 overflow-hidden"
|
||||
className="w-9 h-9 rounded-full flex items-center justify-center text-white text-sm font-bold shrink-0 overflow-hidden"
|
||||
style={{ backgroundColor: u.avatar_color || '#6366f1' }}
|
||||
>
|
||||
{u.avatar_image ? (
|
||||
@@ -1099,7 +1099,7 @@ export default function Admin() {
|
||||
{/* Reset password modal */}
|
||||
{resetPwModal && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm" onClick={() => setResetPwModal(null)} />
|
||||
<div className="fixed inset-0 bg-black/60 backdrop-blur-xs" onClick={() => setResetPwModal(null)} />
|
||||
<div className="relative bg-th-card rounded-2xl border border-th-border shadow-2xl w-full max-w-sm p-6">
|
||||
<h3 className="text-lg font-semibold text-th-text mb-4">{t('admin.resetPasswordTitle')}</h3>
|
||||
<form onSubmit={handleResetPassword}>
|
||||
@@ -1131,7 +1131,7 @@ export default function Admin() {
|
||||
{/* Create user modal */}
|
||||
{showCreateUser && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm" onClick={() => setShowCreateUser(false)} />
|
||||
<div className="fixed inset-0 bg-black/60 backdrop-blur-xs" onClick={() => setShowCreateUser(false)} />
|
||||
<div className="relative bg-th-card rounded-2xl border border-th-border shadow-2xl w-full max-w-md p-6">
|
||||
<h3 className="text-lg font-semibold text-th-text mb-4">{t('admin.createUserTitle')}</h3>
|
||||
<form onSubmit={handleCreateUser} className="space-y-4">
|
||||
@@ -1219,7 +1219,7 @@ export default function Admin() {
|
||||
{/* All rooms modal */}
|
||||
{showAllRoomsModal && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm" onClick={() => setShowAllRoomsModal(false)} />
|
||||
<div className="fixed inset-0 bg-black/60 backdrop-blur-xs" onClick={() => setShowAllRoomsModal(false)} />
|
||||
<div className="relative bg-th-card rounded-2xl border border-th-border shadow-2xl w-full max-w-4xl max-h-[85vh] flex flex-col">
|
||||
<div className="flex items-center justify-between p-6 border-b border-th-border">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -454,7 +454,7 @@ export default function Calendar() {
|
||||
<div
|
||||
key={ev.id}
|
||||
onClick={(e) => { e.stopPropagation(); setShowDetail(ev); }}
|
||||
className="text-[10px] leading-tight px-1.5 py-0.5 rounded truncate text-white font-medium cursor-pointer hover:opacity-80 transition-opacity"
|
||||
className="text-[10px] leading-tight px-1.5 py-0.5 rounded-sm truncate text-white font-medium cursor-pointer hover:opacity-80 transition-opacity"
|
||||
style={{ backgroundColor: ev.color || '#6366f1' }}
|
||||
title={ev.title}
|
||||
>
|
||||
@@ -492,11 +492,11 @@ export default function Calendar() {
|
||||
<div
|
||||
key={ev.id}
|
||||
onClick={(e) => { e.stopPropagation(); setShowDetail(ev); }}
|
||||
className="text-xs px-2 py-1.5 rounded text-white font-medium cursor-pointer hover:opacity-80 transition-opacity"
|
||||
className="text-xs px-2 py-1.5 rounded-sm text-white font-medium cursor-pointer hover:opacity-80 transition-opacity"
|
||||
style={{ backgroundColor: ev.color || '#6366f1' }}
|
||||
>
|
||||
<div className="flex items-center gap-1 truncate">
|
||||
{ev.reminder_minutes && <Bell size={9} className="flex-shrink-0 opacity-70" />}
|
||||
{ev.reminder_minutes && <Bell size={9} className="shrink-0 opacity-70" />}
|
||||
<span className="truncate">{ev.title}</span>
|
||||
</div>
|
||||
<div className="opacity-80 text-[10px]">{formatTime(ev.start_time)} - {formatTime(ev.end_time)}</div>
|
||||
@@ -555,7 +555,7 @@ export default function Calendar() {
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 -mt-2 text-xs text-th-text-s">
|
||||
<Globe size={12} className="flex-shrink-0" />
|
||||
<Globe size={12} className="shrink-0" />
|
||||
<span>{getLocalTimezone()}</span>
|
||||
</div>
|
||||
|
||||
@@ -750,7 +750,7 @@ export default function Calendar() {
|
||||
className="w-full flex items-center gap-3 px-4 py-2.5 hover:bg-th-hover transition-colors text-left"
|
||||
>
|
||||
<div
|
||||
className="w-8 h-8 rounded-full flex items-center justify-center text-white text-xs font-bold flex-shrink-0"
|
||||
className="w-8 h-8 rounded-full flex items-center justify-center text-white text-xs font-bold shrink-0"
|
||||
style={{ backgroundColor: u.avatar_color || '#6366f1' }}
|
||||
>
|
||||
{(u.display_name || u.name).split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)}
|
||||
@@ -772,7 +772,7 @@ export default function Calendar() {
|
||||
<div key={u.user_id} className="flex items-center justify-between gap-3 p-3 bg-th-bg-s rounded-lg border border-th-border border-dashed">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<div
|
||||
className="w-8 h-8 rounded-full flex items-center justify-center text-white text-xs font-bold flex-shrink-0"
|
||||
className="w-8 h-8 rounded-full flex items-center justify-center text-white text-xs font-bold shrink-0"
|
||||
style={{ backgroundColor: u.avatar_color || '#6366f1' }}
|
||||
>
|
||||
{(u.display_name || u.name).split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)}
|
||||
@@ -799,7 +799,7 @@ export default function Calendar() {
|
||||
<div key={u.id} className="flex items-center justify-between gap-3 p-3 bg-th-bg-s rounded-lg border border-th-border">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<div
|
||||
className="w-8 h-8 rounded-full flex items-center justify-center text-white text-xs font-bold flex-shrink-0"
|
||||
className="w-8 h-8 rounded-full flex items-center justify-center text-white text-xs font-bold shrink-0"
|
||||
style={{ backgroundColor: u.avatar_color || '#6366f1' }}
|
||||
>
|
||||
{(u.display_name || u.name).split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)}
|
||||
|
||||
@@ -250,7 +250,7 @@ export default function Dashboard() {
|
||||
type="checkbox"
|
||||
checked={newRoom.mute_on_join}
|
||||
onChange={e => setNewRoom({ ...newRoom, mute_on_join: e.target.checked })}
|
||||
className="w-4 h-4 rounded border-th-border text-th-accent focus:ring-th-ring"
|
||||
className="w-4 h-4 rounded-sm border-th-border text-th-accent focus:ring-th-ring"
|
||||
/>
|
||||
<span className="text-sm text-th-text">{t('dashboard.muteOnJoin')}</span>
|
||||
</label>
|
||||
@@ -259,7 +259,7 @@ export default function Dashboard() {
|
||||
type="checkbox"
|
||||
checked={newRoom.record_meeting}
|
||||
onChange={e => setNewRoom({ ...newRoom, record_meeting: e.target.checked })}
|
||||
className="w-4 h-4 rounded border-th-border text-th-accent focus:ring-th-ring"
|
||||
className="w-4 h-4 rounded-sm border-th-border text-th-accent focus:ring-th-ring"
|
||||
/>
|
||||
<span className="text-sm text-th-text">{t('dashboard.allowRecording')}</span>
|
||||
</label>
|
||||
|
||||
@@ -93,7 +93,7 @@ export default function FederatedRoomDetail() {
|
||||
{isDeleted && (
|
||||
<div className="card p-4 mb-4 border-red-500/30 bg-red-500/10">
|
||||
<div className="flex items-center gap-3">
|
||||
<AlertTriangle size={20} className="text-red-500 flex-shrink-0" />
|
||||
<AlertTriangle size={20} className="text-red-500 shrink-0" />
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-red-500">{t('federation.roomDeleted')}</p>
|
||||
<p className="text-xs text-th-text-s mt-0.5">{t('federation.roomDeletedNotice')}</p>
|
||||
@@ -106,7 +106,7 @@ export default function FederatedRoomDetail() {
|
||||
<div className="card p-6 mb-4">
|
||||
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
|
||||
<div className="flex items-start gap-3 min-w-0">
|
||||
<div className={`w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0 mt-0.5 ${isDeleted ? 'bg-red-500/15' : 'bg-th-accent/15'}`}>
|
||||
<div className={`w-10 h-10 rounded-lg flex items-center justify-center shrink-0 mt-0.5 ${isDeleted ? 'bg-red-500/15' : 'bg-th-accent/15'}`}>
|
||||
{isDeleted ? <AlertTriangle size={20} className="text-red-500" /> : <Globe size={20} className="text-th-accent" />}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
@@ -128,7 +128,7 @@ export default function FederatedRoomDetail() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
{!isDeleted && (
|
||||
<button
|
||||
onClick={handleJoin}
|
||||
@@ -189,7 +189,7 @@ export default function FederatedRoomDetail() {
|
||||
|
||||
{room.meet_id && (
|
||||
<div className="flex items-start gap-3">
|
||||
<Hash size={16} className="text-th-accent flex-shrink-0 mt-0.5" />
|
||||
<Hash size={16} className="text-th-accent shrink-0 mt-0.5" />
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs text-th-text-s mb-0.5">{t('federation.meetingId')}</p>
|
||||
<p className="text-sm font-mono text-th-text break-all">{room.meet_id}</p>
|
||||
@@ -198,7 +198,7 @@ export default function FederatedRoomDetail() {
|
||||
)}
|
||||
|
||||
<div className="flex items-start gap-3">
|
||||
<Link2 size={16} className="text-th-accent flex-shrink-0 mt-0.5" />
|
||||
<Link2 size={16} className="text-th-accent shrink-0 mt-0.5" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-xs text-th-text-s mb-0.5">{t('federation.joinUrl')}</p>
|
||||
<p className="text-sm font-mono text-th-text break-all opacity-60 select-all">{room.join_url}</p>
|
||||
|
||||
@@ -167,7 +167,7 @@ export default function FederationInbox() {
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Mail size={16} className="text-th-accent flex-shrink-0" />
|
||||
<Mail size={16} className="text-th-accent shrink-0" />
|
||||
<h3 className="text-base font-semibold text-th-text truncate">{inv.room_name}</h3>
|
||||
</div>
|
||||
<p className="text-sm text-th-text-s">
|
||||
@@ -180,7 +180,7 @@ export default function FederationInbox() {
|
||||
{new Date(inv.created_at).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<button onClick={() => handleAccept(inv.id)} className="btn-primary text-sm">
|
||||
<Check size={16} />
|
||||
{t('federation.accept')}
|
||||
@@ -200,7 +200,7 @@ export default function FederationInbox() {
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Calendar size={16} className="text-th-success flex-shrink-0" />
|
||||
<Calendar size={16} className="text-th-success shrink-0" />
|
||||
<span className="text-xs font-semibold uppercase tracking-wide text-th-success mr-1">
|
||||
{t('federation.calendarEvent')}
|
||||
</span>
|
||||
@@ -219,7 +219,7 @@ export default function FederationInbox() {
|
||||
{new Date(inv.created_at).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<button onClick={() => handleCalAccept(inv.id)} className="btn-primary text-sm">
|
||||
<Check size={16} />
|
||||
{t('federation.accept')}
|
||||
@@ -239,7 +239,7 @@ export default function FederationInbox() {
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Calendar size={16} className="text-th-accent flex-shrink-0" />
|
||||
<Calendar size={16} className="text-th-accent shrink-0" />
|
||||
<span className="text-xs font-semibold uppercase tracking-wide text-th-accent mr-1">
|
||||
{t('federation.localCalendarEvent')}
|
||||
</span>
|
||||
@@ -258,7 +258,7 @@ export default function FederationInbox() {
|
||||
{new Date(inv.created_at).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<button onClick={() => handleLocalCalAccept(inv.id)} className="btn-primary text-sm">
|
||||
<Check size={16} />
|
||||
{t('federation.accept')}
|
||||
@@ -287,13 +287,13 @@ export default function FederationInbox() {
|
||||
<div key={`room-past-${inv.id}`} className="card p-4 opacity-70">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="min-w-0 flex items-center gap-2">
|
||||
<Mail size={14} className="text-th-text-s flex-shrink-0" />
|
||||
<Mail size={14} className="text-th-text-s shrink-0" />
|
||||
<div className="min-w-0">
|
||||
<h3 className="text-sm font-medium text-th-text truncate">{inv.room_name}</h3>
|
||||
<p className="text-xs text-th-text-s">{inv.from_user}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<span className={`text-xs font-medium px-2 py-1 rounded-full ${inv.status === 'accepted'
|
||||
? 'bg-th-success/15 text-th-success'
|
||||
: 'bg-th-error/15 text-th-error'
|
||||
@@ -326,13 +326,13 @@ export default function FederationInbox() {
|
||||
<div key={`cal-past-${inv.id}`} className="card p-4 opacity-70">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="min-w-0 flex items-center gap-2">
|
||||
<Calendar size={14} className="text-th-text-s flex-shrink-0" />
|
||||
<Calendar size={14} className="text-th-text-s shrink-0" />
|
||||
<div className="min-w-0">
|
||||
<h3 className="text-sm font-medium text-th-text truncate">{inv.title}</h3>
|
||||
<p className="text-xs text-th-text-s">{inv.from_user} · {new Date(inv.start_time).toLocaleDateString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<span className={`text-xs font-medium px-2 py-1 rounded-full ${inv.status === 'accepted'
|
||||
? 'bg-th-success/15 text-th-success'
|
||||
: 'bg-th-error/15 text-th-error'
|
||||
@@ -365,13 +365,13 @@ export default function FederationInbox() {
|
||||
<div key={`localcal-past-${inv.id}`} className="card p-4 opacity-70">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="min-w-0 flex items-center gap-2">
|
||||
<Calendar size={14} className="text-th-text-s flex-shrink-0" />
|
||||
<Calendar size={14} className="text-th-text-s shrink-0" />
|
||||
<div className="min-w-0">
|
||||
<h3 className="text-sm font-medium text-th-text truncate">{inv.title}</h3>
|
||||
<p className="text-xs text-th-text-s">{inv.from_name} · {new Date(inv.start_time).toLocaleDateString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<span className={`text-xs font-medium px-2 py-1 rounded-full ${inv.status === 'accepted'
|
||||
? 'bg-th-success/15 text-th-success'
|
||||
: 'bg-th-error/15 text-th-error'
|
||||
|
||||
@@ -283,7 +283,7 @@ export default function GuestJoin() {
|
||||
{roomInfo.allow_recording && (
|
||||
<div className="rounded-xl border border-amber-500/30 bg-amber-500/10 p-4 space-y-3">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertCircle size={16} className="text-amber-500 flex-shrink-0 mt-0.5" />
|
||||
<AlertCircle size={16} className="text-amber-500 shrink-0 mt-0.5" />
|
||||
<p className="text-sm text-amber-400">{t('room.guestRecordingNotice')}</p>
|
||||
</div>
|
||||
<label className="flex items-center gap-2.5 cursor-pointer">
|
||||
@@ -291,7 +291,7 @@ export default function GuestJoin() {
|
||||
type="checkbox"
|
||||
checked={recordingConsent}
|
||||
onChange={e => setRecordingConsent(e.target.checked)}
|
||||
className="w-4 h-4 rounded accent-amber-500 cursor-pointer"
|
||||
className="w-4 h-4 rounded-sm accent-amber-500 cursor-pointer"
|
||||
/>
|
||||
<span className="text-sm text-th-text">{t('room.guestRecordingConsent')}</span>
|
||||
</label>
|
||||
|
||||
+1
-1
@@ -257,7 +257,7 @@ export default function Login() {
|
||||
{needsVerification && (
|
||||
<div className="mt-4 p-4 rounded-xl bg-amber-500/10 border border-amber-500/30 space-y-2">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertTriangle size={16} className="text-amber-400 flex-shrink-0 mt-0.5" />
|
||||
<AlertTriangle size={16} className="text-amber-400 shrink-0 mt-0.5" />
|
||||
<p className="text-sm text-amber-200">{t('auth.emailVerificationBanner')}</p>
|
||||
</div>
|
||||
<button
|
||||
|
||||
+12
-12
@@ -511,7 +511,7 @@ export default function RoomDetail() {
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-th-text-s">{t('room.meetingId')}</span>
|
||||
<code className="bg-th-bg-s px-2 py-0.5 rounded text-xs text-th-text font-mono">{room.uid}</code>
|
||||
<code className="bg-th-bg-s px-2 py-0.5 rounded-sm text-xs text-th-text font-mono">{room.uid}</code>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-th-text-s">{t('room.status')}</span>
|
||||
@@ -630,7 +630,7 @@ export default function RoomDetail() {
|
||||
type="checkbox"
|
||||
checked={!!editRoom.mute_on_join}
|
||||
onChange={e => setEditRoom({ ...editRoom, mute_on_join: e.target.checked })}
|
||||
className="w-4 h-4 rounded border-th-border text-th-accent focus:ring-th-ring"
|
||||
className="w-4 h-4 rounded-sm border-th-border text-th-accent focus:ring-th-ring"
|
||||
/>
|
||||
<span className="text-sm text-th-text">{t('room.muteOnJoin')}</span>
|
||||
</label>
|
||||
@@ -639,7 +639,7 @@ export default function RoomDetail() {
|
||||
type="checkbox"
|
||||
checked={!!editRoom.require_approval}
|
||||
onChange={e => setEditRoom({ ...editRoom, require_approval: e.target.checked })}
|
||||
className="w-4 h-4 rounded border-th-border text-th-accent focus:ring-th-ring"
|
||||
className="w-4 h-4 rounded-sm border-th-border text-th-accent focus:ring-th-ring"
|
||||
/>
|
||||
<span className="text-sm text-th-text">{t('room.requireApproval')}</span>
|
||||
</label>
|
||||
@@ -648,7 +648,7 @@ export default function RoomDetail() {
|
||||
type="checkbox"
|
||||
checked={!!editRoom.anyone_can_start}
|
||||
onChange={e => setEditRoom({ ...editRoom, anyone_can_start: e.target.checked })}
|
||||
className="w-4 h-4 rounded border-th-border text-th-accent focus:ring-th-ring"
|
||||
className="w-4 h-4 rounded-sm border-th-border text-th-accent focus:ring-th-ring"
|
||||
/>
|
||||
<span className="text-sm text-th-text">{t('room.anyoneCanStart')}</span>
|
||||
</label>
|
||||
@@ -657,7 +657,7 @@ export default function RoomDetail() {
|
||||
type="checkbox"
|
||||
checked={!!editRoom.all_join_moderator}
|
||||
onChange={e => setEditRoom({ ...editRoom, all_join_moderator: e.target.checked })}
|
||||
className="w-4 h-4 rounded border-th-border text-th-accent focus:ring-th-ring"
|
||||
className="w-4 h-4 rounded-sm border-th-border text-th-accent focus:ring-th-ring"
|
||||
/>
|
||||
<span className="text-sm text-th-text">{t('room.allJoinModerator')}</span>
|
||||
</label>
|
||||
@@ -666,7 +666,7 @@ export default function RoomDetail() {
|
||||
type="checkbox"
|
||||
checked={!!editRoom.record_meeting}
|
||||
onChange={e => setEditRoom({ ...editRoom, record_meeting: e.target.checked })}
|
||||
className="w-4 h-4 rounded border-th-border text-th-accent focus:ring-th-ring"
|
||||
className="w-4 h-4 rounded-sm border-th-border text-th-accent focus:ring-th-ring"
|
||||
/>
|
||||
<span className="text-sm text-th-text">{t('room.allowRecording')}</span>
|
||||
</label>
|
||||
@@ -675,7 +675,7 @@ export default function RoomDetail() {
|
||||
type="checkbox"
|
||||
checked={!!editRoom.learning_analytics}
|
||||
onChange={e => setEditRoom({ ...editRoom, learning_analytics: e.target.checked })}
|
||||
className="w-4 h-4 rounded border-th-border text-th-accent focus:ring-th-ring"
|
||||
className="w-4 h-4 rounded-sm border-th-border text-th-accent focus:ring-th-ring"
|
||||
/>
|
||||
<span className="text-sm text-th-text">{t('room.enableAnalytics')}</span>
|
||||
</label>
|
||||
@@ -743,7 +743,7 @@ export default function RoomDetail() {
|
||||
{room.presentation_file ? (
|
||||
<div className="flex items-center justify-between gap-3 p-3 bg-th-bg-s rounded-lg border border-th-border">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<FileText size={16} className="text-th-accent flex-shrink-0" />
|
||||
<FileText size={16} className="text-th-accent shrink-0" />
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs text-th-text-s">{t('room.presentationCurrent')}</p>
|
||||
<p className="text-sm text-th-text font-medium truncate">
|
||||
@@ -755,7 +755,7 @@ export default function RoomDetail() {
|
||||
type="button"
|
||||
onClick={handlePresentationRemove}
|
||||
disabled={removingPresentation}
|
||||
className="btn-ghost text-th-error hover:bg-th-error/10 flex-shrink-0 text-xs py-1.5 px-3"
|
||||
className="btn-ghost text-th-error hover:bg-th-error/10 shrink-0 text-xs py-1.5 px-3"
|
||||
>
|
||||
{removingPresentation ? <Loader2 size={14} className="animate-spin" /> : <Trash2 size={14} />}
|
||||
{t('room.presentationRemove')}
|
||||
@@ -814,7 +814,7 @@ export default function RoomDetail() {
|
||||
className="w-full flex items-center gap-3 px-4 py-2.5 hover:bg-th-hover transition-colors text-left"
|
||||
>
|
||||
<div
|
||||
className="w-8 h-8 rounded-full flex items-center justify-center text-white text-xs font-bold flex-shrink-0 overflow-hidden"
|
||||
className="w-8 h-8 rounded-full flex items-center justify-center text-white text-xs font-bold shrink-0 overflow-hidden"
|
||||
style={{ backgroundColor: u.avatar_color || '#6366f1' }}
|
||||
>
|
||||
{u.avatar_image ? (
|
||||
@@ -840,7 +840,7 @@ export default function RoomDetail() {
|
||||
<div key={u.id} className="flex items-center justify-between gap-3 p-3 bg-th-bg-s rounded-lg border border-th-border">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<div
|
||||
className="w-8 h-8 rounded-full flex items-center justify-center text-white text-xs font-bold flex-shrink-0 overflow-hidden"
|
||||
className="w-8 h-8 rounded-full flex items-center justify-center text-white text-xs font-bold shrink-0 overflow-hidden"
|
||||
style={{ backgroundColor: u.avatar_color || '#6366f1' }}
|
||||
>
|
||||
{u.avatar_image ? (
|
||||
@@ -857,7 +857,7 @@ export default function RoomDetail() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleUnshare(u.id)}
|
||||
className="p-1.5 rounded-lg hover:bg-th-hover text-th-text-s hover:text-th-error transition-colors flex-shrink-0"
|
||||
className="p-1.5 rounded-lg hover:bg-th-hover text-th-text-s hover:text-th-error transition-colors shrink-0"
|
||||
title={t('room.shareRemove')}
|
||||
>
|
||||
<X size={16} />
|
||||
|
||||
+10
-10
@@ -267,7 +267,7 @@ export default function Settings() {
|
||||
|
||||
<div className="flex flex-col md:flex-row gap-6">
|
||||
{/* Section nav */}
|
||||
<div className="md:w-56 flex-shrink-0">
|
||||
<div className="md:w-56 shrink-0">
|
||||
<nav className="flex md:flex-col gap-1">
|
||||
{sections.map(s => (
|
||||
<button
|
||||
@@ -495,7 +495,7 @@ export default function Settings() {
|
||||
/* 2FA is enabled */
|
||||
<div>
|
||||
<div className="flex items-center gap-3 p-4 rounded-xl bg-emerald-500/10 border border-emerald-500/30 mb-5">
|
||||
<ShieldCheck size={22} className="text-emerald-400 flex-shrink-0" />
|
||||
<ShieldCheck size={22} className="text-emerald-400 shrink-0" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-emerald-300">{t('settings.security.statusEnabled')}</p>
|
||||
<p className="text-xs text-emerald-400/70">{t('settings.security.statusEnabledDesc')}</p>
|
||||
@@ -569,7 +569,7 @@ export default function Settings() {
|
||||
</code>
|
||||
<button
|
||||
onClick={() => { navigator.clipboard.writeText(twoFaSetupData.secret); toast.success(t('room.linkCopied')); }}
|
||||
className="btn-ghost py-1.5 px-2 flex-shrink-0"
|
||||
className="btn-ghost py-1.5 px-2 shrink-0"
|
||||
>
|
||||
<Copy size={14} />
|
||||
</button>
|
||||
@@ -605,7 +605,7 @@ export default function Settings() {
|
||||
/* 2FA is disabled — show enable button */
|
||||
<div>
|
||||
<div className="flex items-center gap-3 p-4 rounded-xl bg-th-bg-t border border-th-border mb-5">
|
||||
<ShieldOff size={22} className="text-th-text-s flex-shrink-0" />
|
||||
<ShieldOff size={22} className="text-th-text-s shrink-0" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-th-text">{t('settings.security.statusDisabled')}</p>
|
||||
<p className="text-xs text-th-text-s">{t('settings.security.statusDisabledDesc')}</p>
|
||||
@@ -667,7 +667,7 @@ export default function Settings() {
|
||||
>
|
||||
{/* Color preview */}
|
||||
<div
|
||||
className="w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0 border"
|
||||
className="w-10 h-10 rounded-lg flex items-center justify-center shrink-0 border"
|
||||
style={{ backgroundColor: th.colors.bg, borderColor: th.colors.accent + '40' }}
|
||||
>
|
||||
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: th.colors.accent }} />
|
||||
@@ -699,7 +699,7 @@ export default function Settings() {
|
||||
</code>
|
||||
<button
|
||||
onClick={() => { navigator.clipboard.writeText(`${window.location.origin}/caldav/`); toast.success(t('room.linkCopied')); }}
|
||||
className="btn-ghost py-1.5 px-2 flex-shrink-0"
|
||||
className="btn-ghost py-1.5 px-2 shrink-0"
|
||||
>
|
||||
<Copy size={14} />
|
||||
</button>
|
||||
@@ -713,7 +713,7 @@ export default function Settings() {
|
||||
</code>
|
||||
<button
|
||||
onClick={() => { navigator.clipboard.writeText(user?.email || ''); toast.success(t('room.linkCopied')); }}
|
||||
className="btn-ghost py-1.5 px-2 flex-shrink-0"
|
||||
className="btn-ghost py-1.5 px-2 shrink-0"
|
||||
>
|
||||
<Copy size={14} />
|
||||
</button>
|
||||
@@ -732,12 +732,12 @@ export default function Settings() {
|
||||
<code className="flex-1 text-xs bg-th-bg-t px-3 py-2 rounded-lg font-mono text-th-text break-all">
|
||||
{tokenVisible ? newlyCreatedToken : '•'.repeat(48)}
|
||||
</code>
|
||||
<button onClick={() => setTokenVisible(v => !v)} className="btn-ghost py-1.5 px-2 flex-shrink-0">
|
||||
<button onClick={() => setTokenVisible(v => !v)} className="btn-ghost py-1.5 px-2 shrink-0">
|
||||
{tokenVisible ? <EyeOff size={14} /> : <Eye size={14} />}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { navigator.clipboard.writeText(newlyCreatedToken); toast.success(t('room.linkCopied')); }}
|
||||
className="btn-ghost py-1.5 px-2 flex-shrink-0"
|
||||
className="btn-ghost py-1.5 px-2 shrink-0"
|
||||
>
|
||||
<Copy size={14} />
|
||||
</button>
|
||||
@@ -790,7 +790,7 @@ export default function Settings() {
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleRevokeToken(tk.id)}
|
||||
className="btn-ghost py-1 px-2 text-th-error hover:text-th-error flex-shrink-0"
|
||||
className="btn-ghost py-1 px-2 text-th-error hover:text-th-error shrink-0"
|
||||
title={t('settings.caldav.revoke')}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
th: {
|
||||
bg: 'var(--bg-primary)',
|
||||
'bg-s': 'var(--bg-secondary)',
|
||||
'bg-t': 'var(--bg-tertiary)',
|
||||
text: 'var(--text-primary)',
|
||||
'text-s': 'var(--text-secondary)',
|
||||
accent: 'var(--accent)',
|
||||
'accent-h': 'var(--accent-hover)',
|
||||
'accent-t': 'var(--accent-text)',
|
||||
border: 'var(--border)',
|
||||
card: 'var(--card-bg)',
|
||||
input: 'var(--input-bg)',
|
||||
'input-b': 'var(--input-border)',
|
||||
nav: 'var(--nav-bg)',
|
||||
side: 'var(--sidebar-bg)',
|
||||
hover: 'var(--hover-bg)',
|
||||
success: 'var(--success)',
|
||||
warning: 'var(--warning)',
|
||||
error: 'var(--error)',
|
||||
ring: 'var(--ring)',
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'system-ui', '-apple-system', 'sans-serif'],
|
||||
},
|
||||
boxShadow: {
|
||||
'th': '0 1px 3px 0 var(--shadow-color), 0 1px 2px -1px var(--shadow-color)',
|
||||
'th-lg': '0 10px 15px -3px var(--shadow-color), 0 4px 6px -4px var(--shadow-color)',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
Reference in New Issue
Block a user