= {
+ short: { year: '2-digit', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric' },
+ medium: { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric' },
+ long: { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric' },
+ full: { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric', timeZoneName: 'short' },
+ };
+
+ return new Intl.DateTimeFormat(locale, styleMap[style] || styleMap.medium).format(d);
+}
+
+/**
+ * Format a relative time (e.g., "2 days ago", "in 3 hours")
+ */
+export function formatRelativeTime(
+ date: Date | string | number,
+ locale = 'en',
+): string {
+ const d = date instanceof Date ? date : new Date(date);
+ const now = new Date();
+ const diffMs = d.getTime() - now.getTime();
+ const diffSec = Math.round(diffMs / 1000);
+ const diffMin = Math.round(diffSec / 60);
+ const diffHour = Math.round(diffMin / 60);
+ const diffDay = Math.round(diffHour / 24);
+
+ const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
+
+ if (Math.abs(diffSec) < 60) return rtf.format(diffSec, 'second');
+ if (Math.abs(diffMin) < 60) return rtf.format(diffMin, 'minute');
+ if (Math.abs(diffHour) < 24) return rtf.format(diffHour, 'hour');
+ if (Math.abs(diffDay) < 30) return rtf.format(diffDay, 'day');
+
+ const diffMonth = Math.round(diffDay / 30);
+ if (Math.abs(diffMonth) < 12) return rtf.format(diffMonth, 'month');
+
+ return rtf.format(Math.round(diffDay / 365), 'year');
+}
+
+/**
+ * Format a currency value according to locale conventions
+ */
+export function formatCurrency(
+ value: number,
+ options: CurrencyFormatOptions = {},
+): string {
+ const { locale = 'en', currency = 'USD', minimumFractionDigits, maximumFractionDigits } = options;
+
+ return new Intl.NumberFormat(locale, {
+ style: 'currency',
+ currency,
+ minimumFractionDigits,
+ maximumFractionDigits,
+ }).format(value);
+}
+
+/**
+ * Format a number according to locale conventions
+ */
+export function formatNumber(
+ value: number,
+ options: NumberFormatOptions = {},
+): string {
+ const { locale = 'en', ...rest } = options;
+ return new Intl.NumberFormat(locale, rest).format(value);
+}
diff --git a/packages/i18n/src/utils/index.ts b/packages/i18n/src/utils/index.ts
new file mode 100644
index 00000000..667dc921
--- /dev/null
+++ b/packages/i18n/src/utils/index.ts
@@ -0,0 +1,10 @@
+export {
+ formatDate,
+ formatDateTime,
+ formatRelativeTime,
+ formatCurrency,
+ formatNumber,
+ type DateFormatOptions,
+ type CurrencyFormatOptions,
+ type NumberFormatOptions,
+} from './formatting';
diff --git a/packages/i18n/tsconfig.json b/packages/i18n/tsconfig.json
new file mode 100644
index 00000000..2a86bf02
--- /dev/null
+++ b/packages/i18n/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "dist",
+ "rootDir": "src",
+ "noEmit": false,
+ "declaration": true,
+ "composite": true
+ },
+ "include": ["src"],
+ "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"]
+}
diff --git a/packages/react/src/LazyPluginLoader.tsx b/packages/react/src/LazyPluginLoader.tsx
index c13cd40e..14c2b6c4 100644
--- a/packages/react/src/LazyPluginLoader.tsx
+++ b/packages/react/src/LazyPluginLoader.tsx
@@ -6,13 +6,83 @@
* LICENSE file in the root directory of this source tree.
*/
-import React, { lazy, Suspense } from 'react';
+import React, { lazy, Suspense, Component } from 'react';
export interface LazyPluginOptions {
/**
* Fallback component to show while loading
*/
fallback?: React.ReactNode;
+ /**
+ * Number of retry attempts on load failure (default: 2)
+ */
+ retries?: number;
+ /**
+ * Delay in ms between retries (default: 1000)
+ */
+ retryDelay?: number;
+ /**
+ * Custom error fallback component
+ */
+ errorFallback?: React.ComponentType<{ error: Error; retry: () => void }>;
+}
+
+/**
+ * Error boundary for lazy-loaded plugins
+ */
+interface ErrorBoundaryState {
+ hasError: boolean;
+ error: Error | null;
+}
+
+class PluginErrorBoundary extends Component<
+ { fallback?: React.ComponentType<{ error: Error; retry: () => void }>; children: React.ReactNode },
+ ErrorBoundaryState
+> {
+ state: ErrorBoundaryState = { hasError: false, error: null };
+
+ static getDerivedStateFromError(error: Error): ErrorBoundaryState {
+ return { hasError: true, error };
+ }
+
+ handleRetry = () => {
+ this.setState({ hasError: false, error: null });
+ };
+
+ render() {
+ if (this.state.hasError && this.state.error) {
+ const FallbackComponent = this.props.fallback;
+ if (FallbackComponent) {
+ return ;
+ }
+ return null;
+ }
+ return this.props.children;
+ }
+}
+
+/**
+ * Create a lazy-loaded import function with retry support
+ */
+function createRetryImport(
+ importFn: () => Promise<{ default: React.ComponentType
}>,
+ retries: number,
+ retryDelay: number,
+): () => Promise<{ default: React.ComponentType
}> {
+ return () => {
+ let attempt = 0;
+ const tryImport = (): Promise<{ default: React.ComponentType
}> =>
+ importFn().catch((err) => {
+ attempt++;
+ if (attempt <= retries) {
+ return new Promise((resolve) =>
+ setTimeout(() => resolve(tryImport()), retryDelay),
+ );
+ }
+ throw err;
+ });
+ return tryImport();
+ };
}
/**
@@ -34,19 +104,71 @@ export interface LazyPluginOptions {
* () => import('@object-ui/plugin-grid'),
* { fallback:
Loading grid...
}
* );
+ *
+ * // With retry and error handling
+ * const ObjectGrid = createLazyPlugin(
+ * () => import('@object-ui/plugin-grid'),
+ * {
+ * retries: 3,
+ * errorFallback: ({ error, retry }) => (
+ *
+ *
Failed to load: {error.message}
+ *
+ *
+ * ),
+ * }
+ * );
* ```
*/
export function createLazyPlugin(
importFn: () => Promise<{ default: React.ComponentType
}>,
options?: LazyPluginOptions
): React.ComponentType
{
- const LazyComponent = lazy(importFn);
+ const { retries = 2, retryDelay = 1000, errorFallback } = options || {};
+
+ const retryImport = retries > 0
+ ? createRetryImport(importFn, retries, retryDelay)
+ : importFn;
+
+ const LazyComponent = lazy(retryImport);
- const PluginWrapper: React.FC
= (props) => (
-
-
-
- );
+ const PluginWrapper: React.FC
= (props) => {
+ const content = (
+
+
+
+ );
+
+ if (errorFallback) {
+ return (
+
+ {content}
+
+ );
+ }
+
+ return content;
+ };
return PluginWrapper;
}
+
+/**
+ * Preload a plugin module without rendering it.
+ * Useful for preloading plugins that will be needed soon.
+ *
+ * @param importFn - Dynamic import function
+ * @returns Promise that resolves when the module is loaded
+ *
+ * @example
+ * ```tsx
+ * // Preload on hover
+ * const loadGrid = () => import('@object-ui/plugin-grid');
+ *
+ * ```
+ */
+export function preloadPlugin(
+ importFn: () => Promise,
+): Promise {
+ return importFn();
+}
diff --git a/playwright.config.ts b/playwright.config.ts
new file mode 100644
index 00000000..74a04106
--- /dev/null
+++ b/playwright.config.ts
@@ -0,0 +1,61 @@
+import { defineConfig, devices } from '@playwright/test';
+
+/**
+ * Playwright E2E test configuration for Object UI
+ * @see https://playwright.dev/docs/test-configuration
+ */
+export default defineConfig({
+ testDir: './e2e',
+ /* Run tests in files in parallel */
+ fullyParallel: true,
+ /* Fail the build on CI if you accidentally left test.only in the source code */
+ forbidOnly: !!process.env.CI,
+ /* Retry on CI only */
+ retries: process.env.CI ? 2 : 0,
+ /* Opt out of parallel tests on CI */
+ workers: process.env.CI ? 1 : undefined,
+ /* Reporter to use */
+ reporter: process.env.CI ? 'github' : 'html',
+ /* Shared settings for all projects */
+ use: {
+ /* Base URL to use in actions like `await page.goto('/')` */
+ baseURL: 'http://localhost:5173',
+ /* Collect trace when retrying the failed test */
+ trace: 'on-first-retry',
+ /* Screenshot on failure */
+ screenshot: 'only-on-failure',
+ },
+
+ /* Configure projects for major browsers */
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+ {
+ name: 'firefox',
+ use: { ...devices['Desktop Firefox'] },
+ },
+ {
+ name: 'webkit',
+ use: { ...devices['Desktop Safari'] },
+ },
+ /* Test against mobile viewports */
+ {
+ name: 'Mobile Chrome',
+ use: { ...devices['Pixel 5'] },
+ },
+ {
+ name: 'Mobile Safari',
+ use: { ...devices['iPhone 12'] },
+ },
+ ],
+
+ /* Run your local dev server before starting the tests */
+ webServer: {
+ command: 'pnpm run dev:console',
+ url: 'http://localhost:5173',
+ reuseExistingServer: !process.env.CI,
+ timeout: 120 * 1000,
+ },
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 20eaa06e..a33bec8a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -57,6 +57,9 @@ importers:
'@objectstack/runtime':
specifier: ^1.1.0
version: 1.1.0(pino@8.21.0)
+ '@playwright/test':
+ specifier: ^1.58.2
+ version: 1.58.2
'@storybook/addon-essentials':
specifier: ^8.6.14
version: 8.6.14(@types/react@19.2.10)(storybook@8.6.15(prettier@3.8.1))
@@ -411,19 +414,19 @@ importers:
version: 35.0.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
fumadocs-core:
specifier: 16.5.0
- version: 16.5.0(@types/react@19.2.10)(lucide-react@0.563.0(react@19.2.4))(next@16.1.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(zod@4.3.6)
+ version: 16.5.0(@types/react@19.2.10)(lucide-react@0.563.0(react@19.2.4))(next@16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(zod@4.3.6)
fumadocs-mdx:
specifier: 14.2.6
- version: 14.2.6(@types/react@19.2.10)(fumadocs-core@16.5.0(@types/react@19.2.10)(lucide-react@0.563.0(react@19.2.4))(next@16.1.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(zod@4.3.6))(next@16.1.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(vite@7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0))
+ version: 14.2.6(@types/react@19.2.10)(fumadocs-core@16.5.0(@types/react@19.2.10)(lucide-react@0.563.0(react@19.2.4))(next@16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(zod@4.3.6))(next@16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(vite@7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0))
fumadocs-ui:
specifier: 16.5.0
- version: 16.5.0(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(fumadocs-core@16.5.0(@types/react@19.2.10)(lucide-react@0.563.0(react@19.2.4))(next@16.1.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(zod@4.3.6))(next@16.1.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(tailwindcss@4.1.18)
+ version: 16.5.0(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(fumadocs-core@16.5.0(@types/react@19.2.10)(lucide-react@0.563.0(react@19.2.4))(next@16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(zod@4.3.6))(next@16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(tailwindcss@4.1.18)
lucide-react:
specifier: ^0.563.0
version: 0.563.0(react@19.2.4)
next:
specifier: 16.1.6
- version: 16.1.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ version: 16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react:
specifier: 19.2.4
version: 19.2.4
@@ -955,6 +958,28 @@ importers:
specifier: ^4.5.4
version: 4.5.4(@types/node@25.2.0)(rollup@4.57.1)(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0))
+ packages/i18n:
+ dependencies:
+ i18next:
+ specifier: ^25.8.4
+ version: 25.8.4(typescript@5.9.3)
+ react-i18next:
+ specifier: ^16.5.4
+ version: 16.5.4(i18next@25.8.4(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)
+ devDependencies:
+ '@types/react':
+ specifier: 19.2.10
+ version: 19.2.10
+ react:
+ specifier: 19.2.4
+ version: 19.2.4
+ typescript:
+ specifier: ^5.9.3
+ version: 5.9.3
+ vitest:
+ specifier: ^4.0.18
+ version: 4.0.18(@types/node@25.2.0)(@vitest/ui@4.0.18)(happy-dom@20.4.0)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@1.8.0))(lightningcss@1.30.2)(msw@2.12.7(@types/node@25.2.0)(typescript@5.9.3))(tsx@4.21.0)
+
packages/layout:
dependencies:
'@object-ui/components':
@@ -3583,6 +3608,11 @@ packages:
resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==}
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
+ '@playwright/test@1.58.2':
+ resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==}
+ engines: {node: '>=18'}
+ hasBin: true
+
'@polka/url@1.0.0-next.29':
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
@@ -7432,6 +7462,9 @@ packages:
html-escaper@2.0.2:
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
+ html-parse-stringify@3.0.1:
+ resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
+
html-url-attributes@3.0.1:
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
@@ -7472,6 +7505,14 @@ packages:
resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==}
engines: {node: '>=18.18.0'}
+ i18next@25.8.4:
+ resolution: {integrity: sha512-a9A0MnUjKvzjEN/26ZY1okpra9kA8MEwzYEz1BNm+IyxUKPRH6ihf0p7vj8YvULwZHKHl3zkJ6KOt4hewxBecQ==}
+ peerDependencies:
+ typescript: ^5
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
iconv-lite@0.4.24:
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
engines: {node: '>=0.10.0'}
@@ -9042,11 +9083,21 @@ packages:
engines: {node: '>=18'}
hasBin: true
+ playwright-core@1.58.2:
+ resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==}
+ engines: {node: '>=18'}
+ hasBin: true
+
playwright@1.58.1:
resolution: {integrity: sha512-+2uTZHxSCcxjvGc5C891LrS1/NlxglGxzrC4seZiVjcYVQfUa87wBL6rTDqzGjuoWNjnBzRqKmF6zRYGMvQUaQ==}
engines: {node: '>=18'}
hasBin: true
+ playwright@1.58.2:
+ resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==}
+ engines: {node: '>=18'}
+ hasBin: true
+
pluralize@2.0.0:
resolution: {integrity: sha512-TqNZzQCD4S42De9IfnnBvILN7HAW7riLqsCyp8lgjXeysyPlX5HhqKAcJHHHb9XskE4/a+7VGC9zzx8Ls0jOAw==}
@@ -9266,6 +9317,22 @@ packages:
peerDependencies:
react: 19.2.4
+ react-i18next@16.5.4:
+ resolution: {integrity: sha512-6yj+dcfMncEC21QPhOTsW8mOSO+pzFmT6uvU7XXdvM/Cp38zJkmTeMeKmTrmCMD5ToT79FmiE/mRWiYWcJYW4g==}
+ peerDependencies:
+ i18next: '>= 25.6.2'
+ react: 19.2.4
+ react-dom: '*'
+ react-native: '*'
+ typescript: ^5
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+ react-native:
+ optional: true
+ typescript:
+ optional: true
+
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
@@ -10700,6 +10767,10 @@ packages:
jsdom:
optional: true
+ void-elements@3.1.0:
+ resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
+ engines: {node: '>=0.10.0'}
+
vscode-uri@3.1.0:
resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
@@ -11861,9 +11932,9 @@ snapshots:
'@formatjs/fast-memoize': 3.1.0
tslib: 2.8.1
- '@fumadocs/ui@16.5.0(@types/react@19.2.10)(fumadocs-core@16.5.0(@types/react@19.2.10)(lucide-react@0.563.0(react@19.2.4))(next@16.1.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(zod@4.3.6))(next@16.1.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(tailwindcss@4.1.18)':
+ '@fumadocs/ui@16.5.0(@types/react@19.2.10)(fumadocs-core@16.5.0(@types/react@19.2.10)(lucide-react@0.563.0(react@19.2.4))(next@16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(zod@4.3.6))(next@16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(tailwindcss@4.1.18)':
dependencies:
- fumadocs-core: 16.5.0(@types/react@19.2.10)(lucide-react@0.563.0(react@19.2.4))(next@16.1.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(zod@4.3.6)
+ fumadocs-core: 16.5.0(@types/react@19.2.10)(lucide-react@0.563.0(react@19.2.4))(next@16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(zod@4.3.6)
next-themes: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
postcss-selector-parser: 7.1.1
react: 19.2.4
@@ -11871,7 +11942,7 @@ snapshots:
tailwind-merge: 3.4.0
optionalDependencies:
'@types/react': 19.2.10
- next: 16.1.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ next: 16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
tailwindcss: 4.1.18
'@hapi/address@5.1.1':
@@ -12667,6 +12738,10 @@ snapshots:
'@pkgr/core@0.2.9': {}
+ '@playwright/test@1.58.2':
+ dependencies:
+ playwright: 1.58.2
+
'@polka/url@1.0.0-next.29': {}
'@radix-ui/number@1.1.1': {}
@@ -16581,7 +16656,7 @@ snapshots:
fsevents@2.3.3:
optional: true
- fumadocs-core@16.5.0(@types/react@19.2.10)(lucide-react@0.563.0(react@19.2.4))(next@16.1.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(zod@4.3.6):
+ fumadocs-core@16.5.0(@types/react@19.2.10)(lucide-react@0.563.0(react@19.2.4))(next@16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(zod@4.3.6):
dependencies:
'@formatjs/intl-localematcher': 0.8.1
'@orama/orama': 3.1.18
@@ -16605,7 +16680,7 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.10
lucide-react: 0.563.0(react@19.2.4)
- next: 16.1.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ next: 16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
react-router: 7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -16613,14 +16688,14 @@ snapshots:
transitivePeerDependencies:
- supports-color
- fumadocs-mdx@14.2.6(@types/react@19.2.10)(fumadocs-core@16.5.0(@types/react@19.2.10)(lucide-react@0.563.0(react@19.2.4))(next@16.1.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(zod@4.3.6))(next@16.1.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(vite@7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)):
+ fumadocs-mdx@14.2.6(@types/react@19.2.10)(fumadocs-core@16.5.0(@types/react@19.2.10)(lucide-react@0.563.0(react@19.2.4))(next@16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(zod@4.3.6))(next@16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(vite@7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)):
dependencies:
'@mdx-js/mdx': 3.1.1
'@standard-schema/spec': 1.1.0
chokidar: 5.0.0
esbuild: 0.27.2
estree-util-value-to-estree: 3.5.0
- fumadocs-core: 16.5.0(@types/react@19.2.10)(lucide-react@0.563.0(react@19.2.4))(next@16.1.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(zod@4.3.6)
+ fumadocs-core: 16.5.0(@types/react@19.2.10)(lucide-react@0.563.0(react@19.2.4))(next@16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(zod@4.3.6)
js-yaml: 4.1.1
mdast-util-to-markdown: 2.1.2
picocolors: 1.1.1
@@ -16635,15 +16710,15 @@ snapshots:
zod: 4.3.6
optionalDependencies:
'@types/react': 19.2.10
- next: 16.1.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ next: 16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react: 19.2.4
vite: 7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)
transitivePeerDependencies:
- supports-color
- fumadocs-ui@16.5.0(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(fumadocs-core@16.5.0(@types/react@19.2.10)(lucide-react@0.563.0(react@19.2.4))(next@16.1.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(zod@4.3.6))(next@16.1.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(tailwindcss@4.1.18):
+ fumadocs-ui@16.5.0(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(fumadocs-core@16.5.0(@types/react@19.2.10)(lucide-react@0.563.0(react@19.2.4))(next@16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(zod@4.3.6))(next@16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(tailwindcss@4.1.18):
dependencies:
- '@fumadocs/ui': 16.5.0(@types/react@19.2.10)(fumadocs-core@16.5.0(@types/react@19.2.10)(lucide-react@0.563.0(react@19.2.4))(next@16.1.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(zod@4.3.6))(next@16.1.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(tailwindcss@4.1.18)
+ '@fumadocs/ui': 16.5.0(@types/react@19.2.10)(fumadocs-core@16.5.0(@types/react@19.2.10)(lucide-react@0.563.0(react@19.2.4))(next@16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(zod@4.3.6))(next@16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(tailwindcss@4.1.18)
'@radix-ui/react-accordion': 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -16655,7 +16730,7 @@ snapshots:
'@radix-ui/react-slot': 1.2.4(@types/react@19.2.10)(react@19.2.4)
'@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
class-variance-authority: 0.7.1
- fumadocs-core: 16.5.0(@types/react@19.2.10)(lucide-react@0.563.0(react@19.2.4))(next@16.1.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(zod@4.3.6)
+ fumadocs-core: 16.5.0(@types/react@19.2.10)(lucide-react@0.563.0(react@19.2.4))(next@16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(zod@4.3.6)
lucide-react: 0.563.0(react@19.2.4)
next-themes: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react: 19.2.4
@@ -16664,7 +16739,7 @@ snapshots:
scroll-into-view-if-needed: 3.1.0
optionalDependencies:
'@types/react': 19.2.10
- next: 16.1.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ next: 16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
tailwindcss: 4.1.18
transitivePeerDependencies:
- '@types/react-dom'
@@ -16964,6 +17039,10 @@ snapshots:
html-escaper@2.0.2: {}
+ html-parse-stringify@3.0.1:
+ dependencies:
+ void-elements: 3.1.0
+
html-url-attributes@3.0.1: {}
html-void-elements@3.0.0: {}
@@ -17014,6 +17093,12 @@ snapshots:
human-signals@8.0.1: {}
+ i18next@25.8.4(typescript@5.9.3):
+ dependencies:
+ '@babel/runtime': 7.28.6
+ optionalDependencies:
+ typescript: 5.9.3
+
iconv-lite@0.4.24:
dependencies:
safer-buffer: 2.1.2
@@ -18603,7 +18688,7 @@ snapshots:
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
- next@16.1.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+ next@16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
'@next/env': 16.1.6
'@swc/helpers': 0.5.15
@@ -18622,6 +18707,7 @@ snapshots:
'@next/swc-linux-x64-musl': 16.1.6
'@next/swc-win32-arm64-msvc': 16.1.6
'@next/swc-win32-x64-msvc': 16.1.6
+ '@playwright/test': 1.58.2
sharp: 0.34.5
transitivePeerDependencies:
- '@babel/core'
@@ -19031,12 +19117,20 @@ snapshots:
playwright-core@1.58.1: {}
+ playwright-core@1.58.2: {}
+
playwright@1.58.1:
dependencies:
playwright-core: 1.58.1
optionalDependencies:
fsevents: 2.3.2
+ playwright@1.58.2:
+ dependencies:
+ playwright-core: 1.58.2
+ optionalDependencies:
+ fsevents: 2.3.2
+
pluralize@2.0.0: {}
pluralize@8.0.0: {}
@@ -19269,6 +19363,17 @@ snapshots:
dependencies:
react: 19.2.4
+ react-i18next@16.5.4(i18next@25.8.4(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3):
+ dependencies:
+ '@babel/runtime': 7.28.6
+ html-parse-stringify: 3.0.1
+ i18next: 25.8.4(typescript@5.9.3)
+ react: 19.2.4
+ use-sync-external-store: 1.6.0(react@19.2.4)
+ optionalDependencies:
+ react-dom: 19.2.4(react@19.2.4)
+ typescript: 5.9.3
+
react-is@16.13.1: {}
react-is@17.0.2: {}
@@ -20946,6 +21051,8 @@ snapshots:
- tsx
- yaml
+ void-elements@3.1.0: {}
+
vscode-uri@3.1.0: {}
vue-template-compiler@2.7.16:
diff --git a/vitest.config.mts b/vitest.config.mts
index cbfaf4b1..95803026 100644
--- a/vitest.config.mts
+++ b/vitest.config.mts
@@ -11,7 +11,7 @@ export default defineConfig({
environment: 'happy-dom',
testTimeout: 15000, // Increase default timeout for integration tests with MSW
setupFiles: [path.resolve(__dirname, 'vitest.setup.tsx')],
- exclude: ['**/node_modules/**', '**/dist/**', '**/cypress/**', '**/.{idea,git,cache,output,temp}/**'],
+ exclude: ['**/node_modules/**', '**/dist/**', '**/cypress/**', '**/e2e/**', '**/.{idea,git,cache,output,temp}/**'],
passWithNoTests: true,
coverage: {
provider: 'v8',
@@ -39,6 +39,7 @@ export default defineConfig({
},
resolve: {
alias: {
+ '@object-ui/i18n': path.resolve(__dirname, './packages/i18n/src'),
'@object-ui/core': path.resolve(__dirname, './packages/core/src'),
'@object-ui/types/zod': path.resolve(__dirname, './packages/types/src/zod/index.zod.ts'),
'@object-ui/types': path.resolve(__dirname, './packages/types/src'),