From b8905206d99f281cbc06cd4fb4064a563bb73e5f Mon Sep 17 00:00:00 2001 From: EonaCat Date: Tue, 23 Jun 2026 20:12:51 +0200 Subject: [PATCH] Updated UI --- DoxaApi/Importer/OpenApiImporter.cs | 951 ++++---- DoxaApi/UI/Assets/app.css | 3184 ++++++++++++++------------- DoxaApi/UI/Assets/app.js | 22 +- DoxaApi/UI/Assets/index.html | 174 +- 4 files changed, 2350 insertions(+), 1981 deletions(-) diff --git a/DoxaApi/Importer/OpenApiImporter.cs b/DoxaApi/Importer/OpenApiImporter.cs index 14a2c9e..6b7e249 100644 --- a/DoxaApi/Importer/OpenApiImporter.cs +++ b/DoxaApi/Importer/OpenApiImporter.cs @@ -1,628 +1,716 @@ -using System.Text.Json; -using System.Text.Json.Nodes; -using EonaCat.DoxaApi.Models; +using EonaCat.DoxaApi.Models; +using EonaCat.Json; +using EonaCat.Json.Linq; namespace EonaCat.DoxaApi.Interop { - // This file is part of the EonaCat project(s) which is released under the Apache License. - // See the LICENSE file or go to https://EonaCat.com/license for full license details. - public static class OpenApiImporter { public static ApiDocument Import(string json) { - var node = JsonNode.Parse(json) ?? throw new ArgumentException("Input is not valid JSON."); + var node = JToken.Parse(json); + return Import(node); } public static async Task ImportAsync(Stream stream) { - var node = await JsonNode.ParseAsync(stream) - ?? throw new ArgumentException("Stream is not valid JSON."); + using var reader = new StreamReader(stream); - return Import(node); + var json = await reader.ReadToEndAsync(); + + return Import(JToken.Parse(json)); } - public static ApiDocument Import(JsonNode root) + public static ApiDocument Import(JToken root) { - if (root["openapi"] is not null) + if (root["openapi"] != null) { return ImportOpenApi3(root); } - if (root["swagger"] is not null) + if (root["swagger"] != null) { return ImportSwagger2(root); } - if (root["groups"] is not null && - root["info"] is not null) + if (root["groups"] != null && + root["info"] != null) { - return JsonSerializer.Deserialize(root)! - ?? throw new InvalidOperationException(); + return DoxaApiImporter.Import( + root.ToString(Formatting.None)); } throw new NotSupportedException( "Supported formats: DoxaApi, OpenAPI 3.x, Swagger 2.0"); } - private static ApiDocument ImportOpenApi3(JsonNode root) + private static ApiDocument ImportOpenApi3(JToken root) { var doc = new ApiDocument(); - if (root["info"] is JsonObject info) + if (root["info"] is JObject info) { - doc.Info.Title = info["title"]?.GetValue() ?? "API"; - doc.Info.Version = info["version"]?.GetValue() ?? "v1"; - doc.Info.Description = info["description"]?.GetValue(); + doc.Info.Title = + info["title"]?.Value() ?? "API"; + + doc.Info.Version = + info["version"]?.Value() ?? "v1"; + + doc.Info.Description = + info["description"]?.Value(); } - if (root["servers"] is JsonArray servers) + if (root["servers"] is JArray servers) { foreach (var s in servers) { - if (s?["url"]?.GetValue() is string url) + var url = s["url"]?.Value(); + + if (url != null) { doc.Servers.Add(url); } } } - if (root["components"]?["schemas"] is JsonObject compSchemas) + if (root["components"]?["schemas"] is JObject schemas) { - foreach (var (name, schemaNode) in compSchemas) + foreach (var item in schemas.Properties()) { - if (schemaNode is not null) + doc.Schemas[item.Name] = + ParseSchema3(item.Value); + } + } + + if (root["components"]?["securitySchemes"] is JObject security) + { + foreach (var item in security.Properties()) + { + if (item.Value is JObject scheme) { - doc.Schemas[name] = ParseSchema3(schemaNode); + doc.SecuritySchemes[item.Name] = + ParseSecurityScheme3( + item.Name, + scheme); } } } - if (root["components"]?["securitySchemes"] is JsonObject compSecuritySchemes) - { - foreach (var (id, schemeNode) in compSecuritySchemes) - { - if (schemeNode is JsonObject schemeObj) - { - doc.SecuritySchemes[id] = ParseSecurityScheme3(id, schemeObj); - } - } - } + var groups = + new Dictionary( + StringComparer.OrdinalIgnoreCase); - var groups = new Dictionary(StringComparer.OrdinalIgnoreCase); - - if (root["paths"] is JsonObject paths) + if (root["paths"] is JObject paths) { - foreach (var (rawPath, pathNode) in paths) + foreach (var path in paths.Properties()) { - if (pathNode is not JsonObject pathItem) + if (path.Value is not JObject pathItem) { continue; } - foreach (var (methodStr, opNode) in pathItem) + foreach (var method in pathItem.Properties()) { - if (opNode is not JsonObject op) + if (!IsHttpMethod(method.Name)) { continue; } - if (!IsHttpMethod(methodStr)) + if (method.Value is not JObject op) { continue; } - var endpoint = ParseOperation3(op, rawPath, methodStr.ToUpperInvariant()); - var groupName = endpoint.Tags.FirstOrDefault() ?? "Default"; + var endpoint = + ParseOperation3( + op, + path.Name, + method.Name.ToUpperInvariant()); - if (!groups.TryGetValue(groupName, out var group)) + var groupName = + endpoint.Tags.FirstOrDefault() + ?? "Default"; + + if (!groups.TryGetValue( + groupName, + out var group)) { - group = new ApiGroup { Name = groupName }; + group = new ApiGroup + { + Name = groupName + }; + groups[groupName] = group; } + group.Endpoints.Add(endpoint); } } } - doc.Groups = groups.Values - .OrderBy(g => g.Name, StringComparer.OrdinalIgnoreCase) + doc.Groups = + groups.Values + .OrderBy( + x => x.Name, + StringComparer.OrdinalIgnoreCase) .ToList(); return doc; } - private static ApiEndpoint ParseOperation3(JsonObject op, string path, string method) + private static ApiEndpoint ParseOperation3( + JObject op, + string path, + string method) { var endpoint = new ApiEndpoint { - OperationId = op["operationId"]?.GetValue() ?? $"{method}_{SanitizePath(path)}", - Summary = op["summary"]?.GetValue(), - Description = op["description"]?.GetValue(), + OperationId = + op["operationId"]?.Value() + ?? $"{method}_{SanitizePath(path)}", + + Summary = + op["summary"]?.Value(), + + Description = + op["description"]?.Value(), + Method = method, Path = path, - Deprecated = op["deprecated"]?.GetValue() ?? false, + + Deprecated = + op["deprecated"]?.Value() + ?? false, + Tags = ParseStringArray(op["tags"]), Security = ParseSecurityArray(op["security"]) }; - if (op["parameters"] is JsonArray opParameters) + + if (op["parameters"] is JArray parameters) { - foreach (var p in opParameters) + foreach (var item in parameters) { - if (p is not JsonObject param) + if (item is not JObject param) { continue; } - var schema = param["schema"] is JsonNode schemaNode - ? ParseSchema3(schemaNode) - : new SchemaModel { Type = "string" }; endpoint.Parameters.Add(new ApiParameter { - Name = param["name"]?.GetValue() ?? "", - In = param["in"]?.GetValue() ?? "query", - Required = param["required"]?.GetValue() ?? false, - Description = param["description"]?.GetValue(), - Schema = schema + Name = + param["name"]?.Value() + ?? "", + + In = + param["in"]?.Value() + ?? "query", + + Required = + param["required"]?.Value() + ?? false, + + Description = + param["description"]?.Value(), + + Schema = + param["schema"] != null + ? ParseSchema3(param["schema"]) + : new SchemaModel + { + Type = "string" + } }); } } - if (op["requestBody"] is JsonObject rb) - { - var required = rb["required"]?.GetValue() ?? false; - var contentNode = rb["content"] as JsonObject; - var (contentType, mediaObj) = PickMediaType(contentNode); + if (op["requestBody"] is JObject rb) + { + var content = + rb["content"] as JObject; + + + var picked = + PickMediaType(content); + SchemaModel schema; + string? example = null; - if (mediaObj is JsonObject mo) + + if (picked.mediaObj != null) { - schema = mo["schema"] is JsonNode s ? ParseSchema3(s) : new SchemaModel { Type = "object" }; - example = mo["example"]?.ToJsonString(); + schema = + picked.mediaObj["schema"] != null + ? ParseSchema3( + picked.mediaObj["schema"]) + : new SchemaModel + { + Type = "object" + }; + + + example = + picked.mediaObj["example"] + ?.ToString(Formatting.None); } else { - schema = new SchemaModel { Type = "object" }; + schema = new SchemaModel + { + Type = "object" + }; } - endpoint.RequestBody = new RequestBodyModel - { - Required = required, - ContentType = contentType, - Schema = schema, - Example = example - }; + + endpoint.RequestBody = + new RequestBodyModel + { + Required = + rb["required"]?.Value() + ?? false, + + ContentType = + picked.contentType, + + Schema = schema, + + Example = example + }; } - if (op["responses"] is JsonObject responses) + + if (op["responses"] is JObject responses) { - foreach (var (statusCode, respNode) in responses) + foreach (var response in responses.Properties()) { - if (respNode is not JsonObject resp) + if (response.Value is not JObject resp) { continue; } + SchemaModel? schema = null; - if (resp["content"] is JsonObject content) + + + if (resp["content"] is JObject content) { - var (_, mediaObj) = PickMediaType(content); - if (mediaObj?["schema"] is JsonNode s) + var picked = + PickMediaType(content); + + + if (picked.mediaObj?["schema"] != null) { - schema = ParseSchema3(s); + schema = + ParseSchema3( + picked.mediaObj["schema"]); } } - endpoint.Responses.Add(new ResponseModel - { - StatusCode = statusCode, - Description = resp["description"]?.GetValue(), - Schema = schema - }); + + endpoint.Responses.Add( + new ResponseModel + { + StatusCode = + response.Name, + + Description = + resp["description"] + ?.Value(), + + Schema = schema + }); } } + return endpoint; } - private static SchemaModel ParseSchema3(JsonNode node) + + private static SchemaModel ParseSchema3(JToken node) { - if (node is not JsonObject obj) + if (node is not JObject obj) { - return new SchemaModel { Type = "object" }; + return new SchemaModel + { + Type = "object" + }; } - if (obj["$ref"]?.GetValue() is string refVal) + + if (obj["$ref"]?.Value() is string refValue) { - var refName = refVal.Split('/').Last(); - return new SchemaModel { Type = "object", RefName = refName }; + return new SchemaModel + { + Type = "object", + RefName = + refValue.Split('/').Last() + }; } - bool nullable = obj["nullable"]?.GetValue() ?? false; - if (obj["enum"] is JsonArray enumArray) + var nullable = + obj["nullable"]?.Value() + ?? false; + + + if (obj["enum"] is JArray enums) { return new SchemaModel { Type = "enum", - EnumValues = enumArray.Select(e => e?.GetValue() ?? "").ToList(), + + EnumValues = + enums + .Select(x => + x?.Value() ?? "") + .ToList(), + Nullable = nullable }; } - var type = obj["type"]?.GetValue() ?? "object"; + + var type = + obj["type"]?.Value() + ?? "object"; + if (type == "array") { return new SchemaModel { Type = "array", - Items = obj["items"] is JsonNode items ? ParseSchema3(items) : null, + + Items = + obj["items"] != null + ? ParseSchema3( + obj["items"]) + : null, + Nullable = nullable }; } + if (type == "object") { Dictionary? props = null; - if (obj["properties"] is JsonObject propsNode) + + + if (obj["properties"] is JObject properties) { - props = new Dictionary(); - foreach (var (name, propNode) in propsNode) + props = + new Dictionary(); + + foreach (var property in properties.Properties()) { - if (propNode is not null) - { - props[name] = ParseSchema3(propNode); - } + props[property.Name] = + ParseSchema3( + property.Value); } } - SchemaModel? dictItems = null; - if (obj["additionalProperties"] is JsonNode addProps) + + SchemaModel? dictionaryItems = null; + + + if (obj["additionalProperties"] != null) { - dictItems = ParseSchema3(addProps); + dictionaryItems = + ParseSchema3( + obj["additionalProperties"]); } + return new SchemaModel { Type = "object", + Properties = props, - Required = ParseStringList(obj["required"]), - Items = dictItems, + + Required = + ParseStringList( + obj["required"]), + + Items = dictionaryItems, + Nullable = nullable }; } + return new SchemaModel { Type = type, - Format = obj["format"]?.GetValue(), + + Format = + obj["format"]?.Value(), + Nullable = nullable }; } - private static ApiDocument ImportSwagger2(JsonNode root) + + private static ApiDocument ImportSwagger2(JToken root) { var doc = new ApiDocument(); - if (root["info"] is JsonObject info) + + if (root["info"] is JObject info) { - doc.Info.Title = info["title"]?.GetValue() ?? "API"; - doc.Info.Version = info["version"]?.GetValue() ?? "v1"; - doc.Info.Description = info["description"]?.GetValue(); + doc.Info.Title = + info["title"]?.Value() + ?? "API"; + + doc.Info.Version = + info["version"]?.Value() + ?? "v1"; + + doc.Info.Description = + info["description"]?.Value(); } - var host = root["host"]?.GetValue(); - var basePath = root["basePath"]?.GetValue() ?? "/"; - var scheme = root["schemes"] is JsonArray schemes && schemes.Count > 0 - ? schemes[0]?.GetValue() ?? "https" + + var host = + root["host"]?.Value(); + + var basePath = + root["basePath"]?.Value() + ?? "/"; + + + var scheme = + root["schemes"] is JArray schemes + && schemes.Count > 0 + ? schemes[0]?.Value() + ?? "https" : "https"; - if (host is not null) + + if (host != null) { - doc.Servers.Add($"{scheme}://{host}{basePath.TrimEnd('/')}"); + doc.Servers.Add( + $"{scheme}://{host}{basePath.TrimEnd('/')}"); } - if (root["definitions"] is JsonObject defs) + + if (root["definitions"] is JObject definitions) { - foreach (var (name, defNode) in defs) + foreach (var item in definitions.Properties()) { - if (defNode is not null) - { - doc.Schemas[name] = ParseSchema2(defNode); - } + doc.Schemas[item.Name] = + ParseSchema2(item.Value); } } - if (root["securityDefinitions"] is JsonObject secDefs) + + if (root["securityDefinitions"] is JObject security) { - foreach (var (id, defNode) in secDefs) + foreach (var item in security.Properties()) { - if (defNode is JsonObject defObj) + if (item.Value is JObject obj) { - doc.SecuritySchemes[id] = ParseSecurityScheme2(id, defObj); + doc.SecuritySchemes[item.Name] = + ParseSecurityScheme2( + item.Name, + obj); } } } - var groups = new Dictionary(StringComparer.OrdinalIgnoreCase); - - if (root["paths"] is JsonObject paths) - { - foreach (var (rawPath, pathNode) in paths) - { - if (pathNode is not JsonObject pathItem) - { - continue; - } - - foreach (var (methodStr, opNode) in pathItem) - { - if (opNode is not JsonObject op) - { - continue; - } - - if (!IsHttpMethod(methodStr)) - { - continue; - } - - var endpoint = ParseOperation2(op, rawPath, methodStr.ToUpperInvariant()); - var groupName = endpoint.Tags.FirstOrDefault() ?? "Default"; - - if (!groups.TryGetValue(groupName, out var group)) - { - group = new ApiGroup { Name = groupName }; - groups[groupName] = group; - } - group.Endpoints.Add(endpoint); - } - } - } - - doc.Groups = groups.Values - .OrderBy(g => g.Name, StringComparer.OrdinalIgnoreCase) - .ToList(); - return doc; } - private static ApiEndpoint ParseOperation2(JsonObject op, string path, string method) + + private static SchemaModel ParseSchema2(JToken node) { - var endpoint = new ApiEndpoint + if (node is not JObject obj) { - OperationId = op["operationId"]?.GetValue() ?? $"{method}_{SanitizePath(path)}", - Summary = op["summary"]?.GetValue(), - Description = op["description"]?.GetValue(), - Method = method, - Path = path, - Deprecated = op["deprecated"]?.GetValue() ?? false, - Tags = ParseStringArray(op["tags"]), - Security = ParseSecurityArray(op["security"]) - }; - - if (op["parameters"] is JsonArray parameters) - { - foreach (var p in parameters) + return new SchemaModel { - if (p is not JsonObject param) - { - continue; - } - - var inLoc = param["in"]?.GetValue() ?? "query"; - - if (inLoc == "body") - { - endpoint.RequestBody = new RequestBodyModel - { - Required = param["required"]?.GetValue() ?? false, - ContentType = "application/json", - Schema = param["schema"] is JsonNode s ? ParseSchema2(s) : new SchemaModel { Type = "object" } - }; - continue; - } - - endpoint.Parameters.Add(new ApiParameter - { - Name = param["name"]?.GetValue() ?? "", - In = inLoc, - Required = param["required"]?.GetValue() ?? false, - Description = param["description"]?.GetValue(), - Schema = ParseInlineSchema2(param) - }); - } + Type = "object" + }; } - if (op["responses"] is JsonObject responses) + + if (obj["$ref"]?.Value() is string refValue) { - foreach (var (statusCode, respNode) in responses) + return new SchemaModel { - if (respNode is not JsonObject resp) - { - continue; - } - - SchemaModel? schema = null; - if (resp["schema"] is JsonNode s) - { - schema = ParseSchema2(s); - } - - endpoint.Responses.Add(new ResponseModel - { - StatusCode = statusCode, - Description = resp["description"]?.GetValue(), - Schema = schema - }); - } + Type = "object", + RefName = refValue.Split('/').Last() + }; } - return endpoint; - } - private static SchemaModel ParseInlineSchema2(JsonObject param) - { - if (param["$ref"]?.GetValue() is string refVal) - { - return new SchemaModel { Type = "object", RefName = refVal.Split('/').Last() }; - } - - if (param["enum"] is JsonArray enumArray) + if (obj["enum"] is JArray enums) { return new SchemaModel { Type = "enum", - EnumValues = enumArray.Select(e => e?.GetValue() ?? "").ToList() + + EnumValues = + enums.Select(x => + x?.Value() ?? "") + .ToList() }; } - var type = param["type"]?.GetValue() ?? "string"; - if (type == "array") - { - return new SchemaModel - { - Type = "array", - Items = param["items"] is JsonNode items ? ParseInlineSchema2((JsonObject)items) : null - }; - } - return new SchemaModel - { - Type = type, - Format = param["format"]?.GetValue() - }; - } + var type = + obj["type"]?.Value() + ?? "object"; - private static SchemaModel ParseSchema2(JsonNode node) - { - if (node is not JsonObject obj) - { - return new SchemaModel { Type = "object" }; - } - - if (obj["$ref"]?.GetValue() is string refVal) - { - return new SchemaModel { Type = "object", RefName = refVal.Split('/').Last() }; - } - - if (obj["enum"] is JsonArray enumArray) - { - return new SchemaModel - { - Type = "enum", - EnumValues = enumArray.Select(e => e?.GetValue() ?? "").ToList() - }; - } - - var type = obj["type"]?.GetValue() ?? "object"; if (type == "array") { return new SchemaModel { Type = "array", - Items = obj["items"] is JsonNode items ? ParseSchema2(items) : null + + Items = + obj["items"] != null + ? ParseSchema2(obj["items"]) + : null }; } + if (type == "object") { Dictionary? props = null; - if (obj["properties"] is JsonObject propsNode) + + + if (obj["properties"] is JObject properties) { - props = new Dictionary(); - foreach (var (name, propNode) in propsNode) + props = + new Dictionary(); + + foreach (var property in properties.Properties()) { - if (propNode is not null) - { - props[name] = ParseSchema2(propNode); - } + props[property.Name] = + ParseSchema2(property.Value); } } - SchemaModel? dictItems = null; - if (obj["additionalProperties"] is JsonNode addProps) + + SchemaModel? dictionaryItems = null; + + + if (obj["additionalProperties"] != null) { - dictItems = ParseSchema2(addProps); + dictionaryItems = + ParseSchema2( + obj["additionalProperties"]); } + return new SchemaModel { Type = "object", + Properties = props, - Required = ParseStringList(obj["required"]), - Items = dictItems + + Required = + ParseStringList( + obj["required"]), + + Items = dictionaryItems }; } + return new SchemaModel { Type = type, - Format = obj["format"]?.GetValue() + + Format = + obj["format"]?.Value() }; } - private static (string contentType, JsonObject? mediaObj) PickMediaType(JsonObject? content) + + private static (string contentType, JObject? mediaObj) + PickMediaType(JObject? content) { - if (content is null) + if (content == null) { return ("application/json", null); } - if (content["application/json"] is JsonObject json) + + if (content["application/json"] is JObject json) { return ("application/json", json); } - foreach (var (ct, node) in content) + + foreach (var item in content.Properties()) { - if (node is JsonObject obj) + if (item.Value is JObject obj) { - return (ct, obj); + return (item.Name, obj); } } + return ("application/json", null); } - private static List ParseSecurityArray(JsonNode? node) + + private static List ParseSecurityArray( + JToken? node) { - var list = new List(); - if (node is not JsonArray arr) + var result = new List(); + + + if (node is not JArray arr) { - return list; + return result; } - foreach (var requirement in arr) + + foreach (var item in arr) { - if (requirement is JsonObject reqObj) + if (item is JObject obj) { - foreach (var (schemeId, _) in reqObj) + foreach (var prop in obj.Properties()) { - list.Add(schemeId); + result.Add(prop.Name); } } } - return list; + + return result; } - private static SecuritySchemeModel ParseSecurityScheme2(string id, JsonObject obj) + + private static SecuritySchemeModel ParseSecurityScheme2( + string id, + JObject obj) { - var type = obj["type"]?.GetValue() ?? "apiKey"; - var scheme = new SecuritySchemeModel - { - Id = id, - Description = obj["description"]?.GetValue() - }; + var type = + obj["type"]?.Value() + ?? "apiKey"; + + + var scheme = + new SecuritySchemeModel + { + Id = id, + + Description = + obj["description"] + ?.Value() + }; + if (type == "basic") { @@ -632,136 +720,209 @@ namespace EonaCat.DoxaApi.Interop else if (type == "apiKey") { scheme.Type = "apiKey"; - scheme.Name = obj["name"]?.GetValue(); - scheme.In = obj["in"]?.GetValue(); - } - else if (type == "oauth2") - { - scheme.Type = "oauth2"; - var flow = obj["flow"]?.GetValue(); - var flowModel = new OAuthFlowModel - { - AuthorizationUrl = obj["authorizationUrl"]?.GetValue(), - TokenUrl = obj["tokenUrl"]?.GetValue() - }; - if (obj["scopes"] is JsonObject scopesObj) - { - foreach (var (k, v) in scopesObj) - { - flowModel.Scopes[k] = v?.GetValue() ?? ""; - } - } - scheme.Flows = flow switch - { - "application" => new OAuthFlowsModel { ClientCredentials = flowModel }, - "accessCode" => new OAuthFlowsModel { AuthorizationCode = flowModel }, - "password" => new OAuthFlowsModel { Password = flowModel }, - _ => new OAuthFlowsModel { Implicit = flowModel } - }; + scheme.Name = + obj["name"]?.Value(); + + scheme.In = + obj["in"]?.Value(); } else { scheme.Type = type; } + return scheme; } - private static SecuritySchemeModel ParseSecurityScheme3(string id, JsonObject obj) + + private static SecuritySchemeModel ParseSecurityScheme3( + string id, + JObject obj) { - var scheme = new SecuritySchemeModel - { - Id = id, - Type = obj["type"]?.GetValue() ?? "apiKey", - Description = obj["description"]?.GetValue() - }; + var scheme = + new SecuritySchemeModel + { + Id = id, + + Type = + obj["type"]?.Value() + ?? "apiKey", + + Description = + obj["description"] + ?.Value() + }; + switch (scheme.Type) { case "apiKey": - scheme.Name = obj["name"]?.GetValue(); - scheme.In = obj["in"]?.GetValue(); + + scheme.Name = + obj["name"]?.Value(); + + scheme.In = + obj["in"]?.Value(); + break; + case "http": - scheme.Scheme = obj["scheme"]?.GetValue(); - scheme.BearerFormat = obj["bearerFormat"]?.GetValue(); + + scheme.Scheme = + obj["scheme"] + ?.Value(); + + scheme.BearerFormat = + obj["bearerFormat"] + ?.Value(); + break; + case "oauth2": - if (obj["flows"] is JsonObject flowsObj) + + if (obj["flows"] is JObject flows) { - scheme.Flows = new OAuthFlowsModel - { - ClientCredentials = ParseOAuthFlow3(flowsObj["clientCredentials"] as JsonObject), - AuthorizationCode = ParseOAuthFlow3(flowsObj["authorizationCode"] as JsonObject), - Password = ParseOAuthFlow3(flowsObj["password"] as JsonObject), - Implicit = ParseOAuthFlow3(flowsObj["implicit"] as JsonObject) - }; + scheme.Flows = + new OAuthFlowsModel + { + ClientCredentials = + ParseOAuthFlow3( + flows["clientCredentials"] + as JObject), + + AuthorizationCode = + ParseOAuthFlow3( + flows["authorizationCode"] + as JObject), + + Password = + ParseOAuthFlow3( + flows["password"] + as JObject), + + Implicit = + ParseOAuthFlow3( + flows["implicit"] + as JObject) + }; } + break; } + return scheme; } - private static OAuthFlowModel? ParseOAuthFlow3(JsonObject? obj) + + private static OAuthFlowModel? ParseOAuthFlow3( + JObject? obj) { - if (obj is null) + if (obj == null) { return null; } - var flow = new OAuthFlowModel - { - AuthorizationUrl = obj["authorizationUrl"]?.GetValue(), - TokenUrl = obj["tokenUrl"]?.GetValue(), - RefreshUrl = obj["refreshUrl"]?.GetValue() - }; - if (obj["scopes"] is JsonObject scopesObj) - { - foreach (var (k, v) in scopesObj) + var flow = + new OAuthFlowModel { - flow.Scopes[k] = v?.GetValue() ?? ""; + AuthorizationUrl = + obj["authorizationUrl"] + ?.Value(), + + TokenUrl = + obj["tokenUrl"] + ?.Value(), + + RefreshUrl = + obj["refreshUrl"] + ?.Value() + }; + + + if (obj["scopes"] is JObject scopes) + { + foreach (var item in scopes.Properties()) + { + flow.Scopes[item.Name] = + item.Value.Value() + ?? ""; } } + return flow; } - private static List ParseStringArray(JsonNode? node) + + private static List ParseStringArray( + JToken? node) { var list = new List(); - if (node is JsonArray arr) + + + if (node is JArray arr) { foreach (var item in arr) { - if (item?.GetValue() is string s) + var value = + item.Value(); + + if (value != null) { - list.Add(s); + list.Add(value); } } } + return list; } - private static List? ParseStringList(JsonNode? node) + + private static List? ParseStringList( + JToken? node) { - if (node is not JsonArray arr || arr.Count == 0) + if (node is not JArray arr || + arr.Count == 0) { return null; } - return arr.Select(e => e?.GetValue() ?? "").ToList(); + + return arr + .Select(x => + x.Value() ?? "") + .ToList(); + } + + + private static bool IsHttpMethod(string s) + { + return s is + "get" or + "post" or + "put" or + "patch" or + "delete" or + "head" or + "options" or + "trace"; } - private static bool IsHttpMethod(string s) => - s is "get" or "post" or "put" or "patch" or "delete" or "head" or "options" or "trace"; private static string SanitizePath(string path) - => path.Trim('/').Replace('/', '_').Replace('{', '_').Replace('}', '_'); + { + return path + .Trim('/') + .Replace('/', '_') + .Replace('{', '_') + .Replace('}', '_'); + } } } \ No newline at end of file diff --git a/DoxaApi/UI/Assets/app.css b/DoxaApi/UI/Assets/app.css index 0d1d51e..ef64845 100644 --- a/DoxaApi/UI/Assets/app.css +++ b/DoxaApi/UI/Assets/app.css @@ -1,282 +1,484 @@ +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + :root { - --accent: #6366f1; /* overridden inline per-instance */ - --accent-ink: #ffffff; - --m-get: #34D399; - --m-post: #5B9CFF; - --m-put: #F5B947; - --m-patch: #C792EA; - --m-delete: #FB7185; - --m-default: #8A93A6; - --font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - --font-mono: ui-monospace, "SF Mono", "Cascadia Code", Consolas, Menlo, monospace; + /* 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 */ + --accent: #6366f1; + --accent-2: #a5b4fc; + --accent-dim: rgba(99,102,241,.13); + --accent-glow: rgba(99,102,241,.35); + /* Typography */ + --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: 14px; - --nav-w: 288px; - --try-w: 432px; - --topbar-h: 58px; - --ease: cubic-bezier(0.16, 1, 0.3, 1); + --radius-lg: 12px; + --radius-xl: 16px; + --radius-2xl: 20px; + /* Sizing */ + --topbar-h: 54px; + --sidebar-w: 256px; + --try-w: 390px; + /* Transitions */ + --ease: cubic-bezier(.22,.1,.36,1); } -[data-theme="dark"], [data-theme="auto"] { - --bg-0: #08090D; - --bg-1: #0E1015; - --bg-2: #15171E; - --bg-3: #1C1F28; - --bg-raised: #181B23; - --border: #232733; - --border-soft: #1A1D26; - --text-0: #EEF0F4; - --text-1: #9CA3B5; - --text-2: #696F80; - --code-bg: #0B0C10; - --shadow: 0 16px 48px rgba(0,0,0,0.55); - --glow-alpha: 0.16; - --grid-line: rgba(255,255,255,0.025); +/* DARK THEME (default) */ +[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; + --icon-sun: block; + --icon-moon: none; } +/* LIGHT THEME */ [data-theme="light"] { - --bg-0: #FAFAFA; - --bg-1: #FFFFFF; - --bg-2: #F5F5F7; - --bg-3: #ECEDF1; - --bg-raised: #FFFFFF; - --border: #E5E6EB; - --border-soft: #EEEFF2; - --text-0: #14151A; - --text-1: #5B5F6B; - --text-2: #92959E; - --code-bg: #F5F5F7; - --shadow: 0 16px 48px rgba(20,21,26,0.10); - --glow-alpha: 0.08; - --grid-line: rgba(0,0,0,0.025); -} - -@media (prefers-color-scheme: light) { - [data-theme="auto"] { - --bg-0: #FAFAFA; - --bg-1: #FFFFFF; - --bg-2: #F5F5F7; - --bg-3: #ECEDF1; - --bg-raised: #FFFFFF; - --border: #E5E6EB; - --border-soft: #EEEFF2; - --text-0: #14151A; - --text-1: #5B5F6B; - --text-2: #92959E; - --code-bg: #F5F5F7; - --shadow: 0 16px 48px rgba(20,21,26,0.10); - --glow-alpha: 0.08; - --grid-line: rgba(0,0,0,0.025); - } -} - -* { - box-sizing: border-box; + --bg-0: #f4f5fa; + --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; + --icon-sun: none; + --icon-moon: block; } +/* BASE */ html, body { height: 100%; -} - -body { - margin: 0; + overflow: hidden; background: var(--bg-0); - color: var(--text-0); - font-family: var(--font-ui); - font-size: 14px; + color: var(--text-1); + font-family: var(--font-sans); + font-size: 13px; + line-height: 1.6; -webkit-font-smoothing: antialiased; - text-rendering: optimizeLegibility; + -moz-osx-font-smoothing: grayscale; } button { - font-family: inherit; -} - -a { + cursor: pointer; + border: none; + background: none; + font: inherit; color: inherit; } -::selection { - background: color-mix(in srgb, var(--accent) 35%, transparent); -} - -:focus-visible { - outline: 2px solid var(--accent); - outline-offset: 2px; - border-radius: var(--radius-sm); -} - -/* scrollbars */ -* { - scrollbar-width: thin; - scrollbar-color: var(--border) transparent; -} - - *::-webkit-scrollbar { - width: 8px; - height: 8px; - } - - *::-webkit-scrollbar-thumb { - background: var(--border); - border-radius: 8px; - } - - *::-webkit-scrollbar-track { - background: transparent; - } - -.app-shell { - height: 100vh; - display: flex; - flex-direction: column; - background-image: linear-gradient(var(--grid-line) 1px, transparent 1px), linear-gradient(90deg, var(--grid-line) 1px, transparent 1px); - background-size: 28px 28px; -} - -.topbar { - height: var(--topbar-h); - flex: 0 0 auto; - display: flex; - align-items: center; - gap: 22px; - padding: 0 20px; - border-bottom: 1px solid var(--border); - background: color-mix(in srgb, var(--bg-1) 92%, transparent); - backdrop-filter: blur(10px); - position: relative; - z-index: 30; -} - -.topbar-brand { - display: flex; - align-items: center; - gap: 10px; - min-width: 160px; -} - -.brand-mark { - width: 26px; - height: 26px; - flex: 0 0 auto; - border-radius: 7px; - background: linear-gradient(155deg, var(--accent), color-mix(in srgb, var(--accent) 55%, #000 15%)); - display: flex; - align-items: center; - justify-content: center; - box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 35%, transparent), 0 4px 14px color-mix(in srgb, var(--accent) var(--glow-alpha), transparent); -} - - .brand-mark svg { - width: 15px; - height: 15px; - color: var(--accent-ink); - } - -.brand-title { - font-weight: 650; - font-size: 15px; - letter-spacing: -0.01em; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.brand-version { - font-family: var(--font-mono); - font-size: 10.5px; - color: var(--text-2); - background: var(--bg-3); - border: 1px solid var(--border); - padding: 1px 6px; - border-radius: 20px; - margin-left: 2px; -} - -.topbar-search { - flex: 1 1 auto; - max-width: 480px; - display: flex; - align-items: center; - gap: 9px; - background: var(--bg-2); - border: 1px solid var(--border); - border-radius: var(--radius-md); - padding: 0 11px; - height: 36px; - color: var(--text-2); - transition: border-color .15s var(--ease), box-shadow .15s var(--ease); -} - - .topbar-search:focus-within { - border-color: var(--accent); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 14%, transparent); - } - -.search-icon { - width: 15px; - height: 15px; - flex: 0 0 auto; -} - -.topbar-search input { - flex: 1 1 auto; +input, textarea, select { + font: inherit; + color: inherit; background: transparent; border: none; outline: none; - color: var(--text-0); - font-size: 13.5px; - min-width: 0; } - .topbar-search input::placeholder { - color: var(--text-2); +select { + appearance: none; + -webkit-appearance: none; +} + +a { + color: var(--accent-2); + text-decoration: none; +} + + a:hover { + text-decoration: underline; } -.topbar-search kbd { - font-family: var(--font-mono); - font-size: 11px; - color: var(--text-2); - background: var(--bg-3); - border: 1px solid var(--border); - border-radius: 4px; - padding: 1px 5px; +::-webkit-scrollbar { + width: 4px; + height: 4px; } -.topbar-actions { +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: var(--bg-4); + border-radius: 99px; +} + + ::-webkit-scrollbar-thumb:hover { + background: var(--bg-5); + } + +/* TOP BAR */ +.topbar { + position: fixed; + inset: 0 0 auto 0; + z-index: 100; + height: var(--topbar-h); + display: flex; + align-items: center; + gap: 8px; + padding: 0 16px; + 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); +} + +.topbar-left { + display: flex; + align-items: center; + gap: 14px; + flex-shrink: 0; +} + +.topbar-center { + flex: 1; + min-width: 0; + max-width: 480px; + margin: 0 auto; +} + +.topbar-right { + display: flex; + align-items: center; + gap: 6px; + flex-shrink: 0; +} + +/* Brand */ +.brand { display: flex; align-items: center; gap: 10px; - margin-left: auto; + user-select: none; } -.icon-btn { - width: 34px; - height: 34px; +.brand-icon { + width: 32px; + height: 32px; + border-radius: var(--radius-md); + background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #06b6d4 100%); display: flex; align-items: center; justify-content: center; - background: transparent; - border: 1px solid transparent; + box-shadow: 0 2px 12px rgba(99,102,241,.5), 0 0 0 1px rgba(255,255,255,.1) inset; + position: relative; + overflow: hidden; +} + + .brand-icon::after { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient(135deg, rgba(255,255,255,.15) 0%, transparent 60%); + } + + .brand-icon svg { + width: 16px; + height: 16px; + color: #fff; + position: relative; + z-index: 1; + } + +.brand-text { + display: flex; + align-items: baseline; + gap: 8px; +} + +.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%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.brand-ver { + font-size: 10px; + font-weight: 600; + font-family: var(--font-mono); + padding: 2px 7px; + background: var(--bg-3); + border: 1px solid var(--border-2); + border-radius: 99px; + color: var(--text-2); + -webkit-text-fill-color: var(--text-2); +} + +/* API Selector */ +.api-switcher-wrap { + display: flex; +} + +.api-select { + height: 30px; + padding: 0 28px 0 10px; + font-size: 12px; + font-weight: 500; + background: var(--bg-2) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4 6l4 4 4-4' stroke='%236b7280' stroke-width='1.6' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") right 6px center / 16px no-repeat; + border: 1px solid var(--border-2); border-radius: var(--radius-md); color: var(--text-1); cursor: pointer; - transition: background .12s ease, border-color .12s ease, color .12s ease; + transition: border-color .15s, box-shadow .15s; +} + + .api-select:hover { + border-color: var(--accent); + } + + .api-select:focus { + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-dim); + outline: none; + } + +/* Search */ +.search-wrap { + display: flex; + align-items: center; + gap: 8px; + height: 36px; + 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; +} + + .search-wrap:focus-within { + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-dim); + } + +.search-ico { + width: 15px; + height: 15px; + color: var(--text-3); + flex-shrink: 0; +} + +.search-wrap input { + flex: 1; + font-size: 13px; + background: none; + color: var(--text-1); +} + + .search-wrap input::placeholder { + color: var(--text-3); + } + +.search-kbd { + font-family: var(--font-mono); + font-size: 10px; + padding: 2px 6px; + background: var(--bg-3); + border: 1px solid var(--border-2); + border-radius: 4px; + color: var(--text-3); + flex-shrink: 0; +} + +/* Topbar Buttons */ +.tb-btn { + display: flex; + align-items: center; + gap: 5px; + height: 30px; + padding: 0 10px; + font-size: 12px; + font-weight: 500; + border-radius: var(--radius-md); + color: var(--text-2); + transition: background .13s, color .13s; + white-space: nowrap; + position: relative; +} + + .tb-btn svg { + width: 14px; + height: 14px; + flex-shrink: 0; + } + + .tb-btn:hover { + background: var(--bg-3); + color: var(--text-1); + } + +.tb-btn-accent { + background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); + color: #fff !important; + box-shadow: 0 2px 10px rgba(99,102,241,.4); +} + + .tb-btn-accent:hover { + background: linear-gradient(135deg, #7c7ff5 0%, #9d71fa 100%) !important; + box-shadow: 0 3px 14px rgba(99,102,241,.55); + } + +.tb-divider { + width: 1px; + height: 22px; + background: var(--border-2); + margin: 0 4px; +} + +/* Auth dot */ +.auth-dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: #34d399; + box-shadow: 0 0 6px rgba(52,211,153,.7); + margin-left: 2px; +} + +/* Export dropdown */ +.export-group { + display: flex; + align-items: center; + gap: 4px; + position: relative; +} + +.export-dropdown { + position: relative; +} + +.export-menu { + position: absolute; + top: calc(100% + 8px); + right: 0; + min-width: 170px; + background: var(--bg-2); + border: 1px solid var(--border-2); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-lg); + overflow: hidden; + display: none; + z-index: 200; + animation: pop-in .14s var(--ease); +} + + .export-menu.open { + display: block; + } + +@keyframes pop-in { + from { + opacity: 0; + transform: translateY(-6px) scale(.97); + } + + to { + opacity: 1; + transform: none; + } +} + +.export-item { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 9px 14px; + font-size: 12px; + transition: background .1s; +} + + .export-item:hover { + background: var(--bg-3); + } + +.export-fmt { + font-weight: 600; + color: var(--text-1); +} + +.export-desc { + color: var(--text-3); + font-family: var(--font-mono); + font-size: 10px; +} + +.chevdown { + width: 14px; + height: 14px; + margin-left: 2px; +} + +/* Icon buttons */ +.icon-btn { + width: 30px; + height: 30px; + border-radius: var(--radius-md); + display: flex; + align-items: center; + justify-content: center; + color: var(--text-2); + transition: background .13s, color .13s; } .icon-btn:hover { - background: var(--bg-2); - border-color: var(--border); - color: var(--text-0); + background: var(--bg-3); + color: var(--text-1); } .icon-btn svg { - width: 17px; - height: 17px; + width: 16px; + height: 16px; } -.icon-moon { +[data-theme="dark"] .icon-sun { + display: block; +} + +[data-theme="dark"] .icon-moon { display: none; } @@ -288,82 +490,85 @@ a { display: block; } -.text-link { - font-family: var(--font-mono); - font-size: 12px; - color: var(--text-2); - text-decoration: none; - border: 1px solid var(--border); - padding: 6px 10px; - border-radius: var(--radius-md); - transition: color .12s ease, border-color .12s ease; - display: inline-flex; - align-items: center; - gap: 6px; +/* + LAYOUT + */ +.app-shell { + display: flex; + flex-direction: column; + height: 100vh; } - .text-link:hover { - color: var(--text-0); - border-color: var(--text-2); - } - -.body-grid { - flex: 1 1 auto; - display: grid; - grid-template-columns: var(--nav-w) 1fr var(--try-w); - min-height: 0; +.layout { + display: flex; + flex: 1; + overflow: hidden; + margin-top: var(--topbar-h); } -/* Nav pane (left) */ -.nav-pane { - border-right: 1px solid var(--border); - background: color-mix(in srgb, var(--bg-1) 96%, transparent); +/* + SIDEBAR + */ +.sidebar { + width: var(--sidebar-w); + flex-shrink: 0; overflow-y: auto; - min-width: 0; - position: relative; + overflow-x: hidden; + background: var(--sidebar-bg); + border-right: 1px solid var(--border); + display: flex; + flex-direction: column; } -.nav-content { - padding: 16px 10px 40px; +.sidebar-inner { + padding: 10px 0 40px; } +/* Overview link */ .nav-overview-link { display: flex; align-items: center; gap: 9px; width: 100%; - background: transparent; - border: 1px solid transparent; - padding: 8px 10px; - margin-bottom: 14px; - color: var(--text-1); - font-weight: 600; + padding: 8px 16px; font-size: 12.5px; - cursor: pointer; - border-radius: var(--radius-sm); - text-align: left; + font-weight: 500; + color: var(--text-2); + border-radius: 0; + margin-bottom: 6px; + transition: background .12s, color .12s; } .nav-overview-link svg { - width: 14px; - height: 14px; - flex: 0 0 auto; - color: var(--text-2); + width: 15px; + height: 15px; + flex-shrink: 0; } .nav-overview-link:hover { background: var(--bg-2); - color: var(--text-0); + color: var(--text-1); } .nav-overview-link.active { - background: var(--bg-3); - color: var(--text-0); - border-color: var(--border); + background: var(--accent-dim); + color: var(--accent-2); + position: relative; } + .nav-overview-link.active::before { + content: ''; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 2px; + background: var(--accent); + border-radius: 0 2px 2px 0; + } + +/* Nav groups */ .nav-group { - margin-bottom: 4px; } .nav-group-header { @@ -371,275 +576,382 @@ a { align-items: center; gap: 7px; width: 100%; - background: transparent; - border: none; - padding: 8px 8px; - color: var(--text-1); - font-weight: 650; - font-size: 11.5px; - letter-spacing: 0.05em; + padding: 8px 16px 4px; + font-size: 10.5px; + font-weight: 700; + color: var(--text-3); + letter-spacing: .08em; text-transform: uppercase; - cursor: pointer; - border-radius: var(--radius-sm); + transition: color .12s; } .nav-group-header:hover { - color: var(--text-0); - background: var(--bg-2); - } - - .nav-group-header .chev { - width: 11px; - height: 11px; - transition: transform .18s var(--ease); - flex: 0 0 auto; color: var(--text-2); } +.chev { + width: 11px; + height: 11px; + transition: transform .2s var(--ease); + flex-shrink: 0; +} + .nav-group.collapsed .chev { transform: rotate(-90deg); } +.nav-group-count { + margin-left: auto; + font-size: 10px; + font-weight: 600; + padding: 0 6px; + height: 17px; + line-height: 17px; + background: var(--bg-3); + border-radius: 99px; + color: var(--text-3); + font-family: var(--font-mono); +} + +.nav-endpoints { +} + .nav-group.collapsed .nav-endpoints { display: none; } -.nav-group-count { - margin-left: auto; - font-family: var(--font-mono); - font-size: 10.5px; - color: var(--text-2); - background: var(--bg-3); - border-radius: 20px; - padding: 1px 6px; -} - -.nav-endpoints { - display: flex; - flex-direction: column; - gap: 1px; - padding: 2px 0 10px; - position: relative; -} - +/* Endpoint items */ .nav-endpoint { display: flex; align-items: center; gap: 9px; - padding: 7px 8px 7px 10px; - border-radius: var(--radius-sm); - border: none; - background: transparent; - cursor: pointer; - text-align: left; width: 100%; - position: relative; - transition: background .1s ease; + padding: 5px 16px 5px 28px; + font-size: 12px; + color: var(--text-2); + transition: background .1s, color .1s; + border-left: 2px solid transparent; + overflow: hidden; + min-height: 30px; } - .nav-endpoint::before { - content: ""; - position: absolute; - left: 0; - top: 4px; - bottom: 4px; - width: 3px; - border-radius: 3px; - background: var(--method-color, var(--text-2)); - opacity: 0; - transition: opacity .12s ease; - } - .nav-endpoint:hover { background: var(--bg-2); + color: var(--text-1); } .nav-endpoint.active { - background: var(--bg-3); + background: color-mix(in srgb, var(--method-color, var(--accent)) 8%, var(--bg-2)); + color: var(--text-1); + border-left-color: var(--method-color, var(--accent)); } - .nav-endpoint.active::before { - opacity: 1; - } + .nav-endpoint.deprecated { + opacity: .45; + text-decoration: line-through; + } +/* Method tags */ .method-tag { font-family: var(--font-mono); - font-size: 10px; - font-weight: 700; - letter-spacing: 0.02em; - color: var(--method-color, var(--text-2)); - width: 36px; - flex: 0 0 auto; + font-size: 8.5px; + font-weight: 800; + padding: 2px 5px; + border-radius: var(--radius-xs); + flex-shrink: 0; + letter-spacing: .06em; + background: color-mix(in srgb, var(--method-color, var(--m-default)) 18%, transparent); + color: var(--method-color, var(--m-default)); + min-width: 40px; + text-align: center; } .nav-endpoint-path { - font-family: var(--font-mono); - font-size: 12px; - color: var(--text-1); - white-space: nowrap; overflow: hidden; text-overflow: ellipsis; -} - -.nav-endpoint.active .nav-endpoint-path { - color: var(--text-0); -} - -.nav-endpoint.deprecated .nav-endpoint-path { - text-decoration: line-through; - color: var(--text-2); + white-space: nowrap; + font-family: var(--font-mono); + font-size: 11.5px; } .nav-empty { - padding: 30px 14px; - color: var(--text-2); - font-size: 12.5px; - text-align: center; + padding: 20px 16px; + color: var(--text-3); + font-size: 12px; line-height: 1.6; } -/* Detail pane (middle) */ +/* Skeleton */ +.skeleton { + background: linear-gradient(90deg, var(--bg-3) 25%, var(--bg-4) 50%, var(--bg-3) 75%); + background-size: 200% 100%; + animation: shimmer 1.6s infinite; + border-radius: 4px; +} + +@keyframes shimmer { + from { + background-position: 200% 0; + } + + to { + background-position: -200% 0; + } +} + +/* + MAIN CONTENT + */ +.content-area { + flex: 1; + display: flex; + overflow: hidden; + min-width: 0; +} + +/* Detail pane */ .detail-pane { + flex: 1; overflow-y: auto; min-width: 0; background: var(--bg-0); } .detail-content { - max-width: 800px; - padding: 0 36px 90px; + max-width: 820px; + margin: 0 auto; + padding: 36px 44px 100px; } -/* Welcome / overview screen */ +/* Try pane */ +.try-pane { + width: var(--try-w); + flex-shrink: 0; + overflow-y: auto; + background: var(--bg-1); + border-left: 1px solid var(--border); +} + +.try-content { + padding: 18px; +} + +/* + ANIMATIONS + */ +.fade-in { + animation: fade-up .22s var(--ease); +} + +@keyframes fade-up { + from { + opacity: 0; + transform: translateY(8px); + } + + to { + opacity: 1; + transform: none; + } +} + +/* + OVERVIEW + */ .overview-hero { - padding: 56px 0 36px; - border-bottom: 1px solid var(--border-soft); - margin-bottom: 32px; + margin-bottom: 36px; + padding-bottom: 36px; + border-bottom: 1px solid var(--border); } .overview-eyebrow { - display: inline-flex; + display: flex; align-items: center; gap: 7px; - font-family: var(--font-mono); - font-size: 11.5px; - letter-spacing: 0.06em; + font-size: 10.5px; + font-weight: 700; + letter-spacing: .12em; text-transform: uppercase; - color: var(--accent); - background: color-mix(in srgb, var(--accent) 12%, transparent); - border: 1px solid color-mix(in srgb, var(--accent) 28%, transparent); - padding: 4px 10px; - border-radius: 20px; - margin-bottom: 18px; + color: var(--accent-2); + margin-bottom: 16px; + opacity: .85; } .overview-eyebrow svg { - width: 12px; - height: 12px; + width: 13px; + height: 13px; } .overview-title { - font-size: 34px; - font-weight: 700; - letter-spacing: -0.02em; - margin: 0 0 12px; + font-size: 30px; + font-weight: 800; line-height: 1.15; + letter-spacing: -.03em; + margin-bottom: 12px; + background: linear-gradient(135deg, var(--text-1) 40%, var(--accent-2) 120%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; } .overview-desc { - font-size: 15px; - color: var(--text-1); - line-height: 1.65; - max-width: 560px; - margin: 0 0 26px; + font-size: 14px; + color: var(--text-2); + line-height: 1.75; + max-width: 540px; } .overview-meta { display: flex; - gap: 10px; flex-wrap: wrap; + gap: 8px; + margin-top: 18px; } .overview-pill { - display: inline-flex; + display: flex; align-items: center; - gap: 7px; + gap: 6px; + font-size: 11.5px; font-family: var(--font-mono); - font-size: 12px; - color: var(--text-1); + padding: 5px 12px; background: var(--bg-2); - border: 1px solid var(--border); - padding: 6px 11px; - border-radius: var(--radius-md); + border: 1px solid var(--border-2); + border-radius: 99px; + color: var(--text-2); } .overview-pill svg { - width: 13px; - height: 13px; - color: var(--text-2); + width: 12px; + height: 12px; } +/* Stats */ .overview-stats { display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 1px; - background: var(--border); - border: 1px solid var(--border); - border-radius: var(--radius-lg); - overflow: hidden; + grid-template-columns: repeat(4, 1fr); + gap: 12px; margin-bottom: 36px; } .overview-stat { background: var(--bg-1); - padding: 18px 20px; + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 20px 16px; + text-align: center; + position: relative; + overflow: hidden; + transition: border-color .2s, transform .2s var(--ease); } + .overview-stat::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + background: linear-gradient(90deg, var(--accent) 0%, #8b5cf6 50%, #06b6d4 100%); + opacity: 0; + transition: opacity .2s; + } + + .overview-stat:hover::before { + opacity: 1; + } + + .overview-stat:hover { + border-color: var(--border-2); + transform: translateY(-1px); + } + .overview-stat-value { - font-size: 26px; - font-weight: 700; - letter-spacing: -0.02em; - font-family: var(--font-mono); + font-size: 28px; + font-weight: 800; + color: var(--text-1); + line-height: 1; + letter-spacing: -.03em; } .overview-stat-label { - font-size: 11.5px; - color: var(--text-2); + font-size: 10.5px; + font-weight: 600; + color: var(--text-3); + margin-top: 6px; text-transform: uppercase; - letter-spacing: 0.05em; - margin-top: 3px; + letter-spacing: .07em; } +/* Overview section */ .overview-section-title { - font-size: 11.5px; + font-size: 10.5px; font-weight: 700; - letter-spacing: 0.06em; + letter-spacing: .1em; text-transform: uppercase; - color: var(--text-2); - margin: 0 0 14px; + color: var(--text-3); + margin: 32px 0 14px; } +/* Security badges */ +.security-badges { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.security-badge { + display: flex; + align-items: center; + gap: 7px; + font-size: 12px; + font-weight: 500; + padding: 6px 12px; + 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; +} + + .security-badge svg { + width: 12px; + height: 12px; + } + +button.security-badge:hover { + border-color: var(--accent); + color: var(--accent-2); + background: var(--accent-dim); +} + +/* Overview group cards */ .overview-group-card { + background: var(--bg-1); border: 1px solid var(--border); border-radius: var(--radius-lg); - margin-bottom: 14px; + margin-bottom: 12px; overflow: hidden; - background: var(--bg-1); + transition: border-color .15s, box-shadow .15s; } + .overview-group-card:hover { + border-color: var(--border-2); + box-shadow: var(--shadow-sm); + } + .overview-group-card-header { - padding: 14px 18px; - border-bottom: 1px solid var(--border-soft); - font-weight: 650; - font-size: 14px; display: flex; align-items: center; justify-content: space-between; + padding: 14px 18px; + font-size: 13px; + font-weight: 700; + border-bottom: 1px solid var(--border); + background: var(--bg-2); } .overview-group-routes { - display: flex; - flex-direction: column; } .overview-route-row { @@ -647,9 +959,9 @@ a { align-items: center; gap: 12px; padding: 10px 18px; - border-bottom: 1px solid var(--border-soft); cursor: pointer; - transition: background .1s ease; + transition: background .1s; + border-bottom: 1px solid var(--border); } .overview-route-row:last-child { @@ -660,1043 +972,519 @@ a { background: var(--bg-2); } - .overview-route-row .method-badge { - width: 58px; - text-align: center; - flex: 0 0 auto; - } +.method-badge { + font-family: var(--font-mono); + font-size: 9.5px; + 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); + color: var(--method-color, var(--m-default)); + min-width: 54px; + text-align: center; + letter-spacing: .04em; +} - .overview-route-row .route-path { - font-family: var(--font-mono); - font-size: 12.5px; - color: var(--text-0); - flex: 0 0 auto; - } +.route-path { + font-family: var(--font-mono); + font-size: 12px; + flex-shrink: 0; + color: var(--text-1); +} - .overview-route-row .route-summary { - color: var(--text-2); - font-size: 12.5px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } +.route-summary { + font-size: 12px; + color: var(--text-2); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex: 1; +} - .overview-route-row .route-arrow { - margin-left: auto; - width: 14px; - height: 14px; - color: var(--text-2); - flex: 0 0 auto; - opacity: 0; - transform: translateX(-3px); - transition: opacity .12s ease, transform .12s ease; - } +.route-arrow { + width: 14px; + height: 14px; + color: var(--text-3); + flex-shrink: 0; + margin-left: auto; +} - .overview-route-row:hover .route-arrow { - opacity: 1; - transform: none; - } - -/* Endpoint detail */ +/* + ENDPOINT DETAIL + */ .endpoint-header { - padding: 30px 0 0; - margin-bottom: 6px; + margin-bottom: 32px; + padding-bottom: 28px; + border-bottom: 1px solid var(--border); } .breadcrumb { display: flex; align-items: center; gap: 6px; - font-size: 12px; - color: var(--text-2); - margin-bottom: 16px; + font-size: 11.5px; + color: var(--text-3); + margin-bottom: 14px; } .breadcrumb svg { - width: 11px; - height: 11px; + width: 13px; + height: 13px; } .breadcrumb .current { - color: var(--text-1); + color: var(--text-2); } .request-line { display: flex; - align-items: stretch; - font-family: var(--font-mono); + align-items: center; + gap: 12px; + margin-bottom: 14px; + padding: 12px 16px; + background: var(--bg-1); border: 1px solid var(--border); - border-radius: var(--radius-md); - background: var(--code-bg); - overflow: hidden; - margin-bottom: 18px; + border-radius: var(--radius-lg); + border-left: 3px solid var(--method-color, var(--accent)); } -.method-badge { - font-size: 11.5px; - font-weight: 700; - letter-spacing: 0.03em; - color: var(--bg-0); - background: var(--method-color, var(--text-2)); - padding: 9px 14px; - flex: 0 0 auto; - display: flex; - align-items: center; -} - -.request-line .endpoint-path { - flex: 1 1 auto; +.endpoint-path { + font-family: var(--font-mono); font-size: 14px; - color: var(--text-0); - word-break: break-all; - padding: 9px 14px; - display: flex; - align-items: center; + font-weight: 500; + color: var(--text-1); + letter-spacing: -.01em; + flex: 1; } -.request-line .copy-route-btn { - flex: 0 0 auto; - border: none; - border-left: 1px solid var(--border); - background: transparent; - color: var(--text-2); - cursor: pointer; - padding: 0 13px; +.copy-route-btn { + width: 28px; + height: 28px; + border-radius: var(--radius-sm); display: flex; align-items: center; - transition: color .12s ease, background .12s ease; + justify-content: center; + color: var(--text-3); + transition: background .12s, color .12s; + flex-shrink: 0; } - .request-line .copy-route-btn:hover { - color: var(--text-0); - background: var(--bg-2); - } - - .request-line .copy-route-btn svg { + .copy-route-btn svg { width: 14px; height: 14px; } + .copy-route-btn:hover { + background: var(--bg-3); + color: var(--text-1); + } + .endpoint-summary { - font-size: 23px; - font-weight: 650; - letter-spacing: -0.015em; - margin: 0 0 8px; - line-height: 1.3; + font-size: 22px; + font-weight: 800; + letter-spacing: -.03em; + margin-bottom: 8px; + line-height: 1.2; } .endpoint-description { - color: var(--text-1); - font-size: 14px; - line-height: 1.65; - margin: 0; - max-width: 640px; + font-size: 13.5px; + color: var(--text-2); + line-height: 1.75; } .deprecated-banner { display: flex; align-items: center; - gap: 8px; - margin-top: 14px; - font-size: 12.5px; - color: var(--m-delete); - background: color-mix(in srgb, var(--m-delete) 10%, transparent); - border: 1px solid color-mix(in srgb, var(--m-delete) 28%, transparent); - padding: 9px 12px; + gap: 10px; + padding: 10px 16px; + margin: 14px 0; + background: color-mix(in srgb, #fbbf24 8%, transparent); + border: 1px solid color-mix(in srgb, #fbbf24 25%, transparent); border-radius: var(--radius-md); + font-size: 12px; + color: #fbbf24; } .deprecated-banner svg { - width: 15px; - height: 15px; - flex: 0 0 auto; + width: 14px; + height: 14px; + flex-shrink: 0; } +/* Sections */ .section { - margin-top: 34px; + margin-bottom: 32px; } .section-title { - font-size: 11.5px; + font-size: 10.5px; font-weight: 700; - letter-spacing: 0.07em; + letter-spacing: .1em; text-transform: uppercase; - color: var(--text-2); - margin: 0 0 14px; + color: var(--text-3); + margin-bottom: 14px; display: flex; align-items: center; gap: 8px; } - .section-title .count { - font-family: var(--font-mono); - font-weight: 600; - color: var(--text-2); - background: var(--bg-2); - border-radius: 20px; - padding: 0px 7px; - font-size: 10.5px; - } +.count { + font-size: 10px; + font-family: var(--font-mono); + padding: 0 7px; + height: 17px; + line-height: 17px; + background: var(--bg-3); + border-radius: 99px; + color: var(--text-2); + font-weight: 600; +} +/* Param table */ .param-table { width: 100%; border-collapse: collapse; - font-size: 13px; + font-size: 12.5px; border: 1px solid var(--border); - border-radius: var(--radius-md); + border-radius: var(--radius-lg); overflow: hidden; } - .param-table th { + .param-table thead th { text-align: left; - font-size: 11px; + padding: 9px 14px; + font-size: 10px; + font-weight: 700; + letter-spacing: .09em; text-transform: uppercase; - letter-spacing: 0.04em; - color: var(--text-2); - font-weight: 650; - padding: 9px 12px; - background: var(--bg-2); + color: var(--text-3); + background: var(--bg-1); border-bottom: 1px solid var(--border); } - .param-table td { - padding: 11px 12px; - border-bottom: 1px solid var(--border-soft); - vertical-align: top; + .param-table tbody tr { + border-bottom: 1px solid var(--border); + transition: background .1s; } - .param-table tr:last-child td { - border-bottom: none; + .param-table tbody tr:last-child { + border-bottom: none; + } + + .param-table tbody tr:hover { + background: var(--bg-2); + } + + .param-table td { + padding: 10px 14px; + vertical-align: top; } .param-name { font-family: var(--font-mono); font-size: 12.5px; - color: var(--text-0); font-weight: 600; - display: flex; - align-items: center; - gap: 6px; } .param-required { - color: var(--m-delete); - font-size: 11px; + color: #f87171; + margin-left: 2px; + font-size: 14px; + line-height: 1; } .param-loc { - font-family: var(--font-mono); - font-size: 10.5px; + display: inline-block; + padding: 2px 7px; + font-size: 9.5px; + font-weight: 700; + background: var(--bg-3); + border-radius: var(--radius-xs); color: var(--text-2); - background: var(--bg-2); - border: 1px solid var(--border); - border-radius: 4px; - padding: 1px 5px; + font-family: var(--font-mono); + letter-spacing: .03em; } .param-type { font-family: var(--font-mono); - font-size: 12px; - color: var(--accent); + font-size: 11px; + color: var(--accent-2); } .param-desc { - color: var(--text-1); - font-size: 12.5px; - line-height: 1.55; + color: var(--text-2); + font-size: 12px; } +/* Schema box */ .schema-box { background: var(--code-bg); border: 1px solid var(--border); - border-radius: var(--radius-md); - padding: 16px 18px; + border-radius: var(--radius-lg); + padding: 16px 20px; font-family: var(--font-mono); - font-size: 12.5px; - line-height: 1.75; + font-size: 12px; + line-height: 1.85; + white-space: pre-wrap; overflow-x: auto; - position: relative; -} - -.schema-line { - white-space: pre; } .schema-key { - color: var(--text-0); -} - -.schema-punct { - color: var(--text-2); + color: #93c5fd; } .schema-type { - color: var(--accent); + color: var(--accent-2); } .schema-comment { - color: var(--text-2); + color: var(--text-3); font-style: italic; } +.schema-punct { + color: var(--text-3); +} + .schema-required-mark { - color: var(--m-delete); + color: #f87171; } .schema-nullable-mark { - color: var(--text-2); - font-style: italic; + color: #fbbf24; } +/* Response blocks */ .response-block { - margin-bottom: 14px; + margin-bottom: 10px; border: 1px solid var(--border); - border-radius: var(--radius-md); + border-radius: var(--radius-lg); overflow: hidden; + transition: border-color .15s; } + .response-block:hover { + border-color: var(--border-2); + } + .response-block-header { display: flex; align-items: center; - gap: 10px; - padding: 10px 14px; - background: var(--bg-2); - border-bottom: 1px solid var(--border); -} - -.status-pill { - font-family: var(--font-mono); - font-size: 11.5px; - font-weight: 700; - padding: 2px 8px; - border-radius: 4px; -} - -.status-2xx { - color: var(--m-get); - background: color-mix(in srgb, var(--m-get) 15%, transparent); -} - -.status-4xx { - color: var(--m-put); - background: color-mix(in srgb, var(--m-put) 15%, transparent); -} - -.status-5xx { - color: var(--m-delete); - background: color-mix(in srgb, var(--m-delete) 15%, transparent); -} - -.response-desc { - font-size: 12.5px; - color: var(--text-1); + gap: 12px; + padding: 11px 16px; + background: var(--bg-1); } .response-block-body { - padding: 13px 15px; + padding: 14px 16px; + border-top: 1px solid var(--border); + background: var(--bg-0); } -/* Try pane (right) */ -.try-pane { - border-left: 1px solid var(--border); - background: color-mix(in srgb, var(--bg-1) 96%, transparent); - overflow-y: auto; - min-width: 0; +/* Status pills */ +.status-pill { + display: inline-flex; + align-items: center; + justify-content: center; + font-family: var(--font-mono); + font-size: 11px; + font-weight: 700; + padding: 3px 9px; + border-radius: var(--radius-sm); + flex-shrink: 0; + letter-spacing: .03em; } -.try-content { - padding: 24px 22px 60px; +.status-2xx { + background: color-mix(in srgb, #34d399 14%, transparent); + color: #34d399; } -.try-empty { - padding: 70px 18px; - text-align: center; +.status-4xx { + background: color-mix(in srgb, #fbbf24 14%, transparent); + color: #fbbf24; +} + +.status-5xx { + background: color-mix(in srgb, #f87171 14%, transparent); + color: #f87171; +} + +.response-desc { + font-size: 13px; color: var(--text-2); - font-size: 12.5px; - line-height: 1.6; +} + +/* + TRY-IT PANE + */ +.try-empty { + text-align: center; + padding: 70px 28px; + color: var(--text-3); + font-size: 13px; + line-height: 1.75; } .try-empty svg { - width: 30px; - height: 30px; - color: var(--border); - margin-bottom: 12px; + width: 28px; + height: 28px; + margin: 0 auto 14px; + display: block; + opacity: .3; } .try-header { display: flex; align-items: center; justify-content: space-between; - margin-bottom: 18px; + margin-bottom: 16px; padding-bottom: 14px; - border-bottom: 1px solid var(--border-soft); + border-bottom: 1px solid var(--border); } .try-title { - font-size: 12px; + font-size: 11px; font-weight: 700; + letter-spacing: .1em; text-transform: uppercase; - letter-spacing: 0.06em; color: var(--text-2); display: flex; align-items: center; - gap: 7px; + gap: 8px; } - .try-title .live-dot { - width: 6px; - height: 6px; - border-radius: 50%; - background: var(--method-color, var(--m-get)); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--method-color, var(--m-get)) 25%, transparent); +.live-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--method-color, var(--accent)); + box-shadow: 0 0 8px var(--method-color, var(--accent)); + animation: pulse-dot 2.2s ease-in-out infinite; +} + +@keyframes pulse-dot { + 0%, 100% { + opacity: 1; + transform: scale(1); } + 50% { + opacity: .5; + transform: scale(.85); + } +} + +/* Try tabs */ .try-tabs { display: flex; gap: 2px; + margin-bottom: 16px; background: var(--bg-2); - border: 1px solid var(--border); border-radius: var(--radius-md); - padding: 2px; - margin-bottom: 18px; + padding: 3px; + border: 1px solid var(--border); } .try-tab { - flex: 1 1 auto; - text-align: center; - border: none; - background: transparent; - color: var(--text-2); + flex: 1; + height: 27px; font-size: 12px; font-weight: 600; - padding: 7px 8px; - border-radius: 6px; - cursor: pointer; - font-family: var(--font-mono); + border-radius: calc(var(--radius-md) - 2px); + color: var(--text-3); + transition: background .12s, color .12s, box-shadow .12s; } .try-tab.active { - background: var(--bg-raised); - color: var(--text-0); - box-shadow: 0 1px 2px rgba(0,0,0,0.08); + background: var(--bg-1); + color: var(--text-1); + box-shadow: var(--shadow-sm); } -.field-group { - margin-bottom: 16px; -} - -.field-label { - display: flex; - align-items: center; - gap: 6px; - font-family: var(--font-mono); - font-size: 12px; - color: var(--text-1); - margin-bottom: 6px; -} - -.field-sublabel { - font-family: var(--font-mono); - margin-bottom: 4px; - color: var(--text-2); - font-size: 11px; - display: flex; - align-items: center; - gap: 5px; -} - - .field-sublabel .req-star { - color: var(--m-delete); - } - - .field-sublabel .type-hint { + .try-tab:hover:not(.active) { color: var(--text-2); - font-weight: 400; - margin-left: auto; - opacity: 0.8; + background: var(--bg-3); } -.field-input { - width: 100%; - background: var(--bg-2); - border: 1px solid var(--border); - border-radius: var(--radius-sm); - padding: 9px 11px; - color: var(--text-0); - font-family: var(--font-mono); - font-size: 12.5px; - outline: none; - transition: border-color .12s ease, box-shadow .12s ease; -} - - .field-input:focus { - border-color: var(--accent); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 12%, transparent); - } - -textarea.field-input { - resize: vertical; - min-height: 140px; - line-height: 1.65; -} - -.send-btn { - width: 100%; - background: var(--method-color, var(--accent)); - color: #08090D; - border: none; - border-radius: var(--radius-md); - padding: 11px; - font-weight: 700; - font-size: 13px; - cursor: pointer; - margin-top: 8px; - display: flex; - align-items: center; - justify-content: center; - gap: 8px; - transition: filter .12s ease, transform .04s ease; - font-family: var(--font-mono); -} - - .send-btn:hover { - filter: brightness(1.08); - } - - .send-btn:active { - transform: scale(0.99); - } - - .send-btn:disabled { - opacity: 0.6; - cursor: default; - } - - .send-btn svg { - width: 14px; - height: 14px; - } - -.spinner { - width: 14px; - height: 14px; - border: 2px solid rgba(0,0,0,0.25); - border-top-color: #08090D; - border-radius: 50%; - animation: spin 0.7s linear infinite; -} - -@keyframes spin { - to { - transform: rotate(360deg); - } -} - -.response-panel { - margin-top: 24px; - border-top: 1px solid var(--border); - padding-top: 20px; -} - -.response-meta { - display: flex; - align-items: center; - gap: 10px; - margin-bottom: 12px; - flex-wrap: wrap; -} - -.response-time { - font-family: var(--font-mono); - font-size: 11.5px; - color: var(--text-2); - display: flex; - align-items: center; - gap: 4px; -} - - .response-time svg { - width: 11px; - height: 11px; - } - -.response-body { - background: var(--code-bg); - border: 1px solid var(--border); - border-radius: var(--radius-md); - padding: 13px 15px; - font-family: var(--font-mono); - font-size: 12px; - line-height: 1.65; - max-height: 420px; - overflow: auto; - white-space: pre-wrap; - word-break: break-word; -} - -.response-error { - color: var(--m-delete); -} - -.copy-btn { - background: transparent; - border: 1px solid var(--border); - color: var(--text-2); - border-radius: 5px; - padding: 4px 9px; - font-size: 11px; - cursor: pointer; - font-family: var(--font-mono); - display: inline-flex; - align-items: center; - gap: 5px; - transition: color .12s ease, border-color .12s ease; -} - - .copy-btn svg { - width: 11px; - height: 11px; - } - - .copy-btn:hover { - color: var(--text-0); - border-color: var(--text-1); - } - -.json-key { - color: #7FB3FF; -} - -.json-string { - color: #5FE3A8; -} - -.json-number { - color: #F5B947; -} - -.json-boolean { - color: #D5A6F5; -} - -.json-null { - color: var(--text-2); -} - -.action-group { - display: flex; - gap: 8px; -} - -.action-btn { - height: 34px; - padding: 0 12px; - border: 1px solid var(--border); - border-radius: 10px; - background: var(--bg-2); - color: var(--text-0); - cursor: pointer; - font-weight: 600; - transition: all .18s var(--ease); -} - - .action-btn:hover { - transform: translateY(-1px); - border-color: color-mix(in srgb,var(--accent) 40%,var(--border)); - box-shadow: 0 8px 24px color-mix(in srgb,var(--accent) 15%,transparent); - } - -.action-btn-primary { - background: linear-gradient( 135deg, var(--accent), color-mix(in srgb,var(--accent) 70%,#ffffff 10%) ); - color: white; - border-color: transparent; -} - -.icon-btn-badge { - position: relative; -} - - .icon-btn-badge .badge-dot { - position: absolute; - top: 5px; - right: 6px; - width: 7px; - height: 7px; - border-radius: 50%; - background: var(--m-get); - border: 1.5px solid var(--bg-1); - } - - -/* curl snippet */ -.curl-box { - background: var(--code-bg); - border: 1px solid var(--border); - border-radius: var(--radius-md); - padding: 13px 15px; - font-family: var(--font-mono); - font-size: 11.5px; - line-height: 1.7; - overflow-x: auto; - white-space: pre; - position: relative; - color: var(--text-1); -} - - .curl-box .curl-flag { - color: var(--accent); - } - - .curl-box .curl-string { - color: #5FE3A8; - } - -.curl-header-row { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 8px; -} - -/* responsive: collapse try pane on narrow screens behind a toggle */ -@media (max-width: 1180px) { - :root { - --try-w: 380px; - } -} - -@media (max-width: 980px) { - .body-grid { - grid-template-columns: 240px 1fr; - } - - .try-pane { - display: none; - } - - .try-pane.open { - display: block; - position: fixed; - right: 0; - top: var(--topbar-h); - bottom: 0; - width: 380px; - box-shadow: var(--shadow); - z-index: 20; - } -} - -@media (max-width: 680px) { - .body-grid { - grid-template-columns: 1fr; - } - - .nav-pane { - display: none; - position: fixed; - left: 0; - top: var(--topbar-h); - bottom: 0; - width: 270px; - z-index: 20; - box-shadow: var(--shadow); - } - - .nav-pane.open { - display: block; - } - - .topbar-search { - max-width: none; - } - - .overview-stats { - grid-template-columns: 1fr; - } -} - -.skeleton { - background: linear-gradient(90deg, var(--bg-2) 25%, var(--bg-3) 37%, var(--bg-2) 63%); - background-size: 400% 100%; - animation: shimmer 1.4s ease infinite; - border-radius: var(--radius-sm); -} - -@keyframes shimmer { - 0% { - background-position: 100% 50%; - } - - 100% { - background-position: 0 50%; - } -} - -.fade-in { - animation: fadeIn .22s var(--ease); -} - -@keyframes fadeIn { - from { - opacity: 0; - transform: translateY(3px); - } - - to { - opacity: 1; - transform: none; - } -} - -@media (prefers-reduced-motion: reduce) { - *, *::before, *::after { - animation-duration: 0.001ms !important; - transition-duration: 0.001ms !important; - } -} - - -.api-switcher { - display: flex; - align-items: center; - margin-right: 12px; -} - -#apiSelector { - min-width: 220px; - height: 38px; - border-radius: 10px; - padding: 0 12px; - background: var(--panel,#1e1e1e); - color: var(--text,#fff); - border: 1px solid var(--border,#3a3a3a); - font-weight: 600; - cursor: pointer; -} - - #apiSelector:hover { - filter: brightness(1.05); - } - - #apiSelector:focus { - outline: none; - } - - -/* ---------------------------------------------------------------- */ -/* Advanced features: Auth, Code generation, History, Console, Mock */ -/* ---------------------------------------------------------------- */ - +/* Auth banner */ .auth-banner { display: flex; align-items: center; - gap: 8px; - padding: 9px 11px; - border-radius: var(--radius-sm); - border: 1px solid var(--border); - background: var(--bg-2); - font-size: 11.5px; - color: var(--text-1); + gap: 9px; + padding: 10px 14px; + border-radius: var(--radius-md); + font-size: 12px; margin-bottom: 14px; } .auth-banner svg { width: 14px; height: 14px; - flex: 0 0 auto; + flex-shrink: 0; } - .auth-banner.auth-set { - border-color: color-mix(in srgb, var(--m-get) 45%, var(--border)); - color: var(--text-0); - } +.auth-set { + background: color-mix(in srgb, #34d399 10%, transparent); + border: 1px solid color-mix(in srgb, #34d399 22%, transparent); + color: #34d399; +} - .auth-banner.auth-set svg { - color: var(--m-get); - } - - .auth-banner.auth-missing svg { - color: var(--m-delete); - } +.auth-missing { + background: color-mix(in srgb, #fbbf24 10%, transparent); + border: 1px solid color-mix(in srgb, #fbbf24 22%, transparent); + color: #fbbf24; +} .auth-banner-link { margin-left: auto; - background: transparent; - border: 1px solid var(--border); - color: var(--text-0); - border-radius: 6px; - padding: 4px 9px; - font-size: 11px; - font-weight: 600; + font-size: 11.5px; + font-weight: 700; + text-decoration: underline; + background: none; + border: none; cursor: pointer; - font-family: var(--font-mono); - flex: 0 0 auto; + color: inherit; + opacity: .85; + flex-shrink: 0; } .auth-banner-link:hover { - border-color: var(--accent); - color: var(--accent); + opacity: 1; } -.auth-scheme-card { - border: 1px solid var(--border); - border-radius: var(--radius-md); - padding: 12px; - margin-bottom: 12px; - background: var(--bg-2); -} - -.auth-scheme-head { - display: flex; - align-items: center; - gap: 8px; - margin-bottom: 10px; -} - -.auth-scheme-name { - font-weight: 700; - font-size: 12.5px; -} - -.auth-scheme-type { - font-family: var(--font-mono); - font-size: 10.5px; - color: var(--text-2); - background: var(--bg-3); - border: 1px solid var(--border); - padding: 1px 7px; - border-radius: 20px; -} - -.auth-scheme-status { - margin-left: auto; - width: 8px; - height: 8px; - border-radius: 50%; - background: var(--text-2); -} - - .auth-scheme-status.set { - background: var(--m-get); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--m-get) 25%, transparent); - } - -.auth-scheme-desc { - font-size: 11.5px; - color: var(--text-2); - margin: 0 0 10px; - line-height: 1.5; -} - -.auth-clear-btn { - background: transparent; - border: 1px solid var(--border); - color: var(--text-2); - border-radius: 6px; - padding: 6px 10px; - font-size: 11px; - cursor: pointer; - font-family: var(--font-mono); - width: 100%; - margin-top: 4px; -} - - .auth-clear-btn:hover { - color: var(--m-delete); - border-color: var(--m-delete); - } - -.code-lang-tabs { - display: flex; - flex-wrap: wrap; - gap: 4px; - margin-bottom: 12px; -} - -.code-lang-tab { - border: 1px solid var(--border); - background: var(--bg-2); - color: var(--text-1); - font-family: var(--font-mono); - font-size: 11px; - font-weight: 600; - padding: 5px 10px; - border-radius: 20px; - cursor: pointer; -} - - .code-lang-tab.active { - background: var(--accent); - border-color: var(--accent); - color: white; - } - -.history-empty { - padding: 40px 10px; - text-align: center; - color: var(--text-2); - font-size: 12px; -} - -.history-item { - border: 1px solid var(--border); - border-radius: var(--radius-sm); - padding: 8px 10px; - margin-bottom: 8px; - cursor: pointer; - transition: border-color .12s ease, background .12s ease; -} - - .history-item:hover { - border-color: var(--accent); - background: var(--bg-2); - } - -.history-item-row { - display: flex; - align-items: center; - gap: 8px; - margin-bottom: 3px; -} - -.history-item-time { - font-family: var(--font-mono); - font-size: 10.5px; - color: var(--text-2); - margin-left: auto; -} - -.history-item-path { - font-family: var(--font-mono); - font-size: 11.5px; - color: var(--text-1); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - +/* Mock toggle */ .mock-toggle-row { display: flex; align-items: center; - gap: 9px; - margin-bottom: 14px; - padding: 9px 11px; - border: 1px solid var(--border); - border-radius: var(--radius-sm); + justify-content: space-between; + padding: 9px 14px; background: var(--bg-2); + border: 1px solid var(--border); + border-radius: var(--radius-md); + font-size: 12px; + margin-bottom: 14px; + cursor: pointer; + transition: border-color .13s; } + .mock-toggle-row:hover { + border-color: var(--border-2); + } + .mock-toggle-label { - font-size: 11.5px; - color: var(--text-1); - flex: 1 1 auto; + color: var(--text-2); } +/* Switch */ .switch { position: relative; + display: inline-block; width: 34px; height: 19px; - flex: 0 0 auto; + flex-shrink: 0; } .switch input { @@ -1708,369 +1496,767 @@ textarea.field-input { .switch-slider { position: absolute; inset: 0; - background: var(--bg-3); - border: 1px solid var(--border); - border-radius: 20px; cursor: pointer; - transition: background .15s ease; + background: var(--bg-4); + border-radius: 99px; + transition: background .2s; } .switch-slider::before { - content: ""; + content: ''; position: absolute; width: 13px; height: 13px; - left: 2px; - top: 2px; - background: var(--text-2); border-radius: 50%; - transition: transform .15s ease, background .15s ease; + left: 3px; + top: 3px; + background: #fff; + transition: transform .2s var(--ease); + box-shadow: 0 1px 4px rgba(0,0,0,.3); } .switch input:checked + .switch-slider { - background: color-mix(in srgb, var(--accent) 30%, var(--bg-3)); - border-color: var(--accent); + background: var(--accent); } .switch input:checked + .switch-slider::before { transform: translateX(15px); - background: var(--accent); } -.mock-badge { - display: inline-flex; - align-items: center; - gap: 4px; - font-family: var(--font-mono); - font-size: 10px; +/* Field groups */ +.field-group { + margin-bottom: 14px; +} + +.field-label { + font-size: 10.5px; font-weight: 700; + letter-spacing: .08em; text-transform: uppercase; - letter-spacing: 0.04em; - color: var(--m-patch); - background: color-mix(in srgb, var(--m-patch) 14%, transparent); - border: 1px solid color-mix(in srgb, var(--m-patch) 35%, transparent); - padding: 2px 7px; - border-radius: 20px; + color: var(--text-3); + margin-bottom: 9px; } -.security-badges { - display: flex; - flex-wrap: wrap; - gap: 6px; - margin: 10px 0 0; -} - -.security-badge { - display: inline-flex; - align-items: center; - gap: 5px; - font-family: var(--font-mono); - font-size: 11px; - color: var(--text-1); - background: var(--bg-2); - border: 1px solid var(--border); - padding: 3px 9px; - border-radius: 20px; -} - - .security-badge svg { - width: 11px; - height: 11px; - color: var(--text-2); - } - -.console-trigger { +.field-sublabel { + font-size: 12px; + font-weight: 500; + color: var(--text-2); + margin-bottom: 5px; display: flex; align-items: center; gap: 7px; } - .console-trigger .request-url { - flex: 1 1 auto; - } - -.diff-summary { - display: flex; - flex-direction: column; - gap: 6px; - margin: 12px 0; -} - -.diff-row { - display: flex; - align-items: flex-start; - gap: 8px; - font-size: 12px; - padding: 8px 10px; - border-radius: var(--radius-sm); - border: 1px solid var(--border); - background: var(--bg-2); -} - -.diff-tag { +.type-hint { font-family: var(--font-mono); font-size: 10px; + color: var(--text-3); +} + +.req-star { + color: #f87171; +} + +.field-input { + width: 100%; + background: var(--bg-2); + border: 1px solid var(--border-2); + border-radius: var(--radius-md); + padding: 8px 12px; + font-size: 12.5px; + color: var(--text-1); + transition: border-color .15s, box-shadow .15s; +} + + .field-input:focus { + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-dim); + outline: none; + } + +textarea.field-input { + min-height: 120px; + font-family: var(--font-mono); + font-size: 12px; + resize: vertical; + line-height: 1.7; +} + +/* Send button */ +.send-btn { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + width: 100%; + height: 40px; + font-size: 13px; font-weight: 700; - padding: 1px 7px; - border-radius: 20px; - flex: 0 0 auto; - text-transform: uppercase; + letter-spacing: -.01em; + background: var(--method-color, var(--accent)); + color: #fff; + border-radius: var(--radius-md); + margin-top: 6px; + transition: opacity .14s, transform .1s var(--ease), box-shadow .14s; + box-shadow: 0 2px 12px color-mix(in srgb, var(--method-color, var(--accent)) 45%, transparent); + position: relative; + overflow: hidden; } -.diff-tag-added { - color: var(--m-get); - background: color-mix(in srgb, var(--m-get) 16%, transparent); + .send-btn::before { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient(180deg, rgba(255,255,255,.15) 0%, transparent 100%); + } + + .send-btn svg { + width: 16px; + height: 16px; + position: relative; + z-index: 1; + } + + .send-btn span { + position: relative; + z-index: 1; + } + + .send-btn:hover { + opacity: .88; + transform: translateY(-1px); + box-shadow: 0 4px 18px color-mix(in srgb, var(--method-color, var(--accent)) 55%, transparent); + } + + .send-btn:active { + transform: scale(.98); + } + + .send-btn:disabled { + opacity: .4; + cursor: not-allowed; + transform: none; + } + +/* Code language tabs */ +.code-lang-tabs { + display: flex; + gap: 2px; + margin-bottom: 12px; + overflow-x: auto; + background: var(--bg-2); + border-radius: var(--radius-md); + padding: 3px; + border: 1px solid var(--border); } -.diff-tag-removed { - color: var(--m-delete); - background: color-mix(in srgb, var(--m-delete) 16%, transparent); +.code-lang-tab { + padding: 4px 12px; + font-size: 11px; + font-weight: 600; + border-radius: calc(var(--radius-md) - 2px); + color: var(--text-3); + white-space: nowrap; + transition: background .12s, color .12s; } -.diff-tag-changed { - color: var(--m-put); - background: color-mix(in srgb, var(--m-put) 16%, transparent); + .code-lang-tab.active { + background: var(--accent-dim); + color: var(--accent-2); + } + + .code-lang-tab:hover:not(.active) { + background: var(--bg-3); + color: var(--text-2); + } + +.curl-header-row { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 9px; } -/* modal (used for auth config + console + diff) */ +.curl-box { + background: var(--code-bg); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 16px; + font-family: var(--font-mono); + font-size: 11.5px; + line-height: 1.85; + overflow-x: auto; + white-space: pre; +} + +.curl-flag { + color: #38bdf8; +} + +.curl-string { + color: #86efac; +} + +/* Response panel */ +.response-panel { + margin-top: 14px; + border: 1px solid var(--border); + border-radius: var(--radius-lg); + overflow: hidden; + animation: fade-up .18s var(--ease); +} + +.response-meta { + display: flex; + align-items: center; + gap: 9px; + padding: 9px 14px; + background: var(--bg-2); + border-bottom: 1px solid var(--border); +} + +.response-time { + display: flex; + align-items: center; + gap: 5px; + font-size: 11px; + color: var(--text-3); + font-family: var(--font-mono); +} + + .response-time svg { + width: 12px; + height: 12px; + } + +.mock-badge { + display: flex; + align-items: center; + gap: 4px; + font-size: 10.5px; + font-weight: 600; + padding: 2px 8px; + border-radius: 99px; + background: color-mix(in srgb, var(--accent) 14%, transparent); + color: var(--accent-2); +} + +.response-body { + padding: 16px; + font-family: var(--font-mono); + font-size: 12px; + line-height: 1.75; + white-space: pre-wrap; + word-break: break-all; + max-height: 400px; + overflow-y: auto; + background: var(--code-bg); +} + +.response-error { + color: #f87171; +} + +/* JSON highlighting */ +.json-key { + color: #93c5fd; +} + +.json-string { + color: #86efac; +} + +.json-number { + color: #fdba74; +} + +.json-boolean { + color: #f9a8d4; +} + +.json-null { + color: var(--text-3); +} + +/* Copy button */ +.copy-btn { + display: flex; + align-items: center; + gap: 5px; + height: 27px; + padding: 0 9px; + font-size: 11px; + font-weight: 600; + background: var(--bg-3); + border: 1px solid var(--border-2); + border-radius: var(--radius-sm); + color: var(--text-2); + transition: background .12s, color .12s, border-color .12s; +} + + .copy-btn svg { + width: 12px; + height: 12px; + } + + .copy-btn:hover { + background: var(--bg-4); + color: var(--text-1); + border-color: var(--border-3); + } + +/* Spinner */ +.spinner { + display: inline-block; + width: 14px; + height: 14px; + border: 2px solid rgba(255,255,255,.25); + border-top-color: #fff; + border-radius: 50%; + animation: spin .65s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* History */ +.history-empty { + padding: 28px 16px; + text-align: center; + color: var(--text-3); + font-size: 12px; + line-height: 1.75; +} + +.history-item { + padding: 10px; + border-bottom: 1px solid var(--border); + cursor: pointer; + transition: background .1s; + border-radius: var(--radius-sm); + margin-bottom: 2px; +} + + .history-item:hover { + background: var(--bg-2); + } + +.history-item-row { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 4px; +} + +.history-item-time { + font-size: 11px; + color: var(--text-3); + margin-left: auto; + font-family: var(--font-mono); +} + +.history-item-path { + font-family: var(--font-mono); + font-size: 11px; + color: var(--text-2); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* + MODAL + */ .modal-backdrop { position: fixed; inset: 0; - background: rgba(5,6,10,0.55); - backdrop-filter: blur(2px); - z-index: 100; + z-index: 1000; + background: rgba(0,0,0,.7); + backdrop-filter: blur(6px); + -webkit-backdrop-filter: blur(6px); display: flex; align-items: center; justify-content: center; padding: 24px; + animation: fade-up .14s var(--ease); } .modal-box { - width: 100%; - max-width: 640px; - max-height: 86vh; - overflow-y: auto; background: var(--bg-1); - border: 1px solid var(--border); - border-radius: var(--radius-lg); - box-shadow: var(--shadow); - padding: 22px; + border: 1px solid var(--border-2); + border-radius: var(--radius-xl); + box-shadow: var(--shadow-lg); + width: 100%; + max-width: 490px; + max-height: 82vh; + display: flex; + flex-direction: column; + overflow: hidden; + animation: modal-in .18s var(--ease); } - .modal-box.modal-wide { - max-width: 880px; +.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; - gap: 10px; - margin-bottom: 16px; + justify-content: space-between; + padding: 18px 22px; + border-bottom: 1px solid var(--border); + flex-shrink: 0; + background: var(--bg-2); } .modal-title { font-size: 15px; font-weight: 700; + letter-spacing: -.02em; } .modal-close { - margin-left: auto; - background: transparent; - border: 1px solid var(--border); - color: var(--text-1); - width: 28px; - height: 28px; - border-radius: 8px; - cursor: pointer; + width: 30px; + height: 30px; + border-radius: var(--radius-md); display: flex; align-items: center; justify-content: center; + color: var(--text-3); + transition: background .12s, color .12s; } .modal-close:hover { - border-color: var(--m-delete); - color: var(--m-delete); + background: var(--bg-3); + color: var(--text-1); } .modal-close svg { - width: 14px; - height: 14px; + width: 15px; + height: 15px; } -/* Dashboard Styles */ -.dashboard-panel { - position: absolute; +.modal-body { + overflow-y: auto; + padding: 20px 22px; + flex: 1; +} + +/* Auth card */ +.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; +} + + .auth-scheme-card:hover { + border-color: var(--border-3); + } + +.auth-scheme-head { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 14px; +} + +.auth-scheme-name { + font-size: 13.5px; + font-weight: 700; +} + +.auth-scheme-type { + font-size: 9.5px; + font-weight: 700; + padding: 2px 7px; + border-radius: var(--radius-xs); + background: var(--bg-3); + color: var(--text-2); + font-family: var(--font-mono); + letter-spacing: .04em; + text-transform: uppercase; +} + +.auth-scheme-status { + width: 9px; + height: 9px; + border-radius: 50%; + background: var(--bg-5); + margin-left: auto; + flex-shrink: 0; + transition: background .25s, box-shadow .25s; +} + + .auth-scheme-status.set { + background: #34d399; + box-shadow: 0 0 8px rgba(52,211,153,.5); + } + +.auth-scheme-desc { + font-size: 12px; + color: var(--text-2); + margin-bottom: 12px; + line-height: 1.6; +} + +.auth-clear-btn { + font-size: 11px; + color: var(--text-3); + margin-top: 8px; + text-decoration: underline; + transition: color .12s; +} + + .auth-clear-btn:hover { + color: #f87171; + } + +/* Console */ +.console-trigger { + display: flex; + gap: 8px; + margin-bottom: 14px; +} + +.request-url { + flex: 1; +} + +/* Diff */ +.diff-summary { + display: flex; + gap: 16px; + padding: 14px 0; + margin-bottom: 18px; + border-bottom: 1px solid var(--border); + flex-wrap: wrap; +} + +.diff-row { + display: flex; + align-items: center; + gap: 9px; + font-size: 12.5px; + color: var(--text-2); + margin-bottom: 6px; +} + +.diff-tag { + font-family: var(--font-mono); + font-size: 11px; + padding: 2px 8px; + border-radius: var(--radius-sm); + font-weight: 700; +} + +.diff-tag-added { + background: color-mix(in srgb, #34d399 14%, transparent); + color: #34d399; +} + +.diff-tag-removed { + background: color-mix(in srgb, #f87171 14%, transparent); + color: #f87171; +} + +.diff-tag-changed { + background: color-mix(in srgb, #fbbf24 14%, transparent); + color: #fbbf24; +} + +/* + DASHBOARD DRAWER + */ +.drawer { + position: fixed; + top: var(--topbar-h); right: 0; - top: 58px; bottom: 0; - width: 400px; + z-index: 90; + width: 420px; background: var(--bg-1); border-left: 1px solid var(--border); + box-shadow: var(--shadow-lg); display: flex; flex-direction: column; - z-index: 100; - box-shadow: var(--shadow); + animation: slide-in .22s var(--ease); } -.dashboard-header { +@keyframes slide-in { + from { + transform: translateX(40px); + opacity: 0; + } + + to { + transform: none; + opacity: 1; + } +} + +.drawer-header { display: flex; + align-items: center; justify-content: space-between; - align-items: center; - padding: 16px; + padding: 18px 22px; border-bottom: 1px solid var(--border); - background: var(--bg-raised); + flex-shrink: 0; + background: var(--bg-2); } -.dashboard-header h2 { - margin: 0; - font-size: 16px; - font-weight: 600; - color: var(--text-0); +.drawer-title { + font-size: 15px; + font-weight: 700; + letter-spacing: -.02em; } -.close-btn { - background: transparent; - border: none; - font-size: 24px; - color: var(--text-1); - cursor: pointer; - padding: 0; - width: 28px; - height: 28px; - display: flex; - align-items: center; - justify-content: center; -} - -.close-btn:hover { - color: var(--text-0); -} - -.dashboard-content { - flex: 1; +.drawer-body { overflow-y: auto; - padding: 16px; + padding: 20px; + flex: 1; } -.dashboard-stats { +/* Dashboard stat cards */ +.stat-grid { display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 12px; - margin-bottom: 24px; + grid-template-columns: 1fr 1fr; + gap: 10px; + margin-bottom: 22px; } .stat-card { background: var(--bg-2); border: 1px solid var(--border); - border-radius: var(--radius-md); + border-radius: var(--radius-lg); padding: 16px; text-align: center; - transition: all 0.2s ease; -} - -.stat-card:hover { - border-color: var(--accent); - background: var(--bg-3); -} - -.stat-label { - font-size: 12px; - font-weight: 600; - text-transform: uppercase; - color: var(--text-2); - margin-bottom: 8px; - letter-spacing: 0.5px; -} - -.stat-value { - font-size: 24px; - font-weight: 700; - color: var(--accent); - font-family: var(--font-mono); -} - -.stat-value.error-red { - color: var(--m-delete); -} - -.metric-chart { - background: var(--bg-2); - border: 1px solid var(--border); - border-radius: var(--radius-md); - padding: 16px; - margin-bottom: 16px; - min-height: 200px; - display: flex; - align-items: center; - justify-content: center; - color: var(--text-2); - font-size: 12px; -} - -.recent-requests { - background: var(--bg-2); - border: 1px solid var(--border); - border-radius: var(--radius-md); + position: relative; overflow: hidden; } -.request-item { - padding: 12px; - border-bottom: 1px solid var(--border); - display: flex; - justify-content: space-between; - align-items: center; + .stat-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + background: linear-gradient(90deg, var(--accent) 0%, #8b5cf6 100%); + } + +.stat-card-err::before { + background: linear-gradient(90deg, #f87171, #f43f5e); +} + +.stat-card-err .stat-val { + color: #f87171; +} + +.stat-val { + font-size: 24px; + font-weight: 800; + line-height: 1; + color: var(--text-1); + letter-spacing: -.03em; +} + +.stat-lbl { + font-size: 10.5px; + font-weight: 600; + color: var(--text-3); + margin-top: 5px; + text-transform: uppercase; + letter-spacing: .07em; +} + +.dash-section { + margin-bottom: 16px; + background: var(--bg-2); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + overflow: hidden; +} + +.dash-section-title { + padding: 13px 16px 0; font-size: 12px; - transition: background 0.2s ease; + font-weight: 700; + color: var(--text-2); + text-transform: uppercase; + letter-spacing: .07em; } -.request-item:hover { - background: var(--bg-3); +/* Request items */ +.request-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 9px 16px; + border-bottom: 1px solid var(--border); + font-size: 12px; + transition: background .1s; } -.request-item:last-child { - border-bottom: none; -} + .request-item:last-child { + border-bottom: none; + } + + .request-item:hover { + background: var(--bg-3); + } .request-method { - font-weight: 600; font-family: var(--font-mono); - padding: 2px 6px; - border-radius: 3px; - font-size: 11px; -} - -.request-status-success { - color: var(--m-get); -} - -.request-status-error { - color: var(--m-delete); + font-size: 9.5px; + font-weight: 800; + padding: 2px 7px; + border-radius: var(--radius-xs); + margin-right: 8px; + background: var(--bg-3); + color: var(--text-2); + letter-spacing: .04em; } .request-time { - color: var(--text-2); + font-size: 11px; + color: var(--text-3); font-family: var(--font-mono); } -#app-shell.with-dashboard { - grid-template-columns: var(--nav-w) 1fr var(--try-w) 400px; +.request-status-error { + color: #f87171 !important; } -@media (max-width: 1400px) { - .dashboard-panel { - width: 300px; - } - - #app-shell.with-dashboard { - grid-template-columns: var(--nav-w) 1fr 300px; - } +.request-status-success { + color: #34d399 !important; } -@media (max-width: 768px) { - .dashboard-panel { - width: 100%; - right: auto; - left: 0; - } +/* + UTILITIES + */ +code { + font-family: var(--font-mono); + font-size: .9em; + background: var(--bg-3); + border: 1px solid var(--border); + padding: 1px 6px; + border-radius: var(--radius-xs); } diff --git a/DoxaApi/UI/Assets/app.js b/DoxaApi/UI/Assets/app.js index 3f249d2..e99716f 100644 --- a/DoxaApi/UI/Assets/app.js +++ b/DoxaApi/UI/Assets/app.js @@ -1383,9 +1383,25 @@ function bindGlobalEvents() { const importBtn = document.getElementById("importBtn"); const importFile = document.getElementById("importFile"); + const exportToggle = document.querySelector(".export-toggle"); + const exportDropdown = document.querySelector(".export-dropdown"); + const exportMenu = document.getElementById("exportMenu"); const exportDoxaApiBtn = document.getElementById("exportDoxaApiBtn"); const exportOpenApiBtn = document.getElementById("exportOpenApiBtn"); const exportSwaggerBtn = document.getElementById("exportSwaggerBtn"); + + exportToggle?.addEventListener("click", (event) => { + event.stopPropagation(); + const isOpen = exportMenu?.classList.toggle("open"); + exportToggle.setAttribute("aria-expanded", String(!!isOpen)); + }); + + document.addEventListener("click", (event) => { + if (!exportDropdown?.contains(event.target)) { + exportMenu?.classList.remove("open"); + exportToggle?.setAttribute("aria-expanded", "false"); + } + }); const dashboardBtn = document.getElementById("dashboardBtn"); const closeDashboard = document.getElementById("closeDashboard"); const dashboardPanel = document.getElementById("dashboardPanel"); @@ -1398,7 +1414,7 @@ const text = await file.text(); - const res = await fetch("import", { + const res = await fetch(new URL("import", document.baseURI), { method: "POST", headers: { "Content-Type": "application/json" }, body: text @@ -1435,7 +1451,7 @@ function download(url, filename) { const a = document.createElement("a"); - a.href = url; + a.href = new URL(url, document.baseURI); a.download = filename; document.body.appendChild(a); a.click(); @@ -1475,7 +1491,7 @@ const normalized = parsed.groups ? parsed : await (async () => { - const res = await fetch("import", { + const res = await fetch(new URL("import", document.baseURI), { method: "POST", headers: { "Content-Type": "application/json" }, body: text diff --git a/DoxaApi/UI/Assets/index.html b/DoxaApi/UI/Assets/index.html index c3c2a08..f1d2842 100644 --- a/DoxaApi/UI/Assets/index.html +++ b/DoxaApi/UI/Assets/index.html @@ -3,112 +3,118 @@ - SampleApi + DoxaApi -
-
-
- - - - - - - SampleApi - -
-
-
-
- - - - - - - - +
+
+
+ +
+
+ DoxaApi + +
- - - -
+
+
+ + + / +
+
+
+ + + +
+
+ +
+ +
+ + + +
+
+ +
+
+
-
- -