--- url: /migration-guides/vue-mapbox-gl.md --- # @studiometa/vue-mapbox-gl → @studiometa/ui-mapbox This guide helps you migrate from the Vue 3 library [`@studiometa/vue-mapbox-gl`](https://www.npmjs.com/package/@studiometa/vue-mapbox-gl) to the [js-toolkit](https://js-toolkit.studiometa.dev/) components published in [`@studiometa/ui-mapbox`](https://www.npmjs.com/package/@studiometa/ui-mapbox). \[\[toc]] ## Why migrate `@studiometa/ui-mapbox` re-implements the Mapbox GL components on top of [js-toolkit](https://js-toolkit.studiometa.dev/) instead of Vue. The main benefits are: * **No Vue dependency** — the components are plain js-toolkit classes bound to the DOM through `data-component` attributes. You do not need Vue in your project to render a map. * **Lighter for simple maps** — you ship the map logic and Mapbox GL, without a framework runtime, which keeps the footprint small for mostly-static maps. * **Server-rendered friendly** — the map is authored as regular HTML and enhanced in place, which fits Twig/Blade/Nunjucks templates naturally. The trade-off is that component options are **not reactive** (see [Reactivity caveat](#reactivity-caveat)). If your map relies heavily on reactive props driven by application state, weigh that difference before migrating. ## Install and setup Replace the Vue library with the js-toolkit package and its peers. ```bash npm remove @studiometa/vue-mapbox-gl npm install @studiometa/ui-mapbox mapbox-gl # Optional, only if you use the geocoder npm install @mapbox/mapbox-gl-geocoder ``` The Mapbox GL stylesheet is still required. Keep importing it as before: ```css @import 'mapbox-gl/dist/mapbox-gl.css'; ``` Instead of registering the components on a Vue app, register them with js-toolkit. Every component is self-registering — `MapboxMap` no longer declares its children, so each one must be registered with [`registerComponent`](https://js-toolkit.studiometa.dev/api/helpers/registerComponent.html). Register only the ones you use — a bare map needs only `MapboxMap`, but every marker, control, source or cluster you declare needs its own registration. Registration order does not matter, because a child registered before its `MapboxMap` still wires up once the map connects. Because `mapbox-gl` is heavy (~230 kB gzipped), the recommended default is to lazy-register each component with js-toolkit's [`importWhen*` helpers](https://js-toolkit.studiometa.dev/api/helpers/importWhenVisible.html) and the per-component subpaths (each subpath's default export is the component class): ```js import { registerComponent, importWhenVisible } from '@studiometa/js-toolkit'; // Register only the components your page uses; order doesn't matter. registerComponent(importWhenVisible(() => import('@studiometa/ui-mapbox/MapboxMap'), 'MapboxMap')); registerComponent( importWhenVisible(() => import('@studiometa/ui-mapbox/MapboxMarker'), 'MapboxMarker'), ); registerComponent( importWhenVisible(() => import('@studiometa/ui-mapbox/MapboxPopup'), 'MapboxPopup'), ); ``` ## Component mapping Most Vue components have a same-named js-toolkit equivalent, which you author as `data-component` elements nested inside a `MapboxMap` instead of Vue templates. The clustering and store-locator components are the exception — they were re-architected around a new `MapboxClusterItem`, detailed below the table. | `@studiometa/vue-mapbox-gl` | `@studiometa/ui-mapbox` | Notes | | --------------------------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | `MapboxMap` | `MapboxMap` | Root component, owns the map instance. | | `MapboxMarker` | `MapboxMarker` | | | `MapboxPopup` | `MapboxPopup` | Content comes from the element's inner HTML. | | `MapboxNavigationControl` | `MapboxNavigationControl` | | | `MapboxGeolocateControl` | `MapboxGeolocateControl` | | | `MapboxFullscreenControl` | `MapboxFullscreenControl` | Ported. | | `MapboxGeocoder` | `MapboxGeocoder` | Needs the optional `@mapbox/mapbox-gl-geocoder`. | | `MapboxSource` | `MapboxSource` | Ported. | | `MapboxLayer` | `MapboxLayer` | | | `MapboxImage` | `MapboxImage` | Ported. | | `MapboxImages` | `MapboxImages` | Ported. | | `MapboxCluster` | `MapboxCluster` | Re-architected — the source is derived from child `MapboxClusterItem`s, not a `data` prop. See [below](#mapboxcluster-data). | | — | `MapboxClusterItem` | **New** — a rendered cluster entry (list item + map feature). See [below](#mapboxcluster-data). | | `StoreLocator` | `StoreLocator` | Re-implemented as a thin orchestrator over `MapboxMap` + `MapboxCluster` + `MapboxClusterItem`s. See its [documentation](/reference/items/StoreLocator/). | | `VueScroller` | — | No equivalent needed — the store list is a plain scrollable element, styled with CSS. | ::: tip StoreLocator is now available `StoreLocator` has been re-implemented as a thin **orchestrator** over a `MapboxMap` + `MapboxCluster` + `MapboxClusterItem`s (plus an optional `MapboxGeocoder`). The single-purpose `StoreLocatorItem` class of the Vue library is gone: each store is a `MapboxClusterItem` that is at once the sidebar list entry and the map feature. The `VueScroller` helper has no equivalent — the list is a plain scrollable element you style with CSS. See the [`StoreLocator` documentation](/reference/items/StoreLocator/). ::: ## API translation ### Props → options Vue props become js-toolkit `data-option-*` attributes on the corresponding element, in kebab-case. Objects and arrays are passed as JSON strings. ```html ``` ```html
``` The js-toolkit `MapboxMap` exposes a focused set of options (`access-token`, `zoom`, `center`) and forwards them to the Mapbox `Map` constructor, so any other serializable map option can be added as a `data-option-*` attribute. See the [full options reference](/reference/items/MapboxMap/js-api). A key structural difference: the map needs an explicit **`container` ref** child (`
`), and child components that only act as declarative definitions (markers, popups, controls, sources, layers, images, clusters) are wrapped in a `hidden` element so they do not take part in the document flow. ### Event listeners → emitted events The Vue library re-emits Mapbox map events prefixed with `mb-` (e.g. `@mb-load`, `@mb-click`). The js-toolkit components use the `map-` prefix instead. Listen to the emitted events from a parent component with `on` handler methods. | Vue listener | js-toolkit emitted event | Parent handler method | | ----------------------------- | ------------------------ | -------------------------------- | | `@mb-load` (map loaded) | `map-load` | `onMapboxMapMapLoad` | | `@mb-click` | `map-click` | `onMapboxMapMapClick` | | `@mb-moveend` | `map-moveend` | `onMapboxMapMapMoveend` | | `@mb-zoomend` | `map-zoomend` | `onMapboxMapMapZoomend` | | `MapboxCluster` cluster click | `map-cluster-click` | `onMapboxClusterMapClusterClick` | | `MapboxCluster` feature click | `map-item-click` | `onMapboxClusterMapItemClick` | Every public event uses the `map-` prefix. The `MapboxMap` component re-emits the full list of Mapbox map events; `MapboxCluster` emits `map-cluster-click`, `map-item-click` (an unclustered point, resolved back to the registered `MapboxClusterItem` behind it) and `map-update` (the item set changed); `MapboxGeocoder` emits `map-result`; `MapboxImage` and `MapboxImages` emit `map-ready`; map children emit `map-error` when guarded lifecycle work fails; and `StoreLocator` emits `map-select`, `map-deselect` and `map-filter`. See each component's events in the [JS API](/reference/items/MapboxMap/js-api). ```js import { Base, createApp } from '@studiometa/js-toolkit'; import { MapboxMap } from '@studiometa/ui-mapbox'; class App extends Base { static config = { name: 'App', components: { MapboxMap }, }; onMapboxMapMapLoad({ args: [map] }) { console.log('Map is ready', map); } onMapboxMapMapClick({ args: [event] }) { console.log('Clicked at', event.lngLat); } } createApp(App); ``` ### Slots → DOM children and refs Vue slots become real DOM children: * The `MapboxMap` default slot content becomes child elements inside the `MapboxMap` element. The map canvas target moves from an internal ref to an explicit `
`. * The `MapboxMarker` / `MapboxPopup` content (Vue `default` / `popup` slots) becomes the inner HTML of the corresponding element. A popup nested inside a marker is attached to it automatically. ```html ``` ```html ``` ### MapboxCluster data {#mapboxcluster-data} In the Vue library, the `MapboxCluster` `data` prop accepted a URL string or an inline GeoJSON object. The re-architected `MapboxCluster` has **no `data` option at all**: it is a pure source driver whose features *are* its rendered children. Each point is a `MapboxClusterItem` element (living in the cluster's subtree, typically a sidebar list `
  • `) that self-registers with its closest `MapboxCluster`; the cluster derives its clustered GeoJSON `FeatureCollection` from that registry and rebuilds it (debounced) whenever items mount or unmount. The same markup is at once the sidebar list and the map source. ```html ``` To load points from a server, render the `MapboxClusterItem` list from your backend (or swap it with a [`Fetch`](/reference/items/Fetch/)): mounting/terminating the items drives the map data, no `data` URL needed. For one-off imperative control you can still push a `FeatureCollection` straight to the source with the cluster's `setData(data)` method, but the registry remains the source of truth and the next item change overwrites it. See the [`MapboxCluster` reference](/reference/items/MapboxMap/js-api#mapboxcluster) and the [`StoreLocator`](/reference/items/StoreLocator/) built on top of it. ## Reactivity caveat In `@studiometa/vue-mapbox-gl`, changing a prop updated the map reactively (moving the center, updating the source data, and so on). In `@studiometa/ui-mapbox`, **options are read once at mount and are not reactive**. To update the map after mount, call the underlying Mapbox objects directly through the component instances: ```js import { getInstance } from '@studiometa/js-toolkit'; import { MapboxMap } from '@studiometa/ui-mapbox'; const mapboxMap = getInstance(element, MapboxMap); // Move the map mapboxMap.map.setCenter([2.35, 48.86]); mapboxMap.map.setZoom(12); // Update a source mapboxMap.map.getSource('points').setData(newGeoJson); ``` Each component exposes its Mapbox object (`map`, `marker`, `popup`, `control`, …). See the [JS API](/reference/items/MapboxMap/js-api#reactivity-and-updates) for the full list. Two things the new lifecycle now handles for you, which the Vue library did not: * **Switching the base style keeps your layers.** Calling `map.setStyle(…)` wipes every source, layer and sprite, but each declarative child re-injects its contribution on the map's `style.load`, so your sources, layers, images and clusters survive a base-style change (resolving [`vue-mapbox-gl` #248](https://github.com/studiometa/vue-mapbox-gl/issues/248)). You no longer need to re-declare data after a style switch. * **Async ergonomics are internal.** Children resolve their parent map, wait for it to load, adopt or re-add resources across `Fetch` swaps and map remounts, and load the geocoder module on demand — all inside the shared base class, so you author plain declarative markup instead of orchestrating imperative `map.on('load', …)` wiring. ## Before / after A complete map with a marker and an attached popup. **Before — `@studiometa/vue-mapbox-gl`:** ```vue ``` **After — `@studiometa/ui-mapbox`:** ```html
    ``` ```js import { Base, createApp } from '@studiometa/js-toolkit'; import { MapboxMap } from '@studiometa/ui-mapbox'; class App extends Base { static config = { name: 'App', components: { MapboxMap }, }; onMapboxMapMapLoad({ args: [map] }) { // former @mb-load handler } } createApp(App); ``` --- --- url: /reference/items/Frame/js-api/abstract-frame-trigger.md --- # AbstractFrameTrigger ## Options ### `requestInit` * Type: `RequestInit` * Default: `{}` Customizes the options for the fetch request. ### `headers` * Type: `Record` * Default: `{}` Adds custom headers to the fetch request. The headers merge with any `$options.requestInit.headers` already defined. ## Getters ### `url` * Return: `URL` * Default: `this.$el.href` or `this.$el.action` depending on the type of the root element ### `requestInit` * Return: `RequestInit` * Default: the `requestInit` and `headers` options merged ## Methods ### `trigger` * Return: `Promise` This method will be called by components extending the `AbstractFrameTrigger` to notify its parent `Frame` to fetch content. ## Emits ### `frame-trigger` * Parameters: * `url` (`URL`): the value returned by the `url` getter * `requestInit` (`RequestInit`): the value returned by the `requestInit` getter Emitted from the [`trigger` method](#trigger), when the component needs a request to be made by its parent `Frame` component. ### `frame-fetch-before` * Parameters: * `url` (`URL`): the URL requested * `requestInit` (`RequestInit`): options for the fetch function Emitted before the request starts. ### `frame-fetch` * Parameters: * `url` (`URL`): the URL requested * `requestInit` (`RequestInit`): options for the fetch function Emitted when the request starts. ### `frame-fetch-after` * Parameters: * `url` (`URL`): the URL requested * `requestInit` (`RequestInit`): options for the fetch function * `content` (`string | Error`): the content of the request if successful, an error otherwise Emitted after the fetch request, be it successful or not. The third parameter can be either the content of the response or an error instance. ### `frame-content` * Parameters: * `url` (`URL`): the URL requested * `requestInit` (`RequestInit`): options for the fetch function * `content` (`string`): the content of the response Emitted when the content of the request has been successfully received, but before its insertion in the current page. ### `frame-content-after` * Parameters: * `url` (`URL`): the URL requested * `requestInit` (`RequestInit`): options for the fetch function * `content` (`string`): the content of the response Emitted after each `FrameTarget` component has received and updated its content, but before the final update on the root of the application which scan the updated content for components to mount. ### `frame-error` * Parameters: * `url` (`URL`): the URL requested * `requestInit` (`RequestInit`): options for the fetch function * `error` (`Error`): a fetch error Emitted when the fetch request or the update of the content throw an error. --- --- url: /reference/items/Accordion.md --- # Accordion ::: warning Legacy component `Accordion` remains available for backward compatibility. Use [`Disclosure` and `DisclosureGroup`](/reference/items/Disclosure/) for new work: they provide accessible `hidden`/`inert` state, independent dynamic child registration, nested groups and composable transitions without changing this legacy API. ::: ## Usage After you install the [package](/guide/installation/), include the template in your project: ```js import { registerComponent } from '@studiometa/js-toolkit'; import { Accordion } from '@studiometa/ui'; registerComponent(Accordion); ``` ```twig{16} {% set items = [ { title: 'Title #1', content: 'Content #1' }, { title: 'Title #2', content: 'Content #2' }, { title: 'Title #3', content: 'Content #3' } ] %} {% include '@ui/Accordion/Accordion.twig' with { items: items } %} ``` --- --- url: /reference/items/Accordion/anatomy.md --- # Anatomy `Accordion` is a compound component: a root orchestrates one or more `AccordionItem` children, each wiring its parts through `data-ref` attributes. Use this map to see which parts exist and how they nest. ## Structure ``` Accordion data-component="Accordion" └─ AccordionItem (× n) data-component="AccordionItem" ├─ button [data-ref="btn"] the toggle, controls the panel └─ container [data-ref="container"] the collapsible wrapper └─ content [data-ref="content"] the panel content ``` ## Parts | Part | Selector | Required | Role | | --- | --- | --- | --- | | Root | `data-component="Accordion"` | Yes | Groups the items, handles `autoclose`, forwards the shared `item` options. | | Item | `data-component="AccordionItem"` | Yes (× n) | A single expandable section. Holds the `isOpen` and `styles` options. | | Trigger | `data-ref="btn"` | Yes | The ` ``` ### `target` * Type: `string` * Available formats: * `Name` * `Name OtherName` * `Name(.selector)` * `Name(.selector) OtherName([value="other-selector"])` Defines the components used as targets to the [effect callback](#effect). Multiple components can be defined by using a single space as delimiter. ::: info Name definition The `Action` component will use the `name` property defined in the static `config` object of each class to resolve components on the page. ```js {3} class Foo extends Base { static config = { name: 'Foo', }; } ``` ::: #### No target By not defining the `target` option, the default target will be the current `Action` instance. The following will toggle the `clicked` class on the ` ``` #### Single target The following configuration will use all `Foo` components as targets, the effect callback will be triggered for each and every one of them. ```html {3} ``` #### Multiple targets ```html {3} ``` #### Reduce the list of target with a selector In the following example, the effect callback will only be triggered on the `Foo` component with the `foo` id. ```html {3,7}
    ...
    ...
    ``` ### `effect` * Type: `string` * Required Defines a small piece of JavaScript executed in the context of the current target. The following variables are available: * `this` (`HTMLElement`): the current element * `ctx` (`Record`): the current targeted component in an object with a uniq key being its name set in the static `config.name` property and the value being the component instance * `event` (`Event`): the event that triggered the action * `target` (`Base`): a direct reference to the current targeted component * `action` (`Base`): a direct reference to the current action component * `$el` (`HTMLElement`): a direct reference to the targeted element * `Action` (`Base`): a direct reference to the current action component * `` (`Base`): a direct reference to the `Name` component mounted on the current element #### Simple effect vs callback effect The effect can also define an arrow function which will be executed as well. The following examples are similar: ```html {3,9} ``` #### Accessing the current element Use `this` to access the current element: ```html {3-4} ``` #### Accessing the target element Use `$el` to access the current element: ```html {3-4}
    ...
    ``` #### Accessing the current instances mounted on the current element Use the name of the component mounted on the current element to get access to its instance. ```html {3} ``` By default, the `Action` variabl will always refer to the current `Action` instance of the current element. The following example will log `true` when the button is clicked: ```html {3} ``` #### Advanced usage with destructuration This can be useful to destructure the first `ctx` parameter and make a direct reference to the targeted component's name when multiple target are defined: ```html {3,4} ``` ::: warning Advanced pattern The pattern described above with multiple components as targets is an advanced pattern that should be used with care, as it adds complexity to the DOM that might not be necessary. ::: ### `on:[.]` * Type: `string` * Format: `[[()] -> ]` Combines the [`on`](#on), [`target`](#target) and [`effect`](#effect) options into a single attribute. Attaches multiple events to a single `Action` component. ```html {3} ``` ::: warning Virtual option This is a virtual option, meaning that it can be used in HTML but will not be present in the `$options` property of the component in JavaScript. ::: --- --- url: /reference/all-exports.md --- # All exports An exhaustive lookup of the supported public symbols from `@studiometa/ui`, `@studiometa/ui-mapbox`, `@studiometa/ui-motion` and the Composer/Twig surface. Compatibility wildcard paths and `.js` aliases are not duplicated. --- --- url: /reference/items/AnchorNav.md --- # AnchorNav ## Usage This component can be directly imported and defined as a dependency of your application: ```js import { registerComponent } from '@studiometa/js-toolkit'; import { AnchorNav } from '@studiometa/ui'; registerComponent(AnchorNav); ``` Then in your html make sure to have similar id and href for the link and the target. ```html{16}
    ``` You can then add options from the [withTransition](/reference/items/Transition/#transition) and [withMountWhenInView](https://js-toolkit.studiometa.dev/api/decorators/withMountWhenInView.html): ```html{16}
    ``` --- --- url: /reference/items/AnchorNav/anatomy.md --- # Anatomy `AnchorNav` is a compound component pairing navigation links with the sections they point to. Each link is matched to a target through the anchor's `href` and the target's `id`. Use this map to see which parts exist and how they nest. ## Structure ``` AnchorNav data-component="AnchorNav" ├─ AnchorNavLink (× n) data-component="AnchorNavLink" └─ AnchorNavTarget (× n) data-component="AnchorNavTarget"
    ``` ## Parts | Part | Selector | Required | Role | | --- | --- | --- | --- | | Root | `data-component="AnchorNav"` | Yes | Connects each link to its target and toggles the active link. | | Link | `data-component="AnchorNavLink"` | Yes (× n) | An anchor whose `href="#id"` matches a target. Accepts [`withTransition`](../Transition/#transition) options. | | Target | `data-component="AnchorNavTarget"` | Yes (× n) | A section whose `id` matches a link. Accepts [`withMountWhenInView`](https://js-toolkit.studiometa.dev/api/decorators/withMountWhenInView.html) options. | A link and its target are paired when the link's `href` (minus the `#`) equals the target's `id`. See the [JavaScript API](./js-api.md) for the options exposed by each part. --- --- url: /reference/items/AnchorNav/js-api.md --- # JS API ## AnchorNav The `AnchorNav` component does not expose any specific JavaScript API. ## AnchorNavLink The `AnchorNavLink` class implements the features of the [`Transition` primitive](/reference/items/Transition/). ## AnchorNavTarget The `AnchorNavTarget` class implements the features of the [`withMountWhenInView` decorator](https://js-toolkit.studiometa.dev/api/decorators/withMountWhenInView.html). --- --- url: /reference/items/AnchorScrollTo.md --- # AnchorScrollTo The `AnchorScrollTo` component is a small interface to the [`scrollTo` utility function](https://js-toolkit.studiometa.dev/utils/scrollTo.html) from the [@studiometa/js-toolkit package](https://js-toolkit.studiometa.dev). ::: warning It should be used on `` elements only. ::: ## Usage This component can be directly imported and defined as a dependency of your application and set up to be instanciated on elements matching the `a[href^="#"]` selector: ```js import { registerComponent } from '@studiometa/js-toolkit'; import { AnchorScrollTo } from '@studiometa/ui'; registerComponent(AnchorScrollTo, 'a[href^="#"]'); ``` --- --- url: /reference/items/AnchorScrollTo/examples.md --- # Examples ## Enable on all hash links :::code-group ```js import { registerComponent } from '@studiometa/js-toolkit'; import { AnchorScrollTo } from '@studiometa/ui'; registerComponent(AnchorScrollTo, 'a[href^="#"]'); ``` ::: --- --- url: /reference/items/AnchorScrollTo/js-api.md --- # JS API ## Getter ### `targetSelector` * Return: `string | HTMLElement | number | { left?: number, top?: number }` * Default: `this.$el.hash` By default, this getter returns the hash portion of its root element as it must be an `` element. --- --- url: /reference/items/animationScrollWithEase.md --- # animationScrollWithEase ::: warning Deprecated `animationScrollWithEase` is deprecated. Extend [`ScrollAnimationTarget`](/reference/items/ScrollAnimation/) for new implementations. ::: The `animationScrollWithEase` decorator adds eased interpolation to a legacy scroll animation class. It remains exported for migration compatibility. ## Migration Replace a decorated `AbstractScrollAnimation` subclass with a `ScrollAnimationTarget` subclass and configure its interpolation through the current ScrollAnimation API. See the [ScrollAnimation migration notes](/migration-guides/1.0-2.0/#remove-animationscrollwithease-decorator) and the [current ScrollAnimation reference](/reference/items/ScrollAnimation/). --- --- url: /guide/autoloading.md --- # Autoloading `@studiometa/ui` ships a declarative autoloader entry. You import one side-effect module. The [`@studiometa/js-toolkit`](https://js-toolkit.studiometa.dev) autoload runtime then finds components from their `data-component` attribute and loads each component's JavaScript when it is needed. The autoloader works the same way with a bundler and from a CDN. The same markup runs in a bundled application, a content site, a prototype, or a CMS template. The `@studiometa/ui/autoload` entry ships JavaScript only — no Twig templates and no stylesheets. Read [Limitations](#limitations) before you use it in an application. For a no-build setup, load the entry from an ESM CDN such as [esm.sh](https://esm.sh). esm.sh serves any npm package as native ES modules and resolves the `@studiometa/js-toolkit` peer dependency. The examples below use esm.sh. ## Quick start Add a module script. Import the `@studiometa/ui/autoload` entry. The import registers the `@studiometa/ui` manifest and starts discovery. There is nothing more to call. ```html ``` The runtime scans the document for `data-component` tokens, imports the matching modules, and registers them with the [js-toolkit](https://js-toolkit.studiometa.dev) runtime. The declarative contract (`data-component`, `data-ref`, `data-option-*`) is the same one that [Declarative runtime](/guide/concepts/declarative-runtime) describes. Pin an exact version in production. See [Version pinning](#version-pinning). ## Activate a package Import one side-effect entry for each component package you use. Each import registers that package's manifest with the shared runtime. ```html ``` The same entries resolve from `node_modules` when you install the packages and let your bundler resolve them: ```js import '@studiometa/ui/autoload'; import '@studiometa/ui-mapbox/autoload'; import '@studiometa/ui-motion/autoload'; ``` Import the entries at the top of one module. The manifests register before the runtime starts, so they join into one loader over the composed set. Import `@studiometa/ui-mapbox/autoload` only when you use the Mapbox family — see [Mapbox integration](#mapbox-integration) — and `@studiometa/ui-motion/autoload` only when you use [Motion](#motion-integration). Every component on the page must resolve to one `@studiometa/js-toolkit` runtime. `@studiometa/ui`, `@studiometa/ui-mapbox` and `@studiometa/ui-motion` share the same `@studiometa/js-toolkit` peer, so one pin on that peer resolves the whole set. ### Version pinning A version reference follows the package's npm tags and semver ranges: ```html ``` Pin an exact version in production. An exact-version URL is immutable and stays cached the longest, and it makes every entry on the page resolve to the same version. A versionless URL and `@latest` follow the latest stable release. ## Loading strategies Each component has a default loading strategy in its manifest. Override it per element with `data-load`. There are four strategies: | Strategy | The component loads when… | | ------------- | -------------------------------------------------------------------------------------------- | | `eager` | the runtime starts. | | `visible` | the element is near the viewport (200px margin, `IntersectionObserver`). Default for Mapbox. | | `idle` | the browser is idle (`requestIdleCallback`, 2-second timeout). | | `interaction` | the first `pointerover`, `pointerdown`, or `focusin` on the element. | ```html
    Loads on hover, touch, or focus
    ``` When a browser API is not available, the strategy falls back to eager loading. The runtime resolves the strategy in this order: 1. An [eager ``](#eager-components) declaration. It always wins. 2. A valid `data-load` attribute. An invalid value logs a warning and uses the manifest default. 3. The manifest default. ### Eager components Make a component load and mount at once, whatever its strategy, with a `` element: ```html ``` The `content` is a comma-separated list of tokens. Several metas concatenate. The runtime trims whitespace and drops empty and duplicate tokens. It ignores an unknown token and logs a warning. ## Component discovery The runtime scans the document at startup and then observes it with a `MutationObserver`. It discovers: * **Initial markup** — every `[data-component]` present when the script runs. * **Added subtrees** — elements inserted after startup. * **Multiple tokens** — space-separated values such as `data-component="Action Timer"`. The runtime imports and registers each token once for the whole document. When a component registers, it also registers the child components it exposes. For example, `Accordion` brings in `AccordionItem`, so nested families work without a list of every child. The runtime cleans up observers and listeners when an element leaves the DOM. Discovery follows node insertion and removal, so the runtime does not observe a change to the `data-component` attribute of an element already in the DOM. Add or replace the element instead. When `MutationObserver` is not available, the initial scan still runs, but the runtime does not discover added components. ## Programmatic API The autoload runtime and its helpers live in `@studiometa/js-toolkit`. The `@studiometa/ui/autoload` entry is only the side-effect wrapper that registers the `@studiometa/ui` manifest; the programmatic API — `autoload`, `registerManifests`, `defineManifest`, and the glob adapters — is imported directly from `@studiometa/js-toolkit`. Use it to compose a subset of packages, to scope discovery to a `root` element, or to build your own entry. ```js import { autoload } from '@studiometa/js-toolkit'; import { manifest as uiManifest } from '@studiometa/ui/manifest'; const handle = autoload({ manifests: [uiManifest], eager: ['Action'] }); handle.stop(); // stop discovery ``` To autoload your own js-toolkit components, build a manifest from your component files with `defineManifest` and register it with `registerManifests`, both from `@studiometa/js-toolkit`. See the [js-toolkit autoload guide](https://js-toolkit.studiometa.dev) and the [autoload API reference](https://js-toolkit.studiometa.dev/api/autoload) for every export. ## Manual imports You can also import components directly from the CDN, without the autoloader. This suits a script that builds components itself. ```js import { Action, Modal } from 'https://esm.sh/@studiometa/ui@1.10.0'; // the whole surface import { Action } from 'https://esm.sh/@studiometa/ui@1.10.0/Action'; // one component ``` Manual imports and the autoloader do not conflict, while every import on the page resolves to one js-toolkit runtime. Pin the same version everywhere. ## Mapbox integration Mapbox components load like any other component, but the autoloader does not bundle `mapbox-gl`. You provide it. This keeps you in control of the Mapbox version and its Web Worker. Declare an [import map](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap) before the autoload script. Point the bare specifiers at a source of your choice. Add the geocoder entry only when you use `MapboxGeocoder`. ```html ``` Load the Mapbox stylesheet yourself, and the geocoder stylesheet only when you use `MapboxGeocoder`. Give each map a valid access token through its `data-option-map-options`. Mapbox components default to the `visible` strategy, so the map code loads when a map nears the viewport. You own the `mapbox-gl` module, so its Web Worker is same-origin and a strict Content Security Policy works. When you load `mapbox-gl` from a CDN that builds its worker from a `blob:` URL, allow it: `Content-Security-Policy: worker-src blob:;`. ## Motion integration The `Motion` component loads like any other component, but the autoloader does not bundle the [`motion`](https://motion.dev) library. You provide it through an import map, which keeps you in control of the Motion version: ```html ``` The component resolves `motion` lazily the first time an animation is built. In a bundled build you can inject a specific entry — such as the smaller `motion/mini` — with `provideMotion()`; see the [Motion JS API](/reference/items/Motion/js-api#providing-the-motion-dependency). ## Shopify integration Most Shopify components autoload as they do in a bundled build. `FetchShopifyPartial` is the exception. The `@shopify/partial-rendering` adapter is not resolved in a no-build setup, so the component logs a diagnostic and falls back to the base [`Fetch`](/reference/items/Fetch/) behavior. Use a bundled build when you need partial rendering. ## Diagnostics The runtime logs warnings under the `[@studiometa/js-toolkit/autoload]` prefix for recoverable conditions: a conflicting runtime version, an unknown token, an unknown eager component, an invalid `data-load` value, an invalid manifest strategy, or an unavailable browser API. When a component fails to import or register, the runtime logs an error and dispatches a bubbling `js-toolkit:error` `CustomEvent` on the document element. Its `detail` carries the `token`, the `stage` (`import` or `registration`), and the `error`: ```js document.addEventListener('js-toolkit:error', (event) => { const { token, stage, error } = event.detail; console.error(`Component ${token} failed at ${stage}:`, error); }); ``` esm.sh serves a source map with every asset, so developer tools show the original sources. The autoloader targets ES2020 module browsers (Chrome 63+, Firefox 67+, Safari 11.1+, Edge 79+). ## Limitations A no-build install trades flexibility for a zero-build setup. Its constraints are deliberate: * **One runtime version per page.** Several imports of the entries are safe; they share one runtime. Two different `@studiometa/js-toolkit` autoload runtimes cannot coexist — the second activation warns and does nothing. Pin one version. * **One js-toolkit runtime.** Every component on the page must resolve to one `@studiometa/js-toolkit` runtime. Do not mix CDN components with a separate npm build that bundles its own copy. * **No `data-component` mutation.** The runtime observes inserted and removed nodes only. * **No Shadow DOM.** Components own standard light-DOM elements. * **No templates or stylesheets.** No Twig, no `data-mount`, and no CSS — including no Mapbox CSS. * **ES2020 module browsers only.** * **Mapbox is not provided.** You supply `mapbox-gl` and its CSS through an import map. * **Motion is not provided.** You supply `motion` through an import map. * **Shopify partial rendering is excluded.** `FetchShopifyPartial` falls back to base `Fetch`. ## Next steps * [js-toolkit autoload guide](https://js-toolkit.studiometa.dev) — autoload your own components. * [autoload API reference](https://js-toolkit.studiometa.dev/api/autoload) — every export. * [Declarative runtime](/guide/concepts/declarative-runtime) — the `data-component` / `data-option-*` contract. --- --- url: /reference/items/Button.md --- # Button Use this component to create buttons. ## Usage After you install the [package](/guide/installation/), include the template in your project: ```twig {% include '@ui/Button/Button.twig' %} ``` --- --- url: /reference/items/Button/examples.md --- # Examples ## Styled buttons :::code-group ```twig {% for size in ['s', 'm', 'l'] %}
    {% for theme in ['primary', 'secondary', 'tertiary'] %}
    {{ include( '@ui/Button/StyledButton.twig', { label: theme|capitalize } ) }} {{ include( '@ui/Button/StyledButton.twig', { label: theme|capitalize, attr: { disabled: true } } ) }} {{ include( '@ui/Button/StyledButton.twig', { label: theme|capitalize, icon: 'mdi:globe' } ) }} {{ include( '@ui/Button/StyledButton.twig', { label: theme|capitalize, icon: 'mdi:globe', icon_position: 'start' } ) }} {{ include( '@ui/Button/StyledButton.twig', { label: theme|capitalize, icon: 'mdi:globe', icon_position: 'end' } ) }} {{ include( '@ui/Button/StyledButton.twig', { label: theme|capitalize, icon: 'mdi:globe', icon_only: true } ) }}
    {% endfor %}
    {% endfor %} ``` ::: ## Rounded styled buttons :::code-group ```twig {% for size in ['s', 'm', 'l'] %}
    {% for theme in ['primary', 'secondary', 'tertiary'] %}
    {{ include( '@ui/Button/StyledButtonRounded.twig', { label: theme|capitalize } ) }} {{ include( '@ui/Button/StyledButtonRounded.twig', { label: theme|capitalize, attr: { disabled: true } } ) }} {{ include( '@ui/Button/StyledButtonRounded.twig', { label: theme|capitalize, icon: 'mdi:globe' } ) }} {{ include( '@ui/Button/StyledButtonRounded.twig', { label: theme|capitalize, icon: 'mdi:globe', icon_position: 'start' } ) }} {{ include( '@ui/Button/StyledButtonRounded.twig', { label: theme|capitalize, icon: 'mdi:globe', icon_position: 'end' } ) }} {{ include( '@ui/Button/StyledButtonRounded.twig', { label: theme|capitalize, icon: 'mdi:globe', icon_only: true } ) }}
    {% endfor %}
    {% endfor %} ``` ::: --- --- url: /reference/items/Button/twig-api.md --- # Twig API ## Parameters ### `label` * Type: `string` The button's label. ### `tag` * Type: `string` The button's tag. The default value is inferred from the other parameters. The `
    ` tag is used when the [`href` parameter](#href) is defined or when the [`attr` parameter](#attr) is defined with an `href` property, otherwise the `
    ``` ::: ### Vertical carousel Set the [`axis`](./js-api#axis) option to `y` to scroll vertically instead of horizontally: ```twig
    ...
    ``` --- --- url: /reference/items/Carousel/examples.md --- # Examples ## Horizontal ## Vertical ## Boundaries The `boundary` option, inherited from the [`Indexable`](/reference/items/Indexable/) primitive, controls what happens at the ends of the track. Use `clamp` (the default), `loop` or `bounce`. Watch the Prev/Next buttons: they disable at the ends in `clamp` mode but stay active in `loop` and `bounce`. --- --- url: /reference/items/Carousel/js-api.md --- # JS API The `Carousel` class extends the [`Indexable` primitive](/reference/items/Indexable/). It inherits its index-navigation methods (`goTo()`, `goNext()`, `goPrev()`), its `boundary` and `reverse` options and its `index` event, so make sure to have a look at [its API reference](/reference/items/Indexable/js-api) too. A carousel is composed of several components working together: * `Carousel` — the root component, extending `Indexable` * `CarouselWrapper` — the scrollable container holding the items * `CarouselItem` — a single slide * `CarouselBtn` — a previous, next or go-to control * `CarouselDrag` — optional pointer-drag behavior, mounted only on fine-pointer devices ## Options ### `axis` * Type: `'x' | 'y'` * Default: `'x'` Defines the scroll direction of the carousel: `'x'` for horizontal, `'y'` for vertical. ```html {2}
    ...
    ``` ### `boundary` * Type: `'clamp' | 'loop' | 'bounce'` * Default: `'clamp'` Inherited from the [`Indexable`](/reference/items/Indexable/js-api#boundary) primitive, it controls what happens at the ends of the track: `clamp` stops at the first/last item, `loop` wraps around, and `bounce` reverses direction. The `CarouselBtn` controls follow it — `prev`/`next` disable at the ends in `clamp` mode but stay active in `loop` and `bounce`. See the [boundaries example](./examples#boundaries). ```html {3}
    ...
    ``` The [`reverse`](/reference/items/Indexable/js-api#reverse) option is also inherited from `Indexable`. The `total` option has no effect here: the carousel derives its length from the number of `CarouselItem` children. ## Getters ### `isHorizontal` * Type: `boolean` Whether the carousel scrolls horizontally (the `axis` option is `'x'`). ### `isVertical` * Type: `boolean` Whether the carousel scrolls vertically (the `axis` option is `'y'`). ### `items` * Type: `CarouselItem[]` The carousel's item components. ### `length` * Type: `number` The number of items, used as the `Indexable` length. ### `wrapper` * Type: `CarouselWrapper` The scrollable wrapper component. ### `progress` * Type: `number` The current scroll progress, between `0` and `1`. ## Methods ### `goTo(indexOrInstruction)` * Arguments: `number | 'next' | 'previous' | 'first' | 'last' | 'random'` * Returns: `Promise` Scroll to the given item. Accepts an index or one of the [`Indexable` instructions](/reference/items/Indexable/js-api). The `goNext()` and `goPrev()` shortcuts are inherited from the primitive. ## Events ### `progress` Emitted while the carousel scrolls, with the current progress between `0` and `1`. The same value is reflected on the root element through the [`--carousel-progress`](#carousel-progress) custom property. ```js onCarouselProgress(progress) { // progress is a number between 0 and 1 } ``` The `index` event is inherited from the [`Indexable` primitive](/reference/items/Indexable/js-api) and emitted whenever the current index changes. ## CSS custom properties ### `--carousel-progress` Set on the root `Carousel` element, reflects the scroll progress between `0` and `1`. ### `--carousel-item-active` Set on each `CarouselItem`, equals `1` when the item is the current one and `0` otherwise. Use it to style the active slide. ## CarouselBtn A control button, delegating to the parent carousel on click. ### `action` * Type: `'next' | 'prev' | string` Use `next` or `prev` to step through the items, or a numeric string (e.g. `"2"`) to jump to a specific index. The button disables itself automatically when its action is unavailable, e.g. `prev` on the first item or `next` on the last one. ```html {3} ``` ## CarouselDrag Adds pointer-drag navigation to the wrapper, built with the [`withDrag`](https://js-toolkit.studiometa.dev/api/decorators/withDrag.html) and [`withMountOnMediaQuery`](https://js-toolkit.studiometa.dev/api/decorators/withMountOnMediaQuery.html) decorators. It only mounts on fine-pointer devices (`(pointer: fine)`), leaving native CSS scroll-snap to handle touch devices. Apply it to the same element as `CarouselWrapper`. --- --- url: /reference/items/CircularMarquee.md --- # CircularMarquee Use this component to create CircularMarquee, a spinning on scroll circular text. This is made using the `` capabilities. ## Usage After you install the [package](/guide/installation/), include the Twig template and load the JavaScript component in your project: ```twig {% include '@ui/CircularMarquee/CircularMarquee.twig' with { id: 'unique-id', radius: 120, outer_radius: 150, content: ' My text content' } only %} ``` ```js import { registerComponent } from '@studiometa/js-toolkit'; import { CircularMarquee } from '@studiometa/ui'; registerComponent(CircularMarquee); ``` --- --- url: /reference/items/CircularMarquee/twig-api.md --- # Twig API ## Parameters ### `id` * Type: `string` The Marquee id's. Mandatory to have it unique, as it is used inside the `` tag to make a reference ### `outer_radius` * Type: `number` * Default: `250` The outer radius of the ``. Must be greater than `radius`. ### `radius` * Type: `number` * Default: `220` The radius of the text in the ``. Must be smaller than `outer_radius`. ### `content` * Type: `string` The text to insert inside the CircularMarquee ### `content_attr` * Type: `array` Custom attributes for the content element. ### `sensitivity` * Type: `number` * Default: `0.1` The sensitivity control the speed and direction of the animation. A negative value makes it spin clockwise. The higher the value, the faster the animation will be. --- --- url: /reference/items/ClickOutside.md --- # ClickOutside The `ClickOutside` primitive dispatches a native `click-outside` event on its element whenever the user clicks anywhere outside of it. ## Usage `ClickOutside` defines no behaviour of its own beyond emitting the event, so it is meant to be paired with the [`Action`](/reference/items/Action/index.html) component on the same element. Add both components to an element and listen to the emitted event with the [`on:` option](/reference/items/Action/js-api.html#on-event-modifier): `data-on:click-outside="..."`. In the following example, a dropdown menu is toggled by a button and closes as soon as a click lands outside of it. :::code-group ```js import { registerComponent } from '@studiometa/js-toolkit'; import { Action, ClickOutside, Target } from '@studiometa/ui'; registerComponent(Action); registerComponent(ClickOutside); registerComponent(Target); ``` ::: --- --- url: /reference/items/ClickOutside/examples.md --- # Examples ## Expandable card The simplest pairing: `ClickOutside` and `Action` share the same element and each effect toggles a single class. Clicking the card opens it, clicking anywhere outside collapses it. Because the trigger is the element itself, the opening click is always *inside* and never fights the close. :::code-group ```js import { registerComponent } from '@studiometa/js-toolkit'; import { Action, ClickOutside } from '@studiometa/ui'; registerComponent(Action); registerComponent(ClickOutside); ``` ::: ## Inline edit A label turns into an input on click and commits back to a static label when a click lands outside. Keeping the input *inside* the `ClickOutside` element is what makes this reliable — clicking the field never triggers the outside-click that would close the editor. :::code-group ```js import { registerComponent } from '@studiometa/js-toolkit'; import { Action, ClickOutside } from '@studiometa/ui'; registerComponent(Action); registerComponent(ClickOutside); ``` ::: ::: tip Keep the trigger inside `ClickOutside` reports every click that lands outside its root element. If the control that *opens* a panel sits outside that element, the very click that opens it will also be seen as an outside click and immediately close it again. Nest the trigger inside the `ClickOutside` element — as in both examples above and the [dropdown on the overview page](./index.md) — to avoid this. ::: --- --- url: /reference/items/ClickOutside/js-api.md --- # JS API The `ClickOutside` primitive has no options or refs. It listens to clicks on the `document` and, whenever one lands outside of its root element, dispatches a single [`click-outside`](#click-outside) event on that element. It is meant to be paired with the [`Action`](/reference/items/Action/) component on the same element to react to the event declaratively. ## Events ### `click-outside` * Type: `CustomEvent` * Bubbles: `false` * Detail: `{ event: MouseEvent }` Dispatched on the component's root element when a `click` occurs outside of it. The detection uses [`Event.composedPath()`](https://developer.mozilla.org/en-US/docs/Web/API/Event/composedPath), so a click on the element itself or on any of its descendants is considered *inside* and does **not** trigger the event. The original `MouseEvent` that triggered the detection is forwarded on the `detail.event` property. #### Listening with `Action` Because [`Action`](/reference/items/Action/) binds native listeners on its element, add both components to the same element and use the [`on:` option](/reference/items/Action/js-api.html#on-event-modifier) to react to the emitted event: ```html {3}
    ...
    ``` #### Accessing the original event The original `MouseEvent` is available through `event.detail.event` in the effect: ```html {3}
    ...
    ``` --- --- url: /reference/components.md --- # Components Ready-to-use interface and behavior solutions, grouped by the task they help accomplish. JavaScript, Twig and Liquid are delivery surfaces rather than separate component categories. --- --- url: /guide/concepts/composition.md --- # Composition The library separates ready-to-use solutions from reusable building blocks. Classification describes an item's primary role, not whether it has markup or can mount directly. ## Choose the right abstraction | Need | Prefer | Example | | --- | --- | --- | | A complete interaction or interface solution | Component | [`Dialog`](/reference/items/Dialog/), [`Slider`](/reference/items/Slider/) | | Lower-level behavior to compose or extend | Primitive | [`Transition`](/reference/items/Transition/), [`Indexable`](/reference/items/Indexable/) | | Reusable behavior applied to a class | Decorator | [`withTransition`](/reference/items/withTransition/), [`withIndex`](/reference/items/withIndex/) | | A standalone operation | Helper | [`viewTransition`](/reference/items/view-transition-helper/) | | Local behavior expressed in markup | Declarative component | [`Action`](/reference/items/Action/), the [Data family](./index.md#the-data-family) | | Coordination across unrelated page features | Application component | [`createApp`](./declarative-runtime.md#when-to-use-createapp) | Start at the highest-level abstraction that satisfies the requirement. Moving to a lower-level primitive or custom class gives more control but also makes the application responsible for more state, structure and accessibility behavior. ## Compound component families A compound component divides one feature into a root orchestrator and focused children. The root registers child classes through its js-toolkit `components` configuration, and markup declares the hierarchy: ```html
    ``` The `Accordion` family includes its item behavior and public contracts. Other examples include Slider, Carousel, Menu, Frame, ScrollAnimation and MapboxMap. A family's canonical Reference page documents related symbols together even when consumers can import those symbols independently. Use an item's **Anatomy** page when its child hierarchy, matching refs or required structure forms part of the contract. ## Co-located components Several independent behaviors can share one element through a space-separated `data-component` value: ```html
    Next page ``` Co-location is useful when each behavior can operate independently. Prefer a compound family when children exchange events, inherit configuration or depend on a specific hierarchy. ## Class composition with decorators A decorator accepts a js-toolkit component class and returns a class with an additional contract: ```js import { Base } from '@studiometa/js-toolkit'; import { withTransition } from '@studiometa/ui'; class Disclosure extends withTransition(Base) { // Disclosure-specific behavior } ``` Decorators keep cross-cutting behavior reusable without forcing unrelated classes into one inheritance hierarchy. Apply only decorators whose options, refs and lifecycle requirements the component can satisfy. ## Extending primitives Extend a primitive when it provides the correct state or service model but the application needs a specialized public behavior: ```js import { Transition } from '@studiometa/ui'; class DisclosureTransition extends Transition { open() { return this.enter(); } close() { return this.leave(); } } ``` Before extending, check whether a component already solves the interaction, whether options can configure it, or whether a decorator exposes the same capability with less custom code. ## Customization decision Use the narrowest supported boundary: 1. Set a documented JavaScript option or Twig parameter. 2. Fill a Twig block or pass root attributes. 3. Compose existing components on the same element or in a documented family. 4. Apply a public decorator or extend a public primitive. 5. Extend or override a package Twig template. 6. Create an application component for cross-feature coordination. 7. Build a new reusable component only when none of the existing contracts fit. Keeping customization at a public boundary makes upgrades easier and leaves internal implementation details free to change. ## Related concepts * [Declarative runtime](./declarative-runtime.md) * [Templates and customization](./templates-and-customization.md) * [Reference taxonomy](/reference/) --- --- url: /guide/concepts.md --- # Concepts `@studiometa/ui` combines server-rendered markup with declarative JavaScript behavior. This section explains the mental model shared by every package and Reference item. Use the [Guide](/guide/) for task-oriented instructions and the [Reference](/reference/) for exact APIs. ## Mental model 1. **Start with meaningful markup.** Write HTML directly or render a Twig or Liquid template. Native HTML semantics remain the baseline. 2. **Add behavior declaratively.** Register JavaScript component classes once, then connect them to markup with `data-component`, `data-option-*` and `data-ref` attributes. 3. **Compose focused parts.** Ready-to-use components can contain child components, reuse primitives, apply decorators or share an element with another behavior. 4. **Customize at the narrowest boundary.** Prefer options and template parameters first, composition second, and class extension or template overrides only when the public configuration is not enough. This separation keeps markup visible to the server and browser while JavaScript progressively enhances it. ## Learn the architecture * [Packages and surfaces](./packages-and-surfaces.md) explains what the NPM and Composer packages provide and how JavaScript, Twig and Liquid differ. * [Declarative runtime](./declarative-runtime.md) explains registration, data attributes, events, lifecycle and application-level orchestration. * [Composition](./composition.md) explains components, primitives, decorators, helpers and compound families. * [Templates and customization](./templates-and-customization.md) explains parameters, blocks, attributes, namespaces, overrides and styling ownership. ## Vocabulary The documentation uses a small, consistent vocabulary: * **Reference item** — a documented public concept with one canonical page, such as `Dialog`, `Transition` or `withTransition`. * **Symbol** — a named public export documented by a Reference item, such as a class, function, type, constant or template. * **Component** — a ready-to-use interface or behavior solution, such as [`Dialog`](/reference/items/Dialog/) or [`Slider`](/reference/items/Slider/). It can be visual or headless. * **Primitive** — a low-level, usually headless building block intended primarily for composition, such as [`Transition`](/reference/items/Transition/) or [`Sentinel`](/reference/items/Sentinel/). * **Decorator** — a higher-order function that adds reusable behavior to a js-toolkit component class, such as [`withTransition`](/reference/items/withTransition/). * **Helper** — a supported plain function that operates independently of a component class, such as [`viewTransition`](/reference/items/view-transition-helper/). * **Family** — related symbols that cooperate as one feature, such as `Accordion` and `AccordionItem`. * **Surface** — the runtime or authoring format through which an item is used: JavaScript, Twig or Liquid. * **Parameter** — a value passed to a Twig template. Twig API pages document parameters and blocks. * **Option** — a value passed to a JavaScript component, commonly through a `data-option-*` attribute. JavaScript API pages document options, refs, methods and events. * **Status** — the support stage of an item or symbol: stable, preview or deprecated. ## Declarative behavior without a custom class Some components let you express application behavior directly in HTML instead of writing a new JavaScript class. [`Action`](/reference/items/Action/) runs an effect in response to an event: ```html ``` ### The Data family The Data family adds scoped reactivity to plain HTML: * [`DataModel`](/reference/items/DataModel/) reads values from form controls. * [`DataBind`](/reference/items/DataBind/) writes values into the DOM. * [`DataComputed`](/reference/items/DataComputed/) derives values. * [`DataEffect`](/reference/items/DataEffect/) runs an effect when a value changes. * [`DataScope`](/reference/items/DataScope/) defines the boundary shared by those components. Use these components for local declarative state. Write a normal custom component for reusable behavior owned by one root element; use an application component only for page-level refs or coordination across otherwise unrelated features. ## Next steps * [Install the packages](/guide/installation/). * [Follow the usage quickstart](/guide/usage/). * [Browse the complete Reference](/reference/). --- --- url: /guide/contributing.md --- # Contributing ## Branching The [repository](https://github.com/studiometa/ui) for `@studiometa/ui` follows a trunk-based workflow with `main` as the single principal branch: * `feature/...` for new features * `fix/...` for bug fixes Every merge should be first approved by opening a pull-request against the `main` branch. ## Local development Clone the project and install the required dependencies. The back-end can be run with [ddev](https://github.com/drud/ddev) which is preconfigured. ```bash git clone https://github.com:studiometa/ui.git cd ui/ # Install NPM dependencies npm install ddev start # Launch the dev environment npm run docs:dev ``` You will then be able to open the documentation development URL [localhost:5173/-/](http://localhost:5173/-/) in your browser. The TypeScript files from the `@studiometa/ui` packages can be imported in the playground by using a ESM import statement: ```js import { Figure } from '@studiometa/ui'; ``` While in dev mode, the source TypeScript files will be automatically rebuild on change and made available to the playground at [localhost:5173/-/play/index.html](http://localhost:5173/-/play/index.html). You can also [open the repository in GitPod](https://gitpod.io/#https://github.com/studiometa/ui) and start the dev environment there. ### Remote development with Gitpod The repository is also configured to easily run in [Gitpod](https://gitpod.io). Just click on the following button to get started: [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/studiometa/ui) ## Adding a component A component can either be a single TypeScript file, a single Twig template or for more complex features, a combination of both. ::: tip 💡 Pro tip Take inspiration from existing components for new implementations. ::: ### TypeScript The default template for a TypeScript component is as follow: ```ts import { Base } from '@studiometa/js-toolkit'; import type { BaseConfig, BaseProps } from '@studiometa/js-toolkit'; export interface NameProps extends BaseProps {} /** * Name class. */ export default class Name extends Base { static config: BaseConfig = { name: 'Name', }; } ``` Once your component is ready, export its public content from the family `index.ts` file and from `packages/ui/index.ts`. Add or update its entry in the documentation reference catalog so its symbols, package, surfaces and lifecycle status remain discoverable: ```diff export * from './Modal/index.js'; +export * from './MyComponent/index.js'; export * from './Panel/index.js'; ``` ### Twig All Twig templates should at least expose an `attr` variable that can be used to configure the attributes of the HTML root element of the template. To facilitate this pattern, we use functions added by our [studiometa/twig-toolkit](https://github.com/studiometa/twig-toolkit). ```twig {% set default_attributes = { class: 'block' } %} {% set required_attributes = { data_component: 'MyComponent' } %} {% set attributes = merge_html_attributes( attr ?? null, default_attributes, required_attributes, ) %}
    ...
    ``` ## Publishing a new version To publish a new version, create a release branch from `main`: ```sh git checkout main git pull origin main git checkout -b release/0.2.41 ``` In this release branch, update the version number of all packages with the following command: ```sh npm version 0.2.41 -ws --include-workspace-root ``` Edit the `CHANGELOG.md` file to add the version number and the date of the release: ```diff ## [Unreleased] +## [v0.2.41](https://github.com/studiometa/ui/compare/0.2.40..0.2.41) (2024-02-13) + ``` Open a pull-request against `main` to validate the changes and make sure all tests are passing. Once the PR is approved, merge it and tag the release: ```sh git checkout main git pull origin main git tag 0.2.41 --message 'v0.2.41' git push origin main git push origin --tags ``` GitHub actions will create a release on GitHub and publish the `@studiometa/ui` packages to NPM. The [Packagist package](https://packagist.org/packages/studiometa/ui) will be updated with the latest tag as well. --- --- url: /reference/items/Cursor.md --- # Cursor Use the cursor component to add a custom cursor to your project. ## Usage After you install the [package](/guide/installation/), include the template in your project: ```js import { registerComponent } from '@studiometa/js-toolkit'; import { Cursor } from '@studiometa/ui'; registerComponent(Cursor); ``` ```twig {% include '@ui/Cursor/Cursor.twig' only %} ``` --- --- url: /reference/items/Cursor/examples.md --- # Examples ## Simple :::code-group ```js import { registerComponent } from '@studiometa/js-toolkit'; import { Cursor } from '@studiometa/ui'; registerComponent(Cursor); ``` ::: --- --- url: /reference/items/Cursor/js-api.md --- # JS API The `Cursor` component extends the [`Base` class](https://js-toolkit.studiometa.dev/api/configuration.html) using the [Pointer Service](https://js-toolkit.studiometa.dev/api/services/usePointer.html). It inherits their respective APIs. See both linked references. ## Options ### `growSelectors` * Type: `string` * Default: `'a, a *, button, button *, [data-cursor-grow], [data-cursor-grow] *'` Specifies which elements trigger a "Cursor grow". ### `shrinkSelectors` * Type: `string` * Default: `'[data-cursor-shrink], [data-cursor-shrink] *'` Specifies which elements trigger a "Cursor shrink". ### `scale` * Type: `number` * Default: `1` Sets the default scale of the cursor. ### `growTo` * Type: `number` * Default: `2` Sets the scale of the cursor when it grows. ### `shrinkTo` * Type: `number` * Default: `0.5` Sets the scale of the cursor when it shrinks. ### `translateDampFactor` * Type: `number` * Default: `0.25` Sets the translate damp factor. ### `growDampFactor` * Type: `number` * Default: `0.25` Sets the grow damp factor. ### `shrinkDampFactor` * Type: `number` * Default: `0.25` Sets the shrink damp factor. --- --- url: /reference/items/Cursor/twig-api.md --- # Twig API ## Parameters ### `attr` * Type: `array` Custom attributes for the root element. ## Blocks ### `content` Custom content for the root element. Defaults to `''`. --- --- url: /reference/items/DataBind.md --- # DataBind Use the `DataBind` to create a one-way binding of a property of the targeted DOM element. This component should be used with the [`DataModel` component](../DataModel/index.md), which handles two-way bindings. The related [`DataComputed`](../DataComputed/index.md) and [`DataEffect`](../DataEffect/index.md) components can also be used for computed values and side effects respectively. ## Usage Import the components in your main app and use the [`DataModel` component](../DataModel/index.md) on HTML `
    ` elements and the `DataBind` and [`DataComputed`](../DataComputed/index.md) components on other elements that need to be updated accordingly. The [`DataEffect` component](../DataEffect/index.md) can be used to execute side effects when the value changes. ### Basic usage ::: code-group ```js [app.js] twoslash import { registerComponents } from '@studiometa/js-toolkit'; import { Action, DataBind, DataModel, DataScope } from '@studiometa/ui'; registerComponents(Action, DataScope, DataBind, DataModel); ``` ```html [index.html]
    Hello world
    ``` ::: ### Multiple virtual bindings Use virtual `data-bind:*` attributes to update several parts of an element — its class, ARIA state, and content — from one value, without a JavaScript class. See the [virtual bindings reference](./js-api.md#virtual-bindings) for the full syntax. The following disclosure keeps its button label while updating its class, ARIA state, and panel visibility from one scoped value. The `DataModel` uses its `value` property to hydrate the initial state; virtual bindings then retain the reactive value without replacing the label. ```html
    ``` ### Conditional rendering Use the `data-bind:if` binding on a `