feat: headless demo page that renders a table from a manifest URL

Merged
MX
4d ago
+2657-125

Summary

  • /headless (shortcut /h): a page with no schema of its own. It reads a manifest URL from ?manifest= and renders that endpoint. It starts on /drizzle/api/schema and has a second example, /headless/api/orders/schema, an in-memory array served as plain JSON with an inferred schema, offset paging and no chart or live mode. Inspect lists what the endpoint declares and runs runEndpointConformance against it in the browser. Any https:// endpoint with CORS headers can be pasted (http:// for localhost only); other URL forms are rejected.
  • data-table-remote: rendering DataTableRemote for the first time surfaced gaps.
    • searchParamsSerializer is optional and derived from the manifest. AGENTS.md and the Cursor rules already showed the component without it.
    • uuid and live are part of the URL state, so the detail sheet opens on a table with a select column and live mode has state to flip.
    • commandSlot and toolbarActions may be functions of the resolved manifest, filter schema, refresh and fetchPreviousPage. sheetSlot receives the same context as a second argument.
    • extraColumns appends columns after the manifest's; defaultColumnVisibility is applied over the manifest's defaults.
  • data-table-sheet: a record value renders as JSON and a missing value as a dash, instead of [object Object] and null.
  • data-table-query (first commit): runEndpointConformance failed this repo's own Drizzle endpoint on size, because createDrizzleHandler returns a group of rows sharing one cursor value whole (six here, probe size five). An oversized page now passes when every row carries the page's nextCursor in a timestamp column, with a note in the check's detail. The manifest has to name that timestamp column; a body that is null or not an object is now a failed shape check instead of a throw. An endpoint that ignores size still fails. Docs describe size as a target.
  • Web app: the shared logs schema declares level-indicator and status-code as its wire displays (the custom cells still render on /infinite and /drizzle), the loading skeleton no longer uses Math.random() (hydration mismatch), and the live button takes its timestamp column as a prop. Docs, README, sitemap and the home examples list include the new page.

Row actions: DataTableRemote does not draw them itself, so a headless table does not have to install the actions block. It gains extraColumns and defaultColumnVisibility, and the host places the actions block's column and bulk bar. The page does this for the logs example, as /drizzle does, and only for a manifest this site serves. The docs show the wiring.

Test plan

  • Local checks pass (format, lint, typecheck, tests)
  • CI passes
  • Manually verified in browser (if UI change)
  • Registry rebuilt (if packages/registry/src/ changed)

Browser checks were run in headless Chromium against a local dev server: filters, sort, infinite scroll, detail sheet and its URL state, command palette, live mode, Inspect on both examples, a cross-origin endpoint with CORS, six rejected or failing URLs, mobile width and dark mode. The fixed kit was also run against the production /drizzle/api at its default probe size: 9 passed.

Breaking changes

None. Behaviour changes to note:

  • In DataTableRemote, a column whose key is a reserved request parameter (uuid, live, sort, cursor, direction, size) keeps its column but loses its filter, with a console warning. uuid and live are now part of the URL state.
  • The sheet's fallback for record and missing values changed as described above.
  • A passing conformance check may now carry a detail.
  • /headless/api/orders and its manifest send access-control-allow-origin: * (public sample data).

Summary by cubic

Adds a /headless demo page that renders a table from any manifest URL and closes the gaps in DataTableRemote that real usage surfaced.

  • /headless reads the manifest from ?manifest=, starts on /drizzle/api/schema, and ships a second plain-JSON endpoint (/headless/api/orders) with an inferred schema and offset paging. Manifest URLs are validated (https only, http for localhost; tabs, line breaks and spaces are rejected), and the rows URL is derived from the manifest path alone so a query string on the manifest is not copied onto row requests. This site's own endpoint pasted as an absolute URL is treated as served here, so it keeps row actions and loses the "another host" note.
  • Inspect lists what the endpoint declares and runs runEndpointConformance in the browser; any https:// URL with CORS headers can be pasted, http:// only for localhost. The rows response is validated against the list contract instead of rendering an empty table.
  • The endpoint row is the top strip of the table's main column on the logs example, whose row actions render the way /drizzle does: a bulk bar on a selection and a hidden row-menu column. Actions appear only for manifests this site serves.
  • DataTableRemote now derives its search param serializer from the manifest, keeps uuid and live in URL state, lets commandSlot, toolbarActions, and sheetSlot be functions of the resolved manifest, and takes extraColumns and defaultColumnVisibility so the host can append the actions block. It still does not draw row actions itself.
  • The detail sheet renders a record as JSON, a missing value as a dash instead of [object Object] and null, and [unserializable] for values JSON cannot write.
  • runEndpointConformance accepts a page larger than size when every row carries the page's nextCursor in a timestamp column named by the manifest; size is documented as a target. A body that is not an object is a failed shape check, not a throw.
  • The loading skeleton no longer uses Math.random() (hydration mismatch), and the live button takes its timestamp column as a prop.

Behavior changes

  • uuid, live, sort, cursor, direction, size, plus any key the pagination strategy owns (offset or a custom cursor key), are reserved URL-state keys in DataTableRemote; a filter on a column with any of them is dropped with a warning, and row selection is seeded from uuid only when there is no select column. readPath reads own properties only, so a primary key of toString or constructor no longer looks present on every row.
  • The sheet's fallback for record and missing values changed as described above, and a passing conformance check may carry a detail note.
  • /headless/api/orders and its manifest send access-control-allow-origin: * (public sample data).