@optilogic/core
v2.5.0
Published
Core UI components for Optilogic - A professional React component library
Readme
@optilogic/core
Core UI components for opti-ui - A professional React component library built with Tailwind CSS and Radix UI primitives.
Installation
npm install @optilogic/corePeer Dependencies
Install the required peer dependencies:
npm install react react-dom tailwindcssOptional peer dependencies for specific components:
# DataGrid virtualization
npm install @tanstack/react-virtual
# DatePicker
npm install date-fns react-day-picker
# Toaster notifications
npm install sonnerSetup
1. Configure Tailwind CSS
Add the opti-ui preset to your Tailwind configuration:
// tailwind.config.js
import { optiUiPreset } from '@optilogic/core/tailwind-preset';
export default {
presets: [optiUiPreset],
content: [
'./src/**/*.{js,ts,jsx,tsx}',
'./node_modules/@optilogic/core/dist/**/*.{js,mjs}',
],
};2. Import Styles
Import the base styles in your app's entry point:
import '@optilogic/core/styles.css';Or define the CSS variables yourself using the reference in the styles.css file.
Usage
import { Button, Input, Card, CardHeader, CardTitle, CardContent } from '@optilogic/core';
function App() {
return (
<Card>
<CardHeader>
<CardTitle>Welcome</CardTitle>
</CardHeader>
<CardContent>
<Input placeholder="Enter your name" />
<Button variant="primary">Submit</Button>
</CardContent>
</Card>
);
}Components
Primitives
- Button, Input, Label, Textarea
- Badge, Checkbox, CheckIndicator, Switch
- Progress, Separator, Skeleton
Radix-based
- Select, Tabs, ToggleGroup, Accordion
- Tooltip, Popover, DropdownMenu
- AlertDialog
Layout
- Card, Table, Modal
- ResizablePanel, ResizeHandle
Data
- DataGrid (with virtualization)
- Autocomplete, Combobox, MultiSelect
- Picker (see below)
Feedback
- Chip, LoadingSpinner, Toaster
- ConfirmationModal
Utility
- IconButton, CopyButton, ContextMenu
Picker
Select and MultiSelect take a fixed option shape. Picker takes your own
objects and a description of how to read them, and gives you categories, rich
rows, search, filters, recents and pinned selection as base behavior.
<Picker
items={databases}
selected={ids}
onSelectionChange={setIds}
mode="multi"
layout="two-line"
fields={{
key: "id",
label: "name",
description: "path",
group: "type",
icon: "type",
badge: "tags",
meta: ["rows", "updated"],
disabled: "locked",
disabledReason: "lockedReason",
}}
icons={{ postgres: <Database />, duckdb: <Table2 /> }}
filters={[
{ id: "type", label: "Type", field: "type" },
{ id: "selected", label: "Selected only", type: "toggle", source: "selected" },
]}
groupOrder={["Databases", "Models"]}
pinSelected
recents={{ key: "databases" }}
/>Customizing, in four layers
Reach for the next layer only when the previous one runs out.
| Layer | You write | Use when |
| --- | --- | --- |
| Declarative | fields, layout, density, columns, filters | the common case; no code |
| classNames | classNames={{ row, groupHeader, badge, … }} | restyling parts, structure unchanged |
| slots | slots={{ row: (item, ctx, defaultNode) => … }} | decorating the default rendering |
| usePicker | the whole surface | a bespoke layout on the same engine |
Every slot receives the node the picker would have rendered as its last argument, so wrapping a row in a tooltip or appending to a group header doesn't mean reimplementing either.
usePicker is the whole engine with no surface attached — grouping, search
ranking, keyboard navigation, filters, recents and the selection rules
(single/multi, maxSelected, refusing disabled rows). commit returns false
when a row refuses the pick, so a custom surface knows not to treat it as
landed:
const picker = usePicker({
items, selected, fields,
mode: "multi",
maxSelected: 3,
onSelectionChange: setSelected,
});
picker.sections.map((section) => ( /* your own layout */ ));Filtering
filters builds the filter menu. filter is different — a static predicate for
what may be offered at all (a type allow-list), applied before search and before
the menu, and the menu derives its options from what survives it.
Disabled rows
A row that can't be picked stays visible on purpose — the user should see that
the thing exists and why it isn't available. disabledAppearance controls how
it presents: dim (default), muted, or plain. fields.disabledReason is
exempt in every mode, so the explanation stays legible.
Declarative first
The prop surface is designed to be describable as JSON: fields are dot paths,
filters are field/operator/value comparisons, and layout, mode, surface
and columns are string enums. The function props — getKey, getGroup,
isItemDisabled, search, slots — are escape hatches, and each one wins over
its declarative counterpart when both are supplied.
Search and recents
The built-in matcher is token-AND with positional weighting (exact > prefix >
word-start > substring), so core takes no fuzzy-search dependency. Pass search
to swap in Fuse or match-sorter. Recents persist through localStorage by
default; hosts with their own account-scoped store inject it as
recents={{ key, scope, storage }}.
License
MIT
