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
16 changes: 16 additions & 0 deletions content/docs/guide/plugin-development.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -95,6 +95,7 @@ export interface BoardProps {

### 2. Build the Implementation

<!-- doc-snippet: fragment — step 2 of the tutorial: this is the plugin package's own src/BoardImpl.tsx and it imports ./types, the sibling module step 1 tells the reader to write, so it cannot resolve in isolation -->
```tsx
// src/BoardImpl.tsx
import React from 'react';
Expand DownExpand Up@@ -133,6 +134,7 @@ export default function BoardImpl({ schema, className }: BoardProps) {

### 3. Create the Entry Point

<!-- doc-snippet: fragment — step 3 of the tutorial: the plugin package's own src/index.tsx, importing ./BoardImpl and ./types — both are files the reader created in steps 1 and 2 -->
```tsx
// src/index.tsx
import React, { Suspense } from 'react';
Expand DownExpand Up@@ -180,6 +182,8 @@ Field widgets follow the `FieldWidgetComponentProps` interface from `@object-ui/

```typescript
// FieldWidgetComponentProps<T> shape (from packages/fields/src/widgets/types.ts)
import type { FieldMetadata } from '@object-ui/types';

type FieldWidgetComponentProps<T = any> = {
value: T;
onChange: (val: T) => void;
Expand DownExpand Up@@ -209,6 +213,7 @@ grids, reports). That is precisely why a **field widget** needs an adapter when
it is rendered from a schema node instead of from a form. Wrap it once, at
registration, and the widget only ever implements one contract:

<!-- doc-snippet: fragment — registration excerpt: ColorPickerField is the widget the reader writes in the next section, shown here first so the registration call reads in one piece -->
```tsx
import { ComponentRegistry } from '@object-ui/core';
import { withFieldCarrier } from '@object-ui/fields';
Expand DownExpand Up@@ -292,6 +297,7 @@ export function ColorPickerField({

Register it as a field widget:

<!-- doc-snippet: fragment — the plugin package's own src/index.tsx again, importing ./ColorPickerField — the file written in the block immediately above -->
```tsx
// src/index.tsx
import { ComponentRegistry } from '@object-ui/core';
Expand All@@ -317,6 +323,7 @@ export { ColorPickerField };

Namespaces prevent type collisions between plugins:

<!-- doc-snippet: fragment — continues the board example: BoardRenderer is the component defined in step 3's src/index.tsx, not re-declared here -->
```tsx
import { ComponentRegistry } from '@object-ui/core';

Expand All@@ -337,6 +344,8 @@ Use `skipFallback: true` in the metadata if you do **not** want the component to
### Querying Registered Components

```tsx
import { ComponentRegistry } from '@object-ui/core';

ComponentRegistry.has('board'); // boolean
ComponentRegistry.getAllTypes(); // string[]
ComponentRegistry.getNamespaceComponents('plugin-board'); // ComponentConfig[]
Expand All@@ -346,6 +355,7 @@ ComponentRegistry.getNamespaceComponents('plugin-board'); // ComponentConfig

Define your schema interface in `types.ts` and extend `BaseSchema`:

<!-- doc-snippet: fragment — abridged restatement of step 1's src/types.ts — BoardColumn and BoardItem are the interfaces declared alongside it there, elided to keep the extends BaseSchema line in focus -->
```typescript
import type { BaseSchema } from '@object-ui/types';

Expand All@@ -358,6 +368,7 @@ export interface BoardSchema extends BaseSchema {

Declare `ComponentInput` entries when registering so the visual designer can offer a property panel:

<!-- doc-snippet: fragment — continues the board example: ComponentRegistry and BoardRenderer both come from step 3's src/index.tsx; this block shows only the inputs metadata -->
```tsx
ComponentRegistry.register('board', BoardRenderer, {
inputs: [
Expand All@@ -378,10 +389,14 @@ ComponentRegistry.register('board', BoardRenderer, {

ObjectUI uses **Vitest + React Testing Library**. Place tests next to the implementation.

<!-- doc-snippet: fragment — the plugin package's own src/BoardImpl.test.tsx, importing ./BoardImpl — the implementation the reader wrote in step 2 -->
```tsx
// src/BoardImpl.test.tsx
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
// `toBeInTheDocument` is a jest-dom matcher, not a Vitest one — without this
// import the assertions below do not type-check and do not run.
import '@testing-library/jest-dom';
import BoardImpl from './BoardImpl';

const schema = {
Expand DownExpand Up@@ -458,6 +473,7 @@ npm publish --access public
pnpm add @object-ui/plugin-board
```

<!-- doc-snippet: fragment — consumer-side excerpt: @object-ui/plugin-board is the package the reader has just been taught to build and publish, so it does not resolve from this repo -->
```tsx
// app/main.tsx — import once, auto-registers
import '@object-ui/plugin-board';
Expand Down
11 changes: 10 additions & 1 deletion content/docs/guide/schema-rendering.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,7 +31,7 @@ function App() {
const schema = {
type: "page",
title: "My Dashboard",
body: { /* ... */ }
body: { type: "text", value: "Hello" }
}

return <SchemaRenderer schema={schema} />
Expand All@@ -43,6 +43,8 @@ function App() {
Every schema object must have at minimum a `type` field:

```typescript
import type { CSSProperties } from 'react'

interface BaseSchema {
type: string // Component type identifier
id?: string // Optional unique identifier
Expand DownExpand Up@@ -74,6 +76,7 @@ interface BaseSchema {

The `SchemaRenderer` accepts a `data` prop that provides context for expressions:

<!-- doc-snippet: fragment — continues the block above — SchemaRenderer and schema are already in scope there; the closing JSX line is the call shown in place, not a statement that parses on its own -->
```tsx
const data = {
user: { name: "John", role: "admin" },
Expand All@@ -98,6 +101,7 @@ Use expression syntax `${}` to reference data:

The schema renderer uses a component registry to map schema types to React components:

<!-- doc-snippet: fragment — MyComponent is the reader's own React component, named here to show what register() takes as its second argument -->
```tsx
import { ComponentRegistry } from '@object-ui/core'

Expand DownExpand Up@@ -212,6 +216,7 @@ Object UI includes a powerful expression system for dynamic behavior:

Components can emit events that you handle in React:

<!-- doc-snippet: fragment — prop excerpt: the JSX call is shown alone to isolate onAction and onSubmit; schema and SchemaRenderer come from the first example on this page -->
```tsx
<SchemaRenderer
schema={schema}
Expand DownExpand Up@@ -263,6 +268,7 @@ The renderer automatically memoizes components to prevent unnecessary re-renders

Use dynamic imports for heavy components:

<!-- doc-snippet: fragment — code-splitting excerpt: ./HeavyChart is the reader's own component file, and registry is whichever registry instance the host already holds -->
```tsx
import { lazy } from 'react'

Expand All@@ -275,6 +281,7 @@ registry.register('heavy-chart', HeavyChart)

The renderer includes built-in error boundaries:

<!-- doc-snippet: fragment — prop excerpt: the JSX call is shown alone to isolate onError; schema and SchemaRenderer come from the first example on this page -->
```tsx
<SchemaRenderer
schema={schema}
Expand DownExpand Up@@ -330,6 +337,7 @@ const pageSchema = {

Pass all necessary data upfront:

<!-- doc-snippet: fragment — best-practice excerpt: userData, userSettings and dashboardStats are the reader's own values, and the closing JSX line is shown in place rather than as a parseable statement -->
```tsx
// ✅ Good
const data = {
Expand All@@ -345,6 +353,7 @@ const data = {

Move logic to expressions instead of creating conditional schemas:

<!-- doc-snippet: fragment — a bad/good contrast pair in one fence: schema is deliberately declared twice so the two spellings sit side by side, and user, adminSchema and userSchema are the reader's own values -->
```tsx
// ❌ Bad
const schema = user.isAdmin ? adminSchema : userSchema
Expand Down
69 changes: 50 additions & 19 deletions content/docs/guide/theming.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,6 +55,7 @@ ObjectUI follows the Shadcn convention. Design tokens are defined as HSL channel

Components reference these tokens through Tailwind:

<!-- doc-snippet: fragment — two sibling elements shown without a parent: the fence deliberately holds two JSX roots because the point is the Tailwind class names, not a renderable tree -->
```tsx
<div className="bg-background text-foreground border-border" />
<button className="bg-primary text-primary-foreground" />
Expand DownExpand Up@@ -87,6 +88,7 @@ To recolour ObjectUI, override the token values rather than the utilities — ei

ObjectUI uses [class-variance-authority](https://cva.style) to define type-safe component variants. Each component exports a `*Variants` function alongside the component itself:

<!-- doc-snippet: fragment — quotes the published Badge source: it imports class-variance-authority, which the reader installs in their own app but which is not a dependency of this repository's root, so the specifier does not resolve here -->
```tsx
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@object-ui/components";
Expand DownExpand Up@@ -119,6 +121,7 @@ function Badge({ className, variant, ...props }: BadgeProps) {

To add a custom variant to an existing component, wrap it:

<!-- doc-snippet: fragment — continues the block above — cva, Badge and cn are already imported there; this block shows only the wrapper -->
```tsx
const statusVariants = cva("", {
variants: {
Expand All@@ -139,6 +142,7 @@ function StatusBadge({ status, className, ...props }: { status: "active" | "pend

The `cn()` utility from `@object-ui/components` combines `clsx` and `tailwind-merge` so that later classes win over earlier ones without producing duplicate utilities:

<!-- doc-snippet: fragment — usage excerpt for cn(): isActive and className are the reader's own values, shown bare so the merge behaviour is the only thing on screen -->
```tsx
import { cn } from "@object-ui/components";

Expand All@@ -152,6 +156,7 @@ cn("base-class", isActive && "bg-primary", className);

Every ObjectUI component accepts a `className` prop that is merged via `cn()`, so consumers can always override styles:

<!-- doc-snippet: fragment — prop excerpt: a single Button usage, shown to demonstrate that className is merged; Button comes from the reader's own import of @object-ui/components -->
```tsx
<Button className="rounded-full px-8" variant="outline">
Custom Shape
Expand All@@ -162,6 +167,7 @@ Every ObjectUI component accepts a `className` prop that is merged via `cn()`, s

ObjectUI supports three modes: `light`, `dark`, and `auto` (follows system preference). The `ThemeProvider` applies a `light` or `dark` class to the root element and listens for `prefers-color-scheme` changes.

<!-- doc-snippet: fragment — MyApp is the reader's own root component — the block shows where ThemeProvider goes, not what it wraps -->
```tsx
import { ThemeProvider } from "@object-ui/react";

Expand DownExpand Up@@ -196,23 +202,33 @@ There is no `darkMode` option to set on your side. ObjectUI declares the class-b

Register one or more theme definitions with `ThemeProvider`. Each theme is a plain JSON object:

<!-- doc-snippet: fragment — App is the reader's own root component; the trailing ThemeProvider usage is shown in place so the theme object and its consumer read as one unit -->
```tsx
import type { Theme } from "@object-ui/types";

const corporateTheme: Theme = {
name: "corporate",
label: "Corporate",
// `colors` keys are ColorPalette keys, NOT Shadcn variable names. The engine
// maps them: text → --foreground, surface → --card, error → --destructive,
// textSecondary → --muted-foreground. A key that is not on ColorPalette is
// dropped, so the *-foreground pairs go through `customVars` below.
colors: {
primary: "220 70% 50%",
"primary-foreground": "0 0% 100%",
background: "0 0% 100%",
foreground: "220 20% 10%",
text: "220 20% 10%",
accent: "200 80% 55%",
},
// Emitted verbatim as `--<key>: <value>` — the declared door for any custom
// property that has no ColorPalette key.
customVars: {
"primary-foreground": "0 0% 100%",
"accent-foreground": "0 0% 100%",
},
fonts: {
sans: "IBM Plex Sans, system-ui",
typography: {
fontFamily: { base: "IBM Plex Sans, system-ui" },
},
radius: "0.375rem",
borderRadius: { base: "0.375rem" },
};

<ThemeProvider themes={[corporateTheme]} defaultTheme="corporate">
Expand All@@ -222,13 +238,16 @@ const corporateTheme: Theme = {

Themes support **inheritance** via the `extends` field. A child theme only needs to declare its overrides:

<!-- doc-snippet: fragment — continues the block above — corporateTheme is declared there; App is the reader's own root component -->
```tsx
const darkCorporate: Theme = {
name: "corporate-dark",
label: "Corporate Dark",
extends: "corporate",
colors: {
primary: "220 70% 50%",
background: "220 20% 8%",
foreground: "0 0% 95%",
text: "0 0% 95%",
},
};

Expand All@@ -242,26 +261,36 @@ const darkCorporate: Theme = {
Start from HSL values and define both light and dark variants:

```tsx
const brand: Theme = {
import type { Theme } from "@object-ui/types";

export const brand: Theme = {
name: "brand",
label: "Brand",
colors: {
// Light palette
primary: "262 83% 58%", // Purple
// Every key here is a ColorPalette key; the comment is the CSS variable
// the engine emits it as.
primary: "262 83% 58%", // --primary (Purple)
secondary: "262 30% 94%", // --secondary
accent: "160 84% 39%", // --accent (Teal accent)
background: "0 0% 100%", // --background
surface: "0 0% 100%", // --card
text: "262 20% 10%", // --foreground
textSecondary: "262 10% 45%", // --muted-foreground
disabled: "262 20% 95%", // --muted
border: "262 20% 90%", // --border
error: "0 84% 60%", // --destructive
},
// Shadcn pairs the palette with foreground/ring variables that have no
// ColorPalette key of their own. Author those here — they are emitted
// verbatim as `--<key>: <value>`.
customVars: {
"primary-foreground": "0 0% 100%",
secondary: "262 30% 94%",
"secondary-foreground": "262 83% 30%",
accent: "160 84% 39%", // Teal accent
"accent-foreground": "0 0% 100%",
background: "0 0% 100%",
foreground: "262 20% 10%",
muted: "262 20% 95%",
"muted-foreground": "262 10% 45%",
border: "262 20% 90%",
ring: "262 83% 58%",
destructive: "0 84% 60%",
"destructive-foreground": "0 0% 100%",
ring: "262 83% 58%",
},
radius: "0.5rem",
borderRadius: { base: "0.5rem" }, // --radius
};
```

Expand DownExpand Up@@ -289,6 +318,7 @@ function ThemeSwitcher() {

Enable **persistence** so the user's choice survives page reloads:

<!-- doc-snippet: fragment — persistence excerpt: lightTheme, darkTheme and brandTheme are the reader's own theme objects and App is their root component -->
```tsx
<ThemeProvider
themes={[lightTheme, darkTheme, brandTheme]}
Expand All@@ -308,6 +338,7 @@ ObjectUI components use **logical CSS properties** (`ms-`, `me-`, `ps-`, `pe-`)

1. Set the `dir` attribute on your root element:

<!-- doc-snippet: fragment — an opening tag on its own, shown to isolate the dir and lang attributes — it is deliberately unclosed because the reader's document supplies the rest -->
```tsx
<html dir="rtl" lang="ar">
```
Expand Down
Loading
Loading