diff --git a/README.md b/README.md index 69bca88..3cc9a0d 100644 --- a/README.md +++ b/README.md @@ -140,14 +140,29 @@ import {Button, Avatar} from "@offload-project/hallogen"; Higher-level, Inertia-aware building blocks: -`form-section` · `header` · `resource-clear-filters` · `resource-table` · `breadcrumb-items-render` · -`menu-items-render` · `navbar-items-render` · `pagination-render` · `sidebar-items-render` · `svg-html-render` +`form-section` · `header-package` · `resource-clear-filters` · `resource-filters` · `resource-table` · +`breadcrumb-items-render` · `menu-items-render` · `navbar-items-render` · `pagination-render` · +`sidebar-items-render` · `svg-html-render` ### Shared style presets -`tailwind-variants` style definitions shared across components, importable for building custom variants: +[`tailwind-variants`](https://www.tailwind-variants.org/) style definitions shared across components, +importable for building your own variants. Available from the barrel or the `shared/` subpath: -`shared/badge-styles` · `shared/button-styles` · `shared/field-styles` · `shared/toggle-styles` +| Module | Exports | +|------------------------|-----------------------------------------------------------------------| +| `shared/badge-styles` | `badgeStyles` | +| `shared/button-styles` | `buttonStyles` | +| `shared/field-styles` | `labelStyles`, `descriptionStyles`, `fieldErrorStyles`, `fieldStyles` | +| `shared/toggle-styles` | `toggleStyles` | + +```tsx +import {buttonStyles} from "@offload-project/hallogen/shared/button-styles"; +// or from the barrel: import { buttonStyles } from "@offload-project/hallogen"; + +// reuse the button's variants on a custom element +Link button; +``` ## Hooks diff --git a/package.json b/package.json index 9501307..4134a7a 100644 --- a/package.json +++ b/package.json @@ -502,6 +502,10 @@ "types": "./dist/components/composite/resource-clear-filters.d.ts", "import": "./dist/components/composite/resource-clear-filters.js" }, + "./resource-filters": { + "types": "./dist/components/composite/resource-filters.d.ts", + "import": "./dist/components/composite/resource-filters.js" + }, "./resource-table": { "types": "./dist/components/composite/resource-table.d.ts", "import": "./dist/components/composite/resource-table.js" diff --git a/src/components/composite/resource-filters.tsx b/src/components/composite/resource-filters.tsx new file mode 100644 index 0000000..8af763c --- /dev/null +++ b/src/components/composite/resource-filters.tsx @@ -0,0 +1,56 @@ +import { SearchField, SearchInput } from "@offload-project/hallogen/search-field"; +import { Toolbar, ToolbarLeft, ToolbarRight } from "@offload-project/hallogen/toolbar"; +import type { ReactNode, SubmitEvent } from "react"; +import type { Key } from "react-aria-components"; +import { ResourceClearFilters } from "@/components/composite/resource-clear-filters"; +import { TableColumnMenu } from "@/components/ui/table.tsx"; +import type { ColumnDef } from "@/types"; + +interface Props { + searchLabel: string; + searchPlaceholder: string; + searchDefaultValue: string | null; + onSearchSubmit: (event: SubmitEvent) => void; + /** Toggleable columns for the column-visibility menu. */ + columns: ColumnDef[]; + visible: Set; + onColumnsChange: (visible: Set) => void; + /** Current filter state, used to decide whether to show "Clear filters". */ + filters: object; + clearUrl: string; + /** Optional filter dropdown(s) rendered between search and the column menu. */ + children?: ReactNode; +} + +/** + * The standard admin index filter bar: a search field, an optional filter + * dropdown slot, the column-visibility menu, and a clear-filters link. + */ +export function ResourceFilters({ + searchLabel, + searchPlaceholder, + searchDefaultValue, + onSearchSubmit, + columns, + visible, + onColumnsChange, + filters, + clearUrl, + children, +}: Props) { + return ( + + + +
+ + + +
+ {children} + + +
+
+ ); +} diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx index 3ee7a6f..74f3417 100644 --- a/src/components/ui/checkbox.tsx +++ b/src/components/ui/checkbox.tsx @@ -1,4 +1,5 @@ import { CheckIcon, MinusIcon } from "lucide-react"; +import type { ComponentProps } from "react"; import { CheckboxButton, type CheckboxButtonProps, @@ -17,69 +18,89 @@ export function CheckboxGroup({ className, ...props }: CheckboxGroupProps) { {...props} data-slot="control" className={cx( - "space-y-3 has-[[slot=description]]:not-has-[[slot=errorMessage]]:space-y-6 has-[[slot=description]]:**:data-[slot=label]:font-medium **:[[slot=description]]:block", + "space-y-3 has-[[slot=description]]:space-y-6 has-[[slot=description]]:**:data-[slot=label]:font-medium **:[[slot=description]]:block", className, )} /> ); } +/** The field wrapper that owns the checkbox's form input and state. Compose with `CheckboxControl`. */ export function CheckboxField({ className, ...props }: CheckboxFieldProps) { return ( ); } -export function Checkbox({ className, ...props }: CheckboxButtonProps) { +/** The clickable area of a checkbox (indicator + label). Must be rendered inside a `CheckboxField`. */ +export function CheckboxControl({ className, children, ...props }: CheckboxButtonProps) { return ( - {composeRenderProps(props.children, (children, { isSelected, isIndeterminate, isInvalid }) => { + {composeRenderProps(children, (children, { isSelected, isIndeterminate, isFocusVisible, isInvalid }) => { + const isStringChild = typeof children === "string"; const indicator = isIndeterminate ? ( ) : isSelected ? ( ) : null; + const content = isStringChild ? {children} : children; + return (
{indicator} - + {content}
); })}
); } + +/** A self-contained checkbox: a `CheckboxField` (owns the input) wrapping a `CheckboxControl`. */ +export function Checkbox({ children, className, ...props }: CheckboxFieldProps) { + return ( + + {children} + + ); +} + +export function CheckboxLabel(props: ComponentProps) { + return