@omnikit-js/ui 0.5.4 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +100 -255
  2. package/dist/components/client/index.d.mts +7 -0
  3. package/dist/components/client/index.mjs +6 -0
  4. package/dist/components/client/index.mjs.map +1 -0
  5. package/dist/components/server/index.d.mts +2 -0
  6. package/dist/components/server/index.mjs +201 -0
  7. package/dist/components/server/index.mjs.map +1 -0
  8. package/dist/index-D-5etDTV.d.mts +80 -0
  9. package/dist/index.d.mts +48 -0
  10. package/dist/index.mjs +204 -0
  11. package/dist/index.mjs.map +1 -0
  12. package/package.json +41 -77
  13. package/LICENSE +0 -21
  14. package/dist/styles.css +0 -2486
  15. package/dist/styles.isolated.css +0 -2643
  16. package/dist/styles.prefixed.css +0 -248
  17. package/src/components/BillingClient.tsx +0 -686
  18. package/src/components/BillingServer.tsx +0 -129
  19. package/src/components/PaymentMethodForm.tsx +0 -125
  20. package/src/components/SubscriptionConfirmModal.tsx +0 -213
  21. package/src/components/theme-provider.tsx +0 -11
  22. package/src/components/ui/alert.tsx +0 -59
  23. package/src/components/ui/avatar.tsx +0 -53
  24. package/src/components/ui/badge.tsx +0 -46
  25. package/src/components/ui/button.tsx +0 -59
  26. package/src/components/ui/card-brand-icon.tsx +0 -88
  27. package/src/components/ui/card.tsx +0 -92
  28. package/src/components/ui/chart.tsx +0 -353
  29. package/src/components/ui/dialog.tsx +0 -122
  30. package/src/components/ui/dropdown-menu.tsx +0 -257
  31. package/src/components/ui/input.tsx +0 -21
  32. package/src/components/ui/label.tsx +0 -24
  33. package/src/components/ui/navigation-menu.tsx +0 -168
  34. package/src/components/ui/progress.tsx +0 -31
  35. package/src/components/ui/radio-group.tsx +0 -44
  36. package/src/components/ui/select.tsx +0 -185
  37. package/src/components/ui/separator.tsx +0 -28
  38. package/src/components/ui/switch.tsx +0 -31
  39. package/src/components/ui/tabs.tsx +0 -66
  40. package/src/components/ui/textarea.tsx +0 -18
  41. package/src/components/ui/tooltip.tsx +0 -30
  42. package/src/index.ts +0 -13
  43. package/src/lib/utils.ts +0 -6
  44. package/src/styles/globals.css +0 -1
  45. package/src/styles/isolated.css +0 -316
  46. package/src/styles/main.css +0 -95
  47. package/src/styles/prefixed-main.css +0 -95
  48. package/src/styles/prefixed.css +0 -1
package/README.md CHANGED
@@ -1,300 +1,145 @@
1
1
  # @omnikit-js/ui
2
2
 
3
- A comprehensive SaaS billing component for Next.js applications with Stripe integration. Built with React Server Components support and Tailwind CSS v4.
4
-
5
- ## Features
6
-
7
- - 🚀 **Server Components Support** - Optimized for Next.js App Router
8
- - 💳 **Stripe Integration** - Complete payment processing and subscription management
9
- - 🎨 **Beautiful UI** - Built with Tailwind CSS v4 and Radix UI
10
- - 📊 **Usage Tracking** - Monitor and display resource usage
11
- - 🔄 **Subscription Management** - Handle upgrades, downgrades, and cancellations
12
- - 💰 **Payment Methods** - Add, remove, and manage multiple payment methods
13
- - 📜 **Billing History** - View and download past invoices
14
- - 🔐 **Secure** - Server-side API key handling
15
- - 🎯 **No Style Conflicts** - Prefixed CSS option to avoid conflicts with your app
3
+ React component library for OmniKit serverless platform. Provides both server and client components for building data-driven applications.
16
4
 
17
5
  ## Installation
18
6
 
19
7
  ```bash
20
8
  npm install @omnikit-js/ui
9
+ # or
10
+ pnpm add @omnikit-js/ui
11
+ # or
12
+ yarn add @omnikit-js/ui
21
13
  ```
22
14
 
23
- ## Usage
24
-
25
- ### Option 1: Standard CSS (For Apps Without Tailwind)
26
-
27
- If your app doesn't use Tailwind CSS or you want to use the component's default styles:
28
-
29
- ```javascript
30
- // Import the CSS in your app's entry point (e.g., _app.tsx or layout.tsx)
31
- import '@omnikit-js/ui/dist/styles.css'
15
+ ## Components
32
16
 
33
- // Use the component
34
- import { Billing } from '@omnikit-js/ui'
35
-
36
- export default function BillingPage() {
37
- return (
38
- <Billing
39
- userId="user_123"
40
- apiUrl="https://your-api.com"
41
- apiKey="your-api-key"
42
- />
43
- )
44
- }
45
- ```
17
+ ### DataList
46
18
 
47
- ### Option 2: Isolated CSS (Recommended for Apps With Style Conflicts)
19
+ A server-side component that fetches and displays data from OmniKit API with zero client JavaScript.
48
20
 
49
- To completely isolate the component styles and avoid any conflicts:
21
+ **Features:**
22
+ - Server-side rendering (SSR)
23
+ - Automatic data fetching from OmniKit `/data/{table}` endpoints
24
+ - Support for filtering, sorting, pagination
25
+ - Tailwind CSS styling (customizable)
26
+ - TypeScript support
50
27
 
51
- ```javascript
52
- // Import the isolated CSS version
53
- import '@omnikit-js/ui/dist/styles.isolated.css'
28
+ **Usage:**
54
29
 
55
- // Use the component
56
- import { Billing } from '@omnikit-js/ui'
30
+ ```tsx
31
+ import { DataList } from '@omnikit-js/ui'
57
32
 
58
- export default function BillingPage() {
33
+ export default async function UsersPage() {
59
34
  return (
60
- <Billing
61
- userId="user_123"
62
- apiUrl="https://your-api.com"
63
- apiKey="your-api-key"
35
+ <DataList
36
+ table="users"
37
+ baseUrl={process.env.OMNIKIT_BASE_URL!}
38
+ apiKey={process.env.OMNIKIT_API_KEY!}
39
+ columns={['name', 'email', 'created_at']}
40
+ filter={{ active: true }}
41
+ limit={20}
64
42
  />
65
43
  )
66
44
  }
67
45
  ```
68
46
 
69
- **Note:** The isolated version wraps all styles in `.omnikit-billing-component` and uses `!important` for critical styles like progress bars to prevent conflicts.
70
-
71
- ### Next.js App Router Example
72
-
73
- ```javascript
74
- // app/billing/page.tsx
75
- import '@omnikit-js/ui/dist/styles.isolated.css'
76
- import { Billing } from '@omnikit-js/ui'
77
-
78
- export default function BillingPage() {
79
- return (
80
- <main className="container mx-auto p-4">
81
- <h1 className="text-2xl font-bold mb-6">Billing & Subscription</h1>
82
- <Billing
83
- userId="user_123"
84
- apiUrl={process.env.NEXT_PUBLIC_API_URL}
85
- apiKey={process.env.API_KEY}
86
- />
87
- </main>
88
- )
89
- }
90
- ```
91
-
92
- ### Next.js Pages Router Example
93
-
94
- ```javascript
95
- // pages/_app.tsx
96
- import '@omnikit-js/ui/dist/styles.isolated.css'
97
- import type { AppProps } from 'next/app'
98
-
99
- export default function App({ Component, pageProps }: AppProps) {
100
- return <Component {...pageProps} />
101
- }
102
-
103
- // pages/billing.tsx
104
- import { Billing } from '@omnikit-js/ui'
105
-
106
- export default function BillingPage() {
107
- return (
108
- <div className="container mx-auto p-4">
109
- <h1 className="text-2xl font-bold mb-6">Billing & Subscription</h1>
110
- <Billing
111
- userId="user_123"
112
- apiUrl={process.env.NEXT_PUBLIC_API_URL}
113
- apiKey={process.env.API_KEY}
114
- />
115
- </div>
116
- )
117
- }
118
- ```
119
-
120
- ### Using Client Component Directly
121
-
122
- If you need to use the client component directly (e.g., in a client-side only context):
123
-
124
- ```jsx
125
- 'use client';
126
-
127
- import { BillingClient } from '@omnikit-js/ui';
128
-
129
- export default function ClientBillingPage({ customerData, pricingData, paymentMethods }) {
130
- return (
131
- <BillingClient
132
- customerData={customerData}
133
- pricingData={pricingData}
134
- paymentMethods={paymentMethods}
135
- apiUrl="https://api.yourdomain.com"
136
- apiKey="your-api-key"
137
- />
138
- );
139
- }
47
+ **Props:**
48
+
49
+ | Prop | Type | Required | Description |
50
+ |------|------|----------|-------------|
51
+ | `table` | `string` | Yes | Table name to query |
52
+ | `baseUrl` | `string` | Yes | OmniKit API base URL (e.g., `http://localhost:3000/omnikit`) |
53
+ | `apiKey` | `string` | No | API Key for authentication |
54
+ | `jwt` | `string` | No | JWT token (alternative to apiKey) |
55
+ | `columns` | `Column[]` | Yes | Columns to display |
56
+ | `keyField` | `string` | No | Row key field (default: `'id'`) |
57
+ | `filter` | `object` | No | Query filter |
58
+ | `sort` | `string` | No | Sort order (e.g., `'name:asc'`) |
59
+ | `limit` | `number` | No | Max records to fetch |
60
+ | `offset` | `number` | No | Pagination offset |
61
+ | `className` | `string` | No | Custom container class |
62
+ | `emptyMessage` | `string` | No | Message when no data |
63
+ | `errorMessage` | `string` | No | Error message |
64
+
65
+ **Custom Columns:**
66
+
67
+ ```tsx
68
+ <DataList
69
+ table="users"
70
+ baseUrl={baseUrl}
71
+ apiKey={apiKey}
72
+ columns={[
73
+ 'name',
74
+ 'email',
75
+ {
76
+ key: 'created_at',
77
+ label: 'Joined',
78
+ render: (value) => new Date(value).toLocaleDateString(),
79
+ },
80
+ {
81
+ key: 'active',
82
+ label: 'Status',
83
+ render: (value) => value ? '✓ Active' : '✗ Inactive',
84
+ },
85
+ ]}
86
+ />
140
87
  ```
141
88
 
142
- ## Resolving Style Conflicts
143
-
144
- If you experience style conflicts, especially with progress bars or grid layouts:
89
+ ## API Client
145
90
 
146
- ### Progress Bar Color Conflicts
91
+ Use the OmniKit client for custom data fetching:
147
92
 
148
- Override the progress bar styles in your global CSS:
93
+ ```tsx
94
+ import { createOmniKitClient } from '@omnikit-js/ui'
149
95
 
150
- ```css
151
- /* Force specific progress bar colors */
152
- .omnikit-progress-fill {
153
- background-color: #3b82f6 !important;
154
- }
96
+ const client = createOmniKitClient({
97
+ baseUrl: 'http://localhost:3000/omnikit',
98
+ apiKey: 'your-api-key',
99
+ })
155
100
 
156
- /* Or for dark mode */
157
- @media (prefers-color-scheme: dark) {
158
- .omnikit-progress-fill {
159
- background-color: #60a5fa !important;
160
- }
161
- }
162
- ```
101
+ // Query data
102
+ const users = await client.query('users', {
103
+ filter: { active: true },
104
+ sort: 'name:asc',
105
+ limit: 10,
106
+ })
163
107
 
164
- ### Grid Layout Conflicts
108
+ // Find by ID
109
+ const user = await client.findById('users', '123')
165
110
 
166
- Wrap the component in a scoped container to isolate styles:
167
-
168
- ```css
169
- /* Create an isolation container */
170
- .billing-wrapper {
171
- /* Reset conflicting styles */
172
- all: initial;
173
- font-family: inherit;
174
- color: inherit;
175
- }
176
-
177
- .billing-wrapper * {
178
- box-sizing: border-box;
179
- }
111
+ // Insert
112
+ await client.insert('users', { name: 'John', email: 'john@example.com' })
180
113
 
181
- /* Fix specific grid issues */
182
- .billing-wrapper .omnikit-pricing-grid {
183
- display: grid !important;
184
- gap: 1rem !important;
185
- }
186
- ```
114
+ // Update
115
+ await client.update('users', '123', { name: 'Jane' })
187
116
 
188
- ```javascript
189
- <div className="billing-wrapper">
190
- <Billing userId="user_123" />
191
- </div>
117
+ // Delete
118
+ await client.delete('users', '123')
192
119
  ```
193
120
 
194
- ## Component Props
195
-
196
- ### Billing (Server Component)
197
-
198
- | Prop | Type | Default | Description |
199
- |------|------|---------|-------------|
200
- | userId | string | required | The user ID to fetch billing data for |
201
- | apiUrl | string | `process.env.NEXT_PUBLIC_API_URL` | The API base URL |
202
- | apiKey | string | `process.env.API_KEY` | The API key for authentication |
203
- | projectId | string | `process.env.NEXT_PUBLIC_API_PROJECT` | The project ID for multi-tenant support |
204
-
205
- ### BillingClient (Client Component)
121
+ ## Development
206
122
 
207
- | Prop | Type | Description |
208
- |------|------|-------------|
209
- | customerData | object | Customer data including subscriptions and usage |
210
- | pricingData | object | Available pricing plans and products |
211
- | paymentMethods | object | User's payment methods |
212
- | apiUrl | string | The API base URL |
213
- | apiKey | string | The API key for authentication |
123
+ ### Quick Start
214
124
 
215
- ## Environment Variables
125
+ Run library watch build + demo app:
216
126
 
217
- Create a `.env.local` file in your Next.js project:
218
-
219
- ```env
220
- # API Configuration
221
- NEXT_PUBLIC_API_URL=http://localhost:3000
222
- API_KEY=your-secret-api-key
223
-
224
- # Stripe Configuration
225
- NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_your_publishable_key
226
-
227
- # Project Configuration (optional)
228
- NEXT_PUBLIC_API_PROJECT=1
127
+ ```bash
128
+ cd /Users/marcoschwartz/Documents/code/frontends/omnikit/omnikit-ui
129
+ ./dev.sh
229
130
  ```
230
131
 
231
- ## API Endpoints
132
+ Visit: **http://localhost:3060**
232
133
 
233
- The component expects the following API endpoints:
134
+ See [QUICKSTART.md](QUICKSTART.md) for detailed instructions.
234
135
 
235
- - `GET /billing/customers/:userId` - Fetch customer data
236
- - `GET /billing/prices?project_id=:projectId` - Fetch pricing plans
237
- - `GET /billing/payment-methods?user_id=:userId` - Fetch payment methods
238
- - `POST /billing/setup-intent` - Create Stripe setup intent
239
- - `POST /billing/subscriptions` - Create subscription
240
- - `DELETE /billing/subscriptions/:id` - Cancel subscription
241
- - `POST /billing/payment-methods/:id/default` - Set default payment method
242
- - `DELETE /billing/payment-methods/:id` - Delete payment method
136
+ ### Tech Stack
243
137
 
244
- ## Troubleshooting
245
-
246
- ### Common Issues and Solutions
247
-
248
- 1. **Progress bar showing wrong color**
249
- - Use the prefixed CSS version
250
- - Override `.omnikit-progress-fill` with `!important`
251
-
252
- 2. **Grid layout breaking on pricing page**
253
- - Check for global grid styles in your app
254
- - Use the isolation container approach shown above
255
- - Ensure no conflicting CSS Grid properties
256
-
257
- 3. **Modal z-index conflicts**
258
- - Component modals use `z-50` (z-index: 50)
259
- - Adjust your app's z-index scale if needed
260
-
261
- 4. **Font inconsistencies**
262
- - The component inherits font-family
263
- - Set font on a parent element
264
-
265
- 5. **Styles not loading**
266
- - Ensure you're importing the CSS file
267
- - Check the correct path: `@omnikit-js/ui/dist/styles.css` or `@omnikit-js/ui/dist/styles.isolated.css`
268
- - For apps with style conflicts, use the isolated version
269
-
270
- ## Version History
271
-
272
- - **0.5.3** - Added isolated CSS option for better style conflict resolution
273
- - **0.5.2** - Fixed CSS exports in package.json
274
- - **0.5.1** - Tailwind CSS v4 support with prefix option
275
- - **0.3.5** - Previous version with Tailwind CSS v3
276
-
277
- ## Browser Support
278
-
279
- Requires modern browsers:
280
- - Safari 16.4+
281
- - Chrome 111+
282
- - Firefox 128+
283
-
284
- For older browser support, use version 0.3.5.
285
-
286
- ## TypeScript
287
-
288
- The package includes TypeScript definitions. No additional setup required.
138
+ - **React 18/19** - Peer dependency
139
+ - **TypeScript** - Full type safety
140
+ - **Tailwind CSS v4** - Styling (requires Tailwind in your project)
141
+ - **tsup** - Build tooling
289
142
 
290
143
  ## License
291
144
 
292
145
  MIT
293
-
294
- ## Contributing
295
-
296
- Contributions are welcome! Please feel free to submit a Pull Request.
297
-
298
- ## Support
299
-
300
- For issues and feature requests, please use the [GitHub issue tracker](https://github.com/omnikit-ui/billing/issues).
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Client Components
3
+ * These components run on the client side
4
+ */
5
+ declare const OMNIKIT_CLIENT_VERSION = "0.1.0";
6
+
7
+ export { OMNIKIT_CLIENT_VERSION };
@@ -0,0 +1,6 @@
1
+ // src/components/client/index.ts
2
+ var OMNIKIT_CLIENT_VERSION = "0.1.0";
3
+
4
+ export { OMNIKIT_CLIENT_VERSION };
5
+ //# sourceMappingURL=index.mjs.map
6
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/client/index.ts"],"names":[],"mappings":";AAMO,IAAM,sBAAA,GAAyB","file":"index.mjs","sourcesContent":["/**\n * Client Components\n * These components run on the client side\n */\n\n// Placeholder for future client components\nexport const OMNIKIT_CLIENT_VERSION = '0.1.0'\n"]}
@@ -0,0 +1,2 @@
1
+ export { C as Column, c as ColumnDefinition, D as DataList, b as DataListProps } from '../../index-D-5etDTV.mjs';
2
+ import 'react/jsx-runtime';
@@ -0,0 +1,201 @@
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
+
3
+ // src/lib/omnikit-client.ts
4
+ var OmniKitClient = class {
5
+ constructor(config) {
6
+ this.baseUrl = config.baseUrl.replace(/\/$/, "");
7
+ this.apiKey = config.apiKey;
8
+ this.jwt = config.jwt;
9
+ }
10
+ getHeaders() {
11
+ const headers = {
12
+ "Content-Type": "application/json"
13
+ };
14
+ if (this.jwt) {
15
+ headers["Authorization"] = `Bearer ${this.jwt}`;
16
+ } else if (this.apiKey) {
17
+ headers["X-API-Key"] = this.apiKey;
18
+ }
19
+ return headers;
20
+ }
21
+ async request(endpoint, options) {
22
+ const url = `${this.baseUrl}${endpoint}`;
23
+ const response = await fetch(url, {
24
+ ...options,
25
+ headers: {
26
+ ...this.getHeaders(),
27
+ ...options?.headers
28
+ }
29
+ });
30
+ if (!response.ok) {
31
+ const error = await response.json().catch(() => ({ error: "Request failed" }));
32
+ throw new Error(error.error || `HTTP ${response.status}`);
33
+ }
34
+ return response.json();
35
+ }
36
+ /**
37
+ * Query records from a table
38
+ */
39
+ async query(table, options) {
40
+ const params = new URLSearchParams();
41
+ if (options?.filter) {
42
+ params.append("filter", JSON.stringify(options.filter));
43
+ }
44
+ if (options?.sort) {
45
+ params.append("sort", options.sort);
46
+ }
47
+ if (options?.limit) {
48
+ params.append("limit", options.limit.toString());
49
+ }
50
+ if (options?.offset) {
51
+ params.append("offset", options.offset.toString());
52
+ }
53
+ const queryString = params.toString();
54
+ const endpoint = `/data/${table}${queryString ? `?${queryString}` : ""}`;
55
+ return this.request(endpoint);
56
+ }
57
+ /**
58
+ * Find a record by ID
59
+ */
60
+ async findById(table, id) {
61
+ return this.request(`/data/${table}/${id}`);
62
+ }
63
+ /**
64
+ * Insert a new record
65
+ */
66
+ async insert(table, data) {
67
+ return this.request(`/data/${table}`, {
68
+ method: "POST",
69
+ body: JSON.stringify(data)
70
+ });
71
+ }
72
+ /**
73
+ * Update an existing record
74
+ */
75
+ async update(table, id, data) {
76
+ return this.request(`/data/${table}/${id}`, {
77
+ method: "PUT",
78
+ body: JSON.stringify(data)
79
+ });
80
+ }
81
+ /**
82
+ * Delete a record
83
+ */
84
+ async delete(table, id) {
85
+ return this.request(`/data/${table}/${id}`, {
86
+ method: "DELETE"
87
+ });
88
+ }
89
+ /**
90
+ * Upsert a record (insert or update)
91
+ */
92
+ async upsert(table, data) {
93
+ return this.request(`/data/${table}`, {
94
+ method: "PUT",
95
+ body: JSON.stringify(data)
96
+ });
97
+ }
98
+ };
99
+ function createOmniKitClient(config) {
100
+ return new OmniKitClient(config);
101
+ }
102
+ function normalizeColumn(column) {
103
+ if (typeof column === "string") {
104
+ return {
105
+ key: column,
106
+ label: column.charAt(0).toUpperCase() + column.slice(1).replace(/_/g, " "),
107
+ sortable: true
108
+ };
109
+ }
110
+ return {
111
+ ...column,
112
+ label: column.label || column.key.charAt(0).toUpperCase() + column.key.slice(1).replace(/_/g, " ")
113
+ };
114
+ }
115
+ function getCellValue(row, key) {
116
+ return key.split(".").reduce((obj, k) => obj?.[k], row);
117
+ }
118
+ async function DataList({
119
+ table,
120
+ baseUrl,
121
+ apiKey,
122
+ jwt,
123
+ columns,
124
+ keyField = "id",
125
+ filter,
126
+ sort,
127
+ limit,
128
+ offset,
129
+ className = "",
130
+ headerClassName = "",
131
+ rowClassName = "",
132
+ cellClassName = "",
133
+ emptyMessage = "No data found",
134
+ errorMessage = "Failed to load data"
135
+ }) {
136
+ const client = createOmniKitClient({ baseUrl, apiKey, jwt });
137
+ let data = [];
138
+ let error = null;
139
+ try {
140
+ const response = await client.query(table, {
141
+ filter,
142
+ sort,
143
+ limit,
144
+ offset
145
+ });
146
+ if (response.success) {
147
+ data = response.data;
148
+ } else {
149
+ error = response.error || errorMessage;
150
+ }
151
+ } catch (err) {
152
+ error = err instanceof Error ? err.message : errorMessage;
153
+ }
154
+ const normalizedColumns = columns.map(normalizeColumn);
155
+ if (error) {
156
+ return /* @__PURE__ */ jsxs("div", { className: `rounded-lg border border-red-200 dark:border-red-800 bg-red-50 dark:bg-red-900/20 p-4 text-red-800 dark:text-red-200 ${className}`, children: [
157
+ /* @__PURE__ */ jsx("p", { className: "font-medium", children: "Error" }),
158
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm", children: error })
159
+ ] });
160
+ }
161
+ if (data.length === 0) {
162
+ return /* @__PURE__ */ jsx("div", { className: `rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 p-8 text-center text-gray-600 dark:text-gray-400 ${className}`, children: /* @__PURE__ */ jsx("p", { children: emptyMessage }) });
163
+ }
164
+ return /* @__PURE__ */ jsx("div", { className: `overflow-x-auto rounded-lg border border-gray-200 dark:border-gray-700 ${className}`, children: /* @__PURE__ */ jsxs("table", { className: "min-w-full divide-y divide-gray-200 dark:divide-gray-700", children: [
165
+ /* @__PURE__ */ jsx("thead", { className: `bg-gray-50 dark:bg-gray-800 ${headerClassName}`, children: /* @__PURE__ */ jsx("tr", { children: normalizedColumns.map((column) => /* @__PURE__ */ jsx(
166
+ "th",
167
+ {
168
+ className: `px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-700 dark:text-gray-300 ${column.className || ""}`,
169
+ children: column.label
170
+ },
171
+ column.key
172
+ )) }) }),
173
+ /* @__PURE__ */ jsx("tbody", { className: "divide-y divide-gray-200 dark:divide-gray-700 bg-white dark:bg-gray-900", children: data.map((row) => {
174
+ const rowKey = String(row[keyField]);
175
+ const computedRowClassName = typeof rowClassName === "function" ? rowClassName(row) : rowClassName;
176
+ return /* @__PURE__ */ jsx(
177
+ "tr",
178
+ {
179
+ className: `hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors ${computedRowClassName}`,
180
+ children: normalizedColumns.map((column) => {
181
+ const cellValue = getCellValue(row, column.key);
182
+ const computedCellClassName = typeof cellClassName === "function" ? cellClassName(column, row) : cellClassName;
183
+ return /* @__PURE__ */ jsx(
184
+ "td",
185
+ {
186
+ className: `px-6 py-4 text-sm text-gray-900 dark:text-gray-100 ${computedCellClassName}`,
187
+ children: column.render ? column.render(cellValue, row) : cellValue
188
+ },
189
+ column.key
190
+ );
191
+ })
192
+ },
193
+ rowKey
194
+ );
195
+ }) })
196
+ ] }) });
197
+ }
198
+
199
+ export { DataList };
200
+ //# sourceMappingURL=index.mjs.map
201
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/lib/omnikit-client.ts","../../../src/components/server/DataList/index.tsx"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * OmniKit API Client\n * Wrapper for making requests to OmniKit serverless platform\n */\n\nimport type {\n QueryOptions,\n QueryResponse,\n SingleRecordResponse,\n MutationResponse,\n OmniKitConfig,\n} from './types'\n\nexport class OmniKitClient {\n private baseUrl: string\n private apiKey?: string\n private jwt?: string\n\n constructor(config: OmniKitConfig) {\n this.baseUrl = config.baseUrl.replace(/\\/$/, '') // Remove trailing slash\n this.apiKey = config.apiKey\n this.jwt = config.jwt\n }\n\n private getHeaders(): HeadersInit {\n const headers: HeadersInit = {\n 'Content-Type': 'application/json',\n }\n\n if (this.jwt) {\n headers['Authorization'] = `Bearer ${this.jwt}`\n } else if (this.apiKey) {\n headers['X-API-Key'] = this.apiKey\n }\n\n return headers\n }\n\n private async request<T>(\n endpoint: string,\n options?: RequestInit\n ): Promise<T> {\n const url = `${this.baseUrl}${endpoint}`\n\n const response = await fetch(url, {\n ...options,\n headers: {\n ...this.getHeaders(),\n ...options?.headers,\n },\n })\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({ error: 'Request failed' }))\n throw new Error(error.error || `HTTP ${response.status}`)\n }\n\n return response.json()\n }\n\n /**\n * Query records from a table\n */\n async query<T = any>(\n table: string,\n options?: QueryOptions\n ): Promise<QueryResponse<T>> {\n const params = new URLSearchParams()\n\n if (options?.filter) {\n params.append('filter', JSON.stringify(options.filter))\n }\n if (options?.sort) {\n params.append('sort', options.sort)\n }\n if (options?.limit) {\n params.append('limit', options.limit.toString())\n }\n if (options?.offset) {\n params.append('offset', options.offset.toString())\n }\n\n const queryString = params.toString()\n const endpoint = `/data/${table}${queryString ? `?${queryString}` : ''}`\n\n return this.request<QueryResponse<T>>(endpoint)\n }\n\n /**\n * Find a record by ID\n */\n async findById<T = any>(\n table: string,\n id: string | number\n ): Promise<SingleRecordResponse<T>> {\n return this.request<SingleRecordResponse<T>>(`/data/${table}/${id}`)\n }\n\n /**\n * Insert a new record\n */\n async insert(\n table: string,\n data: Record<string, any>\n ): Promise<MutationResponse> {\n return this.request<MutationResponse>(`/data/${table}`, {\n method: 'POST',\n body: JSON.stringify(data),\n })\n }\n\n /**\n * Update an existing record\n */\n async update(\n table: string,\n id: string | number,\n data: Record<string, any>\n ): Promise<MutationResponse> {\n return this.request<MutationResponse>(`/data/${table}/${id}`, {\n method: 'PUT',\n body: JSON.stringify(data),\n })\n }\n\n /**\n * Delete a record\n */\n async delete(table: string, id: string | number): Promise<MutationResponse> {\n return this.request<MutationResponse>(`/data/${table}/${id}`, {\n method: 'DELETE',\n })\n }\n\n /**\n * Upsert a record (insert or update)\n */\n async upsert(\n table: string,\n data: Record<string, any>\n ): Promise<MutationResponse> {\n return this.request<MutationResponse>(`/data/${table}`, {\n method: 'PUT',\n body: JSON.stringify(data),\n })\n }\n}\n\n/**\n * Create a new OmniKit client instance\n */\nexport function createOmniKitClient(config: OmniKitConfig): OmniKitClient {\n return new OmniKitClient(config)\n}\n","/**\n * DataList Component\n *\n * A server-side React component that fetches and displays data from OmniKit API.\n * Renders on the server with zero client JavaScript for data fetching.\n *\n * @example\n * ```tsx\n * import { DataList } from '@omnikit-js/ui'\n *\n * export default function UsersPage() {\n * return (\n * <DataList\n * table=\"users\"\n * baseUrl={process.env.OMNIKIT_BASE_URL}\n * apiKey={process.env.OMNIKIT_API_KEY}\n * columns={['name', 'email', 'created_at']}\n * filter={{ active: true }}\n * limit={20}\n * />\n * )\n * }\n * ```\n */\n\nimport { createOmniKitClient } from '../../../lib/omnikit-client'\nimport type { DataListProps, Column, ColumnDefinition } from './types'\n\nfunction normalizeColumn<T>(column: Column<T>): ColumnDefinition<T> {\n if (typeof column === 'string') {\n return {\n key: column,\n label: column.charAt(0).toUpperCase() + column.slice(1).replace(/_/g, ' '),\n sortable: true,\n }\n }\n return {\n ...column,\n label: column.label || column.key.charAt(0).toUpperCase() + column.key.slice(1).replace(/_/g, ' '),\n }\n}\n\nfunction getCellValue(row: any, key: string): any {\n return key.split('.').reduce((obj, k) => obj?.[k], row)\n}\n\nexport async function DataList<T extends Record<string, any>>({\n table,\n baseUrl,\n apiKey,\n jwt,\n columns,\n keyField = 'id' as keyof T,\n filter,\n sort,\n limit,\n offset,\n className = '',\n headerClassName = '',\n rowClassName = '',\n cellClassName = '',\n emptyMessage = 'No data found',\n errorMessage = 'Failed to load data',\n}: DataListProps<T>) {\n // Create API client\n const client = createOmniKitClient({ baseUrl, apiKey, jwt })\n\n // Fetch data\n let data: T[] = []\n let error: string | null = null\n\n try {\n const response = await client.query<T>(table, {\n filter,\n sort,\n limit,\n offset,\n })\n\n if (response.success) {\n data = response.data\n } else {\n error = response.error || errorMessage\n }\n } catch (err) {\n error = err instanceof Error ? err.message : errorMessage\n }\n\n // Normalize columns\n const normalizedColumns = columns.map(normalizeColumn)\n\n // Error state\n if (error) {\n return (\n <div className={`rounded-lg border border-red-200 dark:border-red-800 bg-red-50 dark:bg-red-900/20 p-4 text-red-800 dark:text-red-200 ${className}`}>\n <p className=\"font-medium\">Error</p>\n <p className=\"mt-1 text-sm\">{error}</p>\n </div>\n )\n }\n\n // Empty state\n if (data.length === 0) {\n return (\n <div className={`rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 p-8 text-center text-gray-600 dark:text-gray-400 ${className}`}>\n <p>{emptyMessage}</p>\n </div>\n )\n }\n\n // Render table\n return (\n <div className={`overflow-x-auto rounded-lg border border-gray-200 dark:border-gray-700 ${className}`}>\n <table className=\"min-w-full divide-y divide-gray-200 dark:divide-gray-700\">\n <thead className={`bg-gray-50 dark:bg-gray-800 ${headerClassName}`}>\n <tr>\n {normalizedColumns.map((column) => (\n <th\n key={column.key}\n className={`px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-700 dark:text-gray-300 ${column.className || ''}`}\n >\n {column.label}\n </th>\n ))}\n </tr>\n </thead>\n <tbody className=\"divide-y divide-gray-200 dark:divide-gray-700 bg-white dark:bg-gray-900\">\n {data.map((row) => {\n const rowKey = String(row[keyField])\n const computedRowClassName = typeof rowClassName === 'function'\n ? rowClassName(row)\n : rowClassName\n\n return (\n <tr\n key={rowKey}\n className={`hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors ${computedRowClassName}`}\n >\n {normalizedColumns.map((column) => {\n const cellValue = getCellValue(row, column.key)\n const computedCellClassName = typeof cellClassName === 'function'\n ? cellClassName(column, row)\n : cellClassName\n\n return (\n <td\n key={column.key}\n className={`px-6 py-4 text-sm text-gray-900 dark:text-gray-100 ${computedCellClassName}`}\n >\n {column.render ? column.render(cellValue, row) : cellValue}\n </td>\n )\n })}\n </tr>\n )\n })}\n </tbody>\n </table>\n </div>\n )\n}\n"]}