# Toolbars, translation and keyboard use

> The React, Vue and custom-element toolbars, the framework-neutral ToolbarModel, keyboard shortcuts, aiming, and i18n.

RageLayer ships three ready-made toolbars — a React component, a Vue component and a
custom element — and they are all thin renderers over one framework-neutral `ToolbarModel`. If you
are building your own UI, that model is published too, so you get the behaviour without the markup.

| You are using | Import | What you get |
| --- | --- | --- |
| React / Next.js | `ragelayer/react` | `<RageLayer />` |
| Vue / Nuxt | `ragelayer/vue` | `<RageLayer />` |
| Anything else | `ragelayer/element` | `<rage-layer>` |
| Your own UI | `ragelayer/toolbar` | `ToolbarModel` |

The built-in views keep the current control's name and gesture visible instead of asking visitors
to decode icon silhouettes. On narrow screens the controls stay in one horizontally scrollable row,
retain 44 px targets, and raise the guide text to 14 px.

![The focused Classic toolbar on a phone viewport](/ragelayer/docs/demo-mobile.png)

## The custom element

The custom element is the shortest path to a real toolbar on any stack — Svelte, Angular, Solid,
Qwik, Astro, or plain HTML — because it is just an element.

```html
<script type="module">
  import "ragelayer/element";
</script>

<rage-layer initial-tool="hammer" sound></rage-layer>
```

Importing the entry registers `<rage-layer>`. It builds its UI in a shadow root, so the host
page's CSS cannot reach in and its own styles cannot leak out, and it disposes the engine when the
element leaves the document.

Attributes cover the common cases (`initial-tool`, `sound`). For anything richer — a
custom toolset, engine options, translated strings — call `configure()` before connecting it:

```ts
import { RageLayerElement } from "ragelayer/element";
import { hammer, gun } from "ragelayer/tools";

const rageLayer = new RageLayerElement();
rageLayer.configure({
  tools: [hammer, gun],
  history: true,
  strings: { close: "Dismiss" },
});
rageLayer.addEventListener("ragelayer-close", () => rageLayer.remove());
document.body.append(rageLayer);
```

The element emits `ragelayer-close` when the visitor presses the close button; hosts normally remove it in
response. `rageLayer.rageLayerEngine` exposes the live engine.

**Bundlers and the registration side effect**

`import "ragelayer/element"` exists for its side effect. The package marks that one entry
as having side effects, so tree-shaking will not drop it — but if your bundler is configured to
ignore `sideEffects`, import `defineRageLayerElement` and call it explicitly instead.

## The Vue component

```vue
<script setup lang="ts">
import { ref } from "vue";
import { RageLayer } from "ragelayer/vue";

const open = ref(false);
</script>

<template>
  <button @click="open = true">Destroy this page</button>
  <RageLayer v-if="open" @close="open = false" />
</template>
```

It renders nothing until it is mounted in a browser, so it is safe in a Nuxt page without
`<ClientOnly>`, and it disposes its engine on unmount. `@ready` hands you the engine if you want to
drive it yourself.

## Building your own toolbar

`ToolbarModel` gives you the button list and every behaviour a RageLayer toolbar needs: which tool
is selected, whether undo is available, the capture-status chip, keyboard shortcuts that correctly
ignore typing and IME composition, roving focus, and snapshot export. Its
`state.hint` is the current control's plain-language instruction; the built-in toolbars keep that
instruction visible above the icons and update it on hover, focus, and keyboard navigation.

```ts
import { mountRageLayer } from "ragelayer";
import { ToolbarModel } from "ragelayer/toolbar";

const engine = mountRageLayer({ history: true, initialTool: null });
const toolbar = new ToolbarModel(engine, { onClose: () => engine.dispose() });

const unsubscribe = toolbar.subscribe((state) => {
  render({
    buttons: state.buttons.map((button) => ({
      label: button.label, // accessible name
      title: button.title, // tooltip, including the shortcut
      icon: button.icon, // a tool's drawn art, or null
      iconPath: button.iconPath, // an action's SVG path data, or undefined
      pressed: button.pressed,
      disabled: button.disabled,
      onClick: button.run,
    })),
    hint: state.hint, // visible instruction for the focused / hovered control
  });
});

window.addEventListener("keydown", (event) => {
  if (toolbar.handleKeyDown(event)) event.preventDefault();
});
```

Call `toolbar.destroy()` and `unsubscribe()` when your UI goes away. Disposing the engine destroys
the model automatically.

Tool buttons carry `icon`, a data URL of the drawn art. Action buttons carry `iconPath`, SVG path
data on a 24×24 grid drawn in `currentColor` — one colour token then moves a control's idle, hover
and disabled states together, which platform-drawn emoji could not do. Render it yourself, or use
the helpers:

```ts
import { TOOLBAR_ICONS, toolbarIconElement, toolbarIconSvg } from "ragelayer";

button.append(toolbarIconElement(state.iconPath)); // detached <svg> node
element.innerHTML = toolbarIconSvg(TOOLBAR_ICONS.snapshot); // markup

// TOOLBAR_ICONS also covers controls the built-in toolbar has no button for,
// such as pause and play.
```

## Keyboard shortcuts

| Key | Action |
| --- | --- |
| `1`–`9`, `0` | Select the first ten tools |
| `P` | Save a picture of the wreckage |
| `R` | Repair everything |
| `M` | Toggle sound |
| `Esc` | Put the tool away, then close |
| `Cmd/Ctrl+Z` | Undo (when history is enabled) |
| `Cmd/Ctrl+Shift+Z` | Redo |

Shortcuts never fire while the visitor is typing in an input, textarea, select or contenteditable,
mid-IME-composition, or holding a key down.

## Using a tool without a pointer

The built-in toolbars are keyboard-operable, but the canvas is a pointer surface, and the
toolbars offer no keyboard route onto it. A visitor without a pointing device can select the
hammer and then not swing it.

If that matters for your host, `engine.strike()` is the public hook to build a route on:

```ts
// Use the active tool at a document point, with no pointer involved.
engine.strike(x, y);

// Tools that work while held — a flamethrower, a water hose — need a duration.
engine.strike(x, y, { holdMs: 400 });
```

`strike` runs the same `onDown`/`onUp` pair a click produces and takes a history checkpoint, so a
keyboard-driven blow is undoable exactly like any other. Custom tools need no special handling to
be reachable this way. Drawing a cursor for it is up to you — the engine no longer renders one.

## Translating and rewording

Every user-visible string the built-in UI produces can be replaced. This is how you translate the
toy — and also how you match your own tone of voice.

```ts
import type { RageLayerStrings } from "ragelayer/toolbar";

const french: Partial<RageLayerStrings> = {
  toolbarLabel: "Outils de destruction",
  repair: "Tout réparer",
  close: "Fermer",
  tools: {
    hammer: { name: "Marteau", hint: "frappez — les zones solides résistent" },
    broom: { name: "Balai", hint: "balayez pour nettoyer" },
  },
};
```

Pass it as `strings` to the React component, the Vue component, `element.configure()`, or
`new ToolbarModel(engine, { strings })`. Anything you leave out keeps its English default, and any
tool you do not name keeps its built-in name and hint. No built-in string carries a placeholder,
but `formatString` is exported for strings of your own that do; unknown names are left as written.

`DEFAULT_STRINGS` is exported if you want to see the full list, and `resolveStrings()` merges
overrides the same way the components do.
