Lumeo

File Viewer

Universal preview surface — detects the file type from MIME / extension and renders inline: PDF, images, video, audio, Markdown, source code, JSON, CSV, and plain text. Unknown types fall back to a download CTA. One component replaces a stack of bespoke per-type renderers.

When to Use

  • Document or attachment lists where each row links to a heterogeneous file (PDF report, screenshot, CSV export, README)
  • File managers / DMS surfaces that need a quick preview pane next to the tree
  • Audit / compliance flows where the user has to inspect the actual content before approving an action
  • Ticketing systems where customers attach mixed-format evidence
  • Anywhere consumers would otherwise build a switch over Pdf / Image / Video / Markdown viewers by hand

How Detection Works

Resolution runs in this order — each step wins over the next:

  1. Explicit Kind parameter (anything other than Auto short-circuits)
  2. Explicit MimeType parameter (most reliable for blob URLs / signed URLs where the extension is hidden)
  3. Optional HEAD request Content-Type — off by default (AutoHead="true" to enable) because many CDNs reject HEAD with 405
  4. URL extension — the last-resort guess from the path segment
sample.svg
sample.svg
README.md

Lumeo FileViewer

A universal preview surface for Blazor — drop in a URL, get the right inline viewer.

Highlights

  • Auto-detect by MIME, optional HEAD, or URL extension
  • Built-in renderers for PDF, image, video, audio, markdown, source code, JSON, CSV, and plain text
  • Auth-aware fetches via HttpClient parameter or ConfigureRequest delegate
  • Safety capsMaxBytes for text fetches, MaxCsvRows for tabular data
  • Pluggable per-kind overrides via CustomRenderers

Quick start

<FileViewer Src="@DocumentUrl"
            FileName="@Document.Name"
            OnLoaded="HandleLoaded"
            Class="h-[480px]" />

Resolution order

  1. Explicit Kind parameter (anything other than Auto wins)
  2. Explicit MimeType parameter
  3. HEAD request Content-Type (when AutoHead="true")
  4. URL extension

Markdown is rendered with .DisableHtml() — raw <script> and <iframe> in user-supplied markdown never reach the DOM.

For more, see the docs site.

data.csv
componentcategorynuget_packagefirst_release
AccordionNavigationLumeo1.0.0
DataGridData DisplayLumeo.DataGrid2.0.0
FileViewerData DisplayLumeo.FileViewer3.2.5
MapData DisplayLumeo.Maps3.2.0
PdfViewerData DisplayLumeo.PdfViewer3.1.0
QueryBuilderFormsLumeo2.1.0
RichTextEditorFormsLumeo.Editor2.0.0
SchedulerData DisplayLumeo.Scheduler2.0.0
ToastFeedbackLumeo1.0.0
TreeViewData DisplayLumeo1.0.0
Program.cs
using Lumeo;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;

var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");

// Standard HttpClient registration — FileViewer picks this up automatically
// for fetching text-based files (Markdown, JSON, CSV, Code, Text).
builder.Services.AddScoped(sp => new HttpClient
{
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
});

builder.Services.AddLumeo();

await builder.Build().RunAsync();

/ 14
100%

Force a kind when the URL has no extension (typical for blob / signed URLs).

/ 14
100%

Provide MIME from your backend metadata response — most reliable for opaque URLs.

screenshot.svg
screenshot.svg
README.md

Lumeo FileViewer

A universal preview surface for Blazor — drop in a URL, get the right inline viewer.

Highlights

  • Auto-detect by MIME, optional HEAD, or URL extension
  • Built-in renderers for PDF, image, video, audio, markdown, source code, JSON, CSV, and plain text
  • Auth-aware fetches via HttpClient parameter or ConfigureRequest delegate
  • Safety capsMaxBytes for text fetches, MaxCsvRows for tabular data
  • Pluggable per-kind overrides via CustomRenderers

Quick start

<FileViewer Src="@DocumentUrl"
            FileName="@Document.Name"
            OnLoaded="HandleLoaded"
            Class="h-[480px]" />

Resolution order

  1. Explicit Kind parameter (anything other than Auto wins)
  2. Explicit MimeType parameter
  3. HEAD request Content-Type (when AutoHead="true")
  4. URL extension

Markdown is rendered with .DisableHtml() — raw <script> and <iframe> in user-supplied markdown never reach the DOM.

For more, see the docs site.

archive.zip

Preview unavailable

This file type cannot be previewed inline.

does-not-exist.csv
<!DOCTYPE html>
<html lang=en>
<head>
<meta charset=utf-8 />
<meta name=viewport content=width=device-width, initial-scale=1.0 />
<title>Lumeo &mdash; Blazor Component Library</title>
<base href=/ />
<link href=_framework/dotnet.yhbg0qfzvu.js rel=preload as=script fetchpriority=high crossorigin=anonymous integrity=sha256-ygm0zZGACbeRLESIM3TIQgHjeWKOMFcnkZRreY23G/U= />
<!-- Preload the Blazor runtime JS so it downloads in parallel with HTML parsing -->
<link rel=preload as=script href=_framework/blazor.webassembly.zxhwjtv6sc.js crossorigin>
<link rel=icon type=image/svg+xml href=favicon.svg />
<!-- SEO + social sharing -->
<meta name=description content=Lumeo — 154 accessible Blazor components inspired by shadcn/ui. AI primitives, motion, full DataGrid, 14 locales, Tailwind CSS v4. MIT, .NET 10. />
<meta name=theme-color content=#0a0a0a />
<!-- Open Graph (Discord LinkedIn Slack Facebook) -->
<meta property=og:site_name content=Lumeo />
<meta property=og:type content=website />
<meta property=og:title content=Lumeo — Blazor component library you'd actually want to ship />
<meta property=og:description content=154 accessible Blazor components across 7 packages — ~400 KB core + opt-in satellites (Charts, DataGrid, Editor, Scheduler, Gantt, PdfViewer, Maps). Tailwind v4 native, AI primitives, MIT, .NET 10. />
<meta property=og:url content=https://lumeo.nativ.sh />
<meta property=og:image content=https://lumeo.nativ.sh/social-preview.png />
<meta property=og:image:width content=1280 />
<meta property=og:image:height content=640 />
<!-- Twitter / X summary card -->
<meta name=twitter:card content=summary_large_image />
<meta name=twitter:title content=Lumeo — Blazor component library />
<meta name=twitter:description content=154 components across 7 packages. ~400 KB core + opt-in satellites. Tailwind v4, AI, 14 locales, MIT. />
<meta name=twitter:image content=https://lumeo.nativ.sh/social-preview.png />
<!-- Consent banner FOUC guard — runs synchronously BEFORE Blazor boots so the
banner never flashes for users who already gave (or rejected) consent.
Adds .lumeo-consent-decided to <html> if the consent localStorage entry exists;
a CSS rule below force-hides any element with class .lumeo-consent-banner. -->
<script>
(function () {
try {
if (localStorage.getItem('lumeo:consent:v1')) {
document.documentElement.classList.add('lumeo-consent-decided');
}
} catch (e) { /* private mode / cookies blocked — fall through banner shows once */ }
})();
</script>
<!-- Splash screen — plain CSS loads before Tailwind -->
<style>
html.lumeo-consent-decided .lumeo-consent-banner { display: none !important; }
.lumeo-splash {
position: fixed; inset: 0; z-index: 9999;
display: flex; align-items: center; justify-content: center;
background: #ffffff; color: hsl(240 5.9% 10%);
}
html.dark .lumeo-splash { background: hsl(240 10% 3.9%); color: hsl(0 0% 98%); }
.lumeo-splash-inner { display: flex; flex-direction: column; align-items: center; gap: 2.5rem; text-align: center; }
.lumeo-splash-logo-wrap { position: relative; width: 88px; height: 88px; display: flex; align-items: center; justify-content: center; }
.lumeo-splash-ripple { position: absolute; inset: 0; border-radius: 50%; border: 1px solid currentColor; opacity: 0; }
@keyframes splash-ripple { 0% { transform: scale(1); opacity: 0.5; } 100% { transform: scale(7); opacity: 0; } }
.lumeo-splash-ripple.r1 { animation: splash-ripple 5.5s cubic-bezier(0 0.45 0.2 1) 1.2s infinite; }
.lumeo-splash-ripple.r2 { animation: splash-ripple 5.5s cubic-bezier(0 0.45 0.2 1) 3s infinite; }
.lumeo-splash-ripple.r3 { animation: splash-ripple 5.5s cubic-bezier(0 0.45 0.2 1) 4.8s infinite; }
.lumeo-splash-svg { position: relative; z-index: 1; width: 88px; height: 88px; animation: splash-logo-in 1.4s cubic-bezier(0.16 1 0.3 1) both; }
@keyframes splash-logo-in { from { opacity: 0; transform: scale(0.65); } to { opacity: 1; transform: scale(1); } }
.lumeo-splash-ring { animation: splash-ring-breathe 4.5s ease-in-out 1.4s infinite; }
@keyframes splash-ring-breathe { 0% 100% { opacity: 0.28; } 50% { opacity: 0.65; } }
.lumeo-splash-dot { transform-origin: 16px 16px; animation: splash-dot-breathe 4.5s ease-in-out 1.4s infinite; }
@keyframes splash-dot-breathe { 0% 100% { transform: scale(1); opacity: 0.7; } 50% { transform: scale(1.22); opacity: 1; } }
.lumeo-splash-texts { display: flex; flex-direction: column; align-items: center; gap: 0.55rem; }
.lumeo-splash-name { font-family: system-ui -apple-system sans-serif; font-size: 2rem; font-weight: 600; letter-spacing: 0.02em; line-height: 1; animation: splash-name-in 1s cubic-bezier(0.16 1 0.3 1) 0.8s both; }
@keyframes splash-name-in { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: translateY(0); } }
.lumeo-splash-tagline { font-family: system-ui -apple-system sans-serif; font-size: 0.68rem; letter-spacing: 0.18em; text-transform: uppercase; animation: splash-tagline-in 1s cubic-bezier(0.16 1 0.3 1) 1.3s both; }
@keyframes splash-tagline-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 0.35; transform: translateY(0); } }
</style>
<!-- Pre-built Tailwind CSS (compiled from Styles/tailwind.css via @tailwindcss/cli).
`?v=d4faf93` is rewritten by the deploy workflow with the git SHA so the browser
re-fetches whenever the CSS source changes — no more stale cached tailwind.out.css. -->
<link rel=stylesheet href=css/tailwind.out.css?v=d4faf93 />
<!-- Lumeo theme CSS -->
<link rel=stylesheet href=_content/Lumeo/css/lumeo.css?v=d4faf93 />
<link rel=stylesheet href=_content/Lumeo/css/themes/_blue.css?v=d4faf93 />
<link rel=stylesheet href=_content/Lumeo/css/themes/_green.css?v=d4faf93 />
<link rel=stylesheet href=_content/Lumeo/css/themes/_rose.css?v=d4faf93 />
<link rel=stylesheet href=_content/Lumeo/css/themes/_orange.css?v=d4faf93 />
<link rel=stylesheet href=_content/Lumeo/css/themes/_violet.css?v=d4faf93 />
<link rel=stylesheet href=_content/Lumeo/css/themes/_amber.css?v=d4faf93 />
<link rel=stylesheet href=_content/Lumeo/css/themes/_teal.css?v=d4faf93 />
<!-- Self-hosted fonts (GDPR compliant) -->
<link rel=stylesheet href=css/fonts.css?v=d4faf93 />
<!-- App styles -->
<link rel=stylesheet href=css/app.css?v=d4faf93 />
<link href=Lumeo.Docs.styles.css rel=stylesheet />
<script type=importmap>{
imports: {
./_framework/blazor.webassembly.js: ./_framework/blazor.webassembly.zxhwjtv6sc.js
./_framework/dotnet.native.js: ./_framework/dotnet.native.15yu4u8ihv.js
./_framework/dotnet.runtime.js: ./_framework/dotnet.runtime.r2kbxkuujc.js
./_framework/dotnet.js: ./_framework/dotnet.yhbg0qfzvu.js
}
scopes: {}
integrity: {
./_framework/blazor.webassembly.js: sha256-1xlHuu1iNOJiMMz2BCq95uekwHf6pdpTcgVNKVBboPs=
./_framework/blazor.webassembly.zxhwjtv6sc.js: sha256-1xlHuu1iNOJiMMz2BCq95uekwHf6pdpTcgVNKVBboPs=
./_framework/dotnet.js: sha256-ygm0zZGACbeRLESIM3TIQgHjeWKOMFcnkZRreY23G/U=
./_framework/dotnet.native.15yu4u8ihv.js: sha256-qzvOAv5J3WbrnpQmSzAZbAih2NlXCpCAU3RsRjsUo+Y=
./_framework/dotnet.native.js: sha256-qzvOAv5J3WbrnpQmSzAZbAih2NlXCpCAU3RsRjsUo+Y=
./_framework/dotnet.runtime.js: sha256-YyudibIWETMKrLb+nAZdJ+xDY7HY6BWf6s32UAdcvCU=
./_framework/dotnet.runtime.r2kbxkuujc.js: sha256-YyudibIWETMKrLb+nAZdJ+xDY7HY6BWf6s32UAdcvCU=
./_framework/dotnet.yhbg0qfzvu.js: sha256-ygm0zZGACbeRLESIM3TIQgHjeWKOMFcnkZRreY23G/U=
}
}</script>
<!-- Algolia search — keys injected at build time by the deploy workflow. -->
<meta name=algolia-app-id content=HBUUWJXGUJ />
<meta name=algolia-search-key content=63e2253db63054def494cdb450f4e08d />
</head>
<body class=bg-background text-foreground>
<div id=app>
<div class=lumeo-splash aria-hidden=true role=presentation>
<div class=lumeo-splash-inner>
<div class=lumeo-splash-logo-wrap>
<div class=lumeo-splash-ripple r1></div>
<div class=lumeo-splash-ripple r2></div>
<div class=lumeo-splash-ripple r3></div>
<svg class=lumeo-splash-svg viewBox=0 0 32 32 fill=none xmlns=http://www.w3.org/2000/svg>
<circle cx=16 cy=16 r=9 fill=none stroke=currentColor stroke-width=2 class=lumeo-splash-ring />
<circle cx=16 cy=16 r=3 fill=currentColor class=lumeo-splash-dot />
</svg>
</div>
<div class=lumeo-splash-texts>
<span class=lumeo-splash-name>Lumeo</span>
<span class=lumeo-splash-tagline>Blazor Component Library</span>
</div>
</div>
</div>
</div>
<div id=blazor-error-ui data-nosnippet style=display:none; position:fixed; bottom:0; width:100%; background:#ffcccc; padding:0.6rem 1rem; z-index:1000;>
An unhandled error has occurred.
<a href=. class=reload>Reload</a>
<span class=dismiss style=cursor:pointer; margin-left:1rem;>X</span>
</div>
<!-- theme.js runs synchronously to apply dark/light class before first paint
(avoids flash of wrong theme). All other scripts are deferred. -->
<script src=_content/Lumeo/js/theme.js></script>
<script src=js/docs.js defer></script>
<script src=js/nav-scroll.js defer></script>
<script src=js/algolia-search.js defer></script>
<script src=js/constellation.js defer></script>
<script src=_framework/blazor.webassembly.zxhwjtv6sc.js></script>
</body>
</html>
sample.svg
sample.svg
sample.svg

Auth-Aware Fetching

Text-based kinds (Markdown, Code, JSON, CSV, Text) are fetched via HttpClient. For signed-but-not-presigned URLs you have three knobs:

  • HttpClient="..." — pass a pre-configured client (with handlers attached) directly
  • ConfigureRequest="..." — mutate the outgoing HttpRequestMessage per call (typical for Authorization headers)
  • Register HttpClient in DI — picked up automatically (the standard Blazor WASM pattern)

Safety Limits

  • MaxBytes (default 10 MB) — refused upfront via Content-Length when known, truncated mid-stream otherwise. No 100 MB OOMs.
  • MaxCsvRows (default 1000) — CSV parser stops after this many rows and adds a truncation notice.
  • Markdown is rendered with .DisableHtml() — raw <script> / <iframe> in user-supplied .md never reach the DOM.
  • SVG renders via <img> (not inline) — embedded scripts in SVG can't execute.
  • In-flight fetches are cancelled via CancellationToken when Src changes.

Accessibility

  • Body region wears role="document" with an aria-label derived from the file name or kind.
  • Loading + error states announce via the default Spinner / EmptyState components (both come pre-wired with aria-live).
  • Video / audio kinds use native browser controls with their built-in keyboard handling.
  • The download anchor is keyboard-reachable with a visible focus ring on the toolbar.