@atomm-developer/generator-workbench 0.1.0 → 0.1.2

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.es.js CHANGED
@@ -21,6 +21,22 @@ function createAuthController(args) {
21
21
  }
22
22
  function createExportController(args) {
23
23
  const { sdk, runtime, config, element } = args;
24
+ async function ensureLoggedIn() {
25
+ if (sdk.auth.getStatus().isLogin) {
26
+ return;
27
+ }
28
+ await sdk.auth.login();
29
+ if (!sdk.auth.getStatus().isLogin) {
30
+ throw new Error("[generator-workbench] login is required before export");
31
+ }
32
+ }
33
+ async function consumeBilling() {
34
+ var _a;
35
+ if (!((_a = sdk.billing) == null ? void 0 : _a.consume)) {
36
+ return;
37
+ }
38
+ await sdk.billing.consume();
39
+ }
24
40
  return {
25
41
  async exportSvg() {
26
42
  var _a;
@@ -29,6 +45,8 @@ function createExportController(args) {
29
45
  runtime,
30
46
  element
31
47
  }));
48
+ await ensureLoggedIn();
49
+ await consumeBilling();
32
50
  return sdk.export.download({ format: "svg" });
33
51
  },
34
52
  async openInStudio() {
@@ -38,6 +56,8 @@ function createExportController(args) {
38
56
  runtime,
39
57
  element
40
58
  }));
59
+ await ensureLoggedIn();
60
+ await consumeBilling();
41
61
  return sdk.export.openInStudio({ format: "svg" });
42
62
  }
43
63
  };
@@ -86,12 +106,35 @@ function assert(condition, message) {
86
106
  throw new Error(message);
87
107
  }
88
108
  }
89
- function collectPanelFieldPaths(panelSchema) {
90
- return panelSchema.groups.flatMap(
91
- (group) => group.fields.map((field) => {
92
- var _a, _b;
93
- return (_b = (_a = field.bind) == null ? void 0 : _a.path) == null ? void 0 : _b.trim();
94
- }).filter((path) => Boolean(path))
109
+ function collectPanelFieldGroups(panelSchema, allowedFieldPaths) {
110
+ const allowedPathSet = (allowedFieldPaths == null ? void 0 : allowedFieldPaths.length) ? new Set(allowedFieldPaths) : null;
111
+ return panelSchema.groups.map((group) => {
112
+ var _a;
113
+ const fields = group.fields.map((field) => {
114
+ var _a2, _b, _c;
115
+ const path = (_b = (_a2 = field.bind) == null ? void 0 : _a2.path) == null ? void 0 : _b.trim();
116
+ if (!path) {
117
+ return null;
118
+ }
119
+ if (allowedPathSet && !allowedPathSet.has(path)) {
120
+ return null;
121
+ }
122
+ return {
123
+ id: field.id,
124
+ label: ((_c = field.label) == null ? void 0 : _c.trim()) || field.id || path,
125
+ path
126
+ };
127
+ }).filter((field) => Boolean(field));
128
+ return {
129
+ id: group.id,
130
+ title: ((_a = group.title) == null ? void 0 : _a.trim()) || group.id,
131
+ fields
132
+ };
133
+ }).filter((group) => group.fields.length > 0);
134
+ }
135
+ function collectPanelFieldPaths(panelSchema, allowedFieldPaths) {
136
+ return collectPanelFieldGroups(panelSchema, allowedFieldPaths).flatMap(
137
+ (group) => group.fields.map((field) => field.path)
95
138
  );
96
139
  }
97
140
  function resolveGeneratorId(panelSchema, state) {
@@ -109,6 +152,27 @@ function resolveGeneratorId(panelSchema, state) {
109
152
  }
110
153
  function createTemplateController(args) {
111
154
  const { sdk, runtime, config } = args;
155
+ function prepareTemplateExport() {
156
+ var _a;
157
+ const state = runtime.getState();
158
+ const panelSchema = runtime.getPanelSchema();
159
+ const configuredFieldPaths = (_a = config.getTemplateFieldPaths) == null ? void 0 : _a.call(config, panelSchema);
160
+ const fieldGroups = collectPanelFieldGroups(panelSchema, configuredFieldPaths);
161
+ const defaultSelectedFieldPaths = fieldGroups.flatMap(
162
+ (group) => group.fields.map((field) => field.path)
163
+ );
164
+ assert(
165
+ defaultSelectedFieldPaths.length > 0,
166
+ "[generator-workbench] exportTemplate requires at least one panel field path"
167
+ );
168
+ return {
169
+ generatorId: resolveGeneratorId(panelSchema, state),
170
+ state,
171
+ panelSchema,
172
+ fieldGroups,
173
+ selectedFieldPaths: defaultSelectedFieldPaths
174
+ };
175
+ }
112
176
  return {
113
177
  async importTemplate(file) {
114
178
  const text = await file.text();
@@ -124,21 +188,21 @@ function createTemplateController(args) {
124
188
  panelFilter: capturedPanelFilter
125
189
  };
126
190
  },
127
- async exportTemplate() {
128
- var _a, _b;
129
- const state = runtime.getState();
130
- const panelSchema = runtime.getPanelSchema();
131
- const selectedFieldPaths = ((_a = config.getTemplateFieldPaths) == null ? void 0 : _a.call(config, panelSchema)) ?? collectPanelFieldPaths(panelSchema);
191
+ prepareTemplateExport,
192
+ async exportTemplate(selectedFieldPaths) {
193
+ var _a;
194
+ const prepared = prepareTemplateExport();
195
+ const finalSelectedFieldPaths = (selectedFieldPaths == null ? void 0 : selectedFieldPaths.length) ? collectPanelFieldPaths(prepared.panelSchema, selectedFieldPaths) : prepared.selectedFieldPaths;
132
196
  assert(
133
- selectedFieldPaths.length > 0,
197
+ finalSelectedFieldPaths.length > 0,
134
198
  "[generator-workbench] exportTemplate requires at least one panel field path"
135
199
  );
136
200
  const template = sdk.template.build({
137
- generatorId: resolveGeneratorId(panelSchema, state),
138
- state,
139
- panelSchema,
140
- selectedFieldPaths,
141
- templateMeta: (_b = config.getTemplateMeta) == null ? void 0 : _b.call(config)
201
+ generatorId: prepared.generatorId,
202
+ state: prepared.state,
203
+ panelSchema: prepared.panelSchema,
204
+ selectedFieldPaths: finalSelectedFieldPaths,
205
+ templateMeta: (_a = config.getTemplateMeta) == null ? void 0 : _a.call(config)
142
206
  });
143
207
  sdk.template.download(template);
144
208
  return { template };
@@ -151,6 +215,8 @@ function createInitialWorkbenchState() {
151
215
  isLogin: false,
152
216
  userInfo: null
153
217
  },
218
+ creditsBalance: 0,
219
+ billingUsage: null,
154
220
  busy: {
155
221
  login: false,
156
222
  importTemplate: false,
@@ -173,6 +239,26 @@ function dispatchWorkbenchEvent(target, name, detail) {
173
239
  })
174
240
  );
175
241
  }
242
+ function ensureBrowserProcessEnv() {
243
+ const browserGlobal = globalThis;
244
+ if (!browserGlobal.process) {
245
+ browserGlobal.process = {
246
+ env: {
247
+ NODE_ENV: "production"
248
+ }
249
+ };
250
+ return;
251
+ }
252
+ if (!browserGlobal.process.env) {
253
+ browserGlobal.process.env = {
254
+ NODE_ENV: "production"
255
+ };
256
+ return;
257
+ }
258
+ if (!browserGlobal.process.env.NODE_ENV) {
259
+ browserGlobal.process.env.NODE_ENV = "production";
260
+ }
261
+ }
176
262
  function queryRequired(root, selector) {
177
263
  const node = root.querySelector(selector);
178
264
  if (!node) {
@@ -185,267 +271,853 @@ function queryRequired(root, selector) {
185
271
  function collectWorkbenchRefs(root) {
186
272
  return {
187
273
  root,
188
- topbar: queryRequired(root, ".topbar"),
189
274
  workspace: queryRequired(root, '[data-role="workspace"]'),
190
- logoArea: queryRequired(root, '[data-role="logo-area"]'),
191
- importTemplateBtn: queryRequired(root, '[data-role="import-template"]'),
192
- exportTemplateBtn: queryRequired(root, '[data-role="export-template"]'),
193
- loginBtn: queryRequired(root, '[data-role="login"]'),
194
- avatarButton: queryRequired(root, '[data-role="avatar-button"]'),
195
- avatarImage: queryRequired(root, '[data-role="avatar-image"]'),
196
- avatarMenu: queryRequired(root, '[data-role="avatar-menu"]'),
197
- logoutBtn: queryRequired(root, '[data-role="logout"]'),
198
- panelSidebar: queryRequired(root, '[data-role="panel-sidebar"]'),
199
- panelHost: queryRequired(root, '[data-role="panel-host"]'),
200
- panelActions: queryRequired(root, '[data-role="panel-actions"]'),
275
+ logoArea: root.querySelector('[data-role="logo-area"]'),
201
276
  canvasHost: queryRequired(root, '[data-role="canvas-host"]'),
202
- fabTrigger: queryRequired(root, '[data-role="fab-trigger"]'),
203
- fabMenu: queryRequired(root, '[data-role="fab-menu"]'),
204
- exportSvgBtn: queryRequired(root, '[data-role="export-svg"]'),
205
- openInStudioBtn: queryRequired(root, '[data-role="open-in-studio"]'),
277
+ panelHost: queryRequired(root, '[data-role="panel-host"]'),
206
278
  templateFileInput: queryRequired(root, '[data-role="template-file-input"]')
207
279
  };
208
280
  }
209
- function applyWorkbenchConfig(refs, config) {
210
- refs.logoArea.textContent = config.logoText || config.title;
211
- refs.workspace.classList.toggle("panel-left", config.panelTarget === "left");
212
- refs.workspace.classList.toggle("panel-right", config.panelTarget !== "left");
213
- refs.importTemplateBtn.hidden = config.templateEnabled === false;
214
- refs.exportTemplateBtn.hidden = config.templateEnabled === false;
215
- refs.exportSvgBtn.hidden = config.exportEnabled === false;
216
- refs.openInStudioBtn.hidden = config.studioEnabled === false;
217
- }
218
- const WORKBENCH_TEMPLATE = `
219
- <style>
220
- :host {
221
- display: block;
222
- min-height: 100%;
223
- color: #111827;
224
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
225
- }
281
+ const ATOMM_UI_CDN_CSS_URL = "https://minio-download.makeblock.com/resource/atomm-ui-umd/dist-browser/atomm-ui.min.css";
282
+ const CREDIT_ICON_DATA_URI = "data:image/svg+xml,%3csvg%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20xmlns='http://www.w3.org/2000/svg'%3e%3ccircle%20cx='12'%20cy='12'%20r='9'%20fill='url(%23paint0_linear_503_69253)'/%3e%3cg%20filter='url(%23filter0_d_503_69253)'%3e%3crect%20x='7.75781'%20y='12'%20width='6'%20height='6'%20rx='1'%20transform='rotate(-45%207.75781%2012)'%20fill='url(%23paint1_linear_503_69253)'/%3e%3c/g%3e%3cdefs%3e%3cfilter%20id='filter0_d_503_69253'%20x='2.17188'%20y='4.17188'%20width='19.6572'%20height='19.6562'%20filterUnits='userSpaceOnUse'%20color-interpolation-filters='sRGB'%3e%3cfeFlood%20flood-opacity='0'%20result='BackgroundImageFix'/%3e%3cfeColorMatrix%20in='SourceAlpha'%20type='matrix'%20values='0%200%200%200%200%200%200%200%200%200%200%200%200%200%200%200%200%200%20127%200'%20result='hardAlpha'/%3e%3cfeOffset%20dy='2'/%3e%3cfeGaussianBlur%20stdDeviation='3'/%3e%3cfeComposite%20in2='hardAlpha'%20operator='out'/%3e%3cfeColorMatrix%20type='matrix'%20values='0%200%200%200%201%200%200%200%200%200.206957%200%200%200%200%200.0670085%200%200%200%201%200'/%3e%3cfeBlend%20mode='normal'%20in2='BackgroundImageFix'%20result='effect1_dropShadow_503_69253'/%3e%3cfeBlend%20mode='normal'%20in='SourceGraphic'%20in2='effect1_dropShadow_503_69253'%20result='shape'/%3e%3c/filter%3e%3clinearGradient%20id='paint0_linear_503_69253'%20x1='12'%20y1='3'%20x2='12'%20y2='21'%20gradientUnits='userSpaceOnUse'%3e%3cstop%20stop-color='%23FFA346'/%3e%3cstop%20offset='1'%20stop-color='%23FF7C23'/%3e%3c/linearGradient%3e%3clinearGradient%20id='paint1_linear_503_69253'%20x1='13.2327'%20y1='12.5252'%20x2='8.63652'%20y2='18.5356'%20gradientUnits='userSpaceOnUse'%3e%3cstop%20stop-color='white'/%3e%3cstop%20offset='1'%20stop-color='%23FFD3B5'/%3e%3c/linearGradient%3e%3c/defs%3e%3c/svg%3e";
283
+ const EXPORT_ICON_DATA_URI = "data:image/svg+xml,%3Csvg%20viewBox%3D'0%200%201088%201024'%20xmlns%3D'http%3A//www.w3.org/2000/svg'%3E%3Cpath%20d%3D'M416%20128v85.312h-128A42.688%2042.688%200%200%200%20245.312%20256v512c0%2023.552%2019.136%2042.688%2042.688%2042.688h512a42.688%2042.688%200%200%200%2042.688-42.688V597.312H928V768a128%20128%200%200%201-128%20128h-512a128%20128%200%200%201-128-128V256a128%20128%200%200%201%20128-128h128z'%20fill%3D'%23ffffff'/%3E%3Cpath%20d%3D'M723.52%20520.832L924.352%20320l-200.832-200.832-60.352%2060.352%2097.856%2097.792h-67.712A298.688%20298.688%200%200%200%20394.688%20576v85.312H480V576a213.312%20213.312%200%200%201%20213.312-213.312h67.712l-97.856%2097.792%2060.352%2060.352z'%20fill%3D'%23ffffff'/%3E%3C/svg%3E";
284
+ const WORKBENCH_SHELL_STYLES = `
285
+ :host {
286
+ display: block;
287
+ height: 100%;
288
+ color: #111827;
289
+ font-family: Inter, "Montserrat", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
290
+ --bg-soft: #f3f4f6;
291
+ --border-default: #e5e7eb;
292
+ --text-primary: #111827;
293
+ --text-secondary: #4b5563;
294
+ --text-muted: #9ca3af;
295
+ }
226
296
 
227
- * {
228
- box-sizing: border-box;
229
- }
297
+ * {
298
+ box-sizing: border-box;
299
+ }
230
300
 
231
- .shell {
232
- min-height: 100%;
233
- display: flex;
234
- flex-direction: column;
235
- background: #f5f7fb;
236
- }
301
+ [data-workbench-vue-root] {
302
+ height: 100%;
303
+ }
237
304
 
238
- .topbar {
239
- height: 56px;
240
- display: flex;
241
- align-items: center;
242
- justify-content: space-between;
243
- gap: 16px;
244
- padding: 0 16px;
245
- background: #fff;
246
- border-bottom: 1px solid #e5e7eb;
247
- }
305
+ .shell {
306
+ height: 100%;
307
+ display: flex;
308
+ flex-direction: column;
309
+ background:
310
+ radial-gradient(rgba(17, 24, 39, 0.06) 1px, transparent 1px),
311
+ linear-gradient(180deg, rgba(255, 255, 255, 0.7), rgba(249, 250, 251, 0.96));
312
+ background-size: 20px 20px, auto;
313
+ }
248
314
 
249
- .logo-area {
250
- font-weight: 600;
251
- }
315
+ .topbar,
316
+ .app-topbar {
317
+ height: 64px;
318
+ display: flex;
319
+ align-items: center;
320
+ justify-content: space-between;
321
+ gap: 12px;
322
+ padding: 0 24px;
323
+ background: rgba(255, 255, 255, 0.92);
324
+ border-bottom: 1px solid var(--border-default);
325
+ backdrop-filter: blur(12px);
326
+ position: relative;
327
+ z-index: 20;
328
+ }
252
329
 
253
- .topbar-actions {
254
- display: flex;
255
- align-items: center;
256
- gap: 8px;
257
- }
330
+ .logo-area,
331
+ .app-topbar-main {
332
+ display: flex;
333
+ align-items: center;
334
+ gap: 18px;
335
+ min-width: 0;
336
+ flex: 1;
337
+ }
258
338
 
259
- .workspace {
260
- flex: 1;
261
- min-height: 0;
262
- display: grid;
263
- grid-template-columns: minmax(0, 1fr) 320px;
264
- }
339
+ .app-topbar-nav {
340
+ display: flex;
341
+ align-items: center;
342
+ gap: 10px;
343
+ flex-wrap: wrap;
344
+ }
265
345
 
266
- .workspace.panel-left {
267
- grid-template-columns: 320px minmax(0, 1fr);
268
- }
346
+ .workspace {
347
+ flex: 1;
348
+ min-height: 0;
349
+ display: grid;
350
+ grid-template-columns: minmax(0, 1fr) 320px;
351
+ }
269
352
 
270
- .workspace.panel-right .canvas-host {
271
- grid-column: 1;
272
- }
353
+ .workspace.panel-left {
354
+ grid-template-columns: 320px minmax(0, 1fr);
355
+ }
273
356
 
274
- .workspace.panel-right .panel-sidebar {
275
- grid-column: 2;
276
- }
357
+ .workspace.panel-right .canvas-host {
358
+ grid-column: 1;
359
+ }
277
360
 
278
- .workspace.panel-left .panel-sidebar {
279
- grid-column: 1;
280
- }
361
+ .workspace.panel-right .panel-sidebar {
362
+ grid-column: 2;
363
+ }
281
364
 
282
- .workspace.panel-left .canvas-host {
283
- grid-column: 2;
284
- }
365
+ .workspace.panel-left .panel-sidebar {
366
+ grid-column: 1;
367
+ }
285
368
 
286
- .panel-sidebar {
287
- min-height: 0;
288
- display: flex;
289
- flex-direction: column;
290
- background: #fff;
291
- }
369
+ .workspace.panel-left .canvas-host {
370
+ grid-column: 2;
371
+ }
292
372
 
293
- .panel-host {
294
- flex: 1;
295
- min-height: 0;
296
- overflow: auto;
297
- }
373
+ .panel-sidebar {
374
+ min-height: 0;
375
+ display: flex;
376
+ flex-direction: column;
377
+ background: #ffffff;
378
+ }
298
379
 
299
- .canvas-host {
300
- position: relative;
301
- min-height: 480px;
302
- padding: 24px;
303
- display: grid;
304
- place-items: center;
305
- }
380
+ .panel-host {
381
+ flex: 1;
382
+ min-height: 0;
383
+ overflow: auto;
384
+ }
306
385
 
307
- button {
308
- border: 0;
309
- border-radius: 10px;
310
- padding: 10px 14px;
311
- cursor: pointer;
312
- background: #111827;
313
- color: #fff;
314
- font: inherit;
315
- }
386
+ .canvas-host {
387
+ position: relative;
388
+ min-height: 480px;
389
+ display: grid;
390
+ place-items: center;
391
+ }
316
392
 
317
- button.secondary {
318
- background: #e5e7eb;
319
- color: #111827;
320
- }
393
+ .brand-logo {
394
+ height: 32px;
395
+ width: 92px;
396
+ display: block;
397
+ cursor: pointer;
398
+ flex: 0 0 auto;
399
+ }
321
400
 
322
- button:focus-visible {
323
- outline: 2px solid #4338ca;
324
- outline-offset: 2px;
325
- }
401
+ .app-topbar-auth {
402
+ display: flex;
403
+ align-items: center;
404
+ justify-content: flex-end;
405
+ gap: 12px;
406
+ flex: 0 0 auto;
407
+ }
326
408
 
327
- .auth-area {
328
- position: relative;
329
- display: flex;
330
- align-items: center;
331
- }
409
+ .topbar-credits-badge {
410
+ display: inline-flex;
411
+ align-items: center;
412
+ gap: 4px;
413
+ padding: 0;
414
+ color: #2d3541;
415
+ font-size: 14px;
416
+ font-weight: 500;
417
+ line-height: 1;
418
+ white-space: nowrap;
419
+ cursor: default;
420
+ }
332
421
 
333
- .avatar-button {
334
- display: none;
335
- align-items: center;
336
- gap: 8px;
337
- background: transparent;
338
- color: #111827;
339
- padding: 0;
340
- }
422
+ .topbar-credits-value {
423
+ font-variant-numeric: tabular-nums;
424
+ }
341
425
 
342
- .avatar-image {
343
- width: 32px;
344
- height: 32px;
345
- border-radius: 999px;
346
- object-fit: cover;
347
- background: #e5e7eb;
348
- }
426
+ .credits-token-icon {
427
+ width: 20px;
428
+ height: 20px;
429
+ flex: 0 0 16px;
430
+ user-select: none;
431
+ -webkit-user-drag: none;
432
+ }
349
433
 
350
- .avatar-menu {
351
- position: absolute;
352
- right: 0;
353
- top: calc(100% + 8px);
354
- min-width: 140px;
355
- padding: 8px;
356
- display: none;
357
- background: #fff;
358
- border: 1px solid #e5e7eb;
359
- border-radius: 12px;
360
- box-shadow: 0 12px 32px rgba(15, 23, 42, 0.08);
361
- }
434
+ .credits-token-icon--sm {
435
+ width: 16px;
436
+ height: 16px;
437
+ flex-basis: 16px;
438
+ }
362
439
 
363
- .avatar-menu.is-open,
364
- .fab-menu.is-open {
365
- display: block;
366
- }
440
+ .auth-avatar-trigger {
441
+ display: inline-flex;
442
+ align-items: center;
443
+ gap: 6px;
444
+ padding: 4px 8px 4px 4px;
445
+ border-radius: 999px;
446
+ cursor: pointer;
447
+ transition: background 150ms ease;
448
+ }
367
449
 
368
- .avatar-menu button,
369
- .fab-menu button {
370
- width: 100%;
371
- }
450
+ .auth-avatar-trigger:hover {
451
+ background: var(--bg-soft);
452
+ }
372
453
 
373
- .panel-actions {
374
- padding: 12px;
375
- border-top: 1px solid #e5e7eb;
376
- }
454
+ .auth-avatar {
455
+ width: 40px;
456
+ height: 40px;
457
+ border-radius: 50%;
458
+ display: grid;
459
+ place-items: center;
460
+ overflow: hidden;
461
+ flex: 0 0 auto;
462
+ background: #f3f4f6;
463
+ color: var(--text-secondary);
464
+ font-size: 14px;
465
+ font-weight: 600;
466
+ }
377
467
 
378
- .workspace.panel-right .panel-sidebar {
379
- border-left: 1px solid #e5e7eb;
380
- }
468
+ .auth-avatar--lg {
469
+ width: 40px;
470
+ height: 40px;
471
+ font-size: 14px;
472
+ }
381
473
 
382
- .workspace.panel-left .panel-sidebar {
383
- border-right: 1px solid #e5e7eb;
384
- }
474
+ .auth-avatar img {
475
+ width: 100%;
476
+ height: 100%;
477
+ object-fit: cover;
478
+ display: block;
479
+ }
385
480
 
386
- .fab {
387
- display: flex;
388
- flex-direction: column;
389
- align-items: stretch;
390
- gap: 8px;
391
- width: 100%;
392
- }
481
+ .auth-popover-body {
482
+ min-width: 200px;
483
+ padding: 4px 0;
484
+ }
393
485
 
394
- .fab-menu {
395
- width: 100%;
396
- display: none;
397
- padding: 8px;
398
- background: #fff;
399
- border: 1px solid #e5e7eb;
400
- border-radius: 12px;
401
- box-shadow: 0 16px 40px rgba(15, 23, 42, 0.12);
402
- }
486
+ .auth-popover-header {
487
+ display: flex;
488
+ align-items: center;
489
+ gap: 12px;
490
+ padding: 12px 16px;
491
+ }
403
492
 
404
- .fab-menu button + button {
405
- margin-top: 8px;
406
- }
407
- </style>
493
+ .auth-popover-user-text {
494
+ min-width: 0;
495
+ flex: 1;
496
+ }
497
+
498
+ .auth-popover-name {
499
+ display: block;
500
+ font-size: 14px;
501
+ font-weight: 600;
502
+ color: var(--text-primary);
503
+ overflow: hidden;
504
+ text-overflow: ellipsis;
505
+ white-space: nowrap;
506
+ }
507
+
508
+ .auth-popover-sub {
509
+ display: block;
510
+ margin-top: 2px;
511
+ font-size: 12px;
512
+ color: var(--text-secondary);
513
+ overflow: hidden;
514
+ text-overflow: ellipsis;
515
+ white-space: nowrap;
516
+ }
517
+
518
+ .auth-popover-divider {
519
+ height: 1px;
520
+ background: var(--border-default);
521
+ margin: 4px 0;
522
+ }
523
+
524
+ .auth-popover-action {
525
+ display: flex;
526
+ align-items: center;
527
+ gap: 8px;
528
+ padding: 8px 16px;
529
+ font-size: 13px;
530
+ color: var(--text-secondary);
531
+ cursor: pointer;
532
+ transition: all 150ms ease;
533
+ }
534
+
535
+ .auth-popover-action:hover {
536
+ background: var(--bg-soft);
537
+ color: var(--text-primary);
538
+ }
539
+
540
+ .auth-hover-card {
541
+ position: relative;
542
+ display: inline-flex;
543
+ align-items: center;
544
+ }
545
+
546
+ .auth-hover-panel {
547
+ position: absolute;
548
+ top: calc(100% + 8px);
549
+ right: 0;
550
+ z-index: 40;
551
+ min-width: 200px;
552
+ background: #fff;
553
+ border: 1px solid var(--border-default);
554
+ border-radius: 12px;
555
+ box-shadow: 0 8px 28px rgba(17, 24, 39, 0.12);
556
+ opacity: 0;
557
+ transform: translateY(4px);
558
+ pointer-events: none;
559
+ transition: opacity 150ms ease, transform 150ms ease;
560
+ }
561
+
562
+ .auth-hover-card:hover .auth-hover-panel,
563
+ .auth-hover-card:focus-within .auth-hover-panel {
564
+ opacity: 1;
565
+ transform: translateY(0);
566
+ pointer-events: auto;
567
+ }
568
+
569
+ .panel-actions,
570
+ #sidebar-footer {
571
+ flex-shrink: 0;
572
+ padding: 14px 24px;
573
+ border-top: 1px solid var(--border-default);
574
+ background: #ffffff;
575
+ display: flex;
576
+ align-items: center;
577
+ }
578
+
579
+ .sidebar-footer-export {
580
+ width: 100%;
581
+ }
582
+
583
+ .sidebar-footer-export .sidebar-export-trigger-btn,
584
+ .sidebar-footer-export .sidebar-export-trigger-btn-wrap,
585
+ .sidebar-export-trigger-btn-wrap {
586
+ width: 100%;
587
+ }
588
+
589
+ .sidebar-export-dropdown-menu {
590
+ width: 268px;
591
+ }
592
+
593
+ .sidebar-export-trigger-btn img {
594
+ margin-right: 4px;
595
+ }
596
+
597
+ .export-credits-hint {
598
+ display: inline-flex;
599
+ align-items: center;
600
+ justify-content: center;
601
+ gap: 2px;
602
+ padding: 0;
603
+ font-size: 12px;
604
+ font-weight: 600;
605
+ line-height: 1;
606
+ color: #ff7c23;
607
+ margin-left: 10px;
608
+ white-space: nowrap;
609
+ }
610
+
611
+ .export-credits-hint-value {
612
+ color: white;
613
+ font-size: 13px;
614
+ font-weight: 700;
615
+ font-variant-numeric: tabular-nums;
616
+ }
617
+
618
+ .export-overlay-menu,
619
+ .fab-menu-content {
620
+ padding: 4px;
621
+ }
622
+
623
+ .export-overlay-item {
624
+ display: flex;
625
+ align-items: center;
626
+ gap: 8px;
627
+ padding: 8px 12px;
628
+ border-radius: 6px;
629
+ font-size: 14px;
630
+ color: var(--text-primary);
631
+ cursor: pointer;
632
+ transition: background 120ms ease;
633
+ user-select: none;
634
+ }
635
+
636
+ .export-overlay-item:hover {
637
+ background: var(--bg-soft);
638
+ }
639
+
640
+ .export-overlay-item-icon {
641
+ width: 16px;
642
+ height: 16px;
643
+ border-radius: 4px;
644
+ background: #cbd5e1;
645
+ display: inline-block;
646
+ flex: 0 0 auto;
647
+ }
648
+
649
+ .export-overlay-item-icon--svg {
650
+ background: linear-gradient(180deg, #94a3b8, #64748b);
651
+ }
652
+
653
+ .export-overlay-item-icon--studio {
654
+ background: linear-gradient(180deg, #cbd5e1, #94a3b8);
655
+ }
656
+
657
+ .workspace.panel-right .panel-sidebar {
658
+ border-left: 1px solid var(--border-default);
659
+ }
660
+
661
+ .workspace.panel-left .panel-sidebar {
662
+ border-right: 1px solid var(--border-default);
663
+ }
664
+
665
+ .template-export-modal {
666
+ display: grid;
667
+ gap: 16px;
668
+ }
669
+
670
+
671
+
672
+ .template-export-groups {
673
+ display: grid;
674
+ gap: 12px;
675
+ max-height: 360px;
676
+ overflow: auto;
677
+ }
678
+
679
+ .template-export-group {
680
+ display: grid;
681
+ gap: 10px;
682
+ padding: 14px 16px;
683
+ border: 1px solid var(--border-default);
684
+ border-radius: 12px;
685
+ background: #ffffff;
686
+ }
687
+
688
+ .template-export-group-title {
689
+ font-size: 13px;
690
+ font-weight: 700;
691
+ color: var(--text-primary);
692
+ }
693
+
694
+ .template-export-field-list {
695
+ display: grid;
696
+ gap: 10px;
697
+ }
698
+
699
+ .template-export-field {
700
+ display: grid;
701
+ gap: 4px;
702
+ }
703
+
704
+ .template-export-field-path {
705
+ padding-left: 28px;
706
+ font-size: 12px;
707
+ color: var(--text-muted);
708
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
709
+ }
710
+
711
+ .template-export-footer {
712
+ display: flex;
713
+ justify-content: flex-end;
714
+ gap: 12px;
715
+ padding: 8px 24px;
716
+ }
717
+ `;
718
+ const WORKBENCH_VUE_TEMPLATE = `
408
719
  <div class="shell">
409
- <header class="topbar">
410
- <div class="logo-area" data-role="logo-area"></div>
411
- <div class="topbar-actions">
412
- <button class="secondary" data-role="import-template">导入模板</button>
413
- <button class="secondary" data-role="export-template">生成模板</button>
414
- <div class="auth-area" data-role="auth-area">
415
- <button data-role="login">登录</button>
416
- <button class="avatar-button" data-role="avatar-button" aria-expanded="false">
417
- <img class="avatar-image" data-role="avatar-image" alt="User avatar" />
418
- </button>
419
- <div class="avatar-menu" data-role="avatar-menu">
420
- <button class="secondary" data-role="logout">退出登录</button>
720
+ <header v-if="state.shellMode !== 'template'" class="topbar app-topbar">
721
+ <div class="logo-area app-topbar-main" data-role="logo-area">
722
+ <img
723
+ :src="state.logoSrc"
724
+ :alt="state.logoText || 'Atomm'"
725
+ class="brand-logo"
726
+ data-role="brand-logo"
727
+ draggable="false"
728
+ />
729
+ <div class="app-topbar-nav">
730
+ <xt-button
731
+ v-show="state.templateEnabled"
732
+ type="secondary"
733
+ size="small"
734
+ data-role="import-template"
735
+ @click="callbacks.onImportTemplate()"
736
+ >导入模板</xt-button>
737
+ <xt-button
738
+ v-show="state.templateEnabled"
739
+ type="secondary"
740
+ size="small"
741
+ data-role="export-template"
742
+ @click="callbacks.onExportTemplate()"
743
+ >生成模板</xt-button>
744
+ </div>
745
+ </div>
746
+ <div class="app-topbar-auth">
747
+ <div
748
+ v-if="state.isLogin"
749
+ class="topbar-credits-badge"
750
+ data-role="topbar-credits"
751
+ title="Remaining credits"
752
+ >
753
+ <img src="${CREDIT_ICON_DATA_URI}" alt="" class="credits-token-icon" draggable="false" />
754
+ <span class="topbar-credits-value" data-role="topbar-credits-value">{{ state.creditsBalance }}</span>
755
+ </div>
756
+
757
+ <div
758
+ v-if="state.isLogin && state.avatarMenuTrigger === 'hover'"
759
+ class="auth-hover-card"
760
+ >
761
+ <div class="auth-avatar-trigger" data-role="avatar-button" tabindex="0">
762
+ <div class="auth-avatar">
763
+ <img
764
+ v-if="state.avatarSrc"
765
+ :src="state.avatarSrc"
766
+ alt="Avatar"
767
+ data-role="avatar-image"
768
+ />
769
+ <span v-else data-role="avatar-image">{{ state.avatarText }}</span>
770
+ </div>
771
+ </div>
772
+ <div class="auth-hover-panel">
773
+ <div class="auth-popover-body" data-role="avatar-menu">
774
+ <div class="auth-popover-header">
775
+ <div class="auth-avatar auth-avatar--lg">
776
+ <img v-if="state.avatarSrc" :src="state.avatarSrc" alt="Avatar" />
777
+ <span v-else>{{ state.avatarText }}</span>
778
+ </div>
779
+ <div class="auth-popover-user-text">
780
+ <span class="auth-popover-name">{{ state.authDisplayName }}</span>
781
+ <span class="auth-popover-sub">{{ state.authSubline }}</span>
782
+ </div>
783
+ </div>
784
+ <div class="auth-popover-divider"></div>
785
+ <div class="auth-popover-action" data-role="logout" @click="callbacks.onLogout()">
786
+ Logout
787
+ </div>
788
+ </div>
421
789
  </div>
422
790
  </div>
791
+
792
+ <xt-dropdown-menu
793
+ v-else-if="state.isLogin"
794
+ trigger="click"
795
+ :portal-disabled="true"
796
+ placement="bottomRight"
797
+ >
798
+ <template #trigger>
799
+ <div class="auth-avatar-trigger" data-role="avatar-button">
800
+ <div class="auth-avatar">
801
+ <img
802
+ v-if="state.avatarSrc"
803
+ :src="state.avatarSrc"
804
+ alt="Avatar"
805
+ data-role="avatar-image"
806
+ />
807
+ <span v-else data-role="avatar-image">{{ state.avatarText }}</span>
808
+ </div>
809
+ </div>
810
+ </template>
811
+ <template #overlay>
812
+ <div class="auth-popover-body" data-role="avatar-menu">
813
+ <div class="auth-popover-header">
814
+ <div class="auth-avatar auth-avatar--lg">
815
+ <img v-if="state.avatarSrc" :src="state.avatarSrc" alt="Avatar" />
816
+ <span v-else>{{ state.avatarText }}</span>
817
+ </div>
818
+ <div class="auth-popover-user-text">
819
+ <span class="auth-popover-name">{{ state.authDisplayName }}</span>
820
+ <span class="auth-popover-sub">{{ state.authSubline }}</span>
821
+ </div>
822
+ </div>
823
+ <div class="auth-popover-divider"></div>
824
+ <div class="auth-popover-action" data-role="logout" @click="callbacks.onLogout()">
825
+ Logout
826
+ </div>
827
+ </div>
828
+ </template>
829
+ </xt-dropdown-menu>
830
+
831
+ <xt-button
832
+ size="small"
833
+ type="secondary"
834
+ data-role="login"
835
+ :loading="state.loginLoading"
836
+ v-if="!state.isLogin"
837
+ @click="callbacks.onLogin()"
838
+ >Login</xt-button>
423
839
  </div>
424
840
  </header>
425
- <div class="workspace" data-role="workspace">
841
+
842
+ <div
843
+ class="workspace"
844
+ :class="state.panelTarget === 'left' ? 'panel-left' : 'panel-right'"
845
+ data-role="workspace"
846
+ >
426
847
  <main class="canvas-host" data-role="canvas-host"></main>
427
848
  <aside class="panel-sidebar" data-role="panel-sidebar">
428
849
  <div class="panel-host" data-role="panel-host"></div>
429
- <div class="panel-actions" data-role="panel-actions">
430
- <div class="fab">
431
- <div class="fab-menu" data-role="fab-menu">
432
- <button class="secondary" data-role="open-in-studio">导入 Studio</button>
433
- <button class="secondary" data-role="export-svg">导出 SVG</button>
434
- </div>
435
- <button data-role="fab-trigger" aria-expanded="false">导出</button>
436
- </div>
850
+ <div id="sidebar-footer" class="sidebar-footer-export panel-actions" data-role="panel-actions">
851
+ <xt-dropdown-menu
852
+ trigger="click"
853
+ :portal-disabled="true"
854
+ placement="topLeft"
855
+ domTriggerClass="sidebar-export-trigger-btn-wrap"
856
+ :show-trigger-icon="false"
857
+ domContentClass="sidebar-export-dropdown-menu"
858
+ >
859
+ <template #trigger>
860
+ <xt-button class="sidebar-export-trigger-btn" block data-role="fab-trigger">
861
+ <template #icon>
862
+ <img
863
+ src="${EXPORT_ICON_DATA_URI}"
864
+ alt=""
865
+ width="16"
866
+ height="16"
867
+ draggable="false"
868
+ />
869
+ </template>
870
+ Export SVG
871
+ <template #append-icon>
872
+ <div v-if="state.isLogin" class="export-credits-hint">
873
+ <img src="${CREDIT_ICON_DATA_URI}" alt="" class="credits-token-icon credits-token-icon--sm" draggable="false" />
874
+ <span class="export-credits-hint-value" data-role="export-credits-value">{{ state.exportCreditsCost }}</span>
875
+ </div>
876
+ </template>
877
+ </xt-button>
878
+ </template>
879
+ <template #overlay>
880
+ <div class="export-overlay-menu fab-menu-content" data-role="fab-menu">
881
+ <div
882
+ v-show="state.exportEnabled"
883
+ class="export-overlay-item"
884
+ data-role="export-svg"
885
+ @click="callbacks.onExportSvg()"
886
+ >
887
+ <span class="export-overlay-item-icon export-overlay-item-icon--svg"></span>
888
+ Export SVG
889
+ </div>
890
+ <div
891
+ v-show="state.studioEnabled"
892
+ class="export-overlay-item"
893
+ data-role="open-in-studio"
894
+ @click="callbacks.onOpenInStudio()"
895
+ >
896
+ <span class="export-overlay-item-icon export-overlay-item-icon--studio"></span>
897
+ Open in Studio
898
+ </div>
899
+ </div>
900
+ </template>
901
+ </xt-dropdown-menu>
437
902
  </div>
438
903
  </aside>
439
904
  </div>
440
- <input data-role="template-file-input" type="file" accept="application/json" hidden />
905
+
906
+ <input
907
+ data-role="template-file-input"
908
+ type="file"
909
+ accept="application/json"
910
+ style="display:none"
911
+ @change="callbacks.onFileChange($event)"
912
+ />
913
+
914
+ <xt-modal
915
+ :model-value="state.templateDialogOpen"
916
+ title="发布模板"
917
+ :show-footer="false"
918
+ :portal-disabled="true"
919
+ @update:model-value="callbacks.onToggleTemplateDialog($event)"
920
+ >
921
+ <div
922
+ v-if="state.templateDialogOpen"
923
+ class="template-export-modal"
924
+ data-role="template-export-modal"
925
+ >
926
+
927
+ <div class="template-export-groups">
928
+ <div
929
+ v-for="group in state.templateFieldGroups"
930
+ :key="group.id"
931
+ class="template-export-group"
932
+ >
933
+ <div class="template-export-group-title">{{ group.title }}</div>
934
+ <div class="template-export-field-list">
935
+ <div
936
+ v-for="field in group.fields"
937
+ :key="field.path"
938
+ class="template-export-field"
939
+ data-role="template-export-field"
940
+ >
941
+ <xt-checkbox
942
+ :model-value="state.templateSelectedFieldPaths.includes(field.path)"
943
+ :disabled="state.templateExportLoading"
944
+ @update:model-value="callbacks.onToggleTemplateField(field.path, $event)"
945
+ >{{ field.label }}</xt-checkbox>
946
+ <div class="template-export-field-path">{{ field.path }}</div>
947
+ </div>
948
+ </div>
949
+ </div>
950
+ </div>
951
+
952
+ <div class="template-export-footer">
953
+ <xt-button
954
+ type="secondary"
955
+ :disabled="state.templateExportLoading"
956
+ @click="callbacks.onCloseTemplateDialog()"
957
+ >取消</xt-button>
958
+ <xt-button
959
+ type="primary"
960
+ data-role="template-export-confirm"
961
+ :loading="state.templateExportLoading"
962
+ :disabled="state.templateSelectedFieldPaths.length === 0"
963
+ @click="callbacks.onConfirmTemplateExport()"
964
+ >发布模板</xt-button>
965
+ </div>
966
+ </div>
967
+ </xt-modal>
441
968
  </div>
442
969
  `;
443
- function renderWorkbenchShell(root, _config) {
444
- root.innerHTML = WORKBENCH_TEMPLATE;
445
- return collectWorkbenchRefs(root);
970
+ function requireVue() {
971
+ const Vue = globalThis.Vue;
972
+ if (!Vue || typeof Vue.createApp !== "function") {
973
+ throw new Error(
974
+ '[generator-workbench] Vue 3 is required. Load it via CDN: <script src="https://unpkg.com/vue@3/dist/vue.global.js"><\/script>'
975
+ );
976
+ }
977
+ return Vue;
978
+ }
979
+ function injectStyles(root, cdnCssUrl) {
980
+ const link = document.createElement("link");
981
+ link.rel = "stylesheet";
982
+ link.href = cdnCssUrl || ATOMM_UI_CDN_CSS_URL;
983
+ root.appendChild(link);
984
+ const style = document.createElement("style");
985
+ style.textContent = WORKBENCH_SHELL_STYLES;
986
+ root.appendChild(style);
987
+ }
988
+ function stateFromConfig(vue, config) {
989
+ return vue.reactive({
990
+ shellMode: config.mode || "full",
991
+ logoText: config.logoText || config.title,
992
+ logoSrc: config.logoUrl || "https://storage-us.atomm.com/resource/xart/static/agent/imgs/atomm-logo.svg",
993
+ panelTarget: config.panelTarget || "right",
994
+ avatarMenuTrigger: config.avatarMenuTrigger || "hover",
995
+ templateEnabled: config.templateEnabled !== false,
996
+ exportEnabled: config.exportEnabled !== false,
997
+ studioEnabled: config.studioEnabled !== false,
998
+ isLogin: false,
999
+ avatarSrc: "",
1000
+ avatarText: "U",
1001
+ authDisplayName: "",
1002
+ authSubline: "",
1003
+ creditsBalance: 0,
1004
+ exportCreditsCost: 1,
1005
+ loginLoading: false,
1006
+ templateDialogOpen: false,
1007
+ templateExportLoading: false,
1008
+ templateSelectedFieldPaths: [],
1009
+ templateFieldGroups: []
1010
+ });
1011
+ }
1012
+ function createNoopCallbacks() {
1013
+ return {
1014
+ onLogin: () => {
1015
+ },
1016
+ onLogout: () => {
1017
+ },
1018
+ onImportTemplate: () => {
1019
+ },
1020
+ onExportTemplate: () => {
1021
+ },
1022
+ onCloseTemplateDialog: () => {
1023
+ },
1024
+ onToggleTemplateDialog: () => {
1025
+ },
1026
+ onToggleTemplateField: () => {
1027
+ },
1028
+ onConfirmTemplateExport: () => {
1029
+ },
1030
+ onExportSvg: () => {
1031
+ },
1032
+ onOpenInStudio: () => {
1033
+ },
1034
+ onFileChange: () => {
1035
+ }
1036
+ };
1037
+ }
1038
+ function registerFallbackComponents(app) {
1039
+ app.component("xt-button", {
1040
+ inheritAttrs: true,
1041
+ template: '<button v-bind="$attrs"><slot name="icon" /><slot /><slot name="append-icon" /></button>'
1042
+ });
1043
+ app.component("xt-avatar", {
1044
+ inheritAttrs: true,
1045
+ template: '<span v-bind="$attrs"><slot /></span>'
1046
+ });
1047
+ app.component("xt-dropdown-menu", {
1048
+ inheritAttrs: true,
1049
+ template: '<div v-bind="$attrs"><slot name="trigger" /><slot name="overlay" /><slot /></div>'
1050
+ });
1051
+ app.component("xt-hover-card", {
1052
+ inheritAttrs: true,
1053
+ template: '<div v-bind="$attrs"><slot name="trigger" /><slot name="content" /></div>'
1054
+ });
1055
+ app.component("xt-modal", {
1056
+ inheritAttrs: false,
1057
+ props: {
1058
+ modelValue: { type: Boolean, default: false },
1059
+ title: { type: String, default: "" }
1060
+ },
1061
+ emits: ["update:modelValue"],
1062
+ template: `
1063
+ <div v-if="modelValue" v-bind="$attrs">
1064
+ <div>{{ title }}</div>
1065
+ <slot />
1066
+ </div>
1067
+ `
1068
+ });
1069
+ app.component("xt-checkbox", {
1070
+ inheritAttrs: false,
1071
+ props: {
1072
+ modelValue: { type: Boolean, default: false },
1073
+ label: { type: String, default: "" },
1074
+ disabled: { type: Boolean, default: false }
1075
+ },
1076
+ emits: ["update:modelValue", "change"],
1077
+ template: `
1078
+ <label v-bind="$attrs">
1079
+ <input
1080
+ type="checkbox"
1081
+ :checked="modelValue"
1082
+ :disabled="disabled"
1083
+ @change="$emit('update:modelValue', $event.target.checked)"
1084
+ />
1085
+ <span><slot>{{ label }}</slot></span>
1086
+ </label>
1087
+ `
1088
+ });
1089
+ }
1090
+ function renderWorkbenchShell(root, config) {
1091
+ const vue = requireVue();
1092
+ ensureBrowserProcessEnv();
1093
+ injectStyles(root, config.atommUiCssUrl);
1094
+ const state = stateFromConfig(vue, config);
1095
+ const callbacks = createNoopCallbacks();
1096
+ const container = document.createElement("div");
1097
+ container.setAttribute("data-workbench-vue-root", "");
1098
+ root.appendChild(container);
1099
+ const AtommUI = globalThis.AtommUI;
1100
+ const app = vue.createApp({
1101
+ setup() {
1102
+ return { state, callbacks };
1103
+ },
1104
+ template: WORKBENCH_VUE_TEMPLATE
1105
+ });
1106
+ if (AtommUI) {
1107
+ app.use(AtommUI);
1108
+ } else {
1109
+ registerFallbackComponents(app);
1110
+ }
1111
+ app.mount(container);
1112
+ const refs = collectWorkbenchRefs(root);
1113
+ return { state, callbacks, app, refs };
1114
+ }
1115
+ function unmountWorkbenchShell(context) {
1116
+ context.app.unmount();
446
1117
  }
447
1118
  const DEFAULT_CONFIG = {
448
1119
  title: "",
1120
+ mode: "full",
449
1121
  templateEnabled: true,
450
1122
  exportEnabled: true,
451
1123
  studioEnabled: true,
@@ -453,6 +1125,7 @@ const DEFAULT_CONFIG = {
453
1125
  panelTarget: "right",
454
1126
  avatarMenuTrigger: "hover"
455
1127
  };
1128
+ const TOKEN_STORAGE_KEY_PREFIX = "__atomm_sdk_token__";
456
1129
  function normalizeWorkbenchConfig(config) {
457
1130
  return {
458
1131
  ...DEFAULT_CONFIG,
@@ -465,14 +1138,16 @@ class GeneratorWorkbenchElement extends HTMLElement {
465
1138
  __publicField(this, "_sdk", null);
466
1139
  __publicField(this, "_runtime", null);
467
1140
  __publicField(this, "_config", { ...DEFAULT_CONFIG });
468
- __publicField(this, "_refs", null);
469
1141
  __publicField(this, "_mounted", false);
470
1142
  __publicField(this, "_state", createInitialWorkbenchState());
471
1143
  __publicField(this, "_cleanupAuth");
1144
+ __publicField(this, "_cleanupCredits");
1145
+ __publicField(this, "_cleanupBilling");
472
1146
  __publicField(this, "_authController");
473
1147
  __publicField(this, "_templateController");
474
1148
  __publicField(this, "_exportController");
475
1149
  __publicField(this, "_runtimeController");
1150
+ __publicField(this, "_shellContext");
476
1151
  this.attachShadow({ mode: "open" });
477
1152
  }
478
1153
  get sdk() {
@@ -520,11 +1195,13 @@ class GeneratorWorkbenchElement extends HTMLElement {
520
1195
  }
521
1196
  this.render();
522
1197
  this.bindControllers();
523
- this.bindShellEvents();
1198
+ this.bindShellCallbacks();
524
1199
  this.bindAuthState();
525
- await ((_a = this._runtimeController) == null ? void 0 : _a.mountCanvas(this.requireRefs().canvasHost));
1200
+ await this.syncBillingState();
1201
+ const refs = this.requireRefs();
1202
+ await ((_a = this._runtimeController) == null ? void 0 : _a.mountCanvas(refs.canvasHost));
526
1203
  await ((_b = this._runtimeController) == null ? void 0 : _b.mountPanel(
527
- this.requireRefs().panelHost,
1204
+ refs.panelHost,
528
1205
  this._state.activePanelFilter
529
1206
  ));
530
1207
  this._mounted = true;
@@ -534,10 +1211,18 @@ class GeneratorWorkbenchElement extends HTMLElement {
534
1211
  });
535
1212
  }
536
1213
  async unmount() {
537
- var _a, _b;
1214
+ var _a, _b, _c, _d;
538
1215
  (_a = this._cleanupAuth) == null ? void 0 : _a.call(this);
539
1216
  this._cleanupAuth = void 0;
540
- await ((_b = this._runtimeController) == null ? void 0 : _b.unmountAll());
1217
+ (_b = this._cleanupCredits) == null ? void 0 : _b.call(this);
1218
+ this._cleanupCredits = void 0;
1219
+ (_c = this._cleanupBilling) == null ? void 0 : _c.call(this);
1220
+ this._cleanupBilling = void 0;
1221
+ await ((_d = this._runtimeController) == null ? void 0 : _d.unmountAll());
1222
+ if (this._shellContext) {
1223
+ unmountWorkbenchShell(this._shellContext);
1224
+ this._shellContext = void 0;
1225
+ }
541
1226
  this._mounted = false;
542
1227
  }
543
1228
  refreshLayout() {
@@ -546,9 +1231,10 @@ class GeneratorWorkbenchElement extends HTMLElement {
546
1231
  try {
547
1232
  const result = await this.requireTemplateController().importTemplate(file);
548
1233
  this._state.activePanelFilter = result.panelFilter;
549
- if (this._runtimeController && this._refs) {
1234
+ const refs = this.requireRefs();
1235
+ if (this._runtimeController) {
550
1236
  await this._runtimeController.remountPanel(
551
- this._refs.panelHost,
1237
+ refs.panelHost,
552
1238
  result.panelFilter
553
1239
  );
554
1240
  }
@@ -558,11 +1244,28 @@ class GeneratorWorkbenchElement extends HTMLElement {
558
1244
  }
559
1245
  }
560
1246
  async exportTemplate() {
1247
+ return this.exportTemplateWithFields();
1248
+ }
1249
+ async exportTemplateWithFields(selectedFieldPaths) {
561
1250
  try {
562
- const result = await this.requireTemplateController().exportTemplate();
1251
+ this._state.busy.exportTemplate = true;
1252
+ if (this._shellContext) {
1253
+ this._shellContext.state.templateExportLoading = true;
1254
+ }
1255
+ const result = await this.requireTemplateController().exportTemplate(
1256
+ selectedFieldPaths
1257
+ );
563
1258
  dispatchWorkbenchEvent(this, "template-exported", result);
1259
+ if (selectedFieldPaths) {
1260
+ this.closeTemplateExportDialog();
1261
+ }
564
1262
  } catch (error) {
565
1263
  this.handleError("template", error);
1264
+ } finally {
1265
+ this._state.busy.exportTemplate = false;
1266
+ if (this._shellContext) {
1267
+ this._shellContext.state.templateExportLoading = false;
1268
+ }
566
1269
  }
567
1270
  }
568
1271
  async exportSvg() {
@@ -581,11 +1284,37 @@ class GeneratorWorkbenchElement extends HTMLElement {
581
1284
  this.handleError("export", error);
582
1285
  }
583
1286
  }
1287
+ async setAuthToken(token) {
1288
+ var _a, _b;
1289
+ const normalizedToken = token.trim();
1290
+ if (!normalizedToken) {
1291
+ throw new Error("[generator-workbench] token is required");
1292
+ }
1293
+ if (!this._sdk) {
1294
+ throw new Error("[generator-workbench] sdk is required before setAuthToken()");
1295
+ }
1296
+ const appKey = (_b = (_a = this._sdk).getAppKey) == null ? void 0 : _b.call(_a);
1297
+ if (!appKey) {
1298
+ throw new Error("[generator-workbench] sdk.getAppKey() is required for setAuthToken()");
1299
+ }
1300
+ if (typeof this._sdk.auth.syncToken !== "function") {
1301
+ throw new Error("[generator-workbench] sdk.auth.syncToken() is required for setAuthToken()");
1302
+ }
1303
+ try {
1304
+ localStorage.setItem(`${TOKEN_STORAGE_KEY_PREFIX}${appKey}`, normalizedToken);
1305
+ } catch {
1306
+ }
1307
+ await this._sdk.auth.syncToken(normalizedToken);
1308
+ }
584
1309
  render() {
585
1310
  if (!this.shadowRoot) return;
586
- this._refs = renderWorkbenchShell(this.shadowRoot, this._config);
587
- applyWorkbenchConfig(this._refs, this._config);
1311
+ if (this._shellContext) {
1312
+ unmountWorkbenchShell(this._shellContext);
1313
+ this.shadowRoot.innerHTML = "";
1314
+ }
1315
+ this._shellContext = renderWorkbenchShell(this.shadowRoot, this._config);
588
1316
  this.syncAuthUI();
1317
+ this.syncBillingUI();
589
1318
  }
590
1319
  bindControllers() {
591
1320
  if (!this._sdk || !this._runtime) return;
@@ -605,47 +1334,86 @@ class GeneratorWorkbenchElement extends HTMLElement {
605
1334
  runtime: this._runtime
606
1335
  });
607
1336
  }
608
- bindShellEvents() {
609
- const refs = this.requireRefs();
610
- refs.loginBtn.addEventListener("click", () => {
1337
+ bindShellCallbacks() {
1338
+ if (!this._shellContext) return;
1339
+ const { callbacks } = this._shellContext;
1340
+ callbacks.onLogin = () => {
611
1341
  void this.handleLogin();
612
- });
613
- refs.logoutBtn.addEventListener("click", () => {
1342
+ };
1343
+ callbacks.onLogout = () => {
614
1344
  void this.handleLogout();
615
- });
616
- refs.importTemplateBtn.addEventListener("click", () => {
617
- refs.templateFileInput.click();
618
- });
619
- refs.exportTemplateBtn.addEventListener("click", () => {
620
- void this.exportTemplate();
621
- });
622
- refs.templateFileInput.addEventListener("change", () => {
1345
+ };
1346
+ callbacks.onImportTemplate = () => {
1347
+ this.requireRefs().templateFileInput.click();
1348
+ };
1349
+ callbacks.onExportTemplate = () => {
1350
+ this.openTemplateExportDialog();
1351
+ };
1352
+ callbacks.onCloseTemplateDialog = () => {
1353
+ this.closeTemplateExportDialog();
1354
+ };
1355
+ callbacks.onToggleTemplateDialog = (open) => {
1356
+ if (!open) {
1357
+ this.closeTemplateExportDialog();
1358
+ }
1359
+ };
1360
+ callbacks.onToggleTemplateField = (path, checked) => {
1361
+ this.toggleTemplateExportField(path, checked);
1362
+ };
1363
+ callbacks.onConfirmTemplateExport = () => {
623
1364
  var _a;
624
- const file = (_a = refs.templateFileInput.files) == null ? void 0 : _a[0];
625
- if (!file) return;
626
- void this.importTemplate(file);
627
- refs.templateFileInput.value = "";
628
- });
629
- refs.fabTrigger.addEventListener("click", () => {
630
- this.toggleFabMenu();
631
- });
632
- refs.exportSvgBtn.addEventListener("click", () => {
1365
+ void this.exportTemplateWithFields(
1366
+ ((_a = this._shellContext) == null ? void 0 : _a.state.templateSelectedFieldPaths) || []
1367
+ );
1368
+ };
1369
+ callbacks.onExportSvg = () => {
633
1370
  void this.exportSvg();
634
- });
635
- refs.openInStudioBtn.addEventListener("click", () => {
1371
+ };
1372
+ callbacks.onOpenInStudio = () => {
636
1373
  void this.openInStudio();
637
- });
638
- refs.avatarButton.addEventListener("click", () => {
639
- this.toggleAvatarMenu();
640
- });
641
- if (this._config.avatarMenuTrigger === "hover") {
642
- refs.avatarButton.addEventListener("mouseenter", () => {
643
- this.setAvatarMenuOpen(true);
644
- });
645
- refs.avatarMenu.addEventListener("mouseleave", () => {
646
- this.setAvatarMenuOpen(false);
647
- });
1374
+ };
1375
+ callbacks.onFileChange = (event) => {
1376
+ var _a;
1377
+ const input = event.target;
1378
+ const file = (_a = input.files) == null ? void 0 : _a[0];
1379
+ if (!file) return;
1380
+ void this.importTemplate(file);
1381
+ input.value = "";
1382
+ };
1383
+ }
1384
+ openTemplateExportDialog() {
1385
+ try {
1386
+ if (!this._shellContext) return;
1387
+ const prepared = this.requireTemplateController().prepareTemplateExport();
1388
+ this._shellContext.state.templateFieldGroups = prepared.fieldGroups.map(
1389
+ (group) => ({
1390
+ ...group,
1391
+ fields: group.fields.map((field) => ({ ...field }))
1392
+ })
1393
+ );
1394
+ this._shellContext.state.templateSelectedFieldPaths = [
1395
+ ...prepared.selectedFieldPaths
1396
+ ];
1397
+ this._shellContext.state.templateDialogOpen = true;
1398
+ } catch (error) {
1399
+ this.handleError("template", error);
1400
+ }
1401
+ }
1402
+ closeTemplateExportDialog() {
1403
+ if (!this._shellContext) return;
1404
+ this._shellContext.state.templateDialogOpen = false;
1405
+ this._shellContext.state.templateFieldGroups = [];
1406
+ this._shellContext.state.templateSelectedFieldPaths = [];
1407
+ }
1408
+ toggleTemplateExportField(path, checked) {
1409
+ if (!this._shellContext) return;
1410
+ const selected = new Set(this._shellContext.state.templateSelectedFieldPaths);
1411
+ if (checked) {
1412
+ selected.add(path);
1413
+ } else {
1414
+ selected.delete(path);
648
1415
  }
1416
+ this._shellContext.state.templateSelectedFieldPaths = Array.from(selected);
649
1417
  }
650
1418
  bindAuthState() {
651
1419
  var _a;
@@ -654,52 +1422,111 @@ class GeneratorWorkbenchElement extends HTMLElement {
654
1422
  this._cleanupAuth = controller.subscribe((status) => {
655
1423
  this._state.authStatus = status;
656
1424
  this.syncAuthUI();
1425
+ void this.syncBillingState(status);
657
1426
  dispatchWorkbenchEvent(this, "auth-change", status);
658
1427
  });
1428
+ this.bindBillingSubscriptions();
1429
+ }
1430
+ bindBillingSubscriptions() {
1431
+ var _a, _b, _c, _d;
1432
+ (_a = this._cleanupCredits) == null ? void 0 : _a.call(this);
1433
+ this._cleanupCredits = void 0;
1434
+ (_b = this._cleanupBilling) == null ? void 0 : _b.call(this);
1435
+ this._cleanupBilling = void 0;
1436
+ const creditsModule = (_c = this._sdk) == null ? void 0 : _c.credits;
1437
+ if (typeof (creditsModule == null ? void 0 : creditsModule.onChange) === "function") {
1438
+ this._cleanupCredits = creditsModule.onChange((balance) => {
1439
+ this._state.creditsBalance = Number(balance) || 0;
1440
+ this.syncBillingUI();
1441
+ });
1442
+ }
1443
+ const billingModule = (_d = this._sdk) == null ? void 0 : _d.billing;
1444
+ if (typeof (billingModule == null ? void 0 : billingModule.onChange) === "function") {
1445
+ this._cleanupBilling = billingModule.onChange((usage) => {
1446
+ this.applyBillingUsage(usage);
1447
+ });
1448
+ }
659
1449
  }
660
1450
  syncAuthUI() {
661
- if (!this._refs) return;
662
- const { loginBtn, avatarButton, avatarImage } = this._refs;
1451
+ var _a, _b, _c, _d;
1452
+ if (!this._shellContext) return;
1453
+ const { state } = this._shellContext;
663
1454
  const { isLogin, userInfo } = this._state.authStatus;
664
- loginBtn.style.display = isLogin ? "none" : "";
665
- avatarButton.style.display = isLogin ? "inline-flex" : "none";
666
- avatarImage.src = isLogin ? (userInfo == null ? void 0 : userInfo.headpic) || "" : "";
667
- if (!isLogin) {
668
- this.setAvatarMenuOpen(false);
669
- }
670
- }
671
- toggleFabMenu() {
672
- this._state.menu.fabOpen = !this._state.menu.fabOpen;
673
- this.syncMenuUI();
674
- }
675
- toggleAvatarMenu() {
676
- this.setAvatarMenuOpen(!this._state.menu.avatarOpen);
677
- }
678
- setAvatarMenuOpen(open) {
679
- this._state.menu.avatarOpen = open;
680
- this.syncMenuUI();
681
- }
682
- syncMenuUI() {
683
- if (!this._refs) return;
684
- this._refs.avatarMenu.classList.toggle("is-open", this._state.menu.avatarOpen);
685
- this._refs.fabMenu.classList.toggle("is-open", this._state.menu.fabOpen);
686
- this._refs.avatarButton.setAttribute(
687
- "aria-expanded",
688
- String(this._state.menu.avatarOpen)
689
- );
690
- this._refs.fabTrigger.setAttribute(
691
- "aria-expanded",
692
- String(this._state.menu.fabOpen)
693
- );
1455
+ const displayName = ((_a = userInfo == null ? void 0 : userInfo.userName) == null ? void 0 : _a.trim()) || ((_b = userInfo == null ? void 0 : userInfo.email) == null ? void 0 : _b.trim()) || ((_c = userInfo == null ? void 0 : userInfo.phoneNumber) == null ? void 0 : _c.trim()) || "";
1456
+ const avatarText = displayName ? displayName.charAt(0).toUpperCase() : "U";
1457
+ const authSubline = ((_d = userInfo == null ? void 0 : userInfo.email) == null ? void 0 : _d.trim()) || ((userInfo == null ? void 0 : userInfo.phoneNumber) ? `${userInfo.phoneZone || ""}${userInfo.phoneNumber}` : "Connected to Generator SDK");
1458
+ state.isLogin = isLogin;
1459
+ state.avatarSrc = isLogin ? (userInfo == null ? void 0 : userInfo.headpic) || "" : "";
1460
+ state.avatarText = avatarText;
1461
+ state.authDisplayName = displayName;
1462
+ state.authSubline = isLogin ? authSubline : "";
1463
+ }
1464
+ syncBillingUI() {
1465
+ if (!this._shellContext) return;
1466
+ const { state } = this._shellContext;
1467
+ const usage = this._state.billingUsage;
1468
+ state.creditsBalance = this._state.creditsBalance;
1469
+ state.exportCreditsCost = (usage == null ? void 0 : usage.creditsPerUse) ?? (usage && "unitPrice" in usage ? Number(usage.unitPrice) || 1 : 1);
1470
+ }
1471
+ applyBillingUsage(usage) {
1472
+ this._state.billingUsage = usage || null;
1473
+ if (usage && typeof usage.creditsBalance === "number") {
1474
+ this._state.creditsBalance = usage.creditsBalance;
1475
+ }
1476
+ this.syncBillingUI();
1477
+ }
1478
+ resetBillingState() {
1479
+ this._state.creditsBalance = 0;
1480
+ this._state.billingUsage = null;
1481
+ this.syncBillingUI();
1482
+ }
1483
+ async syncBillingState(status = this._state.authStatus) {
1484
+ var _a, _b, _c, _d, _e, _f, _g, _h;
1485
+ if (!status.isLogin || !this._sdk) {
1486
+ this.resetBillingState();
1487
+ return;
1488
+ }
1489
+ const cachedBalance = (_b = (_a = this._sdk.credits) == null ? void 0 : _a.getCachedBalance) == null ? void 0 : _b.call(_a);
1490
+ if (typeof cachedBalance === "number") {
1491
+ this._state.creditsBalance = cachedBalance;
1492
+ }
1493
+ const cachedUsage = (_d = (_c = this._sdk.billing) == null ? void 0 : _c.getCachedUsage) == null ? void 0 : _d.call(_c);
1494
+ if (cachedUsage) {
1495
+ this.applyBillingUsage(cachedUsage);
1496
+ } else {
1497
+ this.syncBillingUI();
1498
+ }
1499
+ try {
1500
+ const [creditsResult, usage] = await Promise.all([
1501
+ (_f = (_e = this._sdk.credits) == null ? void 0 : _e.getBalance) == null ? void 0 : _f.call(_e),
1502
+ (_h = (_g = this._sdk.billing) == null ? void 0 : _g.getUsage) == null ? void 0 : _h.call(_g)
1503
+ ]);
1504
+ if (typeof (creditsResult == null ? void 0 : creditsResult.quota) === "number") {
1505
+ this._state.creditsBalance = creditsResult.quota;
1506
+ }
1507
+ if (usage) {
1508
+ this.applyBillingUsage(usage);
1509
+ return;
1510
+ }
1511
+ this.syncBillingUI();
1512
+ } catch (error) {
1513
+ this.handleError("auth", error);
1514
+ }
694
1515
  }
695
1516
  async handleLogin() {
696
1517
  try {
697
1518
  this._state.busy.login = true;
1519
+ if (this._shellContext) {
1520
+ this._shellContext.state.loginLoading = true;
1521
+ }
698
1522
  await this.requireAuthController().login();
699
1523
  } catch (error) {
700
1524
  this.handleError("auth", error);
701
1525
  } finally {
702
1526
  this._state.busy.login = false;
1527
+ if (this._shellContext) {
1528
+ this._shellContext.state.loginLoading = false;
1529
+ }
703
1530
  }
704
1531
  }
705
1532
  async handleLogout() {
@@ -719,10 +1546,10 @@ class GeneratorWorkbenchElement extends HTMLElement {
719
1546
  });
720
1547
  }
721
1548
  requireRefs() {
722
- if (!this._refs) {
723
- throw new Error("[generator-workbench] refs are not ready");
1549
+ if (!this._shellContext) {
1550
+ throw new Error("[generator-workbench] shell context is not ready");
724
1551
  }
725
- return this._refs;
1552
+ return this._shellContext.refs;
726
1553
  }
727
1554
  requireAuthController() {
728
1555
  if (!this._authController) {