@suiteportal/studio 0.1.1

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.
package/dist/index.js ADDED
@@ -0,0 +1,303 @@
1
+ // src/index.ts
2
+ import { serve } from "@hono/node-server";
3
+ import { resolve, dirname } from "path";
4
+ import { fileURLToPath } from "url";
5
+ import { existsSync } from "fs";
6
+ import { execSync } from "child_process";
7
+ import { createClient } from "@suiteportal/client-runtime";
8
+ import { loadSchema } from "@suiteportal/client-runtime";
9
+
10
+ // src/server/app.ts
11
+ import { Hono as Hono4 } from "hono";
12
+ import { serveStatic } from "@hono/node-server/serve-static";
13
+ import { cors } from "hono/cors";
14
+
15
+ // src/server/routes/schema.ts
16
+ import { Hono } from "hono";
17
+ function createSchemaRoutes(schema) {
18
+ const app = new Hono();
19
+ app.get("/", (c) => {
20
+ const records = Object.entries(schema.records).map(([id, def]) => ({
21
+ id,
22
+ label: def.label,
23
+ isCustom: def.isCustom,
24
+ fieldCount: Object.keys(def.fields).length,
25
+ relationCount: Object.keys(def.relations).length,
26
+ sublistCount: Object.keys(def.sublists).length
27
+ }));
28
+ return c.json({
29
+ generatedAt: schema.generatedAt,
30
+ accountId: schema.accountId,
31
+ recordCount: records.length,
32
+ records
33
+ });
34
+ });
35
+ app.get("/:type", (c) => {
36
+ const type = c.req.param("type");
37
+ const record = schema.records[type];
38
+ if (!record) {
39
+ return c.json({ error: `Record type "${type}" not found` }, 404);
40
+ }
41
+ return c.json(record);
42
+ });
43
+ return app;
44
+ }
45
+
46
+ // src/server/routes/records.ts
47
+ import { Hono as Hono2 } from "hono";
48
+ function pickDefaultColumns(fields) {
49
+ const select = {};
50
+ if (fields["id"]) select["id"] = true;
51
+ for (const [id, f] of Object.entries(fields)) {
52
+ if (select[id]) continue;
53
+ if (id.startsWith("cust")) continue;
54
+ if (f.type === "unknown" || f.nativeType === "reference") continue;
55
+ if (f.queryable === false) continue;
56
+ select[id] = true;
57
+ if (Object.keys(select).length >= 12) break;
58
+ }
59
+ return select;
60
+ }
61
+ function createRecordRoutes(client, schema) {
62
+ const app = new Hono2();
63
+ app.get("/:type", async (c) => {
64
+ const type = c.req.param("type");
65
+ const recordDef = schema.records[type];
66
+ if (!recordDef) {
67
+ return c.json({ error: `Record type "${type}" not found` }, 404);
68
+ }
69
+ const delegate = client.$delegate(type);
70
+ const whereParam = c.req.query("where");
71
+ const selectParam = c.req.query("select");
72
+ const orderByParam = c.req.query("orderBy");
73
+ const includeParam = c.req.query("include");
74
+ const take = c.req.query("take");
75
+ const skip = c.req.query("skip");
76
+ const args = {};
77
+ if (whereParam) {
78
+ try {
79
+ args["where"] = JSON.parse(whereParam);
80
+ } catch {
81
+ return c.json({ error: "Invalid where JSON" }, 400);
82
+ }
83
+ }
84
+ if (selectParam) {
85
+ try {
86
+ args["select"] = JSON.parse(selectParam);
87
+ } catch {
88
+ return c.json({ error: "Invalid select JSON" }, 400);
89
+ }
90
+ }
91
+ if (orderByParam) {
92
+ try {
93
+ args["orderBy"] = JSON.parse(orderByParam);
94
+ } catch {
95
+ return c.json({ error: "Invalid orderBy JSON" }, 400);
96
+ }
97
+ }
98
+ if (includeParam) {
99
+ try {
100
+ args["include"] = JSON.parse(includeParam);
101
+ } catch {
102
+ return c.json({ error: "Invalid include JSON" }, 400);
103
+ }
104
+ }
105
+ if (take) args["take"] = parseInt(take, 10);
106
+ if (skip) args["skip"] = parseInt(skip, 10);
107
+ if (!args["take"]) args["take"] = 50;
108
+ if (!args["select"]) {
109
+ args["select"] = pickDefaultColumns(recordDef.fields);
110
+ }
111
+ try {
112
+ const rows = await delegate.findMany(args);
113
+ return c.json({ data: rows, count: rows.length });
114
+ } catch (err) {
115
+ const status = err.status;
116
+ if (status === 400) {
117
+ try {
118
+ const fallbackArgs = { ...args, select: { id: true } };
119
+ const rows = await delegate.findMany(fallbackArgs);
120
+ return c.json({ data: rows, count: rows.length });
121
+ } catch {
122
+ throw err;
123
+ }
124
+ }
125
+ throw err;
126
+ }
127
+ });
128
+ app.get("/:type/count", async (c) => {
129
+ const type = c.req.param("type");
130
+ if (!schema.records[type]) {
131
+ return c.json({ error: `Record type "${type}" not found` }, 404);
132
+ }
133
+ const delegate = client.$delegate(type);
134
+ const whereParam = c.req.query("where");
135
+ const args = {};
136
+ if (whereParam) {
137
+ try {
138
+ args["where"] = JSON.parse(whereParam);
139
+ } catch {
140
+ return c.json({ error: "Invalid where JSON" }, 400);
141
+ }
142
+ }
143
+ const count = await delegate.count(args);
144
+ return c.json({ count });
145
+ });
146
+ app.post("/:type", async (c) => {
147
+ const type = c.req.param("type");
148
+ if (!schema.records[type]) {
149
+ return c.json({ error: `Record type "${type}" not found` }, 404);
150
+ }
151
+ const delegate = client.$delegate(type);
152
+ const body = await c.req.json();
153
+ const result = await delegate.create({ data: body });
154
+ return c.json(result, 201);
155
+ });
156
+ app.patch("/:type/:id", async (c) => {
157
+ const type = c.req.param("type");
158
+ const id = c.req.param("id");
159
+ if (!schema.records[type]) {
160
+ return c.json({ error: `Record type "${type}" not found` }, 404);
161
+ }
162
+ const delegate = client.$delegate(type);
163
+ const body = await c.req.json();
164
+ const result = await delegate.update({
165
+ where: { id: { equals: id } },
166
+ data: body
167
+ });
168
+ return c.json(result);
169
+ });
170
+ app.delete("/:type/:id", async (c) => {
171
+ const type = c.req.param("type");
172
+ const id = c.req.param("id");
173
+ if (!schema.records[type]) {
174
+ return c.json({ error: `Record type "${type}" not found` }, 404);
175
+ }
176
+ const delegate = client.$delegate(type);
177
+ await delegate.delete({ where: { id: { equals: id } } });
178
+ return c.json({ success: true });
179
+ });
180
+ return app;
181
+ }
182
+
183
+ // src/server/routes/query.ts
184
+ import { Hono as Hono3 } from "hono";
185
+ function createQueryRoutes(client) {
186
+ const app = new Hono3();
187
+ app.post("/", async (c) => {
188
+ const body = await c.req.json();
189
+ if (!body.sql || typeof body.sql !== "string") {
190
+ return c.json({ error: 'Missing "sql" field in request body' }, 400);
191
+ }
192
+ const rows = await client.$queryRaw(body.sql);
193
+ return c.json({ data: rows, count: rows.length });
194
+ });
195
+ return app;
196
+ }
197
+
198
+ // src/server/middleware/error-handler.ts
199
+ import { NetSuiteError, AuthError, RateLimitError, TimeoutError } from "@suiteportal/connector";
200
+ var onError = (err, c) => {
201
+ if (err instanceof AuthError) {
202
+ return c.json({ error: "Authentication failed", message: err.message }, 401);
203
+ }
204
+ if (err instanceof RateLimitError) {
205
+ return c.json({ error: "Rate limit exceeded", message: err.message }, 429);
206
+ }
207
+ if (err instanceof TimeoutError) {
208
+ return c.json({ error: "Request timeout", message: err.message }, 504);
209
+ }
210
+ if (err instanceof NetSuiteError) {
211
+ const status = err.status && err.status >= 400 && err.status < 600 ? err.status : 502;
212
+ const details = err.details;
213
+ const errorDetails = details?.["o:errorDetails"];
214
+ return c.json({
215
+ error: "NetSuite error",
216
+ message: err.message,
217
+ status,
218
+ details: errorDetails ?? void 0
219
+ }, status);
220
+ }
221
+ if (err instanceof Error) {
222
+ console.error(`[studio] ${err.message}`);
223
+ return c.json({ error: "Internal error", message: err.message }, 500);
224
+ }
225
+ return c.json({ error: "Unknown error" }, 500);
226
+ };
227
+
228
+ // src/server/app.ts
229
+ function createApp(options) {
230
+ const { client, schema, frontendDir } = options;
231
+ const app = new Hono4();
232
+ app.onError(onError);
233
+ app.use("*", cors());
234
+ app.route("/api/schema", createSchemaRoutes(schema));
235
+ app.route("/api/records", createRecordRoutes(client, schema));
236
+ app.route("/api/query", createQueryRoutes(client));
237
+ app.get("/api/health", (c) => c.json({ status: "ok" }));
238
+ if (frontendDir) {
239
+ app.use("/*", serveStatic({ root: frontendDir }));
240
+ app.get("*", serveStatic({ root: frontendDir, path: "/index.html" }));
241
+ }
242
+ return app;
243
+ }
244
+
245
+ // src/index.ts
246
+ async function startStudio(config, options = {}) {
247
+ const port = options.port ?? 4480;
248
+ const shouldOpen = options.open ?? true;
249
+ const schemaPath = options.schemaPath ?? ".suiteportal/schema.json";
250
+ const schema = await loadSchema(schemaPath);
251
+ const client = await createClient(config, { schemaPath });
252
+ const currentDir = typeof __dirname !== "undefined" ? __dirname : dirname(fileURLToPath(import.meta.url));
253
+ const studioRoot = resolve(currentDir, "..");
254
+ const frontendDir = resolve(studioRoot, "dist-frontend");
255
+ if (!existsSync(frontendDir)) {
256
+ const viteConfig = resolve(studioRoot, "vite.config.ts");
257
+ if (existsSync(viteConfig)) {
258
+ console.log("\n Building Studio frontend...");
259
+ try {
260
+ execSync("npx vite build", { cwd: studioRoot, stdio: "pipe" });
261
+ console.log(" Frontend built successfully.\n");
262
+ } catch (e) {
263
+ console.log(" Could not build frontend \u2014 starting in API-only mode.\n");
264
+ }
265
+ }
266
+ }
267
+ const hasFrontend = existsSync(frontendDir);
268
+ const app = createApp({
269
+ client,
270
+ schema,
271
+ frontendDir: hasFrontend ? frontendDir : void 0
272
+ });
273
+ const server = serve({
274
+ fetch: app.fetch,
275
+ port
276
+ });
277
+ const url = `http://localhost:${port}`;
278
+ console.log(`
279
+ SuitePortal Studio running at ${url}
280
+ `);
281
+ if (!hasFrontend) {
282
+ console.log(" (Frontend unavailable \u2014 API-only mode)\n");
283
+ }
284
+ if (shouldOpen && hasFrontend) {
285
+ try {
286
+ const openModule = await import("open");
287
+ await openModule.default(url);
288
+ } catch {
289
+ }
290
+ }
291
+ return {
292
+ port,
293
+ close: () => {
294
+ server.close();
295
+ client.$disconnect();
296
+ }
297
+ };
298
+ }
299
+ export {
300
+ createApp,
301
+ startStudio
302
+ };
303
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/server/app.ts","../src/server/routes/schema.ts","../src/server/routes/records.ts","../src/server/routes/query.ts","../src/server/middleware/error-handler.ts"],"sourcesContent":["import { serve } from '@hono/node-server';\nimport { resolve, dirname } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { existsSync } from 'node:fs';\nimport { execSync } from 'node:child_process';\nimport type { NetSuiteConfig } from '@suiteportal/connector';\nimport { createClient } from '@suiteportal/client-runtime';\nimport { loadSchema } from '@suiteportal/client-runtime';\nimport { createApp } from './server/app.js';\n\nexport interface StudioOptions {\n /** Port to listen on. Default: 4480 */\n port?: number;\n /** Whether to open the browser. Default: true */\n open?: boolean;\n /** Path to schema.json. Default: '.suiteportal/schema.json' */\n schemaPath?: string;\n}\n\nexport interface StudioServer {\n port: number;\n close: () => void;\n}\n\n/**\n * Start SuitePortal Studio — a local web UI for browsing NetSuite data.\n */\nexport async function startStudio(\n config: NetSuiteConfig,\n options: StudioOptions = {},\n): Promise<StudioServer> {\n const port = options.port ?? 4480;\n const shouldOpen = options.open ?? true;\n const schemaPath = options.schemaPath ?? '.suiteportal/schema.json';\n\n // Load schema\n const schema = await loadSchema(schemaPath);\n\n // Create ORM client\n const client = await createClient(config, { schemaPath });\n\n // Resolve frontend dist directory (works in both ESM and CJS)\n const currentDir = typeof __dirname !== 'undefined'\n ? __dirname\n : dirname(fileURLToPath(import.meta.url));\n const studioRoot = resolve(currentDir, '..');\n const frontendDir = resolve(studioRoot, 'dist-frontend');\n\n // Auto-build frontend if not present\n if (!existsSync(frontendDir)) {\n const viteConfig = resolve(studioRoot, 'vite.config.ts');\n if (existsSync(viteConfig)) {\n console.log('\\n Building Studio frontend...');\n try {\n execSync('npx vite build', { cwd: studioRoot, stdio: 'pipe' });\n console.log(' Frontend built successfully.\\n');\n } catch (e) {\n console.log(' Could not build frontend — starting in API-only mode.\\n');\n }\n }\n }\n\n const hasFrontend = existsSync(frontendDir);\n\n // Create Hono app\n const app = createApp({\n client,\n schema,\n frontendDir: hasFrontend ? frontendDir : undefined,\n });\n\n // Start server\n const server = serve({\n fetch: app.fetch,\n port,\n });\n\n const url = `http://localhost:${port}`;\n console.log(`\\n SuitePortal Studio running at ${url}\\n`);\n\n if (!hasFrontend) {\n console.log(' (Frontend unavailable — API-only mode)\\n');\n }\n\n // Open browser\n if (shouldOpen && hasFrontend) {\n try {\n const openModule = await import('open');\n await openModule.default(url);\n } catch {\n // open is optional — silently ignore if unavailable\n }\n }\n\n return {\n port,\n close: () => { server.close(); client.$disconnect(); },\n };\n}\n\n// Re-export app creation for testing\nexport { createApp } from './server/app.js';\nexport type { CreateAppOptions } from './server/app.js';\n","import { Hono } from 'hono';\nimport { serveStatic } from '@hono/node-server/serve-static';\nimport { cors } from 'hono/cors';\nimport type { SuitePortalClient } from '@suiteportal/client-runtime';\nimport type { NormalizedSchema } from '@suiteportal/introspector';\nimport { createSchemaRoutes } from './routes/schema.js';\nimport { createRecordRoutes } from './routes/records.js';\nimport { createQueryRoutes } from './routes/query.js';\nimport { onError } from './middleware/error-handler.js';\n\nexport interface CreateAppOptions {\n client: SuitePortalClient;\n schema: NormalizedSchema;\n frontendDir?: string;\n}\n\nexport function createApp(options: CreateAppOptions): Hono {\n const { client, schema, frontendDir } = options;\n const app = new Hono();\n\n // Global error handler — prevents stack traces from leaking to console\n app.onError(onError);\n\n // Middleware\n app.use('*', cors());\n\n // API routes\n app.route('/api/schema', createSchemaRoutes(schema));\n app.route('/api/records', createRecordRoutes(client, schema));\n app.route('/api/query', createQueryRoutes(client));\n\n // Health check\n app.get('/api/health', (c) => c.json({ status: 'ok' }));\n\n // Serve static frontend\n if (frontendDir) {\n app.use('/*', serveStatic({ root: frontendDir }));\n // SPA fallback: serve index.html for all non-API routes\n app.get('*', serveStatic({ root: frontendDir, path: '/index.html' }));\n }\n\n return app;\n}\n","import { Hono } from 'hono';\nimport type { NormalizedSchema } from '@suiteportal/introspector';\n\nexport function createSchemaRoutes(schema: NormalizedSchema): Hono {\n const app = new Hono();\n\n // List all record types (summary)\n app.get('/', (c) => {\n const records = Object.entries(schema.records).map(([id, def]) => ({\n id,\n label: def.label,\n isCustom: def.isCustom,\n fieldCount: Object.keys(def.fields).length,\n relationCount: Object.keys(def.relations).length,\n sublistCount: Object.keys(def.sublists).length,\n }));\n\n return c.json({\n generatedAt: schema.generatedAt,\n accountId: schema.accountId,\n recordCount: records.length,\n records,\n });\n });\n\n // Get full record definition\n app.get('/:type', (c) => {\n const type = c.req.param('type');\n const record = schema.records[type];\n\n if (!record) {\n return c.json({ error: `Record type \"${type}\" not found` }, 404);\n }\n\n return c.json(record);\n });\n\n return app;\n}\n","import { Hono } from 'hono';\nimport type { SuitePortalClient } from '@suiteportal/client-runtime';\nimport type { NormalizedSchema, FieldDefinition } from '@suiteportal/introspector';\n\n/**\n * Pick safe default columns for a record type.\n * Uses the `queryable` flag from the schema (set during introspection).\n * Falls back to heuristics for schemas generated before queryability probing.\n */\nfunction pickDefaultColumns(fields: Record<string, FieldDefinition>): Record<string, boolean> {\n const select: Record<string, boolean> = {};\n\n // Always include id if present\n if (fields['id']) select['id'] = true;\n\n // Use queryable fields: non-custom, known type, not reference, not marked non-queryable\n for (const [id, f] of Object.entries(fields)) {\n if (select[id]) continue;\n if (id.startsWith('cust')) continue;\n if (f.type === 'unknown' || f.nativeType === 'reference') continue;\n if (f.queryable === false) continue;\n select[id] = true;\n if (Object.keys(select).length >= 12) break;\n }\n\n return select;\n}\n\nexport function createRecordRoutes(client: SuitePortalClient, schema: NormalizedSchema): Hono {\n const app = new Hono();\n\n // GET /api/records/:type — findMany\n app.get('/:type', async (c) => {\n const type = c.req.param('type');\n const recordDef = schema.records[type];\n\n if (!recordDef) {\n return c.json({ error: `Record type \"${type}\" not found` }, 404);\n }\n\n const delegate = client.$delegate(type);\n\n const whereParam = c.req.query('where');\n const selectParam = c.req.query('select');\n const orderByParam = c.req.query('orderBy');\n const includeParam = c.req.query('include');\n const take = c.req.query('take');\n const skip = c.req.query('skip');\n\n const args: Record<string, unknown> = {};\n\n if (whereParam) {\n try { args['where'] = JSON.parse(whereParam); } catch { return c.json({ error: 'Invalid where JSON' }, 400); }\n }\n if (selectParam) {\n try { args['select'] = JSON.parse(selectParam); } catch { return c.json({ error: 'Invalid select JSON' }, 400); }\n }\n if (orderByParam) {\n try { args['orderBy'] = JSON.parse(orderByParam); } catch { return c.json({ error: 'Invalid orderBy JSON' }, 400); }\n }\n if (includeParam) {\n try { args['include'] = JSON.parse(includeParam); } catch { return c.json({ error: 'Invalid include JSON' }, 400); }\n }\n if (take) args['take'] = parseInt(take, 10);\n if (skip) args['skip'] = parseInt(skip, 10);\n\n // Default take to 50\n if (!args['take']) args['take'] = 50;\n\n // If no explicit select, use safe default columns to avoid SuiteQL 400 errors\n if (!args['select']) {\n args['select'] = pickDefaultColumns(recordDef.fields);\n }\n\n // Try the query; if it fails with 400 (bad column), retry with minimal columns\n try {\n const rows = await delegate.findMany(args);\n return c.json({ data: rows, count: rows.length });\n } catch (err: unknown) {\n const status = (err as { status?: number }).status;\n if (status === 400) {\n // Retry with just id\n try {\n const fallbackArgs = { ...args, select: { id: true } };\n const rows = await delegate.findMany(fallbackArgs);\n return c.json({ data: rows, count: rows.length });\n } catch {\n throw err; // rethrow original error\n }\n }\n throw err;\n }\n });\n\n // GET /api/records/:type/count\n app.get('/:type/count', async (c) => {\n const type = c.req.param('type');\n\n if (!schema.records[type]) {\n return c.json({ error: `Record type \"${type}\" not found` }, 404);\n }\n\n const delegate = client.$delegate(type);\n\n const whereParam = c.req.query('where');\n const args: Record<string, unknown> = {};\n if (whereParam) {\n try { args['where'] = JSON.parse(whereParam); } catch { return c.json({ error: 'Invalid where JSON' }, 400); }\n }\n\n const count = await delegate.count(args);\n return c.json({ count });\n });\n\n // POST /api/records/:type — create\n app.post('/:type', async (c) => {\n const type = c.req.param('type');\n\n if (!schema.records[type]) {\n return c.json({ error: `Record type \"${type}\" not found` }, 404);\n }\n\n const delegate = client.$delegate(type);\n const body = await c.req.json<Record<string, unknown>>();\n const result = await delegate.create({ data: body });\n return c.json(result, 201);\n });\n\n // PATCH /api/records/:type/:id — update\n app.patch('/:type/:id', async (c) => {\n const type = c.req.param('type');\n const id = c.req.param('id');\n\n if (!schema.records[type]) {\n return c.json({ error: `Record type \"${type}\" not found` }, 404);\n }\n\n const delegate = client.$delegate(type);\n const body = await c.req.json<Record<string, unknown>>();\n const result = await delegate.update({\n where: { id: { equals: id } },\n data: body,\n });\n return c.json(result);\n });\n\n // DELETE /api/records/:type/:id\n app.delete('/:type/:id', async (c) => {\n const type = c.req.param('type');\n const id = c.req.param('id');\n\n if (!schema.records[type]) {\n return c.json({ error: `Record type \"${type}\" not found` }, 404);\n }\n\n const delegate = client.$delegate(type);\n await delegate.delete({ where: { id: { equals: id } } });\n return c.json({ success: true });\n });\n\n return app;\n}\n","import { Hono } from 'hono';\nimport type { SuitePortalClient } from '@suiteportal/client-runtime';\n\nexport function createQueryRoutes(client: SuitePortalClient): Hono {\n const app = new Hono();\n\n // POST /api/query — raw SuiteQL\n app.post('/', async (c) => {\n const body = await c.req.json<{ sql?: string }>();\n\n if (!body.sql || typeof body.sql !== 'string') {\n return c.json({ error: 'Missing \"sql\" field in request body' }, 400);\n }\n\n const rows = await client.$queryRaw(body.sql);\n return c.json({ data: rows, count: rows.length });\n });\n\n return app;\n}\n","import type { ErrorHandler } from 'hono';\nimport { NetSuiteError, AuthError, RateLimitError, TimeoutError } from '@suiteportal/connector';\n\n/**\n * Hono onError handler — maps NetSuiteError subclasses to HTTP responses.\n * This prevents unhandled errors from printing stack traces to the console.\n */\nexport const onError: ErrorHandler = (err, c) => {\n if (err instanceof AuthError) {\n return c.json({ error: 'Authentication failed', message: err.message }, 401);\n }\n if (err instanceof RateLimitError) {\n return c.json({ error: 'Rate limit exceeded', message: err.message }, 429);\n }\n if (err instanceof TimeoutError) {\n return c.json({ error: 'Request timeout', message: err.message }, 504);\n }\n if (err instanceof NetSuiteError) {\n const status = err.status && err.status >= 400 && err.status < 600 ? err.status : 502;\n const details = err.details as Record<string, unknown> | undefined;\n const errorDetails = details?.['o:errorDetails'];\n return c.json({\n error: 'NetSuite error',\n message: err.message,\n status,\n details: errorDetails ?? undefined,\n }, status as 400);\n }\n if (err instanceof Error) {\n console.error(`[studio] ${err.message}`);\n return c.json({ error: 'Internal error', message: err.message }, 500);\n }\n return c.json({ error: 'Unknown error' }, 500);\n};\n"],"mappings":"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","names":["Hono","Hono","Hono","Hono"]}
@@ -0,0 +1 @@
1
+ *,*:before,*:after{box-sizing:border-box;margin:0;padding:0}:root{--bg-primary: #0a0a0f;--bg-secondary: #12121a;--bg-tertiary: #1a1a26;--bg-hover: #22222e;--bg-active: #2a2a3a;--border: #2a2a3a;--border-subtle: #1e1e2a;--text-primary: #e8e8f0;--text-secondary: #9090a8;--text-muted: #606078;--accent: #6366f1;--accent-hover: #818cf8;--accent-dim: rgba(99, 102, 241, .15);--danger: #ef4444;--danger-hover: #f87171;--danger-dim: rgba(239, 68, 68, .15);--success: #22c55e;--success-dim: rgba(34, 197, 94, .15);--warning: #f59e0b;--radius-sm: 4px;--radius-md: 8px;--radius-lg: 12px;--shadow-sm: 0 1px 2px rgba(0,0,0,.3);--shadow-md: 0 4px 12px rgba(0,0,0,.4);--shadow-lg: 0 8px 24px rgba(0,0,0,.5);--font-mono: "JetBrains Mono", "Fira Code", "SF Mono", monospace;--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;--transition: .15s ease}html,body,#root{height:100%}body{font-family:var(--font-sans);background:var(--bg-primary);color:var(--text-primary);line-height:1.5;-webkit-font-smoothing:antialiased}.studio-layout{display:flex;height:100vh}.studio-sidebar{width:280px;min-width:280px;background:var(--bg-secondary);border-right:1px solid var(--border);display:flex;flex-direction:column;overflow:hidden}.studio-main{flex:1;display:flex;flex-direction:column;overflow:hidden}.sidebar-header{padding:16px 20px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px}.sidebar-logo{font-size:15px;font-weight:600;letter-spacing:-.3px}.sidebar-logo span{color:var(--accent)}.sidebar-badge{font-size:10px;font-weight:600;padding:2px 6px;background:var(--accent-dim);color:var(--accent);border-radius:4px;text-transform:uppercase;letter-spacing:.5px}.sidebar-search{padding:12px 16px;border-bottom:1px solid var(--border-subtle)}.sidebar-search input{width:100%;padding:8px 12px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text-primary);font-size:13px;outline:none;transition:border-color var(--transition)}.sidebar-search input:focus{border-color:var(--accent)}.sidebar-search input::placeholder{color:var(--text-muted)}.sidebar-nav{flex:1;overflow-y:auto;padding:8px 0}.sidebar-section-label{padding:8px 20px 4px;font-size:10px;font-weight:700;color:var(--text-muted);text-transform:uppercase;letter-spacing:1px}.sidebar-item{display:flex;align-items:center;gap:8px;padding:6px 20px;cursor:pointer;font-size:13px;color:var(--text-secondary);transition:all var(--transition);border-left:2px solid transparent}.sidebar-item:hover{background:var(--bg-hover);color:var(--text-primary)}.sidebar-item.active{background:var(--accent-dim);color:var(--accent);border-left-color:var(--accent)}.sidebar-item-badge{margin-left:auto;font-size:11px;color:var(--text-muted);font-family:var(--font-mono)}.sidebar-item-icon{font-size:14px;width:18px;text-align:center;opacity:.7}.sidebar-tab-bar{display:flex;border-bottom:1px solid var(--border)}.sidebar-tab{flex:1;padding:10px;font-size:12px;font-weight:600;text-align:center;cursor:pointer;color:var(--text-muted);border-bottom:2px solid transparent;transition:all var(--transition);background:none;border-top:none;border-left:none;border-right:none}.sidebar-tab:hover{color:var(--text-secondary)}.sidebar-tab.active{color:var(--accent);border-bottom-color:var(--accent)}.content-header{padding:16px 24px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;background:var(--bg-secondary)}.content-title{font-size:16px;font-weight:600}.content-subtitle{font-size:12px;color:var(--text-muted);margin-left:8px}.content-actions{display:flex;gap:8px}.btn{padding:7px 14px;border-radius:var(--radius-sm);font-size:13px;font-weight:500;cursor:pointer;transition:all var(--transition);border:1px solid var(--border);display:inline-flex;align-items:center;gap:6px}.btn-primary{background:var(--accent);color:#fff;border-color:var(--accent)}.btn-primary:hover{background:var(--accent-hover)}.btn-ghost{background:transparent;color:var(--text-secondary)}.btn-ghost:hover{background:var(--bg-hover);color:var(--text-primary)}.btn-danger{background:var(--danger-dim);color:var(--danger);border-color:transparent}.btn-danger:hover{background:var(--danger);color:#fff}.btn-sm{padding:4px 10px;font-size:12px}.btn:disabled{opacity:.5;cursor:not-allowed}.table-container{flex:1;overflow:auto}table{width:100%;border-collapse:collapse;font-size:13px}thead{position:sticky;top:0;z-index:1}th{padding:10px 16px;text-align:left;font-weight:600;font-size:11px;text-transform:uppercase;letter-spacing:.5px;color:var(--text-muted);background:var(--bg-secondary);border-bottom:1px solid var(--border);cursor:pointer;-webkit-user-select:none;user-select:none;white-space:nowrap}th:hover{color:var(--text-secondary)}th.sorted{color:var(--accent)}td{padding:8px 16px;border-bottom:1px solid var(--border-subtle);max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--font-mono);font-size:12px}tr:hover td{background:var(--bg-hover)}tr.selected td{background:var(--accent-dim)}.cell-null{color:var(--text-muted);font-style:italic}.cell-bool-true{color:var(--success)}.cell-bool-false{color:var(--danger)}.cell-number{color:var(--warning)}.pagination{display:flex;align-items:center;justify-content:space-between;padding:12px 24px;border-top:1px solid var(--border);background:var(--bg-secondary);font-size:13px}.pagination-info{color:var(--text-muted)}.pagination-controls{display:flex;gap:4px}.pagination-btn{padding:4px 12px;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg-tertiary);color:var(--text-secondary);cursor:pointer;font-size:12px;transition:all var(--transition)}.pagination-btn:hover:not(:disabled){background:var(--bg-hover);color:var(--text-primary)}.pagination-btn:disabled{opacity:.3;cursor:not-allowed}.pagination-btn.active{background:var(--accent);color:#fff;border-color:var(--accent)}.filter-bar{padding:8px 24px;border-bottom:1px solid var(--border-subtle);display:flex;align-items:center;gap:8px;flex-wrap:wrap;background:var(--bg-secondary)}.filter-chip{display:flex;align-items:center;gap:4px;padding:4px 10px;background:var(--accent-dim);color:var(--accent);border-radius:20px;font-size:12px}.filter-chip button{background:none;border:none;color:var(--accent);cursor:pointer;font-size:14px;line-height:1;padding:0 2px}.filter-add{padding:4px 10px;background:var(--bg-tertiary);border:1px dashed var(--border);border-radius:20px;color:var(--text-muted);font-size:12px;cursor:pointer;transition:all var(--transition)}.filter-add:hover{border-color:var(--accent);color:var(--accent)}.modal-overlay{position:fixed;top:0;right:0;bottom:0;left:0;background:#000000b3;display:flex;align-items:center;justify-content:center;z-index:100;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.modal{background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius-lg);width:90%;max-width:640px;max-height:85vh;display:flex;flex-direction:column;box-shadow:var(--shadow-lg)}.modal-header{padding:20px 24px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between}.modal-title{font-size:16px;font-weight:600}.modal-close{background:none;border:none;color:var(--text-muted);cursor:pointer;font-size:20px;padding:4px}.modal-close:hover{color:var(--text-primary)}.modal-body{padding:24px;overflow-y:auto;flex:1}.modal-footer{padding:16px 24px;border-top:1px solid var(--border);display:flex;justify-content:flex-end;gap:8px}.form-group{margin-bottom:16px}.form-label{display:block;font-size:12px;font-weight:600;color:var(--text-secondary);margin-bottom:6px;text-transform:uppercase;letter-spacing:.5px}.form-label .required{color:var(--danger)}.form-input{width:100%;padding:8px 12px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text-primary);font-size:13px;font-family:var(--font-mono);outline:none;transition:border-color var(--transition)}.form-input:focus{border-color:var(--accent)}.form-input::placeholder{color:var(--text-muted)}select.form-input{cursor:pointer}textarea.form-input{min-height:80px;resize:vertical;font-family:var(--font-mono)}.form-hint{font-size:11px;color:var(--text-muted);margin-top:4px}.query-editor{display:flex;flex-direction:column;flex:1}.query-input-area{padding:16px 24px;border-bottom:1px solid var(--border)}.query-textarea{width:100%;min-height:120px;padding:12px 16px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:var(--radius-md);color:var(--text-primary);font-family:var(--font-mono);font-size:13px;resize:vertical;outline:none;transition:border-color var(--transition)}.query-textarea:focus{border-color:var(--accent)}.query-actions{display:flex;gap:8px;margin-top:12px}.query-results{flex:1;overflow:auto}.query-status{padding:8px 24px;font-size:12px;color:var(--text-muted);border-bottom:1px solid var(--border-subtle)}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;flex:1;padding:48px;color:var(--text-muted)}.empty-state-icon{font-size:48px;margin-bottom:16px;opacity:.3}.empty-state-title{font-size:16px;font-weight:600;margin-bottom:8px;color:var(--text-secondary)}.empty-state-text{font-size:13px;text-align:center;max-width:400px}.loading-spinner{width:24px;height:24px;border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.error-banner{padding:10px 24px;background:var(--danger-dim);color:var(--danger);font-size:13px;border-bottom:1px solid rgba(239,68,68,.2)}.column-chooser{position:relative}.column-chooser-dropdown{position:absolute;top:100%;right:0;z-index:50;background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius-md);box-shadow:var(--shadow-lg);padding:8px 0;min-width:220px;max-height:360px;overflow-y:auto}.column-chooser-item{display:flex;align-items:center;gap:8px;padding:6px 16px;font-size:13px;cursor:pointer;transition:background var(--transition)}.column-chooser-item:hover{background:var(--bg-hover)}.column-chooser-item input{accent-color:var(--accent)}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}::-webkit-scrollbar-thumb:hover{background:var(--text-muted)}.confirm-text{font-size:14px;color:var(--text-secondary);line-height:1.6}.confirm-text strong{color:var(--text-primary)}