# `supero-ui.js` — Modularization Plan

> **Status:** plan only. No files moved yet. Sign-off needed on the proposed
> layering before execution.
>
> **Scope:** carve the 15 verbatim-split files into a **layered architecture**
> (api_core / services_api / ui_core / ui_service / ui_app) without breaking
> runtime. All work stays in `web/`. The canonical `../supero-ui.js`
> remains untouched.

---

## TL;DR — can we do it without breaking?

**Yes.** The handoff doc's §4c finding ("backend code reaches sideways into
UI, this is a coupling problem not a splitting problem") **overstated the
problem**. After looking at the actual code:

- **SuperoClient has zero UI dependencies.** Searched its full body —
  calls only `this.*`, internal helpers (`_extractObjectUuid`,
  `_qualifySchema`), browser APIs (`fetch`, `Promise`, `setTimeout`),
  and standard JS (`Array.isArray`, `Object.assign`).
- **The `services` map has zero UI dependencies.** It only calls `_svc`,
  which only calls `client.request`. Pure data.
- **The `SuperoTransactionalRegistry` + error classes + `_classifyHttpError`
  + `_txnRunAction` are pure** (UI lives in separate functions in the
  same file).

Pass-10 of `_verify.py` flagged 36 "upward references" — but the
breakdown is:

| Category | Count | What it actually is | Fix needed? |
|----------|-------|---------------------|-------------|
| **Comment-only in file 01 header** | 12 | File header docstring literally lists "Provides: SuperoClient, CrudModal, formatters…" | none |
| **Dispatcher late-binding pattern** | 9 | `if (typeof ImageCard === 'undefined') return null; React.createElement(ImageCard, props)` — dispatcher *optionally* uses components by name, doesn't depend on them | none (intentional pattern) |
| **`typeof X !== 'undefined'` guards elsewhere** | 4 | Same late-binding pattern in DataTab → TransactionalListView, ActionButtons → ACTION_OPS_UX | none |
| **Local var shadowing the global** | 1 | `var services = _cache.services \|\| {}` inside SchemaRegistry — not the global `services` map | none (false positive) |
| **`useState`/`useEffect` aliases** | 2 | File 02 uses `useState`/`useEffect` which are React aliases defined in file 05 | trivial — move aliases to a util layer OR use `React.useState` directly |
| **Misplaced UI in a "schema" file** | ~3 | `ChildListPanel` (line 675 of file 02) is a UI component sitting in the schema-registry file. Calls `getStatusColor` etc. | trivial — move ChildListPanel to ui_service |
| **Real intra-UI compositions** | ~5 | ActionButtons → `openInitiatorFormModal` / `_suiRenderCardActions`; shells → `_NavWidgetBar`; transactional UI → `_suiEmptyStateProps` | none for backend carve-out (these are UI-to-UI, both end up in ui_service) |

**Net: zero backend → UI calls. The clean carve is possible.**

The remaining fixes for a *fully* layered architecture (UI layer
self-contained too) are minor (move ChildListPanel, move React aliases).

---

## Proposed layering

Five layers, loaded in this order (dependency-respecting):

```
┌─────────────────────────────────────────────────────────────────┐
│ 00-header        config, load guards, React/Tailwind sanity     │
├─────────────────────────────────────────────────────────────────┤
│ 10-api_core      data contract + transport                      │
│   SuperoClient, SuperoSchemaRegistry, SuperoUISchemaResolver,   │
│   _qualifySchema, _SYSTEM_TYPES, fileService,                   │
│   _extractObjectUuid, formatCurrency/Number/Date/FieldValue,    │
│   resolveImageUrl, state-machine helpers (getEnabledOperations, │
│   parseStateTransitionError, getStateMetadata)                  │
├─────────────────────────────────────────────────────────────────┤
│ 20-services_api  service execution contract (data only)         │
│   _svc, services map (email/sms/stripe/billing/…/ai),           │
│   aiChatStream/aiConfirmAction/aiRejectAction,                  │
│   SuperoTransactionalRegistry, Txn*Error (6 classes),           │
│   _classifyHttpError, _txnRunAction,                            │
│   SERVICE_MANIFESTS, SuperoServiceRegistry, ACTION_OPS,         │
│   ACTION_OPS_UX, _superoBackendIdForKey                         │
├─────────────────────────────────────────────────────────────────┤
│ 30-ui_core       UI primitives (no service knowledge)           │
│   _router, icons, button factory, Card/Section/Badge/Skeleton/  │
│   Spinner/EmptyState, IconButton, AuthedImg, _SUPERO_TENANT_*,  │
│   theme system (_DEFAULT_THEME, SUPERO_THEME_PRESETS, _hexToHSL,│
│   _hslToHex, _generatePalette, _resolveThemePreset, THEME,      │
│   _LIGHT_TOKENS, _DARK_TOKENS, _resolveTheme, _applyTheme,      │
│   _injectGlobalStyles), ErrorBoundary, _validateConfig, logger, │
│   getLayoutType, renderMarkdown, Sparkline, StatCard,           │
│   DashboardChart, _CHART_PALETTE                                │
├─────────────────────────────────────────────────────────────────┤
│ 40-ui_service    service-aware UI (knows api_core + services)   │
│   SuperoFieldEditor, SuperoUISchemaResolver-renderers,          │
│   ChildListPanel, ImageFieldInput, ImageListFieldInput,         │
│   ParentChainSelector, TenantSwitcher, BrowseList,              │
│   ReferenceDropdowns, TagListInput, CheckboxGrid,               │
│   SlideOverForm, CrudModal, DeleteModal, SvgBarChart,           │
│   WorkflowBadge, WorkflowTriggerButton, InvoicePayPanel,        │
│   parse422Error, buildFieldsFromAttrs, ToolStepRow,             │
│   AIChatPanel, ImageCard, ProfileCard, GenericCard,             │
│   TemplateCard, ActionButtons, CinemaCard, MagazineListCard,    │
│   CompactImageRow, CleanListRow, KanbanView,                    │
│   SuperoViewDispatcher, SuperoCardDispatcher,                   │
│   SuperoDetailDispatcher, DetailFrame/GenericDetail/Profile/    │
│   Transactional Detail, DetailView, FullPageCreate, DataTab,    │
│   WorkflowTestPanel, SuperoWidgetRegistry, RecentListWidget,    │
│   TransactionalListView, TransactionalDetailView, TxnStateBadge,│
│   TxnActionButton, txnToast, SuperoModal + initiator flow,      │
│   ServiceWidget, _suiRenderField/Output/Card*, _ServiceAction*  │
├─────────────────────────────────────────────────────────────────┤
│ 50-ui_app        composite app pages (knows ui_service)         │
│   LoginPage, SignupPage, ForgotPasswordPage, GoogleSignInButton,│
│   PasswordInput, SUPERO_HERO_VARIANTS, SUPERO_STATS_VARIANTS,   │
│   SUPERO_FEATURE_GRID_VARIANTS, HeroSection, LandingNav/Footer/ │
│   Description, CreateTenantPage, JoinTenantPage, LandingPage,   │
│   Sidebar, Dashboard, TopNavShell, MagazineShell, MinimalShell, │
│   SearchOverlay, ChangePasswordModal/Mount, AppShell,           │
│   _NavWidgetBar, _NavWidget                                     │
├─────────────────────────────────────────────────────────────────┤
│ 99-exports       all window.X = X assignments, in one place      │
└─────────────────────────────────────────────────────────────────┘
```

### Why five layers (not the user's 4)?

The user proposed `api_core / services_api / ui_core / ui_service`. I'd
add `ui_app` as a fifth, because:

- `ui_core` should be **app-agnostic** (a Button, a Card, a router, a
  toast — could be used in any Supero app)
- `ui_service` should be **service-aware** (knows about CRUD, schemas,
  workflows — but not about *this specific generated app's* pages)
- `ui_app` is **composite/app-shell** code (Login page, AppShell, Dashboard,
  Landing variants) — this is the largest chunk (~5,000 lines)
- Lumping `ui_app` into `ui_service` creates a 12,000-line `ui_service`
  layer that's hard to navigate; separating it gives a clean tree.

If you prefer 4 layers, merge `ui_app` into `ui_service` — works the
same at runtime, just less readable.

---

## Proposed new file layout (in `web/`)

| Layer | File | Approx. lines | Notes |
|-------|------|---------------|-------|
| 00 | `00-header.js` | 105 | unchanged from `01-header-config.js` |
| 10 | `10-api-client.js` | ~900 | SuperoClient, _extractObjectUuid, schema qualifier |
| 10 | `11-api-schemas.js` | ~700 | SuperoSchemaRegistry, _SYSTEM_TYPES, SuperoUISchemaResolver (data parts only) |
| 10 | `12-api-helpers.js` | ~400 | fileService, formatters, resolveImageUrl, state-machine helpers |
| 20 | `20-services-map.js` | ~250 | _svc, services map, AI chat stream calls |
| 20 | `21-services-transactional.js` | ~250 | TransactionalRegistry, Txn*Error, _classifyHttpError, _txnRunAction |
| 20 | `22-services-catalog.js` | ~900 | SERVICE_MANIFESTS, SuperoServiceRegistry, ACTION_OPS, ACTION_OPS_UX |
| 30 | `30-ui-primitives.js` | ~1,200 | _router, icons, buttons, Card/Section/Badge/Skeleton/Spinner, AuthedImg, tenant ctx |
| 30 | `31-ui-theme.js` | ~700 | theme presets, palette, tokens, _resolveTheme/_applyTheme/_injectGlobalStyles |
| 30 | `32-ui-charts-text.js` | ~400 | Sparkline, StatCard, DashboardChart, renderMarkdown, ErrorBoundary |
| 40 | `40-ui-fields-forms.js` | ~1,400 | SuperoFieldEditor, ChildListPanel, image inputs, BrowseList, ReferenceDropdowns, TagListInput, CheckboxGrid, SlideOverForm, CrudModal, DeleteModal |
| 40 | `41-ui-cards-actions.js` | ~1,000 | ImageCard/Profile/Generic/Template/Cinema/Magazine/Compact/CleanList, KanbanView, ActionButtons |
| 40 | `42-ui-dispatchers.js` | ~400 | View/Card/Detail Dispatchers + late-binding registry |
| 40 | `43-ui-detail-data.js` | ~1,500 | DetailFrame, GenericDetail, ProfileDetail, TransactionalDetail, DetailView, FullPageCreate, DataTab |
| 40 | `44-ui-transactional.js` | ~800 | TransactionalListView, TransactionalDetailView, TxnStateBadge, TxnActionButton, txnToast |
| 40 | `45-ui-modal-initiator.js` | ~700 | SuperoModal + entire initiator-flow subsystem |
| 40 | `46-ui-service-action.js` | ~1,800 | ServiceWidget, _sui* helpers, _ServiceActionForm/Result |
| 40 | `47-ui-chat-invoice.js` | ~700 | AIChatPanel, InvoicePayPanel, parse422Error, buildFieldsFromAttrs |
| 40 | `48-ui-workflow-widgets.js` | ~400 | WorkflowBadge, WorkflowTriggerButton, WorkflowTestPanel, SvgBarChart, SuperoWidgetRegistry |
| 50 | `50-app-auth.js` | ~700 | LoginPage, SignupPage, ForgotPasswordPage, GoogleSignInButton, PasswordInput |
| 50 | `51-app-landing.js` | ~2,800 | Hero/Stats/FeatureGrid variants, HeroSection, LandingNav/Footer/Description, LandingPage |
| 50 | `52-app-tenant.js` | ~300 | CreateTenantPage, JoinTenantPage |
| 50 | `53-app-shell.js` | ~1,500 | Sidebar, Dashboard, TopNavShell, MagazineShell, MinimalShell, SearchOverlay, ChangePasswordModal, AppShell |
| 50 | `54-app-nav-widgets.js` | ~200 | _NavWidgetBar, _NavWidget |
| 99 | `99-exports.js` | ~150 | Every `window.X = X` assignment, consolidated |

**~25 files total** (vs current 15). Each one fits on one screen of a
file browser; the layer prefix makes ordering and load-sequence obvious.

---

## Required code changes (the "small fixes")

These are the only edits beyond moving symbols between files. Each is
mechanical and reversible.

### 1. Eliminate React-hook aliases dependency

**Current:** file 02 (schema-registry) uses `useState` / `useEffect`.
These are aliased in file 05 as `var useState = React.useState`. So
file 02 references identifiers defined in file 05.

**Options (pick one):**
- **(a)** Move the alias block (`var useState = React.useState; var
  useEffect = React.useEffect; var useRef = React.useRef; var useCallback
  = React.useCallback;`) to file `00-header.js`. One-line move, 4 aliases.
- **(b)** Replace bare `useState` / `useEffect` calls with `React.useState`
  / `React.useEffect`. ~20 grep-able call sites.

Recommend **(a)** — fewer touches, zero behavior change.

### 2. Move `ChildListPanel` out of the api_core layer

**Current:** `ChildListPanel` (file 02, line 675) is a React component
that calls `getStatusColor` (defined in file 06, ui_service). It's
misplaced in a schema-registry file.

**Fix:** move the `function ChildListPanel(_props) { … }` block
(lines 675–778 of current file 02) into the new `40-ui-fields-forms.js`.
No code change inside the function; just relocation.

### 3. Move `SuperoFieldEditor` out of the api_core layer

**Current:** `SuperoFieldEditor` (file 02, line 215) is also UI.
Defined as an IIFE that builds an editor-atom dispatcher.

**Fix:** move the whole IIFE block (lines 215–344 of current file 02)
into `40-ui-fields-forms.js`. The atom-dispatcher pattern is the same
as SuperoViewDispatcher, so it sits naturally next to forms.

### 4. Sentinel-mark the late-binding registry pattern

**Current:** files 04, 11, 09 use `if (typeof X === 'undefined')`
guards to optionally reference components from later-loaded files.
This is intentional but undocumented.

**Fix:** add a one-line `// LATE_BINDING_REGISTRY_V1` comment above
each `typeof X === 'undefined'` guard, so future readers know it's a
pattern, not a bug. Purely documentation; zero behavior change.
This also lets `_verify.py` pass 10 filter them out.

### 5. Consolidate `window.X = X` exports into `99-exports.js`

**Current:** 132 `window.X = X` assignments scattered across files
(top of file, inline after declaration, end of file).

**Fix:** move them all to `99-exports.js`. The declarations stay where
they are; only the export lines move. The exports file becomes the
single grep-able list of "what this library exposes."

---

## Verification contract change

The current contract is **byte-identical join** (concatenating split
files reproduces canonical bit-for-bit). That contract is **incompatible
with modularization** because moving symbols changes byte order.

**Proposed new contract** (`_verify_layered.py`):

| Pass | Check | Why |
|------|-------|-----|
| 1 | File count + total bytes ≤ canonical (cleanup may reduce) | basic sanity |
| 2 | Every `window.X = X` from canonical is present in `99-exports.js` exactly once | runtime contract preserved |
| 3 | Every top-level symbol from canonical is defined in exactly one new file | nothing lost |
| 4 | Layer ordering: no file in layer N references a symbol defined in layer ≥ N+1 (excluding `typeof X === 'undefined'` late-binding guards) | the layered architecture is real, not nominal |
| 5 | Each file is valid UTF-8 + ends with `\n` | same as before |
| 6 | **Join + run smoke test:** `cat 00-*.js 10-*.js … 99-*.js > /tmp/joined.js`, then check `node -c /tmp/joined.js` parses cleanly (and ideally load it in a browser, generate an app, click around) | semantic equivalence |
| 7 | **Backstop:** preserve the current 15 `NN-*.js` files in a `web/_verbatim_backup/` subdir, so the byte-identical guarantee against canonical is still available | rollback safety |

We **lose** byte-identical-join. We **gain**:
- Real layered architecture
- Layer-violation detection (catches future regressions)
- Single grep-able exports file
- Each file is small enough to read top-to-bottom

---

## Risks and mitigations

| Risk | Likelihood | Mitigation |
|------|-----------|------------|
| **Hidden ordering dependency** breaks when files are reordered (e.g., a top-level `if (X)` reads X before X is assigned) | Medium | Pass 6 catches this with `node -c` parse + actual browser load. Plus the backup in `_verbatim_backup/` makes rollback trivial. |
| **`window.X = X` consolidation** misses an inline conditional assignment (e.g., `if (typeof window !== "undefined") { window.X = X; }`) | Low | Pass 2 verifies counts match; a missing one fails loudly. |
| **Late-binding `typeof` guards** rely on script load order: if a layer-4 component is referenced from a layer-3 dispatcher, the dispatcher only works *after* layer 4 loads | Already true in canonical | Load order in canonical is: decl-position-order. The proposed layer order preserves this: dispatchers (layer 4) load after the components they optionally reference, so guards always pass. Verified case-by-case in proposed layout. |
| **Drift between web/ and canonical** if someone edits canonical | Already true | Add `_check_against_canonical.py` script: re-runs `_split.py` against canonical, asserts the verbatim backup is still byte-identical, and flags symbol drift. |
| **Refactor accidentally changes behavior** (e.g., moves a symbol but breaks a closure capture) | Low | All proposed moves are top-level symbols (functions, classes, vars). No closure captures across file boundaries because the current monolith doesn't use them either — everything is `window.*` or top-level `var`. |

---

## Estimated work

| Phase | What | Roughly |
|-------|------|---------|
| **A** | Write `_layered.py` that takes the current 15 files + a layer-mapping config and produces the ~25 new layered files; run it; verify with `_verify_layered.py` | half a day |
| **B** | Manual fixes (#1–#5 above): React aliases, move ChildListPanel + SuperoFieldEditor, sentinel-mark late-binding, consolidate exports | half a day |
| **C** | Join the layered files; load in a browser; smoke-test (login → CRUD → service action → transactional list) | 1–2 hours |
| **D** | Once green: optional propagation to canonical via the 5-layer rollout (handoff §3): update PROJECT_MANIFEST + server.py + CDN + test_bundle.py + e2e_tests.py + index.html template. Or **skip D entirely** and keep the layered split as the editing-aid + the canonical as the shipped artifact (same model as the current verbatim split) | 1 day if propagating; 0 if staying as editing aid |

---

## Open decisions (need your call before I execute)

1. **Layer count: 5 or 4?** 5 separates `ui_app` from `ui_service` (~25
   files); 4 lumps them (~20 files but `ui_service` becomes ~8,000 lines).
2. **React-hook aliases: move to header (recommended) or use `React.*` everywhere?**
3. **Sentinel-mark late-binding guards: yes (recommended, makes intent explicit) or no (less churn)?**
4. **Consolidate exports into `99-exports.js`: yes (recommended, single contract surface) or leave them scattered?**
5. **Propagate to canonical (Phase D)?** Default: no — keep web/ as editing aid until you explicitly say "ship this."
6. **Keep the verbatim backup in `web/_verbatim_backup/`?** Recommended: yes (rollback insurance).
