Files
tailwind-ui-library/AGENTS.md
T
tben 73a25f54c2 Add bunext playground for local component development
Runs at http://localhost:7010 via bun run dev. The playground imports the library from src through a committed symlink and tsconfig path alias, so component edits hot-reload without a build step. It has its own install root and is excluded from the published package.
2026-10-19 04:23:07 +01:00

8.2 KiB

Agent Instructions

Stack

  • What this is: @moduletrace/twui, a React component library for Tailwind CSS v4, published to the Moduletrace Gitea npm registry (https://git.tben.me/api/packages/moduletrace/npm/). It is consumed by other projects (e.g. presets/bunext-sqlite), so every change here is a change to a shared, published API.
  • Runtime/tooling: Bun, not Node. Use bun install, bun run <script>, bun test, bunx. Never npm/npx/yarn/pnpm.
  • Build: tsc emits src/ → dist/ (JS + .d.ts), then scripts/post-compile.ts runs. There is no Next.js, Vite, PostCSS config or tailwind.config in this repo - don't add them.
  • Styling: Tailwind v4 via src/base.css (@import "tailwindcss", @source "./", @theme inline tokens). tailwind-merge (twMerge) for class composition.
  • Icons: lucide-react (peer dependency).

Commands

Command What it does
bun run typecheck tsc --noEmit - must be clean before finishing any change
bun test src Unit tests (src/**/*.test.ts)
bun run compile Clean build into dist/ + post-compile step
bun run dev Start the playground in dev/ at http://localhost:7010
bun run dev:types tsc --watch, for consumers working against dist/
bun run dev:typecheck Typecheck the playground (dev/tsconfig.json)
./publish.sh "msg" Typecheck → test → compile → git commit/push → bun publish

publish.sh pushes and publishes - only run it when explicitly asked. Bump version in package.json before publishing.

Layout

Path Contents
src/index.ts Root barrel (import { Button } from "@moduletrace/twui")
src/base.css Base stylesheet + default theme tokens; shipped as @moduletrace/twui/base.css
src/layout/ Primitives: Stack, Row, Container, Section, Button, H1-H5, P, Span, Link, Img, ...
src/elements/ Composite UI: Card, Modal, Popover, Table, Tabs, Toast, Tag, LucideIcon, ...
src/elements/ai/ AI prompt blocks (optional peers: openai types, react-markdown)
src/form/ Form, Input (folder index), Select, SearchSelect, Textarea, Checkbox, uploads
src/hooks/ useStatus, useLocalStorage, useWebSocket, ...
src/editors/ AceEditor (optional ace-builds), TinyMCE (folder index, loaded from a URL at runtime)
src/mdx/, src/next-js/ MDX helpers (optional next-mdx-remote, remark-gfm, rehype-prism-plus)
src/composites/docs/ Docs page layout pieces
src/utils/ Pure helpers (slugify, numberfy, ejson, fetchApi, ...) with colocated tests
src/(partials)/, src/(functions)/, src/(utils)/ Internal helpers, not meant for direct import
src/types.ts Shared types/constants
scripts/post-compile.ts Copies base.css, adds .js to relative imports, prepends "use client"
dev/ Local bunext playground, own node_modules/lockfile; never published. dev/twui -> ../src drives HMR for library edits

Public API Rules (MANDATORY)

  • Deep import paths are public API. Consumers import @moduletrace/twui/<folder>/<File> (the "./*" export maps to dist/*.js). Renaming or moving a file, or renaming an exported name, is a breaking change - avoid it, or call it out and bump the version accordingly.
  • Folder-index modules need explicit exports entries. "./*" can't resolve form/Input → form/Input/index.js. If you add a component as Foo/index.tsx, add a matching entry in package.json exports (see ./form/Input, ./editors/TinyMCE, ./composites/docs).
  • Keep the barrel in sync. A new component/hook/util must be added to src/index.ts as export { default as Name } from "./path" (plus export * from "./path" if it exports types/constants). Exception: anything that imports an optional peer (editors, mdx, next-js, elements/ai, RemoteCodeBlock) stays deep-import only - the barrel must never pull in optional peers.
  • Exported names must be unique across the barrel. Name props types after their component (TWUIButtonProps, TWUI_TABS_PROPS), never copy another component's type name. bun run typecheck reports clashes as TS2308.
  • twui-* class names are API too. Every component appends a stable twui-<component>[-variant] class (e.g. twui-button-primary, twui-card) that consumers target in their own CSS. Keep existing ones; add one for new components/variants and list it in the component's JSDoc as @className twui-... (see src/elements/Card.tsx).
  • Theme tokens are API. Components use semantic token utilities (bg-primary, hover:bg-primary-hover, dark:bg-primary-dark, text-foreground-light, ...) defined in src/base.css @theme inline. Don't hardcode hex colors in components; if a new token is needed, add it to base.css with a sensible default.

Dependencies

  • dependencies: only tailwind-merge and lodash. Keep this list minimal.
  • peerDependencies: react, react-dom, tailwindcss (^4), lucide-react.
  • Feature-specific packages are optional peers (peerDependenciesMeta.optional) and also listed in devDependencies so the repo typechecks. Follow that pattern for any new third-party package used by a single feature, and update the optional-peers table in README.md.
  • Don't let optional peers leak into .d.ts files reachable from the barrel (e.g. src/types.ts must not import from ace-builds).

Conventions

  • Relative imports only inside src/ - there is no @/ path alias. Don't add file extensions in source; post-compile.ts adds .js to the emitted output.
  • One component per file, default export, named in PascalCase matching the file (hooks/utils use their function name). Prop types extend the underlying element's props (DetailedHTMLProps<...> or ComponentProps<typeof Stack>) and spread the rest onto the root element.
  • Merge classes with twMerge, with the caller's className last so it wins.
  • Constant unions are exported as as const arrays plus a derived type (TWUIButtonColors / TWUIButtonColorType).
  • Don't write "use client" in source - post-compile.ts adds it to every emitted module except the barrel, types.js, utils/ and (utils)/. Keep utils/ free of React/DOM-only code so it stays server-safe.
  • Env access: never read process.env directly in components (it throws in browser bundles without process). Wrap each lookup in try/catch and keep the full literal process.env.X expression so bundlers can inline it - see src/editors/TinyMCE/default-base-url.ts.
  • SSR safety: guard window/document/localStorage access inside effects or typeof window !== "undefined" checks, never at module top level.
  • Tests: pure helpers in src/utils/ get a colocated *.test.ts using bun:test. Test files are excluded from the build.

Verifying a Change

  1. bun run typecheck and bun test src pass.
  2. For packaging/exports changes: bun run compile, then bun pm pack and install the tarball into a scratch project. Check that root and deep imports typecheck with skipLibCheck: false, and that @import "@moduletrace/twui/base.css" produces the component classes when run through the Tailwind CLI.