diff --git a/.github/WORKFLOWS.md b/.github/WORKFLOWS.md
index d53b775b..e02a78da 100644
--- a/.github/WORKFLOWS.md
+++ b/.github/WORKFLOWS.md
@@ -106,7 +106,6 @@ This document describes all GitHub Actions workflows and automation configured f
**Labels Applied:**
- `kernel` - Changes in `packages/kernel/**`
- `server` - Changes in `packages/server/**`
-- `ui` - Changes in `packages/ui/**`
- `presets` - Changes in `packages/presets/**`
- `documentation` - Changes in docs or markdown files
- `workflows` - Changes in `.github/**`
diff --git a/.github/labeler.yml b/.github/labeler.yml
index 6741a6a2..9a35e52c 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -10,10 +10,6 @@
- changed-files:
- any-glob-to-any-file: 'packages/server/**/*'
-'ui':
- - changed-files:
- - any-glob-to-any-file: 'packages/ui/**/*'
-
'presets':
- changed-files:
- any-glob-to-any-file: 'packages/presets/**/*'
diff --git a/.github/labels.yml b/.github/labels.yml
index b6d3faad..ff6fe212 100644
--- a/.github/labels.yml
+++ b/.github/labels.yml
@@ -11,10 +11,6 @@ labels:
color: '0366d6'
description: 'Changes in @objectos/server package'
- - name: ui
- color: '0366d6'
- description: 'Changes in @objectos/ui package'
-
- name: presets
color: '0366d6'
description: 'Changes in preset packages'
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index d3cc0b59..164d1bb2 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -6,7 +6,7 @@ ObjectOS is a **metadata-driven runtime engine** that transforms declarative YAM
## Protocol Foundation: @objectstack/spec
-ObjectOS is built on the **[@objectstack/spec](https://www.npmjs.com/package/@objectstack/spec)** protocol, which defines the "DNA" of the ObjectStack ecosystem. The spec provides:
+ObjectOS is built on the **[@objectstack/spec](https://github.com/objectstack-ai/spec)** protocol, which defines the "DNA" of the ObjectStack ecosystem. The spec provides:
### 1. **Strict Type Definitions**
- **Zod Schemas**: Runtime validation for configuration and data
@@ -36,7 +36,7 @@ All ObjectOS plugins must conform to this lifecycle for consistency and predicta
## The Three-Repository Model
### @objectstack/spec (Protocol Definition)
-- **Location**: https://www.npmjs.com/package/@objectstack/spec
+- **Location**: https://github.com/objectstack-ai/spec
- **Purpose**: Defines the protocol and type contracts
- **Key Exports**:
- `Data.*` - Object schemas, field types, queries
@@ -60,7 +60,6 @@ All ObjectOS plugins must conform to this lifecycle for consistency and predicta
- **Key Packages**:
- `@objectos/kernel` - Core execution engine
- `@objectos/server` - HTTP API layer
- - `@objectos/ui` - React UI components
## Core Architectural Principle
@@ -302,41 +301,9 @@ export class ObjectDataController {
| DELETE | `/api/data/:object/:id` | Delete record |
| GET | `/api/metadata/:object` | Get object metadata |
-## Layer 5: UI Layer (@objectos/ui)
+## Layer 5: UI Layer
-### Component Architecture
-
-The UI layer provides **metadata-driven React components**:
-
-```typescript
-// Automatically generates a data grid from metadata
-
-
-// Automatically generates a form from metadata
-
-```
-
-### Key Components
-
-1. **ObjectGrid**: Airtable-like data grid with inline editing
-2. **ObjectForm**: Salesforce-like detail form with sections
-3. **ObjectChart**: Chart component for analytics
-4. **FilterBuilder**: Visual query builder
-
-### Design System
-
-- **Framework**: React 18+ with TypeScript
-- **Styling**: Tailwind CSS
-- **Components**: Shadcn/ui
-- **State**: React Query for server state
-- **Grid**: TanStack Table
+**Note**: The UI layer has been moved to a separate project and is no longer part of this monorepo. The UI components are developed independently and can be integrated with ObjectOS through the API layer.
## Extension Points
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 2145bff7..b06589b2 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -18,20 +18,21 @@ objectos/
├── packages/
│ ├── kernel/ # @objectos/kernel - Core runtime engine
│ ├── server/ # @objectos/server - NestJS HTTP server
-│ ├── ui/ # @objectos/ui - React components
│ └── presets/ # @objectos/preset-* - Standard metadata
+├── apps/
+│ └── site/ # @objectos/site - Documentation site
├── examples/ # Example applications
-├── docs/ # VitePress documentation
-└── apps/ # Full-stack applications (if any)
+└── docs/ # VitePress documentation
```
+**Note**: The UI package (`@objectos/ui`) has been moved to a separate repository and is developed independently.
+
### Package Responsibilities
| Package | Role | Can Import | Cannot Import |
|---------|------|------------|---------------|
| `@objectos/kernel` | Core logic, object registry, hooks | `@objectql/types`, `@objectql/core` | `pg`, `express`, `nest` |
| `@objectos/server` | HTTP layer, REST API | `@objectos/kernel`, `@nestjs/*` | `knex`, direct SQL |
-| `@objectos/ui` | React components | `@objectos/kernel` types | Server-specific code |
| `@objectos/preset-*` | Metadata YAML files | None | No .ts files allowed |
## Development Standards
diff --git a/QUICK_REFERENCE.md b/QUICK_REFERENCE.md
index 374c04da..f45c3a09 100644
--- a/QUICK_REFERENCE.md
+++ b/QUICK_REFERENCE.md
@@ -113,19 +113,9 @@ NestJS HTTP server.
- `DELETE /api/data/:object/:id` - Delete record
- `GET /api/metadata/:object` - Get metadata
-### @objectos/ui
+### UI Components
-React UI components.
-
-```tsx
-import { ObjectGrid, ObjectForm } from '@objectos/ui';
-
-// Auto-generated data grid
-
-
-// Auto-generated form
-
-```
+**Note**: The UI components have been moved to a separate project and are no longer part of this monorepo. They are developed independently and can be integrated with ObjectOS through the API layer.
## Field Types
diff --git a/README.md b/README.md
index e0e954c9..a6f4e368 100644
--- a/README.md
+++ b/README.md
@@ -52,7 +52,7 @@ ObjectOS is built as a modular Monorepo using **NestJS** and follows the **@obje
### Protocol Compliance
-ObjectOS adheres to the [@objectstack/spec](https://www.npmjs.com/package/@objectstack/spec) protocol, which defines:
+ObjectOS adheres to the [@objectstack/spec](https://github.com/objectstack-ai/spec) protocol, which defines:
- **Kernel Protocol**: Plugin lifecycle, manifest structure, and context interfaces
- **Data Protocol**: Object schemas, field types, queries, and hooks
- **System Protocol**: Audit logging, events, and job scheduling
diff --git a/apps/site/content/docs/guide/index.mdx b/apps/site/content/docs/guide/index.mdx
index e7d895ae..612814b3 100644
--- a/apps/site/content/docs/guide/index.mdx
+++ b/apps/site/content/docs/guide/index.mdx
@@ -23,7 +23,6 @@ ObjectOS is a **metadata-driven runtime engine** that interprets and executes bu
│ ObjectOS (Runtime Repository - This One) │
│ - @objectos/kernel: Execution engine │
│ - @objectos/server: HTTP API layer │
-│ - @objectos/ui: React components │
└─────────────────────┬───────────────────────────┘
│
▼
@@ -304,22 +303,9 @@ curl -X POST http://localhost:3000/api/data/contacts \
}'
```
-## Using the UI Components
+## Using UI Components
-ObjectOS provides React components that automatically render based on metadata:
-
-```tsx
-import { ObjectGrid, ObjectForm } from '@objectos/ui';
-
-function ContactsPage() {
- return (
-
-
Contacts
- {/* Automatically generates a data grid */}
-
-
- );
-}
+> **Note**: The UI components have been moved to a separate project and are no longer part of this monorepo. They can be integrated with ObjectOS through the HTTP API layer (`@objectos/server`) which provides REST endpoints for metadata and data access.
function ContactDetail({ contactId }) {
return (
diff --git a/apps/site/content/docs/guide/logic-actions.mdx b/apps/site/content/docs/guide/logic-actions.mdx
index 04e464c2..46b51473 100644
--- a/apps/site/content/docs/guide/logic-actions.mdx
+++ b/apps/site/content/docs/guide/logic-actions.mdx
@@ -466,20 +466,24 @@ const result = await kernel.executeAction('contacts.sendEmail', {
### From UI
-```typescript
-import { useAction } from '@objectos/ui';
+> **Note**: The UI components have been moved to a separate project. The example below shows the conceptual pattern for invoking actions from a UI application.
+```typescript
+// Example pattern for UI integration
function ContactDetail({ contactId }) {
- const sendEmail = useAction('contacts.sendEmail');
-
const handleSendEmail = async () => {
- const result = await sendEmail({
- id: contactId,
- subject: 'Follow up',
- body: 'Thank you for your interest'
+ const result = await fetch('/api/actions/contacts.sendEmail', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ id: contactId,
+ subject: 'Follow up',
+ body: 'Thank you for your interest'
+ })
});
- alert(result.message);
+ const data = await result.json();
+ alert(data.message);
};
return (
diff --git a/apps/site/content/docs/guide/platform-components.mdx b/apps/site/content/docs/guide/platform-components.mdx
index ec30a355..c4b26dea 100644
--- a/apps/site/content/docs/guide/platform-components.mdx
+++ b/apps/site/content/docs/guide/platform-components.mdx
@@ -53,18 +53,9 @@ The `@objectos/server` package is the Gateway. It translates HTTP/WebSockets int
## 🖥️ 3. Interaction Support (UI Layer)
-The `@objectos/ui` (Components) and `@objectos/web` (App) packages provide the human interface.
+> **Note**: The UI components have been moved to a separate project and are no longer part of this monorepo.
-### Component Breakdown
-
-| Component | Responsibility | Implementation Notes |
-| :--- | :--- | :--- |
-| **ObjectGrid** | Data Table with "Excel-like" features. | Uses **TanStack Table**. Implements Virtual Scroll for 100k+ rows. |
-| **ObjectForm** | Dynamic Record Editor. | Uses **React-Hook-Form**. Generates Zod schema from Metadata at runtime. |
-| **LayoutShell** | Application chrome (Sidebar, Header). | Responsive. Adapts menu based on user permissions. |
-| **DataQueryHook** | React Query wrapper for API. | Cache management. `useQuery(['data', 'contacts'], ...)` |
-
-### Functional Realization: "Dynamic Types"
+The UI layer provides the human interface, integrating with ObjectOS through the HTTP API exposed by `@objectos/server`.
* **Design**: The UI downloads metadata initially.
* **Flow**: `schema.json` received -> `FieldFactory` maps `type: 'date'` to ` ` -> Renders Cell.
diff --git a/apps/site/content/docs/guide/ui-framework.mdx b/apps/site/content/docs/guide/ui-framework.mdx
index 2bb29c44..60e6bbe5 100644
--- a/apps/site/content/docs/guide/ui-framework.mdx
+++ b/apps/site/content/docs/guide/ui-framework.mdx
@@ -2,7 +2,9 @@
title: Standard UI Components Reference
---
-This document defines the standard component library for `@objectos/ui`. These components are the reference implementations for the **View & Layout Specifications**.
+> **Note**: The UI components have been moved to a separate project and are no longer part of this monorepo. This document is kept for reference and describes the design principles for UI components that integrate with ObjectOS.
+
+This document defines the standard component library for UI components. These components are the reference implementations for the **View & Layout Specifications**.
---
diff --git a/apps/web/index.html b/apps/web/index.html
deleted file mode 100644
index bea339ad..00000000
--- a/apps/web/index.html
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
- ObjectOS
-
-
-
-
-
-
-
diff --git a/apps/web/package.json b/apps/web/package.json
deleted file mode 100644
index bef1b24b..00000000
--- a/apps/web/package.json
+++ /dev/null
@@ -1,46 +0,0 @@
-{
- "name": "@objectos/web",
- "private": true,
- "version": "0.1.0",
- "license": "AGPL-3.0",
- "type": "module",
- "scripts": {
- "dev": "vite",
- "build:watch": "vite build --watch",
- "build": "tsc -b && vite build",
- "test": "echo \"No tests specified\" && exit 0",
- "lint": "eslint .",
- "preview": "vite preview"
- },
- "dependencies": {
- "@objectos/ui": "workspace:*",
- "@objectql/types": "^3.0.1",
- "ag-grid-community": "^35.0.0",
- "ag-grid-react": "^35.0.0",
- "better-auth": "^1.4.10",
- "clsx": "^2.1.0",
- "lucide-react": "^0.344.0",
- "react": "^18.3.1",
- "react-dom": "^18.3.1",
- "react-hook-form": "^7.54.2",
- "react-router-dom": "^7.12.0",
- "tailwind-merge": "^2.2.1"
- },
- "devDependencies": {
- "@eslint/js": "^9.17.0",
- "@tailwindcss/postcss": "^4.1.18",
- "@types/react": "^19.0.11",
- "@types/react-dom": "^19.0.5",
- "@vitejs/plugin-react": "^4.3.4",
- "eslint": "^9.17.0",
- "eslint-plugin-react-hooks": "^5.0.0",
- "eslint-plugin-react-refresh": "^0.4.16",
- "globals": "^15.14.0",
- "postcss": "^8.4.49",
- "tailwindcss": "^4.1.18",
- "tailwindcss-animate": "^1.0.7",
- "typescript": "~5.6.2",
- "typescript-eslint": "^8.18.2",
- "vite": "^6.0.5"
- }
-}
diff --git a/apps/web/postcss.config.js b/apps/web/postcss.config.js
deleted file mode 100644
index a7f73a2d..00000000
--- a/apps/web/postcss.config.js
+++ /dev/null
@@ -1,5 +0,0 @@
-export default {
- plugins: {
- '@tailwindcss/postcss': {},
- },
-}
diff --git a/apps/web/public/logo.svg b/apps/web/public/logo.svg
deleted file mode 100644
index e56ca292..00000000
--- a/apps/web/public/logo.svg
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx
deleted file mode 100644
index c5605145..00000000
--- a/apps/web/src/App.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-import { Routes, Route, Navigate } from 'react-router-dom';
-import AppList from './pages/AppList';
-import Login from './pages/Login';
-import AppDashboard from './pages/AppDashboard'; // New App Home
-import Settings from './pages/Settings';
-import Organization from './pages/Organization';
-import { AuthProvider, useAuth } from './context/AuthContext';
-import { MainLayout } from './layouts/MainLayout';
-import { WorkspaceLayout } from './layouts/WorkspaceLayout';
-import { ObjectListRoute } from './pages/objects/ObjectListRoute';
-import { ObjectDetailRoute } from './pages/objects/ObjectDetailRoute';
-import * as paths from './routes';
-
-function AppContent() {
- const { user, loading } = useAuth();
-
- if (loading) {
- return (
-
- );
- }
-
- // Auth Routing
- if (!user) {
- return (
-
- } />
- } />
-
- );
- }
-
- return (
-
- } />
-
- {/* Main App Selection Layout */}
- }>
- } />
- } />
-
-
- {/* Workspace/Dashboard Layout */}
- }>
- {/* The App Home Dashboard showing menu shortcuts */}
- } />
-
- {/* Object Routes */}
- } />
- } />
- } />
- {/* Legacy/Compat routes support */}
- } />
-
- {/* Global/Standard Routes */}
- } />
- } />
-
-
- {/* Fallback */}
- } />
-
- );
-}
-
-function App() {
- return (
-
-
-
- );
-}
-
-export default App;
-
diff --git a/apps/web/src/components/DynamicIcon.tsx b/apps/web/src/components/DynamicIcon.tsx
deleted file mode 100644
index 246ab28a..00000000
--- a/apps/web/src/components/DynamicIcon.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import React from 'react';
-import * as LucideIcons from 'lucide-react';
-
-interface DynamicIconProps extends React.ComponentProps<'svg'> {
- name?: string;
- fallback?: React.ElementType;
- className?: string;
-}
-
-export function DynamicIcon({ name, fallback, className, ...props }: DynamicIconProps) {
- const Fallback = fallback || LucideIcons.FileText;
-
- if (!name) {
- return ;
- }
-
- let iconName = name;
-
- // Handle Remix Icon names (ri-dashboard-line -> Dashboard)
- // Common mappings if needed, or just stripping prefixes
- if (name.startsWith('ri-')) {
- iconName = name
- .replace(/^ri-/, '')
- .replace(/-line$/, '')
- .replace(/-fill$/, '');
- }
-
- // Convert kebab-case to PascalCase (dashboard-layout -> DashboardLayout)
- const pascalName = iconName
- .split('-')
- .map(part => part.charAt(0).toUpperCase() + part.slice(1))
- .join('');
-
- // Try to find the icon in Lucide
- // 1. Exact PascalCase match
- // 2. Case-insensitive match (less likely needed if normalization is good)
- const IconComponent = (LucideIcons as any)[pascalName] || (LucideIcons as any)[iconName];
-
- if (IconComponent) {
- return ;
- }
-
- // If not found, return fallback
- return ;
-}
diff --git a/apps/web/src/components/app-sidebar.tsx b/apps/web/src/components/app-sidebar.tsx
deleted file mode 100644
index b95be45b..00000000
--- a/apps/web/src/components/app-sidebar.tsx
+++ /dev/null
@@ -1,204 +0,0 @@
-import * as React from "react"
-import {
- Sidebar,
- SidebarContent,
- SidebarGroup,
- SidebarGroupContent,
- SidebarGroupLabel,
- SidebarHeader,
- SidebarMenu,
- SidebarMenuButton,
- SidebarMenuItem,
- SidebarRail,
- SidebarFooter,
- SidebarMenuBadge,
- SidebarMenuSub,
- SidebarMenuSubItem,
- SidebarMenuSubButton,
- Collapsible,
- CollapsibleTrigger,
- CollapsibleContent,
-} from "@objectos/ui"
-import * as LucideIcons from "lucide-react"
-import { useRouter } from "../hooks/useRouter"
-import { NavUser } from "./nav-user"
-import { useAuth } from "../context/AuthContext"
-import { DynamicIcon } from "./DynamicIcon"
-
-export function AppSidebar({ objects, appMetadata, ...props }: React.ComponentProps & { objects: Record, appMetadata?: any }) {
- const { path, navigate } = useRouter()
- const { user } = useAuth()
-
- // Parse current app context
- const parts = path.split('/');
- const currentApp = parts[1] === 'app' ? parts[2] : null;
- const getObjectPath = (name: string) => currentApp ? `/app/${currentApp}/object/${name}` : `/object/${name}`;
-
- const rawMenu = appMetadata?.menu;
-
- // Robust check for grouped vs flat menu structure
- // A section has 'items' but NO 'type', 'object', or 'url'
- const isSection = (item: any) => item && item.items && Array.isArray(item.items) && !item.type && !item.object && !item.url;
-
- const isGrouped = Array.isArray(rawMenu) && rawMenu.length > 0 && isSection(rawMenu[0]);
-
- // Use a special key/flag for the default wrapper to hide the label later
- const menuSections = rawMenu ? (isGrouped ? rawMenu : [{ label: 'Menu', items: rawMenu, _isDefaultWrapper: true }]) : [];
-
- const renderMenuItem = (item: any, idx: number) => {
- if (item.visible === false) return null;
-
- // Handle Separator/Divider
- if (item.type === 'divider') {
- return
;
- }
-
- // Default label if missing (e.g. for dividers context or malformed data)
- const label = item.label || '';
- const itemType = item.type || 'page';
-
- // Determine active state
- let isActive = false;
- if (itemType === 'object') {
- isActive = path.includes(`/object/${item.object}`);
- } else if (itemType === 'page' || itemType === 'url') {
- isActive = item.url ? path.endsWith(item.url) : false;
- }
-
- const handleClick = () => {
- if (itemType === 'object' && item.object) {
- navigate(getObjectPath(item.object));
- } else if (itemType === 'page' && item.url) {
- navigate(item.url);
- } else if (itemType === 'url' && item.url) {
- window.open(item.url, '_blank');
- }
- };
-
- // Handle Nested Items (Submenus)
- if (item.items && item.items.length > 0) {
- return (
-
-
-
-
-
- {label}
-
-
-
-
-
- {item.items.map((subItem: any, subIdx: number) => {
- if (subItem.visible === false) return null;
- const subItemType = subItem.type || 'page';
- return (
-
- {
- if (subItemType === 'object') navigate(getObjectPath(subItem.object_name));
- else if (subItemType === 'page') navigate(subItem.url);
- else if (subItemType === 'url') window.open(subItem.url, '_blank');
- }}
- >
- {subItem.label}
- {subItem.badge && {subItem.badge} }
-
-
- );
- })}
-
-
-
-
- );
- }
-
- return (
-
-
-
- {label}
- {item.badge && {item.badge} }
-
-
- );
- };
-
- return (
-
-
-
-
- navigate('/')}>
-
-
-
-
- {appMetadata?.label || 'ObjectOS'}
-
-
-
-
-
-
- {menuSections.map((section: any, idx: number) => {
- const isCollapsible = section.collapsible === true;
- const isCollapsed = section.collapsed === true;
-
- // Helper to render group content
- const renderGroupContent = () => (
-
- {section.items?.map((item: any, itemIdx: number) => renderMenuItem(item, itemIdx))}
-
- );
-
- if (isCollapsible) {
- return (
-
-
-
-
- {section.label}
-
-
-
-
-
- {renderGroupContent()}
-
-
-
-
- );
- }
-
- return (
-
- {!section._isDefaultWrapper && {section.label} }
-
- {renderGroupContent()}
-
-
- );
- })}
-
-
-
-
-
-
- )
-}
diff --git a/apps/web/src/components/dashboard/EnhancedObjectListView.tsx b/apps/web/src/components/dashboard/EnhancedObjectListView.tsx
deleted file mode 100644
index c25f12dd..00000000
--- a/apps/web/src/components/dashboard/EnhancedObjectListView.tsx
+++ /dev/null
@@ -1,130 +0,0 @@
-import { useState, useEffect, useCallback } from 'react';
-import { ObjectGridTable } from '@objectos/ui';
-import type { ObjectConfig } from '@objectql/types';
-
-/**
- * Enhanced ObjectListView using ObjectGridTable
- * This is an improved version that uses metadata-driven AG Grid table
- */
-
-interface EnhancedObjectListViewProps {
- objectName: string;
- user: any;
-}
-
-export function EnhancedObjectListView({ objectName, user }: EnhancedObjectListViewProps) {
- const [data, setData] = useState([]);
- const [objectConfig, setObjectConfig] = useState(null);
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState(null);
-
- const getHeaders = useCallback(() => {
- const headers: Record = { 'Content-Type': 'application/json' };
- // Use the actual user ID from props instead of hard-coded value
- if (user?.id || user?._id) {
- headers['x-user-id'] = user.id || user._id;
- }
- return headers;
- }, [user]);
-
- // Fetch object metadata
- useEffect(() => {
- if (!objectName) return;
-
- fetch(`/api/metadata/object/${objectName}`, { headers: getHeaders() })
- .then(async res => {
- if (!res.ok) {
- throw new Error(await res.text() || res.statusText);
- }
- return res.json();
- })
- .then(config => {
- setObjectConfig(config);
- })
- .catch(err => {
- console.error('Failed to load object metadata:', err);
- setError(err.message);
- });
- }, [objectName, getHeaders]);
-
- // Fetch data
- useEffect(() => {
- if (!objectName) return;
-
- setLoading(true);
- setError(null);
-
- fetch(`/api/data/${objectName}`, { headers: getHeaders() })
- .then(async res => {
- if (!res.ok) {
- const contentType = res.headers.get("content-type");
- if (contentType && contentType.indexOf("application/json") !== -1) {
- const json = await res.json();
- throw new Error(json.error || "Failed to load data");
- }
- throw new Error(await res.text() || res.statusText);
- }
- return res.json();
- })
- .then(result => {
- const items = Array.isArray(result) ? result : (result.list || []);
- setData(items);
- })
- .catch(err => {
- console.error(err);
- setError(err.message);
- setData([]);
- })
- .finally(() => setLoading(false));
- }, [objectName, getHeaders]);
-
- const handleSelectionChanged = (selectedRows: any[]) => {
- console.log('Selected rows:', selectedRows);
- // You can add more selection handling logic here
- };
-
- if (error) {
- return (
-
- );
- }
-
- if (!objectConfig) {
- return (
-
-
Loading object metadata...
-
- );
- }
-
- return (
-
-
-
{objectConfig.label || objectName}
- {objectConfig.description && (
-
{objectConfig.description}
- )}
-
-
- {loading ? (
-
- ) : (
-
- )}
-
- );
-}
diff --git a/apps/web/src/components/dashboard/ObjectDetailView.tsx b/apps/web/src/components/dashboard/ObjectDetailView.tsx
deleted file mode 100644
index c3cd2e36..00000000
--- a/apps/web/src/components/dashboard/ObjectDetailView.tsx
+++ /dev/null
@@ -1,136 +0,0 @@
-import { useState, useEffect } from 'react';
-import { Button, Dialog, DialogContent, DialogHeader, DialogTitle, Spinner, ObjectForm } from '@objectos/ui';
-import { getHeaders } from '../../lib/api';
-import { ChevronLeft, Pencil, Trash } from 'lucide-react';
-
-interface ObjectDetailViewProps {
- objectName: string;
- recordId: string;
- navigate: (path: string) => void;
- objectSchema: any;
-}
-
-export function ObjectDetailView({ objectName, recordId, navigate, objectSchema }: ObjectDetailViewProps) {
- const [data, setData] = useState(null);
- const [schema, setSchema] = useState(null);
- const [isEditing, setIsEditing] = useState(false);
- const [loading, setLoading] = useState(true);
-
- const label = objectSchema?.label || objectSchema?.title || objectName;
-
- useEffect(() => {
- setLoading(true);
- Promise.all([
- fetch(`/api/data/${objectName}/${recordId}`, { headers: getHeaders() }).then(async r => {
- if (!r.ok) throw new Error("Failed to load record");
- return r.json();
- }),
- fetch(`/api/metadata/object/${objectName}`, { headers: getHeaders() }).then(r => r.json())
- ]).then(([record, schemaData]) => {
- setData(record);
- setSchema(schemaData);
- }).catch(console.error)
- .finally(() => setLoading(false));
- }, [objectName, recordId]);
-
- const handleDelete = () => {
- if (!confirm('Are you sure you want to delete this record?')) return;
- fetch(`/api/data/${objectName}/${recordId}`, {
- method: 'DELETE',
- headers: getHeaders()
- }).then(() => navigate(`/object/${objectName}`))
- .catch(e => alert(e.message));
- };
-
- const handleUpdate = (formData: any) => {
- fetch(`/api/data/${objectName}/${recordId}`, {
- method: 'PUT',
- headers: getHeaders(),
- body: JSON.stringify(formData)
- }).then(async res => {
- if(!res.ok) throw new Error(await res.text());
- return res.json();
- }).then(() => {
- setIsEditing(false);
- // Reload data
- fetch(`/api/data/${objectName}/${recordId}`, { headers: getHeaders() })
- .then(r => r.json())
- .then(setData);
- }).catch(e => alert(e.message));
- };
-
- if (loading) return (
-
-
-
- );
-
- if (!data) return Record not found
;
-
- return (
-
- {/* Header */}
-
-
-
navigate(`/object/${objectName}`)} className="rounded-full">
-
-
-
-
- {label}
- /
- {recordId}
-
-
{data.name || data.title || recordId}
-
-
-
-
-
setIsEditing(true)} className="gap-2">
-
- Edit
-
-
-
- Delete
-
-
-
-
- {/* Content */}
-
-
- {Object.entries(data).map(([key, value]) => {
- if (['id', '_id', '__v'].includes(key)) return null;
- const fieldLabel = schema?.fields?.[key]?.label || key;
-
- return (
-
-
{fieldLabel}
-
- {typeof value === 'object' ? JSON.stringify(value) : String(value)}
-
-
- )
- })}
-
-
-
-
-
-
- Edit {label}
-
- {schema && (
- setIsEditing(false)}
- />
- )}
-
-
-
- );
-}
diff --git a/apps/web/src/components/dashboard/ObjectListView.tsx b/apps/web/src/components/dashboard/ObjectListView.tsx
deleted file mode 100644
index 0ee797ee..00000000
--- a/apps/web/src/components/dashboard/ObjectListView.tsx
+++ /dev/null
@@ -1,182 +0,0 @@
-import { useState, useEffect, useCallback } from 'react';
-import {
- Button,
- Dialog,
- DialogContent,
- DialogHeader,
- DialogTitle,
- ObjectGridTable,
- Input,
- ObjectForm
-} from '@objectos/ui';
-import { Plus, RefreshCw, Search } from 'lucide-react';
-
-interface ObjectListViewProps {
- objectName: string;
- user: any;
- isCreating: boolean;
- navigate: (path: string) => void;
- objectSchema: any;
-}
-
-export function ObjectListView({ objectName, user, isCreating, navigate, objectSchema }: ObjectListViewProps) {
- const [data, setData] = useState([]);
- const [loading, setLoading] = useState(false);
- const [searchTerm, setSearchTerm] = useState('');
-
- // Use schema label or title or object name
- const label = objectSchema?.label || objectSchema?.title || objectName;
-
- // Headers helper
- const getHeaders = useCallback(() => {
- const headers: Record = { 'Content-Type': 'application/json' };
- if (user?.id || user?._id) headers['x-user-id'] = user.id || user._id;
- return headers;
- }, [user]);
-
- // Data Fetching
- const fetchData = useCallback(() => {
- if (!objectName) return;
- setLoading(true);
-
- const params = new URLSearchParams();
- if (searchTerm) {
- let textFields: string[] = [];
-
- if (objectSchema?.fields) {
- const fieldsArr = Array.isArray(objectSchema.fields)
- ? objectSchema.fields
- : Object.values(objectSchema.fields);
-
- textFields = fieldsArr
- .filter((field: any) => !field.type || field.type === 'string')
- .map((field: any) => field.name);
- } else {
- textFields = ['name', 'title', 'description', 'email'];
- }
-
- if (textFields.length > 0) {
- const searchFilters: any[] = [];
- textFields.forEach((field, index) => {
- // OR logic for simple search
- if (index > 0) searchFilters.push('or');
- searchFilters.push([field, 'contains', searchTerm]);
- });
- params.append('filters', JSON.stringify(searchFilters));
- }
- }
-
- // Add default sort descending by created/updated if possible?
- // params.append('sort', 'created:desc');
-
- fetch(`/api/data/${objectName}?${params.toString()}`, { headers: getHeaders() })
- .then(async res => {
- if (!res.ok) throw new Error(await res.text() || res.statusText);
- return res.json();
- })
- .then(result => {
- const items = Array.isArray(result) ? result : (result.list || result.data || result.value || []);
- setData(items);
- })
- .catch(err => {
- console.error(err);
- setData([]);
- })
- .finally(() => setLoading(false));
- }, [objectName, searchTerm, objectSchema, getHeaders]);
-
- useEffect(() => {
- fetchData();
- }, [objectName, fetchData]); // Re-fetch when objectName changes or fetchData logic changes
-
- const handleCreate = (formData: any) => {
- fetch(`/api/data/${objectName}`, {
- method: 'POST',
- headers: getHeaders(),
- body: JSON.stringify(formData)
- })
- .then(async res => {
- if (!res.ok) throw new Error(await res.text());
- return res.json();
- })
- .then(() => {
- navigate('..');
- fetchData();
- })
- .catch(err => alert(err.message));
- }
-
- const onRowClick = (event: any) => {
- const id = event.data?.id || event.data?._id;
- if (id) navigate(`${id}`);
- };
-
- return (
-
-
-
-
{label}
-
- {data.length} records
-
-
-
-
-
- setSearchTerm(e.target.value)}
- onKeyDown={(e) => e.key === 'Enter' && fetchData()}
- className="pl-8"
- />
-
-
-
- Refresh
-
-
navigate('new')} size="sm">
-
- New
-
-
-
-
-
- {objectSchema ? (
-
- ) : (
-
- Loading schema...
-
- )}
-
-
-
!open && navigate('..')}
- >
-
-
- New {label}
-
- {objectSchema && (
- navigate('..')}
- />
- )}
-
-
-
- );
-}
diff --git a/apps/web/src/components/dashboard/ObjectNotFound.tsx b/apps/web/src/components/dashboard/ObjectNotFound.tsx
deleted file mode 100644
index ce6d7274..00000000
--- a/apps/web/src/components/dashboard/ObjectNotFound.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import { useRouter } from '../../hooks/useRouter';
-
-interface ObjectNotFoundProps {
- objectName: string;
-}
-
-export function ObjectNotFound({ objectName }: ObjectNotFoundProps) {
- const { navigate } = useRouter();
-
- return (
-
-
-
-
-
Object Not Found
-
- The object "{objectName}" does not exist or you do not have permission to view it.
-
-
navigate('/')}
- className="mt-6 px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90"
- >
- Return to Dashboard
-
-
- );
-}
diff --git a/apps/web/src/components/dashboard/SettingsView.tsx b/apps/web/src/components/dashboard/SettingsView.tsx
deleted file mode 100644
index 3a34e1c3..00000000
--- a/apps/web/src/components/dashboard/SettingsView.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import {
- Card,
- CardContent,
- CardDescription,
- CardHeader,
- CardTitle,
- Badge
-} from '@objectos/ui';
-
-interface SettingsViewProps {
- objectCount?: number;
-}
-
-export function SettingsView({ objectCount = 0 }: SettingsViewProps) {
- return (
-
-
-
-
Settings
-
- Manage your server configuration and view system status.
-
-
-
-
- About ObjectOS
- System information and status.
-
-
-
- Version
- v0.2.0
-
-
- Environment
- Development
-
-
- Collections
- {objectCount}
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/components/dashboard/SidebarItem.tsx b/apps/web/src/components/dashboard/SidebarItem.tsx
deleted file mode 100644
index e1b4db71..00000000
--- a/apps/web/src/components/dashboard/SidebarItem.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import React from 'react';
-
-interface SidebarItemProps {
- icon: React.ComponentType<{ className?: string }>;
- label: string;
- active: boolean;
- onClick: () => void;
-}
-
-export function SidebarItem({ icon: Icon, label, active, onClick }: SidebarItemProps) {
- return (
-
-
- {label}
-
- );
-}
diff --git a/apps/web/src/components/nav-user.tsx b/apps/web/src/components/nav-user.tsx
deleted file mode 100644
index 584545f7..00000000
--- a/apps/web/src/components/nav-user.tsx
+++ /dev/null
@@ -1,114 +0,0 @@
-"use client"
-
-import {
- Bell as BellIcon,
- CreditCard as CreditCardIcon,
- LogOut as LogOutIcon,
- MoreVertical as MoreVerticalIcon,
- User as UserCircleIcon,
-} from "lucide-react"
-
-import {
- Avatar,
- AvatarFallback,
- AvatarImage,
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuGroup,
- DropdownMenuItem,
- DropdownMenuLabel,
- DropdownMenuSeparator,
- DropdownMenuTrigger,
- SidebarMenu,
- SidebarMenuButton,
- SidebarMenuItem,
- useSidebar,
-} from "@objectos/ui"
-import { useAuth } from "../context/AuthContext"
-
-export function NavUser({
- user,
-}: {
- user: {
- name: string
- email: string
- avatar?: string
- }
-}) {
- const { isMobile } = useSidebar()
- const { signOut } = useAuth();
-
- return (
-
-
-
-
-
-
-
- CN
-
-
- {user.name}
-
- {user.email}
-
-
-
-
-
-
-
-
-
-
- CN
-
-
- {user.name}
-
- {user.email}
-
-
-
-
-
-
- {
- window.history.pushState({}, '', '/settings');
- window.dispatchEvent(new Event('pushstate'));
- }}>
-
- Settings
-
- {
- window.history.pushState({}, '', '/organization');
- window.dispatchEvent(new Event('pushstate'));
- }}>
-
- Organization
-
-
-
- Notifications
-
-
-
-
-
- Log out
-
-
-
-
-
- )
-}
diff --git a/apps/web/src/components/ui/spinner.tsx b/apps/web/src/components/ui/spinner.tsx
deleted file mode 100644
index 13309001..00000000
--- a/apps/web/src/components/ui/spinner.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import { cn } from "@objectos/ui";
-
-export function Spinner({ className }: { className?: string }) {
- return (
-
- Loading...
-
- );
-}
diff --git a/apps/web/src/context/AuthContext.tsx b/apps/web/src/context/AuthContext.tsx
deleted file mode 100644
index 50ec34ac..00000000
--- a/apps/web/src/context/AuthContext.tsx
+++ /dev/null
@@ -1,98 +0,0 @@
-import React, { createContext, useContext, useEffect, useState } from 'react';
-import { authClient } from '../lib/auth';
-
-interface User {
- id: string;
- email: string;
- name?: string;
- [key: string]: any;
-}
-
-interface Session {
- session: {
- id: string;
- userId: string;
- activeOrganizationId?: string | null;
- [key: string]: any;
- };
- user: User;
-}
-
-interface AuthContextType {
- user: User | null;
- session: Session["session"] | null;
- loading: boolean;
- signIn: (email: string, password: string) => Promise;
- signUp: (email: string, password: string, name: string) => Promise;
- signOut: () => Promise;
-}
-
-const AuthContext = createContext(undefined);
-
-export function AuthProvider({ children }: { children: React.ReactNode }) {
- const [user, setUser] = useState(null);
- const [session, setSession] = useState(null);
- const [loading, setLoading] = useState(true);
-
- useEffect(() => {
- authClient.getSession().then(({ data }) => {
- if (data?.user) {
- setUser(data.user);
- setSession(data.session);
- } else {
- setUser(null);
- setSession(null);
- }
- setLoading(false);
- }).catch(() => setLoading(false));
- }, []);
-
- const signIn = async (email: string, password: string) => {
- const { error } = await authClient.signIn.email({
- email,
- password
- });
- if (error) throw error;
- // Refresh session to Ensure we get the full session object
- const sessionData = await authClient.getSession();
- if (sessionData.data) {
- setUser(sessionData.data.user);
- setSession(sessionData.data.session);
- }
- };
-
- const signUp = async (email: string, password: string, name: string) => {
- const { error } = await authClient.signUp.email({
- email,
- password,
- name
- });
- if (error) throw error;
- // Refresh session
- const sessionData = await authClient.getSession();
- if (sessionData.data) {
- setUser(sessionData.data.user);
- setSession(sessionData.data.session);
- }
- };
-
- const signOut = async () => {
- await authClient.signOut();
- setUser(null);
- setSession(null);
- };
-
- return (
-
- {children}
-
- );
-}
-
-export function useAuth() {
- const context = useContext(AuthContext);
- if (context === undefined) {
- throw new Error('useAuth must be used within an AuthProvider');
- }
- return context;
-}
diff --git a/apps/web/src/hooks/useObjectSchema.ts b/apps/web/src/hooks/useObjectSchema.ts
deleted file mode 100644
index 91244eb3..00000000
--- a/apps/web/src/hooks/useObjectSchema.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-import { useState, useEffect } from 'react';
-import { getHeaders } from '../lib/api';
-
-const schemaCache: Record = {};
-
-export function useObjectSchema(objectName: string) {
- const [schema, setSchema] = useState(null);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
-
- useEffect(() => {
- if (!objectName) return;
-
- if (schemaCache[objectName]) {
- setSchema(schemaCache[objectName]);
- setLoading(false);
- return;
- }
-
- setLoading(true);
- // We could fetch single object too: /api/metadata/object/:name
- // But current API might be bulk. Let's try single if available or filter from bulk (inefficient but works for now)
- // Optimization: Create /api/metadata/object/:name endpoint in backend or assume bulk cache in context.
- // For now, let's fetch list and find. (Or just assume the API supports name, which standard ObjectQL usually does)
-
- fetch(`/api/metadata/object/${objectName}`, { headers: getHeaders() })
- .then(async res => {
- if (res.status === 404) return null; // Not found
- if (!res.ok) {
- // Fallback to bulk if single endpoint fails?
- // Let's try bulk list as fallback or primay if we know backend
- throw new Error('Failed to load schema');
- }
- return res.json();
- })
- .then(data => {
- if (data) {
- // Normalize fields from Array to Record if needed
- if (data.fields && Array.isArray(data.fields)) {
- const fieldRecord: Record = {};
- data.fields.forEach((f: any) => {
- if (f.name) fieldRecord[f.name] = f;
- });
- data.fields = fieldRecord;
- }
-
- schemaCache[objectName] = data;
- setSchema(data);
- setLoading(false);
- } else {
- // Trigger fallback
- throw new Error('Not found');
- }
- })
- .catch(() => {
- // Fallback: Fetch all
- fetch('/api/metadata/object', { headers: getHeaders() })
- .then(res => res.json())
- .then(result => {
- const list = Array.isArray(result) ? result : (result.object || result.data || []);
- const found = list.find((o: any) => o.name === objectName);
- if (found) {
- schemaCache[objectName] = found;
- setSchema(found);
- setError(null);
- } else {
- setError(new Error('Object not found'));
- }
- })
- .catch(e => setError(e))
- .finally(() => setLoading(false));
- });
-
- }, [objectName]);
-
- return { schema, loading, error };
-}
diff --git a/apps/web/src/hooks/useRouter.ts b/apps/web/src/hooks/useRouter.ts
deleted file mode 100644
index dd3bfd41..00000000
--- a/apps/web/src/hooks/useRouter.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { useNavigate, useLocation } from 'react-router-dom';
-
-export function useRouter() {
- const navigate = useNavigate();
- const location = useLocation();
-
- return {
- path: location.pathname,
- navigate: (path: string) => navigate(path),
- search: location.search
- };
-}
diff --git a/apps/web/src/index.css b/apps/web/src/index.css
deleted file mode 100644
index d4b50785..00000000
--- a/apps/web/src/index.css
+++ /dev/null
@@ -1 +0,0 @@
-@import 'tailwindcss';
diff --git a/apps/web/src/layouts/MainLayout.tsx b/apps/web/src/layouts/MainLayout.tsx
deleted file mode 100644
index 4b46ee80..00000000
--- a/apps/web/src/layouts/MainLayout.tsx
+++ /dev/null
@@ -1,82 +0,0 @@
-import {
- Avatar,
- AvatarFallback,
- AvatarImage,
- DropdownMenu,
- DropdownMenuTrigger,
- DropdownMenuContent,
- DropdownMenuLabel,
- DropdownMenuSeparator,
- DropdownMenuGroup,
- DropdownMenuItem
-} from '@objectos/ui';
-import { LogOut, Settings as SettingsIcon, Building, Bell } from 'lucide-react';
-import { useAuth } from '../context/AuthContext';
-import { Outlet } from 'react-router-dom';
-
-export function MainLayout() {
- const { user, signOut } = useAuth();
-
- return (
-
-
-
- ObjectOS
-
-
- {/* User Menu */}
-
-
-
-
-
- CN
-
-
-
-
-
-
-
-
- CN
-
-
- {user?.name}
- {user?.email}
-
-
-
-
-
-
-
- Organization
-
-
-
- Settings
-
-
-
- Notifications
-
-
-
-
-
- Log out
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/layouts/WorkspaceLayout.tsx b/apps/web/src/layouts/WorkspaceLayout.tsx
deleted file mode 100644
index f67ef149..00000000
--- a/apps/web/src/layouts/WorkspaceLayout.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-import { useState, useEffect, useRef } from 'react';
-import {
- SidebarProvider,
- SidebarInset,
- SidebarTrigger,
- Separator,
- Breadcrumb,
- BreadcrumbItem,
- BreadcrumbList,
- BreadcrumbPage
-} from '@objectos/ui';
-import { AppSidebar } from '../components/app-sidebar';
-import { Outlet, useLocation } from 'react-router-dom';
-
-export function WorkspaceLayout() {
- const location = useLocation();
- const [currentAppMetadata, setCurrentAppMetadata] = useState(null);
- const lastFetchedApp = useRef(null);
-
- // Determines current app from URL parameters or path parsing
- // Since this layout wraps routes like /app/:appName/*, we can try to extract it from location if useParams isn't populated yet by the parent?
- // Actually, in clustered routes, useParams matches the current route match.
- // If the Route is }>, then params.appName works.
- // But if we use nested routes, we might need to parse.
-
- // Simplest: Check path parts
- const appName = location.pathname.split('/')[2];
- const isAppRoute = location.pathname.startsWith('/app/');
-
- useEffect(() => {
- if (isAppRoute && appName) {
- // Avoid re-fetching if we already have this app loaded
- if (lastFetchedApp.current === appName) {
- return;
- }
-
- lastFetchedApp.current = appName;
-
- fetch(`/api/metadata/app/${appName}`)
- .then(res => {
- if (!res.ok) throw new Error('App not found');
- return res.json();
- })
- .then(data => {
- setCurrentAppMetadata(data);
- })
- .catch(err => {
- console.error(err);
- setCurrentAppMetadata(null);
- lastFetchedApp.current = null;
- });
- } else {
- if (lastFetchedApp.current) {
- setCurrentAppMetadata(null);
- lastFetchedApp.current = null;
- }
- }
- }, [isAppRoute, appName]);
-
- const getPageTitle = () => {
- if (location.pathname === '/settings') return 'Settings';
- if (location.pathname === '/organization') return 'Organization';
- if (appName) return `App: ${appName}`;
- return 'Dashboard';
- };
-
- return (
-
-
-
-
-
-
-
-
-
-
- {getPageTitle()}
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/web/src/lib/api.ts b/apps/web/src/lib/api.ts
deleted file mode 100644
index 0d7e44d5..00000000
--- a/apps/web/src/lib/api.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export const getHeaders = () => {
- const headers: Record = { 'Content-Type': 'application/json' };
- return headers;
-};
diff --git a/apps/web/src/lib/auth.ts b/apps/web/src/lib/auth.ts
deleted file mode 100644
index 904f147c..00000000
--- a/apps/web/src/lib/auth.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { createAuthClient } from "better-auth/react"
-import { organizationClient } from "better-auth/client/plugins"
-
-export const authClient = createAuthClient({
- baseURL: typeof window !== "undefined" ? window.location.origin + "/api/auth" : "http://localhost:3000/api/auth",
- plugins: [
- organizationClient()
- ]
-})
diff --git a/apps/web/src/lib/utils.ts b/apps/web/src/lib/utils.ts
deleted file mode 100644
index d084ccad..00000000
--- a/apps/web/src/lib/utils.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { type ClassValue, clsx } from "clsx"
-import { twMerge } from "tailwind-merge"
-
-export function cn(...inputs: ClassValue[]) {
- return twMerge(clsx(inputs))
-}
diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx
deleted file mode 100644
index 21e96b9c..00000000
--- a/apps/web/src/main.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import React from 'react'
-import ReactDOM from 'react-dom/client'
-import { BrowserRouter } from 'react-router-dom'
-import App from './App'
-import { ModuleRegistry, AllCommunityModule } from 'ag-grid-community';
-
-// Register AG Grid Modules
-ModuleRegistry.registerModules([ AllCommunityModule ]);
-
-import 'ag-grid-community/styles/ag-grid.css'
-import 'ag-grid-community/styles/ag-theme-alpine.css'
-import '@objectos/ui/dist/index.css'
-import './index.css'
-
-ReactDOM.createRoot(document.getElementById('root')!).render(
-
-
-
-
- ,
-)
diff --git a/apps/web/src/pages/AppDashboard.tsx b/apps/web/src/pages/AppDashboard.tsx
deleted file mode 100644
index 4560c3cf..00000000
--- a/apps/web/src/pages/AppDashboard.tsx
+++ /dev/null
@@ -1,113 +0,0 @@
-import { useState, useEffect } from 'react';
-import { useParams } from 'react-router-dom';
-import {
- Card,
- Spinner
-} from '@objectos/ui';
-import { useRouter } from '../hooks/useRouter';
-import { DynamicIcon } from '../components/DynamicIcon';
-import { getHeaders } from '../lib/api';
-
-export default function AppDashboard() {
- const { appName } = useParams();
- const { navigate } = useRouter();
- const [app, setApp] = useState(null);
- const [loading, setLoading] = useState(true);
-
- useEffect(() => {
- if (!appName) return;
- setLoading(true);
- fetch(`/api/metadata/app/${appName}`, { headers: getHeaders() })
- .then(res => {
- if (!res.ok) throw new Error('App not found');
- return res.json();
- })
- .then(data => {
- setApp(data);
- setLoading(false);
- })
- .catch(err => {
- console.error(err);
- setLoading(false);
- });
- }, [appName]);
-
- if (loading) return
;
- if (!app) return App not found
;
-
- // Helper to resolve absolute vs relative paths
- const resolveLink = (item: any) => {
- if (item.object) return `/app/${appName}/object/${item.object}`;
- if (item.page) return `/app/${appName}/page/${item.page}`;
- if (item.url) return item.url;
- return '#';
- };
-
- const MenuItemCard = ({ item }: { item: any }) => (
- navigate(resolveLink(item))}
- >
-
-
-
- {item.label || item.object || 'Item'}
- {item.description && {item.description} }
-
- );
-
- const MenuSection = ({ section }: { section: any }) => {
- const title = section.label;
- const items = section.items || [];
-
- if (!items.length) return null;
-
- return (
-
- {title && !section._isDefaultWrapper && (
-
- {section.icon && }
- {title}
-
- )}
-
- {items.map((item: any, idx: number) => (
-
- ))}
-
-
- );
- };
-
- // Prepare sections
- const rawMenu = app.menu;
- const isSection = (item: any) => item && item.items && Array.isArray(item.items) && !item.type && !item.object && !item.url;
- const isGrouped = Array.isArray(rawMenu) && rawMenu.length > 0 && isSection(rawMenu[0]);
- const sections = rawMenu ? (isGrouped ? rawMenu : [{ label: 'Menu', items: rawMenu, _isDefaultWrapper: true }]) : [];
-
- return (
-
-
-
-
-
-
-
-
{app.label || app.name}
-
{app.description || `Welcome to ${app.label || app.name}`}
-
-
-
-
- {sections.length > 0 ? (
- sections.map((section: any, idx: number) => (
-
- ))
- ) : (
-
-
This app has no menu items configured.
-
- )}
-
- );
-}
diff --git a/apps/web/src/pages/AppList.tsx b/apps/web/src/pages/AppList.tsx
deleted file mode 100644
index 07c5c77e..00000000
--- a/apps/web/src/pages/AppList.tsx
+++ /dev/null
@@ -1,119 +0,0 @@
-import { useState, useEffect } from 'react';
-import {
- Card,
- CardHeader,
- CardTitle,
- CardDescription,
- CardContent,
- Button,
- Spinner
-} from '@objectos/ui';
-import { useRouter } from '../hooks/useRouter';
-import { getHeaders } from '../lib/api';
-import { AppWindow, ChevronRight } from 'lucide-react';
-import { DynamicIcon } from '../components/DynamicIcon';
-
-interface App {
- id?: string;
- name: string;
- label?: string; // App Label for display
- description?: string;
- code?: string;
- icon?: string;
- color?: string;
- dark?: boolean;
- menu?: any[];
-}
-
-export default function AppList() {
- const [apps, setApps] = useState([]);
- const [loading, setLoading] = useState(true);
- const { navigate } = useRouter();
-
- useEffect(() => {
- fetch('/api/metadata/app', { headers: getHeaders() })
- .then(res => res.json())
- .then(data => {
- // Handle wrapped response { app: [...] } or direct array [...]
- const appList = Array.isArray(data) ? data : (data.app || []);
- if (Array.isArray(appList)) {
- setApps(appList);
- }
- setLoading(false);
- })
- .catch(err => {
- console.error('Failed to load apps', err);
- setLoading(false);
- });
- }, []);
-
- if (loading) {
- return
;
- }
-
- return (
-
-
My Apps
-
Select an application to start working
-
- {apps.length === 0 ? (
-
-
-
No apps found
-
- Get started by creating your first application in the backend configuration.
-
-
- ) : (
-
- {apps.map((app, idx) => {
- const appCode = app.code || app.id || app.name; // Fallback for code
- const appName = app.label || app.name; // Use label if available, fallback to name
- const appColor = app.color;
- const key = app.id || app.code || `app-${idx}`;
-
- return (
-
navigate(`/app/${appCode}`)}
- >
- {/* Color strip on top or side if defined */}
- {appColor && (
-
- )}
-
-
-
- {appName}
-
- {app.description || 'No description provided.'}
-
-
-
- {/* Additional info bits */}
-
-
- );
- })}
-
- )}
-
- );
-}
diff --git a/apps/web/src/pages/Dashboard.tsx b/apps/web/src/pages/Dashboard.tsx
deleted file mode 100644
index 29a6fed3..00000000
--- a/apps/web/src/pages/Dashboard.tsx
+++ /dev/null
@@ -1,129 +0,0 @@
-import { useState, useEffect } from 'react';
-import { Spinner } from '@objectos/ui';
-import { useAuth } from '../context/AuthContext';
-import { useRouter } from '../hooks/useRouter';
-import { ObjectListView } from '../components/dashboard/ObjectListView';
-import { ObjectDetailView } from '../components/dashboard/ObjectDetailView';
-import { SettingsView } from '../components/dashboard/SettingsView';
-import { ObjectNotFound } from '../components/dashboard/ObjectNotFound';
-import { DashboardHome } from './DashboardHome';
-import { getHeaders } from '../lib/api';
-
-export default function Dashboard() {
- const { user } = useAuth();
- const [loading, setLoading] = useState(true);
- const [objects, setObjects] = useState>({});
- const [apps, setApps] = useState([]);
- const { path, navigate } = useRouter();
-
- // Parse path
- // Support patterns:
- // 1. /object/:objectName/:recordId?
- // 2. /app/:appName/object/:objectName/:recordId?
- const parts = path.split('/');
-
- let objectName: string | null = null;
- let recordId: string | undefined = undefined;
- let appName: string | null = null;
-
- if (parts[1] === 'object') {
- objectName = parts[2];
- recordId = parts[3];
- } else if (parts[1] === 'app' && parts[3] === 'object') {
- appName = parts[2];
- objectName = parts[4];
- recordId = parts[5];
- }
-
- // Wrap navigate to preserve app context
- const wrappedNavigate = (to: string) => {
- if (appName && to.startsWith('/object/')) {
- navigate(to.replace('/object/', `/app/${appName}/object/`));
- } else {
- navigate(to);
- }
- };
-
- useEffect(() => {
- if (user) {
- fetch('/api/data/app?limit=100', { headers: getHeaders() })
- .then(res => res.json())
- .then(result => {
- const data = Array.isArray(result) ? result : (result.data || []);
- setApps(data);
- })
- .catch(console.error);
-
- // Fetch objects
- fetch('/api/metadata/object', { headers: getHeaders() })
- .then(res => res.json())
- .then(result => {
- // Handle potential wrapper format { object: [...] } or { data: [...] }
- const list = Array.isArray(result) ? result : (result.object || result.data || []);
-
- // Convert array to map
- const objectsMap: Record = {};
- if (Array.isArray(list)) {
- list.forEach((obj: any) => {
- if (obj && obj.name) {
- objectsMap[obj.name] = obj;
- }
- });
- }
-
- setObjects(objectsMap);
- setLoading(false);
- })
- .catch(err => {
- console.error("Failed to fetch objects", err);
- setLoading(false);
- });
- }
- }, [user]);
-
- if (loading) {
- return (
-
-
-
- );
- }
-
- // Since Dashboard is now rendered INSIDE App.tsx's Layout,
- // we only need to render the content specific to the route
-
- if (path === '/settings') {
- return ;
- }
-
- // Handle Object Routes
- if (objectName) {
- if (objects[objectName]) {
- if (recordId) {
- return (
-
- );
- }
- return (
-
- );
- } else {
- // Object requested but not found in metadata
- return ;
- }
- }
-
- // Default Dashboard View (App Selection)
- return ;
-}
diff --git a/apps/web/src/pages/DashboardHome.tsx b/apps/web/src/pages/DashboardHome.tsx
deleted file mode 100644
index f6a76e09..00000000
--- a/apps/web/src/pages/DashboardHome.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-import {
- Card,
- CardHeader,
- CardTitle,
- CardDescription
-} from '@objectos/ui';
-import { useRouter } from '../hooks/useRouter';
-
-interface App {
- id?: string;
- _id?: string;
- name: string;
- slug?: string;
- description?: string;
- icon?: string;
- objects?: string[];
-}
-
-interface DashboardHomeProps {
- apps: App[];
-}
-
-export function DashboardHome({ apps }: DashboardHomeProps) {
- const { navigate } = useRouter();
-
- return (
-
-
-
-
Applications
-
- Select an application to start working.
-
-
-
-
-
- {apps.map(app => (
-
{
- // Navigate to first object or app root
- if (app.objects && Array.isArray(app.objects) && app.objects.length > 0) {
- navigate(`/app/${app.slug || app.name}/object/${app.objects[0]}`);
- } else {
- navigate(`/app/${app.slug || app.name}`);
- }
- }}
- >
-
-
-
-
-
- {app.name}
-
- {app.description || 'No description provided'}
-
-
-
-
- ))}
-
- {apps.length === 0 && (
-
-
-
-
-
No Applications Found
-
Admin needs to create an application first.
-
- )}
-
-
- );
-}
diff --git a/apps/web/src/pages/Login.tsx b/apps/web/src/pages/Login.tsx
deleted file mode 100644
index 1994d548..00000000
--- a/apps/web/src/pages/Login.tsx
+++ /dev/null
@@ -1,115 +0,0 @@
-import { useState } from 'react';
-import { Card, Input, Button, Label, Spinner } from '@objectos/ui';
-import { useAuth } from '../context/AuthContext';
-import { Database } from 'lucide-react';
-
-export default function Login() {
- const { signIn, signUp } = useAuth();
- const [isSignIn, setIsSignIn] = useState(true);
- const [email, setEmail] = useState('');
- const [password, setPassword] = useState('');
- const [name, setName] = useState('');
- const [error, setError] = useState('');
- const [loading, setLoading] = useState(false);
-
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
- setError('');
- setLoading(true);
- try {
- if (isSignIn) {
- await signIn(email, password);
- } else {
- await signUp(email, password, name);
- }
- // App component handles redirection via auth state change
- } catch (err: any) {
- console.error(err);
- setError(err.message || err.error?.message || 'Authentication failed');
- setLoading(false);
- }
- };
-
- return (
-
- {/* Decorative background elements */}
-
-
-
-
-
-
- {isSignIn ? 'Welcome back' : 'Create account'}
-
-
- {isSignIn ? 'Enter your details to access your workspace' : 'Start your journey with ObjectOS'}
-
-
-
-
-
-
-
-
- {isSignIn ? "New to ObjectQL? " : "Already have an account? "}
- { setIsSignIn(!isSignIn); setError(''); }}
- className="font-medium text-[#0071e3] hover:text-[#0077ED] transition-colors"
- >
- {isSignIn ? "Sign up now" : "Log in"}
-
-
-
-
- );
-}
diff --git a/apps/web/src/pages/Organization.tsx b/apps/web/src/pages/Organization.tsx
deleted file mode 100644
index 8e2f4df0..00000000
--- a/apps/web/src/pages/Organization.tsx
+++ /dev/null
@@ -1,191 +0,0 @@
-import { useEffect, useState } from 'react';
-import { Card, Input, Label, Button, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@objectos/ui';
-import { useAuth } from '../context/AuthContext';
-import { authClient } from '../lib/auth';
-
-export default function Organization() {
- const { session } = useAuth();
- const [organizations, setOrganizations] = useState([]);
- const [activeOrg, setActiveOrg] = useState(null);
- const [members, setMembers] = useState([]);
- const [loading, setLoading] = useState(true);
-
- // Create Org Form
- const [newOrgName, setNewOrgName] = useState('');
- const [newOrgSlug, setNewOrgSlug] = useState('');
-
- // Invite Member Form
- const [inviteEmail, setInviteEmail] = useState('');
- const [inviteRole, setInviteRole] = useState('user');
-
- useEffect(() => {
- loadOrganizations();
- }, [session?.activeOrganizationId]);
-
- const loadOrganizations = async () => {
- try {
- const { data: orgs } = await authClient.organization.list();
- setOrganizations(orgs || []);
-
- if (session?.activeOrganizationId) {
- const current = orgs?.find(o => o.id === session.activeOrganizationId);
- setActiveOrg(current);
- if (current) loadMembers();
- }
- } catch (e) {
- console.error(e);
- } finally {
- setLoading(false);
- }
- };
-
- const loadMembers = async () => {
- // listMembers uses the current active organization from session/headers
- const { data } = await authClient.organization.listMembers();
- setMembers(data?.members || []);
- };
-
- const createOrg = async () => {
- const { data, error } = await authClient.organization.create({
- name: newOrgName,
- slug: newOrgSlug
- });
- if (error) alert(error.message);
- if (data) {
- await authClient.organization.setActive({ organizationId: data.id });
- window.location.reload(); // Simple reload to refresh context
- }
- };
-
- const inviteMember = async () => {
- if (!activeOrg) return;
- const { data, error } = await authClient.organization.inviteMember({
- email: inviteEmail,
- role: inviteRole as "member" | "admin" | "owner",
- });
- if (error) alert(error.message);
- if (data) {
- setInviteEmail('');
- alert('Invitation sent!');
- }
- };
-
- if (loading) return Loading...
;
-
- return (
-
-
-
Organization Management
- {!activeOrg && (
-
-
- Create Organization
-
-
-
- Create New Organization
-
-
-
- Name
- setNewOrgName(e.target.value)} />
-
-
- Slug
- setNewOrgSlug(e.target.value)} />
-
-
Create
-
-
-
- )}
-
-
- {!activeOrg ? (
-
- You are not currently in an active organization.
- {organizations.length > 0 && (
-
-
Your Organizations:
-
- {organizations.map(org => (
- {
- await authClient.organization.setActive({ organizationId: org.id });
- window.location.reload();
- }}>
- Switch to {org.name}
-
- ))}
-
-
- )}
-
- ) : (
- <>
-
-
-
-
{activeOrg.name}
-
Slug: {activeOrg.slug}
-
-
{
- await authClient.organization.setActive({ organizationId: null });
- window.location.reload();
- }}>
- Exit Organization
-
-
-
-
-
-
-
Members
-
-
- Invite Member
-
-
-
- Invite Member
-
-
-
- Email
- setInviteEmail(e.target.value)} />
-
-
- Role
- setInviteRole(e.target.value)} />
-
-
Send Invitation
-
-
-
-
-
-
-
-
- User
- Email
- Role
- Joined
-
-
-
- {members.map(member => (
-
- {member.user.name}
- {member.user.email}
- {member.role}
- {new Date(member.createdAt).toLocaleDateString()}
-
- ))}
-
-
-
- >
- )}
-
- );
-}
diff --git a/apps/web/src/pages/Settings.tsx b/apps/web/src/pages/Settings.tsx
deleted file mode 100644
index d56f29c8..00000000
--- a/apps/web/src/pages/Settings.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import { Card, Input, Label, Button } from '@objectos/ui';
-import { useAuth } from '../context/AuthContext';
-
-export default function Settings() {
- const { user, signOut } = useAuth();
-
- return (
-
-
Account Settings
-
-
- Profile Information
-
-
-
-
- Danger Zone
-
- Sign out of your account on this device.
-
- signOut()}>
- Sign Out
-
-
-
- );
-}
diff --git a/apps/web/src/pages/objects/ObjectDetailRoute.tsx b/apps/web/src/pages/objects/ObjectDetailRoute.tsx
deleted file mode 100644
index 5eb59e31..00000000
--- a/apps/web/src/pages/objects/ObjectDetailRoute.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import { useParams } from 'react-router-dom';
-import { ObjectDetailView } from '../../components/dashboard/ObjectDetailView';
-import { useObjectSchema } from '../../hooks/useObjectSchema';
-import { ObjectNotFound } from '../../components/dashboard/ObjectNotFound';
-import { Spinner } from '@objectos/ui';
-import { useRouter } from '../../hooks/useRouter';
-
-export function ObjectDetailRoute() {
- const { objectName, recordId } = useParams(); // Matches /view/:recordId
- // Also support legacy param if needed by checking path, but let's stick to standard params
- const id = recordId;
-
- const { schema, loading, error } = useObjectSchema(objectName || '');
- const { navigate } = useRouter();
-
- if (loading) return
;
- if (error || !schema) return ;
-
- return (
-
- );
-}
diff --git a/apps/web/src/pages/objects/ObjectListRoute.tsx b/apps/web/src/pages/objects/ObjectListRoute.tsx
deleted file mode 100644
index 6d057a6a..00000000
--- a/apps/web/src/pages/objects/ObjectListRoute.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import { useParams } from 'react-router-dom';
-import { ObjectListView } from '../../components/dashboard/ObjectListView';
-import { useObjectSchema } from '../../hooks/useObjectSchema';
-import { useAuth } from '../../context/AuthContext';
-import { ObjectNotFound } from '../../components/dashboard/ObjectNotFound';
-import { Spinner } from '@objectos/ui';
-import { useRouter } from '../../hooks/useRouter';
-
-export function ObjectListRoute({ isCreating = false }: { isCreating?: boolean }) {
- const { objectName } = useParams();
- const { schema, loading, error } = useObjectSchema(objectName || '');
- const { user } = useAuth();
- const { navigate } = useRouter();
-
- if (loading) return
;
-
- // In React Router v6, error boundary is better, but simple check works
- if (error || !schema) return ;
-
- return (
-
- );
-}
diff --git a/apps/web/src/routes.ts b/apps/web/src/routes.ts
deleted file mode 100644
index 61e254c2..00000000
--- a/apps/web/src/routes.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-// Root Routes
-export const ROOT = '/';
-export const LOGIN = '/login';
-export const APPS = '/apps';
-
-// App Routes (Workspace)
-export const APP_ROOT = '/app/:appName';
-export const APP_DASHBOARD = '/app/:appName/dashboard';
-export const APP_OBJECT_LIST = '/app/:appName/object/:objectName';
-export const APP_OBJECT_DETAIL = '/app/:appName/object/:objectName/view/:recordId';
-export const APP_OBJECT_EDIT = '/app/:appName/object/:objectName/edit/:recordId';
-export const APP_OBJECT_NEW = '/app/:appName/object/:objectName/new';
-export const APP_PAGE = '/app/:appName/page/:pageId';
-
-// Global Routes (Legacy/Global Context)
-export const OBJECT_LIST = '/object/:objectName';
-export const OBJECT_DETAIL = '/object/:objectName/view/:recordId'; // Standardized view/edit
-export const SETTINGS = '/settings';
-export const ORGANIZATION = '/organization';
-export const USER_PROFILE = '/user/profile';
-
-// Route Generators
-export const routes = {
- root: () => ROOT,
- login: () => LOGIN,
- apps: () => APPS,
- app: (appName: string) => `/app/${appName}`,
- objectList: (appName: string, objectName: string) => `/app/${appName}/object/${objectName}`,
- objectDetail: (appName: string, objectName: string, id: string) => `/app/${appName}/object/${objectName}/view/${id}`,
- objectEdit: (appName: string, objectName: string, id: string) => `/app/${appName}/object/${objectName}/edit/${id}`,
- objectNew: (appName: string, objectName: string) => `/app/${appName}/object/${objectName}/new`,
-};
diff --git a/apps/web/tailwind.config.js b/apps/web/tailwind.config.js
deleted file mode 100644
index bd8131e2..00000000
--- a/apps/web/tailwind.config.js
+++ /dev/null
@@ -1,7 +0,0 @@
-/** @type {import('tailwindcss').Config} */
-export default {
- content: [
- "./index.html",
- "./src/**/*.{js,ts,jsx,tsx}",
- ],
-}
diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json
deleted file mode 100644
index 3934b8f6..00000000
--- a/apps/web/tsconfig.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "compilerOptions": {
- "target": "ES2020",
- "useDefineForClassFields": true,
- "lib": ["ES2020", "DOM", "DOM.Iterable"],
- "module": "ESNext",
- "skipLibCheck": true,
- "moduleResolution": "bundler",
- "allowImportingTsExtensions": true,
- "resolveJsonModule": true,
- "isolatedModules": true,
- "noEmit": true,
- "jsx": "react-jsx",
- "strict": true,
- "noUnusedLocals": true,
- "noUnusedParameters": true,
- "noFallthroughCasesInSwitch": true
- },
- "include": ["src"],
- "references": [{ "path": "./tsconfig.node.json" }]
-}
diff --git a/apps/web/tsconfig.node.json b/apps/web/tsconfig.node.json
deleted file mode 100644
index 9b748374..00000000
--- a/apps/web/tsconfig.node.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "compilerOptions": {
- "composite": true,
- "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
- "skipLibCheck": true,
- "module": "ESNext",
- "moduleResolution": "bundler",
- "allowSyntheticDefaultImports": true,
- "strict": true,
- "noEmit": false,
- "emitDeclarationOnly": true
- },
- "include": ["vite.config.ts"]
-}
diff --git a/apps/web/vite.config.d.ts b/apps/web/vite.config.d.ts
deleted file mode 100644
index 340562af..00000000
--- a/apps/web/vite.config.d.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-declare const _default: import("vite").UserConfig;
-export default _default;
diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts
deleted file mode 100644
index a883de68..00000000
--- a/apps/web/vite.config.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { defineConfig } from 'vite'
-import react from '@vitejs/plugin-react'
-import path from 'path'
-
-// https://vitejs.dev/config/
-export default defineConfig({
- plugins: [react() as any],
- resolve: {
- alias: {
- '@': path.resolve(__dirname, './src'),
- },
- },
- server: {
- proxy: {
- '/api': {
- target: 'http://localhost:3000',
- changeOrigin: true,
- }
- }
- }
-})
diff --git a/docs/SPEC_REFACTORING.md b/docs/SPEC_REFACTORING.md
index 716b04b4..83c4050e 100644
--- a/docs/SPEC_REFACTORING.md
+++ b/docs/SPEC_REFACTORING.md
@@ -2,7 +2,7 @@
## Overview
-This document describes the refactoring of ObjectOS to align with the [@objectstack/spec](https://www.npmjs.com/package/@objectstack/spec) protocol version 0.3.3.
+This document describes the refactoring of ObjectOS to align with the [@objectstack/spec](https://github.com/objectstack-ai/spec) protocol version 0.3.3.
## What is @objectstack/spec?
@@ -253,7 +253,7 @@ See `packages/kernel/src/plugins/example-spec-plugin.ts` for a complete, product
## Resources
-- [@objectstack/spec on npm](https://www.npmjs.com/package/@objectstack/spec)
+- [@objectstack/spec on GitHub](https://github.com/objectstack-ai/spec)
- [Example Plugin](../packages/kernel/src/plugins/example-spec-plugin.ts)
- [Kernel README](../packages/kernel/README.md)
- [Architecture Documentation](../ARCHITECTURE.md)
diff --git a/docs/guide/index.md b/docs/guide/index.md
index 88a4fa8d..22bba5e7 100644
--- a/docs/guide/index.md
+++ b/docs/guide/index.md
@@ -21,7 +21,6 @@ ObjectOS is a **metadata-driven runtime engine** that interprets and executes bu
│ ObjectOS (Runtime Repository - This One) │
│ - @objectos/kernel: Execution engine │
│ - @objectos/server: HTTP API layer │
-│ - @objectos/ui: React components │
└─────────────────────┬───────────────────────────┘
│
▼
@@ -302,22 +301,9 @@ curl -X POST http://localhost:3000/api/data/contacts \
}'
```
-## Using the UI Components
+## Using UI Components
-ObjectOS provides React components that automatically render based on metadata:
-
-```tsx
-import { ObjectGrid, ObjectForm } from '@objectos/ui';
-
-function ContactsPage() {
- return (
-
-
Contacts
- {/* Automatically generates a data grid */}
-
-
- );
-}
+**Note**: The UI components have been moved to a separate project. They can be integrated with ObjectOS through the HTTP API layer (`@objectos/server`) which provides REST endpoints for metadata and data access.
function ContactDetail({ contactId }) {
return (
diff --git a/docs/guide/logic-actions.md b/docs/guide/logic-actions.md
index f61a56d3..c12455c9 100644
--- a/docs/guide/logic-actions.md
+++ b/docs/guide/logic-actions.md
@@ -464,20 +464,24 @@ const result = await kernel.executeAction('contacts.sendEmail', {
### From UI
-```typescript
-import { useAction } from '@objectos/ui';
+> **Note**: The UI components have been moved to a separate project. The example below shows the conceptual pattern for invoking actions from a UI application.
+```typescript
+// Example pattern for UI integration
function ContactDetail({ contactId }) {
- const sendEmail = useAction('contacts.sendEmail');
-
const handleSendEmail = async () => {
- const result = await sendEmail({
- id: contactId,
- subject: 'Follow up',
- body: 'Thank you for your interest'
+ const result = await fetch('/api/actions/contacts.sendEmail', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ id: contactId,
+ subject: 'Follow up',
+ body: 'Thank you for your interest'
+ })
});
- alert(result.message);
+ const data = await result.json();
+ alert(data.message);
};
return (
diff --git a/docs/guide/platform-components.md b/docs/guide/platform-components.md
index f9a16620..7e1fa814 100644
--- a/docs/guide/platform-components.md
+++ b/docs/guide/platform-components.md
@@ -40,7 +40,7 @@ The `@objectos/server` package is the Gateway. It translates HTTP/WebSockets int
| :--- | :--- | :--- |
| **ObjectQLController** | Generic REST endpoint for all objects. | `GET /:objectName/*`. No need to write manual controllers for new objects. |
| **AuthProvider** | Authentication strategy manager. | Wraps `better-auth`. Supports pluggable strategies (GitHub, Google, SSO). |
-| **StaticServeModule** | Hosting the compiled frontend. | Resolves `@objectos/web` dist path dynamically for production deployments. |
+| **StaticServeModule** | Hosting the compiled frontend. | Serves static assets for frontend applications. |
| **ExceptionFilter** | Standardized error formatting. | Converts `ObjectOSError` into JSON: `{ error: { code: 404, message: "..." } }`. |
### Functional Realization: "Context-Aware Request"
@@ -51,18 +51,9 @@ The `@objectos/server` package is the Gateway. It translates HTTP/WebSockets int
## 🖥️ 3. Interaction Support (UI Layer)
-The `@objectos/ui` (Components) and `@objectos/web` (App) packages provide the human interface.
+> **Note**: The UI components have been moved to a separate project and are no longer part of this monorepo.
-### Component Breakdown
-
-| Component | Responsibility | Implementation Notes |
-| :--- | :--- | :--- |
-| **ObjectGrid** | Data Table with "Excel-like" features. | Uses **TanStack Table**. Implements Virtual Scroll for 100k+ rows. |
-| **ObjectForm** | Dynamic Record Editor. | Uses **React-Hook-Form**. Generates Zod schema from Metadata at runtime. |
-| **LayoutShell** | Application chrome (Sidebar, Header). | Responsive. Adapts menu based on user permissions. |
-| **DataQueryHook** | React Query wrapper for API. | Cache management. `useQuery(['data', 'contacts'], ...)` |
-
-### Functional Realization: "Dynamic Types"
+The UI layer provides the human interface, integrating with ObjectOS through the HTTP API exposed by `@objectos/server`.
* **Design**: The UI downloads metadata initially.
* **Flow**: `schema.json` received -> `FieldFactory` maps `type: 'date'` to ` ` -> Renders Cell.
diff --git a/docs/guide/ui-framework.md b/docs/guide/ui-framework.md
index 5d14d994..308d60d0 100644
--- a/docs/guide/ui-framework.md
+++ b/docs/guide/ui-framework.md
@@ -1,6 +1,8 @@
# Standard UI Components Reference
-This document defines the standard component library for `@objectos/ui`. These components are the reference implementations for the **View & Layout Specifications**.
+> **Note**: The UI components (`@objectos/ui`) have been moved to a separate project and are no longer part of this monorepo. This document is kept for reference and describes the design principles for UI components that integrate with ObjectOS.
+
+This document defines the standard component library for UI components. These components are the reference implementations for the **View & Layout Specifications**.
---
diff --git a/package.json b/package.json
index 1e70b9aa..480c7f89 100644
--- a/package.json
+++ b/package.json
@@ -11,10 +11,8 @@
]
},
"scripts": {
- "dev": "concurrently \"pnpm run server\" \"pnpm run web:watch\" --kill-others --names \"SERVER,WEB\" -c \"magenta,blue\"",
+ "dev": "pnpm --filter @objectos/server dev",
"server": "pnpm --filter @objectos/server dev",
- "web": "pnpm --filter @objectos/web dev",
- "web:watch": "pnpm --filter @objectos/web build:watch",
"build": "tsc -b && pnpm -r build",
"start": "pnpm --filter @objectos/server start:prod",
"test": "pnpm -r test",
diff --git a/packages/kernel/README.md b/packages/kernel/README.md
index fb2f7f1d..485385f3 100644
--- a/packages/kernel/README.md
+++ b/packages/kernel/README.md
@@ -1,6 +1,6 @@
# @objectos/kernel
-The core runtime engine for ObjectOS - a metadata-driven platform built on the [@objectstack/spec](https://www.npmjs.com/package/@objectstack/spec) protocol.
+The core runtime engine for ObjectOS - a metadata-driven platform built on the [@objectstack/spec](https://github.com/objectstack-ai/spec) protocol.
## Overview
diff --git a/packages/server/package.json b/packages/server/package.json
index 9067d4dd..72605f4b 100644
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -21,7 +21,6 @@
"@nestjs/serve-static": "^4.0.2",
"@objectos/kernel": "workspace:*",
"@objectos/preset-base": "workspace:*",
- "@objectos/web": "workspace:*",
"@objectql/core": "^3.0.1",
"@objectql/driver-sql": "^3.0.1",
"@objectql/driver-mongo": "^3.0.1",
diff --git a/packages/server/src/app.module.ts b/packages/server/src/app.module.ts
index 348ea2c1..770147e2 100644
--- a/packages/server/src/app.module.ts
+++ b/packages/server/src/app.module.ts
@@ -3,22 +3,14 @@ import { AppController } from './app.controller.js';
import { AppService } from './app.service.js';
import { ObjectQLModule } from './objectql/objectql.module.js';
import { AuthModule } from './auth/auth.module.js';
-import { ServeStaticModule } from '@nestjs/serve-static';
-import { join, resolve, dirname } from 'path';
import { AuthMiddleware } from './auth/auth.middleware.js';
import { ObjectOS } from '@objectos/kernel';
import { createRESTHandler, createMetadataHandler, createNodeHandler } from '@objectql/server';
-const clientDistPath = resolve(dirname(require.resolve('@objectos/web/package.json')), 'dist');
-
@Module({
imports: [
ObjectQLModule,
AuthModule,
- ServeStaticModule.forRoot({
- rootPath: clientDistPath,
- exclude: ['/api/(.*)'],
- }),
],
controllers: [AppController],
providers: [AppService],
diff --git a/packages/ui/components.json b/packages/ui/components.json
deleted file mode 100644
index dbd2a8fc..00000000
--- a/packages/ui/components.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "$schema": "https://ui.shadcn.com/schema.json",
- "style": "default",
- "rsc": false,
- "tsx": true,
- "tailwind": {
- "config": "tailwind.config.js",
- "css": "src/styles.css",
- "baseColor": "slate",
- "cssVariables": true,
- "prefix": ""
- },
- "aliases": {
- "components": "@/components",
- "utils": "@/lib/utils",
- "ui": "@/components/ui",
- "lib": "@/lib",
- "hooks": "@/hooks"
- }
-}
diff --git a/packages/ui/examples/BasicDynamicForm.tsx b/packages/ui/examples/BasicDynamicForm.tsx
deleted file mode 100644
index b30a0497..00000000
--- a/packages/ui/examples/BasicDynamicForm.tsx
+++ /dev/null
@@ -1,91 +0,0 @@
-/**
- * Basic DynamicForm Example
- *
- * This example demonstrates the simplest usage of DynamicForm
- * without sections or tabs.
- */
-
-import React from 'react';
-import { DynamicForm } from '@objectos/ui';
-import type { ObjectConfig } from '@objectql/types';
-
-const userConfig: ObjectConfig = {
- name: 'user',
- label: 'User',
- fields: {
- firstName: {
- name: 'firstName',
- label: 'First Name',
- type: 'text',
- required: true,
- max_length: 50,
- },
- lastName: {
- name: 'lastName',
- label: 'Last Name',
- type: 'text',
- required: true,
- max_length: 50,
- },
- email: {
- name: 'email',
- label: 'Email Address',
- type: 'email',
- required: true,
- },
- phone: {
- name: 'phone',
- label: 'Phone Number',
- type: 'phone',
- },
- age: {
- name: 'age',
- label: 'Age',
- type: 'number',
- min: 18,
- max: 100,
- },
- bio: {
- name: 'bio',
- label: 'Biography',
- type: 'textarea',
- max_length: 500,
- help_text: 'Tell us about yourself',
- },
- newsletter: {
- name: 'newsletter',
- label: 'Subscribe to newsletter',
- type: 'boolean',
- defaultValue: true,
- },
- },
-};
-
-export function BasicDynamicFormExample() {
- const handleSubmit = async (data: any) => {
- console.log('Form submitted:', data);
-
- // Simulate API call
- await new Promise(resolve => setTimeout(resolve, 1000));
-
- alert('User created successfully!');
- };
-
- const handleCancel = () => {
- console.log('Form cancelled');
- };
-
- return (
-
-
Create New User
-
-
-
- );
-}
diff --git a/packages/ui/examples/ConditionalForm.tsx b/packages/ui/examples/ConditionalForm.tsx
deleted file mode 100644
index 20b7a6fa..00000000
--- a/packages/ui/examples/ConditionalForm.tsx
+++ /dev/null
@@ -1,169 +0,0 @@
-/**
- * Conditional Form Example
- *
- * This example demonstrates field dependencies and conditional visibility.
- * Fields appear/disappear based on other field values.
- */
-
-import React from 'react';
-import { DynamicForm } from '@objectos/ui';
-import type { ObjectConfig } from '@objectql/types';
-import type { FieldDependency } from '@objectos/ui';
-
-const applicationConfig: ObjectConfig = {
- name: 'application',
- label: 'Job Application',
- fields: {
- // Basic Info
- fullName: {
- name: 'fullName',
- label: 'Full Name',
- type: 'text',
- required: true,
- },
- email: {
- name: 'email',
- label: 'Email',
- type: 'email',
- required: true,
- },
-
- // Employment Status
- currentlyEmployed: {
- name: 'currentlyEmployed',
- label: 'Are you currently employed?',
- type: 'boolean',
- },
- currentEmployer: {
- name: 'currentEmployer',
- label: 'Current Employer',
- type: 'text',
- },
- currentJobTitle: {
- name: 'currentJobTitle',
- label: 'Current Job Title',
- type: 'text',
- },
- noticePeriod: {
- name: 'noticePeriod',
- label: 'Notice Period (days)',
- type: 'number',
- min: 0,
- },
-
- // Education
- highestDegree: {
- name: 'highestDegree',
- label: 'Highest Degree',
- type: 'select',
- options: [
- { label: 'High School', value: 'highschool' },
- { label: 'Bachelor\'s', value: 'bachelors' },
- { label: 'Master\'s', value: 'masters' },
- { label: 'PhD', value: 'phd' },
- ],
- required: true,
- },
- university: {
- name: 'university',
- label: 'University/College Name',
- type: 'text',
- },
- graduationYear: {
- name: 'graduationYear',
- label: 'Graduation Year',
- type: 'number',
- min: 1950,
- max: 2030,
- },
-
- // Relocation
- willingToRelocate: {
- name: 'willingToRelocate',
- label: 'Willing to relocate?',
- type: 'boolean',
- },
- preferredLocations: {
- name: 'preferredLocations',
- label: 'Preferred Locations',
- type: 'textarea',
- help_text: 'List cities you would consider',
- },
-
- // Sponsorship
- requiresSponsorship: {
- name: 'requiresSponsorship',
- label: 'Do you require visa sponsorship?',
- type: 'boolean',
- },
- visaType: {
- name: 'visaType',
- label: 'Current Visa Type',
- type: 'text',
- },
- },
-};
-
-const fieldDependencies: Record = {
- // Only show current employment fields if currently employed
- currentEmployer: {
- dependsOn: 'currentlyEmployed',
- condition: (value) => value === true,
- },
- currentJobTitle: {
- dependsOn: 'currentlyEmployed',
- condition: (value) => value === true,
- },
- noticePeriod: {
- dependsOn: 'currentlyEmployed',
- condition: (value) => value === true,
- },
-
- // Only show university/year for degree holders
- university: {
- dependsOn: 'highestDegree',
- condition: (value) => value && value !== 'highschool',
- },
- graduationYear: {
- dependsOn: 'highestDegree',
- condition: (value) => value && value !== 'highschool',
- },
-
- // Only show location preferences if willing to relocate
- preferredLocations: {
- dependsOn: 'willingToRelocate',
- condition: (value) => value === true,
- },
-
- // Only show visa type if requires sponsorship
- visaType: {
- dependsOn: 'requiresSponsorship',
- condition: (value) => value === true,
- },
-};
-
-export function ConditionalFormExample() {
- const handleSubmit = async (data: any) => {
- console.log('Application submitted:', data);
- await new Promise(resolve => setTimeout(resolve, 1000));
- alert('Application submitted successfully!');
- };
-
- return (
-
-
Job Application Form
-
- Notice how fields appear and disappear based on your answers!
-
-
-
-
- );
-}
diff --git a/packages/ui/examples/ObjectFormExample.tsx b/packages/ui/examples/ObjectFormExample.tsx
deleted file mode 100644
index 1330f796..00000000
--- a/packages/ui/examples/ObjectFormExample.tsx
+++ /dev/null
@@ -1,183 +0,0 @@
-import React, { useState } from 'react';
-import { ObjectForm } from '@objectos/ui';
-import type { ObjectConfig } from '@objectql/types';
-
-/**
- * Example usage of ObjectForm component
- * This demonstrates how to use the metadata-driven form
- */
-
-// Define object metadata
-const taskObjectConfig: ObjectConfig = {
- name: 'task',
- label: 'Task',
- description: 'Task management object',
- fields: {
- title: {
- name: 'title',
- label: 'Title',
- type: 'text',
- required: true,
- max_length: 200,
- },
- description: {
- name: 'description',
- label: 'Description',
- type: 'textarea',
- help_text: 'Provide a detailed description of the task',
- },
- status: {
- name: 'status',
- label: 'Status',
- type: 'select',
- options: [
- { label: 'To Do', value: 'todo' },
- { label: 'In Progress', value: 'in_progress' },
- { label: 'Done', value: 'done' },
- ],
- defaultValue: 'todo',
- required: true,
- },
- priority: {
- name: 'priority',
- label: 'Priority',
- type: 'select',
- options: [
- { label: 'Low', value: 'low' },
- { label: 'Medium', value: 'medium' },
- { label: 'High', value: 'high' },
- ],
- defaultValue: 'medium',
- },
- assignee: {
- name: 'assignee',
- label: 'Assignee',
- type: 'lookup',
- reference_to: 'user',
- help_text: 'Select a user to assign this task to',
- },
- is_completed: {
- name: 'is_completed',
- label: 'Mark as Completed',
- type: 'boolean',
- defaultValue: false,
- },
- due_date: {
- name: 'due_date',
- label: 'Due Date',
- type: 'date',
- },
- progress: {
- name: 'progress',
- label: 'Progress (%)',
- type: 'percent',
- min: 0,
- max: 100,
- defaultValue: 0,
- },
- estimated_hours: {
- name: 'estimated_hours',
- label: 'Estimated Hours',
- type: 'number',
- min: 0,
- help_text: 'Estimated time to complete this task',
- },
- budget: {
- name: 'budget',
- label: 'Budget',
- type: 'currency',
- min: 0,
- },
- contact_email: {
- name: 'contact_email',
- label: 'Contact Email',
- type: 'email',
- },
- reference_url: {
- name: 'reference_url',
- label: 'Reference URL',
- type: 'url',
- help_text: 'Link to external resources or documentation',
- },
- },
-};
-
-export default function ExampleObjectForm() {
- const [isSubmitting, setIsSubmitting] = useState(false);
- const [submittedData, setSubmittedData] = useState(null);
-
- // Example: Editing an existing task
- const initialValues = {
- title: 'Implement ObjectForm component',
- description: 'Create a metadata-driven form component similar to ObjectGridTable',
- status: 'in_progress',
- priority: 'high',
- is_completed: false,
- progress: 75,
- estimated_hours: 8,
- budget: 2000,
- contact_email: 'developer@example.com',
- reference_url: 'https://github.com/objectql/objectos',
- };
-
- const handleSubmit = async (data: Record) => {
- setIsSubmitting(true);
-
- // Simulate API call
- await new Promise(resolve => setTimeout(resolve, 1000));
-
- console.log('Form submitted:', data);
- setSubmittedData(data);
- setIsSubmitting(false);
- };
-
- const handleCancel = () => {
- console.log('Form cancelled');
- };
-
- return (
-
-
-
ObjectForm Example
-
- This form is automatically generated from ObjectQL metadata.
- All fields are validated based on their type and configuration.
-
-
-
-
-
Edit Task
-
-
-
- {submittedData && (
-
-
Submitted Data
-
- {JSON.stringify(submittedData, null, 2)}
-
-
- )}
-
-
-
Create New Task (Empty Form)
-
-
-
- );
-}
diff --git a/packages/ui/examples/ObjectGridTableExample.tsx b/packages/ui/examples/ObjectGridTableExample.tsx
deleted file mode 100644
index f6d9614c..00000000
--- a/packages/ui/examples/ObjectGridTableExample.tsx
+++ /dev/null
@@ -1,176 +0,0 @@
-import React from 'react';
-import { ObjectGridTable } from '@objectos/ui';
-import type { ObjectConfig } from '@objectql/types';
-
-/**
- * Example usage of ObjectGridTable component
- * This demonstrates how to use the metadata-driven AG Grid table
- */
-
-// Define object metadata
-const taskObjectConfig: ObjectConfig = {
- name: 'task',
- label: 'Task',
- description: 'Task management object',
- fields: {
- _id: {
- name: '_id',
- label: 'ID',
- type: 'text',
- readonly: true,
- hidden: true,
- },
- title: {
- name: 'title',
- label: 'Title',
- type: 'text',
- required: true,
- },
- description: {
- name: 'description',
- label: 'Description',
- type: 'textarea',
- },
- status: {
- name: 'status',
- label: 'Status',
- type: 'select',
- options: [
- { label: 'To Do', value: 'todo' },
- { label: 'In Progress', value: 'in_progress' },
- { label: 'Done', value: 'done' },
- ],
- defaultValue: 'todo',
- },
- priority: {
- name: 'priority',
- label: 'Priority',
- type: 'select',
- options: [
- { label: 'Low', value: 'low' },
- { label: 'Medium', value: 'medium' },
- { label: 'High', value: 'high' },
- ],
- },
- assignee: {
- name: 'assignee',
- label: 'Assignee',
- type: 'lookup',
- reference_to: 'user',
- },
- is_completed: {
- name: 'is_completed',
- label: 'Completed',
- type: 'boolean',
- defaultValue: false,
- },
- due_date: {
- name: 'due_date',
- label: 'Due Date',
- type: 'date',
- },
- created_at: {
- name: 'created_at',
- label: 'Created At',
- type: 'datetime',
- readonly: true,
- },
- progress: {
- name: 'progress',
- label: 'Progress',
- type: 'percent',
- min: 0,
- max: 100,
- },
- budget: {
- name: 'budget',
- label: 'Budget',
- type: 'currency',
- },
- email: {
- name: 'email',
- label: 'Contact Email',
- type: 'email',
- },
- reference_url: {
- name: 'reference_url',
- label: 'Reference URL',
- type: 'url',
- },
- },
-};
-
-// Sample data
-const sampleData = [
- {
- _id: '1',
- title: 'Implement AG Grid metadata integration',
- description: 'Create ObjectGridTable component that uses ObjectConfig',
- status: 'in_progress',
- priority: 'high',
- assignee: { _id: 'user1', name: 'John Doe' },
- is_completed: false,
- due_date: new Date('2026-01-15'),
- created_at: new Date('2026-01-10'),
- progress: 75,
- budget: 5000,
- email: 'john@example.com',
- reference_url: 'https://github.com/objectql/objectos',
- },
- {
- _id: '2',
- title: 'Write documentation',
- description: 'Document the new ObjectGridTable component',
- status: 'todo',
- priority: 'medium',
- assignee: { _id: 'user2', name: 'Jane Smith' },
- is_completed: false,
- due_date: new Date('2026-01-20'),
- created_at: new Date('2026-01-11'),
- progress: 0,
- budget: 2000,
- email: 'jane@example.com',
- reference_url: 'https://docs.objectos.dev',
- },
- {
- _id: '3',
- title: 'Review and test',
- description: 'Test all field type renderers',
- status: 'done',
- priority: 'high',
- assignee: { _id: 'user1', name: 'John Doe' },
- is_completed: true,
- due_date: new Date('2026-01-12'),
- created_at: new Date('2026-01-08'),
- progress: 100,
- budget: 1500,
- email: 'john@example.com',
- reference_url: 'https://testing.objectos.dev',
- },
-];
-
-export default function ExampleObjectGridTable() {
- const handleSelectionChanged = (selectedRows: any[]) => {
- console.log('Selected rows:', selectedRows);
- };
-
- return (
-
-
ObjectGridTable Example
-
- This table is automatically generated from ObjectQL metadata.
- Each field type is rendered with an appropriate cell renderer.
-
-
-
-
- );
-}
diff --git a/packages/ui/examples/SectionedForm.tsx b/packages/ui/examples/SectionedForm.tsx
deleted file mode 100644
index a841a832..00000000
--- a/packages/ui/examples/SectionedForm.tsx
+++ /dev/null
@@ -1,178 +0,0 @@
-/**
- * Sectioned Form Example
- *
- * This example demonstrates using FormSection to organize
- * form fields into logical groups.
- */
-
-import React from 'react';
-import { DynamicForm } from '@objectos/ui';
-import type { ObjectConfig } from '@objectql/types';
-import { User, Briefcase, Shield, Settings } from 'lucide-react';
-import type { FormSectionConfig } from '@objectos/ui';
-
-const employeeConfig: ObjectConfig = {
- name: 'employee',
- label: 'Employee',
- fields: {
- // Personal Info
- firstName: {
- name: 'firstName',
- label: 'First Name',
- type: 'text',
- required: true,
- },
- lastName: {
- name: 'lastName',
- label: 'Last Name',
- type: 'text',
- required: true,
- },
- email: {
- name: 'email',
- label: 'Email',
- type: 'email',
- required: true,
- },
- phone: {
- name: 'phone',
- label: 'Phone',
- type: 'phone',
- },
-
- // Employment Info
- employeeId: {
- name: 'employeeId',
- label: 'Employee ID',
- type: 'text',
- required: true,
- },
- department: {
- name: 'department',
- label: 'Department',
- type: 'select',
- options: [
- { label: 'Engineering', value: 'engineering' },
- { label: 'Sales', value: 'sales' },
- { label: 'Marketing', value: 'marketing' },
- { label: 'HR', value: 'hr' },
- ],
- required: true,
- },
- jobTitle: {
- name: 'jobTitle',
- label: 'Job Title',
- type: 'text',
- required: true,
- },
- startDate: {
- name: 'startDate',
- label: 'Start Date',
- type: 'date',
- required: true,
- },
-
- // Security
- username: {
- name: 'username',
- label: 'Username',
- type: 'text',
- required: true,
- min_length: 3,
- max_length: 20,
- },
- password: {
- name: 'password',
- label: 'Password',
- type: 'password',
- required: true,
- min_length: 8,
- },
-
- // Preferences
- timezone: {
- name: 'timezone',
- label: 'Timezone',
- type: 'select',
- options: [
- { label: 'UTC', value: 'UTC' },
- { label: 'EST', value: 'America/New_York' },
- { label: 'PST', value: 'America/Los_Angeles' },
- ],
- },
- language: {
- name: 'language',
- label: 'Language',
- type: 'select',
- options: [
- { label: 'English', value: 'en' },
- { label: 'Spanish', value: 'es' },
- { label: 'French', value: 'fr' },
- ],
- },
- },
-};
-
-const sections: FormSectionConfig[] = [
- {
- id: 'personal',
- title: 'Personal Information',
- description: 'Basic employee details',
- icon: User,
- fields: ['firstName', 'lastName', 'email', 'phone'],
- collapsible: false,
- },
- {
- id: 'employment',
- title: 'Employment Details',
- description: 'Job and department information',
- icon: Briefcase,
- fields: ['employeeId', 'department', 'jobTitle', 'startDate'],
- collapsible: true,
- defaultCollapsed: false,
- },
- {
- id: 'security',
- title: 'Security Settings',
- description: 'Login credentials',
- icon: Shield,
- fields: ['username', 'password'],
- collapsible: true,
- defaultCollapsed: true,
- columns: 1,
- },
- {
- id: 'preferences',
- title: 'User Preferences',
- icon: Settings,
- fields: ['timezone', 'language'],
- collapsible: true,
- defaultCollapsed: true,
- },
-];
-
-export function SectionedFormExample() {
- const handleSubmit = async (data: any) => {
- console.log('Employee data:', data);
- await new Promise(resolve => setTimeout(resolve, 1000));
- alert('Employee created!');
- };
-
- const handleCancel = () => {
- console.log('Cancelled');
- };
-
- return (
-
-
New Employee Registration
-
-
-
- );
-}
diff --git a/packages/ui/package.json b/packages/ui/package.json
deleted file mode 100644
index 121df686..00000000
--- a/packages/ui/package.json
+++ /dev/null
@@ -1,96 +0,0 @@
-{
- "name": "@objectos/ui",
- "version": "0.1.0",
- "private": false,
- "license": "AGPL-3.0",
- "main": "dist/index.js",
- "module": "dist/index.mjs",
- "types": "dist/index.d.ts",
- "files": [
- "dist"
- ],
- "scripts": {
- "build": "npm run build:css && tsup",
- "build:css": "postcss src/styles.css -o dist/index.css",
- "dev": "tsup --watch",
- "lint": "eslint src/**",
- "test": "vitest run",
- "test:watch": "vitest",
- "test:ui": "vitest --ui",
- "test:coverage": "vitest run --coverage"
- },
- "peerDependencies": {
- "react": ">=18",
- "react-dom": ">=18"
- },
- "devDependencies": {
- "@tailwindcss/postcss": "^4.1.18",
- "@testing-library/jest-dom": "^6.9.1",
- "@testing-library/react": "^16.3.1",
- "@types/react": "^19.0.11",
- "@types/react-dom": "^19.0.5",
- "jsdom": "^27.4.0",
- "postcss": "^8.4.0",
- "postcss-cli": "^11.0.0",
- "react": "^18.2.0",
- "react-dom": "^18.2.0",
- "tailwindcss": "^4.1.18",
- "tsup": "^8.0.0",
- "typescript": "^5.0.0",
- "vitest": "^4.0.16"
- },
- "dependencies": {
- "@dnd-kit/core": "^6.3.1",
- "@dnd-kit/modifiers": "^9.0.0",
- "@dnd-kit/sortable": "^10.0.0",
- "@dnd-kit/utilities": "^3.2.2",
- "@hookform/resolvers": "^5.2.2",
- "@objectql/types": "^3.0.1",
- "@radix-ui/react-accordion": "^1.2.12",
- "@radix-ui/react-alert-dialog": "^1.1.15",
- "@radix-ui/react-aspect-ratio": "^1.1.8",
- "@radix-ui/react-avatar": "^1.1.11",
- "@radix-ui/react-checkbox": "^1.3.3",
- "@radix-ui/react-collapsible": "^1.1.12",
- "@radix-ui/react-context-menu": "^2.2.16",
- "@radix-ui/react-dialog": "^1.1.15",
- "@radix-ui/react-dropdown-menu": "^2.1.16",
- "@radix-ui/react-hover-card": "^1.1.15",
- "@radix-ui/react-label": "^2.1.8",
- "@radix-ui/react-menubar": "^1.1.16",
- "@radix-ui/react-navigation-menu": "^1.2.14",
- "@radix-ui/react-popover": "^1.1.15",
- "@radix-ui/react-progress": "^1.1.8",
- "@radix-ui/react-radio-group": "^1.3.8",
- "@radix-ui/react-scroll-area": "^1.2.10",
- "@radix-ui/react-select": "^2.2.6",
- "@radix-ui/react-separator": "^1.1.8",
- "@radix-ui/react-slider": "^1.3.6",
- "@radix-ui/react-slot": "^1.2.4",
- "@radix-ui/react-switch": "^1.2.6",
- "@radix-ui/react-tabs": "^1.1.13",
- "@radix-ui/react-toggle": "^1.1.10",
- "@radix-ui/react-toggle-group": "^1.1.11",
- "@radix-ui/react-tooltip": "^1.2.8",
- "@tanstack/react-table": "^8.21.3",
- "ag-grid-community": "^35.0.0",
- "ag-grid-react": "^35.0.0",
- "class-variance-authority": "^0.7.1",
- "clsx": "^2.1.1",
- "cmdk": "^1.1.1",
- "date-fns": "^4.1.0",
- "embla-carousel-react": "^8.6.0",
- "input-otp": "^1.4.2",
- "lucide-react": "^0.562.0",
- "next-themes": "^0.4.6",
- "react-day-picker": "^9.13.0",
- "react-hook-form": "^7.70.0",
- "react-resizable-panels": "^4.3.3",
- "recharts": "^2.15.4",
- "sonner": "^2.0.7",
- "tailwind-merge": "^3.4.0",
- "tailwindcss-animate": "^1.0.7",
- "vaul": "^1.1.2",
- "zod": "^4.3.5"
- }
-}
diff --git a/packages/ui/postcss.config.js b/packages/ui/postcss.config.js
deleted file mode 100644
index 52b9b4ba..00000000
--- a/packages/ui/postcss.config.js
+++ /dev/null
@@ -1,5 +0,0 @@
-module.exports = {
- plugins: {
- '@tailwindcss/postcss': {},
- },
-}
diff --git a/packages/ui/src/components/__tests__/FormActions.test.tsx b/packages/ui/src/components/__tests__/FormActions.test.tsx
deleted file mode 100644
index 1012a17e..00000000
--- a/packages/ui/src/components/__tests__/FormActions.test.tsx
+++ /dev/null
@@ -1,149 +0,0 @@
-import { render, screen, fireEvent } from '@testing-library/react'
-import '@testing-library/jest-dom'
-import { FormActions } from '../forms/FormActions'
-import { describe, it, expect, vi } from 'vitest'
-
-describe('FormActions', () => {
- it('renders save button by default', () => {
- const onSave = vi.fn()
- render( )
-
- expect(screen.getByRole('button', { name: 'Save' })).toBeInTheDocument()
- })
-
- it('calls onSave when save button is clicked', () => {
- const onSave = vi.fn()
- render( )
-
- fireEvent.click(screen.getByRole('button', { name: 'Save' }))
- expect(onSave).toHaveBeenCalledTimes(1)
- })
-
- it('renders cancel button when onCancel is provided', () => {
- const onCancel = vi.fn()
- render( )
-
- expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument()
- })
-
- it('calls onCancel when cancel button is clicked', () => {
- const onCancel = vi.fn()
- render( )
-
- fireEvent.click(screen.getByRole('button', { name: 'Cancel' }))
- expect(onCancel).toHaveBeenCalledTimes(1)
- })
-
- it('renders Save & New button when onSaveAndNew is provided', () => {
- const onSaveAndNew = vi.fn()
- render( )
-
- expect(screen.getByRole('button', { name: 'Save & New' })).toBeInTheDocument()
- })
-
- it('calls onSaveAndNew when Save & New button is clicked', () => {
- const onSaveAndNew = vi.fn()
- render( )
-
- fireEvent.click(screen.getByRole('button', { name: 'Save & New' }))
- expect(onSaveAndNew).toHaveBeenCalledTimes(1)
- })
-
- it('hides cancel button when hideCancel is true', () => {
- render( )
-
- expect(screen.queryByRole('button', { name: 'Cancel' })).not.toBeInTheDocument()
- })
-
- it('hides Save & New button when hideSaveAndNew is true', () => {
- render( )
-
- expect(screen.queryByRole('button', { name: 'Save & New' })).not.toBeInTheDocument()
- })
-
- it('shows loading state when isSubmitting is true', () => {
- render( )
-
- expect(screen.getByText('Saving...')).toBeInTheDocument()
- })
-
- it('disables all buttons when isSubmitting is true', () => {
- render(
-
- )
-
- const buttons = screen.getAllByRole('button')
- buttons.forEach(button => {
- expect(button).toBeDisabled()
- })
- })
-
- it('uses custom button text when provided', () => {
- render(
-
- )
-
- expect(screen.getByRole('button', { name: 'Submit' })).toBeInTheDocument()
- expect(screen.getByRole('button', { name: 'Back' })).toBeInTheDocument()
- expect(screen.getByRole('button', { name: 'Submit & Create Another' })).toBeInTheDocument()
- })
-
- it('shows validation error count when errorCount is provided', () => {
- render( )
-
- expect(screen.getByText('Please fix 3 errors before saving')).toBeInTheDocument()
- })
-
- it('shows singular error message when errorCount is 1', () => {
- render( )
-
- expect(screen.getByText('Please fix 1 error before saving')).toBeInTheDocument()
- })
-
- it('disables save buttons when there are errors', () => {
- render( )
-
- const saveButton = screen.getByRole('button', { name: 'Save' })
- const saveAndNewButton = screen.getByRole('button', { name: 'Save & New' })
-
- expect(saveButton).toBeDisabled()
- expect(saveAndNewButton).toBeDisabled()
- })
-
- it('does not disable cancel button when there are errors', () => {
- render( )
-
- const cancelButton = screen.getByRole('button', { name: 'Cancel' })
- expect(cancelButton).not.toBeDisabled()
- })
-
- it('aligns buttons to the left when align="left"', () => {
- const { container } = render( )
-
- expect(container.querySelector('.justify-start')).toBeInTheDocument()
- })
-
- it('aligns buttons to the center when align="center"', () => {
- const { container } = render( )
-
- expect(container.querySelector('.justify-center')).toBeInTheDocument()
- })
-
- it('aligns buttons to the right by default', () => {
- const { container } = render( )
-
- expect(container.querySelector('.justify-end')).toBeInTheDocument()
- })
-})
diff --git a/packages/ui/src/components/__tests__/FormSection.test.tsx b/packages/ui/src/components/__tests__/FormSection.test.tsx
deleted file mode 100644
index 646d0539..00000000
--- a/packages/ui/src/components/__tests__/FormSection.test.tsx
+++ /dev/null
@@ -1,133 +0,0 @@
-import { render, screen } from '@testing-library/react'
-import '@testing-library/jest-dom'
-import { FormSection } from '../forms/FormSection'
-import { describe, it, expect } from 'vitest'
-import { User } from 'lucide-react'
-
-describe('FormSection', () => {
- it('renders section with title', () => {
- render(
-
- Test content
-
- )
-
- expect(screen.getByText('Personal Information')).toBeInTheDocument()
- expect(screen.getByText('Test content')).toBeInTheDocument()
- })
-
- it('renders section with description', () => {
- render(
-
- Test content
-
- )
-
- expect(screen.getByText('Personal Information')).toBeInTheDocument()
- expect(screen.getByText('Enter your personal details')).toBeInTheDocument()
- })
-
- it('renders section with icon', () => {
- const { container } = render(
-
- Test content
-
- )
-
- // Check for icon by class
- expect(container.querySelector('svg')).toBeInTheDocument()
- })
-
- it('renders non-collapsible section by default', () => {
- render(
-
- Test content
-
- )
-
- // Content should be visible immediately
- expect(screen.getByText('Test content')).toBeVisible()
-
- // Should not have collapse trigger button
- const buttons = screen.queryAllByRole('button')
- expect(buttons.length).toBe(0)
- })
-
- it('renders collapsible section when collapsible=true', () => {
- render(
-
- Test content
-
- )
-
- // Should have a button for collapsing
- const button = screen.getByRole('button')
- expect(button).toBeInTheDocument()
- })
-
- it('starts expanded when collapsible but not defaultCollapsed', () => {
- render(
-
- Test content
-
- )
-
- // Content should be visible
- expect(screen.getByText('Test content')).toBeInTheDocument()
- })
-
- it('starts collapsed when defaultCollapsed=true', () => {
- render(
-
- Test content
-
- )
-
- // Component should render - testing Radix Collapsible's internal state is complex in JSDOM
- expect(screen.getByText('Personal Information')).toBeInTheDocument()
- })
-
- it('applies single column layout when columns=1', () => {
- const { container } = render(
-
- Test content
-
- )
-
- // Check for grid-cols-1 class
- const grid = container.querySelector('.grid-cols-1')
- expect(grid).toBeInTheDocument()
- })
-
- it('applies two column layout when columns=2', () => {
- const { container } = render(
-
- Test content
-
- )
-
- // Check for md:grid-cols-2 class
- const grid = container.querySelector('.md\\:grid-cols-2')
- expect(grid).toBeInTheDocument()
- })
-
- it('applies custom className', () => {
- const { container } = render(
-
- Test content
-
- )
-
- expect(container.querySelector('.custom-class')).toBeInTheDocument()
- })
-})
diff --git a/packages/ui/src/components/app-sidebar.tsx b/packages/ui/src/components/app-sidebar.tsx
deleted file mode 100644
index 7b4576b9..00000000
--- a/packages/ui/src/components/app-sidebar.tsx
+++ /dev/null
@@ -1,179 +0,0 @@
-import * as React from "react"
-import {
- ArrowUpCircleIcon,
- BarChartIcon,
- CameraIcon,
- ClipboardListIcon,
- DatabaseIcon,
- FileCodeIcon,
- FileIcon,
- FileTextIcon,
- FolderIcon,
- HelpCircleIcon,
- LayoutDashboardIcon,
- ListIcon,
- SearchIcon,
- SettingsIcon,
- UsersIcon,
-} from "lucide-react"
-
-import { NavDocuments } from "@/components/nav-documents"
-import { NavMain } from "@/components/nav-main"
-import { NavSecondary } from "@/components/nav-secondary"
-import { NavUser } from "@/components/nav-user"
-import {
- Sidebar,
- SidebarContent,
- SidebarFooter,
- SidebarHeader,
- SidebarMenu,
- SidebarMenuButton,
- SidebarMenuItem,
-} from "@/components/ui/sidebar"
-
-const data = {
- user: {
- name: "shadcn",
- email: "m@example.com",
- avatar: "/avatars/shadcn.jpg",
- },
- navMain: [
- {
- title: "Dashboard",
- url: "#",
- icon: LayoutDashboardIcon,
- },
- {
- title: "Lifecycle",
- url: "#",
- icon: ListIcon,
- },
- {
- title: "Analytics",
- url: "#",
- icon: BarChartIcon,
- },
- {
- title: "Projects",
- url: "#",
- icon: FolderIcon,
- },
- {
- title: "Team",
- url: "#",
- icon: UsersIcon,
- },
- ],
- navClouds: [
- {
- title: "Capture",
- icon: CameraIcon,
- isActive: true,
- url: "#",
- items: [
- {
- title: "Active Proposals",
- url: "#",
- },
- {
- title: "Archived",
- url: "#",
- },
- ],
- },
- {
- title: "Proposal",
- icon: FileTextIcon,
- url: "#",
- items: [
- {
- title: "Active Proposals",
- url: "#",
- },
- {
- title: "Archived",
- url: "#",
- },
- ],
- },
- {
- title: "Prompts",
- icon: FileCodeIcon,
- url: "#",
- items: [
- {
- title: "Active Proposals",
- url: "#",
- },
- {
- title: "Archived",
- url: "#",
- },
- ],
- },
- ],
- navSecondary: [
- {
- title: "Settings",
- url: "#",
- icon: SettingsIcon,
- },
- {
- title: "Get Help",
- url: "#",
- icon: HelpCircleIcon,
- },
- {
- title: "Search",
- url: "#",
- icon: SearchIcon,
- },
- ],
- documents: [
- {
- name: "Data Library",
- url: "#",
- icon: DatabaseIcon,
- },
- {
- name: "Reports",
- url: "#",
- icon: ClipboardListIcon,
- },
- {
- name: "Word Assistant",
- url: "#",
- icon: FileIcon,
- },
- ],
-}
-
-export function AppSidebar({ ...props }: React.ComponentProps) {
- return (
-
-
-
-
-
-
-
- Acme Inc.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- )
-}
diff --git a/packages/ui/src/components/fields/BooleanField.tsx b/packages/ui/src/components/fields/BooleanField.tsx
deleted file mode 100644
index 00b4118b..00000000
--- a/packages/ui/src/components/fields/BooleanField.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import React from "react"
-import { Switch } from "../ui/switch"
-import { Checkbox } from "../ui/checkbox"
-import { Label } from "../ui/label"
-import { cn } from "@/lib/utils"
-import { FieldProps } from "./types"
-
-export interface BooleanFieldProps extends FieldProps {
- variant?: "switch" | "checkbox"
-}
-
-export function BooleanField({
- value,
- onChange,
- disabled,
- readOnly,
- className,
- error,
- label,
- required,
- description,
- name,
- variant = "switch",
-}: BooleanFieldProps) {
-
- if (variant === "checkbox") {
- return (
-
-
- onChange?.(checked === true)}
- disabled={disabled || readOnly}
- className={cn(error && "border-destructive")}
- />
-
- {label}
- {required && * }
-
-
-
- {description && !error && (
-
{description}
- )}
- {error &&
{error}
}
-
- )
- }
-
- // Default Switch
- return (
-
-
-
- {label}
- {required && * }
-
- {description && (
-
{description}
- )}
- {error &&
{error}
}
-
-
-
- )
-}
diff --git a/packages/ui/src/components/fields/DateField.tsx b/packages/ui/src/components/fields/DateField.tsx
deleted file mode 100644
index 77d7ad46..00000000
--- a/packages/ui/src/components/fields/DateField.tsx
+++ /dev/null
@@ -1,68 +0,0 @@
-import * as React from "react"
-import { format } from "date-fns"
-import { Calendar as CalendarIcon, X } from "lucide-react"
-
-import { cn } from "@/lib/utils"
-import { Button } from "../ui/button"
-import { Calendar } from "../ui/calendar"
-import {
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from "../ui/popover"
-import { Label } from "../ui/label"
-import { FieldProps } from "./types"
-
-export function DateField({
- value,
- onChange,
- disabled,
- readOnly,
- className,
- placeholder,
- error,
- label,
- required,
- description,
- name,
-}: FieldProps) {
- return (
-
- {label && (
-
- {label}
- {required && * }
-
- )}
-
-
-
-
- {value ? format(new Date(value), "PPP") : {placeholder || "Pick a date"} }
-
-
-
-
-
-
- {description && !error && (
-
{description}
- )}
- {error &&
{error}
}
-
- )
-}
diff --git a/packages/ui/src/components/fields/Field.tsx b/packages/ui/src/components/fields/Field.tsx
deleted file mode 100644
index d7d5e437..00000000
--- a/packages/ui/src/components/fields/Field.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import React from 'react'
-import { FieldProps } from './types'
-import { TextField } from './TextField'
-import { NumberField } from './NumberField'
-import { BooleanField } from './BooleanField'
-import { SelectField } from './SelectField'
-import { DateField } from './DateField'
-import { TextAreaField } from './TextAreaField'
-import { LookupField } from './LookupField'
-
-export interface GenericFieldProps extends FieldProps {
- type: string
- referenceTo?: string
-}
-
-export function Field({ type, referenceTo, ...props }: GenericFieldProps) {
- switch (type) {
- case 'text':
-
- case 'string':
- case 'email':
- case 'url':
- case 'password':
- case 'tel':
- return
- case 'number':
- case 'integer':
- case 'float':
- case 'currency':
- case 'percent':
- return
- case 'boolean':
- return
- case 'date':
- case 'datetime':
- return
- case 'select':
- return
- case 'textarea':
- case 'longtext':
- return
- case 'lookup':
- case 'master_detail':
- if (referenceTo) {
- return
- }
- return
- default:
- return
- }
-}
diff --git a/packages/ui/src/components/fields/LookupField.tsx b/packages/ui/src/components/fields/LookupField.tsx
deleted file mode 100644
index 6f838eee..00000000
--- a/packages/ui/src/components/fields/LookupField.tsx
+++ /dev/null
@@ -1,183 +0,0 @@
-import * as React from "react"
-import { Check, ChevronsUpDown, Loader2, X } from "lucide-react"
-
-import { cn } from "@/lib/utils"
-import { Button } from "../ui/button"
-import {
- Command,
- CommandEmpty,
- CommandGroup,
- CommandInput,
- CommandItem,
- CommandList,
-} from "../ui/command"
-import {
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from "../ui/popover"
-import { Label } from "../ui/label"
-import { LookupFieldProps } from "./types"
-import { useDebounce } from "../../hooks/use-debounce"
-
-export function LookupField({
- value,
- onChange,
- disabled,
- readOnly,
- className,
- placeholder,
- error,
- label,
- required,
- description,
- name,
- referenceTo,
-}: LookupFieldProps) {
- const [open, setOpen] = React.useState(false)
- const [items, setItems] = React.useState([])
- const [loading, setLoading] = React.useState(false)
- const [contentLabel, setContentLabel] = React.useState("")
- const [search, setSearch] = React.useState("")
-
- const debouncedSearch = useDebounce(search, 300)
-
- // Fetch initial label
- React.useEffect(() => {
- if (value && !contentLabel) {
- if (typeof value === 'object' && (value as any).name) {
- setContentLabel((value as any).name || (value as any).title || (value as any)._id);
- return;
- }
-
- if (typeof value !== 'string') return;
-
- // TODO: Use a proper data fetching client
- fetch(`/api/data/${referenceTo}/${value}`)
- .then(res => res.json())
- .then(data => {
- if (data) {
- setContentLabel(data.name || data.title || data.email || data._id || value);
- }
- })
- .catch(() => setContentLabel(value));
- }
- }, [value, referenceTo, contentLabel]);
-
- // Search items
- React.useEffect(() => {
- if (!open) return;
-
- setLoading(true);
- const params = new URLSearchParams();
-
- if (debouncedSearch) {
- const filter = JSON.stringify([['name', 'contains', debouncedSearch], 'or', ['title', 'contains', debouncedSearch]]);
- params.append('filters', filter);
- }
-
- params.append('top', '20');
-
- fetch(`/api/data/${referenceTo}?${params.toString()}`)
- .then(res => res.json())
- .then(data => {
- const list = Array.isArray(data) ? data : (data.list || []);
- setItems(list);
- })
- .catch(console.error)
- .finally(() => setLoading(false));
-
- }, [open, debouncedSearch, referenceTo]);
-
- const handleSelect = (itemId: string, item: any) => {
- onChange?.(itemId === value ? undefined : itemId)
- setContentLabel(item.name || item.title || item.email || item._id);
- setOpen(false)
- }
-
- const handleClear = (e: React.MouseEvent) => {
- e.stopPropagation();
- onChange?.(undefined);
- setContentLabel("");
- }
-
- return (
-
- {label && (
-
- {label}
- {required && * }
-
- )}
-
-
-
-
- {value ? contentLabel || value : (placeholder || "Select record...")}
-
- {value && !disabled && !readOnly ? (
-
- ) : (
-
- )}
-
-
-
-
-
-
- {loading && Loading...
}
-
- {!loading && items.length === 0 && (
- No results found.
- )}
-
- {!loading && items.length > 0 && (
-
- {items.map((item) => {
- const itemId = item._id || item.id;
- const itemLabel = item.name || item.title || item.email || itemId;
- return (
- handleSelect(itemId, item)}
- >
-
- {itemLabel}
-
- )
- })}
-
- )}
-
-
-
-
- {description && !error && (
-
{description}
- )}
- {error &&
{error}
}
-
- )
-}
diff --git a/packages/ui/src/components/fields/NumberField.tsx b/packages/ui/src/components/fields/NumberField.tsx
deleted file mode 100644
index d59becc9..00000000
--- a/packages/ui/src/components/fields/NumberField.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import * as React from "react"
-import { cn } from "@/lib/utils"
-import { Input } from "../ui/input"
-import { Label } from "../ui/label"
-import { FieldProps } from "./types"
-
-export function NumberField({
- value,
- onChange,
- disabled,
- readOnly,
- className,
- placeholder,
- error,
- label,
- required,
- description,
- name,
-}: FieldProps) {
- return (
-
- {label && (
-
- {label}
- {required && * }
-
- )}
-
{
- const val = e.target.value;
- onChange?.(val === "" ? undefined : Number(val));
- }}
- disabled={disabled}
- readOnly={readOnly}
- placeholder={placeholder}
- className={cn(
- error && "border-destructive focus-visible:ring-destructive"
- )}
- />
- {description && !error && (
-
{description}
- )}
- {error &&
{error}
}
-
- )
-}
diff --git a/packages/ui/src/components/fields/SelectField.tsx b/packages/ui/src/components/fields/SelectField.tsx
deleted file mode 100644
index f2678076..00000000
--- a/packages/ui/src/components/fields/SelectField.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import * as React from "react"
-import { cn } from "@/lib/utils"
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "../ui/select"
-import { Label } from "../ui/label"
-import { FieldProps } from "./types"
-
-export function SelectField({
- value,
- onChange,
- disabled,
- readOnly,
- className,
- placeholder,
- error,
- label,
- required,
- description,
- name,
- options = [],
-}: FieldProps) {
-
- // Normalize options
- const normalizedOptions = React.useMemo(() => {
- return options.map((opt) => {
- if (typeof opt === 'string') return { label: opt, value: opt };
- return opt;
- });
- }, [options]);
-
- return (
-
- {label && (
-
- {label}
- {required && * }
-
- )}
-
-
-
-
-
- {normalizedOptions.map((option) => (
-
- {option.label}
-
- ))}
-
-
- {description && !error && (
-
{description}
- )}
- {error &&
{error}
}
-
- )
-}
diff --git a/packages/ui/src/components/fields/TextAreaField.tsx b/packages/ui/src/components/fields/TextAreaField.tsx
deleted file mode 100644
index 1ed59af8..00000000
--- a/packages/ui/src/components/fields/TextAreaField.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import * as React from "react"
-import { cn } from "@/lib/utils"
-import { Textarea } from "../ui/textarea"
-import { Label } from "../ui/label"
-import { FieldProps } from "./types"
-
-export function TextAreaField({
- value,
- onChange,
- disabled,
- readOnly,
- className,
- placeholder,
- error,
- label,
- required,
- description,
- name,
- rows = 3,
-}: FieldProps & { rows?: number }) {
- return (
-
- {label && (
-
- {label}
- {required && * }
-
- )}
-
- )
-}
diff --git a/packages/ui/src/components/fields/TextField.tsx b/packages/ui/src/components/fields/TextField.tsx
deleted file mode 100644
index 5e050b78..00000000
--- a/packages/ui/src/components/fields/TextField.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import * as React from "react"
-import { cn } from "@/lib/utils"
-import { Input } from "../ui/input"
-import { Label } from "../ui/label"
-import { FieldProps } from "./types"
-
-export function TextField({
- value,
- onChange,
- disabled,
- readOnly,
- className,
- placeholder,
- error,
- label,
- required,
- description,
- name,
- type = "text",
-}: FieldProps & { type?: string }) {
- return (
-
- {label && (
-
- {label}
- {required && * }
-
- )}
-
onChange?.(e.target.value)}
- disabled={disabled}
- readOnly={readOnly}
- placeholder={placeholder}
- className={cn(
- error && "border-destructive focus-visible:ring-destructive"
- )}
- />
- {description && !error && (
-
{description}
- )}
- {error &&
{error}
}
-
- )
-}
diff --git a/packages/ui/src/components/fields/index.ts b/packages/ui/src/components/fields/index.ts
deleted file mode 100644
index b2a02036..00000000
--- a/packages/ui/src/components/fields/index.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-export * from "./types"
-export * from "./TextField"
-export * from "./NumberField"
-export * from "./TextAreaField"
-export * from "./BooleanField"
-export * from "./SelectField"
-export * from "./DateField"
-export * from "./Field"
-export * from "./LookupField"
diff --git a/packages/ui/src/components/fields/types.ts b/packages/ui/src/components/fields/types.ts
deleted file mode 100644
index 56f2bc81..00000000
--- a/packages/ui/src/components/fields/types.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-export interface FieldProps {
- value?: T
- onChange?: (value: T | undefined) => void
- disabled?: boolean
- readOnly?: boolean
- className?: string
- placeholder?: string
- error?: string
-
- // Basic metadata
- name?: string
- label?: string
- required?: boolean
- description?: string
-
- // Specific options
- options?: ({ label: string; value: any } | string)[]
-}
-
-export interface LookupFieldProps extends FieldProps {
- referenceTo: string
-}
diff --git a/packages/ui/src/components/forms/FormActions.tsx b/packages/ui/src/components/forms/FormActions.tsx
deleted file mode 100644
index ebad0040..00000000
--- a/packages/ui/src/components/forms/FormActions.tsx
+++ /dev/null
@@ -1,125 +0,0 @@
-import * as React from "react"
-import { Button } from "@/components/ui/button"
-import { Loader2 } from "lucide-react"
-import { cn } from "@/lib/utils"
-
-export interface FormActionsProps {
- /** Whether the form is in submitting state */
- isSubmitting?: boolean
- /** Callback for Save button */
- onSave?: () => void
- /** Callback for Save & New button (optional) */
- onSaveAndNew?: () => void
- /** Callback for Cancel button */
- onCancel?: () => void
- /** Text for Save button */
- saveText?: string
- /** Text for Save & New button */
- saveAndNewText?: string
- /** Text for Cancel button */
- cancelText?: string
- /** Hide Save & New button */
- hideSaveAndNew?: boolean
- /** Hide Cancel button */
- hideCancel?: boolean
- /** Show validation errors count */
- errorCount?: number
- /** Additional CSS classes */
- className?: string
- /** Position of the actions (left, center, right) */
- align?: 'left' | 'center' | 'right'
-}
-
-/**
- * FormActions - Form action buttons component
- *
- * Provides consistent form action buttons with:
- * - Save button
- * - Save & New button (optional)
- * - Cancel button
- * - Loading states
- * - Validation feedback
- */
-export function FormActions({
- isSubmitting = false,
- onSave,
- onSaveAndNew,
- onCancel,
- saveText = 'Save',
- saveAndNewText = 'Save & New',
- cancelText = 'Cancel',
- hideSaveAndNew = false,
- hideCancel = false,
- errorCount,
- className,
- align = 'right',
-}: FormActionsProps) {
- const alignmentClass = {
- left: 'justify-start',
- center: 'justify-center',
- right: 'justify-end',
- }[align]
-
- return (
-
- {/* Validation feedback */}
- {errorCount !== undefined && errorCount > 0 && (
-
- {errorCount === 1
- ? "Please fix 1 error before saving"
- : `Please fix ${errorCount} errors before saving`
- }
-
- )}
-
- {/* Action buttons */}
-
- {!hideCancel && onCancel && (
-
- {cancelText}
-
- )}
-
- {!hideSaveAndNew && onSaveAndNew && (
- 0)}
- >
- {isSubmitting ? (
- <>
-
- Saving...
- >
- ) : (
- saveAndNewText
- )}
-
- )}
-
- {onSave && (
- 0)}
- >
- {isSubmitting ? (
- <>
-
- Saving...
- >
- ) : (
- saveText
- )}
-
- )}
-
-
- )
-}
diff --git a/packages/ui/src/components/forms/FormSection.tsx b/packages/ui/src/components/forms/FormSection.tsx
deleted file mode 100644
index 79b0e276..00000000
--- a/packages/ui/src/components/forms/FormSection.tsx
+++ /dev/null
@@ -1,114 +0,0 @@
-import * as React from "react"
-import { ChevronDown, ChevronRight, type LucideIcon } from "lucide-react"
-import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
-import { cn } from "@/lib/utils"
-
-export interface FormSectionProps {
- /** Section title */
- title: string
- /** Section description (optional) */
- description?: string
- /** Icon to display next to title (optional) */
- icon?: LucideIcon
- /** Whether the section is collapsible */
- collapsible?: boolean
- /** Whether the section is initially collapsed (only applies if collapsible is true) */
- defaultCollapsed?: boolean
- /** Layout: 1-column or 2-column */
- columns?: 1 | 2
- /** Child form fields */
- children: React.ReactNode
- /** Additional CSS classes */
- className?: string
-}
-
-/**
- * FormSection - A grouping container for form fields
- *
- * Provides a way to organize form fields into logical sections with optional:
- * - Collapsible behavior
- * - Icons in headers
- * - 1-column or 2-column layouts
- * - Section descriptions
- */
-export function FormSection({
- title,
- description,
- icon: Icon,
- collapsible = false,
- defaultCollapsed = false,
- columns = 2,
- children,
- className,
-}: FormSectionProps) {
- const [isOpen, setIsOpen] = React.useState(!defaultCollapsed)
-
- const headerContent = (
-
- {Icon && (
-
- )}
-
-
- {title}
-
- {description && (
-
- {description}
-
- )}
-
- {collapsible && (
-
- {isOpen ? (
-
- ) : (
-
- )}
-
- )}
-
- )
-
- const contentGrid = (
-
- {children}
-
- )
-
- if (collapsible) {
- return (
-
-
-
- {headerContent}
-
-
-
- {contentGrid}
-
-
- )
- }
-
- return (
-
-
- {headerContent}
-
- {contentGrid}
-
- )
-}
diff --git a/packages/ui/src/components/grids/ObjectGrid.tsx b/packages/ui/src/components/grids/ObjectGrid.tsx
deleted file mode 100644
index c3233cf7..00000000
--- a/packages/ui/src/components/grids/ObjectGrid.tsx
+++ /dev/null
@@ -1,557 +0,0 @@
-import * as React from "react"
-import { AgGridReact } from "ag-grid-react"
-import {
- ModuleRegistry,
- AllCommunityModule,
- type ColDef,
- type GridReadyEvent,
- type CellClickedEvent,
- type GridApi,
- type ICellRendererParams,
- type RowSelectedEvent,
- type CellEditRequestEvent,
-} from "ag-grid-community"
-
-// Register AG Grid Modules
-ModuleRegistry.registerModules([ AllCommunityModule ]);
-
-import "ag-grid-community/styles/ag-grid.css"
-import "ag-grid-community/styles/ag-theme-alpine.css"
-import { format } from "date-fns"
-import {
- CheckCircle2Icon,
- XCircleIcon,
- CalendarIcon,
-} from "lucide-react"
-
-import { Badge } from "@/components/ui/badge"
-
-// Import types from @objectql/types
-import type { ObjectConfig, FieldConfig, FieldType } from '@objectql/types'
-
-/**
- * Extended ColDef with custom properties for field metadata
- */
-export interface ExtendedColDef extends ColDef {
- fieldType?: FieldType
- fieldOptions?: any[]
-}
-
-/**
- * Props for ObjectGrid component
- */
-export interface ObjectGridProps {
- /** Object metadata configuration */
- objectConfig: ObjectConfig
- /** Row data to display */
- data: any[]
- /** Height of the grid */
- height?: string | number
- /** Enable pagination */
- pagination?: boolean
- /** Page size */
- pageSize?: number
- /** Enable row selection */
- rowSelection?: 'single' | 'multiple' | boolean
- /** Enable inline editing */
- editable?: boolean
- /** Enable column resizing */
- enableColumnResizing?: boolean
- /** Enable column reordering */
- enableColumnReordering?: boolean
- /** Enable column pinning */
- enableColumnPinning?: boolean
- /** Enable row dragging */
- enableRowDrag?: boolean
- /** Enable context menu */
- enableContextMenu?: boolean
- /** Enable CSV export */
- enableCsvExport?: boolean
- /** Enable Excel export */
- enableExcelExport?: boolean
- /** Callback when grid is ready */
- onGridReady?: (params: GridReadyEvent) => void
- /** Callback when cell is clicked */
- onCellClicked?: (event: CellClickedEvent) => void
- /** Callback when row is selected */
- onSelectionChanged?: (selectedRows: any[]) => void
- /** Callback when cell edit is requested */
- onCellEditRequest?: (event: CellEditRequestEvent) => void
- /** Additional column definitions to merge */
- additionalColumns?: ColDef[]
- /** Custom column definitions override */
- customColumnDefs?: ColDef[]
- /** Allow legacy calls with rowSelection boolean */
- legacyRowSelection?: boolean
-}
-
-// Backward compatibility for ObjectGridTableProps
-export type ObjectGridTableProps = ObjectGridProps
-
-/**
- * Cell renderer for boolean fields
- */
-const BooleanCellRenderer = (props: ICellRendererParams) => {
- const value = props.value
-
- if (value === null || value === undefined) {
- return -
- }
-
- return (
-
- {value ? (
-
- ) : (
-
- )}
-
- )
-}
-
-/**
- * Cell renderer for date/datetime fields
- */
-const DateCellRenderer = (props: ICellRendererParams & { fieldType?: FieldType }) => {
- const { value, fieldType } = props
-
- if (!value) {
- return -
- }
-
- try {
- const date = new Date(value)
- if (isNaN(date.getTime())) {
- return {String(value)}
- }
-
- if (fieldType === 'datetime') {
- return (
-
-
- {format(date, "PPp")}
-
- )
- }
-
- return (
-
-
- {format(date, "PP")}
-
- )
- } catch (e) {
- return {String(value)}
- }
-}
-
-/**
- * Cell renderer for number fields (including currency and percent)
- */
-const NumberCellRenderer = (props: ICellRendererParams & { fieldType?: FieldType }) => {
- const { value, fieldType } = props
-
- if (value === null || value === undefined) {
- return -
- }
-
- const num = Number(value)
-
- if (isNaN(num)) {
- return {String(value)}
- }
-
- let formatted = num.toLocaleString()
-
- if (fieldType === 'currency') {
- // TODO: Make locale and currency configurable via field config
- // For now, use browser's default locale with USD
- formatted = new Intl.NumberFormat(undefined, {
- style: 'currency',
- currency: 'USD',
- }).format(num)
- } else if (fieldType === 'percent') {
- formatted = `${num.toFixed(2)}%`
- }
-
- return {formatted}
-}
-
-/**
- * Cell renderer for select fields with options
- */
-const SelectCellRenderer = (props: ICellRendererParams & { fieldOptions?: any[] }) => {
- const { value, fieldOptions } = props
- const options = fieldOptions || []
-
- if (!value) {
- return -
- }
-
- const option = options.find((opt: any) => {
- const optValue = typeof opt === 'string' ? opt : opt.value
- return optValue === value
- })
-
- const label = option
- ? (typeof option === 'string' ? option : option.label)
- : String(value)
-
- return (
-
- {label}
-
- )
-}
-
-/**
- * Cell renderer for lookup/relationship fields
- */
-const LookupCellRenderer = (props: ICellRendererParams) => {
- const { value } = props
-
- if (!value) {
- return -
- }
-
- if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
- const displayValue = value.name || value.label || value.title || value._id
- return {displayValue}
- }
-
- return {String(value)}
-}
-
-/**
- * Sanitize email to prevent XSS attacks
- */
-const sanitizeEmail = (email: string): string => {
- const sanitized = String(email).trim();
- // Basic validation - must contain @ and not start with dangerous protocols
- const lowerEmail = sanitized.toLowerCase();
- if (!sanitized.includes('@') ||
- lowerEmail.startsWith('javascript:') ||
- lowerEmail.startsWith('data:') ||
- lowerEmail.startsWith('vbscript:') ||
- lowerEmail.startsWith('file:')) {
- return '';
- }
- return sanitized;
-}
-
-/**
- * Sanitize URL to prevent XSS attacks
- */
-const sanitizeUrl = (url: string): string => {
- const sanitized = String(url).trim();
- // Only allow http and https protocols
- if (!sanitized.match(/^https?:\/\//i)) {
- return '';
- }
- // Prevent javascript:, data:, vbscript:, and other dangerous protocols
- const lowerUrl = sanitized.toLowerCase();
- if (lowerUrl.match(/^(javascript|data|vbscript|file|about):/)) {
- return '';
- }
- return sanitized;
-}
-
-/**
- * Cell renderer for email fields
- */
-const EmailCellRenderer = (props: ICellRendererParams) => {
- const { value } = props
-
- if (!value) {
- return -
- }
-
- const sanitizedEmail = sanitizeEmail(value);
-
- if (!sanitizedEmail) {
- // Don't display potentially malicious content
- return Invalid email
- }
-
- return (
- e.stopPropagation()}
- >
- {sanitizedEmail}
-
- )
-}
-
-/**
- * Cell renderer for URL fields
- */
-const UrlCellRenderer = (props: ICellRendererParams) => {
- const { value } = props
-
- if (!value) {
- return -
- }
-
- const sanitizedUrl = sanitizeUrl(value);
-
- if (!sanitizedUrl) {
- // Don't display potentially malicious content
- return Invalid URL
- }
-
- return (
- e.stopPropagation()}
- >
- {sanitizedUrl}
-
- )
-}
-
-/**
- * Get the appropriate cell renderer based on field type
- */
-function getCellRendererForFieldType(fieldType: FieldType): any {
- switch (fieldType) {
- case 'boolean':
- return BooleanCellRenderer
-
- case 'date':
- case 'datetime':
- case 'time':
- return DateCellRenderer
-
- case 'number':
- case 'currency':
- case 'percent':
- return NumberCellRenderer
-
- case 'select':
- return SelectCellRenderer
-
- case 'lookup':
- case 'master_detail':
- return LookupCellRenderer
-
- case 'email':
- return EmailCellRenderer
-
- case 'url':
- return UrlCellRenderer
-
- default:
- return undefined
- }
-}
-
-/**
- * Generate AG Grid column definitions from ObjectQL object metadata
- */
-function generateColumnDefs(
- objectConfig: ObjectConfig,
- editable: boolean = false,
- enableColumnPinning: boolean = false,
- enableRowDrag: boolean = false
-): ColDef[] {
- const columnDefs: ColDef[] = []
-
- // Add Row Drag Column if enabled
- if (enableRowDrag) {
- columnDefs.push({
- field: '_drag',
- headerName: '',
- width: 50,
- minWidth: 50,
- maxWidth: 50,
- rowDrag: true,
- suppressHeaderMenuButton: true,
- resizable: false,
- sortable: false,
- filter: false,
- pinned: 'left',
- cellClass: 'cursor-move',
- })
- }
-
- const fields = objectConfig.fields || {}
-
- const entries: [string, FieldConfig][] = Array.isArray(fields)
- ? fields.map((f: any) => [f.name, f])
- : Object.entries(fields);
-
- entries.forEach(([fieldName, fieldConfig]: [string, FieldConfig]) => {
- if (fieldConfig.hidden) {
- return
- }
-
- const colDef: ColDef = {
- field: fieldName,
- headerName: fieldConfig.label || fieldName,
- sortable: true,
- filter: true,
- resizable: true,
- editable: editable && !fieldConfig.readonly && fieldConfig.type !== 'formula',
- cellRendererParams: {
- fieldType: fieldConfig.type,
- fieldOptions: fieldConfig.options,
- }
- }
-
- // Enable pinning if configured
- if (enableColumnPinning) {
- colDef.pinned = null // Allow user to pin columns
- }
-
- const cellRenderer = getCellRendererForFieldType(fieldConfig.type)
- if (cellRenderer) {
- colDef.cellRenderer = cellRenderer
- }
-
- // Set column width hints based on field type
- if (fieldConfig.type === 'boolean') {
- colDef.width = 80
- colDef.maxWidth = 100
- } else if (fieldConfig.type === 'date' || fieldConfig.type === 'datetime') {
- colDef.minWidth = 150
- } else if (fieldConfig.type === 'number' || fieldConfig.type === 'currency' || fieldConfig.type === 'percent') {
- colDef.minWidth = 100
- colDef.headerClass = 'ag-right-aligned-header'
- colDef.cellClass = 'ag-right-aligned-cell'
- } else if (fieldConfig.type === 'textarea' || fieldConfig.type === 'markdown') {
- colDef.minWidth = 200
- colDef.flex = 2
- } else if (fieldConfig.type === 'select') {
- colDef.minWidth = 120
- } else {
- colDef.minWidth = 120
- colDef.flex = 1
- }
-
- columnDefs.push(colDef)
- })
-
- return columnDefs
-}
-
-/**
- * ObjectGrid - A production-ready AG Grid component with all features
- *
- * Features:
- * - Column resizing, reordering, and pinning
- * - Row selection (single, multi, checkbox)
- * - Inline editing with validation
- * - Virtual scrolling for 100k+ rows
- * - Export to CSV/Excel
- * - Context menu actions
- * - Keyboard navigation
- */
-export function ObjectGrid({
- objectConfig,
- data,
- height = 600,
- pagination = true,
- pageSize = 10,
- rowSelection = false, // new standard is string 'single'|'multiple' or false
- legacyRowSelection, // backward compat param
- editable = false,
- enableColumnResizing = true,
- enableColumnReordering = true,
- enableColumnPinning = true,
- enableRowDrag = false,
- enableContextMenu = true,
- enableCsvExport = true,
- enableExcelExport = false,
- onGridReady,
- onCellClicked,
- onSelectionChanged,
- onCellEditRequest,
- additionalColumns = [],
- customColumnDefs,
-}: ObjectGridProps) {
- const [gridApi, setGridApi] = React.useState(null)
- const gridRef = React.useRef(null)
-
- // Resolve row selection mode (handle backward compatibility)
- const selectionMode = React.useMemo(() => {
- if (typeof rowSelection === 'string') return rowSelection;
- // Legacy boolean support
- if (rowSelection === true || legacyRowSelection === true) return 'multiple';
- return false;
- }, [rowSelection, legacyRowSelection]);
-
- // Generate column definitions from metadata
- const columnDefs = React.useMemo(() => {
- if (customColumnDefs) {
- return customColumnDefs
- }
- const generatedCols = generateColumnDefs(objectConfig, editable, enableColumnPinning, enableRowDrag)
- return [...generatedCols, ...additionalColumns]
- }, [objectConfig, editable, enableColumnPinning, enableRowDrag, additionalColumns, customColumnDefs])
-
- const defaultColDef = React.useMemo(() => ({
- resizable: enableColumnResizing,
- sortable: true,
- filter: true,
- editable: false, // Individual columns control editability
- }), [enableColumnResizing])
-
- const handleGridReady = (params: GridReadyEvent) => {
- setGridApi(params.api)
- onGridReady?.(params)
- }
-
- const handleSelectionChanged = React.useCallback(() => {
- if (!gridApi || !onSelectionChanged) return
- const selectedRows = gridApi.getSelectedRows()
- onSelectionChanged(selectedRows)
- }, [gridApi, onSelectionChanged])
-
- const handleCellEditRequest = React.useCallback((event: CellEditRequestEvent) => {
- onCellEditRequest?.(event)
- }, [onCellEditRequest])
-
- const selection = React.useMemo(() => {
- if (!selectionMode) return undefined;
- return {
- mode: selectionMode === 'multiple' ? 'multiRow' : 'singleRow',
- checkboxes: selectionMode === 'multiple',
- headerCheckbox: selectionMode === 'multiple',
- enableClickSelection: true,
- } as const;
- }, [selectionMode]);
-
- return (
-
-
params.data._id || params.data.id}
- enableRangeSelection={true}
- rowDragManaged={enableRowDrag}
- />
-
- )
-}
-
-// Backward compatibility alias
-export const ObjectGridTable = ObjectGrid
diff --git a/packages/ui/src/components/grids/index.ts b/packages/ui/src/components/grids/index.ts
deleted file mode 100644
index 638527e6..00000000
--- a/packages/ui/src/components/grids/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from './ObjectGrid';
diff --git a/packages/ui/src/components/layouts/DashboardLayout.tsx b/packages/ui/src/components/layouts/DashboardLayout.tsx
deleted file mode 100644
index f22c6dd9..00000000
--- a/packages/ui/src/components/layouts/DashboardLayout.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import React from 'react';
-// import { Responsive, WidthProvider } from 'react-grid-layout';
-
-// const ResponsiveGridLayout = WidthProvider(Responsive);
-
-export interface DashboardLayoutProps {
- // TODO: Define props based on page spec
- components: any[];
-}
-
-export const DashboardLayout: React.FC = ({ components }) => {
- return (
-
- {/* TODO: Implement React Grid Layout or CSS Grid */}
-
- Dashboard Layout Placeholder
-
-
- );
-};
diff --git a/packages/ui/src/components/layouts/ObjectPage.tsx b/packages/ui/src/components/layouts/ObjectPage.tsx
deleted file mode 100644
index eb40cad5..00000000
--- a/packages/ui/src/components/layouts/ObjectPage.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import React from 'react';
-
-export interface ObjectPageProps {
- pageId: string;
- context?: Record;
-}
-
-export const ObjectPage: React.FC = ({ pageId, context }) => {
- // TODO: Load page definition from Metadata Kernel
- // TODO: Resolve permissions
- // TODO: Inject PageContext
-
- return (
-
- {/* Placeholder for Layout Renderer */}
-
-
Page: {pageId}
- {/* Render Layout based on page config */}
-
-
- );
-};
diff --git a/packages/ui/src/components/layouts/SingleColumnLayout.tsx b/packages/ui/src/components/layouts/SingleColumnLayout.tsx
deleted file mode 100644
index 16b0cf0c..00000000
--- a/packages/ui/src/components/layouts/SingleColumnLayout.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import React from 'react';
-
-export interface SingleColumnLayoutProps {
- // TODO: Define props
- components: any[];
-}
-
-export const SingleColumnLayout: React.FC = ({ components }) => {
- return (
-
-
- Single Column Layout Placeholder
-
-
- );
-};
diff --git a/packages/ui/src/components/nav-documents.tsx b/packages/ui/src/components/nav-documents.tsx
deleted file mode 100644
index 6f4f182e..00000000
--- a/packages/ui/src/components/nav-documents.tsx
+++ /dev/null
@@ -1,85 +0,0 @@
-"use client"
-
-import {
- FolderIcon,
- MoreHorizontalIcon,
- ShareIcon,
- type LucideIcon,
-} from "lucide-react"
-
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu"
-import {
- SidebarGroup,
- SidebarGroupLabel,
- SidebarMenu,
- SidebarMenuAction,
- SidebarMenuButton,
- SidebarMenuItem,
- useSidebar,
-} from "@/components/ui/sidebar"
-
-export function NavDocuments({
- items,
-}: {
- items: {
- name: string
- url: string
- icon: LucideIcon
- }[]
-}) {
- const { isMobile } = useSidebar()
-
- return (
-
- Documents
-
- {items.map((item) => (
-
-
-
-
- {item.name}
-
-
-
-
-
-
- More
-
-
-
-
-
- Open
-
-
-
- Share
-
-
-
-
- ))}
-
-
-
- More
-
-
-
-
- )
-}
diff --git a/packages/ui/src/components/nav-main.tsx b/packages/ui/src/components/nav-main.tsx
deleted file mode 100644
index f33c9e43..00000000
--- a/packages/ui/src/components/nav-main.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import { MailIcon, PlusCircleIcon, type LucideIcon } from "lucide-react"
-
-import { Button } from "@/components/ui/button"
-import {
- SidebarGroup,
- SidebarGroupContent,
- SidebarMenu,
- SidebarMenuButton,
- SidebarMenuItem,
-} from "@/components/ui/sidebar"
-
-export function NavMain({
- items,
-}: {
- items: {
- title: string
- url: string
- icon?: LucideIcon
- }[]
-}) {
- return (
-
-
-
-
-
-
- Quick Create
-
-
-
- Inbox
-
-
-
-
- {items.map((item) => (
-
-
- {item.icon && }
- {item.title}
-
-
- ))}
-
-
-
- )
-}
diff --git a/packages/ui/src/components/nav-secondary.tsx b/packages/ui/src/components/nav-secondary.tsx
deleted file mode 100644
index e3d707e2..00000000
--- a/packages/ui/src/components/nav-secondary.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { LucideIcon } from "lucide-react"
-
-import {
- SidebarGroup,
- SidebarGroupContent,
- SidebarMenu,
- SidebarMenuButton,
- SidebarMenuItem,
-} from "@/components/ui/sidebar"
-
-export function NavSecondary({
- items,
- ...props
-}: {
- items: {
- title: string
- url: string
- icon: LucideIcon
- }[]
-} & React.ComponentPropsWithoutRef) {
- return (
-
-
-
- {items.map((item) => (
-
-
-
-
- {item.title}
-
-
-
- ))}
-
-
-
- )
-}
diff --git a/packages/ui/src/components/nav-user.tsx b/packages/ui/src/components/nav-user.tsx
deleted file mode 100644
index ddc3dcba..00000000
--- a/packages/ui/src/components/nav-user.tsx
+++ /dev/null
@@ -1,108 +0,0 @@
-import {
- BellIcon,
- CreditCardIcon,
- LogOutIcon,
- MoreVerticalIcon,
- UserCircleIcon,
-} from "lucide-react"
-
-import {
- Avatar,
- AvatarFallback,
- AvatarImage,
-} from "@/components/ui/avatar"
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuGroup,
- DropdownMenuItem,
- DropdownMenuLabel,
- DropdownMenuSeparator,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu"
-import {
- SidebarMenu,
- SidebarMenuButton,
- SidebarMenuItem,
- useSidebar,
-} from "@/components/ui/sidebar"
-
-export function NavUser({
- user,
-}: {
- user: {
- name: string
- email: string
- avatar: string
- }
-}) {
- const { isMobile } = useSidebar()
-
- return (
-
-
-
-
-
-
-
- CN
-
-
- {user.name}
-
- {user.email}
-
-
-
-
-
-
-
-
-
-
- CN
-
-
- {user.name}
-
- {user.email}
-
-
-
-
-
-
-
-
- Account
-
-
-
- Billing
-
-
-
- Notifications
-
-
-
-
-
- Log out
-
-
-
-
-
- )
-}
diff --git a/packages/ui/src/components/object-form.tsx b/packages/ui/src/components/object-form.tsx
deleted file mode 100644
index f818f41f..00000000
--- a/packages/ui/src/components/object-form.tsx
+++ /dev/null
@@ -1,511 +0,0 @@
-import * as React from "react"
-import { useForm, Controller, UseFormReturn } from "react-hook-form"
-import { zodResolver } from "@hookform/resolvers/zod"
-import { z } from "zod"
-import type { ObjectConfig, FieldConfig } from '@objectql/types'
-
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
-import { Field } from "@/components/fields/Field"
-import { FormSection } from "./forms/FormSection"
-import { FormActions } from "./forms/FormActions"
-import { cn } from "@/lib/utils"
-import type { LucideIcon } from "lucide-react"
-
-/**
- * Section configuration for ObjectForm
- */
-export interface FormSectionConfig {
- /** Section identifier */
- id: string
- /** Section title */
- title: string
- /** Section description (optional) */
- description?: string
- /** Icon for the section header */
- icon?: LucideIcon
- /** Field names that belong to this section */
- fields: string[]
- /** Whether this section is collapsible */
- collapsible?: boolean
- /** Whether this section is initially collapsed */
- defaultCollapsed?: boolean
- /** Number of columns for this section */
- columns?: 1 | 2
- /** Condition to show this section */
- visible?: (values: Record) => boolean
-}
-
-/**
- * Tab configuration for ObjectForm
- */
-export interface FormTabConfig {
- /** Tab identifier */
- id: string
- /** Tab label */
- label: string
- /** Sections in this tab */
- sections: FormSectionConfig[]
-}
-
-/**
- * Field dependency configuration
- */
-export interface FieldDependency {
- /** Source field that this field depends on */
- dependsOn: string
- /** Condition to show this field based on source field value */
- condition: (sourceValue: any, allValues: Record) => boolean
-}
-
-/**
- * Props for ObjectForm component
- */
-export interface ObjectFormProps {
- /** Object metadata configuration */
- objectConfig: ObjectConfig
- /** Initial form values */
- initialValues?: Record
- /** Callback when form is submitted with valid data */
- onSubmit: (data: Record) => void | Promise
- /** Callback when form is cancelled */
- onCancel?: () => void
- /** Callback for Save & New action */
- onSaveAndNew?: (data: Record) => void | Promise
- /** Whether the form is in loading/submitting state */
- isSubmitting?: boolean
- /** Custom submit button text */
- submitText?: string
- /** Custom Save & New button text */
- saveAndNewText?: string
- /** Custom cancel button text */
- cancelText?: string
- /** Hide Save & New button */
- hideSaveAndNew?: boolean
- /** Hide cancel button */
- hideCancelButton?: boolean
- /** Additional CSS classes for the form */
- className?: string
- /** Number of columns in the form grid (1 or 2) - used when no sections/tabs defined */
- columns?: 1 | 2
- /** Section configurations (optional, for organized layouts) */
- sections?: FormSectionConfig[]
- /** Tab configurations (optional, for tabbed layouts) */
- tabs?: FormTabConfig[]
- /** Field dependencies for conditional visibility */
- fieldDependencies?: Record
- /** Expose form methods to parent component */
- formRef?: React.MutableRefObject | null>
- /** Enable real-time validation */
- realtimeValidation?: boolean
-}
-
-/**
- * Determine if a field should span full width
- */
-function isFullWidthField(fieldConfig: FieldConfig): boolean {
- const fullWidthTypes = ['textarea', 'markdown', 'html', 'grid', 'object']
- return fullWidthTypes.includes(fieldConfig.type)
-}
-
-/**
- * Determine if a field should be hidden from the form
- */
-function shouldHideField(fieldName: string, fieldConfig: FieldConfig): boolean {
- if (fieldConfig.hidden) {
- return true
- }
-
- const systemFields = ['_id', 'id', 'created_at', 'updated_at', 'created_by', 'updated_by']
- if (systemFields.includes(fieldName)) {
- return true
- }
-
- if (fieldConfig.readonly && ['auto_number', 'formula', 'summary'].includes(fieldConfig.type)) {
- return true
- }
-
- return false
-}
-
-/**
- * Build Zod schema from ObjectConfig for validation
- */
-function buildZodSchema(objectConfig: ObjectConfig): z.ZodObject {
- const shape: Record = {}
-
- const fieldsConfig = objectConfig.fields || {}
- Object.entries(fieldsConfig).forEach(([fieldName, fieldConfig]) => {
- const field = fieldConfig as FieldConfig
-
- if (shouldHideField(fieldName, field)) {
- return
- }
-
- let fieldSchema: z.ZodTypeAny
-
- // Base schema based on field type
- switch (field.type) {
- case 'number':
- case 'currency':
- case 'percent':
- fieldSchema = z.number({ message: `${field.label || fieldName} must be a number` })
- if (field.min !== undefined) {
- fieldSchema = (fieldSchema as z.ZodNumber).min(field.min, { message: `Minimum value is ${field.min}` })
- }
- if (field.max !== undefined) {
- fieldSchema = (fieldSchema as z.ZodNumber).max(field.max, { message: `Maximum value is ${field.max}` })
- }
- break
-
- case 'email':
- fieldSchema = z.string()
- if (!field.regex) {
- fieldSchema = (fieldSchema as z.ZodString).email({ message: 'Invalid email address' })
- }
- break
-
- case 'url':
- fieldSchema = z.string()
- if (!field.regex) {
- fieldSchema = (fieldSchema as z.ZodString).url({ message: 'Invalid URL' })
- }
- break
-
- case 'boolean':
- fieldSchema = z.boolean().optional()
- break
-
- case 'date':
- case 'datetime':
- fieldSchema = z.union([z.string(), z.date()]).optional()
- break
-
- // Explicit cases for common field types
- case 'text':
- case 'password':
- case 'phone':
- case 'textarea':
- case 'markdown':
- case 'html':
- case 'select':
- case 'lookup':
- fieldSchema = z.string()
- break
-
- default:
- // Fallback for any other field types
- fieldSchema = z.string()
- }
-
- // Apply string-related validations
- // Note: Using 'as any' here is necessary due to Zod 4.x TypeScript limitations
- // TypeScript loses type information after instanceof checks with Zod's builder pattern
- // We use 'in' operator to safely check for method existence before calling
- let currentSchema = fieldSchema as any
- if (field.min_length !== undefined && 'min' in currentSchema) {
- currentSchema = currentSchema.min(field.min_length, { message: `Minimum length is ${field.min_length} characters` })
- }
- if (field.max_length !== undefined && 'max' in currentSchema) {
- currentSchema = currentSchema.max(field.max_length, { message: `Maximum length is ${field.max_length} characters` })
- }
- if (field.regex && 'regex' in currentSchema) {
- currentSchema = currentSchema.regex(new RegExp(field.regex), { message: 'Invalid format' })
- }
- fieldSchema = currentSchema
-
- // Handle required fields
- // Using 'as any' with 'in' check for type safety - Zod 4.x limitation
- if (field.required) {
- if ('min' in fieldSchema) {
- fieldSchema = (fieldSchema as any).min(1, { message: `${field.label || fieldName} is required` })
- }
- } else {
- fieldSchema = fieldSchema.optional()
- }
-
- shape[fieldName] = fieldSchema
- })
-
- return z.object(shape)
-}
-
-/**
- * ObjectForm - An advanced metadata-driven form generator
- *
- * Features:
- * - Metadata-driven field generation
- * - Zod-based validation (automatic)
- * - Section-based layouts
- * - Tab-based layouts
- * - Conditional field visibility
- * - Field dependencies
- * - Real-time validation
- */
-export function ObjectForm({
- objectConfig,
- initialValues = {},
- onSubmit,
- onCancel,
- onSaveAndNew,
- isSubmitting = false,
- submitText = 'Save',
- saveAndNewText = 'Save & New',
- cancelText = 'Cancel',
- hideSaveAndNew = false,
- hideCancelButton = false,
- className,
- columns = 2,
- sections,
- tabs,
- fieldDependencies = {},
- formRef,
- realtimeValidation = false,
-}: ObjectFormProps) {
- const validationSchema = React.useMemo(() => buildZodSchema(objectConfig), [objectConfig])
-
- const form = useForm({
- defaultValues: initialValues,
- resolver: zodResolver(validationSchema),
- mode: realtimeValidation ? 'onChange' : 'onSubmit',
- })
-
- const { control, handleSubmit, reset, watch, formState } = form
- const watchedValues = watch()
-
- // Expose form methods to parent if formRef is provided
- React.useEffect(() => {
- if (formRef) {
- formRef.current = form
- }
- }, [form, formRef])
-
- // Reset form when initialValues change
- React.useEffect(() => {
- reset(initialValues)
- }, [initialValues, reset])
-
- // Get visible fields based on dependencies
- const getVisibleFields = React.useCallback((fields: string[]) => {
- return fields.filter(fieldName => {
- const dependency = fieldDependencies[fieldName]
- if (!dependency) return true
-
- const sourceValue = watchedValues[dependency.dependsOn]
- return dependency.condition(sourceValue, watchedValues)
- })
- }, [fieldDependencies, watchedValues])
-
- // Render a single field
- const renderField = (fieldName: string, fieldConfig: FieldConfig, cols: 1 | 2 = columns) => {
- const isFullWidth = isFullWidthField(fieldConfig)
-
- return (
- (
-
-
-
- )}
- />
- )
- }
-
- // Get all fields from config
- const allFields = React.useMemo(() => {
- const fieldsConfig = objectConfig.fields || {}
- return Object.entries(fieldsConfig).filter(([fieldName, fieldConfig]) => {
- return !shouldHideField(fieldName, fieldConfig as FieldConfig)
- })
- }, [objectConfig])
-
- const handleFormSubmit = async (data: Record) => {
- try {
- await onSubmit(data)
- } catch (error) {
- console.error('Form submission error:', error)
- }
- }
-
- const handleSaveAndNew = async () => {
- const isValid = await form.trigger()
- if (!isValid) return
-
- const data = form.getValues()
- try {
- if (onSaveAndNew) {
- await onSaveAndNew(data)
- form.reset() // Reset form for new entry
- }
- } catch (error) {
- console.error('Save & New error:', error)
- }
- }
-
- // Render section-based layout
- const renderSectionLayout = () => {
- if (!sections || sections.length === 0) return null
-
- return (
-
- {sections.map(section => {
- // Check section visibility
- if (section.visible && !section.visible(watchedValues)) {
- return null
- }
-
- const visibleFields = getVisibleFields(section.fields)
- if (visibleFields.length === 0) return null
-
- return (
-
- {visibleFields.map(fieldName => {
- const fieldConfig = objectConfig.fields?.[fieldName] as FieldConfig
- if (!fieldConfig) return null
- return renderField(fieldName, fieldConfig, section.columns || columns)
- })}
-
- )
- })}
-
- )
- }
-
- // Render tab-based layout
- const renderTabLayout = () => {
- if (!tabs || tabs.length === 0) return null
-
- return (
-
-
- {tabs.map(tab => (
-
- {tab.label}
-
- ))}
-
- {tabs.map(tab => (
-
- {tab.sections.map(section => {
- // Check section visibility
- if (section.visible && !section.visible(watchedValues)) {
- return null
- }
-
- const visibleFields = getVisibleFields(section.fields)
- if (visibleFields.length === 0) return null
-
- return (
-
- {visibleFields.map(fieldName => {
- const fieldConfig = objectConfig.fields?.[fieldName] as FieldConfig
- if (!fieldConfig) return null
- return renderField(fieldName, fieldConfig, section.columns || columns)
- })}
-
- )
- })}
-
- ))}
-
- )
- }
-
- // Render default grid layout (no sections/tabs)
- const renderDefaultLayout = () => {
- return (
-
- {allFields.map(([fieldName, fieldConfig]) => {
- const field = fieldConfig as FieldConfig
-
- // Check field dependencies
- const dependency = fieldDependencies[fieldName]
- if (dependency) {
- const sourceValue = watchedValues[dependency.dependsOn]
- if (!dependency.condition(sourceValue, watchedValues)) {
- return null
- }
- }
-
- return renderField(fieldName, field)
- })}
-
- )
- }
-
- const errorCount = Object.keys(formState.errors).length
-
- return (
-
- )
-}
-
-/**
- * Hook to create a controlled ObjectForm instance
- * Useful when you need to access form methods from parent component
- */
-export function useObjectForm(objectConfig: ObjectConfig, initialValues?: Record) {
- const formRef = React.useRef | null>(null)
-
- return {
- formRef,
- getValues: () => formRef.current?.getValues(),
- setValue: (name: string, value: any) => formRef.current?.setValue(name, value),
- reset: (values?: Record) => formRef.current?.reset(values),
- trigger: (name?: string | string[]) => formRef.current?.trigger(name),
- }
-}
diff --git a/packages/ui/src/components/section-cards.tsx b/packages/ui/src/components/section-cards.tsx
deleted file mode 100644
index 579f3cc4..00000000
--- a/packages/ui/src/components/section-cards.tsx
+++ /dev/null
@@ -1,101 +0,0 @@
-import { TrendingDownIcon, TrendingUpIcon } from "lucide-react"
-
-import { Badge } from "@/components/ui/badge"
-import {
- Card,
- CardDescription,
- CardFooter,
- CardHeader,
- CardTitle,
-} from "@/components/ui/card"
-
-export function SectionCards() {
- return (
-
-
-
- Total Revenue
-
- $1,250.00
-
-
-
-
- +12.5%
-
-
-
-
-
- Trending up this month
-
-
- Visitors for the last 6 months
-
-
-
-
-
- New Customers
-
- 1,234
-
-
-
-
- -20%
-
-
-
-
-
- Down 20% this period
-
-
- Acquisition needs attention
-
-
-
-
-
- Active Accounts
-
- 45,678
-
-
-
-
- +12.5%
-
-
-
-
-
- Strong user retention
-
- Engagement exceed targets
-
-
-
-
- Growth Rate
-
- 4.5%
-
-
-
-
- +4.5%
-
-
-
-
-
- Steady performance
-
- Meets growth projections
-
-
-
- )
-}
diff --git a/packages/ui/src/components/shell/FilterBuilder.tsx b/packages/ui/src/components/shell/FilterBuilder.tsx
deleted file mode 100644
index 3e337d16..00000000
--- a/packages/ui/src/components/shell/FilterBuilder.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import React from 'react';
-import { Button } from '@/components/ui/button';
-
-export const FilterBuilder = () => {
- return (
-
-
-
Filter Builder Placeholder
-
Add Filter
-
-
- );
-};
diff --git a/packages/ui/src/components/shell/ViewToolbar.tsx b/packages/ui/src/components/shell/ViewToolbar.tsx
deleted file mode 100644
index 740056be..00000000
--- a/packages/ui/src/components/shell/ViewToolbar.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import React from 'react';
-import { Button } from '@/components/ui/button';
-
-export interface ViewToolbarProps {
- title: string;
- actions?: any[];
- viewSwitcherOptions?: any[];
-}
-
-export const ViewToolbar: React.FC = ({ title }) => {
- return (
-
-
{title}
-
- {/* Breadcrumbs, View Switcher, Actions */}
- Actions
-
-
- );
-};
diff --git a/packages/ui/src/components/site-header.tsx b/packages/ui/src/components/site-header.tsx
deleted file mode 100644
index eb16559d..00000000
--- a/packages/ui/src/components/site-header.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import { Separator } from "@/components/ui/separator"
-import { SidebarTrigger } from "@/components/ui/sidebar"
-
-export function SiteHeader() {
- return (
-
- )
-}
diff --git a/packages/ui/src/components/ui/accordion.tsx b/packages/ui/src/components/ui/accordion.tsx
deleted file mode 100644
index e6a723d0..00000000
--- a/packages/ui/src/components/ui/accordion.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import * as React from "react"
-import * as AccordionPrimitive from "@radix-ui/react-accordion"
-import { ChevronDown } from "lucide-react"
-
-import { cn } from "@/lib/utils"
-
-const Accordion = AccordionPrimitive.Root
-
-const AccordionItem = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-AccordionItem.displayName = "AccordionItem"
-
-const AccordionTrigger = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, children, ...props }, ref) => (
-
- svg]:rotate-180",
- className
- )}
- {...props}
- >
- {children}
-
-
-
-))
-AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
-
-const AccordionContent = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, children, ...props }, ref) => (
-
- {children}
-
-))
-
-AccordionContent.displayName = AccordionPrimitive.Content.displayName
-
-export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
diff --git a/packages/ui/src/components/ui/alert-dialog.tsx b/packages/ui/src/components/ui/alert-dialog.tsx
deleted file mode 100644
index 25e7b474..00000000
--- a/packages/ui/src/components/ui/alert-dialog.tsx
+++ /dev/null
@@ -1,141 +0,0 @@
-"use client"
-
-import * as React from "react"
-import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
-
-import { cn } from "@/lib/utils"
-import { buttonVariants } from "@/components/ui/button"
-
-const AlertDialog = AlertDialogPrimitive.Root
-
-const AlertDialogTrigger = AlertDialogPrimitive.Trigger
-
-const AlertDialogPortal = AlertDialogPrimitive.Portal
-
-const AlertDialogOverlay = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
-
-const AlertDialogContent = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-
-
-
-))
-AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
-
-const AlertDialogHeader = ({
- className,
- ...props
-}: React.HTMLAttributes) => (
-
-)
-AlertDialogHeader.displayName = "AlertDialogHeader"
-
-const AlertDialogFooter = ({
- className,
- ...props
-}: React.HTMLAttributes) => (
-
-)
-AlertDialogFooter.displayName = "AlertDialogFooter"
-
-const AlertDialogTitle = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
-
-const AlertDialogDescription = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-AlertDialogDescription.displayName =
- AlertDialogPrimitive.Description.displayName
-
-const AlertDialogAction = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
-
-const AlertDialogCancel = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
-
-export {
- AlertDialog,
- AlertDialogPortal,
- AlertDialogOverlay,
- AlertDialogTrigger,
- AlertDialogContent,
- AlertDialogHeader,
- AlertDialogFooter,
- AlertDialogTitle,
- AlertDialogDescription,
- AlertDialogAction,
- AlertDialogCancel,
-}
diff --git a/packages/ui/src/components/ui/alert.tsx b/packages/ui/src/components/ui/alert.tsx
deleted file mode 100644
index 41fa7e05..00000000
--- a/packages/ui/src/components/ui/alert.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-import * as React from "react"
-import { cva, type VariantProps } from "class-variance-authority"
-
-import { cn } from "@/lib/utils"
-
-const alertVariants = cva(
- "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
- {
- variants: {
- variant: {
- default: "bg-background text-foreground",
- destructive:
- "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
- },
- },
- defaultVariants: {
- variant: "default",
- },
- }
-)
-
-const Alert = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes & VariantProps
->(({ className, variant, ...props }, ref) => (
-
-))
-Alert.displayName = "Alert"
-
-const AlertTitle = React.forwardRef<
- HTMLParagraphElement,
- React.HTMLAttributes
->(({ className, ...props }, ref) => (
-
-))
-AlertTitle.displayName = "AlertTitle"
-
-const AlertDescription = React.forwardRef<
- HTMLParagraphElement,
- React.HTMLAttributes
->(({ className, ...props }, ref) => (
-
-))
-AlertDescription.displayName = "AlertDescription"
-
-export { Alert, AlertTitle, AlertDescription }
diff --git a/packages/ui/src/components/ui/aspect-ratio.tsx b/packages/ui/src/components/ui/aspect-ratio.tsx
deleted file mode 100644
index c4abbf37..00000000
--- a/packages/ui/src/components/ui/aspect-ratio.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
-
-const AspectRatio = AspectRatioPrimitive.Root
-
-export { AspectRatio }
diff --git a/packages/ui/src/components/ui/avatar.tsx b/packages/ui/src/components/ui/avatar.tsx
deleted file mode 100644
index 991f56ec..00000000
--- a/packages/ui/src/components/ui/avatar.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import * as React from "react"
-import * as AvatarPrimitive from "@radix-ui/react-avatar"
-
-import { cn } from "@/lib/utils"
-
-const Avatar = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-Avatar.displayName = AvatarPrimitive.Root.displayName
-
-const AvatarImage = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-AvatarImage.displayName = AvatarPrimitive.Image.displayName
-
-const AvatarFallback = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
-
-export { Avatar, AvatarImage, AvatarFallback }
diff --git a/packages/ui/src/components/ui/badge.tsx b/packages/ui/src/components/ui/badge.tsx
deleted file mode 100644
index f000e3ef..00000000
--- a/packages/ui/src/components/ui/badge.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import * as React from "react"
-import { cva, type VariantProps } from "class-variance-authority"
-
-import { cn } from "@/lib/utils"
-
-const badgeVariants = cva(
- "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
- {
- variants: {
- variant: {
- default:
- "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
- secondary:
- "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
- destructive:
- "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
- outline: "text-foreground",
- },
- },
- defaultVariants: {
- variant: "default",
- },
- }
-)
-
-export interface BadgeProps
- extends React.HTMLAttributes,
- VariantProps {}
-
-function Badge({ className, variant, ...props }: BadgeProps) {
- return (
-
- )
-}
-
-export { Badge, badgeVariants }
diff --git a/packages/ui/src/components/ui/breadcrumb.tsx b/packages/ui/src/components/ui/breadcrumb.tsx
deleted file mode 100644
index 60e6c96f..00000000
--- a/packages/ui/src/components/ui/breadcrumb.tsx
+++ /dev/null
@@ -1,115 +0,0 @@
-import * as React from "react"
-import { Slot } from "@radix-ui/react-slot"
-import { ChevronRight, MoreHorizontal } from "lucide-react"
-
-import { cn } from "@/lib/utils"
-
-const Breadcrumb = React.forwardRef<
- HTMLElement,
- React.ComponentPropsWithoutRef<"nav"> & {
- separator?: React.ReactNode
- }
->(({ ...props }, ref) => )
-Breadcrumb.displayName = "Breadcrumb"
-
-const BreadcrumbList = React.forwardRef<
- HTMLOListElement,
- React.ComponentPropsWithoutRef<"ol">
->(({ className, ...props }, ref) => (
-
-))
-BreadcrumbList.displayName = "BreadcrumbList"
-
-const BreadcrumbItem = React.forwardRef<
- HTMLLIElement,
- React.ComponentPropsWithoutRef<"li">
->(({ className, ...props }, ref) => (
-
-))
-BreadcrumbItem.displayName = "BreadcrumbItem"
-
-const BreadcrumbLink = React.forwardRef<
- HTMLAnchorElement,
- React.ComponentPropsWithoutRef<"a"> & {
- asChild?: boolean
- }
->(({ asChild, className, ...props }, ref) => {
- const Comp = asChild ? Slot : "a"
-
- return (
-
- )
-})
-BreadcrumbLink.displayName = "BreadcrumbLink"
-
-const BreadcrumbPage = React.forwardRef<
- HTMLSpanElement,
- React.ComponentPropsWithoutRef<"span">
->(({ className, ...props }, ref) => (
-
-))
-BreadcrumbPage.displayName = "BreadcrumbPage"
-
-const BreadcrumbSeparator = ({
- children,
- className,
- ...props
-}: React.ComponentProps<"li">) => (
- svg]:w-3.5 [&>svg]:h-3.5", className)}
- {...props}
- >
- {children ?? }
-
-)
-BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
-
-const BreadcrumbEllipsis = ({
- className,
- ...props
-}: React.ComponentProps<"span">) => (
-
-
- More
-
-)
-BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
-
-export {
- Breadcrumb,
- BreadcrumbList,
- BreadcrumbItem,
- BreadcrumbLink,
- BreadcrumbPage,
- BreadcrumbSeparator,
- BreadcrumbEllipsis,
-}
diff --git a/packages/ui/src/components/ui/button.tsx b/packages/ui/src/components/ui/button.tsx
deleted file mode 100644
index 36496a28..00000000
--- a/packages/ui/src/components/ui/button.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import * as React from "react"
-import { Slot } from "@radix-ui/react-slot"
-import { cva, type VariantProps } from "class-variance-authority"
-
-import { cn } from "@/lib/utils"
-
-const buttonVariants = cva(
- "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
- {
- variants: {
- variant: {
- default: "bg-primary text-primary-foreground hover:bg-primary/90",
- destructive:
- "bg-destructive text-destructive-foreground hover:bg-destructive/90",
- outline:
- "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
- secondary:
- "bg-secondary text-secondary-foreground hover:bg-secondary/80",
- ghost: "hover:bg-accent hover:text-accent-foreground",
- link: "text-primary underline-offset-4 hover:underline",
- },
- size: {
- default: "h-10 px-4 py-2",
- sm: "h-9 rounded-md px-3",
- lg: "h-11 rounded-md px-8",
- icon: "h-10 w-10",
- },
- },
- defaultVariants: {
- variant: "default",
- size: "default",
- },
- }
-)
-
-export interface ButtonProps
- extends React.ButtonHTMLAttributes,
- VariantProps {
- asChild?: boolean
-}
-
-const Button = React.forwardRef(
- ({ className, variant, size, asChild = false, ...props }, ref) => {
- const Comp = asChild ? Slot : "button"
- return (
-
- )
- }
-)
-Button.displayName = "Button"
-
-export { Button, buttonVariants }
diff --git a/packages/ui/src/components/ui/calendar.tsx b/packages/ui/src/components/ui/calendar.tsx
deleted file mode 100644
index a623682d..00000000
--- a/packages/ui/src/components/ui/calendar.tsx
+++ /dev/null
@@ -1,213 +0,0 @@
-"use client"
-
-import * as React from "react"
-import {
- ChevronDownIcon,
- ChevronLeftIcon,
- ChevronRightIcon,
-} from "lucide-react"
-import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
-
-import { cn } from "@/lib/utils"
-import { Button, buttonVariants } from "@/components/ui/button"
-
-function Calendar({
- className,
- classNames,
- showOutsideDays = true,
- captionLayout = "label",
- buttonVariant = "ghost",
- formatters,
- components,
- ...props
-}: React.ComponentProps & {
- buttonVariant?: React.ComponentProps["variant"]
-}) {
- const defaultClassNames = getDefaultClassNames()
-
- return (
- svg]:rotate-180`,
- String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
- className
- )}
- captionLayout={captionLayout}
- formatters={{
- formatMonthDropdown: (date) =>
- date.toLocaleString("default", { month: "short" }),
- ...formatters,
- }}
- classNames={{
- root: cn("w-fit", defaultClassNames.root),
- months: cn(
- "relative flex flex-col gap-4 md:flex-row",
- defaultClassNames.months
- ),
- month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
- nav: cn(
- "absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
- defaultClassNames.nav
- ),
- button_previous: cn(
- buttonVariants({ variant: buttonVariant }),
- "h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50",
- defaultClassNames.button_previous
- ),
- button_next: cn(
- buttonVariants({ variant: buttonVariant }),
- "h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50",
- defaultClassNames.button_next
- ),
- month_caption: cn(
- "flex h-[--cell-size] w-full items-center justify-center px-[--cell-size]",
- defaultClassNames.month_caption
- ),
- dropdowns: cn(
- "flex h-[--cell-size] w-full items-center justify-center gap-1.5 text-sm font-medium",
- defaultClassNames.dropdowns
- ),
- dropdown_root: cn(
- "has-focus:border-ring border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] relative rounded-md border",
- defaultClassNames.dropdown_root
- ),
- dropdown: cn(
- "bg-popover absolute inset-0 opacity-0",
- defaultClassNames.dropdown
- ),
- caption_label: cn(
- "select-none font-medium",
- captionLayout === "label"
- ? "text-sm"
- : "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5",
- defaultClassNames.caption_label
- ),
- table: "w-full border-collapse",
- weekdays: cn("flex", defaultClassNames.weekdays),
- weekday: cn(
- "text-muted-foreground flex-1 select-none rounded-md text-[0.8rem] font-normal",
- defaultClassNames.weekday
- ),
- week: cn("mt-2 flex w-full", defaultClassNames.week),
- week_number_header: cn(
- "w-[--cell-size] select-none",
- defaultClassNames.week_number_header
- ),
- week_number: cn(
- "text-muted-foreground select-none text-[0.8rem]",
- defaultClassNames.week_number
- ),
- day: cn(
- "group/day relative aspect-square h-full w-full select-none p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md",
- defaultClassNames.day
- ),
- range_start: cn(
- "bg-accent rounded-l-md",
- defaultClassNames.range_start
- ),
- range_middle: cn("rounded-none", defaultClassNames.range_middle),
- range_end: cn("bg-accent rounded-r-md", defaultClassNames.range_end),
- today: cn(
- "bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
- defaultClassNames.today
- ),
- outside: cn(
- "text-muted-foreground aria-selected:text-muted-foreground",
- defaultClassNames.outside
- ),
- disabled: cn(
- "text-muted-foreground opacity-50",
- defaultClassNames.disabled
- ),
- hidden: cn("invisible", defaultClassNames.hidden),
- ...classNames,
- }}
- components={{
- Root: ({ className, rootRef, ...props }) => {
- return (
-
- )
- },
- Chevron: ({ className, orientation, ...props }) => {
- if (orientation === "left") {
- return (
-
- )
- }
-
- if (orientation === "right") {
- return (
-
- )
- }
-
- return (
-
- )
- },
- DayButton: CalendarDayButton,
- WeekNumber: ({ children, ...props }) => {
- return (
-
-
- {children}
-
-
- )
- },
- ...components,
- }}
- {...props}
- />
- )
-}
-
-function CalendarDayButton({
- className,
- day,
- modifiers,
- ...props
-}: React.ComponentProps) {
- const defaultClassNames = getDefaultClassNames()
-
- const ref = React.useRef(null)
- React.useEffect(() => {
- if (modifiers.focused) ref.current?.focus()
- }, [modifiers.focused])
-
- return (
- span]:text-xs [&>span]:opacity-70",
- defaultClassNames.day,
- className
- )}
- {...props}
- />
- )
-}
-
-export { Calendar, CalendarDayButton }
diff --git a/packages/ui/src/components/ui/card.tsx b/packages/ui/src/components/ui/card.tsx
deleted file mode 100644
index f62edea5..00000000
--- a/packages/ui/src/components/ui/card.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import * as React from "react"
-
-import { cn } from "@/lib/utils"
-
-const Card = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes
->(({ className, ...props }, ref) => (
-
-))
-Card.displayName = "Card"
-
-const CardHeader = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes
->(({ className, ...props }, ref) => (
-
-))
-CardHeader.displayName = "CardHeader"
-
-const CardTitle = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes
->(({ className, ...props }, ref) => (
-
-))
-CardTitle.displayName = "CardTitle"
-
-const CardDescription = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes
->(({ className, ...props }, ref) => (
-
-))
-CardDescription.displayName = "CardDescription"
-
-const CardContent = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes
->(({ className, ...props }, ref) => (
-
-))
-CardContent.displayName = "CardContent"
-
-const CardFooter = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes
->(({ className, ...props }, ref) => (
-
-))
-CardFooter.displayName = "CardFooter"
-
-export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
diff --git a/packages/ui/src/components/ui/carousel.tsx b/packages/ui/src/components/ui/carousel.tsx
deleted file mode 100644
index 9c2b9bf3..00000000
--- a/packages/ui/src/components/ui/carousel.tsx
+++ /dev/null
@@ -1,260 +0,0 @@
-import * as React from "react"
-import useEmblaCarousel, {
- type UseEmblaCarouselType,
-} from "embla-carousel-react"
-import { ArrowLeft, ArrowRight } from "lucide-react"
-
-import { cn } from "@/lib/utils"
-import { Button } from "@/components/ui/button"
-
-type CarouselApi = UseEmblaCarouselType[1]
-type UseCarouselParameters = Parameters
-type CarouselOptions = UseCarouselParameters[0]
-type CarouselPlugin = UseCarouselParameters[1]
-
-type CarouselProps = {
- opts?: CarouselOptions
- plugins?: CarouselPlugin
- orientation?: "horizontal" | "vertical"
- setApi?: (api: CarouselApi) => void
-}
-
-type CarouselContextProps = {
- carouselRef: ReturnType[0]
- api: ReturnType[1]
- scrollPrev: () => void
- scrollNext: () => void
- canScrollPrev: boolean
- canScrollNext: boolean
-} & CarouselProps
-
-const CarouselContext = React.createContext(null)
-
-function useCarousel() {
- const context = React.useContext(CarouselContext)
-
- if (!context) {
- throw new Error("useCarousel must be used within a ")
- }
-
- return context
-}
-
-const Carousel = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes & CarouselProps
->(
- (
- {
- orientation = "horizontal",
- opts,
- setApi,
- plugins,
- className,
- children,
- ...props
- },
- ref
- ) => {
- const [carouselRef, api] = useEmblaCarousel(
- {
- ...opts,
- axis: orientation === "horizontal" ? "x" : "y",
- },
- plugins
- )
- const [canScrollPrev, setCanScrollPrev] = React.useState(false)
- const [canScrollNext, setCanScrollNext] = React.useState(false)
-
- const onSelect = React.useCallback((api: CarouselApi) => {
- if (!api) {
- return
- }
-
- setCanScrollPrev(api.canScrollPrev())
- setCanScrollNext(api.canScrollNext())
- }, [])
-
- const scrollPrev = React.useCallback(() => {
- api?.scrollPrev()
- }, [api])
-
- const scrollNext = React.useCallback(() => {
- api?.scrollNext()
- }, [api])
-
- const handleKeyDown = React.useCallback(
- (event: React.KeyboardEvent) => {
- if (event.key === "ArrowLeft") {
- event.preventDefault()
- scrollPrev()
- } else if (event.key === "ArrowRight") {
- event.preventDefault()
- scrollNext()
- }
- },
- [scrollPrev, scrollNext]
- )
-
- React.useEffect(() => {
- if (!api || !setApi) {
- return
- }
-
- setApi(api)
- }, [api, setApi])
-
- React.useEffect(() => {
- if (!api) {
- return
- }
-
- onSelect(api)
- api.on("reInit", onSelect)
- api.on("select", onSelect)
-
- return () => {
- api?.off("select", onSelect)
- }
- }, [api, onSelect])
-
- return (
-
-
- {children}
-
-
- )
- }
-)
-Carousel.displayName = "Carousel"
-
-const CarouselContent = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes
->(({ className, ...props }, ref) => {
- const { carouselRef, orientation } = useCarousel()
-
- return (
-
- )
-})
-CarouselContent.displayName = "CarouselContent"
-
-const CarouselItem = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes
->(({ className, ...props }, ref) => {
- const { orientation } = useCarousel()
-
- return (
-
- )
-})
-CarouselItem.displayName = "CarouselItem"
-
-const CarouselPrevious = React.forwardRef<
- HTMLButtonElement,
- React.ComponentProps
->(({ className, variant = "outline", size = "icon", ...props }, ref) => {
- const { orientation, scrollPrev, canScrollPrev } = useCarousel()
-
- return (
-
-
- Previous slide
-
- )
-})
-CarouselPrevious.displayName = "CarouselPrevious"
-
-const CarouselNext = React.forwardRef<
- HTMLButtonElement,
- React.ComponentProps
->(({ className, variant = "outline", size = "icon", ...props }, ref) => {
- const { orientation, scrollNext, canScrollNext } = useCarousel()
-
- return (
-
-
- Next slide
-
- )
-})
-CarouselNext.displayName = "CarouselNext"
-
-export {
- type CarouselApi,
- Carousel,
- CarouselContent,
- CarouselItem,
- CarouselPrevious,
- CarouselNext,
-}
diff --git a/packages/ui/src/components/ui/chart.tsx b/packages/ui/src/components/ui/chart.tsx
deleted file mode 100644
index f7b762f5..00000000
--- a/packages/ui/src/components/ui/chart.tsx
+++ /dev/null
@@ -1,369 +0,0 @@
-"use client"
-
-import * as React from "react"
-import * as RechartsPrimitive from "recharts"
-
-import { cn } from "@/lib/utils"
-
-// Format: { THEME_NAME: CSS_SELECTOR }
-const THEMES = { light: "", dark: ".dark" } as const
-
-export type ChartConfig = {
- [k in string]: {
- label?: React.ReactNode
- icon?: React.ComponentType
- } & (
- | { color?: string; theme?: never }
- | { color?: never; theme: Record }
- )
-}
-
-type ChartContextProps = {
- config: ChartConfig
-}
-
-const ChartContext = React.createContext(null)
-
-function useChart() {
- const context = React.useContext(ChartContext)
-
- if (!context) {
- throw new Error("useChart must be used within a ")
- }
-
- return context
-}
-
-const ChartContainer = React.forwardRef<
- HTMLDivElement,
- React.ComponentProps<"div"> & {
- config: ChartConfig
- children: React.ComponentProps<
- typeof RechartsPrimitive.ResponsiveContainer
- >["children"]
- }
->(({ id, className, children, config, ...props }, ref) => {
- const uniqueId = React.useId()
- const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
-
- return (
-
-
-
-
- {children}
-
-
-
- )
-})
-ChartContainer.displayName = "Chart"
-
-const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
- const colorConfig = Object.entries(config).filter(
- ([, config]) => config.theme || config.color
- )
-
- if (!colorConfig.length) {
- return null
- }
-
- return (
-