shelving
Version:
Toolkit for using data in JavaScript.
154 lines (105 loc) • 7.15 kB
Markdown
# UI utilities
Low-level helpers shared across every UI component. You rarely call these directly in application code — they are the building blocks that every component in the library uses internally.
## CSS classes — `getClass` and `getModuleClass`
These two functions appear in almost every component in the library. Every component that accepts variant props (`small`, `primary`, `plain`, etc.) uses them to build its `className`.
**`getClass(...classes)`** joins any mix of strings, `null`/`undefined` (ignored), nested arrays, and `Variants` objects into a single class string. A `Variants` object is a plain boolean dictionary: keys whose value is strictly `true` are included; all others are ignored. This means you can pass component `props` directly and boolean variant flags are picked up automatically.
```ts
import { getClass } from "shelving/ui";
getClass("button", null, { primary: true, small: false });
// → "button primary"
getClass("box", ["a", "b"]);
// → "box a b"
```
**`getModuleClass(module, ...classes)`** does the same but maps each resolved class name through a CSS module dictionary, yielding only hashed names that exist in the module. If `module` is not a CSS module object (e.g. the environment doesn't process `.module.css` files) it returns `undefined` silently — components degrade gracefully.
```ts
import { getModuleClass } from "shelving/ui";
import BUTTON_CSS from "./Button.module.css";
// Inside a component — pass the base class name and then the whole props object.
getModuleClass(BUTTON_CSS, "button", variants);
// → hashed class string containing "button" plus any matching true-valued variant keys
```
The canonical component pattern combines both:
```tsx
export function Button({ children, ...variants }: ButtonProps): ReactElement {
return (
<button
className={getClass(
BLOCK_CLASS, //
getModuleClass(BUTTON_CSS, "button", variants),
)}
>
{children}
</button>
);
}
```
The `Classes` type describes all accepted input forms: `string | null | undefined | Classes[] | Variants`.
## Context — `requireContext`
**`requireContext(context, caller?)`** reads a React context and throws `RequiredError` if the value is `null` or `undefined`. Use it instead of `use(context)` when a context must be provided by an ancestor. All `require*()` hooks in the library follow this pattern.
```ts
import { requireContext } from "shelving/ui";
function requireNavigation(): NavigationStore {
return requireContext(NavigationContext, requireNavigation);
}
```
## Metadata — `Meta` and `PossibleMeta`
Types and functions for page-level metadata (URL, title, description, CSP, stylesheets, etc.). Key exports:
- **`Meta`** — the canonical metadata shape used internally.
- **`PossibleMeta`** — the looser input shape accepted by `<HTML>`, `<Navigation>`, `<Router>`, and `<TreeApp>` props.
- **`mergeMeta(meta1, meta2)`** — merges two `Meta` objects, resolving URLs, joining titles (`"Page - App"`), and absolutifying asset hrefs.
- **`createMeta(possible)`** — initialises a `Meta` from a `PossibleMeta` from scratch.
- **`joinTitles(...titles)`** — joins non-empty titles with ` - `.
- **`joinMetaCSP(csp)`** — serialises a structured CSP object to a string.
You interact with `PossibleMeta` mostly via component props (`url`, `root`, `title`, `description`, etc.) rather than calling the merge helpers directly.
## Notice dispatch — `notify` family
A thin event bus for toast-style notifications. Components dispatch custom events on the DOM; `<Notices>` listens and displays them. See [ui/notice](/ui/notice) for the components.
| Function | What it does |
|---|---|
| `notify(message, status?, el?)` | Dispatch a notice event (defaults to `window`) |
| `notifySuccess(message, el?)` | Shorthand for a success notice |
| `notifyError(message, el?)` | Shorthand for an error notice |
| `notifyThrown(thrown, el?)` | Extract a message from a caught value and dispatch an error |
| `callNotified(callback, ...args)` | Run a callback; dispatch success or error automatically |
| `awaitNotified(promise)` | Same for an already-pending promise |
| `subscribeNotices(callback, el?)` | Listen for notice events; returns an unsubscribe function |
```ts
import { callNotified } from "shelving/ui";
// Return a string for success; throw (a string) for error.
callNotified(async () => {
await saveData();
return "Saved!";
});
```
## Scroll — `useScrollIntersect` and `getMostVisibleObserverEntry`
**`useScrollIntersect(onEnter?, onLeave?)`** attaches an `IntersectionObserver` to an element ref and fires callbacks when it enters or leaves the visible area. Returns a `RefObject` to attach to the target DOM element.
**`getMostVisibleObserverEntry(entries)`** picks the `IntersectionObserverEntry` with the highest `intersectionRatio` from an `IntersectionObserver` callback's entry list.
## State — `useDebouncedState` and `useDebouncedCallback`
**`useDebouncedState(initial, delay?)`** returns `[value, setter]`. The setter delays updating state until `delay` ms after the last call (default 500 ms). Use for search inputs that should wait for the user to stop typing.
**`useDebouncedCallback(callback, delay?)`** returns a debounced version of `callback`. Automatically clears any pending timeout on unmount.
```ts
import { useDebouncedState } from "shelving/ui";
const [query, setQuery] = useDebouncedState("", 300);
```
## Focus — `focusFirstFocusable`, `loopFocus`, `eventLoopFocus`
Helpers for keyboard and focus management inside dialogs and menus:
- **`focusFirstFocusable(el)`** — moves focus to the first focusable descendant of `el`.
- **`loopFocus(element, nextTarget)`** — if `nextTarget` is outside `element`, loops focus back to the first focusable child inside it.
- **`eventLoopFocus(event)`** — convenience wrapper for use directly as an `onBlur` event handler.
## Refresh — `useRefresh`
**`useRefresh(interval)`** forces a component to re-render every `interval` milliseconds. Use for live clocks, countdown timers, or any display that must stay current without an external store.
```ts
import { useRefresh } from "shelving/ui";
useRefresh(1000); // re-render every second
```
## Props — `ChildProps` and `OptionalChildProps`
Shared base interfaces for the `children` prop. Component props interfaces `extend` one of them instead of redeclaring `children` inline, which keeps the `readonly children: ReactNode` shape consistent everywhere.
- **`ChildProps`** — `readonly children: ReactNode` (children are required).
- **`OptionalChildProps`** — `readonly children?: ReactNode | undefined` (children are optional).
```ts
import type { OptionalChildProps } from "shelving/ui";
export interface BlockquoteProps extends OptionalChildProps {}
```
## See also
- [ui/notice](/ui/notice) — `<Notice>`, `<Notices>`, and the store layer built on top of the `notify` helpers
- [ui/router](/ui/router) — uses `requireContext` and `PossibleMeta` from this module
- [ui/tree](/ui/tree) — tree shell components that use `getClass`/`getModuleClass` throughout