···18181919### Lex Extension Architecture
20202121-The lex extension lives in `extensions/lex/` and provides a "studio" interface for working with AT Protocol lexicon schemas. Based on the codebase structure:
2121+The lex extension lives in `extensions/lex/` and is a **fully declarative extension** (`"background": false` in manifest). No background.js — all logic runs in the UI pages directly. Files:
2222+2323+- **`manifest.json`** -- Declares two commands: "lexicon studio" (opens workspace window) and "new" (also opens studio). Both open `peek://lex/home.html` as a workspace window with `webSecurity: false` (needed for cross-origin lexicon fetches).
2424+- **`home.js`** -- The main studio UI. Handles OAuth login, PDS browsing (collections, records), lexicon exploration, record creation/deletion, and the "Recent Lexicons" sidebar. Imports from `lexicon-ui.js` and `atproto.js`.
2525+- **`chain-form.js`** -- A compact popup form for cmd palette chaining. Opened by cmd panel's `openChainPopup()` as a modal child window. Receives an NSID via pubsub, resolves the schema, generates a form, and creates the record directly via XRPC. The "chain" refers to cmd palette chaining, not nested definitions.
2626+- **`lexicon-ui.js`** -- The core form generation engine. Contains: lexicon schema resolution (layered: memory cache → persisted settings → GitHub raw URLs → lexicon.store API → domain-based resolution), dynamic form generation from schemas, form data collection, rich text facet detection, and raw JSON editor fallback.
2727+- **`atproto.js`** -- AT Protocol client: OAuth login, XRPC calls, blob upload, record CRUD, actor search.
2828+- **`home.css`** -- Studio UI styling.
2929+3030+### Current Form Generation (lexicon-ui.js)
3131+3232+The `generateForm(nsid, schema, lexicon, options)` function takes a lexicon record schema and produces a DOM `<form>` element. It:
3333+3434+1. **Sorts fields**: required first, then alphabetical, `createdAt` always last
3535+2. **Renders each field** via `renderField()` → `renderInput()` which dispatches by type:
3636+ - **string**: text input, with format-specific variants: `datetime` → `datetime-local`, `uri` → url input, `did` → text with placeholder, `handle` → text, `language` → dropdown of 20 languages + custom BCP-47. Long text (maxLength > 500 or field name matches content/body/text/description) → textarea with character counter. `enum` → closed dropdown, `knownValues` → open dropdown with "Other..." option.
3737+ - **integer/number**: number input with min/max/step constraints
3838+ - **boolean**: checkbox
3939+ - **blob**: file upload with size info
4040+ - **ref**: resolves the `$ref` (local `#name` or external `nsid#name`), renders the resolved type inline. Recursive up to depth 4.
4141+ - **union**: type selector dropdown, then renders the selected variant's fields inline
4242+ - **array**: "Add item" button, renders each item's input, "Remove" buttons, item counter with optional maxLength
4343+ - **object**: renders nested properties recursively
4444+ - **unknown/bytes/fallback**: raw JSON textarea
4545+3. **Auto-fills** `createdAt` with current datetime
4646+4. **Data collection** via `collectFormData()` mirrors the rendering structure — walks the DOM collecting values by type, handles unions ($type tagging), arrays, nested objects, blob files, and datetime ISO conversion. Special-cases `app.bsky.feed.post` for rich text facet detection (URLs, @mentions, #hashtags with byte-level indexing).
4747+4848+### Schema Resolution
22492323-- **`manifest.json`** -- Extension manifest declaring the lex extension's capabilities, pages, and settings within Peek's extension system.
2424-- **`chain-form.js`** -- The core form generation component. Handles dynamic form rendering from lexicon schema definitions. "Chain" likely refers to the chained/nested nature of lexicon definitions where objects contain refs to other definitions.
2525-- **Background/home scripts** -- Handle lexicon loading, parsing, and data management.
5050+Lexicon schemas are resolved via a layered strategy in `resolveLexicon(nsid)`:
5151+1. In-memory `Map` cache
5252+2. Persisted cache in extension settings (`lexiconSchemas` key) — restored on first cache miss
5353+3. GitHub raw URLs for known prefixes: `app.bsky.*`, `com.atproto.*`, `chat.bsky.*`, `tools.ozone.*`, `fyi.frontpage.*`, `community.lexicon.*`
5454+4. `api.lexicon.store` REST API
5555+5. Domain-based resolution: NSID authority → domain → `/lexicons/{path}.json`
5656+5757+Resolved schemas are persisted to extension settings with `fetchedAt` timestamps.
5858+5959+### What the Studio Looks Like
6060+6161+The home.js UI has multiple view states managed by navigation:
6262+- **Login/Identity panel** — OAuth login to a PDS, profile display
6363+- **My Lexicons** — browsable list of collections on the user's PDS with record counts
6464+- **Records list** — paginated record list within a collection, with delete capability
6565+- **Record detail** — raw JSON view of a single record
6666+- **Create Record** — either the generated form (via `generateForm()`) or raw JSON editor
6767+- **Explore** — browse known lexicon registries, search for lexicons
6868+- **Recent Lexicons** — sidebar section (always visible) showing recently interacted-with lexicons sorted by recency
26692727-### Current Form Generation
7070+### Dynamic Command Registration
28712929-The current system takes a lexicon schema definition and generates an editor form automatically. This is a "raw" editor -- it maps lexicon field types directly to basic HTML form inputs without any domain-specific customization. For example, a `datetime` field gets a plain text input rather than a date picker, and a `ref` to a user DID gets a text input rather than a contact selector.
7272+The studio dynamically registers `new {FriendlyName}` commands in the cmd palette for every known collection NSID. This means users can type "new Post" or "new Follow" to get a chain-form popup for that lexicon type. Commands are rebuilt when collections change.
30733174### AT Protocol Lexicon Format
3275···425468- Published templates get the security and addressing benefits of Web Tiles
426469- The fiddle UI can work at any layer
427470471471+### Building on lexicon-ui.js (Key Insight)
472472+473473+The existing `renderInput()` dispatch in `lexicon-ui.js` is already a primitive widget system — it maps schema types to DOM elements. The template system should **extend this, not replace it**:
474474+475475+1. **Edit templates** are essentially a configuration layer over `renderInput()`. Instead of the hardcoded type→input mapping, an edit template specifies which widget to use per field, plus layout. The auto-generated default becomes: "for each field, pick the widget that `renderInput()` would have picked, arrange vertically."
476476+477477+2. **The `generateForm()` function already handles**: field sorting, required markers, descriptions, labels, auto-fill, recursive depth limits. Templates should be able to override these defaults per-field while inheriting the rest.
478478+479479+3. **`collectFormData()` already handles**: type coercion, union $type tagging, nested objects, arrays, blob files, datetime normalization, facet detection. New widgets need to implement the same `getValue()` contract so the collection logic works unchanged.
480480+481481+4. **Schema resolution is already solved**: `resolveLexicon()` with its layered cache handles cross-reference resolution. The template system gets this for free.
482482+483483+The practical path: add a `templateOverrides` parameter to `generateForm()` that lets fields be remapped to different widgets, reordered, grouped into sections, or hidden. This preserves the existing auto-generation as the default while allowing progressive customization.
484484+428485### MASL Evaluation Summary
429486430487MASL (Metadata for Arbitrary Structures and Links) is specifically a **metadata and manifest format**, not a template definition language. It describes resources (files, blobs) with their CIDs, sizes, and MIME types. It is the manifest format for Web Tiles, not the template language itself.
···523580524581### Field Type to Widget Mapping
525582526526-The widget system maps lexicon field types and formats to appropriate UI widgets. Each widget handles both display (detail view) and input (edit view).
583583+The widget system maps lexicon field types and formats to appropriate UI widgets. The "Current" column shows what `lexicon-ui.js` does today; "Template upgrade" shows what a template could specify instead.
527584528528-| Lexicon Type | Format/Constraint | Default Widget | Alternative Widgets |
585585+| Lexicon Type | Format/Constraint | Current (lexicon-ui.js) | Template Upgrade |
529586|---|---|---|---|
530530-| `string` | (none) | `text` | `textarea`, `richtext`, `select` |
531531-| `string` | `maxLength > 256` | `textarea` | `richtext` |
532532-| `string` | `datetime` | `datetime` | `date`, `time`, `daterange` |
533533-| `string` | `uri` | `uri-input` | `text` |
534534-| `string` | `at-uri` | `at-uri-picker` | `text` |
535535-| `string` | `did` | `did-picker` | `handle-picker`, `text` |
536536-| `string` | `handle` | `handle-picker` | `text` |
537537-| `string` | `language` | `language-select` | `text` |
538538-| `string` | `enum` values | `select` | `radio` |
539539-| `integer` | (none) | `number` | `slider`, `rating` |
540540-| `integer` | `min`/`max` defined | `slider` | `number` |
541541-| `boolean` | (none) | `toggle` | `checkbox` |
542542-| `number` | (none) | `number` | `slider` |
543543-| `blob` | (none) | `blob-upload` | `image-upload` |
544544-| `bytes` | (none) | `blob-upload` | `text` (base64) |
545545-| `array` | items: string | `tag-picker` | `multiselect`, `text-list` |
546546-| `array` | items: ref | `ref-list` | `inline-editor` |
547547-| `ref` | (none) | `ref-picker` | `inline-editor` |
548548-| `union` | (none) | `union-selector` | (auto from variants) |
549549-| `object` | (none) | `nested-form` | `json-editor` |
587587+| `string` | (none) | `<input type="text">` | `richtext`, `select`, `code` |
588588+| `string` | long text (>500 chars or name matches content/body/text) | `<textarea>` with char counter | `richtext` with markdown toolbar |
589589+| `string` | `const` | `<input type="hidden">` | (keep hidden) |
590590+| `string` | `enum` | closed `<select>` dropdown | `radio` buttons, pill selector |
591591+| `string` | `knownValues` | open `<select>` + "Other..." text input | same + autocomplete |
592592+| `string` | `datetime` | `<input type="datetime-local" step="1">` | date picker, daterange, relative |
593593+| `string` | `uri` | `<input type="url">` | link preview, embed picker |
594594+| `string` | `at-uri` | `<input type="text">` placeholder | record picker with search |
595595+| `string` | `did` | `<input type="text">` placeholder | `did-picker` with avatar/search |
596596+| `string` | `handle` | `<input type="text">` placeholder | `handle-picker` with search |
597597+| `string` | `language` | 20-language `<select>` + custom BCP-47 | same (already good) |
598598+| `string` | `cid`, `nsid`, `tid`, `record-key` | `<input type="text">` | (keep as-is, niche types) |
599599+| `integer`/`number` | (none) | `<input type="number">` with min/max/step | `slider`, `rating` |
600600+| `boolean` | (none) | `<input type="checkbox">` | `toggle` switch |
601601+| `blob` | (none) | `<input type="file">` with size info | drag-drop with preview, image crop |
602602+| `bytes` | (none) | JSON textarea fallback | hex editor, base64 |
603603+| `array` | any items | "Add item" button + per-item inputs + "Remove" + counter | `tag-picker`, `multiselect`, sortable list |
604604+| `ref` | (none) | resolves and renders inline (recursive to depth 4) | `ref-picker` with search, inline collapse |
605605+| `union` | (none) | type `<select>` + renders selected variant inline | tabbed variants, visual type selector |
606606+| `object` | (none) | renders nested properties recursively | collapsible sections, tabs |
607607+| unknown/fallback | (none) | JSON `<textarea>` | `json-editor` with syntax highlighting |
550608551609### Widget Interface
552610553553-Each widget should implement a standard interface:
611611+Each widget should implement a standard interface compatible with `collectFormData()`:
554612555613```
556614Widget Contract:
557615 - render(value, schema, options) -> DOM element
558558- - getValue() -> field value
616616+ - getValue() -> field value // must match what collectFieldValue() expects
559617 - validate(value, schema) -> { valid: boolean, errors: string[] }
560618 - onChange(callback) -> void
561619 - setReadOnly(boolean) -> void
562620```
621621+622622+**Compatibility note**: The existing `collectFormData()` collects values by querying DOM elements (`input`, `textarea`, `select`) by name attribute. New widgets MUST either:
623623+- Use standard form elements with `name` attributes (simplest — works with existing collection)
624624+- OR register a custom collector in a widget registry that `collectFormData()` consults
563625564626### Custom Widgets
565627···679741680742### Phase 1: Foundation (Edit Templates Only)
681743682682-**Goal:** Replace the default generated form with a configurable one.
744744+**Goal:** Make the existing form generation configurable without breaking it.
683745684684-- Define the `fieldDef` and `editTemplate` JSON schema
685685-- Build the widget registry with 10 core widgets: `text`, `textarea`, `number`, `toggle`, `select`, `date`, `datetime`, `did-picker`, `blob-upload`, `json-editor`
686686-- Implement auto-generation of an `editTemplate` from a lexicon schema (replacing current chain-form behavior with a configurable equivalent)
687687-- Add a "Customize Form" button that opens a simple field editor (reorder fields, change widgets, set labels)
688688-- Store custom edit templates in extension settings (local only)
746746+- Add a `templateOverrides` parameter to `generateForm()` in `lexicon-ui.js` — an optional JSON object that can override widget type, label, placeholder, hidden, readOnly, order, and width per field path
747747+- Build a widget registry that `renderInput()` consults before falling back to its hardcoded type dispatch. Initially register the existing renderers as named widgets (text, textarea, number, checkbox, select, datetime-local, file, json-fallback)
748748+- Add 3-4 new widgets that are clear upgrades: `toggle` (replaces checkbox for booleans), `did-picker` (search by handle, show avatar — replaces plain text input for DIDs), `richtext` (markdown toolbar — replaces plain textarea for long text)
749749+- Auto-generate a default `editTemplate` JSON from any lexicon schema (captures what `generateForm()` would do, as serializable JSON)
750750+- Add a "Customize Form" button in the studio's Create Record view that opens a simple field editor (reorder fields, change widgets, set labels)
751751+- Store custom edit templates in extension settings keyed by NSID
752752+- `chain-form.js` also reads templates via the same path — so cmd palette "new" commands get customized forms too
689753690690-**Validates:** Widget system works, field mapping is correct, the customization UX is usable.
754754+**Validates:** Widget registry works, existing forms don't break, customization UX is usable, templates serialize correctly.
691755692756### Phase 2: Fiddle UI (Two-Pane Start)
693757