Changeset a762898 for lan-frontend/src
- Timestamp:
- 01/26/26 16:19:33 (5 months ago)
- Branches:
- master
- Children:
- ba17441
- Parents:
- e4c61dd
- Location:
- lan-frontend/src
- Files:
-
- 3 added
- 2 edited
-
App.jsx (modified) (2 diffs)
-
components/NetIntelViz.jsx (added)
-
components/SideMenu.jsx (added)
-
styles/netinel.css (added)
-
styles/theme.css (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
lan-frontend/src/App.jsx
re4c61dd ra762898 1 import React, { useEffect, useMemo, useState} from "react";1 import React, {useEffect, useMemo, useState} from "react"; 2 2 import "./styles/theme.css"; 3 import "./styles/netinel.css"; 3 4 4 5 import Login from "./components/Login"; … … 7 8 import ComputerDetailsModal from "./components/ComputerDetailsModal"; 8 9 import AIAssistantDrawer from "./components/AIAssistantDrawer"; 10 import NetIntelViz from "./components/NetIntelViz.jsx"; 11 import SideMenu from "./components/SideMenu"; 12 9 13 10 14 export default function App() { 11 const [stats, setStats] = useState(null); 12 const [computers, setComputers] = useState([]); 13 const [loading, setLoading] = useState(true); 14 const [lastUpdated, setLastUpdated] = useState(null); 15 16 const [query, setQuery] = useState(""); 17 const [status, setStatus] = useState(""); 18 19 const [selectedComputer, setSelectedComputer] = useState(null); 20 const [aiOpen, setAiOpen] = useState(false); 21 const [aiScope, setAiScope] = useState(null); 22 const [selectedEnv, setSelectedEnv] = useState("default"); 23 24 const [me, setMe] = useState(null); 25 const [meLoading, setMeLoading] = useState(true); 26 27 // NEW: environment setting (switch) 28 const [saveProcHistory, setSaveProcHistory] = useState(false); 29 30 async function loadMe() { 31 setMeLoading(true); 32 try { 33 const r = await fetch("/api/me", { credentials: "include" }); 34 if (r.ok) { 35 const data = await r.json(); 36 setMe(data.user); 37 } else { 38 setMe(null); 39 } 40 } catch (e) { 41 console.error("loadMe error", e); 42 setMe(null); 43 } finally { 44 setMeLoading(false); 15 const [stats, setStats] = useState(null); 16 const [computers, setComputers] = useState([]); 17 const [loading, setLoading] = useState(true); 18 const [lastUpdated, setLastUpdated] = useState(null); 19 20 const [query, setQuery] = useState(""); 21 const [status, setStatus] = useState(""); 22 23 const [selectedComputer, setSelectedComputer] = useState(null); 24 const [aiOpen, setAiOpen] = useState(false); 25 const [aiScope, setAiScope] = useState(null); 26 const [selectedEnv, setSelectedEnv] = useState("default"); 27 28 const [me, setMe] = useState(null); 29 const [meLoading, setMeLoading] = useState(true); 30 31 const [activeView, setActiveView] = useState("dashboard"); 32 // "dashboard" | "visualizations" | "account" 33 34 35 // NEW: environment setting (switch) 36 const [saveProcHistory, setSaveProcHistory] = useState(false); 37 38 async function loadMe() { 39 setMeLoading(true); 40 try { 41 const r = await fetch("/api/me", {credentials: "include"}); 42 if (r.ok) { 43 const data = await r.json(); 44 setMe(data.user); 45 } else { 46 setMe(null); 47 } 48 } catch (e) { 49 console.error("loadMe error", e); 50 setMe(null); 51 } finally { 52 setMeLoading(false); 53 } 45 54 } 46 } 47 48 async function refresh(){49 try {50 setLoading(true);51 const [s, c] = await Promise.all([52 fetch("/api/stats", { credentials: "include" }).then((r) => r.json()),53 fetch("/api/computers", {54 credentials: "include",55 headers: { "X-Env": selectedEnv },56 }).then((r) => r.json()),57 ]); 58 59 setStats(s);60 setComputers(Array.isArray(c) ? c : []);61 setLastUpdated(new Date().toISOString());62 } catch (e) {63 console.error("refresh error", e);64 } finally {65 setLoading(false);55 56 async function refresh() { 57 try { 58 setLoading(true); 59 const [s, c] = await Promise.all([ 60 fetch("/api/stats", {credentials: "include"}).then((r) => r.json()), 61 fetch("/api/computers", { 62 credentials: "include", 63 headers: {"X-Env": selectedEnv}, 64 }).then((r) => r.json()), 65 ]); 66 67 setStats(s); 68 setComputers(Array.isArray(c) ? c : []); 69 setLastUpdated(new Date().toISOString()); 70 } catch (e) { 71 console.error("refresh error", e); 72 } finally { 73 setLoading(false); 74 } 66 75 } 67 } 68 69 // NEW: load env settings (switch state) for selected env70 async function loadEnvSettings(env){71 try {72 // само admin ќе има пристап (ако не си admin, ќе падне во else и ќе стане false)73 const r = await fetch(`/api/admin/env-settings/${encodeURIComponent(env)}`, {74 credentials: "include",75 }); 76 77 if (r.ok) {78 const data = await r.json();79 setSaveProcHistory(!!data.save_process_history);80 } else {81 setSaveProcHistory(false);82 }83 } catch (e) {84 console.error("loadEnvSettings error",e);85 setSaveProcHistory(false);76 77 // NEW: load env settings (switch state) for selected env 78 async function loadEnvSettings(env) { 79 try { 80 // само admin ќе има пристап (ако не си admin, ќе падне во else и ќе стане false) 81 const r = await fetch(`/api/admin/env-settings/${encodeURIComponent(env)}`, { 82 credentials: "include", 83 }); 84 85 if (r.ok) { 86 const data = await r.json(); 87 setSaveProcHistory(!!data.save_process_history); 88 } else { 89 setSaveProcHistory(false); 90 } 91 } catch (e) { 92 console.error("loadEnvSettings error", e); 93 setSaveProcHistory(false); 94 } 86 95 } 87 } 88 89 // NEW: set env setting (toggle)90 async function setEnvProcessHistory(env, enabled) {91 // optimistic UI92 setSaveProcHistory(enabled); 93 94 try{95 const r = await fetch(`/api/admin/env-settings/${encodeURIComponent(env)}`, {96 method: "POST",97 credentials: "include",98 headers: { "Content-Type": "application/json" },99 body: JSON.stringify({ save_process_history: enabled }),100 }); 101 102 if (r.ok) {103 const data = await r.json();104 setSaveProcHistory(!!data.save_process_history);105 } else {106 // rollback107 setSaveProcHistory(!enabled);108 console.error("setEnvProcessHistory failed", await r.text());109 }110 } catch (e) {111 setSaveProcHistory(!enabled);112 console.error("setEnvProcessHistory error", e);96 97 // NEW: set env setting (toggle) 98 async function setEnvProcessHistory(env, enabled) { 99 // optimistic UI 100 setSaveProcHistory(enabled); 101 102 try { 103 const r = await fetch(`/api/admin/env-settings/${encodeURIComponent(env)}`, { 104 method: "POST", 105 credentials: "include", 106 headers: {"Content-Type": "application/json"}, 107 body: JSON.stringify({save_process_history: enabled}), 108 }); 109 110 if (r.ok) { 111 const data = await r.json(); 112 setSaveProcHistory(!!data.save_process_history); 113 } else { 114 // rollback 115 setSaveProcHistory(!enabled); 116 console.error("setEnvProcessHistory failed", await r.text()); 117 } 118 } catch (e) { 119 setSaveProcHistory(!enabled); 120 console.error("setEnvProcessHistory error", e); 121 } 113 122 } 114 } 115 116 // on mount: check session 117 useEffect(() => { 118 loadMe(); 119 }, []); 120 121 // when logged in OR env changed, refresh data + load env switch state 122 useEffect(() => { 123 if (!me) return; 124 loadEnvSettings(selectedEnv); 125 refresh(); 126 // eslint-disable-next-line react-hooks/exhaustive-deps 127 }, [me, selectedEnv]); 128 129 const filtered = useMemo(() => { 130 const q = query.trim().toLowerCase(); 131 return (computers || []) 132 .filter((c) => (status ? c.status === status : true)) 133 .filter((c) => { 134 if (!q) return true; 135 const hay = [c.name, c.user, c.ip, c.os, c.status] 136 .filter(Boolean) 137 .join(" ") 138 .toLowerCase(); 139 return hay.includes(q); 140 }); 141 }, [computers, query, status]); 142 143 // Gate UI 144 if (meLoading) return <div style={{ padding: 20 }}>Loading…</div>; 145 if (!me) return <Login onDone={loadMe} />; 146 147 return ( 148 <div className="app-wrap"> 149 <TopBar 150 onOpenAI={() => setAiOpen(true)} 151 onRefresh={refresh} 152 lastUpdated={lastUpdated} 153 onEnvChange={(env) => setSelectedEnv(env)} 154 selectedEnv={selectedEnv} 155 me={me} 156 onLoggedOut={loadMe} 157 // NEW props for switch: 158 saveProcHistory={saveProcHistory} 159 onToggleProcHistory={(enabled) => setEnvProcessHistory(selectedEnv, enabled)} 160 /> 161 162 {/* STATS */} 163 <div className="stats-grid"> 164 <div className="stat-card"> 165 <div className="label">Total Computers</div> 166 <div className="value">{computers.length}</div> 167 <div className="sub">Monitored Systems</div> 123 124 // on mount: check session 125 useEffect(() => { 126 loadMe(); 127 }, []); 128 129 // when logged in OR env changed, refresh data + load env switch state 130 useEffect(() => { 131 if (!me) return; 132 loadEnvSettings(selectedEnv); 133 refresh(); 134 // eslint-disable-next-line react-hooks/exhaustive-deps 135 }, [me, selectedEnv]); 136 137 const filtered = useMemo(() => { 138 const q = query.trim().toLowerCase(); 139 return (computers || []) 140 .filter((c) => (status ? c.status === status : true)) 141 .filter((c) => { 142 if (!q) return true; 143 const hay = [c.name, c.user, c.ip, c.os, c.status] 144 .filter(Boolean) 145 .join(" ") 146 .toLowerCase(); 147 return hay.includes(q); 148 }); 149 }, [computers, query, status]); 150 151 // Gate UI 152 if (meLoading) return <div style={{padding: 20}}>Loading…</div>; 153 if (!me) return <Login onDone={loadMe}/>; 154 155 return ( 156 <div className="app-shell"> 157 <div className="layout"> 158 <SideMenu activeView={activeView} setActiveView={setActiveView}/> 159 <main className="content"> 160 {/* TopBar + pages */} 161 </main> 162 </div> 163 164 <div className="cont"> 165 <TopBar 166 onOpenAI={() => setAiOpen(true)} 167 onRefresh={refresh} 168 lastUpdated={lastUpdated} 169 onEnvChange={(env) => setSelectedEnv(env)} 170 selectedEnv={selectedEnv} 171 me={me} 172 onLoggedOut={loadMe} 173 saveProcHistory={saveProcHistory} 174 onToggleProcHistory={(enabled) => setEnvProcessHistory(selectedEnv, enabled)} 175 /> 176 177 <div className="app-main"> 178 {/* ====== VIEW SWITCH ====== */} 179 {activeView === "dashboard" && ( 180 <> 181 {/* STATS */} 182 <div className="stats-grid"> 183 <div className="stat-card"> 184 <div className="label">Total Computers</div> 185 <div className="value">{computers.length}</div> 186 <div className="sub">Monitored Systems</div> 187 </div> 188 189 <div className="stat-card"> 190 <div className="label">Sysmon Events (24h)</div> 191 <div className="value">{stats?.total_sysmon_events ?? "—"}</div> 192 <div className="sub">Security Events</div> 193 </div> 194 195 <div className="stat-card"> 196 <div className="label">Network Connections (24h)</div> 197 <div className="value">{stats?.total_network_connections ?? "—"}</div> 198 <div className="sub">Captured Connections</div> 199 </div> 200 </div> 201 202 {/* FILTERS */} 203 <div className="panel" style={{marginBottom: 14}}> 204 <div className="panel-head"> 205 <div className="panel-title">Filters</div> 206 <div style={{color: "var(--dim)", fontSize: 12}}> 207 {loading ? "Refreshing…" : `${filtered.length} shown`} 208 </div> 209 </div> 210 <div className="panel-body"> 211 <div className="filters"> 212 <input 213 className="input" 214 value={query} 215 onChange={(e) => setQuery(e.target.value)} 216 placeholder="Search by name / user / ip / os…" 217 /> 218 219 <select 220 className="select" 221 value={status} 222 onChange={(e) => setStatus(e.target.value)} 223 > 224 <option value="">All status</option> 225 <option value="online">Online</option> 226 <option value="idle">Idle</option> 227 <option value="offline">Offline</option> 228 </select> 229 230 <button className="btn primary" onClick={refresh}> 231 Refresh 232 </button> 233 </div> 234 </div> 235 </div> 236 237 {/* COMPUTERS */} 238 <div className="panel"> 239 <div className="panel-head"> 240 <div className="panel-title">Connected Computers</div> 241 <div style={{display: "flex", gap: 10, alignItems: "center"}}> 242 <span className="pill">{filtered.length} computers</span> 243 <button 244 className="btn" 245 onClick={() => { 246 setQuery(""); 247 setStatus(""); 248 }} 249 > 250 Reset 251 </button> 252 </div> 253 </div> 254 <div className="panel-body"> 255 <div className="computers-grid"> 256 {filtered.map((c) => ( 257 <ComputerCard 258 key={c.name} 259 c={c} 260 onOpen={(pc) => { 261 setSelectedComputer(pc); 262 setAiScope(pc.name); 263 }} 264 /> 265 ))} 266 {filtered.length === 0 && ( 267 <div style={{color: "var(--dim)"}}> 268 No computers match filter. 269 </div> 270 )} 271 </div> 272 </div> 273 </div> 274 </> 275 )} 276 277 {activeView === "visualizations" && ( 278 <> 279 <div className="panel" style={{marginBottom: 14}}> 280 <div className="panel-head"> 281 <div className="panel-title">Visualizations</div> 282 <div style={{color: "var(--dim)", fontSize: 12}}> 283 Network graphs / charts / tables 284 </div> 285 </div> 286 <div className="panel-body"> 287 <NetIntelViz baseUrl="http://localhost:5555"/> 288 </div> 289 </div> 290 </> 291 )} 292 293 {activeView === "account" && ( 294 <div className="panel"> 295 <div className="panel-head"> 296 <div className="panel-title">Account</div> 297 <div style={{color: "var(--dim)", fontSize: 12}}> 298 Profile info 299 </div> 300 </div> 301 <div className="panel-body"> 302 <div style={{display: "grid", gap: 10}}> 303 <div className="pill">Email: {me.email}</div> 304 <div className="pill">Role: {me.role}</div> 305 <div className="pill">Tenant ID: {me.tenant_id}</div> 306 <div className="pill">Name: {me.name || "—"}</div> 307 </div> 308 </div> 309 </div> 310 )} 311 </div> 312 313 {/* DETAILS MODAL */} 314 <ComputerDetailsModal 315 open={!!selectedComputer} 316 computer={selectedComputer} 317 onClose={() => setSelectedComputer(null)} 318 onAskAI={(name) => { 319 setAiScope(name); 320 setAiOpen(true); 321 }} 322 /> 323 324 {/* AI DRAWER */} 325 <AIAssistantDrawer 326 open={aiOpen} 327 onClose={() => setAiOpen(false)} 328 computers={computers} 329 scope={aiScope} 330 setScope={setAiScope} 331 apiBase={import.meta.env.VITE_API_BASE} 332 /> 333 </div> 168 334 </div> 169 170 <div className="stat-card"> 171 <div className="label">Sysmon Events (24h)</div> 172 <div className="value">{stats?.total_sysmon_events ?? "—"}</div> 173 <div className="sub">Security Events</div> 174 </div> 175 176 <div className="stat-card"> 177 <div className="label">Network Connections (24h)</div> 178 <div className="value">{stats?.total_network_connections ?? "—"}</div> 179 <div className="sub">Captured Connections</div> 180 </div> 181 </div> 182 183 {/* FILTERS */} 184 <div className="panel" style={{ marginBottom: 14 }}> 185 <div className="panel-head"> 186 <div className="panel-title">Filters</div> 187 <div style={{ color: "var(--dim)", fontSize: 12 }}> 188 {loading ? "Refreshing…" : `${filtered.length} shown`} 189 </div> 190 </div> 191 <div className="panel-body"> 192 <div className="filters"> 193 <input 194 className="input" 195 value={query} 196 onChange={(e) => setQuery(e.target.value)} 197 placeholder="Search by name / user / ip / os…" 198 /> 199 200 <select 201 className="select" 202 value={status} 203 onChange={(e) => setStatus(e.target.value)} 204 > 205 <option value="">All status</option> 206 <option value="online">Online</option> 207 <option value="idle">Idle</option> 208 <option value="offline">Offline</option> 209 </select> 210 211 <button className="btn primary" onClick={refresh}> 212 Refresh 213 </button> 214 </div> 215 </div> 216 </div> 217 218 {/* COMPUTERS */} 219 <div className="panel"> 220 <div className="panel-head"> 221 <div className="panel-title">Connected Computers</div> 222 <div style={{ display: "flex", gap: 10, alignItems: "center" }}> 223 <span className="pill">{filtered.length} computers</span> 224 <button 225 className="btn" 226 onClick={() => { 227 setQuery(""); 228 setStatus(""); 229 }} 230 > 231 Reset 232 </button> 233 </div> 234 </div> 235 <div className="panel-body"> 236 <div className="computers-grid"> 237 {filtered.map((c) => ( 238 <ComputerCard 239 key={c.name} 240 c={c} 241 onOpen={(pc) => { 242 setSelectedComputer(pc); 243 setAiScope(pc.name); 244 }} 245 /> 246 ))} 247 {filtered.length === 0 && ( 248 <div style={{ color: "var(--dim)" }}>No computers match filter.</div> 249 )} 250 </div> 251 </div> 252 </div> 253 254 {/* DETAILS MODAL */} 255 <ComputerDetailsModal 256 open={!!selectedComputer} 257 computer={selectedComputer} 258 onClose={() => setSelectedComputer(null)} 259 onAskAI={(name) => { 260 setAiScope(name); 261 setAiOpen(true); 262 }} 263 /> 264 265 {/* AI DRAWER */} 266 <AIAssistantDrawer 267 open={aiOpen} 268 onClose={() => setAiOpen(false)} 269 computers={computers} 270 scope={aiScope} 271 setScope={setAiScope} 272 apiBase={import.meta.env.VITE_API_BASE} 273 /> 274 </div> 275 ); 335 ); 276 336 } -
lan-frontend/src/styles/theme.css
re4c61dd ra762898 3 3 Dark Navy + Light Blue 4 4 ========================= */ 5 @tailwind base; 6 @tailwind components; 7 @tailwind utilities; 5 8 6 9 :root{ … … 548 551 border-color: rgba(88, 167, 255, .35); 549 552 } 553 554 /* ====== LAYOUT with LEFT SIDEBAR ====== */ 555 .app-shell { 556 display: grid; 557 grid-template-columns: 240px 1fr; 558 min-height: 100vh; 559 } 560 561 .app-main { 562 padding: 14px; 563 } 564 565 /* Sidebar */ 566 .side { 567 background: rgba(255,255,255,0.04); 568 border-right: 1px solid rgba(255,255,255,0.10); 569 padding: 14px; 570 position: sticky; 571 top: 0; 572 height: 100vh; 573 } 574 575 .side-brand { 576 display: flex; 577 gap: 10px; 578 align-items: center; 579 padding: 10px; 580 border: 1px solid rgba(255,255,255,0.10); 581 border-radius: 14px; 582 background: rgba(0,0,0,0.12); 583 margin-bottom: 14px; 584 } 585 586 .side-brand-dot { 587 width: 10px; 588 height: 10px; 589 border-radius: 999px; 590 background: #7dd3fc; 591 box-shadow: 0 0 0 4px rgba(125,211,252,0.15); 592 } 593 594 .side-brand-title { 595 font-weight: 700; 596 letter-spacing: 0.2px; 597 } 598 599 .side-brand-sub { 600 font-size: 12px; 601 color: var(--dim); 602 } 603 604 .side-section-title { 605 font-size: 11px; 606 color: var(--dim); 607 margin: 12px 8px 8px; 608 letter-spacing: 0.12em; 609 } 610 611 .side-item { 612 width: 100%; 613 display: flex; 614 align-items: center; 615 gap: 10px; 616 padding: 10px 12px; 617 border-radius: 12px; 618 border: 1px solid transparent; 619 background: transparent; 620 color: inherit; 621 cursor: pointer; 622 text-align: left; 623 } 624 625 .side-item:hover { 626 background: rgba(255,255,255,0.06); 627 border-color: rgba(255,255,255,0.08); 628 } 629 630 .side-item.active { 631 background: rgba(125,211,252,0.12); 632 border-color: rgba(125,211,252,0.25); 633 } 634 635 .side-icon { 636 width: 26px; 637 display: grid; 638 place-items: center; 639 font-size: 18px; 640 } 641 642 .side-label { 643 font-size: 14px; 644 font-weight: 600; 645 } 646 647 .side-footer { 648 position: absolute; 649 bottom: 12px; 650 left: 14px; 651 right: 14px; 652 padding: 10px; 653 border-radius: 12px; 654 border: 1px solid rgba(255,255,255,0.10); 655 background: rgba(0,0,0,0.12); 656 } 657 658 .side-footer-hint { 659 font-size: 12px; 660 color: var(--dim); 661 } 662 /* ==== Collapsible Side Menu ==== */ 663 .side { 664 position: sticky; 665 top: 0; 666 height: 100vh; 667 z-index: 20; 668 669 width: 260px; 670 transition: width 180ms ease; 671 border-right: 1px solid rgba(255,255,255,0.08); 672 background: rgba(6, 12, 24, 0.72); 673 backdrop-filter: blur(10px); 674 } 675 676 .side.collapsed { 677 width: 54px; /* само тенка лента */ 678 } 679 680 .side-inner { 681 height: 100%; 682 padding: 14px 10px; 683 display: flex; 684 flex-direction: column; 685 } 686 687 .side-handle { 688 position: absolute; 689 left: 6px; 690 top: 10px; 691 width: 42px; 692 height: 34px; 693 border-radius: 12px; 694 border: 1px solid rgba(255,255,255,0.12); 695 background: rgba(255,255,255,0.06); 696 color: rgba(255,255,255,0.9); 697 cursor: pointer; 698 } 699 700 .side-brand { 701 display: flex; 702 align-items: center; 703 gap: 10px; 704 padding: 10px; 705 margin-top: 38px; /* за да не се судри со handle */ 706 border-radius: 16px; 707 background: rgba(255,255,255,0.03); 708 border: 1px solid rgba(255,255,255,0.06); 709 } 710 711 .side-brand-dot { 712 width: 12px; 713 height: 12px; 714 border-radius: 999px; 715 background: rgba(80, 160, 255, 0.95); 716 box-shadow: 0 0 18px rgba(80,160,255,0.45); 717 } 718 719 .side-brand-text { min-width: 0; } 720 .side-brand-title { font-weight: 800; color: rgba(255,255,255,0.92); } 721 .side-brand-sub { font-size: 12px; color: rgba(255,255,255,0.55); } 722 723 .side-section { margin-top: 14px; } 724 .side-section-title { 725 font-size: 11px; 726 letter-spacing: 0.12em; 727 color: rgba(255,255,255,0.45); 728 padding: 10px; 729 } 730 731 .side-item { 732 width: 100%; 733 display: flex; 734 align-items: center; 735 gap: 10px; 736 padding: 10px 12px; 737 margin: 6px 0; 738 border-radius: 14px; 739 border: 1px solid transparent; 740 background: transparent; 741 color: rgba(255,255,255,0.82); 742 cursor: pointer; 743 text-align: left; 744 } 745 746 .side-item:hover { 747 background: rgba(255,255,255,0.06); 748 border-color: rgba(255,255,255,0.08); 749 } 750 751 .side-item.active { 752 background: rgba(80,160,255,0.16); 753 border-color: rgba(80,160,255,0.25); 754 } 755 756 .side-icon { 757 width: 28px; 758 display: grid; 759 place-items: center; 760 font-size: 18px; 761 } 762 763 .side-label { 764 white-space: nowrap; 765 overflow: hidden; 766 text-overflow: ellipsis; 767 } 768 769 .side-footer { 770 margin-top: auto; 771 padding: 10px; 772 color: rgba(255,255,255,0.55); 773 font-size: 12px; 774 } 775 .layout { display: flex; } 776 .content { flex: 1; min-width: 0; } 777 /* collapsed = центрирање иконите + тргање празни простори */ 778 .side.collapsed .side-inner { 779 padding: 10px 6px; 780 align-items: center; 781 } 782 783 .side.collapsed .side-brand { 784 width: 42px; 785 padding: 10px 0; 786 justify-content: center; 787 margin-top: 48px; 788 } 789 790 .side.collapsed .side-item { 791 justify-content: center; 792 padding: 10px 0; 793 } 794 795 .side.collapsed .side-icon { 796 width: 42px; 797 } 798 799 /* handle во средина (поубаво) */ 800 .side-handle { 801 left: 6px; 802 top: 10px; 803 width: 42px; 804 height: 34px; 805 } 806 807 /* кога е open да си е нормално */ 808 .side.open .side-inner { 809 align-items: stretch; 810 } 811 812 .cont{ 813 margin-right: 50px; 814 }
Note:
See TracChangeset
for help on using the changeset viewer.
