tbenandClaude Opus 5.5 7d7eae5801 Custom Radios indicator with one-line labels; keep Pagination within its parent
Radios: each radio stays on one line, with its label clamped to a single
line (full text on hover) like Checkbox. Pass `wrap` to let labels flow
onto multiple lines. The native radio is replaced by a custom indicator
aligned to the label's first line; the input stays (visually hidden) for
forms and keyboard use, so inputProps.className now lands on a hidden
input - style .twui-radio-indicator instead. Adds twui-radios,
twui-radio, twui-radio-label and twui-radio-indicator classes.

Pagination: flex children could not shrink below their content, so many
pages pushed the component past its parent. Add min-w-0 so the page
buttons scroll inside the parent instead. Adds twui-pagination.

Playground: demos for both, and bump bunext to 1.3.5.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 08:52:11 +01:00
2026-10-20 13:35:20 +01:00
2026-10-26 10:45:46 +01:00

@moduletrace/twui

A modular skeletal React component framework for Tailwind CSS v4.

Install

The package is published to the Moduletrace Gitea registry. Add this to your project's .npmrc:

@moduletrace:registry=https://git.tben.me/api/packages/moduletrace/npm/

Then install the package with its required peers:

bun add @moduletrace/twui react react-dom lucide-react tailwindcss

Optional peers

Only install these if you use the matching components:

Feature Packages
editors/AceEditor ace-builds
elements/RemoteCodeBlock, mdx/markdown/* next-mdx-remote, remark-gfm, rehype-prism-plus
mdx/mdx-components, next-js/hooks/useMDXComponents @types/mdx (dev)
elements/ai/* react-markdown, remark-gfm (+ openai types for the legacy AIPrompt* blocks)

editors/TinyMCE loads TinyMCE from a URL at runtime. Set it with the base_url prop, or with the NEXT_PUBLIC_TINYMCE_BASE_URL / BUNEXT_PUBLIC_TINYMCE_BASE_URL public env vars.

CSS

Import base.css once in your main stylesheet. It includes @import "tailwindcss", the default theme tokens, and an @source rule so Tailwind generates the classes the components use. Override tokens in your own @theme inline block:

@import "@moduletrace/twui/base.css";

@theme inline {
    --color-primary: #e2b433;
    --color-primary-hover: #9e7503;
    --color-primary-dark: #e2b433;
    --color-primary-dark-hover: #f7cc59;
    /* ...see base.css for the full list of tokens */
}

.twui-button-general {
    @apply rounded-full;
}

Usage

Import from the root entry:

import { Button, Stack, Row, useStatus } from "@moduletrace/twui";

Or use deep imports, which mirror the source folder layout:

import Button from "@moduletrace/twui/layout/Button";
import Input from "@moduletrace/twui/form/Input";
import TinyMCEEditor from "@moduletrace/twui/editors/TinyMCE";

Editors, MDX, Next.js helpers, the AI blocks and RemoteCodeBlock are deep-import only, so the root entry never pulls in optional peers.

AI chat

elements/ai/AIChat is a complete chat UI. Give it a height and a send function. send can return a string, an async iterable of chunks, a ReadableStream, or a fetch Response:

import AIChat from "@moduletrace/twui/elements/ai/AIChat";

<AIChat
    className="h-[80vh]"
    history={{ storageKey: "my-app-chats" }}
    models={["gpt-5", "claude-opus-5-5"]}
    send={({ messages, model, signal }) =>
        fetch("/api/chat", {
            method: "POST",
            body: JSON.stringify({ messages, model }),
            signal,
        })
    }
/>;
Feature Props
Streaming replies, stop, regenerate, retry built in
Saved conversations (sidebar, search, pin, rename, delete) history (true, options, or a useAIChatHistory() result), generateTitle in the options
Read aloud (text-to-speech) speech (browser voice by default, or { speak } returning audio), autoSpeak
Voice input (speech-to-text) dictation (browser recognition by default, or { transcribe })
Model picker models, model, onModelChange
Edit messages, thumbs up/down, markdown export editable, feedback / onFeedback, exportable
Empty state, avatars, custom rendering suggestions, emptyState, userImageUrl, renderMessageContent, renderMessageActions

To drive the chat from outside, call useAIChat (and useAIChatHistory) yourself and pass the results as chat and history. The pieces (AIChatMessage, AIChatComposer, AIChatSidebar, useAIChatSpeech, useAIChatDictation) are exported from elements/ai/ for custom layouts.

Component modules are marked "use client", so they work in the Next.js App Router.

Development

bun install
bun run typecheck
bun test src
bun run compile   # tsc → dist/, then scripts/post-compile.ts

Playground

dev/ is a local bunext app that imports the library straight from src/ (through the committed dev/twui symlink and a tsconfig path alias), so component edits hot-reload without running tsc. It has its own node_modules and is excluded from the published package by the files whitelist.

cd dev
bun install
cd ..
bun run dev          # http://localhost:7010

Install the library dependencies at the repo root first: the symlinked sources resolve react, tailwind-merge, lucide-react and the optional peers from the root node_modules.

Other useful scripts:

bun run dev:types      # tsc --watch, for consumers working against dist/
bun run dev:typecheck  # typecheck the playground pages and the sources they import

Publishing

export GITBEN_NPM_TOKEN=...
./publish.sh "Commit message"

publish.sh typechecks, tests, compiles, commits, pushes, and runs bun publish. Bump version in package.json first.

S
Description
No description provided
Readme
562 KiB
Languages
TypeScript 98.7%
CSS 1.1%
Shell 0.2%