@swisper.ai/web-core
v0.0.7
Published
Core TypeScript utilities and types
Readme
@swisper.ai/web-core
Core TypeScript utilities and types. It holds the small React hooks and helpers that the Echo DS
packages share: no components, no CSS, no provider. It runs in the browser only, because its hooks
use ResizeObserver, IntersectionObserver and matchMedia and its CSS-variable helpers read
getComputedStyle. useReducedMotion and useControlledProps are the exceptions: they also render
on the server.
Boundary
Code belongs here if it is a framework-level helper or React hook with no markup, no styling and no
dependency on another @swisper.ai/* package. Components and hooks tied to Echo's look or theme
belong in @swisper.ai/ui, design tokens in @swisper.ai/design-system.
Install
npm install @swisper.ai/web-core| Peer | Range | Why |
| ----------- | ---------- | --------- |
| react | >=18.0.0 | Hooks |
| react-dom | >=18.0.0 | Rendering |
clsx and tailwind-merge are bundled into dist/, which imports nothing at runtime but React.
Setup
- Nothing to configure: import what you need.
- Use it:
import { cn, formatElementMeasurement, useElementMeasure } from '@swisper.ai/web-core';
export function Panel({ isActive }: { isActive: boolean }) {
const { ref, width } = useElementMeasure<HTMLDivElement>();
return (
<div ref={ref} className={cn('p-16', isActive && 'p-24')}>
Width: {formatElementMeasurement(width, 'unknown')}
</div>
);
}API
| Export | Why it exists |
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| cn | Joins class names and resolves Tailwind conflicts (clsx, then tailwind-merge), so a class passed in by a caller replaces the default it conflicts with. Tuned for Tailwind v4: a leading-* class survives next to a text size, whatever their order. The one cn for every @swisper.ai/* package |
| useElementMeasure | Tracks an element's border-box width and height with a ResizeObserver. Returns a callback ref and whole-pixel sizes, or 'unset' until there is a size |
| formatElementMeasurement | Turns a size from useElementMeasure into a CSS length: '320px', or a fallback ('100%' by default) for 'unset' |
| useIntersectionVisibility | Whether an element is visible inside a root element (or the viewport), via IntersectionObserver. Waits for an element that isn't in the DOM yet, so it works with late-mounted rows |
| useReducedMotion | Whether the user asked for reduced motion (prefers-reduced-motion: reduce), for motion CSS can't switch off: a JS-driven animation, an autoplaying video, smooth scrolling. Updates when the setting changes. Returns false on the server and while hydrating, unless you pass a getServerSnapshot |
| useControlledProps | State for a prop the parent may control or leave to the component: the value / defaultValue / onChange trio. Keeps the value itself while value is undefined, and returns the parent's once it is anything else, null included. Returns [value, setValue]; setValue keeps its identity and calls onChange either way |
| getCssVariableValue | Reads a CSS variable on :root (a theme size or breakpoint) as a number in pixels. Takes the unit it must be in ('px', 'rem' or 'em') and converts rem/em with the root font size |
| getVariableNumberValue | Reads a numeric CSS variable from any element (:root by default). Requires pixels unless isPx: false |
Troubleshooting
| Symptom | Cause | Fix |
| --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- |
| "Invalid hook call", or a context is missing | The app resolves two copies of React | Add resolve: { dedupe: ['react', 'react-dom'] } to the Vite config |
| useElementMeasure keeps returning 'unset' | The returned ref isn't attached to an element, or the element has no size yet (a zero size is reported as 'unset') | Pass ref to the element's ref prop; render formatElementMeasurement(value) to fall back meanwhile |
| useIntersectionVisibility always returns false | targetElement or rootElement was read from useRef().current during render, so the hook got null and never re-ran | Keep the elements in state with a callback ref (ref={setElement}) and pass the state |
| ReferenceError: ResizeObserver is not defined in a unit test | jsdom implements neither ResizeObserver nor IntersectionObserver | Stub both in the test setup file |
| TypeError: window.matchMedia is not a function in a unit test | jsdom doesn't implement matchMedia, which useReducedMotion calls | Assign a window.matchMedia stub in the test setup file |
| getCssVariableValue or getVariableNumberValue throws | The variable isn't defined on the element, is in another unit, or the call ran outside a browser (SSR) | Load the stylesheet that defines it before the call, and call it in an effect or event handler rather than at import |
