experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

docs: refine lex studio templates design doc with actual codebase analysis

+101 -37
+101 -37
LEX_TEMPLATES_DESIGN.md
··· 18 18 19 19 ### Lex Extension Architecture 20 20 21 - The lex extension lives in `extensions/lex/` and provides a "studio" interface for working with AT Protocol lexicon schemas. Based on the codebase structure: 21 + 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: 22 + 23 + - **`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). 24 + - **`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`. 25 + - **`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. 26 + - **`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. 27 + - **`atproto.js`** -- AT Protocol client: OAuth login, XRPC calls, blob upload, record CRUD, actor search. 28 + - **`home.css`** -- Studio UI styling. 29 + 30 + ### Current Form Generation (lexicon-ui.js) 31 + 32 + The `generateForm(nsid, schema, lexicon, options)` function takes a lexicon record schema and produces a DOM `<form>` element. It: 33 + 34 + 1. **Sorts fields**: required first, then alphabetical, `createdAt` always last 35 + 2. **Renders each field** via `renderField()` → `renderInput()` which dispatches by type: 36 + - **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. 37 + - **integer/number**: number input with min/max/step constraints 38 + - **boolean**: checkbox 39 + - **blob**: file upload with size info 40 + - **ref**: resolves the `$ref` (local `#name` or external `nsid#name`), renders the resolved type inline. Recursive up to depth 4. 41 + - **union**: type selector dropdown, then renders the selected variant's fields inline 42 + - **array**: "Add item" button, renders each item's input, "Remove" buttons, item counter with optional maxLength 43 + - **object**: renders nested properties recursively 44 + - **unknown/bytes/fallback**: raw JSON textarea 45 + 3. **Auto-fills** `createdAt` with current datetime 46 + 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). 47 + 48 + ### Schema Resolution 22 49 23 - - **`manifest.json`** -- Extension manifest declaring the lex extension's capabilities, pages, and settings within Peek's extension system. 24 - - **`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. 25 - - **Background/home scripts** -- Handle lexicon loading, parsing, and data management. 50 + Lexicon schemas are resolved via a layered strategy in `resolveLexicon(nsid)`: 51 + 1. In-memory `Map` cache 52 + 2. Persisted cache in extension settings (`lexiconSchemas` key) — restored on first cache miss 53 + 3. GitHub raw URLs for known prefixes: `app.bsky.*`, `com.atproto.*`, `chat.bsky.*`, `tools.ozone.*`, `fyi.frontpage.*`, `community.lexicon.*` 54 + 4. `api.lexicon.store` REST API 55 + 5. Domain-based resolution: NSID authority → domain → `/lexicons/{path}.json` 56 + 57 + Resolved schemas are persisted to extension settings with `fetchedAt` timestamps. 58 + 59 + ### What the Studio Looks Like 60 + 61 + The home.js UI has multiple view states managed by navigation: 62 + - **Login/Identity panel** — OAuth login to a PDS, profile display 63 + - **My Lexicons** — browsable list of collections on the user's PDS with record counts 64 + - **Records list** — paginated record list within a collection, with delete capability 65 + - **Record detail** — raw JSON view of a single record 66 + - **Create Record** — either the generated form (via `generateForm()`) or raw JSON editor 67 + - **Explore** — browse known lexicon registries, search for lexicons 68 + - **Recent Lexicons** — sidebar section (always visible) showing recently interacted-with lexicons sorted by recency 26 69 27 - ### Current Form Generation 70 + ### Dynamic Command Registration 28 71 29 - 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. 72 + 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. 30 73 31 74 ### AT Protocol Lexicon Format 32 75 ··· 425 468 - Published templates get the security and addressing benefits of Web Tiles 426 469 - The fiddle UI can work at any layer 427 470 471 + ### Building on lexicon-ui.js (Key Insight) 472 + 473 + 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**: 474 + 475 + 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." 476 + 477 + 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. 478 + 479 + 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. 480 + 481 + 4. **Schema resolution is already solved**: `resolveLexicon()` with its layered cache handles cross-reference resolution. The template system gets this for free. 482 + 483 + 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. 484 + 428 485 ### MASL Evaluation Summary 429 486 430 487 MASL (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. ··· 523 580 524 581 ### Field Type to Widget Mapping 525 582 526 - The widget system maps lexicon field types and formats to appropriate UI widgets. Each widget handles both display (detail view) and input (edit view). 583 + 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. 527 584 528 - | Lexicon Type | Format/Constraint | Default Widget | Alternative Widgets | 585 + | Lexicon Type | Format/Constraint | Current (lexicon-ui.js) | Template Upgrade | 529 586 |---|---|---|---| 530 - | `string` | (none) | `text` | `textarea`, `richtext`, `select` | 531 - | `string` | `maxLength > 256` | `textarea` | `richtext` | 532 - | `string` | `datetime` | `datetime` | `date`, `time`, `daterange` | 533 - | `string` | `uri` | `uri-input` | `text` | 534 - | `string` | `at-uri` | `at-uri-picker` | `text` | 535 - | `string` | `did` | `did-picker` | `handle-picker`, `text` | 536 - | `string` | `handle` | `handle-picker` | `text` | 537 - | `string` | `language` | `language-select` | `text` | 538 - | `string` | `enum` values | `select` | `radio` | 539 - | `integer` | (none) | `number` | `slider`, `rating` | 540 - | `integer` | `min`/`max` defined | `slider` | `number` | 541 - | `boolean` | (none) | `toggle` | `checkbox` | 542 - | `number` | (none) | `number` | `slider` | 543 - | `blob` | (none) | `blob-upload` | `image-upload` | 544 - | `bytes` | (none) | `blob-upload` | `text` (base64) | 545 - | `array` | items: string | `tag-picker` | `multiselect`, `text-list` | 546 - | `array` | items: ref | `ref-list` | `inline-editor` | 547 - | `ref` | (none) | `ref-picker` | `inline-editor` | 548 - | `union` | (none) | `union-selector` | (auto from variants) | 549 - | `object` | (none) | `nested-form` | `json-editor` | 587 + | `string` | (none) | `<input type="text">` | `richtext`, `select`, `code` | 588 + | `string` | long text (>500 chars or name matches content/body/text) | `<textarea>` with char counter | `richtext` with markdown toolbar | 589 + | `string` | `const` | `<input type="hidden">` | (keep hidden) | 590 + | `string` | `enum` | closed `<select>` dropdown | `radio` buttons, pill selector | 591 + | `string` | `knownValues` | open `<select>` + "Other..." text input | same + autocomplete | 592 + | `string` | `datetime` | `<input type="datetime-local" step="1">` | date picker, daterange, relative | 593 + | `string` | `uri` | `<input type="url">` | link preview, embed picker | 594 + | `string` | `at-uri` | `<input type="text">` placeholder | record picker with search | 595 + | `string` | `did` | `<input type="text">` placeholder | `did-picker` with avatar/search | 596 + | `string` | `handle` | `<input type="text">` placeholder | `handle-picker` with search | 597 + | `string` | `language` | 20-language `<select>` + custom BCP-47 | same (already good) | 598 + | `string` | `cid`, `nsid`, `tid`, `record-key` | `<input type="text">` | (keep as-is, niche types) | 599 + | `integer`/`number` | (none) | `<input type="number">` with min/max/step | `slider`, `rating` | 600 + | `boolean` | (none) | `<input type="checkbox">` | `toggle` switch | 601 + | `blob` | (none) | `<input type="file">` with size info | drag-drop with preview, image crop | 602 + | `bytes` | (none) | JSON textarea fallback | hex editor, base64 | 603 + | `array` | any items | "Add item" button + per-item inputs + "Remove" + counter | `tag-picker`, `multiselect`, sortable list | 604 + | `ref` | (none) | resolves and renders inline (recursive to depth 4) | `ref-picker` with search, inline collapse | 605 + | `union` | (none) | type `<select>` + renders selected variant inline | tabbed variants, visual type selector | 606 + | `object` | (none) | renders nested properties recursively | collapsible sections, tabs | 607 + | unknown/fallback | (none) | JSON `<textarea>` | `json-editor` with syntax highlighting | 550 608 551 609 ### Widget Interface 552 610 553 - Each widget should implement a standard interface: 611 + Each widget should implement a standard interface compatible with `collectFormData()`: 554 612 555 613 ``` 556 614 Widget Contract: 557 615 - render(value, schema, options) -> DOM element 558 - - getValue() -> field value 616 + - getValue() -> field value // must match what collectFieldValue() expects 559 617 - validate(value, schema) -> { valid: boolean, errors: string[] } 560 618 - onChange(callback) -> void 561 619 - setReadOnly(boolean) -> void 562 620 ``` 621 + 622 + **Compatibility note**: The existing `collectFormData()` collects values by querying DOM elements (`input`, `textarea`, `select`) by name attribute. New widgets MUST either: 623 + - Use standard form elements with `name` attributes (simplest — works with existing collection) 624 + - OR register a custom collector in a widget registry that `collectFormData()` consults 563 625 564 626 ### Custom Widgets 565 627 ··· 679 741 680 742 ### Phase 1: Foundation (Edit Templates Only) 681 743 682 - **Goal:** Replace the default generated form with a configurable one. 744 + **Goal:** Make the existing form generation configurable without breaking it. 683 745 684 - - Define the `fieldDef` and `editTemplate` JSON schema 685 - - Build the widget registry with 10 core widgets: `text`, `textarea`, `number`, `toggle`, `select`, `date`, `datetime`, `did-picker`, `blob-upload`, `json-editor` 686 - - Implement auto-generation of an `editTemplate` from a lexicon schema (replacing current chain-form behavior with a configurable equivalent) 687 - - Add a "Customize Form" button that opens a simple field editor (reorder fields, change widgets, set labels) 688 - - Store custom edit templates in extension settings (local only) 746 + - 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 747 + - 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) 748 + - 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) 749 + - Auto-generate a default `editTemplate` JSON from any lexicon schema (captures what `generateForm()` would do, as serializable JSON) 750 + - Add a "Customize Form" button in the studio's Create Record view that opens a simple field editor (reorder fields, change widgets, set labels) 751 + - Store custom edit templates in extension settings keyed by NSID 752 + - `chain-form.js` also reads templates via the same path — so cmd palette "new" commands get customized forms too 689 753 690 - **Validates:** Widget system works, field mapping is correct, the customization UX is usable. 754 + **Validates:** Widget registry works, existing forms don't break, customization UX is usable, templates serialize correctly. 691 755 692 756 ### Phase 2: Fiddle UI (Two-Pane Start) 693 757