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
31 changes: 28 additions & 3 deletions content/docs/guide/architecture.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -140,10 +140,21 @@ The `SchemaRenderer` component:

```tsx
import { SchemaRenderer } from '@object-ui/react'
import type { BaseSchema } from '@object-ui/types'

// The schema from step 1, as the object the renderer receives.
const schema: BaseSchema = {
type: 'card',
title: 'Welcome',
body: {
type: 'text',
value: 'Hello, ${user.name}!',
},
}

function App() {
const data = { user: { name: "Alice" } }
const data = { user: { name: 'Alice' } }

return <SchemaRenderer schema={schema} data={data} />
}
```
Expand All@@ -152,6 +163,7 @@ function App() {

The registry maps type strings to React components:

<!-- doc-snippet: fragment — registry excerpt — CardComponent and TextComponent are placeholder names for the reader's own components, and ComponentRegistry is imported where it is used further down the page -->
```typescript
// During app initialization
ComponentRegistry.register('card', CardComponent)
Expand All@@ -165,6 +177,7 @@ const Component = ComponentRegistry.get('card') // → CardComponent

The registered component renders with evaluated props:

<!-- doc-snippet: fragment — the JSX the registry produces for step 1's schema; CardComponent and TextComponent are the placeholder components registered in the block above -->
```tsx
<CardComponent title="Welcome">
<TextComponent value="Hello, Alice!" />
Expand All@@ -179,6 +192,7 @@ ObjectUI uses two registry systems for extensibility:

Maps schema types to React components:

<!-- doc-snippet: fragment — MyWidgetComponent is a placeholder for the reader's own component; the block shows the register() call's metadata argument, not a runnable module -->
```tsx
import { ComponentRegistry } from '@object-ui/core'

Expand All@@ -197,6 +211,7 @@ ComponentRegistry.register('my-widget', MyWidgetComponent, {

Maps field types to input components:

<!-- doc-snippet: fragment — RatingFieldComponent is a placeholder for the reader's own field renderer -->
```tsx
import { registerFieldRenderer } from '@object-ui/fields'

Expand DownExpand Up@@ -274,6 +289,7 @@ ObjectUI uses **Tailwind CSS** exclusively for styling:

All component variants use `cva` for type-safe variants:

<!-- doc-snippet: fragment — quotes how @object-ui/components declares its variants internally; class-variance-authority is that package's own dependency, not a module resolvable from the docs root -->
```tsx
import { cva } from 'class-variance-authority'

Expand All@@ -299,6 +315,7 @@ const buttonVariants = cva(

Use `cn()` helper (tailwind-merge + clsx) for class overrides:

<!-- doc-snippet: fragment — a one-line usage excerpt; '@/lib/utils' is the reader's app path alias and Button and props come from the surrounding component -->
```tsx
import { cn } from '@/lib/utils'

Expand All@@ -319,11 +336,15 @@ ObjectUI is built with **TypeScript** in strict mode:
```typescript
import type { ComponentSchema, ButtonSchema } from '@object-ui/types'

function handleClick() {
// ...
}

const schema: ButtonSchema = {
type: 'button',
text: 'Click me',
variant: 'default', // ✅ Type-checked
onClick: 'handleClick'
onClick: handleClick, // ✅ a handler, not its name — onClick is () => void | Promise<void>
}
```

Expand All@@ -345,6 +366,7 @@ Heavy dependencies only go in plugins:

Don't import components directly - use registries:

<!-- doc-snippet: fragment — a good/bad contrast pair mixing an import, bare JSX and a bare schema literal — three separate excerpts in one block, none a module -->
```tsx
// ❌ Bad
import { MyGrid } from './MyGrid'
Expand All@@ -359,6 +381,7 @@ ComponentRegistry.register('my-grid', MyGrid)

Never use inline styles or CSS-in-JS:

<!-- doc-snippet: fragment — a good/bad contrast pair of two unclosed div openings, quoted to compare the style attribute with a Tailwind class -->
```tsx
// ❌ Bad
<div style={{ backgroundColor: 'red' }}>
Expand All@@ -371,6 +394,7 @@ Never use inline styles or CSS-in-JS:

Use expressions for dynamic content:

<!-- doc-snippet: fragment — a good/bad contrast pair of two bare schema object literals -->
```tsx
// ❌ Bad - hardcoded
{ type: 'text', value: 'Hello, John!' }
Expand All@@ -389,6 +413,7 @@ When creating a plugin:
4. Add documentation in `content/docs/plugins/`
5. Add to plugins meta.json

<!-- doc-snippet: fragment — the reader's new plugin package index.tsx; './MyWidget' is the sibling source file in that package -->
```typescript
// packages/plugin-mywidget/src/index.tsx
import { ComponentRegistry } from '@object-ui/core'
Expand Down
14 changes: 13 additions & 1 deletion content/docs/guide/building-crud-app.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -36,6 +36,7 @@ pnpm add -D tailwindcss @tailwindcss/vite

Add Tailwind to your `vite.config.ts`:

<!-- doc-snippet: fragment — a vite.config.ts for the app the reader is scaffolding; '@vitejs/plugin-react' and '@tailwindcss/vite' are that app's devDependencies, not this repo's -->
```ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
Expand DownExpand Up@@ -163,7 +164,10 @@ export class RestDataSource implements DataSource {
const query = new URLSearchParams();
if (params?.$top) query.set('$top', String(params.$top));
if (params?.$skip) query.set('$skip', String(params.$skip));
if (params?.$orderby) query.set('$orderby', params.$orderby);
// `$orderby` is a union — an OData clause string, a map, or an array of
// fields. This backend speaks the string form, so narrow to it rather
// than stringifying a shape the server cannot parse.
if (typeof params?.$orderby === 'string') query.set('$orderby', params.$orderby);
if (params?.$search) query.set('$search', params.$search);
const res = await fetch(`${this.baseUrl}/${resource}?${query}`);
const data = await res.json();
Expand DownExpand Up@@ -208,6 +212,7 @@ Wire everything together in `src/App.tsx`. `SchemaRendererProvider` injects the
data source once, and every `SchemaRenderer` beneath it renders its schema
against that one adapter:

<!-- doc-snippet: fragment — the reader's src/App.tsx; './setup' and './data/rest-data-source' are the project files created in Steps 2 and 4 -->
```tsx
import './setup';
import { SchemaRenderer, SchemaRendererProvider } from '@object-ui/react';
Expand DownExpand Up@@ -259,6 +264,7 @@ resolved** panel naming itself and the object it was about to read.

ObjectUI generates forms directly from your schema. Extend `App.tsx` with form state:

<!-- doc-snippet: fragment — two lines to paste into the App component of Step 5 — the useState import and the surrounding function body are already there -->
```tsx
const [showForm, setShowForm] = useState(false);
const [editId, setEditId] = useState<string | null>(null);
Expand All@@ -269,6 +275,7 @@ Add a "New Task" button and handle row clicks to open the edit form:
Both of these render inside the `SchemaRendererProvider` from Step 5, so neither
carries a data source of its own:

<!-- doc-snippet: fragment — JSX to place inside the Step 5 App component; showForm, editId and their setters are the state declared in the block above -->
```tsx
<SchemaRenderer
schema={{ type: 'object-grid', objectName: 'task' }}
Expand DownExpand Up@@ -314,6 +321,7 @@ it declaratively, with the spec's per-element `dataSource` binding
(fetched through your data source's `getObjectSchema` / `listViews`) and
composes that view's `filter` and `sort` onto the query for you:

<!-- doc-snippet: fragment — an excerpt mixing a state declaration with the JSX it drives, to be placed inside the App component; it is not a standalone module -->
```tsx
const [activeView, setActiveView] = useState('all');

Expand DownExpand Up@@ -352,6 +360,8 @@ server decides.
Create a detail page that renders a single record with all its fields:

```tsx
import { SchemaRenderer } from '@object-ui/react';

function TaskDetail({ taskId, onBack }: { taskId: string; onBack: () => void }) {
return (
<div className="min-h-screen bg-background p-6">
Expand DownExpand Up@@ -390,6 +400,7 @@ Use this component in your main app with simple routing state, or integrate with

**Environment config** — Keep your API URL configurable:

<!-- doc-snippet: fragment — continues Step 4 — RestDataSource is the class defined there, and import.meta.env is Vite's typing in the reader's own app -->
```ts
const dataSource = new RestDataSource(
import.meta.env.VITE_API_URL || 'http://localhost:3000/api'
Expand All@@ -402,6 +413,7 @@ const dataSource = new RestDataSource(

**Authentication** — Extend `RestDataSource` to inject auth headers:

<!-- doc-snippet: fragment — extends the RestDataSource class defined in Step 4 -->
```ts
class AuthenticatedDataSource extends RestDataSource {
constructor(baseUrl: string, private getToken: () => string) {
Expand Down
10 changes: 10 additions & 0 deletions content/docs/guide/plugins.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -203,6 +203,7 @@ Kanban board component with drag-and-drop powered by @dnd-kit.

Plugins use React's `lazy()` and `Suspense` to load heavy dependencies on-demand:

<!-- doc-snippet: fragment — excerpt of a plugin package's own source; './MonacoImpl' is a sibling file in the reader's package, not a module resolvable from this repo -->
```typescript
// The plugin structure
import React, { Suspense } from 'react'
Expand DownExpand Up@@ -248,6 +249,7 @@ Without lazy loading, all this code would be in your main bundle!

Plugins automatically register their components when imported:

<!-- doc-snippet: fragment — continues the block above — CodeEditorRenderer is defined there, and this line is the tail of the same plugin index.tsx -->
```typescript
// In the plugin's index.tsx
import { ComponentRegistry } from '@object-ui/core'
Expand DownExpand Up@@ -284,6 +286,7 @@ cd packages/plugin-myfeature

### 2. Create Heavy Implementation

<!-- doc-snippet: fragment — the reader's new package importing its own heavy dependency; 'heavy-library' is a placeholder name, not an installed module -->
```typescript
// src/MyFeatureImpl.tsx
import HeavyLibrary from 'heavy-library'
Expand All@@ -295,6 +298,7 @@ export default function MyFeatureImpl(props) {

### 3. Create Lazy Wrapper

<!-- doc-snippet: fragment — the reader's new src/index.tsx; './MyFeatureImpl' is the sibling file created in the previous step -->
```typescript
// src/index.tsx
import React, { Suspense } from 'react'
Expand DownExpand Up@@ -334,6 +338,7 @@ export interface MyFeatureSchema extends BaseSchema {

### 5. Configure Build

<!-- doc-snippet: fragment — a vite.config.ts for the reader's plugin package; '@vitejs/plugin-react' is that package's devDependency, not this repo's -->
```typescript
// vite.config.ts
import { defineConfig } from 'vite'
Expand DownExpand Up@@ -420,6 +425,7 @@ Heavy imports go in the `*Impl.tsx` file.

Always show a meaningful skeleton while loading:

<!-- doc-snippet: fragment — a bare JSX excerpt showing the Suspense wrapper shape; Suspense, Skeleton, LazyComponent and props all come from the surrounding component -->
```typescript
<Suspense fallback={
<Skeleton className="w-full h-[400px]" />
Expand All@@ -432,6 +438,7 @@ Always show a meaningful skeleton while loading:

Make your plugin type-safe:

<!-- doc-snippet: fragment — re-export excerpt from the reader's package; './types' is the file created in step 4 -->
```typescript
export type { MyFeatureSchema } from './types'
```
Expand DownExpand Up@@ -474,6 +481,7 @@ ls -lh dist/

Check that you imported it in your app:

<!-- doc-snippet: fragment — the app-side import of '@object-ui/plugin-myfeature', the package this guide teaches the reader to publish -->
```typescript
import '@object-ui/plugin-myfeature'
```
Expand All@@ -482,6 +490,7 @@ import '@object-ui/plugin-myfeature'

Make sure types are exported:

<!-- doc-snippet: fragment — re-export from '@object-ui/plugin-myfeature', the reader's own published package -->
```typescript
export type { MyFeatureSchema } from '@object-ui/plugin-myfeature'
```
Expand All@@ -499,6 +508,7 @@ Check that the implementation is in a separate file:

Check that ComponentRegistry.register() is called at the module level:

<!-- doc-snippet: fragment — a good/bad contrast pair; ComponentRegistry and MyFeatureRenderer are the ambient names of the plugin index.tsx being discussed -->
```typescript
// ✅ Good - runs on import
ComponentRegistry.register('my-feature', MyFeatureRenderer)
Expand Down
7 changes: 7 additions & 0 deletions content/docs/rfcs/0001-clipboard-paste.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -162,6 +162,7 @@ Key rules:

### 5.1 Parser (`@object-ui/core/clipboard`)

<!-- doc-snippet: fragment — RFC signature excerpt — parseClipboard is declared without a body because this section proposes the module's shape, not its implementation -->
```ts
export interface ParsedClipboard {
/** 2D string matrix, rows × cells, never null */
Expand All@@ -188,6 +189,7 @@ Parser handles:

### 5.2 Coercer (`@object-ui/core/clipboard`)

<!-- doc-snippet: fragment — RFC signature excerpt — coerceCell is declared without a body; this section proposes the coercer surface, and nothing implements it yet -->
```ts
export type CoercerType =
| 'text' | 'number' | 'integer' | 'currency' | 'percent'
Expand DownExpand Up@@ -240,6 +242,7 @@ Coercion details per type (v1):

### 5.3 React Hook (`@object-ui/fields/clipboard`)

<!-- doc-snippet: fragment — RFC signature excerpt — usePasteToGrid is declared without a body, and ColumnCoercer / CellRange are the types proposed in the sections above -->
```ts
export interface UsePasteToGridOptions {
/** Columns currently visible / pasteable, in visual order */
Expand DownExpand Up@@ -287,6 +290,7 @@ export function usePasteToGrid(opts: UsePasteToGridOptions): UsePasteToGridResul

### 5.4 Preview dialog component

<!-- doc-snippet: fragment — a JSX usage sketch whose handler bodies are elided with '...'; it shows the proposed dialog's props, not runnable code -->
```tsx
<PastePreviewDialog
open
Expand DownExpand Up@@ -358,6 +362,7 @@ quick-paste is opt-in via `usePasteToGrid({ preview: 'auto' })`.

Hosts expose paste behind a flag so apps can opt-in per grid:

<!-- doc-snippet: fragment — a JSX sketch with the remaining ObjectGrid props elided as '...'; the point is the features key, not a complete element -->
```tsx
<ObjectGrid
features={{ clipboardPaste: 'preview' }} // 'off' | 'preview' | 'auto'
Expand All@@ -373,6 +378,7 @@ stable release cycle the default becomes `'preview'`.

### 7.1 ObjectGrid (master, staged)

<!-- doc-snippet: fragment — proposed host wiring — the hook, coercersFromObjectSchema, applyCommands and ObjectGridImpl are all names this RFC is proposing, and the element is elided with '...' -->
```tsx
const { onPaste, previewDialog } = usePasteToGrid({
columns: coercersFromObjectSchema(schema),
Expand DownExpand Up@@ -410,6 +416,7 @@ return (

### 7.2 EditableGridField (child, staged)

<!-- doc-snippet: fragment — proposed host wiring for EditableGridField; usePasteToGrid, coercersFromGridFieldColumns and applyCommands are proposed names, and field/value/onChange are the component's own props -->
```tsx
const { onPaste, previewDialog } = usePasteToGrid({
columns: coercersFromGridFieldColumns(field.columns),
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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
31 changes: 28 additions & 3 deletions content/docs/guide/architecture.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -140,10 +140,21 @@ The `SchemaRenderer` component:

```tsx
import { SchemaRenderer } from '@object-ui/react'
import type { BaseSchema } from '@object-ui/types'

// The schema from step 1, as the object the renderer receives.
const schema: BaseSchema = {
type: 'card',
title: 'Welcome',
body: {
type: 'text',
value: 'Hello, ${user.name}!',
},
}

function App() {
const data = { user: { name: "Alice" } }
const data = { user: { name: 'Alice' } }

return <SchemaRenderer schema={schema} data={data} />
}
```
Expand All@@ -152,6 +163,7 @@ function App() {

The registry maps type strings to React components:

<!-- doc-snippet: fragment — registry excerpt — CardComponent and TextComponent are placeholder names for the reader's own components, and ComponentRegistry is imported where it is used further down the page -->
```typescript
// During app initialization
ComponentRegistry.register('card', CardComponent)
Expand All@@ -165,6 +177,7 @@ const Component = ComponentRegistry.get('card') // → CardComponent

The registered component renders with evaluated props:

<!-- doc-snippet: fragment — the JSX the registry produces for step 1's schema; CardComponent and TextComponent are the placeholder components registered in the block above -->
```tsx
<CardComponent title="Welcome">
<TextComponent value="Hello, Alice!" />
Expand All@@ -179,6 +192,7 @@ ObjectUI uses two registry systems for extensibility:

Maps schema types to React components:

<!-- doc-snippet: fragment — MyWidgetComponent is a placeholder for the reader's own component; the block shows the register() call's metadata argument, not a runnable module -->
```tsx
import { ComponentRegistry } from '@object-ui/core'

Expand All@@ -197,6 +211,7 @@ ComponentRegistry.register('my-widget', MyWidgetComponent, {

Maps field types to input components:

<!-- doc-snippet: fragment — RatingFieldComponent is a placeholder for the reader's own field renderer -->
```tsx
import { registerFieldRenderer } from '@object-ui/fields'

Expand DownExpand Up@@ -274,6 +289,7 @@ ObjectUI uses **Tailwind CSS** exclusively for styling:

All component variants use `cva` for type-safe variants:

<!-- doc-snippet: fragment — quotes how @object-ui/components declares its variants internally; class-variance-authority is that package's own dependency, not a module resolvable from the docs root -->
```tsx
import { cva } from 'class-variance-authority'

Expand All@@ -299,6 +315,7 @@ const buttonVariants = cva(

Use `cn()` helper (tailwind-merge + clsx) for class overrides:

<!-- doc-snippet: fragment — a one-line usage excerpt; '@/lib/utils' is the reader's app path alias and Button and props come from the surrounding component -->
```tsx
import { cn } from '@/lib/utils'

Expand All@@ -319,11 +336,15 @@ ObjectUI is built with **TypeScript** in strict mode:
```typescript
import type { ComponentSchema, ButtonSchema } from '@object-ui/types'

function handleClick() {
// ...
}

const schema: ButtonSchema = {
type: 'button',
text: 'Click me',
variant: 'default', // ✅ Type-checked
onClick: 'handleClick'
onClick: handleClick, // ✅ a handler, not its name — onClick is () => void | Promise<void>
}
```

Expand All@@ -345,6 +366,7 @@ Heavy dependencies only go in plugins:

Don't import components directly - use registries:

<!-- doc-snippet: fragment — a good/bad contrast pair mixing an import, bare JSX and a bare schema literal — three separate excerpts in one block, none a module -->
```tsx
// ❌ Bad
import { MyGrid } from './MyGrid'
Expand All@@ -359,6 +381,7 @@ ComponentRegistry.register('my-grid', MyGrid)

Never use inline styles or CSS-in-JS:

<!-- doc-snippet: fragment — a good/bad contrast pair of two unclosed div openings, quoted to compare the style attribute with a Tailwind class -->
```tsx
// ❌ Bad
<div style={{ backgroundColor: 'red' }}>
Expand All@@ -371,6 +394,7 @@ Never use inline styles or CSS-in-JS:

Use expressions for dynamic content:

<!-- doc-snippet: fragment — a good/bad contrast pair of two bare schema object literals -->
```tsx
// ❌ Bad - hardcoded
{ type: 'text', value: 'Hello, John!' }
Expand All@@ -389,6 +413,7 @@ When creating a plugin:
4. Add documentation in `content/docs/plugins/`
5. Add to plugins meta.json

<!-- doc-snippet: fragment — the reader's new plugin package index.tsx; './MyWidget' is the sibling source file in that package -->
```typescript
// packages/plugin-mywidget/src/index.tsx
import { ComponentRegistry } from '@object-ui/core'
Expand Down
14 changes: 13 additions & 1 deletion content/docs/guide/building-crud-app.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -36,6 +36,7 @@ pnpm add -D tailwindcss @tailwindcss/vite

Add Tailwind to your `vite.config.ts`:

<!-- doc-snippet: fragment — a vite.config.ts for the app the reader is scaffolding; '@vitejs/plugin-react' and '@tailwindcss/vite' are that app's devDependencies, not this repo's -->
```ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
Expand DownExpand Up@@ -163,7 +164,10 @@ export class RestDataSource implements DataSource {
const query = new URLSearchParams();
if (params?.$top) query.set('$top', String(params.$top));
if (params?.$skip) query.set('$skip', String(params.$skip));
if (params?.$orderby) query.set('$orderby', params.$orderby);
// `$orderby` is a union — an OData clause string, a map, or an array of
// fields. This backend speaks the string form, so narrow to it rather
// than stringifying a shape the server cannot parse.
if (typeof params?.$orderby === 'string') query.set('$orderby', params.$orderby);
if (params?.$search) query.set('$search', params.$search);
const res = await fetch(`${this.baseUrl}/${resource}?${query}`);
const data = await res.json();
Expand DownExpand Up@@ -208,6 +212,7 @@ Wire everything together in `src/App.tsx`. `SchemaRendererProvider` injects the
data source once, and every `SchemaRenderer` beneath it renders its schema
against that one adapter:

<!-- doc-snippet: fragment — the reader's src/App.tsx; './setup' and './data/rest-data-source' are the project files created in Steps 2 and 4 -->
```tsx
import './setup';
import { SchemaRenderer, SchemaRendererProvider } from '@object-ui/react';
Expand DownExpand Up@@ -259,6 +264,7 @@ resolved** panel naming itself and the object it was about to read.

ObjectUI generates forms directly from your schema. Extend `App.tsx` with form state:

<!-- doc-snippet: fragment — two lines to paste into the App component of Step 5 — the useState import and the surrounding function body are already there -->
```tsx
const [showForm, setShowForm] = useState(false);
const [editId, setEditId] = useState<string | null>(null);
Expand All@@ -269,6 +275,7 @@ Add a "New Task" button and handle row clicks to open the edit form:
Both of these render inside the `SchemaRendererProvider` from Step 5, so neither
carries a data source of its own:

<!-- doc-snippet: fragment — JSX to place inside the Step 5 App component; showForm, editId and their setters are the state declared in the block above -->
```tsx
<SchemaRenderer
schema={{ type: 'object-grid', objectName: 'task' }}
Expand DownExpand Up@@ -314,6 +321,7 @@ it declaratively, with the spec's per-element `dataSource` binding
(fetched through your data source's `getObjectSchema` / `listViews`) and
composes that view's `filter` and `sort` onto the query for you:

<!-- doc-snippet: fragment — an excerpt mixing a state declaration with the JSX it drives, to be placed inside the App component; it is not a standalone module -->
```tsx
const [activeView, setActiveView] = useState('all');

Expand DownExpand Up@@ -352,6 +360,8 @@ server decides.
Create a detail page that renders a single record with all its fields:

```tsx
import { SchemaRenderer } from '@object-ui/react';

function TaskDetail({ taskId, onBack }: { taskId: string; onBack: () => void }) {
return (
<div className="min-h-screen bg-background p-6">
Expand DownExpand Up@@ -390,6 +400,7 @@ Use this component in your main app with simple routing state, or integrate with

**Environment config** — Keep your API URL configurable:

<!-- doc-snippet: fragment — continues Step 4 — RestDataSource is the class defined there, and import.meta.env is Vite's typing in the reader's own app -->
```ts
const dataSource = new RestDataSource(
import.meta.env.VITE_API_URL || 'http://localhost:3000/api'
Expand All@@ -402,6 +413,7 @@ const dataSource = new RestDataSource(

**Authentication** — Extend `RestDataSource` to inject auth headers:

<!-- doc-snippet: fragment — extends the RestDataSource class defined in Step 4 -->
```ts
class AuthenticatedDataSource extends RestDataSource {
constructor(baseUrl: string, private getToken: () => string) {
Expand Down
10 changes: 10 additions & 0 deletions content/docs/guide/plugins.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -203,6 +203,7 @@ Kanban board component with drag-and-drop powered by @dnd-kit.

Plugins use React's `lazy()` and `Suspense` to load heavy dependencies on-demand:

<!-- doc-snippet: fragment — excerpt of a plugin package's own source; './MonacoImpl' is a sibling file in the reader's package, not a module resolvable from this repo -->
```typescript
// The plugin structure
import React, { Suspense } from 'react'
Expand DownExpand Up@@ -248,6 +249,7 @@ Without lazy loading, all this code would be in your main bundle!

Plugins automatically register their components when imported:

<!-- doc-snippet: fragment — continues the block above — CodeEditorRenderer is defined there, and this line is the tail of the same plugin index.tsx -->
```typescript
// In the plugin's index.tsx
import { ComponentRegistry } from '@object-ui/core'
Expand DownExpand Up@@ -284,6 +286,7 @@ cd packages/plugin-myfeature

### 2. Create Heavy Implementation

<!-- doc-snippet: fragment — the reader's new package importing its own heavy dependency; 'heavy-library' is a placeholder name, not an installed module -->
```typescript
// src/MyFeatureImpl.tsx
import HeavyLibrary from 'heavy-library'
Expand All@@ -295,6 +298,7 @@ export default function MyFeatureImpl(props) {

### 3. Create Lazy Wrapper

<!-- doc-snippet: fragment — the reader's new src/index.tsx; './MyFeatureImpl' is the sibling file created in the previous step -->
```typescript
// src/index.tsx
import React, { Suspense } from 'react'
Expand DownExpand Up@@ -334,6 +338,7 @@ export interface MyFeatureSchema extends BaseSchema {

### 5. Configure Build

<!-- doc-snippet: fragment — a vite.config.ts for the reader's plugin package; '@vitejs/plugin-react' is that package's devDependency, not this repo's -->
```typescript
// vite.config.ts
import { defineConfig } from 'vite'
Expand DownExpand Up@@ -420,6 +425,7 @@ Heavy imports go in the `*Impl.tsx` file.

Always show a meaningful skeleton while loading:

<!-- doc-snippet: fragment — a bare JSX excerpt showing the Suspense wrapper shape; Suspense, Skeleton, LazyComponent and props all come from the surrounding component -->
```typescript
<Suspense fallback={
<Skeleton className="w-full h-[400px]" />
Expand All@@ -432,6 +438,7 @@ Always show a meaningful skeleton while loading:

Make your plugin type-safe:

<!-- doc-snippet: fragment — re-export excerpt from the reader's package; './types' is the file created in step 4 -->
```typescript
export type { MyFeatureSchema } from './types'
```
Expand DownExpand Up@@ -474,6 +481,7 @@ ls -lh dist/

Check that you imported it in your app:

<!-- doc-snippet: fragment — the app-side import of '@object-ui/plugin-myfeature', the package this guide teaches the reader to publish -->
```typescript
import '@object-ui/plugin-myfeature'
```
Expand All@@ -482,6 +490,7 @@ import '@object-ui/plugin-myfeature'

Make sure types are exported:

<!-- doc-snippet: fragment — re-export from '@object-ui/plugin-myfeature', the reader's own published package -->
```typescript
export type { MyFeatureSchema } from '@object-ui/plugin-myfeature'
```
Expand All@@ -499,6 +508,7 @@ Check that the implementation is in a separate file:

Check that ComponentRegistry.register() is called at the module level:

<!-- doc-snippet: fragment — a good/bad contrast pair; ComponentRegistry and MyFeatureRenderer are the ambient names of the plugin index.tsx being discussed -->
```typescript
// ✅ Good - runs on import
ComponentRegistry.register('my-feature', MyFeatureRenderer)
Expand Down
7 changes: 7 additions & 0 deletions content/docs/rfcs/0001-clipboard-paste.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -162,6 +162,7 @@ Key rules:

### 5.1 Parser (`@object-ui/core/clipboard`)

<!-- doc-snippet: fragment — RFC signature excerpt — parseClipboard is declared without a body because this section proposes the module's shape, not its implementation -->
```ts
export interface ParsedClipboard {
/** 2D string matrix, rows × cells, never null */
Expand All@@ -188,6 +189,7 @@ Parser handles:

### 5.2 Coercer (`@object-ui/core/clipboard`)

<!-- doc-snippet: fragment — RFC signature excerpt — coerceCell is declared without a body; this section proposes the coercer surface, and nothing implements it yet -->
```ts
export type CoercerType =
| 'text' | 'number' | 'integer' | 'currency' | 'percent'
Expand DownExpand Up@@ -240,6 +242,7 @@ Coercion details per type (v1):

### 5.3 React Hook (`@object-ui/fields/clipboard`)

<!-- doc-snippet: fragment — RFC signature excerpt — usePasteToGrid is declared without a body, and ColumnCoercer / CellRange are the types proposed in the sections above -->
```ts
export interface UsePasteToGridOptions {
/** Columns currently visible / pasteable, in visual order */
Expand DownExpand Up@@ -287,6 +290,7 @@ export function usePasteToGrid(opts: UsePasteToGridOptions): UsePasteToGridResul

### 5.4 Preview dialog component

<!-- doc-snippet: fragment — a JSX usage sketch whose handler bodies are elided with '...'; it shows the proposed dialog's props, not runnable code -->
```tsx
<PastePreviewDialog
open
Expand DownExpand Up@@ -358,6 +362,7 @@ quick-paste is opt-in via `usePasteToGrid({ preview: 'auto' })`.

Hosts expose paste behind a flag so apps can opt-in per grid:

<!-- doc-snippet: fragment — a JSX sketch with the remaining ObjectGrid props elided as '...'; the point is the features key, not a complete element -->
```tsx
<ObjectGrid
features={{ clipboardPaste: 'preview' }} // 'off' | 'preview' | 'auto'
Expand All@@ -373,6 +378,7 @@ stable release cycle the default becomes `'preview'`.

### 7.1 ObjectGrid (master, staged)

<!-- doc-snippet: fragment — proposed host wiring — the hook, coercersFromObjectSchema, applyCommands and ObjectGridImpl are all names this RFC is proposing, and the element is elided with '...' -->
```tsx
const { onPaste, previewDialog } = usePasteToGrid({
columns: coercersFromObjectSchema(schema),
Expand DownExpand Up@@ -410,6 +416,7 @@ return (

### 7.2 EditableGridField (child, staged)

<!-- doc-snippet: fragment — proposed host wiring for EditableGridField; usePasteToGrid, coercersFromGridFieldColumns and applyCommands are proposed names, and field/value/onChange are the component's own props -->
```tsx
const { onPaste, previewDialog } = usePasteToGrid({
columns: coercersFromGridFieldColumns(field.columns),
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
31 changes: 28 additions & 3 deletions content/docs/guide/architecture.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -140,10 +140,21 @@ The `SchemaRenderer` component:

```tsx
import { SchemaRenderer } from '@object-ui/react'
import type { BaseSchema } from '@object-ui/types'

// The schema from step 1, as the object the renderer receives.
const schema: BaseSchema = {
type: 'card',
title: 'Welcome',
body: {
type: 'text',
value: 'Hello, ${user.name}!',
},
}

function App() {
const data = { user: { name: "Alice" } }
const data = { user: { name: 'Alice' } }

return <SchemaRenderer schema={schema} data={data} />
}
```
Expand All@@ -152,6 +163,7 @@ function App() {

The registry maps type strings to React components:

<!-- doc-snippet: fragment — registry excerpt — CardComponent and TextComponent are placeholder names for the reader's own components, and ComponentRegistry is imported where it is used further down the page -->
```typescript
// During app initialization
ComponentRegistry.register('card', CardComponent)
Expand All@@ -165,6 +177,7 @@ const Component = ComponentRegistry.get('card') // → CardComponent

The registered component renders with evaluated props:

<!-- doc-snippet: fragment — the JSX the registry produces for step 1's schema; CardComponent and TextComponent are the placeholder components registered in the block above -->
```tsx
<CardComponent title="Welcome">
<TextComponent value="Hello, Alice!" />
Expand All@@ -179,6 +192,7 @@ ObjectUI uses two registry systems for extensibility:

Maps schema types to React components:

<!-- doc-snippet: fragment — MyWidgetComponent is a placeholder for the reader's own component; the block shows the register() call's metadata argument, not a runnable module -->
```tsx
import { ComponentRegistry } from '@object-ui/core'

Expand All@@ -197,6 +211,7 @@ ComponentRegistry.register('my-widget', MyWidgetComponent, {

Maps field types to input components:

<!-- doc-snippet: fragment — RatingFieldComponent is a placeholder for the reader's own field renderer -->
```tsx
import { registerFieldRenderer } from '@object-ui/fields'

Expand DownExpand Up@@ -274,6 +289,7 @@ ObjectUI uses **Tailwind CSS** exclusively for styling:

All component variants use `cva` for type-safe variants:

<!-- doc-snippet: fragment — quotes how @object-ui/components declares its variants internally; class-variance-authority is that package's own dependency, not a module resolvable from the docs root -->
```tsx
import { cva } from 'class-variance-authority'

Expand All@@ -299,6 +315,7 @@ const buttonVariants = cva(

Use `cn()` helper (tailwind-merge + clsx) for class overrides:

<!-- doc-snippet: fragment — a one-line usage excerpt; '@/lib/utils' is the reader's app path alias and Button and props come from the surrounding component -->
```tsx
import { cn } from '@/lib/utils'

Expand All@@ -319,11 +336,15 @@ ObjectUI is built with **TypeScript** in strict mode:
```typescript
import type { ComponentSchema, ButtonSchema } from '@object-ui/types'

function handleClick() {
// ...
}

const schema: ButtonSchema = {
type: 'button',
text: 'Click me',
variant: 'default', // ✅ Type-checked
onClick: 'handleClick'
onClick: handleClick, // ✅ a handler, not its name — onClick is () => void | Promise<void>
}
```

Expand All@@ -345,6 +366,7 @@ Heavy dependencies only go in plugins:

Don't import components directly - use registries:

<!-- doc-snippet: fragment — a good/bad contrast pair mixing an import, bare JSX and a bare schema literal — three separate excerpts in one block, none a module -->
```tsx
// ❌ Bad
import { MyGrid } from './MyGrid'
Expand All@@ -359,6 +381,7 @@ ComponentRegistry.register('my-grid', MyGrid)

Never use inline styles or CSS-in-JS:

<!-- doc-snippet: fragment — a good/bad contrast pair of two unclosed div openings, quoted to compare the style attribute with a Tailwind class -->
```tsx
// ❌ Bad
<div style={{ backgroundColor: 'red' }}>
Expand All@@ -371,6 +394,7 @@ Never use inline styles or CSS-in-JS:

Use expressions for dynamic content:

<!-- doc-snippet: fragment — a good/bad contrast pair of two bare schema object literals -->
```tsx
// ❌ Bad - hardcoded
{ type: 'text', value: 'Hello, John!' }
Expand All@@ -389,6 +413,7 @@ When creating a plugin:
4. Add documentation in `content/docs/plugins/`
5. Add to plugins meta.json

<!-- doc-snippet: fragment — the reader's new plugin package index.tsx; './MyWidget' is the sibling source file in that package -->
```typescript
// packages/plugin-mywidget/src/index.tsx
import { ComponentRegistry } from '@object-ui/core'
Expand Down
14 changes: 13 additions & 1 deletion content/docs/guide/building-crud-app.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -36,6 +36,7 @@ pnpm add -D tailwindcss @tailwindcss/vite

Add Tailwind to your `vite.config.ts`:

<!-- doc-snippet: fragment — a vite.config.ts for the app the reader is scaffolding; '@vitejs/plugin-react' and '@tailwindcss/vite' are that app's devDependencies, not this repo's -->
```ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
Expand DownExpand Up@@ -163,7 +164,10 @@ export class RestDataSource implements DataSource {
const query = new URLSearchParams();
if (params?.$top) query.set('$top', String(params.$top));
if (params?.$skip) query.set('$skip', String(params.$skip));
if (params?.$orderby) query.set('$orderby', params.$orderby);
// `$orderby` is a union — an OData clause string, a map, or an array of
// fields. This backend speaks the string form, so narrow to it rather
// than stringifying a shape the server cannot parse.
if (typeof params?.$orderby === 'string') query.set('$orderby', params.$orderby);
if (params?.$search) query.set('$search', params.$search);
const res = await fetch(`${this.baseUrl}/${resource}?${query}`);
const data = await res.json();
Expand DownExpand Up@@ -208,6 +212,7 @@ Wire everything together in `src/App.tsx`. `SchemaRendererProvider` injects the
data source once, and every `SchemaRenderer` beneath it renders its schema
against that one adapter:

<!-- doc-snippet: fragment — the reader's src/App.tsx; './setup' and './data/rest-data-source' are the project files created in Steps 2 and 4 -->
```tsx
import './setup';
import { SchemaRenderer, SchemaRendererProvider } from '@object-ui/react';
Expand DownExpand Up@@ -259,6 +264,7 @@ resolved** panel naming itself and the object it was about to read.

ObjectUI generates forms directly from your schema. Extend `App.tsx` with form state:

<!-- doc-snippet: fragment — two lines to paste into the App component of Step 5 — the useState import and the surrounding function body are already there -->
```tsx
const [showForm, setShowForm] = useState(false);
const [editId, setEditId] = useState<string | null>(null);
Expand All@@ -269,6 +275,7 @@ Add a "New Task" button and handle row clicks to open the edit form:
Both of these render inside the `SchemaRendererProvider` from Step 5, so neither
carries a data source of its own:

<!-- doc-snippet: fragment — JSX to place inside the Step 5 App component; showForm, editId and their setters are the state declared in the block above -->
```tsx
<SchemaRenderer
schema={{ type: 'object-grid', objectName: 'task' }}
Expand DownExpand Up@@ -314,6 +321,7 @@ it declaratively, with the spec's per-element `dataSource` binding
(fetched through your data source's `getObjectSchema` / `listViews`) and
composes that view's `filter` and `sort` onto the query for you:

<!-- doc-snippet: fragment — an excerpt mixing a state declaration with the JSX it drives, to be placed inside the App component; it is not a standalone module -->
```tsx
const [activeView, setActiveView] = useState('all');

Expand DownExpand Up@@ -352,6 +360,8 @@ server decides.
Create a detail page that renders a single record with all its fields:

```tsx
import { SchemaRenderer } from '@object-ui/react';

function TaskDetail({ taskId, onBack }: { taskId: string; onBack: () => void }) {
return (
<div className="min-h-screen bg-background p-6">
Expand DownExpand Up@@ -390,6 +400,7 @@ Use this component in your main app with simple routing state, or integrate with

**Environment config** — Keep your API URL configurable:

<!-- doc-snippet: fragment — continues Step 4 — RestDataSource is the class defined there, and import.meta.env is Vite's typing in the reader's own app -->
```ts
const dataSource = new RestDataSource(
import.meta.env.VITE_API_URL || 'http://localhost:3000/api'
Expand All@@ -402,6 +413,7 @@ const dataSource = new RestDataSource(

**Authentication** — Extend `RestDataSource` to inject auth headers:

<!-- doc-snippet: fragment — extends the RestDataSource class defined in Step 4 -->
```ts
class AuthenticatedDataSource extends RestDataSource {
constructor(baseUrl: string, private getToken: () => string) {
Expand Down
10 changes: 10 additions & 0 deletions content/docs/guide/plugins.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -203,6 +203,7 @@ Kanban board component with drag-and-drop powered by @dnd-kit.

Plugins use React's `lazy()` and `Suspense` to load heavy dependencies on-demand:

<!-- doc-snippet: fragment — excerpt of a plugin package's own source; './MonacoImpl' is a sibling file in the reader's package, not a module resolvable from this repo -->
```typescript
// The plugin structure
import React, { Suspense } from 'react'
Expand DownExpand Up@@ -248,6 +249,7 @@ Without lazy loading, all this code would be in your main bundle!

Plugins automatically register their components when imported:

<!-- doc-snippet: fragment — continues the block above — CodeEditorRenderer is defined there, and this line is the tail of the same plugin index.tsx -->
```typescript
// In the plugin's index.tsx
import { ComponentRegistry } from '@object-ui/core'
Expand DownExpand Up@@ -284,6 +286,7 @@ cd packages/plugin-myfeature

### 2. Create Heavy Implementation

<!-- doc-snippet: fragment — the reader's new package importing its own heavy dependency; 'heavy-library' is a placeholder name, not an installed module -->
```typescript
// src/MyFeatureImpl.tsx
import HeavyLibrary from 'heavy-library'
Expand All@@ -295,6 +298,7 @@ export default function MyFeatureImpl(props) {

### 3. Create Lazy Wrapper

<!-- doc-snippet: fragment — the reader's new src/index.tsx; './MyFeatureImpl' is the sibling file created in the previous step -->
```typescript
// src/index.tsx
import React, { Suspense } from 'react'
Expand DownExpand Up@@ -334,6 +338,7 @@ export interface MyFeatureSchema extends BaseSchema {

### 5. Configure Build

<!-- doc-snippet: fragment — a vite.config.ts for the reader's plugin package; '@vitejs/plugin-react' is that package's devDependency, not this repo's -->
```typescript
// vite.config.ts
import { defineConfig } from 'vite'
Expand DownExpand Up@@ -420,6 +425,7 @@ Heavy imports go in the `*Impl.tsx` file.

Always show a meaningful skeleton while loading:

<!-- doc-snippet: fragment — a bare JSX excerpt showing the Suspense wrapper shape; Suspense, Skeleton, LazyComponent and props all come from the surrounding component -->
```typescript
<Suspense fallback={
<Skeleton className="w-full h-[400px]" />
Expand All@@ -432,6 +438,7 @@ Always show a meaningful skeleton while loading:

Make your plugin type-safe:

<!-- doc-snippet: fragment — re-export excerpt from the reader's package; './types' is the file created in step 4 -->
```typescript
export type { MyFeatureSchema } from './types'
```
Expand DownExpand Up@@ -474,6 +481,7 @@ ls -lh dist/

Check that you imported it in your app:

<!-- doc-snippet: fragment — the app-side import of '@object-ui/plugin-myfeature', the package this guide teaches the reader to publish -->
```typescript
import '@object-ui/plugin-myfeature'
```
Expand All@@ -482,6 +490,7 @@ import '@object-ui/plugin-myfeature'

Make sure types are exported:

<!-- doc-snippet: fragment — re-export from '@object-ui/plugin-myfeature', the reader's own published package -->
```typescript
export type { MyFeatureSchema } from '@object-ui/plugin-myfeature'
```
Expand All@@ -499,6 +508,7 @@ Check that the implementation is in a separate file:

Check that ComponentRegistry.register() is called at the module level:

<!-- doc-snippet: fragment — a good/bad contrast pair; ComponentRegistry and MyFeatureRenderer are the ambient names of the plugin index.tsx being discussed -->
```typescript
// ✅ Good - runs on import
ComponentRegistry.register('my-feature', MyFeatureRenderer)
Expand Down
7 changes: 7 additions & 0 deletions content/docs/rfcs/0001-clipboard-paste.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -162,6 +162,7 @@ Key rules:

### 5.1 Parser (`@object-ui/core/clipboard`)

<!-- doc-snippet: fragment — RFC signature excerpt — parseClipboard is declared without a body because this section proposes the module's shape, not its implementation -->
```ts
export interface ParsedClipboard {
/** 2D string matrix, rows × cells, never null */
Expand All@@ -188,6 +189,7 @@ Parser handles:

### 5.2 Coercer (`@object-ui/core/clipboard`)

<!-- doc-snippet: fragment — RFC signature excerpt — coerceCell is declared without a body; this section proposes the coercer surface, and nothing implements it yet -->
```ts
export type CoercerType =
| 'text' | 'number' | 'integer' | 'currency' | 'percent'
Expand DownExpand Up@@ -240,6 +242,7 @@ Coercion details per type (v1):

### 5.3 React Hook (`@object-ui/fields/clipboard`)

<!-- doc-snippet: fragment — RFC signature excerpt — usePasteToGrid is declared without a body, and ColumnCoercer / CellRange are the types proposed in the sections above -->
```ts
export interface UsePasteToGridOptions {
/** Columns currently visible / pasteable, in visual order */
Expand DownExpand Up@@ -287,6 +290,7 @@ export function usePasteToGrid(opts: UsePasteToGridOptions): UsePasteToGridResul

### 5.4 Preview dialog component

<!-- doc-snippet: fragment — a JSX usage sketch whose handler bodies are elided with '...'; it shows the proposed dialog's props, not runnable code -->
```tsx
<PastePreviewDialog
open
Expand DownExpand Up@@ -358,6 +362,7 @@ quick-paste is opt-in via `usePasteToGrid({ preview: 'auto' })`.

Hosts expose paste behind a flag so apps can opt-in per grid:

<!-- doc-snippet: fragment — a JSX sketch with the remaining ObjectGrid props elided as '...'; the point is the features key, not a complete element -->
```tsx
<ObjectGrid
features={{ clipboardPaste: 'preview' }} // 'off' | 'preview' | 'auto'
Expand All@@ -373,6 +378,7 @@ stable release cycle the default becomes `'preview'`.

### 7.1 ObjectGrid (master, staged)

<!-- doc-snippet: fragment — proposed host wiring — the hook, coercersFromObjectSchema, applyCommands and ObjectGridImpl are all names this RFC is proposing, and the element is elided with '...' -->
```tsx
const { onPaste, previewDialog } = usePasteToGrid({
columns: coercersFromObjectSchema(schema),
Expand DownExpand Up@@ -410,6 +416,7 @@ return (

### 7.2 EditableGridField (child, staged)

<!-- doc-snippet: fragment — proposed host wiring for EditableGridField; usePasteToGrid, coercersFromGridFieldColumns and applyCommands are proposed names, and field/value/onChange are the component's own props -->
```tsx
const { onPaste, previewDialog } = usePasteToGrid({
columns: coercersFromGridFieldColumns(field.columns),
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
31 changes: 28 additions & 3 deletions content/docs/guide/architecture.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -140,10 +140,21 @@ The `SchemaRenderer` component:

```tsx
import { SchemaRenderer } from '@object-ui/react'
import type { BaseSchema } from '@object-ui/types'

// The schema from step 1, as the object the renderer receives.
const schema: BaseSchema = {
type: 'card',
title: 'Welcome',
body: {
type: 'text',
value: 'Hello, ${user.name}!',
},
}

function App() {
const data = { user: { name: "Alice" } }
const data = { user: { name: 'Alice' } }

return <SchemaRenderer schema={schema} data={data} />
}
```
Expand All@@ -152,6 +163,7 @@ function App() {

The registry maps type strings to React components:

<!-- doc-snippet: fragment — registry excerpt — CardComponent and TextComponent are placeholder names for the reader's own components, and ComponentRegistry is imported where it is used further down the page -->
```typescript
// During app initialization
ComponentRegistry.register('card', CardComponent)
Expand All@@ -165,6 +177,7 @@ const Component = ComponentRegistry.get('card') // → CardComponent

The registered component renders with evaluated props:

<!-- doc-snippet: fragment — the JSX the registry produces for step 1's schema; CardComponent and TextComponent are the placeholder components registered in the block above -->
```tsx
<CardComponent title="Welcome">
<TextComponent value="Hello, Alice!" />
Expand All@@ -179,6 +192,7 @@ ObjectUI uses two registry systems for extensibility:

Maps schema types to React components:

<!-- doc-snippet: fragment — MyWidgetComponent is a placeholder for the reader's own component; the block shows the register() call's metadata argument, not a runnable module -->
```tsx
import { ComponentRegistry } from '@object-ui/core'

Expand All@@ -197,6 +211,7 @@ ComponentRegistry.register('my-widget', MyWidgetComponent, {

Maps field types to input components:

<!-- doc-snippet: fragment — RatingFieldComponent is a placeholder for the reader's own field renderer -->
```tsx
import { registerFieldRenderer } from '@object-ui/fields'

Expand DownExpand Up@@ -274,6 +289,7 @@ ObjectUI uses **Tailwind CSS** exclusively for styling:

All component variants use `cva` for type-safe variants:

<!-- doc-snippet: fragment — quotes how @object-ui/components declares its variants internally; class-variance-authority is that package's own dependency, not a module resolvable from the docs root -->
```tsx
import { cva } from 'class-variance-authority'

Expand All@@ -299,6 +315,7 @@ const buttonVariants = cva(

Use `cn()` helper (tailwind-merge + clsx) for class overrides:

<!-- doc-snippet: fragment — a one-line usage excerpt; '@/lib/utils' is the reader's app path alias and Button and props come from the surrounding component -->
```tsx
import { cn } from '@/lib/utils'

Expand All@@ -319,11 +336,15 @@ ObjectUI is built with **TypeScript** in strict mode:
```typescript
import type { ComponentSchema, ButtonSchema } from '@object-ui/types'

function handleClick() {
// ...
}

const schema: ButtonSchema = {
type: 'button',
text: 'Click me',
variant: 'default', // ✅ Type-checked
onClick: 'handleClick'
onClick: handleClick, // ✅ a handler, not its name — onClick is () => void | Promise<void>
}
```

Expand All@@ -345,6 +366,7 @@ Heavy dependencies only go in plugins:

Don't import components directly - use registries:

<!-- doc-snippet: fragment — a good/bad contrast pair mixing an import, bare JSX and a bare schema literal — three separate excerpts in one block, none a module -->
```tsx
// ❌ Bad
import { MyGrid } from './MyGrid'
Expand All@@ -359,6 +381,7 @@ ComponentRegistry.register('my-grid', MyGrid)

Never use inline styles or CSS-in-JS:

<!-- doc-snippet: fragment — a good/bad contrast pair of two unclosed div openings, quoted to compare the style attribute with a Tailwind class -->
```tsx
// ❌ Bad
<div style={{ backgroundColor: 'red' }}>
Expand All@@ -371,6 +394,7 @@ Never use inline styles or CSS-in-JS:

Use expressions for dynamic content:

<!-- doc-snippet: fragment — a good/bad contrast pair of two bare schema object literals -->
```tsx
// ❌ Bad - hardcoded
{ type: 'text', value: 'Hello, John!' }
Expand All@@ -389,6 +413,7 @@ When creating a plugin:
4. Add documentation in `content/docs/plugins/`
5. Add to plugins meta.json

<!-- doc-snippet: fragment — the reader's new plugin package index.tsx; './MyWidget' is the sibling source file in that package -->
```typescript
// packages/plugin-mywidget/src/index.tsx
import { ComponentRegistry } from '@object-ui/core'
Expand Down
14 changes: 13 additions & 1 deletion content/docs/guide/building-crud-app.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -36,6 +36,7 @@ pnpm add -D tailwindcss @tailwindcss/vite

Add Tailwind to your `vite.config.ts`:

<!-- doc-snippet: fragment — a vite.config.ts for the app the reader is scaffolding; '@vitejs/plugin-react' and '@tailwindcss/vite' are that app's devDependencies, not this repo's -->
```ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
Expand DownExpand Up@@ -163,7 +164,10 @@ export class RestDataSource implements DataSource {
const query = new URLSearchParams();
if (params?.$top) query.set('$top', String(params.$top));
if (params?.$skip) query.set('$skip', String(params.$skip));
if (params?.$orderby) query.set('$orderby', params.$orderby);
// `$orderby` is a union — an OData clause string, a map, or an array of
// fields. This backend speaks the string form, so narrow to it rather
// than stringifying a shape the server cannot parse.
if (typeof params?.$orderby === 'string') query.set('$orderby', params.$orderby);
if (params?.$search) query.set('$search', params.$search);
const res = await fetch(`${this.baseUrl}/${resource}?${query}`);
const data = await res.json();
Expand DownExpand Up@@ -208,6 +212,7 @@ Wire everything together in `src/App.tsx`. `SchemaRendererProvider` injects the
data source once, and every `SchemaRenderer` beneath it renders its schema
against that one adapter:

<!-- doc-snippet: fragment — the reader's src/App.tsx; './setup' and './data/rest-data-source' are the project files created in Steps 2 and 4 -->
```tsx
import './setup';
import { SchemaRenderer, SchemaRendererProvider } from '@object-ui/react';
Expand DownExpand Up@@ -259,6 +264,7 @@ resolved** panel naming itself and the object it was about to read.

ObjectUI generates forms directly from your schema. Extend `App.tsx` with form state:

<!-- doc-snippet: fragment — two lines to paste into the App component of Step 5 — the useState import and the surrounding function body are already there -->
```tsx
const [showForm, setShowForm] = useState(false);
const [editId, setEditId] = useState<string | null>(null);
Expand All@@ -269,6 +275,7 @@ Add a "New Task" button and handle row clicks to open the edit form:
Both of these render inside the `SchemaRendererProvider` from Step 5, so neither
carries a data source of its own:

<!-- doc-snippet: fragment — JSX to place inside the Step 5 App component; showForm, editId and their setters are the state declared in the block above -->
```tsx
<SchemaRenderer
schema={{ type: 'object-grid', objectName: 'task' }}
Expand DownExpand Up@@ -314,6 +321,7 @@ it declaratively, with the spec's per-element `dataSource` binding
(fetched through your data source's `getObjectSchema` / `listViews`) and
composes that view's `filter` and `sort` onto the query for you:

<!-- doc-snippet: fragment — an excerpt mixing a state declaration with the JSX it drives, to be placed inside the App component; it is not a standalone module -->
```tsx
const [activeView, setActiveView] = useState('all');

Expand DownExpand Up@@ -352,6 +360,8 @@ server decides.
Create a detail page that renders a single record with all its fields:

```tsx
import { SchemaRenderer } from '@object-ui/react';

function TaskDetail({ taskId, onBack }: { taskId: string; onBack: () => void }) {
return (
<div className="min-h-screen bg-background p-6">
Expand DownExpand Up@@ -390,6 +400,7 @@ Use this component in your main app with simple routing state, or integrate with

**Environment config** — Keep your API URL configurable:

<!-- doc-snippet: fragment — continues Step 4 — RestDataSource is the class defined there, and import.meta.env is Vite's typing in the reader's own app -->
```ts
const dataSource = new RestDataSource(
import.meta.env.VITE_API_URL || 'http://localhost:3000/api'
Expand All@@ -402,6 +413,7 @@ const dataSource = new RestDataSource(

**Authentication** — Extend `RestDataSource` to inject auth headers:

<!-- doc-snippet: fragment — extends the RestDataSource class defined in Step 4 -->
```ts
class AuthenticatedDataSource extends RestDataSource {
constructor(baseUrl: string, private getToken: () => string) {
Expand Down
10 changes: 10 additions & 0 deletions content/docs/guide/plugins.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -203,6 +203,7 @@ Kanban board component with drag-and-drop powered by @dnd-kit.

Plugins use React's `lazy()` and `Suspense` to load heavy dependencies on-demand:

<!-- doc-snippet: fragment — excerpt of a plugin package's own source; './MonacoImpl' is a sibling file in the reader's package, not a module resolvable from this repo -->
```typescript
// The plugin structure
import React, { Suspense } from 'react'
Expand DownExpand Up@@ -248,6 +249,7 @@ Without lazy loading, all this code would be in your main bundle!

Plugins automatically register their components when imported:

<!-- doc-snippet: fragment — continues the block above — CodeEditorRenderer is defined there, and this line is the tail of the same plugin index.tsx -->
```typescript
// In the plugin's index.tsx
import { ComponentRegistry } from '@object-ui/core'
Expand DownExpand Up@@ -284,6 +286,7 @@ cd packages/plugin-myfeature

### 2. Create Heavy Implementation

<!-- doc-snippet: fragment — the reader's new package importing its own heavy dependency; 'heavy-library' is a placeholder name, not an installed module -->
```typescript
// src/MyFeatureImpl.tsx
import HeavyLibrary from 'heavy-library'
Expand All@@ -295,6 +298,7 @@ export default function MyFeatureImpl(props) {

### 3. Create Lazy Wrapper

<!-- doc-snippet: fragment — the reader's new src/index.tsx; './MyFeatureImpl' is the sibling file created in the previous step -->
```typescript
// src/index.tsx
import React, { Suspense } from 'react'
Expand DownExpand Up@@ -334,6 +338,7 @@ export interface MyFeatureSchema extends BaseSchema {

### 5. Configure Build

<!-- doc-snippet: fragment — a vite.config.ts for the reader's plugin package; '@vitejs/plugin-react' is that package's devDependency, not this repo's -->
```typescript
// vite.config.ts
import { defineConfig } from 'vite'
Expand DownExpand Up@@ -420,6 +425,7 @@ Heavy imports go in the `*Impl.tsx` file.

Always show a meaningful skeleton while loading:

<!-- doc-snippet: fragment — a bare JSX excerpt showing the Suspense wrapper shape; Suspense, Skeleton, LazyComponent and props all come from the surrounding component -->
```typescript
<Suspense fallback={
<Skeleton className="w-full h-[400px]" />
Expand All@@ -432,6 +438,7 @@ Always show a meaningful skeleton while loading:

Make your plugin type-safe:

<!-- doc-snippet: fragment — re-export excerpt from the reader's package; './types' is the file created in step 4 -->
```typescript
export type { MyFeatureSchema } from './types'
```
Expand DownExpand Up@@ -474,6 +481,7 @@ ls -lh dist/

Check that you imported it in your app:

<!-- doc-snippet: fragment — the app-side import of '@object-ui/plugin-myfeature', the package this guide teaches the reader to publish -->
```typescript
import '@object-ui/plugin-myfeature'
```
Expand All@@ -482,6 +490,7 @@ import '@object-ui/plugin-myfeature'

Make sure types are exported:

<!-- doc-snippet: fragment — re-export from '@object-ui/plugin-myfeature', the reader's own published package -->
```typescript
export type { MyFeatureSchema } from '@object-ui/plugin-myfeature'
```
Expand All@@ -499,6 +508,7 @@ Check that the implementation is in a separate file:

Check that ComponentRegistry.register() is called at the module level:

<!-- doc-snippet: fragment — a good/bad contrast pair; ComponentRegistry and MyFeatureRenderer are the ambient names of the plugin index.tsx being discussed -->
```typescript
// ✅ Good - runs on import
ComponentRegistry.register('my-feature', MyFeatureRenderer)
Expand Down
7 changes: 7 additions & 0 deletions content/docs/rfcs/0001-clipboard-paste.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -162,6 +162,7 @@ Key rules:

### 5.1 Parser (`@object-ui/core/clipboard`)

<!-- doc-snippet: fragment — RFC signature excerpt — parseClipboard is declared without a body because this section proposes the module's shape, not its implementation -->
```ts
export interface ParsedClipboard {
/** 2D string matrix, rows × cells, never null */
Expand All@@ -188,6 +189,7 @@ Parser handles:

### 5.2 Coercer (`@object-ui/core/clipboard`)

<!-- doc-snippet: fragment — RFC signature excerpt — coerceCell is declared without a body; this section proposes the coercer surface, and nothing implements it yet -->
```ts
export type CoercerType =
| 'text' | 'number' | 'integer' | 'currency' | 'percent'
Expand DownExpand Up@@ -240,6 +242,7 @@ Coercion details per type (v1):

### 5.3 React Hook (`@object-ui/fields/clipboard`)

<!-- doc-snippet: fragment — RFC signature excerpt — usePasteToGrid is declared without a body, and ColumnCoercer / CellRange are the types proposed in the sections above -->
```ts
export interface UsePasteToGridOptions {
/** Columns currently visible / pasteable, in visual order */
Expand DownExpand Up@@ -287,6 +290,7 @@ export function usePasteToGrid(opts: UsePasteToGridOptions): UsePasteToGridResul

### 5.4 Preview dialog component

<!-- doc-snippet: fragment — a JSX usage sketch whose handler bodies are elided with '...'; it shows the proposed dialog's props, not runnable code -->
```tsx
<PastePreviewDialog
open
Expand DownExpand Up@@ -358,6 +362,7 @@ quick-paste is opt-in via `usePasteToGrid({ preview: 'auto' })`.

Hosts expose paste behind a flag so apps can opt-in per grid:

<!-- doc-snippet: fragment — a JSX sketch with the remaining ObjectGrid props elided as '...'; the point is the features key, not a complete element -->
```tsx
<ObjectGrid
features={{ clipboardPaste: 'preview' }} // 'off' | 'preview' | 'auto'
Expand All@@ -373,6 +378,7 @@ stable release cycle the default becomes `'preview'`.

### 7.1 ObjectGrid (master, staged)

<!-- doc-snippet: fragment — proposed host wiring — the hook, coercersFromObjectSchema, applyCommands and ObjectGridImpl are all names this RFC is proposing, and the element is elided with '...' -->
```tsx
const { onPaste, previewDialog } = usePasteToGrid({
columns: coercersFromObjectSchema(schema),
Expand DownExpand Up@@ -410,6 +416,7 @@ return (

### 7.2 EditableGridField (child, staged)

<!-- doc-snippet: fragment — proposed host wiring for EditableGridField; usePasteToGrid, coercersFromGridFieldColumns and applyCommands are proposed names, and field/value/onChange are the component's own props -->
```tsx
const { onPaste, previewDialog } = usePasteToGrid({
columns: coercersFromGridFieldColumns(field.columns),
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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
31 changes: 28 additions & 3 deletions content/docs/guide/architecture.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -140,10 +140,21 @@ The `SchemaRenderer` component:

```tsx
import { SchemaRenderer } from '@object-ui/react'
import type { BaseSchema } from '@object-ui/types'

// The schema from step 1, as the object the renderer receives.
const schema: BaseSchema = {
type: 'card',
title: 'Welcome',
body: {
type: 'text',
value: 'Hello, ${user.name}!',
},
}

function App() {
const data = { user: { name: "Alice" } }
const data = { user: { name: 'Alice' } }

return <SchemaRenderer schema={schema} data={data} />
}
```
Expand All@@ -152,6 +163,7 @@ function App() {

The registry maps type strings to React components:

<!-- doc-snippet: fragment — registry excerpt — CardComponent and TextComponent are placeholder names for the reader's own components, and ComponentRegistry is imported where it is used further down the page -->
```typescript
// During app initialization
ComponentRegistry.register('card', CardComponent)
Expand All@@ -165,6 +177,7 @@ const Component = ComponentRegistry.get('card') // → CardComponent

The registered component renders with evaluated props:

<!-- doc-snippet: fragment — the JSX the registry produces for step 1's schema; CardComponent and TextComponent are the placeholder components registered in the block above -->
```tsx
<CardComponent title="Welcome">
<TextComponent value="Hello, Alice!" />
Expand All@@ -179,6 +192,7 @@ ObjectUI uses two registry systems for extensibility:

Maps schema types to React components:

<!-- doc-snippet: fragment — MyWidgetComponent is a placeholder for the reader's own component; the block shows the register() call's metadata argument, not a runnable module -->
```tsx
import { ComponentRegistry } from '@object-ui/core'

Expand All@@ -197,6 +211,7 @@ ComponentRegistry.register('my-widget', MyWidgetComponent, {

Maps field types to input components:

<!-- doc-snippet: fragment — RatingFieldComponent is a placeholder for the reader's own field renderer -->
```tsx
import { registerFieldRenderer } from '@object-ui/fields'

Expand DownExpand Up@@ -274,6 +289,7 @@ ObjectUI uses **Tailwind CSS** exclusively for styling:

All component variants use `cva` for type-safe variants:

<!-- doc-snippet: fragment — quotes how @object-ui/components declares its variants internally; class-variance-authority is that package's own dependency, not a module resolvable from the docs root -->
```tsx
import { cva } from 'class-variance-authority'

Expand All@@ -299,6 +315,7 @@ const buttonVariants = cva(

Use `cn()` helper (tailwind-merge + clsx) for class overrides:

<!-- doc-snippet: fragment — a one-line usage excerpt; '@/lib/utils' is the reader's app path alias and Button and props come from the surrounding component -->
```tsx
import { cn } from '@/lib/utils'

Expand All@@ -319,11 +336,15 @@ ObjectUI is built with **TypeScript** in strict mode:
```typescript
import type { ComponentSchema, ButtonSchema } from '@object-ui/types'

function handleClick() {
// ...
}

const schema: ButtonSchema = {
type: 'button',
text: 'Click me',
variant: 'default', // ✅ Type-checked
onClick: 'handleClick'
onClick: handleClick, // ✅ a handler, not its name — onClick is () => void | Promise<void>
}
```

Expand All@@ -345,6 +366,7 @@ Heavy dependencies only go in plugins:

Don't import components directly - use registries:

<!-- doc-snippet: fragment — a good/bad contrast pair mixing an import, bare JSX and a bare schema literal — three separate excerpts in one block, none a module -->
```tsx
// ❌ Bad
import { MyGrid } from './MyGrid'
Expand All@@ -359,6 +381,7 @@ ComponentRegistry.register('my-grid', MyGrid)

Never use inline styles or CSS-in-JS:

<!-- doc-snippet: fragment — a good/bad contrast pair of two unclosed div openings, quoted to compare the style attribute with a Tailwind class -->
```tsx
// ❌ Bad
<div style={{ backgroundColor: 'red' }}>
Expand All@@ -371,6 +394,7 @@ Never use inline styles or CSS-in-JS:

Use expressions for dynamic content:

<!-- doc-snippet: fragment — a good/bad contrast pair of two bare schema object literals -->
```tsx
// ❌ Bad - hardcoded
{ type: 'text', value: 'Hello, John!' }
Expand All@@ -389,6 +413,7 @@ When creating a plugin:
4. Add documentation in `content/docs/plugins/`
5. Add to plugins meta.json

<!-- doc-snippet: fragment — the reader's new plugin package index.tsx; './MyWidget' is the sibling source file in that package -->
```typescript
// packages/plugin-mywidget/src/index.tsx
import { ComponentRegistry } from '@object-ui/core'
Expand Down
14 changes: 13 additions & 1 deletion content/docs/guide/building-crud-app.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -36,6 +36,7 @@ pnpm add -D tailwindcss @tailwindcss/vite

Add Tailwind to your `vite.config.ts`:

<!-- doc-snippet: fragment — a vite.config.ts for the app the reader is scaffolding; '@vitejs/plugin-react' and '@tailwindcss/vite' are that app's devDependencies, not this repo's -->
```ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
Expand DownExpand Up@@ -163,7 +164,10 @@ export class RestDataSource implements DataSource {
const query = new URLSearchParams();
if (params?.$top) query.set('$top', String(params.$top));
if (params?.$skip) query.set('$skip', String(params.$skip));
if (params?.$orderby) query.set('$orderby', params.$orderby);
// `$orderby` is a union — an OData clause string, a map, or an array of
// fields. This backend speaks the string form, so narrow to it rather
// than stringifying a shape the server cannot parse.
if (typeof params?.$orderby === 'string') query.set('$orderby', params.$orderby);
if (params?.$search) query.set('$search', params.$search);
const res = await fetch(`${this.baseUrl}/${resource}?${query}`);
const data = await res.json();
Expand DownExpand Up@@ -208,6 +212,7 @@ Wire everything together in `src/App.tsx`. `SchemaRendererProvider` injects the
data source once, and every `SchemaRenderer` beneath it renders its schema
against that one adapter:

<!-- doc-snippet: fragment — the reader's src/App.tsx; './setup' and './data/rest-data-source' are the project files created in Steps 2 and 4 -->
```tsx
import './setup';
import { SchemaRenderer, SchemaRendererProvider } from '@object-ui/react';
Expand DownExpand Up@@ -259,6 +264,7 @@ resolved** panel naming itself and the object it was about to read.

ObjectUI generates forms directly from your schema. Extend `App.tsx` with form state:

<!-- doc-snippet: fragment — two lines to paste into the App component of Step 5 — the useState import and the surrounding function body are already there -->
```tsx
const [showForm, setShowForm] = useState(false);
const [editId, setEditId] = useState<string | null>(null);
Expand All@@ -269,6 +275,7 @@ Add a "New Task" button and handle row clicks to open the edit form:
Both of these render inside the `SchemaRendererProvider` from Step 5, so neither
carries a data source of its own:

<!-- doc-snippet: fragment — JSX to place inside the Step 5 App component; showForm, editId and their setters are the state declared in the block above -->
```tsx
<SchemaRenderer
schema={{ type: 'object-grid', objectName: 'task' }}
Expand DownExpand Up@@ -314,6 +321,7 @@ it declaratively, with the spec's per-element `dataSource` binding
(fetched through your data source's `getObjectSchema` / `listViews`) and
composes that view's `filter` and `sort` onto the query for you:

<!-- doc-snippet: fragment — an excerpt mixing a state declaration with the JSX it drives, to be placed inside the App component; it is not a standalone module -->
```tsx
const [activeView, setActiveView] = useState('all');

Expand DownExpand Up@@ -352,6 +360,8 @@ server decides.
Create a detail page that renders a single record with all its fields:

```tsx
import { SchemaRenderer } from '@object-ui/react';

function TaskDetail({ taskId, onBack }: { taskId: string; onBack: () => void }) {
return (
<div className="min-h-screen bg-background p-6">
Expand DownExpand Up@@ -390,6 +400,7 @@ Use this component in your main app with simple routing state, or integrate with

**Environment config** — Keep your API URL configurable:

<!-- doc-snippet: fragment — continues Step 4 — RestDataSource is the class defined there, and import.meta.env is Vite's typing in the reader's own app -->
```ts
const dataSource = new RestDataSource(
import.meta.env.VITE_API_URL || 'http://localhost:3000/api'
Expand All@@ -402,6 +413,7 @@ const dataSource = new RestDataSource(

**Authentication** — Extend `RestDataSource` to inject auth headers:

<!-- doc-snippet: fragment — extends the RestDataSource class defined in Step 4 -->
```ts
class AuthenticatedDataSource extends RestDataSource {
constructor(baseUrl: string, private getToken: () => string) {
Expand Down
10 changes: 10 additions & 0 deletions content/docs/guide/plugins.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -203,6 +203,7 @@ Kanban board component with drag-and-drop powered by @dnd-kit.

Plugins use React's `lazy()` and `Suspense` to load heavy dependencies on-demand:

<!-- doc-snippet: fragment — excerpt of a plugin package's own source; './MonacoImpl' is a sibling file in the reader's package, not a module resolvable from this repo -->
```typescript
// The plugin structure
import React, { Suspense } from 'react'
Expand DownExpand Up@@ -248,6 +249,7 @@ Without lazy loading, all this code would be in your main bundle!

Plugins automatically register their components when imported:

<!-- doc-snippet: fragment — continues the block above — CodeEditorRenderer is defined there, and this line is the tail of the same plugin index.tsx -->
```typescript
// In the plugin's index.tsx
import { ComponentRegistry } from '@object-ui/core'
Expand DownExpand Up@@ -284,6 +286,7 @@ cd packages/plugin-myfeature

### 2. Create Heavy Implementation

<!-- doc-snippet: fragment — the reader's new package importing its own heavy dependency; 'heavy-library' is a placeholder name, not an installed module -->
```typescript
// src/MyFeatureImpl.tsx
import HeavyLibrary from 'heavy-library'
Expand All@@ -295,6 +298,7 @@ export default function MyFeatureImpl(props) {

### 3. Create Lazy Wrapper

<!-- doc-snippet: fragment — the reader's new src/index.tsx; './MyFeatureImpl' is the sibling file created in the previous step -->
```typescript
// src/index.tsx
import React, { Suspense } from 'react'
Expand DownExpand Up@@ -334,6 +338,7 @@ export interface MyFeatureSchema extends BaseSchema {

### 5. Configure Build

<!-- doc-snippet: fragment — a vite.config.ts for the reader's plugin package; '@vitejs/plugin-react' is that package's devDependency, not this repo's -->
```typescript
// vite.config.ts
import { defineConfig } from 'vite'
Expand DownExpand Up@@ -420,6 +425,7 @@ Heavy imports go in the `*Impl.tsx` file.

Always show a meaningful skeleton while loading:

<!-- doc-snippet: fragment — a bare JSX excerpt showing the Suspense wrapper shape; Suspense, Skeleton, LazyComponent and props all come from the surrounding component -->
```typescript
<Suspense fallback={
<Skeleton className="w-full h-[400px]" />
Expand All@@ -432,6 +438,7 @@ Always show a meaningful skeleton while loading:

Make your plugin type-safe:

<!-- doc-snippet: fragment — re-export excerpt from the reader's package; './types' is the file created in step 4 -->
```typescript
export type { MyFeatureSchema } from './types'
```
Expand DownExpand Up@@ -474,6 +481,7 @@ ls -lh dist/

Check that you imported it in your app:

<!-- doc-snippet: fragment — the app-side import of '@object-ui/plugin-myfeature', the package this guide teaches the reader to publish -->
```typescript
import '@object-ui/plugin-myfeature'
```
Expand All@@ -482,6 +490,7 @@ import '@object-ui/plugin-myfeature'

Make sure types are exported:

<!-- doc-snippet: fragment — re-export from '@object-ui/plugin-myfeature', the reader's own published package -->
```typescript
export type { MyFeatureSchema } from '@object-ui/plugin-myfeature'
```
Expand All@@ -499,6 +508,7 @@ Check that the implementation is in a separate file:

Check that ComponentRegistry.register() is called at the module level:

<!-- doc-snippet: fragment — a good/bad contrast pair; ComponentRegistry and MyFeatureRenderer are the ambient names of the plugin index.tsx being discussed -->
```typescript
// ✅ Good - runs on import
ComponentRegistry.register('my-feature', MyFeatureRenderer)
Expand Down
7 changes: 7 additions & 0 deletions content/docs/rfcs/0001-clipboard-paste.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -162,6 +162,7 @@ Key rules:

### 5.1 Parser (`@object-ui/core/clipboard`)

<!-- doc-snippet: fragment — RFC signature excerpt — parseClipboard is declared without a body because this section proposes the module's shape, not its implementation -->
```ts
export interface ParsedClipboard {
/** 2D string matrix, rows × cells, never null */
Expand All@@ -188,6 +189,7 @@ Parser handles:

### 5.2 Coercer (`@object-ui/core/clipboard`)

<!-- doc-snippet: fragment — RFC signature excerpt — coerceCell is declared without a body; this section proposes the coercer surface, and nothing implements it yet -->
```ts
export type CoercerType =
| 'text' | 'number' | 'integer' | 'currency' | 'percent'
Expand DownExpand Up@@ -240,6 +242,7 @@ Coercion details per type (v1):

### 5.3 React Hook (`@object-ui/fields/clipboard`)

<!-- doc-snippet: fragment — RFC signature excerpt — usePasteToGrid is declared without a body, and ColumnCoercer / CellRange are the types proposed in the sections above -->
```ts
export interface UsePasteToGridOptions {
/** Columns currently visible / pasteable, in visual order */
Expand DownExpand Up@@ -287,6 +290,7 @@ export function usePasteToGrid(opts: UsePasteToGridOptions): UsePasteToGridResul

### 5.4 Preview dialog component

<!-- doc-snippet: fragment — a JSX usage sketch whose handler bodies are elided with '...'; it shows the proposed dialog's props, not runnable code -->
```tsx
<PastePreviewDialog
open
Expand DownExpand Up@@ -358,6 +362,7 @@ quick-paste is opt-in via `usePasteToGrid({ preview: 'auto' })`.

Hosts expose paste behind a flag so apps can opt-in per grid:

<!-- doc-snippet: fragment — a JSX sketch with the remaining ObjectGrid props elided as '...'; the point is the features key, not a complete element -->
```tsx
<ObjectGrid
features={{ clipboardPaste: 'preview' }} // 'off' | 'preview' | 'auto'
Expand All@@ -373,6 +378,7 @@ stable release cycle the default becomes `'preview'`.

### 7.1 ObjectGrid (master, staged)

<!-- doc-snippet: fragment — proposed host wiring — the hook, coercersFromObjectSchema, applyCommands and ObjectGridImpl are all names this RFC is proposing, and the element is elided with '...' -->
```tsx
const { onPaste, previewDialog } = usePasteToGrid({
columns: coercersFromObjectSchema(schema),
Expand DownExpand Up@@ -410,6 +416,7 @@ return (

### 7.2 EditableGridField (child, staged)

<!-- doc-snippet: fragment — proposed host wiring for EditableGridField; usePasteToGrid, coercersFromGridFieldColumns and applyCommands are proposed names, and field/value/onChange are the component's own props -->
```tsx
const { onPaste, previewDialog } = usePasteToGrid({
columns: coercersFromGridFieldColumns(field.columns),
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
31 changes: 28 additions & 3 deletions content/docs/guide/architecture.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -140,10 +140,21 @@ The `SchemaRenderer` component:

```tsx
import { SchemaRenderer } from '@object-ui/react'
import type { BaseSchema } from '@object-ui/types'

// The schema from step 1, as the object the renderer receives.
const schema: BaseSchema = {
type: 'card',
title: 'Welcome',
body: {
type: 'text',
value: 'Hello, ${user.name}!',
},
}

function App() {
const data = { user: { name: "Alice" } }
const data = { user: { name: 'Alice' } }

return <SchemaRenderer schema={schema} data={data} />
}
```
Expand All@@ -152,6 +163,7 @@ function App() {

The registry maps type strings to React components:

<!-- doc-snippet: fragment — registry excerpt — CardComponent and TextComponent are placeholder names for the reader's own components, and ComponentRegistry is imported where it is used further down the page -->
```typescript
// During app initialization
ComponentRegistry.register('card', CardComponent)
Expand All@@ -165,6 +177,7 @@ const Component = ComponentRegistry.get('card') // → CardComponent

The registered component renders with evaluated props:

<!-- doc-snippet: fragment — the JSX the registry produces for step 1's schema; CardComponent and TextComponent are the placeholder components registered in the block above -->
```tsx
<CardComponent title="Welcome">
<TextComponent value="Hello, Alice!" />
Expand All@@ -179,6 +192,7 @@ ObjectUI uses two registry systems for extensibility:

Maps schema types to React components:

<!-- doc-snippet: fragment — MyWidgetComponent is a placeholder for the reader's own component; the block shows the register() call's metadata argument, not a runnable module -->
```tsx
import { ComponentRegistry } from '@object-ui/core'

Expand All@@ -197,6 +211,7 @@ ComponentRegistry.register('my-widget', MyWidgetComponent, {

Maps field types to input components:

<!-- doc-snippet: fragment — RatingFieldComponent is a placeholder for the reader's own field renderer -->
```tsx
import { registerFieldRenderer } from '@object-ui/fields'

Expand DownExpand Up@@ -274,6 +289,7 @@ ObjectUI uses **Tailwind CSS** exclusively for styling:

All component variants use `cva` for type-safe variants:

<!-- doc-snippet: fragment — quotes how @object-ui/components declares its variants internally; class-variance-authority is that package's own dependency, not a module resolvable from the docs root -->
```tsx
import { cva } from 'class-variance-authority'

Expand All@@ -299,6 +315,7 @@ const buttonVariants = cva(

Use `cn()` helper (tailwind-merge + clsx) for class overrides:

<!-- doc-snippet: fragment — a one-line usage excerpt; '@/lib/utils' is the reader's app path alias and Button and props come from the surrounding component -->
```tsx
import { cn } from '@/lib/utils'

Expand All@@ -319,11 +336,15 @@ ObjectUI is built with **TypeScript** in strict mode:
```typescript
import type { ComponentSchema, ButtonSchema } from '@object-ui/types'

function handleClick() {
// ...
}

const schema: ButtonSchema = {
type: 'button',
text: 'Click me',
variant: 'default', // ✅ Type-checked
onClick: 'handleClick'
onClick: handleClick, // ✅ a handler, not its name — onClick is () => void | Promise<void>
}
```

Expand All@@ -345,6 +366,7 @@ Heavy dependencies only go in plugins:

Don't import components directly - use registries:

<!-- doc-snippet: fragment — a good/bad contrast pair mixing an import, bare JSX and a bare schema literal — three separate excerpts in one block, none a module -->
```tsx
// ❌ Bad
import { MyGrid } from './MyGrid'
Expand All@@ -359,6 +381,7 @@ ComponentRegistry.register('my-grid', MyGrid)

Never use inline styles or CSS-in-JS:

<!-- doc-snippet: fragment — a good/bad contrast pair of two unclosed div openings, quoted to compare the style attribute with a Tailwind class -->
```tsx
// ❌ Bad
<div style={{ backgroundColor: 'red' }}>
Expand All@@ -371,6 +394,7 @@ Never use inline styles or CSS-in-JS:

Use expressions for dynamic content:

<!-- doc-snippet: fragment — a good/bad contrast pair of two bare schema object literals -->
```tsx
// ❌ Bad - hardcoded
{ type: 'text', value: 'Hello, John!' }
Expand All@@ -389,6 +413,7 @@ When creating a plugin:
4. Add documentation in `content/docs/plugins/`
5. Add to plugins meta.json

<!-- doc-snippet: fragment — the reader's new plugin package index.tsx; './MyWidget' is the sibling source file in that package -->
```typescript
// packages/plugin-mywidget/src/index.tsx
import { ComponentRegistry } from '@object-ui/core'
Expand Down
14 changes: 13 additions & 1 deletion content/docs/guide/building-crud-app.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -36,6 +36,7 @@ pnpm add -D tailwindcss @tailwindcss/vite

Add Tailwind to your `vite.config.ts`:

<!-- doc-snippet: fragment — a vite.config.ts for the app the reader is scaffolding; '@vitejs/plugin-react' and '@tailwindcss/vite' are that app's devDependencies, not this repo's -->
```ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
Expand DownExpand Up@@ -163,7 +164,10 @@ export class RestDataSource implements DataSource {
const query = new URLSearchParams();
if (params?.$top) query.set('$top', String(params.$top));
if (params?.$skip) query.set('$skip', String(params.$skip));
if (params?.$orderby) query.set('$orderby', params.$orderby);
// `$orderby` is a union — an OData clause string, a map, or an array of
// fields. This backend speaks the string form, so narrow to it rather
// than stringifying a shape the server cannot parse.
if (typeof params?.$orderby === 'string') query.set('$orderby', params.$orderby);
if (params?.$search) query.set('$search', params.$search);
const res = await fetch(`${this.baseUrl}/${resource}?${query}`);
const data = await res.json();
Expand DownExpand Up@@ -208,6 +212,7 @@ Wire everything together in `src/App.tsx`. `SchemaRendererProvider` injects the
data source once, and every `SchemaRenderer` beneath it renders its schema
against that one adapter:

<!-- doc-snippet: fragment — the reader's src/App.tsx; './setup' and './data/rest-data-source' are the project files created in Steps 2 and 4 -->
```tsx
import './setup';
import { SchemaRenderer, SchemaRendererProvider } from '@object-ui/react';
Expand DownExpand Up@@ -259,6 +264,7 @@ resolved** panel naming itself and the object it was about to read.

ObjectUI generates forms directly from your schema. Extend `App.tsx` with form state:

<!-- doc-snippet: fragment — two lines to paste into the App component of Step 5 — the useState import and the surrounding function body are already there -->
```tsx
const [showForm, setShowForm] = useState(false);
const [editId, setEditId] = useState<string | null>(null);
Expand All@@ -269,6 +275,7 @@ Add a "New Task" button and handle row clicks to open the edit form:
Both of these render inside the `SchemaRendererProvider` from Step 5, so neither
carries a data source of its own:

<!-- doc-snippet: fragment — JSX to place inside the Step 5 App component; showForm, editId and their setters are the state declared in the block above -->
```tsx
<SchemaRenderer
schema={{ type: 'object-grid', objectName: 'task' }}
Expand DownExpand Up@@ -314,6 +321,7 @@ it declaratively, with the spec's per-element `dataSource` binding
(fetched through your data source's `getObjectSchema` / `listViews`) and
composes that view's `filter` and `sort` onto the query for you:

<!-- doc-snippet: fragment — an excerpt mixing a state declaration with the JSX it drives, to be placed inside the App component; it is not a standalone module -->
```tsx
const [activeView, setActiveView] = useState('all');

Expand DownExpand Up@@ -352,6 +360,8 @@ server decides.
Create a detail page that renders a single record with all its fields:

```tsx
import { SchemaRenderer } from '@object-ui/react';

function TaskDetail({ taskId, onBack }: { taskId: string; onBack: () => void }) {
return (
<div className="min-h-screen bg-background p-6">
Expand DownExpand Up@@ -390,6 +400,7 @@ Use this component in your main app with simple routing state, or integrate with

**Environment config** — Keep your API URL configurable:

<!-- doc-snippet: fragment — continues Step 4 — RestDataSource is the class defined there, and import.meta.env is Vite's typing in the reader's own app -->
```ts
const dataSource = new RestDataSource(
import.meta.env.VITE_API_URL || 'http://localhost:3000/api'
Expand All@@ -402,6 +413,7 @@ const dataSource = new RestDataSource(

**Authentication** — Extend `RestDataSource` to inject auth headers:

<!-- doc-snippet: fragment — extends the RestDataSource class defined in Step 4 -->
```ts
class AuthenticatedDataSource extends RestDataSource {
constructor(baseUrl: string, private getToken: () => string) {
Expand Down
10 changes: 10 additions & 0 deletions content/docs/guide/plugins.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -203,6 +203,7 @@ Kanban board component with drag-and-drop powered by @dnd-kit.

Plugins use React's `lazy()` and `Suspense` to load heavy dependencies on-demand:

<!-- doc-snippet: fragment — excerpt of a plugin package's own source; './MonacoImpl' is a sibling file in the reader's package, not a module resolvable from this repo -->
```typescript
// The plugin structure
import React, { Suspense } from 'react'
Expand DownExpand Up@@ -248,6 +249,7 @@ Without lazy loading, all this code would be in your main bundle!

Plugins automatically register their components when imported:

<!-- doc-snippet: fragment — continues the block above — CodeEditorRenderer is defined there, and this line is the tail of the same plugin index.tsx -->
```typescript
// In the plugin's index.tsx
import { ComponentRegistry } from '@object-ui/core'
Expand DownExpand Up@@ -284,6 +286,7 @@ cd packages/plugin-myfeature

### 2. Create Heavy Implementation

<!-- doc-snippet: fragment — the reader's new package importing its own heavy dependency; 'heavy-library' is a placeholder name, not an installed module -->
```typescript
// src/MyFeatureImpl.tsx
import HeavyLibrary from 'heavy-library'
Expand All@@ -295,6 +298,7 @@ export default function MyFeatureImpl(props) {

### 3. Create Lazy Wrapper

<!-- doc-snippet: fragment — the reader's new src/index.tsx; './MyFeatureImpl' is the sibling file created in the previous step -->
```typescript
// src/index.tsx
import React, { Suspense } from 'react'
Expand DownExpand Up@@ -334,6 +338,7 @@ export interface MyFeatureSchema extends BaseSchema {

### 5. Configure Build

<!-- doc-snippet: fragment — a vite.config.ts for the reader's plugin package; '@vitejs/plugin-react' is that package's devDependency, not this repo's -->
```typescript
// vite.config.ts
import { defineConfig } from 'vite'
Expand DownExpand Up@@ -420,6 +425,7 @@ Heavy imports go in the `*Impl.tsx` file.

Always show a meaningful skeleton while loading:

<!-- doc-snippet: fragment — a bare JSX excerpt showing the Suspense wrapper shape; Suspense, Skeleton, LazyComponent and props all come from the surrounding component -->
```typescript
<Suspense fallback={
<Skeleton className="w-full h-[400px]" />
Expand All@@ -432,6 +438,7 @@ Always show a meaningful skeleton while loading:

Make your plugin type-safe:

<!-- doc-snippet: fragment — re-export excerpt from the reader's package; './types' is the file created in step 4 -->
```typescript
export type { MyFeatureSchema } from './types'
```
Expand DownExpand Up@@ -474,6 +481,7 @@ ls -lh dist/

Check that you imported it in your app:

<!-- doc-snippet: fragment — the app-side import of '@object-ui/plugin-myfeature', the package this guide teaches the reader to publish -->
```typescript
import '@object-ui/plugin-myfeature'
```
Expand All@@ -482,6 +490,7 @@ import '@object-ui/plugin-myfeature'

Make sure types are exported:

<!-- doc-snippet: fragment — re-export from '@object-ui/plugin-myfeature', the reader's own published package -->
```typescript
export type { MyFeatureSchema } from '@object-ui/plugin-myfeature'
```
Expand All@@ -499,6 +508,7 @@ Check that the implementation is in a separate file:

Check that ComponentRegistry.register() is called at the module level:

<!-- doc-snippet: fragment — a good/bad contrast pair; ComponentRegistry and MyFeatureRenderer are the ambient names of the plugin index.tsx being discussed -->
```typescript
// ✅ Good - runs on import
ComponentRegistry.register('my-feature', MyFeatureRenderer)
Expand Down
7 changes: 7 additions & 0 deletions content/docs/rfcs/0001-clipboard-paste.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -162,6 +162,7 @@ Key rules:

### 5.1 Parser (`@object-ui/core/clipboard`)

<!-- doc-snippet: fragment — RFC signature excerpt — parseClipboard is declared without a body because this section proposes the module's shape, not its implementation -->
```ts
export interface ParsedClipboard {
/** 2D string matrix, rows × cells, never null */
Expand All@@ -188,6 +189,7 @@ Parser handles:

### 5.2 Coercer (`@object-ui/core/clipboard`)

<!-- doc-snippet: fragment — RFC signature excerpt — coerceCell is declared without a body; this section proposes the coercer surface, and nothing implements it yet -->
```ts
export type CoercerType =
| 'text' | 'number' | 'integer' | 'currency' | 'percent'
Expand DownExpand Up@@ -240,6 +242,7 @@ Coercion details per type (v1):

### 5.3 React Hook (`@object-ui/fields/clipboard`)

<!-- doc-snippet: fragment — RFC signature excerpt — usePasteToGrid is declared without a body, and ColumnCoercer / CellRange are the types proposed in the sections above -->
```ts
export interface UsePasteToGridOptions {
/** Columns currently visible / pasteable, in visual order */
Expand DownExpand Up@@ -287,6 +290,7 @@ export function usePasteToGrid(opts: UsePasteToGridOptions): UsePasteToGridResul

### 5.4 Preview dialog component

<!-- doc-snippet: fragment — a JSX usage sketch whose handler bodies are elided with '...'; it shows the proposed dialog's props, not runnable code -->
```tsx
<PastePreviewDialog
open
Expand DownExpand Up@@ -358,6 +362,7 @@ quick-paste is opt-in via `usePasteToGrid({ preview: 'auto' })`.

Hosts expose paste behind a flag so apps can opt-in per grid:

<!-- doc-snippet: fragment — a JSX sketch with the remaining ObjectGrid props elided as '...'; the point is the features key, not a complete element -->
```tsx
<ObjectGrid
features={{ clipboardPaste: 'preview' }} // 'off' | 'preview' | 'auto'
Expand All@@ -373,6 +378,7 @@ stable release cycle the default becomes `'preview'`.

### 7.1 ObjectGrid (master, staged)

<!-- doc-snippet: fragment — proposed host wiring — the hook, coercersFromObjectSchema, applyCommands and ObjectGridImpl are all names this RFC is proposing, and the element is elided with '...' -->
```tsx
const { onPaste, previewDialog } = usePasteToGrid({
columns: coercersFromObjectSchema(schema),
Expand DownExpand Up@@ -410,6 +416,7 @@ return (

### 7.2 EditableGridField (child, staged)

<!-- doc-snippet: fragment — proposed host wiring for EditableGridField; usePasteToGrid, coercersFromGridFieldColumns and applyCommands are proposed names, and field/value/onChange are the component's own props -->
```tsx
const { onPaste, previewDialog } = usePasteToGrid({
columns: coercersFromGridFieldColumns(field.columns),
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
31 changes: 28 additions & 3 deletions content/docs/guide/architecture.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -140,10 +140,21 @@ The `SchemaRenderer` component:

```tsx
import { SchemaRenderer } from '@object-ui/react'
import type { BaseSchema } from '@object-ui/types'

// The schema from step 1, as the object the renderer receives.
const schema: BaseSchema = {
type: 'card',
title: 'Welcome',
body: {
type: 'text',
value: 'Hello, ${user.name}!',
},
}

function App() {
const data = { user: { name: "Alice" } }
const data = { user: { name: 'Alice' } }

return <SchemaRenderer schema={schema} data={data} />
}
```
Expand All@@ -152,6 +163,7 @@ function App() {

The registry maps type strings to React components:

<!-- doc-snippet: fragment — registry excerpt — CardComponent and TextComponent are placeholder names for the reader's own components, and ComponentRegistry is imported where it is used further down the page -->
```typescript
// During app initialization
ComponentRegistry.register('card', CardComponent)
Expand All@@ -165,6 +177,7 @@ const Component = ComponentRegistry.get('card') // → CardComponent

The registered component renders with evaluated props:

<!-- doc-snippet: fragment — the JSX the registry produces for step 1's schema; CardComponent and TextComponent are the placeholder components registered in the block above -->
```tsx
<CardComponent title="Welcome">
<TextComponent value="Hello, Alice!" />
Expand All@@ -179,6 +192,7 @@ ObjectUI uses two registry systems for extensibility:

Maps schema types to React components:

<!-- doc-snippet: fragment — MyWidgetComponent is a placeholder for the reader's own component; the block shows the register() call's metadata argument, not a runnable module -->
```tsx
import { ComponentRegistry } from '@object-ui/core'

Expand All@@ -197,6 +211,7 @@ ComponentRegistry.register('my-widget', MyWidgetComponent, {

Maps field types to input components:

<!-- doc-snippet: fragment — RatingFieldComponent is a placeholder for the reader's own field renderer -->
```tsx
import { registerFieldRenderer } from '@object-ui/fields'

Expand DownExpand Up@@ -274,6 +289,7 @@ ObjectUI uses **Tailwind CSS** exclusively for styling:

All component variants use `cva` for type-safe variants:

<!-- doc-snippet: fragment — quotes how @object-ui/components declares its variants internally; class-variance-authority is that package's own dependency, not a module resolvable from the docs root -->
```tsx
import { cva } from 'class-variance-authority'

Expand All@@ -299,6 +315,7 @@ const buttonVariants = cva(

Use `cn()` helper (tailwind-merge + clsx) for class overrides:

<!-- doc-snippet: fragment — a one-line usage excerpt; '@/lib/utils' is the reader's app path alias and Button and props come from the surrounding component -->
```tsx
import { cn } from '@/lib/utils'

Expand All@@ -319,11 +336,15 @@ ObjectUI is built with **TypeScript** in strict mode:
```typescript
import type { ComponentSchema, ButtonSchema } from '@object-ui/types'

function handleClick() {
// ...
}

const schema: ButtonSchema = {
type: 'button',
text: 'Click me',
variant: 'default', // ✅ Type-checked
onClick: 'handleClick'
onClick: handleClick, // ✅ a handler, not its name — onClick is () => void | Promise<void>
}
```

Expand All@@ -345,6 +366,7 @@ Heavy dependencies only go in plugins:

Don't import components directly - use registries:

<!-- doc-snippet: fragment — a good/bad contrast pair mixing an import, bare JSX and a bare schema literal — three separate excerpts in one block, none a module -->
```tsx
// ❌ Bad
import { MyGrid } from './MyGrid'
Expand All@@ -359,6 +381,7 @@ ComponentRegistry.register('my-grid', MyGrid)

Never use inline styles or CSS-in-JS:

<!-- doc-snippet: fragment — a good/bad contrast pair of two unclosed div openings, quoted to compare the style attribute with a Tailwind class -->
```tsx
// ❌ Bad
<div style={{ backgroundColor: 'red' }}>
Expand All@@ -371,6 +394,7 @@ Never use inline styles or CSS-in-JS:

Use expressions for dynamic content:

<!-- doc-snippet: fragment — a good/bad contrast pair of two bare schema object literals -->
```tsx
// ❌ Bad - hardcoded
{ type: 'text', value: 'Hello, John!' }
Expand All@@ -389,6 +413,7 @@ When creating a plugin:
4. Add documentation in `content/docs/plugins/`
5. Add to plugins meta.json

<!-- doc-snippet: fragment — the reader's new plugin package index.tsx; './MyWidget' is the sibling source file in that package -->
```typescript
// packages/plugin-mywidget/src/index.tsx
import { ComponentRegistry } from '@object-ui/core'
Expand Down
14 changes: 13 additions & 1 deletion content/docs/guide/building-crud-app.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -36,6 +36,7 @@ pnpm add -D tailwindcss @tailwindcss/vite

Add Tailwind to your `vite.config.ts`:

<!-- doc-snippet: fragment — a vite.config.ts for the app the reader is scaffolding; '@vitejs/plugin-react' and '@tailwindcss/vite' are that app's devDependencies, not this repo's -->
```ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
Expand DownExpand Up@@ -163,7 +164,10 @@ export class RestDataSource implements DataSource {
const query = new URLSearchParams();
if (params?.$top) query.set('$top', String(params.$top));
if (params?.$skip) query.set('$skip', String(params.$skip));
if (params?.$orderby) query.set('$orderby', params.$orderby);
// `$orderby` is a union — an OData clause string, a map, or an array of
// fields. This backend speaks the string form, so narrow to it rather
// than stringifying a shape the server cannot parse.
if (typeof params?.$orderby === 'string') query.set('$orderby', params.$orderby);
if (params?.$search) query.set('$search', params.$search);
const res = await fetch(`${this.baseUrl}/${resource}?${query}`);
const data = await res.json();
Expand DownExpand Up@@ -208,6 +212,7 @@ Wire everything together in `src/App.tsx`. `SchemaRendererProvider` injects the
data source once, and every `SchemaRenderer` beneath it renders its schema
against that one adapter:

<!-- doc-snippet: fragment — the reader's src/App.tsx; './setup' and './data/rest-data-source' are the project files created in Steps 2 and 4 -->
```tsx
import './setup';
import { SchemaRenderer, SchemaRendererProvider } from '@object-ui/react';
Expand DownExpand Up@@ -259,6 +264,7 @@ resolved** panel naming itself and the object it was about to read.

ObjectUI generates forms directly from your schema. Extend `App.tsx` with form state:

<!-- doc-snippet: fragment — two lines to paste into the App component of Step 5 — the useState import and the surrounding function body are already there -->
```tsx
const [showForm, setShowForm] = useState(false);
const [editId, setEditId] = useState<string | null>(null);
Expand All@@ -269,6 +275,7 @@ Add a "New Task" button and handle row clicks to open the edit form:
Both of these render inside the `SchemaRendererProvider` from Step 5, so neither
carries a data source of its own:

<!-- doc-snippet: fragment — JSX to place inside the Step 5 App component; showForm, editId and their setters are the state declared in the block above -->
```tsx
<SchemaRenderer
schema={{ type: 'object-grid', objectName: 'task' }}
Expand DownExpand Up@@ -314,6 +321,7 @@ it declaratively, with the spec's per-element `dataSource` binding
(fetched through your data source's `getObjectSchema` / `listViews`) and
composes that view's `filter` and `sort` onto the query for you:

<!-- doc-snippet: fragment — an excerpt mixing a state declaration with the JSX it drives, to be placed inside the App component; it is not a standalone module -->
```tsx
const [activeView, setActiveView] = useState('all');

Expand DownExpand Up@@ -352,6 +360,8 @@ server decides.
Create a detail page that renders a single record with all its fields:

```tsx
import { SchemaRenderer } from '@object-ui/react';

function TaskDetail({ taskId, onBack }: { taskId: string; onBack: () => void }) {
return (
<div className="min-h-screen bg-background p-6">
Expand DownExpand Up@@ -390,6 +400,7 @@ Use this component in your main app with simple routing state, or integrate with

**Environment config** — Keep your API URL configurable:

<!-- doc-snippet: fragment — continues Step 4 — RestDataSource is the class defined there, and import.meta.env is Vite's typing in the reader's own app -->
```ts
const dataSource = new RestDataSource(
import.meta.env.VITE_API_URL || 'http://localhost:3000/api'
Expand All@@ -402,6 +413,7 @@ const dataSource = new RestDataSource(

**Authentication** — Extend `RestDataSource` to inject auth headers:

<!-- doc-snippet: fragment — extends the RestDataSource class defined in Step 4 -->
```ts
class AuthenticatedDataSource extends RestDataSource {
constructor(baseUrl: string, private getToken: () => string) {
Expand Down
10 changes: 10 additions & 0 deletions content/docs/guide/plugins.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -203,6 +203,7 @@ Kanban board component with drag-and-drop powered by @dnd-kit.

Plugins use React's `lazy()` and `Suspense` to load heavy dependencies on-demand:

<!-- doc-snippet: fragment — excerpt of a plugin package's own source; './MonacoImpl' is a sibling file in the reader's package, not a module resolvable from this repo -->
```typescript
// The plugin structure
import React, { Suspense } from 'react'
Expand DownExpand Up@@ -248,6 +249,7 @@ Without lazy loading, all this code would be in your main bundle!

Plugins automatically register their components when imported:

<!-- doc-snippet: fragment — continues the block above — CodeEditorRenderer is defined there, and this line is the tail of the same plugin index.tsx -->
```typescript
// In the plugin's index.tsx
import { ComponentRegistry } from '@object-ui/core'
Expand DownExpand Up@@ -284,6 +286,7 @@ cd packages/plugin-myfeature

### 2. Create Heavy Implementation

<!-- doc-snippet: fragment — the reader's new package importing its own heavy dependency; 'heavy-library' is a placeholder name, not an installed module -->
```typescript
// src/MyFeatureImpl.tsx
import HeavyLibrary from 'heavy-library'
Expand All@@ -295,6 +298,7 @@ export default function MyFeatureImpl(props) {

### 3. Create Lazy Wrapper

<!-- doc-snippet: fragment — the reader's new src/index.tsx; './MyFeatureImpl' is the sibling file created in the previous step -->
```typescript
// src/index.tsx
import React, { Suspense } from 'react'
Expand DownExpand Up@@ -334,6 +338,7 @@ export interface MyFeatureSchema extends BaseSchema {

### 5. Configure Build

<!-- doc-snippet: fragment — a vite.config.ts for the reader's plugin package; '@vitejs/plugin-react' is that package's devDependency, not this repo's -->
```typescript
// vite.config.ts
import { defineConfig } from 'vite'
Expand DownExpand Up@@ -420,6 +425,7 @@ Heavy imports go in the `*Impl.tsx` file.

Always show a meaningful skeleton while loading:

<!-- doc-snippet: fragment — a bare JSX excerpt showing the Suspense wrapper shape; Suspense, Skeleton, LazyComponent and props all come from the surrounding component -->
```typescript
<Suspense fallback={
<Skeleton className="w-full h-[400px]" />
Expand All@@ -432,6 +438,7 @@ Always show a meaningful skeleton while loading:

Make your plugin type-safe:

<!-- doc-snippet: fragment — re-export excerpt from the reader's package; './types' is the file created in step 4 -->
```typescript
export type { MyFeatureSchema } from './types'
```
Expand DownExpand Up@@ -474,6 +481,7 @@ ls -lh dist/

Check that you imported it in your app:

<!-- doc-snippet: fragment — the app-side import of '@object-ui/plugin-myfeature', the package this guide teaches the reader to publish -->
```typescript
import '@object-ui/plugin-myfeature'
```
Expand All@@ -482,6 +490,7 @@ import '@object-ui/plugin-myfeature'

Make sure types are exported:

<!-- doc-snippet: fragment — re-export from '@object-ui/plugin-myfeature', the reader's own published package -->
```typescript
export type { MyFeatureSchema } from '@object-ui/plugin-myfeature'
```
Expand All@@ -499,6 +508,7 @@ Check that the implementation is in a separate file:

Check that ComponentRegistry.register() is called at the module level:

<!-- doc-snippet: fragment — a good/bad contrast pair; ComponentRegistry and MyFeatureRenderer are the ambient names of the plugin index.tsx being discussed -->
```typescript
// ✅ Good - runs on import
ComponentRegistry.register('my-feature', MyFeatureRenderer)
Expand Down
7 changes: 7 additions & 0 deletions content/docs/rfcs/0001-clipboard-paste.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -162,6 +162,7 @@ Key rules:

### 5.1 Parser (`@object-ui/core/clipboard`)

<!-- doc-snippet: fragment — RFC signature excerpt — parseClipboard is declared without a body because this section proposes the module's shape, not its implementation -->
```ts
export interface ParsedClipboard {
/** 2D string matrix, rows × cells, never null */
Expand All@@ -188,6 +189,7 @@ Parser handles:

### 5.2 Coercer (`@object-ui/core/clipboard`)

<!-- doc-snippet: fragment — RFC signature excerpt — coerceCell is declared without a body; this section proposes the coercer surface, and nothing implements it yet -->
```ts
export type CoercerType =
| 'text' | 'number' | 'integer' | 'currency' | 'percent'
Expand DownExpand Up@@ -240,6 +242,7 @@ Coercion details per type (v1):

### 5.3 React Hook (`@object-ui/fields/clipboard`)

<!-- doc-snippet: fragment — RFC signature excerpt — usePasteToGrid is declared without a body, and ColumnCoercer / CellRange are the types proposed in the sections above -->
```ts
export interface UsePasteToGridOptions {
/** Columns currently visible / pasteable, in visual order */
Expand DownExpand Up@@ -287,6 +290,7 @@ export function usePasteToGrid(opts: UsePasteToGridOptions): UsePasteToGridResul

### 5.4 Preview dialog component

<!-- doc-snippet: fragment — a JSX usage sketch whose handler bodies are elided with '...'; it shows the proposed dialog's props, not runnable code -->
```tsx
<PastePreviewDialog
open
Expand DownExpand Up@@ -358,6 +362,7 @@ quick-paste is opt-in via `usePasteToGrid({ preview: 'auto' })`.

Hosts expose paste behind a flag so apps can opt-in per grid:

<!-- doc-snippet: fragment — a JSX sketch with the remaining ObjectGrid props elided as '...'; the point is the features key, not a complete element -->
```tsx
<ObjectGrid
features={{ clipboardPaste: 'preview' }} // 'off' | 'preview' | 'auto'
Expand All@@ -373,6 +378,7 @@ stable release cycle the default becomes `'preview'`.

### 7.1 ObjectGrid (master, staged)

<!-- doc-snippet: fragment — proposed host wiring — the hook, coercersFromObjectSchema, applyCommands and ObjectGridImpl are all names this RFC is proposing, and the element is elided with '...' -->
```tsx
const { onPaste, previewDialog } = usePasteToGrid({
columns: coercersFromObjectSchema(schema),
Expand DownExpand Up@@ -410,6 +416,7 @@ return (

### 7.2 EditableGridField (child, staged)

<!-- doc-snippet: fragment — proposed host wiring for EditableGridField; usePasteToGrid, coercersFromGridFieldColumns and applyCommands are proposed names, and field/value/onChange are the component's own props -->
```tsx
const { onPaste, previewDialog } = usePasteToGrid({
columns: coercersFromGridFieldColumns(field.columns),
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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
31 changes: 28 additions & 3 deletions content/docs/guide/architecture.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -140,10 +140,21 @@ The `SchemaRenderer` component:

```tsx
import { SchemaRenderer } from '@object-ui/react'
import type { BaseSchema } from '@object-ui/types'

// The schema from step 1, as the object the renderer receives.
const schema: BaseSchema = {
type: 'card',
title: 'Welcome',
body: {
type: 'text',
value: 'Hello, ${user.name}!',
},
}

function App() {
const data = { user: { name: "Alice" } }
const data = { user: { name: 'Alice' } }

return <SchemaRenderer schema={schema} data={data} />
}
```
Expand All@@ -152,6 +163,7 @@ function App() {

The registry maps type strings to React components:

<!-- doc-snippet: fragment — registry excerpt — CardComponent and TextComponent are placeholder names for the reader's own components, and ComponentRegistry is imported where it is used further down the page -->
```typescript
// During app initialization
ComponentRegistry.register('card', CardComponent)
Expand All@@ -165,6 +177,7 @@ const Component = ComponentRegistry.get('card') // → CardComponent

The registered component renders with evaluated props:

<!-- doc-snippet: fragment — the JSX the registry produces for step 1's schema; CardComponent and TextComponent are the placeholder components registered in the block above -->
```tsx
<CardComponent title="Welcome">
<TextComponent value="Hello, Alice!" />
Expand All@@ -179,6 +192,7 @@ ObjectUI uses two registry systems for extensibility:

Maps schema types to React components:

<!-- doc-snippet: fragment — MyWidgetComponent is a placeholder for the reader's own component; the block shows the register() call's metadata argument, not a runnable module -->
```tsx
import { ComponentRegistry } from '@object-ui/core'

Expand All@@ -197,6 +211,7 @@ ComponentRegistry.register('my-widget', MyWidgetComponent, {

Maps field types to input components:

<!-- doc-snippet: fragment — RatingFieldComponent is a placeholder for the reader's own field renderer -->
```tsx
import { registerFieldRenderer } from '@object-ui/fields'

Expand DownExpand Up@@ -274,6 +289,7 @@ ObjectUI uses **Tailwind CSS** exclusively for styling:

All component variants use `cva` for type-safe variants:

<!-- doc-snippet: fragment — quotes how @object-ui/components declares its variants internally; class-variance-authority is that package's own dependency, not a module resolvable from the docs root -->
```tsx
import { cva } from 'class-variance-authority'

Expand All@@ -299,6 +315,7 @@ const buttonVariants = cva(

Use `cn()` helper (tailwind-merge + clsx) for class overrides:

<!-- doc-snippet: fragment — a one-line usage excerpt; '@/lib/utils' is the reader's app path alias and Button and props come from the surrounding component -->
```tsx
import { cn } from '@/lib/utils'

Expand All@@ -319,11 +336,15 @@ ObjectUI is built with **TypeScript** in strict mode:
```typescript
import type { ComponentSchema, ButtonSchema } from '@object-ui/types'

function handleClick() {
// ...
}

const schema: ButtonSchema = {
type: 'button',
text: 'Click me',
variant: 'default', // ✅ Type-checked
onClick: 'handleClick'
onClick: handleClick, // ✅ a handler, not its name — onClick is () => void | Promise<void>
}
```

Expand All@@ -345,6 +366,7 @@ Heavy dependencies only go in plugins:

Don't import components directly - use registries:

<!-- doc-snippet: fragment — a good/bad contrast pair mixing an import, bare JSX and a bare schema literal — three separate excerpts in one block, none a module -->
```tsx
// ❌ Bad
import { MyGrid } from './MyGrid'
Expand All@@ -359,6 +381,7 @@ ComponentRegistry.register('my-grid', MyGrid)

Never use inline styles or CSS-in-JS:

<!-- doc-snippet: fragment — a good/bad contrast pair of two unclosed div openings, quoted to compare the style attribute with a Tailwind class -->
```tsx
// ❌ Bad
<div style={{ backgroundColor: 'red' }}>
Expand All@@ -371,6 +394,7 @@ Never use inline styles or CSS-in-JS:

Use expressions for dynamic content:

<!-- doc-snippet: fragment — a good/bad contrast pair of two bare schema object literals -->
```tsx
// ❌ Bad - hardcoded
{ type: 'text', value: 'Hello, John!' }
Expand All@@ -389,6 +413,7 @@ When creating a plugin:
4. Add documentation in `content/docs/plugins/`
5. Add to plugins meta.json

<!-- doc-snippet: fragment — the reader's new plugin package index.tsx; './MyWidget' is the sibling source file in that package -->
```typescript
// packages/plugin-mywidget/src/index.tsx
import { ComponentRegistry } from '@object-ui/core'
Expand Down
14 changes: 13 additions & 1 deletion content/docs/guide/building-crud-app.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -36,6 +36,7 @@ pnpm add -D tailwindcss @tailwindcss/vite

Add Tailwind to your `vite.config.ts`:

<!-- doc-snippet: fragment — a vite.config.ts for the app the reader is scaffolding; '@vitejs/plugin-react' and '@tailwindcss/vite' are that app's devDependencies, not this repo's -->
```ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
Expand DownExpand Up@@ -163,7 +164,10 @@ export class RestDataSource implements DataSource {
const query = new URLSearchParams();
if (params?.$top) query.set('$top', String(params.$top));
if (params?.$skip) query.set('$skip', String(params.$skip));
if (params?.$orderby) query.set('$orderby', params.$orderby);
// `$orderby` is a union — an OData clause string, a map, or an array of
// fields. This backend speaks the string form, so narrow to it rather
// than stringifying a shape the server cannot parse.
if (typeof params?.$orderby === 'string') query.set('$orderby', params.$orderby);
if (params?.$search) query.set('$search', params.$search);
const res = await fetch(`${this.baseUrl}/${resource}?${query}`);
const data = await res.json();
Expand DownExpand Up@@ -208,6 +212,7 @@ Wire everything together in `src/App.tsx`. `SchemaRendererProvider` injects the
data source once, and every `SchemaRenderer` beneath it renders its schema
against that one adapter:

<!-- doc-snippet: fragment — the reader's src/App.tsx; './setup' and './data/rest-data-source' are the project files created in Steps 2 and 4 -->
```tsx
import './setup';
import { SchemaRenderer, SchemaRendererProvider } from '@object-ui/react';
Expand DownExpand Up@@ -259,6 +264,7 @@ resolved** panel naming itself and the object it was about to read.

ObjectUI generates forms directly from your schema. Extend `App.tsx` with form state:

<!-- doc-snippet: fragment — two lines to paste into the App component of Step 5 — the useState import and the surrounding function body are already there -->
```tsx
const [showForm, setShowForm] = useState(false);
const [editId, setEditId] = useState<string | null>(null);
Expand All@@ -269,6 +275,7 @@ Add a "New Task" button and handle row clicks to open the edit form:
Both of these render inside the `SchemaRendererProvider` from Step 5, so neither
carries a data source of its own:

<!-- doc-snippet: fragment — JSX to place inside the Step 5 App component; showForm, editId and their setters are the state declared in the block above -->
```tsx
<SchemaRenderer
schema={{ type: 'object-grid', objectName: 'task' }}
Expand DownExpand Up@@ -314,6 +321,7 @@ it declaratively, with the spec's per-element `dataSource` binding
(fetched through your data source's `getObjectSchema` / `listViews`) and
composes that view's `filter` and `sort` onto the query for you:

<!-- doc-snippet: fragment — an excerpt mixing a state declaration with the JSX it drives, to be placed inside the App component; it is not a standalone module -->
```tsx
const [activeView, setActiveView] = useState('all');

Expand DownExpand Up@@ -352,6 +360,8 @@ server decides.
Create a detail page that renders a single record with all its fields:

```tsx
import { SchemaRenderer } from '@object-ui/react';

function TaskDetail({ taskId, onBack }: { taskId: string; onBack: () => void }) {
return (
<div className="min-h-screen bg-background p-6">
Expand DownExpand Up@@ -390,6 +400,7 @@ Use this component in your main app with simple routing state, or integrate with

**Environment config** — Keep your API URL configurable:

<!-- doc-snippet: fragment — continues Step 4 — RestDataSource is the class defined there, and import.meta.env is Vite's typing in the reader's own app -->
```ts
const dataSource = new RestDataSource(
import.meta.env.VITE_API_URL || 'http://localhost:3000/api'
Expand All@@ -402,6 +413,7 @@ const dataSource = new RestDataSource(

**Authentication** — Extend `RestDataSource` to inject auth headers:

<!-- doc-snippet: fragment — extends the RestDataSource class defined in Step 4 -->
```ts
class AuthenticatedDataSource extends RestDataSource {
constructor(baseUrl: string, private getToken: () => string) {
Expand Down
10 changes: 10 additions & 0 deletions content/docs/guide/plugins.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -203,6 +203,7 @@ Kanban board component with drag-and-drop powered by @dnd-kit.

Plugins use React's `lazy()` and `Suspense` to load heavy dependencies on-demand:

<!-- doc-snippet: fragment — excerpt of a plugin package's own source; './MonacoImpl' is a sibling file in the reader's package, not a module resolvable from this repo -->
```typescript
// The plugin structure
import React, { Suspense } from 'react'
Expand DownExpand Up@@ -248,6 +249,7 @@ Without lazy loading, all this code would be in your main bundle!

Plugins automatically register their components when imported:

<!-- doc-snippet: fragment — continues the block above — CodeEditorRenderer is defined there, and this line is the tail of the same plugin index.tsx -->
```typescript
// In the plugin's index.tsx
import { ComponentRegistry } from '@object-ui/core'
Expand DownExpand Up@@ -284,6 +286,7 @@ cd packages/plugin-myfeature

### 2. Create Heavy Implementation

<!-- doc-snippet: fragment — the reader's new package importing its own heavy dependency; 'heavy-library' is a placeholder name, not an installed module -->
```typescript
// src/MyFeatureImpl.tsx
import HeavyLibrary from 'heavy-library'
Expand All@@ -295,6 +298,7 @@ export default function MyFeatureImpl(props) {

### 3. Create Lazy Wrapper

<!-- doc-snippet: fragment — the reader's new src/index.tsx; './MyFeatureImpl' is the sibling file created in the previous step -->
```typescript
// src/index.tsx
import React, { Suspense } from 'react'
Expand DownExpand Up@@ -334,6 +338,7 @@ export interface MyFeatureSchema extends BaseSchema {

### 5. Configure Build

<!-- doc-snippet: fragment — a vite.config.ts for the reader's plugin package; '@vitejs/plugin-react' is that package's devDependency, not this repo's -->
```typescript
// vite.config.ts
import { defineConfig } from 'vite'
Expand DownExpand Up@@ -420,6 +425,7 @@ Heavy imports go in the `*Impl.tsx` file.

Always show a meaningful skeleton while loading:

<!-- doc-snippet: fragment — a bare JSX excerpt showing the Suspense wrapper shape; Suspense, Skeleton, LazyComponent and props all come from the surrounding component -->
```typescript
<Suspense fallback={
<Skeleton className="w-full h-[400px]" />
Expand All@@ -432,6 +438,7 @@ Always show a meaningful skeleton while loading:

Make your plugin type-safe:

<!-- doc-snippet: fragment — re-export excerpt from the reader's package; './types' is the file created in step 4 -->
```typescript
export type { MyFeatureSchema } from './types'
```
Expand DownExpand Up@@ -474,6 +481,7 @@ ls -lh dist/

Check that you imported it in your app:

<!-- doc-snippet: fragment — the app-side import of '@object-ui/plugin-myfeature', the package this guide teaches the reader to publish -->
```typescript
import '@object-ui/plugin-myfeature'
```
Expand All@@ -482,6 +490,7 @@ import '@object-ui/plugin-myfeature'

Make sure types are exported:

<!-- doc-snippet: fragment — re-export from '@object-ui/plugin-myfeature', the reader's own published package -->
```typescript
export type { MyFeatureSchema } from '@object-ui/plugin-myfeature'
```
Expand All@@ -499,6 +508,7 @@ Check that the implementation is in a separate file:

Check that ComponentRegistry.register() is called at the module level:

<!-- doc-snippet: fragment — a good/bad contrast pair; ComponentRegistry and MyFeatureRenderer are the ambient names of the plugin index.tsx being discussed -->
```typescript
// ✅ Good - runs on import
ComponentRegistry.register('my-feature', MyFeatureRenderer)
Expand Down
7 changes: 7 additions & 0 deletions content/docs/rfcs/0001-clipboard-paste.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -162,6 +162,7 @@ Key rules:

### 5.1 Parser (`@object-ui/core/clipboard`)

<!-- doc-snippet: fragment — RFC signature excerpt — parseClipboard is declared without a body because this section proposes the module's shape, not its implementation -->
```ts
export interface ParsedClipboard {
/** 2D string matrix, rows × cells, never null */
Expand All@@ -188,6 +189,7 @@ Parser handles:

### 5.2 Coercer (`@object-ui/core/clipboard`)

<!-- doc-snippet: fragment — RFC signature excerpt — coerceCell is declared without a body; this section proposes the coercer surface, and nothing implements it yet -->
```ts
export type CoercerType =
| 'text' | 'number' | 'integer' | 'currency' | 'percent'
Expand DownExpand Up@@ -240,6 +242,7 @@ Coercion details per type (v1):

### 5.3 React Hook (`@object-ui/fields/clipboard`)

<!-- doc-snippet: fragment — RFC signature excerpt — usePasteToGrid is declared without a body, and ColumnCoercer / CellRange are the types proposed in the sections above -->
```ts
export interface UsePasteToGridOptions {
/** Columns currently visible / pasteable, in visual order */
Expand DownExpand Up@@ -287,6 +290,7 @@ export function usePasteToGrid(opts: UsePasteToGridOptions): UsePasteToGridResul

### 5.4 Preview dialog component

<!-- doc-snippet: fragment — a JSX usage sketch whose handler bodies are elided with '...'; it shows the proposed dialog's props, not runnable code -->
```tsx
<PastePreviewDialog
open
Expand DownExpand Up@@ -358,6 +362,7 @@ quick-paste is opt-in via `usePasteToGrid({ preview: 'auto' })`.

Hosts expose paste behind a flag so apps can opt-in per grid:

<!-- doc-snippet: fragment — a JSX sketch with the remaining ObjectGrid props elided as '...'; the point is the features key, not a complete element -->
```tsx
<ObjectGrid
features={{ clipboardPaste: 'preview' }} // 'off' | 'preview' | 'auto'
Expand All@@ -373,6 +378,7 @@ stable release cycle the default becomes `'preview'`.

### 7.1 ObjectGrid (master, staged)

<!-- doc-snippet: fragment — proposed host wiring — the hook, coercersFromObjectSchema, applyCommands and ObjectGridImpl are all names this RFC is proposing, and the element is elided with '...' -->
```tsx
const { onPaste, previewDialog } = usePasteToGrid({
columns: coercersFromObjectSchema(schema),
Expand DownExpand Up@@ -410,6 +416,7 @@ return (

### 7.2 EditableGridField (child, staged)

<!-- doc-snippet: fragment — proposed host wiring for EditableGridField; usePasteToGrid, coercersFromGridFieldColumns and applyCommands are proposed names, and field/value/onChange are the component's own props -->
```tsx
const { onPaste, previewDialog } = usePasteToGrid({
columns: coercersFromGridFieldColumns(field.columns),
Expand Down
Loading
Loading