From a416451ccb77742d25f71d81da540594fba720e7 Mon Sep 17 00:00:00 2001 From: EonaCat Date: Wed, 24 Jun 2026 07:04:59 +0200 Subject: [PATCH] Updated UI --- DoxaApi/UI/Assets/app.css | 1584 ++++++++++++++++++++---- DoxaApi/UI/Assets/app.js | 457 ++++++- DoxaApi/UI/Assets/code-generator.js | 407 ++++++ DoxaApi/UI/Assets/index.html | 5 + DoxaApi/UI/Assets/request-builder.js | 399 ++++++ DoxaApi/UI/Assets/workspace-manager.js | 435 +++++++ 6 files changed, 3024 insertions(+), 263 deletions(-) create mode 100644 DoxaApi/UI/Assets/code-generator.js create mode 100644 DoxaApi/UI/Assets/request-builder.js create mode 100644 DoxaApi/UI/Assets/workspace-manager.js diff --git a/DoxaApi/UI/Assets/app.css b/DoxaApi/UI/Assets/app.css index ef64845..9ed707f 100644 --- a/DoxaApi/UI/Assets/app.css +++ b/DoxaApi/UI/Assets/app.css @@ -5,82 +5,118 @@ } :root { - /* Method colors */ - --m-get: #34d399; - --m-post: #818cf8; - --m-put: #fbbf24; - --m-patch: #fb923c; - --m-delete: #f87171; - --m-head: #c084fc; - --m-options: #22d3ee; - --m-default: #64748b; - /* Accent palette */ + /* Premium Method colors with enhanced vibrancy */ + --m-get: #10b981; + --m-post: #6366f1; + --m-put: #f59e0b; + --m-patch: #f97316; + --m-delete: #ef4444; + --m-head: #a855f7; + --m-options: #06b6d4; + --m-default: #6b7280; + + /* Enhanced Accent palette with gradients */ --accent: #6366f1; - --accent-2: #a5b4fc; - --accent-dim: rgba(99,102,241,.13); - --accent-glow: rgba(99,102,241,.35); - /* Typography */ + --accent-2: #818cf8; + --accent-3: #c4b5fd; + --accent-dim: rgba(99,102,241,.12); + --accent-glow: rgba(99,102,241,.3); + --accent-gradient: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); + + /* Premium gradient backgrounds */ + --gradient-subtle: linear-gradient(135deg, rgba(99,102,241,.05) 0%, rgba(139,92,246,.05) 100%); + --gradient-strong: linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #06b6d4 100%); + --gradient-warm: linear-gradient(135deg, #f97316 0%, #f59e0b 100%); + + /* Typography - enhanced */ --font-sans: 'Inter', system-ui, -apple-system, sans-serif; --font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace; - /* Radii */ - --radius-xs: 3px; - --radius-sm: 5px; - --radius-md: 8px; - --radius-lg: 12px; - --radius-xl: 16px; + --font-weight-light: 300; + --font-weight-normal: 400; + --font-weight-medium: 500; + --font-weight-semibold: 600; + --font-weight-bold: 700; + + /* Radii - refined */ + --radius-xs: 2px; + --radius-sm: 4px; + --radius-md: 6px; + --radius-lg: 10px; + --radius-xl: 14px; --radius-2xl: 20px; + --radius-full: 9999px; + /* Sizing */ - --topbar-h: 54px; - --sidebar-w: 256px; - --try-w: 390px; - /* Transitions */ + --topbar-h: 56px; + --sidebar-w: 260px; + --try-w: 400px; + + /* Transitions - smooth and sophisticated */ --ease: cubic-bezier(.22,.1,.36,1); + --ease-out: cubic-bezier(.16,.1,0,1); + --ease-in: cubic-bezier(.52,0,.3,1); + --ease-smooth: cubic-bezier(.4,.0,.2,1); + + /* Animations */ + --duration-fast: 150ms; + --duration-base: 250ms; + --duration-slow: 350ms; } -/* DARK THEME (default) */ +/* DARK THEME (default) - Premium */ [data-theme="dark"] { - --bg-0: #0a0b10; - --bg-1: #0f1117; - --bg-2: #161921; - --bg-3: #1e2130; - --bg-4: #252a3a; - --bg-5: #2d3347; - --border: rgba(255,255,255,.055); - --border-2: rgba(255,255,255,.10); - --border-3: rgba(255,255,255,.16); - --text-1: #eef0f8; - --text-2: #7a82a6; - --text-3: #404766; - --shadow-sm: 0 1px 4px rgba(0,0,0,.5), 0 2px 8px rgba(0,0,0,.3); - --shadow: 0 4px 20px rgba(0,0,0,.6), 0 8px 40px rgba(0,0,0,.3); - --shadow-lg: 0 16px 60px rgba(0,0,0,.7), 0 4px 16px rgba(0,0,0,.4); - --topbar-bg: rgba(15,17,23,.92); - --sidebar-bg: rgba(15,17,23,.98); - --code-bg: #070809; + --bg-0: #060609; + --bg-1: #0a0d14; + --bg-2: #0f1219; + --bg-3: #151a28; + --bg-4: #1d2235; + --bg-5: #252d42; + --border: rgba(255,255,255,.05); + --border-2: rgba(255,255,255,.08); + --border-3: rgba(255,255,255,.12); + --text-1: #f0f4f8; + --text-2: #8b92b0; + --text-3: #4a5268; + --text-hover: #ffffff; + --shadow-xs: 0 1px 2px rgba(0,0,0,.3); + --shadow-sm: 0 2px 6px rgba(0,0,0,.5), 0 4px 12px rgba(0,0,0,.2); + --shadow: 0 8px 24px rgba(0,0,0,.6), 0 12px 48px rgba(0,0,0,.3); + --shadow-lg: 0 20px 60px rgba(0,0,0,.7), 0 8px 24px rgba(0,0,0,.4); + --shadow-xl: 0 30px 80px rgba(0,0,0,.8); + --topbar-bg: rgba(10,13,20,.85); + --sidebar-bg: rgba(10,13,20,.90); + --code-bg: #020203; + --code-border: rgba(255,255,255,.08); + --overlay: rgba(0,0,0,.3); --icon-sun: block; --icon-moon: none; } -/* LIGHT THEME */ +/* LIGHT THEME - Premium */ [data-theme="light"] { - --bg-0: #f4f5fa; + --bg-0: #f8f9fc; --bg-1: #ffffff; - --bg-2: #f0f1f8; - --bg-3: #e6e8f4; - --bg-4: #dcdff0; - --bg-5: #d2d5e8; - --border: rgba(0,0,0,.07); - --border-2: rgba(0,0,0,.12); - --border-3: rgba(0,0,0,.18); - --text-1: #0e1028; - --text-2: #5a628c; - --text-3: #9298b8; - --shadow-sm: 0 1px 4px rgba(0,0,0,.08), 0 2px 8px rgba(0,0,0,.04); - --shadow: 0 4px 20px rgba(0,0,0,.1), 0 8px 40px rgba(0,0,0,.05); - --shadow-lg: 0 16px 60px rgba(0,0,0,.15), 0 4px 16px rgba(0,0,0,.07); - --topbar-bg: rgba(255,255,255,.92); - --sidebar-bg: rgba(255,255,255,.98); - --code-bg: #f8f9fd; + --bg-2: #f3f5fa; + --bg-3: #eff1f8; + --bg-4: #e8ebf5; + --bg-5: #e0e4f0; + --border: rgba(0,0,0,.06); + --border-2: rgba(0,0,0,.10); + --border-3: rgba(0,0,0,.14); + --text-1: #0d1428; + --text-2: #63718a; + --text-3: #959db4; + --text-hover: #000000; + --shadow-xs: 0 1px 2px rgba(0,0,0,.05); + --shadow-sm: 0 2px 6px rgba(0,0,0,.08), 0 4px 12px rgba(0,0,0,.03); + --shadow: 0 8px 24px rgba(0,0,0,.12), 0 12px 48px rgba(0,0,0,.06); + --shadow-lg: 0 20px 60px rgba(0,0,0,.15), 0 8px 24px rgba(0,0,0,.08); + --shadow-xl: 0 30px 80px rgba(0,0,0,.18); + --topbar-bg: rgba(255,255,255,.88); + --sidebar-bg: rgba(255,255,255,.95); + --code-bg: #f7f8fb; + --code-border: rgba(0,0,0,.08); + --overlay: rgba(0,0,0,.2); --icon-sun: none; --icon-moon: block; } @@ -146,7 +182,7 @@ a { background: var(--bg-5); } -/* TOP BAR */ +/* TOP BAR - Premium Enhancement */ .topbar { position: fixed; inset: 0 0 auto 0; @@ -154,12 +190,22 @@ a { height: var(--topbar-h); display: flex; align-items: center; - gap: 8px; - padding: 0 16px; + gap: 12px; + padding: 0 20px; background: var(--topbar-bg); border-bottom: 1px solid var(--border); - backdrop-filter: blur(20px) saturate(1.8); - -webkit-backdrop-filter: blur(20px) saturate(1.8); + backdrop-filter: blur(12px) saturate(1.5); + -webkit-backdrop-filter: blur(12px) saturate(1.5); + box-shadow: 0 1px 3px var(--overlay); + transition: all var(--duration-base) var(--ease-smooth); +} + +[data-theme="dark"] .topbar { + background: linear-gradient(180deg, rgba(10,13,20,.95) 0%, rgba(10,13,20,.90) 100%); +} + +[data-theme="light"] .topbar { + background: linear-gradient(180deg, rgba(255,255,255,.95) 0%, rgba(255,255,255,.88) 100%); } .topbar-left { @@ -183,69 +229,77 @@ a { flex-shrink: 0; } -/* Brand */ +/* Brand - Premium */ .brand { display: flex; align-items: center; - gap: 10px; + gap: 12px; user-select: none; + transition: opacity var(--duration-fast) var(--ease-smooth); +} + +.brand:hover { + opacity: 0.8; } .brand-icon { - width: 32px; - height: 32px; - border-radius: var(--radius-md); - background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #06b6d4 100%); + width: 36px; + height: 36px; + border-radius: var(--radius-lg); + background: var(--gradient-strong); display: flex; align-items: center; justify-content: center; - box-shadow: 0 2px 12px rgba(99,102,241,.5), 0 0 0 1px rgba(255,255,255,.1) inset; + box-shadow: 0 4px 16px rgba(99,102,241,.4), 0 0 0 1px rgba(255,255,255,.2) inset; position: relative; overflow: hidden; + flex-shrink: 0; } - .brand-icon::after { + .brand-icon::before { content: ''; position: absolute; inset: 0; - background: linear-gradient(135deg, rgba(255,255,255,.15) 0%, transparent 60%); + background: linear-gradient(135deg, rgba(255,255,255,.2) 0%, transparent 60%); } .brand-icon svg { - width: 16px; - height: 16px; + width: 18px; + height: 18px; color: #fff; position: relative; z-index: 1; + filter: drop-shadow(0 1px 2px rgba(0,0,0,.3)); } .brand-text { display: flex; align-items: baseline; gap: 8px; + flex-shrink: 0; } .brand-name { font-size: 15px; font-weight: 700; - color: var(--text-1); letter-spacing: -.02em; - background: linear-gradient(135deg, var(--text-1) 40%, var(--accent-2) 100%); + background: linear-gradient(135deg, var(--text-1) 30%, var(--accent-2) 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; + text-shadow: 0 1px 2px rgba(0,0,0,.1); } .brand-ver { - font-size: 10px; - font-weight: 600; + font-size: 11px; + font-weight: 700; font-family: var(--font-mono); - padding: 2px 7px; + padding: 3px 8px; background: var(--bg-3); border: 1px solid var(--border-2); - border-radius: 99px; + border-radius: var(--radius-full); color: var(--text-2); - -webkit-text-fill-color: var(--text-2); + flex-shrink: 0; } /* API Selector */ @@ -276,36 +330,60 @@ a { outline: none; } -/* Search */ +/* Search - Enhanced */ .search-wrap { display: flex; align-items: center; gap: 8px; - height: 36px; + height: 38px; background: var(--bg-2); border: 1px solid var(--border-2); border-radius: var(--radius-lg); padding: 0 12px; - transition: border-color .15s, box-shadow .15s; + transition: all var(--duration-fast) var(--ease-smooth); + position: relative; } + .search-wrap::before { + content: ''; + position: absolute; + inset: 0; + background: var(--gradient-subtle); + border-radius: inherit; + pointer-events: none; + opacity: 0; + transition: opacity var(--duration-base) var(--ease-smooth); + } + .search-wrap:focus-within { border-color: var(--accent); - box-shadow: 0 0 0 3px var(--accent-dim); + box-shadow: 0 0 0 3px var(--accent-dim), 0 4px 12px var(--overlay); + } + + .search-wrap:focus-within::before { + opacity: 1; } .search-ico { - width: 15px; - height: 15px; + width: 16px; + height: 16px; color: var(--text-3); flex-shrink: 0; + transition: color var(--duration-fast) var(--ease-smooth); +} + +.search-wrap:focus-within .search-ico { + color: var(--accent); } .search-wrap input { flex: 1; font-size: 13px; + font-weight: 500; background: none; color: var(--text-1); + position: relative; + z-index: 1; } .search-wrap input::placeholder { @@ -315,57 +393,89 @@ a { .search-kbd { font-family: var(--font-mono); font-size: 10px; - padding: 2px 6px; + padding: 3px 6px; background: var(--bg-3); border: 1px solid var(--border-2); - border-radius: 4px; + border-radius: var(--radius-xs); color: var(--text-3); flex-shrink: 0; + font-weight: 500; } -/* Topbar Buttons */ +/* Topbar Buttons - Enhanced */ .tb-btn { display: flex; align-items: center; - gap: 5px; - height: 30px; - padding: 0 10px; + justify-content: center; + gap: 6px; + height: 32px; + padding: 0 12px; font-size: 12px; font-weight: 500; border-radius: var(--radius-md); color: var(--text-2); - transition: background .13s, color .13s; + background: transparent; + transition: all var(--duration-fast) var(--ease-smooth); white-space: nowrap; position: relative; + cursor: pointer; } + .tb-btn::before { + content: ''; + position: absolute; + inset: 0; + background: var(--bg-2); + border-radius: inherit; + opacity: 0; + transition: opacity var(--duration-fast) var(--ease-smooth); + z-index: -1; + } + .tb-btn svg { - width: 14px; - height: 14px; + width: 15px; + height: 15px; flex-shrink: 0; + transition: color var(--duration-fast) var(--ease-smooth); } .tb-btn:hover { - background: var(--bg-3); color: var(--text-1); } + .tb-btn:hover::before { + opacity: 1; + } + + .tb-btn:active { + transform: scale(0.98); + } + .tb-btn-accent { - background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); + background: var(--gradient-strong); color: #fff !important; - box-shadow: 0 2px 10px rgba(99,102,241,.4); + box-shadow: 0 4px 12px rgba(99,102,241,.35); + font-weight: 600; } + .tb-btn-accent::before { + display: none !important; + } + .tb-btn-accent:hover { - background: linear-gradient(135deg, #7c7ff5 0%, #9d71fa 100%) !important; - box-shadow: 0 3px 14px rgba(99,102,241,.55); + box-shadow: 0 6px 20px rgba(99,102,241,.5) !important; + transform: translateY(-1px); + } + + .tb-btn-accent:active { + transform: translateY(0); } .tb-divider { width: 1px; - height: 22px; - background: var(--border-2); - margin: 0 4px; + height: 18px; + background: linear-gradient(180deg, transparent 0%, var(--border-2) 50%, transparent 100%); + margin: 0 6px; } /* Auth dot */ @@ -390,19 +500,22 @@ a { position: relative; } +/* Export dropdown - Enhanced */ .export-menu { position: absolute; top: calc(100% + 8px); right: 0; - min-width: 170px; + min-width: 180px; background: var(--bg-2); border: 1px solid var(--border-2); border-radius: var(--radius-lg); - box-shadow: var(--shadow-lg); + box-shadow: 0 10px 32px var(--overlay); overflow: hidden; display: none; z-index: 200; - animation: pop-in .14s var(--ease); + animation: pop-in .14s var(--ease-smooth); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); } .export-menu.open { @@ -412,9 +525,8 @@ a { @keyframes pop-in { from { opacity: 0; - transform: translateY(-6px) scale(.97); + transform: translateY(-8px) scale(.95); } - to { opacity: 1; transform: none; @@ -426,13 +538,15 @@ a { align-items: center; justify-content: space-between; width: 100%; - padding: 9px 14px; + padding: 10px 14px; font-size: 12px; - transition: background .1s; + transition: all var(--duration-fast) var(--ease-smooth); + cursor: pointer; } .export-item:hover { background: var(--bg-3); + transform: translateX(2px); } .export-fmt { @@ -452,26 +566,51 @@ a { margin-left: 2px; } -/* Icon buttons */ +/* Icon buttons - Enhanced */ .icon-btn { - width: 30px; - height: 30px; + width: 32px; + height: 32px; border-radius: var(--radius-md); display: flex; align-items: center; justify-content: center; color: var(--text-2); - transition: background .13s, color .13s; + transition: all var(--duration-fast) var(--ease-smooth); + cursor: pointer; + position: relative; } + .icon-btn::before { + content: ''; + position: absolute; + inset: 0; + background: var(--bg-2); + border-radius: inherit; + opacity: 0; + transition: opacity var(--duration-fast) var(--ease-smooth); + z-index: -1; + } + .icon-btn:hover { - background: var(--bg-3); color: var(--text-1); } + .icon-btn:hover::before { + opacity: 1; + } + + .icon-btn:active { + transform: scale(0.92); + } + .icon-btn svg { width: 16px; height: 16px; + transition: transform var(--duration-base) var(--ease-smooth); + } + + .icon-btn:hover svg { + transform: rotate(12deg); } [data-theme="dark"] .icon-sun { @@ -507,8 +646,8 @@ a { } /* - SIDEBAR - */ + SIDEBAR - Enhanced +*/ .sidebar { width: var(--sidebar-w); flex-shrink: 0; @@ -518,40 +657,45 @@ a { border-right: 1px solid var(--border); display: flex; flex-direction: column; + transition: background var(--duration-base) var(--ease-smooth); } .sidebar-inner { - padding: 10px 0 40px; + padding: 12px 0 40px; } -/* Overview link */ +/* Overview link - Enhanced */ .nav-overview-link { display: flex; align-items: center; - gap: 9px; + gap: 10px; width: 100%; - padding: 8px 16px; - font-size: 12.5px; + padding: 9px 16px; + font-size: 12px; font-weight: 500; color: var(--text-2); border-radius: 0; - margin-bottom: 6px; - transition: background .12s, color .12s; + margin-bottom: 4px; + transition: all var(--duration-fast) var(--ease-smooth); + position: relative; + cursor: pointer; } .nav-overview-link svg { width: 15px; height: 15px; flex-shrink: 0; + transition: color var(--duration-fast) var(--ease-smooth); } .nav-overview-link:hover { background: var(--bg-2); color: var(--text-1); + padding-left: 18px; } .nav-overview-link.active { - background: var(--accent-dim); + background: linear-gradient(90deg, var(--accent-dim) 0%, transparent 100%); color: var(--accent-2); position: relative; } @@ -560,29 +704,34 @@ a { content: ''; position: absolute; left: 0; - top: 0; - bottom: 0; - width: 2px; - background: var(--accent); + top: 50%; + transform: translateY(-50%); + width: 3px; + height: 18px; + background: var(--accent-gradient); border-radius: 0 2px 2px 0; + box-shadow: 2px 0 8px rgba(99,102,241,.3); } -/* Nav groups */ +/* Nav groups - Enhanced */ .nav-group { + margin-bottom: 2px; } .nav-group-header { display: flex; align-items: center; - gap: 7px; + gap: 8px; width: 100%; - padding: 8px 16px 4px; - font-size: 10.5px; + padding: 10px 16px 6px; + font-size: 10px; font-weight: 700; color: var(--text-3); letter-spacing: .08em; text-transform: uppercase; - transition: color .12s; + transition: all var(--duration-fast) var(--ease-smooth); + cursor: pointer; + user-select: none; } .nav-group-header:hover { @@ -590,9 +739,9 @@ a { } .chev { - width: 11px; - height: 11px; - transition: transform .2s var(--ease); + width: 12px; + height: 12px; + transition: transform var(--duration-base) var(--ease-smooth); flex-shrink: 0; } @@ -603,12 +752,12 @@ a { .nav-group-count { margin-left: auto; font-size: 10px; - font-weight: 600; - padding: 0 6px; - height: 17px; - line-height: 17px; + font-weight: 700; + padding: 1px 7px; + height: 18px; + line-height: 18px; background: var(--bg-3); - border-radius: 99px; + border-radius: var(--radius-full); color: var(--text-3); font-family: var(--font-mono); } @@ -620,50 +769,89 @@ a { display: none; } -/* Endpoint items */ +/* Endpoint items - Enhanced */ .nav-endpoint { display: flex; align-items: center; - gap: 9px; + gap: 10px; width: 100%; - padding: 5px 16px 5px 28px; + padding: 6px 16px 6px 28px; font-size: 12px; + font-weight: 500; color: var(--text-2); - transition: background .1s, color .1s; - border-left: 2px solid transparent; + transition: all var(--duration-fast) var(--ease-smooth); + border-left: 3px solid transparent; overflow: hidden; - min-height: 30px; + min-height: 32px; + cursor: pointer; + position: relative; } + .nav-endpoint::before { + content: ''; + position: absolute; + inset: 0; + background: var(--gradient-subtle); + opacity: 0; + transition: opacity var(--duration-fast) var(--ease-smooth); + z-index: -1; + } + .nav-endpoint:hover { background: var(--bg-2); color: var(--text-1); + padding-left: 30px; + } + + .nav-endpoint:hover::before { + opacity: 1; } .nav-endpoint.active { - background: color-mix(in srgb, var(--method-color, var(--accent)) 8%, var(--bg-2)); + background: color-mix(in srgb, var(--method-color, var(--accent)) 12%, var(--bg-2)); color: var(--text-1); border-left-color: var(--method-color, var(--accent)); + font-weight: 600; + } + + .nav-endpoint.active::after { + content: ''; + position: absolute; + right: 0; + top: 50%; + transform: translateY(-50%); + width: 2px; + height: 12px; + background: var(--method-color, var(--accent)); + border-radius: 1px 0 0 1px; + box-shadow: -2px 0 6px rgba(99,102,241,.3); } .nav-endpoint.deprecated { - opacity: .45; + opacity: 0.5; text-decoration: line-through; } -/* Method tags */ +/* Method tags - Enhanced */ .method-tag { font-family: var(--font-mono); - font-size: 8.5px; + font-size: 9px; font-weight: 800; - padding: 2px 5px; - border-radius: var(--radius-xs); + padding: 2px 6px; + border-radius: var(--radius-sm); flex-shrink: 0; letter-spacing: .06em; - background: color-mix(in srgb, var(--method-color, var(--m-default)) 18%, transparent); + background: color-mix(in srgb, var(--method-color, var(--m-default)) 20%, transparent); color: var(--method-color, var(--m-default)); - min-width: 40px; + min-width: 42px; text-align: center; + border: 1px solid color-mix(in srgb, var(--method-color, var(--m-default)) 35%, transparent); + transition: all var(--duration-fast) var(--ease-smooth); +} + +.nav-endpoint:hover .method-tag { + background: color-mix(in srgb, var(--method-color, var(--m-default)) 30%, transparent); + border-color: color-mix(in srgb, var(--method-color, var(--m-default)) 50%, transparent); } .nav-endpoint-path { @@ -671,7 +859,8 @@ a { text-overflow: ellipsis; white-space: nowrap; font-family: var(--font-mono); - font-size: 11.5px; + font-size: 11px; + font-weight: 400; } .nav-empty { @@ -737,21 +926,88 @@ a { } /* - ANIMATIONS - */ + ANIMATIONS - Enhanced +*/ .fade-in { - animation: fade-up .22s var(--ease); + animation: fade-up var(--duration-base) var(--ease); } @keyframes fade-up { from { opacity: 0; - transform: translateY(8px); + transform: translateY(12px); } - to { opacity: 1; - transform: none; + transform: translate(0); + } +} + +@keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes slide-in-left { + from { + opacity: 0; + transform: translateX(-16px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes slide-down { + from { + opacity: 0; + transform: translateY(-12px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes scale-in { + from { + opacity: 0; + transform: scale(0.95); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes bounce-in { + 0% { + opacity: 0; + transform: scale(0.3); + } + 50% { + opacity: 1; + transform: scale(1.05); + } + 70% { + transform: scale(0.9); + } + 100% { + transform: scale(1); + } +} + +@keyframes shimmer { + 0% { + background-position: -1000px 0; + } + 100% { + background-position: 1000px 0; } } @@ -893,7 +1149,7 @@ a { margin: 32px 0 14px; } -/* Security badges */ +/* Security badges - Enhanced */ .security-badges { display: flex; flex-wrap: wrap; @@ -905,50 +1161,73 @@ a { align-items: center; gap: 7px; font-size: 12px; - font-weight: 500; - padding: 6px 12px; + font-weight: 600; + padding: 7px 13px; background: var(--bg-2); border: 1px solid var(--border-2); border-radius: var(--radius-md); color: var(--text-2); - transition: border-color .14s, color .14s, background .14s; + transition: all var(--duration-base) var(--ease-smooth)); + cursor: pointer; + position: relative; + overflow: hidden; } + .security-badge::before { + content: ''; + position: absolute; + inset: 0; + background: var(--gradient-subtle); + opacity: 0; + transition: opacity var(--duration-base) var(--ease-smooth)); + z-index: -1; + } + .security-badge svg { - width: 12px; - height: 12px; + width: 13px; + height: 13px; + transition: transform var(--duration-base) var(--ease-smooth)); } button.security-badge:hover { border-color: var(--accent); color: var(--accent-2); background: var(--accent-dim); + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(99,102,241,.2); } -/* Overview group cards */ +button.security-badge:hover svg { + transform: rotate(12deg) scale(1.15); +} + +/* Overview group cards - Enhanced */ .overview-group-card { background: var(--bg-1); border: 1px solid var(--border); border-radius: var(--radius-lg); - margin-bottom: 12px; + margin-bottom: 14px; overflow: hidden; - transition: border-color .15s, box-shadow .15s; + transition: all var(--duration-base) var(--ease-smooth); + box-shadow: 0 1px 3px var(--overlay); } .overview-group-card:hover { border-color: var(--border-2); box-shadow: var(--shadow-sm); + transform: translateY(-2px); } .overview-group-card-header { display: flex; align-items: center; justify-content: space-between; - padding: 14px 18px; + padding: 16px 18px; font-size: 13px; font-weight: 700; border-bottom: 1px solid var(--border); - background: var(--bg-2); + background: linear-gradient(90deg, var(--bg-2) 0%, transparent 100%); + letter-spacing: -.01em; } .overview-group-routes { @@ -958,39 +1237,63 @@ button.security-badge:hover { display: flex; align-items: center; gap: 12px; - padding: 10px 18px; + padding: 12px 18px; cursor: pointer; - transition: background .1s; + transition: all var(--duration-fast) var(--ease-smooth); border-bottom: 1px solid var(--border); + position: relative; } + .overview-route-row::before { + content: ''; + position: absolute; + inset: 0; + background: var(--gradient-subtle); + opacity: 0; + transition: opacity var(--duration-fast) var(--ease-smooth); + z-index: -1; + } + .overview-route-row:last-child { border-bottom: none; } .overview-route-row:hover { - background: var(--bg-2); + transform: translateX(2px); + } + + .overview-route-row:hover::before { + opacity: 1; } .method-badge { font-family: var(--font-mono); - font-size: 9.5px; + font-size: 9px; font-weight: 800; padding: 3px 8px; border-radius: var(--radius-sm); flex-shrink: 0; - background: color-mix(in srgb, var(--method-color, var(--m-default)) 15%, transparent); + background: color-mix(in srgb, var(--method-color, var(--m-default)) 18%, transparent); color: var(--method-color, var(--m-default)); - min-width: 54px; + min-width: 50px; text-align: center; letter-spacing: .04em; + border: 1px solid color-mix(in srgb, var(--method-color, var(--m-default)) 30%, transparent); + transition: all var(--duration-fast) var(--ease-smooth); +} + +.overview-route-row:hover .method-badge { + background: color-mix(in srgb, var(--method-color, var(--m-default)) 28%, transparent); + border-color: color-mix(in srgb, var(--method-color, var(--m-default)) 50%, transparent); } .route-path { font-family: var(--font-mono); - font-size: 12px; + font-size: 11px; + font-weight: 500; flex-shrink: 0; color: var(--text-1); + letter-spacing: -.01em; } .route-summary { @@ -1003,11 +1306,19 @@ button.security-badge:hover { } .route-arrow { - width: 14px; - height: 14px; + width: 16px; + height: 16px; color: var(--text-3); flex-shrink: 0; margin-left: auto; + transition: all var(--duration-fast) var(--ease-smooth); + opacity: 0; +} + +.overview-route-row:hover .route-arrow { + opacity: 1; + color: var(--accent-2); + transform: translateX(4px); } /* @@ -1283,7 +1594,7 @@ button.security-badge:hover { background: var(--bg-0); } -/* Status pills */ +/* Status pills - Enhanced */ .status-pill { display: inline-flex; align-items: center; @@ -1291,25 +1602,48 @@ button.security-badge:hover { font-family: var(--font-mono); font-size: 11px; font-weight: 700; - padding: 3px 9px; - border-radius: var(--radius-sm); + padding: 4px 10px; + border-radius: var(--radius-md); flex-shrink: 0; - letter-spacing: .03em; + letter-spacing: .04em; + border: 1px solid; + transition: all var(--duration-fast) var(--ease-smooth)); } .status-2xx { - background: color-mix(in srgb, #34d399 14%, transparent); - color: #34d399; + background: color-mix(in srgb, #10b981 16%, transparent); + color: #10b981; + border-color: color-mix(in srgb, #10b981 30%, transparent); +} + +.status-2xx:hover { + background: color-mix(in srgb, #10b981 25%, transparent); + border-color: #10b981; + box-shadow: 0 0 8px rgba(16,185,129,.2); } .status-4xx { - background: color-mix(in srgb, #fbbf24 14%, transparent); - color: #fbbf24; + background: color-mix(in srgb, #f59e0b 16%, transparent); + color: #f59e0b; + border-color: color-mix(in srgb, #f59e0b 30%, transparent); +} + +.status-4xx:hover { + background: color-mix(in srgb, #f59e0b 25%, transparent); + border-color: #f59e0b; + box-shadow: 0 0 8px rgba(245,158,11,.2); } .status-5xx { - background: color-mix(in srgb, #f87171 14%, transparent); - color: #f87171; + background: color-mix(in srgb, #ef4444 16%, transparent); + color: #ef4444; + border-color: color-mix(in srgb, #ef4444 30%, transparent); +} + +.status-5xx:hover { + background: color-mix(in srgb, #ef4444 25%, transparent); + border-color: #ef4444; + box-shadow: 0 0 8px rgba(239,68,68,.2); } .response-desc { @@ -1369,11 +1703,12 @@ button.security-badge:hover { 0%, 100% { opacity: 1; transform: scale(1); + box-shadow: 0 0 12px color-mix(in srgb, var(--method-color, var(--accent)) 100%, transparent); } - 50% { - opacity: .5; - transform: scale(.85); + opacity: 0.6; + transform: scale(0.8); + box-shadow: 0 0 20px color-mix(in srgb, var(--method-color, var(--accent)) 60%, transparent); } } @@ -1805,15 +2140,16 @@ textarea.field-input { border-color: var(--border-3); } -/* Spinner */ +/* Spinner - Enhanced */ .spinner { display: inline-block; - width: 14px; - height: 14px; - border: 2px solid rgba(255,255,255,.25); - border-top-color: #fff; + width: 16px; + height: 16px; + border: 2.5px solid rgba(99,102,241,.15); + border-top-color: var(--accent); border-radius: 50%; - animation: spin .65s linear infinite; + animation: spin 0.8s cubic-bezier(.8,.2,.2,.8) infinite; + box-shadow: 0 0 8px rgba(99,102,241,.2); } @keyframes spin { @@ -1868,66 +2204,68 @@ textarea.field-input { } /* - MODAL - */ + MODAL - Premium with Glassmorphism +*/ .modal-backdrop { position: fixed; inset: 0; z-index: 1000; - background: rgba(0,0,0,.7); - backdrop-filter: blur(6px); - -webkit-backdrop-filter: blur(6px); + background: rgba(0,0,0,.5); + backdrop-filter: blur(8px) saturate(1.5); + -webkit-backdrop-filter: blur(8px) saturate(1.5); display: flex; align-items: center; justify-content: center; padding: 24px; - animation: fade-up .14s var(--ease); + animation: fade-in var(--duration-base) var(--ease-smooth)); } .modal-box { background: var(--bg-1); border: 1px solid var(--border-2); border-radius: var(--radius-xl); - box-shadow: var(--shadow-lg); + background: linear-gradient(135deg, var(--bg-1) 0%, color-mix(in srgb, var(--bg-1) 95%, var(--accent)) 100%); + box-shadow: 0 10px 40px rgba(0,0,0,.3), 0 0 1px rgba(99,102,241,.2) inset; width: 100%; max-width: 490px; max-height: 82vh; display: flex; flex-direction: column; overflow: hidden; - animation: modal-in .18s var(--ease); + animation: modal-in var(--duration-base) var(--ease-smooth)); } + .modal-box:before { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient(135deg, rgba(99,102,241,.05) 0%, transparent 100%); + pointer-events: none; + z-index: -1; + } + .modal-wide { max-width: 660px; } -@keyframes modal-in { - from { - opacity: 0; - transform: scale(.95) translateY(14px); - } - - to { - opacity: 1; - transform: none; - } -} - .modal-head { display: flex; align-items: center; justify-content: space-between; - padding: 18px 22px; + padding: 20px 24px; border-bottom: 1px solid var(--border); flex-shrink: 0; - background: var(--bg-2); + background: linear-gradient(90deg, var(--bg-2) 0%, transparent 100%); } .modal-title { - font-size: 15px; + font-size: 16px; font-weight: 700; - letter-spacing: -.02em; + letter-spacing: -.01em; + background: linear-gradient(135deg, var(--text-1) 40%, var(--accent-2) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; } .modal-close { @@ -1957,18 +2295,36 @@ textarea.field-input { flex: 1; } -/* Auth card */ +/* Auth card - Enhanced */ .auth-scheme-card { background: var(--bg-2); border: 1px solid var(--border-2); border-radius: var(--radius-lg); padding: 18px; - margin-bottom: 12px; - transition: border-color .15s; + margin-bottom: 14px; + transition: all var(--duration-base) var(--ease-smooth)); + position: relative; + overflow: hidden; } + .auth-scheme-card::before { + content: ''; + position: absolute; + inset: 0; + background: var(--gradient-subtle); + opacity: 0; + transition: opacity var(--duration-base) var(--ease-smooth)); + z-index: -1; + } + .auth-scheme-card:hover { - border-color: var(--border-3); + border-color: var(--accent); + box-shadow: var(--shadow-sm); + transform: translateY(-2px); + } + + .auth-scheme-card:hover::before { + opacity: 1; } .auth-scheme-head { @@ -2083,8 +2439,8 @@ textarea.field-input { } /* - DASHBOARD DRAWER - */ + DASHBOARD DRAWER - Premium with Glassmorphism +*/ .drawer { position: fixed; top: var(--topbar-h); @@ -2094,20 +2450,21 @@ textarea.field-input { width: 420px; background: var(--bg-1); border-left: 1px solid var(--border); - box-shadow: var(--shadow-lg); + box-shadow: -10px 0 40px rgba(0,0,0,.3); display: flex; flex-direction: column; - animation: slide-in .22s var(--ease); + animation: slide-in-right var(--duration-base) var(--ease-smooth)); + backdrop-filter: blur(2px); + -webkit-backdrop-filter: blur(2px); } -@keyframes slide-in { +@keyframes slide-in-right { from { - transform: translateX(40px); + transform: translateX(100%); opacity: 0; } - to { - transform: none; + transform: translateX(0); opacity: 1; } } @@ -2116,40 +2473,46 @@ textarea.field-input { display: flex; align-items: center; justify-content: space-between; - padding: 18px 22px; + padding: 20px 22px; border-bottom: 1px solid var(--border); flex-shrink: 0; - background: var(--bg-2); + background: linear-gradient(90deg, var(--bg-2) 0%, transparent 100%); } .drawer-title { - font-size: 15px; + font-size: 16px; font-weight: 700; - letter-spacing: -.02em; + letter-spacing: -.01em; + background: linear-gradient(135deg, var(--text-1) 40%, var(--accent-2) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; } .drawer-body { overflow-y: auto; - padding: 20px; + padding: 22px; flex: 1; } -/* Dashboard stat cards */ +/* Dashboard stat cards - Enhanced */ .stat-grid { display: grid; grid-template-columns: 1fr 1fr; - gap: 10px; - margin-bottom: 22px; + gap: 12px; + margin-bottom: 24px; } .stat-card { background: var(--bg-2); border: 1px solid var(--border); border-radius: var(--radius-lg); - padding: 16px; + padding: 18px; text-align: center; position: relative; overflow: hidden; + transition: all var(--duration-base) var(--ease-smooth); + cursor: pointer; } .stat-card::before { @@ -2158,41 +2521,73 @@ textarea.field-input { top: 0; left: 0; right: 0; - height: 2px; + height: 3px; background: linear-gradient(90deg, var(--accent) 0%, #8b5cf6 100%); } + .stat-card::after { + content: ''; + position: absolute; + inset: 0; + background: var(--gradient-subtle); + opacity: 0; + transition: opacity var(--duration-base) var(--ease-smooth); + z-index: -1; + } + + .stat-card:hover { + border-color: var(--border-2); + box-shadow: var(--shadow-sm); + transform: translateY(-4px); + } + + .stat-card:hover::after { + opacity: 1; + } + .stat-card-err::before { - background: linear-gradient(90deg, #f87171, #f43f5e); + background: linear-gradient(90deg, #ef4444, #f87171); } .stat-card-err .stat-val { - color: #f87171; + color: #ef4444; } .stat-val { - font-size: 24px; + font-size: 26px; font-weight: 800; line-height: 1; color: var(--text-1); - letter-spacing: -.03em; + letter-spacing: -.02em; + transition: all var(--duration-fast) var(--ease-smooth); +} + +.stat-card:hover .stat-val { + font-size: 28px; } .stat-lbl { - font-size: 10.5px; + font-size: 11px; font-weight: 600; color: var(--text-3); - margin-top: 5px; + margin-top: 6px; text-transform: uppercase; - letter-spacing: .07em; + letter-spacing: .08em; + transition: color var(--duration-fast) var(--ease-smooth); +} + +.stat-card:hover .stat-lbl { + color: var(--text-2); } .dash-section { - margin-bottom: 16px; + margin-bottom: 18px; background: var(--bg-2); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; + box-shadow: 0 1px 2px var(--overlay); + transition: all var(--duration-base) var(--ease-smooth); } .dash-section-title { @@ -2251,7 +2646,7 @@ textarea.field-input { /* UTILITIES - */ +*/ code { font-family: var(--font-mono); font-size: .9em; @@ -2260,3 +2655,670 @@ code { padding: 1px 6px; border-radius: var(--radius-xs); } + +/** + * ADVANCED REQUEST BUILDER STYLES + * New UI components for enhanced request handling + */ + +/* Request Parameter Editor */ +.params-editor { + display: flex; + flex-direction: column; + gap: 12px; + padding: 12px; +} + +.param-group { + border: 1px solid var(--border); + border-radius: var(--radius-md); + background: var(--bg-2); + overflow: hidden; +} + +.param-group-header { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 12px; + background: var(--bg-3); + border-bottom: 1px solid var(--border); + font-size: 12px; + font-weight: 600; + cursor: pointer; + user-select: none; +} + +.param-group-header:hover { + background: var(--bg-4); +} + +.param-group-chevron { + width: 16px; + height: 16px; + stroke: currentColor; + transition: transform 0.2s; +} + +.param-group.collapsed .param-group-chevron { + transform: rotate(-90deg); +} + +.param-group.collapsed .param-items { + display: none; +} + +.param-items { + display: flex; + flex-direction: column; +} + +.param-row { + display: grid; + grid-template-columns: 1fr 2fr 1fr auto; + gap: 8px; + align-items: center; + padding: 10px 12px; + border-bottom: 1px solid var(--border); +} + +.param-row:last-child { + border-bottom: none; +} + +.param-row.required .param-name::before { + content: '*'; + color: #f87171; + margin-right: 4px; +} + +.param-name { + font-size: 12px; + font-weight: 500; + color: var(--text-1); + font-family: var(--font-mono); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.param-input { + padding: 6px 8px; + background: var(--bg-1); + border: 1px solid var(--border-2); + border-radius: var(--radius-sm); + color: var(--text-1); + font-family: var(--font-mono); + font-size: 12px; +} + +.param-input:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 2px var(--accent-dim); +} + +.param-type { + font-size: 11px; + color: var(--text-3); + padding: 4px 8px; + background: var(--bg-1); + border: 1px solid var(--border); + border-radius: var(--radius-sm); +} + +.param-remove { + width: 24px; + height: 24px; + padding: 4px; + background: none; + border: none; + color: var(--text-3); + cursor: pointer; + border-radius: var(--radius-sm); + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; +} + +.param-remove:hover { + background: rgba(248, 113, 113, 0.1); + color: #f87171; +} + +/* Code Block & Generator */ +.code-generator { + display: flex; + flex-direction: column; + gap: 8px; + padding: 12px; + background: var(--bg-2); + border-radius: var(--radius-md); + border: 1px solid var(--border); +} + +.code-lang-select { + display: flex; + gap: 6px; + flex-wrap: wrap; +} + +.code-lang-btn { + padding: 6px 12px; + background: var(--bg-1); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + font-size: 12px; + color: var(--text-2); + cursor: pointer; + transition: all 0.2s; + font-family: var(--font-mono); +} + +.code-lang-btn:hover { + border-color: var(--accent); + color: var(--accent); +} + +.code-lang-btn.active { + background: var(--accent); + border-color: var(--accent); + color: white; +} + +.code-block { + background: var(--code-bg); + border: 1px solid var(--code-border); + border-radius: var(--radius-lg); + padding: 14px; + font-family: var(--font-mono); + font-size: 12px; + overflow-x: auto; + max-height: 400px; + overflow-y: auto; + white-space: pre; + line-height: 1.6; + transition: all var(--duration-base) var(--ease-smooth)); + position: relative; + background: linear-gradient(135deg, var(--code-bg) 0%, color-mix(in srgb, var(--code-bg) 90%, var(--accent)) 100%); +} + + .code-block:hover { + border-color: var(--border-3); + box-shadow: 0 4px 12px var(--overlay); + } + +.code-block code { + background: none; + border: none; + padding: 0; + color: var(--text-1); + font-size: 12px; + line-height: 1.6; + letter-spacing: -.02em; +} + +.code-copy-btn { + align-self: flex-start; + padding: 7px 14px; + background: var(--accent-gradient); + border: none; + border-radius: var(--radius-md); + color: white; + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: all var(--duration-base) var(--ease-smooth)); + box-shadow: 0 2px 8px rgba(99,102,241,.2); +} + + .code-copy-btn:hover { + box-shadow: 0 4px 14px rgba(99,102,241,.35); + transform: translateY(-2px); + } + + .code-copy-btn:active { + transform: translateY(0); + } + + .code-copy-btn.copied { + background: linear-gradient(135deg, #10b981 0%, #34d399 100%); + box-shadow: 0 2px 8px rgba(16,185,129,.3); + } + +/* Response Preview - Enhanced */ +.response-preview { + margin-top: 12px; + border: 1px solid var(--border); + border-radius: var(--radius-lg); + background: var(--bg-2); + overflow: hidden; + box-shadow: 0 2px 8px var(--overlay); + transition: all var(--duration-base) var(--ease-smooth)); +} + +.response-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 14px; + background: linear-gradient(90deg, var(--bg-3) 0%, transparent 100%); + border-bottom: 1px solid var(--border); +} + +.response-status { + font-size: 13px; + font-weight: 700; + font-family: var(--font-mono); + letter-spacing: -.01em; +} + + .response-status.success { + color: #10b981; + text-shadow: 0 0 8px rgba(16,185,129,.2); + } + + .response-status.error { + color: #ef4444; + text-shadow: 0 0 8px rgba(239,68,68,.2); + } + +.response-timing { + font-size: 11px; + color: var(--text-3); + font-family: var(--font-mono); + font-weight: 500; + opacity: 0.8; +} + +.response-tabs { + display: flex; + gap: 0; + border-bottom: 1px solid var(--border); + background: var(--bg-2); +} + +.response-tab { + flex: 0 0 auto; + padding: 11px 14px; + background: none; + border: none; + border-bottom: 2px solid transparent; + font-size: 12px; + font-weight: 500; + color: var(--text-2); + cursor: pointer; + transition: all var(--duration-fast) var(--ease-smooth)); + position: relative; +} + +.response-tab:hover { + color: var(--text-1); +} + +.response-tab.active { + color: var(--accent); + border-bottom-color: var(--accent); +} + +.response-body { + padding: 12px; + font-family: var(--font-mono); + font-size: 11px; + white-space: pre-wrap; + word-break: break-word; + max-height: 400px; + overflow-y: auto; + color: var(--text-1); +} + +/* Workspaces & Collections */ +.workspace-panel { + display: flex; + flex-direction: column; + gap: 10px; + padding: 12px; +} + +.workspace-selector { + padding: 6px 10px; + background: var(--bg-1); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text-1); + font-size: 12px; + cursor: pointer; +} + +.workspace-selector:hover { + border-color: var(--accent); +} + +.collection-list { + display: flex; + flex-direction: column; + gap: 4px; +} + +.collection-item { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 10px; + background: var(--bg-2); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + cursor: pointer; + transition: all 0.2s; +} + +.collection-item:hover { + background: var(--bg-3); + border-color: var(--accent); +} + +.collection-item.active { + background: var(--accent-dim); + border-color: var(--accent); +} + +.collection-icon { + width: 16px; + height: 16px; + flex-shrink: 0; +} + +.collection-name { + flex: 1; + font-size: 12px; + color: var(--text-1); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.collection-count { + font-size: 11px; + color: var(--text-3); + padding: 2px 6px; + background: var(--bg-1); + border-radius: var(--radius-xs); +} + +/* Request Save Dialog */ +.save-request-dialog { + display: flex; + flex-direction: column; + gap: 12px; +} + +.dialog-section { + display: flex; + flex-direction: column; + gap: 6px; +} + +.dialog-label { + font-size: 12px; + font-weight: 500; + color: var(--text-1); +} + +.dialog-input { + padding: 8px 10px; + background: var(--bg-1); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text-1); + font-size: 12px; +} + +.dialog-input:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 2px var(--accent-dim); +} + +.dialog-textarea { + padding: 8px 10px; + background: var(--bg-1); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text-1); + font-size: 12px; + font-family: var(--font-sans); + min-height: 80px; + resize: vertical; +} + +.dialog-textarea:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 2px var(--accent-dim); +} + +.dialog-buttons { + display: flex; + gap: 8px; + justify-content: flex-end; +} + +.btn-primary { + padding: 8px 16px; + background: var(--accent); + border: none; + border-radius: var(--radius-sm); + color: white; + font-size: 12px; + font-weight: 500; + cursor: pointer; + transition: background 0.2s; +} + +.btn-primary:hover { + background: var(--accent-2); +} + +.btn-secondary { + padding: 8px 16px; + background: var(--bg-3); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text-1); + font-size: 12px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; +} + +.btn-secondary:hover { + border-color: var(--accent); + color: var(--accent); +} + +/* Environment Manager */ +.env-list { + display: flex; + flex-direction: column; + gap: 6px; +} + +.env-item { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 10px; + background: var(--bg-2); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + cursor: pointer; + transition: all 0.2s; +} + +.env-item:hover { + background: var(--bg-3); +} + +.env-item.active { + background: var(--accent-dim); + border-color: var(--accent); +} + +.env-radio { + width: 16px; + height: 16px; + border-radius: 50%; + border: 2px solid currentColor; + cursor: pointer; +} + +.env-item.active .env-radio { + background: var(--accent); + border-color: var(--accent); +} + +.env-name { + flex: 1; + font-size: 12px; + color: var(--text-1); +} + +/* Validation Errors & Warnings */ +.validation-error { + display: flex; + align-items: flex-start; + gap: 8px; + padding: 10px; + background: rgba(248, 113, 113, 0.1); + border: 1px solid rgba(248, 113, 113, 0.3); + border-radius: var(--radius-sm); + font-size: 12px; + color: #f87171; +} + +.validation-warning { + display: flex; + align-items: flex-start; + gap: 8px; + padding: 10px; + background: rgba(251, 191, 36, 0.1); + border: 1px solid rgba(251, 191, 36, 0.3); + border-radius: var(--radius-sm); + font-size: 12px; + color: #fbbf24; +} + +.validation-icon { + width: 14px; + height: 14px; + flex-shrink: 0; + margin-top: 2px; +} + +.validation-message { + flex: 1; +} + +/* Loading & Skeleton States */ +.skeleton-line { + height: 12px; + background: linear-gradient(90deg, var(--bg-3) 0%, var(--bg-4) 50%, var(--bg-3) 100%); + background-size: 200% 100%; + animation: skeleton-loading 1.5s infinite; + border-radius: var(--radius-xs); +} + +@keyframes skeleton-loading { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} + +.loading-spinner { + display: inline-block; + width: 16px; + height: 16px; + border: 2px solid var(--border); + border-top-color: var(--accent); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* Keyboard Shortcut Hints */ +.kbd { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 20px; + height: 20px; + padding: 0 4px; + background: var(--bg-3); + border: 1px solid var(--border); + border-radius: var(--radius-xs); + font-family: var(--font-mono); + font-size: 10px; + color: var(--text-2); + font-weight: 500; +} + +/* Tags */ +.tag { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 8px; + background: var(--accent-dim); + border: 1px solid var(--accent); + border-radius: var(--radius-sm); + font-size: 11px; + color: var(--accent); +} + +.tag-remove { + display: flex; + align-items: center; + justify-content: center; + width: 12px; + height: 12px; + background: none; + border: none; + color: inherit; + cursor: pointer; + opacity: 0.7; + transition: opacity 0.2s; +} + +.tag-remove:hover { + opacity: 1; +} + +/* Responsive adjustments */ +@media (max-width: 1024px) { + .param-row { + grid-template-columns: 1fr 1fr auto; + } + + .param-type { + display: none; + } +} + +@media (max-width: 768px) { + .param-row { + grid-template-columns: 1fr; + } + + .code-lang-btn { + flex: 1; + } +} diff --git a/DoxaApi/UI/Assets/app.js b/DoxaApi/UI/Assets/app.js index e99716f..d5d9140 100644 --- a/DoxaApi/UI/Assets/app.js +++ b/DoxaApi/UI/Assets/app.js @@ -1610,7 +1610,7 @@ dashboardPanel.style.display = "block"; try { - const res = await fetch("analytics"); + const res = await fetch(new URL("analytics", document.baseURI)); if (!res.ok) throw new Error("Failed to load analytics"); const analyticsData = await res.json(); @@ -1619,7 +1619,7 @@ // Refresh every 5 seconds if (window.dashboardInterval) clearInterval(window.dashboardInterval); window.dashboardInterval = setInterval(async () => { - const res = await fetch("analytics"); + const res = await fetch(new URL("analytics", document.baseURI)); if (res.ok) { const data = await res.json(); updateDashboard(data); @@ -1689,5 +1689,458 @@ `; } + /** + * Enhanced Try Pane - Advanced Request Builder + * Integrates RequestBuilder and CodeGenerator modules + */ + function renderAdvancedTryPane(group, endpoint) { + if (!endpoint) return renderTryEmpty(); + + const paramState = new RequestBuilder.ParameterState(); + RequestBuilder.populateDefaults(endpoint, paramState); + + let html = ` +
+
+
${escapeHtml(endpoint.method)} ${escapeHtml(endpoint.path)}
+ ${endpoint.deprecated ? 'DEPRECATED' : ''} +
+ +
+ `; + + // Path parameters + if (endpoint.parameters?.filter(p => p.in === 'path').length > 0) { + html += renderParamGroup('Path Parameters', endpoint.parameters.filter(p => p.in === 'path'), paramState); + } + + // Query parameters + if (endpoint.parameters?.filter(p => p.in === 'query').length > 0) { + html += renderParamGroup('Query Parameters', endpoint.parameters.filter(p => p.in === 'query'), paramState); + } + + // Headers + if (endpoint.parameters?.filter(p => p.in === 'header').length > 0) { + html += renderParamGroup('Headers', endpoint.parameters.filter(p => p.in === 'header'), paramState); + } + + // Request body + if (endpoint.requestBody) { + html += ` +
+
+ + + + Request Body +
+
+
+ ${endpoint.requestBody.example ? JSON.stringify(endpoint.requestBody.example, null, 2) : '{}'} +
+
+
+ `; + } + + html += `
`; + + // Code Generator + html += ` +
+
Generate Code
+
+ `; + + CodeGenerator.getSupportedLanguages().forEach(lang => { + html += ``; + }); + + html += ` +
+
+ // Select a language above +
+ +
+ `; + + // Response preview area + html += ` + + `; + + html += `
`; + + el.try.innerHTML = html; + + // Bind code generator + document.querySelectorAll('.code-lang-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + document.querySelectorAll('.code-lang-btn').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + activeCodeLang = btn.dataset.lang; + updateCodePreview(endpoint, paramState); + }); + }); + + // Initial code preview + updateCodePreview(endpoint, paramState); + + // Copy code button + document.getElementById('codeCopyBtn')?.addEventListener('click', () => { + const code = document.getElementById('codeBlock').textContent; + navigator.clipboard.writeText(code).then(() => { + const btn = document.getElementById('codeCopyBtn'); + btn.classList.add('copied'); + btn.textContent = 'Copied!'; + setTimeout(() => { + btn.classList.remove('copied'); + btn.textContent = 'Copy Code'; + }, 2000); + }); + }); + } + + /** + * Render parameter group + */ + function renderParamGroup(name, parameters, paramState) { + if (!parameters.length) return ''; + + const paramIn = parameters[0].in; + let html = ` +
+
+ + + + ${name} +
+
+ `; + + parameters.forEach(param => { + html += ` +
+
${escapeHtml(param.name)}
+ +
${param.schema?.type || 'string'}
+ +
+ `; + }); + + html += `
`; + return html; + } + + /** + * Update code preview based on current parameters + */ + function updateCodePreview(endpoint, paramState) { + const currentLangBtn = document.querySelector('.code-lang-btn.active'); + const lang = currentLangBtn?.dataset.lang || 'curl'; + + // Gather current param values from UI + document.querySelectorAll('.param-input').forEach(input => { + const paramIn = input.dataset.paramIn; + const paramName = input.dataset.paramName; + if (paramIn && paramName) { + paramState[paramIn][paramName] = input.value; + } + }); + + // Get request body if present + const bodyInput = document.querySelector('[data-request-body="true"]'); + if (bodyInput) { + try { + paramState.body = JSON.parse(bodyInput.textContent); + } catch (e) { + paramState.body = bodyInput.textContent; + } + } + + const request = RequestBuilder.buildRequest(endpoint, paramState, window.location.origin); + const code = CodeGenerator.generate(lang, request); + document.getElementById('codeBlock').textContent = code; + } + + /** + * Integration point for workspace manager + */ + function initializeWorkspaceManager() { + // Load initial workspaces + const workspaces = WorkspaceManager.getWorkspaces(); + if (workspaces.length === 0) { + const defaultWs = new WorkspaceManager.Workspace('My Workspace'); + const defaultCol = new WorkspaceManager.Collection('API Collection'); + defaultWs.collections.push(defaultCol); + WorkspaceManager.saveWorkspace(defaultWs); + } + + // Setup environment selector if environments panel exists + const envList = document.querySelector('.env-list'); + if (envList) { + const environments = WorkspaceManager.getEnvironments(); + if (environments.length === 0) { + const defaultEnv = new WorkspaceManager.Environment('Default Environment'); + WorkspaceManager.saveEnvironment(defaultEnv); + } + } + } + + /** + * Save current request to collection + */ + function saveCurrentRequest() { + if (!activeEndpoint || !spec) return; + + const name = prompt('Request name:', `${activeEndpoint.method} ${activeEndpoint.path}`); + if (!name) return; + + const workspace = WorkspaceManager.getCurrentWorkspace(); + const collection = workspace.collections[0]; + if (!collection) return; + + const saved = new WorkspaceManager.SavedRequest( + name, + activeEndpoint.method, + activeEndpoint.path + ); + saved.description = activeEndpoint.description || ''; + + // Gather current parameters + document.querySelectorAll('.param-input').forEach(input => { + const paramIn = input.dataset.paramIn; + const paramName = input.dataset.paramName; + if (paramIn && paramName && input.value) { + saved.params[paramIn][paramName] = input.value; + } + }); + + // Save body + const bodyInput = document.querySelector('[data-request-body="true"]'); + if (bodyInput) { + saved.body = bodyInput.textContent; + } + + WorkspaceManager.saveRequestToCollection(workspace.id, collection.id, saved); + alert(`Request saved to "${collection.name}"`); + } + + /** + * Keyboard shortcuts and accessibility + */ + function initializeKeyboardShortcuts() { + const shortcuts = { + '/': () => document.getElementById('searchInput')?.focus(), + 'KeyS': (e) => e.ctrlKey && saveCurrentRequest(), + 'KeyK': (e) => e.ctrlKey && toggleCommandPalette(), + 'Tab': (e) => handleTabNavigation(e), + 'Escape': () => closeModals() + }; + + document.addEventListener('keydown', (e) => { + // Don't trigger shortcuts when typing in input fields + if (e.target.matches('input, textarea, [contenteditable]')) { + if (e.key === 'Escape') { + e.target.blur(); + } + return; + } + + const handler = shortcuts[e.code] || shortcuts[e.key]; + if (handler) { + handler(e); + } + }); + + // Show keyboard shortcut hints + addShortcutHints(); + } + + /** + * Add visual keyboard shortcut hints to UI + */ + function addShortcutHints() { + const searchInput = document.getElementById('searchInput'); + if (searchInput && !searchInput.parentElement.querySelector('.kbd')) { + const kbd = document.createElement('kbd'); + kbd.className = 'kbd search-kbd'; + kbd.textContent = '/'; + searchInput.parentElement.appendChild(kbd); + } + } + + /** + * Toggle command palette (future enhancement) + */ + function toggleCommandPalette() { + // Placeholder for future command palette implementation + console.log('Command palette not yet implemented'); + } + + /** + * Handle Tab navigation for better accessibility + */ + function handleTabNavigation(e) { + const focusableElements = document.querySelectorAll( + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' + ); + + if (focusableElements.length === 0) return; + + const currentIndex = Array.from(focusableElements).indexOf(document.activeElement); + let nextIndex = e.shiftKey ? currentIndex - 1 : currentIndex + 1; + + if (nextIndex >= focusableElements.length) nextIndex = 0; + if (nextIndex < 0) nextIndex = focusableElements.length - 1; + + const nextElement = focusableElements[nextIndex]; + if (nextElement) { + e.preventDefault(); + nextElement.focus(); + } + } + + /** + * Close all open modals + */ + function closeModals() { + document.querySelectorAll('[role="dialog"], .modal, .drawer').forEach(modal => { + modal.style.display = 'none'; + }); + } + + /** + * Enhance accessibility with ARIA labels + */ + function enhanceAccessibility() { + // Add ARIA roles and labels + const sidebar = document.getElementById('sidebar'); + if (sidebar) sidebar.setAttribute('role', 'navigation'); + + const detailPane = document.getElementById('detailPane'); + if (detailPane) detailPane.setAttribute('role', 'main'); + + const tryPane = document.getElementById('tryPane'); + if (tryPane) tryPane.setAttribute('role', 'complementary'); + + // Add aria-labels to icon buttons + document.querySelectorAll('.icon-btn').forEach(btn => { + if (!btn.getAttribute('aria-label')) { + btn.setAttribute('aria-label', btn.title || btn.textContent); + } + }); + + // Add roles to custom components + document.querySelectorAll('.param-group-header').forEach(header => { + header.setAttribute('role', 'button'); + header.setAttribute('tabindex', '0'); + }); + + document.querySelectorAll('[data-overview], [data-endpoint]').forEach(link => { + link.setAttribute('role', 'menuitem'); + }); + } + + /** + * Add focus visible styles for keyboard navigation + */ + function initializeFocusStyles() { + const style = document.createElement('style'); + style.textContent = ` + :focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + } + + button:focus-visible, + a:focus-visible, + input:focus-visible { + outline: 2px solid var(--accent); + outline-offset: -2px; + } + `; + document.head.appendChild(style); + } + + /** + * Enhanced error handling and user feedback + */ + function showNotification(message, type = 'info', duration = 3000) { + const notification = document.createElement('div'); + notification.className = `notification notification-${type}`; + notification.setAttribute('role', 'status'); + notification.setAttribute('aria-live', 'polite'); + notification.textContent = message; + notification.style.cssText = ` + position: fixed; + bottom: 20px; + right: 20px; + padding: 12px 16px; + background: var(--bg-2); + border: 1px solid var(--border); + border-radius: var(--radius-md); + color: var(--text-1); + z-index: 9999; + animation: slideIn 0.3s ease-out; + `; + + document.body.appendChild(notification); + + if (duration > 0) { + setTimeout(() => { + notification.style.animation = 'slideOut 0.3s ease-out'; + setTimeout(() => notification.remove(), 300); + }, duration); + } + + return notification; + } + + /** + * Improved error logging + */ + const originalConsoleError = console.error; + console.error = function(...args) { + originalConsoleError.apply(console, args); + const message = args[0]?.toString?.() || String(args[0]); + if (message && !message.includes('HTTP')) { + showNotification(`Error: ${message.substring(0, 50)}`, 'error', 5000); + } + }; + + /** + * Initialize all enhancements + */ + function initializeEnhancements() { + initializeWorkspaceManager(); + initializeKeyboardShortcuts(); + enhanceAccessibility(); + initializeFocusStyles(); + } + + // Call on load + initializeEnhancements(); + init(); })(); \ No newline at end of file diff --git a/DoxaApi/UI/Assets/code-generator.js b/DoxaApi/UI/Assets/code-generator.js new file mode 100644 index 0000000..a5da9d5 --- /dev/null +++ b/DoxaApi/UI/Assets/code-generator.js @@ -0,0 +1,407 @@ +/** + * Code Generation Module + * Generates API request code snippets in multiple languages and frameworks + */ +const CodeGenerator = (() => { + "use strict"; + + /** + * Generate code in specified language + */ + function generate(language, request, options = {}) { + const generator = generators[language.toLowerCase()]; + if (!generator) { + return `// Language "${language}" not supported`; + } + return generator(request, options); + } + + /** + * Get list of supported languages + */ + function getSupportedLanguages() { + return Object.keys(generators).sort(); + } + + // Language generators + const generators = { + curl: (req) => generateCurl(req), + fetch: (req) => generateFetch(req), + axios: (req) => generateAxios(req), + httpclient: (req) => generateHttpClient(req), + httppy: (req) => generatePythonHttp(req), + requests: (req) => generatePythonRequests(req), + httpgo: (req) => generateGoHttp(req), + csharp: (req) => generateCSharp(req), + javascript: (req) => generateFetch(req), + typescript: (req) => generateFetch(req), + ruby: (req) => generateRuby(req), + php: (req) => generatePhp(req), + java: (req) => generateJava(req), + kotlin: (req) => generateKotlin(req), + powershell: (req) => generatePowerShell(req), + swift: (req) => generateSwift(req), + }; + + function escapeString(str) { + return str + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"') + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r') + .replace(/\t/g, '\\t'); + } + + function escapeStringForJson(str) { + return JSON.stringify(str); + } + + // ============= cURL ============= + function generateCurl(req) { + let cmd = 'curl -X ' + req.method + ' '; + + // Headers + Object.entries(req.headers || {}).forEach(([key, value]) => { + cmd += `-H ${escapeStringForJson(key + ': ' + value)} `; + }); + + // Body + if (req.body) { + cmd += `-d ${escapeStringForJson(req.body)} `; + } + + // URL + cmd += escapeStringForJson(req.url); + + return cmd; + } + + // ============= Fetch API ============= + function generateFetch(req) { + let code = `const url = '${escapeString(req.url)}';\n\n`; + code += `const options = {\n`; + code += ` method: '${req.method}',\n`; + + if (Object.keys(req.headers || {}).length > 0) { + code += ` headers: {\n`; + Object.entries(req.headers).forEach(([key, value]) => { + code += ` '${escapeString(key)}': '${escapeString(String(value))}',\n`; + }); + code += ` },\n`; + } + + if (req.body) { + code += ` body: ${typeof req.body === 'string' && req.body.startsWith('{') ? 'JSON.stringify(' + req.body + ')' : escapeStringForJson(req.body)},\n`; + } + + code += `};\n\n`; + code += `try {\n`; + code += ` const response = await fetch(url, options);\n`; + code += ` const data = await response.json();\n`; + code += ` console.log('Success:', data);\n`; + code += `} catch (error) {\n`; + code += ` console.error('Error:', error);\n`; + code += `}`; + + return code; + } + + // ============= Axios ============= + function generateAxios(req) { + let code = `import axios from 'axios';\n\n`; + code += `const config = {\n`; + code += ` method: '${req.method.toLowerCase()}',\n`; + code += ` url: '${escapeString(req.url)}',\n`; + + if (Object.keys(req.headers || {}).length > 0) { + code += ` headers: {\n`; + Object.entries(req.headers).forEach(([key, value]) => { + code += ` '${escapeString(key)}': '${escapeString(String(value))}',\n`; + }); + code += ` },\n`; + } + + if (req.body) { + code += ` data: ${typeof req.body === 'string' && req.body.startsWith('{') ? req.body : escapeStringForJson(req.body)},\n`; + } + + code += `};\n\n`; + code += `axios(config)\n`; + code += ` .then((response) => console.log(response.data))\n`; + code += ` .catch((error) => console.error(error));`; + + return code; + } + + // ============= Python - Requests ============= + function generatePythonRequests(req) { + let code = `import requests\n\n`; + code += `url = '${escapeString(req.url)}'\n`; + + if (Object.keys(req.headers || {}).length > 0) { + code += `headers = {\n`; + Object.entries(req.headers).forEach(([key, value]) => { + code += ` '${escapeString(key)}': '${escapeString(String(value))}',\n`; + }); + code += `}\n`; + } else { + code += `headers = {}\n`; + } + + if (req.body) { + code += `\ndata = ${req.body}\n\n`; + code += `response = requests.${req.method.toLowerCase()}(url, json=data, headers=headers)\n`; + } else { + code += `\nresponse = requests.${req.method.toLowerCase()}(url, headers=headers)\n`; + } + + code += `print(response.json())`; + + return code; + } + + // ============= Python - HTTP ============= + function generatePythonHttp(req) { + let code = `import http.client\nimport json\n\n`; + const url = new URL(req.url); + const host = url.hostname; + const path = url.pathname + url.search; + + code += `conn = http.client.HTTPSConnection("${host}")\n`; + + if (Object.keys(req.headers || {}).length > 0) { + code += `headers = {\n`; + Object.entries(req.headers).forEach(([key, value]) => { + code += ` "${escapeString(key)}": "${escapeString(String(value))}",\n`; + }); + code += `}\n`; + } else { + code += `headers = {}\n`; + } + + if (req.body) { + code += `\nbody = ${req.body}\n`; + code += `conn.request("${req.method}", "${escapeString(path)}", json.dumps(body), headers)\n`; + } else { + code += `\nconn.request("${req.method}", "${escapeString(path)}", headers=headers)\n`; + } + + code += `response = conn.getresponse()\ndata = response.read()\nprint(data.decode())`; + + return code; + } + + // ============= Go ============= + function generateGoHttp(req) { + let code = `package main\n\nimport (\n\t"fmt"\n\t"io"\n\t"net/http"\n)\n\n`; + code += `func main() {\n`; + code += `\treq, _ := http.NewRequest("${req.method}", "${escapeString(req.url)}", nil)\n`; + + Object.entries(req.headers || {}).forEach(([key, value]) => { + code += `\treq.Header.Add("${escapeString(key)}", "${escapeString(String(value))}")\n`; + }); + + code += `\n\tres, _ := http.DefaultClient.Do(req)\n`; + code += `\tdefer res.Body.Close()\n`; + code += `\tbody, _ := io.ReadAll(res.Body)\n`; + code += `\tfmt.Println(string(body))\n`; + code += `}`; + + return code; + } + + // ============= C# / .NET ============= + function generateCSharp(req) { + let code = `using System;\nusing System.Net.Http;\nusing System.Threading.Tasks;\n\n`; + code += `class Program {\n`; + code += ` static async Task Main() {\n`; + code += ` using (var client = new HttpClient()) {\n`; + + Object.entries(req.headers || {}).forEach(([key, value]) => { + if (key.toLowerCase() !== 'content-type') { + code += ` client.DefaultRequestHeaders.Add("${escapeString(key)}", "${escapeString(String(value))}");\n`; + } + }); + + if (req.body) { + code += `\n var content = new StringContent("${escapeString(req.body)}", null, "application/json");\n`; + code += ` var response = await client.${req.method.charAt(0) + req.method.slice(1).toLowerCase()}Async("${escapeString(req.url)}", content);\n`; + } else { + code += `\n var response = await client.GetAsync("${escapeString(req.url)}");\n`; + } + + code += ` var result = await response.Content.ReadAsStringAsync();\n`; + code += ` Console.WriteLine(result);\n`; + code += ` }\n`; + code += ` }\n`; + code += `}`; + + return code; + } + + // ============= Ruby ============= + function generateRuby(req) { + let code = `require 'net/http'\nrequire 'json'\n\n`; + code += `url = URI('${escapeString(req.url)}')\n`; + code += `http = Net::HTTP.new(url.host, url.port)\n`; + code += `http.use_ssl = true\n\n`; + + const methodMap = { 'GET': 'Get', 'POST': 'Post', 'PUT': 'Put', 'PATCH': 'Patch', 'DELETE': 'Delete' }; + const method = methodMap[req.method] || 'Get'; + + code += `request = Net::HTTP::${method}.new(url.request_uri)\n`; + + Object.entries(req.headers || {}).forEach(([key, value]) => { + code += `request['${escapeString(key)}'] = '${escapeString(String(value))}'\n`; + }); + + if (req.body) { + code += `request.body = ${req.body}\n`; + } + + code += `\nresponse = http.request(request)\nputs response.body`; + + return code; + } + + // ============= PHP ============= + function generatePhp(req) { + let code = ` array(\n`; + code += ` 'method' => $method,\n`; + + if (Object.keys(req.headers || {}).length > 0) { + code += ` 'header' => array(\n`; + Object.entries(req.headers).forEach(([key, value]) => { + code += ` '${escapeString(key)}: ${String(value)}',\n`; + }); + code += ` ),\n`; + } + + if (req.body) { + code += ` 'content' => '${escapeString(req.body)}',\n`; + } + + code += ` )\n);\n\n`; + code += `$context = stream_context_create($options);\n`; + code += `$response = file_get_contents($url, false, $context);\n`; + code += `echo $response;\n\n?>`; + + return code; + } + + // ============= Java ============= + function generateJava(req) { + let code = `import java.net.http.HttpClient;\n`; + code += `import java.net.http.HttpRequest;\n`; + code += `import java.net.http.HttpResponse;\n`; + code += `import java.net.URI;\n\n`; + code += `public class ApiRequest {\n`; + code += ` public static void main(String[] args) throws Exception {\n`; + code += ` HttpClient client = HttpClient.newHttpClient();\n\n`; + code += ` HttpRequest request = HttpRequest.newBuilder()\n`; + code += ` .uri(URI.create("${escapeString(req.url)}"))\n`; + code += ` .method("${req.method}", HttpRequest.BodyPublishers.noBody())\n`; + + Object.entries(req.headers || {}).forEach(([key, value]) => { + code += ` .header("${escapeString(key)}", "${escapeString(String(value))}")\n`; + }); + + code += ` .build();\n\n`; + code += ` HttpResponse response = client.send(request,\n`; + code += ` HttpResponse.BodyHandlers.ofString());\n\n`; + code += ` System.out.println(response.body());\n`; + code += ` }\n`; + code += `}`; + + return code; + } + + // ============= Kotlin ============= + function generateKotlin(req) { + let code = `import java.net.HttpURLConnection\nimport java.net.URL\n\n`; + code += `fun main() {\n`; + code += ` val url = URL("${escapeString(req.url)}")\n`; + code += ` val connection = url.openConnection() as HttpURLConnection\n`; + code += ` connection.requestMethod = "${req.method}"\n\n`; + + Object.entries(req.headers || {}).forEach(([key, value]) => { + code += ` connection.setRequestProperty("${escapeString(key)}", "${escapeString(String(value))}")\n`; + }); + + code += `\n val inputStream = connection.inputStream\n`; + code += ` val response = inputStream.bufferedReader().use { it.readText() }\n`; + code += ` println(response)\n`; + code += `}`; + + return code; + } + + // ============= PowerShell ============= + function generatePowerShell(req) { + let code = `$uri = "${escapeString(req.url)}"\n`; + code += `$method = "${req.method}"\n`; + + if (Object.keys(req.headers || {}).length > 0) { + code += `$headers = @{\n`; + Object.entries(req.headers).forEach(([key, value]) => { + code += ` "${escapeString(key)}" = "${escapeString(String(value))}"\n`; + }); + code += `}\n\n`; + } + + if (req.body) { + code += `$body = '${escapeString(req.body)}'\n`; + code += `$response = Invoke-WebRequest -Uri $uri -Method $method -Headers $headers -Body $body\n`; + } else { + code += `$response = Invoke-WebRequest -Uri $uri -Method $method`; + if (Object.keys(req.headers || {}).length > 0) { + code += ` -Headers $headers`; + } + code += `\n`; + } + + code += `$response.Content`; + + return code; + } + + // ============= Swift ============= + function generateSwift(req) { + let code = `import Foundation\n\nlet url = URL(string: "${escapeString(req.url)}")!\n`; + code += `var request = URLRequest(url: url)\n`; + code += `request.httpMethod = "${req.method}"\n\n`; + + Object.entries(req.headers || {}).forEach(([key, value]) => { + code += `request.setValue("${escapeString(String(value))}", forHTTPHeaderField: "${escapeString(key)}")\n`; + }); + + if (req.body) { + code += `\nrequest.httpBody = "${escapeString(req.body)}".data(using: .utf8)\n`; + } + + code += `\nlet task = URLSession.shared.dataTask(with: request) { data, response, error in\n`; + code += ` if let data = data {\n`; + code += ` let json = String(data: data, encoding: .utf8)!\n`; + code += ` print(json)\n`; + code += ` }\n`; + code += `}\n`; + code += `task.resume()`; + + return code; + } + + // Public API + return { + generate, + getSupportedLanguages, + escapeString + }; +})(); + +// Export for use in other modules +if (typeof module !== 'undefined' && module.exports) { + module.exports = CodeGenerator; +} diff --git a/DoxaApi/UI/Assets/index.html b/DoxaApi/UI/Assets/index.html index f1d2842..ea1805a 100644 --- a/DoxaApi/UI/Assets/index.html +++ b/DoxaApi/UI/Assets/index.html @@ -115,6 +115,11 @@ + + + + + diff --git a/DoxaApi/UI/Assets/request-builder.js b/DoxaApi/UI/Assets/request-builder.js new file mode 100644 index 0000000..0c8fc1b --- /dev/null +++ b/DoxaApi/UI/Assets/request-builder.js @@ -0,0 +1,399 @@ +/** + * Advanced Request Builder Module + * Handles parameter composition, validation, defaults, and intelligent request building + */ +const RequestBuilder = (() => { + "use strict"; + + /** + * Parameter state management + */ + class ParameterState { + constructor() { + this.path = {}; + this.query = {}; + this.header = {}; + this.cookie = {}; + this.body = null; + this.bodyType = 'json'; // json, form, raw, binary + this.formData = {}; + } + + clone() { + const copy = new ParameterState(); + copy.path = { ...this.path }; + copy.query = { ...this.query }; + copy.header = { ...this.header }; + copy.cookie = { ...this.cookie }; + copy.body = this.body; + copy.bodyType = this.bodyType; + copy.formData = { ...this.formData }; + return copy; + } + + clear() { + this.path = {}; + this.query = {}; + this.header = {}; + this.cookie = {}; + this.body = null; + this.bodyType = 'json'; + this.formData = {}; + } + } + + /** + * Build request object from endpoint definition and parameter state + */ + function buildRequest(endpoint, paramState, baseUrl) { + const req = { + method: endpoint.method, + path: resolvePath(endpoint.path, paramState.path), + url: buildUrl(baseUrl, endpoint.path, paramState), + headers: buildHeaders(endpoint, paramState), + query: paramState.query, + body: buildBody(endpoint, paramState), + cookies: paramState.cookie + }; + return req; + } + + /** + * Resolve path parameters + */ + function resolvePath(pathTemplate, pathParams) { + let path = pathTemplate; + Object.entries(pathParams).forEach(([key, value]) => { + // Encode URI component but preserve slashes in path + path = path.replace(`:${key}`, encodeURIComponent(String(value || ''))); + path = path.replace(`{${key}}`, encodeURIComponent(String(value || ''))); + }); + return path; + } + + /** + * Build complete URL with query parameters + */ + function buildUrl(baseUrl, path, paramState) { + let resolvedPath = resolvePath(path, paramState.path); + let url = baseUrl.replace(/\/$/, '') + resolvedPath; + + const queryParams = new URLSearchParams(); + Object.entries(paramState.query).forEach(([key, value]) => { + if (value !== undefined && value !== null && value !== '') { + queryParams.append(key, String(value)); + } + }); + + const qs = queryParams.toString(); + if (qs) url += '?' + qs; + return url; + } + + /** + * Build headers including auth, content-type, etc + */ + function buildHeaders(endpoint, paramState) { + const headers = {}; + + // User-provided headers + Object.entries(paramState.header).forEach(([key, value]) => { + if (value !== undefined && value !== null && value !== '') { + headers[key] = String(value); + } + }); + + // Auto content-type based on body + if (paramState.body !== null && paramState.body !== undefined) { + if (paramState.bodyType === 'json') { + headers['Content-Type'] = 'application/json'; + } else if (paramState.bodyType === 'form') { + headers['Content-Type'] = 'application/x-www-form-urlencoded'; + } + } + + // Copy cookies to header if not already set + if (Object.keys(paramState.cookie).length > 0) { + const cookieStr = Object.entries(paramState.cookie) + .filter(([, v]) => v != null && v !== '') + .map(([k, v]) => `${k}=${encodeURIComponent(v)}`) + .join('; '); + if (cookieStr) headers['Cookie'] = cookieStr; + } + + return headers; + } + + /** + * Build request body + */ + function buildBody(endpoint, paramState) { + if (!paramState.body) return null; + + if (paramState.bodyType === 'json') { + if (typeof paramState.body === 'string') { + try { + // Validate JSON + JSON.parse(paramState.body); + return paramState.body; + } catch (e) { + return null; // Invalid JSON + } + } + return JSON.stringify(paramState.body); + } + + if (paramState.bodyType === 'form') { + const params = new URLSearchParams(); + Object.entries(paramState.formData).forEach(([key, value]) => { + if (value !== undefined && value !== null && value !== '') { + params.append(key, String(value)); + } + }); + return params.toString(); + } + + return paramState.body; + } + + /** + * Validate parameters against endpoint schema + */ + function validateParameters(endpoint, paramState) { + const errors = []; + const warnings = []; + + if (!endpoint.parameters) return { errors, warnings }; + + endpoint.parameters.forEach((param) => { + const value = paramState[param.in]?.[param.name]; + const isEmpty = value === undefined || value === null || value === ''; + + // Check required + if (param.required && isEmpty) { + errors.push(`Parameter '${param.name}' is required`); + } + + // Validate type and format + if (!isEmpty) { + const validation = validateParameterValue(value, param); + if (!validation.valid) { + errors.push(`Parameter '${param.name}': ${validation.message}`); + } + } + + // Check for deprecated params + if (param.deprecated) { + warnings.push(`Parameter '${param.name}' is deprecated`); + } + }); + + return { errors, warnings }; + } + + /** + * Validate a parameter value against its schema + */ + function validateParameterValue(value, param) { + const strValue = String(value); + + // Validate type + if (param.schema?.type) { + switch (param.schema.type) { + case 'integer': + if (!/^-?\d+$/.test(strValue)) { + return { valid: false, message: `must be an integer` }; + } + if (param.schema.minimum !== undefined && parseInt(strValue) < param.schema.minimum) { + return { valid: false, message: `must be >= ${param.schema.minimum}` }; + } + if (param.schema.maximum !== undefined && parseInt(strValue) > param.schema.maximum) { + return { valid: false, message: `must be <= ${param.schema.maximum}` }; + } + break; + + case 'number': + if (!/^-?\d+(\.\d+)?$/.test(strValue)) { + return { valid: false, message: `must be a number` }; + } + break; + + case 'boolean': + if (!['true', 'false', '1', '0'].includes(strValue.toLowerCase())) { + return { valid: false, message: `must be true or false` }; + } + break; + + case 'string': + if (param.schema.minLength !== undefined && strValue.length < param.schema.minLength) { + return { valid: false, message: `must be at least ${param.schema.minLength} characters` }; + } + if (param.schema.maxLength !== undefined && strValue.length > param.schema.maxLength) { + return { valid: false, message: `must be at most ${param.schema.maxLength} characters` }; + } + if (param.schema.pattern && !new RegExp(param.schema.pattern).test(strValue)) { + return { valid: false, message: `invalid format` }; + } + break; + } + } + + // Validate enum + if (param.schema?.enum && !param.schema.enum.includes(value)) { + return { valid: false, message: `must be one of: ${param.schema.enum.join(', ')}` }; + } + + return { valid: true }; + } + + /** + * Fill parameters from examples or defaults + */ + function populateDefaults(endpoint, paramState) { + if (!endpoint.parameters) return; + + endpoint.parameters.forEach((param) => { + if (!paramState[param.in]) paramState[param.in] = {}; + + // Skip if already set + if (paramState[param.in][param.name] !== undefined && paramState[param.in][param.name] !== null) { + return; + } + + // Try example + if (param.example !== undefined) { + paramState[param.in][param.name] = param.example; + return; + } + + // Try schema default + if (param.schema?.default !== undefined) { + paramState[param.in][param.name] = param.schema.default; + return; + } + + // Provide smart defaults based on type + if (param.schema?.type) { + switch (param.schema.type) { + case 'integer': + paramState[param.in][param.name] = param.schema.minimum || 1; + break; + case 'number': + paramState[param.in][param.name] = param.schema.minimum || 0.0; + break; + case 'boolean': + paramState[param.in][param.name] = true; + break; + case 'string': + paramState[param.in][param.name] = ''; + break; + } + } + }); + } + + /** + * Generate example body from schema + */ + function generateExampleBody(schema, schemas) { + if (!schema) return null; + + // Handle references + if (schema.$ref) { + const refName = schema.$ref.split('/').pop(); + schema = schemas?.[refName] || schema; + } + + if (schema.type === 'object') { + const example = {}; + if (schema.properties) { + Object.entries(schema.properties).forEach(([key, propSchema]) => { + if (propSchema.example !== undefined) { + example[key] = propSchema.example; + } else if (propSchema.type === 'string') { + example[key] = `<${key}>`; + } else if (propSchema.type === 'integer') { + example[key] = propSchema.minimum || 1; + } else if (propSchema.type === 'number') { + example[key] = propSchema.minimum || 0.0; + } else if (propSchema.type === 'boolean') { + example[key] = true; + } else if (propSchema.type === 'array') { + example[key] = [generateExampleBody(propSchema.items, schemas)]; + } + }); + } + return example; + } + + if (schema.type === 'array') { + return [generateExampleBody(schema.items, schemas)]; + } + + if (schema.example !== undefined) return schema.example; + + switch (schema.type) { + case 'string': return 'example'; + case 'integer': return 0; + case 'number': return 0.0; + case 'boolean': return true; + default: return null; + } + } + + /** + * Extract suggestion data from response to prefill next request + */ + function extractSuggestions(response, responseSchema) { + const suggestions = {}; + + if (!response || typeof response !== 'object') return suggestions; + + if (responseSchema?.type === 'object' && responseSchema.properties) { + Object.keys(responseSchema.properties).forEach((key) => { + if (response[key] !== undefined) { + suggestions[key] = response[key]; + } + }); + } + + return suggestions; + } + + /** + * Check if request is ready to send + */ + function isRequestReady(endpoint, paramState) { + if (!endpoint) return false; + if (!endpoint.parameters) return true; + + const hasErrors = endpoint.parameters.some((param) => { + if (!param.required) return false; + const value = paramState[param.in]?.[param.name]; + return value === undefined || value === null || value === ''; + }); + + return !hasErrors; + } + + // Public API + return { + ParameterState, + buildRequest, + resolvePath, + buildUrl, + buildHeaders, + buildBody, + validateParameters, + validateParameterValue, + populateDefaults, + generateExampleBody, + extractSuggestions, + isRequestReady + }; +})(); + +// Export for use in other modules +if (typeof module !== 'undefined' && module.exports) { + module.exports = RequestBuilder; +} diff --git a/DoxaApi/UI/Assets/workspace-manager.js b/DoxaApi/UI/Assets/workspace-manager.js new file mode 100644 index 0000000..e0f70d5 --- /dev/null +++ b/DoxaApi/UI/Assets/workspace-manager.js @@ -0,0 +1,435 @@ +/** + * Workspace Manager Module + * Handles request collections, workspaces, environments, and persistence + */ +const WorkspaceManager = (() => { + "use strict"; + + const STORAGE_KEY = 'DoxaApi.workspaces'; + const CURRENT_WS_KEY = 'DoxaApi.currentWorkspace'; + const ENV_KEY = 'DoxaApi.environments'; + + /** + * Workspace structure + */ + class Workspace { + constructor(name = 'Default') { + this.id = generateId(); + this.name = name; + this.description = ''; + this.collections = []; + this.createdAt = Date.now(); + this.updatedAt = Date.now(); + } + } + + /** + * Collection structure (folders for organizing requests) + */ + class Collection { + constructor(name = 'New Collection') { + this.id = generateId(); + this.name = name; + this.description = ''; + this.requests = []; + this.folders = []; + this.createdAt = Date.now(); + } + } + + /** + * Request in a collection + */ + class SavedRequest { + constructor(name = 'New Request', method = 'GET', path = '') { + this.id = generateId(); + this.name = name; + this.method = method; + this.path = path; + this.description = ''; + this.tags = []; + this.params = { + path: {}, + query: {}, + header: {}, + cookie: {} + }; + this.body = null; + this.bodyType = 'json'; + this.formData = {}; + this.auth = null; + this.createdAt = Date.now(); + this.updatedAt = Date.now(); + } + + clone() { + const copy = new SavedRequest(this.name, this.method, this.path); + copy.id = generateId(); + copy.description = this.description; + copy.tags = [...this.tags]; + copy.params = JSON.parse(JSON.stringify(this.params)); + copy.body = this.body; + copy.bodyType = this.bodyType; + copy.formData = { ...this.formData }; + copy.auth = this.auth; + return copy; + } + } + + /** + * Environment for variable substitution + */ + class Environment { + constructor(name = 'Default') { + this.id = generateId(); + this.name = name; + this.description = ''; + this.variables = {}; + this.isActive = false; + this.createdAt = Date.now(); + } + } + + /** + * Generate unique ID + */ + function generateId() { + return 'id_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); + } + + /** + * Get all workspaces + */ + function getWorkspaces() { + try { + const data = localStorage.getItem(STORAGE_KEY); + return data ? JSON.parse(data).map(deserializeWorkspace) : []; + } catch (e) { + console.error('Failed to load workspaces:', e); + return []; + } + } + + /** + * Get current active workspace + */ + function getCurrentWorkspace() { + const workspaces = getWorkspaces(); + if (workspaces.length === 0) { + const ws = new Workspace('Default'); + saveWorkspace(ws); + return ws; + } + return workspaces[0]; + } + + /** + * Save workspace + */ + function saveWorkspace(workspace) { + workspace.updatedAt = Date.now(); + const workspaces = getWorkspaces(); + const index = workspaces.findIndex(w => w.id === workspace.id); + + if (index >= 0) { + workspaces[index] = workspace; + } else { + workspaces.push(workspace); + } + + localStorage.setItem(STORAGE_KEY, JSON.stringify(workspaces)); + return workspace; + } + + /** + * Delete workspace + */ + function deleteWorkspace(workspaceId) { + let workspaces = getWorkspaces(); + workspaces = workspaces.filter(w => w.id !== workspaceId); + localStorage.setItem(STORAGE_KEY, JSON.stringify(workspaces)); + } + + /** + * Add collection to workspace + */ + function addCollection(workspaceId, collection) { + const workspace = getWorkspaces().find(w => w.id === workspaceId); + if (workspace) { + workspace.collections.push(collection); + saveWorkspace(workspace); + } + return collection; + } + + /** + * Save request to collection + */ + function saveRequestToCollection(workspaceId, collectionId, request) { + const workspace = getWorkspaces().find(w => w.id === workspaceId); + if (!workspace) return null; + + const collection = findCollectionRecursive(workspace.collections, collectionId); + if (!collection) return null; + + request.updatedAt = Date.now(); + const index = collection.requests.findIndex(r => r.id === request.id); + + if (index >= 0) { + collection.requests[index] = request; + } else { + collection.requests.push(request); + } + + saveWorkspace(workspace); + return request; + } + + /** + * Find collection recursively in folder structure + */ + function findCollectionRecursive(collections, collectionId) { + for (const col of collections) { + if (col.id === collectionId) return col; + if (col.folders) { + const found = findCollectionRecursive(col.folders, collectionId); + if (found) return found; + } + } + return null; + } + + /** + * Get all requests in workspace + */ + function getAllRequests(workspaceId) { + const workspace = getWorkspaces().find(w => w.id === workspaceId); + if (!workspace) return []; + + const requests = []; + + function traverse(collections) { + collections.forEach(col => { + requests.push(...(col.requests || [])); + if (col.folders) { + traverse(col.folders); + } + }); + } + + traverse(workspace.collections); + return requests; + } + + /** + * Search requests in workspace + */ + function searchRequests(workspaceId, query) { + const allRequests = getAllRequests(workspaceId); + const lowerQuery = query.toLowerCase(); + + return allRequests.filter(req => + req.name.toLowerCase().includes(lowerQuery) || + req.path.toLowerCase().includes(lowerQuery) || + req.description.toLowerCase().includes(lowerQuery) || + (req.tags && req.tags.some(t => t.toLowerCase().includes(lowerQuery))) + ); + } + + /** + * Get environments + */ + function getEnvironments() { + try { + const data = localStorage.getItem(ENV_KEY); + return data ? JSON.parse(data).map(deserializeEnvironment) : []; + } catch (e) { + console.error('Failed to load environments:', e); + return []; + } + } + + /** + * Save environment + */ + function saveEnvironment(environment) { + const environments = getEnvironments(); + const index = environments.findIndex(e => e.id === environment.id); + + if (index >= 0) { + environments[index] = environment; + } else { + environments.push(environment); + } + + localStorage.setItem(ENV_KEY, JSON.stringify(environments)); + return environment; + } + + /** + * Delete environment + */ + function deleteEnvironment(environmentId) { + let environments = getEnvironments(); + environments = environments.filter(e => e.id !== environmentId); + localStorage.setItem(ENV_KEY, JSON.stringify(environments)); + } + + /** + * Get active environment + */ + function getActiveEnvironment() { + const environments = getEnvironments(); + return environments.find(e => e.isActive) || null; + } + + /** + * Set active environment + */ + function setActiveEnvironment(environmentId) { + const environments = getEnvironments(); + environments.forEach(e => e.isActive = e.id === environmentId); + localStorage.setItem(ENV_KEY, JSON.stringify(environments)); + } + + /** + * Substitute variables in string + */ + function substituteVariables(str, environment) { + if (!str || !environment) return str; + + let result = str; + Object.entries(environment.variables).forEach(([name, value]) => { + result = result.replace(new RegExp(`\\$\\{${name}\\}|\\$${name}\\b`, 'g'), String(value || '')); + }); + + return result; + } + + /** + * Export workspace as JSON + */ + function exportWorkspace(workspaceId) { + const workspace = getWorkspaces().find(w => w.id === workspaceId); + if (!workspace) return null; + + return { + version: '1.0', + exported: new Date().toISOString(), + workspace: workspace, + environments: getEnvironments() + }; + } + + /** + * Import workspace from JSON + */ + function importWorkspace(json) { + try { + const data = typeof json === 'string' ? JSON.parse(json) : json; + + // Validate version + if (!data.version) throw new Error('Invalid format: missing version'); + + // Create new workspace with imported data + const workspace = data.workspace; + workspace.id = generateId(); + workspace.createdAt = Date.now(); + workspace.updatedAt = Date.now(); + + // Import environments + if (data.environments && Array.isArray(data.environments)) { + data.environments.forEach(env => { + env.id = generateId(); + saveEnvironment(env); + }); + } + + // Save workspace + return saveWorkspace(workspace); + } catch (e) { + console.error('Failed to import workspace:', e); + return null; + } + } + + /** + * Duplicate request + */ + function duplicateRequest(request) { + const duplicate = request.clone(); + duplicate.name = `${request.name} (copy)`; + return duplicate; + } + + /** + * Duplicate collection + */ + function duplicateCollection(collection) { + const duplicate = new Collection(collection.name + ' (copy)'); + duplicate.description = collection.description; + duplicate.requests = collection.requests.map(r => r.clone ? r.clone() : r); + if (collection.folders) { + duplicate.folders = collection.folders.map(f => duplicateCollection(f)); + } + return duplicate; + } + + /** + * Get recent requests + */ + function getRecentRequests(workspaceId, limit = 10) { + return getAllRequests(workspaceId) + .sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0)) + .slice(0, limit); + } + + /** + * Deserialize workspace (helper for JSON parsing) + */ + function deserializeWorkspace(data) { + const ws = Object.assign(new Workspace(data.name), data); + return ws; + } + + /** + * Deserialize environment (helper for JSON parsing) + */ + function deserializeEnvironment(data) { + const env = Object.assign(new Environment(data.name), data); + return env; + } + + // Public API + return { + Workspace, + Collection, + SavedRequest, + Environment, + generateId, + getWorkspaces, + getCurrentWorkspace, + saveWorkspace, + deleteWorkspace, + addCollection, + saveRequestToCollection, + getAllRequests, + searchRequests, + getEnvironments, + saveEnvironment, + deleteEnvironment, + getActiveEnvironment, + setActiveEnvironment, + substituteVariables, + exportWorkspace, + importWorkspace, + duplicateRequest, + duplicateCollection, + getRecentRequests + }; +})(); + +// Export for use in other modules +if (typeof module !== 'undefined' && module.exports) { + module.exports = WorkspaceManager; +}