@page "/docs/services/keyboard-shortcuts"
@namespace Lumeo.Docs.Pages.Docs.Services

@using Lumeo.Services
@implements IAsyncDisposable
@inject KeyboardShortcutService Shortcuts

<PageTitle>Keyboard Shortcuts — Lumeo</PageTitle>

<PageHeader Title="Keyboard Shortcut Service" Description="Register global keyboard shortcuts from C# code with automatic cleanup." />

<Stack Gap="10">

    <!-- Quick Start -->
    <Stack>
        <Stack Gap="4">
            <Heading Level="2">Quick Start</Heading>
            <Lumeo.Text Size="base" Color="muted" Leading="relaxed">
                The <code class="rounded bg-muted px-1.5 py-0.5 text-xs font-mono text-foreground">KeyboardShortcutService</code> lets you register
                keyboard shortcuts that work globally across the page. Inject it, register shortcuts in
                <code class="rounded bg-muted px-1.5 py-0.5 text-xs font-mono text-foreground">OnAfterRenderAsync</code>, and dispose them when the component unmounts.
            </Lumeo.Text>
            <Stack Class="code-block rounded-lg border border-border/40 bg-muted px-4 py-3 font-mono text-sm text-foreground whitespace-pre">@@implements IAsyncDisposable
@@inject KeyboardShortcutService Shortcuts

@@code {
    private IAsyncDisposable? _shortcut;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            _shortcut = await Shortcuts.RegisterAsync("ctrl+k", async () =>
            {
                // Open search, show command palette, etc.
                await InvokeAsync(StateHasChanged);
            });
        }
    }

    public async ValueTask DisposeAsync()
    {
        if (_shortcut is not null) await _shortcut.DisposeAsync();
    }
}</Stack>
            <Alert ShowIcon="false">
                Always register shortcuts inside <code class="rounded bg-muted px-1.5 py-0.5 text-xs font-mono text-foreground">OnAfterRenderAsync(firstRender)</code>
                because JS interop is not available during server-side prerendering. The returned
                <code class="rounded bg-muted px-1.5 py-0.5 text-xs font-mono text-foreground">IAsyncDisposable</code> handle automatically unregisters the shortcut when disposed.
            </Alert>
        </Stack>
    </Stack>

    <Separator Class="my-4" />

    <!-- Live Demo -->
    <Stack>
        <Stack Gap="4">
            <Heading Level="2">Live Demo</Heading>
            <Lumeo.Text Size="sm" Color="muted">
                A keyboard shortcut is registered on this page right now. Try pressing the key combination below to see it in action.
            </Lumeo.Text>

            <Card Class="p-6">
                <Stack Gap="5">
                    <Flex Align="center" Gap="3">
                        <Lumeo.Text Size="sm" Color="muted">Press</Lumeo.Text>
                        <Flex Align="center" Gap="1">
                            <Kbd>Ctrl</Kbd>
                            <Lumeo.Text Size="xs" Color="muted">+</Lumeo.Text>
                            <Kbd>M</Kbd>
                        </Flex>
                        <Lumeo.Text Size="sm" Color="muted">to trigger the shortcut</Lumeo.Text>
                    </Flex>

                    <Separator Class="my-4" />

                    <Stack Class="grid grid-cols-2 gap-4">
                        <Stack Gap="1">
                            <Lumeo.Text Size="xs" Color="muted" Class="uppercase tracking-wide">Hit count</Lumeo.Text>
                            <Flex Align="center" Gap="2">
                                <Lumeo.Text As="span" Class="text-2xl font-bold text-foreground tabular-nums">@_hitCount</Lumeo.Text>
                                @if (_hitCount > 0)
                                {
                                    <Badge Variant="Badge.BadgeVariant.Secondary">active</Badge>
                                }
                            </Flex>
                        </Stack>
                        <Stack Gap="1">
                            <Lumeo.Text Size="xs" Color="muted" Class="uppercase tracking-wide">Last triggered</Lumeo.Text>
                            <Lumeo.Text Size="sm" Class="tabular-nums">
                                @if (_lastTriggered is not null)
                                {
                                    @_lastTriggered.Value.ToString("HH:mm:ss.fff")
                                }
                                else
                                {
                                    <Lumeo.Text As="span" Class="text-muted-foreground">Never</Lumeo.Text>
                                }
                            </Lumeo.Text>
                        </Stack>
                    </Stack>

                    @if (_hitCount > 0)
                    {
                        <Alert Variant="Alert.AlertVariant.Success">
                            Shortcut triggered! The handler ran and updated the UI via <code class="rounded bg-muted px-1.5 py-0.5 text-xs font-mono text-foreground">StateHasChanged</code>.
                        </Alert>
                    }

                    <Button Variant="Button.ButtonVariant.Outline" Size="Button.ButtonSize.Sm" OnClick="ResetDemo">Reset counter</Button>
                </Stack>
            </Card>

            <Lumeo.Text Size="xs" Color="muted">
                The code behind this demo registers <code class="rounded bg-muted px-1.5 py-0.5 text-xs font-mono text-foreground">ctrl+m</code> in
                <code class="rounded bg-muted px-1.5 py-0.5 text-xs font-mono text-foreground">OnAfterRenderAsync</code> and disposes it when you navigate away.
            </Lumeo.Text>

            <Stack Class="code-block rounded-lg border border-border/40 bg-muted px-4 py-3 font-mono text-sm text-foreground whitespace-pre">@@implements IAsyncDisposable
@@inject KeyboardShortcutService Shortcuts

&lt;Lumeo.Text&gt;Hit count: @@_hitCount&lt;/Lumeo.Text&gt;
&lt;Lumeo.Text&gt;Last triggered: @@_lastTriggered&lt;/Lumeo.Text&gt;

@@code {
    private IAsyncDisposable? _shortcut;
    private int _hitCount;
    private DateTime? _lastTriggered;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            _shortcut = await Shortcuts.RegisterAsync("ctrl+m", async () =>
            {
                _hitCount++;
                _lastTriggered = DateTime.Now;
                await InvokeAsync(StateHasChanged);
            });
        }
    }

    public async ValueTask DisposeAsync()
    {
        if (_shortcut is not null) await _shortcut.DisposeAsync();
    }
}</Stack>
        </Stack>
    </Stack>

    <Separator Class="my-4" />

    <!-- Key Combo Format -->
    <Stack>
        <Stack Gap="4">
            <Heading Level="2">Key Combo Format</Heading>
            <Lumeo.Text Size="sm" Color="muted">
                Key combos use <code class="rounded bg-muted px-1.5 py-0.5 text-xs font-mono text-foreground">modifier+key</code> format, separated by
                <code class="rounded bg-muted px-1.5 py-0.5 text-xs font-mono text-foreground">+</code>. Key names use
                <code class="rounded bg-muted px-1.5 py-0.5 text-xs font-mono text-foreground">KeyboardEvent.key</code> values.
            </Lumeo.Text>
            <Stack Class="rounded-lg border border-border/40 overflow-x-auto">
                <table class="w-full min-w-[600px] text-sm">
                    <thead>
                        <tr class="border-b border-border/40 bg-muted/40">
                            <th class="text-left p-3 font-medium text-foreground">Combo</th>
                            <th class="text-left p-3 font-medium text-foreground">Keys</th>
                            <th class="text-left p-3 font-medium text-foreground">Description</th>
                        </tr>
                    </thead>
                    <tbody class="divide-y divide-border/40">
                        <tr>
                            <td class="p-3 font-mono text-xs text-primary">ctrl+k</td>
                            <td class="p-3"><Flex Gap="1" Align="center"><Kbd>Ctrl</Kbd><Kbd>K</Kbd></Flex></td>
                            <td class="p-3 text-muted-foreground">Common for search / command palette</td>
                        </tr>
                        <tr>
                            <td class="p-3 font-mono text-xs text-primary">ctrl+shift+p</td>
                            <td class="p-3"><Flex Gap="1" Align="center"><Kbd>Ctrl</Kbd><Kbd>Shift</Kbd><Kbd>P</Kbd></Flex></td>
                            <td class="p-3 text-muted-foreground">Multi-modifier combo</td>
                        </tr>
                        <tr>
                            <td class="p-3 font-mono text-xs text-primary">alt+n</td>
                            <td class="p-3"><Flex Gap="1" Align="center"><Kbd>Alt</Kbd><Kbd>N</Kbd></Flex></td>
                            <td class="p-3 text-muted-foreground">Alt modifier</td>
                        </tr>
                        <tr>
                            <td class="p-3 font-mono text-xs text-primary">meta+enter</td>
                            <td class="p-3"><Flex Gap="1" Align="center"><Kbd>Cmd</Kbd><Kbd>Enter</Kbd></Flex></td>
                            <td class="p-3 text-muted-foreground">Cmd (Mac) or Win (Windows)</td>
                        </tr>
                        <tr>
                            <td class="p-3 font-mono text-xs text-primary">escape</td>
                            <td class="p-3"><Kbd>Esc</Kbd></td>
                            <td class="p-3 text-muted-foreground">No modifier needed</td>
                        </tr>
                        <tr>
                            <td class="p-3 font-mono text-xs text-primary">f1</td>
                            <td class="p-3"><Kbd>F1</Kbd></td>
                            <td class="p-3 text-muted-foreground">Function keys</td>
                        </tr>
                    </tbody>
                </table>
            </Stack>
            <Lumeo.Text Size="sm" Color="muted">
                Available modifiers: <code class="rounded bg-muted px-1.5 py-0.5 text-xs font-mono text-foreground">ctrl</code>,
                <code class="rounded bg-muted px-1.5 py-0.5 text-xs font-mono text-foreground">shift</code>,
                <code class="rounded bg-muted px-1.5 py-0.5 text-xs font-mono text-foreground">alt</code>,
                <code class="rounded bg-muted px-1.5 py-0.5 text-xs font-mono text-foreground">meta</code>.
                Order does not matter -- <code class="rounded bg-muted px-1.5 py-0.5 text-xs font-mono text-foreground">shift+ctrl+k</code> and
                <code class="rounded bg-muted px-1.5 py-0.5 text-xs font-mono text-foreground">ctrl+shift+k</code> are identical. The service normalizes
                the combo internally.
            </Lumeo.Text>
        </Stack>
    </Stack>

    <Separator Class="my-4" />

    <!-- Examples -->
    <Stack>
        <Stack Gap="6">
            <Heading Level="2">Examples</Heading>

            <Stack Gap="4">
                <Heading Level="3">Async Handler</Heading>
                <Lumeo.Text Size="sm" Color="muted">
                    Use an async lambda when your handler needs to <code class="rounded bg-muted px-1.5 py-0.5 text-xs font-mono text-foreground">await</code> operations like API calls or navigation.
                </Lumeo.Text>
                <Stack Class="code-block rounded-lg border border-border/40 bg-muted px-4 py-3 font-mono text-sm text-foreground whitespace-pre">_shortcut = await Shortcuts.RegisterAsync("ctrl+s", async () =>
{
    await SaveAsync();
    await InvokeAsync(StateHasChanged);
});</Stack>
            </Stack>

            <Stack Gap="4">
                <Heading Level="3">Sync Handler</Heading>
                <Lumeo.Text Size="sm" Color="muted">
                    Use a synchronous <code class="rounded bg-muted px-1.5 py-0.5 text-xs font-mono text-foreground">Action</code> for simple state changes.
                </Lumeo.Text>
                <Stack Class="code-block rounded-lg border border-border/40 bg-muted px-4 py-3 font-mono text-sm text-foreground whitespace-pre">_shortcut = await Shortcuts.RegisterAsync("escape", () =>
{
    _isOpen = false;
    StateHasChanged();
});</Stack>
            </Stack>

            <Stack Gap="4">
                <Heading Level="3">Disable preventDefault</Heading>
                <Lumeo.Text Size="sm" Color="muted">
                    By default, <code class="rounded bg-muted px-1.5 py-0.5 text-xs font-mono text-foreground">preventDefault</code> is
                    <code class="rounded bg-muted px-1.5 py-0.5 text-xs font-mono text-foreground">true</code>, which stops the browser's native behavior for
                    that key combo. Pass <code class="rounded bg-muted px-1.5 py-0.5 text-xs font-mono text-foreground">false</code> to let the browser action
                    proceed alongside your handler.
                </Lumeo.Text>
                <Stack Class="code-block rounded-lg border border-border/40 bg-muted px-4 py-3 font-mono text-sm text-foreground whitespace-pre">// Browser's ctrl+p (print dialog) still opens, but your handler also fires
_shortcut = await Shortcuts.RegisterAsync("ctrl+p", HandlePrint, preventDefault: false);</Stack>
                <Alert Variant="Alert.AlertVariant.Warning" ShowIcon="false">
                    Be careful when overriding common browser shortcuts like
                    <Kbd>Ctrl</Kbd>+<Kbd>C</Kbd>, <Kbd>Ctrl</Kbd>+<Kbd>V</Kbd>, or <Kbd>Ctrl</Kbd>+<Kbd>T</Kbd>.
                    Always consider whether blocking the native behavior is appropriate for your users.
                </Alert>
            </Stack>

            <Stack Gap="4">
                <Heading Level="3">Multiple Shortcuts</Heading>
                <Lumeo.Text Size="sm" Color="muted">
                    Register several shortcuts and dispose them all at once using a list.
                </Lumeo.Text>
                <Stack Class="code-block rounded-lg border border-border/40 bg-muted px-4 py-3 font-mono text-sm text-foreground whitespace-pre">private List&lt;IAsyncDisposable&gt; _shortcuts = new();

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        _shortcuts.Add(await Shortcuts.RegisterAsync("ctrl+k", OpenSearch));
        _shortcuts.Add(await Shortcuts.RegisterAsync("ctrl+shift+p", OpenCommandPalette));
        _shortcuts.Add(await Shortcuts.RegisterAsync("escape", CloseOverlays));
    }
}

public async ValueTask DisposeAsync()
{
    foreach (var s in _shortcuts) await s.DisposeAsync();
}</Stack>
            </Stack>
        </Stack>
    </Stack>

    <Separator Class="my-4" />

    <!-- Cleanup Pattern -->
    <Stack>
        <Stack Gap="4">
            <Heading Level="2">Cleanup Pattern</Heading>
            <Lumeo.Text Size="sm" Color="muted">
                Shortcut registrations are cleaned up by disposing the <code class="rounded bg-muted px-1.5 py-0.5 text-xs font-mono text-foreground">IAsyncDisposable</code>
                handle returned from <code class="rounded bg-muted px-1.5 py-0.5 text-xs font-mono text-foreground">RegisterAsync</code>. This is the recommended pattern.
                Alternatively, you can call <code class="rounded bg-muted px-1.5 py-0.5 text-xs font-mono text-foreground">UnregisterAsync(id)</code> manually.
            </Lumeo.Text>

            <Heading Level="3">Single shortcut</Heading>
            <Stack Class="code-block rounded-lg border border-border/40 bg-muted px-4 py-3 font-mono text-sm text-foreground whitespace-pre">@@implements IAsyncDisposable

@@code {
    private IAsyncDisposable? _shortcut;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
            _shortcut = await Shortcuts.RegisterAsync("ctrl+k", HandleSearch);
    }

    public async ValueTask DisposeAsync()
    {
        if (_shortcut is not null) await _shortcut.DisposeAsync();
    }
}</Stack>

            <Heading Level="3">Multiple shortcuts with List</Heading>
            <Stack Class="code-block rounded-lg border border-border/40 bg-muted px-4 py-3 font-mono text-sm text-foreground whitespace-pre">@@implements IAsyncDisposable

@@code {
    private readonly List&lt;IAsyncDisposable&gt; _shortcuts = new();

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            _shortcuts.Add(await Shortcuts.RegisterAsync("ctrl+k", OpenSearch));
            _shortcuts.Add(await Shortcuts.RegisterAsync("ctrl+n", CreateNew));
            _shortcuts.Add(await Shortcuts.RegisterAsync("escape", CloseAll));
        }
    }

    public async ValueTask DisposeAsync()
    {
        foreach (var s in _shortcuts) await s.DisposeAsync();
    }
}</Stack>

            <Alert ShowIcon="false">
                The <code class="rounded bg-muted px-1.5 py-0.5 text-xs font-mono text-foreground">IAsyncDisposable</code> handle calls
                <code class="rounded bg-muted px-1.5 py-0.5 text-xs font-mono text-foreground">UnregisterAsync</code> internally and handles
                <code class="rounded bg-muted px-1.5 py-0.5 text-xs font-mono text-foreground">JSDisconnectedException</code> gracefully, so it is safe
                to dispose even when the circuit has disconnected.
            </Alert>
        </Stack>
    </Stack>

    <Separator Class="my-4" />

    <!-- Full API Reference -->
    <Stack>
        <Stack Gap="6">
            <Heading Level="2">API Reference</Heading>

            <Stack>
                <Heading Level="3" Class="mb-3">Methods</Heading>
                <Stack Class="rounded-lg border border-border/40 overflow-x-auto">
                    <table class="w-full min-w-[600px] text-sm">
                        <thead>
                            <tr class="border-b border-border/40 bg-muted/40">
                                <th class="text-left p-3 font-medium text-foreground">Method</th>
                                <th class="text-left p-3 font-medium text-foreground">Returns</th>
                                <th class="text-left p-3 font-medium text-foreground">Description</th>
                            </tr>
                        </thead>
                        <tbody class="divide-y divide-border/40">
                            <tr>
                                <td class="p-3 font-mono text-xs text-primary">RegisterAsync(keyCombo, Func&lt;Task&gt;, preventDefault?)</td>
                                <td class="p-3 font-mono text-xs text-muted-foreground">ValueTask&lt;IAsyncDisposable&gt;</td>
                                <td class="p-3 text-muted-foreground">Registers a shortcut with an async handler. Dispose the returned handle to unregister.</td>
                            </tr>
                            <tr>
                                <td class="p-3 font-mono text-xs text-primary">RegisterAsync(keyCombo, Action, preventDefault?)</td>
                                <td class="p-3 font-mono text-xs text-muted-foreground">ValueTask&lt;IAsyncDisposable&gt;</td>
                                <td class="p-3 text-muted-foreground">Registers a shortcut with a synchronous handler. Wraps the Action internally.</td>
                            </tr>
                            <tr>
                                <td class="p-3 font-mono text-xs text-primary">UnregisterAsync(id)</td>
                                <td class="p-3 font-mono text-xs text-muted-foreground">ValueTask</td>
                                <td class="p-3 text-muted-foreground">Manually unregister by ID. Prefer disposing the handle instead.</td>
                            </tr>
                        </tbody>
                    </table>
                </Stack>
            </Stack>

            <Stack>
                <Heading Level="3" Class="mb-3">Parameters</Heading>
                <Stack Class="rounded-lg border border-border/40 overflow-x-auto">
                    <table class="w-full min-w-[600px] text-sm">
                        <thead>
                            <tr class="border-b border-border/40 bg-muted/40">
                                <th class="text-left p-3 font-medium text-foreground">Parameter</th>
                                <th class="text-left p-3 font-medium text-foreground">Type</th>
                                <th class="text-left p-3 font-medium text-foreground">Default</th>
                                <th class="text-left p-3 font-medium text-foreground">Description</th>
                            </tr>
                        </thead>
                        <tbody class="divide-y divide-border/40">
                            <tr>
                                <td class="p-3 font-mono text-xs text-primary">keyCombo</td>
                                <td class="p-3 font-mono text-xs text-muted-foreground">string</td>
                                <td class="p-3 font-mono text-xs text-muted-foreground">--</td>
                                <td class="p-3 text-muted-foreground">Key combination string, e.g. "ctrl+k", "escape", "ctrl+shift+p". Modifier order does not matter.</td>
                            </tr>
                            <tr>
                                <td class="p-3 font-mono text-xs text-primary">handler</td>
                                <td class="p-3 font-mono text-xs text-muted-foreground">Func&lt;Task&gt; | Action</td>
                                <td class="p-3 font-mono text-xs text-muted-foreground">--</td>
                                <td class="p-3 text-muted-foreground">Callback invoked when the shortcut is triggered. Call <code class="rounded bg-muted px-1.5 py-0.5 text-xs font-mono text-foreground">InvokeAsync(StateHasChanged)</code> inside async handlers to update the UI.</td>
                            </tr>
                            <tr>
                                <td class="p-3 font-mono text-xs text-primary">preventDefault</td>
                                <td class="p-3 font-mono text-xs text-muted-foreground">bool</td>
                                <td class="p-3 font-mono text-xs text-muted-foreground">true</td>
                                <td class="p-3 text-muted-foreground">Whether to call <code class="rounded bg-muted px-1.5 py-0.5 text-xs font-mono text-foreground">e.preventDefault()</code> on the browser keyboard event. Set to <code class="rounded bg-muted px-1.5 py-0.5 text-xs font-mono text-foreground">false</code> to allow the native browser action.</td>
                            </tr>
                        </tbody>
                    </table>
                </Stack>
            </Stack>

        </Stack>
    </Stack>

</Stack>

@code {
    private IAsyncDisposable? _shortcut;
    private int _hitCount;
    private DateTime? _lastTriggered;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            _shortcut = await Shortcuts.RegisterAsync("ctrl+m", async () =>
            {
                _hitCount++;
                _lastTriggered = DateTime.Now;
                await InvokeAsync(StateHasChanged);
            });
        }
    }

    private void ResetDemo()
    {
        _hitCount = 0;
        _lastTriggered = null;
    }

    public async ValueTask DisposeAsync()
    {
        if (_shortcut is not null) await _shortcut.DisposeAsync();
    }
}
