@tc96/properties
v1.2.0
Published
Domain-neutral property views for React applications using COSS and Base UI.
Maintainers
Readme
Properties
Domain-neutral property views for React applications built with COSS, Base UI, and Tailwind CSS.
The package provides small read and edit surfaces that can be composed into DataGrid cells, Kanban cards, detail panels, forms, and other SaaS views. Your application remains responsible for business meaning, permissions, option loading, mutations, validation, and persistence.
Initial primitives
SelectProperty: a typed single-choice badge and Select popupPersonProperty: a person-reference badge with avatar and Select popupMultiPersonProperty: a multi-person badge and multi-select popupDateProperty: a formatted date badge and Calendar popover
The package intentionally does not export StatusProperty, PriorityProperty,
AssignedProperty, or OwnerProperty. Those names encode product semantics and
should be consumer compositions over the neutral primitives.
Requirements
- React 19
- Base UI 1.x
- Tailwind CSS 4
The package is COSS-first. COSS is distributed as source through its component
registry, so the bundled registry item declares the COSS primitives it needs
through registryDependencies. A future Radix implementation will be a separate
adapter that preserves the same value and callback contracts.
Installation
Install the published package with your package manager:
pnpm add @tc96/properties
# or: bun add @tc96/properties
# or: npm install @tc96/propertiesThe consumer must provide the peer dependencies:
pnpm add react react-dom @base-ui/react tailwindcssThe COSS primitives used by the copied source are installed by the registry
route, not by npm peer dependencies. The @coss/* entries are shadcn/COSS
registry items and are not published npm packages.
COSS source location
@tc96/properties supports two installation modes. Use the npm package when you
want to import it from node_modules. Use the bundled registry item when you
want the source copied into the consuming app.
Keep ui mapped to your primitive components and add patterns for TC96
patterns. patterns is not a replacement for ui:
{
"$schema": "https://ui.shadcn.com/schema.json",
"tsx": true,
"aliases": {
"components": "@/components",
"ui": "@/components/ui",
"patterns": "@/components/patterns"
}
}After installing the package, add the local registry item:
bun add @tc96/properties
bunx shadcn@latest add ./node_modules/@tc96/properties/registry/properties.jsonThe registry item targets @components/patterns/properties, which resolves
through aliases.components, and declares the required COSS primitives through
registryDependencies so they resolve into ./components/ui.
Tailwind must scan the installed package and the application theme must expose the standard COSS semantic tokens:
@import "tailwindcss";
@source "../node_modules/@tc96/properties/dist";Select property
import {
SelectProperty,
type SelectPropertyOption,
} from "@tc96/properties";
type Category = "alpha" | "beta";
const categories: SelectPropertyOption<Category>[] = [
{ id: "alpha", label: "Alpha" },
{ id: "beta", label: "Beta" },
];
export function CategoryProperty({
value,
onChange,
}: {
value: Category;
onChange: (value: Category) => void;
}) {
return (
<SelectProperty
ariaLabel="Category"
options={categories}
value={value}
onValueChange={onChange}
/>
);
}Omit the change callback or pass readOnly to render a display-only badge.
Person property
const people = [
{
value: "alex",
avatar: { src: alexAvatarUrl, fallback: "AR" },
name: "Alex Rivera",
description: "[email protected]",
},
];
<PersonProperty
ariaLabel="Reviewer"
options={people}
placeholder="Select a person"
value={reviewerId}
onValueChange={setReviewerId}
/>name is the primary identity text. description is optional secondary text
for consumer-defined context such as an email address or team. avatar accepts
an optional image source and fallback text.
The consumer decides whether a person is a reviewer, owner, assignee, member, or another role. The package only understands a person reference.
PersonProperty accepts null and can emit null when its clear option is
selected. For multi-person values such as a set of assignees, use the neutral
MultiPersonProperty primitive:
<MultiPersonProperty
ariaLabel="Assignees"
options={people}
placeholder="Add assignees"
value={assigneeIds}
onValueChange={setAssigneeIds}
/>Date property
<DateProperty
ariaLabel="Target date"
fallback="No date"
locale="en-US"
timeZone="UTC"
value={targetDate}
onValueChange={setTargetDate}
/>Formatting, serialization, calendar placement, and clearing are configurable.
Business calculations such as whether a date is overdue belong to the consumer;
emphasized only controls generic visual emphasis.
Core contract
@tc96/properties/core exports lightweight definitions without React or
visual dependencies:
import type { PropertyDefinition } from "@tc96/properties/core";
const category: PropertyDefinition<"select"> = {
id: "category",
label: "Category",
type: "select",
};The package does not provide a universal runtime renderer. The current primitives have different value and interaction needs, and a universal renderer would create an unvalidated abstraction. Consumers can compose them explicitly today, either from the npm package or from the copied registry source.
Ownership boundary
| Package owns | Consumer owns | | --- | --- | | accessible trigger and popup composition | business meaning of a property | | generic option, person, and date presentation | authorization and available options | | controlled values and callbacks | remote state, validation, and mutations | | date parsing, formatting, and serialization hooks | canonical storage and time policies | | read-only and disabled presentation | workflows, automation, and navigation |
Development
Use Bun 1.3.14 and Node 24.18.0.
bun install
bun run storybook
bun run lint:ci
bun run typecheck
bun test
bun run buildSee CONTRIBUTING.md and ADR-001.
License
MIT © Gabriel Melo.
