@rozenite/sqlite-plugin 2.0.0 → 2.2.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 (44) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/devtools/assets/{panel-CsA6Ejak.js → panel-B2O8mY15.js} +4 -4
  3. package/dist/devtools/panel.html +1 -1
  4. package/dist/react-native/chunks/index.require.js +4 -16
  5. package/dist/react-native/chunks/sql.require.js +1 -3
  6. package/dist/react-native/chunks/useRozeniteSqlitePlugin.require.js +92 -110
  7. package/dist/react-native/index.d.ts +1 -1
  8. package/dist/rozenite.json +1 -1
  9. package/package.json +23 -23
  10. package/react-native.ts +7 -14
  11. package/src/react-native/adapters/__tests__/expo-sqlite.test.ts +4 -14
  12. package/src/react-native/adapters/expo-sqlite.ts +11 -40
  13. package/src/react-native/adapters/generic.ts +5 -13
  14. package/src/react-native/adapters/index.ts +1 -4
  15. package/src/react-native/sqlite-view.ts +2 -8
  16. package/src/react-native/useRozeniteSqlitePlugin.ts +110 -130
  17. package/src/react-native/useSqliteAgentTools.ts +6 -18
  18. package/src/shared/__tests__/bridge-values.test.ts +6 -11
  19. package/src/shared/__tests__/sql.test.ts +4 -8
  20. package/src/shared/bridge-values.ts +2 -5
  21. package/src/shared/sql.ts +3 -7
  22. package/src/ui/__tests__/sql-editor-utils.test.ts +1 -5
  23. package/src/ui/__tests__/sqlite-drop-mutations.test.ts +6 -3
  24. package/src/ui/__tests__/sqlite-row-mutations.test.ts +1 -3
  25. package/src/ui/__tests__/sqlite-table-column-order.test.ts +4 -19
  26. package/src/ui/cell-detail-drawer.tsx +1 -6
  27. package/src/ui/globals.css +14 -65
  28. package/src/ui/panel.tsx +256 -596
  29. package/src/ui/query-result-table.tsx +8 -26
  30. package/src/ui/sql-editor-utils.ts +24 -55
  31. package/src/ui/sql-editor.tsx +13 -46
  32. package/src/ui/sqlite-data-table.tsx +11 -28
  33. package/src/ui/sqlite-drop-modal.tsx +17 -43
  34. package/src/ui/sqlite-drop-mutations.ts +3 -10
  35. package/src/ui/sqlite-introspection.ts +9 -25
  36. package/src/ui/sqlite-row-delete-modal.tsx +5 -12
  37. package/src/ui/sqlite-row-edit-modal.tsx +22 -64
  38. package/src/ui/sqlite-row-edit-value.ts +2 -11
  39. package/src/ui/sqlite-row-mutations.ts +10 -32
  40. package/src/ui/sqlite-table-column-order.ts +8 -23
  41. package/src/ui/use-sqlite-requests.ts +46 -86
  42. package/src/ui/utils.ts +1 -5
  43. package/src/ui/value-utils.tsx +3 -11
  44. package/tsconfig.json +4 -4
@@ -1,8 +1,4 @@
1
- import type {
2
- SqliteAdapter,
3
- SqliteDatabaseInfo,
4
- SqliteStatementInput,
5
- } from '../shared/types';
1
+ import type { SqliteAdapter, SqliteDatabaseInfo, SqliteStatementInput } from '../shared/types';
6
2
 
7
3
  export type SqliteDatabaseView = SqliteDatabaseInfo & {
8
4
  executeStatements: (
@@ -10,9 +6,7 @@ export type SqliteDatabaseView = SqliteDatabaseInfo & {
10
6
  ) => ReturnType<SqliteAdapter['databases'][number]['executeStatements']>;
11
7
  };
12
8
 
13
- export const createSqliteDatabaseViews = (
14
- adapters: SqliteAdapter[],
15
- ): SqliteDatabaseView[] =>
9
+ export const createSqliteDatabaseViews = (adapters: SqliteAdapter[]): SqliteDatabaseView[] =>
16
10
  adapters.flatMap((adapter) =>
17
11
  adapter.databases.map((database) => ({
18
12
  id: database.id,
@@ -18,15 +18,10 @@ export type RozeniteSqlitePluginOptions = {
18
18
 
19
19
  const safeError = (error: unknown) => formatSqliteError(error);
20
20
 
21
- const isExecuteStatementsError = (
22
- error: unknown,
23
- ): error is SqliteExecuteStatementsError =>
24
- error instanceof Error &&
25
- ('completedResults' in error || 'failedStatementIndex' in error);
26
-
27
- export const useRozeniteSqlitePlugin = ({
28
- adapters,
29
- }: RozeniteSqlitePluginOptions) => {
21
+ const isExecuteStatementsError = (error: unknown): error is SqliteExecuteStatementsError =>
22
+ error instanceof Error && ('completedResults' in error || 'failedStatementIndex' in error);
23
+
24
+ export const useRozeniteSqlitePlugin = ({ adapters }: RozeniteSqlitePluginOptions) => {
30
25
  const views = useMemo(() => createSqliteDatabaseViews(adapters), [adapters]);
31
26
 
32
27
  useSqliteAgentTools(views);
@@ -45,8 +40,7 @@ export const useRozeniteSqlitePlugin = ({
45
40
  databaseId: string,
46
41
  task: () => Promise<T>,
47
42
  ): Promise<T> => {
48
- const queue =
49
- databaseQueuesRef.current.get(databaseId) ?? Promise.resolve();
43
+ const queue = databaseQueuesRef.current.get(databaseId) ?? Promise.resolve();
50
44
  const next = queue.catch(() => undefined).then(task);
51
45
 
52
46
  databaseQueuesRef.current.set(
@@ -70,10 +64,7 @@ export const useRozeniteSqlitePlugin = ({
70
64
  return database;
71
65
  };
72
66
 
73
- const executeStatements = async (
74
- databaseId: string,
75
- statements: SqliteStatementInput[],
76
- ) => {
67
+ const executeStatements = async (databaseId: string, statements: SqliteStatementInput[]) => {
77
68
  const database = resolveDatabase(databaseId);
78
69
  const normalizedStatements = statements.map(({ sql, params }) => ({
79
70
  sql: normalizeSingleStatementSql(sql),
@@ -130,128 +121,117 @@ export const useRozeniteSqlitePlugin = ({
130
121
  );
131
122
 
132
123
  subscriptionsRef.current.push(
133
- client.onMessage(
134
- 'sqlite:query',
135
- async ({ requestId, databaseId, sql, params }) => {
136
- try {
137
- const result = await enqueueDatabaseTask(databaseId, () =>
138
- executeSingleQuery(databaseId, sql, params),
139
- );
140
-
141
- client.send('sqlite:query:result', {
142
- requestId,
143
- databaseId,
144
- result,
145
- });
146
- } catch (error) {
147
- client.send('sqlite:query:result', {
148
- requestId,
149
- databaseId,
150
- error: safeError(error),
151
- });
152
- }
153
- },
154
- ),
124
+ client.onMessage('sqlite:query', async ({ requestId, databaseId, sql, params }) => {
125
+ try {
126
+ const result = await enqueueDatabaseTask(databaseId, () =>
127
+ executeSingleQuery(databaseId, sql, params),
128
+ );
129
+
130
+ client.send('sqlite:query:result', {
131
+ requestId,
132
+ databaseId,
133
+ result,
134
+ });
135
+ } catch (error) {
136
+ client.send('sqlite:query:result', {
137
+ requestId,
138
+ databaseId,
139
+ error: safeError(error),
140
+ });
141
+ }
142
+ }),
155
143
  );
156
144
 
157
145
  subscriptionsRef.current.push(
158
- client.onMessage(
159
- 'sqlite:execute-script',
160
- async ({ requestId, databaseId, sql }) => {
161
- try {
162
- const result = await enqueueDatabaseTask(databaseId, async () => {
163
- const statementSegments = splitSqlStatements(sql);
164
-
165
- if (statementSegments.length === 0) {
166
- throw new Error('Query cannot be empty.');
146
+ client.onMessage('sqlite:execute-script', async ({ requestId, databaseId, sql }) => {
147
+ try {
148
+ const result = await enqueueDatabaseTask(databaseId, async () => {
149
+ const statementSegments = splitSqlStatements(sql);
150
+
151
+ if (statementSegments.length === 0) {
152
+ throw new Error('Query cannot be empty.');
153
+ }
154
+
155
+ const statementInputs = statementSegments.map((statement) => ({
156
+ sql: statement.text,
157
+ }));
158
+
159
+ try {
160
+ const execution = await executeStatements(databaseId, statementInputs);
161
+
162
+ return {
163
+ statements: statementSegments.map((statement, index) => ({
164
+ index,
165
+ start: statement.start,
166
+ end: statement.end,
167
+ input: execution.inputs[index],
168
+ execution: {
169
+ input: execution.inputs[index],
170
+ result: execution.results[index],
171
+ },
172
+ })),
173
+ totalStatementCount: statementSegments.length,
174
+ failedStatementIndex: null,
175
+ };
176
+ } catch (error) {
177
+ if (!isExecuteStatementsError(error)) {
178
+ throw error;
167
179
  }
168
180
 
169
- const statementInputs = statementSegments.map((statement) => ({
170
- sql: statement.text,
181
+ const failedStatementIndex = Math.max(
182
+ 0,
183
+ Math.min(
184
+ typeof error.failedStatementIndex === 'number'
185
+ ? error.failedStatementIndex
186
+ : (error.completedResults?.length ?? 0),
187
+ statementSegments.length - 1,
188
+ ),
189
+ );
190
+ const completedResults = (error.completedResults ?? []).slice(
191
+ 0,
192
+ failedStatementIndex,
193
+ );
194
+ const completedStatements = completedResults.map((queryResult, index) => ({
195
+ index,
196
+ start: statementSegments[index].start,
197
+ end: statementSegments[index].end,
198
+ input: statementInputs[index],
199
+ execution: {
200
+ input: statementInputs[index],
201
+ result: queryResult,
202
+ },
171
203
  }));
172
204
 
173
- try {
174
- const execution = await executeStatements(
175
- databaseId,
176
- statementInputs,
177
- );
178
-
179
- return {
180
- statements: statementSegments.map((statement, index) => ({
181
- index,
182
- start: statement.start,
183
- end: statement.end,
184
- input: execution.inputs[index],
185
- execution: {
186
- input: execution.inputs[index],
187
- result: execution.results[index],
188
- },
189
- })),
190
- totalStatementCount: statementSegments.length,
191
- failedStatementIndex: null,
192
- };
193
- } catch (error) {
194
- if (!isExecuteStatementsError(error)) {
195
- throw error;
196
- }
197
-
198
- const failedStatementIndex = Math.max(
199
- 0,
200
- Math.min(
201
- typeof error.failedStatementIndex === 'number'
202
- ? error.failedStatementIndex
203
- : (error.completedResults?.length ?? 0),
204
- statementSegments.length - 1,
205
- ),
206
- );
207
- const completedResults = (error.completedResults ?? []).slice(
208
- 0,
209
- failedStatementIndex,
210
- );
211
- const completedStatements = completedResults.map(
212
- (queryResult, index) => ({
213
- index,
214
- start: statementSegments[index].start,
215
- end: statementSegments[index].end,
216
- input: statementInputs[index],
217
- execution: {
218
- input: statementInputs[index],
219
- result: queryResult,
220
- },
221
- }),
222
- );
223
-
224
- return {
225
- statements: [
226
- ...completedStatements,
227
- {
228
- index: failedStatementIndex,
229
- start: statementSegments[failedStatementIndex].start,
230
- end: statementSegments[failedStatementIndex].end,
231
- input: statementInputs[failedStatementIndex],
232
- error: safeError(error),
233
- },
234
- ],
235
- totalStatementCount: statementSegments.length,
236
- failedStatementIndex,
237
- };
238
- }
239
- });
240
-
241
- client.send('sqlite:execute-script:result', {
242
- requestId,
243
- databaseId,
244
- result,
245
- });
246
- } catch (error) {
247
- client.send('sqlite:execute-script:result', {
248
- requestId,
249
- databaseId,
250
- error: safeError(error),
251
- });
252
- }
253
- },
254
- ),
205
+ return {
206
+ statements: [
207
+ ...completedStatements,
208
+ {
209
+ index: failedStatementIndex,
210
+ start: statementSegments[failedStatementIndex].start,
211
+ end: statementSegments[failedStatementIndex].end,
212
+ input: statementInputs[failedStatementIndex],
213
+ error: safeError(error),
214
+ },
215
+ ],
216
+ totalStatementCount: statementSegments.length,
217
+ failedStatementIndex,
218
+ };
219
+ }
220
+ });
221
+
222
+ client.send('sqlite:execute-script:result', {
223
+ requestId,
224
+ databaseId,
225
+ result,
226
+ });
227
+ } catch (error) {
228
+ client.send('sqlite:execute-script:result', {
229
+ requestId,
230
+ databaseId,
231
+ error: safeError(error),
232
+ });
233
+ }
234
+ }),
255
235
  );
256
236
 
257
237
  return () => {
@@ -1,8 +1,5 @@
1
1
  import { useCallback } from 'react';
2
- import {
3
- useRozenitePluginAgentTool,
4
- type AgentTool,
5
- } from '@rozenite/agent-bridge';
2
+ import { useRozenitePluginAgentTool, type AgentTool } from '@rozenite/agent-bridge';
6
3
  import { formatSqliteError } from '../shared/bridge-values';
7
4
  import { normalizeSingleStatementSql, splitSqlStatements } from '../shared/sql';
8
5
  import type { SqliteExecuteStatementsError } from '../shared/types';
@@ -34,19 +31,15 @@ const executeSqlTool: AgentTool = {
34
31
  },
35
32
  sql: {
36
33
  type: 'string',
37
- description:
38
- 'SQL to execute. May contain multiple semicolon-separated statements.',
34
+ description: 'SQL to execute. May contain multiple semicolon-separated statements.',
39
35
  },
40
36
  },
41
37
  required: ['databaseId', 'sql'],
42
38
  },
43
39
  };
44
40
 
45
- const isExecuteStatementsError = (
46
- error: unknown,
47
- ): error is SqliteExecuteStatementsError =>
48
- error instanceof Error &&
49
- ('completedResults' in error || 'failedStatementIndex' in error);
41
+ const isExecuteStatementsError = (error: unknown): error is SqliteExecuteStatementsError =>
42
+ error instanceof Error && ('completedResults' in error || 'failedStatementIndex' in error);
50
43
 
51
44
  export const useSqliteAgentTools = (views: SqliteDatabaseView[]) => {
52
45
  const resolveDatabase = useCallback(
@@ -55,9 +48,7 @@ export const useSqliteAgentTools = (views: SqliteDatabaseView[]) => {
55
48
 
56
49
  if (!database) {
57
50
  const available = views.map((v) => v.id).join(', ');
58
- throw new Error(
59
- `Unknown databaseId "${databaseId}". Available: ${available || '(none)'}`,
60
- );
51
+ throw new Error(`Unknown databaseId "${databaseId}". Available: ${available || '(none)'}`);
61
52
  }
62
53
 
63
54
  return database;
@@ -125,10 +116,7 @@ export const useSqliteAgentTools = (views: SqliteDatabaseView[]) => {
125
116
  statementSegments.length - 1,
126
117
  ),
127
118
  );
128
- const completedResults = (error.completedResults ?? []).slice(
129
- 0,
130
- failedStatementIndex,
131
- );
119
+ const completedResults = (error.completedResults ?? []).slice(0, failedStatementIndex);
132
120
 
133
121
  return {
134
122
  databaseId,
@@ -11,21 +11,16 @@ describe('sqlite bridge values', () => {
11
11
  params: [new Uint8Array([1, 2, 255]), { nested: new Uint8Array([9, 8]) }],
12
12
  };
13
13
 
14
- expect(decodeSqliteBridgeValue(encodeSqliteBridgeValue(original))).toEqual(
15
- original,
16
- );
14
+ expect(decodeSqliteBridgeValue(encodeSqliteBridgeValue(original))).toEqual(original);
17
15
  });
18
16
 
19
17
  it('formats nested error details with code and cause information', () => {
20
- const error = Object.assign(
21
- new Error("Calling the 'runAsync' function has failed"),
22
- {
23
- cause: {
24
- code: 'ERR_INTERNAL_SQLITE_ERROR',
25
- reason: 'Invalid bind parameter',
26
- },
18
+ const error = Object.assign(new Error("Calling the 'runAsync' function has failed"), {
19
+ cause: {
20
+ code: 'ERR_INTERNAL_SQLITE_ERROR',
21
+ reason: 'Invalid bind parameter',
27
22
  },
28
- );
23
+ });
29
24
 
30
25
  expect(formatSqliteError(error)).toBe(
31
26
  "Calling the 'runAsync' function has failed\nCaused by: [ERR_INTERNAL_SQLITE_ERROR] Invalid bind parameter",
@@ -1,9 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import {
3
- getStatementAtCursor,
4
- normalizeSingleStatementSql,
5
- splitSqlStatements,
6
- } from '../sql';
2
+ import { getStatementAtCursor, normalizeSingleStatementSql, splitSqlStatements } from '../sql';
7
3
 
8
4
  describe('SQL statement helpers', () => {
9
5
  it('finds the active statement when earlier statements contain comments and quoted semicolons', () => {
@@ -26,9 +22,9 @@ describe('SQL statement helpers', () => {
26
22
  });
27
23
 
28
24
  it('rejects multiple statements', () => {
29
- expect(() =>
30
- normalizeSingleStatementSql('SELECT * FROM projects; DELETE FROM logs;'),
31
- ).toThrow('Only a single SQL statement is supported in v1.');
25
+ expect(() => normalizeSingleStatementSql('SELECT * FROM projects; DELETE FROM logs;')).toThrow(
26
+ 'Only a single SQL statement is supported in v1.',
27
+ );
32
28
  });
33
29
 
34
30
  it('splits statements while preserving source offsets', () => {
@@ -8,9 +8,7 @@ type SqliteEncodedBinaryValue = {
8
8
  const isRecord = (value: unknown): value is Record<string, unknown> =>
9
9
  !!value && typeof value === 'object';
10
10
 
11
- const isEncodedBinaryValue = (
12
- value: unknown,
13
- ): value is SqliteEncodedBinaryValue =>
11
+ const isEncodedBinaryValue = (value: unknown): value is SqliteEncodedBinaryValue =>
14
12
  isRecord(value) &&
15
13
  value[SQLITE_BRIDGE_BINARY_TYPE] === true &&
16
14
  Array.isArray(value.data) &&
@@ -114,8 +112,7 @@ const describeError = (error: unknown): string | null => {
114
112
  const reason = getStringField(error, 'reason');
115
113
  const message = error.message.trim();
116
114
  const detailParts = [message || null, reason].filter(
117
- (part, index, parts): part is string =>
118
- !!part && parts.indexOf(part) === index,
115
+ (part, index, parts): part is string => !!part && parts.indexOf(part) === index,
119
116
  );
120
117
  const detail = detailParts.join(' | ') || error.name;
121
118
 
package/src/shared/sql.ts CHANGED
@@ -324,9 +324,7 @@ export const getStatementAtCursor = (sql: string, cursor: number) => {
324
324
  return null;
325
325
  }
326
326
 
327
- const match = segments.find(
328
- (segment) => cursor >= segment.start && cursor <= segment.end + 1,
329
- );
327
+ const match = segments.find((segment) => cursor >= segment.start && cursor <= segment.end + 1);
330
328
 
331
329
  return match ?? segments[0];
332
330
  };
@@ -413,8 +411,6 @@ export const statementReturnsRows = (statementType: SqliteStatementType) =>
413
411
  statementType === 'explain' ||
414
412
  statementType === 'with';
415
413
 
416
- export const quoteSqlIdentifier = (identifier: string) =>
417
- `"${identifier.replace(/"/g, '""')}"`;
414
+ export const quoteSqlIdentifier = (identifier: string) => `"${identifier.replace(/"/g, '""')}"`;
418
415
 
419
- export const escapeSqlString = (value: string) =>
420
- `'${value.replace(/'/g, "''")}'`;
416
+ export const escapeSqlString = (value: string) => `'${value.replace(/'/g, "''")}'`;
@@ -1,9 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import type {
3
- SqliteColumnInfo,
4
- SqliteEntity,
5
- SqliteSchema,
6
- } from '../sqlite-introspection';
2
+ import type { SqliteColumnInfo, SqliteEntity, SqliteSchema } from '../sqlite-introspection';
7
3
  import {
8
4
  buildSqlCompletionSchema,
9
5
  createSqlEditorColumnCache,
@@ -70,9 +70,12 @@ describe('orderEntitiesForDrop', () => {
70
70
  { schemaName: 'main', name: 'd_view', type: 'view' },
71
71
  ];
72
72
 
73
- expect(orderEntitiesForDrop(entities).map((entity) => entity.name)).toEqual(
74
- ['b_view', 'd_view', 'a_table', 'c_table'],
75
- );
73
+ expect(orderEntitiesForDrop(entities).map((entity) => entity.name)).toEqual([
74
+ 'b_view',
75
+ 'd_view',
76
+ 'a_table',
77
+ 'c_table',
78
+ ]);
76
79
  });
77
80
 
78
81
  it('returns an empty array for an empty database', () => {
@@ -166,9 +166,7 @@ describe('sqlite row mutation helpers', () => {
166
166
  it('disables row mutations for WITHOUT ROWID tables that lack a primary key', () => {
167
167
  expect(
168
168
  getRowMutationDescriptor(
169
- buildEntity(
170
- 'CREATE TABLE projects(title TEXT, slug TEXT) WITHOUT ROWID',
171
- ),
169
+ buildEntity('CREATE TABLE projects(title TEXT, slug TEXT) WITHOUT ROWID'),
172
170
  [
173
171
  {
174
172
  cid: 0,
@@ -23,13 +23,7 @@ describe('sqlite table column order helpers', () => {
23
23
  normalizeTableColumnOrder({
24
24
  columnIds: [SQLITE_ROW_NUMBER_COLUMN_ID, 'name', 'type'],
25
25
  fixedLeadingColumnIds: [SQLITE_ROW_NUMBER_COLUMN_ID],
26
- storedColumnOrder: [
27
- 'type',
28
- SQLITE_ROW_NUMBER_COLUMN_ID,
29
- 'stale',
30
- 'name',
31
- 'type',
32
- ],
26
+ storedColumnOrder: ['type', SQLITE_ROW_NUMBER_COLUMN_ID, 'stale', 'name', 'type'],
33
27
  }),
34
28
  ).toEqual([SQLITE_ROW_NUMBER_COLUMN_ID, 'type', 'name']);
35
29
  });
@@ -49,12 +43,7 @@ describe('sqlite table column order helpers', () => {
49
43
  reorderTableColumnOrder({
50
44
  columnIds: [SQLITE_ROW_NUMBER_COLUMN_ID, 'name', 'type', 'extra'],
51
45
  fixedLeadingColumnIds: [SQLITE_ROW_NUMBER_COLUMN_ID],
52
- storedColumnOrder: [
53
- SQLITE_ROW_NUMBER_COLUMN_ID,
54
- 'name',
55
- 'type',
56
- 'extra',
57
- ],
46
+ storedColumnOrder: [SQLITE_ROW_NUMBER_COLUMN_ID, 'name', 'type', 'extra'],
58
47
  activeColumnId: 'extra',
59
48
  overColumnId: 'name',
60
49
  }),
@@ -73,11 +62,7 @@ describe('sqlite table column order helpers', () => {
73
62
  });
74
63
 
75
64
  it('builds stable table ids for entity and query surfaces', () => {
76
- expect(buildEntityTableId('data', 'db-1', 'main', 'users')).toBe(
77
- 'data:db-1:main:users',
78
- );
79
- expect(buildQueryTableId('db-1', ['id', 'name'])).toBe(
80
- 'query:db-1:["id","name"]',
81
- );
65
+ expect(buildEntityTableId('data', 'db-1', 'main', 'users')).toBe('data:db-1:main:users');
66
+ expect(buildQueryTableId('db-1', ['id', 'name'])).toBe('query:db-1:["id","name"]');
82
67
  });
83
68
  });
@@ -10,12 +10,7 @@ type CellDetailDrawerProps = {
10
10
  onClose: () => void;
11
11
  };
12
12
 
13
- export const CellDetailDrawer = ({
14
- value,
15
- title,
16
- isOpen,
17
- onClose,
18
- }: CellDetailDrawerProps) => {
13
+ export const CellDetailDrawer = ({ value, title, isOpen, onClose }: CellDetailDrawerProps) => {
19
14
  const overlay = useOverlayState({
20
15
  isOpen,
21
16
  onOpenChange: (open: boolean) => {