{
  "manifest": {
    "protocolVersion": 2,
    "pluginId": "org.betterportal.theme.bootstrap1",
    "title": "Bootstrap1 Theme",
    "description": "Bootstrap 5 + htmx theme that renders BetterPortal app shells.",
    "version": "11.0.3",
    "category": "theme",
    "deploymentModes": [
      "self-hosted"
    ],
    "capabilities": [
      "theme",
      "renderer.bootstrap5",
      "view.json",
      "view.metadata"
    ],
    "supportedRenderers": [
      "bootstrap5"
    ],
    "supportedRenderModes": [],
    "views": [],
    "configSchemas": [
      {
        "id": "theme.bootstrap1.app",
        "title": "Theme - Branding & Palette",
        "description": "Per-app branding, palette, and mode for the bootstrap1 theme.",
        "scope": "app",
        "jsonSchema": {
          "brandName": "string",
          "documentTitle": "string",
          "lightLogoUrl": "string",
          "darkLogoUrl": "string",
          "faviconUrl": "string",
          "mode": "string",
          "primary": "string",
          "secondary": "string",
          "success": "string",
          "info": "string",
          "warning": "string",
          "danger": "string"
        },
        "fields": [
          {
            "key": "brandName",
            "title": "Brand Name",
            "description": "Name shown in the top bar.",
            "scope": "app",
            "visibility": "protected",
            "ownership": "mixed",
            "sourceOfTruth": "bp",
            "required": false
          },
          {
            "key": "documentTitle",
            "title": "Browser Title",
            "description": "Title shown in the browser tab. Falls back to the app title when empty.",
            "scope": "app",
            "visibility": "protected",
            "ownership": "mixed",
            "sourceOfTruth": "bp",
            "required": false
          },
          {
            "key": "lightLogoUrl",
            "title": "Light Logo URL",
            "description": "Logo used in light mode.",
            "scope": "app",
            "visibility": "protected",
            "ownership": "mixed",
            "sourceOfTruth": "bp",
            "ui": {
              "control": "url"
            },
            "required": false
          },
          {
            "key": "darkLogoUrl",
            "title": "Dark Logo URL",
            "description": "Logo used in dark mode. Falls back to light logo when empty.",
            "scope": "app",
            "visibility": "protected",
            "ownership": "mixed",
            "sourceOfTruth": "bp",
            "ui": {
              "control": "url"
            },
            "required": false
          },
          {
            "key": "faviconUrl",
            "title": "Favicon URL",
            "description": "Browser tab icon. Falls back to the BetterPortal favicon when empty.",
            "scope": "app",
            "visibility": "protected",
            "ownership": "mixed",
            "sourceOfTruth": "bp",
            "ui": {
              "control": "url"
            },
            "required": false
          },
          {
            "key": "mode",
            "title": "Default Mode",
            "description": "Theme mode.",
            "scope": "app",
            "visibility": "protected",
            "ownership": "mixed",
            "sourceOfTruth": "bp",
            "defaultValue": "system",
            "ui": {
              "control": "select",
              "options": [
                {
                  "value": "light",
                  "label": "Light"
                },
                {
                  "value": "dark",
                  "label": "Dark"
                },
                {
                  "value": "system",
                  "label": "System"
                }
              ]
            },
            "required": false
          },
          {
            "key": "primary",
            "title": "Primary Color",
            "description": "Bootstrap primary palette color (hex).",
            "scope": "app",
            "visibility": "protected",
            "ownership": "mixed",
            "sourceOfTruth": "bp",
            "defaultValue": "#3b82f6",
            "ui": {
              "control": "color"
            },
            "required": false
          },
          {
            "key": "secondary",
            "title": "Secondary Color",
            "description": "Bootstrap secondary palette color (hex).",
            "scope": "app",
            "visibility": "protected",
            "ownership": "mixed",
            "sourceOfTruth": "bp",
            "defaultValue": "#64748b",
            "ui": {
              "control": "color"
            },
            "required": false
          },
          {
            "key": "success",
            "title": "Success Color",
            "description": "Bootstrap success palette color (hex).",
            "scope": "app",
            "visibility": "protected",
            "ownership": "mixed",
            "sourceOfTruth": "bp",
            "defaultValue": "#22c55e",
            "ui": {
              "control": "color"
            },
            "required": false
          },
          {
            "key": "info",
            "title": "Info Color",
            "description": "Bootstrap info palette color (hex).",
            "scope": "app",
            "visibility": "protected",
            "ownership": "mixed",
            "sourceOfTruth": "bp",
            "defaultValue": "#38bdf8",
            "ui": {
              "control": "color"
            },
            "required": false
          },
          {
            "key": "warning",
            "title": "Warning Color",
            "description": "Bootstrap warning palette color (hex).",
            "scope": "app",
            "visibility": "protected",
            "ownership": "mixed",
            "sourceOfTruth": "bp",
            "defaultValue": "#f59e0b",
            "ui": {
              "control": "color"
            },
            "required": false
          },
          {
            "key": "danger",
            "title": "Danger Color",
            "description": "Bootstrap danger palette color (hex).",
            "scope": "app",
            "visibility": "protected",
            "ownership": "mixed",
            "sourceOfTruth": "bp",
            "defaultValue": "#ef4444",
            "ui": {
              "control": "color"
            },
            "required": false
          }
        ]
      }
    ],
    "permissions": [],
    "adminApis": [
      {
        "id": "config.schema",
        "title": "Config Schema",
        "description": "BetterPortal-managed config schemas for this service.",
        "path": "/.well-known/bp/config/schema",
        "methods": [
          "GET"
        ],
        "supportsCustomUi": false
      },
      {
        "id": "config.values",
        "title": "Config Values",
        "description": "Read and write BetterPortal-managed config values.",
        "path": "/.well-known/bp/config",
        "methods": [
          "GET",
          "POST"
        ],
        "supportsCustomUi": false
      }
    ],
    "webhooks": [],
    "apiContracts": [],
    "m2mRequests": [],
    "developerResources": [
      {
        "id": "bootstrap1.ui-guide",
        "kind": "guide",
        "title": "Bootstrap1 UI guide",
        "description": "Layout, component, HTMX, responsive and accessibility rules.",
        "mediaType": "text/markdown; charset=utf-8",
        "content": "> Routing contract: prefer `BPRoute` for links, actions, forms and embedded components/fragments. Use `ctx.route()` only when a destination value is specifically required. Service-view defaults use `viewPath`; the theme owns UI mounting/history. See [the detailed BPRoute contract](https://github.com/BetterCorp/BetterPortal/blob/master/docs/building/bp-route.md).\n\n# Bootstrap1 UI guide\n\nUse Bootstrap 5 classes and server-rendered JSX/HTMX. BetterPortal service renderers return fragments; the theme owns the shell, navigation, visible URL and global assets.\n\n## Layout\n\n- Render primary content for the `main` slot. Never emit another page shell, `<html>`, `<head>`, sidebar navigation or top bar.\n- Check the configured shell before adding a page heading: omit a title/header already shown by the theme. Start with content and a short action row when actions exist; use semantic headings for distinct content sections.\n- Check the active layout for theme-provided Back/Up controls. When present, omit service-level Back/Up buttons and links; retain task-specific actions and contextual record links.\n- The shell renders dynamic titles and parent breadcrumbs supplied by the handler through `ctx.chrome({ title, parents })`. Parent paths belong to the service, such as `/batches`; the shell resolves their public URLs and labels. Back follows browser history; Up appears only for a navigable final parent. Do not construct another title, breadcrumb or Back/Up row in service markup.\n- Use `.container-fluid`, Bootstrap grid classes and responsive gaps. Avoid fixed widths except a declared `data-bp-sidebar-width`.\n- Use cards for grouped settings, tables for comparable records and list groups for status collections. The theme owns app navigation; do not repeat it as navigation cards, link grids or another menu inside a view. Local workflow navigation must serve a distinct need.\n- Keep primary actions on the right of desktop action rows and allow them to wrap on small screens.\n\n## Tables inside cards\n\n- Align the first column's header and row text with the card title, and the last column with the card's right content inset. Align the text, not just the table's outer border.\n- A table inside `.card-body.p-0` or directly inside a card uses cell padding that can be smaller than the title's padding, leaving row text too far left. For this flush layout, give the first and last cells matching outer padding: use `--bs-card-cap-padding-x` when the title is in `.card-header`, or `--bs-card-spacer-x` when it is in `.card-body`. Keep inner column spacing intact.\n- A normally padded `.card-body` already adds an inset; adding full card padding to its table cells doubles that spacing. Account for both wrapper and cell padding instead of blindly adding `.p-3`, `.px-3` or negative margins.\n- Keep `.table-responsive` around the table and use `.table.mb-0` for a flush card table. Apply the same alignment to `thead`, `tbody` and `tfoot`, including empty/loading rows with `colspan`.\n- Check normal and `.table-sm` density, narrow screens and horizontal scrolling. Card titles, column labels and row content should share a deliberate alignment without shrinking text or removing useful cell padding.\n\n## Forms and mutations\n\n- Use real `<form>` elements and labelled controls. Validation text uses `.invalid-feedback` or `.form-text`.\n- Plain internal `<a href=\"/route\">` links and GET/POST forms are automatically upgraded to HTMX and routed through the owning service. A bare `<form>` posts to the current service view. Use `bp-no-override` or `data-bp-no-override` on an element or ancestor only when native browser behavior is intentional.\n- Use the same `ctx.route(input?)` in API handlers and renderers. Omit the target for self. Select `apiPath`/`absoluteApiPath` for API requests, `viewPath`/`absoluteViewPath` for HTML requests, and `uiPath` only for explicit shell navigation/history and `absoluteUiPath` for public links; never prefer UI fields in service-renderer controls. Pass a declared dependency alias in `serviceId`; renderers resolve links from domain data and cursors. API handlers must not add internal URL fields to their response for views. Resolve POST/PUT/DELETE destinations with the corresponding method in the renderer, and use `sse: true` with `absoluteViewPath` for HTML streams.\n- Use `<BPRoute ctx={ctx} service=\"dependency-alias\" path=\"/contract/path\" fragment=\"location.id\">` for cross-service UI fragments. `service` is the dependency alias/key declared in `betterportal.json`; never put service titles, UUIDs, hostnames, or absolute URLs in renderer code. The server consumes `ctx` and resolves the active app-mounted provider.\n- Use `service=\"shell\"` and a singular shell fragment id to reuse active-shell UI such as `theme-selector`. Add `bp-loading`, status-specific `bp-status`, and `bp-error` states as needed. Omit `bp-ok` to insert successful content directly; add it only to wrap success content, without a placeholder template.\n- Renderers receive `ViewRenderContext` as their second argument. Resolve auth view IDs with `ctx.route({ viewId, serviceId })?.viewPath`; keep authorization and business data in the handler.\n- Mutations should return a useful fragment and emit an `HX-Trigger` event for passive regions that need to reload.\n- Disable buttons only while their own request is active. Keep a visible loading indicator and an error fragment.\n\n## Components\n\n- Buttons: `.btn`, with one obvious `.btn-primary` action per group.\n- Status: Bootstrap badges with text; never communicate state by colour alone.\n- Destructive actions: `.btn-outline-danger` or `.btn-danger`, with confirmation proportional to impact.\n- Side panels: wrap content in `data-bp-sidebar`, add `data-bp-sidebar-title`, and open it with `data-bp-sidebar-open`. The theme converts it to an offcanvas.\n- Modals and offcanvas content may be teleported by the shell. Do not depend on a specific DOM parent after activation.\n\n## Accessibility\n\n- Preserve heading order, visible labels, keyboard focus and native controls.\n- Every icon-only action needs an accessible name.\n- Tables need headers; use a responsive wrapper instead of shrinking text.\n- Focus the first meaningful control when opening a task panel and return focus to its trigger when it closes.\n\n## Avoid\n\n- No SPA framework, client router, iframe, client state store or hardcoded service hostname.\n- No inline copy of Bootstrap or shell JavaScript.\n- No theme-origin mutation URL. Service requests go to service URLs.\n- No custom colour that bypasses the app palette when a Bootstrap semantic token exists.\n\n## Colors and appearance\n\nThe theme owns typography, palette and light/dark/system preference. Its declared defaults are overridden by tenant values and then app values through BetterPortal service configuration in Config Manager. The shell maps the effective configuration and mode into CSS variables and component styles; service views inherit them. Do not copy configuration colors into view data or load another Bootstrap/font asset.\n\nUse semantic roles: `.btn-primary` for the main action, `.alert-warning` for a warning, `.btn-danger` for a destructive action and `.text-body-secondary` / `.form-text` for supporting text. Primary is not a fixed hue; the secondary brand color is not the muted-text role. Keep a textual status label.\n\nPrefer complete components such as `.form-control`, `.form-select` and `.card` so their foreground, surface, border, hover, focus and disabled states stay coordinated. Custom component-scoped CSS may consume these theme tokens:\n\n| Role | Token |\n| --- | --- |\n| Page / primary / alternate surface | `--bp-bg` / `--bp-surface` / `--bp-surface-alt` |\n| Main / supporting text | `--bp-text` / `--bp-text-soft` |\n| Border | `--bp-border` |\n| Primary / secondary accent | `--bp-accent` / `--bp-accent-secondary` |\n| Status accents | `--bp-accent-success`, `--bp-accent-info`, `--bp-accent-warning`, `--bp-accent-danger` |\n\nThese variables describe this theme; do not assume every `bootstrap5` renderer or Embedded exports the same contract. Bootstrap semantic variables and shell component rules both affect the final style. Do not redefine root tokens or assume white text is readable on every configured brand color.\n\nThe shell sets `data-bs-theme` and owns appearance preference. Reuse its declared `theme-selector` fragment; do not add a service theme switch, storage key or media-query controller. Verify light/dark modes, options, placeholders, disabled/read-only fields and keyboard focus. Report unreadable defaults with effective configuration and computed styles instead of hardcoded palettes or `!important` fixes.\n\nResearch the [BetterPortal theme contract](https://github.com/BetterCorp/BetterPortal/blob/master/docs/building/themes.md) and [routes/views](https://github.com/BetterCorp/BetterPortal/blob/master/docs/building/routes-and-views.md) alongside the active `/llms-ui.txt`. Master docs may be newer than installed packages; match the deployed version before using an API.\n\n## Form validation before requests\n\nThe backend input schema is the source of truth. Render its applicable HTML5 constraints (`type`, `required`, `min`, `max`, `step`, `minlength`, `maxlength`, `pattern`) and preserve them after swaps. The browser validates the returned HTML controls; it does not execute AnyVali/JSON schema automatically. Numeric inputs use range/step constraints, not `pattern`. Hidden inputs and controls barred from constraint validation are not checked by these APIs; validate computed payloads against the published input contract and always on the server.\n\nPrefer native submit buttons or `form.requestSubmit(submitter)`. For a necessary custom trigger, stop if `form.reportValidity()` is false; `form.checkValidity()` checks without showing validation messages. Never bypass invalid controls with `form.submit()`, synthetic submit events, custom fetch/HTMX posts, `noValidate`/`formNoValidate`, `novalidate`/`formnovalidate` or disabled HTMX validation. Recalculation that accepts fewer fields needs a separate non-nested form/component with its own constraints, not a bypass on the full form. The backend validates every submission, including rules HTML cannot express.\n\nCheck invalid submissions by button, Enter and custom triggers: no POST is sent; a valid form submits once. See [HTML form validation and view guidance](https://github.com/BetterCorp/BetterPortal/blob/master/docs/building/themes.md#preserve-html5-form-validation).\n\n## View boundaries and defect reporting\n\nRead the shared `/llms-ui.txt` rules before implementing a view.\n\n- Render typed handler data. Keep business defaults, validation, calculations, persistence and report generation in handlers/domain helpers; HTML constraints are presentation hints.\n- Initialization, history navigation, prefetched insertion and draft restoration must not submit forms or replay mutations. Keep recalculation and final submission explicit; do not retain one-shot action flags after failure or cancellation.\n- The shell owns HTMX 4 and its bundled `bp-shell, sse` extensions. Do not inject extension assets, register extensions or add unsupported `hx-ext` values. Use supported form encoding and report schema/serialization gaps upstream.\n- Prefer native controls, HTMX triggers, `hx-indicator` and HTMX 4 `hx-disable`. Necessary local scripts use typed `js(() => ...)`, component-scoped selectors, idempotent binding and cleanup on removal. Do not add global submit functions or repeated document-wide listeners. HTMX 2 camelCase lifecycle events are incompatible with the installed HTMX 4 runtime.\n- Browser drafts require an explicit schema, allowed fields, tenant/app/service/user scope, version, expiry and clearing rules. Validate restoration, exclude action/auth fields, and never generate hidden inputs from arbitrary stored keys. Restore without submitting; browser owner metadata is not authorization.\n- Use project issue/reporting tools for reproducible theme/framework defects. Include versions, minimal reproduction, expected/actual behavior and relevant request initiators or computed styles without credentials or customer data. Check existing reports; if access is unavailable, provide a ready-to-file report. Do not replace theme defaults with hardcoded palettes, `!important` patches, duplicate navigation or runtime workarounds.\n- Service errors use handler-side `ctx.diagnostic({ code, reason, attributes? })` before responses outside HTTP 200-399; the view displays a safe error state. Diagnostics do not replace upstream defect reports.\n- Verify failed POST then navigation, Back/Forward, preload, repeated swaps and stale drafts. Inspect request initiators and confirm navigation/restoration never mutates or leaves controls disabled.\n\nBPRoute specifics: outer `as` supplies a default; each state can override `as` and HTML/HTMX attributes. Use bp-ok/bp-loading/bp-noauth/bp-noperm/bp-error; failures replace the active root and do not inherit generated actions. Valueless hx-get/post/put/patch/delete selects the effective method and resolved service destination; explicit values override it. Component or fragment selects GET embedding, never a link; bp-ok attributes optionally wrap fetched HTML, its children/URL actions are ignored, and no authored template placeholder is needed. Use @betterportal/framework as jsxImportSource. See docs/building/bp-route.md and routing-lint.md for complete examples and invalid usages.\n"
      },
      {
        "id": "bootstrap1.skill",
        "kind": "skill",
        "title": "Bootstrap1 UI skill",
        "description": "Agent workflow for producing theme-compatible service UI.",
        "mediaType": "text/markdown; charset=utf-8",
        "content": "> Routing contract: prefer `BPRoute` for links, actions, forms and embedded components/fragments. Use `ctx.route()` only when a destination value is specifically required. Service-view defaults use `viewPath`; the theme owns UI mounting/history. See [the detailed BPRoute contract](https://github.com/BetterCorp/BetterPortal/blob/master/docs/building/bp-route.md).\n\n---\nname: betterportal-bootstrap1-ui\ndescription: Build BetterPortal service UI that fits the active Bootstrap1 shell.\n---\n\n# BetterPortal Bootstrap1 UI\n\n1. Read the active app's `/llms-api.txt`, `/llms-ui.txt` and this theme's UI guide.\n2. Identify the service view, methods, schemas, required permissions and stable `viewId`.\n3. Render server-side JSX/HTMX for the `bootstrap1` theme. Return only service content, never the shell. Check the active layout and omit page titles and Back/Up buttons or links supplied by the theme; keep meaningful section headings and task-specific actions.\n4. Use the same `ctx.route(input?)` in API handlers and renderers. Omit the target for self. Select `apiPath`/`absoluteApiPath` for API requests, `viewPath`/`absoluteViewPath` for HTML requests, and `uiPath` only for explicit shell navigation/history and `absoluteUiPath` for public links; never prefer UI fields in service-renderer controls. Pass a declared dependency alias in `serviceId`; renderers resolve links from domain data and cursors. API handlers must not add internal URL fields to their response for views. Resolve POST/PUT/DELETE destinations with the corresponding method in the renderer, and use `sse: true` with `absoluteViewPath` for HTML streams.\n5. Use the framework `BPRoute` JSX helper with `ctx` for cross-service or active-shell fragments. Reference the dependency alias/key declared in `betterportal.json`, never a title, service UUID, hostname, or absolute URL. Omit `bp-ok` for direct success insertion; an explicit wrapping `bp-ok` uses its attributes as a wrapper and ignores authored children.\n6. Prefer Bootstrap components and native HTML controls. Use scoped CSS or typed local JavaScript for service-specific presentation; report broken or missing platform behavior through project issue/reporting tools instead of patching it in the view.\n7. Include loading, empty, validation, forbidden and service-unavailable states.\n8. Check keyboard operation, visible labels, heading order and responsive wrapping.\n9. Verify JSON and HTML representations against the published schemas.\n\nFor side tasks, use `data-bp-sidebar` and `data-bp-sidebar-open`. For authenticated downloads, use `hx-download`. For live refresh, emit an `HX-Trigger` event and subscribe with `hx-trigger=\"event from:body\"`.\n\nRead the UI guide's colors/appearance section and linked BetterPortal documentation; use the active theme's semantic roles and installed API version. Before delivery, follow its view boundaries and defect-reporting rules. Keep domain work in handlers, never submit during initialization or draft restoration, and never register HTMX extensions from service markup. Preserve schema-backed HTML5 constraints: use native submission or `requestSubmit()`, gate custom triggers with `reportValidity()`, and never disable validation to post an invalid form. Use project reporting tools for reproducible theme/framework defects. Verify failed POST followed by navigation, preload, history and repeated swaps without mutation replay or duplicate listeners.\n\nBPRoute specifics: outer `as` supplies a default; each state can override `as` and HTML/HTMX attributes. Use bp-ok/bp-loading/bp-noauth/bp-noperm/bp-error; failures replace the active root and do not inherit generated actions. Valueless hx-get/post/put/patch/delete selects the effective method and resolved service destination; explicit values override it. Component or fragment selects GET embedding, never a link; bp-ok attributes optionally wrap fetched HTML, its children/URL actions are ignored, and no authored template placeholder is needed. Use @betterportal/framework as jsxImportSource. See docs/building/bp-route.md and routing-lint.md for complete examples and invalid usages.\n"
      },
      {
        "id": "bootstrap1.page-template",
        "kind": "template",
        "title": "Bootstrap1 page template",
        "description": "Minimal server-rendered JSX page structure.",
        "mediaType": "text/plain; charset=utf-8",
        "language": "typescript",
        "content": "import { BPRoute } from \"@betterportal/framework\";\n/* @jsxImportSource @betterportal/framework */\nimport type { RegisteredViewRenderer } from \"@betterportal/framework\";\n\nexport const render: RegisteredViewRenderer[\"render\"] = (data, ctx) => (\n  <section class=\"container-fluid py-3\">\n    <div class=\"d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3\">\n      <div>\n        <p class=\"text-body-secondary mb-0\">Explain the task in one sentence.</p>\n      </div>\n      <BPRoute ctx={ctx} {...{ viewId: \"example.create\" }} class=\"btn btn-primary\"><bp-ok>Create</bp-ok></BPRoute>\n    </div>\n    <div class=\"card shadow-sm\">\n      <div class=\"card-body\">{JSON.stringify(data)}</div>\n    </div>\n  </section>\n);\n"
      }
    ],
    "shell": {
      "service": "bootstrap1",
      "renderer": "bootstrap5",
      "fragments": [
        {
          "id": "footer",
          "kind": "block",
          "title": "Footer fragments",
          "description": "Ordered fragments shown in the page footer.",
          "defaultItems": []
        },
        {
          "id": "nav",
          "kind": "block",
          "title": "Topbar fragments",
          "description": "Ordered fragments shown on the right side of the topbar.",
          "defaultItems": [
            "theme-selector"
          ]
        },
        {
          "id": "theme-selector",
          "kind": "fragment",
          "title": "Theme selector",
          "description": "Lets the user choose light, dark, or system color mode.",
          "defaultItems": []
        }
      ]
    },
    "cacheHints": {
      "metadataTtlSeconds": 1800
    }
  },
  "routes": []
}
