@trokky/studio 2.0.3

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.
@@ -0,0 +1,69 @@
1
+ /**
2
+ * @trokky/studio - Trokky CMS Studio
3
+ *
4
+ * A clean, maintainable Studio for Trokky CMS that works with API routers.
5
+ */
6
+ interface StudioConfig {
7
+ /** API router for Studio to connect to (from @trokky/express or any Trokky API integration) */
8
+ apiRouter: any;
9
+ /** Mount point for Studio (default: '/studio') */
10
+ mount?: string;
11
+ /** Enable authentication */
12
+ auth?: boolean;
13
+ /** Studio branding configuration */
14
+ branding?: {
15
+ title?: string;
16
+ logo?: string;
17
+ theme?: 'light' | 'dark' | 'system';
18
+ };
19
+ /** Custom structure configuration */
20
+ structure?: any;
21
+ /** Custom field types to register */
22
+ customFields?: any[];
23
+ /** Additional Studio configuration */
24
+ config?: {
25
+ pageSize?: number;
26
+ enableDrafts?: boolean;
27
+ enableVersioning?: boolean;
28
+ };
29
+ /** Session management configuration */
30
+ sessionConfig?: {
31
+ /** Auto-refresh token buffer in milliseconds (default: 30000) */
32
+ refreshBufferMs?: number;
33
+ /** Warning display buffer in milliseconds (default: 90000) */
34
+ warningBufferMs?: number;
35
+ /** Session check interval in milliseconds (default: 5000) */
36
+ checkIntervalMs?: number;
37
+ /** Default session timeout in milliseconds (default: 2 hours) */
38
+ defaultTimeoutMs?: number;
39
+ /** Extended session timeout for "Remember Me" in milliseconds (default: 7 days) */
40
+ extendedTimeoutMs?: number;
41
+ /** Inactivity timeout in milliseconds (default: 30 minutes) */
42
+ inactivityTimeoutMs?: number;
43
+ };
44
+ }
45
+ interface StudioMiddleware {
46
+ /** Express router containing Studio routes */
47
+ router: any;
48
+ }
49
+ /**
50
+ * Create a Trokky Studio middleware
51
+ *
52
+ * @example
53
+ * ```typescript
54
+ * import { createStudio } from '@trokky/studio'
55
+ *
56
+ * const studio = createStudio({
57
+ * apiRouter: myApiRouter,
58
+ * schemas: cms.getAllSchemas(),
59
+ * branding: { title: 'My CMS Admin' }
60
+ * })
61
+ *
62
+ * app.use('/studio', studio.router)
63
+ * ```
64
+ */
65
+ export declare function createStudio(config: StudioConfig): Promise<StudioMiddleware>;
66
+ export { createStudioLogger, StudioLogger } from './utils/logger.js';
67
+ export type { LogLevel, StudioLoggerConfig } from './utils/logger.js';
68
+ export { getStudioHTML, getStudioAsset } from './server/assets.js';
69
+ export type { StudioConfig, StudioMiddleware };
@@ -0,0 +1,61 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <script>
5
+ window.TROKKY_CONFIG = {
6
+ mode: 'development',
7
+ basePath: '',
8
+ branding: {
9
+ title: 'Trokky Studio (Dev)',
10
+ theme: 'system'
11
+ },
12
+ structure: null,
13
+ config: {},
14
+ customFields: []
15
+ };
16
+ </script>
17
+ <meta charset="UTF-8" />
18
+ <link rel="icon" type="image/svg+xml" href="/trokky-icon.svg" />
19
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
20
+ <meta name="description" content="Trokky Studio v2 - Zero-config content management interface" />
21
+
22
+ <!-- Theme variables with defaults (can be overridden at runtime) -->
23
+ <style>
24
+ :root {
25
+ /* Default blue theme */
26
+ --color-primary-50: 239 246 255;
27
+ --color-primary-100: 219 234 254;
28
+ --color-primary-200: 191 219 254;
29
+ --color-primary-300: 147 197 253;
30
+ --color-primary-400: 96 165 250;
31
+ --color-primary-500: 59 130 246;
32
+ --color-primary-600: 37 99 235;
33
+ --color-primary-700: 29 78 216;
34
+ --color-primary-800: 30 64 175;
35
+ --color-primary-900: 30 58 138;
36
+ --color-primary-950: 23 37 84;
37
+ }
38
+
39
+ /* Dark mode by default respects system preference */
40
+ @media (prefers-color-scheme: dark) {
41
+ html:not(.light) {
42
+ color-scheme: dark;
43
+ }
44
+ }
45
+ </style>
46
+
47
+ <title>Trokky Studio</title>
48
+ <script type="module" crossorigin src="/assets/index-CSDhJo2Z.js"></script>
49
+ <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-CbXtAM7H.js">
50
+ <link rel="modulepreload" crossorigin href="/assets/router-Dpg4KYTB.js">
51
+ <link rel="modulepreload" crossorigin href="/assets/vendor-igJJvL5X.js">
52
+ <link rel="modulepreload" crossorigin href="/assets/query-CieM36kR.js">
53
+ <link rel="modulepreload" crossorigin href="/assets/ui-D5AGhrb0.js">
54
+ <link rel="stylesheet" crossorigin href="/assets/index-DykyF7LU.css">
55
+ </head>
56
+ <body class="bg-gray-50 dark:bg-gray-900">
57
+ <div id="root"></div>
58
+
59
+
60
+ </body>
61
+ </html>
package/dist/index.js ADDED
@@ -0,0 +1,199 @@
1
+ /**
2
+ * @trokky/studio - Trokky CMS Studio
3
+ *
4
+ * A clean, maintainable Studio for Trokky CMS that works with API routers.
5
+ */
6
+ import { createStudioLogger } from './utils/logger.js';
7
+ /**
8
+ * Create a Trokky Studio middleware
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * import { createStudio } from '@trokky/studio'
13
+ *
14
+ * const studio = createStudio({
15
+ * apiRouter: myApiRouter,
16
+ * schemas: cms.getAllSchemas(),
17
+ * branding: { title: 'My CMS Admin' }
18
+ * })
19
+ *
20
+ * app.use('/studio', studio.router)
21
+ * ```
22
+ */
23
+ export async function createStudio(config) {
24
+ const logger = createStudioLogger('Studio');
25
+ // Validate configuration
26
+ if (!config.apiRouter) {
27
+ throw new Error('apiRouter is required in config');
28
+ }
29
+ // Note: Schemas are now fetched from /api/collections endpoint
30
+ // Import Express synchronously for Node.js environment
31
+ let router;
32
+ try {
33
+ const { createRequire } = await import('module');
34
+ const require = createRequire(import.meta.url);
35
+ const express = require('express');
36
+ router = express.Router();
37
+ }
38
+ catch (e) {
39
+ console.error('[ERROR] Failed to import Express:', e);
40
+ throw new Error('Express is required for Studio but could not be imported');
41
+ }
42
+ logger.info('Creating Studio', {
43
+ mount: config.mount || '/studio',
44
+ auth: config.auth || false,
45
+ customFields: config.customFields?.length || 0
46
+ });
47
+ // Serve Studio static assets
48
+ router.get('/', serveStudioHTML(config));
49
+ router.get('/assets/*', serveStudioAssets());
50
+ // Note: API router is mounted at main server level (/api), not here
51
+ // router.use('/api', config.apiRouter); // Removed - API is at server root level
52
+ // Catch-all route for SPA routing - must be last
53
+ router.get('*', serveStudioHTML(config));
54
+ return { router };
55
+ }
56
+ /**
57
+ * Serve the Studio HTML with injected configuration
58
+ */
59
+ function serveStudioHTML(config) {
60
+ return async (_req, res) => {
61
+ try {
62
+ // Get Studio HTML template (only works in Node.js)
63
+ let html;
64
+ try {
65
+ const { createRequire } = await import('module');
66
+ const require = createRequire(import.meta.url);
67
+ const url = require('url');
68
+ const path = require('path');
69
+ const fs = require('fs');
70
+ const currentFile = url.fileURLToPath(import.meta.url);
71
+ const currentDir = path.dirname(currentFile);
72
+ // Check if we're running from TypeScript source or compiled dist
73
+ const isTypeScript = currentFile.includes('/src/');
74
+ const htmlPath = isTypeScript
75
+ ? path.join(currentDir, '../dist/index.html') // From src/ to dist/
76
+ : path.join(currentDir, 'index.html'); // Already in dist/
77
+ html = fs.readFileSync(htmlPath, 'utf-8');
78
+ }
79
+ catch (e) {
80
+ console.error('[ERROR] Failed to read Studio HTML:', e.message);
81
+ // Provide basic HTML template
82
+ html = `<!DOCTYPE html>
83
+ <html lang="en">
84
+ <head>
85
+ <meta charset="UTF-8">
86
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
87
+ <title>Trokky Studio</title>
88
+ </head>
89
+ <body>
90
+ <div id="root">
91
+ <div style="display: flex; align-items: center; justify-content: center; height: 100vh; font-family: system-ui, sans-serif;">
92
+ <div style="text-align: center;">
93
+ <h1>Trokky Studio</h1>
94
+ <p>Loading...</p>
95
+ </div>
96
+ </div>
97
+ </div>
98
+ </body>
99
+ </html>`;
100
+ }
101
+ // Inject runtime configuration and set base href
102
+ const basePath = _req.baseUrl || '/studio';
103
+ const runtimeConfig = {
104
+ mode: process.env.NODE_ENV || 'production',
105
+ apiUrl: '/api', // API is at server root level
106
+ apiBaseUrl: '/api',
107
+ branding: config.branding || { title: 'Trokky Studio' },
108
+ structure: config.structure || null,
109
+ config: config.config || {},
110
+ customFields: config.customFields || [],
111
+ sessionConfig: config.sessionConfig || null,
112
+ basePath
113
+ };
114
+ // Safe JSON serialization that handles functions and circular references
115
+ const safeStringify = (obj) => {
116
+ try {
117
+ return JSON.stringify(obj, (_, value) => {
118
+ if (typeof value === 'function') {
119
+ return undefined; // Remove functions
120
+ }
121
+ if (typeof value === 'object' && value !== null) {
122
+ // Simple circular reference check
123
+ if (obj._seen && obj._seen.has(value)) {
124
+ return '[Circular]';
125
+ }
126
+ if (!obj._seen)
127
+ obj._seen = new Set();
128
+ obj._seen.add(value);
129
+ }
130
+ return value;
131
+ });
132
+ }
133
+ catch (error) {
134
+ console.error('[ERROR] JSON serialization failed:', error);
135
+ return '{}';
136
+ }
137
+ };
138
+ const configJson = safeStringify(runtimeConfig);
139
+ console.log('[DEBUG] Runtime config length:', configJson.length);
140
+ // Override any hardcoded TROKKY_CONFIG from Vite build
141
+ // Use a more robust replacement that handles nested objects
142
+ html = html.replace(/window\.TROKKY_CONFIG\s*=\s*\{[\s\S]*?\};/g, `window.TROKKY_CONFIG = ${configJson};`);
143
+ html = html.replace('<head>', `<head>
144
+ <base href="${basePath}/">
145
+ <script>
146
+ window.TROKKY_CONFIG = ${configJson};
147
+ </script>`);
148
+ res.setHeader('Content-Type', 'text/html');
149
+ res.send(html);
150
+ }
151
+ catch (error) {
152
+ console.error('[ERROR] Failed to serve Studio HTML:', error);
153
+ res.status(500).json({ error: 'Failed to serve Studio' });
154
+ }
155
+ };
156
+ }
157
+ /**
158
+ * Serve Studio static assets
159
+ */
160
+ function serveStudioAssets() {
161
+ return async (req, res) => {
162
+ try {
163
+ // Assets handling only works in Node.js environment
164
+ const { createRequire } = await import('module');
165
+ const require = createRequire(import.meta.url);
166
+ const url = require('url');
167
+ const path = require('path');
168
+ const currentFile = url.fileURLToPath(import.meta.url);
169
+ const currentDir = path.dirname(currentFile);
170
+ // Check if we're running from TypeScript source or compiled dist
171
+ const isTypeScript = currentFile.includes('/src/');
172
+ const assetPath = isTypeScript
173
+ ? path.join(currentDir, '..', req.path) // From src/ to dist/
174
+ : path.join(currentDir, req.path); // Already in dist/
175
+ // Set proper MIME types
176
+ const ext = path.extname(req.path);
177
+ if (ext === '.css') {
178
+ res.setHeader('Content-Type', 'text/css');
179
+ }
180
+ else if (ext === '.js') {
181
+ res.setHeader('Content-Type', 'application/javascript');
182
+ }
183
+ res.sendFile(assetPath, (err) => {
184
+ if (err) {
185
+ console.error('[ERROR] Failed to serve asset:', req.path, err.message);
186
+ res.status(404).json({ error: 'Asset not found' });
187
+ }
188
+ });
189
+ }
190
+ catch (e) {
191
+ // Return 404 for asset not found
192
+ res.status(404).json({ error: 'Asset not found' });
193
+ }
194
+ };
195
+ }
196
+ // Export Studio logger for custom components
197
+ export { createStudioLogger, StudioLogger } from './utils/logger.js';
198
+ // Export Studio asset serving utilities
199
+ export { getStudioHTML, getStudioAsset } from './server/assets.js';
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Studio Asset Serving Utilities
3
+ *
4
+ * These utilities allow other packages (like @trokky/routes) to serve
5
+ * Studio HTML and assets without needing to resolve package paths.
6
+ */
7
+ export interface StudioConfig {
8
+ mode: 'production';
9
+ apiBasePath: string;
10
+ basePath?: string;
11
+ backendUrl?: string;
12
+ schemas: any[];
13
+ branding?: {
14
+ title?: string;
15
+ theme?: 'light' | 'dark' | 'system';
16
+ };
17
+ structure?: any;
18
+ config?: any;
19
+ customFields?: any[];
20
+ }
21
+ /**
22
+ * Get Studio HTML with injected configuration
23
+ */
24
+ export declare function getStudioHTML(config: StudioConfig, studioPath: string): string;
25
+ /**
26
+ * Get Studio asset content
27
+ */
28
+ export declare function getStudioAsset(assetPath: string): {
29
+ content: Buffer;
30
+ contentType: string;
31
+ } | null;
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Studio Asset Serving Utilities
3
+ *
4
+ * These utilities allow other packages (like @trokky/routes) to serve
5
+ * Studio HTML and assets without needing to resolve package paths.
6
+ */
7
+ import { readFileSync, existsSync } from 'fs';
8
+ import { join, dirname, extname } from 'path';
9
+ import { fileURLToPath } from 'url';
10
+ /**
11
+ * Escape a string for safe embedding in HTML content.
12
+ */
13
+ function escapeHtml(str) {
14
+ return str
15
+ .replace(/&/g, '&amp;')
16
+ .replace(/</g, '&lt;')
17
+ .replace(/>/g, '&gt;')
18
+ .replace(/"/g, '&quot;')
19
+ .replace(/'/g, '&#39;');
20
+ }
21
+ /**
22
+ * Safely serialize an object for embedding inside a <script> tag.
23
+ * Prevents breaking out via </script> or similar sequences.
24
+ */
25
+ function escapeJsonForScript(obj) {
26
+ return JSON.stringify(obj)
27
+ .replace(/</g, '\\u003c')
28
+ .replace(/>/g, '\\u003e')
29
+ .replace(/&/g, '\\u0026')
30
+ .replace(/'/g, '\\u0027');
31
+ }
32
+ // Get the Studio package root directory (assuming we're in dist/server/ when compiled)
33
+ const __filename = fileURLToPath(import.meta.url);
34
+ const __dirname = dirname(__filename);
35
+ const studioDir = join(__dirname, '../..');
36
+ const distDir = join(studioDir, 'dist');
37
+ /**
38
+ * Get Studio HTML with injected configuration
39
+ */
40
+ export function getStudioHTML(config, studioPath) {
41
+ try {
42
+ const htmlPath = join(distDir, 'index.html');
43
+ if (!existsSync(htmlPath)) {
44
+ throw new Error(`Studio HTML not found at ${htmlPath}. Please build @trokky/studio package.`);
45
+ }
46
+ let html = readFileSync(htmlPath, 'utf-8');
47
+ // Replace any existing config with our config (handle multi-line objects)
48
+ html = html.replace(/window\.TROKKY_CONFIG\s*=\s*{[\s\S]*?};/, `window.TROKKY_CONFIG = ${escapeJsonForScript(config)};
49
+
50
+ // TEMPORARY FIX: Override API client path building for dynamic API paths
51
+ window.TROKKY_API_PATH_FIX = function() {
52
+
53
+
54
+ // Wait for the API client to be available
55
+ const checkApiClient = setInterval(() => {
56
+ if (window.TrokkyApiClient) {
57
+
58
+ clearInterval(checkApiClient);
59
+
60
+ // Store original post method
61
+ const originalPost = window.TrokkyApiClient.post;
62
+
63
+ // Override post method to handle dynamic API paths
64
+ window.TrokkyApiClient.post = function(endpoint, data) {
65
+
66
+
67
+ // Replace /api with the correct API base path
68
+ if (endpoint.startsWith('/api')) {
69
+ const newEndpoint = endpoint.replace('/api', '${escapeHtml(config.apiBasePath)}');
70
+
71
+ endpoint = newEndpoint;
72
+ }
73
+
74
+ return originalPost.call(this, endpoint, data);
75
+ };
76
+
77
+
78
+ }
79
+ }, 100);
80
+
81
+ // Timeout after 5 seconds
82
+ setTimeout(() => {
83
+ clearInterval(checkApiClient);
84
+
85
+ }, 5000);
86
+ };
87
+
88
+ // Apply the fix after DOM loads
89
+ if (document.readyState === 'loading') {
90
+ document.addEventListener('DOMContentLoaded', window.TROKKY_API_PATH_FIX);
91
+ } else {
92
+ window.TROKKY_API_PATH_FIX();
93
+ }`);
94
+ // Update asset paths to use our Studio route
95
+ html = html.replace(/src="\/assets\//g, `src="${studioPath}/assets/`);
96
+ html = html.replace(/href="\/assets\//g, `href="${studioPath}/assets/`);
97
+ // Remove favicon reference since we don't have the icon
98
+ html = html.replace(/<link rel="icon"[^>]*>/g, '');
99
+ // Update title if provided
100
+ if (config.branding?.title) {
101
+ html = html.replace(/<title>.*?<\/title>/, `<title>${escapeHtml(config.branding.title)}</title>`);
102
+ }
103
+ return html;
104
+ }
105
+ catch (error) {
106
+ const errorMessage = error instanceof Error ? error.message : String(error);
107
+ // Fallback HTML if built assets are not available
108
+ return `<!DOCTYPE html>
109
+ <html lang="en">
110
+ <head>
111
+ <meta charset="UTF-8">
112
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
113
+ <title>${escapeHtml(config.branding?.title || 'Trokky Studio')}</title>
114
+ <script>
115
+ window.TROKKY_CONFIG = ${escapeJsonForScript(config)};
116
+ </script>
117
+ </head>
118
+ <body>
119
+ <div id="root">
120
+ <div style="display: flex; align-items: center; justify-content: center; height: 100vh; font-family: system-ui, sans-serif;">
121
+ <div style="text-align: center;">
122
+ <h1>${escapeHtml(config.branding?.title || 'Trokky Studio')}</h1>
123
+ <p>Studio not built. Please run: npm run build in @trokky/studio package.</p>
124
+ <p style="font-size: 12px; color: #666; margin-top: 16px;">Error: ${escapeHtml(errorMessage)}</p>
125
+ </div>
126
+ </div>
127
+ </div>
128
+ </body>
129
+ </html>`;
130
+ }
131
+ }
132
+ /**
133
+ * Get Studio asset content
134
+ */
135
+ export function getStudioAsset(assetPath) {
136
+ try {
137
+ const safePath = assetPath.replace(/\.\.\//g, ''); // Prevent directory traversal
138
+ const fullAssetPath = join(distDir, 'assets', safePath);
139
+ if (!existsSync(fullAssetPath)) {
140
+ return null;
141
+ }
142
+ const content = readFileSync(fullAssetPath);
143
+ // Determine content type based on file extension
144
+ const ext = extname(assetPath).toLowerCase();
145
+ let contentType = 'application/octet-stream';
146
+ if (ext === '.js') {
147
+ contentType = 'application/javascript';
148
+ }
149
+ else if (ext === '.css') {
150
+ contentType = 'text/css';
151
+ }
152
+ else if (ext === '.map') {
153
+ contentType = 'application/json';
154
+ }
155
+ else if (ext === '.svg') {
156
+ contentType = 'image/svg+xml';
157
+ }
158
+ else if (ext === '.png') {
159
+ contentType = 'image/png';
160
+ }
161
+ else if (ext === '.jpg' || ext === '.jpeg') {
162
+ contentType = 'image/jpeg';
163
+ }
164
+ return { content, contentType };
165
+ }
166
+ catch (error) {
167
+ return null;
168
+ }
169
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Studio Logger - Browser-optimized logging
3
+ *
4
+ * Lightweight logger for Studio frontend components.
5
+ * Integrates with browser dev tools and provides structured output.
6
+ */
7
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
8
+ export interface StudioLoggerConfig {
9
+ level: LogLevel;
10
+ enabled: boolean;
11
+ prefix: string;
12
+ }
13
+ export declare class StudioLogger {
14
+ private config;
15
+ constructor(component: string, config?: Partial<StudioLoggerConfig>);
16
+ private shouldLog;
17
+ private formatMessage;
18
+ debug(message: string, data?: any): void;
19
+ info(message: string, data?: any): void;
20
+ warn(message: string, data?: any): void;
21
+ error(message: string, error?: Error | any): void;
22
+ }
23
+ /**
24
+ * Create a logger for a Studio component
25
+ */
26
+ export declare function createStudioLogger(component: string): StudioLogger;
27
+ /**
28
+ * Global configuration for all Studio loggers
29
+ */
30
+ export declare const StudioLoggerConfig: {
31
+ setLevel(level: LogLevel): void;
32
+ getLevel(): LogLevel;
33
+ disable(): void;
34
+ enable(): void;
35
+ };
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Studio Logger - Browser-optimized logging
3
+ *
4
+ * Lightweight logger for Studio frontend components.
5
+ * Integrates with browser dev tools and provides structured output.
6
+ */
7
+ const LOG_LEVELS = {
8
+ debug: 0,
9
+ info: 1,
10
+ warn: 2,
11
+ error: 3
12
+ };
13
+ // Default to warn level to reduce console noise
14
+ // Use cross-platform development detection that works in all environments
15
+ let GLOBAL_LOG_LEVEL = (typeof window !== 'undefined' && window.__TROKKY_DEV__ === true) ? 'warn' : 'error';
16
+ export class StudioLogger {
17
+ constructor(component, config = {}) {
18
+ this.config = {
19
+ level: GLOBAL_LOG_LEVEL,
20
+ enabled: true,
21
+ prefix: `[Studio:${component}]`,
22
+ ...config
23
+ };
24
+ }
25
+ shouldLog(level) {
26
+ // Always use the current global log level instead of the instance level
27
+ return this.config.enabled && LOG_LEVELS[level] >= LOG_LEVELS[GLOBAL_LOG_LEVEL];
28
+ }
29
+ formatMessage(level, message) {
30
+ return `${this.config.prefix}[${level}] ${message}`;
31
+ }
32
+ debug(message, data) {
33
+ if (this.shouldLog('debug')) {
34
+ const formatted = this.formatMessage('debug', message);
35
+ if (data) {
36
+ console.debug(formatted, data);
37
+ }
38
+ else {
39
+ console.debug(formatted);
40
+ }
41
+ }
42
+ }
43
+ info(message, data) {
44
+ if (this.shouldLog('info')) {
45
+ const formatted = this.formatMessage('info', message);
46
+ if (data) {
47
+ console.info(formatted, data);
48
+ }
49
+ else {
50
+ console.info(formatted);
51
+ }
52
+ }
53
+ }
54
+ warn(message, data) {
55
+ if (this.shouldLog('warn')) {
56
+ const formatted = this.formatMessage('warn', message);
57
+ if (data) {
58
+ console.warn(formatted, data);
59
+ }
60
+ else {
61
+ console.warn(formatted);
62
+ }
63
+ }
64
+ }
65
+ error(message, error) {
66
+ if (this.shouldLog('error')) {
67
+ const formatted = this.formatMessage('error', message);
68
+ if (error instanceof Error) {
69
+ console.error(formatted, error);
70
+ }
71
+ else if (error) {
72
+ console.error(formatted, error);
73
+ }
74
+ else {
75
+ console.error(formatted);
76
+ }
77
+ }
78
+ }
79
+ }
80
+ /**
81
+ * Create a logger for a Studio component
82
+ */
83
+ export function createStudioLogger(component) {
84
+ return new StudioLogger(component);
85
+ }
86
+ /**
87
+ * Global configuration for all Studio loggers
88
+ */
89
+ export const StudioLoggerConfig = {
90
+ setLevel(level) {
91
+ GLOBAL_LOG_LEVEL = level;
92
+ // Also store in window for persistence across page reloads in dev
93
+ if (typeof window !== 'undefined') {
94
+ window.__TROKKY_LOG_LEVEL__ = level;
95
+ }
96
+ },
97
+ getLevel() {
98
+ return GLOBAL_LOG_LEVEL;
99
+ },
100
+ disable() {
101
+ GLOBAL_LOG_LEVEL = 'error'; // Effectively disable by setting to highest level
102
+ },
103
+ enable() {
104
+ GLOBAL_LOG_LEVEL = (typeof window !== 'undefined' && window.__TROKKY_DEV__ === true) ? 'warn' : 'error';
105
+ }
106
+ };
107
+ // Check for persisted log level from previous session
108
+ if (typeof window !== 'undefined' && window.__TROKKY_LOG_LEVEL__) {
109
+ GLOBAL_LOG_LEVEL = window.__TROKKY_LOG_LEVEL__;
110
+ }
111
+ // Expose logger config to window for easy console control
112
+ if (typeof window !== 'undefined') {
113
+ window.TrokkyLogger = StudioLoggerConfig;
114
+ }