Repository
Where Canon lives in the monorepo, how a component file is built, and the conventions every file follows.
The monorepo#
One Turborepo workspace on Bun. Apps consume packages; nothing in an app is imported by a package.
oration-gtm/ apps/ web/ Next.js app, routes and product components handbook/ Canon docs, the /design route packages/ ui/ Canon: components, hooks, lib, globals.css typescript-config/ shared tsconfig bases DESIGN.md the frozen visual spec PRODUCT.md users, purpose and product principles biome.json formatting and lint rules turbo.json dev, build and check-types pipelines package.json workspaces and root scripts| Package | Path | Role |
|---|---|---|
| agent-service | apps/agent-service | Not part of Canon. |
| chat-service | apps/chat-service | Not part of Canon. |
| customer-apps | apps/customer-apps | Not part of Canon. |
| docs | apps/docs | Not part of Canon. |
| favourite | apps/favourite | Not part of Canon. |
| handbook | apps/handbook | Not part of Canon. |
| integrations | apps/integrations | Not part of Canon. |
| job-manager | apps/job-manager | Not part of Canon. |
| notabswitching | apps/notabswitching | Not part of Canon. |
| omnichannel-service | apps/omnichannel-service | Not part of Canon. |
| proxy | apps/proxy | Not part of Canon. |
| sms-gateway | apps/sms-gateway | Not part of Canon. |
| truemeds-dialog-insights-ui | apps/truemeds-analytics | Not part of Canon. |
| voice-microservice | apps/voice-microservice | Not part of Canon. |
| web | apps/web | Not part of Canon. |
| webapp | apps/webapp | Not part of Canon. |
| whatsapp-bot-app | apps/whatsapp-bot-app | Not part of Canon. |
| workflows | apps/workflows | Not part of Canon. |
| @oration/actions | packages/actions | Not part of Canon. |
| @oration/analytics | packages/analytics | Not part of Canon. |
| @oration/api | packages/api | Not part of Canon. |
| @oration/api-client | packages/api-client | Not part of Canon. |
| @oration/app-config | packages/app-config | Not part of Canon. |
| @oration/audit-logs | packages/audit-logs | Not part of Canon. |
| @oration/auth | packages/auth | Not part of Canon. |
| @oration/bmp | packages/bmp | Not part of Canon. |
| @oration/canon | packages/canon | Canon itself: every component, hook, library and the global stylesheet. |
| @oration/channels | packages/channels | Not part of Canon. |
| @oration/common | packages/common | Not part of Canon. |
| @oration/conversation-analytics | packages/conversation-analytics | Not part of Canon. |
| @oration/core | packages/core | Not part of Canon. |
| @oration/crawler | packages/crawler | Not part of Canon. |
| @oration/db | packages/db | Not part of Canon. |
| @oration/dialog-insights | packages/dialog-insights | Not part of Canon. |
| @oration/emails | packages/emails | Not part of Canon. |
| @oration/evaluation | packages/evaluation | Not part of Canon. |
| @oration/feature-flags | packages/feature-flags | Not part of Canon. |
| @oration/flows | packages/flows | Not part of Canon. |
| @oration/integrations | packages/integrations | Not part of Canon. |
| @oration/json-schema-zod-utils | packages/json-schema-zod-utils | Not part of Canon. |
| @oration/locales | packages/locales | Not part of Canon. |
| @oration/logger | packages/logger | Not part of Canon. |
| @oration/o11y | packages/o11y | Not part of Canon. |
| @oration/regulatory-compliance | packages/regulatory-compliance | Not part of Canon. |
| @oration/rivet | packages/rivet | Not part of Canon. |
| @oration/routing | packages/routing | Not part of Canon. |
| @oration/spacetimedb | packages/spacetimedb | Not part of Canon. |
| @oration/types | packages/types | Not part of Canon. |
| @oration/ui | packages/ui | Not part of Canon. |
| @oration/ui-native | packages/ui-native | Not part of Canon. |
| @oration/utils | packages/utils | Not part of Canon. |
| @oration/vector-db | packages/vector-db | Not part of Canon. |
| @oration/webhooks | packages/webhooks | Not part of Canon. |
Inside packages/canon#
The package's exports map turns each folder into an import path. Counts are read from the file system on every build.
| Folder | Files | Holds | Import as |
|---|---|---|---|
| components | 138 | Atoms, molecules and organisms, one component family per file. | @oration/canon/components/<file> |
| components/ai | 21 | AI primitives: thread, message, prompt bar, tool chip, approval card, streaming text. | @oration/canon/components/ai/<file> |
| components/voice | 5 | Voice orb, waveform, latency timeline, live transcript. | @oration/canon/components/voice/<file> |
| components/prompt-editor | 16 | The Tiptap prompt editor, its variable chips, slash menu and AI proposals. | @oration/canon/components/prompt-editor/prompt-editor |
| components/illustrations | 5 | The ink line drawings behind Illustration, grouped by theme. Import the wrapper, not these. | @oration/canon/components/illustration |
| hooks | 7 | Behavior without markup: mock queries, dirty forms, fluid hover, overflow fades. | @oration/canon/hooks/<file> |
| lib | 7 | Springs, cn, demo controls, the size context, font weights. | @oration/canon/lib/<file> |
| styles/globals.css | 1 | Every token, custom utility and motion rule, 1061 lines. | @oration/canon/globals.css |
Canon documents 165 components. Some files export a whole family (dialog.tsx exports the root, trigger, content, header and footer), and a few internal helpers, such as choice-cards.tsx and the prompt editor's parts, have no page of their own.
Anatomy of a component file#
Every Canon file follows the same shape. Badge is the smallest complete example.
- Client directive.
"use client"only when the file holds state, effects or event handlers. Button has it; Badge doesn't. - Base UI primitive. Interactive parts wrap a primitive from
@base-ui/react/<part>, which brings roles, focus management and keyboard behavior. - cva variants.
cva(base, { variants, defaultVariants })holds the classes, and the variants object is exported so other files can borrow the look. - data-slot. Each rendered part names itself, such as
data-slot="badge". Parents style children through it (in-data-[slot=button-group]) and globals.css keys motion to it. - render prop.
useRenderwithmergePropslets the component render as another element, such as a NextLink, keeping its classes, props and state. - className last.
cn(variants({ variant }), className)merges the caller's classes after the component's, so an override always wins. - Named exports at the bottom. One
export { Badge, badgeVariants }block closes the file. No default exports.
badge.tsx in full#
Read from packages/canon/src/components/badge.tsx when this page builds.
import { mergeProps } from "@base-ui/react/merge-props";import { useRender } from "@base-ui/react/use-render";import { cva, type VariantProps } from "class-variance-authority";import { cn } from "cn";const badgeVariants = cva( "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,background-color,border-color,box-shadow] duration-150 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!", { variants: { variant: { default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80", secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80", destructive: "bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20", outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground", ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50", link: "text-primary underline-offset-4 hover:underline", }, }, defaultVariants: { variant: "default", }, },);function Badge({ className, variant = "default", render, ...props}: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) { return useRender({ defaultTagName: "span", props: mergeProps<"span">( { className: cn(badgeVariants({ variant }), className), }, props, ), render, state: { slot: "badge", variant, }, });}export { Badge, badgeVariants };Conventions#
The small agreements that let files compose without reading each other.
| Convention | Rule |
|---|---|
data-slot | Every rendered part sets one, in kebab case, prefixed by its component (dialog-content, settings-row). 548 distinct names exist today. globals.css keys open and close motion to 18 of them: dropdown-menu-content, dropdown-menu-sub-content, context-menu-content, context-menu-sub-content, menubar-content, popover-content, select-content, combobox-content, hover-card-content, tooltip-content, dialog-content, alert-dialog-content, dialog-overlay, alert-dialog-overlay, sheet-overlay, sheet-content, collapsible-content, illustration. |
data-icon | Mark an icon inside a button or badge data-icon="inline-start" or "inline-end". The padding on that side tightens by 2px so the label stays optically centered. |
| Sizes | xs 24px, sm 28px, default 32px and lg 36px, with square icon-xs, icon-sm, icon and icon-lg at the same heights. |
type="button" | A native button that doesn't submit a form says so. Base UI's Button sets it; hand-written <button> elements must. |
| Icons | From lucide-react, aria-hidden="true" beside text, 16px unless a size class is set. globals.css sets every svg.lucide to a 1.75 stroke. |
| Exports | Named exports in one block at the end of the file. No file in packages/canon has a default export. |
| Imports between Canon files | Use the public path, @oration/canon/components/<file>, even inside packages/canon. The save bar imports Button that way. |
| Timing | Motion comes from spring and exit in @oration/canon/lib/springs. No hand-written durations. |
Two cn functions
Most packages/canon files import cn from the bare cn package, which doesn't know text-13 or text-2xs are font sizes. App code should import cn from @oration/canon/lib/utils, which does.
The docs site#
Canon's pages live in the Handbook app, so every example renders real packages/canon code.
components/design/ registry/ pages.ts (every page), rules.ts (named rules, refusals), loaders.ts (generated), digest.ts (llms.txt), types.ts kit/ authoring primitives: sections, tables, examples, code, usage site/ sidebar, header, search, table of contents, page frame content/ one folder per page: doc.tsx, demos.tsx, guidance.tsapp/design/ page.tsx the overview [...slug]/page.tsx every other page, driven by the registry llms.txt/route.ts the index and brief for agents llms-full.txt/route.ts every component's guidance and props| What | Count |
|---|---|
| Pages in the registry | 212 |
Page bodies (doc.tsx) | 212 |
Component guidance files (guidance.ts) | 165 |
Demo files (demos.tsx) | 206 |
Add a page by adding an entry to registry/pages.ts, then run bun run design:sync in apps/handbook. It regenerates loaders.ts and writes a stub doc.tsx so a new entry never breaks the build. The authoring guide is components/design/README.md.
Tooling#
Commands read from the root, apps/web and apps/handbook package.json files.
| Command | Runs | Use it to |
|---|---|---|
bun run dev | turbo run dev | Start the web app (next dev --port 3000). |
bunx biome check --write <paths> | Biome on the given files | Format and lint, applying safe fixes. Pre-commit runs the same on staged files. |
bun run lint | turbo run lint | Check the whole repo without writing. |
bun run check-types | Not defined | Type-check every package. In apps/web this is bun run typecheck. |
bun run ui-add <name> | Not defined | Pull a shadcn component. The ui alias in apps/web/components.json points at @oration/canon/components, so it lands in packages/canon. |
bun run design:sync | bun scripts/design-sync.ts | Regenerate Canon's loaders and stubs after editing the registry. Run it in apps/handbook. |
- A shadcn component arrives in shadcn's style. Before anyone uses it, bring it onto Canon tokens, add
data-slotnames and write its page. - Never edit
registry/loaders.tsby hand;design:syncowns it.