diff --git a/resources/css/filament-library.css b/resources/css/filament-library.css index 7118822..be49feb 100644 --- a/resources/css/filament-library.css +++ b/resources/css/filament-library.css @@ -30,11 +30,8 @@ width: 100% !important; } -/* Dark mode support for file preview */ -@media (prefers-color-scheme: dark) { - .filament-library-unpreviewable-message { - color: #9ca3af; /* text-gray-400 equivalent for dark mode */ - } +.dark .filament-library-unpreviewable-message { + color: #9ca3af; } /* ========================================================================== @@ -78,14 +75,12 @@ color: #6b7280; } -@media (prefers-color-scheme: dark) { - .filament-library-audio-preview { - background-color: #1f2937; - } +.dark .filament-library-audio-preview { + background-color: #1f2937; +} - .filament-library-audio-preview-label { - color: #9ca3af; - } +.dark .filament-library-audio-preview-label { + color: #9ca3af; } /* Markdown Prose */ @@ -104,30 +99,176 @@ margin-top: 1.5rem; margin-bottom: 0.75rem; font-weight: 600; + color: #111827; +} + +.filament-library-prose h1 { + font-size: 1.5rem; +} + +.filament-library-prose h1:first-child { + margin-top: 0; +} + +.filament-library-prose h2 { + font-size: 1.25rem; +} + +.filament-library-prose h3 { + font-size: 1.125rem; +} + +.filament-library-prose p { + margin-bottom: 1rem; + line-height: 1.625; + color: #374151; +} + +.filament-library-prose p:last-child { + margin-bottom: 0; } .filament-library-prose ul, .filament-library-prose ol { - margin: 0.75rem 0; + margin: 0 0 1rem; padding-left: 1.5rem; + color: #374151; +} + +.filament-library-prose ul { + list-style-type: disc; +} + +.filament-library-prose ol { + list-style-type: decimal; +} + +.filament-library-prose li { + line-height: 1.625; +} + +.filament-library-prose li + li { + margin-top: 0.25rem; +} + +.filament-library-prose a { + font-weight: 500; + color: var(--color-primary-600, #d97706); + text-decoration: underline; +} + +.filament-library-prose strong { + font-weight: 600; + color: #111827; +} + +.filament-library-prose blockquote { + margin: 0 0 1rem; + padding-left: 1rem; + border-left: 4px solid #e5e7eb; + color: #4b5563; + font-style: italic; +} + +.filament-library-prose code { + border-radius: 0.25rem; + background-color: #f3f4f6; + padding: 0.125rem 0.375rem; + font-size: 0.875rem; + color: #111827; } .filament-library-prose pre { overflow-x: auto; + margin-bottom: 1rem; padding: 1rem; - border-radius: 0.375rem; + border-radius: 0.5rem; background-color: #f3f4f6; + font-size: 0.875rem; + color: #111827; +} + +.filament-library-prose pre code { + background-color: transparent; + padding: 0; +} + +.filament-library-prose hr { + margin: 1.5rem 0; + border-color: #e5e7eb; +} + +.filament-library-prose table { + width: 100%; + margin-bottom: 1rem; + border-collapse: collapse; + font-size: 0.875rem; +} + +.filament-library-prose th { + border: 1px solid #e5e7eb; + background-color: #f9fafb; + padding: 0.5rem 0.75rem; + text-align: start; + font-weight: 600; +} + +.filament-library-prose td { + border: 1px solid #e5e7eb; + padding: 0.5rem 0.75rem; +} + +.dark .filament-library-prose { + background-color: #111827; + color: #f3f4f6; +} + +.dark .filament-library-prose h1, +.dark .filament-library-prose h2, +.dark .filament-library-prose h3 { + color: #ffffff; } -@media (prefers-color-scheme: dark) { - .filament-library-prose { - background-color: #111827; - color: #f3f4f6; - } +.dark .filament-library-prose p, +.dark .filament-library-prose ul, +.dark .filament-library-prose ol { + color: #d1d5db; +} + +.dark .filament-library-prose a { + color: var(--color-primary-400, #fbbf24); +} + +.dark .filament-library-prose strong { + color: #ffffff; +} + +.dark .filament-library-prose blockquote { + border-left-color: rgb(255 255 255 / 0.2); + color: #9ca3af; +} + +.dark .filament-library-prose code { + background-color: rgb(255 255 255 / 0.1); + color: #f3f4f6; +} - .filament-library-prose pre { - background-color: #1f2937; - } +.dark .filament-library-prose pre { + background-color: rgb(255 255 255 / 0.1); + color: #f3f4f6; +} + +.dark .filament-library-prose hr { + border-color: rgb(255 255 255 / 0.1); +} + +.dark .filament-library-prose th { + border-color: rgb(255 255 255 / 0.1); + background-color: rgb(255 255 255 / 0.05); +} + +.dark .filament-library-prose td { + border-color: rgb(255 255 255 / 0.1); } /* JSON Structured Previews */ @@ -262,45 +403,43 @@ color: #4b5563; } -@media (prefers-color-scheme: dark) { - .filament-library-json-preview { - background-color: #111827; - color: #f3f4f6; - } +.dark .filament-library-json-preview { + background-color: #111827; + color: #f3f4f6; +} - .filament-library-quiz-question { - border-bottom-color: #374151; - } +.dark .filament-library-quiz-question { + border-bottom-color: #374151; +} - .filament-library-quiz-option { - background-color: #1f2937; - border-color: #374151; - } +.dark .filament-library-quiz-option { + background-color: #1f2937; + border-color: #374151; +} - .filament-library-quiz-option-correct { - background-color: #14532d; - border-color: #166534; - } +.dark .filament-library-quiz-option-correct { + background-color: #14532d; + border-color: #166534; +} - .filament-library-flashcard { - border-color: #374151; - } +.dark .filament-library-flashcard { + border-color: #374151; +} - .filament-library-flashcard-front { - background-color: #1f2937; - border-bottom-color: #374151; - } +.dark .filament-library-flashcard-front { + background-color: #1f2937; + border-bottom-color: #374151; +} - .filament-library-mindmap-label { - background-color: #1e3a5f; - border-color: #1d4ed8; - } +.dark .filament-library-mindmap-label { + background-color: #1e3a5f; + border-color: #1d4ed8; +} - .filament-library-json-tree-key { - color: #d1d5db; - } +.dark .filament-library-json-tree-key { + color: #d1d5db; +} - .filament-library-json-tree-value { - color: #9ca3af; - } +.dark .filament-library-json-tree-value { + color: #9ca3af; } diff --git a/src/Resources/views/infolists/components/previews/json-flashcards.blade.php b/src/Resources/views/infolists/components/previews/json-flashcards.blade.php index 63ed07e..5bd06ef 100644 --- a/src/Resources/views/infolists/components/previews/json-flashcards.blade.php +++ b/src/Resources/views/infolists/components/previews/json-flashcards.blade.php @@ -5,12 +5,12 @@ $cards = is_array($data['cards'] ?? null) ? $data['cards'] : []; @endphp -
+
@if($title) -

{{ $title }}

+

{{ $title }}

@endif -
+
@forelse($cards as $index => $card) @if(! is_array($card)) @continue @@ -21,18 +21,28 @@ $back = $card['back'] ?? $card['definition'] ?? $card['answer'] ?? ''; @endphp -
-
- Front -
{{ $front }}
+
+
+ + {{ __('Front') }} + +
+ {{ $front }} +
-
- Back -
{{ $back }}
+
+ + {{ __('Back') }} + +
+ {{ $back }} +
@empty -

No flashcards found in this file.

+

+ {{ __('No flashcards found in this file.') }} +

@endforelse
diff --git a/src/Resources/views/infolists/components/previews/json-mindmap.blade.php b/src/Resources/views/infolists/components/previews/json-mindmap.blade.php index a8f5c12..534ad98 100644 --- a/src/Resources/views/infolists/components/previews/json-mindmap.blade.php +++ b/src/Resources/views/infolists/components/previews/json-mindmap.blade.php @@ -3,25 +3,188 @@ $data = $preview->parsedJson ?? []; $title = is_string($data['title'] ?? null) ? $data['title'] : null; $root = $data['root'] ?? $data; + + $isBranchFormat = false; + $branches = []; + $rootTopic = null; + + /** @var list $concepts */ + $concepts = []; + + if (isset($data['branches']) && is_array($data['branches'])) { + foreach ($data['branches'] as $branch) { + if (is_array($branch) && isset($branch['leaves']) && is_array($branch['leaves']) && $branch['leaves'] !== []) { + $isBranchFormat = true; + + break; + } + } + } + + if ($isBranchFormat) { + $branches = $data['branches']; + $rootTopic = is_string($data['root_topic'] ?? null) + ? $data['root_topic'] + : (is_string($data['title'] ?? null) ? $data['title'] : __('Mind map')); + + foreach ($branches as $branchIndex => $branch) { + if (! is_array($branch)) { + continue; + } + + $parentTopic = is_string($branch['title'] ?? null) + ? $branch['title'] + : (is_string($branch['label'] ?? null) ? $branch['label'] : __('Topic :number', ['number' => $branchIndex + 1])); + + foreach ($branch['leaves'] ?? [] as $leafIndex => $leaf) { + if (! is_array($leaf)) { + continue; + } + + $leafTitle = is_string($leaf['title'] ?? null) + ? $leaf['title'] + : (is_string($leaf['label'] ?? null) ? $leaf['label'] : __('Concept')); + + $elaboration = is_string($leaf['elaboration'] ?? null) + ? $leaf['elaboration'] + : (is_string($leaf['description'] ?? null) ? $leaf['description'] : ''); + + $concepts[] = [ + 'id' => "b{$branchIndex}-l{$leafIndex}", + 'title' => $leafTitle, + 'elaboration' => $elaboration, + 'parentTopic' => $parentTopic, + ]; + } + } + } @endphp -
- @if($title) -

{{ $title }}

- @endif +@if($isBranchFormat) +
+ @if($title && $title !== $rootTopic) +

{{ $title }}

+ @endif + +
+
+ {{ __('Root topic') }} +
+
+ {{ $rootTopic }} +
+
- @if(isset($data['nodes']) && is_array($data['nodes'])) -
    - @foreach($data['nodes'] as $node) - @if(! is_array($node)) +
    + @foreach($branches as $branchIndex => $branch) + @if(! is_array($branch)) @continue @endif -
  • - {{ $node['label'] ?? $node['title'] ?? $node['name'] ?? $node['text'] ?? 'Node' }} -
  • + + @php + $branchTitle = is_string($branch['title'] ?? null) + ? $branch['title'] + : (is_string($branch['label'] ?? null) ? $branch['label'] : __('Topic :number', ['number' => $branchIndex + 1])); + $leaves = is_array($branch['leaves'] ?? null) ? $branch['leaves'] : []; + @endphp + +
    +
    +
    + {{ __('Topic :number', ['number' => $branchIndex + 1]) }} +
    +
    + {{ $branchTitle }} +
    +
    + + @if($leaves !== []) +
    + @foreach($leaves as $leafIndex => $leaf) + @if(! is_array($leaf)) + @continue + @endif + + @php + $leafTitle = is_string($leaf['title'] ?? null) + ? $leaf['title'] + : (is_string($leaf['label'] ?? null) ? $leaf['label'] : __('Concept')); + $conceptId = "b{$branchIndex}-l{$leafIndex}"; + @endphp + + + @endforeach +
    + @endif +
    @endforeach -
- @else - @include('filament-library::infolists.components.previews.partials.mindmap-node', ['node' => $root, 'depth' => 0]) - @endif -
+
+ +
+
+ {{ __('Concept') }} +
+
+

+
+
+ {{ __('Parent topic') }} +
+ +
+
+
+@else +
+ @if($title) +

{{ $title }}

+ @endif + + @if(isset($data['nodes']) && is_array($data['nodes'])) +
    + @foreach($data['nodes'] as $node) + @if(! is_array($node)) + @continue + @endif + +
  • + {{ $node['label'] ?? $node['title'] ?? $node['name'] ?? $node['text'] ?? __('Node') }} +
  • + @endforeach +
+ @else + @include('filament-library::infolists.components.previews.partials.mindmap-node', ['node' => $root, 'depth' => 0]) + @endif +
+@endif diff --git a/src/Resources/views/infolists/components/previews/json-quiz.blade.php b/src/Resources/views/infolists/components/previews/json-quiz.blade.php index 59211ee..3b97b49 100644 --- a/src/Resources/views/infolists/components/previews/json-quiz.blade.php +++ b/src/Resources/views/infolists/components/previews/json-quiz.blade.php @@ -3,11 +3,41 @@ $data = $preview->parsedJson ?? []; $title = is_string($data['title'] ?? null) ? $data['title'] : null; $questions = is_array($data['questions'] ?? null) ? $data['questions'] : []; + + $resolveIsCorrect = function (array $question, mixed $option, int $optionIndex): bool { + if (array_key_exists('correct_index', $question)) { + $correctIndex = $question['correct_index']; + + if (is_numeric($correctIndex)) { + return (int) $correctIndex === $optionIndex; + } + + if (is_string($correctIndex)) { + $normalized = strtoupper(trim($correctIndex)); + + if (strlen($normalized) === 1 && $normalized >= 'A' && $normalized <= 'Z') { + return $normalized === chr(65 + $optionIndex); + } + } + + return false; + } + + $correct = $question['correct'] ?? $question['correct_answer'] ?? $question['answer'] ?? null; + + if ($correct === null) { + return false; + } + + return $correct === $option + || $correct === $optionIndex + || (is_array($option) && (($option['id'] ?? null) === $correct || ($option['value'] ?? null) === $correct)); + }; @endphp -
+
@if($title) -

{{ $title }}

+

{{ $title }}

@endif @forelse($questions as $index => $question) @@ -18,37 +48,93 @@ @php $stem = $question['stem'] ?? $question['question'] ?? $question['text'] ?? 'Question '.($index + 1); $options = $question['options'] ?? $question['choices'] ?? $question['answers'] ?? []; - $correct = $question['correct'] ?? $question['correct_answer'] ?? $question['answer'] ?? null; + $explanation = is_string($question['explanation'] ?? null) ? $question['explanation'] : null; + $hasCorrectOption = false; + + if (is_array($options)) { + foreach ($options as $optionIndex => $option) { + if ($resolveIsCorrect($question, $option, $optionIndex)) { + $hasCorrectOption = true; + + break; + } + } + } @endphp -
-
Question {{ $index + 1 }}
-
{{ $stem }}
+
! $loop->last, + 'pb-2' => $loop->last, + ])> +
+
+ {{ __('Question :number', ['number' => $index + 1]) }} +
+

+ {{ $stem }} +

+
@if(is_array($options) && $options !== []) -
    +
      @foreach($options as $optionIndex => $option) @php $label = is_array($option) ? ($option['text'] ?? $option['label'] ?? $option['value'] ?? json_encode($option)) : $option; - $isCorrect = $correct !== null && ( - $correct === $option - || $correct === $optionIndex - || (is_array($option) && (($option['id'] ?? null) === $correct || ($option['value'] ?? null) === $correct)) - ); + $isCorrect = $resolveIsCorrect($question, $option, $optionIndex); + $optionLetter = chr(65 + $optionIndex); @endphp -
    • $isCorrect])> - {{ $label }} - @if($isCorrect) - Correct - @endif +
    • $isCorrect, + 'border-gray-200 bg-gray-50/80 dark:border-white/10 dark:bg-white/5' => ! $isCorrect, + ])> + $isCorrect, + 'bg-gray-200 text-gray-700 dark:bg-white/10 dark:text-gray-200' => ! $isCorrect, + ])> + @if($isCorrect) + + @else + {{ $optionLetter }} + @endif + + + {{ $label }} +
    • @endforeach
    @endif + + @if($explanation) +
    is_array($options) && $options !== [], + 'border-emerald-300 bg-emerald-50 dark:border-emerald-500/50 dark:bg-emerald-950/20' => $hasCorrectOption, + 'border-gray-200 bg-gray-50/80 dark:border-white/20 dark:bg-white/5' => ! $hasCorrectOption, + ])> + @if($hasCorrectOption) +

    + + {{ __('Correct!') }} +

    + @endif +

    $hasCorrectOption, + ])> + {{ $explanation }} +

    +
    + @endif
@empty -

No questions found in this quiz file.

+

+ {{ __('No questions found in this quiz file.') }} +

@endforelse
diff --git a/src/Resources/views/infolists/components/previews/markdown.blade.php b/src/Resources/views/infolists/components/previews/markdown.blade.php index 73d23bb..f492d43 100644 --- a/src/Resources/views/infolists/components/previews/markdown.blade.php +++ b/src/Resources/views/infolists/components/previews/markdown.blade.php @@ -1,7 +1,7 @@ @php use Illuminate\Support\Str; - $html = Str::markdown($preview->textContent ?? ''); + $html = Str::markdown(mb_trim($preview->textContent ?? '')); @endphp
diff --git a/src/Resources/views/infolists/components/previews/partials/mindmap-node.blade.php b/src/Resources/views/infolists/components/previews/partials/mindmap-node.blade.php index 2adb0e6..8bdb739 100644 --- a/src/Resources/views/infolists/components/previews/partials/mindmap-node.blade.php +++ b/src/Resources/views/infolists/components/previews/partials/mindmap-node.blade.php @@ -8,9 +8,11 @@ @endphp @if($label !== null) -
    0) style="margin-left: {{ min($depth * 1.25, 6) }}rem;" @endif> -
  • - {{ $label }} +
      0) style="margin-left: {{ min($depth * 1.25, 6) }}rem;" @endif> +
    • + + {{ $label }} + @foreach($children as $child) @include('filament-library::infolists.components.previews.partials.mindmap-node', [ diff --git a/src/Support/LibraryFilePreviewResolver.php b/src/Support/LibraryFilePreviewResolver.php index 781b68a..27194cb 100644 --- a/src/Support/LibraryFilePreviewResolver.php +++ b/src/Support/LibraryFilePreviewResolver.php @@ -280,6 +280,10 @@ private static function looksLikeFlashcards(array $decoded): bool */ private static function looksLikeMindmap(array $decoded): bool { + if (self::looksLikeBranchMindmap($decoded)) { + return true; + } + if (isset($decoded['nodes'], $decoded['edges']) && is_array($decoded['nodes'])) { return true; } @@ -295,6 +299,28 @@ private static function looksLikeMindmap(array $decoded): bool return isset($decoded['children']) && is_array($decoded['children']); } + /** + * @param array $decoded + */ + private static function looksLikeBranchMindmap(array $decoded): bool + { + if (! isset($decoded['branches']) || ! is_array($decoded['branches'])) { + return false; + } + + foreach ($decoded['branches'] as $branch) { + if (! is_array($branch)) { + continue; + } + + if (isset($branch['leaves']) && is_array($branch['leaves']) && $branch['leaves'] !== []) { + return true; + } + } + + return false; + } + /** * @param array $decoded */ diff --git a/tests/Unit/LibraryFilePreviewResolverTest.php b/tests/Unit/LibraryFilePreviewResolverTest.php index 36de5bb..e48bdae 100644 --- a/tests/Unit/LibraryFilePreviewResolverTest.php +++ b/tests/Unit/LibraryFilePreviewResolverTest.php @@ -117,6 +117,140 @@ function previewMedia(array $attributes, ?string $diskPath = null, ?string $cont expect($preview->type)->toBe(LibraryFilePreviewType::JsonFlashcards); }); +it('renders flashcards preview with theme-aware layout', function (): void { + $content = json_encode([ + 'title' => 'Study Flashcards', + 'cards' => [ + ['front' => 'Term', 'back' => 'Definition'], + ], + ], JSON_THROW_ON_ERROR); + + $media = previewMedia([ + 'file_name' => 'flashcard-set.json', + 'name' => 'Flashcard Set', + 'mime_type' => 'application/json', + 'size' => strlen($content), + ], 'library/flashcard-set.json', $content); + + $preview = LibraryFilePreviewResolver::resolve($media); + + $html = view('filament-library::infolists.components.previews.json-flashcards', [ + 'media' => $media, + 'fileUrl' => 'https://example.test/flashcard-set.json', + 'preview' => $preview, + ])->render(); + + expect($html) + ->toContain('Study Flashcards') + ->toContain('Term') + ->toContain('Definition') + ->toContain('lg:grid-cols-3') + ->toContain('flex-1') + ->toContain('bg-white') + ->toContain('dark:bg-gray-900'); +}); + +it('detects mind map json from branches and leaves schema', function (): void { + $content = json_encode([ + 'root_topic' => 'Product Overview', + 'branches' => [ + [ + 'title' => 'Getting Started', + 'leaves' => [ + [ + 'title' => 'Installation', + 'elaboration' => 'Install dependencies with Composer before running the application.', + ], + ], + ], + ], + ], JSON_THROW_ON_ERROR); + + $media = previewMedia([ + 'file_name' => 'product-overview.json', + 'name' => 'Product Overview Mind Map', + 'mime_type' => 'application/json', + 'size' => strlen($content), + ], 'library/product-overview.json', $content); + + $preview = LibraryFilePreviewResolver::resolve($media); + + expect($preview->type)->toBe(LibraryFilePreviewType::JsonMindmap); +}); + +it('renders branch mind map preview with interactive concept panel', function (): void { + $content = json_encode([ + 'root_topic' => 'Product Overview', + 'branches' => [ + [ + 'title' => 'Getting Started', + 'leaves' => [ + [ + 'title' => 'Installation', + 'elaboration' => 'Install dependencies with Composer before running the application.', + ], + ], + ], + ], + ], JSON_THROW_ON_ERROR); + + $media = previewMedia([ + 'file_name' => 'product-overview.json', + 'name' => 'Product Overview Mind Map', + 'mime_type' => 'application/json', + 'size' => strlen($content), + ], 'library/product-overview.json', $content); + + $preview = LibraryFilePreviewResolver::resolve($media); + + $html = view('filament-library::infolists.components.previews.json-mindmap', [ + 'media' => $media, + 'fileUrl' => 'https://example.test/product-overview.json', + 'preview' => $preview, + ])->render(); + + expect($html) + ->toContain('Product Overview') + ->toContain('Getting Started') + ->toContain('Installation') + ->toContain('Install dependencies with Composer before running the application.') + ->toContain('selectedId') + ->toContain('bg-white') + ->toContain('dark:bg-gray-900'); +}); + +it('renders legacy mind map preview with nested children', function (): void { + $content = json_encode([ + 'title' => 'Sales Process Map', + 'label' => 'Sales Process', + 'children' => [ + ['label' => 'Discovery', 'children' => [['label' => 'Needs analysis']]], + ], + ], JSON_THROW_ON_ERROR); + + $media = previewMedia([ + 'file_name' => 'topic-mindmap.json', + 'name' => 'Topic Map', + 'mime_type' => 'application/json', + 'size' => strlen($content), + ], 'library/topic-mindmap.json', $content); + + $preview = LibraryFilePreviewResolver::resolve($media); + + $html = view('filament-library::infolists.components.previews.json-mindmap', [ + 'media' => $media, + 'fileUrl' => 'https://example.test/topic-mindmap.json', + 'preview' => $preview, + ])->render(); + + expect($html) + ->toContain('Sales Process Map') + ->toContain('Sales Process') + ->toContain('Discovery') + ->toContain('Needs analysis') + ->toContain('dark:bg-gray-900'); +}); + it('detects mind map json from nested children', function (): void { $content = json_encode([ 'title' => 'Topic Map', @@ -191,7 +325,84 @@ function previewMedia(array $attributes, ?string $diskPath = null, ?string $cont ])->render(); expect($html)->toContain('and($html)->toContain('Paragraph text.'); + ->and($html)->toContain('Paragraph text.') + ->and($html)->toContain('filament-library-prose'); +}); + +it('renders quiz preview with legacy correct answer keys', function (): void { + $content = json_encode([ + 'title' => 'Product Quiz', + 'questions' => [ + [ + 'stem' => 'What is Laravel?', + 'options' => ['A framework', 'A database'], + 'correct' => 'A framework', + 'explanation' => 'Laravel is a PHP web framework.', + ], + ], + ], JSON_THROW_ON_ERROR); + + $media = previewMedia([ + 'file_name' => 'topic-quiz.json', + 'name' => 'Topic Quiz', + 'mime_type' => 'application/json', + 'size' => strlen($content), + ], 'library/topic-quiz.json', $content); + + $preview = LibraryFilePreviewResolver::resolve($media); + + $html = view('filament-library::infolists.components.previews.json-quiz', [ + 'media' => $media, + 'fileUrl' => 'https://example.test/topic-quiz.json', + 'preview' => $preview, + ])->render(); + + expect($html) + ->toContain('A framework') + ->toContain('Laravel is a PHP web framework.') + ->toContain('Correct!') + ->toContain('bg-white') + ->toContain('dark:bg-gray-900'); +}); + +it('renders quiz preview with correct_index and explanation', function (): void { + $content = json_encode([ + 'title' => 'Platform Knowledge Quiz', + 'questions' => [ + [ + 'question' => 'Which language powers this backend?', + 'options' => [ + 'PHP', + 'Python', + 'Ruby', + 'Java', + ], + 'correct_index' => 0, + 'explanation' => 'PHP is the primary language used by Laravel applications.', + ], + ], + ], JSON_THROW_ON_ERROR); + + $media = previewMedia([ + 'file_name' => 'platform-quiz.json', + 'name' => 'Platform Quiz', + 'mime_type' => 'application/json', + 'size' => strlen($content), + ], 'library/platform-quiz.json', $content); + + $preview = LibraryFilePreviewResolver::resolve($media); + + $html = view('filament-library::infolists.components.previews.json-quiz', [ + 'media' => $media, + 'fileUrl' => 'https://example.test/platform-quiz.json', + 'preview' => $preview, + ])->render(); + + expect($html) + ->toContain('PHP') + ->toContain('PHP is the primary language used by Laravel applications.') + ->toContain('border-emerald-300') + ->toContain('Correct!'); }); it('renders download preview button with a valid signed url href', function (): void {