Editor header: slot for host navigation and a compact preview status when hosted #139

Closed
opened 2026-10-04 00:35:56 +02:00 by dominik.polakovics · 0 comments

What to build

Give the editor's header a slot for host navigation. When deckle runs behind a host that wraps it, such as deckle-hosted, the host can supply where the user is and where they can go: a brand link home, breadcrumb segments with switcher menus, the sections next to the editor, and an account menu. The editor renders them in its own header, so the host does not need a second bar above the editor and the user sees one bar across both products.

Standalone deckle does not change. With no host, the header looks and behaves exactly as it does today.

The same change makes the Hugo status compact when a host is present, because a hosted editor cannot act on the Hugo version or on where the binary comes from.

Why a slot in deckle

The host cannot draw this itself. The editor runs on its own origin and hardcodes absolute paths, so the host can only reverse-proxy it. Framing the editor would stack two headers and break deep links. The editor's header is the only place where one bar is possible.

Design reference

The target is drawn in the dashboard redesign mockup of deckle-hosted: design/hosted-shell-mockup.html in Cloonar/deckle-hosted, state "Editor open". A published copy is at https://claude.ai/artifact/E6dcK8zKFALE3navT8QmhC. The bar in that mockup already uses the editor's own chrome colors, sizes and header height. Cloonar/deckle-hosted#51 specifies the dashboard side of the same bar.

The contract

deckle owns this contract. The host implements the producer.

Where the data comes from

The editor requests GET /api/host on its own origin, with a relative URL, once when it loads.

  • A host that proxies the editor answers this path itself and never forwards it to the harness.
  • The harness never implements this path. It keeps answering 404 there, as it does for every unknown /api/ path. That 404 is how standalone deckle says "no host".
  • Anything other than a 2xx response with a valid body of a supported version means no host navigation: a 404, a network error, a non-JSON body, an unknown version. The editor then shows today's header and reports no error.
  • After a publish started from the editor reaches a final state, the editor requests the data again, so a status mark next to the site name does not go stale. A failed refresh keeps the last good data.

Shape, version 1

Proposed shape. Refine it while implementing if needed, and document the final form, since the host builds against the documentation.

{
  "version": 1,
  "brand": { "label": "deckle", "href": "https://host.example/dashboard" },
  "segments": [
    {
      "label": "Studio Nordlicht",
      "href": "https://host.example/orgs/nordlicht",
      "mark": { "kind": "initial", "text": "S", "color": "#4f46e5" },
      "menu": {
        "groups": [
          {
            "label": "Organizations",
            "items": [
              { "label": "Studio Nordlicht", "detail": "/nordlicht", "meta": "3 sites",
                "href": "https://host.example/orgs/nordlicht", "current": true,
                "mark": { "kind": "initial", "text": "S", "color": "#4f46e5" } }
            ]
          }
        ],
        "footer": [ { "label": "All organizations", "href": "https://host.example/dashboard" } ]
      }
    },
    {
      "label": "Bergland Tourismus",
      "href": "https://host.example/orgs/nordlicht/sites/bergland",
      "mark": { "kind": "status", "status": "ok" },
      "menu": { "groups": [ { "label": "Sites in Studio Nordlicht", "items": [] } ], "footer": [] }
    }
  ],
  "sections": [
    { "label": "Overview", "href": "https://host.example/orgs/nordlicht/sites/bergland" },
    { "label": "Editor", "current": true },
    { "label": "Builds", "href": "https://host.example/orgs/nordlicht/sites/bergland/builds" }
  ],
  "account": {
    "label": "mira@nordlicht.example",
    "detail": "Admin of Studio Nordlicht",
    "items": [ { "label": "Dashboard", "href": "https://host.example/dashboard" } ]
  }
}
  • Every part except version is optional. A host may send only a brand, or segments without menus.
  • deckle stays generic. It knows segments, menus, sections and marks. It does not know organizations, sites or roles.
  • A segment without href is a plain label. A segment without menu has no arrows. A section without href is not a link.
  • mark.kind is initial, a small colored square with one character, or status, a small state mark with status one of ok, busy, error, locked, none. deckle picks the colors for the states from its own tokens and shows locked as a lock icon, so state is never carried by color alone.
  • An item's meta is short trailing text such as a count or a state, and detail is a second line under the label.

Header with host navigation

Left to right, as in the mockup: the brand as a link in place of the "Deckle" wordmark; the segments separated by a slash; the sections with the current one marked; then, unchanged, the publish chip and Publish all; the compact preview status; the account menu.

  • Names navigate, arrows switch. A segment's name follows its href. A separate arrows control next to it opens its menu.
  • A menu shows its groups with their labels, marks the current item, and lists the footer entries below a divider. A menu with more than five items gets a filter field that narrows the list while typing.
  • Menus close on Escape, on outside click and after a choice. They are fully keyboard operable.
  • Long names truncate with the full name on hover. The header never overflows sideways. When the sections no longer fit, they move to a second row that scrolls sideways.

Every host entry is a plain link, followed in the same tab. The editor never sends a POST to the host.

When a form has unsaved changes, following a host link goes through the editor's existing unsaved-changes dialog with Save, Discard and Stay. The browser's generic leave prompt is not the answer here; it stays for closing the tab.

Compact preview status

With host navigation present, the Hugo status pill becomes a dot with a short label:

Hugo state Shown
running green dot, "Preview"
starting amber dot, "Preview starting"
crashed red dot, "Preview crashed"

Everything the pill shows today, the state, restart count, version and binary source, moves into the tooltip and the accessible name. The "disconnected" hint stays. Without host navigation the pill is unchanged.

Safety

  • Host text is rendered as text, never as markup.
  • Only absolute https: and http: URLs are accepted as href. An entry with any other scheme is dropped.
  • color is accepted only as a six-digit hex value. Anything else falls back to a neutral color.
  • The request is same-origin and relative. The editor never requests host data from another origin.

Acceptance criteria

  • With no host data, the header is pixel-for-pixel today's header and the harness answers 404 on the reserved path; a test pins that 404
  • With valid host data, the header shows brand link, segments, sections and account menu in the order above, with Publish all and the publish chip unchanged
  • A segment with a menu shows arrows that open it; a segment without one shows none; a segment without href is not a link
  • The current section and the current menu item are marked visually and for assistive technology
  • A menu with more than five items offers a working filter; menus close on Escape, outside click and choice; everything is reachable by keyboard
  • A 404, a network error, a non-JSON body, an invalid body and an unknown version each leave today's header in place without an error message
  • An entry with a non-http(s) href is dropped; a malformed color falls back; host strings containing markup are shown literally
  • Following a host link with unsaved changes opens the existing unsaved-changes dialog, and Save or Discard then completes the navigation
  • The host data is requested again after a publish from the editor finishes
  • With host data the Hugo status renders compact in all three states with the full details in the tooltip; without host data the pill is unchanged
  • The contract, with path, shape, fallback rules and link rules, is documented next to the README's section on serving behind a reverse proxy
  • Tests cover each criterion above

Out of scope

  • Any knowledge of organizations, sites, roles or deckle-hosted in deckle. The slot is generic.
  • Authentication or authorization of the host data. That is the host's job.
  • The producer of the data. Cloonar/deckle-hosted implements it in a follow-up once this contract is final.
  • Flags or configuration for the harness. The reserved path is the whole interface.
  • Changes to publishing, to the module rail, to the tree or to any module.
  • A dark or light theme switch.

Blocked by

None - can start immediately

## What to build Give the editor's header a slot for host navigation. When deckle runs behind a host that wraps it, such as deckle-hosted, the host can supply where the user is and where they can go: a brand link home, breadcrumb segments with switcher menus, the sections next to the editor, and an account menu. The editor renders them in its own header, so the host does not need a second bar above the editor and the user sees one bar across both products. Standalone deckle does not change. With no host, the header looks and behaves exactly as it does today. The same change makes the Hugo status compact when a host is present, because a hosted editor cannot act on the Hugo version or on where the binary comes from. ## Why a slot in deckle The host cannot draw this itself. The editor runs on its own origin and hardcodes absolute paths, so the host can only reverse-proxy it. Framing the editor would stack two headers and break deep links. The editor's header is the only place where one bar is possible. ## Design reference The target is drawn in the dashboard redesign mockup of deckle-hosted: `design/hosted-shell-mockup.html` in `Cloonar/deckle-hosted`, state "Editor open". A published copy is at https://claude.ai/artifact/E6dcK8zKFALE3navT8QmhC. The bar in that mockup already uses the editor's own chrome colors, sizes and header height. `Cloonar/deckle-hosted#51` specifies the dashboard side of the same bar. ## The contract deckle owns this contract. The host implements the producer. ### Where the data comes from The editor requests `GET /api/host` on its own origin, with a relative URL, once when it loads. - A host that proxies the editor answers this path itself and never forwards it to the harness. - The harness never implements this path. It keeps answering 404 there, as it does for every unknown `/api/` path. That 404 is how standalone deckle says "no host". - Anything other than a 2xx response with a valid body of a supported version means no host navigation: a 404, a network error, a non-JSON body, an unknown version. The editor then shows today's header and reports no error. - After a publish started from the editor reaches a final state, the editor requests the data again, so a status mark next to the site name does not go stale. A failed refresh keeps the last good data. ### Shape, version 1 Proposed shape. Refine it while implementing if needed, and document the final form, since the host builds against the documentation. ```json { "version": 1, "brand": { "label": "deckle", "href": "https://host.example/dashboard" }, "segments": [ { "label": "Studio Nordlicht", "href": "https://host.example/orgs/nordlicht", "mark": { "kind": "initial", "text": "S", "color": "#4f46e5" }, "menu": { "groups": [ { "label": "Organizations", "items": [ { "label": "Studio Nordlicht", "detail": "/nordlicht", "meta": "3 sites", "href": "https://host.example/orgs/nordlicht", "current": true, "mark": { "kind": "initial", "text": "S", "color": "#4f46e5" } } ] } ], "footer": [ { "label": "All organizations", "href": "https://host.example/dashboard" } ] } }, { "label": "Bergland Tourismus", "href": "https://host.example/orgs/nordlicht/sites/bergland", "mark": { "kind": "status", "status": "ok" }, "menu": { "groups": [ { "label": "Sites in Studio Nordlicht", "items": [] } ], "footer": [] } } ], "sections": [ { "label": "Overview", "href": "https://host.example/orgs/nordlicht/sites/bergland" }, { "label": "Editor", "current": true }, { "label": "Builds", "href": "https://host.example/orgs/nordlicht/sites/bergland/builds" } ], "account": { "label": "mira@nordlicht.example", "detail": "Admin of Studio Nordlicht", "items": [ { "label": "Dashboard", "href": "https://host.example/dashboard" } ] } } ``` - Every part except `version` is optional. A host may send only a brand, or segments without menus. - deckle stays generic. It knows segments, menus, sections and marks. It does not know organizations, sites or roles. - A segment without `href` is a plain label. A segment without `menu` has no arrows. A section without `href` is not a link. - `mark.kind` is `initial`, a small colored square with one character, or `status`, a small state mark with `status` one of `ok`, `busy`, `error`, `locked`, `none`. deckle picks the colors for the states from its own tokens and shows `locked` as a lock icon, so state is never carried by color alone. - An item's `meta` is short trailing text such as a count or a state, and `detail` is a second line under the label. ### Header with host navigation Left to right, as in the mockup: the brand as a link in place of the "Deckle" wordmark; the segments separated by a slash; the sections with the current one marked; then, unchanged, the publish chip and Publish all; the compact preview status; the account menu. - **Names navigate, arrows switch.** A segment's name follows its `href`. A separate arrows control next to it opens its menu. - A menu shows its groups with their labels, marks the current item, and lists the footer entries below a divider. A menu with more than five items gets a filter field that narrows the list while typing. - Menus close on Escape, on outside click and after a choice. They are fully keyboard operable. - Long names truncate with the full name on hover. The header never overflows sideways. When the sections no longer fit, they move to a second row that scrolls sideways. ### Following a host link Every host entry is a plain link, followed in the same tab. The editor never sends a POST to the host. When a form has unsaved changes, following a host link goes through the editor's existing unsaved-changes dialog with Save, Discard and Stay. The browser's generic leave prompt is not the answer here; it stays for closing the tab. ### Compact preview status With host navigation present, the Hugo status pill becomes a dot with a short label: | Hugo state | Shown | | --- | --- | | running | green dot, "Preview" | | starting | amber dot, "Preview starting" | | crashed | red dot, "Preview crashed" | Everything the pill shows today, the state, restart count, version and binary source, moves into the tooltip and the accessible name. The "disconnected" hint stays. Without host navigation the pill is unchanged. ### Safety - Host text is rendered as text, never as markup. - Only absolute `https:` and `http:` URLs are accepted as `href`. An entry with any other scheme is dropped. - `color` is accepted only as a six-digit hex value. Anything else falls back to a neutral color. - The request is same-origin and relative. The editor never requests host data from another origin. ## Acceptance criteria - [ ] With no host data, the header is pixel-for-pixel today's header and the harness answers 404 on the reserved path; a test pins that 404 - [ ] With valid host data, the header shows brand link, segments, sections and account menu in the order above, with Publish all and the publish chip unchanged - [ ] A segment with a menu shows arrows that open it; a segment without one shows none; a segment without `href` is not a link - [ ] The current section and the current menu item are marked visually and for assistive technology - [ ] A menu with more than five items offers a working filter; menus close on Escape, outside click and choice; everything is reachable by keyboard - [ ] A 404, a network error, a non-JSON body, an invalid body and an unknown version each leave today's header in place without an error message - [ ] An entry with a non-http(s) `href` is dropped; a malformed color falls back; host strings containing markup are shown literally - [ ] Following a host link with unsaved changes opens the existing unsaved-changes dialog, and Save or Discard then completes the navigation - [ ] The host data is requested again after a publish from the editor finishes - [ ] With host data the Hugo status renders compact in all three states with the full details in the tooltip; without host data the pill is unchanged - [ ] The contract, with path, shape, fallback rules and link rules, is documented next to the README's section on serving behind a reverse proxy - [ ] Tests cover each criterion above ## Out of scope - Any knowledge of organizations, sites, roles or deckle-hosted in deckle. The slot is generic. - Authentication or authorization of the host data. That is the host's job. - The producer of the data. `Cloonar/deckle-hosted` implements it in a follow-up once this contract is final. - Flags or configuration for the harness. The reserved path is the whole interface. - Changes to publishing, to the module rail, to the tree or to any module. - A dark or light theme switch. ## Blocked by None - can start immediately
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/deckle#139
No description provided.