@atomm-developer/generator-workbench
v0.1.63
Published
Unified generator shell based on Web Components
Downloads
2,107
Readme
Generator Workbench
generator-workbench is the unified host shell for generators built on top of generator-sdk and the generator runtime contract.
V1 provides:
- Web Component shell
- guest avatar / login / avatar / logout entry
- credits badge and export credit hint
- built-in invitation action backed by
@atomm/atomm-proInvitationModal - local template import entry and template publish modal
- billing-backed export actions
- optional cloud save and history shell actions
- optional runtime-driven auto-save orchestration
- runtime mounting for either a free workspace host or split canvas / panel hosts
Install
pnpm add @atomm-developer/generator-sdk
pnpm add @atomm-developer/generator-workbenchCDN
<script src="https://static-res.atomm.com/scripts/js/generator-sdk/index.umd.js"></script>
<script src="https://static-res.atomm.com/scripts/js/generator-sdk/generator-workbench/index.umd.js"></script>generator-workbench auto-loads Vue 3 when window.Vue is absent, injects the default atomm-ui stylesheet into its Shadow DOM in a non-blocking way, and auto-loads the default atomm-ui CDN script when window.AtommUI is absent. The shell now starts a single background CSS fetch and patches the same CSS text into both the Shadow DOM and document.head, so CSS loading no longer blocks the initial shell render or duplicates the old <link> + fetch path. The host page no longer needs to add Vue or Atomm UI scripts just for the shell. When invitation, template publish, or credits top-up flows are enabled, the shell also starts an async background preload for atomm-pro.css, while keeping the atomm-pro JavaScript runtime lazy until the user actually opens one of those flows.
Usage
<generator-workbench id="workbench"></generator-workbench>import {
createAtommProHostI18nConfig,
createGlowSensorsReporter,
} from '@atomm-developer/generator-workbench'
GeneratorWorkbench.defineGeneratorWorkbench()
const workbench = document.getElementById('workbench')
workbench.sdk = sdk
workbench.runtime = runtime
workbench.config = {
title: 'My Generator',
mode: 'shell',
readyPolicy: 'runtime-ready',
templateEnabled: true,
isAdminPublishTemplate: false,
invitationEnabled: true,
atommProEnv: 'test',
atommProDomain: 'atomm',
...createAtommProHostI18nConfig(),
exportEnabled: true,
studioEnabled: true,
analytics: {
reporter: createGlowSensorsReporter({
resolveExportPayload({ config, runtime, sdk }) {
const state = runtime.getState()
return {
content_type: config.title,
content_id: sdk.getAppKey?.() || '',
element_name: String(state.params?.vars?.kerf_offset ?? ''),
content_name: String(state.params?.vars?.material_preset ?? ''),
}
},
}),
},
cloudEnabled: true,
historyEnabled: true,
autoSaveEnabled: true,
autoSaveDebounceMs: 1200,
getCloudSaveOptions: ({ state }) => ({
title: 'Draft',
snapshot: state,
}),
}
await workbench.mount()Admin-gated Template Publish
If you want only community admins to see the Publish as Template entry, enable:
workbench.config = {
title: 'My Generator',
templateEnabled: true,
isAdminPublishTemplate: true,
}Behavior:
- When
isAdminPublishTemplateistrue, the publish entry is shown only whensdk.auth.getStatus().userInfo?.isCommunityAdmin === true. - The shell automatically reacts to auth state changes (
login,logout, account switch viaauth.onChange) and updates the publish entry visibility. - Existing projects are unaffected because the default is
false.
Analytics
generator-workbench now enables a Glow-compatible reporter by default for both Download and Open in Studio.
If you do not provide config.analytics.reporter, the workbench automatically falls back to the built-in createGlowSensorsReporter(). Provide analytics.reporter only when you want to override the default mapping or sensor targets.
import { createGlowSensorsReporter } from '@atomm-developer/generator-workbench'
workbench.config = {
title: 'My Generator',
analytics: {
reporter: createGlowSensorsReporter({
sceneName: 'Atomm',
resolveExportPayload({ config, runtime, sdk }) {
const state = runtime.getState()
return {
content_type: config.title,
content_id: sdk.getAppKey?.() || '',
element_name: String(state.params?.vars?.kerf_offset ?? ''),
content_name: String(state.params?.vars?.material_preset ?? ''),
}
},
}),
},
}Reporter behavior:
- Event name:
Generator_export click_position:downloadoropeninstudio- Built-in common fields:
scene_name,item_type item_typedefaults tosdk.getAppKey()- Dispatch targets:
window.dataLayerfor GA4 andwindow.sensors.track(...)for Sensors Analytics
Template publish click:
- Event name:
publishTemplateClick - Payload:
{ content_type: workbench.config.title }
If your runtime state already exposes modelTitle, modelId, params.vars.kerf_offset, and params.vars.material_preset, the reporter can work without a custom resolver. Otherwise, provide resolveExportPayload(...) to map your runtime fields into the Glow schema.
If the host wants to remove the page-level loading state as soon as the shell chrome is visible, switch to:
workbench.config = {
title: 'My Generator',
mode: 'shell',
readyPolicy: 'shell-ready',
}Lifecycle semantics:
readyPolicy: 'runtime-ready'keeps the existing behavior.mount()resolves after runtime mount completes andworkbench-readyfires.readyPolicy: 'shell-ready'makesmount()resolve right after the shell UI is rendered. The workbench then continues mounting the runtime in the background.
The element now dispatches these lifecycle events:
workbench-shell-ready: the Shadow DOM shell and workspace host are visibleworkbench-runtime-ready: the runtime mount has completedworkbench-ready: compatibility event emitted together withworkbench-runtime-ready
If you need to override the default CDN addresses, pass:
workbench.config = {
title: 'My Generator',
vueScriptUrl: 'https://your-cdn/vue.global.prod.js',
vueRouterScriptUrl: 'https://your-cdn/vue-router.global.prod.js',
piniaScriptUrl: 'https://your-cdn/pinia.iife.prod.js',
vueI18nScriptUrl: 'https://your-cdn/vue-i18n.global.prod.js',
atommUiCssUrl: 'https://your-cdn/atomm-ui.css',
atommUiScriptUrl: 'https://your-cdn/atomm-ui.js',
atommProCssUrl: 'https://your-cdn/atomm-pro.css',
atommProScriptUrl: 'https://your-cdn/atomm-pro.js',
}Invitation Modal
By default, the shell adds an Earn Credits button to the left of the top-bar credits badge. When the user is logged out, the top bar also shows a 40x40 guest avatar instead of a text Login button.
import { createAtommProHostI18nConfig } from '@atomm-developer/generator-workbench'
workbench.config = {
title: 'My Generator',
atommProEnv: 'prod',
atommProDomain: 'atomm',
...createAtommProHostI18nConfig(),
}generator-workbench will then auto-load Pinia, Vue Router, Vue I18n, and the bundled atomm-pro browser assets, mount XtAtommProContext inside the shell, and call InvitationModal.open({ configKey }) when the user clicks the button. If the user is still logged out, the shell first calls sdk.auth.login() and opens the invitation/share modal right after login completes.
Notes:
- If you need to hide the invite entry, explicitly pass
invitationEnabled: false. - If
invitationConfigKeyis omitted, the shell defaults it togenerator_${sdk.getAppKey()}. - If you still need a custom referral/share key, you can explicitly pass
invitationConfigKey. - If you need localized invitation copy, you can either pass
atommProLocale/atommProMessagesmanually, or reusecreateAtommProHostI18nConfig()as a host-side starter template.
Shell Modes
mode: 'shell'keeps the top bar, moves#sidebar-footerto a fixed bottom-right floating export entry, and mounts the runtime into a free workspace host so the generator owns the full internal layout.mode: 'full'keeps the classic shell with the top bar plus the built-in right sidebar layout for separate canvas / panel mounting.mode: 'template'hides the top bar, keeps#sidebar-footer, and is useful when the host page already owns branding and login UI.
The shell layout mode above is different from the route capability mode. When the page URL contains ?mode=embed, generator-workbench keeps the current layout mode but force-disables shell integrations for:
- cloud save
- history
- credits badge / export credits hint
- billing-backed export consumption
- invitation / earn credits entry
This is useful when the host site embeds a generator inside an iframe and wants a lighter shell surface. If the route omits mode or uses mode=full, the existing shell behavior stays unchanged.
In this embed route, the shell also hides the top bar and #sidebar-footer export container. The underlying workbench methods and bridge actions still exist, but the shell chrome is no longer visible inside the iframe.
When ?mode=embed is active, the workbench also boots an iframe bridge compatible with the main-site generator protocol:
- outgoing ready signal:
generator_pageLoaded - incoming host commands:
generator_loadTemplateData,generator_setGeneratorData,generator_getTemplateData,generator_getGeneratorData,generator_getFile - outgoing result events:
generator_toTemplateLoaded,generator_toTemplateData,generator_toGeneratorData,generator_toFile,generator_toFileError,generator_toTemplateError,generator_toSelectTemplate
Default bridge mappings:
generator_loadTemplateDataapplies the incoming template throughsdk.template.applyToRuntime(...)generator_setGeneratorDatarestoresdata.infoback intoruntime.setState(...)generator_getTemplateDatabuilds a fresh template from the current runtime state and returns{ template, info, cover, originImageUrl }generator_getFilereads export data fromconfig.embedBridge.getExportData(...)first, then falls back to the SDK export provider registered throughsdk.export.register(...)
Host-side timing rules:
- treat
generator_pageLoadedas the only bridge-ready signal; it is emitted only after the iframe has attached itsmessagelistener - do not use
iframe.onloadas the business-ready signal for bridge commands - queue
generator_loadTemplateData/generator_setGeneratorDataon the host untilgenerator_pageLoadedarrives, then flush them - prefer
generator_loadTemplateDatafor template bootstrap because it returnsgenerator_toTemplateLoaded - keep
generator_setGeneratorDatafor runtime snapshot restore only; it does not emit a dedicated success ack event - if
generator_setGeneratorDatafails, the iframe reportsgenerator_toTemplateErrorwithaction: 'setGeneratorData'
Runtime Route Mode And Events
runtime.mount({ mode }) only describes how the runtime should mount into the current container. It is not enough to represent the real page route capability mode.
generator-workbench now passes an additional routeMode field into every runtime mount call:
await runtime.mount({
mode: 'full',
routeMode: 'embed',
target: 'full',
container,
})This matters in shell mode because the runtime can still be mounted as mode: 'full' while the actual page route is ?mode=embed.
If the runtime needs to notify the host that a parameter field changed, emit:
emit({
type: 'params_change',
data: {
field: 'width',
value: 120,
params: {
width: 120,
height: 128,
},
},
})generator-workbench receives this event and dispatches the DOM custom event runtime-params-change.
If the runtime needs to notify the host that a template card was selected, emit:
emit({
type: 'select_template',
data: {
name: 'Spring Sale',
category: 'marketing',
},
})When the current route is ?mode=embed, generator-workbench forwards this runtime event to the parent page through the iframe bridge as generator_toSelectTemplate.
Runtime Event Channel
generator-workbench now standardizes runtime communication behind a dedicated channel layer:
RuntimeEventRegistry: register runtime event handlers and workbench command handlersRuntimeEventChannel: connectruntime.subscribe(...), route runtime events, and forward workbench commands back to the runtime
The workbench exports both classes as public API, and the built-in shell behavior already uses them internally for:
- runtime
state-change-> auto-save orchestration - runtime
params_change-> DOM eventruntime-params-change - runtime
select_template-> DOM event + iframe bridge forwarding
If the host needs to send a command to the runtime, call:
await workbench.dispatchRuntimeCommand({
type: 'open-login',
data: {
source: 'topbar',
},
})dispatchRuntimeCommand() is the element-facing helper. Internally it forwards to the channel method dispatchWorkbenchCommand(...), which finally calls runtime.dispatchWorkbenchCommand(...).
On the runtime side, implement the optional receiver:
runtime.dispatchWorkbenchCommand = async (command) => {
if (command.type === 'open-login') {
openLoginPanel(command.data)
}
}The recommended direction split is:
runtime.subscribe(listener)forruntime -> workbenchruntime.dispatchWorkbenchCommand(command)forworkbench -> runtime
Error handling note:
- runtime event handler failures and command handler failures are reported through
config.onError/workbench-error dispatchRuntimeCommand()resolves after the dispatch attempt; it does not currently reject on handler errors
If the host needs stricter control, pass config.embedBridge:
targetOrigin: override the defaultpostMessage(..., '*')validateOrigin(origin, event): filter incoming parent messagesgetExportData(action, context): provide bridge export data explicitlygetOriginImageUrl(context): return the origin image URL stored by the host/runtime
In template mode, the host shell can pass an external token into the workbench:
await workbench.setAuthToken(token)This passes the token to sdk.auth.syncToken(token), allowing the SDK to persist it in the shared-domain utoken cookie and refresh the login state.
Billing Behavior
- When
sdk.creditsandsdk.billingare available, the top bar shows the current credits balance after login. - The export trigger reads
sdk.billing.getUsage()and hides the hint completely whenusage.isEnabled = false. - When billing is enabled, the hint switches between
freeRemaining/freeTotal,creditsPerUse, and the30sfree-period countdown. - When free quota remains, the export hint hides the credits token icon and only shows the
remaining/totaltext. - When free quota is exhausted,
exportSvg()andopenInStudio()compareusage.creditsPerUsewithusage.creditsBalance; if balance is insufficient, they open the@atomm/atomm-procredits purchase modal before retrying the export path. exportSvg()andopenInStudio()callsdk.billing.consume()before the actual export action when billing is enabled. If it returnsisBlacklisted = true, the shell blocksDownload / Open in Studioand showsYour account has been suspended due to security concerns.- On the credits path, the shell still calls
sdk.billing.refreshCredits()after the export succeeds.
Cloud Save And History
When the injected SDK exposes cloud and history, generator-workbench can optionally provide:
- a top bar
Save Draftaction - a top bar
Historyaction saveToCloud()for host-triggered saveloadHistory()for host-triggered history queryrestoreHistoryItem(id)for restoring a saved snapshot into the runtimedeleteHistoryItem(id)for deleting a history item from the shell flow
These are shell-level orchestration hooks, not a full project gallery or route-based work manager. The runtime still owns business state, and the host page still owns any larger application workflow.
Before the workbench calls sdk.cloud.save(...) or sdk.cloud.restore(...), it first checks sdk.auth.getToken(). If there is no token, it will call sdk.auth.login() and only continue the cloud action after login completes.
When cloudEnabled is on, the workbench now also supports a route-based bootstrap flow:
- the runtime mounts first so the canvas/panel can render immediately
- after the runtime is mounted, the workbench starts the route bootstrap in the background
- if the page URL contains
?gid=<id>, the background bootstrap callssdk.cloud.restore(id)and writes the restored snapshot back throughruntime.setState(...) - if
gidis missing but the route contains?templateId=<id>, the background bootstrap requests/community/v1/web/making/:id, logs the full response plusdata.generatorInfo.info, and restores thatinfoobject into the runtime throughruntime.setState(...) - if
gidis missing, the background bootstrap callssdk.cloud.save(...)once and writes the returned id back into the current route asgid
For the route template request, the workbench resolves the API base URL from ?env=dev|test|prod first, otherwise it falls back to config.atommProEnv (dev -> dev, test / test_us -> test, everything else -> prod).
This means a pending login flow or a dismissed login modal no longer blocks the runtime from rendering. The cloud bootstrap will either finish later or fail through the normal workbench error channel without blanking the runtime area.
If the page route contains ?mode=embed, this cloud bootstrap flow is skipped even when cloudEnabled was set by the host config.
Cloud/history events are also emitted as DOM custom events:
cloud-save-startcloud-savedruntime-cloud-save-requesthistory-load-starthistory-loadedhistory-restore-starthistory-restoredhistory-delete-starthistory-deleted
Auto-Save
If config.autoSaveEnabled is true and the injected runtime implements subscribe(listener), the workbench can debounce runtime changes into cloud saves:
workbench.config = {
title: 'My Generator',
cloudEnabled: true,
autoSaveEnabled: true,
autoSaveDebounceMs: 1500,
getCloudSaveOptions: ({ state }) => ({
title: 'Draft',
snapshot: state,
}),
}This is intentionally optional and only wires shell-level save orchestration. It does not replace product-level autosave policy, route sync, or record management owned by the host app.
If the runtime wants to request an explicit cloud save with custom payload, it can dispatch:
workbench.dispatchEvent(
new CustomEvent('runtime-cloud-save-request', {
detail: {
title: 'Draft',
snapshot: runtime.getState(),
},
bubbles: true,
composed: true,
}),
)The workbench will debounce this event for 2 seconds and then call sdk.cloud.save(...) with the current route gid injected as options.id.
Template Publish Behavior
- Clicking the top bar template publish action no longer downloads immediately.
generator-workbenchbuilds the publish payload from runtime state, template JSON, template meta, cover data, and origin image data, then opens the shared@atomm/atomm-proPublishTemplateModal.- On the first
Publish as Templateopen, the shell caches the resolvedgeneratorImageandgeneratorTagin memory; if that firstgeneratorImageis uploaded to OSS, the shell also caches the returned URL and reuses that URL for later opens in the same workbench instance. - Reopening the shared publish modal preserves the existing form draft by default;
generator-workbenchno longer resets the modal automatically before every reopen. - If
config.styleis provided, the shell forwards it asinitialData.styleinPublishTemplateModal.open(...). - Runtime can still override publish media through
template_publish_media_change, and the shell normalizesgeneratorInfoto{ appKey, generatorCode, generatorName, info, template, cover, originImageUrl }. - Before the final create/update template request is sent, any local image data still present in top-level
coverorgeneratorInfo.coveris uploaded to OSS and replaced with the returned URL.
Local Preview
pnpm --dir generator-workbench devThis starts the basic example in development mode with:
- local built
generator-workbench - local source
GeneratorSDK GeneratorSDK.init({ env: 'dev' })atommProEnv = 'dev'- cloud/history UI enabled by default in the top bar
Then open http://127.0.0.1:5173/examples/basic/?env=dev.
For a local test environment preview:
pnpm --dir generator-workbench example:testThis starts the same local example with:
- local built
generator-workbench - local source
GeneratorSDK GeneratorSDK.init({ env: 'test' })atommProEnv = 'test_us'- cloud/history UI enabled by default in the top bar
Then open http://127.0.0.1:5173/examples/basic/?env=test.
To verify debounced auto-save in the same example, append ?autoSave=1.
For a CDN-style preview:
pnpm --dir generator-workbench prodThis starts the same example with:
- CDN
generator-workbench - CDN
GeneratorSDK GeneratorSDK.init({ env: 'prod' })atommProEnv = 'prod'- cloud/history UI enabled by default in the top bar
Then open http://127.0.0.1:5173/examples/basic/?env=prod.
For the new shell-mode example, open http://127.0.0.1:5173/examples/shell/.
Both examples enable the invitation button by default. If you want to hide it for comparison, append ?invitation=0.
