{
  "name": "ConsentBanner",
  "category": "Utility",
  "subcategory": null,
  "description": "Consent Banner component.",
  "thumbnail": "/preview-cards/consent-banner.png",
  "hasDocsPage": true,
  "nugetPackage": "Lumeo",
  "files": [
    "UI/ConsentBanner/ConsentBanner.razor"
  ],
  "dependencies": [],
  "packageDependencies": [
    "Blazicons.Lucide"
  ],
  "cssVars": [
    "--color-accent",
    "--color-background",
    "--color-border",
    "--color-foreground",
    "--color-muted",
    "--color-muted-foreground",
    "--color-popover",
    "--color-popover-foreground",
    "--color-primary",
    "--color-primary-foreground",
    "--radius"
  ],
  "api": {
    "name": "ConsentBanner",
    "category": "Utility",
    "subcategory": null,
    "description": "Consent Banner component.",
    "nugetPackage": "Lumeo",
    "files": [
      "UI/ConsentBanner/ConsentBanner.razor"
    ],
    "namespace": "Lumeo",
    "inheritsFrom": null,
    "implements": [
      "IDisposable"
    ],
    "parameters": [
      {
        "name": "Class",
        "type": "string?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "AdditionalAttributes",
        "type": "Dictionary<string, object>?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": true
      },
      {
        "name": "Categories",
        "type": "IReadOnlyList<Lumeo.Services.ConsentCategory>",
        "default": "DefaultCategories",
        "description": "Consent buckets to expose in the preferences dialog. Omit `Required: true` ones if your site doesn't use strictly-necessary cookies.",
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "PrivacyPolicyUrl",
        "type": "string?",
        "default": null,
        "description": "URL to your privacy policy page. If null, the link is hidden.",
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Title",
        "type": "string",
        "default": "\"Cookies & consent\"",
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Description",
        "type": "string",
        "default": "\"We use a minimal set of cookies to run the site and optionally measure anonymous usage to improve it. You choose what's on.\"",
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "PrivacyPolicyLabel",
        "type": "string",
        "default": "\"Read the privacy policy\"",
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "AcceptLabel",
        "type": "string",
        "default": "\"Accept all\"",
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "RejectLabel",
        "type": "string",
        "default": "\"Reject optional\"",
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "CustomizeLabel",
        "type": "string",
        "default": "\"Customize\"",
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "PreferencesTitle",
        "type": "string",
        "default": "\"Consent preferences\"",
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "PreferencesDescription",
        "type": "string",
        "default": "\"Pick which categories you're okay with. You can change this later.\"",
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "RequiredBadge",
        "type": "string",
        "default": "\"Required\"",
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "CancelLabel",
        "type": "string",
        "default": "\"Cancel\"",
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "SaveLabel",
        "type": "string",
        "default": "\"Save preferences\"",
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "AnimationClass",
        "type": "string",
        "default": "\"animate-slide-in-from-bottom\"",
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      }
    ],
    "events": [],
    "enums": [],
    "records": [],
    "cssVars": [
      "--color-accent",
      "--color-background",
      "--color-border",
      "--color-foreground",
      "--color-muted",
      "--color-muted-foreground",
      "--color-popover",
      "--color-popover-foreground",
      "--color-primary",
      "--color-primary-foreground",
      "--radius"
    ],
    "examples": [],
    "subComponents": {},
    "parseFailed": false,
    "parseError": null
  },
  "$schema": "https://lumeo.nativ.sh/registry-component-schema.json",
  "key": "consent-banner",
  "docsUrl": "https://lumeo.nativ.sh/components/consent-banner",
  "sourceUrl": [
    {
      "file": "UI/ConsentBanner/ConsentBanner.razor",
      "url": "https://github.com/Brain2k-0005/Lumeo/tree/master/src/Lumeo/UI/ConsentBanner/ConsentBanner.razor"
    }
  ],
  "source": [
    {
      "path": "UI/ConsentBanner/ConsentBanner.razor",
      "content": "@namespace Lumeo\n@implements IDisposable\n@inject Lumeo.Services.ConsentService Consent\n\n@if (_ready && !Consent.HasDecided && !_preferencesOpen)\n{\n    <div role=\"dialog\" aria-labelledby=\"@_titleId\" aria-describedby=\"@_descId\"\n         class=\"@($\"lumeo-consent-banner fixed bottom-4 left-1/2 -translate-x-1/2 w-[calc(100%-2rem)] max-w-2xl z-50 {AnimationClass} {Class}\".Trim())\"\n         @attributes=\"AdditionalAttributes\">\n        <div class=\"rounded-lg border border-border/60 bg-popover text-popover-foreground shadow-xl p-5\">\n            <div class=\"flex items-start gap-4 flex-wrap md:flex-nowrap\">\n                <div class=\"flex-1 min-w-0\">\n                    <div class=\"flex items-center gap-2 mb-1\">\n                        <Blazicon Svg=\"Lucide.Cookie\" class=\"h-4 w-4 text-primary\" />\n                        <h3 id=\"@_titleId\" class=\"text-sm font-semibold\">@Title</h3>\n                    </div>\n                    <p id=\"@_descId\" class=\"text-sm text-muted-foreground leading-relaxed\">\n                        @Description\n                        @if (!string.IsNullOrEmpty(PrivacyPolicyUrl))\n                        {\n                            <text> </text>\n                            <a href=\"@PrivacyPolicyUrl\" class=\"text-primary hover:underline\">@PrivacyPolicyLabel</a>\n                            <text>.</text>\n                        }\n                    </p>\n                </div>\n                <div class=\"flex flex-wrap items-center gap-2 shrink-0\">\n                    @if (Categories.Any(c => !c.Required))\n                    {\n                        <button type=\"button\"\n                                class=\"inline-flex items-center justify-center rounded-md text-xs font-medium h-8 px-3 text-muted-foreground hover:text-foreground transition-colors\"\n                                @onclick=\"OpenPreferences\">\n                            @CustomizeLabel\n                        </button>\n                    }\n                    <button type=\"button\"\n                            class=\"inline-flex items-center justify-center rounded-md text-xs font-medium h-8 px-3 border border-border/60 bg-background hover:bg-accent transition-colors\"\n                            @onclick=\"RejectAll\">\n                        @RejectLabel\n                    </button>\n                    <button type=\"button\"\n                            class=\"inline-flex items-center justify-center rounded-md text-xs font-medium h-8 px-3 bg-primary text-primary-foreground hover:bg-primary/90 transition-colors\"\n                            @onclick=\"AcceptAll\">\n                        @AcceptLabel\n                    </button>\n                </div>\n            </div>\n        </div>\n    </div>\n}\n\n@if (_preferencesOpen)\n{\n    <div role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"@_prefTitleId\"\n         class=\"fixed inset-0 z-50 flex items-center justify-center p-4 bg-background/70 backdrop-blur-sm animate-fade-in\">\n        <div class=\"w-full max-w-lg rounded-lg border border-border/60 bg-popover text-popover-foreground shadow-2xl\">\n            <div class=\"p-6 border-b border-border/40\">\n                <h3 id=\"@_prefTitleId\" class=\"text-lg font-semibold\">@PreferencesTitle</h3>\n                <p class=\"text-sm text-muted-foreground mt-1 leading-relaxed\">@PreferencesDescription</p>\n            </div>\n\n            <div class=\"p-6 space-y-4 max-h-[50vh] overflow-y-auto\">\n                @foreach (var cat in Categories)\n                {\n                    var catKey = cat.Key;\n                    var locked = cat.Required;\n                    var checkedValue = IsDraftChecked(cat);\n                    var labelClass = locked ? \"opacity-70 cursor-not-allowed\" : \"\";\n                    <label class=\"@($\"flex items-start gap-3 cursor-pointer {labelClass}\")\">\n                        <div class=\"mt-0.5 shrink-0\">\n                            <input type=\"checkbox\"\n                                   class=\"peer inline-flex items-center justify-center h-4 w-4 shrink-0 cursor-pointer rounded border border-primary/60 transition-colors checked:bg-primary checked:border-primary disabled:cursor-not-allowed disabled:opacity-50\"\n                                   checked=\"@checkedValue\"\n                                   disabled=\"@locked\"\n                                   @onchange=\"@(e => ToggleCategory(catKey, e))\" />\n                        </div>\n                        <div class=\"flex-1 min-w-0\">\n                            <div class=\"flex items-center gap-2\">\n                                <span class=\"text-sm font-medium text-foreground\">@cat.Title</span>\n                                @if (locked)\n                                {\n                                    <span class=\"text-[10px] uppercase tracking-wider text-muted-foreground font-semibold\">@RequiredBadge</span>\n                                }\n                            </div>\n                            <p class=\"text-xs text-muted-foreground mt-0.5 leading-relaxed\">@cat.Description</p>\n                        </div>\n                    </label>\n                }\n            </div>\n\n            <div class=\"p-6 border-t border-border/40 flex items-center justify-end gap-2\">\n                <button type=\"button\"\n                        class=\"inline-flex items-center justify-center rounded-md text-xs font-medium h-8 px-3 text-muted-foreground hover:text-foreground\"\n                        @onclick=\"ClosePreferencesNoSave\">\n                    @CancelLabel\n                </button>\n                <button type=\"button\"\n                        class=\"inline-flex items-center justify-center rounded-md text-xs font-medium h-8 px-3 border border-border/60 bg-background hover:bg-accent\"\n                        @onclick=\"RejectAll\">\n                    @RejectLabel\n                </button>\n                <button type=\"button\"\n                        class=\"inline-flex items-center justify-center rounded-md text-xs font-medium h-8 px-3 bg-primary text-primary-foreground hover:bg-primary/90\"\n                        @onclick=\"SavePreferences\">\n                    @SaveLabel\n                </button>\n            </div>\n        </div>\n    </div>\n}\n\n@code {\n    [Parameter] public string? Class { get; set; }\n    [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object>? AdditionalAttributes { get; set; }\n\n    /// <summary>Consent buckets to expose in the preferences dialog. Omit `Required: true` ones if your site doesn't use strictly-necessary cookies.</summary>\n    [Parameter] public IReadOnlyList<Lumeo.Services.ConsentCategory> Categories { get; set; } = DefaultCategories;\n\n    /// <summary>URL to your privacy policy page. If null, the link is hidden.</summary>\n    [Parameter] public string? PrivacyPolicyUrl { get; set; }\n\n    // ---------- Copy — every string is overridable for i18n / tone. ----------\n    [Parameter] public string Title { get; set; } = \"Cookies & consent\";\n    [Parameter] public string Description { get; set; } = \"We use a minimal set of cookies to run the site and optionally measure anonymous usage to improve it. You choose what's on.\";\n    [Parameter] public string PrivacyPolicyLabel { get; set; } = \"Read the privacy policy\";\n    [Parameter] public string AcceptLabel { get; set; } = \"Accept all\";\n    [Parameter] public string RejectLabel { get; set; } = \"Reject optional\";\n    [Parameter] public string CustomizeLabel { get; set; } = \"Customize\";\n    [Parameter] public string PreferencesTitle { get; set; } = \"Consent preferences\";\n    [Parameter] public string PreferencesDescription { get; set; } = \"Pick which categories you're okay with. You can change this later.\";\n    [Parameter] public string RequiredBadge { get; set; } = \"Required\";\n    [Parameter] public string CancelLabel { get; set; } = \"Cancel\";\n    [Parameter] public string SaveLabel { get; set; } = \"Save preferences\";\n\n    [Parameter] public string AnimationClass { get; set; } = \"animate-slide-in-from-bottom\";\n\n    private static readonly IReadOnlyList<Lumeo.Services.ConsentCategory> DefaultCategories = new[]\n    {\n        new Lumeo.Services.ConsentCategory(\"necessary\", \"Strictly necessary\", \"Required for the site to function — theme preference, routing state, session.\", Required: true),\n        new Lumeo.Services.ConsentCategory(\"analytics\",  \"Analytics\",         \"Anonymous aggregate stats on which pages are visited, so we know what to improve.\"),\n    };\n\n    private readonly string _titleId = $\"lumeo-consent-title-{Guid.NewGuid():N}\";\n    private readonly string _descId  = $\"lumeo-consent-desc-{Guid.NewGuid():N}\";\n    private readonly string _prefTitleId = $\"lumeo-consent-pref-title-{Guid.NewGuid():N}\";\n\n    private bool _ready;\n    private bool _preferencesOpen;\n    private Dictionary<string, bool> _draft = new(StringComparer.OrdinalIgnoreCase);\n\n    protected override void OnInitialized()\n    {\n        Consent.OnChange += HandleConsentChange;\n        Consent.OnRequestOpenPreferences += HandleReopenRequest;\n    }\n\n    private void HandleReopenRequest() => InvokeAsync(() =>\n    {\n        OpenPreferences();\n        StateHasChanged();\n    });\n\n    protected override async Task OnAfterRenderAsync(bool firstRender)\n    {\n        if (!firstRender) return;\n        try\n        {\n            await Consent.EnsureLoadedAsync();\n            _ready = true;\n            StateHasChanged();\n        }\n        catch (JSDisconnectedException) { }\n        catch (ObjectDisposedException) { }\n    }\n\n    private void HandleConsentChange() => InvokeAsync(StateHasChanged);\n\n    private async Task AcceptAll()\n    {\n        await Consent.AcceptAllAsync(Categories.Where(c => !c.Required).Select(c => c.Key));\n        _preferencesOpen = false;\n    }\n\n    private async Task RejectAll()\n    {\n        await Consent.RejectAllAsync(Categories.Where(c => !c.Required).Select(c => c.Key));\n        _preferencesOpen = false;\n    }\n\n    private void OpenPreferences()\n    {\n        // Seed the draft from current consent so the toggles reflect reality.\n        _draft = Categories.ToDictionary(c => c.Key, c => c.Required || Consent.HasConsent(c.Key), StringComparer.OrdinalIgnoreCase);\n        _preferencesOpen = true;\n    }\n\n    /// <summary>Public entry point so a \"Manage cookie preferences\" link anywhere in the layout can reopen the dialog.</summary>\n    public void ShowPreferences()\n    {\n        OpenPreferences();\n        StateHasChanged();\n    }\n\n    private void ClosePreferencesNoSave() => _preferencesOpen = false;\n\n    private bool IsDraftChecked(Lumeo.Services.ConsentCategory cat)\n    {\n        if (cat.Required) return true;\n        return _draft.TryGetValue(cat.Key, out var v) && v;\n    }\n\n    private void ToggleCategory(string key, ChangeEventArgs e)\n    {\n        if (e.Value is bool b) _draft[key] = b;\n    }\n\n    private async Task SavePreferences()\n    {\n        await Consent.SetManyAsync(_draft);\n        _preferencesOpen = false;\n    }\n\n    public void Dispose()\n    {\n        Consent.OnChange -= HandleConsentChange;\n        Consent.OnRequestOpenPreferences -= HandleReopenRequest;\n    }\n}\n"
    }
  ],
  "slots": [],
  "serviceDependencies": [
    {
      "service": "ConsentService",
      "namespace": "Lumeo.Services"
    }
  ],
  "cascadingDependencies": [],
  "relatedComponents": [],
  "keyboardInteractions": [],
  "tests": [
    "tests/Lumeo.Tests/Components/ConsentBanner/ConsentBannerTests.cs",
    "tests/Lumeo.Tests/Contract/ComponentContractTests.cs"
  ],
  "mdSummary": "# ConsentBanner\n\nConsent Banner component.\n\n**Package:** `Lumeo` · **Category:** Utility\n**Docs:** https://lumeo.nativ.sh/components/consent-banner\n\n## Parameters\n| Owner | Name | Type | Default | Description |\n|---|---|---|---|---|\n| ConsentBanner | Class | `string?` | `` |  |\n| ConsentBanner | AdditionalAttributes | `Dictionary<string, object>?` | `` |  |\n| ConsentBanner | Categories | `IReadOnlyList<Lumeo.Services.ConsentCategory>` | `DefaultCategories` | Consent buckets to expose in the preferences dialog. Omit `Required: true` ones if your site doesn't use strictly-necessary cookies. |\n| ConsentBanner | PrivacyPolicyUrl | `string?` | `` | URL to your privacy policy page. If null, the link is hidden. |\n| ConsentBanner | Title | `string` | `\"Cookies & consent\"` |  |\n| ConsentBanner | Description | `string` | `\"We use a minimal set of cookies to run the site and optionally measure anonymous usage to improve it. You choose what's on.\"` |  |\n| ConsentBanner | PrivacyPolicyLabel | `string` | `\"Read the privacy policy\"` |  |\n| ConsentBanner | AcceptLabel | `string` | `\"Accept all\"` |  |\n| ConsentBanner | RejectLabel | `string` | `\"Reject optional\"` |  |\n| ConsentBanner | CustomizeLabel | `string` | `\"Customize\"` |  |\n| ConsentBanner | PreferencesTitle | `string` | `\"Consent preferences\"` |  |\n| ConsentBanner | PreferencesDescription | `string` | `\"Pick which categories you're okay with. You can change this later.\"` |  |\n| ConsentBanner | RequiredBadge | `string` | `\"Required\"` |  |\n| ConsentBanner | CancelLabel | `string` | `\"Cancel\"` |  |\n| ConsentBanner | SaveLabel | `string` | `\"Save preferences\"` |  |\n| ConsentBanner | AnimationClass | `string` | `\"animate-slide-in-from-bottom\"` |  |\n\n## Slots\n- (none)\n\n## Events\n- (none)\n\n## Sub-components\n- (none)\n\n## Cascading values it expects\n- (none)\n\n## Services it injects\n- `ConsentService` (Lumeo.Services)\n\n## Keyboard\n—\n\n## Example\n```razor\n<ConsentBanner />\n```\n\n## CSS variables\n`--color-accent`, `--color-background`, `--color-border`, `--color-foreground`, `--color-muted`, `--color-muted-foreground`, `--color-popover`, `--color-popover-foreground`, `--color-primary`, `--color-primary-foreground`, `--radius`\n\n## Files\n- `UI/ConsentBanner/ConsentBanner.razor`\n"
}