diff --git a/docs/adr/0084-application-builder-information-architecture.md b/docs/adr/0084-application-builder-information-architecture.md new file mode 100644 index 0000000000..f65b1fc1d6 --- /dev/null +++ b/docs/adr/0084-application-builder-information-architecture.md @@ -0,0 +1,86 @@ +# ADR-0084: Application-builder information architecture — four content pillars + Settings/Advanced + +**Status**: Accepted (2026-07-01) +**Deciders**: ObjectStack Protocol Architects +**Builds on**: [ADR-0077](./0077-authoring-surface-boundary-hook-flow-validation.md) (route authoring by intent + audience + verifiability — the hook/flow/validation boundary), [ADR-0033](./0033-ai-assisted-metadata-authoring.md) (draft-gated authoring), [ADR-0063](./0063-two-kernel-agents-skills-are-the-extension-primitive.md)/[ADR-0064](./0064-tool-scoping-to-agent.md) (two kernel agents — AI is a platform capability, not app metadata), [ADR-0080](./0080-ai-authored-ui-jsx-source.md)/[ADR-0081](./0081-trusted-react-page-tier.md) (the Interface pillar's page-authoring depth). +**Consumers**: `studio.app.ts` (the builder navigation), the Studio UI, `packages/cli/src/utils/format.ts` (`os` stats grouping), and the build agent (which surface authors which type). + +**Premise**: ObjectStack has far more metadata types than Airtable's clean three, but the **application builder** — the surface a person (or the build agent) uses to *build an app* — must not expose all of them at one altitude, or it stops being learnable. Studio's current grouping is inconsistent (mixes Data with Interface, splits "Logic" from "Automation", scatters the rest). This ADR fixes the builder's information architecture, once, after a long design pass that repeatedly corrected itself. The core question it answers: **which metadata types are the app builder's job, at what altitude, and which belong to entirely different surfaces?** + +> **Trigger**: an extended design conversation ("Airtable categorizes as Data / Automation / Interface — I have more types, how do I present them in the builder?"), which converged by successively ruling things in and out. + +--- + +## TL;DR + +1. **The app builder is for *building an application* — nothing else.** Running it (Operate) and the platform's own machinery (Platform) are different surfaces, different personas; they are **out of the builder entirely**, not deferred. +2. **Four visual content pillars** (Airtable's three plus the one it lacks): **Data · Automation · Interface · Access**. This is what a builder navigates. +3. **Each pillar's editor matches the pillar's natural shape**: Data = grid, Automation = canvas, Interface = builder/source, Access = matrix. +4. **Dashboards live in Interface** (Airtable-style: a dashboard is an interface surface of inline chart blocks). A separate **Analytics** pillar is a *future* split, gated on the reusable dataset/cube layer maturing — not a v1 concern. +5. **A Settings area** (distinct from the content pillars) holds **General** (the app's own basic info) and **Advanced** — the *technical tier* for developers. +6. **Advanced is unified by audience, not by "is it code."** It holds both **Code** (hooks now; functions, custom components, custom field types later) and **Connections** (datasources, connectors, webhooks, mappings — external data/systems). What unites them: technical, beyond visual authoring, developer audience. There is **no separate Integration tier** — connections are Advanced. +7. **v1 ships**: the four pillars + Settings(General + Advanced/**Hooks**). Everything else (the rest of Advanced, Analytics, Operate, Platform) is later or out-of-builder. + +--- + +## The information architecture + +``` +Data · Automation · Interface · Access ⚙ Settings +(four visual content pillars — for builders) ├ General the app's basic info + └ Advanced the technical tier — for developers + ├ Code hooks (v1) · functions · custom components · custom field types + └ Connections datasource · connector · webhook · mapping (later) + +Out of the builder entirely (separate surfaces / personas): + Operate ship & run — packages · migrations · flow runs · audit · api keys + Platform the platform's own capabilities — AI (agent/tool/skill) · i18n · email & notification templates · settings · studio plugins +``` + +### The four content pillars (shape → editor) + +| Pillar | Types (v1 core) | Natural shape | Editor | +|---|---|---|---| +| **Data** | object · field · validation | tabular | a live data **grid** (columns = fields, rows = real records) | +| **Automation** | flow · trigger · action · schedule | sequential | a flow **canvas** (trigger → steps) | +| **Interface** | app · page · view · form · dashboard · report | spatial | a **builder** (canvas + palette) or **source + preview** (html/react pages) | +| **Access** | role · permission · sharing | relational | a permission **matrix** (roles × objects × CRUD + record scope) | + +Charts/metrics are **blocks** inside a dashboard/page bound inline to objects — they reuse the SDUI page renderer, so a dashboard is a page with chart blocks, not a separate subsystem. + +### Settings + +- **General** — the app's own identity and defaults: name, id, icon, description, branding, default landing, and the navigation structure. (This is the `app` definition; it is not one of the content pillars — it's *about* the app, not content *in* it.) +- **Advanced** — the technical escape hatch, sub-grouped **Code** and **Connections** (above). Kept visually separate from General so a builder who opens Settings for app info isn't dropped into code (ADR-0077: same area is fine, but different audiences get different groups). + +--- + +## Principles the IA encodes + +- **Two altitudes, one job.** Authoring the app (the four pillars + Advanced) is the builder's job. Operating it and the platform beneath it are *different jobs, different people, different surfaces* — so they are not in the builder. The builder stays about building. +- **Shape → editor.** A pillar's data has a natural shape; its editor is that shape (grid/canvas/builder/matrix). This is what lets four different surfaces feel like one product. +- **Same renderer.** The builder manipulates the same live artifact the end user sees — edit a field on the real grid, style a page in a live preview, set a permission on the real matrix. This is also what makes AI authoring safe: the agent edits the same flat, explicit metadata a human does. +- **Audience routing (ADR-0077).** Visual/declarative → the four pillars (builder audience). Code and connections → Advanced (developer audience). AI → not here at all (platform capability). +- **Primary home + secondary surface for cross-cutting types.** A type is authored in one place and its *result* may appear elsewhere — e.g. a **datasource** is authored in Advanced › Connections, and the external objects it exposes appear in **Data** (with an "external" badge). No type is authored in two menus. +- **Inline-by-default analytics.** A chart carries its query inline (self-contained); a **dataset** is an explicit opt-in only when the query can't be expressed inline (see #2502), and a dashboard filter is a dashboard variable broadcast to inline charts (see #2501). No implicit/auto-generated datasets — hidden linked entities are hostile to both humans and AI authoring. + +--- + +## Consequences + +- The builder is learnable in one glance: four content tabs shaped like their data, plus Settings. Airtable users transfer instantly (Data/Automation/Interface), and the one addition (Access) is obvious. +- The build agent gets a canonical "this type is authored on this surface" map, and a rule that keeps it out of code (Advanced) and off the platform's turf. +- Deferred and out-of-builder concerns have homes that don't distort the builder: Advanced (technical), Operate/Platform (separate surfaces). +- **Cost**: reconciling Studio's current nav (`studio.app.ts`) and the `os` stats grouping to this IA; and drawing the exact edges of "inline vs dataset" (#2502) and the dashboard-filter mechanism (#2501), tracked separately. + +## Alternatives considered + +- **Six flat pillars** (Data/Automation/Interface/Access/AI/Integration). Rejected — conflates authoring with a platform capability (AI) and a technical concern (Integration), and buries the differentiators as peers. +- **Analytics as a top-level pillar in v1.** Rejected — premature; dashboards are Interface surfaces (Airtable-style) until the reusable dataset/cube layer justifies a split. +- **Integration as its own tier/area.** Rejected — connections are technical, developer-audience, beyond visual authoring → they *are* Advanced. +- **AI as a builder pillar/tab.** Rejected — agent/tool/skill are the platform's capability (build/ask), not metadata *in* the app being built. +- **Operate / Platform inside the builder.** Rejected — different personas and moments; they're separate surfaces, not deferred builder tabs. +- **A bare "Advanced" top-level tab.** Rejected — the app also needs a home for its basic info; folding Advanced under **Settings** gives code a non-intimidating, expected home and reserves Settings for the deferred technical concerns. +- **Implicit auto-generated datasets behind inline charts.** Rejected — hidden linked state; two entities per intent; sync burden; violates the flat/explicit/local property that keeps AI authoring safe. + +Related issues: #2501 (dashboard-level filters), #2502 (inline-vs-dataset expressibility rule). diff --git a/docs/design/builder-ui.md b/docs/design/builder-ui.md new file mode 100644 index 0000000000..841a5e5f62 --- /dev/null +++ b/docs/design/builder-ui.md @@ -0,0 +1,272 @@ +# Application builder — UI design + +**Status**: Living design doc (draft, 2026-07-01). Companion to +[ADR-0084](../adr/0084-application-builder-information-architecture.md). The ADR +records the *decisions and rejected alternatives* (the information architecture); +this doc records *how it looks and flows* — it iterates as the UI is built, and is +not a binding contract. + +This doc specifies the **shell and layout** — how the builder composes surfaces. It +does **not** re-specify the per-item config panels: those are Studio's existing +protocol-generated metadata forms, reused as-is (§1.4). What the builder actually +builds is the composition; the panels are dropped in. + +Each surface has an **HTML mockup** under [`builder-ui/`](./builder-ui/) — the precise +visual target and, because its DOM maps to the component tree and its regions are +tagged `data-build` / `data-reuse`, the implementation blueprint (see +[Mockups](#mockups)). The ASCII sketches inline below are for quick reading. + +--- + +## 1. Core constraints — AI-first, human-confirmable, reuse-first + +Every layout and interaction decision in the builder is judged against these. They +are the north star: a design that fails any of them is wrong, however convenient it +looks. Each pillar's design (below) is checked against them. + +1. **AI generates it in one shot.** The builder must let the agent produce a whole, + correct artifact — an object with its fields, validations, and layout — in a + single pass. This works because the agent edits the *same flat, explicit metadata* + a human does (same-renderer, §3.6) and authors within *constrained, declarative* + surfaces (typed fields, enum options, the condition builder — not free-form code). + The *shape of the metadata*, not a click-through wizard, is what the AI targets. + +2. **The design prevents AI mistakes.** Constrain the space so an invalid result is + hard to express: declarative over code (a condition builder, not hand-written CEL, + §4 Validation), typed and enumerated inputs, and the pre-publish validation gates + (`os validate` / `os build`). Nothing the AI writes goes live implicitly — it lands + as a **draft** (ADR-0033), never auto-published. + +3. **A human confirms it in one pass.** A reviewer approves or rejects with one look: + AI changes arrive as a **draft diff** (ADR-0033), and the artifact renders on one + canvas (the grid + inspector) with no modals hiding state (§3.7) and a non-blocking + inspector, so the whole thing stays visible while it is reviewed. + +4. **Reuse, never rebuild.** Every per-item config panel — the field editor, the + validation-rule editor, object settings, and every other metadata form — **is + Studio's existing protocol-generated form**, generated from the metadata-type + schemas, and is dropped into the inspector unchanged. The builder does **not** + hand-roll config UI. This is not just tidiness: reused, already-verified panels are + panels the AI cannot get wrong and a human has seen before, which is what makes + constraints 1–3 achievable in practice. + +### Build boundary — what the builder builds vs. reuses +This is the answer to *"how do we ensure code written against this doc is correct?"* — +shrink the novel surface to almost nothing: + +- **Built here (the shell / composition):** the top bar + pillar tabs, the left rail, + the per-object facet tab bar, the work-surface chrome, and the wiring that routes a + selection into the inspector and edits into draft metadata. +- **Reused (not rebuilt):** the data grid (the existing ListView renderer), and every + config panel in the inspector (Studio protocol-generated forms). These are + referenced by name; the builder composes them, it does not reimplement them. + +In every mockup this boundary is explicit: regions carry `data-build="shell"` or +`data-reuse=""`, and reused blocks are drawn with a dashed outline. + +--- + +## 2. The shell (every pillar shares it) + +``` +┌───────────────────────────────────────────────────────────────────┐ +│ app ▾ Data · Automation · Interface · Access ⚙ Save · Publish │ ← top bar +├─────────┬───────────────────────────────────────────┬──────────────┤ +│ left │ main (the work surface) │ right │ +│ rail │ + a per-entity facet tab bar (see Data) │ inspector │ +│ entities │ grid / canvas / builder / matrix │ = selected │ +│ + search │ ───────────────────────────────────── │ item's │ +│ + new │ [ reused renderer, e.g. ListView ] │ config │ +│ │ │ [reused form] │ +├─────────┴───────────────────────────────────────────┴──────────────┤ +│ legend: solid = built (shell) · dashed = reused (Studio component) │ +└───────────────────────────────────────────────────────────────────┘ +``` + +Mockup: [`builder-ui/data-pillar.html`](./builder-ui/data-pillar.html) (open in a browser). + +- **Top bar** — the four pillar tabs (Data / Automation / Interface / Access), the + ⚙ Settings entry, the app switcher, and the draft indicator + Save draft / + Publish (draft-gated, ADR-0033). +- **Left rail** — the current pillar's primary entities (objects / automations / + surfaces / roles), with search and New. Collapsible. +- **Main** — the pillar's primary work surface, shaped to the pillar (Data = grid, + Automation = canvas, Interface = builder/source, Access = matrix). +- **Right inspector** — a *persistent, consistent* slot that shows the config panel + for whatever is selected in the main zone. Non-blocking (the main surface stays + visible); slides in on select, closable. This is the universal "inspector" + (Figma / Retool pattern). Its contents are always reused protocol forms (§1.4). + +## 3. Design principles (for best operation experience) + +1. **Consistent three zones across all four pillars** — rail / main / inspector. + Muscle memory: config is always on the right, entities always on the left. +2. **Facet = tab, item = inspector.** Tabs switch *which facet* of an entity you're + on (an object's Records / Fields / Validations …); the right inspector configures + the *selected item* within that facet (a field, a rule, a node). They coexist. +3. **The default tab is the primary surface, not a demoted one.** In Data the first + tab is `Records` (the grid) — you always land on the data; tabs make the other + facets one click away without burying the grid. +4. **Small config → inspector; big designer → main.** A field / rule / node / + component configures in the right inspector. A full designer (the field designer, + flow canvas) *is* the main zone. +5. **Two doors, one metadata.** An object-scoped surface that lives in another + pillar (an object's Actions, Hooks, Views, Permissions) is reachable from the + object (in-context, scoped) AND from its pillar (the cross-object lens). The + metadata is stored once; there are two navigational entries, no duplication. +6. **Same renderer.** The builder manipulates the same live artifact the end user + sees (edit a field on the real grid; set a permission on the real matrix). This + is also what keeps AI authoring safe (§1) — the agent edits the same flat, + explicit metadata a human does. +7. **No modals.** Config surfaces in the inspector or as a focused sub-view with a + breadcrumb; the work surface never fully disappears. Draft/publish is always + visible. +8. **Reuse, never rebuild (§1.4).** Config panels are Studio protocol-generated + forms; the builder composes, it does not reimplement. + +--- + +## 4. Data pillar + +Data is the **object-model workbench**: define objects, their fields, relationships, +and validations, and work with records. Data owns the *data layer and the field +designer*; runtime presentation surfaces (saved grid views, kanban, calendar, pages, +dashboards) belong to Interface. + +Mockup: [`builder-ui/data-pillar.html`](./builder-ui/data-pillar.html). + +``` +┌ objects ─┬ Task ┈ Records · Fields · Validations │ Actions · Hooks · ⋯ ┬ inspector ┐ +│ Account │ ┌ filter · sort · hide ─────────────────────────────────┐ │ Edit field │ +│ Task ◄ │ │ # Title Status [Priority ▾] + │ │ Priority │ +│ Project │ │ 1 Audit IA In review ● Medium │ │ type: select│ +│ Invoice │ │ 2 Design system In progress ● High │ │ options … │ +│ + New │ │ + New record │ │ required │ +└──────────┴─┴───────────────────────────────────────────────────────┴─┴────────────┘ +``` + +### Left rail +The app's objects (v1: owned objects only), with search + New object. + +### Object facet tabs +Per selected object, a tab bar of its facets — grouped: + +- **Schema** (authored in Data): `Records` · `Fields` · `Validations` · `Relationships` · `Lifecycle` +- **Behavior**: `Actions` (Automation) · `Hooks` (Advanced) +- **⋯ More**: `Views` · `Forms` (Interface) · `Permissions` (Access) · `Settings` · `Indexes` + +`Records` is the default. Cross-pillar tabs carry a pillar tag and open **in-context, +scoped to this object** (their pillar is the cross-object lens — the two-doors +principle, §3.5). + +Note: `Actions` / `Hooks` / `Validations` are the three authoring surfaces for logic +on an object, routed by intent per [ADR-0077](../adr/0077-authoring-surface-boundary-hook-flow-validation.md): +declarative validation · user-triggered action · write-path hook. + +### Records and Fields — two views of one field designer +Both tabs design the *same* thing (the object's fields); they differ in presentation, +and both configure a selected field through the *same* right-hand **field editor**. + +- **`Records` — grid / list style (data-forward).** The functional grid: columns = + fields, rows = real records. Preview and inline-edit data, `+` add a column + (= add a field), select a column header to configure that field's properties in + the inspector, `+ New record`. Ephemeral filter / sort / hide / group for looking + at the data (not saved — saved views are Interface). +- **`Fields` — form style (layout-forward).** The field designer as a form canvas: + drag to reorder fields, group them into sections, and configure field properties. + No data rows — this is where the object's default field layout (order + grouping) + is authored. (This is the existing form-style field designer, reused here.) + +The choice between them is a working preference: reach for `Records` when you want to +see data while shaping fields, `Fields` when you want to arrange and group them. + +**Why keep the names `Records` / `Fields`** (rather than `Grid` / `Form`): each name +states the tab's *distinguishing* trait — Records is the one that shows **data**, +Fields is the **pure field/layout** designer. They are also the conventional terms +(Salesforce, Airtable). Style-based names would collide with Interface's existing +`Views` (saved grid/kanban) and `Forms` (end-user form surfaces). + +### Main zone (content of the active tab) +| Tab | Main-zone content | +|---|---| +| **Records** | grid-style field designer — preview data, add columns, select a column → configure the field (see above). | +| **Fields** | form-style field designer — drag-reorder, section grouping, field-property config (see above). | +| **Validations** | the **rules list** (declarative). See below. | +| **Relationships** | lookup / master-detail fields + reverse relationships (list / graph). Relationships are created by adding a `lookup` field type. | +| **Actions / Hooks / Views / Forms / Permissions** | the object's scoped instances, opened in-context. | + +### Right inspector (per-item config — reused protocol forms, §1.4) +Selecting an item in the main zone opens its **existing Studio protocol-generated +form** in the inspector, non-blocking. Nothing here is bespoke to the builder. The +field editor is shared by both Records (selected column) and Fields (selected field): +- a column / field → the **field editor** (type, options, required/unique, field-level validation) +- a validation rule → the **rule editor** (condition builder + message + severity + events) +- a record → record detail +- an action → action config + +### Validation (grounded in the schema) +`ObjectSchema.validations` is an array of `ValidationRuleSchema` — a discriminated +union of six declarative rule types (`packages/spec/src/data/validation.zod.ts`): +`script` (CEL predicate → fails when true), `cross_field`, `format`, +`state_machine`, `json`, `conditional`. Each carries `message`, `severity` +(error/warning/info), and `events` (insert/update/delete). + +- **Field-level** (required / unique / format) → configured in the **field editor** + (they belong to the field). +- **Cross-field / business rules** → the **Validations tab** → a declarative rules + list; each rule edits in the inspector via a **condition builder** (field / + operator / value, and/or) with a raw-expression escape hatch — never hand-written + CEL as the primary path. Validation stays **declarative** (not a hook); Data-owned. + The condition builder is also what makes validation AI-generatable and + human-confirmable in one pass (§1). + +### v1 scope +- **Ship**: owned objects · `Records` grid (data + add/configure columns) · `Fields` + form-style designer (reorder + grouping + field editor, incl. field-level + validation) · `Validations` (condition builder) · object `Settings` (label / icon / + name field / compact / search). Relationships via the `lookup` field type. +- **Defer**: Extended objects (objectExtensions) · External objects (datasources) · + the ERD / model view · formal `Lifecycle` (v1 status = a select field) · `Indexes` · + seed-data UI · saved views / kanban / calendar (presentation → Interface). + +--- + +## 5. Other pillars (to design — same shell) + +- **Automation** — left: automations grouped by trigger (record-change / scheduled / + API / manual action); main: the flow **canvas**; inspector: the selected node's + config; a Runs view for execution history. +- **Interface** — left: surfaces (Apps · Pages · Views · Dashboards · Reports); + main: the page **builder** (structured canvas) or **source + preview** (html/react + pages); inspector: component props. +- **Access** — left: roles; main: the permission **matrix** (objects × CRUD + record + scope); inspector: role / field-level detail. +- **Settings** (⚙) — General (app basic info) and Advanced (Code: hooks · functions; + Connections: datasources), grouped by audience. + +--- + +## Mockups + +Mockups live under [`builder-ui/`](./builder-ui/) as **HTML**, not images — HTML is +the better artifact for an AI to build against and it scales to the many surfaces +still to design: + +- **DOM ≈ component tree.** The markup maps almost 1:1 to the React components to + build, so there is little to infer or get wrong. +- **The build boundary is machine-readable.** Every region is tagged + `data-build="shell"` (built in the builder) or `data-reuse=""` (an + existing Studio/objectui component — e.g. `ListView`, `protocol-form:field` — to + drop in unchanged). Reused blocks render with a dashed outline. +- **One shared look.** [`shell.css`](./builder-ui/shell.css) holds the tokens and + layout primitives (top bar, rail, facet tabs, grid, inspector, form rows, toggles); + each pillar mockup composes them, so a new surface is a small file and stays + visually consistent for free. + +Open any `*.html` in a browser to view it. Current mockups: + +| Surface | Mockup | +|---|---| +| Data pillar | [`data-pillar.html`](./builder-ui/data-pillar.html) | + +More are added per pillar as they are designed. diff --git a/docs/design/builder-ui/data-pillar.html b/docs/design/builder-ui/data-pillar.html new file mode 100644 index 0000000000..8c8bc32ec6 --- /dev/null +++ b/docs/design/builder-ui/data-pillar.html @@ -0,0 +1,127 @@ + + + + + Builder — Data pillar + + + + +
+ + +
+ app ▾ + +
+ + Save draft + +
+
+ +
+ + + + + +
+ + + + + +
+ ⚲ Filter + ↕ Sort + ▨ Hide + ▦ Group + + + New record +
+ + +
+
+ + + + + + + + + + + + + + + + +
#TitleStatusPriority ▾+
1Audit IAIn review● Medium
2Design systemIn progress● High
3API surfaceTodo● Low
4Release notesDone● Medium
+
+ New record
+ grid = existing ListView renderer (reused) +
+
+
+ + + +
+ + +
+ solid = shell / composition (built in the builder) + dashed = existing Studio component (reused, not rebuilt) +
+
+ + diff --git a/docs/design/builder-ui/shell.css b/docs/design/builder-ui/shell.css new file mode 100644 index 0000000000..cca7a5fd92 --- /dev/null +++ b/docs/design/builder-ui/shell.css @@ -0,0 +1,99 @@ +/* Application-builder mockup shell — shared design primitives. + One source of truth for the look; every pillar mockup composes these classes. + Build boundary is encoded in markup: data-build="shell" = built in the builder, + .reuse (data-reuse="") = existing Studio/objectui component, dropped in. */ + +:root { + --surface-1: #ffffff; + --surface-2: #f7f8fa; + --border: #e3e6ea; + --line: #eef0f2; + --text: #1f2733; + --muted: #7a828c; + --faint: #9aa0a8; + --accent: #2f6feb; + --accent-bg: #eaf1fe; + --chip: #f0f1f4; + --radius: 6px; + --font: ui-sans-serif, -apple-system, "Segoe UI", Roboto, sans-serif; +} + +* { box-sizing: border-box; } +body { margin: 0; background: #eceef1; padding: 24px; font-family: var(--font); } + +/* ---- shell frame ---- */ +.builder { + width: 1000px; margin: 0 auto; background: var(--surface-1); + border: 1px solid var(--border); border-radius: 8px; overflow: hidden; + color: var(--text); font-size: 13px; +} +.topbar { + height: 44px; display: flex; align-items: center; gap: 24px; + padding: 0 16px; border-bottom: 1px solid var(--border); +} +.topbar .app { font-weight: 600; } +.topbar .pilltabs { display: flex; gap: 26px; margin: 0 auto; } +.topbar .pilltabs a { color: var(--muted); text-decoration: none; padding-bottom: 6px; } +.topbar .pilltabs a.active { color: var(--text); font-weight: 600; border-bottom: 2px solid var(--accent); } +.topbar .right { display: flex; align-items: center; gap: 14px; color: var(--muted); } +.btn-publish { background: var(--accent); color: #fff; border: 0; border-radius: 5px; padding: 5px 12px; font-weight: 600; font-size: 12px; } + +.body { display: flex; height: 560px; } + +/* ---- left rail ---- */ +.rail { width: 190px; background: var(--surface-2); border-right: 1px solid var(--border); padding: 14px 12px; } +.rail .label { font-size: 10.5px; letter-spacing: .6px; color: var(--muted); font-weight: 600; margin-bottom: 8px; } +.rail .search { width: 100%; height: 26px; border: 1px solid var(--border); border-radius: 5px; background: #fff; color: var(--muted); font-size: 11.5px; padding: 0 10px; line-height: 26px; } +.rail ul { list-style: none; margin: 12px 0 0; padding: 0; } +.rail li { padding: 6px 8px; border-radius: 5px; margin-bottom: 2px; } +.rail li.active { background: var(--accent-bg); font-weight: 600; box-shadow: inset 3px 0 0 var(--accent); } +.rail .new { color: var(--muted); padding: 6px 8px; margin-top: 6px; } + +/* ---- main zone ---- */ +.main { flex: 1; display: flex; flex-direction: column; min-width: 0; } +.facettabs { height: 36px; display: flex; align-items: center; gap: 18px; padding: 0 16px; border-bottom: 1px solid var(--border); } +.facettabs a { color: var(--muted); text-decoration: none; padding-bottom: 8px; margin-top: 8px; display: inline-flex; align-items: center; gap: 5px; } +.facettabs a.active { color: var(--text); font-weight: 600; border-bottom: 2px solid var(--accent); } +.facettabs .sep { width: 1px; height: 18px; background: var(--border); } +.facettabs .tag { font-size: 8.5px; color: var(--muted); background: var(--chip); border-radius: 7px; padding: 1px 6px; } +.toolbar { display: flex; align-items: center; gap: 18px; padding: 8px 16px; color: var(--muted); font-size: 11.5px; } +.toolbar .spacer { flex: 1; } +.toolbar .add { color: var(--accent); font-weight: 600; } +.workarea { flex: 1; padding: 0 16px 16px; overflow: auto; } + +/* ---- reused-component marker ---- */ +.reuse { position: relative; border: 1px dashed #c8ccd2; border-radius: var(--radius); } +.reuse > .reuse-tag { position: absolute; left: 10px; bottom: 6px; font-size: 9.5px; font-style: italic; color: var(--faint); background: var(--surface-1); padding: 0 4px; white-space: nowrap; } + +/* ---- data grid (reused ListView) ---- */ +table.grid { width: 100%; border-collapse: collapse; font-size: 11.5px; } +table.grid th { text-align: left; background: var(--surface-2); color: var(--muted); font-weight: 600; padding: 8px 12px; border-bottom: 1px solid var(--border); } +table.grid td { padding: 8px 12px; border-bottom: 1px solid var(--line); } +table.grid td.muted, table.grid th.muted { color: var(--muted); } +table.grid .col-selected { background: color-mix(in srgb, var(--accent-bg) 60%, transparent); } +table.grid th.col-selected { color: var(--accent); } +.grid-add { color: var(--muted); padding: 8px 12px 26px; } + +/* ---- right inspector (reused protocol form) ---- */ +.inspector { width: 220px; background: var(--surface-2); border-left: 1px solid var(--border); padding: 14px 16px; } +.inspector .eyebrow { font-size: 10.5px; letter-spacing: .4px; color: var(--muted); } +.inspector h3 { margin: 4px 0 10px; font-size: 14px; } +.inspector hr { border: 0; border-top: 1px solid var(--border); margin: 0 0 12px; } +.form { padding: 12px 12px 30px; } +.form .flabel { font-size: 10.5px; color: var(--muted); margin: 12px 0 5px; } +.form .flabel:first-child { margin-top: 0; } +.form .field { height: 26px; border: 1px solid var(--border); border-radius: 5px; background: #fff; font-size: 11.5px; padding: 0 10px; line-height: 26px; } +.form .chips { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; } +.chip { background: var(--chip); border-radius: 11px; padding: 3px 10px; font-size: 10.5px; } +.form .add { color: var(--accent); font-size: 11px; } +.form .toggle-row { display: flex; justify-content: space-between; align-items: center; margin-top: 12px; } +.toggle { width: 36px; height: 18px; border-radius: 9px; background: #d7dbe0; position: relative; } +.toggle::after { content: ""; position: absolute; top: 2px; left: 2px; width: 14px; height: 14px; border-radius: 50%; background: #fff; } +.toggle.on { background: var(--accent); } +.toggle.on::after { left: 20px; } + +/* ---- legend ---- */ +.legend { display: flex; gap: 26px; padding: 8px 16px; border-top: 1px solid var(--border); font-size: 10.5px; color: var(--muted); } +.legend .sw { display: inline-block; width: 14px; height: 12px; vertical-align: -1px; margin-right: 6px; } +.legend .sw.built { border: 1px solid var(--text); } +.legend .sw.reused { border: 1px dashed #c8ccd2; }