> For the complete documentation index, see [llms.txt](https://analog.groupthought.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://analog.groupthought.com/reference/utils.md).

# Utils

The plain TypeScript helpers exported from @groupthought/assembly-ui/utils: DOM checks, layout measurement, focus preservation, state machines, and directive modifier parsing.

The plain TypeScript helpers the plugins are built from, imported from `@groupthought/assembly-ui/utils`. These modules are internal and can change between versions.

## dom

Element checks, form submission plumbing, and scheduling helpers.

| Function                                                                                                                                                                                                                | What it does                                                                                                                                     |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `isHtmlElement(node)`                                                                                                                                                                                                   | Type guard for element nodes; most plugins call it first                                                                                         |
| `isImageElement` / `isVideoElement` / `isSourceElement` / `isScriptElement` / `isAnchorElement` / `isFormElement` / `isDialogElement` / `isInputElement` / `isButtonElement` / `isSummaryElement` / `isTemplateElement` | Per-tag type guards                                                                                                                              |
| `isInputRangeElement(node)` / `isInputNumberElement(node)`                                                                                                                                                              | Guards for `type="range"` and `type="number"` inputs                                                                                             |
| `isHiddenInput(el)`                                                                                                                                                                                                     | Guard for `type="hidden"` inputs                                                                                                                 |
| `isProductForm(el)` / `isInProductForm(el)`                                                                                                                                                                             | True for a `cart/add` form, or an element inside one                                                                                             |
| `isShopifyAppBlock(el)` / `isShopifyElement(el)`                                                                                                                                                                        | True for app-block and Shopify-injected elements                                                                                                 |
| `isTouchOnlyDevice()`                                                                                                                                                                                                   | True when the main pointer cannot hover. Same test as the `touch:` style variant, so JavaScript and CSS agree                                    |
| `isHoverSupported()` / `isPopoverApiSupported()` / `isDialogApiSupported()` / `isFormDataSubmitterSupported()`                                                                                                          | Capability checks                                                                                                                                |
| `isTouchEvent(e)` / `isLeftMouseButton(e)` / `isPrimaryPointer(e)`                                                                                                                                                      | Pointer event checks; `isPrimaryPointer` filters out right clicks and secondary touch points                                                     |
| `isElementKeyboardAccessible(el)`                                                                                                                                                                                       | True when the element is natively focusable                                                                                                      |
| `isNodeOrChild(parent, child)`                                                                                                                                                                                          | True when child is parent or a descendant of it                                                                                                  |
| `getWritingDirection(el)`                                                                                                                                                                                               | `ltr` or `rtl` from computed style                                                                                                               |
| `getFormSubmissionInfo(target, submitter)`                                                                                                                                                                              | Action, method, `FormData`, and enctype from a form, button, or submit input                                                                     |
| `submitFormData(info, init)`                                                                                                                                                                                            | Fetches with that info: JSON body for `application/json`, multipart otherwise, query string for GET                                              |
| `serializeFormData(formData)`                                                                                                                                                                                           | Nested JSON object from bracketed field names (`properties[gift]`)                                                                               |
| `parseMultipart(body, contentType)`                                                                                                                                                                                     | Structured data back out of a multipart request body                                                                                             |
| `isModifiedEvent(e)` / `shouldProcessLinkClick(e, target)`                                                                                                                                                              | True when a click should navigate normally (modifier key, non-left button, `target="_blank"`)                                                    |
| `isClipboardWriteAllowed()`                                                                                                                                                                                             | Resolves whether `clipboard.writeText` can run; assumes yes where the permission cannot be queried                                               |
| `isClickingOutside(e, container)` / `isPointWithinElement(x, y, el)`                                                                                                                                                    | Outside-click checks; the first uses `composedPath()` so content swapped after the click still counts as inside                                  |
| `onDocumentReady(cb)`                                                                                                                                                                                                   | Runs on `DOMContentLoaded`, or at once when the document is already complete                                                                     |
| `requestIdleCallback(cb, options)` / `cancelIdleCallback(id)`                                                                                                                                                           | Idle scheduling with a `setTimeout` fallback. Always pass a `timeout`, or a busy main thread can put the callback off forever                    |
| `onFirstScroll(cb)`                                                                                                                                                                                                     | Runs at the first scroll, or right after the next paint when the page is already scrolled (anchor links, back button). Returns a cancel function |
| `getCartApiSections()`                                                                                                                                                                                                  | Section ids that carry `data-cart-api-section`, for the cart API `sections` parameter                                                            |
| `createCartRequestGroup(onSettle)`                                                                                                                                                                                      | Groups overlapping cart requests and settles once with the request that started last, or `null` when it failed                                   |

## layout-measure

Reads layout without forcing synchronous reflows. Callbacks scheduled in the same frame share one flush with reads running first, so batched reads never interleave with batched writes.

| Function                   | What it does                                                                                                                                                                                 |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `onFirstLayout(el, fn)`    | Runs once the element has a layout box, inside a ResizeObserver callback: reads are free and style writes still land in the same frame's paint. Use it to measure, then apply initial styles |
| `afterNextPaint(fn)`       | Runs after the next paint. Reads are free; writes render one frame later, so never apply initial visual state here. Returns a cancel function                                                |
| `afterNextPaintAsync()`    | Promise form of `afterNextPaint`                                                                                                                                                             |
| `readLayoutAfterPaint(fn)` | Runs the read after the next paint, before that frame's `afterNextPaint` callbacks, and resolves with its result                                                                             |

## preserve-focus

Preserves keyboard focus across DOM swaps (`Alpine.morph` or innerHTML replacement). When a swap replaces the focused node, the browser silently drops focus to `<body>` and the next Tab restarts from the top of the document.

| Function / attribute             | What it does                                                                                                                                                                             |
| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `captureFocus(rootEl)`           | Snapshot of the focused element inside the root: tag, id, name, labels, ancestors, and text selection. Null when focus is elsewhere                                                      |
| `restoreFocus(rootEl, snapshot)` | After the swap, refocuses the surviving or replacement element, or the nearest surviving container's first focusable element. No-op when focus already sits on a real element            |
| `relocateFocusFrom(container)`   | Moves focus out of a subtree before it is hidden or removed: following siblings first, then preceding, then the nearest focusable outside. Prefers a neighbor with the same `aria-label` |
| `data-preserve-focus-key`        | Attribute giving an element a stable identity across renders when its id or label changes                                                                                                |

## state-machine

A typed finite state machine. The async plugins (`async-form`, `async-link`) use it for their request states.

| Member                                                | What it does                                                                                             |
| ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `new StateMachine(initialState, states, transitions)` | States are a string tuple; transitions map each state to the states it can move to                       |
| `machine.state`                                       | Get the current state, or set it. Setting an invalid transition throws                                   |
| `machine.prevState`                                   | The previous state, or null at the initial state                                                         |
| `machine.setTransientState(state, nextState, ms)`     | Enters a state and moves to the next after the timeout, for short-lived UI states like a success message |
| `machine.on(listener)` / `.off(listener)`             | Subscribe to state changes with `(newState, prevState)`                                                  |

## revertible-value

A value that remembers its previous value.

| Member                         | What it does                                                               |
| ------------------------------ | -------------------------------------------------------------------------- |
| `new RevertibleValue(initial)` | Holds `currentValue` and `previousValue`                                   |
| `value`                        | Get or set. Setting a different value moves the old one to `previousValue` |
| `revert()`                     | Restores the previous value                                                |
| `commit()`                     | Makes the current value the new baseline                                   |

## modifiers

Parses values out of directive modifier lists (`x-marquee.speed.60` gives `['speed', '60']`). Each parser returns null when the modifier is absent or malformed.

| Function                                                    | What it does                                                                      |
| ----------------------------------------------------------- | --------------------------------------------------------------------------------- |
| `getModifierValue(modifiers, name)`                         | The raw token after the named modifier                                            |
| `getNumberModifierValue(modifiers, name)`                   | The token as a finite number                                                      |
| `getPositiveNumberModifierValue(modifiers, name)`           | The token as a number greater than zero                                           |
| `getMillisecondsModifierValue(modifiers, name)`             | A `500ms` token as `500`                                                          |
| `getPercentageModifierValue(modifiers, name)`               | A `50%` token as `50`                                                             |
| `getPixelModifierValue(modifiers, name)`                    | A `-12px` token as `-12`                                                          |
| `getMassModifierValue(modifiers, name, minimumMass)`        | A spring mass in units of the minimum mass, so `.mass.20` gives `0.2`             |
| `getSpringModifierConfig(modifiers, defaults, minimumMass)` | Applies `.mass`, `.stiffness`, and `.damping` modifiers to a Motion spring config |

## math

| Function                                         | What it does                                                                            |
| ------------------------------------------------ | --------------------------------------------------------------------------------------- |
| `clamp(n, min, max)`                             | The value clamped between min and max                                                   |
| `mod(n, m)`                                      | Modulo that handles negative numbers: `mod(-1, 5)` is `4`                               |
| `getDPR(element)` / `roundByDPR(element, value)` | The device pixel ratio, and rounding to it so positioned elements land on device pixels |
| `toScrollValue(value)`                           | Rounds the way browsers round scroll positions, to the nearest half integer             |
| `isNumeric(value)`                               | True when a string parses fully as a number                                             |

## object

| Function                                      | What it does                                                                                    |
| --------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `camelizeKeys(record, upperCamel)`            | A copy of the record with snake\_case keys converted to camelCase                               |
| `isPlainObject(x)`                            | True for plain objects; false for arrays and dates                                              |
| `mergeProperties(target, ...sources)`         | Like `Object.assign` but copies property descriptors, so getters and setters keep working       |
| `renameProperty(obj, oldName, newName)`       | Renames a property in place, preserving its descriptor                                          |
| `copyProperty(obj, newObj, oldName, newName)` | Copies a property descriptor to another object                                                  |
| `trackProperty(obj, key, trackingKey)`        | Proxy that writes the old value to the tracking key whenever the tracked value actually changes |

## string

| Function                          | What it does                                 |
| --------------------------------- | -------------------------------------------- |
| `toCamelCase(string, upperCamel)` | snake\_case to camelCase (or UpperCamelCase) |
| `isString(x)`                     | Type guard for strings                       |

## function

| Function        | What it does             |
| --------------- | ------------------------ |
| `isFunction(x)` | Type guard for functions |

## product

Lookups against a product object in Shopify's JSON shape.

| Function                                  | What it does                                                                |
| ----------------------------------------- | --------------------------------------------------------------------------- |
| `getVariantById(product, variantId)`      | The variant with that id, or null                                           |
| `getVariantsForOptions(product, options)` | Every variant whose options match the given values, including a partial set |

## section

| Function               | What it does                                                                    |
| ---------------------- | ------------------------------------------------------------------------------- |
| `closestSectionEl(el)` | The containing `.shopify-section` element, or the element itself when it is one |

## connection

Reads the Network Information API where the browser has it; each check returns undefined where it does not.

| Function                   | What it does                            |
| -------------------------- | --------------------------------------- |
| `isSavingData()`           | True when the visitor has data saver on |
| `isSlowConnection()`       | True on a 2g-class connection           |
| `hasConnection(navigator)` | Type guard for `navigator.connection`   |

## device

| Function             | What it does                                         |
| -------------------- | ---------------------------------------------------- |
| `isReducedMotion()`  | True when the visitor prefers reduced motion         |
| `isLowPowerDevice()` | True with 2 or fewer CPU cores or 2GB or less memory |

## yield-to-main

| Function        | What it does                                                                                                                        |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `yieldToMain()` | Resolves after yielding to the main thread (`scheduler.yield` with a `setTimeout` fallback), to break a long task into smaller ones |
