Files
2026-10-20 13:35:20 +01:00

76 lines
8.4 KiB
Markdown

# 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). Components take icons as elements. Nothing a component imports may look one up by name: `LucideIcon` (`import * as icons`) puts all of lucide in every bundle of an app that reaches it, since bundlers can't tree-shake a namespace lookup, so only an app's own pages may import it.
## 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.