@kollegioai/ui
v1.3.6
Published
Kollegio shared React components — framework-agnostic (plain CSS + tokens)
Readme
@kollegioai/ui
Kollegio's shared React component library — framework-agnostic (plain CSS + design tokens, no Chakra/Tailwind/emotion dependency). Published to npmjs.com.
Companion to
@kollegioai/tokens(colors + typography). This package is the component layer.
Setup (all apps)
Load the token variables and component styles once, at the app entry:
import '@kollegioai/tokens/vars.css'; // --kollegio-* design-token variables
import '@kollegioai/ui/styles.css'; // component CSS (wrapped in @layer kollegio-ui)All component CSS lives in @layer kollegio-ui, so any unlayered app CSS (emotion, Tailwind utilities, inline styles) automatically wins over the variant presets — no specificity fights.
Peer dependency: react >= 18. Nothing else.
Two consumption modes
1. Direct (non-Chakra apps — e.g. landing-v2)
Use the components as-is. Bespoke styling via className or the CSS-variable hooks on style (these also apply inside hover/active states):
import { Button } from '@kollegioai/ui/Button';
<Button variant="primary" buttonSize="lg" onClick={...}>Save</Button>
// override paint via --kui-* hooks
<Button
variant="outline"
style={{ '--kui-btn-radius': '999px', '--kui-btn-hover-bg': 'var(--kollegio-backgroundPrimarySubtle)' }}
>
Pill
</Button>Button hooks: --kui-btn-bg, -color, -border-color, -hover-bg, -hover-color, -hover-border-color, -active-bg, -radius, -h, -min-h, -px, -py, -fs, -gap, -disabled-opacity.
Tooltip hooks: --kui-tooltip-bg, -color, -fs, -radius, -px, -py, -max-w, -z.
2. Chakra bridge (Chakra v3 apps — client/client-src, counsellor)
Wrap the universal component with Chakra's chakra() factory at one adapter seam. Call sites then keep the full Chakra style-prop API (w="150px", bg="backgroundPrimary", _hover={{…}}, responsive values), resolved against the app's own theme:
// src/shared/components/KollegioButton.tsx (adapter seam — once per app)
import { chakra, type HTMLChakraProps } from '@chakra-ui/react';
import { Button as UniversalButton } from '@kollegioai/ui/Button';
const KollegioButton = chakra(UniversalButton, {}, {
// non-style props the factory must pass through to the component
forwardProps: ['variant', 'buttonSize', 'fullWidth', 'isLoading', 'loadingText',
'spinnerPlacement', 'leftIcon', 'rightIcon', 'enableTouchEndClickFix'],
});Because the package CSS is layered, the emotion styles generated by the bridge always override the variant presets.
Caveat: as= / asChild on a bridged component replaces the universal Button itself (losing its logic and classes) — don't use them; handle navigation in onClick instead.
New shared components automatically get both modes — author them universally here, and each Chakra app's seam file decides whether to bridge.
Components
Subpath exports keep bundles lean (sideEffects: false):
| Import | Description |
|---|---|
| @kollegioai/ui/Button | Brand button. Variants: primary, primaryStrong, accent, outline, secondary, tertiary, light, ghost, oauth, logOut. Sizes via buttonSize (sm/md/lg). Also: fullWidth, isLoading/loadingText/spinnerPlacement, leftIcon/rightIcon, enableTouchEndClickFix. |
| @kollegioai/ui/Spinner | Loading spinner (size sm/md/lg, optional label). Inherits currentColor. |
| @kollegioai/ui/Input | Text input. inputSize (sm/md/lg), invalid, fullWidth, leftElement/rightElement adornments. className + chakra-bridge style props target the bordered group. |
| @kollegioai/ui/Textarea | Multiline input. invalid, fullWidth, rows; shares the --kui-input-* paint vars with Input. |
| @kollegioai/ui/Field | Form-field layout: label row (+ required, labelExtra) + control + error/helperText. Pair with Input/Textarea (pass matching id/htmlFor and invalid={!!error}). |
| @kollegioai/ui/Tooltip | Radix-based tooltip (plain-text/ReactNode label), tap-to-open on touch devices. offset accepts a number or { mainAxis, crossAxis }. |
| @kollegioai/ui/TooltipMarkdown | Tooltip whose label renders markdown (pulls in react-markdown — only pay for it if you import this entry). |
| @kollegioai/ui/EllipsisMenu | Three-dot overflow menu (EllipsisMenu + EllipsisMenuOption). |
| @kollegioai/ui | Barrel of all of the above (except TooltipMarkdown). |
Variant/size presets are data in src/recipes/button.ts; scripts/gen-button-css.mjs generates styles/button.css from it at build time.
Storybook
pnpm --filter @kollegioai/ui storybookRenders every component/variant against the token CSS variables — no provider needed.
Test changes in a consumer app (without publishing)
When you change a component here and want to see it in client/client-src (or counsellor) before publishing, don't bump+publish a throwaway version. Use one of the methods below.
Why not
npm link/file:? Those symlink the package, and Vite then resolvesreactfrompackages/ui/node_modules(its dev copy of React 18) instead of the app's React. Two React instances →Invalid hook callcrash. The tarball method below avoids this because the tarball ships nonode_modules—react,react-dom,@radix-ui/*, and@kollegioai/tokensare allexternal, so they resolve to the consumer app's own copies, exactly as a real install would. (If you must symlink for a faster loop, see the note at the end.)
Method 1 — packed tarball (recommended, true-to-published)
This produces the exact artifact npm would publish.
# 1) Build + pack the package → kollegioai-ui-<version>.tgz
cd "landing-v2/packages/ui"
pnpm build
pnpm pack
# 2) Install that tarball into the consumer (absolute path is safest)
cd "../../../client/client-src"
npm install "../../landing-v2/packages/ui/kollegioai-ui-$(node -p "require('../../landing-v2/packages/ui/package.json').version").tgz"
# 3) Run the app and verify
npm run devpackage.json in client-src will now point at the local tarball ("@kollegioai/ui": "file:..."). Don't commit that change.
Method 2 — tarball watch loop (faster iteration)
For repeated edits, keep a rebuild loop running and re-pack/re-install after each change. In one terminal:
cd "landing-v2/packages/ui" && pnpm dev # tsup --watch: rebuilds dist/ on every saveThen, after a batch of edits, refresh the consumer (the tarball reflects whatever dist/ currently holds):
cd "landing-v2/packages/ui" && pnpm pack
cd "../../../client/client-src" && npm install "../../landing-v2/packages/ui/"*.tgz && npm run devRevert to the published version
Once you've published (or to undo local linking), reinstall from the registry and discard the package.json/lockfile edits:
cd "client/client-src"
npm install @kollegioai/ui@latest # or @^<version>
git checkout package.json package-lock.json # drop the file: pointer
rm -f "../../landing-v2/packages/ui/"*.tgz # clean up tarballsFaster symlink loop (advanced). If the re-pack loop is too slow, you can
npm install "../../landing-v2/packages/ui"(symlinked) together withpnpm dev, but you must first stop Vite from double-loading React: addreactandreact-domtoresolve.dedupeinclient-src/vite.config.ts. Without that you'll getInvalid hook call. Revert thevite.config.tsedit afterward.
Build, version & publish
The registry rejects re-publishing an existing version, so every publish starts with a version bump.
1. Bump the version in package.json (semver — patch for fixes, minor for new components, major for breaking prop changes).
// packages/ui/package.json
"version": "1.3.3",Option A — CI auto-publish (recommended)
Just merge the version bump to main. .github/workflows/publish-ui.yml builds and runs pnpm --filter @kollegioai/ui publish --no-git-checks automatically on any change under packages/ui/** (story-only changes are excluded). It authenticates with the NPM_TOKEN repo secret — no local token needed. This is how releases normally go out.
Option B — Manual local publish (pnpm release)
Use when you can't wait for CI. After bumping the version, one command does everything:
cd "landing-v2/packages/ui"
pnpm releaserelease runs scripts/publish.sh, which:
- Loads
NPM_TOKENfrompackages/ui/.env(gitignored — never committed, never echoed). - Builds (
pnpm build). - Writes a temporary
.npmrccontaining only the literal${NPM_TOKEN}placeholder (npm expands it from the env var at publish time — the secret never touches disk). - Runs
pnpm publish --no-git-checks --access public(pnpm rewritesworkspace:^deps to real versions automatically). - Removes the temp
.npmrcon exit via atrap— guaranteed even if the publish fails.
Setup (one-time): create packages/ui/.env with:
NPM_TOKEN=npm_yourAutomationTokenHereThe
NPM_TOKENmust be an Automation token (or a granular token with "bypass two-factor authentication" enabled). The account enforces 2FA for publishing, so a plain publish/login token fails withE403 … bypass 2fa. Both.envand.npmrcare already covered by the repo.gitignore.
Verify & consume
npm view @kollegioai/ui version # confirm the new version is live
# repoint consumers (landing apps auto-update via workspace:^)
cd "/Users/mac/Documents/Kollegio AI/client/client-src" && npm install @kollegioai/ui@^<new-version>
cd "/Users/mac/Documents/Kollegio AI/counsellor" && npm install @kollegioai/ui@^<new-version>For local testing before publishing, skip the registry entirely: pnpm pack and install the resulting tarball in a consumer.
