@kedem/okdb 1.9.2 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +3 -2
  2. package/algorithm-worker-thread.js +1 -0
  3. package/bin/okdb.js +1 -1
  4. package/docs/deployment.md +155 -0
  5. package/docs/diagnostics.md +115 -0
  6. package/docs/embeddings.md +1 -1
  7. package/docs/functions.md +30 -27
  8. package/docs/getting-started.md +14 -3
  9. package/docs/http-api.md +3 -3
  10. package/docs/http-cluster.md +93 -60
  11. package/docs/index.md +13 -1
  12. package/docs/manifest.json +6 -3
  13. package/docs/pipelines.md +1 -1
  14. package/docs/process-registry.md +5 -5
  15. package/docs/processors.md +35 -18
  16. package/docs/queue.md +112 -42
  17. package/docs/sync.md +10 -7
  18. package/docs/ttl.md +1 -1
  19. package/docs/upgrade-2.0.md +290 -0
  20. package/okdb-functions-sandbox-worker.js +1 -1
  21. package/okdb-http-worker-child.js +1 -1
  22. package/okdb-views-bootstrap-worker.js +1 -1
  23. package/okdb.js +1 -1
  24. package/package.json +1 -1
  25. package/public/layouts/_default.ok.html +1 -1
  26. package/public/sections/db/modals/create-env-modal.ok.js +1 -1
  27. package/public/sections/db/modals/demo-env-modal.ok.js +1 -1
  28. package/public/sections/db/parts/db-overview.ok.js +1 -1
  29. package/public/sections/engines/engine-ui-utils.js +1 -1
  30. package/public/sections/engines/parts/engine-declaration-editor.ok.js +1 -1
  31. package/public/sections/engines/parts/generic-engine-panel.ok.js +1 -1
  32. package/public/sections/queue/parts/code-panel.ok.js +1 -1
  33. package/public/sections/queue/parts/job-log-panel.ok.js +1 -1
  34. package/public/sections/queue/parts/queue-jobs.ok.js +1 -1
  35. package/public/sections/sync/parts/sync-topology.ok.js +1 -1
  36. package/public/sections/system/index.ok.html +1 -1
  37. package/public/sections/system/parts/system-process-panel.ok.js +1 -1
  38. package/public/sections/system/parts/system-processing-panel.ok.js +1 -1
  39. package/public/sections/system/parts/system-runtime-overview.ok.js +1 -1
  40. package/public/setup-app.js +1 -1
  41. package/public/setup.html +116 -55
  42. package/types/index.d.ts +33 -3
  43. package/types/options.d.ts +19 -10
  44. package/docs/worker-fleet.md +0 -139
  45. package/okdb-functions-runner-child.js +0 -1
  46. package/okdb-queue-load-handler.js +0 -1
  47. package/okdb-queue-spawn-child.js +0 -1
  48. package/okdb-worker-child.js +0 -1
  49. package/public/sections/system/parts/system-workers-panel.ok.js +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kedem/okdb",
3
- "version": "1.9.2",
3
+ "version": "2.0.1",
4
4
  "description": "A fast, type-oriented database — strong consistency and rich indexing at the core, with sync, vector embeddings, full-text search, and AI tooling built in. Designed for the AI era.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "engines": {
@@ -1 +1 @@
1
- <script type="module">import e from"../components/env-picker.ok.js";import n from"../sections/db/modals/create-env-modal.ok.js";import o from"../sections/db/modals/demo-env-modal.ok.js";import s from"../stores/events-store.js";export default{tag:"default-layout",unwrap:!0,dependencies:[e,n,o],context:e=>({noticesExpanded:!1,dismissNotices(n){n&&(n.stopPropagation(),n.preventDefault()),e.$ok.stores.auth.dismissNotices()},hasEnvOption:n=>!!n&&(e.$ok.stores.okdb.envs||[]).some(e=>e?.name===n),syncEnvSearch(n){const o=e.$vars.$router;if(!o)return;const s=n??null;if((o._search?.env??null)===s)return;const t={...o._search||{}};s?t.env=s:delete t.env,o.replace(o.resolve(o._path||"",t,o._anchor||""))},applyEnvFromSearch(){const n=e.$vars.$router,o=n?._search?.env??null;return!(!o||!this.hasEnvOption(o)||(e.$ok.stores.okdb.selectedEnv!==o&&e.$ok.stores.okdb.selectEnv(o),0))},goToSection(n){const o=e.$vars.$router;if(!o)return;const s={},t=e.$ok.stores.okdb.selectedEnv;t&&(s.env=t);const r="system"===n?"/system/overview":`/${n}`;o.push(o.resolve(r,s))},async switchEnv(n){e.$ok.stores.okdb.selectEnv(n),e.$ok.shared?.toast?.info?.(`Environment: ${n}`)},async openCreateEnvModal(){await e.$ok.shared.modal.open({owner:e,title:"Create Environment",content:'<create-env-modal :modal="modal" :on-done="onDone" />',context:n=>({modal:n,onDone:async n=>{await e.$ok.stores.okdb.reloadEnvs(),e.$ok.stores.okdb.selectEnv(n),e.$ok.shared?.toast?.success?.(`Environment created: ${n}`)}})})},async openDemoModal(){await e.$ok.shared.modal.open({owner:e,title:'<span style="background:linear-gradient(135deg,#4FC1FF,#c084fc 50%,#4ade80);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;font-weight:700;font-size:15px;">✦ Demo Environment</span>',canDrag:!0,content:'<demo-env-modal :modal="modal" :on-done="onDone" />',context:n=>({modal:n,onDone:async n=>{await e.$ok.stores.okdb.reloadEnvs(),e.$ok.stores.okdb.selectEnv(n),e.$ok.shared?.toast?.success?.(`Demo environment "${n}" ready`)}})})}}),init(e){const n=()=>e.$ok.shared?.toast;e.$cache._unsubs=[s.on("type:registered",e=>n()?.info?.(`Type registered: ${e?.type??""}`)),s.on("type:drop",e=>n()?.info?.(`Type dropped: ${e?.type??""}`)),s.on("index:registered",e=>n()?.info?.(`Index registered on ${e?.type??""}: ${e?.key??""}`)),s.on("index:drop",e=>n()?.info?.(`Index dropped on ${e?.type??""}: ${e?.key??""}`)),s.on("fts:registered",e=>n()?.info?.(`FTS index created on ${e?.type??""}: ${e?.key??""}`)),s.on("fts:drop",e=>n()?.info?.(`FTS index dropped on ${e?.type??""}: ${e?.key??""}`)),s.on("env:opened",async o=>{n()?.info?.(`Environment ready: ${o?.name??""}`),await e.$ok.stores.okdb.reloadEnvs()}),s.on("env:removed",async o=>{n()?.info?.(`Environment removed: ${o?.name??""}`),await e.$ok.stores.okdb.reloadEnvs();const s=e.$ok.stores.okdb._pickBestEnv();s!==e.$ok.stores.okdb.selectedEnv&&e.$ok.stores.okdb.selectEnv(s)}),s.on("engine:started",e=>n()?.success?.(`Engine started: ${e?.name??""}`)),s.on("engine:stopped",e=>n()?.info?.(`Engine stopped: ${e?.name??""}`)),s.on("engine:error",e=>n()?.error?.(`Engine error: ${e?.name??""}: ${e?.error??""}`)),s.on("engine:installed",e=>n()?.success?.(`Engine installed: ${e?.name??""}`)),s.on("engine:uninstalled",e=>n()?.info?.(`Engine uninstalled: ${e?.name??""}`)),s.on("embeddings:pipeline:ready",e=>n()?.success?.(`Pipeline ready: ${e?.name??""}`)),s.on("embeddings:indexer:flushed",e=>n()?.success?.(`Embeddings flushed: ${e?.name??""}`))],e.$ext("unmount",()=>{for(const n of e.$cache._unsubs||[])n();e.$cache._unsubs=[]}),this.applyEnvFromSearch()||this.syncEnvSearch(e.$ok.stores.okdb.selectedEnv??null),e.$watch(()=>e.$vars.$router?._search?.env,()=>{const n=e.$vars.$router?._search?.env??null;n&&this.hasEnvOption(n)?e.$ok.stores.okdb.selectedEnv!==n&&e.$ok.stores.okdb.selectEnv(n):this.syncEnvSearch(e.$ok.stores.okdb.selectedEnv??null)}),e.$watch(()=>e.$ok.stores.okdb.selectedEnv,()=>this.syncEnvSearch(e.$ok.stores.okdb.selectedEnv??null))}};</script><template><div class="layout"><aside class="layout__sidebar"><div class="layout__sidebar-cube"><component import="./okdb-cube.ok.html"></component><span class="layout__version">v{{ $ok.stores.okdb.info?.version ?? '…' }}</span></div><div class="layout__env-picker"><env-picker :selected="$ok.stores.okdb.selectedEnv" :envs="$ok.stores.okdb.envs || []" @change="switchEnv($event.detail)" @create="openCreateEnvModal()"/></div><div class="layout__sidebar-body"><if :="$ok.stores.okdb.selectedEnv"><slot name="sidebar"></slot></if></div><div class="layout__sidebar-footer"><div class="layout__auth-chip layout__auth-chip--sidebar" :title="$ok.stores.auth?.modeLabel || 'Authentication'"><div class="layout__auth-meta"><div class="layout__auth-userblock"><div class="layout__auth-user">{{ $ok.stores.auth?.username || 'anonymous' }}</div><if :="$ok.stores.auth?.rolesLabel"><div class="layout__auth-roles">{{ $ok.stores.auth.rolesLabel }}</div></if></div></div><if :="!$ok.stores.auth?.openAccess"><button class="layout__auth-logout" @click="$ok.stores.auth.logout()">Logout</button></if></div></div></aside><div class="layout__right-col"><nav class="layout__section-nav"><button :class="{ active: $route.params.section === 'db' }" :disabled="!$ok.stores.auth.canSection('db')" :title="$ok.stores.auth.canSection('db') ? '' : 'Requires data:read or env:read permission'" @click="$ok.stores.auth.canSection('db') && goToSection('db')"><span class="layout__nav-icon">⬡</span> DB<if :="!$ok.stores.auth.canSection('db')"><span class="layout__nav-lock">🔒</span></if></button> <button :class="{ active: $route.params.section === 'engines' || $route.params.section === 'embeddings' }" :disabled="!$ok.stores.auth.canSection('engines')" :title="$ok.stores.auth.canSection('engines') ? '' : 'Requires env:read permission'" @click="$ok.stores.auth.canSection('engines') && goToSection('engines')"><span class="layout__nav-icon">⛓</span> Engines<if :="!$ok.stores.auth.canSection('engines')"><span class="layout__nav-lock">🔒</span></if></button> <button :class="{ active: $route.params.section === 'files' }" :disabled="!$ok.stores.auth.canSection('files')" :title="$ok.stores.auth.canSection('files') ? '' : 'Requires files:read permission'" @click="$ok.stores.auth.canSection('files') && goToSection('files')"><span class="layout__nav-icon">⬚</span> Files<if :="!$ok.stores.auth.canSection('files')"><span class="layout__nav-lock">🔒</span></if></button> <button :class="{ active: $route.params.section === 'queue' }" :disabled="!$ok.stores.auth.canSection('queue')" :title="$ok.stores.auth.canSection('queue') ? '' : 'Requires queue:read permission'" @click="$ok.stores.auth.canSection('queue') && goToSection('queue')"><span class="layout__nav-icon">⇶</span> Queue<if :="!$ok.stores.auth.canSection('queue')"><span class="layout__nav-lock">🔒</span></if></button> <button :class="{ active: $route.params.section === 'functions' }" :disabled="!$ok.stores.auth.canSection('functions')" :title="$ok.stores.auth.canSection('functions') ? '' : 'Requires functions:read permission'" @click="$ok.stores.auth.canSection('functions') && goToSection('functions')"><span class="layout__nav-icon">ƒ</span> Functions<if :="!$ok.stores.auth.canSection('functions')"><span class="layout__nav-lock">🔒</span></if></button> <button :class="{ active: $route.params.section === 'views' }" @click="goToSection('views')"><span class="layout__nav-icon">⊞</span> Views</button> <button :class="{ active: $route.params.section === 'system' }" :disabled="!$ok.stores.auth.canSection('system')" :title="$ok.stores.auth.canSection('system') ? '' : 'Requires system:read permission'" @click="$ok.stores.auth.canSection('system') && goToSection('system')"><span class="layout__nav-icon">⚙</span> System<if :="!$ok.stores.auth.canSection('system')"><span class="layout__nav-lock">🔒</span></if></button><div class="layout__live-indicator" :title="$ok.stores.events?.connected ? 'Event stream live' : 'Event stream disconnected'"><span :class="$ok.stores.events?.connected ? 'layout__live-dot layout__live-dot--on' : 'layout__live-dot layout__live-dot--off'"></span> <span class="layout__live-label">{{ $ok.stores.events?.connected ? 'Live' : 'Off' }}</span></div><div class="layout__security-pill" :title="($ok.stores.auth?.modeLabel || 'Authentication') + ' · ' + ($ok.stores.auth?.username || 'anonymous')"><span class="layout__security-dot" :class="$ok.stores.auth?.openAccess ? 'layout__security-dot layout__security-dot--open' : 'layout__security-dot layout__security-dot--secure'"></span> <span class="layout__security-label">{{ $ok.stores.auth?.openAccess ? 'Open access' : 'Secure' }}</span></div></nav><div class="layout__main"><if :="$ok.stores.auth?.visibleNotices?.length > 0"><div class="layout__notices"><div class="layout__notices-bar" @click="noticesExpanded = !noticesExpanded"><span class="layout__notices-icon">⚠</span> <span class="layout__notices-summary">{{ $ok.stores.auth.visibleNotices.length }} security notice{{ $ok.stores.auth.visibleNotices.length > 1 ? 's' : '' }}<if :="$ok.stores.auth.visibleNotices[0]">· {{ $ok.stores.auth.visibleNotices[0].title }}<if :="$ok.stores.auth.visibleNotices.length > 1">and {{ $ok.stores.auth.visibleNotices.length - 1 }} more</if></if></span><span class="layout__notices-chevron">{{ noticesExpanded ? '▴' : '▾' }}</span> <button class="layout__notices-dismiss" @click="dismissNotices($event)" title="Dismiss for this session">✕</button></div><if :="noticesExpanded"><div class="layout__notices-detail"><each :of="$ok.stores.auth.visibleNotices" let:item="n"><div class="layout__notice-item"><div class="layout__notice-title">{{ n.title }}</div><div class="layout__notice-msg">{{ n.message }}</div><if :="n.hint"><div class="layout__notice-hint">{{ n.hint }}</div></if><if :="n.action"><a class="layout__notice-action" :href="n.action.href">{{ n.action.label }} →</a></if></div></each></div></if></div></if><header class="layout__header"><slot name="header"></slot></header><main class="layout__content"><if :="$ok.stores.okdb.selectedEnv"><slot></slot></if><if :="!$ok.stores.okdb.selectedEnv"><div class="layout__no-env"><div class="layout__no-env-icon">⬡</div><div class="layout__no-env-title">No environment selected</div><div class="layout__no-env-sub">Create a new environment or load the demo to get started.</div><div class="layout__no-env-actions"><button @click="openCreateEnvModal()" class="layout__no-env-btn">+ Create environment</button> <button @click="openDemoModal()" class="layout__no-env-btn layout__no-env-btn--demo">✦ Load demo</button></div></div></if></main></div></div></div></template><style>.layout{display:flex;flex-direction:row;height:100%;width:100%;min-height:0}.layout__sidebar{display:flex;flex-direction:column;width:210px;flex-shrink:0;border-right:1px solid var(--okdb-app-surface-2);background:var(--okdb-app-bg-deep);overflow:visible}.layout__sidebar-cube{height:150px;flex-shrink:0;display:flex;align-items:center;justify-content:center;position:relative}.layout__version{position:absolute;bottom:8px;right:10px;font-size:10px;font-family:monospace;color:var(--okdb-app-fg-faint);letter-spacing:.04em;pointer-events:none;user-select:none;transition:color .2s}.layout__sidebar-cube:hover .layout__version{color:var(--okdb-app-fg-subtle)}.layout__env-picker{flex-shrink:0;padding:8px 10px 6px;border-top:1px solid var(--okdb-app-surface-2);border-bottom:1px solid var(--okdb-app-surface-2)}.layout__sidebar-body{flex:1;min-height:0;display:flex;flex-direction:column;gap:3px;padding:10px;border-top:1px solid var(--okdb-app-surface-2);overflow-y:auto;overflow-x:visible}.layout__sidebar-footer{flex-shrink:0;padding:8px 10px 10px;border-top:1px solid var(--okdb-app-surface-2);background:var(--okdb-app-bg-deep)}.layout__right-col{flex:1;min-width:0;display:flex;flex-direction:column;min-height:0}.layout__section-nav{display:flex;flex-direction:row;align-items:flex-end;height:150px;padding:0 8px;flex-shrink:0;background:var(--okdb-app-bg-deep);border-bottom:1px solid var(--okdb-app-surface-2)}.layout__section-nav button{display:flex;align-items:center;gap:7px;padding:0 16px;height:44px;border-radius:0;font-size:13px;font-weight:500;background:0 0;border:none;border-bottom:2px solid transparent;color:var(--okdb-app-fg-subtle);cursor:pointer;white-space:nowrap;letter-spacing:.01em;transition:color .15s,border-color .15s,background .15s;margin-bottom:-1px}.layout__section-nav button:hover{background:rgba(255,255,255,.03);border-color:transparent;color:var(--okdb-app-fg-muted);border-bottom-color:var(--okdb-app-border-subtle);transform:none;box-shadow:none}.layout__section-nav button:disabled{opacity:.35;cursor:not-allowed;background:0 0!important;border-color:transparent!important;border-bottom-color:transparent!important;color:var(--okdb-app-fg-faint)!important}.layout__section-nav button:disabled:hover{background:0 0;transform:none;box-shadow:none}.layout__section-nav button.active{background:0 0;border-color:transparent;color:var(--okdb-accent);border-bottom-color:var(--okdb-accent)}.layout__nav-icon{font-size:14px;opacity:.7;flex-shrink:0}.layout__nav-lock{font-size:10px;margin-left:4px;opacity:.7;flex-shrink:0;vertical-align:middle}.layout__section-nav button:hover .layout__nav-icon{opacity:.9}.layout__section-nav button.active .layout__nav-icon{opacity:1}.layout__live-indicator{margin-left:auto;margin-right:8px;margin-bottom:11px;display:flex;align-items:center;gap:5px;cursor:default}.layout__live-dot{width:7px;height:7px;border-radius:50%;display:inline-block;flex-shrink:0}.layout__live-dot--on{background:var(--okdb-success);box-shadow:0 0 5px color-mix(in srgb,var(--okdb-success) 40%,transparent);animation:nav-pulse 2.5s ease-in-out infinite}.layout__live-dot--off{background:var(--okdb-danger);opacity:.6}.layout__live-label{font-size:10px;color:var(--okdb-app-fg-faint);letter-spacing:.04em;text-transform:uppercase}@keyframes nav-pulse{0%,100%{opacity:1}50%{opacity:.35}}.layout__security-pill{margin-bottom:9px;display:inline-flex;align-items:center;gap:7px;padding:6px 10px;border-radius:999px;border:1px solid var(--okdb-app-border);background:var(--okdb-app-bg);color:var(--okdb-app-fg-muted)}.layout__security-dot{width:7px;height:7px;border-radius:50%;display:inline-block;flex-shrink:0}.layout__security-dot--secure{background:var(--okdb-accent);box-shadow:0 0 6px var(--okdb-accent-soft-border)}.layout__security-dot--open{background:var(--okdb-warning);box-shadow:0 0 6px color-mix(in srgb,var(--okdb-warning) 33%,transparent)}.layout__security-label{font-size:10px;line-height:1;text-transform:uppercase;letter-spacing:.08em;white-space:nowrap}.layout__auth-chip{margin-bottom:9px;display:inline-flex;align-items:center;gap:10px;padding:6px 10px;border-radius:10px;border:1px solid var(--okdb-app-border);background:var(--okdb-app-bg)}.layout__auth-chip--sidebar{width:100%;margin-bottom:0;align-items:center;justify-content:space-between;gap:12px;padding:0;border-radius:0;background:0 0;border:none;box-shadow:none}.layout__auth-meta{display:flex;align-items:center;gap:0;min-width:0;flex:1}.layout__auth-userblock{display:flex;flex-direction:column;min-width:0}.layout__auth-badge{font-size:10px;line-height:1;text-transform:uppercase;letter-spacing:.05em;padding:4px 7px;border-radius:999px;border:1px solid transparent;white-space:nowrap}.layout__auth-badge--open{color:var(--okdb-success);background:#4ade8011;border-color:#4ade8033}.layout__auth-badge--users{color:var(--okdb-accent);background:#4fc1FF11;border-color:#4fc1FF33}.layout__auth-user{font-size:11px;font-weight:600;color:var(--okdb-app-fg);line-height:1.1;text-transform:uppercase;letter-spacing:.05em}.layout__auth-roles{font-size:10px;color:var(--okdb-app-fg-muted);line-height:1.1;margin-top:2px;max-width:160px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.layout__auth-logout{font-size:11px;padding:0;border-radius:0;border:none;background:0 0;color:var(--okdb-danger);cursor:pointer;white-space:nowrap;align-self:center;letter-spacing:.04em;text-transform:uppercase;opacity:.88}.layout__auth-logout:hover{background:0 0;color:var(--okdb-danger-hover);opacity:1;transform:none;box-shadow:none}.layout__main{flex:1;display:flex;flex-direction:column;min-height:0;overflow:auto;padding:20px 24px 6px}.layout__header{flex-shrink:0}.layout__content{flex:1;min-height:0;min-width:0}.layout__content>*{animation:layout-fade-in .2s ease backwards}.layout__sidebar-body>*{animation:layout-fade-in .18s ease backwards}@keyframes layout-fade-in{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}.layout__no-env{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:12px;animation:layout-fade-in .25s ease backwards}.layout__no-env-icon{font-size:48px;opacity:.12;line-height:1;margin-bottom:4px}.layout__no-env-title{font-size:18px;font-weight:600;color:var(--okdb-app-fg-muted)}.layout__no-env-sub{font-size:13px;color:var(--okdb-app-fg-subtle);text-align:center;max-width:320px;line-height:1.6}.layout__no-env-actions{display:flex;gap:10px;margin-top:8px}.layout__no-env-btn{padding:8px 18px;font-size:13px;border-radius:7px;border:1px solid var(--okdb-app-border);background:var(--okdb-app-surface);color:var(--okdb-app-fg-subtle);cursor:pointer;transition:background .12s,border-color .12s,color .12s}.layout__no-env-btn:hover{background:var(--okdb-app-surface-2);border-color:var(--okdb-app-border-strong);color:var(--okdb-app-fg-muted);transform:none;box-shadow:none}.layout__no-env-btn--demo{border-color:var(--okdb-accent-2-soft-border);background:var(--okdb-accent-2-soft-bg);color:var(--okdb-accent-2)}.layout__no-env-btn--demo:hover{background:var(--okdb-accent-2-soft-bg);border-color:var(--okdb-accent-2-soft-border);color:var(--okdb-accent-2);transform:none;box-shadow:none}.layout__notices{flex-shrink:0;margin:-20px -24px 16px;border-bottom:1px solid color-mix(in srgb,var(--okdb-warning) 22%,transparent);background:color-mix(in srgb,var(--okdb-warning) 5%,transparent)}.layout__notices-bar{display:flex;align-items:center;gap:10px;padding:9px 16px;cursor:pointer;user-select:none;transition:background .12s}.layout__notices-bar:hover{background:color-mix(in srgb,var(--okdb-warning) 9%,transparent)}.layout__notices-icon{font-size:15px;color:var(--okdb-warning);flex-shrink:0}.layout__notices-summary{font-size:12px;color:var(--okdb-warning-text);flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.layout__notices-chevron{font-size:14px;color:var(--okdb-warning-text);flex-shrink:0;line-height:1}.layout__notices-dismiss{font-size:14px;color:var(--okdb-warning-text);background:0 0;border:none;cursor:pointer;padding:3px 7px;border-radius:4px;flex-shrink:0;line-height:1;transition:color .1s,background .1s}.layout__notices-dismiss:hover{color:var(--okdb-warning);background:color-mix(in srgb,var(--okdb-warning) 15%,transparent)}.layout__notices-detail{padding:0 16px 14px;display:flex;flex-direction:column;gap:10px}.layout__notice-item{padding:12px 14px;border:1px solid color-mix(in srgb,var(--okdb-warning) 20%,transparent);border-radius:6px;background:color-mix(in srgb,var(--okdb-warning) 5%,transparent);display:flex;flex-direction:column;gap:6px}.layout__notice-title{font-size:13px;font-weight:600;color:var(--okdb-warning)}.layout__notice-msg{font-size:12px;color:var(--okdb-warning-text);line-height:1.55}.layout__notice-hint{font-size:12px;color:var(--okdb-warning-text);line-height:1.55;font-style:italic}.layout__notice-action{font-size:12px;color:var(--okdb-accent);text-decoration:none;margin-top:2px;display:inline-block}.layout__notice-action:hover{text-decoration:underline}</style>
1
+ <script type="module">import e from"../components/env-picker.ok.js";import o from"../sections/db/modals/create-env-modal.ok.js";import n from"../sections/db/modals/demo-env-modal.ok.js";import s from"../stores/events-store.js";export default{tag:"default-layout",unwrap:!0,dependencies:[e,o,n],context:e=>({noticesExpanded:!1,dismissNotices(o){o&&(o.stopPropagation(),o.preventDefault()),e.$ok.stores.auth.dismissNotices()},hasEnvOption:o=>!!o&&(e.$ok.stores.okdb.envs||[]).some(e=>e?.name===o),syncEnvSearch(o){const n=e.$vars.$router;if(!n)return;const s=o??null;if((n._search?.env??null)===s)return;const t={...n._search||{}};s?t.env=s:delete t.env,n.replace(n.resolve(n._path||"",t,n._anchor||""))},applyEnvFromSearch(){const o=e.$vars.$router,n=o?._search?.env??null;return!(!n||!this.hasEnvOption(n)||(e.$ok.stores.okdb.selectedEnv!==n&&e.$ok.stores.okdb.selectEnv(n),0))},goToSection(o){const n=e.$vars.$router;if(!n)return;const s={},t=e.$ok.stores.okdb.selectedEnv;t&&(s.env=t);const r="system"===o?"/system/overview":`/${o}`;n.push(n.resolve(r,s))},async switchEnv(o){e.$ok.stores.okdb.selectEnv(o),e.$ok.shared?.toast?.info?.(`Environment: ${o}`)},async openCreateEnvModal(){await e.$ok.shared.modal.open({owner:e,title:"Create Environment",content:'<create-env-modal :modal="modal" :on-done="onDone" />',context:o=>({modal:o,onDone:async o=>{await e.$ok.stores.okdb.reloadEnvs(),e.$ok.stores.okdb.selectEnv(o),e.$ok.shared?.toast?.success?.(`Environment created: ${o}`)}})})},async openDemoModal(){await e.$ok.shared.modal.open({owner:e,title:'<span style="background:linear-gradient(135deg,#4FC1FF,#c084fc 50%,#4ade80);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;font-weight:700;font-size:15px;">✦ Demo Environment</span>',canDrag:!0,content:'<demo-env-modal :modal="modal" :on-done="onDone" />',context:o=>({modal:o,onDone:async o=>{await e.$ok.stores.okdb.reloadEnvs(),e.$ok.stores.okdb.selectEnv(o),e.$ok.shared?.toast?.success?.(`Demo environment "${o}" ready`)}})})}}),init(e){const o=()=>e.$ok.shared?.toast;e.$cache._unsubs=[s.on("type:registered",e=>o()?.info?.(`Type registered: ${e?.type??""}`)),s.on("type:drop",e=>o()?.info?.(`Type dropped: ${e?.type??""}`)),s.on("index:registered",e=>o()?.info?.(`Index registered on ${e?.type??""}: ${e?.key??""}`)),s.on("index:drop",e=>o()?.info?.(`Index dropped on ${e?.type??""}: ${e?.key??""}`)),s.on("fts:registered",e=>o()?.info?.(`FTS index created on ${e?.type??""}: ${e?.key??""}`)),s.on("fts:drop",e=>o()?.info?.(`FTS index dropped on ${e?.type??""}: ${e?.key??""}`)),s.on("env:opened",async n=>{o()?.info?.(`Environment ready: ${n?.name??""}`),await e.$ok.stores.okdb.reloadEnvs()}),s.on("env:removed",async n=>{o()?.info?.(`Environment removed: ${n?.name??""}`),await e.$ok.stores.okdb.reloadEnvs();const s=e.$ok.stores.okdb._pickBestEnv();s!==e.$ok.stores.okdb.selectedEnv&&e.$ok.stores.okdb.selectEnv(s)}),s.on("engine:started",e=>o()?.success?.(`Engine started: ${e?.name??""}`)),s.on("engine:stopped",e=>o()?.info?.(`Engine stopped: ${e?.name??""}`)),s.on("engine:error",e=>o()?.error?.(`Engine error: ${e?.name??""}: ${e?.error??""}`)),s.on("engine:installed",e=>o()?.success?.(`Engine installed: ${e?.name??""}`)),s.on("engine:uninstalled",e=>o()?.info?.(`Engine uninstalled: ${e?.name??""}`)),s.on("embeddings:pipeline:ready",e=>o()?.success?.(`Pipeline ready: ${e?.name??""}`))],e.$ext("unmount",()=>{for(const o of e.$cache._unsubs||[])o();e.$cache._unsubs=[]}),this.applyEnvFromSearch()||this.syncEnvSearch(e.$ok.stores.okdb.selectedEnv??null),e.$watch(()=>e.$vars.$router?._search?.env,()=>{const o=e.$vars.$router?._search?.env??null;o&&this.hasEnvOption(o)?e.$ok.stores.okdb.selectedEnv!==o&&e.$ok.stores.okdb.selectEnv(o):this.syncEnvSearch(e.$ok.stores.okdb.selectedEnv??null)}),e.$watch(()=>e.$ok.stores.okdb.selectedEnv,()=>this.syncEnvSearch(e.$ok.stores.okdb.selectedEnv??null))}};</script><template><div class="layout"><aside class="layout__sidebar"><div class="layout__sidebar-cube"><component import="./okdb-cube.ok.html"></component><span class="layout__version">v{{ $ok.stores.okdb.info?.version ?? '…' }}</span></div><div class="layout__env-picker"><env-picker :selected="$ok.stores.okdb.selectedEnv" :envs="$ok.stores.okdb.envs || []" @change="switchEnv($event.detail)" @create="openCreateEnvModal()"/></div><div class="layout__sidebar-body"><if :="$ok.stores.okdb.selectedEnv"><slot name="sidebar"></slot></if></div><div class="layout__sidebar-footer"><div class="layout__auth-chip layout__auth-chip--sidebar" :title="$ok.stores.auth?.modeLabel || 'Authentication'"><div class="layout__auth-meta"><div class="layout__auth-userblock"><div class="layout__auth-user">{{ $ok.stores.auth?.username || 'anonymous' }}</div><if :="$ok.stores.auth?.rolesLabel"><div class="layout__auth-roles">{{ $ok.stores.auth.rolesLabel }}</div></if></div></div><if :="!$ok.stores.auth?.openAccess"><button class="layout__auth-logout" @click="$ok.stores.auth.logout()">Logout</button></if></div></div></aside><div class="layout__right-col"><nav class="layout__section-nav"><button :class="{ active: $route.params.section === 'db' }" :disabled="!$ok.stores.auth.canSection('db')" :title="$ok.stores.auth.canSection('db') ? '' : 'Requires data:read or env:read permission'" @click="$ok.stores.auth.canSection('db') && goToSection('db')"><span class="layout__nav-icon">⬡</span> DB<if :="!$ok.stores.auth.canSection('db')"><span class="layout__nav-lock">🔒</span></if></button> <button :class="{ active: $route.params.section === 'engines' || $route.params.section === 'embeddings' }" :disabled="!$ok.stores.auth.canSection('engines')" :title="$ok.stores.auth.canSection('engines') ? '' : 'Requires env:read permission'" @click="$ok.stores.auth.canSection('engines') && goToSection('engines')"><span class="layout__nav-icon">⛓</span> Engines<if :="!$ok.stores.auth.canSection('engines')"><span class="layout__nav-lock">🔒</span></if></button> <button :class="{ active: $route.params.section === 'files' }" :disabled="!$ok.stores.auth.canSection('files')" :title="$ok.stores.auth.canSection('files') ? '' : 'Requires files:read permission'" @click="$ok.stores.auth.canSection('files') && goToSection('files')"><span class="layout__nav-icon">⬚</span> Files<if :="!$ok.stores.auth.canSection('files')"><span class="layout__nav-lock">🔒</span></if></button> <button :class="{ active: $route.params.section === 'queue' }" :disabled="!$ok.stores.auth.canSection('queue')" :title="$ok.stores.auth.canSection('queue') ? '' : 'Requires queue:read permission'" @click="$ok.stores.auth.canSection('queue') && goToSection('queue')"><span class="layout__nav-icon">⇶</span> Queue<if :="!$ok.stores.auth.canSection('queue')"><span class="layout__nav-lock">🔒</span></if></button> <button :class="{ active: $route.params.section === 'functions' }" :disabled="!$ok.stores.auth.canSection('functions')" :title="$ok.stores.auth.canSection('functions') ? '' : 'Requires functions:read permission'" @click="$ok.stores.auth.canSection('functions') && goToSection('functions')"><span class="layout__nav-icon">ƒ</span> Functions<if :="!$ok.stores.auth.canSection('functions')"><span class="layout__nav-lock">🔒</span></if></button> <button :class="{ active: $route.params.section === 'views' }" @click="goToSection('views')"><span class="layout__nav-icon">⊞</span> Views</button> <button :class="{ active: $route.params.section === 'system' }" :disabled="!$ok.stores.auth.canSection('system')" :title="$ok.stores.auth.canSection('system') ? '' : 'Requires system:read permission'" @click="$ok.stores.auth.canSection('system') && goToSection('system')"><span class="layout__nav-icon">⚙</span> System<if :="!$ok.stores.auth.canSection('system')"><span class="layout__nav-lock">🔒</span></if></button><div class="layout__live-indicator" :title="$ok.stores.events?.connected ? 'Event stream live' : 'Event stream disconnected'"><span :class="$ok.stores.events?.connected ? 'layout__live-dot layout__live-dot--on' : 'layout__live-dot layout__live-dot--off'"></span> <span class="layout__live-label">{{ $ok.stores.events?.connected ? 'Live' : 'Off' }}</span></div><div class="layout__security-pill" :title="($ok.stores.auth?.modeLabel || 'Authentication') + ' · ' + ($ok.stores.auth?.username || 'anonymous')"><span class="layout__security-dot" :class="$ok.stores.auth?.openAccess ? 'layout__security-dot layout__security-dot--open' : 'layout__security-dot layout__security-dot--secure'"></span> <span class="layout__security-label">{{ $ok.stores.auth?.openAccess ? 'Open access' : 'Secure' }}</span></div></nav><div class="layout__main"><if :="$ok.stores.auth?.visibleNotices?.length > 0"><div class="layout__notices"><div class="layout__notices-bar" @click="noticesExpanded = !noticesExpanded"><span class="layout__notices-icon">⚠</span> <span class="layout__notices-summary">{{ $ok.stores.auth.visibleNotices.length }} security notice{{ $ok.stores.auth.visibleNotices.length > 1 ? 's' : '' }}<if :="$ok.stores.auth.visibleNotices[0]">· {{ $ok.stores.auth.visibleNotices[0].title }}<if :="$ok.stores.auth.visibleNotices.length > 1">and {{ $ok.stores.auth.visibleNotices.length - 1 }} more</if></if></span><span class="layout__notices-chevron">{{ noticesExpanded ? '▴' : '▾' }}</span> <button class="layout__notices-dismiss" @click="dismissNotices($event)" title="Dismiss for this session">✕</button></div><if :="noticesExpanded"><div class="layout__notices-detail"><each :of="$ok.stores.auth.visibleNotices" let:item="n"><div class="layout__notice-item"><div class="layout__notice-title">{{ n.title }}</div><div class="layout__notice-msg">{{ n.message }}</div><if :="n.hint"><div class="layout__notice-hint">{{ n.hint }}</div></if><if :="n.action"><a class="layout__notice-action" :href="n.action.href">{{ n.action.label }} →</a></if></div></each></div></if></div></if><header class="layout__header"><slot name="header"></slot></header><main class="layout__content"><if :="$ok.stores.okdb.selectedEnv"><slot></slot></if><if :="!$ok.stores.okdb.selectedEnv"><div class="layout__no-env"><div class="layout__no-env-icon">⬡</div><div class="layout__no-env-title">No environment selected</div><div class="layout__no-env-sub">Create a new environment or load the demo to get started.</div><div class="layout__no-env-actions"><button @click="openCreateEnvModal()" class="layout__no-env-btn">+ Create environment</button> <button @click="openDemoModal()" class="layout__no-env-btn layout__no-env-btn--demo">✦ Load demo</button></div></div></if></main></div></div></div></template><style>.layout{display:flex;flex-direction:row;height:100%;width:100%;min-height:0}.layout__sidebar{display:flex;flex-direction:column;width:210px;flex-shrink:0;border-right:1px solid var(--okdb-app-surface-2);background:var(--okdb-app-bg-deep);overflow:visible}.layout__sidebar-cube{height:150px;flex-shrink:0;display:flex;align-items:center;justify-content:center;position:relative}.layout__version{position:absolute;bottom:8px;right:10px;font-size:10px;font-family:monospace;color:var(--okdb-app-fg-faint);letter-spacing:.04em;pointer-events:none;user-select:none;transition:color .2s}.layout__sidebar-cube:hover .layout__version{color:var(--okdb-app-fg-subtle)}.layout__env-picker{flex-shrink:0;padding:8px 10px 6px;border-top:1px solid var(--okdb-app-surface-2);border-bottom:1px solid var(--okdb-app-surface-2)}.layout__sidebar-body{flex:1;min-height:0;display:flex;flex-direction:column;gap:3px;padding:10px;border-top:1px solid var(--okdb-app-surface-2);overflow-y:auto;overflow-x:visible}.layout__sidebar-footer{flex-shrink:0;padding:8px 10px 10px;border-top:1px solid var(--okdb-app-surface-2);background:var(--okdb-app-bg-deep)}.layout__right-col{flex:1;min-width:0;display:flex;flex-direction:column;min-height:0}.layout__section-nav{display:flex;flex-direction:row;align-items:flex-end;height:150px;padding:0 8px;flex-shrink:0;background:var(--okdb-app-bg-deep);border-bottom:1px solid var(--okdb-app-surface-2)}.layout__section-nav button{display:flex;align-items:center;gap:7px;padding:0 16px;height:44px;border-radius:0;font-size:13px;font-weight:500;background:0 0;border:none;border-bottom:2px solid transparent;color:var(--okdb-app-fg-subtle);cursor:pointer;white-space:nowrap;letter-spacing:.01em;transition:color .15s,border-color .15s,background .15s;margin-bottom:-1px}.layout__section-nav button:hover{background:rgba(255,255,255,.03);border-color:transparent;color:var(--okdb-app-fg-muted);border-bottom-color:var(--okdb-app-border-subtle);transform:none;box-shadow:none}.layout__section-nav button:disabled{opacity:.35;cursor:not-allowed;background:0 0!important;border-color:transparent!important;border-bottom-color:transparent!important;color:var(--okdb-app-fg-faint)!important}.layout__section-nav button:disabled:hover{background:0 0;transform:none;box-shadow:none}.layout__section-nav button.active{background:0 0;border-color:transparent;color:var(--okdb-accent);border-bottom-color:var(--okdb-accent)}.layout__nav-icon{font-size:14px;opacity:.7;flex-shrink:0}.layout__nav-lock{font-size:10px;margin-left:4px;opacity:.7;flex-shrink:0;vertical-align:middle}.layout__section-nav button:hover .layout__nav-icon{opacity:.9}.layout__section-nav button.active .layout__nav-icon{opacity:1}.layout__live-indicator{margin-left:auto;margin-right:8px;margin-bottom:11px;display:flex;align-items:center;gap:5px;cursor:default}.layout__live-dot{width:7px;height:7px;border-radius:50%;display:inline-block;flex-shrink:0}.layout__live-dot--on{background:var(--okdb-success);box-shadow:0 0 5px color-mix(in srgb,var(--okdb-success) 40%,transparent);animation:nav-pulse 2.5s ease-in-out infinite}.layout__live-dot--off{background:var(--okdb-danger);opacity:.6}.layout__live-label{font-size:10px;color:var(--okdb-app-fg-faint);letter-spacing:.04em;text-transform:uppercase}@keyframes nav-pulse{0%,100%{opacity:1}50%{opacity:.35}}.layout__security-pill{margin-bottom:9px;display:inline-flex;align-items:center;gap:7px;padding:6px 10px;border-radius:999px;border:1px solid var(--okdb-app-border);background:var(--okdb-app-bg);color:var(--okdb-app-fg-muted)}.layout__security-dot{width:7px;height:7px;border-radius:50%;display:inline-block;flex-shrink:0}.layout__security-dot--secure{background:var(--okdb-accent);box-shadow:0 0 6px var(--okdb-accent-soft-border)}.layout__security-dot--open{background:var(--okdb-warning);box-shadow:0 0 6px color-mix(in srgb,var(--okdb-warning) 33%,transparent)}.layout__security-label{font-size:10px;line-height:1;text-transform:uppercase;letter-spacing:.08em;white-space:nowrap}.layout__auth-chip{margin-bottom:9px;display:inline-flex;align-items:center;gap:10px;padding:6px 10px;border-radius:10px;border:1px solid var(--okdb-app-border);background:var(--okdb-app-bg)}.layout__auth-chip--sidebar{width:100%;margin-bottom:0;align-items:center;justify-content:space-between;gap:12px;padding:0;border-radius:0;background:0 0;border:none;box-shadow:none}.layout__auth-meta{display:flex;align-items:center;gap:0;min-width:0;flex:1}.layout__auth-userblock{display:flex;flex-direction:column;min-width:0}.layout__auth-badge{font-size:10px;line-height:1;text-transform:uppercase;letter-spacing:.05em;padding:4px 7px;border-radius:999px;border:1px solid transparent;white-space:nowrap}.layout__auth-badge--open{color:var(--okdb-success);background:#4ade8011;border-color:#4ade8033}.layout__auth-badge--users{color:var(--okdb-accent);background:#4fc1FF11;border-color:#4fc1FF33}.layout__auth-user{font-size:11px;font-weight:600;color:var(--okdb-app-fg);line-height:1.1;text-transform:uppercase;letter-spacing:.05em}.layout__auth-roles{font-size:10px;color:var(--okdb-app-fg-muted);line-height:1.1;margin-top:2px;max-width:160px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.layout__auth-logout{font-size:11px;padding:0;border-radius:0;border:none;background:0 0;color:var(--okdb-danger);cursor:pointer;white-space:nowrap;align-self:center;letter-spacing:.04em;text-transform:uppercase;opacity:.88}.layout__auth-logout:hover{background:0 0;color:var(--okdb-danger-hover);opacity:1;transform:none;box-shadow:none}.layout__main{flex:1;display:flex;flex-direction:column;min-height:0;overflow:auto;padding:20px 24px 6px}.layout__header{flex-shrink:0}.layout__content{flex:1;min-height:0;min-width:0}.layout__content>*{animation:layout-fade-in .2s ease backwards}.layout__sidebar-body>*{animation:layout-fade-in .18s ease backwards}@keyframes layout-fade-in{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}.layout__no-env{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:12px;animation:layout-fade-in .25s ease backwards}.layout__no-env-icon{font-size:48px;opacity:.12;line-height:1;margin-bottom:4px}.layout__no-env-title{font-size:18px;font-weight:600;color:var(--okdb-app-fg-muted)}.layout__no-env-sub{font-size:13px;color:var(--okdb-app-fg-subtle);text-align:center;max-width:320px;line-height:1.6}.layout__no-env-actions{display:flex;gap:10px;margin-top:8px}.layout__no-env-btn{padding:8px 18px;font-size:13px;border-radius:7px;border:1px solid var(--okdb-app-border);background:var(--okdb-app-surface);color:var(--okdb-app-fg-subtle);cursor:pointer;transition:background .12s,border-color .12s,color .12s}.layout__no-env-btn:hover{background:var(--okdb-app-surface-2);border-color:var(--okdb-app-border-strong);color:var(--okdb-app-fg-muted);transform:none;box-shadow:none}.layout__no-env-btn--demo{border-color:var(--okdb-accent-2-soft-border);background:var(--okdb-accent-2-soft-bg);color:var(--okdb-accent-2)}.layout__no-env-btn--demo:hover{background:var(--okdb-accent-2-soft-bg);border-color:var(--okdb-accent-2-soft-border);color:var(--okdb-accent-2);transform:none;box-shadow:none}.layout__notices{flex-shrink:0;margin:-20px -24px 16px;border-bottom:1px solid color-mix(in srgb,var(--okdb-warning) 22%,transparent);background:color-mix(in srgb,var(--okdb-warning) 5%,transparent)}.layout__notices-bar{display:flex;align-items:center;gap:10px;padding:9px 16px;cursor:pointer;user-select:none;transition:background .12s}.layout__notices-bar:hover{background:color-mix(in srgb,var(--okdb-warning) 9%,transparent)}.layout__notices-icon{font-size:15px;color:var(--okdb-warning);flex-shrink:0}.layout__notices-summary{font-size:12px;color:var(--okdb-warning-text);flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.layout__notices-chevron{font-size:14px;color:var(--okdb-warning-text);flex-shrink:0;line-height:1}.layout__notices-dismiss{font-size:14px;color:var(--okdb-warning-text);background:0 0;border:none;cursor:pointer;padding:3px 7px;border-radius:4px;flex-shrink:0;line-height:1;transition:color .1s,background .1s}.layout__notices-dismiss:hover{color:var(--okdb-warning);background:color-mix(in srgb,var(--okdb-warning) 15%,transparent)}.layout__notices-detail{padding:0 16px 14px;display:flex;flex-direction:column;gap:10px}.layout__notice-item{padding:12px 14px;border:1px solid color-mix(in srgb,var(--okdb-warning) 20%,transparent);border-radius:6px;background:color-mix(in srgb,var(--okdb-warning) 5%,transparent);display:flex;flex-direction:column;gap:6px}.layout__notice-title{font-size:13px;font-weight:600;color:var(--okdb-warning)}.layout__notice-msg{font-size:12px;color:var(--okdb-warning-text);line-height:1.55}.layout__notice-hint{font-size:12px;color:var(--okdb-warning-text);line-height:1.55;font-style:italic}.layout__notice-action{font-size:12px;color:var(--okdb-accent);text-decoration:none;margin-top:2px;display:inline-block}.layout__notice-action:hover{text-decoration:underline}</style>
@@ -1 +1 @@
1
- export default{tag:"create-env-modal",attr:{modal:{},onDone:{}},context:e=>({busy:!1,name:"",sync:!0,compression:!0,error:null,get canSave(){return this.name.trim().length>0&&!this.busy},async save(){const n=this.name.trim();if(n){this.error=null,this.busy=!0;try{const t=await fetch(`${__ENV__.API}/envs`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:n,sync:this.sync,compression:this.compression})});if(!t.ok){const e=await t.json().catch(()=>({}));throw new Error(e?.error?.message||t.statusText)}await e.$attr.modal.close(),await(e.$attr.onDone?.(n))}catch(e){this.error=e?.message||"Failed to create environment"}finally{this.busy=!1}}},cancel(){e.$attr.modal.close()}}),template:'\n<div style="min-width:340px; display:flex; flex-direction:column; gap:14px;">\n\n <div style="display:flex; flex-direction:column; gap:4px;">\n <label style="font-size:12px; opacity:.6;">Environment name</label>\n <input\n type="text"\n placeholder="e.g. staging, analytics, archive"\n :value="name"\n @input="name = $el.value"\n @keydown.enter="save()"\n autofocus\n style="font-family:monospace;"\n />\n <div style="font-size:11px; opacity:.45; margin-top:2px;">\n Use letters, numbers, hyphens. Prefix with <code>~</code> for system envs.\n </div>\n </div>\n\n <div style="display:flex; flex-direction:column; gap:8px;">\n <label style="display:flex; align-items:center; gap:8px; cursor:pointer; font-size:13px; user-select:none;">\n <input type="checkbox" :checked="sync" @change="sync = $event.target.checked" />\n <span>Sync-enabled <span style="opacity:.5; font-size:11px;">(HLC clock + changelog)</span></span>\n </label>\n <label style="display:flex; align-items:center; gap:8px; cursor:pointer; font-size:13px; user-select:none;">\n <input type="checkbox" :checked="compression" @change="compression = $event.target.checked" />\n <span>Compression <span style="opacity:.5; font-size:11px;">(smaller on disk, slight CPU overhead)</span></span>\n </label>\n </div>\n\n <if :="error">\n <div style="color:var(--okdb-danger); font-size:13px; padding:6px 10px; background:var(--okdb-danger-deep-bg); border-radius:4px; border:1px solid color-mix(in srgb, var(--okdb-danger) 20%, transparent);">\n {{ error }}\n </div>\n </if>\n\n <div style="display:flex; gap:8px; justify-content:flex-end; padding-top:4px; border-top:1px solid var(--okdb-app-border);">\n <button @click="cancel()">Cancel</button>\n <button class="btn-primary" @click="save()" :disabled="!canSave">\n {{ busy ? \'Creating…\' : \'Create\' }}\n </button>\n </div>\n\n</div>'};
1
+ export default{tag:"create-env-modal",attr:{modal:{},onDone:{}},context:e=>({busy:!1,name:"",compression:!0,error:null,get canSave(){return this.name.trim().length>0&&!this.busy},async save(){const n=this.name.trim();if(n){this.error=null,this.busy=!0;try{const t=await fetch(`${__ENV__.API}/envs`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:n,compression:this.compression})});if(!t.ok){const e=await t.json().catch(()=>({}));throw new Error(e?.error?.message||t.statusText)}await e.$attr.modal.close(),await(e.$attr.onDone?.(n))}catch(e){this.error=e?.message||"Failed to create environment"}finally{this.busy=!1}}},cancel(){e.$attr.modal.close()}}),template:'\n<div style="min-width:340px; display:flex; flex-direction:column; gap:14px;">\n\n <div style="display:flex; flex-direction:column; gap:4px;">\n <label style="font-size:12px; opacity:.6;">Environment name</label>\n <input\n type="text"\n placeholder="e.g. staging, analytics, archive"\n :value="name"\n @input="name = $el.value"\n @keydown.enter="save()"\n autofocus\n style="font-family:monospace;"\n />\n <div style="font-size:11px; opacity:.45; margin-top:2px;">\n Use letters, numbers, hyphens. Prefix with <code>~</code> for system envs.\n </div>\n </div>\n\n <div style="display:flex; flex-direction:column; gap:8px;">\n <label style="display:flex; align-items:center; gap:8px; cursor:pointer; font-size:13px; user-select:none;">\n <input type="checkbox" :checked="compression" @change="compression = $event.target.checked" />\n <span>Compression <span style="opacity:.5; font-size:11px;">(smaller on disk, slight CPU overhead)</span></span>\n </label>\n <div style="font-size:11px; opacity:.45;">\n All user environments participate in sync replication (HLC clock + changelog) automatically.\n </div>\n </div>\n\n <if :="error">\n <div style="color:var(--okdb-danger); font-size:13px; padding:6px 10px; background:var(--okdb-danger-deep-bg); border-radius:4px; border:1px solid color-mix(in srgb, var(--okdb-danger) 20%, transparent);">\n {{ error }}\n </div>\n </if>\n\n <div style="display:flex; gap:8px; justify-content:flex-end; padding-top:4px; border-top:1px solid var(--okdb-app-border);">\n <button @click="cancel()">Cancel</button>\n <button class="btn-primary" @click="save()" :disabled="!canSave">\n {{ busy ? \'Creating…\' : \'Create\' }}\n </button>\n </div>\n\n</div>'};
@@ -1 +1 @@
1
- function makePRNG(e){let n=(2654435769^e)>>>0||1;return()=>(n^=n<<13,n^=n>>>17,n^=n<<5,(n>>>0)/4294967296)}const pick=(e,n)=>e[Math.floor(n()*e.length)],rng=(e,n,t)=>Math.floor(t()*(n-e+1))+e,uid=(e,n)=>`${e}-${String(n).padStart(4,"0")}`,email=(e,n,t)=>`${e.toLowerCase()}.${n.toLowerCase()}${rng(1,99,t)}@${pick(["gmail.com","yahoo.com","outlook.com","proton.me","icloud.com"],t)}`,dateAgo=(e,n)=>new Date(Date.now()-864e5*rng(0,e,n)).toISOString().split("T")[0],dateFwd=(e,n)=>new Date(Date.now()+864e5*rng(1,e,n)).toISOString().split("T")[0],FIRST=["Alice","Bob","Carol","David","Emma","Frank","Grace","Henry","Iris","Jack","Kate","Liam","Mia","Noah","Olivia","Peter","Quinn","Rachel","Sam","Tara","Uma","Victor","Wendy","Yusuf","Zoe"],LAST=["Anderson","Brown","Carter","Davis","Evans","Foster","Green","Hall","Ibrahim","Jones","King","Lee","Martinez","Nelson","Owen","Parker","Quinn","Roberts","Smith","Taylor","Vega","Walsh","Young","Zhang"],CITIES=["New York","London","Berlin","Tokyo","Sydney","Toronto","Paris","Amsterdam","Singapore","Dubai","Seoul","São Paulo","Mumbai","Cape Town","Vienna"],COUNTRIES=["US","GB","DE","FR","JP","AU","CA","BR","IN","SG","NL","SE","ES","IT","MX"],DEVICES=["desktop","mobile","tablet"],PAGES=["/","/products","/about","/pricing","/blog","/contact","/signup","/login","/dashboard","/settings","/docs"],PROD_ADJ=["Ultra","Pro","Elite","Max","Smart","Nano","Eco","Premium","Turbo","Flex","Swift","Vibe"],PROD_NOUN=["Headphones","Keyboard","Monitor","Backpack","Chair","Desk","Lamp","Camera","Speaker","Tablet","Watch","Router","Charger","Mouse","Stand","Pod","Hub","Dock"],PROD_CAT=["Electronics","Furniture","Clothing","Sports","Books","Kitchen","Garden","Toys","Health","Office"],ORDER_STATUS=["pending","processing","shipped","delivered","cancelled"],DOMAINS=["gmail.com","yahoo.com","outlook.com"],CITY_CENTERS=[{name:"London",country:"GB",lat:51.5074,lon:-.1278},{name:"Paris",country:"FR",lat:48.8566,lon:2.3522},{name:"New York",country:"US",lat:40.7128,lon:-74.006},{name:"Tokyo",country:"JP",lat:35.6762,lon:139.6503},{name:"Sydney",country:"AU",lat:-33.8688,lon:151.2093},{name:"Berlin",country:"DE",lat:52.52,lon:13.405},{name:"San Francisco",country:"US",lat:37.7749,lon:-122.4194},{name:"Amsterdam",country:"NL",lat:52.3676,lon:4.9041},{name:"Singapore",country:"SG",lat:1.3521,lon:103.8198},{name:"Barcelona",country:"ES",lat:41.3851,lon:2.1734},{name:"Dubai",country:"AE",lat:25.2048,lon:55.2708},{name:"Toronto",country:"CA",lat:43.6532,lon:-79.3832},{name:"Rome",country:"IT",lat:41.9028,lon:12.4964},{name:"Melbourne",country:"AU",lat:-37.8136,lon:144.9631},{name:"Seoul",country:"KR",lat:37.5665,lon:126.978}],PLACE_CATS=["restaurant","bar","cafe","museum","park","hotel","gym","shop"],PLACE_ADJ=["Rustic","Golden","Silver","Blue","Red","Garden","Harbor","Urban","Artisan","Wild","Local","Velvet","Cozy","Vintage","Hidden","Royal","Grand","Little","Green","Stone"],PLACE_NOUN=["Kitchen","Bistro","Table","Grove","Terrace","Corner","Nook","Den","House","Yard","Quarter","Loft","Market","Square","Room","Yard"],BAR_ADJ=["Lazy","Black","Iron","Brass","Copper","Dark","Old","Rusty","Jolly","Silver","Tipsy","Lucky"],BAR_ANIMAL=["Fox","Dog","Horse","Cat","Bear","Wolf","Owl","Lion","Eagle","Crow","Rabbit","Stag"],CAFE_PRE=["Morning","Daily","Urban","Artisan","Fresh","Local","Roasted","Ground","Wake","Open","Little","Slow"],CAFE_SFX=["Coffee","Roast","Brew","Cup","Bean","Grind","Blend","Espresso","Press","Filter"],MUSEUM_SUB=["Natural History","Modern Art","Contemporary Art","Science & Technology","City History","Photography","Architecture & Design","Maritime Heritage","Living Culture"],PARK_ADJ_P=["Central","Victoria","Riverside","Botanical","Royal","Memorial","Sunset","Green","City","Harbour","Highland","West"],HOTEL_ADJ_H=["Grand","Royal","Imperial","Garden","Harbor","City","Skyline","Heritage","Boutique","Maison","Palazzo"],HOTEL_NOUN_H=["Hotel","Suites","Inn","Residence","Lodge","Place","House","Retreat","Apartments"],GYM_PRE=["Fit","Peak","Core","Power","Active","Move","Lift","Pulse","Force","Prime"],GYM_SFX=["Studio","Club","Zone","Hub","Lab","Base","Works","Space","Performance"],SHOP_ADJ=["General","Local","Corner","Village","Urban","Artisan","Indie","Craft","Makers"],SHOP_NOUN=["Store","Shop","Market","Deli","Co-op","Goods","Supply","Emporium","Collective"],PLACE_DESCS=["Highly rated local favourite with excellent reviews and a warm, welcoming atmosphere.","A must-visit destination known for quality, character and outstanding hospitality.","Hidden gem tucked away in the heart of the city — loved by locals and visitors alike.","Modern establishment with a focus on quality and an unforgettable guest experience.","A community staple for years, proudly serving residents and visitors with heart.","Award-winning venue celebrating local culture, craft and genuine creativity.","Stylish space with impeccable service and an Instagram-worthy interior.","Popular spot among locals for its relaxed vibe and consistently high standards.","A vibrant, energetic space that buzzes with life from morning to night.","Intimate and unpretentious — the kind of place that instantly feels like home.","Iconic neighbourhood institution beloved for its authenticity and character.","Contemporary space blending design, sustainability and an outstanding experience."],PERSON_BIOS=["Passionate foodie and amateur photographer exploring the city one café at a time.","Tech worker who spends weekends hiking and discovering hidden restaurant gems.","Architecture enthusiast and frequent museum-goer with a love for strong espresso.","Local guide and travel blogger sharing the best spots in every city I visit.","Fitness fanatic, craft beer lover and weekend market explorer.","Remote worker building a list of the world's best coworking-friendly cafes.","Night owl and cocktail connoisseur — on a mission to try every great bar in town.","Museum member, park runner and enthusiastic brunch-seeker on weekends.","Lifelong traveller collecting experiences, flavours and stories from around the globe.","Urban explorer with a passion for hole-in-the-wall spots and authentic local culture."],CHECKIN_NOTES=["Amazing experience, will definitely be back!","Great atmosphere and incredibly friendly staff.","Highly recommend to anyone visiting the area.","A perfect spot for a relaxed afternoon.","One of my absolute favourites in the city.","Nice vibes but gets a bit crowded on weekends.","Exactly what I needed today — no complaints.","Good value for the quality of the experience.","Nothing groundbreaking but solid and reliable.","Exceptional quality — genuinely exceeded expectations.","Stumbled in by chance and absolutely loved it.","Great first visit, already planning to return.",null,null,null];function _scatter(e,n,t){return{lat:Math.round(1e6*(e.lat+2*(t()-.5)*n))/1e6,lon:Math.round(1e6*(e.lon+2*(t()-.5)*n))/1e6}}function _placeName(e,n){switch(e){case"restaurant":return`The ${pick(PLACE_ADJ,n)} ${pick(PLACE_NOUN,n)}`;case"bar":return`The ${pick(BAR_ADJ,n)} ${pick(BAR_ANIMAL,n)}`;case"cafe":return`${pick(CAFE_PRE,n)} ${pick(CAFE_SFX,n)}`;case"museum":return`Museum of ${pick(MUSEUM_SUB,n)}`;case"park":return`${pick(PARK_ADJ_P,n)} Park`;case"hotel":return`The ${pick(HOTEL_ADJ_H,n)} ${pick(HOTEL_NOUN_H,n)}`;case"gym":return`${pick(GYM_PRE,n)} ${pick(GYM_SFX,n)}`;case"shop":return`The ${pick(SHOP_ADJ,n)} ${pick(SHOP_NOUN,n)}`;default:return`${pick(PLACE_ADJ,n)} Place`}}function generateCityExplorer(e,n){const t={};return t.places=Array.from({length:e.places},(e,t)=>{const a=pick(CITY_CENTERS,n),o=pick(PLACE_CATS,n);return{key:uid("place",t+1),value:{name:_placeName(o,n),category:o,city:a.name,country:a.country,location:_scatter(a,.035,n),rating:Math.round(rng(28,50,n))/10,reviewCount:rng(5,3e3,n),priceLevel:rng(1,4,n),description:pick(PLACE_DESCS,n),phone:`+${rng(1,99,n)} ${rng(100,999,n)}-${rng(1e3,9999,n)}`,openNow:n()>.3,verified:n()>.25,createdAt:dateAgo(730,n)}}}),t.people=Array.from({length:e.people},(e,t)=>{const a=pick(CITY_CENTERS,n),o=pick(FIRST,n),r=pick(LAST,n);return{key:uid("person",t+1),value:{name:`${o} ${r}`,email:email(o,r,n),city:a.name,country:a.country,home:_scatter(a,.055,n),bio:pick(PERSON_BIOS,n),interests:[...new Set([pick(PLACE_CATS,n),pick(PLACE_CATS,n),pick(PLACE_CATS,n)])],memberSince:dateAgo(1095,n),checkinsCount:rng(0,90,n)}}}),t.checkins=Array.from({length:e.checkins},(t,a)=>({key:uid("ci",a+1),value:{personId:uid("person",rng(1,e.people,n)),placeId:uid("place",rng(1,e.places,n)),rating:rng(1,5,n),note:pick(CHECKIN_NOTES,n),hasPhoto:n()>.58,timestamp:new Date(Date.now()-rng(0,31536e6,n)).toISOString()}})),t}const POST_TITLES=["Getting Started with Modern Web Development","The Complete Guide to Database Optimization","Building Scalable APIs with Node.js","Understanding Event-Driven Architecture","Top 10 UX Patterns for Mobile Apps","Deep Dive into Machine Learning Basics","Docker and Kubernetes: A Practical Guide","Functional Programming in JavaScript","The Future of Distributed Systems","Mastering TypeScript in 2025","Building Real-time Dashboards","Microservices vs Monolith: When to Choose","GraphQL Best Practices for Production","State Management in Modern Frontend","Observability and Monitoring at Scale","Secure by Design: API Security Patterns","WebAssembly: The Next Frontier","Green Software Engineering Principles","AI-Assisted Development Workflows","Database Indexing Deep Dive"],TAG_NAMES=["javascript","react","node","css","typescript","python","docker","aws","graphql","ux","design","databases","security","mobile","devops","architecture","testing","performance","accessibility","seo","analytics","api","open-source","career","tools","tutorial","beginner","advanced"],POST_CATS=["Technology","Design","Business","Tutorial","Opinion","News","Research"],COMMENT_BODIES=["Great article! Really helped me understand the concept.","Thanks for sharing this. Very insightful.","Could you elaborate more on the second point?","This is exactly what I was looking for!","Bookmarked. Will definitely revisit this later.","The examples were very clear. Well done!","I disagree with some points but overall great read.","Would love to see a follow-up article on this topic.","Just implemented this — worked perfectly!","Saved me hours of debugging. Thank you!"],EVT_TYPES=["pageview","click","scroll","form_submit","purchase","signup","login","logout","search","download","video_play","share"],REFERRERS=["google.com","twitter.com","github.com","direct","reddit.com","hackernews.com","linkedin.com"],ROLES=["Engineer","Designer","Manager","Analyst","Director","Lead","Coordinator","Specialist","Architect","Consultant"],DEPTS=["Engineering","Design","Product","Marketing","Sales","Operations","HR","Finance"],LEVELS=["junior","mid","senior","lead","principal","director"],SKILLS=["JavaScript","Python","React","Node.js","SQL","Docker","AWS","TypeScript","GraphQL","Kubernetes","Figma","Analytics","Leadership","Communication","Rust","Go","Swift","Kotlin"],PROJ_NAMES=["Platform Rewrite","Mobile App v2","API Gateway","Data Pipeline","Customer Portal","Analytics Dashboard","Auth Service","Notification System","Search Engine","Billing Integration","Admin Console","CI/CD Overhaul","Performance Audit","Security Hardening","Developer SDK","Internal Tooling","Database Migration","Load Testing","Documentation Site","Onboarding Flow","A/B Testing Framework","Design System","Release Automation","Partner API","Data Warehouse"],PROJ_STATUS=["planning","in-progress","review","completed","on-hold"];function generateEcommerce(e,n){const t={};return t.products=Array.from({length:e.products},(e,t)=>{const a=pick(PROD_ADJ,n),o=pick(PROD_NOUN,n);return{key:uid("prod",t+1),value:{name:`${a} ${o}`,category:pick(PROD_CAT,n),price:Math.round(100*(rng(5,999,n)+.99*n()))/100,stock:rng(0,500,n),rating:Math.round(rng(25,50,n))/10,reviewCount:rng(0,1200,n),description:`The ${a} ${o} is engineered for peak performance and everyday reliability. Features premium materials and an intuitive design.`,sku:`SKU-${rng(1e4,99999,n)}`,brand:pick(["TechCore","UrbanGear","Nomad","Apex","Lux"],n),createdAt:dateAgo(365,n)}}}),t.customers=Array.from({length:e.customers},(e,t)=>{const a=pick(FIRST,n),o=pick(LAST,n);return{key:uid("cust",t+1),value:{name:`${a} ${o}`,email:email(a,o,n),city:pick(CITIES,n),country:pick(COUNTRIES,n),tier:pick(["bronze","silver","gold","platinum"],n),totalOrders:rng(0,50,n),totalSpent:Math.round(100*rng(0,15e3,n))/100,joinDate:dateAgo(730,n),newsletter:n()>.4}}}),t.orders=Array.from({length:e.orders},(t,a)=>{const o=rng(1,5,n),r=Array.from({length:o},()=>{const t=Math.round(100*rng(5,499,n))/100,a=rng(1,4,n);return{productId:uid("prod",rng(1,e.products,n)),qty:a,price:t,total:Math.round(t*a*100)/100}});return{key:uid("ord",a+1),value:{customerId:uid("cust",rng(1,e.customers,n)),status:pick(ORDER_STATUS,n),items:r,subtotal:r.reduce((e,n)=>e+n.total,0),shipping:Math.round(100*rng(0,20,n))/100,total:r.reduce((e,n)=>e+n.total,0)+Math.round(100*rng(0,20,n))/100,createdAt:dateAgo(180,n),shippedAt:n()>.3?dateAgo(60,n):null,address:{city:pick(CITIES,n),country:pick(COUNTRIES,n)}}}}),t.reviews=Array.from({length:e.reviews},(t,a)=>{const o=rng(1,5,n);return{key:uid("rev",a+1),value:{productId:uid("prod",rng(1,e.products,n)),customerId:uid("cust",rng(1,e.customers,n)),stars:o,title:pick(o>=4?["Love it!","Great quality","Exactly as described","Highly recommend","Excellent value"]:["Disappointed","Not worth it","Expected more","Decent but flawed"],n),body:`${o>=4?"Really happy with this purchase.":"Decent but has some issues."} ${pick(["Would recommend to friends.","Using it every day.","Still testing it out.","Mixed feelings overall.","Might return it."],n)}`,verified:n()>.2,helpful:rng(0,200,n),createdAt:dateAgo(365,n)}}}),t}function generateAnalytics(e,n){const t={};return t.sessions=Array.from({length:e.sessions},(e,t)=>({key:uid("sess",t+1),value:{userId:n()>.35?uid("user",rng(1,500,n)):null,device:pick(DEVICES,n),country:pick(COUNTRIES,n),city:pick(CITIES,n),duration:rng(5,3600,n),pageCount:rng(1,30,n),bounced:n()<.38,browser:pick(["Chrome","Firefox","Safari","Edge","Arc"],n),startedAt:new Date(Date.now()-rng(0,2592e6,n)).toISOString()}})),t.events=Array.from({length:e.events},(t,a)=>({key:uid("evt",a+1),value:{type:pick(EVT_TYPES,n),sessionId:uid("sess",rng(1,e.sessions,n)),userId:n()>.35?uid("user",rng(1,500,n)):null,page:pick(PAGES,n),device:pick(DEVICES,n),country:pick(COUNTRIES,n),timestamp:new Date(Date.now()-rng(0,2592e6,n)).toISOString(),value:n()>.7?Math.round(100*rng(1,1e4,n))/100:null}})),t.pageviews=Array.from({length:e.pageviews},(t,a)=>({key:uid("pv",a+1),value:{page:pick(PAGES,n),sessionId:uid("sess",rng(1,e.sessions,n)),referrer:pick(REFERRERS,n),timeOnPage:rng(0,600,n),exitPage:n()>.7,timestamp:new Date(Date.now()-rng(0,2592e6,n)).toISOString()}})),t}function generateBlog(e,n){const t={};t.authors=Array.from({length:e.authors},(e,t)=>{const a=pick(FIRST,n),o=pick(LAST,n),r=pick(["Frontend","Backend","DevOps","Design","Data Science","Mobile","Security","Cloud"],n);return{key:uid("auth",t+1),value:{name:`${a} ${o}`,email:email(a,o,n),bio:`${a} is a ${r} specialist with ${rng(2,18,n)} years of experience building production systems.`,specialty:r,location:pick(CITIES,n),twitter:`@${a.toLowerCase()}${o.toLowerCase()}`,postCount:0}}});const a=Math.min(e.tags,TAG_NAMES.length);return t.tags=Array.from({length:a},(e,t)=>({key:uid("tag",t+1),value:{name:TAG_NAMES[t],count:rng(5,120,n),color:pick(["var(--okdb-accent)","var(--okdb-purple)","var(--okdb-success)","var(--okdb-warning)","var(--okdb-danger)"],n)}})),t.posts=Array.from({length:e.posts},(t,o)=>{const r=POST_TITLES[o%POST_TITLES.length],s=pick(["","","","Deep Dive: ","The Ultimate ","A Practical Guide to ","Why "],n),i=s?`${s}${r}`:r,l=n()>.25?"published":pick(["draft","archived"],n);return{key:uid("post",o+1),value:{title:i,slug:i.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,""),authorId:uid("auth",rng(1,e.authors,n)),category:pick(POST_CATS,n),status:l,tags:[...new Set([uid("tag",rng(1,a,n)),uid("tag",rng(1,a,n))])],content:"This article covers the essentials you need to know. We explore key concepts, practical examples, and real-world applications that you can apply immediately. Whether you're a beginner or an experienced professional, you'll find actionable insights here.",excerpt:`A comprehensive guide to understanding ${r.toLowerCase()} in depth — with practical examples.`,readTime:rng(2,22,n),views:rng(50,8e4,n),likes:rng(5,3e3,n),featured:n()>.85,publishedAt:"published"===l?dateAgo(400,n):null,createdAt:dateAgo(450,n)}}}),t.comments=Array.from({length:e.comments},(t,a)=>({key:uid("cmt",a+1),value:{postId:uid("post",rng(1,e.posts,n)),authorName:`${pick(FIRST,n)} ${pick(LAST,n)}`,email:`user${rng(1,9999,n)}@${pick(DOMAINS,n)}`,body:pick(COMMENT_BODIES,n),likes:rng(0,150,n),approved:n()>.1,createdAt:dateAgo(365,n)}})),t}function generateTeam(e,n){const t={},a=Array.from({length:e.departments},(e,n)=>uid("dept",n+1));return t.departments=a.map((e,t)=>({key:e,value:{name:DEPTS[t%DEPTS.length],head:"",headcount:rng(4,35,n),budget:rng(1e5,3e6,n),location:pick(CITIES,n),description:`The ${DEPTS[t%DEPTS.length]} team drives innovation and ${pick(["growth","quality","efficiency","culture"],n)} across the organisation.`}})),t.employees=Array.from({length:e.employees},(e,t)=>{const o=pick(FIRST,n),r=pick(LAST,n);return{key:uid("emp",t+1),value:{name:`${o} ${r}`,email:email(o,r,n),role:pick(ROLES,n),department:pick(a,n),level:pick(LEVELS,n),skills:[...new Set([pick(SKILLS,n),pick(SKILLS,n),pick(SKILLS,n)])],salary:rng(48e3,28e4,n),hireDate:dateAgo(1825,n),manager:t>0?uid("emp",rng(1,Math.max(1,t),n)):null,location:pick(CITIES,n),remote:n()>.45}}}),t.projects=Array.from({length:e.projects},(t,o)=>{const r=rng(2,8,n);return{key:uid("proj",o+1),value:{name:PROJ_NAMES[o%PROJ_NAMES.length],department:pick(a,n),status:pick(PROJ_STATUS,n),lead:uid("emp",rng(1,e.employees,n)),team:[...new Set(Array.from({length:r},()=>uid("emp",rng(1,e.employees,n))))],budget:rng(1e4,6e5,n),startDate:dateAgo(365,n),deadline:dateFwd(180,n),progress:rng(0,100,n),priority:pick(["low","medium","high","critical"],n),description:`${PROJ_NAMES[o%PROJ_NAMES.length]} is a strategic initiative to improve ${pick(["reliability","performance","user experience","developer productivity","security"],n)}.`}}}),t}const PRESETS=[{id:"ecommerce",icon:"🛒",name:"E-Commerce",tagline:"Products, customers & orders",color:"var(--okdb-success)",glow:"color-mix(in srgb, var(--okdb-success) 20%, transparent)",defaultEnv:"demo-shop",types:[{key:"products",label:"Products",defaultCount:250},{key:"customers",label:"Customers",defaultCount:100},{key:"orders",label:"Orders",defaultCount:500},{key:"reviews",label:"Reviews",defaultCount:300}],indexes:[["products","category"],["products","price"],["customers","city"],["customers","tier"],["orders","status"],["orders","customerId"]],fts:[{type:"products",name:"search",fields:["name","description"]}],generate:generateEcommerce},{id:"analytics",icon:"📊",name:"Analytics",tagline:"Events, sessions & pageviews",color:"var(--okdb-accent)",glow:"color-mix(in srgb, var(--okdb-accent) 20%, transparent)",defaultEnv:"demo-analytics",types:[{key:"events",label:"Events",defaultCount:1e3},{key:"sessions",label:"Sessions",defaultCount:200},{key:"pageviews",label:"Pageviews",defaultCount:2e3}],indexes:[["events","type"],["events","country"],["sessions","userId"],["sessions","device"],["pageviews","page"],["pageviews","sessionId"]],fts:[],generate:generateAnalytics},{id:"blog",icon:"📝",name:"Content Hub",tagline:"Posts, authors & comments",color:"var(--okdb-purple)",glow:"color-mix(in srgb, var(--okdb-purple) 20%, transparent)",defaultEnv:"demo-blog",types:[{key:"posts",label:"Posts",defaultCount:150},{key:"authors",label:"Authors",defaultCount:20},{key:"tags",label:"Tags",defaultCount:28},{key:"comments",label:"Comments",defaultCount:400}],indexes:[["posts","category"],["posts","authorId"],["posts","status"],["comments","postId"],["tags","count"]],fts:[{type:"posts",name:"search",fields:["title","content"]}],generate:generateBlog},{id:"team",icon:"👥",name:"Team Directory",tagline:"Employees, departments & projects",color:"var(--okdb-warning)",glow:"color-mix(in srgb, var(--okdb-warning) 20%, transparent)",defaultEnv:"demo-team",types:[{key:"employees",label:"Employees",defaultCount:80},{key:"departments",label:"Departments",defaultCount:8},{key:"projects",label:"Projects",defaultCount:30}],indexes:[["employees","department"],["employees","level"],["employees","location"],["projects","status"],["projects","department"]],fts:[{type:"employees",name:"search",fields:["name","role","skills"]}],generate:generateTeam},{id:"cityexplorer",icon:"🗺️",name:"City Explorer",tagline:"Geo-indexed places & people",color:"var(--okdb-orange)",glow:"color-mix(in srgb, var(--okdb-orange) 20%, transparent)",defaultEnv:"demo-places",badge:"geo",types:[{key:"places",label:"Places",defaultCount:200},{key:"people",label:"People",defaultCount:80},{key:"checkins",label:"Checkins",defaultCount:300}],indexes:[["places","location",{type:"geo",precision:7}],["places","category"],["places","city"],["places","rating"],["people","home",{type:"geo",precision:5}],["people","city"],["checkins","personId"],["checkins","placeId"]],fts:[{type:"places",name:"search",fields:["name","description"]}],generate:generateCityExplorer}],SCALE_OPTIONS=[{value:.25,label:"¼×",hint:"Tiny"},{value:.5,label:"½×",hint:"Small"},{value:1,label:"1×",hint:"Normal"},{value:2,label:"2×",hint:"Large"},{value:4,label:"4×",hint:"Huge"}],BATCH_SIZE=50;export default{tag:"demo-env-modal",attr:{modal:{},onDone:{}},context:e=>({step:"choose",preset:null,envName:"",scale:1,enableFts:!0,enableIndexes:!0,typeCounts:{},progress:[],totalSteps:0,completedSteps:0,error:null,resultStats:null,PRESETS:PRESETS,SCALE_OPTIONS:SCALE_OPTIONS,get effectiveCounts(){const e={};for(const n of this.preset?.types??[])e[n.key]=Math.max(1,Math.round((this.typeCounts[n.key]??n.defaultCount)*this.scale));return e},get totalRecords(){return Object.values(this.effectiveCounts).reduce((e,n)=>e+n,0)},get progressPct(){return this.totalSteps?Math.min(100,Math.round(this.completedSteps/this.totalSteps*100)):0},selectPreset(e){this.preset=e,this.envName=e.defaultEnv,this.typeCounts={};for(const n of e.types)this.typeCounts[n.key]=n.defaultCount;this.step="configure"},back(){"configure"===this.step&&(this.step="choose")},cancel(){e.$attr.modal.close()},_addStep(e,n,t,a){this.progress.push({id:e,label:n,done:0,total:t,icon:a,status:"pending"}),this.totalSteps++},_start(e){const n=this.progress.find(n=>n.id===e);n&&(n.status="running")},_done(e){const n=this.progress.find(n=>n.id===e);n&&(n.status="done",n.done=n.total,this.completedSteps++)},_error(e){const n=this.progress.find(n=>n.id===e);n&&(n.status="error",this.completedSteps++)},_tick(e,n){const t=this.progress.find(n=>n.id===e);t&&(t.done=n)},async start(){if("generating"===this.step)return;const e=this.envName.trim();if(!e||!this.preset)return;this.step="generating",this.progress=[],this.totalSteps=0,this.completedSteps=0,this.error=null,this.resultStats={env:e,types:{},indexes:0,fts:0,totalRecords:0};const n=this.preset,t=this.effectiveCounts,a=__ENV__.API,o=`${a}/env/${encodeURIComponent(e)}`;this._addStep("env",`Create environment "${e}"`,1,"🌐");for(const e of n.types)this._addStep(`type:${e.key}`,`Register type: ${e.key}`,1,"⬡");for(const e of n.types)this._addStep(`data:${e.key}`,`Insert ${t[e.key].toLocaleString()} ${e.label.toLowerCase()}`,t[e.key],"⬢");if(this.enableIndexes)for(const[e,t,a]of n.indexes){const n="geo"===a?.type;this._addStep(`idx:${e}:${t}`,`${n?"📍 Geo index":"Index"} ${e}.${t}`,1,n?"📍":"⌖")}if(this.enableFts&&n.fts.length)for(const e of n.fts)this._addStep(`fts:${e.type}:${e.name}`,`FTS "${e.name}" on ${e.type}`,1,"⌘");try{this._start("env");const r=await fetch(`${a}/envs`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:e,sync:!0,compression:!0})});if(!r.ok&&409!==r.status){const e=await r.json().catch(()=>({}));throw new Error(e?.error?.message||`Create env failed (${r.status})`)}this._done("env");for(const e of n.types){this._start(`type:${e.key}`);const n=await fetch(`${o}/type/${encodeURIComponent(e.key)}`,{method:"POST"});if(!n.ok&&409!==n.status){const t=await n.json().catch(()=>({}));throw new Error(t?.error?.message||`Register type "${e.key}" failed`)}this._done(`type:${e.key}`)}const s=makePRNG(2147483647&Date.now()),i=n.generate(t,s);for(const e of n.types){const n=`data:${e.key}`,t=i[e.key]??[];this._start(n);let a=0;for(let r=0;r<t.length;r+=50){const s=t.slice(r,r+50),i=s.map(({key:n,value:t})=>({action:"put",type:e.key,key:n,value:t})),l=await fetch(`${o}/transaction`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({operations:i})});if(!l.ok){const n=await l.json().catch(()=>({}));throw new Error(n?.error?.message||`Insert into "${e.key}" failed`)}a+=s.length,this._tick(n,a),await new Promise(e=>setTimeout(e,0))}this.resultStats.types[e.key]=t.length,this.resultStats.totalRecords+=t.length,this._done(n)}if(this.enableIndexes)for(const[e,t,a]of n.indexes){const n=`idx:${e}:${t}`;this._start(n);const r=await fetch(`${o}/type/${encodeURIComponent(e)}/index/${encodeURIComponent(t)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(a||{})});if(!r.ok){const n=await r.json().catch(()=>({}));throw new Error(n?.error?.message||`Index ${e}.${t} failed`)}this.resultStats.indexes++,this._done(n)}if(this.enableFts)for(const e of n.fts){const n=`fts:${e.type}:${e.name}`;this._start(n),(await fetch(`${o}/type/${encodeURIComponent(e.type)}/fts/${encodeURIComponent(e.name)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({fields:e.fields})})).ok?(this.resultStats.fts++,this._done(n)):this._error(n)}this.step="done"}catch(e){this.error=e?.message||"Generation failed";for(const e of this.progress)"running"===e.status&&this._error(e.id)}},async openEnv(){const n=this.envName.trim();e.$ok.stores.okdb.selectEnv(n),await e.$ok.stores.okdb.reloadEnvs(),await e.$attr.modal.close(),await(e.$attr.onDone?.(n))}}),template:'\n<div class="dm">\n\n \x3c!-- ══ STEP: Choose preset ══════════════════════════════════════════════════ --\x3e\n <if :="step === \'choose\'">\n <div class="dm-hero">\n <div class="dm-hero__sparkle">✦</div>\n <div class="dm-hero__title">Generate Demo Environment</div>\n <div class="dm-hero__sub">\n Choose a preset to instantly fill a new environment with realistic data,\n indexes, and full-text search — ready to explore in seconds.\n </div>\n </div>\n\n <div class="dm-presets">\n <button as="repeat" :of="PRESETS" let:item="p"\n class="dm-preset"\n :class="p.badge === \'geo\' ? \'dm-preset--geo\' : \'\'"\n @click="selectPreset(p)"\n :style="\'--pc:\' + p.color + \'; --pg:\' + p.glow + \';\'">\n <div class="dm-preset__bg"></div>\n <div class="dm-preset__icon">{{ p.icon }}</div>\n <div class="dm-preset__name">{{ p.name }}</div>\n <div class="dm-preset__tag">{{ p.tagline }}</div>\n <div class="dm-preset__chips">\n <span>{{ p.types.length }} types</span>\n <span class="dm-chip-dot">·</span>\n <span>{{ p.types.reduce((s,t)=>s+t.defaultCount,0).toLocaleString() }}+ records</span>\n <if :="p.fts.length">\n <span class="dm-chip-dot">·</span>\n <span>FTS</span>\n </if>\n <if :="p.badge === \'geo\'">\n <span class="dm-chip-dot">·</span>\n <span style="color:var(--pc); font-weight:600;">📍 GEO</span>\n </if>\n </div>\n <div class="dm-preset__arrow">→</div>\n </button>\n </div>\n\n <div class="dm-footer">\n <button @click="cancel()">Cancel</button>\n </div>\n </if>\n\n \x3c!-- ══ STEP: Configure ══════════════════════════════════════════════════════ --\x3e\n <if :="step === \'configure\'">\n <div style="display:flex; flex-direction:column; gap:18px;">\n\n <div style="display:flex; align-items:center; gap:10px;">\n <button @click="back()" class="dm-back-btn" title="Back to presets">←</button>\n <span class="dm-preset-badge" :style="\'--pc:\' + preset.color + \'; --pg:\' + preset.glow + \';\'">\n {{ preset.icon }}\n </span>\n <span style="font-weight:700; font-size:15px;">{{ preset.name }}</span>\n <span style="opacity:.4; font-size:12px;">· Configure</span>\n </div>\n\n \x3c!-- Env name --\x3e\n <div style="display:flex; flex-direction:column; gap:5px;">\n <label style="font-size:12px; opacity:.55; letter-spacing:.03em;">Environment name</label>\n <input type="text" :value="envName" @input="envName = $el.value"\n style="font-family:monospace; font-size:13px;" autofocus\n placeholder="e.g. demo-shop" />\n </div>\n\n \x3c!-- Scale --\x3e\n <div style="display:flex; flex-direction:column; gap:8px;">\n <label style="font-size:12px; opacity:.55; letter-spacing:.03em;">Dataset scale</label>\n <div style="display:flex; gap:6px; flex-wrap:wrap;">\n <button as="repeat" :of="SCALE_OPTIONS" let:item="s"\n @click="scale = s.value"\n class="dm-scale-btn"\n :class="scale === s.value ? \'dm-scale-btn--active\' : \'\'"\n :style="scale === s.value ? \'--pc:\' + preset.color : \'\'">\n {{ s.label }}&nbsp;<span style="font-size:10px; opacity:.55;">{{ s.hint }}</span>\n </button>\n </div>\n </div>\n\n \x3c!-- Type counts --\x3e\n <div style="display:flex; flex-direction:column; gap:5px;">\n <label style="font-size:12px; opacity:.55; letter-spacing:.03em;">Types &amp; counts</label>\n <div class="dm-type-table">\n <div as="repeat" :of="preset.types" let:item="t" class="dm-type-row">\n <span class="dm-type-row__name">{{ t.key }}</span>\n <div class="dm-type-row__bar-wrap">\n <div class="dm-type-row__bar"\n :style="\'width:\' + Math.min(100, Math.round((typeCounts[t.key] ?? t.defaultCount) * scale / (t.defaultCount * 4) * 100)) + \'%; background:\' + preset.color + \'55;\'">\n </div>\n </div>\n <span class="dm-type-row__count" :style="\'color:\' + preset.color + \';\'">\n {{ Math.max(1, Math.round((typeCounts[t.key] ?? t.defaultCount) * scale)).toLocaleString() }}\n </span>\n </div>\n </div>\n </div>\n\n \x3c!-- Options --\x3e\n <div style="display:flex; gap:20px; flex-wrap:wrap;">\n <label class="dm-check-label">\n <input type="checkbox" :checked="enableIndexes" @change="enableIndexes = $el.checked" />\n <span>Indexes <span style="opacity:.4; font-size:11px;">({{ preset.indexes.length }})</span></span>\n </label>\n <label class="dm-check-label" :style="!preset.fts.length ? \'opacity:.4; pointer-events:none;\' : \'\'">\n <input type="checkbox" :checked="enableFts" @change="enableFts = $el.checked" :disabled="!preset.fts.length" />\n <span>Full-text search <span style="opacity:.4; font-size:11px;">({{ preset.fts.length }})</span></span>\n </label>\n </div>\n\n \x3c!-- Summary pill --\x3e\n <div class="dm-summary" :style="\'border-color:\' + preset.color + \'33; background:\' + preset.glow + \';\'">\n <span style="opacity:.7;">Will create</span>\n <b :style="\'color:\' + preset.color + \';\'">{{ totalRecords.toLocaleString() }}</b>\n <span style="opacity:.7;">records across</span>\n <b>{{ preset.types.length }}</b>\n <span style="opacity:.7;">types</span>\n <if :="enableIndexes && preset.indexes.length">\n <span style="opacity:.4;">·</span>\n <b style="color:var(--okdb-purple);">{{ preset.indexes.length }}</b>\n <span style="opacity:.7;">indexes</span>\n </if>\n <if :="enableFts && preset.fts.length">\n <span style="opacity:.4;">·</span>\n <b style="color:var(--okdb-accent);">FTS</b>\n </if>\n <span style="opacity:.4;">→</span>\n <b style="font-family:monospace; font-size:12px;">{{ envName || \'…\' }}</b>\n </div>\n\n <div class="dm-footer">\n <button @click="cancel()">Cancel</button>\n <button class="dm-gen-btn" :disabled="!envName.trim()" @click="start()"\n :style="\'--pc:\' + preset.color + \'; --pg:\' + preset.glow + \';\'">\n ✦ Generate {{ totalRecords.toLocaleString() }} records\n </button>\n </div>\n </div>\n </if>\n\n \x3c!-- ══ STEP: Generating ═════════════════════════════════════════════════════ --\x3e\n <if :="step === \'generating\'">\n <div style="display:flex; flex-direction:column; gap:14px;">\n\n \x3c!-- Header / overall progress --\x3e\n <div class="dm-gen-header" :style="\'--pc:\' + preset.color + \'; --pg:\' + preset.glow + \';\'">\n <div style="display:flex; align-items:center; gap:10px; margin-bottom:12px;">\n <span class="dm-pulsing-icon">{{ preset.icon }}</span>\n <span style="font-weight:600; font-size:14px;">Generating {{ preset.name }} data…</span>\n <span style="margin-left:auto; font-family:monospace; font-size:13px; opacity:.55;">{{ progressPct }}%</span>\n </div>\n <div class="dm-progress-track">\n <div class="dm-progress-fill dm-progress-fill--shimmer"\n :style="\'width:\' + progressPct + \'%; background:\' + preset.color + \';\'">\n </div>\n </div>\n </div>\n\n \x3c!-- Per-step rows --\x3e\n <div class="dm-steps">\n <div as="repeat" :of="progress" let:item="p"\n class="dm-step"\n :class="\'dm-step--\' + p.status">\n <span class="dm-step__icon">\n <if :="p.status === \'running\'"><span class="dm-spin">◌</span></if>\n <if :="p.status === \'done\'">✓</if>\n <if :="p.status === \'error\'">✗</if>\n <if :="p.status === \'pending\'">{{ p.icon }}</if>\n </span>\n <span class="dm-step__label">{{ p.label }}</span>\n <if :="p.total > 1 && p.status !== \'pending\'">\n <span class="dm-step__count">{{ p.done.toLocaleString() }} / {{ p.total.toLocaleString() }}</span>\n </if>\n <if :="p.total > 1 && p.status === \'running\'">\n <div class="dm-step__mini-bar">\n <div class="dm-step__mini-fill"\n :style="\'width:\' + (p.total ? Math.round(p.done/p.total*100) : 0) + \'%;\'">\n </div>\n </div>\n </if>\n </div>\n </div>\n\n <if :="error">\n <div class="dm-error">✗ {{ error }}</div>\n <div class="dm-footer"><button @click="cancel()">Close</button></div>\n </if>\n\n </div>\n </if>\n\n \x3c!-- ══ STEP: Done ═══════════════════════════════════════════════════════════ --\x3e\n <if :="step === \'done\'">\n <div style="display:flex; flex-direction:column; align-items:center; gap:22px; padding:12px 0 6px;">\n\n <div class="dm-done-orb" :style="\'--pc:\' + preset.color + \'; --pg:\' + preset.glow + \';\'">\n <span class="dm-done-orb__icon">{{ preset.icon }}</span>\n <div class="dm-done-orb__ring dm-done-orb__ring--1"></div>\n <div class="dm-done-orb__ring dm-done-orb__ring--2"></div>\n </div>\n\n <div style="text-align:center;">\n <div style="font-size:22px; font-weight:700; margin-bottom:8px;">Environment Ready!</div>\n <div style="font-size:13px; font-family:monospace; opacity:.5; letter-spacing:.05em;">{{ resultStats?.env }}</div>\n </div>\n\n <div class="dm-done-stats" :style="\'--pc:\' + preset.color + \'; --pg:\' + preset.glow + \';\'">\n <div class="dm-stat">\n <div class="dm-stat__val" style="color:var(--okdb-success);">{{ resultStats?.totalRecords?.toLocaleString() }}</div>\n <div class="dm-stat__lbl">Records</div>\n </div>\n <div class="dm-stat-divider"></div>\n <div class="dm-stat">\n <div class="dm-stat__val" style="color:var(--okdb-purple);">{{ Object.keys(resultStats?.types ?? {}).length }}</div>\n <div class="dm-stat__lbl">Types</div>\n </div>\n <div class="dm-stat-divider"></div>\n <div class="dm-stat">\n <div class="dm-stat__val" style="color:var(--okdb-accent);">{{ resultStats?.indexes }}</div>\n <div class="dm-stat__lbl">Indexes</div>\n </div>\n <if :="resultStats?.fts > 0">\n <div class="dm-stat-divider"></div>\n <div class="dm-stat">\n <div class="dm-stat__val" style="color:var(--okdb-warning);">{{ resultStats?.fts }}</div>\n <div class="dm-stat__lbl">FTS</div>\n </div>\n </if>\n </div>\n\n <div style="display:flex; gap:10px;">\n <button @click="cancel()">Close</button>\n <button class="dm-gen-btn" @click="openEnv()"\n :style="\'--pc:\' + preset.color + \'; --pg:\' + preset.glow + \';\'">\n Open Environment →\n </button>\n </div>\n\n </div>\n </if>\n\n</div>',style:"\n/* ── Root ──────────────────────────────────────────────────── */\n.dm {\n min-width: 560px;\n max-width: 660px;\n}\n\n/* ── Hero ──────────────────────────────────────────────────── */\n.dm-hero {\n text-align: center;\n padding: 4px 0 18px;\n}\n.dm-hero__sparkle {\n font-size: 30px;\n margin-bottom: 10px;\n background: linear-gradient(135deg, var(--okdb-accent), var(--okdb-purple) 40%, var(--okdb-success) 75%, var(--okdb-warning));\n -webkit-background-clip: text;\n -webkit-text-fill-color: transparent;\n background-clip: text;\n animation: dm-hue 6s linear infinite;\n display: inline-block;\n}\n.dm-hero__title {\n font-size: 20px;\n font-weight: 700;\n margin-bottom: 8px;\n background: linear-gradient(135deg, var(--okdb-app-fg), #ffffff);\n -webkit-background-clip: text;\n -webkit-text-fill-color: transparent;\n background-clip: text;\n}\n.dm-hero__sub {\n font-size: 13px;\n opacity: .55;\n max-width: 400px;\n margin: 0 auto;\n line-height: 1.65;\n}\n\n/* ── Preset grid ───────────────────────────────────────────── */\n.dm-presets {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 10px;\n padding: 4px 0 18px;\n}\n.dm-preset {\n position: relative;\n display: flex;\n flex-direction: column;\n gap: 5px;\n padding: 18px 18px 14px;\n background: var(--okdb-app-bg-deep);\n border: 1px solid var(--okdb-app-border);\n border-radius: 14px;\n cursor: pointer;\n text-align: left;\n overflow: hidden;\n transition: border-color .2s, transform .2s, box-shadow .2s;\n}\n/* City Explorer: spans both columns with a horizontal layout */\n.dm-preset--geo {\n grid-column: 1 / -1;\n flex-direction: row;\n align-items: center;\n gap: 16px;\n padding: 16px 20px;\n}\n.dm-preset--geo .dm-preset__icon {\n font-size: 32px;\n flex-shrink: 0;\n}\n.dm-preset--geo .dm-preset__name,\n.dm-preset--geo .dm-preset__tag,\n.dm-preset--geo .dm-preset__chips { }\n.dm-preset--geo .dm-preset__name { font-size: 16px; }\n.dm-preset--geo .dm-preset__arrow {\n margin-left: auto;\n position: static;\n font-size: 16px;\n opacity: .3;\n transition: opacity .2s, transform .2s;\n}\n.dm-preset--geo:hover .dm-preset__arrow { opacity: .9; transform: translateX(4px); }\n/* Geo pulse ring on the icon */\n.dm-preset--geo .dm-preset__icon::after {\n content: '';\n position: absolute;\n width: 48px;\n height: 48px;\n border-radius: 50%;\n border: 1px solid var(--pc, var(--okdb-orange));\n opacity: 0;\n animation: dm-geo-ring 2.5s ease-out infinite;\n}\n.dm-preset__bg {\n position: absolute;\n inset: 0;\n background: radial-gradient(circle at 25% 25%, var(--pg, transparent), transparent 65%);\n opacity: 0;\n transition: opacity .3s;\n pointer-events: none;\n}\n.dm-preset:hover {\n border-color: var(--pc, var(--okdb-accent));\n box-shadow: 0 0 0 1px var(--pc, var(--okdb-accent))33, 0 8px 28px var(--pg, transparent);\n transform: translateY(-2px);\n}\n.dm-preset--geo:hover { transform: translateY(-2px); }\n.dm-preset:hover .dm-preset__bg { opacity: 1; }\n.dm-preset__icon {\n font-size: 26px;\n line-height: 1;\n margin-bottom: 4px;\n filter: drop-shadow(0 0 8px var(--pc, #fff));\n position: relative;\n}\n.dm-preset__name {\n font-size: 15px;\n font-weight: 700;\n color: var(--pc, #fff);\n position: relative;\n}\n.dm-preset__tag {\n font-size: 12px;\n opacity: .5;\n line-height: 1.4;\n position: relative;\n}\n.dm-preset__chips {\n display: flex;\n gap: 5px;\n font-size: 11px;\n opacity: .45;\n margin-top: 5px;\n position: relative;\n flex-wrap: wrap;\n align-items: center;\n}\n.dm-chip-dot { opacity: .5; }\n.dm-preset__arrow {\n position: absolute;\n bottom: 14px;\n right: 16px;\n font-size: 14px;\n opacity: .2;\n transition: opacity .2s, transform .2s;\n}\n.dm-preset:not(.dm-preset--geo):hover .dm-preset__arrow { opacity: .8; transform: translateX(3px); }\n@keyframes dm-geo-ring {\n 0% { opacity: .6; transform: scale(.6); }\n 100% { opacity: 0; transform: scale(2.2); }\n}\n\n/* ── Back button ───────────────────────────────────────────── */\n.dm-back-btn {\n font-size: 16px;\n padding: 3px 10px;\n opacity: .5;\n border-radius: 6px;\n transition: opacity .15s;\n}\n.dm-back-btn:hover { opacity: 1; }\n\n/* ── Preset badge (configure step) ────────────────────────── */\n.dm-preset-badge {\n font-size: 22px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 36px;\n height: 36px;\n border-radius: 10px;\n background: var(--pg, var(--okdb-app-surface));\n border: 1px solid var(--pc, var(--okdb-app-border))33;\n filter: drop-shadow(0 0 8px var(--pc, transparent));\n}\n\n/* ── Scale buttons ─────────────────────────────────────────── */\n.dm-scale-btn {\n padding: 5px 12px;\n font-size: 12px;\n border-radius: 7px;\n border: 1px solid var(--okdb-app-border);\n transition: all .15s;\n}\n.dm-scale-btn--active {\n background: var(--pc, var(--okdb-accent))22 !important;\n border-color: var(--pc, var(--okdb-accent)) !important;\n color: var(--pc, var(--okdb-accent)) !important;\n font-weight: 600;\n}\n\n/* ── Type count table ──────────────────────────────────────── */\n.dm-type-table {\n background: var(--okdb-app-bg-deep);\n border: 1px solid var(--okdb-app-surface);\n border-radius: 9px;\n overflow: hidden;\n}\n.dm-type-row {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 8px 14px;\n border-bottom: 1px solid var(--okdb-app-border);\n}\n.dm-type-row:last-child { border-bottom: none; }\n.dm-type-row__name {\n font-family: monospace;\n font-size: 12px;\n width: 90px;\n flex-shrink: 0;\n opacity: .8;\n}\n.dm-type-row__bar-wrap {\n flex: 1;\n height: 4px;\n background: var(--okdb-app-surface);\n border-radius: 2px;\n overflow: hidden;\n}\n.dm-type-row__bar {\n height: 100%;\n border-radius: 2px;\n transition: width .25s ease-out;\n min-width: 4px;\n}\n.dm-type-row__count {\n font-family: monospace;\n font-size: 12px;\n font-weight: 600;\n width: 70px;\n text-align: right;\n flex-shrink: 0;\n}\n\n/* ── Checkbox labels ───────────────────────────────────────── */\n.dm-check-label {\n display: flex;\n align-items: center;\n gap: 7px;\n cursor: pointer;\n font-size: 13px;\n user-select: none;\n}\n\n/* ── Summary pill ──────────────────────────────────────────── */\n.dm-summary {\n padding: 10px 14px;\n border-radius: 9px;\n border: 1px solid var(--okdb-app-border);\n font-size: 13px;\n display: flex;\n gap: 5px;\n align-items: center;\n flex-wrap: wrap;\n line-height: 1.6;\n}\n\n/* ── Generate button ───────────────────────────────────────── */\n.dm-gen-btn {\n background: var(--pg, color-mix(in srgb, var(--okdb-accent) 7%, transparent)) !important;\n border: 1px solid var(--pc, var(--okdb-accent)) !important;\n color: var(--pc, var(--okdb-accent)) !important;\n font-weight: 700;\n font-size: 13px;\n padding: 7px 20px;\n border-radius: 8px;\n transition: all .2s;\n letter-spacing: .01em;\n}\n.dm-gen-btn:hover:not(:disabled) {\n background: var(--pc, var(--okdb-accent))33 !important;\n box-shadow: 0 0 20px var(--pc, var(--okdb-accent))44;\n}\n.dm-gen-btn:disabled {\n opacity: .4;\n cursor: not-allowed;\n}\n\n/* ── Footer row ────────────────────────────────────────────── */\n.dm-footer {\n display: flex;\n gap: 8px;\n justify-content: flex-end;\n padding-top: 12px;\n border-top: 1px solid var(--okdb-app-surface);\n}\n\n/* ── Generation header ─────────────────────────────────────── */\n.dm-gen-header {\n padding: 14px 16px;\n background: var(--okdb-app-bg-deep);\n border: 1px solid var(--okdb-app-surface);\n border-radius: 10px;\n box-shadow: inset 0 0 40px var(--pg, transparent);\n}\n.dm-pulsing-icon {\n font-size: 20px;\n display: inline-block;\n animation: dm-pulse 1.5s ease-in-out infinite;\n filter: drop-shadow(0 0 6px var(--pc, transparent));\n}\n.dm-progress-track {\n height: 7px;\n background: var(--okdb-app-surface);\n border-radius: 4px;\n overflow: hidden;\n}\n.dm-progress-fill {\n height: 100%;\n border-radius: 4px;\n transition: width .35s ease-out;\n position: relative;\n}\n.dm-progress-fill--shimmer::after {\n content: '';\n position: absolute;\n inset: 0;\n background: linear-gradient(90deg, transparent, rgba(255,255,255,.25), transparent);\n animation: dm-shimmer 1.5s linear infinite;\n}\n\n/* ── Step rows ─────────────────────────────────────────────── */\n.dm-steps {\n display: flex;\n flex-direction: column;\n gap: 2px;\n max-height: 320px;\n overflow-y: auto;\n padding-right: 4px;\n}\n.dm-step {\n display: flex;\n align-items: center;\n gap: 9px;\n padding: 7px 10px;\n border-radius: 7px;\n font-size: 12px;\n transition: background .1s;\n}\n.dm-step--pending { opacity: .32; }\n.dm-step--running { background: var(--okdb-app-surface); opacity: 1; }\n.dm-step--done { opacity: .6; }\n.dm-step--error { background: var(--okdb-danger-deep-bg); opacity: 1; }\n.dm-step__icon {\n width: 18px;\n text-align: center;\n flex-shrink: 0;\n font-size: 12px;\n}\n.dm-step--done .dm-step__icon { color: var(--okdb-success); }\n.dm-step--running .dm-step__icon { color: var(--okdb-accent); }\n.dm-step--error .dm-step__icon { color: var(--okdb-danger); }\n.dm-step__label { flex: 1; }\n.dm-step__count {\n font-size: 11px;\n font-family: monospace;\n opacity: .5;\n flex-shrink: 0;\n}\n.dm-step__mini-bar {\n width: 70px;\n height: 3px;\n background: var(--okdb-app-surface);\n border-radius: 2px;\n overflow: hidden;\n flex-shrink: 0;\n}\n.dm-step__mini-fill {\n height: 100%;\n background: var(--okdb-accent);\n border-radius: 2px;\n transition: width .2s ease-out;\n}\n\n/* ── Error banner ──────────────────────────────────────────── */\n.dm-error {\n color: var(--okdb-danger);\n font-size: 13px;\n padding: 9px 13px;\n background: var(--okdb-danger-deep-bg);\n border: 1px solid color-mix(in srgb, var(--okdb-danger) 20%, transparent);\n border-radius: 7px;\n}\n\n/* ── Done orb ──────────────────────────────────────────────── */\n.dm-done-orb {\n position: relative;\n width: 96px;\n height: 96px;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n.dm-done-orb__icon {\n font-size: 40px;\n position: relative;\n z-index: 2;\n filter: drop-shadow(0 0 16px var(--pc, var(--okdb-accent)));\n animation: dm-pulse 2.5s ease-in-out infinite;\n}\n.dm-done-orb__ring {\n position: absolute;\n inset: 0;\n border-radius: 50%;\n border: 1px solid var(--pc, var(--okdb-accent));\n}\n.dm-done-orb__ring--1 {\n opacity: .4;\n animation: dm-ring-pulse 2s ease-in-out infinite;\n}\n.dm-done-orb__ring--2 {\n opacity: .2;\n animation: dm-ring-pulse 2s ease-in-out infinite .7s;\n transform: scale(1.3);\n}\n\n/* ── Done stats ────────────────────────────────────────────── */\n.dm-done-stats {\n display: flex;\n align-items: center;\n gap: 20px;\n padding: 16px 32px;\n background: var(--okdb-app-bg-deep);\n border: 1px solid var(--pc, var(--okdb-accent))33;\n border-radius: 14px;\n box-shadow: 0 0 30px var(--pg, transparent);\n}\n.dm-stat {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 5px;\n}\n.dm-stat__val {\n font-size: 28px;\n font-weight: 800;\n line-height: 1;\n letter-spacing: -.02em;\n}\n.dm-stat__lbl {\n font-size: 10px;\n text-transform: uppercase;\n letter-spacing: .08em;\n opacity: .45;\n}\n.dm-stat-divider {\n width: 1px;\n height: 36px;\n background: var(--okdb-app-border);\n flex-shrink: 0;\n}\n\n/* ── Animations ────────────────────────────────────────────── */\n@keyframes dm-hue {\n from { filter: hue-rotate(0deg); }\n to { filter: hue-rotate(360deg); }\n}\n@keyframes dm-pulse {\n 0%, 100% { transform: scale(1); }\n 50% { transform: scale(1.12); }\n}\n@keyframes dm-ring-pulse {\n 0%, 100% { opacity: .3; transform: scale(1); }\n 50% { opacity: .7; transform: scale(1.1); }\n}\n@keyframes dm-shimmer {\n from { transform: translateX(-100%); }\n to { transform: translateX(300%); }\n}\n@keyframes dm-spin {\n from { transform: rotate(0deg); }\n to { transform: rotate(360deg); }\n}\n.dm-spin {\n display: inline-block;\n animation: dm-spin 1s linear infinite;\n}\n"};
1
+ function makePRNG(e){let n=(2654435769^e)>>>0||1;return()=>(n^=n<<13,n^=n>>>17,n^=n<<5,(n>>>0)/4294967296)}const pick=(e,n)=>e[Math.floor(n()*e.length)],rng=(e,n,t)=>Math.floor(t()*(n-e+1))+e,uid=(e,n)=>`${e}-${String(n).padStart(4,"0")}`,email=(e,n,t)=>`${e.toLowerCase()}.${n.toLowerCase()}${rng(1,99,t)}@${pick(["gmail.com","yahoo.com","outlook.com","proton.me","icloud.com"],t)}`,dateAgo=(e,n)=>new Date(Date.now()-864e5*rng(0,e,n)).toISOString().split("T")[0],dateFwd=(e,n)=>new Date(Date.now()+864e5*rng(1,e,n)).toISOString().split("T")[0],FIRST=["Alice","Bob","Carol","David","Emma","Frank","Grace","Henry","Iris","Jack","Kate","Liam","Mia","Noah","Olivia","Peter","Quinn","Rachel","Sam","Tara","Uma","Victor","Wendy","Yusuf","Zoe"],LAST=["Anderson","Brown","Carter","Davis","Evans","Foster","Green","Hall","Ibrahim","Jones","King","Lee","Martinez","Nelson","Owen","Parker","Quinn","Roberts","Smith","Taylor","Vega","Walsh","Young","Zhang"],CITIES=["New York","London","Berlin","Tokyo","Sydney","Toronto","Paris","Amsterdam","Singapore","Dubai","Seoul","São Paulo","Mumbai","Cape Town","Vienna"],COUNTRIES=["US","GB","DE","FR","JP","AU","CA","BR","IN","SG","NL","SE","ES","IT","MX"],DEVICES=["desktop","mobile","tablet"],PAGES=["/","/products","/about","/pricing","/blog","/contact","/signup","/login","/dashboard","/settings","/docs"],PROD_ADJ=["Ultra","Pro","Elite","Max","Smart","Nano","Eco","Premium","Turbo","Flex","Swift","Vibe"],PROD_NOUN=["Headphones","Keyboard","Monitor","Backpack","Chair","Desk","Lamp","Camera","Speaker","Tablet","Watch","Router","Charger","Mouse","Stand","Pod","Hub","Dock"],PROD_CAT=["Electronics","Furniture","Clothing","Sports","Books","Kitchen","Garden","Toys","Health","Office"],ORDER_STATUS=["pending","processing","shipped","delivered","cancelled"],DOMAINS=["gmail.com","yahoo.com","outlook.com"],CITY_CENTERS=[{name:"London",country:"GB",lat:51.5074,lon:-.1278},{name:"Paris",country:"FR",lat:48.8566,lon:2.3522},{name:"New York",country:"US",lat:40.7128,lon:-74.006},{name:"Tokyo",country:"JP",lat:35.6762,lon:139.6503},{name:"Sydney",country:"AU",lat:-33.8688,lon:151.2093},{name:"Berlin",country:"DE",lat:52.52,lon:13.405},{name:"San Francisco",country:"US",lat:37.7749,lon:-122.4194},{name:"Amsterdam",country:"NL",lat:52.3676,lon:4.9041},{name:"Singapore",country:"SG",lat:1.3521,lon:103.8198},{name:"Barcelona",country:"ES",lat:41.3851,lon:2.1734},{name:"Dubai",country:"AE",lat:25.2048,lon:55.2708},{name:"Toronto",country:"CA",lat:43.6532,lon:-79.3832},{name:"Rome",country:"IT",lat:41.9028,lon:12.4964},{name:"Melbourne",country:"AU",lat:-37.8136,lon:144.9631},{name:"Seoul",country:"KR",lat:37.5665,lon:126.978}],PLACE_CATS=["restaurant","bar","cafe","museum","park","hotel","gym","shop"],PLACE_ADJ=["Rustic","Golden","Silver","Blue","Red","Garden","Harbor","Urban","Artisan","Wild","Local","Velvet","Cozy","Vintage","Hidden","Royal","Grand","Little","Green","Stone"],PLACE_NOUN=["Kitchen","Bistro","Table","Grove","Terrace","Corner","Nook","Den","House","Yard","Quarter","Loft","Market","Square","Room","Yard"],BAR_ADJ=["Lazy","Black","Iron","Brass","Copper","Dark","Old","Rusty","Jolly","Silver","Tipsy","Lucky"],BAR_ANIMAL=["Fox","Dog","Horse","Cat","Bear","Wolf","Owl","Lion","Eagle","Crow","Rabbit","Stag"],CAFE_PRE=["Morning","Daily","Urban","Artisan","Fresh","Local","Roasted","Ground","Wake","Open","Little","Slow"],CAFE_SFX=["Coffee","Roast","Brew","Cup","Bean","Grind","Blend","Espresso","Press","Filter"],MUSEUM_SUB=["Natural History","Modern Art","Contemporary Art","Science & Technology","City History","Photography","Architecture & Design","Maritime Heritage","Living Culture"],PARK_ADJ_P=["Central","Victoria","Riverside","Botanical","Royal","Memorial","Sunset","Green","City","Harbour","Highland","West"],HOTEL_ADJ_H=["Grand","Royal","Imperial","Garden","Harbor","City","Skyline","Heritage","Boutique","Maison","Palazzo"],HOTEL_NOUN_H=["Hotel","Suites","Inn","Residence","Lodge","Place","House","Retreat","Apartments"],GYM_PRE=["Fit","Peak","Core","Power","Active","Move","Lift","Pulse","Force","Prime"],GYM_SFX=["Studio","Club","Zone","Hub","Lab","Base","Works","Space","Performance"],SHOP_ADJ=["General","Local","Corner","Village","Urban","Artisan","Indie","Craft","Makers"],SHOP_NOUN=["Store","Shop","Market","Deli","Co-op","Goods","Supply","Emporium","Collective"],PLACE_DESCS=["Highly rated local favourite with excellent reviews and a warm, welcoming atmosphere.","A must-visit destination known for quality, character and outstanding hospitality.","Hidden gem tucked away in the heart of the city — loved by locals and visitors alike.","Modern establishment with a focus on quality and an unforgettable guest experience.","A community staple for years, proudly serving residents and visitors with heart.","Award-winning venue celebrating local culture, craft and genuine creativity.","Stylish space with impeccable service and an Instagram-worthy interior.","Popular spot among locals for its relaxed vibe and consistently high standards.","A vibrant, energetic space that buzzes with life from morning to night.","Intimate and unpretentious — the kind of place that instantly feels like home.","Iconic neighbourhood institution beloved for its authenticity and character.","Contemporary space blending design, sustainability and an outstanding experience."],PERSON_BIOS=["Passionate foodie and amateur photographer exploring the city one café at a time.","Tech worker who spends weekends hiking and discovering hidden restaurant gems.","Architecture enthusiast and frequent museum-goer with a love for strong espresso.","Local guide and travel blogger sharing the best spots in every city I visit.","Fitness fanatic, craft beer lover and weekend market explorer.","Remote worker building a list of the world's best coworking-friendly cafes.","Night owl and cocktail connoisseur — on a mission to try every great bar in town.","Museum member, park runner and enthusiastic brunch-seeker on weekends.","Lifelong traveller collecting experiences, flavours and stories from around the globe.","Urban explorer with a passion for hole-in-the-wall spots and authentic local culture."],CHECKIN_NOTES=["Amazing experience, will definitely be back!","Great atmosphere and incredibly friendly staff.","Highly recommend to anyone visiting the area.","A perfect spot for a relaxed afternoon.","One of my absolute favourites in the city.","Nice vibes but gets a bit crowded on weekends.","Exactly what I needed today — no complaints.","Good value for the quality of the experience.","Nothing groundbreaking but solid and reliable.","Exceptional quality — genuinely exceeded expectations.","Stumbled in by chance and absolutely loved it.","Great first visit, already planning to return.",null,null,null];function _scatter(e,n,t){return{lat:Math.round(1e6*(e.lat+2*(t()-.5)*n))/1e6,lon:Math.round(1e6*(e.lon+2*(t()-.5)*n))/1e6}}function _placeName(e,n){switch(e){case"restaurant":return`The ${pick(PLACE_ADJ,n)} ${pick(PLACE_NOUN,n)}`;case"bar":return`The ${pick(BAR_ADJ,n)} ${pick(BAR_ANIMAL,n)}`;case"cafe":return`${pick(CAFE_PRE,n)} ${pick(CAFE_SFX,n)}`;case"museum":return`Museum of ${pick(MUSEUM_SUB,n)}`;case"park":return`${pick(PARK_ADJ_P,n)} Park`;case"hotel":return`The ${pick(HOTEL_ADJ_H,n)} ${pick(HOTEL_NOUN_H,n)}`;case"gym":return`${pick(GYM_PRE,n)} ${pick(GYM_SFX,n)}`;case"shop":return`The ${pick(SHOP_ADJ,n)} ${pick(SHOP_NOUN,n)}`;default:return`${pick(PLACE_ADJ,n)} Place`}}function generateCityExplorer(e,n){const t={};return t.places=Array.from({length:e.places},(e,t)=>{const a=pick(CITY_CENTERS,n),o=pick(PLACE_CATS,n);return{key:uid("place",t+1),value:{name:_placeName(o,n),category:o,city:a.name,country:a.country,location:_scatter(a,.035,n),rating:Math.round(rng(28,50,n))/10,reviewCount:rng(5,3e3,n),priceLevel:rng(1,4,n),description:pick(PLACE_DESCS,n),phone:`+${rng(1,99,n)} ${rng(100,999,n)}-${rng(1e3,9999,n)}`,openNow:n()>.3,verified:n()>.25,createdAt:dateAgo(730,n)}}}),t.people=Array.from({length:e.people},(e,t)=>{const a=pick(CITY_CENTERS,n),o=pick(FIRST,n),r=pick(LAST,n);return{key:uid("person",t+1),value:{name:`${o} ${r}`,email:email(o,r,n),city:a.name,country:a.country,home:_scatter(a,.055,n),bio:pick(PERSON_BIOS,n),interests:[...new Set([pick(PLACE_CATS,n),pick(PLACE_CATS,n),pick(PLACE_CATS,n)])],memberSince:dateAgo(1095,n),checkinsCount:rng(0,90,n)}}}),t.checkins=Array.from({length:e.checkins},(t,a)=>({key:uid("ci",a+1),value:{personId:uid("person",rng(1,e.people,n)),placeId:uid("place",rng(1,e.places,n)),rating:rng(1,5,n),note:pick(CHECKIN_NOTES,n),hasPhoto:n()>.58,timestamp:new Date(Date.now()-rng(0,31536e6,n)).toISOString()}})),t}const POST_TITLES=["Getting Started with Modern Web Development","The Complete Guide to Database Optimization","Building Scalable APIs with Node.js","Understanding Event-Driven Architecture","Top 10 UX Patterns for Mobile Apps","Deep Dive into Machine Learning Basics","Docker and Kubernetes: A Practical Guide","Functional Programming in JavaScript","The Future of Distributed Systems","Mastering TypeScript in 2025","Building Real-time Dashboards","Microservices vs Monolith: When to Choose","GraphQL Best Practices for Production","State Management in Modern Frontend","Observability and Monitoring at Scale","Secure by Design: API Security Patterns","WebAssembly: The Next Frontier","Green Software Engineering Principles","AI-Assisted Development Workflows","Database Indexing Deep Dive"],TAG_NAMES=["javascript","react","node","css","typescript","python","docker","aws","graphql","ux","design","databases","security","mobile","devops","architecture","testing","performance","accessibility","seo","analytics","api","open-source","career","tools","tutorial","beginner","advanced"],POST_CATS=["Technology","Design","Business","Tutorial","Opinion","News","Research"],COMMENT_BODIES=["Great article! Really helped me understand the concept.","Thanks for sharing this. Very insightful.","Could you elaborate more on the second point?","This is exactly what I was looking for!","Bookmarked. Will definitely revisit this later.","The examples were very clear. Well done!","I disagree with some points but overall great read.","Would love to see a follow-up article on this topic.","Just implemented this — worked perfectly!","Saved me hours of debugging. Thank you!"],EVT_TYPES=["pageview","click","scroll","form_submit","purchase","signup","login","logout","search","download","video_play","share"],REFERRERS=["google.com","twitter.com","github.com","direct","reddit.com","hackernews.com","linkedin.com"],ROLES=["Engineer","Designer","Manager","Analyst","Director","Lead","Coordinator","Specialist","Architect","Consultant"],DEPTS=["Engineering","Design","Product","Marketing","Sales","Operations","HR","Finance"],LEVELS=["junior","mid","senior","lead","principal","director"],SKILLS=["JavaScript","Python","React","Node.js","SQL","Docker","AWS","TypeScript","GraphQL","Kubernetes","Figma","Analytics","Leadership","Communication","Rust","Go","Swift","Kotlin"],PROJ_NAMES=["Platform Rewrite","Mobile App v2","API Gateway","Data Pipeline","Customer Portal","Analytics Dashboard","Auth Service","Notification System","Search Engine","Billing Integration","Admin Console","CI/CD Overhaul","Performance Audit","Security Hardening","Developer SDK","Internal Tooling","Database Migration","Load Testing","Documentation Site","Onboarding Flow","A/B Testing Framework","Design System","Release Automation","Partner API","Data Warehouse"],PROJ_STATUS=["planning","in-progress","review","completed","on-hold"];function generateEcommerce(e,n){const t={};return t.products=Array.from({length:e.products},(e,t)=>{const a=pick(PROD_ADJ,n),o=pick(PROD_NOUN,n);return{key:uid("prod",t+1),value:{name:`${a} ${o}`,category:pick(PROD_CAT,n),price:Math.round(100*(rng(5,999,n)+.99*n()))/100,stock:rng(0,500,n),rating:Math.round(rng(25,50,n))/10,reviewCount:rng(0,1200,n),description:`The ${a} ${o} is engineered for peak performance and everyday reliability. Features premium materials and an intuitive design.`,sku:`SKU-${rng(1e4,99999,n)}`,brand:pick(["TechCore","UrbanGear","Nomad","Apex","Lux"],n),createdAt:dateAgo(365,n)}}}),t.customers=Array.from({length:e.customers},(e,t)=>{const a=pick(FIRST,n),o=pick(LAST,n);return{key:uid("cust",t+1),value:{name:`${a} ${o}`,email:email(a,o,n),city:pick(CITIES,n),country:pick(COUNTRIES,n),tier:pick(["bronze","silver","gold","platinum"],n),totalOrders:rng(0,50,n),totalSpent:Math.round(100*rng(0,15e3,n))/100,joinDate:dateAgo(730,n),newsletter:n()>.4}}}),t.orders=Array.from({length:e.orders},(t,a)=>{const o=rng(1,5,n),r=Array.from({length:o},()=>{const t=Math.round(100*rng(5,499,n))/100,a=rng(1,4,n);return{productId:uid("prod",rng(1,e.products,n)),qty:a,price:t,total:Math.round(t*a*100)/100}});return{key:uid("ord",a+1),value:{customerId:uid("cust",rng(1,e.customers,n)),status:pick(ORDER_STATUS,n),items:r,subtotal:r.reduce((e,n)=>e+n.total,0),shipping:Math.round(100*rng(0,20,n))/100,total:r.reduce((e,n)=>e+n.total,0)+Math.round(100*rng(0,20,n))/100,createdAt:dateAgo(180,n),shippedAt:n()>.3?dateAgo(60,n):null,address:{city:pick(CITIES,n),country:pick(COUNTRIES,n)}}}}),t.reviews=Array.from({length:e.reviews},(t,a)=>{const o=rng(1,5,n);return{key:uid("rev",a+1),value:{productId:uid("prod",rng(1,e.products,n)),customerId:uid("cust",rng(1,e.customers,n)),stars:o,title:pick(o>=4?["Love it!","Great quality","Exactly as described","Highly recommend","Excellent value"]:["Disappointed","Not worth it","Expected more","Decent but flawed"],n),body:`${o>=4?"Really happy with this purchase.":"Decent but has some issues."} ${pick(["Would recommend to friends.","Using it every day.","Still testing it out.","Mixed feelings overall.","Might return it."],n)}`,verified:n()>.2,helpful:rng(0,200,n),createdAt:dateAgo(365,n)}}}),t}function generateAnalytics(e,n){const t={};return t.sessions=Array.from({length:e.sessions},(e,t)=>({key:uid("sess",t+1),value:{userId:n()>.35?uid("user",rng(1,500,n)):null,device:pick(DEVICES,n),country:pick(COUNTRIES,n),city:pick(CITIES,n),duration:rng(5,3600,n),pageCount:rng(1,30,n),bounced:n()<.38,browser:pick(["Chrome","Firefox","Safari","Edge","Arc"],n),startedAt:new Date(Date.now()-rng(0,2592e6,n)).toISOString()}})),t.events=Array.from({length:e.events},(t,a)=>({key:uid("evt",a+1),value:{type:pick(EVT_TYPES,n),sessionId:uid("sess",rng(1,e.sessions,n)),userId:n()>.35?uid("user",rng(1,500,n)):null,page:pick(PAGES,n),device:pick(DEVICES,n),country:pick(COUNTRIES,n),timestamp:new Date(Date.now()-rng(0,2592e6,n)).toISOString(),value:n()>.7?Math.round(100*rng(1,1e4,n))/100:null}})),t.pageviews=Array.from({length:e.pageviews},(t,a)=>({key:uid("pv",a+1),value:{page:pick(PAGES,n),sessionId:uid("sess",rng(1,e.sessions,n)),referrer:pick(REFERRERS,n),timeOnPage:rng(0,600,n),exitPage:n()>.7,timestamp:new Date(Date.now()-rng(0,2592e6,n)).toISOString()}})),t}function generateBlog(e,n){const t={};t.authors=Array.from({length:e.authors},(e,t)=>{const a=pick(FIRST,n),o=pick(LAST,n),r=pick(["Frontend","Backend","DevOps","Design","Data Science","Mobile","Security","Cloud"],n);return{key:uid("auth",t+1),value:{name:`${a} ${o}`,email:email(a,o,n),bio:`${a} is a ${r} specialist with ${rng(2,18,n)} years of experience building production systems.`,specialty:r,location:pick(CITIES,n),twitter:`@${a.toLowerCase()}${o.toLowerCase()}`,postCount:0}}});const a=Math.min(e.tags,TAG_NAMES.length);return t.tags=Array.from({length:a},(e,t)=>({key:uid("tag",t+1),value:{name:TAG_NAMES[t],count:rng(5,120,n),color:pick(["var(--okdb-accent)","var(--okdb-purple)","var(--okdb-success)","var(--okdb-warning)","var(--okdb-danger)"],n)}})),t.posts=Array.from({length:e.posts},(t,o)=>{const r=POST_TITLES[o%POST_TITLES.length],s=pick(["","","","Deep Dive: ","The Ultimate ","A Practical Guide to ","Why "],n),i=s?`${s}${r}`:r,l=n()>.25?"published":pick(["draft","archived"],n);return{key:uid("post",o+1),value:{title:i,slug:i.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,""),authorId:uid("auth",rng(1,e.authors,n)),category:pick(POST_CATS,n),status:l,tags:[...new Set([uid("tag",rng(1,a,n)),uid("tag",rng(1,a,n))])],content:"This article covers the essentials you need to know. We explore key concepts, practical examples, and real-world applications that you can apply immediately. Whether you're a beginner or an experienced professional, you'll find actionable insights here.",excerpt:`A comprehensive guide to understanding ${r.toLowerCase()} in depth — with practical examples.`,readTime:rng(2,22,n),views:rng(50,8e4,n),likes:rng(5,3e3,n),featured:n()>.85,publishedAt:"published"===l?dateAgo(400,n):null,createdAt:dateAgo(450,n)}}}),t.comments=Array.from({length:e.comments},(t,a)=>({key:uid("cmt",a+1),value:{postId:uid("post",rng(1,e.posts,n)),authorName:`${pick(FIRST,n)} ${pick(LAST,n)}`,email:`user${rng(1,9999,n)}@${pick(DOMAINS,n)}`,body:pick(COMMENT_BODIES,n),likes:rng(0,150,n),approved:n()>.1,createdAt:dateAgo(365,n)}})),t}function generateTeam(e,n){const t={},a=Array.from({length:e.departments},(e,n)=>uid("dept",n+1));return t.departments=a.map((e,t)=>({key:e,value:{name:DEPTS[t%DEPTS.length],head:"",headcount:rng(4,35,n),budget:rng(1e5,3e6,n),location:pick(CITIES,n),description:`The ${DEPTS[t%DEPTS.length]} team drives innovation and ${pick(["growth","quality","efficiency","culture"],n)} across the organisation.`}})),t.employees=Array.from({length:e.employees},(e,t)=>{const o=pick(FIRST,n),r=pick(LAST,n);return{key:uid("emp",t+1),value:{name:`${o} ${r}`,email:email(o,r,n),role:pick(ROLES,n),department:pick(a,n),level:pick(LEVELS,n),skills:[...new Set([pick(SKILLS,n),pick(SKILLS,n),pick(SKILLS,n)])],salary:rng(48e3,28e4,n),hireDate:dateAgo(1825,n),manager:t>0?uid("emp",rng(1,Math.max(1,t),n)):null,location:pick(CITIES,n),remote:n()>.45}}}),t.projects=Array.from({length:e.projects},(t,o)=>{const r=rng(2,8,n);return{key:uid("proj",o+1),value:{name:PROJ_NAMES[o%PROJ_NAMES.length],department:pick(a,n),status:pick(PROJ_STATUS,n),lead:uid("emp",rng(1,e.employees,n)),team:[...new Set(Array.from({length:r},()=>uid("emp",rng(1,e.employees,n))))],budget:rng(1e4,6e5,n),startDate:dateAgo(365,n),deadline:dateFwd(180,n),progress:rng(0,100,n),priority:pick(["low","medium","high","critical"],n),description:`${PROJ_NAMES[o%PROJ_NAMES.length]} is a strategic initiative to improve ${pick(["reliability","performance","user experience","developer productivity","security"],n)}.`}}}),t}const PRESETS=[{id:"ecommerce",icon:"🛒",name:"E-Commerce",tagline:"Products, customers & orders",color:"var(--okdb-success)",glow:"color-mix(in srgb, var(--okdb-success) 20%, transparent)",defaultEnv:"demo-shop",types:[{key:"products",label:"Products",defaultCount:250},{key:"customers",label:"Customers",defaultCount:100},{key:"orders",label:"Orders",defaultCount:500},{key:"reviews",label:"Reviews",defaultCount:300}],indexes:[["products","category"],["products","price"],["customers","city"],["customers","tier"],["orders","status"],["orders","customerId"]],fts:[{type:"products",name:"search",fields:["name","description"]}],generate:generateEcommerce},{id:"analytics",icon:"📊",name:"Analytics",tagline:"Events, sessions & pageviews",color:"var(--okdb-accent)",glow:"color-mix(in srgb, var(--okdb-accent) 20%, transparent)",defaultEnv:"demo-analytics",types:[{key:"events",label:"Events",defaultCount:1e3},{key:"sessions",label:"Sessions",defaultCount:200},{key:"pageviews",label:"Pageviews",defaultCount:2e3}],indexes:[["events","type"],["events","country"],["sessions","userId"],["sessions","device"],["pageviews","page"],["pageviews","sessionId"]],fts:[],generate:generateAnalytics},{id:"blog",icon:"📝",name:"Content Hub",tagline:"Posts, authors & comments",color:"var(--okdb-purple)",glow:"color-mix(in srgb, var(--okdb-purple) 20%, transparent)",defaultEnv:"demo-blog",types:[{key:"posts",label:"Posts",defaultCount:150},{key:"authors",label:"Authors",defaultCount:20},{key:"tags",label:"Tags",defaultCount:28},{key:"comments",label:"Comments",defaultCount:400}],indexes:[["posts","category"],["posts","authorId"],["posts","status"],["comments","postId"],["tags","count"]],fts:[{type:"posts",name:"search",fields:["title","content"]}],generate:generateBlog},{id:"team",icon:"👥",name:"Team Directory",tagline:"Employees, departments & projects",color:"var(--okdb-warning)",glow:"color-mix(in srgb, var(--okdb-warning) 20%, transparent)",defaultEnv:"demo-team",types:[{key:"employees",label:"Employees",defaultCount:80},{key:"departments",label:"Departments",defaultCount:8},{key:"projects",label:"Projects",defaultCount:30}],indexes:[["employees","department"],["employees","level"],["employees","location"],["projects","status"],["projects","department"]],fts:[{type:"employees",name:"search",fields:["name","role","skills"]}],generate:generateTeam},{id:"cityexplorer",icon:"🗺️",name:"City Explorer",tagline:"Geo-indexed places & people",color:"var(--okdb-orange)",glow:"color-mix(in srgb, var(--okdb-orange) 20%, transparent)",defaultEnv:"demo-places",badge:"geo",types:[{key:"places",label:"Places",defaultCount:200},{key:"people",label:"People",defaultCount:80},{key:"checkins",label:"Checkins",defaultCount:300}],indexes:[["places","location",{type:"geo",precision:7}],["places","category"],["places","city"],["places","rating"],["people","home",{type:"geo",precision:5}],["people","city"],["checkins","personId"],["checkins","placeId"]],fts:[{type:"places",name:"search",fields:["name","description"]}],generate:generateCityExplorer}],SCALE_OPTIONS=[{value:.25,label:"¼×",hint:"Tiny"},{value:.5,label:"½×",hint:"Small"},{value:1,label:"1×",hint:"Normal"},{value:2,label:"2×",hint:"Large"},{value:4,label:"4×",hint:"Huge"}],BATCH_SIZE=50;export default{tag:"demo-env-modal",attr:{modal:{},onDone:{}},context:e=>({step:"choose",preset:null,envName:"",scale:1,enableFts:!0,enableIndexes:!0,typeCounts:{},progress:[],totalSteps:0,completedSteps:0,error:null,resultStats:null,PRESETS:PRESETS,SCALE_OPTIONS:SCALE_OPTIONS,get effectiveCounts(){const e={};for(const n of this.preset?.types??[])e[n.key]=Math.max(1,Math.round((this.typeCounts[n.key]??n.defaultCount)*this.scale));return e},get totalRecords(){return Object.values(this.effectiveCounts).reduce((e,n)=>e+n,0)},get progressPct(){return this.totalSteps?Math.min(100,Math.round(this.completedSteps/this.totalSteps*100)):0},selectPreset(e){this.preset=e,this.envName=e.defaultEnv,this.typeCounts={};for(const n of e.types)this.typeCounts[n.key]=n.defaultCount;this.step="configure"},back(){"configure"===this.step&&(this.step="choose")},cancel(){e.$attr.modal.close()},_addStep(e,n,t,a){this.progress.push({id:e,label:n,done:0,total:t,icon:a,status:"pending"}),this.totalSteps++},_start(e){const n=this.progress.find(n=>n.id===e);n&&(n.status="running")},_done(e){const n=this.progress.find(n=>n.id===e);n&&(n.status="done",n.done=n.total,this.completedSteps++)},_error(e){const n=this.progress.find(n=>n.id===e);n&&(n.status="error",this.completedSteps++)},_tick(e,n){const t=this.progress.find(n=>n.id===e);t&&(t.done=n)},async start(){if("generating"===this.step)return;const e=this.envName.trim();if(!e||!this.preset)return;this.step="generating",this.progress=[],this.totalSteps=0,this.completedSteps=0,this.error=null,this.resultStats={env:e,types:{},indexes:0,fts:0,totalRecords:0};const n=this.preset,t=this.effectiveCounts,a=__ENV__.API,o=`${a}/env/${encodeURIComponent(e)}`;this._addStep("env",`Create environment "${e}"`,1,"🌐");for(const e of n.types)this._addStep(`type:${e.key}`,`Register type: ${e.key}`,1,"⬡");for(const e of n.types)this._addStep(`data:${e.key}`,`Insert ${t[e.key].toLocaleString()} ${e.label.toLowerCase()}`,t[e.key],"⬢");if(this.enableIndexes)for(const[e,t,a]of n.indexes){const n="geo"===a?.type;this._addStep(`idx:${e}:${t}`,`${n?"📍 Geo index":"Index"} ${e}.${t}`,1,n?"📍":"⌖")}if(this.enableFts&&n.fts.length)for(const e of n.fts)this._addStep(`fts:${e.type}:${e.name}`,`FTS "${e.name}" on ${e.type}`,1,"⌘");try{this._start("env");const r=await fetch(`${a}/envs`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:e,compression:!0})});if(!r.ok&&409!==r.status){const e=await r.json().catch(()=>({}));throw new Error(e?.error?.message||`Create env failed (${r.status})`)}this._done("env");for(const e of n.types){this._start(`type:${e.key}`);const n=await fetch(`${o}/type/${encodeURIComponent(e.key)}`,{method:"POST"});if(!n.ok&&409!==n.status){const t=await n.json().catch(()=>({}));throw new Error(t?.error?.message||`Register type "${e.key}" failed`)}this._done(`type:${e.key}`)}const s=makePRNG(2147483647&Date.now()),i=n.generate(t,s);for(const e of n.types){const n=`data:${e.key}`,t=i[e.key]??[];this._start(n);let a=0;for(let r=0;r<t.length;r+=50){const s=t.slice(r,r+50),i=s.map(({key:n,value:t})=>({action:"put",type:e.key,key:n,value:t})),l=await fetch(`${o}/transaction`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({operations:i})});if(!l.ok){const n=await l.json().catch(()=>({}));throw new Error(n?.error?.message||`Insert into "${e.key}" failed`)}a+=s.length,this._tick(n,a),await new Promise(e=>setTimeout(e,0))}this.resultStats.types[e.key]=t.length,this.resultStats.totalRecords+=t.length,this._done(n)}if(this.enableIndexes)for(const[e,t,a]of n.indexes){const n=`idx:${e}:${t}`;this._start(n);const r=await fetch(`${o}/type/${encodeURIComponent(e)}/index/${encodeURIComponent(t)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(a||{})});if(!r.ok){const n=await r.json().catch(()=>({}));throw new Error(n?.error?.message||`Index ${e}.${t} failed`)}this.resultStats.indexes++,this._done(n)}if(this.enableFts)for(const e of n.fts){const n=`fts:${e.type}:${e.name}`;this._start(n),(await fetch(`${o}/type/${encodeURIComponent(e.type)}/fts/${encodeURIComponent(e.name)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({fields:e.fields})})).ok?(this.resultStats.fts++,this._done(n)):this._error(n)}this.step="done"}catch(e){this.error=e?.message||"Generation failed";for(const e of this.progress)"running"===e.status&&this._error(e.id)}},async openEnv(){const n=this.envName.trim();e.$ok.stores.okdb.selectEnv(n),await e.$ok.stores.okdb.reloadEnvs(),await e.$attr.modal.close(),await(e.$attr.onDone?.(n))}}),template:'\n<div class="dm">\n\n \x3c!-- ══ STEP: Choose preset ══════════════════════════════════════════════════ --\x3e\n <if :="step === \'choose\'">\n <div class="dm-hero">\n <div class="dm-hero__sparkle">✦</div>\n <div class="dm-hero__title">Generate Demo Environment</div>\n <div class="dm-hero__sub">\n Choose a preset to instantly fill a new environment with realistic data,\n indexes, and full-text search — ready to explore in seconds.\n </div>\n </div>\n\n <div class="dm-presets">\n <button as="repeat" :of="PRESETS" let:item="p"\n class="dm-preset"\n :class="p.badge === \'geo\' ? \'dm-preset--geo\' : \'\'"\n @click="selectPreset(p)"\n :style="\'--pc:\' + p.color + \'; --pg:\' + p.glow + \';\'">\n <div class="dm-preset__bg"></div>\n <div class="dm-preset__icon">{{ p.icon }}</div>\n <div class="dm-preset__name">{{ p.name }}</div>\n <div class="dm-preset__tag">{{ p.tagline }}</div>\n <div class="dm-preset__chips">\n <span>{{ p.types.length }} types</span>\n <span class="dm-chip-dot">·</span>\n <span>{{ p.types.reduce((s,t)=>s+t.defaultCount,0).toLocaleString() }}+ records</span>\n <if :="p.fts.length">\n <span class="dm-chip-dot">·</span>\n <span>FTS</span>\n </if>\n <if :="p.badge === \'geo\'">\n <span class="dm-chip-dot">·</span>\n <span style="color:var(--pc); font-weight:600;">📍 GEO</span>\n </if>\n </div>\n <div class="dm-preset__arrow">→</div>\n </button>\n </div>\n\n <div class="dm-footer">\n <button @click="cancel()">Cancel</button>\n </div>\n </if>\n\n \x3c!-- ══ STEP: Configure ══════════════════════════════════════════════════════ --\x3e\n <if :="step === \'configure\'">\n <div style="display:flex; flex-direction:column; gap:18px;">\n\n <div style="display:flex; align-items:center; gap:10px;">\n <button @click="back()" class="dm-back-btn" title="Back to presets">←</button>\n <span class="dm-preset-badge" :style="\'--pc:\' + preset.color + \'; --pg:\' + preset.glow + \';\'">\n {{ preset.icon }}\n </span>\n <span style="font-weight:700; font-size:15px;">{{ preset.name }}</span>\n <span style="opacity:.4; font-size:12px;">· Configure</span>\n </div>\n\n \x3c!-- Env name --\x3e\n <div style="display:flex; flex-direction:column; gap:5px;">\n <label style="font-size:12px; opacity:.55; letter-spacing:.03em;">Environment name</label>\n <input type="text" :value="envName" @input="envName = $el.value"\n style="font-family:monospace; font-size:13px;" autofocus\n placeholder="e.g. demo-shop" />\n </div>\n\n \x3c!-- Scale --\x3e\n <div style="display:flex; flex-direction:column; gap:8px;">\n <label style="font-size:12px; opacity:.55; letter-spacing:.03em;">Dataset scale</label>\n <div style="display:flex; gap:6px; flex-wrap:wrap;">\n <button as="repeat" :of="SCALE_OPTIONS" let:item="s"\n @click="scale = s.value"\n class="dm-scale-btn"\n :class="scale === s.value ? \'dm-scale-btn--active\' : \'\'"\n :style="scale === s.value ? \'--pc:\' + preset.color : \'\'">\n {{ s.label }}&nbsp;<span style="font-size:10px; opacity:.55;">{{ s.hint }}</span>\n </button>\n </div>\n </div>\n\n \x3c!-- Type counts --\x3e\n <div style="display:flex; flex-direction:column; gap:5px;">\n <label style="font-size:12px; opacity:.55; letter-spacing:.03em;">Types &amp; counts</label>\n <div class="dm-type-table">\n <div as="repeat" :of="preset.types" let:item="t" class="dm-type-row">\n <span class="dm-type-row__name">{{ t.key }}</span>\n <div class="dm-type-row__bar-wrap">\n <div class="dm-type-row__bar"\n :style="\'width:\' + Math.min(100, Math.round((typeCounts[t.key] ?? t.defaultCount) * scale / (t.defaultCount * 4) * 100)) + \'%; background:\' + preset.color + \'55;\'">\n </div>\n </div>\n <span class="dm-type-row__count" :style="\'color:\' + preset.color + \';\'">\n {{ Math.max(1, Math.round((typeCounts[t.key] ?? t.defaultCount) * scale)).toLocaleString() }}\n </span>\n </div>\n </div>\n </div>\n\n \x3c!-- Options --\x3e\n <div style="display:flex; gap:20px; flex-wrap:wrap;">\n <label class="dm-check-label">\n <input type="checkbox" :checked="enableIndexes" @change="enableIndexes = $el.checked" />\n <span>Indexes <span style="opacity:.4; font-size:11px;">({{ preset.indexes.length }})</span></span>\n </label>\n <label class="dm-check-label" :style="!preset.fts.length ? \'opacity:.4; pointer-events:none;\' : \'\'">\n <input type="checkbox" :checked="enableFts" @change="enableFts = $el.checked" :disabled="!preset.fts.length" />\n <span>Full-text search <span style="opacity:.4; font-size:11px;">({{ preset.fts.length }})</span></span>\n </label>\n </div>\n\n \x3c!-- Summary pill --\x3e\n <div class="dm-summary" :style="\'border-color:\' + preset.color + \'33; background:\' + preset.glow + \';\'">\n <span style="opacity:.7;">Will create</span>\n <b :style="\'color:\' + preset.color + \';\'">{{ totalRecords.toLocaleString() }}</b>\n <span style="opacity:.7;">records across</span>\n <b>{{ preset.types.length }}</b>\n <span style="opacity:.7;">types</span>\n <if :="enableIndexes && preset.indexes.length">\n <span style="opacity:.4;">·</span>\n <b style="color:var(--okdb-purple);">{{ preset.indexes.length }}</b>\n <span style="opacity:.7;">indexes</span>\n </if>\n <if :="enableFts && preset.fts.length">\n <span style="opacity:.4;">·</span>\n <b style="color:var(--okdb-accent);">FTS</b>\n </if>\n <span style="opacity:.4;">→</span>\n <b style="font-family:monospace; font-size:12px;">{{ envName || \'…\' }}</b>\n </div>\n\n <div class="dm-footer">\n <button @click="cancel()">Cancel</button>\n <button class="dm-gen-btn" :disabled="!envName.trim()" @click="start()"\n :style="\'--pc:\' + preset.color + \'; --pg:\' + preset.glow + \';\'">\n ✦ Generate {{ totalRecords.toLocaleString() }} records\n </button>\n </div>\n </div>\n </if>\n\n \x3c!-- ══ STEP: Generating ═════════════════════════════════════════════════════ --\x3e\n <if :="step === \'generating\'">\n <div style="display:flex; flex-direction:column; gap:14px;">\n\n \x3c!-- Header / overall progress --\x3e\n <div class="dm-gen-header" :style="\'--pc:\' + preset.color + \'; --pg:\' + preset.glow + \';\'">\n <div style="display:flex; align-items:center; gap:10px; margin-bottom:12px;">\n <span class="dm-pulsing-icon">{{ preset.icon }}</span>\n <span style="font-weight:600; font-size:14px;">Generating {{ preset.name }} data…</span>\n <span style="margin-left:auto; font-family:monospace; font-size:13px; opacity:.55;">{{ progressPct }}%</span>\n </div>\n <div class="dm-progress-track">\n <div class="dm-progress-fill dm-progress-fill--shimmer"\n :style="\'width:\' + progressPct + \'%; background:\' + preset.color + \';\'">\n </div>\n </div>\n </div>\n\n \x3c!-- Per-step rows --\x3e\n <div class="dm-steps">\n <div as="repeat" :of="progress" let:item="p"\n class="dm-step"\n :class="\'dm-step--\' + p.status">\n <span class="dm-step__icon">\n <if :="p.status === \'running\'"><span class="dm-spin">◌</span></if>\n <if :="p.status === \'done\'">✓</if>\n <if :="p.status === \'error\'">✗</if>\n <if :="p.status === \'pending\'">{{ p.icon }}</if>\n </span>\n <span class="dm-step__label">{{ p.label }}</span>\n <if :="p.total > 1 && p.status !== \'pending\'">\n <span class="dm-step__count">{{ p.done.toLocaleString() }} / {{ p.total.toLocaleString() }}</span>\n </if>\n <if :="p.total > 1 && p.status === \'running\'">\n <div class="dm-step__mini-bar">\n <div class="dm-step__mini-fill"\n :style="\'width:\' + (p.total ? Math.round(p.done/p.total*100) : 0) + \'%;\'">\n </div>\n </div>\n </if>\n </div>\n </div>\n\n <if :="error">\n <div class="dm-error">✗ {{ error }}</div>\n <div class="dm-footer"><button @click="cancel()">Close</button></div>\n </if>\n\n </div>\n </if>\n\n \x3c!-- ══ STEP: Done ═══════════════════════════════════════════════════════════ --\x3e\n <if :="step === \'done\'">\n <div style="display:flex; flex-direction:column; align-items:center; gap:22px; padding:12px 0 6px;">\n\n <div class="dm-done-orb" :style="\'--pc:\' + preset.color + \'; --pg:\' + preset.glow + \';\'">\n <span class="dm-done-orb__icon">{{ preset.icon }}</span>\n <div class="dm-done-orb__ring dm-done-orb__ring--1"></div>\n <div class="dm-done-orb__ring dm-done-orb__ring--2"></div>\n </div>\n\n <div style="text-align:center;">\n <div style="font-size:22px; font-weight:700; margin-bottom:8px;">Environment Ready!</div>\n <div style="font-size:13px; font-family:monospace; opacity:.5; letter-spacing:.05em;">{{ resultStats?.env }}</div>\n </div>\n\n <div class="dm-done-stats" :style="\'--pc:\' + preset.color + \'; --pg:\' + preset.glow + \';\'">\n <div class="dm-stat">\n <div class="dm-stat__val" style="color:var(--okdb-success);">{{ resultStats?.totalRecords?.toLocaleString() }}</div>\n <div class="dm-stat__lbl">Records</div>\n </div>\n <div class="dm-stat-divider"></div>\n <div class="dm-stat">\n <div class="dm-stat__val" style="color:var(--okdb-purple);">{{ Object.keys(resultStats?.types ?? {}).length }}</div>\n <div class="dm-stat__lbl">Types</div>\n </div>\n <div class="dm-stat-divider"></div>\n <div class="dm-stat">\n <div class="dm-stat__val" style="color:var(--okdb-accent);">{{ resultStats?.indexes }}</div>\n <div class="dm-stat__lbl">Indexes</div>\n </div>\n <if :="resultStats?.fts > 0">\n <div class="dm-stat-divider"></div>\n <div class="dm-stat">\n <div class="dm-stat__val" style="color:var(--okdb-warning);">{{ resultStats?.fts }}</div>\n <div class="dm-stat__lbl">FTS</div>\n </div>\n </if>\n </div>\n\n <div style="display:flex; gap:10px;">\n <button @click="cancel()">Close</button>\n <button class="dm-gen-btn" @click="openEnv()"\n :style="\'--pc:\' + preset.color + \'; --pg:\' + preset.glow + \';\'">\n Open Environment →\n </button>\n </div>\n\n </div>\n </if>\n\n</div>',style:"\n/* ── Root ──────────────────────────────────────────────────── */\n.dm {\n min-width: 560px;\n max-width: 660px;\n}\n\n/* ── Hero ──────────────────────────────────────────────────── */\n.dm-hero {\n text-align: center;\n padding: 4px 0 18px;\n}\n.dm-hero__sparkle {\n font-size: 30px;\n margin-bottom: 10px;\n background: linear-gradient(135deg, var(--okdb-accent), var(--okdb-purple) 40%, var(--okdb-success) 75%, var(--okdb-warning));\n -webkit-background-clip: text;\n -webkit-text-fill-color: transparent;\n background-clip: text;\n animation: dm-hue 6s linear infinite;\n display: inline-block;\n}\n.dm-hero__title {\n font-size: 20px;\n font-weight: 700;\n margin-bottom: 8px;\n background: linear-gradient(135deg, var(--okdb-app-fg), #ffffff);\n -webkit-background-clip: text;\n -webkit-text-fill-color: transparent;\n background-clip: text;\n}\n.dm-hero__sub {\n font-size: 13px;\n opacity: .55;\n max-width: 400px;\n margin: 0 auto;\n line-height: 1.65;\n}\n\n/* ── Preset grid ───────────────────────────────────────────── */\n.dm-presets {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 10px;\n padding: 4px 0 18px;\n}\n.dm-preset {\n position: relative;\n display: flex;\n flex-direction: column;\n gap: 5px;\n padding: 18px 18px 14px;\n background: var(--okdb-app-bg-deep);\n border: 1px solid var(--okdb-app-border);\n border-radius: 14px;\n cursor: pointer;\n text-align: left;\n overflow: hidden;\n transition: border-color .2s, transform .2s, box-shadow .2s;\n}\n/* City Explorer: spans both columns with a horizontal layout */\n.dm-preset--geo {\n grid-column: 1 / -1;\n flex-direction: row;\n align-items: center;\n gap: 16px;\n padding: 16px 20px;\n}\n.dm-preset--geo .dm-preset__icon {\n font-size: 32px;\n flex-shrink: 0;\n}\n.dm-preset--geo .dm-preset__name,\n.dm-preset--geo .dm-preset__tag,\n.dm-preset--geo .dm-preset__chips { }\n.dm-preset--geo .dm-preset__name { font-size: 16px; }\n.dm-preset--geo .dm-preset__arrow {\n margin-left: auto;\n position: static;\n font-size: 16px;\n opacity: .3;\n transition: opacity .2s, transform .2s;\n}\n.dm-preset--geo:hover .dm-preset__arrow { opacity: .9; transform: translateX(4px); }\n/* Geo pulse ring on the icon */\n.dm-preset--geo .dm-preset__icon::after {\n content: '';\n position: absolute;\n width: 48px;\n height: 48px;\n border-radius: 50%;\n border: 1px solid var(--pc, var(--okdb-orange));\n opacity: 0;\n animation: dm-geo-ring 2.5s ease-out infinite;\n}\n.dm-preset__bg {\n position: absolute;\n inset: 0;\n background: radial-gradient(circle at 25% 25%, var(--pg, transparent), transparent 65%);\n opacity: 0;\n transition: opacity .3s;\n pointer-events: none;\n}\n.dm-preset:hover {\n border-color: var(--pc, var(--okdb-accent));\n box-shadow: 0 0 0 1px var(--pc, var(--okdb-accent))33, 0 8px 28px var(--pg, transparent);\n transform: translateY(-2px);\n}\n.dm-preset--geo:hover { transform: translateY(-2px); }\n.dm-preset:hover .dm-preset__bg { opacity: 1; }\n.dm-preset__icon {\n font-size: 26px;\n line-height: 1;\n margin-bottom: 4px;\n filter: drop-shadow(0 0 8px var(--pc, #fff));\n position: relative;\n}\n.dm-preset__name {\n font-size: 15px;\n font-weight: 700;\n color: var(--pc, #fff);\n position: relative;\n}\n.dm-preset__tag {\n font-size: 12px;\n opacity: .5;\n line-height: 1.4;\n position: relative;\n}\n.dm-preset__chips {\n display: flex;\n gap: 5px;\n font-size: 11px;\n opacity: .45;\n margin-top: 5px;\n position: relative;\n flex-wrap: wrap;\n align-items: center;\n}\n.dm-chip-dot { opacity: .5; }\n.dm-preset__arrow {\n position: absolute;\n bottom: 14px;\n right: 16px;\n font-size: 14px;\n opacity: .2;\n transition: opacity .2s, transform .2s;\n}\n.dm-preset:not(.dm-preset--geo):hover .dm-preset__arrow { opacity: .8; transform: translateX(3px); }\n@keyframes dm-geo-ring {\n 0% { opacity: .6; transform: scale(.6); }\n 100% { opacity: 0; transform: scale(2.2); }\n}\n\n/* ── Back button ───────────────────────────────────────────── */\n.dm-back-btn {\n font-size: 16px;\n padding: 3px 10px;\n opacity: .5;\n border-radius: 6px;\n transition: opacity .15s;\n}\n.dm-back-btn:hover { opacity: 1; }\n\n/* ── Preset badge (configure step) ────────────────────────── */\n.dm-preset-badge {\n font-size: 22px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 36px;\n height: 36px;\n border-radius: 10px;\n background: var(--pg, var(--okdb-app-surface));\n border: 1px solid var(--pc, var(--okdb-app-border))33;\n filter: drop-shadow(0 0 8px var(--pc, transparent));\n}\n\n/* ── Scale buttons ─────────────────────────────────────────── */\n.dm-scale-btn {\n padding: 5px 12px;\n font-size: 12px;\n border-radius: 7px;\n border: 1px solid var(--okdb-app-border);\n transition: all .15s;\n}\n.dm-scale-btn--active {\n background: var(--pc, var(--okdb-accent))22 !important;\n border-color: var(--pc, var(--okdb-accent)) !important;\n color: var(--pc, var(--okdb-accent)) !important;\n font-weight: 600;\n}\n\n/* ── Type count table ──────────────────────────────────────── */\n.dm-type-table {\n background: var(--okdb-app-bg-deep);\n border: 1px solid var(--okdb-app-surface);\n border-radius: 9px;\n overflow: hidden;\n}\n.dm-type-row {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 8px 14px;\n border-bottom: 1px solid var(--okdb-app-border);\n}\n.dm-type-row:last-child { border-bottom: none; }\n.dm-type-row__name {\n font-family: monospace;\n font-size: 12px;\n width: 90px;\n flex-shrink: 0;\n opacity: .8;\n}\n.dm-type-row__bar-wrap {\n flex: 1;\n height: 4px;\n background: var(--okdb-app-surface);\n border-radius: 2px;\n overflow: hidden;\n}\n.dm-type-row__bar {\n height: 100%;\n border-radius: 2px;\n transition: width .25s ease-out;\n min-width: 4px;\n}\n.dm-type-row__count {\n font-family: monospace;\n font-size: 12px;\n font-weight: 600;\n width: 70px;\n text-align: right;\n flex-shrink: 0;\n}\n\n/* ── Checkbox labels ───────────────────────────────────────── */\n.dm-check-label {\n display: flex;\n align-items: center;\n gap: 7px;\n cursor: pointer;\n font-size: 13px;\n user-select: none;\n}\n\n/* ── Summary pill ──────────────────────────────────────────── */\n.dm-summary {\n padding: 10px 14px;\n border-radius: 9px;\n border: 1px solid var(--okdb-app-border);\n font-size: 13px;\n display: flex;\n gap: 5px;\n align-items: center;\n flex-wrap: wrap;\n line-height: 1.6;\n}\n\n/* ── Generate button ───────────────────────────────────────── */\n.dm-gen-btn {\n background: var(--pg, color-mix(in srgb, var(--okdb-accent) 7%, transparent)) !important;\n border: 1px solid var(--pc, var(--okdb-accent)) !important;\n color: var(--pc, var(--okdb-accent)) !important;\n font-weight: 700;\n font-size: 13px;\n padding: 7px 20px;\n border-radius: 8px;\n transition: all .2s;\n letter-spacing: .01em;\n}\n.dm-gen-btn:hover:not(:disabled) {\n background: var(--pc, var(--okdb-accent))33 !important;\n box-shadow: 0 0 20px var(--pc, var(--okdb-accent))44;\n}\n.dm-gen-btn:disabled {\n opacity: .4;\n cursor: not-allowed;\n}\n\n/* ── Footer row ────────────────────────────────────────────── */\n.dm-footer {\n display: flex;\n gap: 8px;\n justify-content: flex-end;\n padding-top: 12px;\n border-top: 1px solid var(--okdb-app-surface);\n}\n\n/* ── Generation header ─────────────────────────────────────── */\n.dm-gen-header {\n padding: 14px 16px;\n background: var(--okdb-app-bg-deep);\n border: 1px solid var(--okdb-app-surface);\n border-radius: 10px;\n box-shadow: inset 0 0 40px var(--pg, transparent);\n}\n.dm-pulsing-icon {\n font-size: 20px;\n display: inline-block;\n animation: dm-pulse 1.5s ease-in-out infinite;\n filter: drop-shadow(0 0 6px var(--pc, transparent));\n}\n.dm-progress-track {\n height: 7px;\n background: var(--okdb-app-surface);\n border-radius: 4px;\n overflow: hidden;\n}\n.dm-progress-fill {\n height: 100%;\n border-radius: 4px;\n transition: width .35s ease-out;\n position: relative;\n}\n.dm-progress-fill--shimmer::after {\n content: '';\n position: absolute;\n inset: 0;\n background: linear-gradient(90deg, transparent, rgba(255,255,255,.25), transparent);\n animation: dm-shimmer 1.5s linear infinite;\n}\n\n/* ── Step rows ─────────────────────────────────────────────── */\n.dm-steps {\n display: flex;\n flex-direction: column;\n gap: 2px;\n max-height: 320px;\n overflow-y: auto;\n padding-right: 4px;\n}\n.dm-step {\n display: flex;\n align-items: center;\n gap: 9px;\n padding: 7px 10px;\n border-radius: 7px;\n font-size: 12px;\n transition: background .1s;\n}\n.dm-step--pending { opacity: .32; }\n.dm-step--running { background: var(--okdb-app-surface); opacity: 1; }\n.dm-step--done { opacity: .6; }\n.dm-step--error { background: var(--okdb-danger-deep-bg); opacity: 1; }\n.dm-step__icon {\n width: 18px;\n text-align: center;\n flex-shrink: 0;\n font-size: 12px;\n}\n.dm-step--done .dm-step__icon { color: var(--okdb-success); }\n.dm-step--running .dm-step__icon { color: var(--okdb-accent); }\n.dm-step--error .dm-step__icon { color: var(--okdb-danger); }\n.dm-step__label { flex: 1; }\n.dm-step__count {\n font-size: 11px;\n font-family: monospace;\n opacity: .5;\n flex-shrink: 0;\n}\n.dm-step__mini-bar {\n width: 70px;\n height: 3px;\n background: var(--okdb-app-surface);\n border-radius: 2px;\n overflow: hidden;\n flex-shrink: 0;\n}\n.dm-step__mini-fill {\n height: 100%;\n background: var(--okdb-accent);\n border-radius: 2px;\n transition: width .2s ease-out;\n}\n\n/* ── Error banner ──────────────────────────────────────────── */\n.dm-error {\n color: var(--okdb-danger);\n font-size: 13px;\n padding: 9px 13px;\n background: var(--okdb-danger-deep-bg);\n border: 1px solid color-mix(in srgb, var(--okdb-danger) 20%, transparent);\n border-radius: 7px;\n}\n\n/* ── Done orb ──────────────────────────────────────────────── */\n.dm-done-orb {\n position: relative;\n width: 96px;\n height: 96px;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n.dm-done-orb__icon {\n font-size: 40px;\n position: relative;\n z-index: 2;\n filter: drop-shadow(0 0 16px var(--pc, var(--okdb-accent)));\n animation: dm-pulse 2.5s ease-in-out infinite;\n}\n.dm-done-orb__ring {\n position: absolute;\n inset: 0;\n border-radius: 50%;\n border: 1px solid var(--pc, var(--okdb-accent));\n}\n.dm-done-orb__ring--1 {\n opacity: .4;\n animation: dm-ring-pulse 2s ease-in-out infinite;\n}\n.dm-done-orb__ring--2 {\n opacity: .2;\n animation: dm-ring-pulse 2s ease-in-out infinite .7s;\n transform: scale(1.3);\n}\n\n/* ── Done stats ────────────────────────────────────────────── */\n.dm-done-stats {\n display: flex;\n align-items: center;\n gap: 20px;\n padding: 16px 32px;\n background: var(--okdb-app-bg-deep);\n border: 1px solid var(--pc, var(--okdb-accent))33;\n border-radius: 14px;\n box-shadow: 0 0 30px var(--pg, transparent);\n}\n.dm-stat {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 5px;\n}\n.dm-stat__val {\n font-size: 28px;\n font-weight: 800;\n line-height: 1;\n letter-spacing: -.02em;\n}\n.dm-stat__lbl {\n font-size: 10px;\n text-transform: uppercase;\n letter-spacing: .08em;\n opacity: .45;\n}\n.dm-stat-divider {\n width: 1px;\n height: 36px;\n background: var(--okdb-app-border);\n flex-shrink: 0;\n}\n\n/* ── Animations ────────────────────────────────────────────── */\n@keyframes dm-hue {\n from { filter: hue-rotate(0deg); }\n to { filter: hue-rotate(360deg); }\n}\n@keyframes dm-pulse {\n 0%, 100% { transform: scale(1); }\n 50% { transform: scale(1.12); }\n}\n@keyframes dm-ring-pulse {\n 0%, 100% { opacity: .3; transform: scale(1); }\n 50% { opacity: .7; transform: scale(1.1); }\n}\n@keyframes dm-shimmer {\n from { transform: translateX(-100%); }\n to { transform: translateX(300%); }\n}\n@keyframes dm-spin {\n from { transform: rotate(0deg); }\n to { transform: rotate(360deg); }\n}\n.dm-spin {\n display: inline-block;\n animation: dm-spin 1s linear infinite;\n}\n"};