feat(api): add Stoplight docs renderer

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-01 13:26:55 +00:00
parent 5d5ca8aa51
commit c9cf407530
9 changed files with 112 additions and 1 deletions

View file

@ -34,6 +34,7 @@ class DocumentationController
return match ($defaultUi) {
'swagger' => $this->swagger($request),
'redoc' => $this->redoc($request),
'stoplight' => $this->stoplight($request),
default => $this->scalar($request),
};
}
@ -74,6 +75,19 @@ class DocumentationController
]);
}
/**
* Show Stoplight Elements.
*/
public function stoplight(Request $request): View
{
$config = config('api-docs.ui.stoplight', []);
return view('api-docs::stoplight', [
'specUrl' => route('api.docs.openapi.json'),
'config' => $config,
]);
}
/**
* Get OpenAPI specification as JSON.
*/

View file

@ -20,6 +20,7 @@ Route::get('/', [DocumentationController::class, 'index'])->name('api.docs');
Route::get('/swagger', [DocumentationController::class, 'swagger'])->name('api.docs.swagger');
Route::get('/scalar', [DocumentationController::class, 'scalar'])->name('api.docs.scalar');
Route::get('/redoc', [DocumentationController::class, 'redoc'])->name('api.docs.redoc');
Route::get('/stoplight', [DocumentationController::class, 'stoplight'])->name('api.docs.stoplight');
// OpenAPI specification routes
Route::get('/openapi.json', [DocumentationController::class, 'openApiJson'])

View file

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="API Documentation - Stoplight Elements">
<title>{{ config('api-docs.info.title', 'API Documentation') }} - Stoplight</title>
<style>
html, body {
margin: 0;
min-height: 100%;
background: #0f172a;
}
elements-api {
height: 100vh;
width: 100%;
display: block;
}
</style>
<link rel="stylesheet" href="https://unpkg.com/@stoplight/elements/styles.min.css">
</head>
<body>
<elements-api
apiDescriptionUrl="{{ $specUrl }}"
router="hash"
layout="{{ $config['layout'] ?? 'sidebar' }}"
theme="{{ $config['theme'] ?? 'dark' }}"
hideTryIt="{{ ($config['hide_try_it'] ?? false) ? 'true' : 'false' }}"
></elements-api>
<script src="https://unpkg.com/@stoplight/elements/web-components.min.js"></script>
</body>
</html>

View file

@ -268,6 +268,13 @@ return [
'hide_download_button' => false,
'hide_models' => false,
],
// Stoplight Elements specific options
'stoplight' => [
'theme' => 'dark', // 'dark' or 'light'
'layout' => 'sidebar', // 'sidebar' or 'stacked'
'hide_try_it' => false,
],
],
/*

View file

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
it('renders Stoplight Elements when selected as the default documentation ui', function () {
config(['api-docs.ui.default' => 'stoplight']);
$response = $this->get('/api/docs');
$response->assertOk();
$response->assertSee('elements-api', false);
$response->assertSee('@stoplight/elements', false);
});
it('renders the dedicated Stoplight documentation route', function () {
$response = $this->get('/api/docs/stoplight');
$response->assertOk();
$response->assertSee('elements-api', false);
$response->assertSee('@stoplight/elements', false);
});

View file

@ -65,6 +65,11 @@ class DocsController
return view('api::redoc');
}
public function stoplight(): View
{
return view('api::stoplight');
}
public function openapi(OpenApiGenerator $generator): JsonResponse
{
return response()->json($generator->generate());

View file

@ -28,6 +28,9 @@ Route::get('/scalar', [DocsController::class, 'scalar'])->name('api.scalar');
// ReDoc (three-panel API reference)
Route::get('/redoc', [DocsController::class, 'redoc'])->name('api.redoc');
// Stoplight Elements API reference
Route::get('/stoplight', [DocsController::class, 'stoplight'])->name('api.stoplight');
// OpenAPI spec (rate limited - expensive to generate)
Route::get('/openapi.json', [DocsController::class, 'openapi'])
->middleware('throttle:60,1')

View file

@ -81,7 +81,7 @@
<div class="relative" x-data="{ open: false }" @click.outside="open = false">
<button
@click="open = !open"
class="text-sm flex items-center gap-1 {{ request()->routeIs('api.swagger', 'api.scalar', 'api.redoc') ? 'font-medium text-cyan-600 dark:text-cyan-400' : 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-200' }}"
class="text-sm flex items-center gap-1 {{ request()->routeIs('api.swagger', 'api.scalar', 'api.redoc', 'api.stoplight') ? 'font-medium text-cyan-600 dark:text-cyan-400' : 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-200' }}"
>
API Explorer
<svg class="w-4 h-4 transition-transform" :class="{ 'rotate-180': open }" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
@ -109,6 +109,9 @@
<a href="{{ route('api.redoc') }}" class="block px-4 py-2 text-sm text-zinc-700 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-700 {{ request()->routeIs('api.redoc') ? 'bg-zinc-100 dark:bg-zinc-700' : '' }}">
<i class="fa-solid fa-book w-4 mr-2 text-zinc-400"></i>ReDoc
</a>
<a href="{{ route('api.stoplight') }}" class="block px-4 py-2 text-sm text-zinc-700 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-700 {{ request()->routeIs('api.stoplight') ? 'bg-zinc-100 dark:bg-zinc-700' : '' }}">
<i class="fa-solid fa-layer-group w-4 mr-2 text-zinc-400"></i>Stoplight
</a>
</div>
</div>
</div>

View file

@ -0,0 +1,23 @@
@extends('layouts::docs')
@section('title', 'Stoplight')
@section('description', 'Stoplight Elements API reference for the Core API.')
@section('content')
<div class="min-h-[calc(100vh-4rem)]">
<elements-api
apiDescriptionUrl="{{ route('api.openapi.json') }}"
router="hash"
layout="sidebar"
theme="dark"
></elements-api>
</div>
@endsection
@push('head')
<link rel="stylesheet" href="https://unpkg.com/@stoplight/elements/styles.min.css">
@endpush
@push('scripts')
<script src="https://unpkg.com/@stoplight/elements/web-components.min.js"></script>
@endpush