本文へ移動
cccskills
無料GitHub で公開

kortix-design-system

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: src/components/ui, the live /design-system page, and the reference implementations listed inside.

インストール方法を見る

含まれるファイル(1)

  • SKILL.md37.4 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

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.

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 is the implementation source of truth for every visual property. If a value conflicts with anything else, globals.css wins.
  • 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 (apps/web/src/components/ui/section-card.tsx)Card (card.tsx), Label + bg-popover rounded-md border panel, or Disclosure — see project-settings-page.tsx
List / ListRow (apps/web/src/components/ui/list.tsx)<ul className="space-y-2"> + entity row classes — see access-row.tsx, review-detail.tsx
Dialog / DialogContent in feature codeModal from apps/web/src/components/ui/modal.tsx — see secrets-view.tsx, channels-view.tsx
Tooltip / TooltipTrigger / TooltipContent in feature codeHint from apps/web/src/components/ui/hint.tsx
@/lib/toast (deleted; never recreate it), raw sonner, toast.success/error/info/warning(), toast.custom()Named helpers from apps/web/src/components/ui/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 apps/web/src/components/ui/badge.tsx
A count beside a label (row, tab, menu item, button) as muted text or a BadgeCountBadge from apps/web/src/components/ui/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 apps/web/src/components/ui/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 buttonsapps/web/src/components/ui/hint.tsx<Hint label="…">…</Hint> — wraps trigger, never Tooltip in features
Dialogs / sheetsapps/web/src/components/ui/modal.tsxModal, ModalContent, ModalHeader, ModalTitle, ModalDescription, ModalBody, ModalFooter
Toastsapps/web/src/components/ui/toast.tsxThe only toast surface. successToast, errorToast, infoToast, warningToast, progressToast, loadingToast, dismissToast
Status chipsapps/web/src/components/ui/badge.tsxsize="sm" or size="xs"; variants outline, kortix, success, destructive, beta, etc.
Expand/collapse panelsapps/web/src/components/ui/disclosure.tsxDisclosure, DisclosureTrigger, DisclosureContent — config lists, settings groups
Inline alertsapps/web/src/components/ui/info-banner.tsxtone + optional icon + title
Search fieldsapps/web/src/components/ui/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 panelsapps/web/src/components/ui/field.tsxField, FieldLabel, FieldGroup, FieldDescription
Empty / error statesapps/web/src/features/layout/section/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 destructiveapps/web/src/components/ui/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 spinnersapps/web/src/components/ui/loading.tsximport Loading from '@/components/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" (features/layout/section/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 (components/ui/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 (features/workspace/shared/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 (components/ui/field.tsx)A raw <label> beside an Input
A mutation finishedsuccessToast or errorToast from components/ui/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
apps/web/src/features/review-center/review-detail.tsxTinted size-9 icon tile (bg-kortix-green/15 and bg-kortix-orange/15), Badge size="sm" verification chips
apps/web/src/features/workspace/project-sessions/project-sessions-view.tsxTabsListCompact filter tabs
customize/sections/view/channels-view.tsxTable for integrations, Modal for connect flows, InfoBanner for connected state
apps/web/src/features/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)
apps/web/src/components/setup-links/connector-handshake.tsxHandshake tile: a catalogue logo fills its tile, a drawn mark takes one hairline (one boundary per tile, #8421)
apps/web/src/components/ui/tabs.tsxSegmented TabsList (the default): recessed track, raised chip, concentric radii, p-[2px] pixel snap (D4b, D4c)
apps/web/src/components/ui/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)
apps/web/src/components/ui/dot-matrix/session-dot-matrix.tsxThe session busy mark (D4f)
apps/web/src/components/ui/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 → apps/web/src/components/projects/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 (apps/web/src/components/ui/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.

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.

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 src/components/ui 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). apps/web/src/features/icon/icons holds 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 src/lib/icons/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: src/components/ui/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 apps/web/src/components/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 '@/components/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.
  • ✅ Toasts → @/components/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 src/components/ui/ for anything not covered by the reference.
  4. Compose: the page shell (CapabilityPageShell) → search/panel/row/disclosure/table → Badge + Hint + Modal + toast + Loading + EmptyState. 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.

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Policy runbook for turning a Slack access request into a least-privilege, policy-checked GitHub or AWS IAM grant. Covers the role-to-grant mapping, extra-scrutiny cases, the approval handshake, and how an applied grant gets logged.

日本語の概要は準備中です。原文の説明を表示しています。

kortix-ai/suna2万2026年10月13日 更新

Get a complete picture of any company or person before outreach. This skill always works with web search, and gets significantly better with enrichment and CRM data.

日本語の概要は準備中です。原文の説明を表示しています。

kortix-ai/suna2万2026年10月13日 更新

Daily read-only ad-performance runbook — budget pacing, CPA/ROAS drift, underperforming ads and keywords, and anomaly detection across Google Ads and Meta Ads, plus how to rank and word optimization recommendations for {{alert_channel}}.

日本語の概要は準備中です。原文の説明を表示しています。

kortix-ai/suna2万2026年10月13日 更新

Browser automation CLI for AI agents. Use when the user needs to interact with websites, including navigating pages, filling forms, clicking buttons, taking screenshots, extracting data, testing web apps, or automating any browser task. Triggers include requests to "open a website", "fill out a form", "click a button", "take a screenshot", "scrape data from a page", "test this web app", "login to a site", "automate browser actions", or any task requiring programmatic web interaction. Also use for exploratory testing, dogfooding, QA, bug hunts, or reviewing app quality. Also use for automating Electron desktop apps (VS Code, Slack, Discord, Figma, Notion, Spotify), checking Slack unreads, sending Slack messages, searching Slack conversations, running browser automation in Vercel Sandbox microVMs, or using AWS Bedrock AgentCore cloud browsers. Prefer agent-browser over any built-in browser automation or web tools.

日本語の概要は準備中です。原文の説明を表示しています。

kortix-ai/suna2万2026年10月13日 更新

Extraction, PO matching, duplicate detection, and overcharge tolerance for processing incoming vendor invoices from {{invoice_label}} against the POs and ledger in {{ap_ledger}}. Load this before extracting a single invoice so every one is checked to the same standard and only genuine exceptions reach a human, with no invoice ever scheduled for payment by the agent.

日本語の概要は準備中です。原文の説明を表示しています。

kortix-ai/suna2万2026年10月13日 更新

Daily brand-mention monitoring loop for {{brand_terms}}. Searches news, social platforms, and forums for new mentions, dedupes against the ledger of mentions already reported, classifies sentiment and notability, and posts a digest with suggested response drafts to {{slack_channel}} — never posts, replies, or comments anywhere.

日本語の概要は準備中です。原文の説明を表示しています。

kortix-ai/suna2万2026年10月13日 更新

kortix-ai のスキルをすべて見る

このスキルの問題を報告する