Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions skills/objectui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ skills/objectui/
│ ├── composition.md
│ └── no-touch-zones.md
├── guides/ # Domain-specific deep dives
│ ├── architecture.md
│ ├── app-composition.md
│ ├── page-builder.md
│ ├── plugin-development.md
│ ├── schema-expressions.md
Expand Down Expand Up @@ -78,8 +80,8 @@ The skill stays in sync with the `packages/` tree:

- **Core renderer:** `@object-ui/types`, `core`, `components`, `fields`, `layout`, `react`
- **Integration:** `@object-ui/app-shell`, `providers`, `runner`, `data-objectstack`
- **Platform features:** `@object-ui/auth`, `permissions`, `tenant`, `i18n`, `mobile`, `collaboration`
- **Plugins (19):** `plugin-{grid, list, detail, form, kanban, calendar, timeline, gantt, dashboard, report, charts, map, editor, markdown, view, designer, workflow, ai, chatbot}`
- **Platform features:** `@object-ui/auth`, `permissions`, `i18n`, `mobile`, `collaboration`
- **Plugins (19):** `plugin-{grid, list, detail, form, kanban, calendar, timeline, gantt, dashboard, report, charts, map, editor, markdown, view, tree, designer, ai, chatbot}`
- **Tooling:** `@object-ui/cli`, `create-plugin`, `vscode-extension`

## Maintenance
Expand Down
2 changes: 1 addition & 1 deletion skills/objectui/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ Actions are defined **as data**, not functions. Example:

### 6. Layout as Components

Layouts are just components that render children. Treat `Grid`, `Stack`, `Container` as first-class citizens. Layout schemas declare responsive columns on the node as `columns` — a number, or a breakpoint object keyed `xs` / `sm` / `md` / `lg` / `xl`, with `xs` as the base (e.g. `columns: { xs: 1, md: 2, lg: 4 }`).
Layouts are just components that render children. Treat `Grid`, `Stack`, `Container` as first-class citizens. Layout schemas declare responsive columns on the node as `columns` — a number, or a breakpoint object keyed `xs` / `sm` / `md` / `lg` / `xl`, with `xs` as the base (e.g. `columns: { xs: 1, md: 2, lg: 4 }`). The spec also accepts `2xl`, which the `grid` renderer never reads ([`rules/protocol.md`](./rules/protocol.md)).

### 7. Type Safety over Magic

Expand Down
9 changes: 7 additions & 2 deletions skills/objectui/guides/app-composition.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@ skip your app. Powerful constructs are escape hatches, not defaults.

## What Each Navigation Target Buys You

The nav contract is a discriminated union on `type` (see `NavigationItemSchema`
in `@object-ui/types`; aligned with `@objectstack/spec`). Runtime resolution is
The nav contract is a discriminated union on `type` — nine members:
`object`, `dashboard`, `page`, `report`, `url`, `component`, `group`,
`separator`, `action` (see `NavigationItemSchema` in `@object-ui/types`;
aligned with `@objectstack/spec`). Runtime resolution is
`resolveHref` in `packages/layout/src/NavigationRenderer.tsx` — the single
source of truth for nav → URL mapping.

Expand All @@ -44,7 +46,10 @@ source of truth for nav → URL mapping.
| `{type:'report', reportName}` | `/report/:name` | Report renderer |
| `{type:'page', pageName}` | `/page/:name` | **Bare SDUI rendering only.** No object shell — view switching, actions, and record routing must be hand-assembled in the page schema |
| `{type:'url', url}` | external | External link (`target` controls tab) |
| `{type:'component', componentRef, params?}` | `/component/:ns/:name?…` | A registered bespoke surface. `componentRef` is colon-joined (`metadata:resource`); `params` ride as querystring. `metadata:*` refs are special-cased onto the `/metadata[/:type[/:name]]` routes |
| `{type:'group', children}` | — | Grouping only; no target |
| `{type:'separator'}` | — | A rule in the sidebar; skipped by `resolveHref` and never pinnable |
| `{type:'action'}` | — | Fires a host handler instead of navigating; **dropped entirely** when the host passes no action handler |

## Decision Rules (in priority order)

Expand Down
23 changes: 10 additions & 13 deletions skills/objectui/guides/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ ObjectUI is a strict PNPM Workspace. Pick a package by **role + dependency weigh

| Package | Role | Responsibility |
|---|---|---|
| `@object-ui/app-shell` | Minimal Shell | Framework-agnostic `AppShell`, `ObjectRenderer`, `DashboardRenderer`, `PageRenderer`. Bring-your-own-router. |
| `@object-ui/app-shell` | Minimal Shell | Framework-agnostic `AppShell`, `ObjectView`, `DashboardView`, `PageView`. Bring-your-own-router. |
| `@object-ui/providers` | Context Stack | Reusable `DataSourceProvider`, `MetadataProvider`, `ThemeProvider`. Console-free. |
| `@object-ui/runner` | Universal Runtime | Standalone runtime + dev server for schema-driven apps. Pre-wires popular plugins. |
| `@object-ui/data-*` | Data Adapters | Connectors for REST, ObjectQL, GraphQL (e.g. `@object-ui/data-objectstack`). |
Expand Down Expand Up @@ -59,14 +59,14 @@ ObjectUI is a strict PNPM Workspace. Pick a package by **role + dependency weigh
| `@object-ui/plugin-editor` / `plugin-markdown` | Rich text + markdown editors. |
| `@object-ui/plugin-view` | View switcher / saved views. |
| `@object-ui/plugin-designer` | Visual schema designer canvas. |
| `@object-ui/plugin-workflow` | Workflow / process editor. |
| `@object-ui/plugin-tree` | Hierarchy / tree views (`tree`, `object-tree`). |
| `@object-ui/plugin-ai` / `plugin-chatbot` | AI assistant + chatbot UI. |

### Tooling

| Package | Purpose |
|---|---|
| `@object-ui/cli` | `objectui` CLI: `init`, `dev`, `build`, `start`, `studio`, `validate`, `check`, `lint`, `test`, `generate`, `add`, `doctor`, `analyze`, `create plugin`. |
| `@object-ui/cli` | `objectui` CLI: `init`, `serve`, `dev`, `build`, `start`, `studio`, `validate`, `check`, `lint`, `test`, `generate`, `add`, `doctor`, `analyze`, `create plugin`. |
| `@object-ui/create-plugin` | `pnpm create-plugin <name>` scaffolder for new `plugin-*` packages. |
| `@object-ui/vscode-extension` | VSCode extension: syntax highlighting, IntelliSense, validation for ObjectUI JSON schemas. |

Expand Down Expand Up @@ -131,18 +131,15 @@ See `rules/protocol.md` for which fields are expression-evaluated and which are
How users add their own components (e.g. a `Map` widget):

```typescript
// packages/core/src/registry.ts
export type ComponentImpl = React.FC<{ schema: any; ... }>;
// packages/core/src/registry/Registry.ts — one shared instance, not free functions
export const ComponentRegistry = new Registry<any>();

const registry = new Map<string, ComponentImpl>();
// register(type, component, meta?) — `meta.namespace` makes the key `namespace:type`
ComponentRegistry.register('map', MapRenderer, { namespace: 'plugin-map' });

export function registerComponent(type: string, impl: ComponentImpl) {
registry.set(type, impl);
}

export function resolveComponent(type: string) {
return registry.get(type) || FallbackComponent;
}
// get(type, namespace?) — undefined when nothing is registered; the caller
// (SchemaRenderer) is what falls back, the registry does not.
const impl = ComponentRegistry.get('map');
```

### Pattern B: The Renderer Loop (Recursion)
Expand Down
15 changes: 11 additions & 4 deletions skills/objectui/guides/data-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,23 @@ interface DataSource<T = any> {

// View support (optional)
getView?(objectName: string, viewId: string): Promise<any | null>;
saveView?(objectName: string, viewId: string, config: any): Promise<any>;
createView?(...); updateView?(...); updateViewConfig?(...); deleteView?(...);

// Analytics (optional)
aggregate?(resource: string, params: AggregateParams): Promise<AggregateResult>;

// Custom actions (optional)
execute?(resource: string, action: string, params?: any): Promise<any>;

// Real-time (optional)
onMutation?(callback: (event: MutationEvent) => void): () => void;
}
```

Six members are **required**; the optional half is much larger than the excerpt
above — **32** optional members at `origin/main`, covering bulk/transaction,
views, apps & pages, file upload, and the export / import job lifecycles. Read
`data.ts` before concluding a capability is missing, and note the two spellings
that do **not** exist: there is no `saveView` (write through `updateViewConfig`
/ `createView` / `updateView`) and no generic `execute`.

### QueryParams

```typescript
Expand All @@ -69,6 +73,9 @@ interface QueryParams {
$skip?: number; // OFFSET (for pagination)
$top?: number; // LIMIT (page size)
$expand?: string[]; // JOIN/expand related objects
$search?: string; // free-text search term
$searchFields?: string[]; // fields the search term is matched against
$count?: boolean; // ask the backend for `total`
[key: string]: any; // why an unprefixed `limit` type-checks — and is then dropped
}
```
Expand Down
90 changes: 49 additions & 41 deletions skills/objectui/guides/mobile.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ function ResponsiveLayout() {
}
```

Breakpoint values follow Tailwind defaults:
Breakpoint values (`BREAKPOINTS`, Tailwind-compatible):
- `xs`: 0px (the base — `useBreakpoint` reports it below `sm`)
- `sm`: 640px
- `md`: 768px
- `lg`: 1024px
Expand All @@ -31,51 +32,55 @@ Breakpoint values follow Tailwind defaults:

## Touch gesture handling

One hook, `useGesture`, per gesture. It takes a `type` plus one `onGesture`
callback and returns a **ref** to attach — it does not return a spread-able
handler bag.

```typescript
import { useSwipe, useLongPress, usePinchZoom } from '@object-ui/mobile';
import { useGesture, usePullToRefresh } from '@object-ui/mobile';

function MobileCard() {
const swipeHandlers = useSwipe({
onSwipeLeft: () => showActions(),
onSwipeRight: () => dismiss(),
const swipeRef = useGesture<HTMLDivElement>({
type: 'swipe-left', // threshold?: px (swipes)
onGesture: () => showActions(),
threshold: 50,
});

const longPressHandlers = useLongPress({
onLongPress: () => openContextMenu(),
delay: 500,
const listRef = usePullToRefresh<HTMLDivElement>({
onRefresh: async () => refetch(), // threshold?: px, default 80
});

return (
<div {...swipeHandlers} {...longPressHandlers}>
<CardContent />
</div>
);
return <div ref={swipeRef}><div ref={listRef}><CardContent /></div></div>;
}
```

## Bottom sheet and mobile navigation
`type` is a `GestureType`: `tap`, `double-tap`, `long-press` (tune with
`longPressDuration`, ms), `swipe-left` / `-right` / `-up` / `-down`, `pinch`,
`rotate`, `pan`. `useSpecGesture` is the schema-driven twin, for gestures
declared in metadata rather than in TSX.

```typescript
import { BottomSheet, MobileNav } from '@object-ui/mobile';
## Breakpoint-gated rendering and mobile navigation

function MobileApp() {
return (
<>
<MobileNav items={navItems} />
<MainContent />
<BottomSheet
open={showFilters}
onClose={() => setShowFilters(false)}
snapPoints={[0.5, 0.9]}
>
<FilterPanel />
</BottomSheet>
</>
);
}
`@object-ui/mobile` ships **no** widgets — there is no `BottomSheet` and no
`MobileNav` in it. What it ships is the gate: `ResponsiveContainer` renders its
children only on the breakpoints you name.

```typescript
import { ResponsiveContainer, MobileProvider } from '@object-ui/mobile';
import { Drawer, Sheet } from '@object-ui/components'; // the actual overlays

<MobileProvider>
<ResponsiveContainer maxBreakpoint="sm" fallback={<DesktopFilters />}>
<Drawer>{/* the bottom sheet */}</Drawer>
</ResponsiveContainer>
</MobileProvider>
```

Props: `minBreakpoint` / `maxBreakpoint` / `showOn` / `hideOn` / `fallback`.
Mobile navigation is an **app-shell** concern, not a package export: set
`mobileNavMode` (`'drawer'` | `'bottom_nav'`) on the app schema that
`AppSchemaRenderer` (`@object-ui/layout`) renders.

## Responsive schema layouts

Use responsive column configurations in grid layouts:
Expand All @@ -96,7 +101,9 @@ Use responsive column configurations in grid layouts:

`columns` takes a number or a breakpoint object (`xs` / `sm` / `md` / `lg` /
`xl`), with `xs` as the base — the schema above renders
`grid-cols-1 md:grid-cols-2 lg:grid-cols-4`. Keys belong on the node, not in a
`grid-cols-1 md:grid-cols-2 lg:grid-cols-4`. The spec's map also declares
`2xl`, but the `grid` renderer reads only those five: a `2xl` entry parses and
is then silently dropped, so stop at `xl`. Keys belong on the node, not in a
`props` wrapper: `"props": { "cols": … }` leaves both the columns *and* the card
titles unread (objectui#4001).

Expand All @@ -111,35 +118,36 @@ titles unread (objectui#4001).

## Mobile-optimized form inputs

```typescript
import { MobileSelect, MobileDatePicker } from '@object-ui/mobile';

// MobileSelect uses native <select> on mobile for better UX
// MobileDatePicker uses native date input on mobile
```
There is no separate mobile widget set — no `MobileSelect`, no
`MobileDatePicker`. Field widgets come from `@object-ui/fields` and adapt
themselves; `useTouchTarget` is the hook for enforcing a minimum tap size on a
custom control.

## Offline support

```typescript
import { useOffline } from '@object-ui/react';

function DataForm() {
const { isOnline, queue, syncState } = useOffline();
const { isOnline, pendingCount, syncState } = useOffline();

// When offline, mutations are queued
// When back online, queued mutations are synced automatically

return (
<div>
{!isOnline && <Banner>You are offline. Changes will sync when connected.</Banner>}
{!isOnline && <Banner>You are offline. {pendingCount} change(s) queued.</Banner>}
{syncState === 'syncing' && <Spinner />}
<FormContent />
</div>
);
}
```

Sync states: `idle` → `syncing` → `synced` | `error`
`SyncState` is `'idle' | 'syncing' | 'error' | 'offline'` — there is no
`'synced'`; a drained queue returns to `'idle'`. The rest of `OfflineResult`:
`enabled`, `strategy`, `pendingCount`, `queueMutation`, `sync`, `clearQueue`,
`showIndicator`, `offlineMessage` — and no `queue` array is exposed.

## Viewport considerations

Expand Down
14 changes: 11 additions & 3 deletions skills/objectui/guides/page-builder.md
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,7 @@ Pick plugins by domain — each registers its own `type` strings in the Componen
| Boards / Dashboards | `plugin-kanban`, `plugin-dashboard`, `plugin-report` |
| Visualization | `plugin-charts`, `plugin-map` |
| Editors | `plugin-editor`, `plugin-markdown` |
| Views & Design | `plugin-view`, `plugin-designer`, `plugin-workflow` |
| Views & Design | `plugin-view`, `plugin-tree`, `plugin-designer` |
| AI | `plugin-ai`, `plugin-chatbot` |

For lazy loading, use `LazyPluginLoader` from `@object-ui/react` rather than top-level imports.
Expand All @@ -486,10 +486,18 @@ For lazy loading, use `LazyPluginLoader` from `@object-ui/react` rather than top
For host apps that need more than the raw renderer, prefer `@object-ui/app-shell`:

```tsx
import { AppShell, ObjectRenderer, PageRenderer, DashboardRenderer } from '@object-ui/app-shell';
import { AppShell, ObjectView, PageView, DashboardView } from '@object-ui/app-shell';
```

It exposes `ObjectRenderer`, `PageRenderer`, `DashboardRenderer` and matching providers (`AdapterProvider`, `MetadataProvider`, `ExpressionProvider`). See `guides/project-setup.md` for the decision matrix.
It exposes `ObjectView`, `RecordDetailView`, `PageView`, `DashboardView`,
`ReportView` and matching providers (`AdapterProvider`, `MetadataProvider`,
`ExpressionProvider`). ⚠️ Not `ObjectRenderer` / `PageRenderer` /
`DashboardRenderer`. `ObjectRenderer` exists nowhere in the repo;
`DashboardRenderer` is a public export of `@object-ui/plugin-dashboard`; and
`PageRenderer` is an internal renderer inside `@object-ui/components`,
reachable only through the `page` / `app` / `utility` / `home` / `record`
registry keys it registers, never as an import. See
`guides/project-setup.md` for the decision matrix.

## Common mistakes to avoid

Expand Down
32 changes: 26 additions & 6 deletions skills/objectui/guides/plugin-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,20 +80,38 @@ ComponentRegistry.register('my-widget', MyWidgetRenderer, {
| `isContainer` | `boolean` | Accepts child components |
| `resizable` | `boolean` | Designer allows resizing |
| `resizeConstraints` | `object` | Min/max width/height |
| `tier` | `'public' \| 'internal'` | Public contract tier (ADR-0080). Undefined = internal |
| `labelling` | `'control' \| 'group' \| 'display'` | How a host associates its label; absent ⇒ `'control'` |
| `deprecated` | `object` | Authoring-time deprecation (`surfaces`, `replacement`) |
| `examples` | `array` | Sample schemas for the designer |
| `tags` | `string[]` | Free-form grouping tags |
| `description` | `string` | Longer description for the designer |

Sixteen keys in total: the eleven on `ComponentMeta` (`@object-ui/types`
`base.ts`) plus the five `RegistryComponentMetaExtras` the registry adds
(`tier`, `namespace`, `skipFallback`, `labelling`, `deprecated`).

### ComponentInput types

```typescript
type ComponentInputControlType =
| 'string' | 'number' | 'boolean' | 'enum' | 'array' | 'object'
| 'color' | 'date' | 'code' | 'file' | 'slot';

type ComponentInput = {
name: string; // Maps to component prop
type: 'string' | 'number' | 'boolean' | 'enum' | 'array' | 'object'
| 'color' | 'date' | 'code' | 'file' | 'slot';
// ONE control type, or an ARRAY of them when the input accepts several
// shapes (objectui#3832). Widening the vocabulary is a contract change.
type: ComponentInputControlType | ComponentInputControlType[];
label?: string;
defaultValue?: any;
required?: boolean;
enum?: string[] | Array<{ label: string; value: string }>;
description?: string;
advanced?: boolean; // Hide by default in designer
inputType?: string; // Widget hint for the designer control
min?: number; max?: number; step?: number; // numeric bounds
placeholder?: string;
};
```

Expand Down Expand Up @@ -264,10 +282,12 @@ type FieldWidgetComponentProps<T = any> = {
The slot is named `error` because that is what `FieldWidgetPropsSchema` in
`@objectstack/spec/ui` — the published widget contract — calls it.

The type is **closed**: it also declares the host plumbing and DOM/ARIA
pass-through keys the renderer forwards (`dataSource`, `dependentValues`,
`dependsOn`, `emptyHint`, `compact`, `id`, `name`, `aria-*`, `data-*`), and
nothing else. A key it does not declare is a compile error rather than a silent
The type is **closed**: it also declares the host plumbing
(`dataSource`, `dependentValues`, `dependsOn`, `dependsOnLabels`, `emptyHint`,
`compact`, `onUploadingChange`, `onSelectRecord`, `onCreateNew`) and, by
intersection, the DOM/ARIA pass-through the renderer forwards
(`FieldWidgetDomProps` → `id` / `name`, `AriaAttributes`, and a
`data-${string}` index signature), and nothing else. A key it does not declare is a compile error rather than a silent
`any`, so a typo like `readOnly` for `readonly` is caught at build time instead
of being quietly `undefined` at runtime.

Expand Down
Loading