跳到正文
cccskills
免费在 GitHub 公开

kortix-design-system

Build and update Kortix web screens with established interface components and reference layouts. Guides component selection for forms, lists, modals, loading states, and notifications, including replacements for deprecated components and consistent responsive page structure.

中文简介正在准备中。当前显示英语简介。

原文查看原文说明

Use when building or editing any visual surface in apps/web — a page, screen, list, card, badge, avatar, modal, form, empty state, toast, tooltip, table, or panel — and you need to know WHICH component to compose and which primitives are banned. Load kortix-brand first for the values (color, spacing, type, radius, motion budget); this file assumes those and covers components, layout shells, and reference implementations. Source of truth: packages/ui/src/primitives, the live /design-system page, and the reference implementations listed inside.

查看安装方法

适用场景

  • Build a settings screen with existing components
  • Replace deprecated interface components
  • Standardize loading, empty, and error states
  • Adjust page layouts for narrow screens

包含的文件(1)

  • SKILL.md44.5 KB

SKILL.md(原文)

安装前可以查看交给 AI 代理的指令内容。

Kortix Design System

Track this file: .agents/skills/kortix-design-system/SKILL.md

If you are touching a visual surface in apps/web, follow this. This file owns which component to compose. It does not own values — see below.

Read kortix-brand first. It is the router for the Kortix look and voice. Its visual files (color.md, typography.md, layout.md, effects.md, motion.md) are the value law: the closed allowlist for every color, spacing step, type rung, radius, elevation, easing and duration, plus the rule on whether to animate at all. Its decisions.md records why each rule is what it is. This file assumes you know those values and shows you what to build with them.

Companion skills — load all three

SkillOwnsWins when they conflict
kortix-brandThe value allowlists, voice and claims. Color, spacing, type, radius, elevation, motion budget. Rejected defaults. scripts/audit.sh. Decision history.Values, copy and motion restraint
kortix-design-system (this file)Components, layout shells, reference implementations, banned primitivesWhich component
make-interfaces-feel-betterPolish: concentric radius, optical alignment, tabular nums, hit areas, font smoothingPolish detail

A polish skill will always propose more motion than Kortix wants. kortix-brand is the ceiling all three operate under. Corrections to earlier versions of this file (the shadow ladder that never existed, the press value) are recorded in decisions.md, entries E1 and E2.

Where components live: @kortix/ui

  • One component source. Every primitive is in packages/ui (@kortix/ui): packages/ui/src/primitives/<name>.tsx, helpers in packages/ui/src/lib/, hooks in packages/ui/src/hooks/, the session thread in packages/ui/src/session/ (@kortix/ui/session). apps/web, Kortix Apps, the white-label demo and SDK hosts render the same files (decision K9 in decisions.md).
  • Import the package path. apps/web imports @kortix/ui/<name> for a primitive and @kortix/ui/<entry> (session, files, workspace, …) for a module. apps/web has no re-export shims: tests/unit/web-no-ui-shims.test.ts fails on a file that only re-exports @kortix/ui. Edit the component in packages/ui.
  • Mock the file the import resolves to. bun mocks by resolved file: mock.module('@kortix/ui/<name>') reaches the primitive for apps/web and for the package files that import it relatively. A barrel import (@kortix/ui, @kortix/ui/session) loads every module of the barrel, so a partial mock of one of them must keep the module's other exports: const real = await import('@kortix/ui/button'); mock.module('@kortix/ui/button', () => ({ ...real, Button: Stub })).
  • A new primitive goes in packages/ui/src/primitives/ with relative imports only (never @/, never next/*, never an app store), then pnpm --filter @kortix/ui gen-exports. apps/web imports it as @kortix/ui/<name>. Copy for the user goes through useUiStrings(); a link goes through useUiLink(). npx shadcn add <x> in apps/web writes into apps/web/src/components/ui/ (apps/web/components.json): move the file to packages/ui/src/primitives/, rewrite its imports to relative paths, and import it from @kortix/ui/<name>.
  • Outside apps/web (a Kortix App, an SDK host): follow the package skill, packages/ui/skills/kortix-ui/SKILL.md. A dashboard or App over Kortix sessions: references/custom-dashboard.md. kit.css (k-* classes) is only for plain HTML without React.

Philosophy

  • Simplicity is the brand. Black & white + one accent. Calm, spacious, legible. No decoration that doesn't carry information. Show only important data.
  • Reuse > Compose > Create. In that order. Never hand-roll something the system already provides.
  • Tokens are law. apps/web/src/app/globals.css (the :root / .dark tokens) and packages/ui/src/styles/theme.css (the @theme inline keys, @utility definitions, keyframes and spinner classes that @kortix/ui components use) are the implementation source of truth for every visual property. If a value conflicts with anything else, these two files win.
  • Fast, or none. Kortix is a speed product. Motion exists to make the UI feel faster and to say where a thing came from — never to be admired. The more you animate, the less each animation is worth. The default answer to "should this animate?" is no.
  • AI-native & self-documenting. The living styleguide at /design-system renders every component. When you add a component, add it there too.

Strictly avoid — deprecated primitives

Do not use these in new work or when refactoring screens. They are legacy wrappers; match the hand-composed patterns in the customize section views instead.

BannedUse instead
SectionCard (packages/ui/src/primitives/section-card.tsx)Card (card.tsx), Label + bg-popover rounded-md border panel, or Disclosure — see project-settings-page.tsx
List / ListRow (packages/ui/src/primitives/list.tsx)<ul className="space-y-2"> + entity row classes — see access-row.tsx, review-detail.tsx
Dialog / DialogContent in feature codeModal from packages/ui/src/primitives/modal.tsx — see secrets-view.tsx, channels-view.tsx
Tooltip / TooltipTrigger / TooltipContent in feature codeHint from packages/ui/src/primitives/hint.tsx
@/lib/toast (deleted; never recreate it), raw sonner, toast.success/error/info/warning(), toast.custom()Named helpers from packages/ui/src/primitives/toast.tsx. An action button is the button option (a Button), not sonner's action. tests/unit/toast-single-source.test.ts fails on any other sonner import
Hand-rolled badge <span> chipsBadge from packages/ui/src/primitives/badge.tsx
A count beside a label (row, tab, menu item, button) as muted text or a BadgeCountBadge from packages/ui/src/primitives/count-badge.tsx: the sidebar Review-count recipe (tabular pill, row text colour, 15% tint, 99+). Tone by meaning: waiting, reminder, conflict, neutral. Mobile: CountBadge in apps/mobile/components/kortix/count-badge.tsx
Any icon as a spinner — CircleNotchIcon, SpinnerIcon, SpinnerGapIcon, or the same glyph aliased to Loader/Loader2/IconLoaderLoading from packages/ui/src/primitives/loading.tsx — the spinner. The one other busy mark is SessionDotMatrix, for session-scoped work only (D4f in decisions.md)
Hand-rolled <svg> spinners, animate-spin on non-Loading elementsLoading — animation is built in

When editing a file that already uses banned primitives, migrate to the reference pattern — do not add more usage.

Required primitives — use these, not alternatives

These are mandatory for their job. Import from the paths below; never reimplement or swap in a different library.

JobImport fromNotes
Tooltips on icon buttonspackages/ui/src/primitives/hint.tsx<Hint label="…">…</Hint> — wraps trigger, never Tooltip in features
Dialogs / sheetspackages/ui/src/primitives/modal.tsxModal, ModalContent, ModalHeader, ModalTitle, ModalDescription, ModalBody, ModalFooter
Toastspackages/ui/src/primitives/toast.tsxThe only toast surface. successToast, errorToast, infoToast, warningToast, progressToast, loadingToast, dismissToast
Status chipspackages/ui/src/primitives/badge.tsxsize="sm" or size="xs"; variants outline, kortix, success, destructive, beta, etc.
Expand/collapse panelspackages/ui/src/primitives/disclosure.tsxDisclosure, DisclosureTrigger, DisclosureContent — config lists, settings groups
Inline alertspackages/ui/src/primitives/info-banner.tsxtone + optional icon + title
Search fieldspackages/ui/src/primitives/input-group.tsxInputGroupSearch + InputGroupSearchInput. Every input is the one field surface: bg-popover, border, rounded-md. variant="popover" is deprecated and ignored (D4j)
Forms in panelspackages/ui/src/primitives/field.tsxField, FieldLabel, FieldGroup, FieldDescription
Empty / error statespackages/ui/src/primitives/empty-state.tsx, error-state.tsxsize="sm" in customize sections. An empty state is one muted line and an optional hint, with no icon tile and no card (D4g)
Confirm destructivepackages/ui/src/primitives/confirm-dialog.tsxMandatory before any destructive mutation — including DropdownMenuItem variant="destructive" items (see secrets-view.tsx delete, sections/view/gateway/gateway-keys.tsx revoke). Only accepted alternative: the inline Cancel/confirm button swap used for channel disconnects (channels-view.tsx). Never mutate from a single click
Loading / pending spinnerspackages/ui/src/primitives/loading.tsximport Loading from '@kortix/ui/loading' — default size-4; use className="size-4 shrink-0" in dense buttons. Never CircleNotchIcon, SpinnerIcon, or other icons

Also reach for: Button, ButtonGroup, Input, Select, Switch, Skeleton, Tabs / TabsListCompact, Table, InlineMeta, UserAvatar, EntityAvatar.

Which component for which state

Paths are relative to apps/web/src/. The values and the copy are in kortix-brand (layout.md sections Shells and States, voice-and-tone.md section 4).

SituationComposeNever
The route has not mounted its shell (sign-in, project switch)ProjectPendingScreen (components/projects/project-pending-screen.tsx)A skeleton of a page you cannot draw
A list loads inside a mounted shell and the row shape is knownSkeleton rowsA spinner for a list
A page or list failed to loadErrorState size="sm" (packages/ui/src/primitives/error-state.tsx) inside the host panel, with a "Try again" actionInfoBanner for a load failure, or a wrapper box around it
An inline alert in a view that did load (a connection, a warning)InfoBanner with a toneA hand-colored banner
A list has no itemsEmptyState size="sm" unmodified: one muted line and an optional hint. A first-run list adds PixelKortixMark (packages/ui/src/primitives/pixel-kortix-mark.tsx) as a sibling above itAn icon tile, a card, a className override of its slots, a new prop
People and agents with a roleAccessRow (packages/ui/src/access/access/access-row.tsx), grouped "People" then "Agents" under a LabelA hand-built li, a kind badge, a [&>div]: selector
Any form field, in a panel or in a ModalField, FieldLabel, FieldGroup (packages/ui/src/primitives/field.tsx)A raw <label> beside an Input
A mutation finishedsuccessToast or errorToast from packages/ui/src/primitives/toast.tsxAn inline "Saved" line
A new capability pageCapabilityPageShellCustomizeSectionWrapper (legacy, customize/sections/view/ only)

Reference implementations — customize section views

Read the closest match before building any new screen. Paths below are relative to apps/web/src/features/workspace/ unless given in full.

FilePattern to copy
customize/sections/component/section-wrapper.tsxSection shell: title left, action right, max-w-2xl, responsive header
capabilities/agents/agents-page.tsxConfig entity list: search → rows → detail aside with Badge, ButtonGroup + Hint, toasts
capabilities/skills/skills-page.tsxCapability page in CapabilityPageShell; EmptyState with a docs link
capabilities/project-settings/project-settings-page.tsxForm sections: Label header → bg-popover rounded-md border px-4 py-5 panel; Disclosure for experimental; danger zone as neutral bordered row
customize/sections/view/secrets-view.tsxTable + TabsListCompact filters + Modal forms + DropdownMenu row actions
shared/access/access-row.tsxEntity rows, UserAvatar, InlineMeta — the single row used by every access surface
packages/ui/src/workspace/review/review-detail.tsxTinted size-9 icon tile (bg-kortix-green/15 and bg-kortix-orange/15), Badge size="sm" verification chips
packages/ui/src/workspace/sessions/project-sessions-view.tsxTabsListCompact filter tabs
customize/sections/view/channels-view.tsxTable for integrations, Modal for connect flows, InfoBanner for connected state
packages/ui/src/internal/tunnel/computer-connect.tsxSplit art modal (beams art beside the content), pinned lg actions, one copy button that swaps to a check in place (#8491)
packages/ui/src/markdown/setup-links/connector-handshake.tsxHandshake tile: a catalogue logo fills its tile, a drawn mark takes one hairline (one boundary per tile, #8421)
packages/ui/src/primitives/tabs.tsxSegmented TabsList (the default): recessed track, raised chip, concentric radii, p-[2px] pixel snap (D4b, D4c)
packages/ui/src/primitives/menu-recipe.tsMenu row grid; MENU_PANEL_STATIC and FLOATING_PANEL_SURFACE are instant, MENU_PANEL and FLOATING_PANEL are for the hover card (D4a)
apps/web/src/components/projects/project-pending-screen.tsxThe page-level loading mark (D4h)
packages/ui/src/primitives/dot-matrix/session-dot-matrix.tsxThe session busy mark (D4f)
packages/ui/src/primitives/pixel-kortix-mark.tsxFirst-run empty state art (D4g)
apps/web/src/features/workspace/command-palette.tsxInstant palette, PaletteEmpty one-line empty state

Shells: capabilities/shared/capability-page-shell.tsx (CapabilityPageShell) is the current shell for capability pages. The older CustomizeSectionWrapper (customize/sections/component/section-wrapper.tsx) still backs the customize/sections/view/* screens.

Other references: tinted-icon tiles → packages/ui/src/customize/triggers/schedule-view.tsx; sidebar → project-sidebar/project-sidebar.tsx.

Layout & responsiveness

Always wrap a section in the shell its tree already uses — CapabilityPageShell under capabilities/, CustomizeSectionWrapper under customize/sections/view/. Do not hand-roll the outer shell.

Canonical pattern (from section-wrapper.tsx):

<div className="flex h-full min-h-0 flex-col">
  <div className="min-h-0 flex-1 overflow-y-auto">
    <div className="mx-auto w-full max-w-2xl space-y-5 px-4 py-10 pb-20 lg:py-20">
      <header className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
        <div className="space-y-1">
          <h2 className="text-foreground text-xl font-medium">{title}</h2>
          <span className="flex items-center gap-1">
            <p className="text-muted-foreground text-sm text-balance">{description}</p>
            {/* optional docs link: Button variant="transparent" asChild */}
          </span>
        </div>
        {action ? <div className="mt-2 shrink-0 sm:mt-0">{action}</div> : null}
      </header>
      {children}
    </div>
  </div>
</div>

Rules:

  • Header: title + description left; primary action right. Stacks on mobile (flex-col sm:flex-row). Content always below the header.
  • Container: mx-auto w-full max-w-2xl.
  • Section padding: py-10 pb-20 lg:py-20; space-y-5 between header and body.
  • Major blocks inside body: space-y-4 (search + list), space-y-6 (tab panels), space-y-8 (settings sections).
  • Mobile-first: test narrow width.

Card & panel patterns (no SectionCard)

A panel inside another panel, a form panel in a settings section and a floating panel are bg-popover rounded-md border surfaces. A bordered list that sits directly on a page is bg-background plus border-border: the color rule wins over this recipe (color.md, in-flow section, Q11). Two sanctioned ways to build a panel — never SectionCard:

  • Card (packages/ui/src/primitives/card.tsx) — the codified panel. Use it when the surface has a title/description/action header, distinct content, or a footer. Slots carry the spacing (px-4, pt-5/pb-5, gap-5 between slots — the panel px-4 py-5 rhythm); the bordered element itself has no padding, so flush children (tables, lists, images) sit edge-to-edge.
  • Hand-composed div — for a one-off padded block inside a section, the one-div shorthand bg-popover rounded-md border px-4 py-5 is fine (the project-settings-page.tsx pattern). The moment the panel needs a flush child or an internal seam, move the padding onto inner sections.

Panels are flat: border, no shadow — elevation is for overlays (see Elevation under Tokens).

Card (component)

<Card>
  <CardHeader>
    <CardTitle>Repository</CardTitle>
    <CardDescription>Where builds are pushed.</CardDescription>
    <CardAction>
      <Button size="sm" variant="secondary">Edit</Button>
    </CardAction>
  </CardHeader>
  <CardContent>{/* fields, rows — or a flush table as a direct Card child */}</CardContent>
  <CardFooter className="border-t">{/* meta or trailing actions */}</CardFooter>
</Card>
  • CardTitle is text-sm font-medium — panel-label scale, same as Label. Don't scale it up; a Card is a panel, not a hero.
  • Divided header: <CardHeader className="border-b"> (it compacts to pb-4); divided footer: className="border-t".
  • variant="glass" (bg-card/40 border-border/40 shadow-sm) is the only elevated variant — translucent surfaces over wallpaper/media.

Settings / form panel

<section className="space-y-4">
  <Label>Repository</Label>
  <div className="bg-popover space-y-5 rounded-md border px-4 py-5">
    <FieldGroup className="grid gap-3 sm:grid-cols-2">{/* fields */}</FieldGroup>
  </div>
</section>

Entity row (list item)

<ul className="space-y-2">
  <li className="group bg-popover flex items-center gap-3 rounded-md border px-4 py-2 transition-colors">
    {/* leading: size-9 tinted icon tile */}
    {/* body: min-w-0 flex-1 */}
    {/* trailing: Button size="sm" */}
  </li>
</ul>

Access rows use py-2.5 (shared/access/access-row.tsx). Changes use py-2.

Config entity disclosure (agents, skills, commands)

<div className="space-y-2">
  <Disclosure variant="outline" className="overflow-hidden" open={open} onOpenChange={setOpen}>
    <DisclosureTrigger variant="outline">
      <Button variant="popover" className="flex w-full items-center justify-start rounded-none">
        <span className="truncate text-sm font-medium">{name}</span>
      </Button>
    </DisclosureTrigger>
    <DisclosureContent variant="outline" contentClassName="border-border border-t">
      <div className="relative px-4 py-5">{/* detail */}</div>
    </DisclosureContent>
  </Disclosure>
</div>

Detail header: text-2xl font-semibold tracking-tight title; meta Badge variant="outline" size="sm"; toolbar absolute top-4 right-4 with ButtonGroup + Hint.

Dropdown item with icon + title + description

One recipe for every menu row that carries a glyph, a title and a one-line explanation (the "New ▾" menu on Agents, Skills, Connectors, Triggers and Secrets). Source: RichRowBody in packages/ui/src/customize/shared/new-entity-menu.tsx; Paper "New menu · icon + title + description", V03 (Jay, 2026-10-13).

<DropdownMenuContent align="end" className="w-76">            {/* 280px */}
  <DropdownMenuItem className="items-start gap-2.5 py-1.5">   {/* px-2 rounded-sm from menuRow */}
    <ChatCircleIcon className="text-muted-foreground mt-0.5 size-4 shrink-0" />
    <span className="flex min-w-0 flex-1 flex-col">
      <span className="text-foreground text-sm font-[550]">Create in chat</span>
      <span className="text-muted-foreground mt-0.5 text-xs font-normal text-pretty">
        An agent scaffolds it and opens a change request.
      </span>
    </span>
  </DropdownMenuItem>
</DropdownMenuContent>
  • First-line alignment, after alert.tsx. items-start plus mt-0.5 on the 16px glyph centres it on the title's first 20px line. Never items-center (the glyph drifts to the middle of a two-line block) and never a bare items-start (the glyph sits on the cap height).
  • Two tiers. Title text-sm font-[550] text-foreground, ink even before hover; the row's own text-foreground/80 and font-medium would make it read lighter than the description's job. Description text-xs text-muted-foreground font-normal, mt-0.5 under the title.
  • Width. w-76 (280px) lets a one-sentence description wrap at most once. w-64 wrapped after about 30 characters.
  • Glyph. Muted, size-4, no tile. Use the row's leading glyph, never a second one at the end.

Danger zone (settings)

Neutral bordered row — no red panel fill:

<div className="bg-popover rounded-md border px-4 py-3">
  <div className="flex items-center justify-between gap-4">
    <div className="min-w-0">{/* title text-sm font-medium + description text-xs */}</div>
    <Button variant="destructive" size="sm" onClick={openConfirm}>Archive</Button>
  </div>
</div>

destructive on the button is OK inside ConfirmDialog flow; panel itself stays neutral.

Button conventions

Match the customize views — consistent sizes and variants:

ContextPattern
Section header primary actionButton size="sm" variant="secondary" + PlusIcon (size-4) + label; group with gap-1.5
Empty state CTAButton variant="outline" size="sm" className="gap-1.5"
Docs / secondary linkButton asChild variant="ghost" size="sm" className="gap-1.5"
Row secondary actionButton variant="ghost" size="sm"
Row primary actionButton size="sm" (default variant)
Icon-only with tooltipHint → Button variant="outline" size="icon" inside ButtonGroup
Inline text linkButton variant="transparent" size="sm" asChild
Modal cancelButton variant="outline-ghost"
Pending / in-flight state<Loading className="size-4 shrink-0" /> in buttons; <Loading /> or className="size-4 shrink-0" in headers — never CircleNotchIcon

Icons in buttons: size-3.5 shrink-0 (dense) or size-4 (header). Always shrink-0 on icons. Exception: loading uses Loading, not an icon import.

Button groups — the default for buttons in a row

Two or more buttons side by side that serve one surface are a ButtonGroup (@kortix/ui/button-group). A toolbar cluster, a back/forward pair, a header's action set and a viewer's close cluster are all groups. Do not line buttons up with flex gap-1 and hand-tuned ml-*: the group owns the spacing (gap-px) and joins the corners of neighbours, so a row of controls reads as one object instead of loose icons.

RuleWhy
One variant for every member: all ghost, all secondary or all outline.Mixed weights break the group into a primary plus strays. A real primary action stands outside the group.
The close X joins the group of the actions beside it.It is part of the toolbar, not a separate control.
Every member is a direct child: Hint (it renders asChild) wrapping a Button is fine, and so are asChild links.The corner rounding targets direct children ([&>*]). A <span> or a PopoverAnchor wrapper between the group and the Button takes the rounding instead.
No dividers, gap-*, ml-* or mx-* inside a group.The group sets the rhythm; one override drifts every row.
One group per job. Navigation (back/forward) and page actions (open, full screen, close) are two groups, with the address bar or title between them.A group says "these act on the same thing".
A hidden member ({cond && …}) is fine; a group of one is not: render the lone Button bare.A group of one adds nothing.

Not a group: a modal or form footer (Cancel + Confirm stay a spaced pair, so the destructive or primary choice is not glued to Cancel), and a button beside a text input that is not part of it. Icon buttons inside an address or path pill are a group too: the pill holds the group at its trailing edge.

Reference implementations:

FileGroup
app/[locale]/(public)/share/session/[token]/page.tsxHeader actions, all secondary: open app, full screen, download or copy link, Open in Kortix
packages/ui/src/browser/sandbox-browser-chrome.tsxBack / forward, ghost; Reload and the pill's trailing actions inside the address pill
packages/ui/src/files/viewer/viewer-actions.tsx (ViewerPathPill)Refresh, copy, copy link inside the path pill, ghost
packages/ui/src/session/panel/easy/app-preview.tsxOpen in new tab, full screen, close, ghost
packages/ui/src/files/viewer/file-viewer.tsx, file-preview.tsxSave as PDF or Present, Download, full screen, close, ghost
packages/ui/src/changes/change-list.tsxUnified / split diff toggle, outline

Cursor affordance

Every clickable element shows cursor: pointer. Disabled elements keep cursor: default. Do not add cursor-default to an interactive element.

  • A clickable element is a <button>, a <Link>, or has a role. Never put onClick on a bare div.
  • The base rule in apps/web/src/app/globals.css gives the pointer to buttons, links, label[for], native checkbox/radio/file inputs, selects, summaries, and the button / checkbox / link / menuitem / menuitemcheckbox / menuitemradio / option / radio / switch / tab / treeitem roles. It excludes :disabled, aria-disabled, and data-disabled.
  • A utility class beats @layer base, so a stray cursor-default silently removes the pointer. tests/unit/cursor-affordance.test.ts fails on a cursor-default in packages/ui/src/primitives without an inline reason.

Icons — Phosphor, plus the house glyph set

The only icon library in apps/web is @phosphor-icons/react. lucide-react, react-icons, @mynaui/icons-react, and @icons-pack/react-simple-icons are removed and blocked by ESLint (no-restricted-imports).

  • House glyphs (D4i). packages/ui/src/internal/icons (the glyphs the package renders) and apps/web/src/features/icon/icons (the ones only apps/web renders) hold the glyphs Phosphor lacks and the third-party marks. A house glyph uses currentColor and takes its size from className. When you add one, replace every Phosphor equivalent in the same change.
  • Always import the *Icon-suffixed exports: PlusIcon, MagnifyingGlassIcon, CaretRightIcon — never the deprecated bare names (Plus).
  • Never pass a weight prop. One constant governs the whole app: DEFAULT_ICON_WEIGHT in packages/ui/src/lib/icon-config.ts (currently bold). Change it, save, and every icon flips; commit it and production renders the same. There is no runtime toggle and no dev/prod branching — the constant is the single source of truth in every environment. Compare all six weights at /design-system → Icons.
  • Two delivery paths, same constant, both propless:
    • Client components → IconProvider (root layout) feeds it to Phosphor's IconContext. Import from @phosphor-icons/react.
    • Server components (RSC) → React context does not exist in RSC, so import from @/lib/icons/ssr, where the weight is pre-bound. Importing @phosphor-icons/react/dist/ssr directly is ESLint-blocked (it silently defaults to regular); importing the main entry in a server component crashes the build (createContext at module scope). Adding an icon to a server component? Add its two lines to src/lib/icons/ssr.tsx.
  • Exception — solid intent: status tiles, success checks, destructive trash, and logo glyphs pass an explicit weight="fill" so they stay solid regardless of the global weight. This is the only weight prop in the codebase.
  • Semantic layer: packages/ui/src/primitives/kortix-icons.ts (IconAdd, IconDelete, …) re-exports Phosphor icons; prefer it where already adopted. It re-exports the client entry, so it is client-graph only.
  • Sizing stays Tailwind-first (size-4, size-3.5 shrink-0 in dense buttons); the provider's size: 24 default only covers class-less usages.
  • Background on the RSC rule above: the main entry calls createContext at module scope with no 'use client' directive, so a server-only module graph reaching it crashes the build with TypeError: (0, d.createContext) is not a function. Phosphor's own SSR entry fixes the crash but hardcodes weight="regular", which would quietly ignore DEFAULT_ICON_WEIGHT — that is why @/lib/icons/ssr exists and why the raw entry is ESLint-blocked outside it.

apps/mobile uses Phosphor too (phosphor-react-native, the community React Native port with the same 1,512 glyphs and names). Its rules differ from web in two places, both caused by Metro:

  • Import icons only from @/lib/icons. That registry is the only file that imports the package, one icon file at a time: Metro tree shaking is off, so the package barrel would ship every icon in all six weights.
  • The registry binds DEFAULT_ICON_WEIGHT (apps/mobile/lib/icons/icon-config.ts, bold) onto each icon, because IconContext is exported only from that barrel. weight="fill" is still the only weight prop.
  • apps/mobile/lib/icons/icon-imports.test.ts fails on a retired icon library, a direct package import, an unused registry entry, a non-fill weight, or a spinner glyph.

Motion in components

Budget and easings live in kortix-brand → motion.md. Read the frequency ladder there before adding any animation. The short version:

Kortix is a speed product. Fast, or none. Count how many times a day a user sees the interaction; that number sets the budget. Constant-use surfaces — keyboard nav, dense-list hover, focus moves, table selection — get no animation at all.

What that means for the components in this file:

ComponentMotion
Row hover in a list, sidebar navtransition-colors duration-fast. Color only — never position.
Keyboard-driven selection / arrow-key navNone. transition-none.
Disclosure, Tabs200ms, ease-out. Built in — do not add more.
Modal, Dialog, sheets300ms max, ease-out, enter from scale-0.97. Built into modal.tsx.
ToastOwned by toast.tsx. Do not restyle its motion.
LoadingShips its own rotation. Never add animate-spin.
Buttonsactive:scale-[0.96] + transition-colors. Nothing else. A full-width row presses with active:scale-[0.998] (D4k).
Skeleton → contentCross-fade opacity, or nothing. Never a stagger.

Do not add enter animations to list items. A staggered or animated list is measurably slower to read and to click than the same list appearing at once, and the cost is paid on every render.

Sanctioned exception — the icon-swap morph

Frequency gate: this pattern is for buttons whose state change is meaningful and occasional — copy → copied, connect → connected, follow → following. It is not for a control the user hits repeatedly in a working session. If in doubt, hard-swap the icon; a hard swap is never wrong, it is only plain.

When it does apply: never hard-swap {done ? <CheckIcon/> : <CopyIcon/>} with no transition. Cross-fade the two icons in the same box with blur + scale + opacity so it reads as one morph, not two objects blinking. The blur is what bridges the two states — without it the eye sees two distinct objects.

Values: scale 0.25 → 1, opacity 0 → 1, blur 4px → 0, spring { type: 'spring', duration: 0.3, bounce: 0 }. bounce: 0 is the brand. Always initial={false} so nothing animates on first paint.

import { AnimatePresence, motion } from 'motion/react';

<button
  onClick={handleCopy}
  aria-label={copied ? 'Copied' : 'Copy'}
  className={cn(
    'inline-flex size-7 items-center justify-center rounded-md',
    'text-muted-foreground hover:text-foreground hover:bg-muted-foreground/10',
    'cursor-pointer transition-colors active:scale-[0.96]',
  )}
>
  <span className="relative inline-flex size-3.5 items-center justify-center">
    <AnimatePresence initial={false} mode="popLayout">
      <motion.span
        key={copied ? 'check' : 'copy'}
        initial={{ scale: 0.25, opacity: 0, filter: 'blur(4px)' }}
        animate={{ scale: 1, opacity: 1, filter: 'blur(0px)' }}
        exit={{ scale: 0.25, opacity: 0, filter: 'blur(4px)' }}
        transition={{ type: 'spring', duration: 0.3, bounce: 0 }}
        className="absolute inset-0 inline-flex items-center justify-center"
      >
        {copied ? <CheckIcon className="size-3.5 text-kortix-green" /> : <CopyIcon className="size-3.5" />}
      </motion.span>
    </AnimatePresence>
  </span>
</button>

Rules: both icons share one fixed-size box (relative size-3.5 parent, each child absolute inset-0) so they overlap and the blur bridges the crossfade. Confirmed status colour stays a kortix-* token (text-kortix-green), never raw palette. Ship a prefers-reduced-motion branch — drop the scale and blur, keep the opacity. Reference: CopyButton in packages/ui/src/markdown/copy-button.tsx.

Spacing cheat sheet (from reference views)

LayerClasses
Section wrapper → bodyspace-y-5
Search + content blockspace-y-4
List of rows / disclosuresspace-y-2 inside space-y-4 parent
Settings major sectionsspace-y-8
Tab panel contentspace-y-6
Panel inner paddingpx-4 py-5 (standard), px-4 py-3 (compact row)
Row internal gapgap-3 (row), gap-1.5 (title/meta), gap-2 (button groups)
Detail content below titlemt-8
No-match empty searchpx-3 py-6 text-center text-xs
Card slot rhythmpx-4 all slots; pt-5 header / pb-5 last slot; gap-5 between slots

Padding never sits on a bordered element that hosts flush children (tables, lists, seams) — put it on the slots/inner sections. A single padded block may use the one-div shorthand bg-popover rounded-md border px-4 py-5.

Tokens — see kortix-brand

Every value lives in kortix-brand (references/visual/visual-system.json, explained by the visual guidance files). Colors, spacing steps, type rungs, radius ladder, elevation steps, easings and durations are enumerated there as closed allowlists, with the reasons and the audit.sh that enforces them. This file used to restate them; two copies drift, so the copy is gone.

The five facts you will get wrong from stock Tailwind habit, restated here only because they bite immediately:

FactConsequence
--spacing: 0.23rem, not 0.25remEvery spacing utility is 8% tighter. A 16px mockup padding is p-4, never p-[16px].
--text-xs: 0.8125rem = 13px, not 12pxtext-xs is the workhorse (1999 uses). There is no rung below it.
--radius: 0.625rem → rounded-md is 8pxrounded-md is the default for every panel, row, table and card.
Color is semantic tokens + the seven kortix-* accentsNo raw palette, no hex, and no dark: for color — tokens flip themselves.
In-flow surfaces are flat — border, no shadowShadows mean "floats above the page".

Status pattern — tinted icon tile

<span className={cn(
  'flex size-9 items-center justify-center rounded-sm',
  merged && 'bg-kortix-green/15',
  failed && 'bg-kortix-red/15',
  open && 'bg-kortix-blue/15',
)}>
  <Icon className={cn('size-5', merged && 'text-kortix-green', …)} />
</span>

Use weight="fill" icons at size-5 inside size-8/size-9 tiles. Pair with Badge for text labels when needed (review-detail.tsx, sandbox-provider-coverage.tsx).

Modal pattern (canonical — use modal.tsx)

From secrets-view.tsx / channels-view.tsx — not raw Dialog:

<Modal open={open} onOpenChange={setOpen}>
  <ModalContent className="lg:max-w-lg">
    <ModalHeader>
      <ModalTitle>Title</ModalTitle>
      <ModalDescription>Description</ModalDescription>
    </ModalHeader>
    <form onSubmit={handleSubmit}>
      <ModalBody className="max-h-[60vh] overflow-y-auto">
        {/* fields */}
      </ModalBody>
      <ModalFooter className="sm:justify-between">
        <Button type="button" variant="outline-ghost" onClick={() => setOpen(false)}>
          Cancel
        </Button>
        <Button type="submit" disabled={pending}>
          {pending ? <Loading className="size-4 shrink-0" /> : null}
          Save
        </Button>
      </ModalFooter>
    </form>
  </ModalContent>
</Modal>

Destructive confirms → ConfirmDialog, not a red-styled Modal trigger.

Tabs pattern

  • Default TabsList: the segmented control (recessed track, raised shadow-xs chip). A vertical list (a settings rail) is never segmented (tabs.tsx, capability-tabs.tsx).
  • Primary section tabs: TabsList type="underline" + TabsTrigger className="w-fit flex-none" (channels-view.tsx)
  • Filter / status tabs: TabsListCompact + TabsTriggerCompact (project-sessions-view.tsx, secrets-view.tsx)
  • Tab badge count: <CountBadge count={n} /> inside trigger

Loading pattern (canonical)

Every in-flight spinner is Loading from loading.tsx. There are no exceptions and no second spinner in this codebase. The component ships its own rotate/dash animation, so it never needs animate-spin.

No icon is ever a spinner. Not CircleNotchIcon, not SpinnerIcon, not SpinnerGapIcon, not any other Phosphor glyph, whatever it is aliased to (Loader, Loader2, IconLoader, …). If you catch yourself writing animate-spin on an icon, the answer is Loading. kortix-icons.ts deliberately exports no loader icon so the semantic layer can't offer you one.

import Loading from '@kortix/ui/loading';

// Button pending (replaces action icon)
<Button disabled={pending}>
  {pending ? <Loading className="size-3.5 shrink-0" /> : <PlusIcon className="size-3.5 shrink-0" />}
  Save
</Button>

// Section header action
<Button size="sm" variant="secondary" disabled={pending}>
  {pending ? <Loading className="size-4 shrink-0" /> : <PlusIcon className="size-4" />}
  New
</Button>

// Inline / modal submit
{pending ? <Loading className="size-4 shrink-0" /> : null}

Page-level loading is ProjectPendingScreen (the pulsing Kortix mark), not a skeleton (D4h). Use Skeleton only for a local element whose final shape is known (a field, a logo tile). Use Loading for active async operations (submit, fetch-in-button, mutation pending). For session-scoped work, such as an approve or deny button while a decision saves, use SessionDotMatrix (D4f). Never flash a fallback that the real content then replaces.

Search + loading + empty flow

Standard content block (agents-page.tsx pattern):

<div className="space-y-4">
  <InputGroupSearch>…<InputGroupSearchInput />…</InputGroupSearch>
  {isLoading ? (
    <div className="space-y-1">{/* Skeleton h-7 rounded-md × 5 */}</div>
  ) : isError ? (
    <ErrorState size="sm" action={<Button variant="outline" size="sm">Retry</Button>} />
  ) : items.length === 0 ? (
    <EmptyState size="sm" action={…} />
  ) : (
  /* list */
  )}
</div>

Dos & Don'ts

  • ✅ Page shell → CapabilityPageShell for a new page (CustomizeSectionWrapper only under customize/sections/view/). ❌ hand-rolled outer flex + header, ❌ [&_header_h2]: overrides of the shell header.
  • ✅ Panels → Card (card.tsx) or bg-popover rounded-md border with px-4 py-5. ❌ SectionCard, ❌ padding on a bordered element that hosts flush children.
  • ✅ Elevation → ladder step (shadow-md popovers, shadow-lg modals); in-flow panels stay flat with a border. ❌ dark:shadow-*, ❌ shadow-[…] when a step fits.
  • ✅ Lists → <ul className="space-y-2"> + entity row classes. ❌ List / ListRow, ❌ divide-y Card lists.
  • ✅ Expandable config → Disclosure + Button variant="popover". ❌ custom accordion, ❌ nested rounding unless concentric (inner radius = outer radius minus inset; otherwise the child is flush). See effects.md.
  • ✅ Modals → Modal from modal.tsx. ❌ Dialog/DialogContent in features.
  • ✅ Destructive actions → ConfirmDialog (or the inline two-step Cancel/confirm swap, channels-view.tsx). ❌ firing a delete/revoke mutation directly from a variant="destructive" click.
  • ✅ Tooltips → Hint. ❌ Tooltip primitives in features.
  • ✅ Two or more buttons in a row on one surface → ButtonGroup, one variant, close X included. ❌ flex gap-1 rows of loose buttons, ❌ ml-* / dividers between members, ❌ a group of one, ❌ grouping a modal's Cancel + Confirm.
  • ✅ Toasts → @kortix/ui/toast helpers. ❌ @/lib/toast (deleted), raw sonner, toast.success().
  • ✅ Badges → <Badge size="sm" variant="…">. ❌ hand-rolled chip spans.
  • ✅ Status → tinted icon tile + optional Badge. ❌ raw palette icon colors.
  • ✅ Color → kortix-* + semantic tokens. ❌ text-emerald-600, bg-amber-500.
  • ✅ Clickable elements → cursor: pointer; disabled keep default. ❌ cursor-default on an interactive element.
  • ✅ Meta separators → InlineMeta or text-muted-foreground/40 bullet (&bull;). ❌ inconsistent separators.
  • ✅ Empty → EmptyState size="sm", one muted line (D4g). ❌ centered <p> only, ❌ a forked or restyled EmptyState (Q23).
  • ✅ Alerts → InfoBanner. ❌ hand-rolled colored banners.
  • ✅ Pending spinners → Loading from loading.tsx. ❌ CircleNotchIcon, SpinnerIcon, or any animate-spin icon.
  • ✅ Motion → count the frequency first; transition-colors duration-fast on hover, ease-out on enter/exit, active:scale-[0.96] on press. ❌ transition-all, ❌ bare transition, ❌ ease-in, ❌ anything over 300ms in product UI.
  • ✅ Keyboard-driven interactions → transition-none. ❌ animating arrow-key nav or focus moves.
  • ✅ Every animation ships a prefers-reduced-motion variant. ❌ one variant only.
  • ✅ List items appear at once. ❌ stagger in product UI.
  • ✅ Interactive elements show the pointer from the global rule. ❌ cursor-default on a clickable element.

Workflow checklist

  1. Load kortix-brand — the value allowlists and the motion budget. Then this file, then make-interfaces-feel-better.
  2. Read the closest reference view from the table above. Copy structure, spacing, and primitives — don't invent a new layout dialect.
  3. Skim /design-system and packages/ui/src/primitives/ for anything not covered by the reference.
  4. Compose: the page shell (CapabilityPageShell) → search/panel/row/disclosure/table → Badge + Hint + Modal + toast + Loading + EmptyState. Buttons in a row → ButtonGroup. Never SectionCard, List, or CircleNotchIcon.
  5. Status → tinted icon tile. Color → kortix-*. Radius → rounded-md (panel), rounded-none (flush trigger). Elevation → overlays only; flat border for in-flow panels.
  6. Motion last. Count how often a user sees it. Constant → none. Otherwise ≤300ms, ease-out, named property, token duration, reduced-motion variant.
  7. New primitive? Tokens only, tiny API, add to /design-system.
  8. Verify: .agents/skills/kortix-brand/scripts/audit.sh <your paths> clean, no banned imports, nested rounding only when concentric (see effects.md), light + dark, tsc clean, polish checklist from make-interfaces-feel-better.
  9. Replay the interaction ten times. If you still notice the animation, it is too much.

评价

还没有评价。欢迎分享你的使用感受。

评论

无需登录即可发表。欢迎分享使用感受、提问,或说明在哪些环境下无法运行。

还没有评论。来写第一条吧。

提交后会立即公开。请勿填写邮箱、电话等个人信息。URL 最多 2 个。

0 / 1000 字

此表单受 reCAPTCHA 保护,适用 Google 的隐私权政策和服务条款。

同一仓库的技能

Review Slack access requests against Okta role and team information, then prepare narrowly scoped GitHub or AWS IAM permissions. Apply changes only after an eligible person's approval and record the grant, reasoning, approver, and timestamp.

中文简介正在准备中。当前显示英语简介。

  • Reviewing GitHub repository access requests from Slack
  • Preparing AWS IAM permissions for a specific task
  • Routing sensitive access requests for additional review
kortix-ai/suna2万2026年10月21日 更新

Research companies and people before outreach or meetings. Combines business profiles, recent news, hiring signals, and key contacts into a sourced brief with talking points and suggested questions, adding relationship history when CRM data is connected.

中文简介正在准备中。当前显示英语简介。

  • Research a prospective customer before outreach
  • Prepare background information for meeting attendees
  • Compare a competitor's products and positioning
kortix-ai/suna2万2026年10月21日 更新

Review daily Google Ads and Meta Ads results for budget pacing, declining performance, and unusual activity. Get prioritized findings with supporting numbers and concrete recommendations for a marketing team to assess and carry out.

中文简介正在准备中。当前显示英语简介。

  • Checking whether campaign budgets are being spent on schedule
  • Investigating rising acquisition costs or falling ad returns
  • Finding ads and keywords that spend without converting
kortix-ai/suna2万2026年10月21日 更新

Automate website tasks such as navigating pages, filling forms, clicking buttons, taking screenshots, and extracting data. The skill also supports web app testing, with specialized guides for Electron desktop apps, Slack, and cloud browsers.

中文简介正在准备中。当前显示英语简介。

  • Automate form entry and page navigation
  • Capture screenshots and extract page data
  • Explore web apps for bugs
kortix-ai/suna2万2026年10月21日 更新

Process emailed vendor invoices by extracting details, comparing them with purchase orders and invoice history, and recording duplicates, overcharges, or missing orders. Summarize results in Slack for human review; payment approval and execution remain with people.

中文简介正在准备中。当前显示英语简介。

  • Checking incoming invoices against purchase orders
  • Finding duplicate invoices and likely resends
  • Reviewing unit prices and remaining order balances
kortix-ai/suna2万2026年10月21日 更新

billing

免费

Guide changes to Kortix billing code, admin controls, and pricing screens. Explain how plans, trials, enterprise contracts, and access exceptions should work, where pricing is defined, and which audited routes must handle account changes.

中文简介正在准备中。当前显示英语简介。

  • Update billing logic and its specification together
  • Review how trials and enterprise contracts are handled
  • Find the code that defines prices and allowances
kortix-ai/suna2万2026年10月21日 更新

查看 kortix-ai 的全部技能

报告此技能的问题