Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.yourhq.ai/llms.txt

Use this file to discover all available pages before exploring further.

HQ optimizes for fast, low-ceremony record manipulation. The default is to edit in place; the second-best is a SidePanel alongside the list; modal dialogs are reserved for focused decisions, and wizards for genuine multi-step flows. This page is the rule set.

Choosing a creation surface

Three options, picked by user mode — not by field count:
SurfaceWhenReference
SidePanelSustained record editing. The user is browsing a list and adding or editing one of its rows. The panel staying alongside the list reinforces “you’re still in the list, just editing one thing in it.” Contacts, organizations, knowledge items, collection records.shared/side-panel.tsx, crm/contact-form.tsx
DialogQuick capture and focused decisions. The user is mid-flow — possibly on an unrelated screen — and wants to dump intent into the system and get back to what they were doing. Tasks (regardless of field count: a task is tactical capture, not record management), confirmations, two-step OAuth handoffs.ui/dialog.tsx, tasks/task-form.tsx
WizardMulti-step provisioning where steps depend on each other or async work happens between steps.agents/agent-create-wizard.tsx, onboarding/wizard/onboarding-wizard.tsx
The deciding question isn’t “how many fields does this have?” — it’s “is the user managing records or capturing intent?” A task form with twelve fields is still capture; the user thought of something and wants it in the system fast. A contact form with three fields is still record editing; the user is reviewing the contact list.

SidePanel shape

Width presets: md (480px), lg (560px, the default), xl (720px). Sticky footer carries the primary “Save” / “Create” button on the right and a ghost-variant “Cancel” on its left. Body uses the PropertyRow layout — 28px label column, flex-1 control. Inline Input, Select, TagInput, DynamicField, and Textarea primitives for the controls.

Dialog shape

Square modal overlay (bg-black/40 light, bg-black/60 dark). Title at .text-title, optional description at .text-body muted. The form body should fit without scrolling. Two buttons in the footer.

Wizard shape

Multi-step modal. Step indicator in the header (e.g. “2 of 5”). Steps that perform async work display a Spinner with a status line — the user sees the system thinking. Back/Next buttons; Back is disabled or hidden on irreversible steps (after a network call has succeeded). Status polling drives forward progress automatically when ready.

Inline editing

Editing in place is the default for any property already visible. Users should not have to open a form to change one value.

PropertyRow layout

The standard “label : value” row, used in detail right rails and inline-edit forms. 28px label column on the left (.text-label style), flex-1 control on the right. Borders and padding fade until hover, where the control reveals an accent-colored fill cue. Three small primitives composed inside detail views:
  • InlineText — single-line text. Click to edit, Enter or blur saves, Escape cancels. Optimistic UI. Used for names, emails, phone numbers, anything single-line.
  • InlineTags — multi-value chip input. Comma or Enter commits a tag. Click an X on a chip to remove. Used for tags, multi-select properties.
  • InlineLink — URL with an external-link icon. Clicking the icon opens the link; clicking the text enters edit mode.
References: see the implementations inside crm/contact-detail-view.tsx. When a new module needs the same primitives, lift them into components/shared/ — they are about to become composites.

Type-dispatched cell editing

apps/ui/src/components/collections/collection-cell.tsx is the canonical pattern for inline edit across many field types. One entry point dispatches by field_type:
TypeEditor
text, email, phoneInline text input
numberInline number input with parse / format
date, datetimeDatePickerButton popover
booleanCheckbox
selectSelect
multi_selectCombobox (multi)
urlInline link with external-link icon
rich_textTextarea (or Novel where rendered in detail)
relationEntityLinkPicker
When a module gains a new typed field, extend collection-cell.tsx rather than building a parallel cell renderer.

Rich text vs plaintext

Two surfaces; pick by what the user is writing. Novel (TipTap wrapper) is for documents — surfaces where structure adds value. Headings, lists, task lists, code blocks, embeds, links. Long-form. The user is composing something they’ll come back to and read. apps/ui/src/components/knowledge/novel-editor.tsx The Novel setup at HQ:
  • Extensions — StarterKit, TaskList, TaskItem, HorizontalRule, Link, Image, Underline, CodeBlockLowlight.
  • Slash commands — / opens the command palette inside the editor.
  • Bubble menu — surfaces formatting (bold, italic, underline, code, link) on selection.
  • Syntax highlighting — lowlight with the common language set.
  • Auto-save — on blur. The hosting component owns the persistence call.
Editor styles live in globals.css under .ProseMirror (lines ~265-450). The editor uses a slightly larger type scale than the app body — H1 is 30px, body line-height is 1.7 — because long-form reading benefits from more vertical rhythm. Knowledge pages and skills are the canonical Novel surfaces in HQ today. Textarea is for short-form notes — surfaces where the user is dumping a sentence or a paragraph and getting on with their day. Task descriptions, contact notes, comment bodies, agent prompts, slug fields. The user is not composing a document; they are jotting. A surface should stay on textarea unless three things are true:
  1. The content is genuinely document-shaped — multiple paragraphs, lists, sections.
  2. Downstream consumers (search indexing, agent context, list previews) can handle structured content cleanly, or have been updated to.
  3. The user mode is composition, not capture — they’re not entering this from a quick-create dialog.
If any of those three is no, keep the surface on textarea. Adding rich-text affordances to a quick-capture field adds friction to the dominant flow without earning anything.

Forms

When you do need a form (validation, multi-field saves, server actions), use React Hook Form + Zod through the Form primitives. apps/ui/src/components/ui/form.tsx The stack:
  • Form — wraps FormProvider. Pass the RHF useForm return.
  • FormFieldController wrapper for a single field. Pass control and name.
  • FormItem — auto-IDs the field and binds label/control/description/message.
  • FormLabel — uses Radix Label; auto-toggles data-error="true" on invalid.
  • FormControl — slot wrapper; wires aria-describedby and aria-invalid.
  • FormDescription — helper text below the input.
  • FormMessage — auto-extracts the field’s error message from RHF state.
Error styling is automatic: FormLabel turns destructive on data-error, FormControl border and ring shift to destructive on aria-invalid. Don’t hand-roll error styles per form. Schema lives next to the component (const schema = z.object({ ... })). Keep it simple — Zod is the validation engine, not a domain modeling tool. Database types are imported from lib/<module>/types.ts.

Custom fields

For any record with user-defined fields (contacts, organizations, collections, tasks), use DynamicField and DynamicFieldGroup from shared/. They take a FieldDefinition and render the right editor. Don’t branch on field type inside feature components.

Save semantics

  • Inline edits save on blur, optimistic UI, toast on error. No explicit Save button.
  • SidePanel forms save on the explicit Save button. Disable the button during the async call; show a spinner inside it.
  • Wizards save per-step. The Next button is the save action.
  • Cell edits in tables save on blur or Enter. Escape reverts.
Audit logging happens server-side (the audit_log table). Feature code does not need to log inline edits — the database trigger does.

Cancel and dismiss

  • Inline edits: Escape reverts and closes the editor.
  • SidePanel / Dialog: Escape closes; clicking outside closes; explicit Cancel button closes. If there are unsaved changes, ask once via ConfirmDialog (warning tone).
  • Wizards: dismissal mid-flow is destructive on irreversible steps — disable outside-click close and prompt the user via ConfirmDialog (destructive tone) for explicit X-button closes.

When to add to shared/

If a form pattern is reused in two modules, lift it. Three places to look first when adding:
  • A new typed field editor → extend collection-cell.tsx and DynamicField.
  • A new inline-edit primitive (e.g. inline date) → add to components/shared/ next to inline-edit.tsx.
  • A new wizard pattern → start inside the feature module; promote to shared/ only on the second use.