noeco-design
v0.1.5
Published
Reusable React dashboard architecture, forms, themes, and DataGrid patterns for NoEcosystem products.
Maintainers
Readme
noeco-design
Reusable React product patterns for NoEcosystem dashboards. It includes auth layouts, application shells, page composition, modal forms, semantic brand/theme controls, and the shared DataGrid with hierarchical rows.
Install
npm install noeco-design react react-dom lucide-react tailwindcssUse
import { DashboardPage, DashboardShell, PageHeader } from "noeco-design";
import "noeco-design/styles.css";The stylesheet targets Tailwind CSS 4 and should be imported once from the application's global stylesheet.
For tabular product data, import DataGrid and ColumnDef from the package root. Tree records use subRows with enableExpanding.
The consuming application owns routing, authentication, data fetching, permissions, translations, and domain rules.
Application shell
DashboardShell owns the mobile navigation overlay internally. Selecting any DashboardNavItem rendered inside DashboardShell invokes its existing onSelect callback exactly once and then automatically closes the mobile navigation. This works identically for mouse and keyboard activation (the nav item remains a type="button" button with preserved aria-current, backdrop dismissal, Open navigation / Close navigation labels, and RTL behavior). DashboardNavItem used outside DashboardShell retains its existing behavior. Consuming apps do not need to remount the shell, dispatch synthetic events, click the backdrop, or query the DOM. The close signal is an internal React context owned by DashboardShell; no new public mobileOpen state API was added.
Forms
TextField, TextareaField, and SelectField compose coss Field primitives with accessible labels, descriptions, and errors.
TextField
Field + coss Input; always pass name and an explicit type.
export type TextFieldProps = FieldCopy &
InputProps & {
nativeValidation?: boolean;
errorMessage?: string;
errorId?: string;
};Shared props:
label: string— visibleFieldLabeldescription?: string—FieldDescriptionnativeValidation?: boolean— defaults to native behavior; whenfalse,requiredremains semantic (aria-required="true"on the input) but the nativerequiredattribute is not rendered (no native blocking)errorMessage?: string— when a non-empty string (trimmed check only), renders a singleFieldErrorwith the original messageerrorId?: string— optional ID for the error element; defaults to`${name}-error`whenerrorMessageis present
required with nativeValidation omitted preserves the current native required behavior and exposes semantic required state via aria-required="true". required with nativeValidation={true} renders native required plus aria-required="true". required with nativeValidation={false} omits the native required attribute but still exposes aria-required="true", so required remains semantic while native blocking is disabled. nativeValidation, errorMessage, and errorId are never forwarded as unknown DOM attributes.
Accessibility: when errorMessage is non-empty (after trimming), the field is marked invalid via the Base UI Field API, the input receives aria-invalid="true" and aria-describedby pointing to the resolved error ID, and the error is rendered exactly once via FieldError with that id. Any caller-provided aria-describedby is safely combined with the resolved error ID without duplicate tokens. When no error message exists, aria-invalid and aria-describedby are not set and the existing native/Base UI FieldError validation behavior is preserved. Whitespace-only strings are treated as empty; the original non-empty message is displayed.
Supported custom-validation composition (no capture-phase submit interception needed):
<FormDialog noValidate onSubmit={handleSubmit} open={open} onOpenChange={setOpen} title="Edit period">
<TextField
required
nativeValidation={false}
label="Period name"
name="period-name"
type="text"
value={name}
onChange={(event) => setName(event.target.value)}
errorMessage={fieldError}
/>
</FormDialog>SelectField
Generic Field + coss Select implemented with existing coss Select primitives. The component remains router-, API-, auth-, and domain-agnostic. It does not own company, tenant, or period semantics. Consuming applications own selected state, persistence, endpoint calls, and permission logic.
export type SelectFieldOption = {
label: string;
value: string;
disabled?: boolean;
};Shared props:
label: string— visibleFieldLabeldescription?: string—FieldDescriptionname: string—Fieldname / hidden input nameoptions: readonly SelectFieldOption[]placeholder?: string— defaults to"Select an option"disabled?: boolean— forwarded to the Select rootrequired?: boolean— whennativeValidationis notfalse, forwarded to the Select root as nativerequired; always reflected asaria-required="true"on the trigger for semanticsnativeValidation?: boolean— defaults totrue; whenfalse,requiredremains semantic (aria-required="true"on the trigger) but native/Base UIrequiredconstraint is not registered (norequiredhidden input)size?: "sm" | "default" | "lg"— forwarded toSelectTriggererrorMessage?: string— when a non-empty string (trimmed check only), renders a singleFieldErrorwith the original message and semantictext-destructive-foregroundstylingerrorId?: string— optional ID for the error element; defaults to`${name}-error`whenerrorMessageis present
Disabled options are supported via SelectFieldOption.disabled and forwarded to SelectItem.
Accessibility: when errorMessage is non-empty (after trimming), the field is marked invalid via the Base UI Field API (when supported), the SelectTrigger receives aria-invalid="true" and aria-describedby pointing to the resolved error ID, and the error is rendered exactly once via FieldError with that id. When no error message exists, aria-invalid and aria-describedby are not set and the existing native/Base UI FieldError validation behavior is preserved. Whitespace-only strings are treated as empty; the original non-empty message is displayed.
Modes are mutually exclusive via a discriminated union — consumers cannot provide both value and defaultValue:
Uncontrolled (browser/form-owned, backward compatible):
<SelectField
defaultValue="pro"
label="Plan"
name="plan"
options={[
{ label: "Starter", value: "starter" },
{ label: "Pro", value: "pro" },
]}
/>Controlled (application-owned state):
const [period, setPeriod] = useState<string | null>("2026");
<SelectField
label="Financial period"
name="financial-period"
options={periodOptions}
onValueChange={setPeriod}
value={period}
/>Controlled with server-side field error (error belongs to the field, not the page):
const [period, setPeriod] = useState<string | null>(null);
const fieldError = actionData?.fieldErrors?.["financial-period"]; // string | undefined from the server
<SelectField
errorMessage={fieldError}
label="Financial period"
name="financial-period"
options={periodOptions}
onValueChange={setPeriod}
value={period}
/>
// When fieldError is "Select a period", the component renders:
// <Field invalid> + <SelectTrigger aria-invalid="true" aria-describedby="financial-period-error"> + <FieldError id="financial-period-error">Select a period</FieldError>
// Override the id with errorId="custom-error-id" when you need a custom association.Type shapes:
// Controlled
{ value: string | null; onValueChange: (value: string | null) => void; defaultValue?: never }
// Uncontrolled
{ defaultValue?: string | null; value?: never; onValueChange?: (value: string | null) => void }The component avoids passing both value and defaultValue (even as undefined) to respect Base UI's controlled/uncontrolled distinction, retains FieldLabel/FieldDescription/FieldError, remains keyboard accessible and RTL-compatible via coss primitives, and uses semantic design tokens only.
Semantic required versus native/Base UI constraint validation: nativeValidation defaults to true for backward compatibility — required is forwarded to the Select root (producing a required hidden input and native validation) and aria-required="true" is set on the trigger. With nativeValidation={false}, required is not forwarded to the Select root (no required hidden input, no native blocking) but aria-required="true" is still set on the trigger, preserving semantics while allowing application-owned validation to own errorMessage/errorId. Do not use DOM mutation, MutationObserver, requestAnimationFrame, querySelector, or refs that remove attributes as a workaround — use the nativeValidation prop.
Supported custom-validation composition:
<FormDialog noValidate onSubmit={handleSubmit} open={open} onOpenChange={setOpen} title="Edit record">
<SelectField
required
nativeValidation={false}
label="Period"
name="period"
options={options}
value={period}
onValueChange={setPeriod}
errorMessage={fieldError}
/>
</FormDialog>FormDialog
Controlled modal form. Required: open, onOpenChange, title, onSubmit, and field children. Optional: noValidate?: boolean — when omitted, the prop is not forwarded and preserves the underlying <Form> behavior, currently native constraint validation disabled (Base UI Form defaults to noValidate: true). When provided, it is forwarded as <Form noValidate={noValidate} ...> without replacing or duplicating the Form primitive and without exposing Base UI types. noValidate={true} explicitly disables native constraint blocking; noValidate={false} explicitly enables native validation. Applications with their own accessible validation should explicitly pass noValidate.
<FormDialog noValidate onSubmit={handleSubmit} open={open} onOpenChange={setOpen} title="Edit record">
<SelectField required errorMessage={fieldError} label="Period" name="period" options={options} value={period} onValueChange={setPeriod} />
</FormDialog>Semantic fields should retain required so controls expose aria-required even when native constraint blocking is disabled.
