cyrus-studio 1.0.7 → 1.1.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 (34) hide show
  1. package/README.md +34 -1
  2. package/app/assets/AuthDialog-Dry3B6Q0.js +1 -0
  3. package/app/assets/CanvaSaveModal-BOOuwAeY.js +1 -0
  4. package/app/assets/CodingWorkspace-IGR61s5_.js +4 -0
  5. package/app/assets/CreateAppDialog-DrI4Lkd8.js +1236 -0
  6. package/app/assets/CreateProjectDialog-DI7tlDTn.js +1 -0
  7. package/app/assets/HistoryTimelineDialog-IxHPdvMy.js +1 -0
  8. package/app/assets/ImportModal-C7-xg0X4.js +83 -0
  9. package/app/assets/KeyboardShortcutsDialog-BToCWj6j.js +1 -0
  10. package/app/assets/LiveRuntimeWorkspace-BgtwTTvN.js +1 -0
  11. package/app/assets/ShareExportDialog-Dp3sb0kP.js +1 -0
  12. package/app/assets/UserProfileDialog-CIISMYby.js +1 -0
  13. package/app/assets/WelcomeOnboardingModal-B-_unbCE.js +1 -0
  14. package/app/assets/exportZip-JYUImV2j.js +1 -0
  15. package/app/assets/index-DD12pOJ9.css +2 -0
  16. package/app/assets/index-DKH4UEV-.js +2205 -0
  17. package/app/assets/rolldown-runtime-W7wSyTde.js +1 -0
  18. package/app/assets/themes-lx_odDI8.js +1 -0
  19. package/app/assets/vendor-firebase-DNMzpOGz.js +32 -0
  20. package/app/assets/vendor-icons-CmYui5CW.js +13 -0
  21. package/app/assets/vendor-jszip-Cv4gVObP.js +2 -0
  22. package/app/assets/vendor-react-B3_iTqTw.js +227 -0
  23. package/app/assets/vendor-utils-D92Kf24_.js +1 -0
  24. package/app/favicon.svg +1 -0
  25. package/app/icons.svg +24 -0
  26. package/app/index.html +50 -0
  27. package/app/login-bg.png +0 -0
  28. package/app/test-sdk-sandbox.html +720 -0
  29. package/bin/dev-server.mjs +692 -0
  30. package/bin/htmlParser.mjs +337 -0
  31. package/dist/index.cjs +1167 -36
  32. package/dist/index.d.ts +337 -9
  33. package/dist/index.mjs +1167 -36
  34. package/package.json +22 -4
@@ -0,0 +1,337 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ /**
5
+ * Universal HTML/CSS to Cyrus Canvas IR Parser
6
+ * Parses ANY HTML file (Cyrus exported layer files OR standard semantic HTML pages)
7
+ * into a full visual Cyrus Canvas project with elements, styles, and coordinates.
8
+ */
9
+ export function parseHtmlToCyrusProject(htmlContent, filename = 'Imported Project') {
10
+ if (!htmlContent || typeof htmlContent !== 'string') {
11
+ return createEmptyProject(filename);
12
+ }
13
+
14
+ // 1. Extract all CSS rules from <style> blocks
15
+ const cssRules = {};
16
+ const styleBlockRegex = /<style[^>]*>([\s\S]*?)<\/style>/gi;
17
+ let styleMatch;
18
+ while ((styleMatch = styleBlockRegex.exec(htmlContent)) !== null) {
19
+ const cssText = styleMatch[1];
20
+ const ruleRegex = /([^{]+)\{([^}]+)\}/g;
21
+ let ruleMatch;
22
+ while ((ruleMatch = ruleRegex.exec(cssText)) !== null) {
23
+ const selector = ruleMatch[1].trim();
24
+ const body = ruleMatch[2].trim();
25
+ const props = {};
26
+ body.split(';').forEach(p => {
27
+ const parts = p.split(':');
28
+ if (parts.length >= 2) {
29
+ const k = parts[0].trim().toLowerCase();
30
+ const v = parts.slice(1).join(':').trim();
31
+ if (k && v) props[k] = v;
32
+ }
33
+ });
34
+ // Store under selector
35
+ cssRules[selector] = { ...(cssRules[selector] || {}), ...props };
36
+ }
37
+ }
38
+
39
+ // 2. Parse body / canvas background color
40
+ let backgroundColor = '#0f0f12';
41
+ const bgMatch = htmlContent.match(/\.screen-canvas\s*\{[^}]*background-color:\s*([^;]+);/i) ||
42
+ htmlContent.match(/body\s*\{[^}]*background-color:\s*([^;]+);/i) ||
43
+ htmlContent.match(/style=["'][^"']*background(-color)?:\s*([^;]+);/i);
44
+ if (bgMatch) {
45
+ const rawCol = (bgMatch[1] || bgMatch[2]).trim();
46
+ if (rawCol && !rawCol.startsWith('url')) backgroundColor = rawCol;
47
+ }
48
+
49
+ // 3. Parse page title
50
+ const titleMatch = htmlContent.match(/<title>(.*?)<\/title>/i);
51
+ const pageTitle = titleMatch ? titleMatch[1].trim() : path.basename(filename, path.extname(filename));
52
+
53
+ const parseIntVal = (v, def = 0) => {
54
+ if (!v) return def;
55
+ const num = parseInt(v, 10);
56
+ return isNaN(num) ? def : num;
57
+ };
58
+
59
+ const parseInlineStyles = (styleAttr) => {
60
+ if (!styleAttr) return {};
61
+ const props = {};
62
+ styleAttr.split(';').forEach(p => {
63
+ const parts = p.split(':');
64
+ if (parts.length >= 2) {
65
+ const k = parts[0].trim().toLowerCase();
66
+ const v = parts.slice(1).join(':').trim();
67
+ if (k && v) props[k] = v;
68
+ }
69
+ });
70
+ return props;
71
+ };
72
+
73
+ const elements = [];
74
+
75
+ // 4A. Check if file uses Cyrus Layer class pattern (.layer-*)
76
+ const layerTagRegex = /<(div|button|img|h1|h2|h3|h4|p|span|input|a|header|nav|section)[^>]*class=["'][^"']*layer-([a-zA-Z0-9_]+)[^"']*["'][^>]*>([\s\S]*?)<\/\1>|<img[^>]*class=["'][^"']*layer-([a-zA-Z0-9_]+)[^"']*["'][^>]*\/?>/gi;
77
+ let hasLayerMatch = false;
78
+ let elemMatch;
79
+ let elemIndex = 0;
80
+
81
+ while ((elemMatch = layerTagRegex.exec(htmlContent)) !== null) {
82
+ hasLayerMatch = true;
83
+ const tag = (elemMatch[1] || 'img').toLowerCase();
84
+ const classId = elemMatch[2] || elemMatch[4];
85
+ const innerHtml = (elemMatch[3] || '').trim();
86
+
87
+ // Lookup CSS rules for .layer-<classId>
88
+ const matchedCss = cssRules[`.layer-${classId}`] || cssRules[`#${classId}`] || {};
89
+
90
+ const matchIndex = elemMatch.index;
91
+ const precedingSlice = htmlContent.slice(Math.max(0, matchIndex - 120), matchIndex);
92
+ const commentMatch = precedingSlice.match(/<!--\s*([^\n>]+?)\s*-->\s*$/);
93
+ const commentName = commentMatch ? commentMatch[1].trim() : '';
94
+
95
+ const textMatch = innerHtml.match(/<span>(.*?)<\/span>/i) || innerHtml.match(/<div[^>]*>(.*?)<\/div>/i);
96
+ const cleanText = textMatch ? textMatch[1].replace(/<[^>]+>/g, '').trim() : innerHtml.replace(/<[^>]+>/g, '').trim();
97
+
98
+ let type = 'box';
99
+ const lowerComment = commentName.toLowerCase();
100
+ if (tag === 'button' || lowerComment.includes('button') || lowerComment.includes('cta')) {
101
+ type = 'button';
102
+ } else if (lowerComment.includes('navbar') || lowerComment.includes('nav')) {
103
+ type = 'navbar';
104
+ } else if (lowerComment.includes('search')) {
105
+ type = 'searchBar';
106
+ } else if (lowerComment.includes('chart')) {
107
+ type = 'lineChart';
108
+ } else if (lowerComment.includes('switch') || lowerComment.includes('toggle')) {
109
+ type = 'switch';
110
+ } else if (lowerComment.includes('header') || lowerComment.includes('heading') || tag === 'h1' || tag === 'h2') {
111
+ type = 'heading';
112
+ } else if (lowerComment.includes('pricing')) {
113
+ type = 'pricingCard';
114
+ } else if (lowerComment.includes('card')) {
115
+ type = 'card';
116
+ } else if (tag === 'img') {
117
+ type = 'image';
118
+ }
119
+
120
+ const element = {
121
+ id: classId.startsWith('el_') ? classId : `el_${classId}`,
122
+ type,
123
+ name: commentName || `${type.charAt(0).toUpperCase() + type.slice(1)} ${elemIndex + 1}`,
124
+ x: parseIntVal(matchedCss.left, 40 + (elemIndex * 20)),
125
+ y: parseIntVal(matchedCss.top, 40 + (elemIndex * 60)),
126
+ width: parseIntVal(matchedCss.width, 320),
127
+ height: parseIntVal(matchedCss.height, 44),
128
+ styles: {
129
+ backgroundColor: matchedCss['background-color'] || matchedCss.background || undefined,
130
+ textColor: matchedCss.color || undefined,
131
+ borderRadius: parseIntVal(matchedCss['border-radius'], 0),
132
+ fontSize: parseIntVal(matchedCss['font-size'], 14),
133
+ fontWeight: matchedCss['font-weight'] || undefined,
134
+ border: matchedCss.border || undefined,
135
+ zIndex: parseIntVal(matchedCss['z-index'], elemIndex + 1),
136
+ },
137
+ content: {
138
+ title: cleanText || commentName || undefined,
139
+ text: cleanText || undefined,
140
+ label: cleanText || undefined,
141
+ headline: cleanText || undefined,
142
+ brandName: cleanText || undefined,
143
+ }
144
+ };
145
+
146
+ elements.push(element);
147
+ elemIndex++;
148
+ }
149
+
150
+ // 4B. Generic HTML Fallback Parser (for standard HTML files without .layer-* classes)
151
+ if (!hasLayerMatch) {
152
+ // Extract elements inside body or main content
153
+ const bodyMatch = htmlContent.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
154
+ const contentToParse = bodyMatch ? bodyMatch[1] : htmlContent;
155
+
156
+ // Match all prominent HTML tags
157
+ const genericTagRegex = /<(header|nav|section|main|article|aside|form|div|button|h1|h2|h3|h4|h5|h6|p|input|a|img)[^>]*>([\s\S]*?)<\/\1>|<(input|img)[^>]*\/?>/gi;
158
+ let genericMatch;
159
+ let curY = 40;
160
+ let autoIndex = 0;
161
+
162
+ while ((genericMatch = genericTagRegex.exec(contentToParse)) !== null && autoIndex < 35) {
163
+ const fullTag = genericMatch[0];
164
+ const tag = (genericMatch[1] || genericMatch[3] || 'div').toLowerCase();
165
+ const innerContent = (genericMatch[2] || '').trim();
166
+
167
+ // Extract attributes
168
+ const classAttrMatch = fullTag.match(/class=["']([^"']*)["']/i);
169
+ const idAttrMatch = fullTag.match(/id=["']([^"']*)["']/i);
170
+ const styleAttrMatch = fullTag.match(/style=["']([^"']*)["']/i);
171
+ const typeAttrMatch = fullTag.match(/type=["']([^"']*)["']/i);
172
+ const placeholderMatch = fullTag.match(/placeholder=["']([^"']*)["']/i);
173
+ const srcMatch = fullTag.match(/src=["']([^"']*)["']/i);
174
+
175
+ const classNames = classAttrMatch ? classAttrMatch[1].split(/\s+/) : [];
176
+ const elementId = idAttrMatch ? idAttrMatch[1] : `node_${autoIndex + 1}`;
177
+ const inlineStyle = parseInlineStyles(styleAttrMatch ? styleAttrMatch[1] : '');
178
+
179
+ // Combine matching CSS rules
180
+ let matchedCss = { ...inlineStyle };
181
+ for (const cls of classNames) {
182
+ if (cssRules[`.${cls}`]) {
183
+ matchedCss = { ...cssRules[`.${cls}`], ...matchedCss };
184
+ }
185
+ }
186
+ if (idAttrMatch && cssRules[`#${idAttrMatch[1]}`]) {
187
+ matchedCss = { ...cssRules[`#${idAttrMatch[1]}`], ...matchedCss };
188
+ }
189
+ if (cssRules[tag]) {
190
+ matchedCss = { ...cssRules[tag], ...matchedCss };
191
+ }
192
+
193
+ // Clean text
194
+ const cleanText = innerContent.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
195
+
196
+ // Determine component type
197
+ let type = 'box';
198
+ const inputType = typeAttrMatch ? typeAttrMatch[1].toLowerCase() : '';
199
+ const combinedClass = classNames.join(' ').toLowerCase();
200
+
201
+ if (tag === 'nav' || tag === 'header' || combinedClass.includes('nav') || combinedClass.includes('header')) {
202
+ type = 'navbar';
203
+ } else if (tag === 'button' || inputType === 'submit' || inputType === 'button' || combinedClass.includes('btn') || combinedClass.includes('button')) {
204
+ type = 'button';
205
+ } else if (inputType === 'search' || combinedClass.includes('search')) {
206
+ type = 'searchBar';
207
+ } else if (inputType === 'checkbox' || combinedClass.includes('switch') || combinedClass.includes('toggle')) {
208
+ type = 'switch';
209
+ } else if (tag.startsWith('h') || combinedClass.includes('title') || combinedClass.includes('heading')) {
210
+ type = 'heading';
211
+ } else if (tag === 'p' || tag === 'span' || tag === 'a') {
212
+ type = 'paragraph';
213
+ } else if (tag === 'img' || srcMatch) {
214
+ type = 'image';
215
+ } else if (combinedClass.includes('chart')) {
216
+ type = 'lineChart';
217
+ } else if (combinedClass.includes('card') || combinedClass.includes('panel')) {
218
+ type = 'card';
219
+ }
220
+
221
+ // Skip empty nested generic divs with no styling, no text, and no dimensions
222
+ if (tag === 'div' && !cleanText && !matchedCss['background-color'] && !matchedCss.background && !matchedCss.width && !matchedCss.height) {
223
+ continue;
224
+ }
225
+
226
+ // Calculate width & height
227
+ let width = parseIntVal(matchedCss.width, 0);
228
+ let height = parseIntVal(matchedCss.height, 0);
229
+
230
+ if (!width) {
231
+ if (type === 'navbar') width = 880;
232
+ else if (type === 'card' || type === 'lineChart') width = 420;
233
+ else if (type === 'heading' || type === 'paragraph') width = Math.min(680, Math.max(220, cleanText.length * 10));
234
+ else if (type === 'button') width = Math.min(240, Math.max(120, cleanText.length * 12 + 32));
235
+ else if (type === 'searchBar') width = 320;
236
+ else if (type === 'switch') width = 180;
237
+ else width = 380;
238
+ }
239
+
240
+ if (!height) {
241
+ if (type === 'navbar') height = 64;
242
+ else if (type === 'card') height = 220;
243
+ else if (type === 'lineChart') height = 240;
244
+ else if (type === 'heading') height = 48;
245
+ else if (type === 'paragraph') height = 36;
246
+ else if (type === 'button') height = 44;
247
+ else if (type === 'searchBar') height = 40;
248
+ else if (type === 'switch') height = 36;
249
+ else height = 60;
250
+ }
251
+
252
+ const x = parseIntVal(matchedCss.left, 40);
253
+ const y = parseIntVal(matchedCss.top, curY);
254
+
255
+ curY = Math.max(curY, y + height + 20);
256
+
257
+ elements.push({
258
+ id: `el_${elementId.replace(/[^a-zA-Z0-9_]/g, '_')}`,
259
+ type,
260
+ name: cleanText.slice(0, 24) || `${type.charAt(0).toUpperCase() + type.slice(1)} ${autoIndex + 1}`,
261
+ x,
262
+ y,
263
+ width,
264
+ height,
265
+ styles: {
266
+ backgroundColor: matchedCss['background-color'] || matchedCss.background || undefined,
267
+ textColor: matchedCss.color || undefined,
268
+ borderRadius: parseIntVal(matchedCss['border-radius'], type === 'button' ? 8 : 0),
269
+ fontSize: parseIntVal(matchedCss['font-size'], type === 'heading' ? 24 : 14),
270
+ fontWeight: matchedCss['font-weight'] || (type === 'heading' || type === 'button' ? 'bold' : undefined),
271
+ border: matchedCss.border || undefined,
272
+ zIndex: parseIntVal(matchedCss['z-index'], autoIndex + 1),
273
+ },
274
+ content: {
275
+ title: cleanText || undefined,
276
+ text: cleanText || undefined,
277
+ label: cleanText || undefined,
278
+ headline: cleanText || undefined,
279
+ brandName: type === 'navbar' ? (cleanText || 'Brand') : undefined,
280
+ placeholder: placeholderMatch ? placeholderMatch[1] : undefined,
281
+ }
282
+ });
283
+
284
+ autoIndex++;
285
+ }
286
+ }
287
+
288
+ const cleanBase = path.basename(filename).replace(/[^a-zA-Z0-9]/g, '_');
289
+ const pageId = `page_${cleanBase || 'main'}`;
290
+ const projectId = `proj_${cleanBase || 'main'}`;
291
+
292
+ return {
293
+ id: projectId,
294
+ name: pageTitle,
295
+ version: '1.0',
296
+ backgroundColor,
297
+ pages: [
298
+ {
299
+ id: pageId,
300
+ name: pageTitle,
301
+ backgroundColor,
302
+ settings: {
303
+ backgroundColor,
304
+ width: 1440,
305
+ height: 900,
306
+ minHeight: 900,
307
+ },
308
+ elements
309
+ }
310
+ ]
311
+ };
312
+ }
313
+
314
+ function createEmptyProject(filename = 'Imported Project') {
315
+ const cleanBase = path.basename(filename).replace(/[^a-zA-Z0-9]/g, '_');
316
+ return {
317
+ id: `proj_${cleanBase || 'main'}`,
318
+ name: cleanBase || 'Imported Project',
319
+ version: '1.0',
320
+ backgroundColor: '#0f0f12',
321
+ pages: [
322
+ {
323
+ id: `page_${cleanBase || 'main'}`,
324
+ name: 'Page 1',
325
+ backgroundColor: '#0f0f12',
326
+ settings: {
327
+ backgroundColor: '#0f0f12',
328
+ width: 1440,
329
+ height: 900,
330
+ minHeight: 900,
331
+ },
332
+ elements: []
333
+ }
334
+ ]
335
+ };
336
+ }
337
+