diff --git a/index.html b/index.html index e6fe822..689270a 100644 --- a/index.html +++ b/index.html @@ -5,6 +5,20 @@ CommDesk + + diff --git a/src/App.css b/src/App.css index b8822c2..44fe5c5 100644 --- a/src/App.css +++ b/src/App.css @@ -7,6 +7,14 @@ @custom-variant dark (&:is(.dark *)); @plugin "@tailwindcss/typography"; +/* ─── Prevent flash on load ─────────────────────────────────────────────── */ +html { + color-scheme: light; +} +html.dark { + color-scheme: dark; +} + html, body { margin: 0; @@ -14,6 +22,7 @@ body { width: 100%; height: 100%; overflow-x: hidden; + font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } #root { @@ -21,6 +30,7 @@ body { height: 100%; } +/* ─── Tailwind radius tokens ─────────────────────────────────────────────── */ @theme inline { --radius-sm: calc(var(--radius) - 4px); --radius-md: calc(var(--radius) - 2px); @@ -60,119 +70,435 @@ body { --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); --color-sidebar-border: var(--sidebar-border); --color-sidebar-ring: var(--sidebar-ring); + + /* CommDesk design tokens */ + --color-cd-bg: var(--cd-bg); + --color-cd-surface: var(--cd-surface); + --color-cd-surface-2: var(--cd-surface-2); + --color-cd-surface-3: var(--cd-surface-3); + --color-cd-border: var(--cd-border); + --color-cd-border-subtle: var(--cd-border-subtle); + --color-cd-text: var(--cd-text); + --color-cd-text-2: var(--cd-text-2); + --color-cd-text-muted: var(--cd-text-muted); + --color-cd-primary: var(--cd-primary); + --color-cd-primary-hover: var(--cd-primary-hover); + --color-cd-primary-subtle: var(--cd-primary-subtle); + --color-cd-primary-text: var(--cd-primary-text); + --color-cd-secondary: var(--cd-secondary); + --color-cd-accent: var(--cd-accent); + --color-cd-success: var(--cd-success); + --color-cd-success-subtle: var(--cd-success-subtle); + --color-cd-warning: var(--cd-warning); + --color-cd-warning-subtle: var(--cd-warning-subtle); + --color-cd-danger: var(--cd-danger); + --color-cd-danger-subtle: var(--cd-danger-subtle); + --color-cd-hover: var(--cd-hover); } +/* ─── Light theme (default) ──────────────────────────────────────────────── */ :root { --radius: 0.625rem; - --background: oklch(1 0 0); - --foreground: oklch(0.13 0.028 261.692); - --card: oklch(1 0 0); - --card-foreground: oklch(0.13 0.028 261.692); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.13 0.028 261.692); - --primary: oklch(0.21 0.034 264.665); - --primary-foreground: oklch(0.985 0.002 247.839); - --secondary: oklch(0.967 0.003 264.542); - --secondary-foreground: oklch(0.21 0.034 264.665); - --muted: oklch(0.967 0.003 264.542); - --muted-foreground: oklch(0.551 0.027 264.364); - --accent: oklch(0.967 0.003 264.542); - --accent-foreground: oklch(0.21 0.034 264.665); - --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.928 0.006 264.531); - --input: oklch(0.928 0.006 264.531); - --ring: oklch(0.707 0.022 261.325); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); - --sidebar: oklch(0.985 0.002 247.839); - --sidebar-foreground: oklch(0.13 0.028 261.692); - --sidebar-primary: oklch(0.21 0.034 264.665); - --sidebar-primary-foreground: oklch(0.985 0.002 247.839); - --sidebar-accent: oklch(0.967 0.003 264.542); - --sidebar-accent-foreground: oklch(0.21 0.034 264.665); - --sidebar-border: oklch(0.928 0.006 264.531); - --sidebar-ring: oklch(0.707 0.022 261.325); + + /* Shadcn tokens */ + --background: #f8fafc; + --foreground: #1e293b; + --card: #ffffff; + --card-foreground: #1e293b; + --popover: #ffffff; + --popover-foreground: #1e293b; + --primary: #2563eb; + --primary-foreground: #ffffff; + --secondary: #f1f5f9; + --secondary-foreground: #1e293b; + --muted: #f1f5f9; + --muted-foreground: #64748b; + --accent: #f1f5f9; + --accent-foreground: #1e293b; + --destructive: #dc2626; + --border: #e2e8f0; + --input: #e2e8f0; + --ring: #3b82f6; + --chart-1: #3b82f6; + --chart-2: #22c55e; + --chart-3: #a855f7; + --chart-4: #f59e0b; + --chart-5: #ef4444; + --sidebar: #ffffff; + --sidebar-foreground: #1e293b; + --sidebar-primary: #2563eb; + --sidebar-primary-foreground: #ffffff; + --sidebar-accent: #f1f5f9; + --sidebar-accent-foreground: #1e293b; + --sidebar-border: #e2e8f0; + --sidebar-ring: #3b82f6; + + /* CommDesk design tokens – light */ + --cd-bg: #f8fafc; + --cd-surface: #ffffff; + --cd-surface-2: #f1f5f9; + --cd-surface-3: #eef2f7; + --cd-border: #e2e8f0; + --cd-border-subtle: #f1f5f9; + --cd-text: #1e293b; + --cd-text-2: #64748b; + --cd-text-muted: #94a3b8; + --cd-primary: #2563eb; + --cd-primary-hover: #1d4ed8; + --cd-primary-subtle: #eff6ff; + --cd-primary-text: #1d4ed8; + --cd-secondary: #9333ea; + --cd-accent: #06b6d4; + --cd-success: #16a34a; + --cd-success-subtle: #f0fdf4; + --cd-warning: #ca8a04; + --cd-warning-subtle: #fefce8; + --cd-danger: #dc2626; + --cd-danger-subtle: #fef2f2; + --cd-hover: #f1f5f9; + --cd-shadow: rgba(15, 23, 42, 0.08); + --cd-shadow-md: rgba(15, 23, 42, 0.12); } +/* ─── Dark theme ─────────────────────────────────────────────────────────── */ .dark { - --background: oklch(0.13 0.028 261.692); - --foreground: oklch(0.985 0.002 247.839); - --card: oklch(0.21 0.034 264.665); - --card-foreground: oklch(0.985 0.002 247.839); - --popover: oklch(0.21 0.034 264.665); - --popover-foreground: oklch(0.985 0.002 247.839); - --primary: oklch(0.928 0.006 264.531); - --primary-foreground: oklch(0.21 0.034 264.665); - --secondary: oklch(0.278 0.033 256.848); - --secondary-foreground: oklch(0.985 0.002 247.839); - --muted: oklch(0.278 0.033 256.848); - --muted-foreground: oklch(0.707 0.022 261.325); - --accent: oklch(0.278 0.033 256.848); - --accent-foreground: oklch(0.985 0.002 247.839); - --destructive: oklch(0.704 0.191 22.216); - --border: oklch(1 0 0 / 10%); - --input: oklch(1 0 0 / 15%); - --ring: oklch(0.551 0.027 264.364); - --chart-1: oklch(0.488 0.243 264.376); - --chart-2: oklch(0.696 0.17 162.48); - --chart-3: oklch(0.769 0.188 70.08); - --chart-4: oklch(0.627 0.265 303.9); - --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.21 0.034 264.665); - --sidebar-foreground: oklch(0.985 0.002 247.839); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0.002 247.839); - --sidebar-accent: oklch(0.278 0.033 256.848); - --sidebar-accent-foreground: oklch(0.985 0.002 247.839); - --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.551 0.027 264.364); + /* Shadcn tokens */ + --background: #0f1320; + --foreground: #f8fafc; + --card: #1a1f2e; + --card-foreground: #f8fafc; + --popover: #1a1f2e; + --popover-foreground: #f8fafc; + --primary: #3b82f6; + --primary-foreground: #ffffff; + --secondary: #1e293b; + --secondary-foreground: #f8fafc; + --muted: #1e293b; + --muted-foreground: #94a3b8; + --accent: #1e293b; + --accent-foreground: #f8fafc; + --destructive: #f87171; + --border: rgba(255, 255, 255, 0.08); + --input: rgba(255, 255, 255, 0.08); + --ring: #3b82f6; + --chart-1: #60a5fa; + --chart-2: #4ade80; + --chart-3: #c084fc; + --chart-4: #facc15; + --chart-5: #f87171; + --sidebar: #1a1f2e; + --sidebar-foreground: #f8fafc; + --sidebar-primary: #3b82f6; + --sidebar-primary-foreground: #ffffff; + --sidebar-accent: #1e293b; + --sidebar-accent-foreground: #f8fafc; + --sidebar-border: rgba(255, 255, 255, 0.08); + --sidebar-ring: #3b82f6; + + /* CommDesk design tokens – dark */ + --cd-bg: #0f1320; + --cd-surface: #1a1f2e; + --cd-surface-2: #151929; + --cd-surface-3: #1e293b; + --cd-border: rgba(255, 255, 255, 0.08); + --cd-border-subtle: rgba(255, 255, 255, 0.04); + --cd-text: #f8fafc; + --cd-text-2: #94a3b8; + --cd-text-muted: #64748b; + --cd-primary: #3b82f6; + --cd-primary-hover: #60a5fa; + --cd-primary-subtle: rgba(59, 130, 246, 0.12); + --cd-primary-text: #93c5fd; + --cd-secondary: #a855f7; + --cd-accent: #22d3ee; + --cd-success: #4ade80; + --cd-success-subtle: rgba(74, 222, 128, 0.10); + --cd-warning: #facc15; + --cd-warning-subtle: rgba(250, 204, 21, 0.10); + --cd-danger: #f87171; + --cd-danger-subtle: rgba(248, 113, 113, 0.10); + --cd-hover: rgba(255, 255, 255, 0.05); + --cd-shadow: rgba(0, 0, 0, 0.3); + --cd-shadow-md: rgba(0, 0, 0, 0.4); } +/* ─── Base layer ─────────────────────────────────────────────────────────── */ @layer base { * { @apply border-border outline-ring/50; + transition: background-color 0.2s ease, border-color 0.2s ease, color 0.15s ease; } body { @apply bg-background text-foreground; } } -.inter { - font-family: "Inter", sans-serif; - font-optical-sizing: auto; +/* ─── CommDesk utility classes ───────────────────────────────────────────── */ +@layer components { + /* Cards */ + .cd-card { + background-color: var(--cd-surface); + border: 1px solid var(--cd-border); + border-radius: 1rem; + padding: 1.25rem; + box-shadow: 0 1px 3px var(--cd-shadow); + transition: box-shadow 0.2s ease, transform 0.2s ease; + } + .cd-card:hover { + box-shadow: 0 4px 12px var(--cd-shadow-md); + } + .cd-card-hover { + @apply hover:-translate-y-[2px]; + } - font-style: normal; -} + /* Section title */ + .cd-section-title { + font-size: 1rem; + font-weight: 600; + color: var(--cd-text); + margin-bottom: 1rem; + } -@layer components { - .card { - @apply bg-white/80 backdrop-blur-xl p-4 sm:p-5 rounded-2xl border border-gray-100 shadow-sm; + /* Muted text */ + .cd-text-muted { + font-size: 0.875rem; + color: var(--cd-text-2); } - .card-hover { - @apply hover:shadow-md hover:-translate-y-[2px] transition-all duration-200; + /* Badges */ + .cd-badge { + display: inline-flex; + align-items: center; + padding: 0.2rem 0.65rem; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 500; + line-height: 1.4; + } + .cd-badge-success { + background-color: var(--cd-success-subtle); + color: var(--cd-success); + } + .cd-badge-warning { + background-color: var(--cd-warning-subtle); + color: var(--cd-warning); + } + .cd-badge-danger { + background-color: var(--cd-danger-subtle); + color: var(--cd-danger); + } + .cd-badge-primary { + background-color: var(--cd-primary-subtle); + color: var(--cd-primary-text); + } + .cd-badge-neutral { + background-color: var(--cd-surface-2); + color: var(--cd-text-2); } - .section-title { - @apply text-base sm:text-lg font-semibold text-gray-800; + /* Buttons */ + .cd-btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 0.5rem 1.25rem; + border-radius: 0.5rem; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: all 0.15s ease; + border: 1px solid transparent; + } + .cd-btn:disabled { + opacity: 0.5; + cursor: not-allowed; + } + .cd-btn-primary { + background-color: var(--cd-primary); + color: #ffffff; + border-color: var(--cd-primary); + } + .cd-btn-primary:hover:not(:disabled) { + background-color: var(--cd-primary-hover); + border-color: var(--cd-primary-hover); + } + .cd-btn-secondary { + background-color: var(--cd-surface-2); + color: var(--cd-text); + border-color: var(--cd-border); + } + .cd-btn-secondary:hover:not(:disabled) { + background-color: var(--cd-hover); + } + .cd-btn-ghost { + background-color: transparent; + color: var(--cd-text-2); + border-color: transparent; + } + .cd-btn-ghost:hover:not(:disabled) { + background-color: var(--cd-hover); + color: var(--cd-text); + } + .cd-btn-danger { + background-color: var(--cd-danger-subtle); + color: var(--cd-danger); + border-color: var(--cd-danger-subtle); + } + .cd-btn-danger:hover:not(:disabled) { + background-color: var(--cd-danger); + color: #ffffff; } - .text-muted { - @apply text-xs sm:text-sm text-gray-500; + /* Inputs */ + .cd-input { + width: 100%; + background-color: var(--cd-surface); + border: 1px solid var(--cd-border); + border-radius: 0.5rem; + padding: 0.5rem 0.75rem; + font-size: 0.875rem; + color: var(--cd-text); + outline: none; + transition: border-color 0.15s ease, box-shadow 0.15s ease; + } + .cd-input::placeholder { + color: var(--cd-text-muted); + } + .cd-input:focus { + border-color: var(--cd-primary); + box-shadow: 0 0 0 3px var(--cd-primary-subtle); + } + .cd-input:disabled { + opacity: 0.5; + cursor: not-allowed; } - .badge-success { - @apply bg-green-100 text-green-600 px-2 py-1 rounded-full text-xs; + /* Table */ + .cd-table { + width: 100%; + border-collapse: collapse; + } + .cd-table thead { + background-color: var(--cd-surface-2); + } + .cd-table th { + padding: 0.75rem 1.25rem; + text-align: left; + font-size: 0.75rem; + font-weight: 600; + color: var(--cd-text-2); + text-transform: uppercase; + letter-spacing: 0.05em; + border-bottom: 1px solid var(--cd-border); + } + .cd-table td { + padding: 0.875rem 1.25rem; + font-size: 0.875rem; + color: var(--cd-text); + border-bottom: 1px solid var(--cd-border-subtle); + } + .cd-table tbody tr:hover { + background-color: var(--cd-hover); + } + .cd-table tbody tr:last-child td { + border-bottom: none; } - .badge-warning { - @apply bg-yellow-100 text-yellow-600 px-2 py-1 rounded-full text-xs; + /* Sidebar */ + .cd-sidebar { + background-color: var(--cd-surface); + border-right: 1px solid var(--cd-border); + } + + /* Nav link */ + .cd-nav-link { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.5rem 1rem; + border-radius: 0.5rem; + color: var(--cd-text-2); + font-size: 0.9375rem; + font-weight: 500; + transition: all 0.15s ease; + text-decoration: none; + } + .cd-nav-link:hover { + background-color: var(--cd-hover); + color: var(--cd-text); + } + .cd-nav-link.active { + background-color: var(--cd-primary-subtle); + color: var(--cd-primary-text); } + /* Page background */ + .cd-page { + background-color: var(--cd-bg); + min-height: 100vh; + } + + /* Stat metric box */ + .cd-metric { + background-color: var(--cd-surface-2); + border-radius: 0.75rem; + padding: 0.75rem; + } + + /* Glassmorphism surface */ + .cd-glass { + background-color: color-mix(in srgb, var(--cd-surface) 80%, transparent); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid var(--cd-border); + } + + /* Legacy aliases (keep old .card / .section-title working) */ + .card { + @apply cd-card; + } + .section-title { + @apply cd-section-title; + } + .text-muted { + @apply cd-text-muted; + } + .badge-success { + @apply cd-badge cd-badge-success; + } + .badge-warning { + @apply cd-badge cd-badge-warning; + } .badge-default { - @apply bg-gray-100 text-gray-600 px-2 py-1 rounded-full text-xs; + @apply cd-badge cd-badge-neutral; } } + +/* ─── Smooth theme transition ────────────────────────────────────────────── */ +html { + transition: background-color 0.25s ease; +} + +/* ─── Inter font helper ──────────────────────────────────────────────────── */ +.inter { + font-family: "Inter", sans-serif; + font-optical-sizing: auto; + font-style: normal; +} + +/* ─── Scrollbar styling ──────────────────────────────────────────────────── */ +::-webkit-scrollbar { + width: 6px; + height: 6px; +} +::-webkit-scrollbar-track { + background: transparent; +} +::-webkit-scrollbar-thumb { + background-color: var(--cd-border); + border-radius: 9999px; +} +::-webkit-scrollbar-thumb:hover { + background-color: var(--cd-text-muted); +} diff --git a/src/Component/ui/Button.tsx b/src/Component/ui/Button.tsx index 25fbd84..5cd0949 100644 --- a/src/Component/ui/Button.tsx +++ b/src/Component/ui/Button.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { useTheme } from "@/theme"; type ButtonProps = { text: string; @@ -6,38 +7,41 @@ type ButtonProps = { width?: string; height?: string; onClick: () => void; - textColor?: string; + variant?: "primary" | "secondary" | "ghost" | "danger"; disabled?: boolean; - backgroundColor?: string; + className?: string; }; const Button = ({ text, - textColor, icon, onClick, disabled, width, - backgroundColor, height, + variant = "primary", + className = "", }: ButtonProps) => { + const variantClass = disabled + ? "bg-[var(--cd-surface-2)] text-[var(--cd-text-muted)] cursor-not-allowed" + : variant === "secondary" + ? "cd-btn-secondary" + : variant === "ghost" + ? "cd-btn-ghost" + : variant === "danger" + ? "cd-btn-danger" + : "cd-btn-primary"; + return ( -
- -
+ ); }; diff --git a/src/Component/ui/DropDown.tsx b/src/Component/ui/DropDown.tsx index a1578a2..57eda56 100644 --- a/src/Component/ui/DropDown.tsx +++ b/src/Component/ui/DropDown.tsx @@ -1,14 +1,12 @@ import React, { useEffect, useRef, useState } from "react"; -import { getTheme, ThemeMode } from "../../config/them.config"; import { FaChevronDown } from "react-icons/fa"; +import { useTheme } from "@/theme"; type DropDownProps = { options: string[]; - label?: string; onSelect: (option: string) => void; className?: string; - themeMode?: ThemeMode; value?: string; placeholder?: string; disabled?: boolean; @@ -19,40 +17,24 @@ const DropDown: React.FC = ({ onSelect, className, label, - themeMode = "light", value, placeholder = "Select an option", disabled = false, }) => { + const { theme } = useTheme(); const [open, setOpen] = useState(false); const [selected, setSelected] = useState(value ?? options[0] ?? ""); - const [hoveredOption, setHoveredOption] = useState(null); const containerRef = useRef(null); - const theme = getTheme(themeMode); - useEffect(() => { - if (value !== undefined) { - setSelected(value); - return; - } - - setSelected((previous) => { - if (previous && options.includes(previous)) { - return previous; - } - return options[0] ?? ""; - }); + if (value !== undefined) { setSelected(value); return; } + setSelected((prev) => (prev && options.includes(prev) ? prev : (options[0] ?? ""))); }, [options, value]); useEffect(() => { - const onClickOutside = (event: MouseEvent) => { - if (!containerRef.current) return; - if (!containerRef.current.contains(event.target as Node)) { - setOpen(false); - } + const onClickOutside = (e: MouseEvent) => { + if (!containerRef.current?.contains(e.target as Node)) setOpen(false); }; - document.addEventListener("mousedown", onClickOutside); return () => document.removeEventListener("mousedown", onClickOutside); }, []); @@ -64,18 +46,12 @@ const DropDown: React.FC = ({ setOpen(false); }; - const isDisabled = disabled || options.length === 0; - const displayValue = selected || placeholder; - return ( -
+
{label && (

{label}

@@ -84,55 +60,55 @@ const DropDown: React.FC = ({ {open && options.length > 0 && (
- {options.map((opt) => { - const isHighlighted = hoveredOption === opt || selected === opt; - - return ( - - ); - })} + {options.map((opt) => ( + + ))}
)}
diff --git a/src/Component/ui/Input.tsx b/src/Component/ui/Input.tsx index 3243c28..629f819 100644 --- a/src/Component/ui/Input.tsx +++ b/src/Component/ui/Input.tsx @@ -1,5 +1,5 @@ import React, { forwardRef } from "react"; -import { getTheme } from "../../config/them.config"; +import { useTheme } from "@/theme"; type InputType = "text" | "email" | "password" | "number" | "url" | "tel" | "time"; @@ -9,18 +9,13 @@ type InputProps = { placeholder?: string; value?: string | number; type?: InputType; - onChange?: (name: string, value: string) => void; onKeyDown?: React.KeyboardEventHandler; - error?: string; - leftIcon?: React.ReactNode; rightIcon?: React.ReactNode; - className?: string; inputClassName?: string; - disabled?: boolean; required?: boolean; }; @@ -45,28 +40,34 @@ export const Input = forwardRef( }, ref, ) => { - const theme = getTheme("light"); + const { theme } = useTheme(); return ( -
+
{label && ( -
); }, diff --git a/src/Component/ui/TextArea.tsx b/src/Component/ui/TextArea.tsx index 0f8eab8..d8a7175 100644 --- a/src/Component/ui/TextArea.tsx +++ b/src/Component/ui/TextArea.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { getTheme } from "../../config/them.config"; +import { useTheme } from "@/theme"; type TextAreaProps = { label?: string; @@ -18,13 +18,19 @@ type TextAreaProps = { }; export const TextArea = (props: TextAreaProps) => { - const theme = getTheme("light"); + const { theme } = useTheme(); return ( -
- +
+ {props.label && ( + + )}