standards
ViewsSDK

Views

Declare the list and detail views of your objects next to their schema, and ship them to the workspace in the same sync.


Views are the screens your team sees in the Standards app. listView is the table of records; detailView is the page one record opens into. The ones you declare here are a starting point: on app.standards.new your users change them, add tabs and columns, and create views of their own, and sync keeps theirs while owning yours. Declare them next to your objects and they ship in the same schema.sync.

import { content, detailView, group, listView } from "@stndrds/client";

const IDENTITY = content.form(
  group("identity", "Identity").fields("firstName", "lastName", "email")
);

export const CONTACT_DETAIL = detailView("contact_detail", "Contact")
  .for("contacts")
  .default()
  .tab("general", "Info", IDENTITY)
  .tab("activity", "Activity", content.activity())
  .aside("details", "Details", content.form(group("details", "Details").field("status")))
  .build();

export const CONTACTS = listView("contacts_list", "Contacts")
  .for("contacts")
  .default()
  .tab("all", "All", content.collection().object("contacts").table().columns("firstName", "email"))
  .build();

The view chain

MethodEffect
detailView(name, label) / listView(name, label)Starts a view in record or collection context
.for(object)The object the view belongs to; required
.default()Opens by default for that object and context; one per pair
.tab(name, label, content)Places a content in the main area
.aside(name, label, content)Places a content in the side panel
.description(text), .icon(name)View metadata
.build()Validates and returns { kind: "view", definition }

Every call returns a new builder. A content has no name, label or placement of its own; the view supplies them. Content names are unique within a view, and at least one main content is required. Contents that need a record (everything except a collection on an object) are refused on a listView at build time.

The nine content types

content.collection()          // records of an object or of a relation
content.form(...groups)       // fields of the current record, laid out in groups
content.activity()            // the record's activity feed
content.richtext("body")      // one richtext attribute, full width
content.documents()           // the record's document packs
content.forms().object("x")   // submissions of the form system for an object
content.emails("email")       // threads matched on email attributes
content.meetings("email")     // meetings matched on email attributes
content.custom(extension, o)  // a registered content extension

All of them accept .icon(name). content.custom names an extension already registered on the platform; the extension registry itself is not part of the client surface.

Collections

A collection reads one source: .object(name), .related(attribute) for a relation the current record declares, or .relatedFrom(object, attribute) for the records whose own relation points back at the current record. .related needs one concrete target object; polymorphic and universal relations are not a collection source.

import { content, detailView } from "@stndrds/client";

export const COMPANY_DETAIL = detailView("company_detail", "Company")
  .for("companies")
  .tab("contacts", "Contacts", content.collection()
    .relatedFrom("contacts", "company")
    .table()
    .columns("firstName", "lastName", "email")
    .sort("lastName")
    .openMode("peek")
    .createMode("inline")
    .actions("create", "update", "delete"))
  .build();
OptionNotes
.table(), .list(), .kanban(groupByAttribute)One layout; switching clears layout-specific settings
.columns(...attributes)Columns of a table, fields of a card
.filter(filterState)A FilterState (combinator + rules)
.sort(attribute, direction?)Defaults to asc; repeated sorts append
.actions(...)create, update, delete, attach, detach; .actions() hides every mutation
.openMode("page" | "peek")Omitted means page, and no peek history
.createMode("page" | "peek" | "inline")Omitted follows the resolved open mode
.visibility("workspace" | "private" | "all")Who sees the tab
.displayAttributes(...), .descriptionAttribute(name)Card rendering
.columnSizing(widths)Table and list only
.kanbanColumnOrder(...), .kanbanColumnVisibility(map), .kanbanPinnedColumns(...)Kanban only

Actions never grant permissions: a user without update on the object does not gain it from a collection.

Forms inside a view

group(name, label) holds fields: .field(attribute, { label?, span?, readOnly? }) or .fields(...attributes), plus .description(text) and .collapsible(collapsed?). content.form(...groups) accepts .formColumns(1 | 2 | 3) and .density("comfortable" | "compact" | "spacious").

content.form( is a layout inside a view: it arranges the record's own attributes. The form system (form(), slots, steps, prefill) and workflows are not pushable through a schema source, so they are not part of this SDK.

Views in a source

Pass views next to the objects. Each one is built and validated locally, then sent with the objects on the next schema.sync.

import { defineSchemaSource } from "@stndrds/client";

export const crmSource = defineSchemaSource("crm", {
  objects: [CONTACT, COMPANY],
  views: [CONTACT_DETAIL, CONTACTS, COMPANY_DETAIL],
});

On the workspace a source-owned view is adopted the first time it is pushed, updated on every later change, and released when it leaves the source: the row becomes a custom view and workspace overlays survive. Declaring it again adopts the row back. The Sync page shows these counters in the SyncResult.

@stndrds/schema exports relationProperty(name) to reference a qualified property in a column list; @stndrds/client does not re-export it. Column references from a client-only project stay on plain attributes.

Next steps